class-wp-customize-nav-menu-item-setting.php 27 KB

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