WP_Customize_Nav_Menu_Item_Setting

Customize Setting to represent a nav_menu.

Defined (1)

The class is defined in the following location(s).

/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php  
  1. class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { 
  2.  
  3. const ID_PATTERN = '/^nav_menu_item\[(?P<id>-?\d+)\]$/'; 
  4.  
  5. const POST_TYPE = 'nav_menu_item'; 
  6.  
  7. const TYPE = 'nav_menu_item'; 
  8.  
  9. /** 
  10. * Setting type. 
  11. * @since 4.3.0 
  12. * @access public 
  13. * @var string 
  14. */ 
  15. public $type = self::TYPE; 
  16.  
  17. /** 
  18. * Default setting value. 
  19. * @since 4.3.0 
  20. * @access public 
  21. * @var array 
  22. * @see wp_setup_nav_menu_item() 
  23. */ 
  24. public $default = array( 
  25. // The $menu_item_data for wp_update_nav_menu_item(). 
  26. 'object_id' => 0,  
  27. 'object' => '', // Taxonomy name. 
  28. 'menu_item_parent' => 0, // A.K.A. menu-item-parent-id; note that post_parent is different, and not included. 
  29. 'position' => 0, // A.K.A. menu_order. 
  30. 'type' => 'custom', // Note that type_label is not included here. 
  31. 'title' => '',  
  32. 'url' => '',  
  33. 'target' => '',  
  34. 'attr_title' => '',  
  35. 'description' => '',  
  36. 'classes' => '',  
  37. 'xfn' => '',  
  38. 'status' => 'publish',  
  39. 'original_title' => '',  
  40. 'nav_menu_term_id' => 0, // This will be supplied as the $menu_id arg for wp_update_nav_menu_item(). 
  41. '_invalid' => false,  
  42. ); 
  43.  
  44. /** 
  45. * Default transport. 
  46. * @since 4.3.0 
  47. * @since 4.5.0 Default changed to 'refresh' 
  48. * @access public 
  49. * @var string 
  50. */ 
  51. public $transport = 'refresh'; 
  52.  
  53. /** 
  54. * The post ID represented by this setting instance. This is the db_id. 
  55. * A negative value represents a placeholder ID for a new menu not yet saved. 
  56. * @since 4.3.0 
  57. * @access public 
  58. * @var int 
  59. */ 
  60. public $post_id; 
  61.  
  62. /** 
  63. * Storage of pre-setup menu item to prevent wasted calls to wp_setup_nav_menu_item(). 
  64. * @since 4.3.0 
  65. * @access protected 
  66. * @var array 
  67. */ 
  68. protected $value; 
  69.  
  70. /** 
  71. * Previous (placeholder) post ID used before creating a new menu item. 
  72. * This value will be exported to JS via the customize_save_response filter 
  73. * so that JavaScript can update the settings to refer to the newly-assigned 
  74. * post ID. This value is always negative to indicate it does not refer to 
  75. * a real post. 
  76. * @since 4.3.0 
  77. * @access public 
  78. * @var int 
  79. * @see WP_Customize_Nav_Menu_Item_Setting::update() 
  80. * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response() 
  81. */ 
  82. public $previous_post_id; 
  83.  
  84. /** 
  85. * When previewing or updating a menu item, this stores the previous nav_menu_term_id 
  86. * which ensures that we can apply the proper filters. 
  87. * @since 4.3.0 
  88. * @access public 
  89. * @var int 
  90. */ 
  91. public $original_nav_menu_term_id; 
  92.  
  93. /** 
  94. * Whether or not update() was called. 
  95. * @since 4.3.0 
  96. * @access protected 
  97. * @var bool 
  98. */ 
  99. protected $is_updated = false; 
  100.  
  101. /** 
  102. * Status for calling the update method, used in customize_save_response filter. 
  103. * See {@see 'customize_save_response'}. 
  104. * When status is inserted, the placeholder post ID is stored in $previous_post_id. 
  105. * When status is error, the error is stored in $update_error. 
  106. * @since 4.3.0 
  107. * @access public 
  108. * @var string updated|inserted|deleted|error 
  109. * @see WP_Customize_Nav_Menu_Item_Setting::update() 
  110. * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response() 
  111. */ 
  112. public $update_status; 
  113.  
  114. /** 
  115. * Any error object returned by wp_update_nav_menu_item() when setting is updated. 
  116. * @since 4.3.0 
  117. * @access public 
  118. * @var WP_Error 
  119. * @see WP_Customize_Nav_Menu_Item_Setting::update() 
  120. * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response() 
  121. */ 
  122. public $update_error; 
  123.  
  124. /** 
  125. * Constructor. 
  126. * Any supplied $args override class property defaults. 
  127. * @since 4.3.0 
  128. * @access public 
  129. * @param WP_Customize_Manager $manager Bootstrap Customizer instance. 
  130. * @param string $id An specific ID of the setting. Can be a 
  131. * theme mod or option name. 
  132. * @param array $args Optional. Setting arguments. 
  133. * @throws Exception If $id is not valid for this setting type. 
  134. */ 
  135. public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) { 
  136. if ( empty( $manager->nav_menus ) ) { 
  137. throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' ); 
  138.  
  139. if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) { 
  140. throw new Exception( "Illegal widget setting ID: $id" ); 
  141.  
  142. $this->post_id = intval( $matches['id'] ); 
  143. add_action( 'wp_update_nav_menu_item', array( $this, 'flush_cached_value' ), 10, 2 ); 
  144.  
  145. parent::__construct( $manager, $id, $args ); 
  146.  
  147. // Ensure that an initially-supplied value is valid. 
  148. if ( isset( $this->value ) ) { 
  149. $this->populate_value(); 
  150. foreach ( array_diff( array_keys( $this->default ), array_keys( $this->value ) ) as $missing ) { 
  151. throw new Exception( "Supplied nav_menu_item value missing property: $missing" ); 
  152.  
  153.  
  154. /** 
  155. * Clear the cached value when this nav menu item is updated. 
  156. * @since 4.3.0 
  157. * @access public 
  158. * @param int $menu_id The term ID for the menu. 
  159. * @param int $menu_item_id The post ID for the menu item. 
  160. */ 
  161. public function flush_cached_value( $menu_id, $menu_item_id ) { 
  162. unset( $menu_id ); 
  163. if ( $menu_item_id === $this->post_id ) { 
  164. $this->value = null; 
  165.  
  166. /** 
  167. * Get the instance data for a given nav_menu_item setting. 
  168. * @since 4.3.0 
  169. * @access public 
  170. * @see wp_setup_nav_menu_item() 
  171. * @return array|false Instance data array, or false if the item is marked for deletion. 
  172. */ 
  173. public function value() { 
  174. if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) { 
  175. $undefined = new stdClass(); // Symbol. 
  176. $post_value = $this->post_value( $undefined ); 
  177.  
  178. if ( $undefined === $post_value ) { 
  179. $value = $this->_original_value; 
  180. } else { 
  181. $value = $post_value; 
  182. if ( ! empty( $value ) && empty( $value['original_title'] ) ) { 
  183. $value['original_title'] = $this->get_original_title( (object) $value ); 
  184. } elseif ( isset( $this->value ) ) { 
  185. $value = $this->value; 
  186. } else { 
  187. $value = false; 
  188.  
  189. // Note that a ID of less than one indicates a nav_menu not yet inserted. 
  190. if ( $this->post_id > 0 ) { 
  191. $post = get_post( $this->post_id ); 
  192. if ( $post && self::POST_TYPE === $post->post_type ) { 
  193. $is_title_empty = empty( $post->post_title ); 
  194. $value = (array) wp_setup_nav_menu_item( $post ); 
  195. if ( $is_title_empty ) { 
  196. $value['title'] = ''; 
  197.  
  198. if ( ! is_array( $value ) ) { 
  199. $value = $this->default; 
  200.  
  201. // Cache the value for future calls to avoid having to re-call wp_setup_nav_menu_item(). 
  202. $this->value = $value; 
  203. $this->populate_value(); 
  204. $value = $this->value; 
  205.  
  206. if ( ! empty( $value ) && empty( $value['type_label'] ) ) { 
  207. $value['type_label'] = $this->get_type_label( (object) $value ); 
  208.  
  209. return $value; 
  210.  
  211. /** 
  212. * Get original title. 
  213. * @since 4.7.0 
  214. * @access protected 
  215. * @param object $item Nav menu item. 
  216. * @return string The original title. 
  217. */ 
  218. protected function get_original_title( $item ) { 
  219. $original_title = ''; 
  220. if ( 'post_type' === $item->type && ! empty( $item->object_id ) ) { 
  221. $original_object = get_post( $item->object_id ); 
  222. if ( $original_object ) { 
  223. /** This filter is documented in wp-includes/post-template.php */ 
  224. $original_title = apply_filters( 'the_title', $original_object->post_title, $original_object->ID ); 
  225.  
  226. if ( '' === $original_title ) { 
  227. /** translators: %d: ID of a post */ 
  228. $original_title = sprintf( __( '#%d (no title)' ), $original_object->ID ); 
  229. } elseif ( 'taxonomy' === $item->type && ! empty( $item->object_id ) ) { 
  230. $original_term_title = get_term_field( 'name', $item->object_id, $item->object, 'raw' ); 
  231. if ( ! is_wp_error( $original_term_title ) ) { 
  232. $original_title = $original_term_title; 
  233. } elseif ( 'post_type_archive' === $item->type ) { 
  234. $original_object = get_post_type_object( $item->object ); 
  235. if ( $original_object ) { 
  236. $original_title = $original_object->labels->archives; 
  237. $original_title = html_entity_decode( $original_title, ENT_QUOTES, get_bloginfo( 'charset' ) ); 
  238. return $original_title; 
  239.  
  240. /** 
  241. * Get type label. 
  242. * @since 4.7.0 
  243. * @access protected 
  244. * @param object $item Nav menu item. 
  245. * @returns string The type label. 
  246. */ 
  247. protected function get_type_label( $item ) { 
  248. if ( 'post_type' === $item->type ) { 
  249. $object = get_post_type_object( $item->object ); 
  250. if ( $object ) { 
  251. $type_label = $object->labels->singular_name; 
  252. } else { 
  253. $type_label = $item->object; 
  254. } elseif ( 'taxonomy' === $item->type ) { 
  255. $object = get_taxonomy( $item->object ); 
  256. if ( $object ) { 
  257. $type_label = $object->labels->singular_name; 
  258. } else { 
  259. $type_label = $item->object; 
  260. } elseif ( 'post_type_archive' === $item->type ) { 
  261. $type_label = __( 'Post Type Archive' ); 
  262. } else { 
  263. $type_label = __( 'Custom Link' ); 
  264. return $type_label; 
  265.  
  266. /** 
  267. * Ensure that the value is fully populated with the necessary properties. 
  268. * Translates some properties added by wp_setup_nav_menu_item() and removes others. 
  269. * @since 4.3.0 
  270. * @access protected 
  271. * @see WP_Customize_Nav_Menu_Item_Setting::value() 
  272. */ 
  273. protected function populate_value() { 
  274. if ( ! is_array( $this->value ) ) { 
  275. return; 
  276.  
  277. if ( isset( $this->value['menu_order'] ) ) { 
  278. $this->value['position'] = $this->value['menu_order']; 
  279. unset( $this->value['menu_order'] ); 
  280. if ( isset( $this->value['post_status'] ) ) { 
  281. $this->value['status'] = $this->value['post_status']; 
  282. unset( $this->value['post_status'] ); 
  283.  
  284. if ( ! isset( $this->value['original_title'] ) ) { 
  285. $this->value['original_title'] = $this->get_original_title( (object) $this->value ); 
  286.  
  287. if ( ! isset( $this->value['nav_menu_term_id'] ) && $this->post_id > 0 ) { 
  288. $menus = wp_get_post_terms( $this->post_id, WP_Customize_Nav_Menu_Setting::TAXONOMY, array( 
  289. 'fields' => 'ids',  
  290. ) ); 
  291. if ( ! empty( $menus ) ) { 
  292. $this->value['nav_menu_term_id'] = array_shift( $menus ); 
  293. } else { 
  294. $this->value['nav_menu_term_id'] = 0; 
  295.  
  296. foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) { 
  297. if ( ! is_int( $this->value[ $key ] ) ) { 
  298. $this->value[ $key ] = intval( $this->value[ $key ] ); 
  299. foreach ( array( 'classes', 'xfn' ) as $key ) { 
  300. if ( is_array( $this->value[ $key ] ) ) { 
  301. $this->value[ $key ] = implode( ' ', $this->value[ $key ] ); 
  302.  
  303. if ( ! isset( $this->value['title'] ) ) { 
  304. $this->value['title'] = ''; 
  305.  
  306. if ( ! isset( $this->value['_invalid'] ) ) { 
  307. $this->value['_invalid'] = false; 
  308. $is_known_invalid = ( 
  309. ( ( 'post_type' === $this->value['type'] || 'post_type_archive' === $this->value['type'] ) && ! post_type_exists( $this->value['object'] ) ) 
  310. || 
  311. ( 'taxonomy' === $this->value['type'] && ! taxonomy_exists( $this->value['object'] ) ) 
  312. ); 
  313. if ( $is_known_invalid ) { 
  314. $this->value['_invalid'] = true; 
  315.  
  316. // Remove remaining properties available on a setup nav_menu_item post object which aren't relevant to the setting value. 
  317. $irrelevant_properties = array( 
  318. 'ID',  
  319. 'comment_count',  
  320. 'comment_status',  
  321. 'db_id',  
  322. 'filter',  
  323. 'guid',  
  324. 'ping_status',  
  325. 'pinged',  
  326. 'post_author',  
  327. 'post_content',  
  328. 'post_content_filtered',  
  329. 'post_date',  
  330. 'post_date_gmt',  
  331. 'post_excerpt',  
  332. 'post_mime_type',  
  333. 'post_modified',  
  334. 'post_modified_gmt',  
  335. 'post_name',  
  336. 'post_parent',  
  337. 'post_password',  
  338. 'post_title',  
  339. 'post_type',  
  340. 'to_ping',  
  341. ); 
  342. foreach ( $irrelevant_properties as $property ) { 
  343. unset( $this->value[ $property ] ); 
  344.  
  345. /** 
  346. * Handle previewing the setting. 
  347. * @since 4.3.0 
  348. * @since 4.4.0 Added boolean return value. 
  349. * @access public 
  350. * @see WP_Customize_Manager::post_value() 
  351. * @return bool False if method short-circuited due to no-op. 
  352. */ 
  353. public function preview() { 
  354. if ( $this->is_previewed ) { 
  355. return false; 
  356.  
  357. $undefined = new stdClass(); 
  358. $is_placeholder = ( $this->post_id < 0 ); 
  359. $is_dirty = ( $undefined !== $this->post_value( $undefined ) ); 
  360. if ( ! $is_placeholder && ! $is_dirty ) { 
  361. return false; 
  362.  
  363. $this->is_previewed = true; 
  364. $this->_original_value = $this->value(); 
  365. $this->original_nav_menu_term_id = $this->_original_value['nav_menu_term_id']; 
  366. $this->_previewed_blog_id = get_current_blog_id(); 
  367.  
  368. add_filter( 'wp_get_nav_menu_items', array( $this, 'filter_wp_get_nav_menu_items' ), 10, 3 ); 
  369.  
  370. $sort_callback = array( __CLASS__, 'sort_wp_get_nav_menu_items' ); 
  371. if ( ! has_filter( 'wp_get_nav_menu_items', $sort_callback ) ) { 
  372. add_filter( 'wp_get_nav_menu_items', array( __CLASS__, 'sort_wp_get_nav_menu_items' ), 1000, 3 ); 
  373.  
  374. // @todo Add get_post_metadata filters for plugins to add their data. 
  375.  
  376. return true; 
  377.  
  378. /** 
  379. * Filters the wp_get_nav_menu_items() result to supply the previewed menu items. 
  380. * @since 4.3.0 
  381. * @access public 
  382. * @see wp_get_nav_menu_items() 
  383. * @param array $items An array of menu item post objects. 
  384. * @param object $menu The menu object. 
  385. * @param array $args An array of arguments used to retrieve menu item objects. 
  386. * @return array Array of menu items,  
  387. */ 
  388. public function filter_wp_get_nav_menu_items( $items, $menu, $args ) { 
  389. $this_item = $this->value(); 
  390. $current_nav_menu_term_id = $this_item['nav_menu_term_id']; 
  391. unset( $this_item['nav_menu_term_id'] ); 
  392.  
  393. $should_filter = ( 
  394. $menu->term_id === $this->original_nav_menu_term_id 
  395. || 
  396. $menu->term_id === $current_nav_menu_term_id 
  397. ); 
  398. if ( ! $should_filter ) { 
  399. return $items; 
  400.  
  401. // Handle deleted menu item, or menu item moved to another menu. 
  402. $should_remove = ( 
  403. false === $this_item 
  404. || 
  405. true === $this_item['_invalid'] 
  406. || 
  407. $this->original_nav_menu_term_id === $menu->term_id 
  408. && 
  409. $current_nav_menu_term_id !== $this->original_nav_menu_term_id 
  410. ); 
  411. if ( $should_remove ) { 
  412. $filtered_items = array(); 
  413. foreach ( $items as $item ) { 
  414. if ( $item->db_id !== $this->post_id ) { 
  415. $filtered_items[] = $item; 
  416. return $filtered_items; 
  417.  
  418. $mutated = false; 
  419. $should_update = ( 
  420. is_array( $this_item ) 
  421. && 
  422. $current_nav_menu_term_id === $menu->term_id 
  423. ); 
  424. if ( $should_update ) { 
  425. foreach ( $items as $item ) { 
  426. if ( $item->db_id === $this->post_id ) { 
  427. foreach ( get_object_vars( $this->value_as_wp_post_nav_menu_item() ) as $key => $value ) { 
  428. $item->$key = $value; 
  429. $mutated = true; 
  430.  
  431. // Not found so we have to append it.. 
  432. if ( ! $mutated ) { 
  433. $items[] = $this->value_as_wp_post_nav_menu_item(); 
  434.  
  435. return $items; 
  436.  
  437. /** 
  438. * Re-apply the tail logic also applied on $items by wp_get_nav_menu_items(). 
  439. * @since 4.3.0 
  440. * @access public 
  441. * @static 
  442. * @see wp_get_nav_menu_items() 
  443. * @param array $items An array of menu item post objects. 
  444. * @param object $menu The menu object. 
  445. * @param array $args An array of arguments used to retrieve menu item objects. 
  446. * @return array Array of menu items,  
  447. */ 
  448. public static function sort_wp_get_nav_menu_items( $items, $menu, $args ) { 
  449. // @todo We should probably re-apply some constraints imposed by $args. 
  450. unset( $args['include'] ); 
  451.  
  452. // Remove invalid items only in front end. 
  453. if ( ! is_admin() ) { 
  454. $items = array_filter( $items, '_is_valid_nav_menu_item' ); 
  455.  
  456. if ( ARRAY_A === $args['output'] ) { 
  457. $items = wp_list_sort( $items, array( 
  458. $args['output_key'] => 'ASC',  
  459. ) ); 
  460. $i = 1; 
  461.  
  462. foreach ( $items as $k => $item ) { 
  463. $items[ $k ]->{$args['output_key']} = $i++; 
  464.  
  465. return $items; 
  466.  
  467. /** 
  468. * Get the value emulated into a WP_Post and set up as a nav_menu_item. 
  469. * @since 4.3.0 
  470. * @access public 
  471. * @return WP_Post With wp_setup_nav_menu_item() applied. 
  472. */ 
  473. public function value_as_wp_post_nav_menu_item() { 
  474. $item = (object) $this->value(); 
  475. unset( $item->nav_menu_term_id ); 
  476.  
  477. $item->post_status = $item->status; 
  478. unset( $item->status ); 
  479.  
  480. $item->post_type = 'nav_menu_item'; 
  481. $item->menu_order = $item->position; 
  482. unset( $item->position ); 
  483.  
  484. if ( empty( $item->original_title ) ) { 
  485. $item->original_title = $this->get_original_title( $item ); 
  486. if ( empty( $item->title ) && ! empty( $item->original_title ) ) { 
  487. $item->title = $item->original_title; 
  488. if ( $item->title ) { 
  489. $item->post_title = $item->title; 
  490.  
  491. $item->ID = $this->post_id; 
  492. $item->db_id = $this->post_id; 
  493. $post = new WP_Post( (object) $item ); 
  494.  
  495. if ( empty( $post->post_author ) ) { 
  496. $post->post_author = get_current_user_id(); 
  497.  
  498. if ( ! isset( $post->type_label ) ) { 
  499. $post->type_label = $this->get_type_label( $post ); 
  500.  
  501. // Ensure nav menu item URL is set according to linked object. 
  502. if ( 'post_type' === $post->type && ! empty( $post->object_id ) ) { 
  503. $post->url = get_permalink( $post->object_id ); 
  504. } elseif ( 'taxonomy' === $post->type && ! empty( $post->object ) && ! empty( $post->object_id ) ) { 
  505. $post->url = get_term_link( (int) $post->object_id, $post->object ); 
  506. } elseif ( 'post_type_archive' === $post->type && ! empty( $post->object ) ) { 
  507. $post->url = get_post_type_archive_link( $post->object ); 
  508. if ( is_wp_error( $post->url ) ) { 
  509. $post->url = ''; 
  510.  
  511. /** This filter is documented in wp-includes/nav-menu.php */ 
  512. $post->attr_title = apply_filters( 'nav_menu_attr_title', $post->attr_title ); 
  513.  
  514. /** This filter is documented in wp-includes/nav-menu.php */ 
  515. $post->description = apply_filters( 'nav_menu_description', wp_trim_words( $post->description, 200 ) ); 
  516.  
  517. /** This filter is documented in wp-includes/nav-menu.php */ 
  518. $post = apply_filters( 'wp_setup_nav_menu_item', $post ); 
  519.  
  520. return $post; 
  521.  
  522. /** 
  523. * Sanitize an input. 
  524. * Note that parent::sanitize() erroneously does wp_unslash() on $value, but 
  525. * we remove that in this override. 
  526. * @since 4.3.0 
  527. * @access public 
  528. * @param array $menu_item_value The value to sanitize. 
  529. * @return array|false|null Null if an input isn't valid. False if it is marked for deletion. 
  530. * Otherwise the sanitized value. 
  531. */ 
  532. public function sanitize( $menu_item_value ) { 
  533. // Menu is marked for deletion. 
  534. if ( false === $menu_item_value ) { 
  535. return $menu_item_value; 
  536.  
  537. // Invalid. 
  538. if ( ! is_array( $menu_item_value ) ) { 
  539. return null; 
  540.  
  541. $default = array( 
  542. 'object_id' => 0,  
  543. 'object' => '',  
  544. 'menu_item_parent' => 0,  
  545. 'position' => 0,  
  546. 'type' => 'custom',  
  547. 'title' => '',  
  548. 'url' => '',  
  549. 'target' => '',  
  550. 'attr_title' => '',  
  551. 'description' => '',  
  552. 'classes' => '',  
  553. 'xfn' => '',  
  554. 'status' => 'publish',  
  555. 'original_title' => '',  
  556. 'nav_menu_term_id' => 0,  
  557. '_invalid' => false,  
  558. ); 
  559. $menu_item_value = array_merge( $default, $menu_item_value ); 
  560. $menu_item_value = wp_array_slice_assoc( $menu_item_value, array_keys( $default ) ); 
  561. $menu_item_value['position'] = intval( $menu_item_value['position'] ); 
  562.  
  563. foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) { 
  564. // Note we need to allow negative-integer IDs for previewed objects not inserted yet. 
  565. $menu_item_value[ $key ] = intval( $menu_item_value[ $key ] ); 
  566.  
  567. foreach ( array( 'type', 'object', 'target' ) as $key ) { 
  568. $menu_item_value[ $key ] = sanitize_key( $menu_item_value[ $key ] ); 
  569.  
  570. foreach ( array( 'xfn', 'classes' ) as $key ) { 
  571. $value = $menu_item_value[ $key ]; 
  572. if ( ! is_array( $value ) ) { 
  573. $value = explode( ' ', $value ); 
  574. $menu_item_value[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) ); 
  575.  
  576. $menu_item_value['original_title'] = sanitize_text_field( $menu_item_value['original_title'] ); 
  577.  
  578. // Apply the same filters as when calling wp_insert_post(). 
  579. $menu_item_value['title'] = wp_unslash( apply_filters( 'title_save_pre', wp_slash( $menu_item_value['title'] ) ) ); 
  580. $menu_item_value['attr_title'] = wp_unslash( apply_filters( 'excerpt_save_pre', wp_slash( $menu_item_value['attr_title'] ) ) ); 
  581. $menu_item_value['description'] = wp_unslash( apply_filters( 'content_save_pre', wp_slash( $menu_item_value['description'] ) ) ); 
  582.  
  583. $menu_item_value['url'] = esc_url_raw( $menu_item_value['url'] ); 
  584. if ( 'publish' !== $menu_item_value['status'] ) { 
  585. $menu_item_value['status'] = 'draft'; 
  586.  
  587. $menu_item_value['_invalid'] = (bool) $menu_item_value['_invalid']; 
  588.  
  589. /** This filter is documented in wp-includes/class-wp-customize-setting.php */ 
  590. return apply_filters( "customize_sanitize_{$this->id}", $menu_item_value, $this ); 
  591.  
  592. /** 
  593. * Creates/updates the nav_menu_item post for this setting. 
  594. * Any created menu items will have their assigned post IDs exported to the client 
  595. * via the {@see 'customize_save_response'} filter. Likewise, any errors will be 
  596. * exported to the client via the customize_save_response() filter. 
  597. * To delete a menu, the client can send false as the value. 
  598. * @since 4.3.0 
  599. * @access protected 
  600. * @see wp_update_nav_menu_item() 
  601. * @param array|false $value The menu item array to update. If false, then the menu item will be deleted 
  602. * entirely. See WP_Customize_Nav_Menu_Item_Setting::$default for what the value 
  603. * should consist of. 
  604. * @return null|void 
  605. */ 
  606. protected function update( $value ) { 
  607. if ( $this->is_updated ) { 
  608. return; 
  609.  
  610. $this->is_updated = true; 
  611. $is_placeholder = ( $this->post_id < 0 ); 
  612. $is_delete = ( false === $value ); 
  613.  
  614. // Update the cached value. 
  615. $this->value = $value; 
  616.  
  617. add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) ); 
  618.  
  619. if ( $is_delete ) { 
  620. // If the current setting post is a placeholder, a delete request is a no-op. 
  621. if ( $is_placeholder ) { 
  622. $this->update_status = 'deleted'; 
  623. } else { 
  624. $r = wp_delete_post( $this->post_id, true ); 
  625.  
  626. if ( false === $r ) { 
  627. $this->update_error = new WP_Error( 'delete_failure' ); 
  628. $this->update_status = 'error'; 
  629. } else { 
  630. $this->update_status = 'deleted'; 
  631. // @todo send back the IDs for all associated nav menu items deleted, so these settings (and controls) can be removed from Customizer? 
  632. } else { 
  633.  
  634. // Handle saving menu items for menus that are being newly-created. 
  635. if ( $value['nav_menu_term_id'] < 0 ) { 
  636. $nav_menu_setting_id = sprintf( 'nav_menu[%s]', $value['nav_menu_term_id'] ); 
  637. $nav_menu_setting = $this->manager->get_setting( $nav_menu_setting_id ); 
  638.  
  639. if ( ! $nav_menu_setting || ! ( $nav_menu_setting instanceof WP_Customize_Nav_Menu_Setting ) ) { 
  640. $this->update_status = 'error'; 
  641. $this->update_error = new WP_Error( 'unexpected_nav_menu_setting' ); 
  642. return; 
  643.  
  644. if ( false === $nav_menu_setting->save() ) { 
  645. $this->update_status = 'error'; 
  646. $this->update_error = new WP_Error( 'nav_menu_setting_failure' ); 
  647. return; 
  648.  
  649. if ( $nav_menu_setting->previous_term_id !== intval( $value['nav_menu_term_id'] ) ) { 
  650. $this->update_status = 'error'; 
  651. $this->update_error = new WP_Error( 'unexpected_previous_term_id' ); 
  652. return; 
  653.  
  654. $value['nav_menu_term_id'] = $nav_menu_setting->term_id; 
  655.  
  656. // Handle saving a nav menu item that is a child of a nav menu item being newly-created. 
  657. if ( $value['menu_item_parent'] < 0 ) { 
  658. $parent_nav_menu_item_setting_id = sprintf( 'nav_menu_item[%s]', $value['menu_item_parent'] ); 
  659. $parent_nav_menu_item_setting = $this->manager->get_setting( $parent_nav_menu_item_setting_id ); 
  660.  
  661. if ( ! $parent_nav_menu_item_setting || ! ( $parent_nav_menu_item_setting instanceof WP_Customize_Nav_Menu_Item_Setting ) ) { 
  662. $this->update_status = 'error'; 
  663. $this->update_error = new WP_Error( 'unexpected_nav_menu_item_setting' ); 
  664. return; 
  665.  
  666. if ( false === $parent_nav_menu_item_setting->save() ) { 
  667. $this->update_status = 'error'; 
  668. $this->update_error = new WP_Error( 'nav_menu_item_setting_failure' ); 
  669. return; 
  670.  
  671. if ( $parent_nav_menu_item_setting->previous_post_id !== intval( $value['menu_item_parent'] ) ) { 
  672. $this->update_status = 'error'; 
  673. $this->update_error = new WP_Error( 'unexpected_previous_post_id' ); 
  674. return; 
  675.  
  676. $value['menu_item_parent'] = $parent_nav_menu_item_setting->post_id; 
  677.  
  678. // Insert or update menu. 
  679. $menu_item_data = array( 
  680. 'menu-item-object-id' => $value['object_id'],  
  681. 'menu-item-object' => $value['object'],  
  682. 'menu-item-parent-id' => $value['menu_item_parent'],  
  683. 'menu-item-position' => $value['position'],  
  684. 'menu-item-type' => $value['type'],  
  685. 'menu-item-title' => $value['title'],  
  686. 'menu-item-url' => $value['url'],  
  687. 'menu-item-description' => $value['description'],  
  688. 'menu-item-attr-title' => $value['attr_title'],  
  689. 'menu-item-target' => $value['target'],  
  690. 'menu-item-classes' => $value['classes'],  
  691. 'menu-item-xfn' => $value['xfn'],  
  692. 'menu-item-status' => $value['status'],  
  693. ); 
  694.  
  695. $r = wp_update_nav_menu_item( 
  696. $value['nav_menu_term_id'],  
  697. $is_placeholder ? 0 : $this->post_id,  
  698. wp_slash( $menu_item_data ) 
  699. ); 
  700.  
  701. if ( is_wp_error( $r ) ) { 
  702. $this->update_status = 'error'; 
  703. $this->update_error = $r; 
  704. } else { 
  705. if ( $is_placeholder ) { 
  706. $this->previous_post_id = $this->post_id; 
  707. $this->post_id = $r; 
  708. $this->update_status = 'inserted'; 
  709. } else { 
  710. $this->update_status = 'updated'; 
  711.  
  712.  
  713. /** 
  714. * Export data for the JS client. 
  715. * @since 4.3.0 
  716. * @access public 
  717. * @see WP_Customize_Nav_Menu_Item_Setting::update() 
  718. * @param array $data Additional information passed back to the 'saved' event on `wp.customize`. 
  719. * @return array Save response data. 
  720. */ 
  721. public function amend_customize_save_response( $data ) { 
  722. if ( ! isset( $data['nav_menu_item_updates'] ) ) { 
  723. $data['nav_menu_item_updates'] = array(); 
  724.  
  725. $data['nav_menu_item_updates'][] = array( 
  726. 'post_id' => $this->post_id,  
  727. 'previous_post_id' => $this->previous_post_id,  
  728. 'error' => $this->update_error ? $this->update_error->get_error_code() : null,  
  729. 'status' => $this->update_status,  
  730. ); 
  731. return $data;