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