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