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