/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. if ( ! empty( $value ) && empty( $value['original_title'] ) ) { 
  229. $value['original_title'] = $this->get_original_title( (object) $value ); 
  230. } elseif ( isset( $this->value ) ) { 
  231. $value = $this->value; 
  232. } else { 
  233. $value = false; 
  234.  
  235. // Note that a ID of less than one indicates a nav_menu not yet inserted. 
  236. if ( $this->post_id > 0 ) { 
  237. $post = get_post( $this->post_id ); 
  238. if ( $post && self::POST_TYPE === $post->post_type ) { 
  239. $is_title_empty = empty( $post->post_title ); 
  240. $value = (array) wp_setup_nav_menu_item( $post ); 
  241. if ( $is_title_empty ) { 
  242. $value['title'] = ''; 
  243.  
  244. if ( ! is_array( $value ) ) { 
  245. $value = $this->default; 
  246.  
  247. // Cache the value for future calls to avoid having to re-call wp_setup_nav_menu_item(). 
  248. $this->value = $value; 
  249. $this->populate_value(); 
  250. $value = $this->value; 
  251.  
  252. if ( ! empty( $value ) && empty( $value['type_label'] ) ) { 
  253. $value['type_label'] = $this->get_type_label( (object) $value ); 
  254.  
  255. return $value; 
  256.  
  257. /** 
  258. * Get original title. 
  259. * 
  260. * @since 4.7.0 
  261. * @access protected 
  262. * 
  263. * @param object $item Nav menu item. 
  264. * @return string The original title. 
  265. */ 
  266. protected function get_original_title( $item ) { 
  267. $original_title = ''; 
  268. if ( 'post_type' === $item->type && ! empty( $item->object_id ) ) { 
  269. $original_object = get_post( $item->object_id ); 
  270. if ( $original_object ) { 
  271. /** This filter is documented in wp-includes/post-template.php */ 
  272. $original_title = apply_filters( 'the_title', $original_object->post_title, $original_object->ID ); 
  273.  
  274. if ( '' === $original_title ) { 
  275. /** translators: %d: ID of a post */ 
  276. $original_title = sprintf( __( '#%d (no title)' ), $original_object->ID ); 
  277. } elseif ( 'taxonomy' === $item->type && ! empty( $item->object_id ) ) { 
  278. $original_term_title = get_term_field( 'name', $item->object_id, $item->object, 'raw' ); 
  279. if ( ! is_wp_error( $original_term_title ) ) { 
  280. $original_title = $original_term_title; 
  281. } elseif ( 'post_type_archive' === $item->type ) { 
  282. $original_object = get_post_type_object( $item->object ); 
  283. if ( $original_object ) { 
  284. $original_title = $original_object->labels->archives; 
  285. $original_title = html_entity_decode( $original_title, ENT_QUOTES, get_bloginfo( 'charset' ) ); 
  286. return $original_title; 
  287.  
  288. /** 
  289. * Get type label. 
  290. * 
  291. * @since 4.7.0 
  292. * @access protected 
  293. * 
  294. * @param object $item Nav menu item. 
  295. * @returns string The type label. 
  296. */ 
  297. protected function get_type_label( $item ) { 
  298. if ( 'post_type' === $item->type ) { 
  299. $object = get_post_type_object( $item->object ); 
  300. if ( $object ) { 
  301. $type_label = $object->labels->singular_name; 
  302. } else { 
  303. $type_label = $item->object; 
  304. } elseif ( 'taxonomy' === $item->type ) { 
  305. $object = get_taxonomy( $item->object ); 
  306. if ( $object ) { 
  307. $type_label = $object->labels->singular_name; 
  308. } else { 
  309. $type_label = $item->object; 
  310. } elseif ( 'post_type_archive' === $item->type ) { 
  311. $type_label = __( 'Post Type Archive' ); 
  312. } else { 
  313. $type_label = __( 'Custom Link' ); 
  314. return $type_label; 
  315.  
  316. /** 
  317. * Ensure that the value is fully populated with the necessary properties. 
  318. * 
  319. * Translates some properties added by wp_setup_nav_menu_item() and removes others. 
  320. * 
  321. * @since 4.3.0 
  322. * @access protected 
  323. * 
  324. * @see WP_Customize_Nav_Menu_Item_Setting::value() 
  325. */ 
  326. protected function populate_value() { 
  327. if ( ! is_array( $this->value ) ) { 
  328. return; 
  329.  
  330. if ( isset( $this->value['menu_order'] ) ) { 
  331. $this->value['position'] = $this->value['menu_order']; 
  332. unset( $this->value['menu_order'] ); 
  333. if ( isset( $this->value['post_status'] ) ) { 
  334. $this->value['status'] = $this->value['post_status']; 
  335. unset( $this->value['post_status'] ); 
  336.  
  337. if ( ! isset( $this->value['original_title'] ) ) { 
  338. $this->value['original_title'] = $this->get_original_title( (object) $this->value ); 
  339.  
  340. if ( ! isset( $this->value['nav_menu_term_id'] ) && $this->post_id > 0 ) { 
  341. $menus = wp_get_post_terms( $this->post_id, WP_Customize_Nav_Menu_Setting::TAXONOMY, array( 
  342. 'fields' => 'ids',  
  343. ) ); 
  344. if ( ! empty( $menus ) ) { 
  345. $this->value['nav_menu_term_id'] = array_shift( $menus ); 
  346. } else { 
  347. $this->value['nav_menu_term_id'] = 0; 
  348.  
  349. foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) { 
  350. if ( ! is_int( $this->value[ $key ] ) ) { 
  351. $this->value[ $key ] = intval( $this->value[ $key ] ); 
  352. foreach ( array( 'classes', 'xfn' ) as $key ) { 
  353. if ( is_array( $this->value[ $key ] ) ) { 
  354. $this->value[ $key ] = implode( ' ', $this->value[ $key ] ); 
  355.  
  356. if ( ! isset( $this->value['title'] ) ) { 
  357. $this->value['title'] = ''; 
  358.  
  359. if ( ! isset( $this->value['_invalid'] ) ) { 
  360. $this->value['_invalid'] = false; 
  361. $is_known_invalid = ( 
  362. ( ( 'post_type' === $this->value['type'] || 'post_type_archive' === $this->value['type'] ) && ! post_type_exists( $this->value['object'] ) ) 
  363. || 
  364. ( 'taxonomy' === $this->value['type'] && ! taxonomy_exists( $this->value['object'] ) ) 
  365. ); 
  366. if ( $is_known_invalid ) { 
  367. $this->value['_invalid'] = true; 
  368.  
  369. // Remove remaining properties available on a setup nav_menu_item post object which aren't relevant to the setting value. 
  370. $irrelevant_properties = array( 
  371. 'ID',  
  372. 'comment_count',  
  373. 'comment_status',  
  374. 'db_id',  
  375. 'filter',  
  376. 'guid',  
  377. 'ping_status',  
  378. 'pinged',  
  379. 'post_author',  
  380. 'post_content',  
  381. 'post_content_filtered',  
  382. 'post_date',  
  383. 'post_date_gmt',  
  384. 'post_excerpt',  
  385. 'post_mime_type',  
  386. 'post_modified',  
  387. 'post_modified_gmt',  
  388. 'post_name',  
  389. 'post_parent',  
  390. 'post_password',  
  391. 'post_title',  
  392. 'post_type',  
  393. 'to_ping',  
  394. ); 
  395. foreach ( $irrelevant_properties as $property ) { 
  396. unset( $this->value[ $property ] ); 
  397.  
  398. /** 
  399. * Handle previewing the setting. 
  400. * 
  401. * @since 4.3.0 
  402. * @since 4.4.0 Added boolean return value. 
  403. * @access public 
  404. * 
  405. * @see WP_Customize_Manager::post_value() 
  406. * 
  407. * @return bool False if method short-circuited due to no-op. 
  408. */ 
  409. public function preview() { 
  410. if ( $this->is_previewed ) { 
  411. return false; 
  412.  
  413. $undefined = new stdClass(); 
  414. $is_placeholder = ( $this->post_id < 0 ); 
  415. $is_dirty = ( $undefined !== $this->post_value( $undefined ) ); 
  416. if ( ! $is_placeholder && ! $is_dirty ) { 
  417. return false; 
  418.  
  419. $this->is_previewed = true; 
  420. $this->_original_value = $this->value(); 
  421. $this->original_nav_menu_term_id = $this->_original_value['nav_menu_term_id']; 
  422. $this->_previewed_blog_id = get_current_blog_id(); 
  423.  
  424. add_filter( 'wp_get_nav_menu_items', array( $this, 'filter_wp_get_nav_menu_items' ), 10, 3 ); 
  425.  
  426. $sort_callback = array( __CLASS__, 'sort_wp_get_nav_menu_items' ); 
  427. if ( ! has_filter( 'wp_get_nav_menu_items', $sort_callback ) ) { 
  428. add_filter( 'wp_get_nav_menu_items', array( __CLASS__, 'sort_wp_get_nav_menu_items' ), 1000, 3 ); 
  429.  
  430. // @todo Add get_post_metadata filters for plugins to add their data. 
  431.  
  432. return true; 
  433.  
  434. /** 
  435. * Filters the wp_get_nav_menu_items() result to supply the previewed menu items. 
  436. * 
  437. * @since 4.3.0 
  438. * @access public 
  439. * 
  440. * @see wp_get_nav_menu_items() 
  441. * 
  442. * @param array $items An array of menu item post objects. 
  443. * @param object $menu The menu object. 
  444. * @param array $args An array of arguments used to retrieve menu item objects. 
  445. * @return array Array of menu items,  
  446. */ 
  447. public function filter_wp_get_nav_menu_items( $items, $menu, $args ) { 
  448. $this_item = $this->value(); 
  449. $current_nav_menu_term_id = $this_item['nav_menu_term_id']; 
  450. unset( $this_item['nav_menu_term_id'] ); 
  451.  
  452. $should_filter = ( 
  453. $menu->term_id === $this->original_nav_menu_term_id 
  454. || 
  455. $menu->term_id === $current_nav_menu_term_id 
  456. ); 
  457. if ( ! $should_filter ) { 
  458. return $items; 
  459.  
  460. // Handle deleted menu item, or menu item moved to another menu. 
  461. $should_remove = ( 
  462. false === $this_item 
  463. || 
  464. true === $this_item['_invalid'] 
  465. || 
  466. $this->original_nav_menu_term_id === $menu->term_id 
  467. && 
  468. $current_nav_menu_term_id !== $this->original_nav_menu_term_id 
  469. ); 
  470. if ( $should_remove ) { 
  471. $filtered_items = array(); 
  472. foreach ( $items as $item ) { 
  473. if ( $item->db_id !== $this->post_id ) { 
  474. $filtered_items[] = $item; 
  475. return $filtered_items; 
  476.  
  477. $mutated = false; 
  478. $should_update = ( 
  479. is_array( $this_item ) 
  480. && 
  481. $current_nav_menu_term_id === $menu->term_id 
  482. ); 
  483. if ( $should_update ) { 
  484. foreach ( $items as $item ) { 
  485. if ( $item->db_id === $this->post_id ) { 
  486. foreach ( get_object_vars( $this->value_as_wp_post_nav_menu_item() ) as $key => $value ) { 
  487. $item->$key = $value; 
  488. $mutated = true; 
  489.  
  490. // Not found so we have to append it.. 
  491. if ( ! $mutated ) { 
  492. $items[] = $this->value_as_wp_post_nav_menu_item(); 
  493.  
  494. return $items; 
  495.  
  496. /** 
  497. * Re-apply the tail logic also applied on $items by wp_get_nav_menu_items(). 
  498. * 
  499. * @since 4.3.0 
  500. * @access public 
  501. * @static 
  502. * 
  503. * @see wp_get_nav_menu_items() 
  504. * 
  505. * @param array $items An array of menu item post objects. 
  506. * @param object $menu The menu object. 
  507. * @param array $args An array of arguments used to retrieve menu item objects. 
  508. * @return array Array of menu items,  
  509. */ 
  510. public static function sort_wp_get_nav_menu_items( $items, $menu, $args ) { 
  511. // @todo We should probably re-apply some constraints imposed by $args. 
  512. unset( $args['include'] ); 
  513.  
  514. // Remove invalid items only in front end. 
  515. if ( ! is_admin() ) { 
  516. $items = array_filter( $items, '_is_valid_nav_menu_item' ); 
  517.  
  518. if ( ARRAY_A === $args['output'] ) { 
  519. $items = wp_list_sort( $items, array( 
  520. $args['output_key'] => 'ASC',  
  521. ) ); 
  522. $i = 1; 
  523.  
  524. foreach ( $items as $k => $item ) { 
  525. $items[ $k ]->{$args['output_key']} = $i++; 
  526.  
  527. return $items; 
  528.  
  529. /** 
  530. * Get the value emulated into a WP_Post and set up as a nav_menu_item. 
  531. * 
  532. * @since 4.3.0 
  533. * @access public 
  534. * 
  535. * @return WP_Post With wp_setup_nav_menu_item() applied. 
  536. */ 
  537. public function value_as_wp_post_nav_menu_item() { 
  538. $item = (object) $this->value(); 
  539. unset( $item->nav_menu_term_id ); 
  540.  
  541. $item->post_status = $item->status; 
  542. unset( $item->status ); 
  543.  
  544. $item->post_type = 'nav_menu_item'; 
  545. $item->menu_order = $item->position; 
  546. unset( $item->position ); 
  547.  
  548. if ( empty( $item->original_title ) ) { 
  549. $item->original_title = $this->get_original_title( $item ); 
  550. if ( empty( $item->title ) && ! empty( $item->original_title ) ) { 
  551. $item->title = $item->original_title; 
  552. if ( $item->title ) { 
  553. $item->post_title = $item->title; 
  554.  
  555. $item->ID = $this->post_id; 
  556. $item->db_id = $this->post_id; 
  557. $post = new WP_Post( (object) $item ); 
  558.  
  559. if ( empty( $post->post_author ) ) { 
  560. $post->post_author = get_current_user_id(); 
  561.  
  562. if ( ! isset( $post->type_label ) ) { 
  563. $post->type_label = $this->get_type_label( $post ); 
  564.  
  565. // Ensure nav menu item URL is set according to linked object. 
  566. if ( 'post_type' === $post->type && ! empty( $post->object_id ) ) { 
  567. $post->url = get_permalink( $post->object_id ); 
  568. } elseif ( 'taxonomy' === $post->type && ! empty( $post->object ) && ! empty( $post->object_id ) ) { 
  569. $post->url = get_term_link( (int) $post->object_id, $post->object ); 
  570. } elseif ( 'post_type_archive' === $post->type && ! empty( $post->object ) ) { 
  571. $post->url = get_post_type_archive_link( $post->object ); 
  572. if ( is_wp_error( $post->url ) ) { 
  573. $post->url = ''; 
  574.  
  575. /** This filter is documented in wp-includes/nav-menu.php */ 
  576. $post->attr_title = apply_filters( 'nav_menu_attr_title', $post->attr_title ); 
  577.  
  578. /** This filter is documented in wp-includes/nav-menu.php */ 
  579. $post->description = apply_filters( 'nav_menu_description', wp_trim_words( $post->description, 200 ) ); 
  580.  
  581. /** This filter is documented in wp-includes/nav-menu.php */ 
  582. $post = apply_filters( 'wp_setup_nav_menu_item', $post ); 
  583.  
  584. return $post; 
  585.  
  586. /** 
  587. * Sanitize an input. 
  588. * 
  589. * Note that parent::sanitize() erroneously does wp_unslash() on $value, but 
  590. * we remove that in this override. 
  591. * 
  592. * @since 4.3.0 
  593. * @access public 
  594. * 
  595. * @param array $menu_item_value The value to sanitize. 
  596. * @return array|false|null Null if an input isn't valid. False if it is marked for deletion. 
  597. * Otherwise the sanitized value. 
  598. */ 
  599. public function sanitize( $menu_item_value ) { 
  600. // Menu is marked for deletion. 
  601. if ( false === $menu_item_value ) { 
  602. return $menu_item_value; 
  603.  
  604. // Invalid. 
  605. if ( ! is_array( $menu_item_value ) ) { 
  606. return null; 
  607.  
  608. $default = array( 
  609. 'object_id' => 0,  
  610. 'object' => '',  
  611. 'menu_item_parent' => 0,  
  612. 'position' => 0,  
  613. 'type' => 'custom',  
  614. 'title' => '',  
  615. 'url' => '',  
  616. 'target' => '',  
  617. 'attr_title' => '',  
  618. 'description' => '',  
  619. 'classes' => '',  
  620. 'xfn' => '',  
  621. 'status' => 'publish',  
  622. 'original_title' => '',  
  623. 'nav_menu_term_id' => 0,  
  624. '_invalid' => false,  
  625. ); 
  626. $menu_item_value = array_merge( $default, $menu_item_value ); 
  627. $menu_item_value = wp_array_slice_assoc( $menu_item_value, array_keys( $default ) ); 
  628. $menu_item_value['position'] = intval( $menu_item_value['position'] ); 
  629.  
  630. foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) { 
  631. // Note we need to allow negative-integer IDs for previewed objects not inserted yet. 
  632. $menu_item_value[ $key ] = intval( $menu_item_value[ $key ] ); 
  633.  
  634. foreach ( array( 'type', 'object', 'target' ) as $key ) { 
  635. $menu_item_value[ $key ] = sanitize_key( $menu_item_value[ $key ] ); 
  636.  
  637. foreach ( array( 'xfn', 'classes' ) as $key ) { 
  638. $value = $menu_item_value[ $key ]; 
  639. if ( ! is_array( $value ) ) { 
  640. $value = explode( ' ', $value ); 
  641. $menu_item_value[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) ); 
  642.  
  643. $menu_item_value['original_title'] = sanitize_text_field( $menu_item_value['original_title'] ); 
  644.  
  645. // Apply the same filters as when calling wp_insert_post(). 
  646. $menu_item_value['title'] = wp_unslash( apply_filters( 'title_save_pre', wp_slash( $menu_item_value['title'] ) ) ); 
  647. $menu_item_value['attr_title'] = wp_unslash( apply_filters( 'excerpt_save_pre', wp_slash( $menu_item_value['attr_title'] ) ) ); 
  648. $menu_item_value['description'] = wp_unslash( apply_filters( 'content_save_pre', wp_slash( $menu_item_value['description'] ) ) ); 
  649.  
  650. $menu_item_value['url'] = esc_url_raw( $menu_item_value['url'] ); 
  651. if ( 'publish' !== $menu_item_value['status'] ) { 
  652. $menu_item_value['status'] = 'draft'; 
  653.  
  654. $menu_item_value['_invalid'] = (bool) $menu_item_value['_invalid']; 
  655.  
  656. /** This filter is documented in wp-includes/class-wp-customize-setting.php */ 
  657. return apply_filters( "customize_sanitize_{$this->id}", $menu_item_value, $this ); 
  658.  
  659. /** 
  660. * Creates/updates the nav_menu_item post for this setting. 
  661. * 
  662. * Any created menu items will have their assigned post IDs exported to the client 
  663. * via the {@see 'customize_save_response'} filter. Likewise, any errors will be 
  664. * exported to the client via the customize_save_response() filter. 
  665. * 
  666. * To delete a menu, the client can send false as the value. 
  667. * 
  668. * @since 4.3.0 
  669. * @access protected 
  670. * 
  671. * @see wp_update_nav_menu_item() 
  672. * 
  673. * @param array|false $value The menu item array to update. If false, then the menu item will be deleted 
  674. * entirely. See WP_Customize_Nav_Menu_Item_Setting::$default for what the value 
  675. * should consist of. 
  676. * @return null|void 
  677. */ 
  678. protected function update( $value ) { 
  679. if ( $this->is_updated ) { 
  680. return; 
  681.  
  682. $this->is_updated = true; 
  683. $is_placeholder = ( $this->post_id < 0 ); 
  684. $is_delete = ( false === $value ); 
  685.  
  686. // Update the cached value. 
  687. $this->value = $value; 
  688.  
  689. add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) ); 
  690.  
  691. if ( $is_delete ) { 
  692. // If the current setting post is a placeholder, a delete request is a no-op. 
  693. if ( $is_placeholder ) { 
  694. $this->update_status = 'deleted'; 
  695. } else { 
  696. $r = wp_delete_post( $this->post_id, true ); 
  697.  
  698. if ( false === $r ) { 
  699. $this->update_error = new WP_Error( 'delete_failure' ); 
  700. $this->update_status = 'error'; 
  701. } else { 
  702. $this->update_status = 'deleted'; 
  703. // @todo send back the IDs for all associated nav menu items deleted, so these settings (and controls) can be removed from Customizer? 
  704. } else { 
  705.  
  706. // Handle saving menu items for menus that are being newly-created. 
  707. if ( $value['nav_menu_term_id'] < 0 ) { 
  708. $nav_menu_setting_id = sprintf( 'nav_menu[%s]', $value['nav_menu_term_id'] ); 
  709. $nav_menu_setting = $this->manager->get_setting( $nav_menu_setting_id ); 
  710.  
  711. if ( ! $nav_menu_setting || ! ( $nav_menu_setting instanceof WP_Customize_Nav_Menu_Setting ) ) { 
  712. $this->update_status = 'error'; 
  713. $this->update_error = new WP_Error( 'unexpected_nav_menu_setting' ); 
  714. return; 
  715.  
  716. if ( false === $nav_menu_setting->save() ) { 
  717. $this->update_status = 'error'; 
  718. $this->update_error = new WP_Error( 'nav_menu_setting_failure' ); 
  719. return; 
  720.  
  721. if ( $nav_menu_setting->previous_term_id !== intval( $value['nav_menu_term_id'] ) ) { 
  722. $this->update_status = 'error'; 
  723. $this->update_error = new WP_Error( 'unexpected_previous_term_id' ); 
  724. return; 
  725.  
  726. $value['nav_menu_term_id'] = $nav_menu_setting->term_id; 
  727.  
  728. // Handle saving a nav menu item that is a child of a nav menu item being newly-created. 
  729. if ( $value['menu_item_parent'] < 0 ) { 
  730. $parent_nav_menu_item_setting_id = sprintf( 'nav_menu_item[%s]', $value['menu_item_parent'] ); 
  731. $parent_nav_menu_item_setting = $this->manager->get_setting( $parent_nav_menu_item_setting_id ); 
  732.  
  733. if ( ! $parent_nav_menu_item_setting || ! ( $parent_nav_menu_item_setting instanceof WP_Customize_Nav_Menu_Item_Setting ) ) { 
  734. $this->update_status = 'error'; 
  735. $this->update_error = new WP_Error( 'unexpected_nav_menu_item_setting' ); 
  736. return; 
  737.  
  738. if ( false === $parent_nav_menu_item_setting->save() ) { 
  739. $this->update_status = 'error'; 
  740. $this->update_error = new WP_Error( 'nav_menu_item_setting_failure' ); 
  741. return; 
  742.  
  743. if ( $parent_nav_menu_item_setting->previous_post_id !== intval( $value['menu_item_parent'] ) ) { 
  744. $this->update_status = 'error'; 
  745. $this->update_error = new WP_Error( 'unexpected_previous_post_id' ); 
  746. return; 
  747.  
  748. $value['menu_item_parent'] = $parent_nav_menu_item_setting->post_id; 
  749.  
  750. // Insert or update menu. 
  751. $menu_item_data = array( 
  752. 'menu-item-object-id' => $value['object_id'],  
  753. 'menu-item-object' => $value['object'],  
  754. 'menu-item-parent-id' => $value['menu_item_parent'],  
  755. 'menu-item-position' => $value['position'],  
  756. 'menu-item-type' => $value['type'],  
  757. 'menu-item-title' => $value['title'],  
  758. 'menu-item-url' => $value['url'],  
  759. 'menu-item-description' => $value['description'],  
  760. 'menu-item-attr-title' => $value['attr_title'],  
  761. 'menu-item-target' => $value['target'],  
  762. 'menu-item-classes' => $value['classes'],  
  763. 'menu-item-xfn' => $value['xfn'],  
  764. 'menu-item-status' => $value['status'],  
  765. ); 
  766.  
  767. $r = wp_update_nav_menu_item( 
  768. $value['nav_menu_term_id'],  
  769. $is_placeholder ? 0 : $this->post_id,  
  770. wp_slash( $menu_item_data ) 
  771. ); 
  772.  
  773. if ( is_wp_error( $r ) ) { 
  774. $this->update_status = 'error'; 
  775. $this->update_error = $r; 
  776. } else { 
  777. if ( $is_placeholder ) { 
  778. $this->previous_post_id = $this->post_id; 
  779. $this->post_id = $r; 
  780. $this->update_status = 'inserted'; 
  781. } else { 
  782. $this->update_status = 'updated'; 
  783.  
  784.  
  785. /** 
  786. * Export data for the JS client. 
  787. * 
  788. * @since 4.3.0 
  789. * @access public 
  790. * 
  791. * @see WP_Customize_Nav_Menu_Item_Setting::update() 
  792. * 
  793. * @param array $data Additional information passed back to the 'saved' event on `wp.customize`. 
  794. * @return array Save response data. 
  795. */ 
  796. public function amend_customize_save_response( $data ) { 
  797. if ( ! isset( $data['nav_menu_item_updates'] ) ) { 
  798. $data['nav_menu_item_updates'] = array(); 
  799.  
  800. $data['nav_menu_item_updates'][] = array( 
  801. 'post_id' => $this->post_id,  
  802. 'previous_post_id' => $this->previous_post_id,  
  803. 'error' => $this->update_error ? $this->update_error->get_error_code() : null,  
  804. 'status' => $this->update_status,  
  805. ); 
  806. return $data; 
.