[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/customize/ -> class-wp-customize-nav-menu-item-setting.php (source)

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


Generated : Wed Oct 15 08:20:04 2025 Cross-referenced by PHPXref