WordPress PHP Cross Reference Blogging Systems

Source: /wp-includes/customize/class-wp-customize-nav-menu-setting.php - 656 lines - 19076 bytes - Summary - Text - Print

Description: Customize API: WP_Customize_Nav_Menu_Setting class

   1  <?php
   2  /**
   3   * Customize API: WP_Customize_Nav_Menu_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_get_nav_menu_object()
  19   * @see WP_Customize_Setting
  20   */
  21  class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting {
  22  
  23      const ID_PATTERN = '/^nav_menu\[(?P<id>-?\d+)\]$/';
  24  
  25      const TAXONOMY = 'nav_menu';
  26  
  27      const TYPE = 'nav_menu';
  28  
  29      /**
  30       * Setting type.
  31       *
  32       * @since 4.3.0
  33       * @access public
  34       * @var string
  35       */
  36      public $type = self::TYPE;
  37  
  38      /**
  39       * Default setting value.
  40       *
  41       * @since 4.3.0
  42       * @access public
  43       * @var array
  44       *
  45       * @see wp_get_nav_menu_object()
  46       */
  47      public $default = array(
  48          'name'        => '',
  49          'description' => '',
  50          'parent'      => 0,
  51          'auto_add'    => false,
  52      );
  53  
  54      /**
  55       * Default transport.
  56       *
  57       * @since 4.3.0
  58       * @access public
  59       * @var string
  60       */
  61      public $transport = 'postMessage';
  62  
  63      /**
  64       * The term ID represented by this setting instance.
  65       *
  66       * A negative value represents a placeholder ID for a new menu not yet saved.
  67       *
  68       * @since 4.3.0
  69       * @access public
  70       * @var int
  71       */
  72      public $term_id;
  73  
  74      /**
  75       * Previous (placeholder) term ID used before creating a new menu.
  76       *
  77       * This value will be exported to JS via the {@see 'customize_save_response'} filter
  78       * so that JavaScript can update the settings to refer to the newly-assigned
  79       * term ID. This value is always negative to indicate it does not refer to
  80       * a real term.
  81       *
  82       * @since 4.3.0
  83       * @access public
  84       * @var int
  85       *
  86       * @see WP_Customize_Nav_Menu_Setting::update()
  87       * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
  88       */
  89      public $previous_term_id;
  90  
  91      /**
  92       * Whether or not update() was called.
  93       *
  94       * @since 4.3.0
  95       * @access protected
  96       * @var bool
  97       */
  98      protected $is_updated = false;
  99  
 100      /**
 101       * Status for calling the update method, used in customize_save_response filter.
 102       *
 103       * See {@see 'customize_save_response'}.
 104       *
 105       * When status is inserted, the placeholder term ID is stored in `$previous_term_id`.
 106       * When status is error, the error is stored in `$update_error`.
 107       *
 108       * @since 4.3.0
 109       * @access public
 110       * @var string updated|inserted|deleted|error
 111       *
 112       * @see WP_Customize_Nav_Menu_Setting::update()
 113       * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
 114       */
 115      public $update_status;
 116  
 117      /**
 118       * Any error object returned by wp_update_nav_menu_object() when setting is updated.
 119       *
 120       * @since 4.3.0
 121       * @access public
 122       * @var WP_Error
 123       *
 124       * @see WP_Customize_Nav_Menu_Setting::update()
 125       * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
 126       */
 127      public $update_error;
 128  
 129      /**
 130       * Constructor.
 131       *
 132       * Any supplied $args override class property defaults.
 133       *
 134       * @since 4.3.0
 135       * @access public
 136       *
 137       * @param WP_Customize_Manager $manager Bootstrap Customizer instance.
 138       * @param string               $id      An specific ID of the setting. Can be a
 139       *                                      theme mod or option name.
 140       * @param array                $args    Optional. Setting arguments.
 141       *
 142       * @throws Exception If $id is not valid for this setting type.
 143       */
 144  	public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) {
 145          if ( empty( $manager->nav_menus ) ) {
 146              throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' );
 147          }
 148  
 149          if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) {
 150              throw new Exception( "Illegal widget setting ID: $id" );
 151          }
 152  
 153          $this->term_id = intval( $matches['id'] );
 154  
 155          parent::__construct( $manager, $id, $args );
 156      }
 157  
 158      /**
 159       * Get the instance data for a given widget setting.
 160       *
 161       * @since 4.3.0
 162       * @access public
 163       *
 164       * @see wp_get_nav_menu_object()
 165       *
 166       * @return array Instance data.
 167       */
 168  	public function value() {
 169          if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) {
 170              $undefined  = new stdClass(); // Symbol.
 171              $post_value = $this->post_value( $undefined );
 172  
 173              if ( $undefined === $post_value ) {
 174                  $value = $this->_original_value;
 175              } else {
 176                  $value = $post_value;
 177              }
 178          } else {
 179              $value = false;
 180  
 181              // Note that a term_id of less than one indicates a nav_menu not yet inserted.
 182              if ( $this->term_id > 0 ) {
 183                  $term = wp_get_nav_menu_object( $this->term_id );
 184  
 185                  if ( $term ) {
 186                      $value = wp_array_slice_assoc( (array) $term, array_keys( $this->default ) );
 187  
 188                      $nav_menu_options  = (array) get_option( 'nav_menu_options', array() );
 189                      $value['auto_add'] = false;
 190  
 191                      if ( isset( $nav_menu_options['auto_add'] ) && is_array( $nav_menu_options['auto_add'] ) ) {
 192                          $value['auto_add'] = in_array( $term->term_id, $nav_menu_options['auto_add'] );
 193                      }
 194                  }
 195              }
 196  
 197              if ( ! is_array( $value ) ) {
 198                  $value = $this->default;
 199              }
 200          }
 201          return $value;
 202      }
 203  
 204      /**
 205       * Handle previewing the setting.
 206       *
 207       * @since 4.3.0
 208       * @since 4.4.0 Added boolean return value
 209       * @access public
 210       *
 211       * @see WP_Customize_Manager::post_value()
 212       *
 213       * @return bool False if method short-circuited due to no-op.
 214       */
 215  	public function preview() {
 216          if ( $this->is_previewed ) {
 217              return false;
 218          }
 219  
 220          $undefined = new stdClass();
 221          $is_placeholder = ( $this->term_id < 0 );
 222          $is_dirty = ( $undefined !== $this->post_value( $undefined ) );
 223          if ( ! $is_placeholder && ! $is_dirty ) {
 224              return false;
 225          }
 226  
 227          $this->is_previewed       = true;
 228          $this->_original_value    = $this->value();
 229          $this->_previewed_blog_id = get_current_blog_id();
 230  
 231          add_filter( 'wp_get_nav_menus', array( $this, 'filter_wp_get_nav_menus' ), 10, 2 );
 232          add_filter( 'wp_get_nav_menu_object', array( $this, 'filter_wp_get_nav_menu_object' ), 10, 2 );
 233          add_filter( 'default_option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
 234          add_filter( 'option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
 235  
 236          return true;
 237      }
 238  
 239      /**
 240       * Filters the wp_get_nav_menus() result to ensure the inserted menu object is included, and the deleted one is removed.
 241       *
 242       * @since 4.3.0
 243       * @access public
 244       *
 245       * @see wp_get_nav_menus()
 246       *
 247       * @param array $menus An array of menu objects.
 248       * @param array $args  An array of arguments used to retrieve menu objects.
 249       * @return array
 250       */
 251  	public function filter_wp_get_nav_menus( $menus, $args ) {
 252          if ( get_current_blog_id() !== $this->_previewed_blog_id ) {
 253              return $menus;
 254          }
 255  
 256          $setting_value = $this->value();
 257          $is_delete = ( false === $setting_value );
 258          $index = -1;
 259  
 260          // Find the existing menu item's position in the list.
 261          foreach ( $menus as $i => $menu ) {
 262              if ( (int) $this->term_id === (int) $menu->term_id || (int) $this->previous_term_id === (int) $menu->term_id ) {
 263                  $index = $i;
 264                  break;
 265              }
 266          }
 267  
 268          if ( $is_delete ) {
 269              // Handle deleted menu by removing it from the list.
 270              if ( -1 !== $index ) {
 271                  array_splice( $menus, $index, 1 );
 272              }
 273          } else {
 274              // Handle menus being updated or inserted.
 275              $menu_obj = (object) array_merge( array(
 276                  'term_id'          => $this->term_id,
 277                  'term_taxonomy_id' => $this->term_id,
 278                  'slug'             => sanitize_title( $setting_value['name'] ),
 279                  'count'            => 0,
 280                  'term_group'       => 0,
 281                  'taxonomy'         => self::TAXONOMY,
 282                  'filter'           => 'raw',
 283              ), $setting_value );
 284  
 285              array_splice( $menus, $index, ( -1 === $index ? 0 : 1 ), array( $menu_obj ) );
 286          }
 287  
 288          // Make sure the menu objects get re-sorted after an update/insert.
 289          if ( ! $is_delete && ! empty( $args['orderby'] ) ) {
 290              $this->_current_menus_sort_orderby = $args['orderby'];
 291              usort( $menus, array( $this, '_sort_menus_by_orderby' ) );
 292          }
 293          // @todo add support for $args['hide_empty'] === true
 294  
 295          return $menus;
 296      }
 297  
 298      /**
 299       * Temporary non-closure passing of orderby value to function.
 300       *
 301       * @since 4.3.0
 302       * @access protected
 303       * @var string
 304       *
 305       * @see WP_Customize_Nav_Menu_Setting::filter_wp_get_nav_menus()
 306       * @see WP_Customize_Nav_Menu_Setting::_sort_menus_by_orderby()
 307       */
 308      protected $_current_menus_sort_orderby;
 309  
 310      /**
 311       * Sort menu objects by the class-supplied orderby property.
 312       *
 313       * This is a workaround for a lack of closures.
 314       *
 315       * @since 4.3.0
 316       * @access protected
 317       * @param object $menu1
 318       * @param object $menu2
 319       * @return int
 320       *
 321       * @see WP_Customize_Nav_Menu_Setting::filter_wp_get_nav_menus()
 322       */
 323  	protected function _sort_menus_by_orderby( $menu1, $menu2 ) {
 324          $key = $this->_current_menus_sort_orderby;
 325          return strcmp( $menu1->$key, $menu2->$key );
 326      }
 327  
 328      /**
 329       * Filters the wp_get_nav_menu_object() result to supply the previewed menu object.
 330       *
 331       * Requesting a nav_menu object by anything but ID is not supported.
 332       *
 333       * @since 4.3.0
 334       * @access public
 335       *
 336       * @see wp_get_nav_menu_object()
 337       *
 338       * @param object|null $menu_obj Object returned by wp_get_nav_menu_object().
 339       * @param string      $menu_id  ID of the nav_menu term. Requests by slug or name will be ignored.
 340       * @return object|null
 341       */
 342  	public function filter_wp_get_nav_menu_object( $menu_obj, $menu_id ) {
 343          $ok = (
 344              get_current_blog_id() === $this->_previewed_blog_id
 345              &&
 346              is_int( $menu_id )
 347              &&
 348              $menu_id === $this->term_id
 349          );
 350          if ( ! $ok ) {
 351              return $menu_obj;
 352          }
 353  
 354          $setting_value = $this->value();
 355  
 356          // Handle deleted menus.
 357          if ( false === $setting_value ) {
 358              return false;
 359          }
 360  
 361          // Handle sanitization failure by preventing short-circuiting.
 362          if ( null === $setting_value ) {
 363              return $menu_obj;
 364          }
 365  
 366          $menu_obj = (object) array_merge( array(
 367                  'term_id'          => $this->term_id,
 368                  'term_taxonomy_id' => $this->term_id,
 369                  'slug'             => sanitize_title( $setting_value['name'] ),
 370                  'count'            => 0,
 371                  'term_group'       => 0,
 372                  'taxonomy'         => self::TAXONOMY,
 373                  'filter'           => 'raw',
 374              ), $setting_value );
 375  
 376          return $menu_obj;
 377      }
 378  
 379      /**
 380       * Filters the nav_menu_options option to include this menu's auto_add preference.
 381       *
 382       * @since 4.3.0
 383       * @access public
 384       *
 385       * @param array $nav_menu_options Nav menu options including auto_add.
 386       * @return array (Kaybe) modified nav menu options.
 387       */
 388  	public function filter_nav_menu_options( $nav_menu_options ) {
 389          if ( $this->_previewed_blog_id !== get_current_blog_id() ) {
 390              return $nav_menu_options;
 391          }
 392  
 393          $menu = $this->value();
 394          $nav_menu_options = $this->filter_nav_menu_options_value(
 395              $nav_menu_options,
 396              $this->term_id,
 397              false === $menu ? false : $menu['auto_add']
 398          );
 399  
 400          return $nav_menu_options;
 401      }
 402  
 403      /**
 404       * Sanitize an input.
 405       *
 406       * Note that parent::sanitize() erroneously does wp_unslash() on $value, but
 407       * we remove that in this override.
 408       *
 409       * @since 4.3.0
 410       * @access public
 411       *
 412       * @param array $value The value to sanitize.
 413       * @return array|false|null Null if an input isn't valid. False if it is marked for deletion.
 414       *                          Otherwise the sanitized value.
 415       */
 416  	public function sanitize( $value ) {
 417          // Menu is marked for deletion.
 418          if ( false === $value ) {
 419              return $value;
 420          }
 421  
 422          // Invalid.
 423          if ( ! is_array( $value ) ) {
 424              return null;
 425          }
 426  
 427          $default = array(
 428              'name'        => '',
 429              'description' => '',
 430              'parent'      => 0,
 431              'auto_add'    => false,
 432          );
 433          $value = array_merge( $default, $value );
 434          $value = wp_array_slice_assoc( $value, array_keys( $default ) );
 435  
 436          $value['name']        = trim( esc_html( $value['name'] ) ); // This sanitization code is used in wp-admin/nav-menus.php.
 437          $value['description'] = sanitize_text_field( $value['description'] );
 438          $value['parent']      = max( 0, intval( $value['parent'] ) );
 439          $value['auto_add']    = ! empty( $value['auto_add'] );
 440  
 441          if ( '' === $value['name'] ) {
 442              $value['name'] = _x( '(unnamed)', 'Missing menu name.' );
 443          }
 444  
 445          /** This filter is documented in wp-includes/class-wp-customize-setting.php */
 446          return apply_filters( "customize_sanitize_{$this->id}", $value, $this );
 447      }
 448  
 449      /**
 450       * Storage for data to be sent back to client in customize_save_response filter.
 451       *
 452       * See {@see 'customize_save_response'}.
 453       *
 454       * @access protected
 455       * @since 4.3.0
 456       * @var array
 457       *
 458       * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
 459       */
 460      protected $_widget_nav_menu_updates = array();
 461  
 462      /**
 463       * Create/update the nav_menu term for this setting.
 464       *
 465       * Any created menus will have their assigned term IDs exported to the client
 466       * via the {@see 'customize_save_response'} filter. Likewise, any errors will be exported
 467       * to the client via the customize_save_response() filter.
 468       *
 469       * To delete a menu, the client can send false as the value.
 470       *
 471       * @since 4.3.0
 472       * @access protected
 473       *
 474       * @see wp_update_nav_menu_object()
 475       *
 476       * @param array|false $value {
 477       *     The value to update. Note that slug cannot be updated via wp_update_nav_menu_object().
 478       *     If false, then the menu will be deleted entirely.
 479       *
 480       *     @type string $name        The name of the menu to save.
 481       *     @type string $description The term description. Default empty string.
 482       *     @type int    $parent      The id of the parent term. Default 0.
 483       *     @type bool   $auto_add    Whether pages will auto_add to this menu. Default false.
 484       * }
 485       * @return null|void
 486       */
 487  	protected function update( $value ) {
 488          if ( $this->is_updated ) {
 489              return;
 490          }
 491  
 492          $this->is_updated = true;
 493          $is_placeholder   = ( $this->term_id < 0 );
 494          $is_delete        = ( false === $value );
 495  
 496          add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) );
 497  
 498          $auto_add = null;
 499          if ( $is_delete ) {
 500              // If the current setting term is a placeholder, a delete request is a no-op.
 501              if ( $is_placeholder ) {
 502                  $this->update_status = 'deleted';
 503              } else {
 504                  $r = wp_delete_nav_menu( $this->term_id );
 505  
 506                  if ( is_wp_error( $r ) ) {
 507                      $this->update_status = 'error';
 508                      $this->update_error  = $r;
 509                  } else {
 510                      $this->update_status = 'deleted';
 511                      $auto_add = false;
 512                  }
 513              }
 514          } else {
 515              // Insert or update menu.
 516              $menu_data = wp_array_slice_assoc( $value, array( 'description', 'parent' ) );
 517              $menu_data['menu-name'] = $value['name'];
 518  
 519              $menu_id = $is_placeholder ? 0 : $this->term_id;
 520              $r = wp_update_nav_menu_object( $menu_id, wp_slash( $menu_data ) );
 521              $original_name = $menu_data['menu-name'];
 522              $name_conflict_suffix = 1;
 523              while ( is_wp_error( $r ) && 'menu_exists' === $r->get_error_code() ) {
 524                  $name_conflict_suffix += 1;
 525                  /* translators: 1: original menu name, 2: duplicate count */
 526                  $menu_data['menu-name'] = sprintf( __( '%1$s (%2$d)' ), $original_name, $name_conflict_suffix );
 527                  $r = wp_update_nav_menu_object( $menu_id, wp_slash( $menu_data ) );
 528              }
 529  
 530              if ( is_wp_error( $r ) ) {
 531                  $this->update_status = 'error';
 532                  $this->update_error  = $r;
 533              } else {
 534                  if ( $is_placeholder ) {
 535                      $this->previous_term_id = $this->term_id;
 536                      $this->term_id          = $r;
 537                      $this->update_status    = 'inserted';
 538                  } else {
 539                      $this->update_status = 'updated';
 540                  }
 541  
 542                  $auto_add = $value['auto_add'];
 543              }
 544          }
 545  
 546          if ( null !== $auto_add ) {
 547              $nav_menu_options = $this->filter_nav_menu_options_value(
 548                  (array) get_option( 'nav_menu_options', array() ),
 549                  $this->term_id,
 550                  $auto_add
 551              );
 552              update_option( 'nav_menu_options', $nav_menu_options );
 553          }
 554  
 555          if ( 'inserted' === $this->update_status ) {
 556              // Make sure that new menus assigned to nav menu locations use their new IDs.
 557              foreach ( $this->manager->settings() as $setting ) {
 558                  if ( ! preg_match( '/^nav_menu_locations\[/', $setting->id ) ) {
 559                      continue;
 560                  }
 561  
 562                  $post_value = $setting->post_value( null );
 563                  if ( ! is_null( $post_value ) && $this->previous_term_id === intval( $post_value ) ) {
 564                      $this->manager->set_post_value( $setting->id, $this->term_id );
 565                      $setting->save();
 566                  }
 567              }
 568  
 569              // Make sure that any nav_menu widgets referencing the placeholder nav menu get updated and sent back to client.
 570              foreach ( array_keys( $this->manager->unsanitized_post_values() ) as $setting_id ) {
 571                  $nav_menu_widget_setting = $this->manager->get_setting( $setting_id );
 572                  if ( ! $nav_menu_widget_setting || ! preg_match( '/^widget_nav_menu\[/', $nav_menu_widget_setting->id ) ) {
 573                      continue;
 574                  }
 575  
 576                  $widget_instance = $nav_menu_widget_setting->post_value(); // Note that this calls WP_Customize_Widgets::sanitize_widget_instance().
 577                  if ( empty( $widget_instance['nav_menu'] ) || intval( $widget_instance['nav_menu'] ) !== $this->previous_term_id ) {
 578                      continue;
 579                  }
 580  
 581                  $widget_instance['nav_menu'] = $this->term_id;
 582                  $updated_widget_instance = $this->manager->widgets->sanitize_widget_js_instance( $widget_instance );
 583                  $this->manager->set_post_value( $nav_menu_widget_setting->id, $updated_widget_instance );
 584                  $nav_menu_widget_setting->save();
 585  
 586                  $this->_widget_nav_menu_updates[ $nav_menu_widget_setting->id ] = $updated_widget_instance;
 587              }
 588          }
 589      }
 590  
 591      /**
 592       * Updates a nav_menu_options array.
 593       *
 594       * @since 4.3.0
 595       * @access protected
 596       *
 597       * @see WP_Customize_Nav_Menu_Setting::filter_nav_menu_options()
 598       * @see WP_Customize_Nav_Menu_Setting::update()
 599       *
 600       * @param array $nav_menu_options Array as returned by get_option( 'nav_menu_options' ).
 601       * @param int   $menu_id          The term ID for the given menu.
 602       * @param bool  $auto_add         Whether to auto-add or not.
 603       * @return array (Maybe) modified nav_menu_otions array.
 604       */
 605  	protected function filter_nav_menu_options_value( $nav_menu_options, $menu_id, $auto_add ) {
 606          $nav_menu_options = (array) $nav_menu_options;
 607          if ( ! isset( $nav_menu_options['auto_add'] ) ) {
 608              $nav_menu_options['auto_add'] = array();
 609          }
 610  
 611          $i = array_search( $menu_id, $nav_menu_options['auto_add'] );
 612          if ( $auto_add && false === $i ) {
 613              array_push( $nav_menu_options['auto_add'], $this->term_id );
 614          } elseif ( ! $auto_add && false !== $i ) {
 615              array_splice( $nav_menu_options['auto_add'], $i, 1 );
 616          }
 617  
 618          return $nav_menu_options;
 619      }
 620  
 621      /**
 622       * Export data for the JS client.
 623       *
 624       * @since 4.3.0
 625       * @access public
 626       *
 627       * @see WP_Customize_Nav_Menu_Setting::update()
 628       *
 629       * @param array $data Additional information passed back to the 'saved' event on `wp.customize`.
 630       * @return array Export data.
 631       */
 632  	public function amend_customize_save_response( $data ) {
 633          if ( ! isset( $data['nav_menu_updates'] ) ) {
 634              $data['nav_menu_updates'] = array();
 635          }
 636          if ( ! isset( $data['widget_nav_menu_updates'] ) ) {
 637              $data['widget_nav_menu_updates'] = array();
 638          }
 639  
 640          $data['nav_menu_updates'][] = array(
 641              'term_id'          => $this->term_id,
 642              'previous_term_id' => $this->previous_term_id,
 643              'error'            => $this->update_error ? $this->update_error->get_error_code() : null,
 644              'status'           => $this->update_status,
 645              'saved_value'      => 'deleted' === $this->update_status ? null : $this->value(),
 646          );
 647  
 648          $data['widget_nav_menu_updates'] = array_merge(
 649              $data['widget_nav_menu_updates'],
 650              $this->_widget_nav_menu_updates
 651          );
 652          $this->_widget_nav_menu_updates = array();
 653  
 654          return $data;
 655      }
 656  }

title

Description

title

Description

title

Description

title

title

Body