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