/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php

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