[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

/wp-includes/ -> class-wp-customize-manager.php (source)

   1  <?php
   2  /**
   3   * WordPress Customize Manager classes
   4   *
   5   * @package WordPress
   6   * @subpackage Customize
   7   * @since 3.4.0
   8   */
   9  
  10  /**
  11   * Customize Manager class.
  12   *
  13   * Bootstraps the Customize experience on the server-side.
  14   *
  15   * Sets up the theme-switching process if a theme other than the active one is
  16   * being previewed and customized.
  17   *
  18   * Serves as a factory for Customize Controls and Settings, and
  19   * instantiates default Customize Controls and Settings.
  20   *
  21   * @since 3.4.0
  22   */
  23  final class WP_Customize_Manager {
  24      /**
  25       * An instance of the theme being previewed.
  26       *
  27       * @since 3.4.0
  28       * @var WP_Theme
  29       */
  30      protected $theme;
  31  
  32      /**
  33       * The directory name of the previously active theme (within the theme_root).
  34       *
  35       * @since 3.4.0
  36       * @var string
  37       */
  38      protected $original_stylesheet;
  39  
  40      /**
  41       * Whether this is a Customizer pageload.
  42       *
  43       * @since 3.4.0
  44       * @var bool
  45       */
  46      protected $previewing = false;
  47  
  48      /**
  49       * Methods and properties dealing with managing widgets in the Customizer.
  50       *
  51       * @since 3.9.0
  52       * @var WP_Customize_Widgets
  53       */
  54      public $widgets;
  55  
  56      /**
  57       * Methods and properties dealing with managing nav menus in the Customizer.
  58       *
  59       * @since 4.3.0
  60       * @var WP_Customize_Nav_Menus
  61       */
  62      public $nav_menus;
  63  
  64      /**
  65       * Methods and properties dealing with selective refresh in the Customizer preview.
  66       *
  67       * @since 4.5.0
  68       * @var WP_Customize_Selective_Refresh
  69       */
  70      public $selective_refresh;
  71  
  72      /**
  73       * Registered instances of WP_Customize_Setting.
  74       *
  75       * @since 3.4.0
  76       * @var array
  77       */
  78      protected $settings = array();
  79  
  80      /**
  81       * Sorted top-level instances of WP_Customize_Panel and WP_Customize_Section.
  82       *
  83       * @since 4.0.0
  84       * @var array
  85       */
  86      protected $containers = array();
  87  
  88      /**
  89       * Registered instances of WP_Customize_Panel.
  90       *
  91       * @since 4.0.0
  92       * @var array
  93       */
  94      protected $panels = array();
  95  
  96      /**
  97       * List of core components.
  98       *
  99       * @since 4.5.0
 100       * @var array
 101       */
 102      protected $components = array( 'widgets', 'nav_menus' );
 103  
 104      /**
 105       * Registered instances of WP_Customize_Section.
 106       *
 107       * @since 3.4.0
 108       * @var array
 109       */
 110      protected $sections = array();
 111  
 112      /**
 113       * Registered instances of WP_Customize_Control.
 114       *
 115       * @since 3.4.0
 116       * @var array
 117       */
 118      protected $controls = array();
 119  
 120      /**
 121       * Panel types that may be rendered from JS templates.
 122       *
 123       * @since 4.3.0
 124       * @var array
 125       */
 126      protected $registered_panel_types = array();
 127  
 128      /**
 129       * Section types that may be rendered from JS templates.
 130       *
 131       * @since 4.3.0
 132       * @var array
 133       */
 134      protected $registered_section_types = array();
 135  
 136      /**
 137       * Control types that may be rendered from JS templates.
 138       *
 139       * @since 4.1.0
 140       * @var array
 141       */
 142      protected $registered_control_types = array();
 143  
 144      /**
 145       * Initial URL being previewed.
 146       *
 147       * @since 4.4.0
 148       * @var string
 149       */
 150      protected $preview_url;
 151  
 152      /**
 153       * URL to link the user to when closing the Customizer.
 154       *
 155       * @since 4.4.0
 156       * @var string
 157       */
 158      protected $return_url;
 159  
 160      /**
 161       * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
 162       *
 163       * @since 4.4.0
 164       * @var array
 165       */
 166      protected $autofocus = array();
 167  
 168      /**
 169       * Messenger channel.
 170       *
 171       * @since 4.7.0
 172       * @var string
 173       */
 174      protected $messenger_channel;
 175  
 176      /**
 177       * Whether the autosave revision of the changeset should be loaded.
 178       *
 179       * @since 4.9.0
 180       * @var bool
 181       */
 182      protected $autosaved = false;
 183  
 184      /**
 185       * Whether the changeset branching is allowed.
 186       *
 187       * @since 4.9.0
 188       * @var bool
 189       */
 190      protected $branching = true;
 191  
 192      /**
 193       * Whether settings should be previewed.
 194       *
 195       * @since 4.9.0
 196       * @var bool
 197       */
 198      protected $settings_previewed = true;
 199  
 200      /**
 201       * Whether a starter content changeset was saved.
 202       *
 203       * @since 4.9.0
 204       * @var bool
 205       */
 206      protected $saved_starter_content_changeset = false;
 207  
 208      /**
 209       * Unsanitized values for Customize Settings parsed from $_POST['customized'].
 210       *
 211       * @var array
 212       */
 213      private $_post_values;
 214  
 215      /**
 216       * Changeset UUID.
 217       *
 218       * @since 4.7.0
 219       * @var string
 220       */
 221      private $_changeset_uuid;
 222  
 223      /**
 224       * Changeset post ID.
 225       *
 226       * @since 4.7.0
 227       * @var int|false
 228       */
 229      private $_changeset_post_id;
 230  
 231      /**
 232       * Changeset data loaded from a customize_changeset post.
 233       *
 234       * @since 4.7.0
 235       * @var array
 236       */
 237      private $_changeset_data;
 238  
 239      /**
 240       * Constructor.
 241       *
 242       * @since 3.4.0
 243       * @since 4.7.0 Added $args param.
 244       *
 245       * @param array $args {
 246       *     Args.
 247       *
 248       *     @type null|string|false $changeset_uuid     Changeset UUID, the `post_name` for the customize_changeset post containing the customized state.
 249       *                                                 Defaults to `null` resulting in a UUID to be immediately generated. If `false` is provided, then
 250       *                                                 then the changeset UUID will be determined during `after_setup_theme`: when the
 251       *                                                 `customize_changeset_branching` filter returns false, then the default UUID will be that
 252       *                                                 of the most recent `customize_changeset` post that has a status other than 'auto-draft',
 253       *                                                 'publish', or 'trash'. Otherwise, if changeset branching is enabled, then a random UUID will be used.
 254       *     @type string            $theme              Theme to be previewed (for theme switch). Defaults to customize_theme or theme query params.
 255       *     @type string            $messenger_channel  Messenger channel. Defaults to customize_messenger_channel query param.
 256       *     @type bool              $settings_previewed If settings should be previewed. Defaults to true.
 257       *     @type bool              $branching          If changeset branching is allowed; otherwise, changesets are linear. Defaults to true.
 258       *     @type bool              $autosaved          If data from a changeset's autosaved revision should be loaded if it exists. Defaults to false.
 259       * }
 260       */
 261  	public function __construct( $args = array() ) {
 262  
 263          $args = array_merge(
 264              array_fill_keys( array( 'changeset_uuid', 'theme', 'messenger_channel', 'settings_previewed', 'autosaved', 'branching' ), null ),
 265              $args
 266          );
 267  
 268          // Note that the UUID format will be validated in the setup_theme() method.
 269          if ( ! isset( $args['changeset_uuid'] ) ) {
 270              $args['changeset_uuid'] = wp_generate_uuid4();
 271          }
 272  
 273          // The theme and messenger_channel should be supplied via $args, but they are also looked at in the $_REQUEST global here for back-compat.
 274          if ( ! isset( $args['theme'] ) ) {
 275              if ( isset( $_REQUEST['customize_theme'] ) ) {
 276                  $args['theme'] = wp_unslash( $_REQUEST['customize_theme'] );
 277              } elseif ( isset( $_REQUEST['theme'] ) ) { // Deprecated.
 278                  $args['theme'] = wp_unslash( $_REQUEST['theme'] );
 279              }
 280          }
 281          if ( ! isset( $args['messenger_channel'] ) && isset( $_REQUEST['customize_messenger_channel'] ) ) {
 282              $args['messenger_channel'] = sanitize_key( wp_unslash( $_REQUEST['customize_messenger_channel'] ) );
 283          }
 284  
 285          $this->original_stylesheet = get_stylesheet();
 286          $this->theme               = wp_get_theme( 0 === validate_file( $args['theme'] ) ? $args['theme'] : null );
 287          $this->messenger_channel   = $args['messenger_channel'];
 288          $this->_changeset_uuid     = $args['changeset_uuid'];
 289  
 290          foreach ( array( 'settings_previewed', 'autosaved', 'branching' ) as $key ) {
 291              if ( isset( $args[ $key ] ) ) {
 292                  $this->$key = (bool) $args[ $key ];
 293              }
 294          }
 295  
 296          require_once ( ABSPATH . WPINC . '/class-wp-customize-setting.php' );
 297          require_once ( ABSPATH . WPINC . '/class-wp-customize-panel.php' );
 298          require_once ( ABSPATH . WPINC . '/class-wp-customize-section.php' );
 299          require_once ( ABSPATH . WPINC . '/class-wp-customize-control.php' );
 300  
 301          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-color-control.php' );
 302          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-media-control.php' );
 303          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-upload-control.php' );
 304          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-image-control.php' );
 305          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-background-image-control.php' );
 306          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-background-position-control.php' );
 307          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-cropped-image-control.php' );
 308          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-site-icon-control.php' );
 309          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-header-image-control.php' );
 310          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-theme-control.php' );
 311          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-code-editor-control.php' );
 312          require_once ( ABSPATH . WPINC . '/customize/class-wp-widget-area-customize-control.php' );
 313          require_once ( ABSPATH . WPINC . '/customize/class-wp-widget-form-customize-control.php' );
 314          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-control.php' );
 315          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-control.php' );
 316          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-location-control.php' );
 317          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-name-control.php' );
 318          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-locations-control.php' );
 319          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-auto-add-control.php' );
 320          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-new-menu-control.php' ); // @todo Remove in 5.0. See #42364.
 321  
 322          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menus-panel.php' );
 323  
 324          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-themes-panel.php' );
 325          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-themes-section.php' );
 326          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-sidebar-section.php' );
 327          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-section.php' );
 328          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-new-menu-section.php' ); // @todo Remove in 5.0. See #42364.
 329  
 330          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-custom-css-setting.php' );
 331          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-filter-setting.php' );
 332          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-header-image-setting.php' );
 333          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-background-image-setting.php' );
 334          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-setting.php' );
 335          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-setting.php' );
 336  
 337          /**
 338           * Filters the core Customizer components to load.
 339           *
 340           * This allows Core components to be excluded from being instantiated by
 341           * filtering them out of the array. Note that this filter generally runs
 342           * during the {@see 'plugins_loaded'} action, so it cannot be added
 343           * in a theme.
 344           *
 345           * @since 4.4.0
 346           *
 347           * @see WP_Customize_Manager::__construct()
 348           *
 349           * @param array                $components List of core components to load.
 350           * @param WP_Customize_Manager $this       WP_Customize_Manager instance.
 351           */
 352          $components = apply_filters( 'customize_loaded_components', $this->components, $this );
 353  
 354          require_once ( ABSPATH . WPINC . '/customize/class-wp-customize-selective-refresh.php' );
 355          $this->selective_refresh = new WP_Customize_Selective_Refresh( $this );
 356  
 357          if ( in_array( 'widgets', $components, true ) ) {
 358              require_once ( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
 359              $this->widgets = new WP_Customize_Widgets( $this );
 360          }
 361  
 362          if ( in_array( 'nav_menus', $components, true ) ) {
 363              require_once ( ABSPATH . WPINC . '/class-wp-customize-nav-menus.php' );
 364              $this->nav_menus = new WP_Customize_Nav_Menus( $this );
 365          }
 366  
 367          add_action( 'setup_theme', array( $this, 'setup_theme' ) );
 368          add_action( 'wp_loaded', array( $this, 'wp_loaded' ) );
 369  
 370          // Do not spawn cron (especially the alternate cron) while running the Customizer.
 371          remove_action( 'init', 'wp_cron' );
 372  
 373          // Do not run update checks when rendering the controls.
 374          remove_action( 'admin_init', '_maybe_update_core' );
 375          remove_action( 'admin_init', '_maybe_update_plugins' );
 376          remove_action( 'admin_init', '_maybe_update_themes' );
 377  
 378          add_action( 'wp_ajax_customize_save', array( $this, 'save' ) );
 379          add_action( 'wp_ajax_customize_trash', array( $this, 'handle_changeset_trash_request' ) );
 380          add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
 381          add_action( 'wp_ajax_customize_load_themes', array( $this, 'handle_load_themes_request' ) );
 382          add_filter( 'heartbeat_settings', array( $this, 'add_customize_screen_to_heartbeat_settings' ) );
 383          add_filter( 'heartbeat_received', array( $this, 'check_changeset_lock_with_heartbeat' ), 10, 3 );
 384          add_action( 'wp_ajax_customize_override_changeset_lock', array( $this, 'handle_override_changeset_lock_request' ) );
 385          add_action( 'wp_ajax_customize_dismiss_autosave_or_lock', array( $this, 'handle_dismiss_autosave_or_lock_request' ) );
 386  
 387          add_action( 'customize_register', array( $this, 'register_controls' ) );
 388          add_action( 'customize_register', array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first
 389          add_action( 'customize_controls_init', array( $this, 'prepare_controls' ) );
 390          add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) );
 391  
 392          // Render Common, Panel, Section, and Control templates.
 393          add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_panel_templates' ), 1 );
 394          add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_section_templates' ), 1 );
 395          add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_control_templates' ), 1 );
 396  
 397          // Export header video settings with the partial response.
 398          add_filter( 'customize_render_partials_response', array( $this, 'export_header_video_settings' ), 10, 3 );
 399  
 400          // Export the settings to JS via the _wpCustomizeSettings variable.
 401          add_action( 'customize_controls_print_footer_scripts', array( $this, 'customize_pane_settings' ), 1000 );
 402  
 403          // Add theme update notices.
 404          if ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) ) {
 405              require_once ABSPATH . '/wp-admin/includes/update.php';
 406              add_action( 'customize_controls_print_footer_scripts', 'wp_print_admin_notice_templates' );
 407          }
 408      }
 409  
 410      /**
 411       * Return true if it's an Ajax request.
 412       *
 413       * @since 3.4.0
 414       * @since 4.2.0 Added `$action` param.
 415       *
 416       * @param string|null $action Whether the supplied Ajax action is being run.
 417       * @return bool True if it's an Ajax request, false otherwise.
 418       */
 419  	public function doing_ajax( $action = null ) {
 420          if ( ! wp_doing_ajax() ) {
 421              return false;
 422          }
 423  
 424          if ( ! $action ) {
 425              return true;
 426          } else {
 427              /*
 428               * Note: we can't just use doing_action( "wp_ajax_{$action}" ) because we need
 429               * to check before admin-ajax.php gets to that point.
 430               */
 431              return isset( $_REQUEST['action'] ) && wp_unslash( $_REQUEST['action'] ) === $action;
 432          }
 433      }
 434  
 435      /**
 436       * Custom wp_die wrapper. Returns either the standard message for UI
 437       * or the Ajax message.
 438       *
 439       * @since 3.4.0
 440       *
 441       * @param mixed $ajax_message Ajax return
 442       * @param mixed $message UI message
 443       */
 444  	protected function wp_die( $ajax_message, $message = null ) {
 445          if ( $this->doing_ajax() ) {
 446              wp_die( $ajax_message );
 447          }
 448  
 449          if ( ! $message ) {
 450              $message = __( 'Cheatin&#8217; uh?' );
 451          }
 452  
 453          if ( $this->messenger_channel ) {
 454              ob_start();
 455              wp_enqueue_scripts();
 456              wp_print_scripts( array( 'customize-base' ) );
 457  
 458              $settings = array(
 459                  'messengerArgs' => array(
 460                      'channel' => $this->messenger_channel,
 461                      'url'     => wp_customize_url(),
 462                  ),
 463                  'error'         => $ajax_message,
 464              );
 465              ?>
 466              <script>
 467              ( function( api, settings ) {
 468                  var preview = new api.Messenger( settings.messengerArgs );
 469                  preview.send( 'iframe-loading-error', settings.error );
 470              } )( wp.customize, <?php echo wp_json_encode( $settings ); ?> );
 471              </script>
 472              <?php
 473              $message .= ob_get_clean();
 474          }
 475  
 476          wp_die( $message );
 477      }
 478  
 479      /**
 480       * Return the Ajax wp_die() handler if it's a customized request.
 481       *
 482       * @since 3.4.0
 483       * @deprecated 4.7.0
 484       *
 485       * @return callable Die handler.
 486       */
 487  	public function wp_die_handler() {
 488          _deprecated_function( __METHOD__, '4.7.0' );
 489  
 490          if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) {
 491              return '_ajax_wp_die_handler';
 492          }
 493  
 494          return '_default_wp_die_handler';
 495      }
 496  
 497      /**
 498       * Start preview and customize theme.
 499       *
 500       * Check if customize query variable exist. Init filters to filter the current theme.
 501       *
 502       * @since 3.4.0
 503       *
 504       * @global string $pagenow
 505       */
 506  	public function setup_theme() {
 507          global $pagenow;
 508  
 509          // Check permissions for customize.php access since this method is called before customize.php can run any code,
 510          if ( 'customize.php' === $pagenow && ! current_user_can( 'customize' ) ) {
 511              if ( ! is_user_logged_in() ) {
 512                  auth_redirect();
 513              } else {
 514                  wp_die(
 515                      '<h1>' . __( 'Cheatin&#8217; uh?' ) . '</h1>' .
 516                      '<p>' . __( 'Sorry, you are not allowed to customize this site.' ) . '</p>',
 517                      403
 518                  );
 519              }
 520              return;
 521          }
 522  
 523          // If a changeset was provided is invalid.
 524          if ( isset( $this->_changeset_uuid ) && false !== $this->_changeset_uuid && ! wp_is_uuid( $this->_changeset_uuid ) ) {
 525              $this->wp_die( -1, __( 'Invalid changeset UUID' ) );
 526          }
 527  
 528          /*
 529           * Clear incoming post data if the user lacks a CSRF token (nonce). Note that the customizer
 530           * application will inject the customize_preview_nonce query parameter into all Ajax requests.
 531           * For similar behavior elsewhere in WordPress, see rest_cookie_check_errors() which logs out
 532           * a user when a valid nonce isn't present.
 533           */
 534          $has_post_data_nonce = (
 535              check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'nonce', false )
 536              ||
 537              check_ajax_referer( 'save-customize_' . $this->get_stylesheet(), 'nonce', false )
 538              ||
 539              check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'customize_preview_nonce', false )
 540          );
 541          if ( ! current_user_can( 'customize' ) || ! $has_post_data_nonce ) {
 542              unset( $_POST['customized'] );
 543              unset( $_REQUEST['customized'] );
 544          }
 545  
 546          /*
 547           * If unauthenticated then require a valid changeset UUID to load the preview.
 548           * In this way, the UUID serves as a secret key. If the messenger channel is present,
 549           * then send unauthenticated code to prompt re-auth.
 550           */
 551          if ( ! current_user_can( 'customize' ) && ! $this->changeset_post_id() ) {
 552              $this->wp_die( $this->messenger_channel ? 0 : -1, __( 'Non-existent changeset UUID.' ) );
 553          }
 554  
 555          if ( ! headers_sent() ) {
 556              send_origin_headers();
 557          }
 558  
 559          // Hide the admin bar if we're embedded in the customizer iframe.
 560          if ( $this->messenger_channel ) {
 561              show_admin_bar( false );
 562          }
 563  
 564          if ( $this->is_theme_active() ) {
 565              // Once the theme is loaded, we'll validate it.
 566              add_action( 'after_setup_theme', array( $this, 'after_setup_theme' ) );
 567          } else {
 568              // If the requested theme is not the active theme and the user doesn't have the
 569              // switch_themes cap, bail.
 570              if ( ! current_user_can( 'switch_themes' ) ) {
 571                  $this->wp_die( -1, __( 'Sorry, you are not allowed to edit theme options on this site.' ) );
 572              }
 573  
 574              // If the theme has errors while loading, bail.
 575              if ( $this->theme()->errors() ) {
 576                  $this->wp_die( -1, $this->theme()->errors()->get_error_message() );
 577              }
 578  
 579              // If the theme isn't allowed per multisite settings, bail.
 580              if ( ! $this->theme()->is_allowed() ) {
 581                  $this->wp_die( -1, __( 'The requested theme does not exist.' ) );
 582              }
 583          }
 584  
 585          // Make sure changeset UUID is established immediately after the theme is loaded.
 586          add_action( 'after_setup_theme', array( $this, 'establish_loaded_changeset' ), 5 );
 587  
 588          /*
 589           * Import theme starter content for fresh installations when landing in the customizer.
 590           * Import starter content at after_setup_theme:100 so that any
 591           * add_theme_support( 'starter-content' ) calls will have been made.
 592           */
 593          if ( get_option( 'fresh_site' ) && 'customize.php' === $pagenow ) {
 594              add_action( 'after_setup_theme', array( $this, 'import_theme_starter_content' ), 100 );
 595          }
 596  
 597          $this->start_previewing_theme();
 598      }
 599  
 600      /**
 601       * Establish the loaded changeset.
 602       *
 603       * This method runs right at after_setup_theme and applies the 'customize_changeset_branching' filter to determine
 604       * whether concurrent changesets are allowed. Then if the Customizer is not initialized with a `changeset_uuid` param,
 605       * this method will determine which UUID should be used. If changeset branching is disabled, then the most saved
 606       * changeset will be loaded by default. Otherwise, if there are no existing saved changesets or if changeset branching is
 607       * enabled, then a new UUID will be generated.
 608       *
 609       * @since 4.9.0
 610       */
 611  	public function establish_loaded_changeset() {
 612          if ( empty( $this->_changeset_uuid ) ) {
 613              $changeset_uuid = null;
 614  
 615              if ( ! $this->branching() && $this->is_theme_active() ) {
 616                  $unpublished_changeset_posts = $this->get_changeset_posts(
 617                      array(
 618                          'post_status'               => array_diff( get_post_stati(), array( 'auto-draft', 'publish', 'trash', 'inherit', 'private' ) ),
 619                          'exclude_restore_dismissed' => false,
 620                          'author'                    => 'any',
 621                          'posts_per_page'            => 1,
 622                          'order'                     => 'DESC',
 623                          'orderby'                   => 'date',
 624                      )
 625                  );
 626                  $unpublished_changeset_post  = array_shift( $unpublished_changeset_posts );
 627                  if ( ! empty( $unpublished_changeset_post ) && wp_is_uuid( $unpublished_changeset_post->post_name ) ) {
 628                      $changeset_uuid = $unpublished_changeset_post->post_name;
 629                  }
 630              }
 631  
 632              // If no changeset UUID has been set yet, then generate a new one.
 633              if ( empty( $changeset_uuid ) ) {
 634                  $changeset_uuid = wp_generate_uuid4();
 635              }
 636  
 637              $this->_changeset_uuid = $changeset_uuid;
 638          }
 639  
 640          $this->set_changeset_lock( $this->changeset_post_id() );
 641      }
 642  
 643      /**
 644       * Callback to validate a theme once it is loaded
 645       *
 646       * @since 3.4.0
 647       */
 648  	public function after_setup_theme() {
 649          $doing_ajax_or_is_customized = ( $this->doing_ajax() || isset( $_POST['customized'] ) );
 650          if ( ! $doing_ajax_or_is_customized && ! validate_current_theme() ) {
 651              wp_redirect( 'themes.php?broken=true' );
 652              exit;
 653          }
 654      }
 655  
 656      /**
 657       * If the theme to be previewed isn't the active theme, add filter callbacks
 658       * to swap it out at runtime.
 659       *
 660       * @since 3.4.0
 661       */
 662  	public function start_previewing_theme() {
 663          // Bail if we're already previewing.
 664          if ( $this->is_preview() ) {
 665              return;
 666          }
 667  
 668          $this->previewing = true;
 669  
 670          if ( ! $this->is_theme_active() ) {
 671              add_filter( 'template', array( $this, 'get_template' ) );
 672              add_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
 673              add_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
 674  
 675              // @link: https://core.trac.wordpress.org/ticket/20027
 676              add_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
 677              add_filter( 'pre_option_template', array( $this, 'get_template' ) );
 678  
 679              // Handle custom theme roots.
 680              add_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
 681              add_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
 682          }
 683  
 684          /**
 685           * Fires once the Customizer theme preview has started.
 686           *
 687           * @since 3.4.0
 688           *
 689           * @param WP_Customize_Manager $this WP_Customize_Manager instance.
 690           */
 691          do_action( 'start_previewing_theme', $this );
 692      }
 693  
 694      /**
 695       * Stop previewing the selected theme.
 696       *
 697       * Removes filters to change the current theme.
 698       *
 699       * @since 3.4.0
 700       */
 701  	public function stop_previewing_theme() {
 702          if ( ! $this->is_preview() ) {
 703              return;
 704          }
 705  
 706          $this->previewing = false;
 707  
 708          if ( ! $this->is_theme_active() ) {
 709              remove_filter( 'template', array( $this, 'get_template' ) );
 710              remove_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
 711              remove_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
 712  
 713              // @link: https://core.trac.wordpress.org/ticket/20027
 714              remove_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
 715              remove_filter( 'pre_option_template', array( $this, 'get_template' ) );
 716  
 717              // Handle custom theme roots.
 718              remove_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
 719              remove_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
 720          }
 721  
 722          /**
 723           * Fires once the Customizer theme preview has stopped.
 724           *
 725           * @since 3.4.0
 726           *
 727           * @param WP_Customize_Manager $this WP_Customize_Manager instance.
 728           */
 729          do_action( 'stop_previewing_theme', $this );
 730      }
 731  
 732      /**
 733       * Gets whether settings are or will be previewed.
 734       *
 735       * @since 4.9.0
 736       * @see WP_Customize_Setting::preview()
 737       *
 738       * @return bool
 739       */
 740  	public function settings_previewed() {
 741          return $this->settings_previewed;
 742      }
 743  
 744      /**
 745       * Gets whether data from a changeset's autosaved revision should be loaded if it exists.
 746       *
 747       * @since 4.9.0
 748       * @see WP_Customize_Manager::changeset_data()
 749       *
 750       * @return bool Is using autosaved changeset revision.
 751       */
 752  	public function autosaved() {
 753          return $this->autosaved;
 754      }
 755  
 756      /**
 757       * Whether the changeset branching is allowed.
 758       *
 759       * @since 4.9.0
 760       * @see WP_Customize_Manager::establish_loaded_changeset()
 761       *
 762       * @return bool Is changeset branching.
 763       */
 764  	public function branching() {
 765  
 766          /**
 767           * Filters whether or not changeset branching is allowed.
 768           *
 769           * By default in core, when changeset branching is not allowed, changesets will operate
 770           * linearly in that only one saved changeset will exist at a time (with a 'draft' or
 771           * 'future' status). This makes the Customizer operate in a way that is similar to going to
 772           * "edit" to one existing post: all users will be making changes to the same post, and autosave
 773           * revisions will be made for that post.
 774           *
 775           * By contrast, when changeset branching is allowed, then the model is like users going
 776           * to "add new" for a page and each user makes changes independently of each other since
 777           * they are all operating on their own separate pages, each getting their own separate
 778           * initial auto-drafts and then once initially saved, autosave revisions on top of that
 779           * user's specific post.
 780           *
 781           * Since linear changesets are deemed to be more suitable for the majority of WordPress users,
 782           * they are the default. For WordPress sites that have heavy site management in the Customizer
 783           * by multiple users then branching changesets should be enabled by means of this filter.
 784           *
 785           * @since 4.9.0
 786           *
 787           * @param bool                 $allow_branching Whether branching is allowed. If `false`, the default,
 788           *                                              then only one saved changeset exists at a time.
 789           * @param WP_Customize_Manager $wp_customize    Manager instance.
 790           */
 791          $this->branching = apply_filters( 'customize_changeset_branching', $this->branching, $this );
 792  
 793          return $this->branching;
 794      }
 795  
 796      /**
 797       * Get the changeset UUID.
 798       *
 799       * @since 4.7.0
 800       * @see WP_Customize_Manager::establish_loaded_changeset()
 801       *
 802       * @return string UUID.
 803       */
 804  	public function changeset_uuid() {
 805          if ( empty( $this->_changeset_uuid ) ) {
 806              $this->establish_loaded_changeset();
 807          }
 808          return $this->_changeset_uuid;
 809      }
 810  
 811      /**
 812       * Get the theme being customized.
 813       *
 814       * @since 3.4.0
 815       *
 816       * @return WP_Theme
 817       */
 818  	public function theme() {
 819          if ( ! $this->theme ) {
 820              $this->theme = wp_get_theme();
 821          }
 822          return $this->theme;
 823      }
 824  
 825      /**
 826       * Get the registered settings.
 827       *
 828       * @since 3.4.0
 829       *
 830       * @return array
 831       */
 832  	public function settings() {
 833          return $this->settings;
 834      }
 835  
 836      /**
 837       * Get the registered controls.
 838       *
 839       * @since 3.4.0
 840       *
 841       * @return array
 842       */
 843  	public function controls() {
 844          return $this->controls;
 845      }
 846  
 847      /**
 848       * Get the registered containers.
 849       *
 850       * @since 4.0.0
 851       *
 852       * @return array
 853       */
 854  	public function containers() {
 855          return $this->containers;
 856      }
 857  
 858      /**
 859       * Get the registered sections.
 860       *
 861       * @since 3.4.0
 862       *
 863       * @return array
 864       */
 865  	public function sections() {
 866          return $this->sections;
 867      }
 868  
 869      /**
 870       * Get the registered panels.
 871       *
 872       * @since 4.0.0
 873       *
 874       * @return array Panels.
 875       */
 876  	public function panels() {
 877          return $this->panels;
 878      }
 879  
 880      /**
 881       * Checks if the current theme is active.
 882       *
 883       * @since 3.4.0
 884       *
 885       * @return bool
 886       */
 887  	public function is_theme_active() {
 888          return $this->get_stylesheet() == $this->original_stylesheet;
 889      }
 890  
 891      /**
 892       * Register styles/scripts and initialize the preview of each setting
 893       *
 894       * @since 3.4.0
 895       */
 896  	public function wp_loaded() {
 897  
 898          // Unconditionally register core types for panels, sections, and controls in case plugin unhooks all customize_register actions.
 899          $this->register_panel_type( 'WP_Customize_Panel' );
 900          $this->register_panel_type( 'WP_Customize_Themes_Panel' );
 901          $this->register_section_type( 'WP_Customize_Section' );
 902          $this->register_section_type( 'WP_Customize_Sidebar_Section' );
 903          $this->register_section_type( 'WP_Customize_Themes_Section' );
 904          $this->register_control_type( 'WP_Customize_Color_Control' );
 905          $this->register_control_type( 'WP_Customize_Media_Control' );
 906          $this->register_control_type( 'WP_Customize_Upload_Control' );
 907          $this->register_control_type( 'WP_Customize_Image_Control' );
 908          $this->register_control_type( 'WP_Customize_Background_Image_Control' );
 909          $this->register_control_type( 'WP_Customize_Background_Position_Control' );
 910          $this->register_control_type( 'WP_Customize_Cropped_Image_Control' );
 911          $this->register_control_type( 'WP_Customize_Site_Icon_Control' );
 912          $this->register_control_type( 'WP_Customize_Theme_Control' );
 913          $this->register_control_type( 'WP_Customize_Code_Editor_Control' );
 914          $this->register_control_type( 'WP_Customize_Date_Time_Control' );
 915  
 916          /**
 917           * Fires once WordPress has loaded, allowing scripts and styles to be initialized.
 918           *
 919           * @since 3.4.0
 920           *
 921           * @param WP_Customize_Manager $this WP_Customize_Manager instance.
 922           */
 923          do_action( 'customize_register', $this );
 924  
 925          if ( $this->settings_previewed() ) {
 926              foreach ( $this->settings as $setting ) {
 927                  $setting->preview();
 928              }
 929          }
 930  
 931          if ( $this->is_preview() && ! is_admin() ) {
 932              $this->customize_preview_init();
 933          }
 934      }
 935  
 936      /**
 937       * Prevents Ajax requests from following redirects when previewing a theme
 938       * by issuing a 200 response instead of a 30x.
 939       *
 940       * Instead, the JS will sniff out the location header.
 941       *
 942       * @since 3.4.0
 943       * @deprecated 4.7.0
 944       *
 945       * @param int $status Status.
 946       * @return int
 947       */
 948  	public function wp_redirect_status( $status ) {
 949          _deprecated_function( __FUNCTION__, '4.7.0' );
 950  
 951          if ( $this->is_preview() && ! is_admin() ) {
 952              return 200;
 953          }
 954  
 955          return $status;
 956      }
 957  
 958      /**
 959       * Find the changeset post ID for a given changeset UUID.
 960       *
 961       * @since 4.7.0
 962       *
 963       * @param string $uuid Changeset UUID.
 964       * @return int|null Returns post ID on success and null on failure.
 965       */
 966  	public function find_changeset_post_id( $uuid ) {
 967          $cache_group       = 'customize_changeset_post';
 968          $changeset_post_id = wp_cache_get( $uuid, $cache_group );
 969          if ( $changeset_post_id && 'customize_changeset' === get_post_type( $changeset_post_id ) ) {
 970              return $changeset_post_id;
 971          }
 972  
 973          $changeset_post_query = new WP_Query(
 974              array(
 975                  'post_type'              => 'customize_changeset',
 976                  'post_status'            => get_post_stati(),
 977                  'name'                   => $uuid,
 978                  'posts_per_page'         => 1,
 979                  'no_found_rows'          => true,
 980                  'cache_results'          => true,
 981                  'update_post_meta_cache' => false,
 982                  'update_post_term_cache' => false,
 983                  'lazy_load_term_meta'    => false,
 984              )
 985          );
 986          if ( ! empty( $changeset_post_query->posts ) ) {
 987              // Note: 'fields'=>'ids' is not being used in order to cache the post object as it will be needed.
 988              $changeset_post_id = $changeset_post_query->posts[0]->ID;
 989              wp_cache_set( $uuid, $changeset_post_id, $cache_group );
 990              return $changeset_post_id;
 991          }
 992  
 993          return null;
 994      }
 995  
 996      /**
 997       * Get changeset posts.
 998       *
 999       * @since 4.9.0
1000       *
1001       * @param array $args {
1002       *     Args to pass into `get_posts()` to query changesets.
1003       *
1004       *     @type int    $posts_per_page             Number of posts to return. Defaults to -1 (all posts).
1005       *     @type int    $author                     Post author. Defaults to current user.
1006       *     @type string $post_status                Status of changeset. Defaults to 'auto-draft'.
1007       *     @type bool   $exclude_restore_dismissed  Whether to exclude changeset auto-drafts that have been dismissed. Defaults to true.
1008       * }
1009       * @return WP_Post[] Auto-draft changesets.
1010       */
1011  	protected function get_changeset_posts( $args = array() ) {
1012          $default_args = array(
1013              'exclude_restore_dismissed' => true,
1014              'posts_per_page'            => -1,
1015              'post_type'                 => 'customize_changeset',
1016              'post_status'               => 'auto-draft',
1017              'order'                     => 'DESC',
1018              'orderby'                   => 'date',
1019              'no_found_rows'             => true,
1020              'cache_results'             => true,
1021              'update_post_meta_cache'    => false,
1022              'update_post_term_cache'    => false,
1023              'lazy_load_term_meta'       => false,
1024          );
1025          if ( get_current_user_id() ) {
1026              $default_args['author'] = get_current_user_id();
1027          }
1028          $args = array_merge( $default_args, $args );
1029  
1030          if ( ! empty( $args['exclude_restore_dismissed'] ) ) {
1031              unset( $args['exclude_restore_dismissed'] );
1032              $args['meta_query'] = array(
1033                  array(
1034                      'key'     => '_customize_restore_dismissed',
1035                      'compare' => 'NOT EXISTS',
1036                  ),
1037              );
1038          }
1039  
1040          return get_posts( $args );
1041      }
1042  
1043      /**
1044       * Dismiss all of the current user's auto-drafts (other than the present one).
1045       *
1046       * @since 4.9.0
1047       * @return int The number of auto-drafts that were dismissed.
1048       */
1049  	protected function dismiss_user_auto_draft_changesets() {
1050          $changeset_autodraft_posts = $this->get_changeset_posts(
1051              array(
1052                  'post_status'               => 'auto-draft',
1053                  'exclude_restore_dismissed' => true,
1054                  'posts_per_page'            => -1,
1055              )
1056          );
1057          $dismissed                 = 0;
1058          foreach ( $changeset_autodraft_posts as $autosave_autodraft_post ) {
1059              if ( $autosave_autodraft_post->ID === $this->changeset_post_id() ) {
1060                  continue;
1061              }
1062              if ( update_post_meta( $autosave_autodraft_post->ID, '_customize_restore_dismissed', true ) ) {
1063                  $dismissed++;
1064              }
1065          }
1066          return $dismissed;
1067      }
1068  
1069      /**
1070       * Get the changeset post id for the loaded changeset.
1071       *
1072       * @since 4.7.0
1073       *
1074       * @return int|null Post ID on success or null if there is no post yet saved.
1075       */
1076  	public function changeset_post_id() {
1077          if ( ! isset( $this->_changeset_post_id ) ) {
1078              $post_id = $this->find_changeset_post_id( $this->changeset_uuid() );
1079              if ( ! $post_id ) {
1080                  $post_id = false;
1081              }
1082              $this->_changeset_post_id = $post_id;
1083          }
1084          if ( false === $this->_changeset_post_id ) {
1085              return null;
1086          }
1087          return $this->_changeset_post_id;
1088      }
1089  
1090      /**
1091       * Get the data stored in a changeset post.
1092       *
1093       * @since 4.7.0
1094       *
1095       * @param int $post_id Changeset post ID.
1096       * @return array|WP_Error Changeset data or WP_Error on error.
1097       */
1098  	protected function get_changeset_post_data( $post_id ) {
1099          if ( ! $post_id ) {
1100              return new WP_Error( 'empty_post_id' );
1101          }
1102          $changeset_post = get_post( $post_id );
1103          if ( ! $changeset_post ) {
1104              return new WP_Error( 'missing_post' );
1105          }
1106          if ( 'revision' === $changeset_post->post_type ) {
1107              if ( 'customize_changeset' !== get_post_type( $changeset_post->post_parent ) ) {
1108                  return new WP_Error( 'wrong_post_type' );
1109              }
1110          } elseif ( 'customize_changeset' !== $changeset_post->post_type ) {
1111              return new WP_Error( 'wrong_post_type' );
1112          }
1113          $changeset_data = json_decode( $changeset_post->post_content, true );
1114          if ( function_exists( 'json_last_error' ) && json_last_error() ) {
1115              return new WP_Error( 'json_parse_error', '', json_last_error() );
1116          }
1117          if ( ! is_array( $changeset_data ) ) {
1118              return new WP_Error( 'expected_array' );
1119          }
1120          return $changeset_data;
1121      }
1122  
1123      /**
1124       * Get changeset data.
1125       *
1126       * @since 4.7.0
1127       * @since 4.9.0 This will return the changeset's data with a user's autosave revision merged on top, if one exists and $autosaved is true.
1128       *
1129       * @return array Changeset data.
1130       */
1131  	public function changeset_data() {
1132          if ( isset( $this->_changeset_data ) ) {
1133              return $this->_changeset_data;
1134          }
1135          $changeset_post_id = $this->changeset_post_id();
1136          if ( ! $changeset_post_id ) {
1137              $this->_changeset_data = array();
1138          } else {
1139              if ( $this->autosaved() ) {
1140                  $autosave_post = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
1141                  if ( $autosave_post ) {
1142                      $data = $this->get_changeset_post_data( $autosave_post->ID );
1143                      if ( ! is_wp_error( $data ) ) {
1144                          $this->_changeset_data = $data;
1145                      }
1146                  }
1147              }
1148  
1149              // Load data from the changeset if it was not loaded from an autosave.
1150              if ( ! isset( $this->_changeset_data ) ) {
1151                  $data = $this->get_changeset_post_data( $changeset_post_id );
1152                  if ( ! is_wp_error( $data ) ) {
1153                      $this->_changeset_data = $data;
1154                  } else {
1155                      $this->_changeset_data = array();
1156                  }
1157              }
1158          }
1159          return $this->_changeset_data;
1160      }
1161  
1162      /**
1163       * Starter content setting IDs.
1164       *
1165       * @since 4.7.0
1166       * @var array
1167       */
1168      protected $pending_starter_content_settings_ids = array();
1169  
1170      /**
1171       * Import theme starter content into the customized state.
1172       *
1173       * @since 4.7.0
1174       *
1175       * @param array $starter_content Starter content. Defaults to `get_theme_starter_content()`.
1176       */
1177  	function import_theme_starter_content( $starter_content = array() ) {
1178          if ( empty( $starter_content ) ) {
1179              $starter_content = get_theme_starter_content();
1180          }
1181  
1182          $changeset_data = array();
1183          if ( $this->changeset_post_id() ) {
1184              /*
1185               * Don't re-import starter content into a changeset saved persistently.
1186               * This will need to be revisited in the future once theme switching
1187               * is allowed with drafted/scheduled changesets, since switching to
1188               * another theme could result in more starter content being applied.
1189               * However, when doing an explicit save it is currently possible for
1190               * nav menus and nav menu items specifically to lose their starter_content
1191               * flags, thus resulting in duplicates being created since they fail
1192               * to get re-used. See #40146.
1193               */
1194              if ( 'auto-draft' !== get_post_status( $this->changeset_post_id() ) ) {
1195                  return;
1196              }
1197  
1198              $changeset_data = $this->get_changeset_post_data( $this->changeset_post_id() );
1199          }
1200  
1201          $sidebars_widgets = isset( $starter_content['widgets'] ) && ! empty( $this->widgets ) ? $starter_content['widgets'] : array();
1202          $attachments      = isset( $starter_content['attachments'] ) && ! empty( $this->nav_menus ) ? $starter_content['attachments'] : array();
1203          $posts            = isset( $starter_content['posts'] ) && ! empty( $this->nav_menus ) ? $starter_content['posts'] : array();
1204          $options          = isset( $starter_content['options'] ) ? $starter_content['options'] : array();
1205          $nav_menus        = isset( $starter_content['nav_menus'] ) && ! empty( $this->nav_menus ) ? $starter_content['nav_menus'] : array();
1206          $theme_mods       = isset( $starter_content['theme_mods'] ) ? $starter_content['theme_mods'] : array();
1207  
1208          // Widgets.
1209          $max_widget_numbers = array();
1210          foreach ( $sidebars_widgets as $sidebar_id => $widgets ) {
1211              $sidebar_widget_ids = array();
1212              foreach ( $widgets as $widget ) {
1213                  list( $id_base, $instance ) = $widget;
1214  
1215                  if ( ! isset( $max_widget_numbers[ $id_base ] ) ) {
1216  
1217                      // When $settings is an array-like object, get an intrinsic array for use with array_keys().
1218                      $settings = get_option( "widget_{$id_base}", array() );
1219                      if ( $settings instanceof ArrayObject || $settings instanceof ArrayIterator ) {
1220                          $settings = $settings->getArrayCopy();
1221                      }
1222  
1223                      // Find the max widget number for this type.
1224                      $widget_numbers = array_keys( $settings );
1225                      if ( count( $widget_numbers ) > 0 ) {
1226                          $widget_numbers[]               = 1;
1227                          $max_widget_numbers[ $id_base ] = call_user_func_array( 'max', $widget_numbers );
1228                      } else {
1229                          $max_widget_numbers[ $id_base ] = 1;
1230                      }
1231                  }
1232                  $max_widget_numbers[ $id_base ] += 1;
1233  
1234                  $widget_id  = sprintf( '%s-%d', $id_base, $max_widget_numbers[ $id_base ] );
1235                  $setting_id = sprintf( 'widget_%s[%d]', $id_base, $max_widget_numbers[ $id_base ] );
1236  
1237                  $setting_value = $this->widgets->sanitize_widget_js_instance( $instance );
1238                  if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
1239                      $this->set_post_value( $setting_id, $setting_value );
1240                      $this->pending_starter_content_settings_ids[] = $setting_id;
1241                  }
1242                  $sidebar_widget_ids[] = $widget_id;
1243              }
1244  
1245              $setting_id = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
1246              if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
1247                  $this->set_post_value( $setting_id, $sidebar_widget_ids );
1248                  $this->pending_starter_content_settings_ids[] = $setting_id;
1249              }
1250          }
1251  
1252          $starter_content_auto_draft_post_ids = array();
1253          if ( ! empty( $changeset_data['nav_menus_created_posts']['value'] ) ) {
1254              $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, $changeset_data['nav_menus_created_posts']['value'] );
1255          }
1256  
1257          // Make an index of all the posts needed and what their slugs are.
1258          $needed_posts = array();
1259          $attachments  = $this->prepare_starter_content_attachments( $attachments );
1260          foreach ( $attachments as $attachment ) {
1261              $key                  = 'attachment:' . $attachment['post_name'];
1262              $needed_posts[ $key ] = true;
1263          }
1264          foreach ( array_keys( $posts ) as $post_symbol ) {
1265              if ( empty( $posts[ $post_symbol ]['post_name'] ) && empty( $posts[ $post_symbol ]['post_title'] ) ) {
1266                  unset( $posts[ $post_symbol ] );
1267                  continue;
1268              }
1269              if ( empty( $posts[ $post_symbol ]['post_name'] ) ) {
1270                  $posts[ $post_symbol ]['post_name'] = sanitize_title( $posts[ $post_symbol ]['post_title'] );
1271              }
1272              if ( empty( $posts[ $post_symbol ]['post_type'] ) ) {
1273                  $posts[ $post_symbol ]['post_type'] = 'post';
1274              }
1275              $needed_posts[ $posts[ $post_symbol ]['post_type'] . ':' . $posts[ $post_symbol ]['post_name'] ] = true;
1276          }
1277          $all_post_slugs = array_merge(
1278              wp_list_pluck( $attachments, 'post_name' ),
1279              wp_list_pluck( $posts, 'post_name' )
1280          );
1281  
1282          /*
1283           * Obtain all post types referenced in starter content to use in query.
1284           * This is needed because 'any' will not account for post types not yet registered.
1285           */
1286          $post_types = array_filter( array_merge( array( 'attachment' ), wp_list_pluck( $posts, 'post_type' ) ) );
1287  
1288          // Re-use auto-draft starter content posts referenced in the current customized state.
1289          $existing_starter_content_posts = array();
1290          if ( ! empty( $starter_content_auto_draft_post_ids ) ) {
1291              $existing_posts_query = new WP_Query(
1292                  array(
1293                      'post__in'       => $starter_content_auto_draft_post_ids,
1294                      'post_status'    => 'auto-draft',
1295                      'post_type'      => $post_types,
1296                      'posts_per_page' => -1,
1297                  )
1298              );
1299              foreach ( $existing_posts_query->posts as $existing_post ) {
1300                  $post_name = $existing_post->post_name;
1301                  if ( empty( $post_name ) ) {
1302                      $post_name = get_post_meta( $existing_post->ID, '_customize_draft_post_name', true );
1303                  }
1304                  $existing_starter_content_posts[ $existing_post->post_type . ':' . $post_name ] = $existing_post;
1305              }
1306          }
1307  
1308          // Re-use non-auto-draft posts.
1309          if ( ! empty( $all_post_slugs ) ) {
1310              $existing_posts_query = new WP_Query(
1311                  array(
1312                      'post_name__in'  => $all_post_slugs,
1313                      'post_status'    => array_diff( get_post_stati(), array( 'auto-draft' ) ),
1314                      'post_type'      => 'any',
1315                      'posts_per_page' => -1,
1316                  )
1317              );
1318              foreach ( $existing_posts_query->posts as $existing_post ) {
1319                  $key = $existing_post->post_type . ':' . $existing_post->post_name;
1320                  if ( isset( $needed_posts[ $key ] ) && ! isset( $existing_starter_content_posts[ $key ] ) ) {
1321                      $existing_starter_content_posts[ $key ] = $existing_post;
1322                  }
1323              }
1324          }
1325  
1326          // Attachments are technically posts but handled differently.
1327          if ( ! empty( $attachments ) ) {
1328  
1329              $attachment_ids = array();
1330  
1331              foreach ( $attachments as $symbol => $attachment ) {
1332                  $file_array    = array(
1333                      'name' => $attachment['file_name'],
1334                  );
1335                  $file_path     = $attachment['file_path'];
1336                  $attachment_id = null;
1337                  $attached_file = null;
1338                  if ( isset( $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ] ) ) {
1339                      $attachment_post = $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ];
1340                      $attachment_id   = $attachment_post->ID;
1341                      $attached_file   = get_attached_file( $attachment_id );
1342                      if ( empty( $attached_file ) || ! file_exists( $attached_file ) ) {
1343                          $attachment_id = null;
1344                          $attached_file = null;
1345                      } elseif ( $this->get_stylesheet() !== get_post_meta( $attachment_post->ID, '_starter_content_theme', true ) ) {
1346  
1347                          // Re-generate attachment metadata since it was previously generated for a different theme.
1348                          $metadata = wp_generate_attachment_metadata( $attachment_post->ID, $attached_file );
1349                          wp_update_attachment_metadata( $attachment_id, $metadata );
1350                          update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
1351                      }
1352                  }
1353  
1354                  // Insert the attachment auto-draft because it doesn't yet exist or the attached file is gone.
1355                  if ( ! $attachment_id ) {
1356  
1357                      // Copy file to temp location so that original file won't get deleted from theme after sideloading.
1358                      $temp_file_name = wp_tempnam( basename( $file_path ) );
1359                      if ( $temp_file_name && copy( $file_path, $temp_file_name ) ) {
1360                          $file_array['tmp_name'] = $temp_file_name;
1361                      }
1362                      if ( empty( $file_array['tmp_name'] ) ) {
1363                          continue;
1364                      }
1365  
1366                      $attachment_post_data = array_merge(
1367                          wp_array_slice_assoc( $attachment, array( 'post_title', 'post_content', 'post_excerpt' ) ),
1368                          array(
1369                              'post_status' => 'auto-draft', // So attachment will be garbage collected in a week if changeset is never published.
1370                          )
1371                      );
1372  
1373                      // In PHP < 5.6 filesize() returns 0 for the temp files unless we clear the file status cache.
1374                      // Technically, PHP < 5.6.0 || < 5.5.13 || < 5.4.29 but no need to be so targeted.
1375                      // See https://bugs.php.net/bug.php?id=65701
1376                      if ( version_compare( PHP_VERSION, '5.6', '<' ) ) {
1377                          clearstatcache();
1378                      }
1379  
1380                      $attachment_id = media_handle_sideload( $file_array, 0, null, $attachment_post_data );
1381                      if ( is_wp_error( $attachment_id ) ) {
1382                          continue;
1383                      }
1384                      update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
1385                      update_post_meta( $attachment_id, '_customize_draft_post_name', $attachment['post_name'] );
1386                  }
1387  
1388                  $attachment_ids[ $symbol ] = $attachment_id;
1389              }
1390              $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, array_values( $attachment_ids ) );
1391          }
1392  
1393          // Posts & pages.
1394          if ( ! empty( $posts ) ) {
1395              foreach ( array_keys( $posts ) as $post_symbol ) {
1396                  if ( empty( $posts[ $post_symbol ]['post_type'] ) || empty( $posts[ $post_symbol ]['post_name'] ) ) {
1397                      continue;
1398                  }
1399                  $post_type = $posts[ $post_symbol ]['post_type'];
1400                  if ( ! empty( $posts[ $post_symbol ]['post_name'] ) ) {
1401                      $post_name = $posts[ $post_symbol ]['post_name'];
1402                  } elseif ( ! empty( $posts[ $post_symbol ]['post_title'] ) ) {
1403                      $post_name = sanitize_title( $posts[ $post_symbol ]['post_title'] );
1404                  } else {
1405                      continue;
1406                  }
1407  
1408                  // Use existing auto-draft post if one already exists with the same type and name.
1409                  if ( isset( $existing_starter_content_posts[ $post_type . ':' . $post_name ] ) ) {
1410                      $posts[ $post_symbol ]['ID'] = $existing_starter_content_posts[ $post_type . ':' . $post_name ]->ID;
1411                      continue;
1412                  }
1413  
1414                  // Translate the featured image symbol.
1415                  if ( ! empty( $posts[ $post_symbol ]['thumbnail'] )
1416                      && preg_match( '/^{{(?P<symbol>.+)}}$/', $posts[ $post_symbol ]['thumbnail'], $matches )
1417                      && isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1418                      $posts[ $post_symbol ]['meta_input']['_thumbnail_id'] = $attachment_ids[ $matches['symbol'] ];
1419                  }
1420  
1421                  if ( ! empty( $posts[ $post_symbol ]['template'] ) ) {
1422                      $posts[ $post_symbol ]['meta_input']['_wp_page_template'] = $posts[ $post_symbol ]['template'];
1423                  }
1424  
1425                  $r = $this->nav_menus->insert_auto_draft_post( $posts[ $post_symbol ] );
1426                  if ( $r instanceof WP_Post ) {
1427                      $posts[ $post_symbol ]['ID'] = $r->ID;
1428                  }
1429              }
1430  
1431              $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, wp_list_pluck( $posts, 'ID' ) );
1432          }
1433  
1434          // The nav_menus_created_posts setting is why nav_menus component is dependency for adding posts.
1435          if ( ! empty( $this->nav_menus ) && ! empty( $starter_content_auto_draft_post_ids ) ) {
1436              $setting_id = 'nav_menus_created_posts';
1437              $this->set_post_value( $setting_id, array_unique( array_values( $starter_content_auto_draft_post_ids ) ) );
1438              $this->pending_starter_content_settings_ids[] = $setting_id;
1439          }
1440  
1441          // Nav menus.
1442          $placeholder_id              = -1;
1443          $reused_nav_menu_setting_ids = array();
1444          foreach ( $nav_menus as $nav_menu_location => $nav_menu ) {
1445  
1446              $nav_menu_term_id    = null;
1447              $nav_menu_setting_id = null;
1448              $matches             = array();
1449  
1450              // Look for an existing placeholder menu with starter content to re-use.
1451              foreach ( $changeset_data as $setting_id => $setting_params ) {
1452                  $can_reuse = (
1453                      ! empty( $setting_params['starter_content'] )
1454                      &&
1455                      ! in_array( $setting_id, $reused_nav_menu_setting_ids, true )
1456                      &&
1457                      preg_match( '#^nav_menu\[(?P<nav_menu_id>-?\d+)\]$#', $setting_id, $matches )
1458                  );
1459                  if ( $can_reuse ) {
1460                      $nav_menu_term_id              = intval( $matches['nav_menu_id'] );
1461                      $nav_menu_setting_id           = $setting_id;
1462                      $reused_nav_menu_setting_ids[] = $setting_id;
1463                      break;
1464                  }
1465              }
1466  
1467              if ( ! $nav_menu_term_id ) {
1468                  while ( isset( $changeset_data[ sprintf( 'nav_menu[%d]', $placeholder_id ) ] ) ) {
1469                      $placeholder_id--;
1470                  }
1471                  $nav_menu_term_id    = $placeholder_id;
1472                  $nav_menu_setting_id = sprintf( 'nav_menu[%d]', $placeholder_id );
1473              }
1474  
1475              $this->set_post_value(
1476                  $nav_menu_setting_id, array(
1477                      'name' => isset( $nav_menu['name'] ) ? $nav_menu['name'] : $nav_menu_location,
1478                  )
1479              );
1480              $this->pending_starter_content_settings_ids[] = $nav_menu_setting_id;
1481  
1482              // @todo Add support for menu_item_parent.
1483              $position = 0;
1484              foreach ( $nav_menu['items'] as $nav_menu_item ) {
1485                  $nav_menu_item_setting_id = sprintf( 'nav_menu_item[%d]', $placeholder_id-- );
1486                  if ( ! isset( $nav_menu_item['position'] ) ) {
1487                      $nav_menu_item['position'] = $position++;
1488                  }
1489                  $nav_menu_item['nav_menu_term_id'] = $nav_menu_term_id;
1490  
1491                  if ( isset( $nav_menu_item['object_id'] ) ) {
1492                      if ( 'post_type' === $nav_menu_item['type'] && preg_match( '/^{{(?P<symbol>.+)}}$/', $nav_menu_item['object_id'], $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
1493                          $nav_menu_item['object_id'] = $posts[ $matches['symbol'] ]['ID'];
1494                          if ( empty( $nav_menu_item['title'] ) ) {
1495                              $original_object        = get_post( $nav_menu_item['object_id'] );
1496                              $nav_menu_item['title'] = $original_object->post_title;
1497                          }
1498                      } else {
1499                          continue;
1500                      }
1501                  } else {
1502                      $nav_menu_item['object_id'] = 0;
1503                  }
1504  
1505                  if ( empty( $changeset_data[ $nav_menu_item_setting_id ] ) || ! empty( $changeset_data[ $nav_menu_item_setting_id ]['starter_content'] ) ) {
1506                      $this->set_post_value( $nav_menu_item_setting_id, $nav_menu_item );
1507                      $this->pending_starter_content_settings_ids[] = $nav_menu_item_setting_id;
1508                  }
1509              }
1510  
1511              $setting_id = sprintf( 'nav_menu_locations[%s]', $nav_menu_location );
1512              if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
1513                  $this->set_post_value( $setting_id, $nav_menu_term_id );
1514                  $this->pending_starter_content_settings_ids[] = $setting_id;
1515              }
1516          }
1517  
1518          // Options.
1519          foreach ( $options as $name => $value ) {
1520              if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
1521                  if ( isset( $posts[ $matches['symbol'] ] ) ) {
1522                      $value = $posts[ $matches['symbol'] ]['ID'];
1523                  } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1524                      $value = $attachment_ids[ $matches['symbol'] ];
1525                  } else {
1526                      continue;
1527                  }
1528              }
1529  
1530              if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
1531                  $this->set_post_value( $name, $value );
1532                  $this->pending_starter_content_settings_ids[] = $name;
1533              }
1534          }
1535  
1536          // Theme mods.
1537          foreach ( $theme_mods as $name => $value ) {
1538              if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
1539                  if ( isset( $posts[ $matches['symbol'] ] ) ) {
1540                      $value = $posts[ $matches['symbol'] ]['ID'];
1541                  } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1542                      $value = $attachment_ids[ $matches['symbol'] ];
1543                  } else {
1544                      continue;
1545                  }
1546              }
1547  
1548              // Handle header image as special case since setting has a legacy format.
1549              if ( 'header_image' === $name ) {
1550                  $name     = 'header_image_data';
1551                  $metadata = wp_get_attachment_metadata( $value );
1552                  if ( empty( $metadata ) ) {
1553                      continue;
1554                  }
1555                  $value = array(
1556                      'attachment_id' => $value,
1557                      'url'           => wp_get_attachment_url( $value ),
1558                      'height'        => $metadata['height'],
1559                      'width'         => $metadata['width'],
1560                  );
1561              } elseif ( 'background_image' === $name ) {
1562                  $value = wp_get_attachment_url( $value );
1563              }
1564  
1565              if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
1566                  $this->set_post_value( $name, $value );
1567                  $this->pending_starter_content_settings_ids[] = $name;
1568              }
1569          }
1570  
1571          if ( ! empty( $this->pending_starter_content_settings_ids ) ) {
1572              if ( did_action( 'customize_register' ) ) {
1573                  $this->_save_starter_content_changeset();
1574              } else {
1575                  add_action( 'customize_register', array( $this, '_save_starter_content_changeset' ), 1000 );
1576              }
1577          }
1578      }
1579  
1580      /**
1581       * Prepare starter content attachments.
1582       *
1583       * Ensure that the attachments are valid and that they have slugs and file name/path.
1584       *
1585       * @since 4.7.0
1586       *
1587       * @param array $attachments Attachments.
1588       * @return array Prepared attachments.
1589       */
1590  	protected function prepare_starter_content_attachments( $attachments ) {
1591          $prepared_attachments = array();
1592          if ( empty( $attachments ) ) {
1593              return $prepared_attachments;
1594          }
1595  
1596          // Such is The WordPress Way.
1597          require_once( ABSPATH . 'wp-admin/includes/file.php' );
1598          require_once( ABSPATH . 'wp-admin/includes/media.php' );
1599          require_once( ABSPATH . 'wp-admin/includes/image.php' );
1600  
1601          foreach ( $attachments as $symbol => $attachment ) {
1602  
1603              // A file is required and URLs to files are not currently allowed.
1604              if ( empty( $attachment['file'] ) || preg_match( '#^https?://$#', $attachment['file'] ) ) {
1605                  continue;
1606              }
1607  
1608              $file_path = null;
1609              if ( file_exists( $attachment['file'] ) ) {
1610                  $file_path = $attachment['file']; // Could be absolute path to file in plugin.
1611              } elseif ( is_child_theme() && file_exists( get_stylesheet_directory() . '/' . $attachment['file'] ) ) {
1612                  $file_path = get_stylesheet_directory() . '/' . $attachment['file'];
1613              } elseif ( file_exists( get_template_directory() . '/' . $attachment['file'] ) ) {
1614                  $file_path = get_template_directory() . '/' . $attachment['file'];
1615              } else {
1616                  continue;
1617              }
1618              $file_name = basename( $attachment['file'] );
1619  
1620              // Skip file types that are not recognized.
1621              $checked_filetype = wp_check_filetype( $file_name );
1622              if ( empty( $checked_filetype['type'] ) ) {
1623                  continue;
1624              }
1625  
1626              // Ensure post_name is set since not automatically derived from post_title for new auto-draft posts.
1627              if ( empty( $attachment['post_name'] ) ) {
1628                  if ( ! empty( $attachment['post_title'] ) ) {
1629                      $attachment['post_name'] = sanitize_title( $attachment['post_title'] );
1630                  } else {
1631                      $attachment['post_name'] = sanitize_title( preg_replace( '/\.\w+$/', '', $file_name ) );
1632                  }
1633              }
1634  
1635              $attachment['file_name']         = $file_name;
1636              $attachment['file_path']         = $file_path;
1637              $prepared_attachments[ $symbol ] = $attachment;
1638          }
1639          return $prepared_attachments;
1640      }
1641  
1642      /**
1643       * Save starter content changeset.
1644       *
1645       * @since 4.7.0
1646       */
1647  	public function _save_starter_content_changeset() {
1648  
1649          if ( empty( $this->pending_starter_content_settings_ids ) ) {
1650              return;
1651          }
1652  
1653          $this->save_changeset_post(
1654              array(
1655                  'data'            => array_fill_keys( $this->pending_starter_content_settings_ids, array( 'starter_content' => true ) ),
1656                  'starter_content' => true,
1657              )
1658          );
1659          $this->saved_starter_content_changeset = true;
1660  
1661          $this->pending_starter_content_settings_ids = array();
1662      }
1663  
1664      /**
1665       * Get dirty pre-sanitized setting values in the current customized state.
1666       *
1667       * The returned array consists of a merge of three sources:
1668       * 1. If the theme is not currently active, then the base array is any stashed
1669       *    theme mods that were modified previously but never published.
1670       * 2. The values from the current changeset, if it exists.
1671       * 3. If the user can customize, the values parsed from the incoming
1672       *    `$_POST['customized']` JSON data.
1673       * 4. Any programmatically-set post values via `WP_Customize_Manager::set_post_value()`.
1674       *
1675       * The name "unsanitized_post_values" is a carry-over from when the customized
1676       * state was exclusively sourced from `$_POST['customized']`. Nevertheless,
1677       * the value returned will come from the current changeset post and from the
1678       * incoming post data.
1679       *
1680       * @since 4.1.1
1681       * @since 4.7.0 Added $args param and merging with changeset values and stashed theme mods.
1682       *
1683       * @param array $args {
1684       *     Args.
1685       *
1686       *     @type bool $exclude_changeset Whether the changeset values should also be excluded. Defaults to false.
1687       *     @type bool $exclude_post_data Whether the post input values should also be excluded. Defaults to false when lacking the customize capability.
1688       * }
1689       * @return array
1690       */
1691  	public function unsanitized_post_values( $args = array() ) {
1692          $args = array_merge(
1693              array(
1694                  'exclude_changeset' => false,
1695                  'exclude_post_data' => ! current_user_can( 'customize' ),
1696              ),
1697              $args
1698          );
1699  
1700          $values = array();
1701  
1702          // Let default values be from the stashed theme mods if doing a theme switch and if no changeset is present.
1703          if ( ! $this->is_theme_active() ) {
1704              $stashed_theme_mods = get_option( 'customize_stashed_theme_mods' );
1705              $stylesheet         = $this->get_stylesheet();
1706              if ( isset( $stashed_theme_mods[ $stylesheet ] ) ) {
1707                  $values = array_merge( $values, wp_list_pluck( $stashed_theme_mods[ $stylesheet ], 'value' ) );
1708              }
1709          }
1710  
1711          if ( ! $args['exclude_changeset'] ) {
1712              foreach ( $this->changeset_data() as $setting_id => $setting_params ) {
1713                  if ( ! array_key_exists( 'value', $setting_params ) ) {
1714                      continue;
1715                  }
1716                  if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) {
1717  
1718                      // Ensure that theme mods values are only used if they were saved under the current theme.
1719                      $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
1720                      if ( preg_match( $namespace_pattern, $setting_id, $matches ) && $this->get_stylesheet() === $matches['stylesheet'] ) {
1721                          $values[ $matches['setting_id'] ] = $setting_params['value'];
1722                      }
1723                  } else {
1724                      $values[ $setting_id ] = $setting_params['value'];
1725                  }
1726              }
1727          }
1728  
1729          if ( ! $args['exclude_post_data'] ) {
1730              if ( ! isset( $this->_post_values ) ) {
1731                  if ( isset( $_POST['customized'] ) ) {
1732                      $post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
1733                  } else {
1734                      $post_values = array();
1735                  }
1736                  if ( is_array( $post_values ) ) {
1737                      $this->_post_values = $post_values;
1738                  } else {
1739                      $this->_post_values = array();
1740                  }
1741              }
1742              $values = array_merge( $values, $this->_post_values );
1743          }
1744          return $values;
1745      }
1746  
1747      /**
1748       * Returns the sanitized value for a given setting from the current customized state.
1749       *
1750       * The name "post_value" is a carry-over from when the customized state was exclusively
1751       * sourced from `$_POST['customized']`. Nevertheless, the value returned will come
1752       * from the current changeset post and from the incoming post data.
1753       *
1754       * @since 3.4.0
1755       * @since 4.1.1 Introduced the `$default` parameter.
1756       * @since 4.6.0 `$default` is now returned early when the setting post value is invalid.
1757       *
1758       * @see WP_REST_Server::dispatch()
1759       * @see WP_REST_Request::sanitize_params()
1760       * @see WP_REST_Request::has_valid_params()
1761       *
1762       * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object.
1763       * @param mixed                $default Value returned $setting has no post value (added in 4.2.0)
1764       *                                      or the post value is invalid (added in 4.6.0).
1765       * @return string|mixed $post_value Sanitized value or the $default provided.
1766       */
1767  	public function post_value( $setting, $default = null ) {
1768          $post_values = $this->unsanitized_post_values();
1769          if ( ! array_key_exists( $setting->id, $post_values ) ) {
1770              return $default;
1771          }
1772          $value = $post_values[ $setting->id ];
1773          $valid = $setting->validate( $value );
1774          if ( is_wp_error( $valid ) ) {
1775              return $default;
1776          }
1777          $value = $setting->sanitize( $value );
1778          if ( is_null( $value ) || is_wp_error( $value ) ) {
1779              return $default;
1780          }
1781          return $value;
1782      }
1783  
1784      /**
1785       * Override a setting's value in the current customized state.
1786       *
1787       * The name "post_value" is a carry-over from when the customized state was
1788       * exclusively sourced from `$_POST['customized']`.
1789       *
1790       * @since 4.2.0
1791       *
1792       * @param string $setting_id ID for the WP_Customize_Setting instance.
1793       * @param mixed  $value      Post value.
1794       */
1795  	public function set_post_value( $setting_id, $value ) {
1796          $this->unsanitized_post_values(); // Populate _post_values from $_POST['customized'].
1797          $this->_post_values[ $setting_id ] = $value;
1798  
1799          /**
1800           * Announce when a specific setting's unsanitized post value has been set.
1801           *
1802           * Fires when the WP_Customize_Manager::set_post_value() method is called.
1803           *
1804           * The dynamic portion of the hook name, `$setting_id`, refers to the setting ID.
1805           *
1806           * @since 4.4.0
1807           *
1808           * @param mixed                $value Unsanitized setting post value.
1809           * @param WP_Customize_Manager $this  WP_Customize_Manager instance.
1810           */
1811          do_action( "customize_post_value_set_{$setting_id}", $value, $this );
1812  
1813          /**
1814           * Announce when any setting's unsanitized post value has been set.
1815           *
1816           * Fires when the WP_Customize_Manager::set_post_value() method is called.
1817           *
1818           * This is useful for `WP_Customize_Setting` instances to watch
1819           * in order to update a cached previewed value.
1820           *
1821           * @since 4.4.0
1822           *
1823           * @param string               $setting_id Setting ID.
1824           * @param mixed                $value      Unsanitized setting post value.
1825           * @param WP_Customize_Manager $this       WP_Customize_Manager instance.
1826           */
1827          do_action( 'customize_post_value_set', $setting_id, $value, $this );
1828      }
1829  
1830      /**
1831       * Print JavaScript settings.
1832       *
1833       * @since 3.4.0
1834       */
1835  	public function customize_preview_init() {
1836  
1837          /*
1838           * Now that Customizer previews are loaded into iframes via GET requests
1839           * and natural URLs with transaction UUIDs added, we need to ensure that
1840           * the responses are never cached by proxies. In practice, this will not
1841           * be needed if the user is logged-in anyway. But if anonymous access is
1842           * allowed then the auth cookies would not be sent and WordPress would
1843           * not send no-cache headers by default.
1844           */
1845          if ( ! headers_sent() ) {
1846              nocache_headers();
1847              header( 'X-Robots: noindex, nofollow, noarchive' );
1848          }
1849          add_action( 'wp_head', 'wp_no_robots' );
1850          add_filter( 'wp_headers', array( $this, 'filter_iframe_security_headers' ) );
1851  
1852          /*
1853           * If preview is being served inside the customizer preview iframe, and
1854           * if the user doesn't have customize capability, then it is assumed
1855           * that the user's session has expired and they need to re-authenticate.
1856           */
1857          if ( $this->messenger_channel && ! current_user_can( 'customize' ) ) {
1858              $this->wp_die( -1, __( 'Unauthorized. You may remove the customize_messenger_channel param to preview as frontend.' ) );
1859              return;
1860          }
1861  
1862          $this->prepare_controls();
1863  
1864          add_filter( 'wp_redirect', array( $this, 'add_state_query_params' ) );
1865  
1866          wp_enqueue_script( 'customize-preview' );
1867          wp_enqueue_style( 'customize-preview' );
1868          add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) );
1869          add_action( 'wp_head', array( $this, 'remove_frameless_preview_messenger_channel' ) );
1870          add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 );
1871          add_filter( 'get_edit_post_link', '__return_empty_string' );
1872  
1873          /**
1874           * Fires once the Customizer preview has initialized and JavaScript
1875           * settings have been printed.
1876           *
1877           * @since 3.4.0
1878           *
1879           * @param WP_Customize_Manager $this WP_Customize_Manager instance.
1880           */
1881          do_action( 'customize_preview_init', $this );
1882      }
1883  
1884      /**
1885       * Filter the X-Frame-Options and Content-Security-Policy headers to ensure frontend can load in customizer.
1886       *
1887       * @since 4.7.0
1888       *
1889       * @param array $headers Headers.
1890       * @return array Headers.
1891       */
1892  	public function filter_iframe_security_headers( $headers ) {
1893          $customize_url                      = admin_url( 'customize.php' );
1894          $headers['X-Frame-Options']         = 'ALLOW-FROM ' . $customize_url;
1895          $headers['Content-Security-Policy'] = 'frame-ancestors ' . preg_replace( '#^(\w+://[^/]+).+?$#', '$1', $customize_url );
1896          return $headers;
1897      }
1898  
1899      /**
1900       * Add customize state query params to a given URL if preview is allowed.
1901       *
1902       * @since 4.7.0
1903       * @see wp_redirect()
1904       * @see WP_Customize_Manager::get_allowed_url()
1905       *
1906       * @param string $url URL.
1907       * @return string URL.
1908       */
1909  	public function add_state_query_params( $url ) {
1910          $parsed_original_url = wp_parse_url( $url );
1911          $is_allowed          = false;
1912          foreach ( $this->get_allowed_urls() as $allowed_url ) {
1913              $parsed_allowed_url = wp_parse_url( $allowed_url );
1914              $is_allowed         = (
1915                  $parsed_allowed_url['scheme'] === $parsed_original_url['scheme']
1916                  &&
1917                  $parsed_allowed_url['host'] === $parsed_original_url['host']
1918                  &&
1919                  0 === strpos( $parsed_original_url['path'], $parsed_allowed_url['path'] )
1920              );
1921              if ( $is_allowed ) {
1922                  break;
1923              }
1924          }
1925  
1926          if ( $is_allowed ) {
1927              $query_params = array(
1928                  'customize_changeset_uuid' => $this->changeset_uuid(),
1929              );
1930              if ( ! $this->is_theme_active() ) {
1931                  $query_params['customize_theme'] = $this->get_stylesheet();
1932              }
1933              if ( $this->messenger_channel ) {
1934                  $query_params['customize_messenger_channel'] = $this->messenger_channel;
1935              }
1936              $url = add_query_arg( $query_params, $url );
1937          }
1938  
1939          return $url;
1940      }
1941  
1942      /**
1943       * Prevent sending a 404 status when returning the response for the customize
1944       * preview, since it causes the jQuery Ajax to fail. Send 200 instead.
1945       *
1946       * @since 4.0.0
1947       * @deprecated 4.7.0
1948       */
1949  	public function customize_preview_override_404_status() {
1950          _deprecated_function( __METHOD__, '4.7.0' );
1951      }
1952  
1953      /**
1954       * Print base element for preview frame.
1955       *
1956       * @since 3.4.0
1957       * @deprecated 4.7.0
1958       */
1959  	public function customize_preview_base() {
1960          _deprecated_function( __METHOD__, '4.7.0' );
1961      }
1962  
1963      /**
1964       * Print a workaround to handle HTML5 tags in IE < 9.
1965       *
1966       * @since 3.4.0
1967       * @deprecated 4.7.0 Customizer no longer supports IE8, so all supported browsers recognize HTML5.
1968       */
1969  	public function customize_preview_html5() {
1970          _deprecated_function( __FUNCTION__, '4.7.0' );
1971      }
1972  
1973      /**
1974       * Print CSS for loading indicators for the Customizer preview.
1975       *
1976       * @since 4.2.0
1977       */
1978  	public function customize_preview_loading_style() {
1979          ?>
1980          <style>
1981              body.wp-customizer-unloading {
1982                  opacity: 0.25;
1983                  cursor: progress !important;
1984                  -webkit-transition: opacity 0.5s;
1985                  transition: opacity 0.5s;
1986              }
1987              body.wp-customizer-unloading * {
1988                  pointer-events: none !important;
1989              }
1990              form.customize-unpreviewable,
1991              form.customize-unpreviewable input,
1992              form.customize-unpreviewable select,
1993              form.customize-unpreviewable button,
1994              a.customize-unpreviewable,
1995              area.customize-unpreviewable {
1996                  cursor: not-allowed !important;
1997              }
1998          </style>
1999          <?php
2000      }
2001  
2002      /**
2003       * Remove customize_messenger_channel query parameter from the preview window when it is not in an iframe.
2004       *
2005       * This ensures that the admin bar will be shown. It also ensures that link navigation will
2006       * work as expected since the parent frame is not being sent the URL to navigate to.
2007       *
2008       * @since 4.7.0
2009       */
2010  	public function remove_frameless_preview_messenger_channel() {
2011          if ( ! $this->messenger_channel ) {
2012              return;
2013          }
2014          ?>
2015          <script>
2016          ( function() {
2017              var urlParser, oldQueryParams, newQueryParams, i;
2018              if ( parent !== window ) {
2019                  return;
2020              }
2021              urlParser = document.createElement( 'a' );
2022              urlParser.href = location.href;
2023              oldQueryParams = urlParser.search.substr( 1 ).split( /&/ );
2024              newQueryParams = [];
2025              for ( i = 0; i < oldQueryParams.length; i += 1 ) {
2026                  if ( ! /^customize_messenger_channel=/.test( oldQueryParams[ i ] ) ) {
2027                      newQueryParams.push( oldQueryParams[ i ] );
2028                  }
2029              }
2030              urlParser.search = newQueryParams.join( '&' );
2031              if ( urlParser.search !== location.search ) {
2032                  location.replace( urlParser.href );
2033              }
2034          } )();
2035          </script>
2036          <?php
2037      }
2038  
2039      /**
2040       * Print JavaScript settings for preview frame.
2041       *
2042       * @since 3.4.0
2043       */
2044  	public function customize_preview_settings() {
2045          $post_values                 = $this->unsanitized_post_values( array( 'exclude_changeset' => true ) );
2046          $setting_validities          = $this->validate_setting_values( $post_values );
2047          $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities );
2048  
2049          // Note that the REQUEST_URI is not passed into home_url() since this breaks subdirectory installations.
2050          $self_url           = empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
2051          $state_query_params = array(
2052              'customize_theme',
2053              'customize_changeset_uuid',
2054              'customize_messenger_channel',
2055          );
2056          $self_url           = remove_query_arg( $state_query_params, $self_url );
2057  
2058          $allowed_urls  = $this->get_allowed_urls();
2059          $allowed_hosts = array();
2060          foreach ( $allowed_urls as $allowed_url ) {
2061              $parsed = wp_parse_url( $allowed_url );
2062              if ( empty( $parsed['host'] ) ) {
2063                  continue;
2064              }
2065              $host = $parsed['host'];
2066              if ( ! empty( $parsed['port'] ) ) {
2067                  $host .= ':' . $parsed['port'];
2068              }
2069              $allowed_hosts[] = $host;
2070          }
2071  
2072          $switched_locale = switch_to_locale( get_user_locale() );
2073          $l10n            = array(
2074              'shiftClickToEdit'  => __( 'Shift-click to edit this element.' ),
2075              'linkUnpreviewable' => __( 'This link is not live-previewable.' ),
2076              'formUnpreviewable' => __( 'This form is not live-previewable.' ),
2077          );
2078          if ( $switched_locale ) {
2079              restore_previous_locale();
2080          }
2081  
2082          $settings = array(
2083              'changeset'         => array(
2084                  'uuid'      => $this->changeset_uuid(),
2085                  'autosaved' => $this->autosaved(),
2086              ),
2087              'timeouts'          => array(
2088                  'selectiveRefresh' => 250,
2089                  'keepAliveSend'    => 1000,
2090              ),
2091              'theme'             => array(
2092                  'stylesheet' => $this->get_stylesheet(),
2093                  'active'     => $this->is_theme_active(),
2094              ),
2095              'url'               => array(
2096                  'self'          => $self_url,
2097                  'allowed'       => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
2098                  'allowedHosts'  => array_unique( $allowed_hosts ),
2099                  'isCrossDomain' => $this->is_cross_domain(),
2100              ),
2101              'channel'           => $this->messenger_channel,
2102              'activePanels'      => array(),
2103              'activeSections'    => array(),
2104              'activeControls'    => array(),
2105              'settingValidities' => $exported_setting_validities,
2106              'nonce'             => current_user_can( 'customize' ) ? $this->get_nonces() : array(),
2107              'l10n'              => $l10n,
2108              '_dirty'            => array_keys( $post_values ),
2109          );
2110  
2111          foreach ( $this->panels as $panel_id => $panel ) {
2112              if ( $panel->check_capabilities() ) {
2113                  $settings['activePanels'][ $panel_id ] = $panel->active();
2114                  foreach ( $panel->sections as $section_id => $section ) {
2115                      if ( $section->check_capabilities() ) {
2116                          $settings['activeSections'][ $section_id ] = $section->active();
2117                      }
2118                  }
2119              }
2120          }
2121          foreach ( $this->sections as $id => $section ) {
2122              if ( $section->check_capabilities() ) {
2123                  $settings['activeSections'][ $id ] = $section->active();
2124              }
2125          }
2126          foreach ( $this->controls as $id => $control ) {
2127              if ( $control->check_capabilities() ) {
2128                  $settings['activeControls'][ $id ] = $control->active();
2129              }
2130          }
2131  
2132          ?>
2133          <script type="text/javascript">
2134              var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
2135              _wpCustomizeSettings.values = {};
2136              (function( v ) {
2137                  <?php
2138                  /*
2139                   * Serialize settings separately from the initial _wpCustomizeSettings
2140                   * serialization in order to avoid a peak memory usage spike.
2141                   * @todo We may not even need to export the values at all since the pane syncs them anyway.
2142                   */
2143                  foreach ( $this->settings as $id => $setting ) {
2144                      if ( $setting->check_capabilities() ) {
2145                          printf(
2146                              "v[%s] = %s;\n",
2147                              wp_json_encode( $id ),
2148                              wp_json_encode( $setting->js_value() )
2149                          );
2150                      }
2151                  }
2152                  ?>
2153              })( _wpCustomizeSettings.values );
2154          </script>
2155          <?php
2156      }
2157  
2158      /**
2159       * Prints a signature so we can ensure the Customizer was properly executed.
2160       *
2161       * @since 3.4.0
2162       * @deprecated 4.7.0
2163       */
2164  	public function customize_preview_signature() {
2165          _deprecated_function( __METHOD__, '4.7.0' );
2166      }
2167  
2168      /**
2169       * Removes the signature in case we experience a case where the Customizer was not properly executed.
2170       *
2171       * @since 3.4.0
2172       * @deprecated 4.7.0
2173       *
2174       * @param mixed $return Value passed through for {@see 'wp_die_handler'} filter.
2175       * @return mixed Value passed through for {@see 'wp_die_handler'} filter.
2176       */
2177  	public function remove_preview_signature( $return = null ) {
2178          _deprecated_function( __METHOD__, '4.7.0' );
2179  
2180          return $return;
2181      }
2182  
2183      /**
2184       * Is it a theme preview?
2185       *
2186       * @since 3.4.0
2187       *
2188       * @return bool True if it's a preview, false if not.
2189       */
2190  	public function is_preview() {
2191          return (bool) $this->previewing;
2192      }
2193  
2194      /**
2195       * Retrieve the template name of the previewed theme.
2196       *
2197       * @since 3.4.0
2198       *
2199       * @return string Template name.
2200       */
2201  	public function get_template() {
2202          return $this->theme()->get_template();
2203      }
2204  
2205      /**
2206       * Retrieve the stylesheet name of the previewed theme.
2207       *
2208       * @since 3.4.0
2209       *
2210       * @return string Stylesheet name.
2211       */
2212  	public function get_stylesheet() {
2213          return $this->theme()->get_stylesheet();
2214      }
2215  
2216      /**
2217       * Retrieve the template root of the previewed theme.
2218       *
2219       * @since 3.4.0
2220       *
2221       * @return string Theme root.
2222       */
2223  	public function get_template_root() {
2224          return get_raw_theme_root( $this->get_template(), true );
2225      }
2226  
2227      /**
2228       * Retrieve the stylesheet root of the previewed theme.
2229       *
2230       * @since 3.4.0
2231       *
2232       * @return string Theme root.
2233       */
2234  	public function get_stylesheet_root() {
2235          return get_raw_theme_root( $this->get_stylesheet(), true );
2236      }
2237  
2238      /**
2239       * Filters the current theme and return the name of the previewed theme.
2240       *
2241       * @since 3.4.0
2242       *
2243       * @param $current_theme {@internal Parameter is not used}
2244       * @return string Theme name.
2245       */
2246  	public function current_theme( $current_theme ) {
2247          return $this->theme()->display( 'Name' );
2248      }
2249  
2250      /**
2251       * Validates setting values.
2252       *
2253       * Validation is skipped for unregistered settings or for values that are
2254       * already null since they will be skipped anyway. Sanitization is applied
2255       * to values that pass validation, and values that become null or `WP_Error`
2256       * after sanitizing are marked invalid.
2257       *
2258       * @since 4.6.0
2259       *
2260       * @see WP_REST_Request::has_valid_params()
2261       * @see WP_Customize_Setting::validate()
2262       *
2263       * @param array $setting_values Mapping of setting IDs to values to validate and sanitize.
2264       * @param array $options {
2265       *     Options.
2266       *
2267       *     @type bool $validate_existence  Whether a setting's existence will be checked.
2268       *     @type bool $validate_capability Whether the setting capability will be checked.
2269       * }
2270       * @return array Mapping of setting IDs to return value of validate method calls, either `true` or `WP_Error`.
2271       */
2272  	public function validate_setting_values( $setting_values, $options = array() ) {
2273          $options = wp_parse_args(
2274              $options, array(
2275                  'validate_capability' => false,
2276                  'validate_existence'  => false,
2277              )
2278          );
2279  
2280          $validities = array();
2281          foreach ( $setting_values as $setting_id => $unsanitized_value ) {
2282              $setting = $this->get_setting( $setting_id );
2283              if ( ! $setting ) {
2284                  if ( $options['validate_existence'] ) {
2285                      $validities[ $setting_id ] = new WP_Error( 'unrecognized', __( 'Setting does not exist or is unrecognized.' ) );
2286                  }
2287                  continue;
2288              }
2289              if ( $options['validate_capability'] && ! current_user_can( $setting->capability ) ) {
2290                  $validity = new WP_Error( 'unauthorized', __( 'Unauthorized to modify setting due to capability.' ) );
2291              } else {
2292                  if ( is_null( $unsanitized_value ) ) {
2293                      continue;
2294                  }
2295                  $validity = $setting->validate( $unsanitized_value );
2296              }
2297              if ( ! is_wp_error( $validity ) ) {
2298                  /** This filter is documented in wp-includes/class-wp-customize-setting.php */
2299                  $late_validity = apply_filters( "customize_validate_{$setting->id}", new WP_Error(), $unsanitized_value, $setting );
2300                  if ( ! empty( $late_validity->errors ) ) {
2301                      $validity = $late_validity;
2302                  }
2303              }
2304              if ( ! is_wp_error( $validity ) ) {
2305                  $value = $setting->sanitize( $unsanitized_value );
2306                  if ( is_null( $value ) ) {
2307                      $validity = false;
2308                  } elseif ( is_wp_error( $value ) ) {
2309                      $validity = $value;
2310                  }
2311              }
2312              if ( false === $validity ) {
2313                  $validity = new WP_Error( 'invalid_value', __( 'Invalid value.' ) );
2314              }
2315              $validities[ $setting_id ] = $validity;
2316          }
2317          return $validities;
2318      }
2319  
2320      /**
2321       * Prepares setting validity for exporting to the client (JS).
2322       *
2323       * Converts `WP_Error` instance into array suitable for passing into the
2324       * `wp.customize.Notification` JS model.
2325       *
2326       * @since 4.6.0
2327       *
2328       * @param true|WP_Error $validity Setting validity.
2329       * @return true|array If `$validity` was a WP_Error, the error codes will be array-mapped
2330       *                    to their respective `message` and `data` to pass into the
2331       *                    `wp.customize.Notification` JS model.
2332       */
2333  	public function prepare_setting_validity_for_js( $validity ) {
2334          if ( is_wp_error( $validity ) ) {
2335              $notification = array();
2336              foreach ( $validity->errors as $error_code => $error_messages ) {
2337                  $notification[ $error_code ] = array(
2338                      'message' => join( ' ', $error_messages ),
2339                      'data'    => $validity->get_error_data( $error_code ),
2340                  );
2341              }
2342              return $notification;
2343          } else {
2344              return true;
2345          }
2346      }
2347  
2348      /**
2349       * Handle customize_save WP Ajax request to save/update a changeset.
2350       *
2351       * @since 3.4.0
2352       * @since 4.7.0 The semantics of this method have changed to update a changeset, optionally to also change the status and other attributes.
2353       */
2354  	public function save() {
2355          if ( ! is_user_logged_in() ) {
2356              wp_send_json_error( 'unauthenticated' );
2357          }
2358  
2359          if ( ! $this->is_preview() ) {
2360              wp_send_json_error( 'not_preview' );
2361          }
2362  
2363          $action = 'save-customize_' . $this->get_stylesheet();
2364          if ( ! check_ajax_referer( $action, 'nonce', false ) ) {
2365              wp_send_json_error( 'invalid_nonce' );
2366          }
2367  
2368          $changeset_post_id = $this->changeset_post_id();
2369          $is_new_changeset  = empty( $changeset_post_id );
2370          if ( $is_new_changeset ) {
2371              if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->create_posts ) ) {
2372                  wp_send_json_error( 'cannot_create_changeset_post' );
2373              }
2374          } else {
2375              if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
2376                  wp_send_json_error( 'cannot_edit_changeset_post' );
2377              }
2378          }
2379  
2380          if ( ! empty( $_POST['customize_changeset_data'] ) ) {
2381              $input_changeset_data = json_decode( wp_unslash( $_POST['customize_changeset_data'] ), true );
2382              if ( ! is_array( $input_changeset_data ) ) {
2383                  wp_send_json_error( 'invalid_customize_changeset_data' );
2384              }
2385          } else {
2386              $input_changeset_data = array();
2387          }
2388  
2389          // Validate title.
2390          $changeset_title = null;
2391          if ( isset( $_POST['customize_changeset_title'] ) ) {
2392              $changeset_title = sanitize_text_field( wp_unslash( $_POST['customize_changeset_title'] ) );
2393          }
2394  
2395          // Validate changeset status param.
2396          $is_publish       = null;
2397          $changeset_status = null;
2398          if ( isset( $_POST['customize_changeset_status'] ) ) {
2399              $changeset_status = wp_unslash( $_POST['customize_changeset_status'] );
2400              if ( ! get_post_status_object( $changeset_status ) || ! in_array( $changeset_status, array( 'draft', 'pending', 'publish', 'future' ), true ) ) {
2401                  wp_send_json_error( 'bad_customize_changeset_status', 400 );
2402              }
2403              $is_publish = ( 'publish' === $changeset_status || 'future' === $changeset_status );
2404              if ( $is_publish && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) {
2405                  wp_send_json_error( 'changeset_publish_unauthorized', 403 );
2406              }
2407          }
2408  
2409          /*
2410           * Validate changeset date param. Date is assumed to be in local time for
2411           * the WP if in MySQL format (YYYY-MM-DD HH:MM:SS). Otherwise, the date
2412           * is parsed with strtotime() so that ISO date format may be supplied
2413           * or a string like "+10 minutes".
2414           */
2415          $changeset_date_gmt = null;
2416          if ( isset( $_POST['customize_changeset_date'] ) ) {
2417              $changeset_date = wp_unslash( $_POST['customize_changeset_date'] );
2418              if ( preg_match( '/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/', $changeset_date ) ) {
2419                  $mm         = substr( $changeset_date, 5, 2 );
2420                  $jj         = substr( $changeset_date, 8, 2 );
2421                  $aa         = substr( $changeset_date, 0, 4 );
2422                  $valid_date = wp_checkdate( $mm, $jj, $aa, $changeset_date );
2423                  if ( ! $valid_date ) {
2424                      wp_send_json_error( 'bad_customize_changeset_date', 400 );
2425                  }
2426                  $changeset_date_gmt = get_gmt_from_date( $changeset_date );
2427              } else {
2428                  $timestamp = strtotime( $changeset_date );
2429                  if ( ! $timestamp ) {
2430                      wp_send_json_error( 'bad_customize_changeset_date', 400 );
2431                  }
2432                  $changeset_date_gmt = gmdate( 'Y-m-d H:i:s', $timestamp );
2433              }
2434          }
2435  
2436          $lock_user_id = null;
2437          $autosave     = ! empty( $_POST['customize_changeset_autosave'] );
2438          if ( ! $is_new_changeset ) {
2439              $lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
2440          }
2441  
2442          // Force request to autosave when changeset is locked.
2443          if ( $lock_user_id && ! $autosave ) {
2444              $autosave           = true;
2445              $changeset_status   = null;
2446              $changeset_date_gmt = null;
2447          }
2448  
2449          if ( $autosave && ! defined( 'DOING_AUTOSAVE' ) ) { // Back-compat.
2450              define( 'DOING_AUTOSAVE', true );
2451          }
2452  
2453          $autosaved = false;
2454          $r         = $this->save_changeset_post(
2455              array(
2456                  'status'   => $changeset_status,
2457                  'title'    => $changeset_title,
2458                  'date_gmt' => $changeset_date_gmt,
2459                  'data'     => $input_changeset_data,
2460                  'autosave' => $autosave,
2461              )
2462          );
2463          if ( $autosave && ! is_wp_error( $r ) ) {
2464              $autosaved = true;
2465          }
2466  
2467          // If the changeset was locked and an autosave request wasn't itself an error, then now explicitly return with a failure.
2468          if ( $lock_user_id && ! is_wp_error( $r ) ) {
2469              $r = new WP_Error(
2470                  'changeset_locked',
2471                  __( 'Changeset is being edited by other user.' ),
2472                  array(
2473                      'lock_user' => $this->get_lock_user_data( $lock_user_id ),
2474                  )
2475              );
2476          }
2477  
2478          if ( is_wp_error( $r ) ) {
2479              $response = array(
2480                  'message' => $r->get_error_message(),
2481                  'code'    => $r->get_error_code(),
2482              );
2483              if ( is_array( $r->get_error_data() ) ) {
2484                  $response = array_merge( $response, $r->get_error_data() );
2485              } else {
2486                  $response['data'] = $r->get_error_data();
2487              }
2488          } else {
2489              $response       = $r;
2490              $changeset_post = get_post( $this->changeset_post_id() );
2491  
2492              // Dismiss all other auto-draft changeset posts for this user (they serve like autosave revisions), as there should only be one.
2493              if ( $is_new_changeset ) {
2494                  $this->dismiss_user_auto_draft_changesets();
2495              }
2496  
2497              // Note that if the changeset status was publish, then it will get set to trash if revisions are not supported.
2498              $response['changeset_status'] = $changeset_post->post_status;
2499              if ( $is_publish && 'trash' === $response['changeset_status'] ) {
2500                  $response['changeset_status'] = 'publish';
2501              }
2502  
2503              if ( 'publish' !== $response['changeset_status'] ) {
2504                  $this->set_changeset_lock( $changeset_post->ID );
2505              }
2506  
2507              if ( 'future' === $response['changeset_status'] ) {
2508                  $response['changeset_date'] = $changeset_post->post_date;
2509              }
2510  
2511              if ( 'publish' === $response['changeset_status'] || 'trash' === $response['changeset_status'] ) {
2512                  $response['next_changeset_uuid'] = wp_generate_uuid4();
2513              }
2514          }
2515  
2516          if ( $autosave ) {
2517              $response['autosaved'] = $autosaved;
2518          }
2519  
2520          if ( isset( $response['setting_validities'] ) ) {
2521              $response['setting_validities'] = array_map( array( $this, 'prepare_setting_validity_for_js' ), $response['setting_validities'] );
2522          }
2523  
2524          /**
2525           * Filters response data for a successful customize_save Ajax request.
2526           *
2527           * This filter does not apply if there was a nonce or authentication failure.
2528           *
2529           * @since 4.2.0
2530           *
2531           * @param array                $response Additional information passed back to the 'saved'
2532           *                                       event on `wp.customize`.
2533           * @param WP_Customize_Manager $this     WP_Customize_Manager instance.
2534           */
2535          $response = apply_filters( 'customize_save_response', $response, $this );
2536  
2537          if ( is_wp_error( $r ) ) {
2538              wp_send_json_error( $response );
2539          } else {
2540              wp_send_json_success( $response );
2541          }
2542      }
2543  
2544      /**
2545       * Save the post for the loaded changeset.
2546       *
2547       * @since 4.7.0
2548       *
2549       * @param array $args {
2550       *     Args for changeset post.
2551       *
2552       *     @type array  $data            Optional additional changeset data. Values will be merged on top of any existing post values.
2553       *     @type string $status          Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed.
2554       *     @type string $title           Post title. Optional.
2555       *     @type string $date_gmt        Date in GMT. Optional.
2556       *     @type int    $user_id         ID for user who is saving the changeset. Optional, defaults to the current user ID.
2557       *     @type bool   $starter_content Whether the data is starter content. If false (default), then $starter_content will be cleared for any $data being saved.
2558       *     @type bool   $autosave        Whether this is a request to create an autosave revision.
2559       * }
2560       *
2561       * @return array|WP_Error Returns array on success and WP_Error with array data on error.
2562       */
2563  	function save_changeset_post( $args = array() ) {
2564  
2565          $args = array_merge(
2566              array(
2567                  'status'          => null,
2568                  'title'           => null,
2569                  'data'            => array(),
2570                  'date_gmt'        => null,
2571                  'user_id'         => get_current_user_id(),
2572                  'starter_content' => false,
2573                  'autosave'        => false,
2574              ),
2575              $args
2576          );
2577  
2578          $changeset_post_id       = $this->changeset_post_id();
2579          $existing_changeset_data = array();
2580          if ( $changeset_post_id ) {
2581              $existing_status = get_post_status( $changeset_post_id );
2582              if ( 'publish' === $existing_status || 'trash' === $existing_status ) {
2583                  return new WP_Error(
2584                      'changeset_already_published',
2585                      __( 'The previous set of changes has already been published. Please try saving your current set of changes again.' ),
2586                      array(
2587                          'next_changeset_uuid' => wp_generate_uuid4(),
2588                      )
2589                  );
2590              }
2591  
2592              $existing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
2593              if ( is_wp_error( $existing_changeset_data ) ) {
2594                  return $existing_changeset_data;
2595              }
2596          }
2597  
2598          // Fail if attempting to publish but publish hook is missing.
2599          if ( 'publish' === $args['status'] && false === has_action( 'transition_post_status', '_wp_customize_publish_changeset' ) ) {
2600              return new WP_Error( 'missing_publish_callback' );
2601          }
2602  
2603          // Validate date.
2604          $now = gmdate( 'Y-m-d H:i:59' );
2605          if ( $args['date_gmt'] ) {
2606              $is_future_dated = ( mysql2date( 'U', $args['date_gmt'], false ) > mysql2date( 'U', $now, false ) );
2607              if ( ! $is_future_dated ) {
2608                  return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) ); // Only future dates are allowed.
2609              }
2610  
2611              if ( ! $this->is_theme_active() && ( 'future' === $args['status'] || $is_future_dated ) ) {
2612                  return new WP_Error( 'cannot_schedule_theme_switches' ); // This should be allowed in the future, when theme is a regular setting.
2613              }
2614              $will_remain_auto_draft = ( ! $args['status'] && ( ! $changeset_post_id || 'auto-draft' === get_post_status( $changeset_post_id ) ) );
2615              if ( $will_remain_auto_draft ) {
2616                  return new WP_Error( 'cannot_supply_date_for_auto_draft_changeset' );
2617              }
2618          } elseif ( $changeset_post_id && 'future' === $args['status'] ) {
2619  
2620              // Fail if the new status is future but the existing post's date is not in the future.
2621              $changeset_post = get_post( $changeset_post_id );
2622              if ( mysql2date( 'U', $changeset_post->post_date_gmt, false ) <= mysql2date( 'U', $now, false ) ) {
2623                  return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) );
2624              }
2625          }
2626  
2627          if ( ! empty( $is_future_dated ) && 'publish' === $args['status'] ) {
2628              $args['status'] = 'future';
2629          }
2630  
2631          // Validate autosave param. See _wp_post_revision_fields() for why these fields are disallowed.
2632          if ( $args['autosave'] ) {
2633              if ( $args['date_gmt'] ) {
2634                  return new WP_Error( 'illegal_autosave_with_date_gmt' );
2635              } elseif ( $args['status'] ) {
2636                  return new WP_Error( 'illegal_autosave_with_status' );
2637              } elseif ( $args['user_id'] && get_current_user_id() !== $args['user_id'] ) {
2638                  return new WP_Error( 'illegal_autosave_with_non_current_user' );
2639              }
2640          }
2641  
2642          // The request was made via wp.customize.previewer.save().
2643          $update_transactionally = (bool) $args['status'];
2644          $allow_revision         = (bool) $args['status'];
2645  
2646          // Amend post values with any supplied data.
2647          foreach ( $args['data'] as $setting_id => $setting_params ) {
2648              if ( is_array( $setting_params ) && array_key_exists( 'value', $setting_params ) ) {
2649                  $this->set_post_value( $setting_id, $setting_params['value'] ); // Add to post values so that they can be validated and sanitized.
2650              }
2651          }
2652  
2653          // Note that in addition to post data, this will include any stashed theme mods.
2654          $post_values = $this->unsanitized_post_values(
2655              array(
2656                  'exclude_changeset' => true,
2657                  'exclude_post_data' => false,
2658              )
2659          );
2660          $this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value.
2661  
2662          /*
2663           * Get list of IDs for settings that have values different from what is currently
2664           * saved in the changeset. By skipping any values that are already the same, the
2665           * subset of changed settings can be passed into validate_setting_values to prevent
2666           * an underprivileged modifying a single setting for which they have the capability
2667           * from being blocked from saving. This also prevents a user from touching of the
2668           * previous saved settings and overriding the associated user_id if they made no change.
2669           */
2670          $changed_setting_ids = array();
2671          foreach ( $post_values as $setting_id => $setting_value ) {
2672              $setting = $this->get_setting( $setting_id );
2673  
2674              if ( $setting && 'theme_mod' === $setting->type ) {
2675                  $prefixed_setting_id = $this->get_stylesheet() . '::' . $setting->id;
2676              } else {
2677                  $prefixed_setting_id = $setting_id;
2678              }
2679  
2680              $is_value_changed = (
2681                  ! isset( $existing_changeset_data[ $prefixed_setting_id ] )
2682                  ||
2683                  ! array_key_exists( 'value', $existing_changeset_data[ $prefixed_setting_id ] )
2684                  ||
2685                  $existing_changeset_data[ $prefixed_setting_id ]['value'] !== $setting_value
2686              );
2687              if ( $is_value_changed ) {
2688                  $changed_setting_ids[] = $setting_id;
2689              }
2690          }
2691  
2692          /**
2693           * Fires before save validation happens.
2694           *
2695           * Plugins can add just-in-time {@see 'customize_validate_{$this->ID}'} filters
2696           * at this point to catch any settings registered after `customize_register`.
2697           * The dynamic portion of the hook name, `$this->ID` refers to the setting ID.
2698           *
2699           * @since 4.6.0
2700           *
2701           * @param WP_Customize_Manager $this WP_Customize_Manager instance.
2702           */
2703          do_action( 'customize_save_validation_before', $this );
2704  
2705          // Validate settings.
2706          $validated_values      = array_merge(
2707              array_fill_keys( array_keys( $args['data'] ), null ), // Make sure existence/capability checks are done on value-less setting updates.
2708              $post_values
2709          );
2710          $setting_validities    = $this->validate_setting_values(
2711              $validated_values, array(
2712                  'validate_capability' => true,
2713                  'validate_existence'  => true,
2714              )
2715          );
2716          $invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) );
2717  
2718          /*
2719           * Short-circuit if there are invalid settings the update is transactional.
2720           * A changeset update is transactional when a status is supplied in the request.
2721           */
2722          if ( $update_transactionally && $invalid_setting_count > 0 ) {
2723              $response = array(
2724                  'setting_validities' => $setting_validities,
2725                  /* translators: %s: number of invalid settings */
2726                  'message'            => sprintf( _n( 'Unable to save due to %s invalid setting.', 'Unable to save due to %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ),
2727              );
2728              return new WP_Error( 'transaction_fail', '', $response );
2729          }
2730  
2731          // Obtain/merge data for changeset.
2732          $original_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
2733          $data                    = $original_changeset_data;
2734          if ( is_wp_error( $data ) ) {
2735              $data = array();
2736          }
2737  
2738          // Ensure that all post values are included in the changeset data.
2739          foreach ( $post_values as $setting_id => $post_value ) {
2740              if ( ! isset( $args['data'][ $setting_id ] ) ) {
2741                  $args['data'][ $setting_id ] = array();
2742              }
2743              if ( ! isset( $args['data'][ $setting_id ]['value'] ) ) {
2744                  $args['data'][ $setting_id ]['value'] = $post_value;
2745              }
2746          }
2747  
2748          foreach ( $args['data'] as $setting_id => $setting_params ) {
2749              $setting = $this->get_setting( $setting_id );
2750              if ( ! $setting || ! $setting->check_capabilities() ) {
2751                  continue;
2752              }
2753  
2754              // Skip updating changeset for invalid setting values.
2755              if ( isset( $setting_validities[ $setting_id ] ) && is_wp_error( $setting_validities[ $setting_id ] ) ) {
2756                  continue;
2757              }
2758  
2759              $changeset_setting_id = $setting_id;
2760              if ( 'theme_mod' === $setting->type ) {
2761                  $changeset_setting_id = sprintf( '%s::%s', $this->get_stylesheet(), $setting_id );
2762              }
2763  
2764              if ( null === $setting_params ) {
2765                  // Remove setting from changeset entirely.
2766                  unset( $data[ $changeset_setting_id ] );
2767              } else {
2768  
2769                  if ( ! isset( $data[ $changeset_setting_id ] ) ) {
2770                      $data[ $changeset_setting_id ] = array();
2771                  }
2772  
2773                  // Merge any additional setting params that have been supplied with the existing params.
2774                  $merged_setting_params = array_merge( $data[ $changeset_setting_id ], $setting_params );
2775  
2776                  // Skip updating setting params if unchanged (ensuring the user_id is not overwritten).
2777                  if ( $data[ $changeset_setting_id ] === $merged_setting_params ) {
2778                      continue;
2779                  }
2780  
2781                  $data[ $changeset_setting_id ] = array_merge(
2782                      $merged_setting_params,
2783                      array(
2784                          'type'              => $setting->type,
2785                          'user_id'           => $args['user_id'],
2786                          'date_modified_gmt' => current_time( 'mysql', true ),
2787                      )
2788                  );
2789  
2790                  // Clear starter_content flag in data if changeset is not explicitly being updated for starter content.
2791                  if ( empty( $args['starter_content'] ) ) {
2792                      unset( $data[ $changeset_setting_id ]['starter_content'] );
2793                  }
2794              }
2795          }
2796  
2797          $filter_context = array(
2798              'uuid'          => $this->changeset_uuid(),
2799              'title'         => $args['title'],
2800              'status'        => $args['status'],
2801              'date_gmt'      => $args['date_gmt'],
2802              'post_id'       => $changeset_post_id,
2803              'previous_data' => is_wp_error( $original_changeset_data ) ? array() : $original_changeset_data,
2804              'manager'       => $this,
2805          );
2806  
2807          /**
2808           * Filters the settings' data that will be persisted into the changeset.
2809           *
2810           * Plugins may amend additional data (such as additional meta for settings) into the changeset with this filter.
2811           *
2812           * @since 4.7.0
2813           *
2814           * @param array $data Updated changeset data, mapping setting IDs to arrays containing a $value item and optionally other metadata.
2815           * @param array $context {
2816           *     Filter context.
2817           *
2818           *     @type string               $uuid          Changeset UUID.
2819           *     @type string               $title         Requested title for the changeset post.
2820           *     @type string               $status        Requested status for the changeset post.
2821           *     @type string               $date_gmt      Requested date for the changeset post in MySQL format and GMT timezone.
2822           *     @type int|false            $post_id       Post ID for the changeset, or false if it doesn't exist yet.
2823           *     @type array                $previous_data Previous data contained in the changeset.
2824           *     @type WP_Customize_Manager $manager       Manager instance.
2825           * }
2826           */
2827          $data = apply_filters( 'customize_changeset_save_data', $data, $filter_context );
2828  
2829          // Switch theme if publishing changes now.
2830          if ( 'publish' === $args['status'] && ! $this->is_theme_active() ) {
2831              // Temporarily stop previewing the theme to allow switch_themes() to operate properly.
2832              $this->stop_previewing_theme();
2833              switch_theme( $this->get_stylesheet() );
2834              update_option( 'theme_switched_via_customizer', true );
2835              $this->start_previewing_theme();
2836          }
2837  
2838          // Gather the data for wp_insert_post()/wp_update_post().
2839          $json_options = 0;
2840          if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) {
2841              $json_options |= JSON_UNESCAPED_SLASHES; // Introduced in PHP 5.4. This is only to improve readability as slashes needn't be escaped in storage.
2842          }
2843          $json_options |= JSON_PRETTY_PRINT; // Also introduced in PHP 5.4, but WP defines constant for back compat. See WP Trac #30139.
2844          $post_array    = array(
2845              'post_content' => wp_json_encode( $data, $json_options ),
2846          );
2847          if ( $args['title'] ) {
2848              $post_array['post_title'] = $args['title'];
2849          }
2850          if ( $changeset_post_id ) {
2851              $post_array['ID'] = $changeset_post_id;
2852          } else {
2853              $post_array['post_type']   = 'customize_changeset';
2854              $post_array['post_name']   = $this->changeset_uuid();
2855              $post_array['post_status'] = 'auto-draft';
2856          }
2857          if ( $args['status'] ) {
2858              $post_array['post_status'] = $args['status'];
2859          }
2860  
2861          // Reset post date to now if we are publishing, otherwise pass post_date_gmt and translate for post_date.
2862          if ( 'publish' === $args['status'] ) {
2863              $post_array['post_date_gmt'] = '0000-00-00 00:00:00';
2864              $post_array['post_date']     = '0000-00-00 00:00:00';
2865          } elseif ( $args['date_gmt'] ) {
2866              $post_array['post_date_gmt'] = $args['date_gmt'];
2867              $post_array['post_date']     = get_date_from_gmt( $args['date_gmt'] );
2868          } elseif ( $changeset_post_id && 'auto-draft' === get_post_status( $changeset_post_id ) ) {
2869              /*
2870               * Keep bumping the date for the auto-draft whenever it is modified;
2871               * this extends its life, preserving it from garbage-collection via
2872               * wp_delete_auto_drafts().
2873               */
2874              $post_array['post_date']     = current_time( 'mysql' );
2875              $post_array['post_date_gmt'] = '';
2876          }
2877  
2878          $this->store_changeset_revision = $allow_revision;
2879          add_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ), 5, 3 );
2880  
2881          // Update the changeset post. The publish_customize_changeset action will cause the settings in the changeset to be saved via WP_Customize_Setting::save().
2882          $has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) );
2883          if ( $has_kses ) {
2884              kses_remove_filters(); // Prevent KSES from corrupting JSON in post_content.
2885          }
2886  
2887          // Note that updating a post with publish status will trigger WP_Customize_Manager::publish_changeset_values().
2888          if ( $changeset_post_id ) {
2889              if ( $args['autosave'] && 'auto-draft' !== get_post_status( $changeset_post_id ) ) {
2890                  // See _wp_translate_postdata() for why this is required as it will use the edit_post meta capability.
2891                  add_filter( 'map_meta_cap', array( $this, 'grant_edit_post_capability_for_changeset' ), 10, 4 );
2892                  $post_array['post_ID']   = $post_array['ID'];
2893                  $post_array['post_type'] = 'customize_changeset';
2894                  $r                       = wp_create_post_autosave( wp_slash( $post_array ) );
2895                  remove_filter( 'map_meta_cap', array( $this, 'grant_edit_post_capability_for_changeset' ), 10 );
2896              } else {
2897                  $post_array['edit_date'] = true; // Prevent date clearing.
2898                  $r                       = wp_update_post( wp_slash( $post_array ), true );
2899  
2900                  // Delete autosave revision when the changeset is updated.
2901                  $autosave_draft = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
2902                  if ( $autosave_draft ) {
2903                      wp_delete_post( $autosave_draft->ID, true );
2904                  }
2905              }
2906          } else {
2907              $r = wp_insert_post( wp_slash( $post_array ), true );
2908              if ( ! is_wp_error( $r ) ) {
2909                  $this->_changeset_post_id = $r; // Update cached post ID for the loaded changeset.
2910              }
2911          }
2912          if ( $has_kses ) {
2913              kses_init_filters();
2914          }
2915          $this->_changeset_data = null; // Reset so WP_Customize_Manager::changeset_data() will re-populate with updated contents.
2916  
2917          remove_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ) );
2918  
2919          $response = array(
2920              'setting_validities' => $setting_validities,
2921          );
2922  
2923          if ( is_wp_error( $r ) ) {
2924              $response['changeset_post_save_failure'] = $r->get_error_code();
2925              return new WP_Error( 'changeset_post_save_failure', '', $response );
2926          }
2927  
2928          return $response;
2929      }
2930  
2931      /**
2932       * Trash or delete a changeset post.
2933       *
2934       * The following re-formulates the logic from `wp_trash_post()` as done in
2935       * `wp_publish_post()`. The reason for bypassing `wp_trash_post()` is that it
2936       * will mutate the the `post_content` and the `post_name` when they should be
2937       * untouched.
2938       *
2939       * @since 4.9.0
2940       * @global wpdb $wpdb WordPress database abstraction object.
2941       * @see wp_trash_post()
2942       *
2943       * @param int|WP_Post $post The changeset post.
2944       * @return mixed A WP_Post object for the trashed post or an empty value on failure.
2945       */
2946  	public function trash_changeset_post( $post ) {
2947          global $wpdb;
2948  
2949          $post = get_post( $post );
2950  
2951          if ( ! ( $post instanceof WP_Post ) ) {
2952              return $post;
2953          }
2954          $post_id = $post->ID;
2955  
2956          if ( ! EMPTY_TRASH_DAYS ) {
2957              return wp_delete_post( $post_id, true );
2958          }
2959  
2960          if ( 'trash' === get_post_status( $post ) ) {
2961              return false;
2962          }
2963  
2964          /** This filter is documented in wp-includes/post.php */
2965          $check = apply_filters( 'pre_trash_post', null, $post );
2966          if ( null !== $check ) {
2967              return $check;
2968          }
2969  
2970          /** This action is documented in wp-includes/post.php */
2971          do_action( 'wp_trash_post', $post_id );
2972  
2973          add_post_meta( $post_id, '_wp_trash_meta_status', $post->post_status );
2974          add_post_meta( $post_id, '_wp_trash_meta_time', time() );
2975  
2976          $old_status = $post->post_status;
2977          $new_status = 'trash';
2978          $wpdb->update( $wpdb->posts, array( 'post_status' => $new_status ), array( 'ID' => $post->ID ) );
2979          clean_post_cache( $post->ID );
2980  
2981          $post->post_status = $new_status;
2982          wp_transition_post_status( $new_status, $old_status, $post );
2983  
2984          /** This action is documented in wp-includes/post.php */
2985          do_action( 'edit_post', $post->ID, $post );
2986  
2987          /** This action is documented in wp-includes/post.php */
2988          do_action( "save_post_{$post->post_type}", $post->ID, $post, true );
2989  
2990          /** This action is documented in wp-includes/post.php */
2991          do_action( 'save_post', $post->ID, $post, true );
2992  
2993          /** This action is documented in wp-includes/post.php */
2994          do_action( 'wp_insert_post', $post->ID, $post, true );
2995  
2996          wp_trash_post_comments( $post_id );
2997  
2998          /** This action is documented in wp-includes/post.php */
2999          do_action( 'trashed_post', $post_id );
3000  
3001          return $post;
3002      }
3003  
3004      /**
3005       * Handle request to trash a changeset.
3006       *
3007       * @since 4.9.0
3008       */
3009  	public function handle_changeset_trash_request() {
3010          if ( ! is_user_logged_in() ) {
3011              wp_send_json_error( 'unauthenticated' );
3012          }
3013  
3014          if ( ! $this->is_preview() ) {
3015              wp_send_json_error( 'not_preview' );
3016          }
3017  
3018          if ( ! check_ajax_referer( 'trash_customize_changeset', 'nonce', false ) ) {
3019              wp_send_json_error(
3020                  array(
3021                      'code'    => 'invalid_nonce',
3022                      'message' => __( 'There was an authentication problem. Please reload and try again.' ),
3023                  )
3024              );
3025          }
3026  
3027          $changeset_post_id = $this->changeset_post_id();
3028  
3029          if ( ! $changeset_post_id ) {
3030              wp_send_json_error(
3031                  array(
3032                      'message' => __( 'No changes saved yet, so there is nothing to trash.' ),
3033                      'code'    => 'non_existent_changeset',
3034                  )
3035              );
3036              return;
3037          }
3038  
3039          if ( $changeset_post_id && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) {
3040              wp_send_json_error(
3041                  array(
3042                      'code'    => 'changeset_trash_unauthorized',
3043                      'message' => __( 'Unable to trash changes.' ),
3044                  )
3045              );
3046          }
3047  
3048          if ( 'trash' === get_post_status( $changeset_post_id ) ) {
3049              wp_send_json_error(
3050                  array(
3051                      'message' => __( 'Changes have already been trashed.' ),
3052                      'code'    => 'changeset_already_trashed',
3053                  )
3054              );
3055              return;
3056          }
3057  
3058          $r = $this->trash_changeset_post( $changeset_post_id );
3059          if ( ! ( $r instanceof WP_Post ) ) {
3060              wp_send_json_error(
3061                  array(
3062                      'code'    => 'changeset_trash_failure',
3063                      'message' => __( 'Unable to trash changes.' ),
3064                  )
3065              );
3066          }
3067  
3068          wp_send_json_success(
3069              array(
3070                  'message' => __( 'Changes trashed successfully.' ),
3071              )
3072          );
3073      }
3074  
3075      /**
3076       * Re-map 'edit_post' meta cap for a customize_changeset post to be the same as 'customize' maps.
3077       *
3078       * There is essentially a "meta meta" cap in play here, where 'edit_post' meta cap maps to
3079       * the 'customize' meta cap which then maps to 'edit_theme_options'. This is currently
3080       * required in core for `wp_create_post_autosave()` because it will call
3081       * `_wp_translate_postdata()` which in turn will check if a user can 'edit_post', but the
3082       * the caps for the customize_changeset post type are all mapping to the meta capability.
3083       * This should be able to be removed once #40922 is addressed in core.
3084       *
3085       * @since 4.9.0
3086       * @link https://core.trac.wordpress.org/ticket/40922
3087       * @see WP_Customize_Manager::save_changeset_post()
3088       * @see _wp_translate_postdata()
3089       *
3090       * @param array  $caps    Returns the user's actual capabilities.
3091       * @param string $cap     Capability name.
3092       * @param int    $user_id The user ID.
3093       * @param array  $args    Adds the context to the cap. Typically the object ID.
3094       * @return array Capabilities.
3095       */
3096  	public function grant_edit_post_capability_for_changeset( $caps, $cap, $user_id, $args ) {
3097          if ( 'edit_post' === $cap && ! empty( $args[0] ) && 'customize_changeset' === get_post_type( $args[0] ) ) {
3098              $post_type_obj = get_post_type_object( 'customize_changeset' );
3099              $caps          = map_meta_cap( $post_type_obj->cap->$cap, $user_id );
3100          }
3101          return $caps;
3102      }
3103  
3104      /**
3105       * Marks the changeset post as being currently edited by the current user.
3106       *
3107       * @since 4.9.0
3108       *
3109       * @param int  $changeset_post_id Changeset post id.
3110       * @param bool $take_over Take over the changeset, default is false.
3111       */
3112  	public function set_changeset_lock( $changeset_post_id, $take_over = false ) {
3113          if ( $changeset_post_id ) {
3114              $can_override = ! (bool) get_post_meta( $changeset_post_id, '_edit_lock', true );
3115  
3116              if ( $take_over ) {
3117                  $can_override = true;
3118              }
3119  
3120              if ( $can_override ) {
3121                  $lock = sprintf( '%s:%s', time(), get_current_user_id() );
3122                  update_post_meta( $changeset_post_id, '_edit_lock', $lock );
3123              } else {
3124                  $this->refresh_changeset_lock( $changeset_post_id );
3125              }
3126          }
3127      }
3128  
3129      /**
3130       * Refreshes changeset lock with the current time if current user edited the changeset before.
3131       *
3132       * @since 4.9.0
3133       *
3134       * @param int $changeset_post_id Changeset post id.
3135       */
3136  	public function refresh_changeset_lock( $changeset_post_id ) {
3137          if ( ! $changeset_post_id ) {
3138              return;
3139          }
3140          $lock = get_post_meta( $changeset_post_id, '_edit_lock', true );
3141          $lock = explode( ':', $lock );
3142  
3143          if ( $lock && ! empty( $lock[1] ) ) {
3144              $user_id         = intval( $lock[1] );
3145              $current_user_id = get_current_user_id();
3146              if ( $user_id === $current_user_id ) {
3147                  $lock = sprintf( '%s:%s', time(), $user_id );
3148                  update_post_meta( $changeset_post_id, '_edit_lock', $lock );
3149              }
3150          }
3151      }
3152  
3153      /**
3154       * Filter heartbeat settings for the Customizer.
3155       *
3156       * @since 4.9.0
3157       * @param array $settings Current settings to filter.
3158       * @return array Heartbeat settings.
3159       */
3160  	public function add_customize_screen_to_heartbeat_settings( $settings ) {
3161          global $pagenow;
3162          if ( 'customize.php' === $pagenow ) {
3163              $settings['screenId'] = 'customize';
3164          }
3165          return $settings;
3166      }
3167  
3168      /**
3169       * Get lock user data.
3170       *
3171       * @since 4.9.0
3172       *
3173       * @param int $user_id User ID.
3174       * @return array|null User data formatted for client.
3175       */
3176  	protected function get_lock_user_data( $user_id ) {
3177          if ( ! $user_id ) {
3178              return null;
3179          }
3180          $lock_user = get_userdata( $user_id );
3181          if ( ! $lock_user ) {
3182              return null;
3183          }
3184          return array(
3185              'id'     => $lock_user->ID,
3186              'name'   => $lock_user->display_name,
3187              'avatar' => get_avatar_url( $lock_user->ID, array( 'size' => 128 ) ),
3188          );
3189      }
3190  
3191      /**
3192       * Check locked changeset with heartbeat API.
3193       *
3194       * @since 4.9.0
3195       *
3196       * @param array  $response  The Heartbeat response.
3197       * @param array  $data      The $_POST data sent.
3198       * @param string $screen_id The screen id.
3199       * @return array The Heartbeat response.
3200       */
3201  	public function check_changeset_lock_with_heartbeat( $response, $data, $screen_id ) {
3202          if ( array_key_exists( 'check_changeset_lock', $data ) && 'customize' === $screen_id && current_user_can( 'customize' ) && $this->changeset_post_id() ) {
3203              $lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
3204  
3205              if ( $lock_user_id ) {
3206                  $response['customize_changeset_lock_user'] = $this->get_lock_user_data( $lock_user_id );
3207              } else {
3208  
3209                  // Refreshing time will ensure that the user is sitting on customizer and has not closed the customizer tab.
3210                  $this->refresh_changeset_lock( $this->changeset_post_id() );
3211              }
3212          }
3213  
3214          return $response;
3215      }
3216  
3217      /**
3218       * Removes changeset lock when take over request is sent via Ajax.
3219       *
3220       * @since 4.9.0
3221       */
3222  	public function handle_override_changeset_lock_request() {
3223          if ( ! $this->is_preview() ) {
3224              wp_send_json_error( 'not_preview', 400 );
3225          }
3226  
3227          if ( ! check_ajax_referer( 'customize_override_changeset_lock', 'nonce', false ) ) {
3228              wp_send_json_error(
3229                  array(
3230                      'code'    => 'invalid_nonce',
3231                      'message' => __( 'Security check failed.' ),
3232                  )
3233              );
3234          }
3235  
3236          $changeset_post_id = $this->changeset_post_id();
3237  
3238          if ( empty( $changeset_post_id ) ) {
3239              wp_send_json_error(
3240                  array(
3241                      'code'    => 'no_changeset_found_to_take_over',
3242                      'message' => __( 'No changeset found to take over' ),
3243                  )
3244              );
3245          }
3246  
3247          if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
3248              wp_send_json_error(
3249                  array(
3250                      'code'    => 'cannot_remove_changeset_lock',
3251                      'message' => __( 'Sorry, you are not allowed to take over.' ),
3252                  )
3253              );
3254          }
3255  
3256          $this->set_changeset_lock( $changeset_post_id, true );
3257  
3258          wp_send_json_success( 'changeset_taken_over' );
3259      }
3260  
3261      /**
3262       * Whether a changeset revision should be made.
3263       *
3264       * @since 4.7.0
3265       * @var bool
3266       */
3267      protected $store_changeset_revision;
3268  
3269      /**
3270       * Filters whether a changeset has changed to create a new revision.
3271       *
3272       * Note that this will not be called while a changeset post remains in auto-draft status.
3273       *
3274       * @since 4.7.0
3275       *
3276       * @param bool    $post_has_changed Whether the post has changed.
3277       * @param WP_Post $last_revision    The last revision post object.
3278       * @param WP_Post $post             The post object.
3279       *
3280       * @return bool Whether a revision should be made.
3281       */
3282  	public function _filter_revision_post_has_changed( $post_has_changed, $last_revision, $post ) {
3283          unset( $last_revision );
3284          if ( 'customize_changeset' === $post->post_type ) {
3285              $post_has_changed = $this->store_changeset_revision;
3286          }
3287          return $post_has_changed;
3288      }
3289  
3290      /**
3291       * Publish changeset values.
3292       *
3293       * This will the values contained in a changeset, even changesets that do not
3294       * correspond to current manager instance. This is called by
3295       * `_wp_customize_publish_changeset()` when a customize_changeset post is
3296       * transitioned to the `publish` status. As such, this method should not be
3297       * called directly and instead `wp_publish_post()` should be used.
3298       *
3299       * Please note that if the settings in the changeset are for a non-activated
3300       * theme, the theme must first be switched to (via `switch_theme()`) before
3301       * invoking this method.
3302       *
3303       * @since 4.7.0
3304       * @see _wp_customize_publish_changeset()
3305       * @global wpdb $wpdb
3306       *
3307       * @param int $changeset_post_id ID for customize_changeset post. Defaults to the changeset for the current manager instance.
3308       * @return true|WP_Error True or error info.
3309       */
3310  	public function _publish_changeset_values( $changeset_post_id ) {
3311          global $wpdb;
3312  
3313          $publishing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
3314          if ( is_wp_error( $publishing_changeset_data ) ) {
3315              return $publishing_changeset_data;
3316          }
3317  
3318          $changeset_post = get_post( $changeset_post_id );
3319  
3320          /*
3321           * Temporarily override the changeset context so that it will be read
3322           * in calls to unsanitized_post_values() and so that it will be available
3323           * on the $wp_customize object passed to hooks during the save logic.
3324           */
3325          $previous_changeset_post_id = $this->_changeset_post_id;
3326          $this->_changeset_post_id   = $changeset_post_id;
3327          $previous_changeset_uuid    = $this->_changeset_uuid;
3328          $this->_changeset_uuid      = $changeset_post->post_name;
3329          $previous_changeset_data    = $this->_changeset_data;
3330          $this->_changeset_data      = $publishing_changeset_data;
3331  
3332          // Parse changeset data to identify theme mod settings and user IDs associated with settings to be saved.
3333          $setting_user_ids   = array();
3334          $theme_mod_settings = array();
3335          $namespace_pattern  = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
3336          $matches            = array();
3337          foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) {
3338              $actual_setting_id    = null;
3339              $is_theme_mod_setting = (
3340                  isset( $setting_params['value'] )
3341                  &&
3342                  isset( $setting_params['type'] )
3343                  &&
3344                  'theme_mod' === $setting_params['type']
3345                  &&
3346                  preg_match( $namespace_pattern, $raw_setting_id, $matches )
3347              );
3348              if ( $is_theme_mod_setting ) {
3349                  if ( ! isset( $theme_mod_settings[ $matches['stylesheet'] ] ) ) {
3350                      $theme_mod_settings[ $matches['stylesheet'] ] = array();
3351                  }
3352                  $theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
3353  
3354                  if ( $this->get_stylesheet() === $matches['stylesheet'] ) {
3355                      $actual_setting_id = $matches['setting_id'];
3356                  }
3357              } else {
3358                  $actual_setting_id = $raw_setting_id;
3359              }
3360  
3361              // Keep track of the user IDs for settings actually for this theme.
3362              if ( $actual_setting_id && isset( $setting_params['user_id'] ) ) {
3363                  $setting_user_ids[ $actual_setting_id ] = $setting_params['user_id'];
3364              }
3365          }
3366  
3367          $changeset_setting_values = $this->unsanitized_post_values(
3368              array(
3369                  'exclude_post_data' => true,
3370                  'exclude_changeset' => false,
3371              )
3372          );
3373          $changeset_setting_ids    = array_keys( $changeset_setting_values );
3374          $this->add_dynamic_settings( $changeset_setting_ids );
3375  
3376          /**
3377           * Fires once the theme has switched in the Customizer, but before settings
3378           * have been saved.
3379           *
3380           * @since 3.4.0
3381           *
3382           * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
3383           */
3384          do_action( 'customize_save', $this );
3385  
3386          /*
3387           * Ensure that all settings will allow themselves to be saved. Note that
3388           * this is safe because the setting would have checked the capability
3389           * when the setting value was written into the changeset. So this is why
3390           * an additional capability check is not required here.
3391           */
3392          $original_setting_capabilities = array();
3393          foreach ( $changeset_setting_ids as $setting_id ) {
3394              $setting = $this->get_setting( $setting_id );
3395              if ( $setting && ! isset( $setting_user_ids[ $setting_id ] ) ) {
3396                  $original_setting_capabilities[ $setting->id ] = $setting->capability;
3397                  $setting->capability                           = 'exist';
3398              }
3399          }
3400  
3401          $original_user_id = get_current_user_id();
3402          foreach ( $changeset_setting_ids as $setting_id ) {
3403              $setting = $this->get_setting( $setting_id );
3404              if ( $setting ) {
3405                  /*
3406                   * Set the current user to match the user who saved the value into
3407                   * the changeset so that any filters that apply during the save
3408                   * process will respect the original user's capabilities. This
3409                   * will ensure, for example, that KSES won't strip unsafe HTML
3410                   * when a scheduled changeset publishes via WP Cron.
3411                   */
3412                  if ( isset( $setting_user_ids[ $setting_id ] ) ) {
3413                      wp_set_current_user( $setting_user_ids[ $setting_id ] );
3414                  } else {
3415                      wp_set_current_user( $original_user_id );
3416                  }
3417  
3418                  $setting->save();
3419              }
3420          }
3421          wp_set_current_user( $original_user_id );
3422  
3423          // Update the stashed theme mod settings, removing the active theme's stashed settings, if activated.
3424          if ( did_action( 'switch_theme' ) ) {
3425              $other_theme_mod_settings = $theme_mod_settings;
3426              unset( $other_theme_mod_settings[ $this->get_stylesheet() ] );
3427              $this->update_stashed_theme_mod_settings( $other_theme_mod_settings );
3428          }
3429  
3430          /**
3431           * Fires after Customize settings have been saved.
3432           *
3433           * @since 3.6.0
3434           *
3435           * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
3436           */
3437          do_action( 'customize_save_after', $this );
3438  
3439          // Restore original capabilities.
3440          foreach ( $original_setting_capabilities as $setting_id => $capability ) {
3441              $setting = $this->get_setting( $setting_id );
3442              if ( $setting ) {
3443                  $setting->capability = $capability;
3444              }
3445          }
3446  
3447          // Restore original changeset data.
3448          $this->_changeset_data    = $previous_changeset_data;
3449          $this->_changeset_post_id = $previous_changeset_post_id;
3450          $this->_changeset_uuid    = $previous_changeset_uuid;
3451  
3452          /*
3453           * Convert all autosave revisions into their own auto-drafts so that users can be prompted to
3454           * restore them when a changeset is published, but they had been locked out from including
3455           * their changes in the changeset.
3456           */
3457          $revisions = wp_get_post_revisions( $changeset_post_id, array( 'check_enabled' => false ) );
3458          foreach ( $revisions as $revision ) {
3459              if ( false !== strpos( $revision->post_name, "{$changeset_post_id}-autosave" ) ) {
3460                  $wpdb->update(
3461                      $wpdb->posts,
3462                      array(
3463                          'post_status' => 'auto-draft',
3464                          'post_type'   => 'customize_changeset',
3465                          'post_name'   => wp_generate_uuid4(),
3466                          'post_parent' => 0,
3467                      ),
3468                      array(
3469                          'ID' => $revision->ID,
3470                      )
3471                  );
3472                  clean_post_cache( $revision->ID );
3473              }
3474          }
3475  
3476          return true;
3477      }
3478  
3479      /**
3480       * Update stashed theme mod settings.
3481       *
3482       * @since 4.7.0
3483       *
3484       * @param array $inactive_theme_mod_settings Mapping of stylesheet to arrays of theme mod settings.
3485       * @return array|false Returns array of updated stashed theme mods or false if the update failed or there were no changes.
3486       */
3487  	protected function update_stashed_theme_mod_settings( $inactive_theme_mod_settings ) {
3488          $stashed_theme_mod_settings = get_option( 'customize_stashed_theme_mods' );
3489          if ( empty( $stashed_theme_mod_settings ) ) {
3490              $stashed_theme_mod_settings = array();
3491          }
3492  
3493          // Delete any stashed theme mods for the active theme since they would have been loaded and saved upon activation.
3494          unset( $stashed_theme_mod_settings[ $this->get_stylesheet() ] );
3495  
3496          // Merge inactive theme mods with the stashed theme mod settings.
3497          foreach ( $inactive_theme_mod_settings as $stylesheet => $theme_mod_settings ) {
3498              if ( ! isset( $stashed_theme_mod_settings[ $stylesheet ] ) ) {
3499                  $stashed_theme_mod_settings[ $stylesheet ] = array();
3500              }
3501  
3502              $stashed_theme_mod_settings[ $stylesheet ] = array_merge(
3503                  $stashed_theme_mod_settings[ $stylesheet ],
3504                  $theme_mod_settings
3505              );
3506          }
3507  
3508          $autoload = false;
3509          $result   = update_option( 'customize_stashed_theme_mods', $stashed_theme_mod_settings, $autoload );
3510          if ( ! $result ) {
3511              return false;
3512          }
3513          return $stashed_theme_mod_settings;
3514      }
3515  
3516      /**
3517       * Refresh nonces for the current preview.
3518       *
3519       * @since 4.2.0
3520       */
3521  	public function refresh_nonces() {
3522          if ( ! $this->is_preview() ) {
3523              wp_send_json_error( 'not_preview' );
3524          }
3525  
3526          wp_send_json_success( $this->get_nonces() );
3527      }
3528  
3529      /**
3530       * Delete a given auto-draft changeset or the autosave revision for a given changeset or delete changeset lock.
3531       *
3532       * @since 4.9.0
3533       */
3534  	public function handle_dismiss_autosave_or_lock_request() {
3535          if ( ! $this->is_preview() ) {
3536              wp_send_json_error( 'not_preview', 400 );
3537          }
3538  
3539          if ( ! check_ajax_referer( 'customize_dismiss_autosave_or_lock', 'nonce', false ) ) {
3540              wp_send_json_error( 'invalid_nonce', 403 );
3541          }
3542  
3543          $changeset_post_id = $this->changeset_post_id();
3544          $dismiss_lock      = ! empty( $_POST['dismiss_lock'] );
3545          $dismiss_autosave  = ! empty( $_POST['dismiss_autosave'] );
3546  
3547          if ( $dismiss_lock ) {
3548              if ( empty( $changeset_post_id ) && ! $dismiss_autosave ) {
3549                  wp_send_json_error( 'no_changeset_to_dismiss_lock', 404 );
3550              }
3551              if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) && ! $dismiss_autosave ) {
3552                  wp_send_json_error( 'cannot_remove_changeset_lock', 403 );
3553              }
3554  
3555              delete_post_meta( $changeset_post_id, '_edit_lock' );
3556  
3557              if ( ! $dismiss_autosave ) {
3558                  wp_send_json_success( 'changeset_lock_dismissed' );
3559              }
3560          }
3561  
3562          if ( $dismiss_autosave ) {
3563              if ( empty( $changeset_post_id ) || 'auto-draft' === get_post_status( $changeset_post_id ) ) {
3564                  $dismissed = $this->dismiss_user_auto_draft_changesets();
3565                  if ( $dismissed > 0 ) {
3566                      wp_send_json_success( 'auto_draft_dismissed' );
3567                  } else {
3568                      wp_send_json_error( 'no_auto_draft_to_delete', 404 );
3569                  }
3570              } else {
3571                  $revision = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
3572  
3573                  if ( $revision ) {
3574                      if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) {
3575                          wp_send_json_error( 'cannot_delete_autosave_revision', 403 );
3576                      }
3577  
3578                      if ( ! wp_delete_post( $revision->ID, true ) ) {
3579                          wp_send_json_error( 'autosave_revision_deletion_failure', 500 );
3580                      } else {
3581                          wp_send_json_success( 'autosave_revision_deleted' );
3582                      }
3583                  } else {
3584                      wp_send_json_error( 'no_autosave_revision_to_delete', 404 );
3585                  }
3586              }
3587          }
3588  
3589          wp_send_json_error( 'unknown_error', 500 );
3590      }
3591  
3592      /**
3593       * Add a customize setting.
3594       *
3595       * @since 3.4.0
3596       * @since 4.5.0 Return added WP_Customize_Setting instance.
3597       *
3598       * @param WP_Customize_Setting|string $id   Customize Setting object, or ID.
3599       * @param array                       $args {
3600       *  Optional. Array of properties for the new WP_Customize_Setting. Default empty array.
3601       *
3602       *  @type string       $type                  Type of the setting. Default 'theme_mod'.
3603       *                                            Default 160.
3604       *  @type string       $capability            Capability required for the setting. Default 'edit_theme_options'
3605       *  @type string|array $theme_supports        Theme features required to support the panel. Default is none.
3606       *  @type string       $default               Default value for the setting. Default is empty string.
3607       *  @type string       $transport             Options for rendering the live preview of changes in Theme Customizer.
3608       *                                            Using 'refresh' makes the change visible by reloading the whole preview.
3609       *                                            Using 'postMessage' allows a custom JavaScript to handle live changes.
3610       * @link https://developer.wordpress.org/themes/customize-api
3611       *                                            Default is 'refresh'
3612       *  @type callable     $validate_callback     Server-side validation callback for the setting's value.
3613       *  @type callable     $sanitize_callback     Callback to filter a Customize setting value in un-slashed form.
3614       *  @type callable     $sanitize_js_callback  Callback to convert a Customize PHP setting value to a value that is
3615       *                                            JSON serializable.
3616       *  @type bool         $dirty                 Whether or not the setting is initially dirty when created.
3617       * }
3618       * @return WP_Customize_Setting             The instance of the setting that was added.
3619       */
3620  	public function add_setting( $id, $args = array() ) {
3621          if ( $id instanceof WP_Customize_Setting ) {
3622              $setting = $id;
3623          } else {
3624              $class = 'WP_Customize_Setting';
3625  
3626              /** This filter is documented in wp-includes/class-wp-customize-manager.php */
3627              $args = apply_filters( 'customize_dynamic_setting_args', $args, $id );
3628  
3629              /** This filter is documented in wp-includes/class-wp-customize-manager.php */
3630              $class = apply_filters( 'customize_dynamic_setting_class', $class, $id, $args );
3631  
3632              $setting = new $class( $this, $id, $args );
3633          }
3634  
3635          $this->settings[ $setting->id ] = $setting;
3636          return $setting;
3637      }
3638  
3639      /**
3640       * Register any dynamically-created settings, such as those from $_POST['customized']
3641       * that have no corresponding setting created.
3642       *
3643       * This is a mechanism to "wake up" settings that have been dynamically created
3644       * on the front end and have been sent to WordPress in `$_POST['customized']`. When WP
3645       * loads, the dynamically-created settings then will get created and previewed
3646       * even though they are not directly created statically with code.
3647       *
3648       * @since 4.2.0
3649       *
3650       * @param array $setting_ids The setting IDs to add.
3651       * @return array The WP_Customize_Setting objects added.
3652       */
3653  	public function add_dynamic_settings( $setting_ids ) {
3654          $new_settings = array();
3655          foreach ( $setting_ids as $setting_id ) {
3656              // Skip settings already created
3657              if ( $this->get_setting( $setting_id ) ) {
3658                  continue;
3659              }
3660  
3661              $setting_args  = false;
3662              $setting_class = 'WP_Customize_Setting';
3663  
3664              /**
3665               * Filters a dynamic setting's constructor args.
3666               *
3667               * For a dynamic setting to be registered, this filter must be employed
3668               * to override the default false value with an array of args to pass to
3669               * the WP_Customize_Setting constructor.
3670               *
3671               * @since 4.2.0
3672               *
3673               * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
3674               * @param string      $setting_id   ID for dynamic setting, usually coming from `$_POST['customized']`.
3675               */
3676              $setting_args = apply_filters( 'customize_dynamic_setting_args', $setting_args, $setting_id );
3677              if ( false === $setting_args ) {
3678                  continue;
3679              }
3680  
3681              /**
3682               * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
3683               *
3684               * @since 4.2.0
3685               *
3686               * @param string $setting_class WP_Customize_Setting or a subclass.
3687               * @param string $setting_id    ID for dynamic setting, usually coming from `$_POST['customized']`.
3688               * @param array  $setting_args  WP_Customize_Setting or a subclass.
3689               */
3690              $setting_class = apply_filters( 'customize_dynamic_setting_class', $setting_class, $setting_id, $setting_args );
3691  
3692              $setting = new $setting_class( $this, $setting_id, $setting_args );
3693  
3694              $this->add_setting( $setting );
3695              $new_settings[] = $setting;
3696          }
3697          return $new_settings;
3698      }
3699  
3700      /**
3701       * Retrieve a customize setting.
3702       *
3703       * @since 3.4.0
3704       *
3705       * @param string $id Customize Setting ID.
3706       * @return WP_Customize_Setting|void The setting, if set.
3707       */
3708  	public function get_setting( $id ) {
3709          if ( isset( $this->settings[ $id ] ) ) {
3710              return $this->settings[ $id ];
3711          }
3712      }
3713  
3714      /**
3715       * Remove a customize setting.
3716       *
3717       * @since 3.4.0
3718       *
3719       * @param string $id Customize Setting ID.
3720       */
3721  	public function remove_setting( $id ) {
3722          unset( $this->settings[ $id ] );
3723      }
3724  
3725      /**
3726       * Add a customize panel.
3727       *
3728       * @since 4.0.0
3729       * @since 4.5.0 Return added WP_Customize_Panel instance.
3730       *
3731       * @param WP_Customize_Panel|string $id   Customize Panel object, or Panel ID.
3732       * @param array                     $args {
3733       *  Optional. Array of properties for the new Panel object. Default empty array.
3734       *  @type int          $priority              Priority of the panel, defining the display order of panels and sections.
3735       *                                            Default 160.
3736       *  @type string       $capability            Capability required for the panel. Default `edit_theme_options`
3737       *  @type string|array $theme_supports        Theme features required to support the panel.
3738       *  @type string       $title                 Title of the panel to show in UI.
3739       *  @type string       $description           Description to show in the UI.
3740       *  @type string       $type                  Type of the panel.
3741       *  @type callable     $active_callback       Active callback.
3742       * }
3743       * @return WP_Customize_Panel             The instance of the panel that was added.
3744       */
3745  	public function add_panel( $id, $args = array() ) {
3746          if ( $id instanceof WP_Customize_Panel ) {
3747              $panel = $id;
3748          } else {
3749              $panel = new WP_Customize_Panel( $this, $id, $args );
3750          }
3751  
3752          $this->panels[ $panel->id ] = $panel;
3753          return $panel;
3754      }
3755  
3756      /**
3757       * Retrieve a customize panel.
3758       *
3759       * @since 4.0.0
3760       *
3761       * @param string $id Panel ID to get.
3762       * @return WP_Customize_Panel|void Requested panel instance, if set.
3763       */
3764  	public function get_panel( $id ) {
3765          if ( isset( $this->panels[ $id ] ) ) {
3766              return $this->panels[ $id ];
3767          }
3768      }
3769  
3770      /**
3771       * Remove a customize panel.
3772       *
3773       * @since 4.0.0
3774       *
3775       * @param string $id Panel ID to remove.
3776       */
3777  	public function remove_panel( $id ) {
3778          // Removing core components this way is _doing_it_wrong().
3779          if ( in_array( $id, $this->components, true ) ) {
3780              /* translators: 1: panel id, 2: link to 'customize_loaded_components' filter reference */
3781              $message = sprintf(
3782                  __( 'Removing %1$s manually will cause PHP warnings. Use the %2$s filter instead.' ),
3783                  $id,
3784                  '<a href="' . esc_url( 'https://developer.wordpress.org/reference/hooks/customize_loaded_components/' ) . '"><code>customize_loaded_components</code></a>'
3785              );
3786  
3787              _doing_it_wrong( __METHOD__, $message, '4.5.0' );
3788          }
3789          unset( $this->panels[ $id ] );
3790      }
3791  
3792      /**
3793       * Register a customize panel type.
3794       *
3795       * Registered types are eligible to be rendered via JS and created dynamically.
3796       *
3797       * @since 4.3.0
3798       *
3799       * @see WP_Customize_Panel
3800       *
3801       * @param string $panel Name of a custom panel which is a subclass of WP_Customize_Panel.
3802       */
3803  	public function register_panel_type( $panel ) {
3804          $this->registered_panel_types[] = $panel;
3805      }
3806  
3807      /**
3808       * Render JS templates for all registered panel types.
3809       *
3810       * @since 4.3.0
3811       */
3812  	public function render_panel_templates() {
3813          foreach ( $this->registered_panel_types as $panel_type ) {
3814              $panel = new $panel_type( $this, 'temp', array() );
3815              $panel->print_template();
3816          }
3817      }
3818  
3819      /**
3820       * Add a customize section.
3821       *
3822       * @since 3.4.0
3823       * @since 4.5.0 Return added WP_Customize_Section instance.
3824       *
3825       * @param WP_Customize_Section|string $id   Customize Section object, or Section ID.
3826       * @param array                     $args {
3827       *  Optional. Array of properties for the new Section object. Default empty array.
3828       *  @type int          $priority              Priority of the section, defining the display order of panels and sections.
3829       *                                            Default 160.
3830       *  @type string       $panel                 The panel this section belongs to (if any). Default empty.
3831       *  @type string       $capability            Capability required for the section. Default 'edit_theme_options'
3832       *  @type string|array $theme_supports        Theme features required to support the section.
3833       *  @type string       $title                 Title of the section to show in UI.
3834       *  @type string       $description           Description to show in the UI.
3835       *  @type string       $type                  Type of the section.
3836       *  @type callable     $active_callback       Active callback.
3837       *  @type bool         $description_hidden    Hide the description behind a help icon, instead of inline above the first control. Default false.
3838       * }
3839       * @return WP_Customize_Section             The instance of the section that was added.
3840       */
3841  	public function add_section( $id, $args = array() ) {
3842          if ( $id instanceof WP_Customize_Section ) {
3843              $section = $id;
3844          } else {
3845              $section = new WP_Customize_Section( $this, $id, $args );
3846          }
3847  
3848          $this->sections[ $section->id ] = $section;
3849          return $section;
3850      }
3851  
3852      /**
3853       * Retrieve a customize section.
3854       *
3855       * @since 3.4.0
3856       *
3857       * @param string $id Section ID.
3858       * @return WP_Customize_Section|void The section, if set.
3859       */
3860  	public function get_section( $id ) {
3861          if ( isset( $this->sections[ $id ] ) ) {
3862              return $this->sections[ $id ];
3863          }
3864      }
3865  
3866      /**
3867       * Remove a customize section.
3868       *
3869       * @since 3.4.0
3870       *
3871       * @param string $id Section ID.
3872       */
3873  	public function remove_section( $id ) {
3874          unset( $this->sections[ $id ] );
3875      }
3876  
3877      /**
3878       * Register a customize section type.
3879       *
3880       * Registered types are eligible to be rendered via JS and created dynamically.
3881       *
3882       * @since 4.3.0
3883       *
3884       * @see WP_Customize_Section
3885       *
3886       * @param string $section Name of a custom section which is a subclass of WP_Customize_Section.
3887       */
3888  	public function register_section_type( $section ) {
3889          $this->registered_section_types[] = $section;
3890      }
3891  
3892      /**
3893       * Render JS templates for all registered section types.
3894       *
3895       * @since 4.3.0
3896       */
3897  	public function render_section_templates() {
3898          foreach ( $this->registered_section_types as $section_type ) {
3899              $section = new $section_type( $this, 'temp', array() );
3900              $section->print_template();
3901          }
3902      }
3903  
3904      /**
3905       * Add a customize control.
3906       *
3907       * @since 3.4.0
3908       * @since 4.5.0 Return added WP_Customize_Control instance.
3909       *
3910       * @param WP_Customize_Control|string $id   Customize Control object, or ID.
3911       * @param array                       $args {
3912       *  Optional. Array of properties for the new Control object. Default empty array.
3913       *
3914       *  @type array        $settings              All settings tied to the control. If undefined, defaults to `$setting`.
3915       *                                            IDs in the array correspond to the ID of a registered `WP_Customize_Setting`.
3916       *  @type string       $setting               The primary setting for the control (if there is one). Default is 'default'.
3917       *  @type string       $capability            Capability required to use this control. Normally derived from `$settings`.
3918       *  @type int          $priority              Order priority to load the control. Default 10.
3919       *  @type string       $section               The section this control belongs to. Default empty.
3920       *  @type string       $label                 Label for the control. Default empty.
3921       *  @type string       $description           Description for the control. Default empty.
3922       *  @type array        $choices               List of choices for 'radio' or 'select' type controls, where values
3923       *                                            are the keys, and labels are the values. Default empty array.
3924       *  @type array        $input_attrs           List of custom input attributes for control output, where attribute
3925       *                                            names are the keys and values are the values. Default empty array.
3926       *  @type bool         $allow_addition        Show UI for adding new content, currently only used for the
3927       *                                            dropdown-pages control. Default false.
3928       *  @type string       $type                  The type of the control. Default 'text'.
3929       *  @type callback     $active_callback       Active callback.
3930       * }
3931       * @return WP_Customize_Control             The instance of the control that was added.
3932       */
3933  	public function add_control( $id, $args = array() ) {
3934          if ( $id instanceof WP_Customize_Control ) {
3935              $control = $id;
3936          } else {
3937              $control = new WP_Customize_Control( $this, $id, $args );
3938          }
3939  
3940          $this->controls[ $control->id ] = $control;
3941          return $control;
3942      }
3943  
3944      /**
3945       * Retrieve a customize control.
3946       *
3947       * @since 3.4.0
3948       *
3949       * @param string $id ID of the control.
3950       * @return WP_Customize_Control|void The control object, if set.
3951       */
3952  	public function get_control( $id ) {
3953          if ( isset( $this->controls[ $id ] ) ) {
3954              return $this->controls[ $id ];
3955          }
3956      }
3957  
3958      /**
3959       * Remove a customize control.
3960       *
3961       * @since 3.4.0
3962       *
3963       * @param string $id ID of the control.
3964       */
3965  	public function remove_control( $id ) {
3966          unset( $this->controls[ $id ] );
3967      }
3968  
3969      /**
3970       * Register a customize control type.
3971       *
3972       * Registered types are eligible to be rendered via JS and created dynamically.
3973       *
3974       * @since 4.1.0
3975       *
3976       * @param string $control Name of a custom control which is a subclass of
3977       *                        WP_Customize_Control.
3978       */
3979  	public function register_control_type( $control ) {
3980          $this->registered_control_types[] = $control;
3981      }
3982  
3983      /**
3984       * Render JS templates for all registered control types.
3985       *
3986       * @since 4.1.0
3987       */
3988  	public function render_control_templates() {
3989          foreach ( $this->registered_control_types as $control_type ) {
3990              $control = new $control_type(
3991                  $this, 'temp', array(
3992                      'settings' => array(),
3993                  )
3994              );
3995              $control->print_template();
3996          }
3997          ?>
3998  
3999          <script type="text/html" id="tmpl-customize-control-default-content">
4000              <#
4001              var inputId = _.uniqueId( 'customize-control-default-input-' );
4002              var descriptionId = _.uniqueId( 'customize-control-default-description-' );
4003              var describedByAttr = data.description ? ' aria-describedby="' + descriptionId + '" ' : '';
4004              #>
4005              <# switch ( data.type ) {
4006                  case 'checkbox': #>
4007                      <span class="customize-inside-control-row">
4008                          <input
4009                              id="{{ inputId }}"
4010                              {{{ describedByAttr }}}
4011                              type="checkbox"
4012                              value="{{ data.value }}"
4013                              data-customize-setting-key-link="default"
4014                          >
4015                          <label for="{{ inputId }}">
4016                              {{ data.label }}
4017                          </label>
4018                          <# if ( data.description ) { #>
4019                              <span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span>
4020                          <# } #>
4021                      </span>
4022                      <#
4023                      break;
4024                  case 'radio':
4025                      if ( ! data.choices ) {
4026                          return;
4027                      }
4028                      #>
4029                      <# if ( data.label ) { #>
4030                          <label for="{{ inputId }}" class="customize-control-title">
4031                              {{ data.label }}
4032                          </label>
4033                      <# } #>
4034                      <# if ( data.description ) { #>
4035                          <span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span>
4036                      <# } #>
4037                      <# _.each( data.choices, function( val, key ) { #>
4038                          <span class="customize-inside-control-row">
4039                              <#
4040                              var value, text;
4041                              if ( _.isObject( val ) ) {
4042                                  value = val.value;
4043                                  text = val.text;
4044                              } else {
4045                                  value = key;
4046                                  text = val;
4047                              }
4048                              #>
4049                              <input
4050                                  id="{{ inputId + '-' + value }}"
4051                                  type="radio"
4052                                  value="{{ value }}"
4053                                  name="{{ inputId }}"
4054                                  data-customize-setting-key-link="default"
4055                                  {{{ describedByAttr }}}
4056                              >
4057                              <label for="{{ inputId + '-' + value }}">{{ text }}</label>
4058                          </span>
4059                      <# } ); #>
4060                      <#
4061                      break;
4062                  default:
4063                      #>
4064                      <# if ( data.label ) { #>
4065                          <label for="{{ inputId }}" class="customize-control-title">
4066                              {{ data.label }}
4067                          </label>
4068                      <# } #>
4069                      <# if ( data.description ) { #>
4070                          <span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span>
4071                      <# } #>
4072  
4073                      <#
4074                      var inputAttrs = {
4075                          id: inputId,
4076                          'data-customize-setting-key-link': 'default'
4077                      };
4078                      if ( 'textarea' === data.type ) {
4079                          inputAttrs.rows = '5';
4080                      } else if ( 'button' === data.type ) {
4081                          inputAttrs['class'] = 'button button-secondary';
4082                          inputAttrs.type = 'button';
4083                      } else {
4084                          inputAttrs.type = data.type;
4085                      }
4086                      if ( data.description ) {
4087                          inputAttrs['aria-describedby'] = descriptionId;
4088                      }
4089                      _.extend( inputAttrs, data.input_attrs );
4090                      #>
4091  
4092                      <# if ( 'button' === data.type ) { #>
4093                          <button
4094                              <# _.each( _.extend( inputAttrs ), function( value, key ) { #>
4095                                  {{{ key }}}="{{ value }}"
4096                              <# } ); #>
4097                          >{{ inputAttrs.value }}</button>
4098                      <# } else if ( 'textarea' === data.type ) { #>
4099                          <textarea
4100                              <# _.each( _.extend( inputAttrs ), function( value, key ) { #>
4101                                  {{{ key }}}="{{ value }}"
4102                              <# }); #>
4103                          >{{ inputAttrs.value }}</textarea>
4104                      <# } else if ( 'select' === data.type ) { #>
4105                          <# delete inputAttrs.type; #>
4106                          <select
4107                              <# _.each( _.extend( inputAttrs ), function( value, key ) { #>
4108                                  {{{ key }}}="{{ value }}"
4109                              <# }); #>
4110                              >
4111                              <# _.each( data.choices, function( val, key ) { #>
4112                                  <#
4113                                  var value, text;
4114                                  if ( _.isObject( val ) ) {
4115                                      value = val.value;
4116                                      text = val.text;
4117                                  } else {
4118                                      value = key;
4119                                      text = val;
4120                                  }
4121                                  #>
4122                                  <option value="{{ value }}">{{ text }}</option>
4123                              <# } ); #>
4124                          </select>
4125                      <# } else { #>
4126                          <input
4127                              <# _.each( _.extend( inputAttrs ), function( value, key ) { #>
4128                                  {{{ key }}}="{{ value }}"
4129                              <# }); #>
4130                              >
4131                      <# } #>
4132              <# } #>
4133          </script>
4134  
4135          <script type="text/html" id="tmpl-customize-notification">
4136              <li class="notice notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
4137                  <div class="notification-message">{{{ data.message || data.code }}}</div>
4138                  <# if ( data.dismissible ) { #>
4139                      <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php _e( 'Dismiss' ); ?></span></button>
4140                  <# } #>
4141              </li>
4142          </script>
4143  
4144          <script type="text/html" id="tmpl-customize-changeset-locked-notification">
4145              <li class="notice notice-{{ data.type || 'info' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
4146                  <div class="notification-message customize-changeset-locked-message">
4147                      <img class="customize-changeset-locked-avatar" src="{{ data.lockUser.avatar }}" alt="{{ data.lockUser.name }}">
4148                      <p class="currently-editing">
4149                          <# if ( data.message ) { #>
4150                              {{{ data.message }}}
4151                          <# } else if ( data.allowOverride ) { #>
4152                              <?php
4153                              /* translators: %s: User who is customizing the changeset in customizer. */
4154                              printf( __( '%s is already customizing this site. Do you want to take over?' ), '{{ data.lockUser.name }}' );
4155                              ?>
4156                          <# } else { #>
4157                              <?php
4158                              /* translators: %s: User who is customizing the changeset in customizer. */
4159                              printf( __( '%s is already customizing this site. Please wait until they are done to try customizing. Your latest changes have been autosaved.' ), '{{ data.lockUser.name }}' );
4160                              ?>
4161                          <# } #>
4162                      </p>
4163                      <p class="notice notice-error notice-alt" hidden></p>
4164                      <p class="action-buttons">
4165                          <# if ( data.returnUrl !== data.previewUrl ) { #>
4166                              <a class="button customize-notice-go-back-button" href="{{ data.returnUrl }}"><?php _e( 'Go back' ); ?></a>
4167                          <# } #>
4168                          <a class="button customize-notice-preview-button" href="{{ data.frontendPreviewUrl }}"><?php _e( 'Preview' ); ?></a>
4169                          <# if ( data.allowOverride ) { #>
4170                              <button class="button button-primary wp-tab-last customize-notice-take-over-button"><?php _e( 'Take over' ); ?></button>
4171                          <# } #>
4172                      </p>
4173                  </div>
4174              </li>
4175          </script>
4176  
4177          <script type="text/html" id="tmpl-customize-code-editor-lint-error-notification">
4178              <li class="notice notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
4179                  <div class="notification-message">{{{ data.message || data.code }}}</div>
4180  
4181                  <p>
4182                      <# var elementId = 'el-' + String( Math.random() ); #>
4183                      <input id="{{ elementId }}" type="checkbox">
4184                      <label for="{{ elementId }}"><?php _e( 'Update anyway, even though it might break your site?' ); ?></label>
4185                  </p>
4186              </li>
4187          </script>
4188  
4189          <?php
4190          /* The following template is obsolete in core but retained for plugins. */
4191          ?>
4192          <script type="text/html" id="tmpl-customize-control-notifications">
4193              <ul>
4194                  <# _.each( data.notifications, function( notification ) { #>
4195                      <li class="notice notice-{{ notification.type || 'info' }} {{ data.altNotice ? 'notice-alt' : '' }}" data-code="{{ notification.code }}" data-type="{{ notification.type }}">{{{ notification.message || notification.code }}}</li>
4196                  <# } ); #>
4197              </ul>
4198          </script>
4199  
4200          <script type="text/html" id="tmpl-customize-preview-link-control" >
4201              <# var elementPrefix = _.uniqueId( 'el' ) + '-' #>
4202              <p class="customize-control-title">
4203                  <?php esc_html_e( 'Share Preview Link' ); ?>
4204              </p>
4205              <p class="description customize-control-description"><?php esc_html_e( 'See how changes would look live on your website, and share the preview with people who can\'t access the Customizer.' ); ?></p>
4206              <div class="customize-control-notifications-container"></div>
4207              <div class="preview-link-wrapper">
4208                  <label for="{{ elementPrefix }}customize-preview-link-input" class="screen-reader-text"><?php esc_html_e( 'Preview Link' ); ?></label>
4209                  <a href="" target="">
4210                      <span class="preview-control-element" data-component="url"></span>
4211                      <span class="screen-reader-text"><?php _e( '(opens in a new window)' ); ?></span>
4212                  </a>
4213                  <input id="{{ elementPrefix }}customize-preview-link-input" readonly tabindex="-1" class="preview-control-element" data-component="input">
4214                  <button class="customize-copy-preview-link preview-control-element button button-secondary" data-component="button" data-copy-text="<?php esc_attr_e( 'Copy' ); ?>" data-copied-text="<?php esc_attr_e( 'Copied' ); ?>" ><?php esc_html_e( 'Copy' ); ?></button>
4215              </div>
4216          </script>
4217          <script type="text/html" id="tmpl-customize-selected-changeset-status-control">
4218              <# var inputId = _.uniqueId( 'customize-selected-changeset-status-control-input-' ); #>
4219              <# var descriptionId = _.uniqueId( 'customize-selected-changeset-status-control-description-' ); #>
4220              <# if ( data.label ) { #>
4221                  <label for="{{ inputId }}" class="customize-control-title">{{ data.label }}</label>
4222              <# } #>
4223              <# if ( data.description ) { #>
4224                  <span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span>
4225              <# } #>
4226              <# _.each( data.choices, function( choice ) { #>
4227                  <# var choiceId = inputId + '-' + choice.status; #>
4228                  <span class="customize-inside-control-row">
4229                      <input id="{{ choiceId }}" type="radio" value="{{ choice.status }}" name="{{ inputId }}" data-customize-setting-key-link="default">
4230                      <label for="{{ choiceId }}">{{ choice.label }}</label>
4231                  </span>
4232              <# } ); #>
4233          </script>
4234          <?php
4235      }
4236  
4237      /**
4238       * Helper function to compare two objects by priority, ensuring sort stability via instance_number.
4239       *
4240       * @since 3.4.0
4241       * @deprecated 4.7.0 Use wp_list_sort()
4242       *
4243       * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $a Object A.
4244       * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $b Object B.
4245       * @return int
4246       */
4247  	protected function _cmp_priority( $a, $b ) {
4248          _deprecated_function( __METHOD__, '4.7.0', 'wp_list_sort' );
4249  
4250          if ( $a->priority === $b->priority ) {
4251              return $a->instance_number - $b->instance_number;
4252          } else {
4253              return $a->priority - $b->priority;
4254          }
4255      }
4256  
4257      /**
4258       * Prepare panels, sections, and controls.
4259       *
4260       * For each, check if required related components exist,
4261       * whether the user has the necessary capabilities,
4262       * and sort by priority.
4263       *
4264       * @since 3.4.0
4265       */
4266  	public function prepare_controls() {
4267  
4268          $controls       = array();
4269          $this->controls = wp_list_sort(
4270              $this->controls, array(
4271                  'priority'        => 'ASC',
4272                  'instance_number' => 'ASC',
4273              ), 'ASC', true
4274          );
4275  
4276          foreach ( $this->controls as $id => $control ) {
4277              if ( ! isset( $this->sections[ $control->section ] ) || ! $control->check_capabilities() ) {
4278                  continue;
4279              }
4280  
4281              $this->sections[ $control->section ]->controls[] = $control;
4282              $controls[ $id ]                                 = $control;
4283          }
4284          $this->controls = $controls;
4285  
4286          // Prepare sections.
4287          $this->sections = wp_list_sort(
4288              $this->sections, array(
4289                  'priority'        => 'ASC',
4290                  'instance_number' => 'ASC',
4291              ), 'ASC', true
4292          );
4293          $sections       = array();
4294  
4295          foreach ( $this->sections as $section ) {
4296              if ( ! $section->check_capabilities() ) {
4297                  continue;
4298              }
4299  
4300              $section->controls = wp_list_sort(
4301                  $section->controls, array(
4302                      'priority'        => 'ASC',
4303                      'instance_number' => 'ASC',
4304                  )
4305              );
4306  
4307              if ( ! $section->panel ) {
4308                  // Top-level section.
4309                  $sections[ $section->id ] = $section;
4310              } else {
4311                  // This section belongs to a panel.
4312                  if ( isset( $this->panels [ $section->panel ] ) ) {
4313                      $this->panels[ $section->panel ]->sections[ $section->id ] = $section;
4314                  }
4315              }
4316          }
4317          $this->sections = $sections;
4318  
4319          // Prepare panels.
4320          $this->panels = wp_list_sort(
4321              $this->panels, array(
4322                  'priority'        => 'ASC',
4323                  'instance_number' => 'ASC',
4324              ), 'ASC', true
4325          );
4326          $panels       = array();
4327  
4328          foreach ( $this->panels as $panel ) {
4329              if ( ! $panel->check_capabilities() ) {
4330                  continue;
4331              }
4332  
4333              $panel->sections      = wp_list_sort(
4334                  $panel->sections, array(
4335                      'priority'        => 'ASC',
4336                      'instance_number' => 'ASC',
4337                  ), 'ASC', true
4338              );
4339              $panels[ $panel->id ] = $panel;
4340          }
4341          $this->panels = $panels;
4342  
4343          // Sort panels and top-level sections together.
4344          $this->containers = array_merge( $this->panels, $this->sections );
4345          $this->containers = wp_list_sort(
4346              $this->containers, array(
4347                  'priority'        => 'ASC',
4348                  'instance_number' => 'ASC',
4349              ), 'ASC', true
4350          );
4351      }
4352  
4353      /**
4354       * Enqueue scripts for customize controls.
4355       *
4356       * @since 3.4.0
4357       */
4358  	public function enqueue_control_scripts() {
4359          foreach ( $this->controls as $control ) {
4360              $control->enqueue();
4361          }
4362  
4363          if ( ! is_multisite() && ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) || current_user_can( 'delete_themes' ) ) ) {
4364              wp_enqueue_script( 'updates' );
4365              wp_localize_script(
4366                  'updates', '_wpUpdatesItemCounts', array(
4367                      'totals' => wp_get_update_data(),
4368                  )
4369              );
4370          }
4371      }
4372  
4373      /**
4374       * Determine whether the user agent is iOS.
4375       *
4376       * @since 4.4.0
4377       *
4378       * @return bool Whether the user agent is iOS.
4379       */
4380  	public function is_ios() {
4381          return wp_is_mobile() && preg_match( '/iPad|iPod|iPhone/', $_SERVER['HTTP_USER_AGENT'] );
4382      }
4383  
4384      /**
4385       * Get the template string for the Customizer pane document title.
4386       *
4387       * @since 4.4.0
4388       *
4389       * @return string The template string for the document title.
4390       */
4391  	public function get_document_title_template() {
4392          if ( $this->is_theme_active() ) {
4393              /* translators: %s: document title from the preview */
4394              $document_title_tmpl = __( 'Customize: %s' );
4395          } else {
4396              /* translators: %s: document title from the preview */
4397              $document_title_tmpl = __( 'Live Preview: %s' );
4398          }
4399          $document_title_tmpl = html_entity_decode( $document_title_tmpl, ENT_QUOTES, 'UTF-8' ); // Because exported to JS and assigned to document.title.
4400          return $document_title_tmpl;
4401      }
4402  
4403      /**
4404       * Set the initial URL to be previewed.
4405       *
4406       * URL is validated.
4407       *
4408       * @since 4.4.0
4409       *
4410       * @param string $preview_url URL to be previewed.
4411       */
4412  	public function set_preview_url( $preview_url ) {
4413          $preview_url       = esc_url_raw( $preview_url );
4414          $this->preview_url = wp_validate_redirect( $preview_url, home_url( '/' ) );
4415      }
4416  
4417      /**
4418       * Get the initial URL to be previewed.
4419       *
4420       * @since 4.4.0
4421       *
4422       * @return string URL being previewed.
4423       */
4424  	public function get_preview_url() {
4425          if ( empty( $this->preview_url ) ) {
4426              $preview_url = home_url( '/' );
4427          } else {
4428              $preview_url = $this->preview_url;
4429          }
4430          return $preview_url;
4431      }
4432  
4433      /**
4434       * Determines whether the admin and the frontend are on different domains.
4435       *
4436       * @since 4.7.0
4437       *
4438       * @return bool Whether cross-domain.
4439       */
4440  	public function is_cross_domain() {
4441          $admin_origin = wp_parse_url( admin_url() );
4442          $home_origin  = wp_parse_url( home_url() );
4443          $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );
4444          return $cross_domain;
4445      }
4446  
4447      /**
4448       * Get URLs allowed to be previewed.
4449       *
4450       * If the front end and the admin are served from the same domain, load the
4451       * preview over ssl if the Customizer is being loaded over ssl. This avoids
4452       * insecure content warnings. This is not attempted if the admin and front end
4453       * are on different domains to avoid the case where the front end doesn't have
4454       * ssl certs. Domain mapping plugins can allow other urls in these conditions
4455       * using the customize_allowed_urls filter.
4456       *
4457       * @since 4.7.0
4458       *
4459       * @returns array Allowed URLs.
4460       */
4461  	public function get_allowed_urls() {
4462          $allowed_urls = array( home_url( '/' ) );
4463  
4464          if ( is_ssl() && ! $this->is_cross_domain() ) {
4465              $allowed_urls[] = home_url( '/', 'https' );
4466          }
4467  
4468          /**
4469           * Filters the list of URLs allowed to be clicked and followed in the Customizer preview.
4470           *
4471           * @since 3.4.0
4472           *
4473           * @param array $allowed_urls An array of allowed URLs.
4474           */
4475          $allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) );
4476  
4477          return $allowed_urls;
4478      }
4479  
4480      /**
4481       * Get messenger channel.
4482       *
4483       * @since 4.7.0
4484       *
4485       * @return string Messenger channel.
4486       */
4487  	public function get_messenger_channel() {
4488          return $this->messenger_channel;
4489      }
4490  
4491      /**
4492       * Set URL to link the user to when closing the Customizer.
4493       *
4494       * URL is validated.
4495       *
4496       * @since 4.4.0
4497       *
4498       * @param string $return_url URL for return link.
4499       */
4500  	public function set_return_url( $return_url ) {
4501          $return_url       = esc_url_raw( $return_url );
4502          $return_url       = remove_query_arg( wp_removable_query_args(), $return_url );
4503          $return_url       = wp_validate_redirect( $return_url );
4504          $this->return_url = $return_url;
4505      }
4506  
4507      /**
4508       * Get URL to link the user to when closing the Customizer.
4509       *
4510       * @since 4.4.0
4511       *
4512       * @return string URL for link to close Customizer.
4513       */
4514  	public function get_return_url() {
4515          $referer                    = wp_get_referer();
4516          $excluded_referer_basenames = array( 'customize.php', 'wp-login.php' );
4517  
4518          if ( $this->return_url ) {
4519              $return_url = $this->return_url;
4520          } elseif ( $referer && ! in_array( basename( parse_url( $referer, PHP_URL_PATH ) ), $excluded_referer_basenames, true ) ) {
4521              $return_url = $referer;
4522          } elseif ( $this->preview_url ) {
4523              $return_url = $this->preview_url;
4524          } else {
4525              $return_url = home_url( '/' );
4526          }
4527          return $return_url;
4528      }
4529  
4530      /**
4531       * Set the autofocused constructs.
4532       *
4533       * @since 4.4.0
4534       *
4535       * @param array $autofocus {
4536       *     Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
4537       *
4538       *     @type string [$control]  ID for control to be autofocused.
4539       *     @type string [$section]  ID for section to be autofocused.
4540       *     @type string [$panel]    ID for panel to be autofocused.
4541       * }
4542       */
4543  	public function set_autofocus( $autofocus ) {
4544          $this->autofocus = array_filter( wp_array_slice_assoc( $autofocus, array( 'panel', 'section', 'control' ) ), 'is_string' );
4545      }
4546  
4547      /**
4548       * Get the autofocused constructs.
4549       *
4550       * @since 4.4.0
4551       *
4552       * @return array {
4553       *     Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
4554       *
4555       *     @type string [$control]  ID for control to be autofocused.
4556       *     @type string [$section]  ID for section to be autofocused.
4557       *     @type string [$panel]    ID for panel to be autofocused.
4558       * }
4559       */
4560  	public function get_autofocus() {
4561          return $this->autofocus;
4562      }
4563  
4564      /**
4565       * Get nonces for the Customizer.
4566       *
4567       * @since 4.5.0
4568       *
4569       * @return array Nonces.
4570       */
4571  	public function get_nonces() {
4572          $nonces = array(
4573              'save'                     => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
4574              'preview'                  => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
4575              'switch_themes'            => wp_create_nonce( 'switch_themes' ),
4576              'dismiss_autosave_or_lock' => wp_create_nonce( 'customize_dismiss_autosave_or_lock' ),
4577              'override_lock'            => wp_create_nonce( 'customize_override_changeset_lock' ),
4578              'trash'                    => wp_create_nonce( 'trash_customize_changeset' ),
4579          );
4580  
4581          /**
4582           * Filters nonces for Customizer.
4583           *
4584           * @since 4.2.0
4585           *
4586           * @param array                $nonces Array of refreshed nonces for save and
4587           *                                     preview actions.
4588           * @param WP_Customize_Manager $this   WP_Customize_Manager instance.
4589           */
4590          $nonces = apply_filters( 'customize_refresh_nonces', $nonces, $this );
4591  
4592          return $nonces;
4593      }
4594  
4595      /**
4596       * Print JavaScript settings for parent window.
4597       *
4598       * @since 4.4.0
4599       */
4600  	public function customize_pane_settings() {
4601  
4602          $login_url = add_query_arg(
4603              array(
4604                  'interim-login'   => 1,
4605                  'customize-login' => 1,
4606              ), wp_login_url()
4607          );
4608  
4609          // Ensure dirty flags are set for modified settings.
4610          foreach ( array_keys( $this->unsanitized_post_values() ) as $setting_id ) {
4611              $setting = $this->get_setting( $setting_id );
4612              if ( $setting ) {
4613                  $setting->dirty = true;
4614              }
4615          }
4616  
4617          $autosave_revision_post  = null;
4618          $autosave_autodraft_post = null;
4619          $changeset_post_id       = $this->changeset_post_id();
4620          if ( ! $this->saved_starter_content_changeset && ! $this->autosaved() ) {
4621              if ( $changeset_post_id ) {
4622                  $autosave_revision_post = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
4623              } else {
4624                  $autosave_autodraft_posts = $this->get_changeset_posts(
4625                      array(
4626                          'posts_per_page'            => 1,
4627                          'post_status'               => 'auto-draft',
4628                          'exclude_restore_dismissed' => true,
4629                      )
4630                  );
4631                  if ( ! empty( $autosave_autodraft_posts ) ) {
4632                      $autosave_autodraft_post = array_shift( $autosave_autodraft_posts );
4633                  }
4634              }
4635          }
4636  
4637          $current_user_can_publish = current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts );
4638  
4639          // @todo Include all of the status labels here from script-loader.php, and then allow it to be filtered.
4640          $status_choices = array();
4641          if ( $current_user_can_publish ) {
4642              $status_choices[] = array(
4643                  'status' => 'publish',
4644                  'label'  => __( 'Publish' ),
4645              );
4646          }
4647          $status_choices[] = array(
4648              'status' => 'draft',
4649              'label'  => __( 'Save Draft' ),
4650          );
4651          if ( $current_user_can_publish ) {
4652              $status_choices[] = array(
4653                  'status' => 'future',
4654                  'label'  => _x( 'Schedule', 'customizer changeset action/button label' ),
4655              );
4656          }
4657  
4658          // Prepare Customizer settings to pass to JavaScript.
4659          $changeset_post = null;
4660          if ( $changeset_post_id ) {
4661              $changeset_post = get_post( $changeset_post_id );
4662          }
4663  
4664          // Determine initial date to be at present or future, not past.
4665          $current_time = current_time( 'mysql', false );
4666          $initial_date = $current_time;
4667          if ( $changeset_post ) {
4668              $initial_date = get_the_time( 'Y-m-d H:i:s', $changeset_post->ID );
4669              if ( $initial_date < $current_time ) {
4670                  $initial_date = $current_time;
4671              }
4672          }
4673  
4674          $lock_user_id = false;
4675          if ( $this->changeset_post_id() ) {
4676              $lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
4677          }
4678  
4679          $settings = array(
4680              'changeset'              => array(
4681                  'uuid'                  => $this->changeset_uuid(),
4682                  'branching'             => $this->branching(),
4683                  'autosaved'             => $this->autosaved(),
4684                  'hasAutosaveRevision'   => ! empty( $autosave_revision_post ),
4685                  'latestAutoDraftUuid'   => $autosave_autodraft_post ? $autosave_autodraft_post->post_name : null,
4686                  'status'                => $changeset_post ? $changeset_post->post_status : '',
4687                  'currentUserCanPublish' => $current_user_can_publish,
4688                  'publishDate'           => $initial_date,
4689                  'statusChoices'         => $status_choices,
4690                  'lockUser'              => $lock_user_id ? $this->get_lock_user_data( $lock_user_id ) : null,
4691              ),
4692              'initialServerDate'      => $current_time,
4693              'dateFormat'             => get_option( 'date_format' ),
4694              'timeFormat'             => get_option( 'time_format' ),
4695              'initialServerTimestamp' => floor( microtime( true ) * 1000 ),
4696              'initialClientTimestamp' => -1, // To be set with JS below.
4697              'timeouts'               => array(
4698                  'windowRefresh'           => 250,
4699                  'changesetAutoSave'       => AUTOSAVE_INTERVAL * 1000,
4700                  'keepAliveCheck'          => 2500,
4701                  'reflowPaneContents'      => 100,
4702                  'previewFrameSensitivity' => 2000,
4703              ),
4704              'theme'                  => array(
4705                  'stylesheet' => $this->get_stylesheet(),
4706                  'active'     => $this->is_theme_active(),
4707              ),
4708              'url'                    => array(
4709                  'preview'       => esc_url_raw( $this->get_preview_url() ),
4710                  'return'        => esc_url_raw( $this->get_return_url() ),
4711                  'parent'        => esc_url_raw( admin_url() ),
4712                  'activated'     => esc_url_raw( home_url( '/' ) ),
4713                  'ajax'          => esc_url_raw( admin_url( 'admin-ajax.php', 'relative' ) ),
4714                  'allowed'       => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
4715                  'isCrossDomain' => $this->is_cross_domain(),
4716                  'home'          => esc_url_raw( home_url( '/' ) ),
4717                  'login'         => esc_url_raw( $login_url ),
4718              ),
4719              'browser'                => array(
4720                  'mobile' => wp_is_mobile(),
4721                  'ios'    => $this->is_ios(),
4722              ),
4723              'panels'                 => array(),
4724              'sections'               => array(),
4725              'nonce'                  => $this->get_nonces(),
4726              'autofocus'              => $this->get_autofocus(),
4727              'documentTitleTmpl'      => $this->get_document_title_template(),
4728              'previewableDevices'     => $this->get_previewable_devices(),
4729              'l10n'                   => array(
4730                  'confirmDeleteTheme'   => __( 'Are you sure you want to delete this theme?' ),
4731                  /* translators: %d: number of theme search results, which cannot currently consider singular vs. plural forms */
4732                  'themeSearchResults'   => __( '%d themes found' ),
4733                  /* translators: %d: number of themes being displayed, which cannot currently consider singular vs. plural forms */
4734                  'announceThemeCount'   => __( 'Displaying %d themes' ),
4735                  /* translators: %s: theme name */
4736                  'announceThemeDetails' => __( 'Showing details for theme: %s' ),
4737              ),
4738          );
4739  
4740          // Temporarily disable installation in Customizer. See #42184.
4741          $filesystem_method = get_filesystem_method();
4742          ob_start();
4743          $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
4744          ob_end_clean();
4745          if ( 'direct' !== $filesystem_method && ! $filesystem_credentials_are_stored ) {
4746              $settings['theme']['_filesystemCredentialsNeeded'] = true;
4747          }
4748  
4749          // Prepare Customize Section objects to pass to JavaScript.
4750          foreach ( $this->sections() as $id => $section ) {
4751              if ( $section->check_capabilities() ) {
4752                  $settings['sections'][ $id ] = $section->json();
4753              }
4754          }
4755  
4756          // Prepare Customize Panel objects to pass to JavaScript.
4757          foreach ( $this->panels() as $panel_id => $panel ) {
4758              if ( $panel->check_capabilities() ) {
4759                  $settings['panels'][ $panel_id ] = $panel->json();
4760                  foreach ( $panel->sections as $section_id => $section ) {
4761                      if ( $section->check_capabilities() ) {
4762                          $settings['sections'][ $section_id ] = $section->json();
4763                      }
4764                  }
4765              }
4766          }
4767  
4768          ?>
4769          <script type="text/javascript">
4770              var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
4771              _wpCustomizeSettings.initialClientTimestamp = _.now();
4772              _wpCustomizeSettings.controls = {};
4773              _wpCustomizeSettings.settings = {};
4774              <?php
4775  
4776              // Serialize settings one by one to improve memory usage.
4777              echo "(function ( s ){\n";
4778              foreach ( $this->settings() as $setting ) {
4779                  if ( $setting->check_capabilities() ) {
4780                      printf(
4781                          "s[%s] = %s;\n",
4782                          wp_json_encode( $setting->id ),
4783                          wp_json_encode( $setting->json() )
4784                      );
4785                  }
4786              }
4787              echo "})( _wpCustomizeSettings.settings );\n";
4788  
4789              // Serialize controls one by one to improve memory usage.
4790              echo "(function ( c ){\n";
4791              foreach ( $this->controls() as $control ) {
4792                  if ( $control->check_capabilities() ) {
4793                      printf(
4794                          "c[%s] = %s;\n",
4795                          wp_json_encode( $control->id ),
4796                          wp_json_encode( $control->json() )
4797                      );
4798                  }
4799              }
4800              echo "})( _wpCustomizeSettings.controls );\n";
4801          ?>
4802          </script>
4803          <?php
4804      }
4805  
4806      /**
4807       * Returns a list of devices to allow previewing.
4808       *
4809       * @since 4.5.0
4810       *
4811       * @return array List of devices with labels and default setting.
4812       */
4813  	public function get_previewable_devices() {
4814          $devices = array(
4815              'desktop' => array(
4816                  'label'   => __( 'Enter desktop preview mode' ),
4817                  'default' => true,
4818              ),
4819              'tablet'  => array(
4820                  'label' => __( 'Enter tablet preview mode' ),
4821              ),
4822              'mobile'  => array(
4823                  'label' => __( 'Enter mobile preview mode' ),
4824              ),
4825          );
4826  
4827          /**
4828           * Filters the available devices to allow previewing in the Customizer.
4829           *
4830           * @since 4.5.0
4831           *
4832           * @see WP_Customize_Manager::get_previewable_devices()
4833           *
4834           * @param array $devices List of devices with labels and default setting.
4835           */
4836          $devices = apply_filters( 'customize_previewable_devices', $devices );
4837  
4838          return $devices;
4839      }
4840  
4841      /**
4842       * Register some default controls.
4843       *
4844       * @since 3.4.0
4845       */
4846  	public function register_controls() {
4847  
4848          /* Themes (controls are loaded via ajax) */
4849  
4850          $this->add_panel(
4851              new WP_Customize_Themes_Panel(
4852                  $this, 'themes', array(
4853                      'title'       => $this->theme()->display( 'Name' ),
4854                      'description' => (
4855                      '<p>' . __( 'Looking for a theme? You can search or browse the WordPress.org theme directory, install and preview themes, then activate them right here.' ) . '</p>' .
4856                      '<p>' . __( 'While previewing a new theme, you can continue to tailor things like widgets and menus, and explore theme-specific options.' ) . '</p>'
4857                      ),
4858                      'capability'  => 'switch_themes',
4859                      'priority'    => 0,
4860                  )
4861              )
4862          );
4863  
4864          $this->add_section(
4865              new WP_Customize_Themes_Section(
4866                  $this, 'installed_themes', array(
4867                      'title'      => __( 'Installed themes' ),
4868                      'action'     => 'installed',
4869                      'capability' => 'switch_themes',
4870                      'panel'      => 'themes',
4871                      'priority'   => 0,
4872                  )
4873              )
4874          );
4875  
4876          if ( ! is_multisite() ) {
4877              $this->add_section(
4878                  new WP_Customize_Themes_Section(
4879                      $this, 'wporg_themes', array(
4880                          'title'       => __( 'WordPress.org themes' ),
4881                          'action'      => 'wporg',
4882                          'filter_type' => 'remote',
4883                          'capability'  => 'install_themes',
4884                          'panel'       => 'themes',
4885                          'priority'    => 5,
4886                      )
4887                  )
4888              );
4889          }
4890  
4891          // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
4892          $this->add_setting(
4893              new WP_Customize_Filter_Setting(
4894                  $this, 'active_theme', array(
4895                      'capability' => 'switch_themes',
4896                  )
4897              )
4898          );
4899  
4900          /* Site Identity */
4901  
4902