Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 

902 linhas
26 KiB

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