angelovcom.net

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class-wp-customize-manager.php (201191B)


      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|null
    236 	 */
    237 	private $_changeset_data;
    238 
    239 	/**
    240 	 * Constructor.
    241 	 *
    242 	 * @since 3.4.0
    243 	 * @since 4.7.0 Added `$args` parameter.
    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,
    274 		// but they are also looked at in the $_REQUEST global here for back-compat.
    275 		if ( ! isset( $args['theme'] ) ) {
    276 			if ( isset( $_REQUEST['customize_theme'] ) ) {
    277 				$args['theme'] = wp_unslash( $_REQUEST['customize_theme'] );
    278 			} elseif ( isset( $_REQUEST['theme'] ) ) { // Deprecated.
    279 				$args['theme'] = wp_unslash( $_REQUEST['theme'] );
    280 			}
    281 		}
    282 		if ( ! isset( $args['messenger_channel'] ) && isset( $_REQUEST['customize_messenger_channel'] ) ) {
    283 			$args['messenger_channel'] = sanitize_key( wp_unslash( $_REQUEST['customize_messenger_channel'] ) );
    284 		}
    285 
    286 		$this->original_stylesheet = get_stylesheet();
    287 		$this->theme               = wp_get_theme( 0 === validate_file( $args['theme'] ) ? $args['theme'] : null );
    288 		$this->messenger_channel   = $args['messenger_channel'];
    289 		$this->_changeset_uuid     = $args['changeset_uuid'];
    290 
    291 		foreach ( array( 'settings_previewed', 'autosaved', 'branching' ) as $key ) {
    292 			if ( isset( $args[ $key ] ) ) {
    293 				$this->$key = (bool) $args[ $key ];
    294 			}
    295 		}
    296 
    297 		require_once ABSPATH . WPINC . '/class-wp-customize-setting.php';
    298 		require_once ABSPATH . WPINC . '/class-wp-customize-panel.php';
    299 		require_once ABSPATH . WPINC . '/class-wp-customize-section.php';
    300 		require_once ABSPATH . WPINC . '/class-wp-customize-control.php';
    301 
    302 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-color-control.php';
    303 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-media-control.php';
    304 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-upload-control.php';
    305 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-image-control.php';
    306 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-background-image-control.php';
    307 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-background-position-control.php';
    308 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-cropped-image-control.php';
    309 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-site-icon-control.php';
    310 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-header-image-control.php';
    311 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-theme-control.php';
    312 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-code-editor-control.php';
    313 		require_once ABSPATH . WPINC . '/customize/class-wp-widget-area-customize-control.php';
    314 		require_once ABSPATH . WPINC . '/customize/class-wp-widget-form-customize-control.php';
    315 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-control.php';
    316 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-control.php';
    317 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-location-control.php';
    318 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-name-control.php';
    319 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-locations-control.php';
    320 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-auto-add-control.php';
    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 
    329 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-custom-css-setting.php';
    330 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-filter-setting.php';
    331 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-header-image-setting.php';
    332 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-background-image-setting.php';
    333 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-setting.php';
    334 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-setting.php';
    335 
    336 		/**
    337 		 * Filters the core Customizer components to load.
    338 		 *
    339 		 * This allows Core components to be excluded from being instantiated by
    340 		 * filtering them out of the array. Note that this filter generally runs
    341 		 * during the {@see 'plugins_loaded'} action, so it cannot be added
    342 		 * in a theme.
    343 		 *
    344 		 * @since 4.4.0
    345 		 *
    346 		 * @see WP_Customize_Manager::__construct()
    347 		 *
    348 		 * @param string[]             $components Array of core components to load.
    349 		 * @param WP_Customize_Manager $this       WP_Customize_Manager instance.
    350 		 */
    351 		$components = apply_filters( 'customize_loaded_components', $this->components, $this );
    352 
    353 		require_once ABSPATH . WPINC . '/customize/class-wp-customize-selective-refresh.php';
    354 		$this->selective_refresh = new WP_Customize_Selective_Refresh( $this );
    355 
    356 		if ( in_array( 'widgets', $components, true ) ) {
    357 			require_once ABSPATH . WPINC . '/class-wp-customize-widgets.php';
    358 			$this->widgets = new WP_Customize_Widgets( $this );
    359 		}
    360 
    361 		if ( in_array( 'nav_menus', $components, true ) ) {
    362 			require_once ABSPATH . WPINC . '/class-wp-customize-nav-menus.php';
    363 			$this->nav_menus = new WP_Customize_Nav_Menus( $this );
    364 		}
    365 
    366 		add_action( 'setup_theme', array( $this, 'setup_theme' ) );
    367 		add_action( 'wp_loaded', array( $this, 'wp_loaded' ) );
    368 
    369 		// Do not spawn cron (especially the alternate cron) while running the Customizer.
    370 		remove_action( 'init', 'wp_cron' );
    371 
    372 		// Do not run update checks when rendering the controls.
    373 		remove_action( 'admin_init', '_maybe_update_core' );
    374 		remove_action( 'admin_init', '_maybe_update_plugins' );
    375 		remove_action( 'admin_init', '_maybe_update_themes' );
    376 
    377 		add_action( 'wp_ajax_customize_save', array( $this, 'save' ) );
    378 		add_action( 'wp_ajax_customize_trash', array( $this, 'handle_changeset_trash_request' ) );
    379 		add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
    380 		add_action( 'wp_ajax_customize_load_themes', array( $this, 'handle_load_themes_request' ) );
    381 		add_filter( 'heartbeat_settings', array( $this, 'add_customize_screen_to_heartbeat_settings' ) );
    382 		add_filter( 'heartbeat_received', array( $this, 'check_changeset_lock_with_heartbeat' ), 10, 3 );
    383 		add_action( 'wp_ajax_customize_override_changeset_lock', array( $this, 'handle_override_changeset_lock_request' ) );
    384 		add_action( 'wp_ajax_customize_dismiss_autosave_or_lock', array( $this, 'handle_dismiss_autosave_or_lock_request' ) );
    385 
    386 		add_action( 'customize_register', array( $this, 'register_controls' ) );
    387 		add_action( 'customize_register', array( $this, 'register_dynamic_settings' ), 11 ); // Allow code to create settings first.
    388 		add_action( 'customize_controls_init', array( $this, 'prepare_controls' ) );
    389 		add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) );
    390 
    391 		// Render Common, Panel, Section, and Control templates.
    392 		add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_panel_templates' ), 1 );
    393 		add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_section_templates' ), 1 );
    394 		add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_control_templates' ), 1 );
    395 
    396 		// Export header video settings with the partial response.
    397 		add_filter( 'customize_render_partials_response', array( $this, 'export_header_video_settings' ), 10, 3 );
    398 
    399 		// Export the settings to JS via the _wpCustomizeSettings variable.
    400 		add_action( 'customize_controls_print_footer_scripts', array( $this, 'customize_pane_settings' ), 1000 );
    401 
    402 		// Add theme update notices.
    403 		if ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) ) {
    404 			require_once ABSPATH . 'wp-admin/includes/update.php';
    405 			add_action( 'customize_controls_print_footer_scripts', 'wp_print_admin_notice_templates' );
    406 		}
    407 	}
    408 
    409 	/**
    410 	 * Return true if it's an Ajax request.
    411 	 *
    412 	 * @since 3.4.0
    413 	 * @since 4.2.0 Added `$action` param.
    414 	 *
    415 	 * @param string|null $action Whether the supplied Ajax action is being run.
    416 	 * @return bool True if it's an Ajax request, false otherwise.
    417 	 */
    418 	public function doing_ajax( $action = null ) {
    419 		if ( ! wp_doing_ajax() ) {
    420 			return false;
    421 		}
    422 
    423 		if ( ! $action ) {
    424 			return true;
    425 		} else {
    426 			/*
    427 			 * Note: we can't just use doing_action( "wp_ajax_{$action}" ) because we need
    428 			 * to check before admin-ajax.php gets to that point.
    429 			 */
    430 			return isset( $_REQUEST['action'] ) && wp_unslash( $_REQUEST['action'] ) === $action;
    431 		}
    432 	}
    433 
    434 	/**
    435 	 * Custom wp_die wrapper. Returns either the standard message for UI
    436 	 * or the Ajax message.
    437 	 *
    438 	 * @since 3.4.0
    439 	 *
    440 	 * @param string|WP_Error $ajax_message Ajax return.
    441 	 * @param string          $message      Optional. UI message.
    442 	 */
    443 	protected function wp_die( $ajax_message, $message = null ) {
    444 		if ( $this->doing_ajax() ) {
    445 			wp_die( $ajax_message );
    446 		}
    447 
    448 		if ( ! $message ) {
    449 			$message = __( 'Something went wrong.' );
    450 		}
    451 
    452 		if ( $this->messenger_channel ) {
    453 			ob_start();
    454 			wp_enqueue_scripts();
    455 			wp_print_scripts( array( 'customize-base' ) );
    456 
    457 			$settings = array(
    458 				'messengerArgs' => array(
    459 					'channel' => $this->messenger_channel,
    460 					'url'     => wp_customize_url(),
    461 				),
    462 				'error'         => $ajax_message,
    463 			);
    464 			?>
    465 			<script>
    466 			( function( api, settings ) {
    467 				var preview = new api.Messenger( settings.messengerArgs );
    468 				preview.send( 'iframe-loading-error', settings.error );
    469 			} )( wp.customize, <?php echo wp_json_encode( $settings ); ?> );
    470 			</script>
    471 			<?php
    472 			$message .= ob_get_clean();
    473 		}
    474 
    475 		wp_die( $message );
    476 	}
    477 
    478 	/**
    479 	 * Return the Ajax wp_die() handler if it's a customized request.
    480 	 *
    481 	 * @since 3.4.0
    482 	 * @deprecated 4.7.0
    483 	 *
    484 	 * @return callable Die handler.
    485 	 */
    486 	public function wp_die_handler() {
    487 		_deprecated_function( __METHOD__, '4.7.0' );
    488 
    489 		if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) {
    490 			return '_ajax_wp_die_handler';
    491 		}
    492 
    493 		return '_default_wp_die_handler';
    494 	}
    495 
    496 	/**
    497 	 * Start preview and customize theme.
    498 	 *
    499 	 * Check if customize query variable exist. Init filters to filter the current theme.
    500 	 *
    501 	 * @since 3.4.0
    502 	 *
    503 	 * @global string $pagenow
    504 	 */
    505 	public function setup_theme() {
    506 		global $pagenow;
    507 
    508 		// Check permissions for customize.php access since this method is called before customize.php can run any code.
    509 		if ( 'customize.php' === $pagenow && ! current_user_can( 'customize' ) ) {
    510 			if ( ! is_user_logged_in() ) {
    511 				auth_redirect();
    512 			} else {
    513 				wp_die(
    514 					'<h1>' . __( 'You need a higher level of permission.' ) . '</h1>' .
    515 					'<p>' . __( 'Sorry, you are not allowed to customize this site.' ) . '</p>',
    516 					403
    517 				);
    518 			}
    519 			return;
    520 		}
    521 
    522 		// If a changeset was provided is invalid.
    523 		if ( isset( $this->_changeset_uuid ) && false !== $this->_changeset_uuid && ! wp_is_uuid( $this->_changeset_uuid ) ) {
    524 			$this->wp_die( -1, __( 'Invalid changeset UUID' ) );
    525 		}
    526 
    527 		/*
    528 		 * Clear incoming post data if the user lacks a CSRF token (nonce). Note that the customizer
    529 		 * application will inject the customize_preview_nonce query parameter into all Ajax requests.
    530 		 * For similar behavior elsewhere in WordPress, see rest_cookie_check_errors() which logs out
    531 		 * a user when a valid nonce isn't present.
    532 		 */
    533 		$has_post_data_nonce = (
    534 			check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'nonce', false )
    535 			||
    536 			check_ajax_referer( 'save-customize_' . $this->get_stylesheet(), 'nonce', false )
    537 			||
    538 			check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'customize_preview_nonce', false )
    539 		);
    540 		if ( ! current_user_can( 'customize' ) || ! $has_post_data_nonce ) {
    541 			unset( $_POST['customized'] );
    542 			unset( $_REQUEST['customized'] );
    543 		}
    544 
    545 		/*
    546 		 * If unauthenticated then require a valid changeset UUID to load the preview.
    547 		 * In this way, the UUID serves as a secret key. If the messenger channel is present,
    548 		 * then send unauthenticated code to prompt re-auth.
    549 		 */
    550 		if ( ! current_user_can( 'customize' ) && ! $this->changeset_post_id() ) {
    551 			$this->wp_die( $this->messenger_channel ? 0 : -1, __( 'Non-existent changeset UUID.' ) );
    552 		}
    553 
    554 		if ( ! headers_sent() ) {
    555 			send_origin_headers();
    556 		}
    557 
    558 		// Hide the admin bar if we're embedded in the customizer iframe.
    559 		if ( $this->messenger_channel ) {
    560 			show_admin_bar( false );
    561 		}
    562 
    563 		if ( $this->is_theme_active() ) {
    564 			// Once the theme is loaded, we'll validate it.
    565 			add_action( 'after_setup_theme', array( $this, 'after_setup_theme' ) );
    566 		} else {
    567 			// If the requested theme is not the active theme and the user doesn't have
    568 			// the switch_themes cap, bail.
    569 			if ( ! current_user_can( 'switch_themes' ) ) {
    570 				$this->wp_die( -1, __( 'Sorry, you are not allowed to edit theme options on this site.' ) );
    571 			}
    572 
    573 			// If the theme has errors while loading, bail.
    574 			if ( $this->theme()->errors() ) {
    575 				$this->wp_die( -1, $this->theme()->errors()->get_error_message() );
    576 			}
    577 
    578 			// If the theme isn't allowed per multisite settings, bail.
    579 			if ( ! $this->theme()->is_allowed() ) {
    580 				$this->wp_die( -1, __( 'The requested theme does not exist.' ) );
    581 			}
    582 		}
    583 
    584 		// Make sure changeset UUID is established immediately after the theme is loaded.
    585 		add_action( 'after_setup_theme', array( $this, 'establish_loaded_changeset' ), 5 );
    586 
    587 		/*
    588 		 * Import theme starter content for fresh installations when landing in the customizer.
    589 		 * Import starter content at after_setup_theme:100 so that any
    590 		 * add_theme_support( 'starter-content' ) calls will have been made.
    591 		 */
    592 		if ( get_option( 'fresh_site' ) && 'customize.php' === $pagenow ) {
    593 			add_action( 'after_setup_theme', array( $this, 'import_theme_starter_content' ), 100 );
    594 		}
    595 
    596 		$this->start_previewing_theme();
    597 	}
    598 
    599 	/**
    600 	 * Establish the loaded changeset.
    601 	 *
    602 	 * This method runs right at after_setup_theme and applies the 'customize_changeset_branching' filter to determine
    603 	 * whether concurrent changesets are allowed. Then if the Customizer is not initialized with a `changeset_uuid` param,
    604 	 * this method will determine which UUID should be used. If changeset branching is disabled, then the most saved
    605 	 * changeset will be loaded by default. Otherwise, if there are no existing saved changesets or if changeset branching is
    606 	 * enabled, then a new UUID will be generated.
    607 	 *
    608 	 * @since 4.9.0
    609 	 *
    610 	 * @global string $pagenow
    611 	 */
    612 	public function establish_loaded_changeset() {
    613 		global $pagenow;
    614 
    615 		if ( empty( $this->_changeset_uuid ) ) {
    616 			$changeset_uuid = null;
    617 
    618 			if ( ! $this->branching() && $this->is_theme_active() ) {
    619 				$unpublished_changeset_posts = $this->get_changeset_posts(
    620 					array(
    621 						'post_status'               => array_diff( get_post_stati(), array( 'auto-draft', 'publish', 'trash', 'inherit', 'private' ) ),
    622 						'exclude_restore_dismissed' => false,
    623 						'author'                    => 'any',
    624 						'posts_per_page'            => 1,
    625 						'order'                     => 'DESC',
    626 						'orderby'                   => 'date',
    627 					)
    628 				);
    629 				$unpublished_changeset_post  = array_shift( $unpublished_changeset_posts );
    630 				if ( ! empty( $unpublished_changeset_post ) && wp_is_uuid( $unpublished_changeset_post->post_name ) ) {
    631 					$changeset_uuid = $unpublished_changeset_post->post_name;
    632 				}
    633 			}
    634 
    635 			// If no changeset UUID has been set yet, then generate a new one.
    636 			if ( empty( $changeset_uuid ) ) {
    637 				$changeset_uuid = wp_generate_uuid4();
    638 			}
    639 
    640 			$this->_changeset_uuid = $changeset_uuid;
    641 		}
    642 
    643 		if ( is_admin() && 'customize.php' === $pagenow ) {
    644 			$this->set_changeset_lock( $this->changeset_post_id() );
    645 		}
    646 	}
    647 
    648 	/**
    649 	 * Callback to validate a theme once it is loaded
    650 	 *
    651 	 * @since 3.4.0
    652 	 */
    653 	public function after_setup_theme() {
    654 		$doing_ajax_or_is_customized = ( $this->doing_ajax() || isset( $_POST['customized'] ) );
    655 		if ( ! $doing_ajax_or_is_customized && ! validate_current_theme() ) {
    656 			wp_redirect( 'themes.php?broken=true' );
    657 			exit;
    658 		}
    659 	}
    660 
    661 	/**
    662 	 * If the theme to be previewed isn't the active theme, add filter callbacks
    663 	 * to swap it out at runtime.
    664 	 *
    665 	 * @since 3.4.0
    666 	 */
    667 	public function start_previewing_theme() {
    668 		// Bail if we're already previewing.
    669 		if ( $this->is_preview() ) {
    670 			return;
    671 		}
    672 
    673 		$this->previewing = true;
    674 
    675 		if ( ! $this->is_theme_active() ) {
    676 			add_filter( 'template', array( $this, 'get_template' ) );
    677 			add_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
    678 			add_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
    679 
    680 			// @link: https://core.trac.wordpress.org/ticket/20027
    681 			add_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
    682 			add_filter( 'pre_option_template', array( $this, 'get_template' ) );
    683 
    684 			// Handle custom theme roots.
    685 			add_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
    686 			add_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
    687 		}
    688 
    689 		/**
    690 		 * Fires once the Customizer theme preview has started.
    691 		 *
    692 		 * @since 3.4.0
    693 		 *
    694 		 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
    695 		 */
    696 		do_action( 'start_previewing_theme', $this );
    697 	}
    698 
    699 	/**
    700 	 * Stop previewing the selected theme.
    701 	 *
    702 	 * Removes filters to change the current theme.
    703 	 *
    704 	 * @since 3.4.0
    705 	 */
    706 	public function stop_previewing_theme() {
    707 		if ( ! $this->is_preview() ) {
    708 			return;
    709 		}
    710 
    711 		$this->previewing = false;
    712 
    713 		if ( ! $this->is_theme_active() ) {
    714 			remove_filter( 'template', array( $this, 'get_template' ) );
    715 			remove_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
    716 			remove_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
    717 
    718 			// @link: https://core.trac.wordpress.org/ticket/20027
    719 			remove_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
    720 			remove_filter( 'pre_option_template', array( $this, 'get_template' ) );
    721 
    722 			// Handle custom theme roots.
    723 			remove_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
    724 			remove_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
    725 		}
    726 
    727 		/**
    728 		 * Fires once the Customizer theme preview has stopped.
    729 		 *
    730 		 * @since 3.4.0
    731 		 *
    732 		 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
    733 		 */
    734 		do_action( 'stop_previewing_theme', $this );
    735 	}
    736 
    737 	/**
    738 	 * Gets whether settings are or will be previewed.
    739 	 *
    740 	 * @since 4.9.0
    741 	 *
    742 	 * @see WP_Customize_Setting::preview()
    743 	 *
    744 	 * @return bool
    745 	 */
    746 	public function settings_previewed() {
    747 		return $this->settings_previewed;
    748 	}
    749 
    750 	/**
    751 	 * Gets whether data from a changeset's autosaved revision should be loaded if it exists.
    752 	 *
    753 	 * @since 4.9.0
    754 	 *
    755 	 * @see WP_Customize_Manager::changeset_data()
    756 	 *
    757 	 * @return bool Is using autosaved changeset revision.
    758 	 */
    759 	public function autosaved() {
    760 		return $this->autosaved;
    761 	}
    762 
    763 	/**
    764 	 * Whether the changeset branching is allowed.
    765 	 *
    766 	 * @since 4.9.0
    767 	 *
    768 	 * @see WP_Customize_Manager::establish_loaded_changeset()
    769 	 *
    770 	 * @return bool Is changeset branching.
    771 	 */
    772 	public function branching() {
    773 
    774 		/**
    775 		 * Filters whether or not changeset branching is allowed.
    776 		 *
    777 		 * By default in core, when changeset branching is not allowed, changesets will operate
    778 		 * linearly in that only one saved changeset will exist at a time (with a 'draft' or
    779 		 * 'future' status). This makes the Customizer operate in a way that is similar to going to
    780 		 * "edit" to one existing post: all users will be making changes to the same post, and autosave
    781 		 * revisions will be made for that post.
    782 		 *
    783 		 * By contrast, when changeset branching is allowed, then the model is like users going
    784 		 * to "add new" for a page and each user makes changes independently of each other since
    785 		 * they are all operating on their own separate pages, each getting their own separate
    786 		 * initial auto-drafts and then once initially saved, autosave revisions on top of that
    787 		 * user's specific post.
    788 		 *
    789 		 * Since linear changesets are deemed to be more suitable for the majority of WordPress users,
    790 		 * they are the default. For WordPress sites that have heavy site management in the Customizer
    791 		 * by multiple users then branching changesets should be enabled by means of this filter.
    792 		 *
    793 		 * @since 4.9.0
    794 		 *
    795 		 * @param bool                 $allow_branching Whether branching is allowed. If `false`, the default,
    796 		 *                                              then only one saved changeset exists at a time.
    797 		 * @param WP_Customize_Manager $wp_customize    Manager instance.
    798 		 */
    799 		$this->branching = apply_filters( 'customize_changeset_branching', $this->branching, $this );
    800 
    801 		return $this->branching;
    802 	}
    803 
    804 	/**
    805 	 * Get the changeset UUID.
    806 	 *
    807 	 * @since 4.7.0
    808 	 *
    809 	 * @see WP_Customize_Manager::establish_loaded_changeset()
    810 	 *
    811 	 * @return string UUID.
    812 	 */
    813 	public function changeset_uuid() {
    814 		if ( empty( $this->_changeset_uuid ) ) {
    815 			$this->establish_loaded_changeset();
    816 		}
    817 		return $this->_changeset_uuid;
    818 	}
    819 
    820 	/**
    821 	 * Get the theme being customized.
    822 	 *
    823 	 * @since 3.4.0
    824 	 *
    825 	 * @return WP_Theme
    826 	 */
    827 	public function theme() {
    828 		if ( ! $this->theme ) {
    829 			$this->theme = wp_get_theme();
    830 		}
    831 		return $this->theme;
    832 	}
    833 
    834 	/**
    835 	 * Get the registered settings.
    836 	 *
    837 	 * @since 3.4.0
    838 	 *
    839 	 * @return array
    840 	 */
    841 	public function settings() {
    842 		return $this->settings;
    843 	}
    844 
    845 	/**
    846 	 * Get the registered controls.
    847 	 *
    848 	 * @since 3.4.0
    849 	 *
    850 	 * @return array
    851 	 */
    852 	public function controls() {
    853 		return $this->controls;
    854 	}
    855 
    856 	/**
    857 	 * Get the registered containers.
    858 	 *
    859 	 * @since 4.0.0
    860 	 *
    861 	 * @return array
    862 	 */
    863 	public function containers() {
    864 		return $this->containers;
    865 	}
    866 
    867 	/**
    868 	 * Get the registered sections.
    869 	 *
    870 	 * @since 3.4.0
    871 	 *
    872 	 * @return array
    873 	 */
    874 	public function sections() {
    875 		return $this->sections;
    876 	}
    877 
    878 	/**
    879 	 * Get the registered panels.
    880 	 *
    881 	 * @since 4.0.0
    882 	 *
    883 	 * @return array Panels.
    884 	 */
    885 	public function panels() {
    886 		return $this->panels;
    887 	}
    888 
    889 	/**
    890 	 * Checks if the current theme is active.
    891 	 *
    892 	 * @since 3.4.0
    893 	 *
    894 	 * @return bool
    895 	 */
    896 	public function is_theme_active() {
    897 		return $this->get_stylesheet() === $this->original_stylesheet;
    898 	}
    899 
    900 	/**
    901 	 * Register styles/scripts and initialize the preview of each setting
    902 	 *
    903 	 * @since 3.4.0
    904 	 */
    905 	public function wp_loaded() {
    906 
    907 		// Unconditionally register core types for panels, sections, and controls
    908 		// in case plugin unhooks all customize_register actions.
    909 		$this->register_panel_type( 'WP_Customize_Panel' );
    910 		$this->register_panel_type( 'WP_Customize_Themes_Panel' );
    911 		$this->register_section_type( 'WP_Customize_Section' );
    912 		$this->register_section_type( 'WP_Customize_Sidebar_Section' );
    913 		$this->register_section_type( 'WP_Customize_Themes_Section' );
    914 		$this->register_control_type( 'WP_Customize_Color_Control' );
    915 		$this->register_control_type( 'WP_Customize_Media_Control' );
    916 		$this->register_control_type( 'WP_Customize_Upload_Control' );
    917 		$this->register_control_type( 'WP_Customize_Image_Control' );
    918 		$this->register_control_type( 'WP_Customize_Background_Image_Control' );
    919 		$this->register_control_type( 'WP_Customize_Background_Position_Control' );
    920 		$this->register_control_type( 'WP_Customize_Cropped_Image_Control' );
    921 		$this->register_control_type( 'WP_Customize_Site_Icon_Control' );
    922 		$this->register_control_type( 'WP_Customize_Theme_Control' );
    923 		$this->register_control_type( 'WP_Customize_Code_Editor_Control' );
    924 		$this->register_control_type( 'WP_Customize_Date_Time_Control' );
    925 
    926 		/**
    927 		 * Fires once WordPress has loaded, allowing scripts and styles to be initialized.
    928 		 *
    929 		 * @since 3.4.0
    930 		 *
    931 		 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
    932 		 */
    933 		do_action( 'customize_register', $this );
    934 
    935 		if ( $this->settings_previewed() ) {
    936 			foreach ( $this->settings as $setting ) {
    937 				$setting->preview();
    938 			}
    939 		}
    940 
    941 		if ( $this->is_preview() && ! is_admin() ) {
    942 			$this->customize_preview_init();
    943 		}
    944 	}
    945 
    946 	/**
    947 	 * Prevents Ajax requests from following redirects when previewing a theme
    948 	 * by issuing a 200 response instead of a 30x.
    949 	 *
    950 	 * Instead, the JS will sniff out the location header.
    951 	 *
    952 	 * @since 3.4.0
    953 	 * @deprecated 4.7.0
    954 	 *
    955 	 * @param int $status Status.
    956 	 * @return int
    957 	 */
    958 	public function wp_redirect_status( $status ) {
    959 		_deprecated_function( __FUNCTION__, '4.7.0' );
    960 
    961 		if ( $this->is_preview() && ! is_admin() ) {
    962 			return 200;
    963 		}
    964 
    965 		return $status;
    966 	}
    967 
    968 	/**
    969 	 * Find the changeset post ID for a given changeset UUID.
    970 	 *
    971 	 * @since 4.7.0
    972 	 *
    973 	 * @param string $uuid Changeset UUID.
    974 	 * @return int|null Returns post ID on success and null on failure.
    975 	 */
    976 	public function find_changeset_post_id( $uuid ) {
    977 		$cache_group       = 'customize_changeset_post';
    978 		$changeset_post_id = wp_cache_get( $uuid, $cache_group );
    979 		if ( $changeset_post_id && 'customize_changeset' === get_post_type( $changeset_post_id ) ) {
    980 			return $changeset_post_id;
    981 		}
    982 
    983 		$changeset_post_query = new WP_Query(
    984 			array(
    985 				'post_type'              => 'customize_changeset',
    986 				'post_status'            => get_post_stati(),
    987 				'name'                   => $uuid,
    988 				'posts_per_page'         => 1,
    989 				'no_found_rows'          => true,
    990 				'cache_results'          => true,
    991 				'update_post_meta_cache' => false,
    992 				'update_post_term_cache' => false,
    993 				'lazy_load_term_meta'    => false,
    994 			)
    995 		);
    996 		if ( ! empty( $changeset_post_query->posts ) ) {
    997 			// Note: 'fields'=>'ids' is not being used in order to cache the post object as it will be needed.
    998 			$changeset_post_id = $changeset_post_query->posts[0]->ID;
    999 			wp_cache_set( $uuid, $changeset_post_id, $cache_group );
   1000 			return $changeset_post_id;
   1001 		}
   1002 
   1003 		return null;
   1004 	}
   1005 
   1006 	/**
   1007 	 * Get changeset posts.
   1008 	 *
   1009 	 * @since 4.9.0
   1010 	 *
   1011 	 * @param array $args {
   1012 	 *     Args to pass into `get_posts()` to query changesets.
   1013 	 *
   1014 	 *     @type int    $posts_per_page             Number of posts to return. Defaults to -1 (all posts).
   1015 	 *     @type int    $author                     Post author. Defaults to current user.
   1016 	 *     @type string $post_status                Status of changeset. Defaults to 'auto-draft'.
   1017 	 *     @type bool   $exclude_restore_dismissed  Whether to exclude changeset auto-drafts that have been dismissed. Defaults to true.
   1018 	 * }
   1019 	 * @return WP_Post[] Auto-draft changesets.
   1020 	 */
   1021 	protected function get_changeset_posts( $args = array() ) {
   1022 		$default_args = array(
   1023 			'exclude_restore_dismissed' => true,
   1024 			'posts_per_page'            => -1,
   1025 			'post_type'                 => 'customize_changeset',
   1026 			'post_status'               => 'auto-draft',
   1027 			'order'                     => 'DESC',
   1028 			'orderby'                   => 'date',
   1029 			'no_found_rows'             => true,
   1030 			'cache_results'             => true,
   1031 			'update_post_meta_cache'    => false,
   1032 			'update_post_term_cache'    => false,
   1033 			'lazy_load_term_meta'       => false,
   1034 		);
   1035 		if ( get_current_user_id() ) {
   1036 			$default_args['author'] = get_current_user_id();
   1037 		}
   1038 		$args = array_merge( $default_args, $args );
   1039 
   1040 		if ( ! empty( $args['exclude_restore_dismissed'] ) ) {
   1041 			unset( $args['exclude_restore_dismissed'] );
   1042 			$args['meta_query'] = array(
   1043 				array(
   1044 					'key'     => '_customize_restore_dismissed',
   1045 					'compare' => 'NOT EXISTS',
   1046 				),
   1047 			);
   1048 		}
   1049 
   1050 		return get_posts( $args );
   1051 	}
   1052 
   1053 	/**
   1054 	 * Dismiss all of the current user's auto-drafts (other than the present one).
   1055 	 *
   1056 	 * @since 4.9.0
   1057 	 * @return int The number of auto-drafts that were dismissed.
   1058 	 */
   1059 	protected function dismiss_user_auto_draft_changesets() {
   1060 		$changeset_autodraft_posts = $this->get_changeset_posts(
   1061 			array(
   1062 				'post_status'               => 'auto-draft',
   1063 				'exclude_restore_dismissed' => true,
   1064 				'posts_per_page'            => -1,
   1065 			)
   1066 		);
   1067 		$dismissed                 = 0;
   1068 		foreach ( $changeset_autodraft_posts as $autosave_autodraft_post ) {
   1069 			if ( $autosave_autodraft_post->ID === $this->changeset_post_id() ) {
   1070 				continue;
   1071 			}
   1072 			if ( update_post_meta( $autosave_autodraft_post->ID, '_customize_restore_dismissed', true ) ) {
   1073 				$dismissed++;
   1074 			}
   1075 		}
   1076 		return $dismissed;
   1077 	}
   1078 
   1079 	/**
   1080 	 * Get the changeset post ID for the loaded changeset.
   1081 	 *
   1082 	 * @since 4.7.0
   1083 	 *
   1084 	 * @return int|null Post ID on success or null if there is no post yet saved.
   1085 	 */
   1086 	public function changeset_post_id() {
   1087 		if ( ! isset( $this->_changeset_post_id ) ) {
   1088 			$post_id = $this->find_changeset_post_id( $this->changeset_uuid() );
   1089 			if ( ! $post_id ) {
   1090 				$post_id = false;
   1091 			}
   1092 			$this->_changeset_post_id = $post_id;
   1093 		}
   1094 		if ( false === $this->_changeset_post_id ) {
   1095 			return null;
   1096 		}
   1097 		return $this->_changeset_post_id;
   1098 	}
   1099 
   1100 	/**
   1101 	 * Get the data stored in a changeset post.
   1102 	 *
   1103 	 * @since 4.7.0
   1104 	 *
   1105 	 * @param int $post_id Changeset post ID.
   1106 	 * @return array|WP_Error Changeset data or WP_Error on error.
   1107 	 */
   1108 	protected function get_changeset_post_data( $post_id ) {
   1109 		if ( ! $post_id ) {
   1110 			return new WP_Error( 'empty_post_id' );
   1111 		}
   1112 		$changeset_post = get_post( $post_id );
   1113 		if ( ! $changeset_post ) {
   1114 			return new WP_Error( 'missing_post' );
   1115 		}
   1116 		if ( 'revision' === $changeset_post->post_type ) {
   1117 			if ( 'customize_changeset' !== get_post_type( $changeset_post->post_parent ) ) {
   1118 				return new WP_Error( 'wrong_post_type' );
   1119 			}
   1120 		} elseif ( 'customize_changeset' !== $changeset_post->post_type ) {
   1121 			return new WP_Error( 'wrong_post_type' );
   1122 		}
   1123 		$changeset_data = json_decode( $changeset_post->post_content, true );
   1124 		$last_error     = json_last_error();
   1125 		if ( $last_error ) {
   1126 			return new WP_Error( 'json_parse_error', '', $last_error );
   1127 		}
   1128 		if ( ! is_array( $changeset_data ) ) {
   1129 			return new WP_Error( 'expected_array' );
   1130 		}
   1131 		return $changeset_data;
   1132 	}
   1133 
   1134 	/**
   1135 	 * Get changeset data.
   1136 	 *
   1137 	 * @since 4.7.0
   1138 	 * @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.
   1139 	 *
   1140 	 * @return array Changeset data.
   1141 	 */
   1142 	public function changeset_data() {
   1143 		if ( isset( $this->_changeset_data ) ) {
   1144 			return $this->_changeset_data;
   1145 		}
   1146 		$changeset_post_id = $this->changeset_post_id();
   1147 		if ( ! $changeset_post_id ) {
   1148 			$this->_changeset_data = array();
   1149 		} else {
   1150 			if ( $this->autosaved() && is_user_logged_in() ) {
   1151 				$autosave_post = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
   1152 				if ( $autosave_post ) {
   1153 					$data = $this->get_changeset_post_data( $autosave_post->ID );
   1154 					if ( ! is_wp_error( $data ) ) {
   1155 						$this->_changeset_data = $data;
   1156 					}
   1157 				}
   1158 			}
   1159 
   1160 			// Load data from the changeset if it was not loaded from an autosave.
   1161 			if ( ! isset( $this->_changeset_data ) ) {
   1162 				$data = $this->get_changeset_post_data( $changeset_post_id );
   1163 				if ( ! is_wp_error( $data ) ) {
   1164 					$this->_changeset_data = $data;
   1165 				} else {
   1166 					$this->_changeset_data = array();
   1167 				}
   1168 			}
   1169 		}
   1170 		return $this->_changeset_data;
   1171 	}
   1172 
   1173 	/**
   1174 	 * Starter content setting IDs.
   1175 	 *
   1176 	 * @since 4.7.0
   1177 	 * @var array
   1178 	 */
   1179 	protected $pending_starter_content_settings_ids = array();
   1180 
   1181 	/**
   1182 	 * Import theme starter content into the customized state.
   1183 	 *
   1184 	 * @since 4.7.0
   1185 	 *
   1186 	 * @param array $starter_content Starter content. Defaults to `get_theme_starter_content()`.
   1187 	 */
   1188 	function import_theme_starter_content( $starter_content = array() ) {
   1189 		if ( empty( $starter_content ) ) {
   1190 			$starter_content = get_theme_starter_content();
   1191 		}
   1192 
   1193 		$changeset_data = array();
   1194 		if ( $this->changeset_post_id() ) {
   1195 			/*
   1196 			 * Don't re-import starter content into a changeset saved persistently.
   1197 			 * This will need to be revisited in the future once theme switching
   1198 			 * is allowed with drafted/scheduled changesets, since switching to
   1199 			 * another theme could result in more starter content being applied.
   1200 			 * However, when doing an explicit save it is currently possible for
   1201 			 * nav menus and nav menu items specifically to lose their starter_content
   1202 			 * flags, thus resulting in duplicates being created since they fail
   1203 			 * to get re-used. See #40146.
   1204 			 */
   1205 			if ( 'auto-draft' !== get_post_status( $this->changeset_post_id() ) ) {
   1206 				return;
   1207 			}
   1208 
   1209 			$changeset_data = $this->get_changeset_post_data( $this->changeset_post_id() );
   1210 		}
   1211 
   1212 		$sidebars_widgets = isset( $starter_content['widgets'] ) && ! empty( $this->widgets ) ? $starter_content['widgets'] : array();
   1213 		$attachments      = isset( $starter_content['attachments'] ) && ! empty( $this->nav_menus ) ? $starter_content['attachments'] : array();
   1214 		$posts            = isset( $starter_content['posts'] ) && ! empty( $this->nav_menus ) ? $starter_content['posts'] : array();
   1215 		$options          = isset( $starter_content['options'] ) ? $starter_content['options'] : array();
   1216 		$nav_menus        = isset( $starter_content['nav_menus'] ) && ! empty( $this->nav_menus ) ? $starter_content['nav_menus'] : array();
   1217 		$theme_mods       = isset( $starter_content['theme_mods'] ) ? $starter_content['theme_mods'] : array();
   1218 
   1219 		// Widgets.
   1220 		$max_widget_numbers = array();
   1221 		foreach ( $sidebars_widgets as $sidebar_id => $widgets ) {
   1222 			$sidebar_widget_ids = array();
   1223 			foreach ( $widgets as $widget ) {
   1224 				list( $id_base, $instance ) = $widget;
   1225 
   1226 				if ( ! isset( $max_widget_numbers[ $id_base ] ) ) {
   1227 
   1228 					// When $settings is an array-like object, get an intrinsic array for use with array_keys().
   1229 					$settings = get_option( "widget_{$id_base}", array() );
   1230 					if ( $settings instanceof ArrayObject || $settings instanceof ArrayIterator ) {
   1231 						$settings = $settings->getArrayCopy();
   1232 					}
   1233 
   1234 					unset( $settings['_multiwidget'] );
   1235 
   1236 					// Find the max widget number for this type.
   1237 					$widget_numbers = array_keys( $settings );
   1238 					if ( count( $widget_numbers ) > 0 ) {
   1239 						$widget_numbers[]               = 1;
   1240 						$max_widget_numbers[ $id_base ] = max( ...$widget_numbers );
   1241 					} else {
   1242 						$max_widget_numbers[ $id_base ] = 1;
   1243 					}
   1244 				}
   1245 				$max_widget_numbers[ $id_base ] += 1;
   1246 
   1247 				$widget_id  = sprintf( '%s-%d', $id_base, $max_widget_numbers[ $id_base ] );
   1248 				$setting_id = sprintf( 'widget_%s[%d]', $id_base, $max_widget_numbers[ $id_base ] );
   1249 
   1250 				$setting_value = $this->widgets->sanitize_widget_js_instance( $instance );
   1251 				if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
   1252 					$this->set_post_value( $setting_id, $setting_value );
   1253 					$this->pending_starter_content_settings_ids[] = $setting_id;
   1254 				}
   1255 				$sidebar_widget_ids[] = $widget_id;
   1256 			}
   1257 
   1258 			$setting_id = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
   1259 			if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
   1260 				$this->set_post_value( $setting_id, $sidebar_widget_ids );
   1261 				$this->pending_starter_content_settings_ids[] = $setting_id;
   1262 			}
   1263 		}
   1264 
   1265 		$starter_content_auto_draft_post_ids = array();
   1266 		if ( ! empty( $changeset_data['nav_menus_created_posts']['value'] ) ) {
   1267 			$starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, $changeset_data['nav_menus_created_posts']['value'] );
   1268 		}
   1269 
   1270 		// Make an index of all the posts needed and what their slugs are.
   1271 		$needed_posts = array();
   1272 		$attachments  = $this->prepare_starter_content_attachments( $attachments );
   1273 		foreach ( $attachments as $attachment ) {
   1274 			$key                  = 'attachment:' . $attachment['post_name'];
   1275 			$needed_posts[ $key ] = true;
   1276 		}
   1277 		foreach ( array_keys( $posts ) as $post_symbol ) {
   1278 			if ( empty( $posts[ $post_symbol ]['post_name'] ) && empty( $posts[ $post_symbol ]['post_title'] ) ) {
   1279 				unset( $posts[ $post_symbol ] );
   1280 				continue;
   1281 			}
   1282 			if ( empty( $posts[ $post_symbol ]['post_name'] ) ) {
   1283 				$posts[ $post_symbol ]['post_name'] = sanitize_title( $posts[ $post_symbol ]['post_title'] );
   1284 			}
   1285 			if ( empty( $posts[ $post_symbol ]['post_type'] ) ) {
   1286 				$posts[ $post_symbol ]['post_type'] = 'post';
   1287 			}
   1288 			$needed_posts[ $posts[ $post_symbol ]['post_type'] . ':' . $posts[ $post_symbol ]['post_name'] ] = true;
   1289 		}
   1290 		$all_post_slugs = array_merge(
   1291 			wp_list_pluck( $attachments, 'post_name' ),
   1292 			wp_list_pluck( $posts, 'post_name' )
   1293 		);
   1294 
   1295 		/*
   1296 		 * Obtain all post types referenced in starter content to use in query.
   1297 		 * This is needed because 'any' will not account for post types not yet registered.
   1298 		 */
   1299 		$post_types = array_filter( array_merge( array( 'attachment' ), wp_list_pluck( $posts, 'post_type' ) ) );
   1300 
   1301 		// Re-use auto-draft starter content posts referenced in the current customized state.
   1302 		$existing_starter_content_posts = array();
   1303 		if ( ! empty( $starter_content_auto_draft_post_ids ) ) {
   1304 			$existing_posts_query = new WP_Query(
   1305 				array(
   1306 					'post__in'       => $starter_content_auto_draft_post_ids,
   1307 					'post_status'    => 'auto-draft',
   1308 					'post_type'      => $post_types,
   1309 					'posts_per_page' => -1,
   1310 				)
   1311 			);
   1312 			foreach ( $existing_posts_query->posts as $existing_post ) {
   1313 				$post_name = $existing_post->post_name;
   1314 				if ( empty( $post_name ) ) {
   1315 					$post_name = get_post_meta( $existing_post->ID, '_customize_draft_post_name', true );
   1316 				}
   1317 				$existing_starter_content_posts[ $existing_post->post_type . ':' . $post_name ] = $existing_post;
   1318 			}
   1319 		}
   1320 
   1321 		// Re-use non-auto-draft posts.
   1322 		if ( ! empty( $all_post_slugs ) ) {
   1323 			$existing_posts_query = new WP_Query(
   1324 				array(
   1325 					'post_name__in'  => $all_post_slugs,
   1326 					'post_status'    => array_diff( get_post_stati(), array( 'auto-draft' ) ),
   1327 					'post_type'      => 'any',
   1328 					'posts_per_page' => -1,
   1329 				)
   1330 			);
   1331 			foreach ( $existing_posts_query->posts as $existing_post ) {
   1332 				$key = $existing_post->post_type . ':' . $existing_post->post_name;
   1333 				if ( isset( $needed_posts[ $key ] ) && ! isset( $existing_starter_content_posts[ $key ] ) ) {
   1334 					$existing_starter_content_posts[ $key ] = $existing_post;
   1335 				}
   1336 			}
   1337 		}
   1338 
   1339 		// Attachments are technically posts but handled differently.
   1340 		if ( ! empty( $attachments ) ) {
   1341 
   1342 			$attachment_ids = array();
   1343 
   1344 			foreach ( $attachments as $symbol => $attachment ) {
   1345 				$file_array    = array(
   1346 					'name' => $attachment['file_name'],
   1347 				);
   1348 				$file_path     = $attachment['file_path'];
   1349 				$attachment_id = null;
   1350 				$attached_file = null;
   1351 				if ( isset( $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ] ) ) {
   1352 					$attachment_post = $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ];
   1353 					$attachment_id   = $attachment_post->ID;
   1354 					$attached_file   = get_attached_file( $attachment_id );
   1355 					if ( empty( $attached_file ) || ! file_exists( $attached_file ) ) {
   1356 						$attachment_id = null;
   1357 						$attached_file = null;
   1358 					} elseif ( $this->get_stylesheet() !== get_post_meta( $attachment_post->ID, '_starter_content_theme', true ) ) {
   1359 
   1360 						// Re-generate attachment metadata since it was previously generated for a different theme.
   1361 						$metadata = wp_generate_attachment_metadata( $attachment_post->ID, $attached_file );
   1362 						wp_update_attachment_metadata( $attachment_id, $metadata );
   1363 						update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
   1364 					}
   1365 				}
   1366 
   1367 				// Insert the attachment auto-draft because it doesn't yet exist or the attached file is gone.
   1368 				if ( ! $attachment_id ) {
   1369 
   1370 					// Copy file to temp location so that original file won't get deleted from theme after sideloading.
   1371 					$temp_file_name = wp_tempnam( wp_basename( $file_path ) );
   1372 					if ( $temp_file_name && copy( $file_path, $temp_file_name ) ) {
   1373 						$file_array['tmp_name'] = $temp_file_name;
   1374 					}
   1375 					if ( empty( $file_array['tmp_name'] ) ) {
   1376 						continue;
   1377 					}
   1378 
   1379 					$attachment_post_data = array_merge(
   1380 						wp_array_slice_assoc( $attachment, array( 'post_title', 'post_content', 'post_excerpt' ) ),
   1381 						array(
   1382 							'post_status' => 'auto-draft', // So attachment will be garbage collected in a week if changeset is never published.
   1383 						)
   1384 					);
   1385 
   1386 					$attachment_id = media_handle_sideload( $file_array, 0, null, $attachment_post_data );
   1387 					if ( is_wp_error( $attachment_id ) ) {
   1388 						continue;
   1389 					}
   1390 					update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
   1391 					update_post_meta( $attachment_id, '_customize_draft_post_name', $attachment['post_name'] );
   1392 				}
   1393 
   1394 				$attachment_ids[ $symbol ] = $attachment_id;
   1395 			}
   1396 			$starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, array_values( $attachment_ids ) );
   1397 		}
   1398 
   1399 		// Posts & pages.
   1400 		if ( ! empty( $posts ) ) {
   1401 			foreach ( array_keys( $posts ) as $post_symbol ) {
   1402 				if ( empty( $posts[ $post_symbol ]['post_type'] ) || empty( $posts[ $post_symbol ]['post_name'] ) ) {
   1403 					continue;
   1404 				}
   1405 				$post_type = $posts[ $post_symbol ]['post_type'];
   1406 				if ( ! empty( $posts[ $post_symbol ]['post_name'] ) ) {
   1407 					$post_name = $posts[ $post_symbol ]['post_name'];
   1408 				} elseif ( ! empty( $posts[ $post_symbol ]['post_title'] ) ) {
   1409 					$post_name = sanitize_title( $posts[ $post_symbol ]['post_title'] );
   1410 				} else {
   1411 					continue;
   1412 				}
   1413 
   1414 				// Use existing auto-draft post if one already exists with the same type and name.
   1415 				if ( isset( $existing_starter_content_posts[ $post_type . ':' . $post_name ] ) ) {
   1416 					$posts[ $post_symbol ]['ID'] = $existing_starter_content_posts[ $post_type . ':' . $post_name ]->ID;
   1417 					continue;
   1418 				}
   1419 
   1420 				// Translate the featured image symbol.
   1421 				if ( ! empty( $posts[ $post_symbol ]['thumbnail'] )
   1422 					&& preg_match( '/^{{(?P<symbol>.+)}}$/', $posts[ $post_symbol ]['thumbnail'], $matches )
   1423 					&& isset( $attachment_ids[ $matches['symbol'] ] ) ) {
   1424 					$posts[ $post_symbol ]['meta_input']['_thumbnail_id'] = $attachment_ids[ $matches['symbol'] ];
   1425 				}
   1426 
   1427 				if ( ! empty( $posts[ $post_symbol ]['template'] ) ) {
   1428 					$posts[ $post_symbol ]['meta_input']['_wp_page_template'] = $posts[ $post_symbol ]['template'];
   1429 				}
   1430 
   1431 				$r = $this->nav_menus->insert_auto_draft_post( $posts[ $post_symbol ] );
   1432 				if ( $r instanceof WP_Post ) {
   1433 					$posts[ $post_symbol ]['ID'] = $r->ID;
   1434 				}
   1435 			}
   1436 
   1437 			$starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, wp_list_pluck( $posts, 'ID' ) );
   1438 		}
   1439 
   1440 		// The nav_menus_created_posts setting is why nav_menus component is dependency for adding posts.
   1441 		if ( ! empty( $this->nav_menus ) && ! empty( $starter_content_auto_draft_post_ids ) ) {
   1442 			$setting_id = 'nav_menus_created_posts';
   1443 			$this->set_post_value( $setting_id, array_unique( array_values( $starter_content_auto_draft_post_ids ) ) );
   1444 			$this->pending_starter_content_settings_ids[] = $setting_id;
   1445 		}
   1446 
   1447 		// Nav menus.
   1448 		$placeholder_id              = -1;
   1449 		$reused_nav_menu_setting_ids = array();
   1450 		foreach ( $nav_menus as $nav_menu_location => $nav_menu ) {
   1451 
   1452 			$nav_menu_term_id    = null;
   1453 			$nav_menu_setting_id = null;
   1454 			$matches             = array();
   1455 
   1456 			// Look for an existing placeholder menu with starter content to re-use.
   1457 			foreach ( $changeset_data as $setting_id => $setting_params ) {
   1458 				$can_reuse = (
   1459 					! empty( $setting_params['starter_content'] )
   1460 					&&
   1461 					! in_array( $setting_id, $reused_nav_menu_setting_ids, true )
   1462 					&&
   1463 					preg_match( '#^nav_menu\[(?P<nav_menu_id>-?\d+)\]$#', $setting_id, $matches )
   1464 				);
   1465 				if ( $can_reuse ) {
   1466 					$nav_menu_term_id              = (int) $matches['nav_menu_id'];
   1467 					$nav_menu_setting_id           = $setting_id;
   1468 					$reused_nav_menu_setting_ids[] = $setting_id;
   1469 					break;
   1470 				}
   1471 			}
   1472 
   1473 			if ( ! $nav_menu_term_id ) {
   1474 				while ( isset( $changeset_data[ sprintf( 'nav_menu[%d]', $placeholder_id ) ] ) ) {
   1475 					$placeholder_id--;
   1476 				}
   1477 				$nav_menu_term_id    = $placeholder_id;
   1478 				$nav_menu_setting_id = sprintf( 'nav_menu[%d]', $placeholder_id );
   1479 			}
   1480 
   1481 			$this->set_post_value(
   1482 				$nav_menu_setting_id,
   1483 				array(
   1484 					'name' => isset( $nav_menu['name'] ) ? $nav_menu['name'] : $nav_menu_location,
   1485 				)
   1486 			);
   1487 			$this->pending_starter_content_settings_ids[] = $nav_menu_setting_id;
   1488 
   1489 			// @todo Add support for menu_item_parent.
   1490 			$position = 0;
   1491 			foreach ( $nav_menu['items'] as $nav_menu_item ) {
   1492 				$nav_menu_item_setting_id = sprintf( 'nav_menu_item[%d]', $placeholder_id-- );
   1493 				if ( ! isset( $nav_menu_item['position'] ) ) {
   1494 					$nav_menu_item['position'] = $position++;
   1495 				}
   1496 				$nav_menu_item['nav_menu_term_id'] = $nav_menu_term_id;
   1497 
   1498 				if ( isset( $nav_menu_item['object_id'] ) ) {
   1499 					if ( 'post_type' === $nav_menu_item['type'] && preg_match( '/^{{(?P<symbol>.+)}}$/', $nav_menu_item['object_id'], $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
   1500 						$nav_menu_item['object_id'] = $posts[ $matches['symbol'] ]['ID'];
   1501 						if ( empty( $nav_menu_item['title'] ) ) {
   1502 							$original_object        = get_post( $nav_menu_item['object_id'] );
   1503 							$nav_menu_item['title'] = $original_object->post_title;
   1504 						}
   1505 					} else {
   1506 						continue;
   1507 					}
   1508 				} else {
   1509 					$nav_menu_item['object_id'] = 0;
   1510 				}
   1511 
   1512 				if ( empty( $changeset_data[ $nav_menu_item_setting_id ] ) || ! empty( $changeset_data[ $nav_menu_item_setting_id ]['starter_content'] ) ) {
   1513 					$this->set_post_value( $nav_menu_item_setting_id, $nav_menu_item );
   1514 					$this->pending_starter_content_settings_ids[] = $nav_menu_item_setting_id;
   1515 				}
   1516 			}
   1517 
   1518 			$setting_id = sprintf( 'nav_menu_locations[%s]', $nav_menu_location );
   1519 			if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
   1520 				$this->set_post_value( $setting_id, $nav_menu_term_id );
   1521 				$this->pending_starter_content_settings_ids[] = $setting_id;
   1522 			}
   1523 		}
   1524 
   1525 		// Options.
   1526 		foreach ( $options as $name => $value ) {
   1527 
   1528 			// Serialize the value to check for post symbols.
   1529 			$value = maybe_serialize( $value );
   1530 
   1531 			if ( is_serialized( $value ) ) {
   1532 				if ( preg_match( '/s:\d+:"{{(?P<symbol>.+)}}"/', $value, $matches ) ) {
   1533 					if ( isset( $posts[ $matches['symbol'] ] ) ) {
   1534 						$symbol_match = $posts[ $matches['symbol'] ]['ID'];
   1535 					} elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
   1536 						$symbol_match = $attachment_ids[ $matches['symbol'] ];
   1537 					}
   1538 
   1539 					// If we have any symbol matches, update the values.
   1540 					if ( isset( $symbol_match ) ) {
   1541 						// Replace found string matches with post IDs.
   1542 						$value = str_replace( $matches[0], "i:{$symbol_match}", $value );
   1543 					} else {
   1544 						continue;
   1545 					}
   1546 				}
   1547 			} elseif ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
   1548 				if ( isset( $posts[ $matches['symbol'] ] ) ) {
   1549 					$value = $posts[ $matches['symbol'] ]['ID'];
   1550 				} elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
   1551 					$value = $attachment_ids[ $matches['symbol'] ];
   1552 				} else {
   1553 					continue;
   1554 				}
   1555 			}
   1556 
   1557 			// Unserialize values after checking for post symbols, so they can be properly referenced.
   1558 			$value = maybe_unserialize( $value );
   1559 
   1560 			if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
   1561 				$this->set_post_value( $name, $value );
   1562 				$this->pending_starter_content_settings_ids[] = $name;
   1563 			}
   1564 		}
   1565 
   1566 		// Theme mods.
   1567 		foreach ( $theme_mods as $name => $value ) {
   1568 
   1569 			// Serialize the value to check for post symbols.
   1570 			$value = maybe_serialize( $value );
   1571 
   1572 			// Check if value was serialized.
   1573 			if ( is_serialized( $value ) ) {
   1574 				if ( preg_match( '/s:\d+:"{{(?P<symbol>.+)}}"/', $value, $matches ) ) {
   1575 					if ( isset( $posts[ $matches['symbol'] ] ) ) {
   1576 						$symbol_match = $posts[ $matches['symbol'] ]['ID'];
   1577 					} elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
   1578 						$symbol_match = $attachment_ids[ $matches['symbol'] ];
   1579 					}
   1580 
   1581 					// If we have any symbol matches, update the values.
   1582 					if ( isset( $symbol_match ) ) {
   1583 						// Replace found string matches with post IDs.
   1584 						$value = str_replace( $matches[0], "i:{$symbol_match}", $value );
   1585 					} else {
   1586 						continue;
   1587 					}
   1588 				}
   1589 			} elseif ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
   1590 				if ( isset( $posts[ $matches['symbol'] ] ) ) {
   1591 					$value = $posts[ $matches['symbol'] ]['ID'];
   1592 				} elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
   1593 					$value = $attachment_ids[ $matches['symbol'] ];
   1594 				} else {
   1595 					continue;
   1596 				}
   1597 			}
   1598 
   1599 			// Unserialize values after checking for post symbols, so they can be properly referenced.
   1600 			$value = maybe_unserialize( $value );
   1601 
   1602 			// Handle header image as special case since setting has a legacy format.
   1603 			if ( 'header_image' === $name ) {
   1604 				$name     = 'header_image_data';
   1605 				$metadata = wp_get_attachment_metadata( $value );
   1606 				if ( empty( $metadata ) ) {
   1607 					continue;
   1608 				}
   1609 				$value = array(
   1610 					'attachment_id' => $value,
   1611 					'url'           => wp_get_attachment_url( $value ),
   1612 					'height'        => $metadata['height'],
   1613 					'width'         => $metadata['width'],
   1614 				);
   1615 			} elseif ( 'background_image' === $name ) {
   1616 				$value = wp_get_attachment_url( $value );
   1617 			}
   1618 
   1619 			if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
   1620 				$this->set_post_value( $name, $value );
   1621 				$this->pending_starter_content_settings_ids[] = $name;
   1622 			}
   1623 		}
   1624 
   1625 		if ( ! empty( $this->pending_starter_content_settings_ids ) ) {
   1626 			if ( did_action( 'customize_register' ) ) {
   1627 				$this->_save_starter_content_changeset();
   1628 			} else {
   1629 				add_action( 'customize_register', array( $this, '_save_starter_content_changeset' ), 1000 );
   1630 			}
   1631 		}
   1632 	}
   1633 
   1634 	/**
   1635 	 * Prepare starter content attachments.
   1636 	 *
   1637 	 * Ensure that the attachments are valid and that they have slugs and file name/path.
   1638 	 *
   1639 	 * @since 4.7.0
   1640 	 *
   1641 	 * @param array $attachments Attachments.
   1642 	 * @return array Prepared attachments.
   1643 	 */
   1644 	protected function prepare_starter_content_attachments( $attachments ) {
   1645 		$prepared_attachments = array();
   1646 		if ( empty( $attachments ) ) {
   1647 			return $prepared_attachments;
   1648 		}
   1649 
   1650 		// Such is The WordPress Way.
   1651 		require_once ABSPATH . 'wp-admin/includes/file.php';
   1652 		require_once ABSPATH . 'wp-admin/includes/media.php';
   1653 		require_once ABSPATH . 'wp-admin/includes/image.php';
   1654 
   1655 		foreach ( $attachments as $symbol => $attachment ) {
   1656 
   1657 			// A file is required and URLs to files are not currently allowed.
   1658 			if ( empty( $attachment['file'] ) || preg_match( '#^https?://$#', $attachment['file'] ) ) {
   1659 				continue;
   1660 			}
   1661 
   1662 			$file_path = null;
   1663 			if ( file_exists( $attachment['file'] ) ) {
   1664 				$file_path = $attachment['file']; // Could be absolute path to file in plugin.
   1665 			} elseif ( is_child_theme() && file_exists( get_stylesheet_directory() . '/' . $attachment['file'] ) ) {
   1666 				$file_path = get_stylesheet_directory() . '/' . $attachment['file'];
   1667 			} elseif ( file_exists( get_template_directory() . '/' . $attachment['file'] ) ) {
   1668 				$file_path = get_template_directory() . '/' . $attachment['file'];
   1669 			} else {
   1670 				continue;
   1671 			}
   1672 			$file_name = wp_basename( $attachment['file'] );
   1673 
   1674 			// Skip file types that are not recognized.
   1675 			$checked_filetype = wp_check_filetype( $file_name );
   1676 			if ( empty( $checked_filetype['type'] ) ) {
   1677 				continue;
   1678 			}
   1679 
   1680 			// Ensure post_name is set since not automatically derived from post_title for new auto-draft posts.
   1681 			if ( empty( $attachment['post_name'] ) ) {
   1682 				if ( ! empty( $attachment['post_title'] ) ) {
   1683 					$attachment['post_name'] = sanitize_title( $attachment['post_title'] );
   1684 				} else {
   1685 					$attachment['post_name'] = sanitize_title( preg_replace( '/\.\w+$/', '', $file_name ) );
   1686 				}
   1687 			}
   1688 
   1689 			$attachment['file_name']         = $file_name;
   1690 			$attachment['file_path']         = $file_path;
   1691 			$prepared_attachments[ $symbol ] = $attachment;
   1692 		}
   1693 		return $prepared_attachments;
   1694 	}
   1695 
   1696 	/**
   1697 	 * Save starter content changeset.
   1698 	 *
   1699 	 * @since 4.7.0
   1700 	 */
   1701 	public function _save_starter_content_changeset() {
   1702 
   1703 		if ( empty( $this->pending_starter_content_settings_ids ) ) {
   1704 			return;
   1705 		}
   1706 
   1707 		$this->save_changeset_post(
   1708 			array(
   1709 				'data'            => array_fill_keys( $this->pending_starter_content_settings_ids, array( 'starter_content' => true ) ),
   1710 				'starter_content' => true,
   1711 			)
   1712 		);
   1713 		$this->saved_starter_content_changeset = true;
   1714 
   1715 		$this->pending_starter_content_settings_ids = array();
   1716 	}
   1717 
   1718 	/**
   1719 	 * Get dirty pre-sanitized setting values in the current customized state.
   1720 	 *
   1721 	 * The returned array consists of a merge of three sources:
   1722 	 * 1. If the theme is not currently active, then the base array is any stashed
   1723 	 *    theme mods that were modified previously but never published.
   1724 	 * 2. The values from the current changeset, if it exists.
   1725 	 * 3. If the user can customize, the values parsed from the incoming
   1726 	 *    `$_POST['customized']` JSON data.
   1727 	 * 4. Any programmatically-set post values via `WP_Customize_Manager::set_post_value()`.
   1728 	 *
   1729 	 * The name "unsanitized_post_values" is a carry-over from when the customized
   1730 	 * state was exclusively sourced from `$_POST['customized']`. Nevertheless,
   1731 	 * the value returned will come from the current changeset post and from the
   1732 	 * incoming post data.
   1733 	 *
   1734 	 * @since 4.1.1
   1735 	 * @since 4.7.0 Added `$args` parameter and merging with changeset values and stashed theme mods.
   1736 	 *
   1737 	 * @param array $args {
   1738 	 *     Args.
   1739 	 *
   1740 	 *     @type bool $exclude_changeset Whether the changeset values should also be excluded. Defaults to false.
   1741 	 *     @type bool $exclude_post_data Whether the post input values should also be excluded. Defaults to false when lacking the customize capability.
   1742 	 * }
   1743 	 * @return array
   1744 	 */
   1745 	public function unsanitized_post_values( $args = array() ) {
   1746 		$args = array_merge(
   1747 			array(
   1748 				'exclude_changeset' => false,
   1749 				'exclude_post_data' => ! current_user_can( 'customize' ),
   1750 			),
   1751 			$args
   1752 		);
   1753 
   1754 		$values = array();
   1755 
   1756 		// Let default values be from the stashed theme mods if doing a theme switch and if no changeset is present.
   1757 		if ( ! $this->is_theme_active() ) {
   1758 			$stashed_theme_mods = get_option( 'customize_stashed_theme_mods' );
   1759 			$stylesheet         = $this->get_stylesheet();
   1760 			if ( isset( $stashed_theme_mods[ $stylesheet ] ) ) {
   1761 				$values = array_merge( $values, wp_list_pluck( $stashed_theme_mods[ $stylesheet ], 'value' ) );
   1762 			}
   1763 		}
   1764 
   1765 		if ( ! $args['exclude_changeset'] ) {
   1766 			foreach ( $this->changeset_data() as $setting_id => $setting_params ) {
   1767 				if ( ! array_key_exists( 'value', $setting_params ) ) {
   1768 					continue;
   1769 				}
   1770 				if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) {
   1771 
   1772 					// Ensure that theme mods values are only used if they were saved under the current theme.
   1773 					$namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
   1774 					if ( preg_match( $namespace_pattern, $setting_id, $matches ) && $this->get_stylesheet() === $matches['stylesheet'] ) {
   1775 						$values[ $matches['setting_id'] ] = $setting_params['value'];
   1776 					}
   1777 				} else {
   1778 					$values[ $setting_id ] = $setting_params['value'];
   1779 				}
   1780 			}
   1781 		}
   1782 
   1783 		if ( ! $args['exclude_post_data'] ) {
   1784 			if ( ! isset( $this->_post_values ) ) {
   1785 				if ( isset( $_POST['customized'] ) ) {
   1786 					$post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
   1787 				} else {
   1788 					$post_values = array();
   1789 				}
   1790 				if ( is_array( $post_values ) ) {
   1791 					$this->_post_values = $post_values;
   1792 				} else {
   1793 					$this->_post_values = array();
   1794 				}
   1795 			}
   1796 			$values = array_merge( $values, $this->_post_values );
   1797 		}
   1798 		return $values;
   1799 	}
   1800 
   1801 	/**
   1802 	 * Returns the sanitized value for a given setting from the current customized state.
   1803 	 *
   1804 	 * The name "post_value" is a carry-over from when the customized state was exclusively
   1805 	 * sourced from `$_POST['customized']`. Nevertheless, the value returned will come
   1806 	 * from the current changeset post and from the incoming post data.
   1807 	 *
   1808 	 * @since 3.4.0
   1809 	 * @since 4.1.1 Introduced the `$default` parameter.
   1810 	 * @since 4.6.0 `$default` is now returned early when the setting post value is invalid.
   1811 	 *
   1812 	 * @see WP_REST_Server::dispatch()
   1813 	 * @see WP_REST_Request::sanitize_params()
   1814 	 * @see WP_REST_Request::has_valid_params()
   1815 	 *
   1816 	 * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object.
   1817 	 * @param mixed                $default Value returned $setting has no post value (added in 4.2.0)
   1818 	 *                                      or the post value is invalid (added in 4.6.0).
   1819 	 * @return string|mixed Sanitized value or the $default provided.
   1820 	 */
   1821 	public function post_value( $setting, $default = null ) {
   1822 		$post_values = $this->unsanitized_post_values();
   1823 		if ( ! array_key_exists( $setting->id, $post_values ) ) {
   1824 			return $default;
   1825 		}
   1826 		$value = $post_values[ $setting->id ];
   1827 		$valid = $setting->validate( $value );
   1828 		if ( is_wp_error( $valid ) ) {
   1829 			return $default;
   1830 		}
   1831 		$value = $setting->sanitize( $value );
   1832 		if ( is_null( $value ) || is_wp_error( $value ) ) {
   1833 			return $default;
   1834 		}
   1835 		return $value;
   1836 	}
   1837 
   1838 	/**
   1839 	 * Override a setting's value in the current customized state.
   1840 	 *
   1841 	 * The name "post_value" is a carry-over from when the customized state was
   1842 	 * exclusively sourced from `$_POST['customized']`.
   1843 	 *
   1844 	 * @since 4.2.0
   1845 	 *
   1846 	 * @param string $setting_id ID for the WP_Customize_Setting instance.
   1847 	 * @param mixed  $value      Post value.
   1848 	 */
   1849 	public function set_post_value( $setting_id, $value ) {
   1850 		$this->unsanitized_post_values(); // Populate _post_values from $_POST['customized'].
   1851 		$this->_post_values[ $setting_id ] = $value;
   1852 
   1853 		/**
   1854 		 * Announce when a specific setting's unsanitized post value has been set.
   1855 		 *
   1856 		 * Fires when the WP_Customize_Manager::set_post_value() method is called.
   1857 		 *
   1858 		 * The dynamic portion of the hook name, `$setting_id`, refers to the setting ID.
   1859 		 *
   1860 		 * @since 4.4.0
   1861 		 *
   1862 		 * @param mixed                $value Unsanitized setting post value.
   1863 		 * @param WP_Customize_Manager $this  WP_Customize_Manager instance.
   1864 		 */
   1865 		do_action( "customize_post_value_set_{$setting_id}", $value, $this );
   1866 
   1867 		/**
   1868 		 * Announce when any setting's unsanitized post value has been set.
   1869 		 *
   1870 		 * Fires when the WP_Customize_Manager::set_post_value() method is called.
   1871 		 *
   1872 		 * This is useful for `WP_Customize_Setting` instances to watch
   1873 		 * in order to update a cached previewed value.
   1874 		 *
   1875 		 * @since 4.4.0
   1876 		 *
   1877 		 * @param string               $setting_id Setting ID.
   1878 		 * @param mixed                $value      Unsanitized setting post value.
   1879 		 * @param WP_Customize_Manager $this       WP_Customize_Manager instance.
   1880 		 */
   1881 		do_action( 'customize_post_value_set', $setting_id, $value, $this );
   1882 	}
   1883 
   1884 	/**
   1885 	 * Print JavaScript settings.
   1886 	 *
   1887 	 * @since 3.4.0
   1888 	 */
   1889 	public function customize_preview_init() {
   1890 
   1891 		/*
   1892 		 * Now that Customizer previews are loaded into iframes via GET requests
   1893 		 * and natural URLs with transaction UUIDs added, we need to ensure that
   1894 		 * the responses are never cached by proxies. In practice, this will not
   1895 		 * be needed if the user is logged-in anyway. But if anonymous access is
   1896 		 * allowed then the auth cookies would not be sent and WordPress would
   1897 		 * not send no-cache headers by default.
   1898 		 */
   1899 		if ( ! headers_sent() ) {
   1900 			nocache_headers();
   1901 			header( 'X-Robots: noindex, nofollow, noarchive' );
   1902 		}
   1903 		add_filter( 'wp_robots', 'wp_robots_no_robots' );
   1904 		add_filter( 'wp_headers', array( $this, 'filter_iframe_security_headers' ) );
   1905 
   1906 		/*
   1907 		 * If preview is being served inside the customizer preview iframe, and
   1908 		 * if the user doesn't have customize capability, then it is assumed
   1909 		 * that the user's session has expired and they need to re-authenticate.
   1910 		 */
   1911 		if ( $this->messenger_channel && ! current_user_can( 'customize' ) ) {
   1912 			$this->wp_die(
   1913 				-1,
   1914 				sprintf(
   1915 					/* translators: %s: customize_messenger_channel */
   1916 					__( 'Unauthorized. You may remove the %s param to preview as frontend.' ),
   1917 					'<code>customize_messenger_channel<code>'
   1918 				)
   1919 			);
   1920 			return;
   1921 		}
   1922 
   1923 		$this->prepare_controls();
   1924 
   1925 		add_filter( 'wp_redirect', array( $this, 'add_state_query_params' ) );
   1926 
   1927 		wp_enqueue_script( 'customize-preview' );
   1928 		wp_enqueue_style( 'customize-preview' );
   1929 		add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) );
   1930 		add_action( 'wp_head', array( $this, 'remove_frameless_preview_messenger_channel' ) );
   1931 		add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 );
   1932 		add_filter( 'get_edit_post_link', '__return_empty_string' );
   1933 
   1934 		/**
   1935 		 * Fires once the Customizer preview has initialized and JavaScript
   1936 		 * settings have been printed.
   1937 		 *
   1938 		 * @since 3.4.0
   1939 		 *
   1940 		 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
   1941 		 */
   1942 		do_action( 'customize_preview_init', $this );
   1943 	}
   1944 
   1945 	/**
   1946 	 * Filters the X-Frame-Options and Content-Security-Policy headers to ensure frontend can load in customizer.
   1947 	 *
   1948 	 * @since 4.7.0
   1949 	 *
   1950 	 * @param array $headers Headers.
   1951 	 * @return array Headers.
   1952 	 */
   1953 	public function filter_iframe_security_headers( $headers ) {
   1954 		$headers['X-Frame-Options']         = 'SAMEORIGIN';
   1955 		$headers['Content-Security-Policy'] = "frame-ancestors 'self'";
   1956 		return $headers;
   1957 	}
   1958 
   1959 	/**
   1960 	 * Add customize state query params to a given URL if preview is allowed.
   1961 	 *
   1962 	 * @since 4.7.0
   1963 	 *
   1964 	 * @see wp_redirect()
   1965 	 * @see WP_Customize_Manager::get_allowed_url()
   1966 	 *
   1967 	 * @param string $url URL.
   1968 	 * @return string URL.
   1969 	 */
   1970 	public function add_state_query_params( $url ) {
   1971 		$parsed_original_url = wp_parse_url( $url );
   1972 		$is_allowed          = false;
   1973 		foreach ( $this->get_allowed_urls() as $allowed_url ) {
   1974 			$parsed_allowed_url = wp_parse_url( $allowed_url );
   1975 			$is_allowed         = (
   1976 				$parsed_allowed_url['scheme'] === $parsed_original_url['scheme']
   1977 				&&
   1978 				$parsed_allowed_url['host'] === $parsed_original_url['host']
   1979 				&&
   1980 				0 === strpos( $parsed_original_url['path'], $parsed_allowed_url['path'] )
   1981 			);
   1982 			if ( $is_allowed ) {
   1983 				break;
   1984 			}
   1985 		}
   1986 
   1987 		if ( $is_allowed ) {
   1988 			$query_params = array(
   1989 				'customize_changeset_uuid' => $this->changeset_uuid(),
   1990 			);
   1991 			if ( ! $this->is_theme_active() ) {
   1992 				$query_params['customize_theme'] = $this->get_stylesheet();
   1993 			}
   1994 			if ( $this->messenger_channel ) {
   1995 				$query_params['customize_messenger_channel'] = $this->messenger_channel;
   1996 			}
   1997 			$url = add_query_arg( $query_params, $url );
   1998 		}
   1999 
   2000 		return $url;
   2001 	}
   2002 
   2003 	/**
   2004 	 * Prevent sending a 404 status when returning the response for the customize
   2005 	 * preview, since it causes the jQuery Ajax to fail. Send 200 instead.
   2006 	 *
   2007 	 * @since 4.0.0
   2008 	 * @deprecated 4.7.0
   2009 	 */
   2010 	public function customize_preview_override_404_status() {
   2011 		_deprecated_function( __METHOD__, '4.7.0' );
   2012 	}
   2013 
   2014 	/**
   2015 	 * Print base element for preview frame.
   2016 	 *
   2017 	 * @since 3.4.0
   2018 	 * @deprecated 4.7.0
   2019 	 */
   2020 	public function customize_preview_base() {
   2021 		_deprecated_function( __METHOD__, '4.7.0' );
   2022 	}
   2023 
   2024 	/**
   2025 	 * Print a workaround to handle HTML5 tags in IE < 9.
   2026 	 *
   2027 	 * @since 3.4.0
   2028 	 * @deprecated 4.7.0 Customizer no longer supports IE8, so all supported browsers recognize HTML5.
   2029 	 */
   2030 	public function customize_preview_html5() {
   2031 		_deprecated_function( __FUNCTION__, '4.7.0' );
   2032 	}
   2033 
   2034 	/**
   2035 	 * Print CSS for loading indicators for the Customizer preview.
   2036 	 *
   2037 	 * @since 4.2.0
   2038 	 */
   2039 	public function customize_preview_loading_style() {
   2040 		?>
   2041 		<style>
   2042 			body.wp-customizer-unloading {
   2043 				opacity: 0.25;
   2044 				cursor: progress !important;
   2045 				-webkit-transition: opacity 0.5s;
   2046 				transition: opacity 0.5s;
   2047 			}
   2048 			body.wp-customizer-unloading * {
   2049 				pointer-events: none !important;
   2050 			}
   2051 			form.customize-unpreviewable,
   2052 			form.customize-unpreviewable input,
   2053 			form.customize-unpreviewable select,
   2054 			form.customize-unpreviewable button,
   2055 			a.customize-unpreviewable,
   2056 			area.customize-unpreviewable {
   2057 				cursor: not-allowed !important;
   2058 			}
   2059 		</style>
   2060 		<?php
   2061 	}
   2062 
   2063 	/**
   2064 	 * Remove customize_messenger_channel query parameter from the preview window when it is not in an iframe.
   2065 	 *
   2066 	 * This ensures that the admin bar will be shown. It also ensures that link navigation will
   2067 	 * work as expected since the parent frame is not being sent the URL to navigate to.
   2068 	 *
   2069 	 * @since 4.7.0
   2070 	 */
   2071 	public function remove_frameless_preview_messenger_channel() {
   2072 		if ( ! $this->messenger_channel ) {
   2073 			return;
   2074 		}
   2075 		?>
   2076 		<script>
   2077 		( function() {
   2078 			var urlParser, oldQueryParams, newQueryParams, i;
   2079 			if ( parent !== window ) {
   2080 				return;
   2081 			}
   2082 			urlParser = document.createElement( 'a' );
   2083 			urlParser.href = location.href;
   2084 			oldQueryParams = urlParser.search.substr( 1 ).split( /&/ );
   2085 			newQueryParams = [];
   2086 			for ( i = 0; i < oldQueryParams.length; i += 1 ) {
   2087 				if ( ! /^customize_messenger_channel=/.test( oldQueryParams[ i ] ) ) {
   2088 					newQueryParams.push( oldQueryParams[ i ] );
   2089 				}
   2090 			}
   2091 			urlParser.search = newQueryParams.join( '&' );
   2092 			if ( urlParser.search !== location.search ) {
   2093 				location.replace( urlParser.href );
   2094 			}
   2095 		} )();
   2096 		</script>
   2097 		<?php
   2098 	}
   2099 
   2100 	/**
   2101 	 * Print JavaScript settings for preview frame.
   2102 	 *
   2103 	 * @since 3.4.0
   2104 	 */
   2105 	public function customize_preview_settings() {
   2106 		$post_values                 = $this->unsanitized_post_values( array( 'exclude_changeset' => true ) );
   2107 		$setting_validities          = $this->validate_setting_values( $post_values );
   2108 		$exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities );
   2109 
   2110 		// Note that the REQUEST_URI is not passed into home_url() since this breaks subdirectory installations.
   2111 		$self_url           = empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
   2112 		$state_query_params = array(
   2113 			'customize_theme',
   2114 			'customize_changeset_uuid',
   2115 			'customize_messenger_channel',
   2116 		);
   2117 		$self_url           = remove_query_arg( $state_query_params, $self_url );
   2118 
   2119 		$allowed_urls  = $this->get_allowed_urls();
   2120 		$allowed_hosts = array();
   2121 		foreach ( $allowed_urls as $allowed_url ) {
   2122 			$parsed = wp_parse_url( $allowed_url );
   2123 			if ( empty( $parsed['host'] ) ) {
   2124 				continue;
   2125 			}
   2126 			$host = $parsed['host'];
   2127 			if ( ! empty( $parsed['port'] ) ) {
   2128 				$host .= ':' . $parsed['port'];
   2129 			}
   2130 			$allowed_hosts[] = $host;
   2131 		}
   2132 
   2133 		$switched_locale = switch_to_locale( get_user_locale() );
   2134 		$l10n            = array(
   2135 			'shiftClickToEdit'  => __( 'Shift-click to edit this element.' ),
   2136 			'linkUnpreviewable' => __( 'This link is not live-previewable.' ),
   2137 			'formUnpreviewable' => __( 'This form is not live-previewable.' ),
   2138 		);
   2139 		if ( $switched_locale ) {
   2140 			restore_previous_locale();
   2141 		}
   2142 
   2143 		$settings = array(
   2144 			'changeset'         => array(
   2145 				'uuid'      => $this->changeset_uuid(),
   2146 				'autosaved' => $this->autosaved(),
   2147 			),
   2148 			'timeouts'          => array(
   2149 				'selectiveRefresh' => 250,
   2150 				'keepAliveSend'    => 1000,
   2151 			),
   2152 			'theme'             => array(
   2153 				'stylesheet' => $this->get_stylesheet(),
   2154 				'active'     => $this->is_theme_active(),
   2155 			),
   2156 			'url'               => array(
   2157 				'self'          => $self_url,
   2158 				'allowed'       => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
   2159 				'allowedHosts'  => array_unique( $allowed_hosts ),
   2160 				'isCrossDomain' => $this->is_cross_domain(),
   2161 			),
   2162 			'channel'           => $this->messenger_channel,
   2163 			'activePanels'      => array(),
   2164 			'activeSections'    => array(),
   2165 			'activeControls'    => array(),
   2166 			'settingValidities' => $exported_setting_validities,
   2167 			'nonce'             => current_user_can( 'customize' ) ? $this->get_nonces() : array(),
   2168 			'l10n'              => $l10n,
   2169 			'_dirty'            => array_keys( $post_values ),
   2170 		);
   2171 
   2172 		foreach ( $this->panels as $panel_id => $panel ) {
   2173 			if ( $panel->check_capabilities() ) {
   2174 				$settings['activePanels'][ $panel_id ] = $panel->active();
   2175 				foreach ( $panel->sections as $section_id => $section ) {
   2176 					if ( $section->check_capabilities() ) {
   2177 						$settings['activeSections'][ $section_id ] = $section->active();
   2178 					}
   2179 				}
   2180 			}
   2181 		}
   2182 		foreach ( $this->sections as $id => $section ) {
   2183 			if ( $section->check_capabilities() ) {
   2184 				$settings['activeSections'][ $id ] = $section->active();
   2185 			}
   2186 		}
   2187 		foreach ( $this->controls as $id => $control ) {
   2188 			if ( $control->check_capabilities() ) {
   2189 				$settings['activeControls'][ $id ] = $control->active();
   2190 			}
   2191 		}
   2192 
   2193 		?>
   2194 		<script type="text/javascript">
   2195 			var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
   2196 			_wpCustomizeSettings.values = {};
   2197 			(function( v ) {
   2198 				<?php
   2199 				/*
   2200 				 * Serialize settings separately from the initial _wpCustomizeSettings
   2201 				 * serialization in order to avoid a peak memory usage spike.
   2202 				 * @todo We may not even need to export the values at all since the pane syncs them anyway.
   2203 				 */
   2204 				foreach ( $this->settings as $id => $setting ) {
   2205 					if ( $setting->check_capabilities() ) {
   2206 						printf(
   2207 							"v[%s] = %s;\n",
   2208 							wp_json_encode( $id ),
   2209 							wp_json_encode( $setting->js_value() )
   2210 						);
   2211 					}
   2212 				}
   2213 				?>
   2214 			})( _wpCustomizeSettings.values );
   2215 		</script>
   2216 		<?php
   2217 	}
   2218 
   2219 	/**
   2220 	 * Prints a signature so we can ensure the Customizer was properly executed.
   2221 	 *
   2222 	 * @since 3.4.0
   2223 	 * @deprecated 4.7.0
   2224 	 */
   2225 	public function customize_preview_signature() {
   2226 		_deprecated_function( __METHOD__, '4.7.0' );
   2227 	}
   2228 
   2229 	/**
   2230 	 * Removes the signature in case we experience a case where the Customizer was not properly executed.
   2231 	 *
   2232 	 * @since 3.4.0
   2233 	 * @deprecated 4.7.0
   2234 	 *
   2235 	 * @param mixed $return Value passed through for {@see 'wp_die_handler'} filter.
   2236 	 * @return mixed Value passed through for {@see 'wp_die_handler'} filter.
   2237 	 */
   2238 	public function remove_preview_signature( $return = null ) {
   2239 		_deprecated_function( __METHOD__, '4.7.0' );
   2240 
   2241 		return $return;
   2242 	}
   2243 
   2244 	/**
   2245 	 * Is it a theme preview?
   2246 	 *
   2247 	 * @since 3.4.0
   2248 	 *
   2249 	 * @return bool True if it's a preview, false if not.
   2250 	 */
   2251 	public function is_preview() {
   2252 		return (bool) $this->previewing;
   2253 	}
   2254 
   2255 	/**
   2256 	 * Retrieve the template name of the previewed theme.
   2257 	 *
   2258 	 * @since 3.4.0
   2259 	 *
   2260 	 * @return string Template name.
   2261 	 */
   2262 	public function get_template() {
   2263 		return $this->theme()->get_template();
   2264 	}
   2265 
   2266 	/**
   2267 	 * Retrieve the stylesheet name of the previewed theme.
   2268 	 *
   2269 	 * @since 3.4.0
   2270 	 *
   2271 	 * @return string Stylesheet name.
   2272 	 */
   2273 	public function get_stylesheet() {
   2274 		return $this->theme()->get_stylesheet();
   2275 	}
   2276 
   2277 	/**
   2278 	 * Retrieve the template root of the previewed theme.
   2279 	 *
   2280 	 * @since 3.4.0
   2281 	 *
   2282 	 * @return string Theme root.
   2283 	 */
   2284 	public function get_template_root() {
   2285 		return get_raw_theme_root( $this->get_template(), true );
   2286 	}
   2287 
   2288 	/**
   2289 	 * Retrieve the stylesheet root of the previewed theme.
   2290 	 *
   2291 	 * @since 3.4.0
   2292 	 *
   2293 	 * @return string Theme root.
   2294 	 */
   2295 	public function get_stylesheet_root() {
   2296 		return get_raw_theme_root( $this->get_stylesheet(), true );
   2297 	}
   2298 
   2299 	/**
   2300 	 * Filters the current theme and return the name of the previewed theme.
   2301 	 *
   2302 	 * @since 3.4.0
   2303 	 *
   2304 	 * @param mixed $current_theme {@internal Parameter is not used}
   2305 	 * @return string Theme name.
   2306 	 */
   2307 	public function current_theme( $current_theme ) {
   2308 		return $this->theme()->display( 'Name' );
   2309 	}
   2310 
   2311 	/**
   2312 	 * Validates setting values.
   2313 	 *
   2314 	 * Validation is skipped for unregistered settings or for values that are
   2315 	 * already null since they will be skipped anyway. Sanitization is applied
   2316 	 * to values that pass validation, and values that become null or `WP_Error`
   2317 	 * after sanitizing are marked invalid.
   2318 	 *
   2319 	 * @since 4.6.0
   2320 	 *
   2321 	 * @see WP_REST_Request::has_valid_params()
   2322 	 * @see WP_Customize_Setting::validate()
   2323 	 *
   2324 	 * @param array $setting_values Mapping of setting IDs to values to validate and sanitize.
   2325 	 * @param array $options {
   2326 	 *     Options.
   2327 	 *
   2328 	 *     @type bool $validate_existence  Whether a setting's existence will be checked.
   2329 	 *     @type bool $validate_capability Whether the setting capability will be checked.
   2330 	 * }
   2331 	 * @return array Mapping of setting IDs to return value of validate method calls, either `true` or `WP_Error`.
   2332 	 */
   2333 	public function validate_setting_values( $setting_values, $options = array() ) {
   2334 		$options = wp_parse_args(
   2335 			$options,
   2336 			array(
   2337 				'validate_capability' => false,
   2338 				'validate_existence'  => false,
   2339 			)
   2340 		);
   2341 
   2342 		$validities = array();
   2343 		foreach ( $setting_values as $setting_id => $unsanitized_value ) {
   2344 			$setting = $this->get_setting( $setting_id );
   2345 			if ( ! $setting ) {
   2346 				if ( $options['validate_existence'] ) {
   2347 					$validities[ $setting_id ] = new WP_Error( 'unrecognized', __( 'Setting does not exist or is unrecognized.' ) );
   2348 				}
   2349 				continue;
   2350 			}
   2351 			if ( $options['validate_capability'] && ! current_user_can( $setting->capability ) ) {
   2352 				$validity = new WP_Error( 'unauthorized', __( 'Unauthorized to modify setting due to capability.' ) );
   2353 			} else {
   2354 				if ( is_null( $unsanitized_value ) ) {
   2355 					continue;
   2356 				}
   2357 				$validity = $setting->validate( $unsanitized_value );
   2358 			}
   2359 			if ( ! is_wp_error( $validity ) ) {
   2360 				/** This filter is documented in wp-includes/class-wp-customize-setting.php */
   2361 				$late_validity = apply_filters( "customize_validate_{$setting->id}", new WP_Error(), $unsanitized_value, $setting );
   2362 				if ( is_wp_error( $late_validity ) && $late_validity->has_errors() ) {
   2363 					$validity = $late_validity;
   2364 				}
   2365 			}
   2366 			if ( ! is_wp_error( $validity ) ) {
   2367 				$value = $setting->sanitize( $unsanitized_value );
   2368 				if ( is_null( $value ) ) {
   2369 					$validity = false;
   2370 				} elseif ( is_wp_error( $value ) ) {
   2371 					$validity = $value;
   2372 				}
   2373 			}
   2374 			if ( false === $validity ) {
   2375 				$validity = new WP_Error( 'invalid_value', __( 'Invalid value.' ) );
   2376 			}
   2377 			$validities[ $setting_id ] = $validity;
   2378 		}
   2379 		return $validities;
   2380 	}
   2381 
   2382 	/**
   2383 	 * Prepares setting validity for exporting to the client (JS).
   2384 	 *
   2385 	 * Converts `WP_Error` instance into array suitable for passing into the
   2386 	 * `wp.customize.Notification` JS model.
   2387 	 *
   2388 	 * @since 4.6.0
   2389 	 *
   2390 	 * @param true|WP_Error $validity Setting validity.
   2391 	 * @return true|array If `$validity` was a WP_Error, the error codes will be array-mapped
   2392 	 *                    to their respective `message` and `data` to pass into the
   2393 	 *                    `wp.customize.Notification` JS model.
   2394 	 */
   2395 	public function prepare_setting_validity_for_js( $validity ) {
   2396 		if ( is_wp_error( $validity ) ) {
   2397 			$notification = array();
   2398 			foreach ( $validity->errors as $error_code => $error_messages ) {
   2399 				$notification[ $error_code ] = array(
   2400 					'message' => implode( ' ', $error_messages ),
   2401 					'data'    => $validity->get_error_data( $error_code ),
   2402 				);
   2403 			}
   2404 			return $notification;
   2405 		} else {
   2406 			return true;
   2407 		}
   2408 	}
   2409 
   2410 	/**
   2411 	 * Handle customize_save WP Ajax request to save/update a changeset.
   2412 	 *
   2413 	 * @since 3.4.0
   2414 	 * @since 4.7.0 The semantics of this method have changed to update a changeset, optionally to also change the status and other attributes.
   2415 	 */
   2416 	public function save() {
   2417 		if ( ! is_user_logged_in() ) {
   2418 			wp_send_json_error( 'unauthenticated' );
   2419 		}
   2420 
   2421 		if ( ! $this->is_preview() ) {
   2422 			wp_send_json_error( 'not_preview' );
   2423 		}
   2424 
   2425 		$action = 'save-customize_' . $this->get_stylesheet();
   2426 		if ( ! check_ajax_referer( $action, 'nonce', false ) ) {
   2427 			wp_send_json_error( 'invalid_nonce' );
   2428 		}
   2429 
   2430 		$changeset_post_id = $this->changeset_post_id();
   2431 		$is_new_changeset  = empty( $changeset_post_id );
   2432 		if ( $is_new_changeset ) {
   2433 			if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->create_posts ) ) {
   2434 				wp_send_json_error( 'cannot_create_changeset_post' );
   2435 			}
   2436 		} else {
   2437 			if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
   2438 				wp_send_json_error( 'cannot_edit_changeset_post' );
   2439 			}
   2440 		}
   2441 
   2442 		if ( ! empty( $_POST['customize_changeset_data'] ) ) {
   2443 			$input_changeset_data = json_decode( wp_unslash( $_POST['customize_changeset_data'] ), true );
   2444 			if ( ! is_array( $input_changeset_data ) ) {
   2445 				wp_send_json_error( 'invalid_customize_changeset_data' );
   2446 			}
   2447 		} else {
   2448 			$input_changeset_data = array();
   2449 		}
   2450 
   2451 		// Validate title.
   2452 		$changeset_title = null;
   2453 		if ( isset( $_POST['customize_changeset_title'] ) ) {
   2454 			$changeset_title = sanitize_text_field( wp_unslash( $_POST['customize_changeset_title'] ) );
   2455 		}
   2456 
   2457 		// Validate changeset status param.
   2458 		$is_publish       = null;
   2459 		$changeset_status = null;
   2460 		if ( isset( $_POST['customize_changeset_status'] ) ) {
   2461 			$changeset_status = wp_unslash( $_POST['customize_changeset_status'] );
   2462 			if ( ! get_post_status_object( $changeset_status ) || ! in_array( $changeset_status, array( 'draft', 'pending', 'publish', 'future' ), true ) ) {
   2463 				wp_send_json_error( 'bad_customize_changeset_status', 400 );
   2464 			}
   2465 			$is_publish = ( 'publish' === $changeset_status || 'future' === $changeset_status );
   2466 			if ( $is_publish && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) {
   2467 				wp_send_json_error( 'changeset_publish_unauthorized', 403 );
   2468 			}
   2469 		}
   2470 
   2471 		/*
   2472 		 * Validate changeset date param. Date is assumed to be in local time for
   2473 		 * the WP if in MySQL format (YYYY-MM-DD HH:MM:SS). Otherwise, the date
   2474 		 * is parsed with strtotime() so that ISO date format may be supplied
   2475 		 * or a string like "+10 minutes".
   2476 		 */
   2477 		$changeset_date_gmt = null;
   2478 		if ( isset( $_POST['customize_changeset_date'] ) ) {
   2479 			$changeset_date = wp_unslash( $_POST['customize_changeset_date'] );
   2480 			if ( preg_match( '/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/', $changeset_date ) ) {
   2481 				$mm         = substr( $changeset_date, 5, 2 );
   2482 				$jj         = substr( $changeset_date, 8, 2 );
   2483 				$aa         = substr( $changeset_date, 0, 4 );
   2484 				$valid_date = wp_checkdate( $mm, $jj, $aa, $changeset_date );
   2485 				if ( ! $valid_date ) {
   2486 					wp_send_json_error( 'bad_customize_changeset_date', 400 );
   2487 				}
   2488 				$changeset_date_gmt = get_gmt_from_date( $changeset_date );
   2489 			} else {
   2490 				$timestamp = strtotime( $changeset_date );
   2491 				if ( ! $timestamp ) {
   2492 					wp_send_json_error( 'bad_customize_changeset_date', 400 );
   2493 				}
   2494 				$changeset_date_gmt = gmdate( 'Y-m-d H:i:s', $timestamp );
   2495 			}
   2496 		}
   2497 
   2498 		$lock_user_id = null;
   2499 		$autosave     = ! empty( $_POST['customize_changeset_autosave'] );
   2500 		if ( ! $is_new_changeset ) {
   2501 			$lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
   2502 		}
   2503 
   2504 		// Force request to autosave when changeset is locked.
   2505 		if ( $lock_user_id && ! $autosave ) {
   2506 			$autosave           = true;
   2507 			$changeset_status   = null;
   2508 			$changeset_date_gmt = null;
   2509 		}
   2510 
   2511 		if ( $autosave && ! defined( 'DOING_AUTOSAVE' ) ) { // Back-compat.
   2512 			define( 'DOING_AUTOSAVE', true );
   2513 		}
   2514 
   2515 		$autosaved = false;
   2516 		$r         = $this->save_changeset_post(
   2517 			array(
   2518 				'status'   => $changeset_status,
   2519 				'title'    => $changeset_title,
   2520 				'date_gmt' => $changeset_date_gmt,
   2521 				'data'     => $input_changeset_data,
   2522 				'autosave' => $autosave,
   2523 			)
   2524 		);
   2525 		if ( $autosave && ! is_wp_error( $r ) ) {
   2526 			$autosaved = true;
   2527 		}
   2528 
   2529 		// If the changeset was locked and an autosave request wasn't itself an error, then now explicitly return with a failure.
   2530 		if ( $lock_user_id && ! is_wp_error( $r ) ) {
   2531 			$r = new WP_Error(
   2532 				'changeset_locked',
   2533 				__( 'Changeset is being edited by other user.' ),
   2534 				array(
   2535 					'lock_user' => $this->get_lock_user_data( $lock_user_id ),
   2536 				)
   2537 			);
   2538 		}
   2539 
   2540 		if ( is_wp_error( $r ) ) {
   2541 			$response = array(
   2542 				'message' => $r->get_error_message(),
   2543 				'code'    => $r->get_error_code(),
   2544 			);
   2545 			if ( is_array( $r->get_error_data() ) ) {
   2546 				$response = array_merge( $response, $r->get_error_data() );
   2547 			} else {
   2548 				$response['data'] = $r->get_error_data();
   2549 			}
   2550 		} else {
   2551 			$response       = $r;
   2552 			$changeset_post = get_post( $this->changeset_post_id() );
   2553 
   2554 			// Dismiss all other auto-draft changeset posts for this user (they serve like autosave revisions), as there should only be one.
   2555 			if ( $is_new_changeset ) {
   2556 				$this->dismiss_user_auto_draft_changesets();
   2557 			}
   2558 
   2559 			// Note that if the changeset status was publish, then it will get set to Trash if revisions are not supported.
   2560 			$response['changeset_status'] = $changeset_post->post_status;
   2561 			if ( $is_publish && 'trash' === $response['changeset_status'] ) {
   2562 				$response['changeset_status'] = 'publish';
   2563 			}
   2564 
   2565 			if ( 'publish' !== $response['changeset_status'] ) {
   2566 				$this->set_changeset_lock( $changeset_post->ID );
   2567 			}
   2568 
   2569 			if ( 'future' === $response['changeset_status'] ) {
   2570 				$response['changeset_date'] = $changeset_post->post_date;
   2571 			}
   2572 
   2573 			if ( 'publish' === $response['changeset_status'] || 'trash' === $response['changeset_status'] ) {
   2574 				$response['next_changeset_uuid'] = wp_generate_uuid4();
   2575 			}
   2576 		}
   2577 
   2578 		if ( $autosave ) {
   2579 			$response['autosaved'] = $autosaved;
   2580 		}
   2581 
   2582 		if ( isset( $response['setting_validities'] ) ) {
   2583 			$response['setting_validities'] = array_map( array( $this, 'prepare_setting_validity_for_js' ), $response['setting_validities'] );
   2584 		}
   2585 
   2586 		/**
   2587 		 * Filters response data for a successful customize_save Ajax request.
   2588 		 *
   2589 		 * This filter does not apply if there was a nonce or authentication failure.
   2590 		 *
   2591 		 * @since 4.2.0
   2592 		 *
   2593 		 * @param array                $response Additional information passed back to the 'saved'
   2594 		 *                                       event on `wp.customize`.
   2595 		 * @param WP_Customize_Manager $this     WP_Customize_Manager instance.
   2596 		 */
   2597 		$response = apply_filters( 'customize_save_response', $response, $this );
   2598 
   2599 		if ( is_wp_error( $r ) ) {
   2600 			wp_send_json_error( $response );
   2601 		} else {
   2602 			wp_send_json_success( $response );
   2603 		}
   2604 	}
   2605 
   2606 	/**
   2607 	 * Save the post for the loaded changeset.
   2608 	 *
   2609 	 * @since 4.7.0
   2610 	 *
   2611 	 * @param array $args {
   2612 	 *     Args for changeset post.
   2613 	 *
   2614 	 *     @type array  $data            Optional additional changeset data. Values will be merged on top of any existing post values.
   2615 	 *     @type string $status          Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed.
   2616 	 *     @type string $title           Post title. Optional.
   2617 	 *     @type string $date_gmt        Date in GMT. Optional.
   2618 	 *     @type int    $user_id         ID for user who is saving the changeset. Optional, defaults to the current user ID.
   2619 	 *     @type bool   $starter_content Whether the data is starter content. If false (default), then $starter_content will be cleared for any $data being saved.
   2620 	 *     @type bool   $autosave        Whether this is a request to create an autosave revision.
   2621 	 * }
   2622 	 *
   2623 	 * @return array|WP_Error Returns array on success and WP_Error with array data on error.
   2624 	 */
   2625 	function save_changeset_post( $args = array() ) {
   2626 
   2627 		$args = array_merge(
   2628 			array(
   2629 				'status'          => null,
   2630 				'title'           => null,
   2631 				'data'            => array(),
   2632 				'date_gmt'        => null,
   2633 				'user_id'         => get_current_user_id(),
   2634 				'starter_content' => false,
   2635 				'autosave'        => false,
   2636 			),
   2637 			$args
   2638 		);
   2639 
   2640 		$changeset_post_id       = $this->changeset_post_id();
   2641 		$existing_changeset_data = array();
   2642 		if ( $changeset_post_id ) {
   2643 			$existing_status = get_post_status( $changeset_post_id );
   2644 			if ( 'publish' === $existing_status || 'trash' === $existing_status ) {
   2645 				return new WP_Error(
   2646 					'changeset_already_published',
   2647 					__( 'The previous set of changes has already been published. Please try saving your current set of changes again.' ),
   2648 					array(
   2649 						'next_changeset_uuid' => wp_generate_uuid4(),
   2650 					)
   2651 				);
   2652 			}
   2653 
   2654 			$existing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
   2655 			if ( is_wp_error( $existing_changeset_data ) ) {
   2656 				return $existing_changeset_data;
   2657 			}
   2658 		}
   2659 
   2660 		// Fail if attempting to publish but publish hook is missing.
   2661 		if ( 'publish' === $args['status'] && false === has_action( 'transition_post_status', '_wp_customize_publish_changeset' ) ) {
   2662 			return new WP_Error( 'missing_publish_callback' );
   2663 		}
   2664 
   2665 		// Validate date.
   2666 		$now = gmdate( 'Y-m-d H:i:59' );
   2667 		if ( $args['date_gmt'] ) {
   2668 			$is_future_dated = ( mysql2date( 'U', $args['date_gmt'], false ) > mysql2date( 'U', $now, false ) );
   2669 			if ( ! $is_future_dated ) {
   2670 				return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) ); // Only future dates are allowed.
   2671 			}
   2672 
   2673 			if ( ! $this->is_theme_active() && ( 'future' === $args['status'] || $is_future_dated ) ) {
   2674 				return new WP_Error( 'cannot_schedule_theme_switches' ); // This should be allowed in the future, when theme is a regular setting.
   2675 			}
   2676 			$will_remain_auto_draft = ( ! $args['status'] && ( ! $changeset_post_id || 'auto-draft' === get_post_status( $changeset_post_id ) ) );
   2677 			if ( $will_remain_auto_draft ) {
   2678 				return new WP_Error( 'cannot_supply_date_for_auto_draft_changeset' );
   2679 			}
   2680 		} elseif ( $changeset_post_id && 'future' === $args['status'] ) {
   2681 
   2682 			// Fail if the new status is future but the existing post's date is not in the future.
   2683 			$changeset_post = get_post( $changeset_post_id );
   2684 			if ( mysql2date( 'U', $changeset_post->post_date_gmt, false ) <= mysql2date( 'U', $now, false ) ) {
   2685 				return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) );
   2686 			}
   2687 		}
   2688 
   2689 		if ( ! empty( $is_future_dated ) && 'publish' === $args['status'] ) {
   2690 			$args['status'] = 'future';
   2691 		}
   2692 
   2693 		// Validate autosave param. See _wp_post_revision_fields() for why these fields are disallowed.
   2694 		if ( $args['autosave'] ) {
   2695 			if ( $args['date_gmt'] ) {
   2696 				return new WP_Error( 'illegal_autosave_with_date_gmt' );
   2697 			} elseif ( $args['status'] ) {
   2698 				return new WP_Error( 'illegal_autosave_with_status' );
   2699 			} elseif ( $args['user_id'] && get_current_user_id() !== $args['user_id'] ) {
   2700 				return new WP_Error( 'illegal_autosave_with_non_current_user' );
   2701 			}
   2702 		}
   2703 
   2704 		// The request was made via wp.customize.previewer.save().
   2705 		$update_transactionally = (bool) $args['status'];
   2706 		$allow_revision         = (bool) $args['status'];
   2707 
   2708 		// Amend post values with any supplied data.
   2709 		foreach ( $args['data'] as $setting_id => $setting_params ) {
   2710 			if ( is_array( $setting_params ) && array_key_exists( 'value', $setting_params ) ) {
   2711 				$this->set_post_value( $setting_id, $setting_params['value'] ); // Add to post values so that they can be validated and sanitized.
   2712 			}
   2713 		}
   2714 
   2715 		// Note that in addition to post data, this will include any stashed theme mods.
   2716 		$post_values = $this->unsanitized_post_values(
   2717 			array(
   2718 				'exclude_changeset' => true,
   2719 				'exclude_post_data' => false,
   2720 			)
   2721 		);
   2722 		$this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value.
   2723 
   2724 		/*
   2725 		 * Get list of IDs for settings that have values different from what is currently
   2726 		 * saved in the changeset. By skipping any values that are already the same, the
   2727 		 * subset of changed settings can be passed into validate_setting_values to prevent
   2728 		 * an underprivileged modifying a single setting for which they have the capability
   2729 		 * from being blocked from saving. This also prevents a user from touching of the
   2730 		 * previous saved settings and overriding the associated user_id if they made no change.
   2731 		 */
   2732 		$changed_setting_ids = array();
   2733 		foreach ( $post_values as $setting_id => $setting_value ) {
   2734 			$setting = $this->get_setting( $setting_id );
   2735 
   2736 			if ( $setting && 'theme_mod' === $setting->type ) {
   2737 				$prefixed_setting_id = $this->get_stylesheet() . '::' . $setting->id;
   2738 			} else {
   2739 				$prefixed_setting_id = $setting_id;
   2740 			}
   2741 
   2742 			$is_value_changed = (
   2743 				! isset( $existing_changeset_data[ $prefixed_setting_id ] )
   2744 				||
   2745 				! array_key_exists( 'value', $existing_changeset_data[ $prefixed_setting_id ] )
   2746 				||
   2747 				$existing_changeset_data[ $prefixed_setting_id ]['value'] !== $setting_value
   2748 			);
   2749 			if ( $is_value_changed ) {
   2750 				$changed_setting_ids[] = $setting_id;
   2751 			}
   2752 		}
   2753 
   2754 		/**
   2755 		 * Fires before save validation happens.
   2756 		 *
   2757 		 * Plugins can add just-in-time {@see 'customize_validate_{$this->ID}'} filters
   2758 		 * at this point to catch any settings registered after `customize_register`.
   2759 		 * The dynamic portion of the hook name, `$this->ID` refers to the setting ID.
   2760 		 *
   2761 		 * @since 4.6.0
   2762 		 *
   2763 		 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
   2764 		 */
   2765 		do_action( 'customize_save_validation_before', $this );
   2766 
   2767 		// Validate settings.
   2768 		$validated_values      = array_merge(
   2769 			array_fill_keys( array_keys( $args['data'] ), null ), // Make sure existence/capability checks are done on value-less setting updates.
   2770 			$post_values
   2771 		);
   2772 		$setting_validities    = $this->validate_setting_values(
   2773 			$validated_values,
   2774 			array(
   2775 				'validate_capability' => true,
   2776 				'validate_existence'  => true,
   2777 			)
   2778 		);
   2779 		$invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) );
   2780 
   2781 		/*
   2782 		 * Short-circuit if there are invalid settings the update is transactional.
   2783 		 * A changeset update is transactional when a status is supplied in the request.
   2784 		 */
   2785 		if ( $update_transactionally && $invalid_setting_count > 0 ) {
   2786 			$response = array(
   2787 				'setting_validities' => $setting_validities,
   2788 				/* translators: %s: Number of invalid settings. */
   2789 				'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 ) ),
   2790 			);
   2791 			return new WP_Error( 'transaction_fail', '', $response );
   2792 		}
   2793 
   2794 		// Obtain/merge data for changeset.
   2795 		$original_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
   2796 		$data                    = $original_changeset_data;
   2797 		if ( is_wp_error( $data ) ) {
   2798 			$data = array();
   2799 		}
   2800 
   2801 		// Ensure that all post values are included in the changeset data.
   2802 		foreach ( $post_values as $setting_id => $post_value ) {
   2803 			if ( ! isset( $args['data'][ $setting_id ] ) ) {
   2804 				$args['data'][ $setting_id ] = array();
   2805 			}
   2806 			if ( ! isset( $args['data'][ $setting_id ]['value'] ) ) {
   2807 				$args['data'][ $setting_id ]['value'] = $post_value;
   2808 			}
   2809 		}
   2810 
   2811 		foreach ( $args['data'] as $setting_id => $setting_params ) {
   2812 			$setting = $this->get_setting( $setting_id );
   2813 			if ( ! $setting || ! $setting->check_capabilities() ) {
   2814 				continue;
   2815 			}
   2816 
   2817 			// Skip updating changeset for invalid setting values.
   2818 			if ( isset( $setting_validities[ $setting_id ] ) && is_wp_error( $setting_validities[ $setting_id ] ) ) {
   2819 				continue;
   2820 			}
   2821 
   2822 			$changeset_setting_id = $setting_id;
   2823 			if ( 'theme_mod' === $setting->type ) {
   2824 				$changeset_setting_id = sprintf( '%s::%s', $this->get_stylesheet(), $setting_id );
   2825 			}
   2826 
   2827 			if ( null === $setting_params ) {
   2828 				// Remove setting from changeset entirely.
   2829 				unset( $data[ $changeset_setting_id ] );
   2830 			} else {
   2831 
   2832 				if ( ! isset( $data[ $changeset_setting_id ] ) ) {
   2833 					$data[ $changeset_setting_id ] = array();
   2834 				}
   2835 
   2836 				// Merge any additional setting params that have been supplied with the existing params.
   2837 				$merged_setting_params = array_merge( $data[ $changeset_setting_id ], $setting_params );
   2838 
   2839 				// Skip updating setting params if unchanged (ensuring the user_id is not overwritten).
   2840 				if ( $data[ $changeset_setting_id ] === $merged_setting_params ) {
   2841 					continue;
   2842 				}
   2843 
   2844 				$data[ $changeset_setting_id ] = array_merge(
   2845 					$merged_setting_params,
   2846 					array(
   2847 						'type'              => $setting->type,
   2848 						'user_id'           => $args['user_id'],
   2849 						'date_modified_gmt' => current_time( 'mysql', true ),
   2850 					)
   2851 				);
   2852 
   2853 				// Clear starter_content flag in data if changeset is not explicitly being updated for starter content.
   2854 				if ( empty( $args['starter_content'] ) ) {
   2855 					unset( $data[ $changeset_setting_id ]['starter_content'] );
   2856 				}
   2857 			}
   2858 		}
   2859 
   2860 		$filter_context = array(
   2861 			'uuid'          => $this->changeset_uuid(),
   2862 			'title'         => $args['title'],
   2863 			'status'        => $args['status'],
   2864 			'date_gmt'      => $args['date_gmt'],
   2865 			'post_id'       => $changeset_post_id,
   2866 			'previous_data' => is_wp_error( $original_changeset_data ) ? array() : $original_changeset_data,
   2867 			'manager'       => $this,
   2868 		);
   2869 
   2870 		/**
   2871 		 * Filters the settings' data that will be persisted into the changeset.
   2872 		 *
   2873 		 * Plugins may amend additional data (such as additional meta for settings) into the changeset with this filter.
   2874 		 *
   2875 		 * @since 4.7.0
   2876 		 *
   2877 		 * @param array $data Updated changeset data, mapping setting IDs to arrays containing a $value item and optionally other metadata.
   2878 		 * @param array $context {
   2879 		 *     Filter context.
   2880 		 *
   2881 		 *     @type string               $uuid          Changeset UUID.
   2882 		 *     @type string               $title         Requested title for the changeset post.
   2883 		 *     @type string               $status        Requested status for the changeset post.
   2884 		 *     @type string               $date_gmt      Requested date for the changeset post in MySQL format and GMT timezone.
   2885 		 *     @type int|false            $post_id       Post ID for the changeset, or false if it doesn't exist yet.
   2886 		 *     @type array                $previous_data Previous data contained in the changeset.
   2887 		 *     @type WP_Customize_Manager $manager       Manager instance.
   2888 		 * }
   2889 		 */
   2890 		$data = apply_filters( 'customize_changeset_save_data', $data, $filter_context );
   2891 
   2892 		// Switch theme if publishing changes now.
   2893 		if ( 'publish' === $args['status'] && ! $this->is_theme_active() ) {
   2894 			// Temporarily stop previewing the theme to allow switch_themes() to operate properly.
   2895 			$this->stop_previewing_theme();
   2896 			switch_theme( $this->get_stylesheet() );
   2897 			update_option( 'theme_switched_via_customizer', true );
   2898 			$this->start_previewing_theme();
   2899 		}
   2900 
   2901 		// Gather the data for wp_insert_post()/wp_update_post().
   2902 		$post_array = array(
   2903 			// JSON_UNESCAPED_SLASHES is only to improve readability as slashes needn't be escaped in storage.
   2904 			'post_content' => wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT ),
   2905 		);
   2906 		if ( $args['title'] ) {
   2907 			$post_array['post_title'] = $args['title'];
   2908 		}
   2909 		if ( $changeset_post_id ) {
   2910 			$post_array['ID'] = $changeset_post_id;
   2911 		} else {
   2912 			$post_array['post_type']   = 'customize_changeset';
   2913 			$post_array['post_name']   = $this->changeset_uuid();
   2914 			$post_array['post_status'] = 'auto-draft';
   2915 		}
   2916 		if ( $args['status'] ) {
   2917 			$post_array['post_status'] = $args['status'];
   2918 		}
   2919 
   2920 		// Reset post date to now if we are publishing, otherwise pass post_date_gmt and translate for post_date.
   2921 		if ( 'publish' === $args['status'] ) {
   2922 			$post_array['post_date_gmt'] = '0000-00-00 00:00:00';
   2923 			$post_array['post_date']     = '0000-00-00 00:00:00';
   2924 		} elseif ( $args['date_gmt'] ) {
   2925 			$post_array['post_date_gmt'] = $args['date_gmt'];
   2926 			$post_array['post_date']     = get_date_from_gmt( $args['date_gmt'] );
   2927 		} elseif ( $changeset_post_id && 'auto-draft' === get_post_status( $changeset_post_id ) ) {
   2928 			/*
   2929 			 * Keep bumping the date for the auto-draft whenever it is modified;
   2930 			 * this extends its life, preserving it from garbage-collection via
   2931 			 * wp_delete_auto_drafts().
   2932 			 */
   2933 			$post_array['post_date']     = current_time( 'mysql' );
   2934 			$post_array['post_date_gmt'] = '';
   2935 		}
   2936 
   2937 		$this->store_changeset_revision = $allow_revision;
   2938 		add_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ), 5, 3 );
   2939 
   2940 		/*
   2941 		 * Update the changeset post. The publish_customize_changeset action will cause the settings in the
   2942 		 * changeset to be saved via WP_Customize_Setting::save(). Updating a post with publish status will
   2943 		 * trigger WP_Customize_Manager::publish_changeset_values().
   2944 		 */
   2945 		add_filter( 'wp_insert_post_data', array( $this, 'preserve_insert_changeset_post_content' ), 5, 3 );
   2946 		if ( $changeset_post_id ) {
   2947 			if ( $args['autosave'] && 'auto-draft' !== get_post_status( $changeset_post_id ) ) {
   2948 				// See _wp_translate_postdata() for why this is required as it will use the edit_post meta capability.
   2949 				add_filter( 'map_meta_cap', array( $this, 'grant_edit_post_capability_for_changeset' ), 10, 4 );
   2950 
   2951 				$post_array['post_ID']   = $post_array['ID'];
   2952 				$post_array['post_type'] = 'customize_changeset';
   2953 
   2954 				$r = wp_create_post_autosave( wp_slash( $post_array ) );
   2955 
   2956 				remove_filter( 'map_meta_cap', array( $this, 'grant_edit_post_capability_for_changeset' ), 10 );
   2957 			} else {
   2958 				$post_array['edit_date'] = true; // Prevent date clearing.
   2959 
   2960 				$r = wp_update_post( wp_slash( $post_array ), true );
   2961 
   2962 				// Delete autosave revision for user when the changeset is updated.
   2963 				if ( ! empty( $args['user_id'] ) ) {
   2964 					$autosave_draft = wp_get_post_autosave( $changeset_post_id, $args['user_id'] );
   2965 					if ( $autosave_draft ) {
   2966 						wp_delete_post( $autosave_draft->ID, true );
   2967 					}
   2968 				}
   2969 			}
   2970 		} else {
   2971 			$r = wp_insert_post( wp_slash( $post_array ), true );
   2972 			if ( ! is_wp_error( $r ) ) {
   2973 				$this->_changeset_post_id = $r; // Update cached post ID for the loaded changeset.
   2974 			}
   2975 		}
   2976 		remove_filter( 'wp_insert_post_data', array( $this, 'preserve_insert_changeset_post_content' ), 5 );
   2977 
   2978 		$this->_changeset_data = null; // Reset so WP_Customize_Manager::changeset_data() will re-populate with updated contents.
   2979 
   2980 		remove_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ) );
   2981 
   2982 		$response = array(
   2983 			'setting_validities' => $setting_validities,
   2984 		);
   2985 
   2986 		if ( is_wp_error( $r ) ) {
   2987 			$response['changeset_post_save_failure'] = $r->get_error_code();
   2988 			return new WP_Error( 'changeset_post_save_failure', '', $response );
   2989 		}
   2990 
   2991 		return $response;
   2992 	}
   2993 
   2994 	/**
   2995 	 * Preserve the initial JSON post_content passed to save into the post.
   2996 	 *
   2997 	 * This is needed to prevent KSES and other {@see 'content_save_pre'} filters
   2998 	 * from corrupting JSON data.
   2999 	 *
   3000 	 * Note that WP_Customize_Manager::validate_setting_values() have already
   3001 	 * run on the setting values being serialized as JSON into the post content
   3002 	 * so it is pre-sanitized.
   3003 	 *
   3004 	 * Also, the sanitization logic is re-run through the respective
   3005 	 * WP_Customize_Setting::sanitize() method when being read out of the
   3006 	 * changeset, via WP_Customize_Manager::post_value(), and this sanitized
   3007 	 * value will also be sent into WP_Customize_Setting::update() for
   3008 	 * persisting to the DB.
   3009 	 *
   3010 	 * Multiple users can collaborate on a single changeset, where one user may
   3011 	 * have the unfiltered_html capability but another may not. A user with
   3012 	 * unfiltered_html may add a script tag to some field which needs to be kept
   3013 	 * intact even when another user updates the changeset to modify another field
   3014 	 * when they do not have unfiltered_html.
   3015 	 *
   3016 	 * @since 5.4.1
   3017 	 *
   3018 	 * @param array $data                An array of slashed and processed post data.
   3019 	 * @param array $postarr             An array of sanitized (and slashed) but otherwise unmodified post data.
   3020 	 * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed post data as originally passed to wp_insert_post().
   3021 	 * @return array Filtered post data.
   3022 	 */
   3023 	public function preserve_insert_changeset_post_content( $data, $postarr, $unsanitized_postarr ) {
   3024 		if (
   3025 			isset( $data['post_type'] ) &&
   3026 			isset( $unsanitized_postarr['post_content'] ) &&
   3027 			'customize_changeset' === $data['post_type'] ||
   3028 			(
   3029 				'revision' === $data['post_type'] &&
   3030 				! empty( $data['post_parent'] ) &&
   3031 				'customize_changeset' === get_post_type( $data['post_parent'] )
   3032 			)
   3033 		) {
   3034 			$data['post_content'] = $unsanitized_postarr['post_content'];
   3035 		}
   3036 		return $data;
   3037 	}
   3038 
   3039 	/**
   3040 	 * Trash or delete a changeset post.
   3041 	 *
   3042 	 * The following re-formulates the logic from `wp_trash_post()` as done in
   3043 	 * `wp_publish_post()`. The reason for bypassing `wp_trash_post()` is that it
   3044 	 * will mutate the the `post_content` and the `post_name` when they should be
   3045 	 * untouched.
   3046 	 *
   3047 	 * @since 4.9.0
   3048 	 *
   3049 	 * @see wp_trash_post()
   3050 	 * @global wpdb $wpdb WordPress database abstraction object.
   3051 	 *
   3052 	 * @param int|WP_Post $post The changeset post.
   3053 	 * @return mixed A WP_Post object for the trashed post or an empty value on failure.
   3054 	 */
   3055 	public function trash_changeset_post( $post ) {
   3056 		global $wpdb;
   3057 
   3058 		$post = get_post( $post );
   3059 
   3060 		if ( ! ( $post instanceof WP_Post ) ) {
   3061 			return $post;
   3062 		}
   3063 		$post_id = $post->ID;
   3064 
   3065 		if ( ! EMPTY_TRASH_DAYS ) {
   3066 			return wp_delete_post( $post_id, true );
   3067 		}
   3068 
   3069 		if ( 'trash' === get_post_status( $post ) ) {
   3070 			return false;
   3071 		}
   3072 
   3073 		/** This filter is documented in wp-includes/post.php */
   3074 		$check = apply_filters( 'pre_trash_post', null, $post );
   3075 		if ( null !== $check ) {
   3076 			return $check;
   3077 		}
   3078 
   3079 		/** This action is documented in wp-includes/post.php */
   3080 		do_action( 'wp_trash_post', $post_id );
   3081 
   3082 		add_post_meta( $post_id, '_wp_trash_meta_status', $post->post_status );
   3083 		add_post_meta( $post_id, '_wp_trash_meta_time', time() );
   3084 
   3085 		$old_status = $post->post_status;
   3086 		$new_status = 'trash';
   3087 		$wpdb->update( $wpdb->posts, array( 'post_status' => $new_status ), array( 'ID' => $post->ID ) );
   3088 		clean_post_cache( $post->ID );
   3089 
   3090 		$post->post_status = $new_status;
   3091 		wp_transition_post_status( $new_status, $old_status, $post );
   3092 
   3093 		/** This action is documented in wp-includes/post.php */
   3094 		do_action( "edit_post_{$post->post_type}", $post->ID, $post );
   3095 
   3096 		/** This action is documented in wp-includes/post.php */
   3097 		do_action( 'edit_post', $post->ID, $post );
   3098 
   3099 		/** This action is documented in wp-includes/post.php */
   3100 		do_action( "save_post_{$post->post_type}", $post->ID, $post, true );
   3101 
   3102 		/** This action is documented in wp-includes/post.php */
   3103 		do_action( 'save_post', $post->ID, $post, true );
   3104 
   3105 		/** This action is documented in wp-includes/post.php */
   3106 		do_action( 'wp_insert_post', $post->ID, $post, true );
   3107 
   3108 		wp_after_insert_post( get_post( $post_id ), true, $post );
   3109 
   3110 		wp_trash_post_comments( $post_id );
   3111 
   3112 		/** This action is documented in wp-includes/post.php */
   3113 		do_action( 'trashed_post', $post_id );
   3114 
   3115 		return $post;
   3116 	}
   3117 
   3118 	/**
   3119 	 * Handle request to trash a changeset.
   3120 	 *
   3121 	 * @since 4.9.0
   3122 	 */
   3123 	public function handle_changeset_trash_request() {
   3124 		if ( ! is_user_logged_in() ) {
   3125 			wp_send_json_error( 'unauthenticated' );
   3126 		}
   3127 
   3128 		if ( ! $this->is_preview() ) {
   3129 			wp_send_json_error( 'not_preview' );
   3130 		}
   3131 
   3132 		if ( ! check_ajax_referer( 'trash_customize_changeset', 'nonce', false ) ) {
   3133 			wp_send_json_error(
   3134 				array(
   3135 					'code'    => 'invalid_nonce',
   3136 					'message' => __( 'There was an authentication problem. Please reload and try again.' ),
   3137 				)
   3138 			);
   3139 		}
   3140 
   3141 		$changeset_post_id = $this->changeset_post_id();
   3142 
   3143 		if ( ! $changeset_post_id ) {
   3144 			wp_send_json_error(
   3145 				array(
   3146 					'message' => __( 'No changes saved yet, so there is nothing to trash.' ),
   3147 					'code'    => 'non_existent_changeset',
   3148 				)
   3149 			);
   3150 			return;
   3151 		}
   3152 
   3153 		if ( $changeset_post_id ) {
   3154 			if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) {
   3155 				wp_send_json_error(
   3156 					array(
   3157 						'code'    => 'changeset_trash_unauthorized',
   3158 						'message' => __( 'Unable to trash changes.' ),
   3159 					)
   3160 				);
   3161 			}
   3162 
   3163 			$lock_user = (int) wp_check_post_lock( $changeset_post_id );
   3164 
   3165 			if ( $lock_user && get_current_user_id() !== $lock_user ) {
   3166 				wp_send_json_error(
   3167 					array(
   3168 						'code'     => 'changeset_locked',
   3169 						'message'  => __( 'Changeset is being edited by other user.' ),
   3170 						'lockUser' => $this->get_lock_user_data( $lock_user ),
   3171 					)
   3172 				);
   3173 			}
   3174 		}
   3175 
   3176 		if ( 'trash' === get_post_status( $changeset_post_id ) ) {
   3177 			wp_send_json_error(
   3178 				array(
   3179 					'message' => __( 'Changes have already been trashed.' ),
   3180 					'code'    => 'changeset_already_trashed',
   3181 				)
   3182 			);
   3183 			return;
   3184 		}
   3185 
   3186 		$r = $this->trash_changeset_post( $changeset_post_id );
   3187 		if ( ! ( $r instanceof WP_Post ) ) {
   3188 			wp_send_json_error(
   3189 				array(
   3190 					'code'    => 'changeset_trash_failure',
   3191 					'message' => __( 'Unable to trash changes.' ),
   3192 				)
   3193 			);
   3194 		}
   3195 
   3196 		wp_send_json_success(
   3197 			array(
   3198 				'message' => __( 'Changes trashed successfully.' ),
   3199 			)
   3200 		);
   3201 	}
   3202 
   3203 	/**
   3204 	 * Re-map 'edit_post' meta cap for a customize_changeset post to be the same as 'customize' maps.
   3205 	 *
   3206 	 * There is essentially a "meta meta" cap in play here, where 'edit_post' meta cap maps to
   3207 	 * the 'customize' meta cap which then maps to 'edit_theme_options'. This is currently
   3208 	 * required in core for `wp_create_post_autosave()` because it will call
   3209 	 * `_wp_translate_postdata()` which in turn will check if a user can 'edit_post', but the
   3210 	 * the caps for the customize_changeset post type are all mapping to the meta capability.
   3211 	 * This should be able to be removed once #40922 is addressed in core.
   3212 	 *
   3213 	 * @since 4.9.0
   3214 	 *
   3215 	 * @link https://core.trac.wordpress.org/ticket/40922
   3216 	 * @see WP_Customize_Manager::save_changeset_post()
   3217 	 * @see _wp_translate_postdata()
   3218 	 *
   3219 	 * @param string[] $caps    Array of the user's capabilities.
   3220 	 * @param string   $cap     Capability name.
   3221 	 * @param int      $user_id The user ID.
   3222 	 * @param array    $args    Adds the context to the cap. Typically the object ID.
   3223 	 * @return array Capabilities.
   3224 	 */
   3225 	public function grant_edit_post_capability_for_changeset( $caps, $cap, $user_id, $args ) {
   3226 		if ( 'edit_post' === $cap && ! empty( $args[0] ) && 'customize_changeset' === get_post_type( $args[0] ) ) {
   3227 			$post_type_obj = get_post_type_object( 'customize_changeset' );
   3228 			$caps          = map_meta_cap( $post_type_obj->cap->$cap, $user_id );
   3229 		}
   3230 		return $caps;
   3231 	}
   3232 
   3233 	/**
   3234 	 * Marks the changeset post as being currently edited by the current user.
   3235 	 *
   3236 	 * @since 4.9.0
   3237 	 *
   3238 	 * @param int  $changeset_post_id Changeset post ID.
   3239 	 * @param bool $take_over Whether to take over the changeset. Default false.
   3240 	 */
   3241 	public function set_changeset_lock( $changeset_post_id, $take_over = false ) {
   3242 		if ( $changeset_post_id ) {
   3243 			$can_override = ! (bool) get_post_meta( $changeset_post_id, '_edit_lock', true );
   3244 
   3245 			if ( $take_over ) {
   3246 				$can_override = true;
   3247 			}
   3248 
   3249 			if ( $can_override ) {
   3250 				$lock = sprintf( '%s:%s', time(), get_current_user_id() );
   3251 				update_post_meta( $changeset_post_id, '_edit_lock', $lock );
   3252 			} else {
   3253 				$this->refresh_changeset_lock( $changeset_post_id );
   3254 			}
   3255 		}
   3256 	}
   3257 
   3258 	/**
   3259 	 * Refreshes changeset lock with the current time if current user edited the changeset before.
   3260 	 *
   3261 	 * @since 4.9.0
   3262 	 *
   3263 	 * @param int $changeset_post_id Changeset post ID.
   3264 	 */
   3265 	public function refresh_changeset_lock( $changeset_post_id ) {
   3266 		if ( ! $changeset_post_id ) {
   3267 			return;
   3268 		}
   3269 		$lock = get_post_meta( $changeset_post_id, '_edit_lock', true );
   3270 		$lock = explode( ':', $lock );
   3271 
   3272 		if ( $lock && ! empty( $lock[1] ) ) {
   3273 			$user_id         = (int) $lock[1];
   3274 			$current_user_id = get_current_user_id();
   3275 			if ( $user_id === $current_user_id ) {
   3276 				$lock = sprintf( '%s:%s', time(), $user_id );
   3277 				update_post_meta( $changeset_post_id, '_edit_lock', $lock );
   3278 			}
   3279 		}
   3280 	}
   3281 
   3282 	/**
   3283 	 * Filters heartbeat settings for the Customizer.
   3284 	 *
   3285 	 * @since 4.9.0
   3286 	 * @param array $settings Current settings to filter.
   3287 	 * @return array Heartbeat settings.
   3288 	 */
   3289 	public function add_customize_screen_to_heartbeat_settings( $settings ) {
   3290 		global $pagenow;
   3291 		if ( 'customize.php' === $pagenow ) {
   3292 			$settings['screenId'] = 'customize';
   3293 		}
   3294 		return $settings;
   3295 	}
   3296 
   3297 	/**
   3298 	 * Get lock user data.
   3299 	 *
   3300 	 * @since 4.9.0
   3301 	 *
   3302 	 * @param int $user_id User ID.
   3303 	 * @return array|null User data formatted for client.
   3304 	 */
   3305 	protected function get_lock_user_data( $user_id ) {
   3306 		if ( ! $user_id ) {
   3307 			return null;
   3308 		}
   3309 		$lock_user = get_userdata( $user_id );
   3310 		if ( ! $lock_user ) {
   3311 			return null;
   3312 		}
   3313 		return array(
   3314 			'id'     => $lock_user->ID,
   3315 			'name'   => $lock_user->display_name,
   3316 			'avatar' => get_avatar_url( $lock_user->ID, array( 'size' => 128 ) ),
   3317 		);
   3318 	}
   3319 
   3320 	/**
   3321 	 * Check locked changeset with heartbeat API.
   3322 	 *
   3323 	 * @since 4.9.0
   3324 	 *
   3325 	 * @param array  $response  The Heartbeat response.
   3326 	 * @param array  $data      The $_POST data sent.
   3327 	 * @param string $screen_id The screen id.
   3328 	 * @return array The Heartbeat response.
   3329 	 */
   3330 	public function check_changeset_lock_with_heartbeat( $response, $data, $screen_id ) {
   3331 		if ( isset( $data['changeset_uuid'] ) ) {
   3332 			$changeset_post_id = $this->find_changeset_post_id( $data['changeset_uuid'] );
   3333 		} else {
   3334 			$changeset_post_id = $this->changeset_post_id();
   3335 		}
   3336 
   3337 		if (
   3338 			array_key_exists( 'check_changeset_lock', $data )
   3339 			&& 'customize' === $screen_id
   3340 			&& $changeset_post_id
   3341 			&& current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id )
   3342 		) {
   3343 			$lock_user_id = wp_check_post_lock( $changeset_post_id );
   3344 
   3345 			if ( $lock_user_id ) {
   3346 				$response['customize_changeset_lock_user'] = $this->get_lock_user_data( $lock_user_id );
   3347 			} else {
   3348 
   3349 				// Refreshing time will ensure that the user is sitting on customizer and has not closed the customizer tab.
   3350 				$this->refresh_changeset_lock( $changeset_post_id );
   3351 			}
   3352 		}
   3353 
   3354 		return $response;
   3355 	}
   3356 
   3357 	/**
   3358 	 * Removes changeset lock when take over request is sent via Ajax.
   3359 	 *
   3360 	 * @since 4.9.0
   3361 	 */
   3362 	public function handle_override_changeset_lock_request() {
   3363 		if ( ! $this->is_preview() ) {
   3364 			wp_send_json_error( 'not_preview', 400 );
   3365 		}
   3366 
   3367 		if ( ! check_ajax_referer( 'customize_override_changeset_lock', 'nonce', false ) ) {
   3368 			wp_send_json_error(
   3369 				array(
   3370 					'code'    => 'invalid_nonce',
   3371 					'message' => __( 'Security check failed.' ),
   3372 				)
   3373 			);
   3374 		}
   3375 
   3376 		$changeset_post_id = $this->changeset_post_id();
   3377 
   3378 		if ( empty( $changeset_post_id ) ) {
   3379 			wp_send_json_error(
   3380 				array(
   3381 					'code'    => 'no_changeset_found_to_take_over',
   3382 					'message' => __( 'No changeset found to take over' ),
   3383 				)
   3384 			);
   3385 		}
   3386 
   3387 		if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
   3388 			wp_send_json_error(
   3389 				array(
   3390 					'code'    => 'cannot_remove_changeset_lock',
   3391 					'message' => __( 'Sorry, you are not allowed to take over.' ),
   3392 				)
   3393 			);
   3394 		}
   3395 
   3396 		$this->set_changeset_lock( $changeset_post_id, true );
   3397 
   3398 		wp_send_json_success( 'changeset_taken_over' );
   3399 	}
   3400 
   3401 	/**
   3402 	 * Whether a changeset revision should be made.
   3403 	 *
   3404 	 * @since 4.7.0
   3405 	 * @var bool
   3406 	 */
   3407 	protected $store_changeset_revision;
   3408 
   3409 	/**
   3410 	 * Filters whether a changeset has changed to create a new revision.
   3411 	 *
   3412 	 * Note that this will not be called while a changeset post remains in auto-draft status.
   3413 	 *
   3414 	 * @since 4.7.0
   3415 	 *
   3416 	 * @param bool    $post_has_changed Whether the post has changed.
   3417 	 * @param WP_Post $last_revision    The last revision post object.
   3418 	 * @param WP_Post $post             The post object.
   3419 	 * @return bool Whether a revision should be made.
   3420 	 */
   3421 	public function _filter_revision_post_has_changed( $post_has_changed, $last_revision, $post ) {
   3422 		unset( $last_revision );
   3423 		if ( 'customize_changeset' === $post->post_type ) {
   3424 			$post_has_changed = $this->store_changeset_revision;
   3425 		}
   3426 		return $post_has_changed;
   3427 	}
   3428 
   3429 	/**
   3430 	 * Publish changeset values.
   3431 	 *
   3432 	 * This will the values contained in a changeset, even changesets that do not
   3433 	 * correspond to current manager instance. This is called by
   3434 	 * `_wp_customize_publish_changeset()` when a customize_changeset post is
   3435 	 * transitioned to the `publish` status. As such, this method should not be
   3436 	 * called directly and instead `wp_publish_post()` should be used.
   3437 	 *
   3438 	 * Please note that if the settings in the changeset are for a non-activated
   3439 	 * theme, the theme must first be switched to (via `switch_theme()`) before
   3440 	 * invoking this method.
   3441 	 *
   3442 	 * @since 4.7.0
   3443 	 *
   3444 	 * @see _wp_customize_publish_changeset()
   3445 	 * @global wpdb $wpdb WordPress database abstraction object.
   3446 	 *
   3447 	 * @param int $changeset_post_id ID for customize_changeset post. Defaults to the changeset for the current manager instance.
   3448 	 * @return true|WP_Error True or error info.
   3449 	 */
   3450 	public function _publish_changeset_values( $changeset_post_id ) {
   3451 		global $wpdb;
   3452 
   3453 		$publishing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
   3454 		if ( is_wp_error( $publishing_changeset_data ) ) {
   3455 			return $publishing_changeset_data;
   3456 		}
   3457 
   3458 		$changeset_post = get_post( $changeset_post_id );
   3459 
   3460 		/*
   3461 		 * Temporarily override the changeset context so that it will be read
   3462 		 * in calls to unsanitized_post_values() and so that it will be available
   3463 		 * on the $wp_customize object passed to hooks during the save logic.
   3464 		 */
   3465 		$previous_changeset_post_id = $this->_changeset_post_id;
   3466 		$this->_changeset_post_id   = $changeset_post_id;
   3467 		$previous_changeset_uuid    = $this->_changeset_uuid;
   3468 		$this->_changeset_uuid      = $changeset_post->post_name;
   3469 		$previous_changeset_data    = $this->_changeset_data;
   3470 		$this->_changeset_data      = $publishing_changeset_data;
   3471 
   3472 		// Parse changeset data to identify theme mod settings and user IDs associated with settings to be saved.
   3473 		$setting_user_ids   = array();
   3474 		$theme_mod_settings = array();
   3475 		$namespace_pattern  = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
   3476 		$matches            = array();
   3477 		foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) {
   3478 			$actual_setting_id    = null;
   3479 			$is_theme_mod_setting = (
   3480 				isset( $setting_params['value'] )
   3481 				&&
   3482 				isset( $setting_params['type'] )
   3483 				&&
   3484 				'theme_mod' === $setting_params['type']
   3485 				&&
   3486 				preg_match( $namespace_pattern, $raw_setting_id, $matches )
   3487 			);
   3488 			if ( $is_theme_mod_setting ) {
   3489 				if ( ! isset( $theme_mod_settings[ $matches['stylesheet'] ] ) ) {
   3490 					$theme_mod_settings[ $matches['stylesheet'] ] = array();
   3491 				}
   3492 				$theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
   3493 
   3494 				if ( $this->get_stylesheet() === $matches['stylesheet'] ) {
   3495 					$actual_setting_id = $matches['setting_id'];
   3496 				}
   3497 			} else {
   3498 				$actual_setting_id = $raw_setting_id;
   3499 			}
   3500 
   3501 			// Keep track of the user IDs for settings actually for this theme.
   3502 			if ( $actual_setting_id && isset( $setting_params['user_id'] ) ) {
   3503 				$setting_user_ids[ $actual_setting_id ] = $setting_params['user_id'];
   3504 			}
   3505 		}
   3506 
   3507 		$changeset_setting_values = $this->unsanitized_post_values(
   3508 			array(
   3509 				'exclude_post_data' => true,
   3510 				'exclude_changeset' => false,
   3511 			)
   3512 		);
   3513 		$changeset_setting_ids    = array_keys( $changeset_setting_values );
   3514 		$this->add_dynamic_settings( $changeset_setting_ids );
   3515 
   3516 		/**
   3517 		 * Fires once the theme has switched in the Customizer, but before settings
   3518 		 * have been saved.
   3519 		 *
   3520 		 * @since 3.4.0
   3521 		 *
   3522 		 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
   3523 		 */
   3524 		do_action( 'customize_save', $this );
   3525 
   3526 		/*
   3527 		 * Ensure that all settings will allow themselves to be saved. Note that
   3528 		 * this is safe because the setting would have checked the capability
   3529 		 * when the setting value was written into the changeset. So this is why
   3530 		 * an additional capability check is not required here.
   3531 		 */
   3532 		$original_setting_capabilities = array();
   3533 		foreach ( $changeset_setting_ids as $setting_id ) {
   3534 			$setting = $this->get_setting( $setting_id );
   3535 			if ( $setting && ! isset( $setting_user_ids[ $setting_id ] ) ) {
   3536 				$original_setting_capabilities[ $setting->id ] = $setting->capability;
   3537 				$setting->capability                           = 'exist';
   3538 			}
   3539 		}
   3540 
   3541 		$original_user_id = get_current_user_id();
   3542 		foreach ( $changeset_setting_ids as $setting_id ) {
   3543 			$setting = $this->get_setting( $setting_id );
   3544 			if ( $setting ) {
   3545 				/*
   3546 				 * Set the current user to match the user who saved the value into
   3547 				 * the changeset so that any filters that apply during the save
   3548 				 * process will respect the original user's capabilities. This
   3549 				 * will ensure, for example, that KSES won't strip unsafe HTML
   3550 				 * when a scheduled changeset publishes via WP Cron.
   3551 				 */
   3552 				if ( isset( $setting_user_ids[ $setting_id ] ) ) {
   3553 					wp_set_current_user( $setting_user_ids[ $setting_id ] );
   3554 				} else {
   3555 					wp_set_current_user( $original_user_id );
   3556 				}
   3557 
   3558 				$setting->save();
   3559 			}
   3560 		}
   3561 		wp_set_current_user( $original_user_id );
   3562 
   3563 		// Update the stashed theme mod settings, removing the active theme's stashed settings, if activated.
   3564 		if ( did_action( 'switch_theme' ) ) {
   3565 			$other_theme_mod_settings = $theme_mod_settings;
   3566 			unset( $other_theme_mod_settings[ $this->get_stylesheet() ] );
   3567 			$this->update_stashed_theme_mod_settings( $other_theme_mod_settings );
   3568 		}
   3569 
   3570 		/**
   3571 		 * Fires after Customize settings have been saved.
   3572 		 *
   3573 		 * @since 3.6.0
   3574 		 *
   3575 		 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
   3576 		 */
   3577 		do_action( 'customize_save_after', $this );
   3578 
   3579 		// Restore original capabilities.
   3580 		foreach ( $original_setting_capabilities as $setting_id => $capability ) {
   3581 			$setting = $this->get_setting( $setting_id );
   3582 			if ( $setting ) {
   3583 				$setting->capability = $capability;
   3584 			}
   3585 		}
   3586 
   3587 		// Restore original changeset data.
   3588 		$this->_changeset_data    = $previous_changeset_data;
   3589 		$this->_changeset_post_id = $previous_changeset_post_id;
   3590 		$this->_changeset_uuid    = $previous_changeset_uuid;
   3591 
   3592 		/*
   3593 		 * Convert all autosave revisions into their own auto-drafts so that users can be prompted to
   3594 		 * restore them when a changeset is published, but they had been locked out from including
   3595 		 * their changes in the changeset.
   3596 		 */
   3597 		$revisions = wp_get_post_revisions( $changeset_post_id, array( 'check_enabled' => false ) );
   3598 		foreach ( $revisions as $revision ) {
   3599 			if ( false !== strpos( $revision->post_name, "{$changeset_post_id}-autosave" ) ) {
   3600 				$wpdb->update(
   3601 					$wpdb->posts,
   3602 					array(
   3603 						'post_status' => 'auto-draft',
   3604 						'post_type'   => 'customize_changeset',
   3605 						'post_name'   => wp_generate_uuid4(),
   3606 						'post_parent' => 0,
   3607 					),
   3608 					array(
   3609 						'ID' => $revision->ID,
   3610 					)
   3611 				);
   3612 				clean_post_cache( $revision->ID );
   3613 			}
   3614 		}
   3615 
   3616 		return true;
   3617 	}
   3618 
   3619 	/**
   3620 	 * Update stashed theme mod settings.
   3621 	 *
   3622 	 * @since 4.7.0
   3623 	 *
   3624 	 * @param array $inactive_theme_mod_settings Mapping of stylesheet to arrays of theme mod settings.
   3625 	 * @return array|false Returns array of updated stashed theme mods or false if the update failed or there were no changes.
   3626 	 */
   3627 	protected function update_stashed_theme_mod_settings( $inactive_theme_mod_settings ) {
   3628 		$stashed_theme_mod_settings = get_option( 'customize_stashed_theme_mods' );
   3629 		if ( empty( $stashed_theme_mod_settings ) ) {
   3630 			$stashed_theme_mod_settings = array();
   3631 		}
   3632 
   3633 		// Delete any stashed theme mods for the active theme since they would have been loaded and saved upon activation.
   3634 		unset( $stashed_theme_mod_settings[ $this->get_stylesheet() ] );
   3635 
   3636 		// Merge inactive theme mods with the stashed theme mod settings.
   3637 		foreach ( $inactive_theme_mod_settings as $stylesheet => $theme_mod_settings ) {
   3638 			if ( ! isset( $stashed_theme_mod_settings[ $stylesheet ] ) ) {
   3639 				$stashed_theme_mod_settings[ $stylesheet ] = array();
   3640 			}
   3641 
   3642 			$stashed_theme_mod_settings[ $stylesheet ] = array_merge(
   3643 				$stashed_theme_mod_settings[ $stylesheet ],
   3644 				$theme_mod_settings
   3645 			);
   3646 		}
   3647 
   3648 		$autoload = false;
   3649 		$result   = update_option( 'customize_stashed_theme_mods', $stashed_theme_mod_settings, $autoload );
   3650 		if ( ! $result ) {
   3651 			return false;
   3652 		}
   3653 		return $stashed_theme_mod_settings;
   3654 	}
   3655 
   3656 	/**
   3657 	 * Refresh nonces for the current preview.
   3658 	 *
   3659 	 * @since 4.2.0
   3660 	 */
   3661 	public function refresh_nonces() {
   3662 		if ( ! $this->is_preview() ) {
   3663 			wp_send_json_error( 'not_preview' );
   3664 		}
   3665 
   3666 		wp_send_json_success( $this->get_nonces() );
   3667 	}
   3668 
   3669 	/**
   3670 	 * Delete a given auto-draft changeset or the autosave revision for a given changeset or delete changeset lock.
   3671 	 *
   3672 	 * @since 4.9.0
   3673 	 */
   3674 	public function handle_dismiss_autosave_or_lock_request() {
   3675 		// Calls to dismiss_user_auto_draft_changesets() and wp_get_post_autosave() require non-zero get_current_user_id().
   3676 		if ( ! is_user_logged_in() ) {
   3677 			wp_send_json_error( 'unauthenticated', 401 );
   3678 		}
   3679 
   3680 		if ( ! $this->is_preview() ) {
   3681 			wp_send_json_error( 'not_preview', 400 );
   3682 		}
   3683 
   3684 		if ( ! check_ajax_referer( 'customize_dismiss_autosave_or_lock', 'nonce', false ) ) {
   3685 			wp_send_json_error( 'invalid_nonce', 403 );
   3686 		}
   3687 
   3688 		$changeset_post_id = $this->changeset_post_id();
   3689 		$dismiss_lock      = ! empty( $_POST['dismiss_lock'] );
   3690 		$dismiss_autosave  = ! empty( $_POST['dismiss_autosave'] );
   3691 
   3692 		if ( $dismiss_lock ) {
   3693 			if ( empty( $changeset_post_id ) && ! $dismiss_autosave ) {
   3694 				wp_send_json_error( 'no_changeset_to_dismiss_lock', 404 );
   3695 			}
   3696 			if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) && ! $dismiss_autosave ) {
   3697 				wp_send_json_error( 'cannot_remove_changeset_lock', 403 );
   3698 			}
   3699 
   3700 			delete_post_meta( $changeset_post_id, '_edit_lock' );
   3701 
   3702 			if ( ! $dismiss_autosave ) {
   3703 				wp_send_json_success( 'changeset_lock_dismissed' );
   3704 			}
   3705 		}
   3706 
   3707 		if ( $dismiss_autosave ) {
   3708 			if ( empty( $changeset_post_id ) || 'auto-draft' === get_post_status( $changeset_post_id ) ) {
   3709 				$dismissed = $this->dismiss_user_auto_draft_changesets();
   3710 				if ( $dismissed > 0 ) {
   3711 					wp_send_json_success( 'auto_draft_dismissed' );
   3712 				} else {
   3713 					wp_send_json_error( 'no_auto_draft_to_delete', 404 );
   3714 				}
   3715 			} else {
   3716 				$revision = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
   3717 
   3718 				if ( $revision ) {
   3719 					if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) {
   3720 						wp_send_json_error( 'cannot_delete_autosave_revision', 403 );
   3721 					}
   3722 
   3723 					if ( ! wp_delete_post( $revision->ID, true ) ) {
   3724 						wp_send_json_error( 'autosave_revision_deletion_failure', 500 );
   3725 					} else {
   3726 						wp_send_json_success( 'autosave_revision_deleted' );
   3727 					}
   3728 				} else {
   3729 					wp_send_json_error( 'no_autosave_revision_to_delete', 404 );
   3730 				}
   3731 			}
   3732 		}
   3733 
   3734 		wp_send_json_error( 'unknown_error', 500 );
   3735 	}
   3736 
   3737 	/**
   3738 	 * Add a customize setting.
   3739 	 *
   3740 	 * @since 3.4.0
   3741 	 * @since 4.5.0 Return added WP_Customize_Setting instance.
   3742 	 *
   3743 	 * @see WP_Customize_Setting::__construct()
   3744 	 * @link https://developer.wordpress.org/themes/customize-api
   3745 	 *
   3746 	 * @param WP_Customize_Setting|string $id   Customize Setting object, or ID.
   3747 	 * @param array                       $args Optional. Array of properties for the new Setting object.
   3748 	 *                                          See WP_Customize_Setting::__construct() for information
   3749 	 *                                          on accepted arguments. Default empty array.
   3750 	 * @return WP_Customize_Setting The instance of the setting that was added.
   3751 	 */
   3752 	public function add_setting( $id, $args = array() ) {
   3753 		if ( $id instanceof WP_Customize_Setting ) {
   3754 			$setting = $id;
   3755 		} else {
   3756 			$class = 'WP_Customize_Setting';
   3757 
   3758 			/** This filter is documented in wp-includes/class-wp-customize-manager.php */
   3759 			$args = apply_filters( 'customize_dynamic_setting_args', $args, $id );
   3760 
   3761 			/** This filter is documented in wp-includes/class-wp-customize-manager.php */
   3762 			$class = apply_filters( 'customize_dynamic_setting_class', $class, $id, $args );
   3763 
   3764 			$setting = new $class( $this, $id, $args );
   3765 		}
   3766 
   3767 		$this->settings[ $setting->id ] = $setting;
   3768 		return $setting;
   3769 	}
   3770 
   3771 	/**
   3772 	 * Register any dynamically-created settings, such as those from $_POST['customized']
   3773 	 * that have no corresponding setting created.
   3774 	 *
   3775 	 * This is a mechanism to "wake up" settings that have been dynamically created
   3776 	 * on the front end and have been sent to WordPress in `$_POST['customized']`. When WP
   3777 	 * loads, the dynamically-created settings then will get created and previewed
   3778 	 * even though they are not directly created statically with code.
   3779 	 *
   3780 	 * @since 4.2.0
   3781 	 *
   3782 	 * @param array $setting_ids The setting IDs to add.
   3783 	 * @return array The WP_Customize_Setting objects added.
   3784 	 */
   3785 	public function add_dynamic_settings( $setting_ids ) {
   3786 		$new_settings = array();
   3787 		foreach ( $setting_ids as $setting_id ) {
   3788 			// Skip settings already created.
   3789 			if ( $this->get_setting( $setting_id ) ) {
   3790 				continue;
   3791 			}
   3792 
   3793 			$setting_args  = false;
   3794 			$setting_class = 'WP_Customize_Setting';
   3795 
   3796 			/**
   3797 			 * Filters a dynamic setting's constructor args.
   3798 			 *
   3799 			 * For a dynamic setting to be registered, this filter must be employed
   3800 			 * to override the default false value with an array of args to pass to
   3801 			 * the WP_Customize_Setting constructor.
   3802 			 *
   3803 			 * @since 4.2.0
   3804 			 *
   3805 			 * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
   3806 			 * @param string      $setting_id   ID for dynamic setting, usually coming from `$_POST['customized']`.
   3807 			 */
   3808 			$setting_args = apply_filters( 'customize_dynamic_setting_args', $setting_args, $setting_id );
   3809 			if ( false === $setting_args ) {
   3810 				continue;
   3811 			}
   3812 
   3813 			/**
   3814 			 * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
   3815 			 *
   3816 			 * @since 4.2.0
   3817 			 *
   3818 			 * @param string $setting_class WP_Customize_Setting or a subclass.
   3819 			 * @param string $setting_id    ID for dynamic setting, usually coming from `$_POST['customized']`.
   3820 			 * @param array  $setting_args  WP_Customize_Setting or a subclass.
   3821 			 */
   3822 			$setting_class = apply_filters( 'customize_dynamic_setting_class', $setting_class, $setting_id, $setting_args );
   3823 
   3824 			$setting = new $setting_class( $this, $setting_id, $setting_args );
   3825 
   3826 			$this->add_setting( $setting );
   3827 			$new_settings[] = $setting;
   3828 		}
   3829 		return $new_settings;
   3830 	}
   3831 
   3832 	/**
   3833 	 * Retrieve a customize setting.
   3834 	 *
   3835 	 * @since 3.4.0
   3836 	 *
   3837 	 * @param string $id Customize Setting ID.
   3838 	 * @return WP_Customize_Setting|void The setting, if set.
   3839 	 */
   3840 	public function get_setting( $id ) {
   3841 		if ( isset( $this->settings[ $id ] ) ) {
   3842 			return $this->settings[ $id ];
   3843 		}
   3844 	}
   3845 
   3846 	/**
   3847 	 * Remove a customize setting.
   3848 	 *
   3849 	 * Note that removing the setting doesn't destroy the WP_Customize_Setting instance or remove its filters.
   3850 	 *
   3851 	 * @since 3.4.0
   3852 	 *
   3853 	 * @param string $id Customize Setting ID.
   3854 	 */
   3855 	public function remove_setting( $id ) {
   3856 		unset( $this->settings[ $id ] );
   3857 	}
   3858 
   3859 	/**
   3860 	 * Add a customize panel.
   3861 	 *
   3862 	 * @since 4.0.0
   3863 	 * @since 4.5.0 Return added WP_Customize_Panel instance.
   3864 	 *
   3865 	 * @see WP_Customize_Panel::__construct()
   3866 	 *
   3867 	 * @param WP_Customize_Panel|string $id   Customize Panel object, or ID.
   3868 	 * @param array                     $args Optional. Array of properties for the new Panel object.
   3869 	 *                                        See WP_Customize_Panel::__construct() for information
   3870 	 *                                        on accepted arguments. Default empty array.
   3871 	 * @return WP_Customize_Panel The instance of the panel that was added.
   3872 	 */
   3873 	public function add_panel( $id, $args = array() ) {
   3874 		if ( $id instanceof WP_Customize_Panel ) {
   3875 			$panel = $id;
   3876 		} else {
   3877 			$panel = new WP_Customize_Panel( $this, $id, $args );
   3878 		}
   3879 
   3880 		$this->panels[ $panel->id ] = $panel;
   3881 		return $panel;
   3882 	}
   3883 
   3884 	/**
   3885 	 * Retrieve a customize panel.
   3886 	 *
   3887 	 * @since 4.0.0
   3888 	 *
   3889 	 * @param string $id Panel ID to get.
   3890 	 * @return WP_Customize_Panel|void Requested panel instance, if set.
   3891 	 */
   3892 	public function get_panel( $id ) {
   3893 		if ( isset( $this->panels[ $id ] ) ) {
   3894 			return $this->panels[ $id ];
   3895 		}
   3896 	}
   3897 
   3898 	/**
   3899 	 * Remove a customize panel.
   3900 	 *
   3901 	 * Note that removing the panel doesn't destroy the WP_Customize_Panel instance or remove its filters.
   3902 	 *
   3903 	 * @since 4.0.0
   3904 	 *
   3905 	 * @param string $id Panel ID to remove.
   3906 	 */
   3907 	public function remove_panel( $id ) {
   3908 		// Removing core components this way is _doing_it_wrong().
   3909 		if ( in_array( $id, $this->components, true ) ) {
   3910 			_doing_it_wrong(
   3911 				__METHOD__,
   3912 				sprintf(
   3913 					/* translators: 1: Panel ID, 2: Link to 'customize_loaded_components' filter reference. */
   3914 					__( 'Removing %1$s manually will cause PHP warnings. Use the %2$s filter instead.' ),
   3915 					$id,
   3916 					sprintf(
   3917 						'<a href="%1$s">%2$s</a>',
   3918 						esc_url( 'https://developer.wordpress.org/reference/hooks/customize_loaded_components/' ),
   3919 						'<code>customize_loaded_components</code>'
   3920 					)
   3921 				),
   3922 				'4.5.0'
   3923 			);
   3924 		}
   3925 		unset( $this->panels[ $id ] );
   3926 	}
   3927 
   3928 	/**
   3929 	 * Register a customize panel type.
   3930 	 *
   3931 	 * Registered types are eligible to be rendered via JS and created dynamically.
   3932 	 *
   3933 	 * @since 4.3.0
   3934 	 *
   3935 	 * @see WP_Customize_Panel
   3936 	 *
   3937 	 * @param string $panel Name of a custom panel which is a subclass of WP_Customize_Panel.
   3938 	 */
   3939 	public function register_panel_type( $panel ) {
   3940 		$this->registered_panel_types[] = $panel;
   3941 	}
   3942 
   3943 	/**
   3944 	 * Render JS templates for all registered panel types.
   3945 	 *
   3946 	 * @since 4.3.0
   3947 	 */
   3948 	public function render_panel_templates() {
   3949 		foreach ( $this->registered_panel_types as $panel_type ) {
   3950 			$panel = new $panel_type( $this, 'temp', array() );
   3951 			$panel->print_template();
   3952 		}
   3953 	}
   3954 
   3955 	/**
   3956 	 * Add a customize section.
   3957 	 *
   3958 	 * @since 3.4.0
   3959 	 * @since 4.5.0 Return added WP_Customize_Section instance.
   3960 	 *
   3961 	 * @see WP_Customize_Section::__construct()
   3962 	 *
   3963 	 * @param WP_Customize_Section|string $id   Customize Section object, or ID.
   3964 	 * @param array                       $args Optional. Array of properties for the new Section object.
   3965 	 *                                          See WP_Customize_Section::__construct() for information
   3966 	 *                                          on accepted arguments. Default empty array.
   3967 	 * @return WP_Customize_Section The instance of the section that was added.
   3968 	 */
   3969 	public function add_section( $id, $args = array() ) {
   3970 		if ( $id instanceof WP_Customize_Section ) {
   3971 			$section = $id;
   3972 		} else {
   3973 			$section = new WP_Customize_Section( $this, $id, $args );
   3974 		}
   3975 
   3976 		$this->sections[ $section->id ] = $section;
   3977 		return $section;
   3978 	}
   3979 
   3980 	/**
   3981 	 * Retrieve a customize section.
   3982 	 *
   3983 	 * @since 3.4.0
   3984 	 *
   3985 	 * @param string $id Section ID.
   3986 	 * @return WP_Customize_Section|void The section, if set.
   3987 	 */
   3988 	public function get_section( $id ) {
   3989 		if ( isset( $this->sections[ $id ] ) ) {
   3990 			return $this->sections[ $id ];
   3991 		}
   3992 	}
   3993 
   3994 	/**
   3995 	 * Remove a customize section.
   3996 	 *
   3997 	 * Note that removing the section doesn't destroy the WP_Customize_Section instance or remove its filters.
   3998 	 *
   3999 	 * @since 3.4.0
   4000 	 *
   4001 	 * @param string $id Section ID.
   4002 	 */
   4003 	public function remove_section( $id ) {
   4004 		unset( $this->sections[ $id ] );
   4005 	}
   4006 
   4007 	/**
   4008 	 * Register a customize section type.
   4009 	 *
   4010 	 * Registered types are eligible to be rendered via JS and created dynamically.
   4011 	 *
   4012 	 * @since 4.3.0
   4013 	 *
   4014 	 * @see WP_Customize_Section
   4015 	 *
   4016 	 * @param string $section Name of a custom section which is a subclass of WP_Customize_Section.
   4017 	 */
   4018 	public function register_section_type( $section ) {
   4019 		$this->registered_section_types[] = $section;
   4020 	}
   4021 
   4022 	/**
   4023 	 * Render JS templates for all registered section types.
   4024 	 *
   4025 	 * @since 4.3.0
   4026 	 */
   4027 	public function render_section_templates() {
   4028 		foreach ( $this->registered_section_types as $section_type ) {
   4029 			$section = new $section_type( $this, 'temp', array() );
   4030 			$section->print_template();
   4031 		}
   4032 	}
   4033 
   4034 	/**
   4035 	 * Add a customize control.
   4036 	 *
   4037 	 * @since 3.4.0
   4038 	 * @since 4.5.0 Return added WP_Customize_Control instance.
   4039 	 *
   4040 	 * @see WP_Customize_Control::__construct()
   4041 	 *
   4042 	 * @param WP_Customize_Control|string $id   Customize Control object, or ID.
   4043 	 * @param array                       $args Optional. Array of properties for the new Control object.
   4044 	 *                                          See WP_Customize_Control::__construct() for information
   4045 	 *                                          on accepted arguments. Default empty array.
   4046 	 * @return WP_Customize_Control The instance of the control that was added.
   4047 	 */
   4048 	public function add_control( $id, $args = array() ) {
   4049 		if ( $id instanceof WP_Customize_Control ) {
   4050 			$control = $id;
   4051 		} else {
   4052 			$control = new WP_Customize_Control( $this, $id, $args );
   4053 		}
   4054 
   4055 		$this->controls[ $control->id ] = $control;
   4056 		return $control;
   4057 	}
   4058 
   4059 	/**
   4060 	 * Retrieve a customize control.
   4061 	 *
   4062 	 * @since 3.4.0
   4063 	 *
   4064 	 * @param string $id ID of the control.
   4065 	 * @return WP_Customize_Control|void The control object, if set.
   4066 	 */
   4067 	public function get_control( $id ) {
   4068 		if ( isset( $this->controls[ $id ] ) ) {
   4069 			return $this->controls[ $id ];
   4070 		}
   4071 	}
   4072 
   4073 	/**
   4074 	 * Remove a customize control.
   4075 	 *
   4076 	 * Note that removing the control doesn't destroy the WP_Customize_Control instance or remove its filters.
   4077 	 *
   4078 	 * @since 3.4.0
   4079 	 *
   4080 	 * @param string $id ID of the control.
   4081 	 */
   4082 	public function remove_control( $id ) {
   4083 		unset( $this->controls[ $id ] );
   4084 	}
   4085 
   4086 	/**
   4087 	 * Register a customize control type.
   4088 	 *
   4089 	 * Registered types are eligible to be rendered via JS and created dynamically.
   4090 	 *
   4091 	 * @since 4.1.0
   4092 	 *
   4093 	 * @param string $control Name of a custom control which is a subclass of
   4094 	 *                        WP_Customize_Control.
   4095 	 */
   4096 	public function register_control_type( $control ) {
   4097 		$this->registered_control_types[] = $control;
   4098 	}
   4099 
   4100 	/**
   4101 	 * Render JS templates for all registered control types.
   4102 	 *
   4103 	 * @since 4.1.0
   4104 	 */
   4105 	public function render_control_templates() {
   4106 		if ( $this->branching() ) {
   4107 			$l10n = array(
   4108 				/* translators: %s: User who is customizing the changeset in customizer. */
   4109 				'locked'                => __( '%s is already customizing this changeset. Please wait until they are done to try customizing. Your latest changes have been autosaved.' ),
   4110 				/* translators: %s: User who is customizing the changeset in customizer. */
   4111 				'locked_allow_override' => __( '%s is already customizing this changeset. Do you want to take over?' ),
   4112 			);
   4113 		} else {
   4114 			$l10n = array(
   4115 				/* translators: %s: User who is customizing the changeset in customizer. */
   4116 				'locked'                => __( '%s is already customizing this site. Please wait until they are done to try customizing. Your latest changes have been autosaved.' ),
   4117 				/* translators: %s: User who is customizing the changeset in customizer. */
   4118 				'locked_allow_override' => __( '%s is already customizing this site. Do you want to take over?' ),
   4119 			);
   4120 		}
   4121 
   4122 		foreach ( $this->registered_control_types as $control_type ) {
   4123 			$control = new $control_type(
   4124 				$this,
   4125 				'temp',
   4126 				array(
   4127 					'settings' => array(),
   4128 				)
   4129 			);
   4130 			$control->print_template();
   4131 		}
   4132 		?>
   4133 
   4134 		<script type="text/html" id="tmpl-customize-control-default-content">
   4135 			<#
   4136 			var inputId = _.uniqueId( 'customize-control-default-input-' );
   4137 			var descriptionId = _.uniqueId( 'customize-control-default-description-' );
   4138 			var describedByAttr = data.description ? ' aria-describedby="' + descriptionId + '" ' : '';
   4139 			#>
   4140 			<# switch ( data.type ) {
   4141 				case 'checkbox': #>
   4142 					<span class="customize-inside-control-row">
   4143 						<input
   4144 							id="{{ inputId }}"
   4145 							{{{ describedByAttr }}}
   4146 							type="checkbox"
   4147 							value="{{ data.value }}"
   4148 							data-customize-setting-key-link="default"
   4149 						>
   4150 						<label for="{{ inputId }}">
   4151 							{{ data.label }}
   4152 						</label>
   4153 						<# if ( data.description ) { #>
   4154 							<span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span>
   4155 						<# } #>
   4156 					</span>
   4157 					<#
   4158 					break;
   4159 				case 'radio':
   4160 					if ( ! data.choices ) {
   4161 						return;
   4162 					}
   4163 					#>
   4164 					<# if ( data.label ) { #>
   4165 						<label for="{{ inputId }}" class="customize-control-title">
   4166 							{{ data.label }}
   4167 						</label>
   4168 					<# } #>
   4169 					<# if ( data.description ) { #>
   4170 						<span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span>
   4171 					<# } #>
   4172 					<# _.each( data.choices, function( val, key ) { #>
   4173 						<span class="customize-inside-control-row">
   4174 							<#
   4175 							var value, text;
   4176 							if ( _.isObject( val ) ) {
   4177 								value = val.value;
   4178 								text = val.text;
   4179 							} else {
   4180 								value = key;
   4181 								text = val;
   4182 							}
   4183 							#>
   4184 							<input
   4185 								id="{{ inputId + '-' + value }}"
   4186 								type="radio"
   4187 								value="{{ value }}"
   4188 								name="{{ inputId }}"
   4189 								data-customize-setting-key-link="default"
   4190 								{{{ describedByAttr }}}
   4191 							>
   4192 							<label for="{{ inputId + '-' + value }}">{{ text }}</label>
   4193 						</span>
   4194 					<# } ); #>
   4195 					<#
   4196 					break;
   4197 				default:
   4198 					#>
   4199 					<# if ( data.label ) { #>
   4200 						<label for="{{ inputId }}" class="customize-control-title">
   4201 							{{ data.label }}
   4202 						</label>
   4203 					<# } #>
   4204 					<# if ( data.description ) { #>
   4205 						<span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span>
   4206 					<# } #>
   4207 
   4208 					<#
   4209 					var inputAttrs = {
   4210 						id: inputId,
   4211 						'data-customize-setting-key-link': 'default'
   4212 					};
   4213 					if ( 'textarea' === data.type ) {
   4214 						inputAttrs.rows = '5';
   4215 					} else if ( 'button' === data.type ) {
   4216 						inputAttrs['class'] = 'button button-secondary';
   4217 						inputAttrs.type = 'button';
   4218 					} else {
   4219 						inputAttrs.type = data.type;
   4220 					}
   4221 					if ( data.description ) {
   4222 						inputAttrs['aria-describedby'] = descriptionId;
   4223 					}
   4224 					_.extend( inputAttrs, data.input_attrs );
   4225 					#>
   4226 
   4227 					<# if ( 'button' === data.type ) { #>
   4228 						<button
   4229 							<# _.each( _.extend( inputAttrs ), function( value, key ) { #>
   4230 								{{{ key }}}="{{ value }}"
   4231 							<# } ); #>
   4232 						>{{ inputAttrs.value }}</button>
   4233 					<# } else if ( 'textarea' === data.type ) { #>
   4234 						<textarea
   4235 							<# _.each( _.extend( inputAttrs ), function( value, key ) { #>
   4236 								{{{ key }}}="{{ value }}"
   4237 							<# }); #>
   4238 						>{{ inputAttrs.value }}</textarea>
   4239 					<# } else if ( 'select' === data.type ) { #>
   4240 						<# delete inputAttrs.type; #>
   4241 						<select
   4242 							<# _.each( _.extend( inputAttrs ), function( value, key ) { #>
   4243 								{{{ key }}}="{{ value }}"
   4244 							<# }); #>
   4245 							>
   4246 							<# _.each( data.choices, function( val, key ) { #>
   4247 								<#
   4248 								var value, text;
   4249 								if ( _.isObject( val ) ) {
   4250 									value = val.value;
   4251 									text = val.text;
   4252 								} else {
   4253 									value = key;
   4254 									text = val;
   4255 								}
   4256 								#>
   4257 								<option value="{{ value }}">{{ text }}</option>
   4258 							<# } ); #>
   4259 						</select>
   4260 					<# } else { #>
   4261 						<input
   4262 							<# _.each( _.extend( inputAttrs ), function( value, key ) { #>
   4263 								{{{ key }}}="{{ value }}"
   4264 							<# }); #>
   4265 							>
   4266 					<# } #>
   4267 			<# } #>
   4268 		</script>
   4269 
   4270 		<script type="text/html" id="tmpl-customize-notification">
   4271 			<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 }}">
   4272 				<div class="notification-message">{{{ data.message || data.code }}}</div>
   4273 				<# if ( data.dismissible ) { #>
   4274 					<button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php _e( 'Dismiss' ); ?></span></button>
   4275 				<# } #>
   4276 			</li>
   4277 		</script>
   4278 
   4279 		<script type="text/html" id="tmpl-customize-changeset-locked-notification">
   4280 			<li class="notice notice-{{ data.type || 'info' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
   4281 				<div class="notification-message customize-changeset-locked-message">
   4282 					<img class="customize-changeset-locked-avatar" src="{{ data.lockUser.avatar }}" alt="{{ data.lockUser.name }}" />
   4283 					<p class="currently-editing">
   4284 						<# if ( data.message ) { #>
   4285 							{{{ data.message }}}
   4286 						<# } else if ( data.allowOverride ) { #>
   4287 							<?php
   4288 							echo esc_html( sprintf( $l10n['locked_allow_override'], '{{ data.lockUser.name }}' ) );
   4289 							?>
   4290 						<# } else { #>
   4291 							<?php
   4292 							echo esc_html( sprintf( $l10n['locked'], '{{ data.lockUser.name }}' ) );
   4293 							?>
   4294 						<# } #>
   4295 					</p>
   4296 					<p class="notice notice-error notice-alt" hidden></p>
   4297 					<p class="action-buttons">
   4298 						<# if ( data.returnUrl !== data.previewUrl ) { #>
   4299 							<a class="button customize-notice-go-back-button" href="{{ data.returnUrl }}"><?php _e( 'Go back' ); ?></a>
   4300 						<# } #>
   4301 						<a class="button customize-notice-preview-button" href="{{ data.frontendPreviewUrl }}"><?php _e( 'Preview' ); ?></a>
   4302 						<# if ( data.allowOverride ) { #>
   4303 							<button class="button button-primary wp-tab-last customize-notice-take-over-button"><?php _e( 'Take over' ); ?></button>
   4304 						<# } #>
   4305 					</p>
   4306 				</div>
   4307 			</li>
   4308 		</script>
   4309 
   4310 		<script type="text/html" id="tmpl-customize-code-editor-lint-error-notification">
   4311 			<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 }}">
   4312 				<div class="notification-message">{{{ data.message || data.code }}}</div>
   4313 
   4314 				<p>
   4315 					<# var elementId = 'el-' + String( Math.random() ); #>
   4316 					<input id="{{ elementId }}" type="checkbox">
   4317 					<label for="{{ elementId }}"><?php _e( 'Update anyway, even though it might break your site?' ); ?></label>
   4318 				</p>
   4319 			</li>
   4320 		</script>
   4321 
   4322 		<?php
   4323 		/* The following template is obsolete in core but retained for plugins. */
   4324 		?>
   4325 		<script type="text/html" id="tmpl-customize-control-notifications">
   4326 			<ul>
   4327 				<# _.each( data.notifications, function( notification ) { #>
   4328 					<li class="notice notice-{{ notification.type || 'info' }} {{ data.altNotice ? 'notice-alt' : '' }}" data-code="{{ notification.code }}" data-type="{{ notification.type }}">{{{ notification.message || notification.code }}}</li>
   4329 				<# } ); #>
   4330 			</ul>
   4331 		</script>
   4332 
   4333 		<script type="text/html" id="tmpl-customize-preview-link-control" >
   4334 			<# var elementPrefix = _.uniqueId( 'el' ) + '-' #>
   4335 			<p class="customize-control-title">
   4336 				<?php esc_html_e( 'Share Preview Link' ); ?>
   4337 			</p>
   4338 			<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>
   4339 			<div class="customize-control-notifications-container"></div>
   4340 			<div class="preview-link-wrapper">
   4341 				<label for="{{ elementPrefix }}customize-preview-link-input" class="screen-reader-text"><?php esc_html_e( 'Preview Link' ); ?></label>
   4342 				<a href="" target="">
   4343 					<span class="preview-control-element" data-component="url"></span>
   4344 					<span class="screen-reader-text"><?php _e( '(opens in a new tab)' ); ?></span>
   4345 				</a>
   4346 				<input id="{{ elementPrefix }}customize-preview-link-input" readonly tabindex="-1" class="preview-control-element" data-component="input">
   4347 				<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>
   4348 			</div>
   4349 		</script>
   4350 		<script type="text/html" id="tmpl-customize-selected-changeset-status-control">
   4351 			<# var inputId = _.uniqueId( 'customize-selected-changeset-status-control-input-' ); #>
   4352 			<# var descriptionId = _.uniqueId( 'customize-selected-changeset-status-control-description-' ); #>
   4353 			<# if ( data.label ) { #>
   4354 				<label for="{{ inputId }}" class="customize-control-title">{{ data.label }}</label>
   4355 			<# } #>
   4356 			<# if ( data.description ) { #>
   4357 				<span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span>
   4358 			<# } #>
   4359 			<# _.each( data.choices, function( choice ) { #>
   4360 				<# var choiceId = inputId + '-' + choice.status; #>
   4361 				<span class="customize-inside-control-row">
   4362 					<input id="{{ choiceId }}" type="radio" value="{{ choice.status }}" name="{{ inputId }}" data-customize-setting-key-link="default">
   4363 					<label for="{{ choiceId }}">{{ choice.label }}</label>
   4364 				</span>
   4365 			<# } ); #>
   4366 		</script>
   4367 		<?php
   4368 	}
   4369 
   4370 	/**
   4371 	 * Helper function to compare two objects by priority, ensuring sort stability via instance_number.
   4372 	 *
   4373 	 * @since 3.4.0
   4374 	 * @deprecated 4.7.0 Use wp_list_sort()
   4375 	 *
   4376 	 * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $a Object A.
   4377 	 * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $b Object B.
   4378 	 * @return int
   4379 	 */
   4380 	protected function _cmp_priority( $a, $b ) {
   4381 		_deprecated_function( __METHOD__, '4.7.0', 'wp_list_sort' );
   4382 
   4383 		if ( $a->priority === $b->priority ) {
   4384 			return $a->instance_number - $b->instance_number;
   4385 		} else {
   4386 			return $a->priority - $b->priority;
   4387 		}
   4388 	}
   4389 
   4390 	/**
   4391 	 * Prepare panels, sections, and controls.
   4392 	 *
   4393 	 * For each, check if required related components exist,
   4394 	 * whether the user has the necessary capabilities,
   4395 	 * and sort by priority.
   4396 	 *
   4397 	 * @since 3.4.0
   4398 	 */
   4399 	public function prepare_controls() {
   4400 
   4401 		$controls       = array();
   4402 		$this->controls = wp_list_sort(
   4403 			$this->controls,
   4404 			array(
   4405 				'priority'        => 'ASC',
   4406 				'instance_number' => 'ASC',
   4407 			),
   4408 			'ASC',
   4409 			true
   4410 		);
   4411 
   4412 		foreach ( $this->controls as $id => $control ) {
   4413 			if ( ! isset( $this->sections[ $control->section ] ) || ! $control->check_capabilities() ) {
   4414 				continue;
   4415 			}
   4416 
   4417 			$this->sections[ $control->section ]->controls[] = $control;
   4418 			$controls[ $id ]                                 = $control;
   4419 		}
   4420 		$this->controls = $controls;
   4421 
   4422 		// Prepare sections.
   4423 		$this->sections = wp_list_sort(
   4424 			$this->sections,
   4425 			array(
   4426 				'priority'        => 'ASC',
   4427 				'instance_number' => 'ASC',
   4428 			),
   4429 			'ASC',
   4430 			true
   4431 		);
   4432 		$sections       = array();
   4433 
   4434 		foreach ( $this->sections as $section ) {
   4435 			if ( ! $section->check_capabilities() ) {
   4436 				continue;
   4437 			}
   4438 
   4439 			$section->controls = wp_list_sort(
   4440 				$section->controls,
   4441 				array(
   4442 					'priority'        => 'ASC',
   4443 					'instance_number' => 'ASC',
   4444 				)
   4445 			);
   4446 
   4447 			if ( ! $section->panel ) {
   4448 				// Top-level section.
   4449 				$sections[ $section->id ] = $section;
   4450 			} else {
   4451 				// This section belongs to a panel.
   4452 				if ( isset( $this->panels [ $section->panel ] ) ) {
   4453 					$this->panels[ $section->panel ]->sections[ $section->id ] = $section;
   4454 				}
   4455 			}
   4456 		}
   4457 		$this->sections = $sections;
   4458 
   4459 		// Prepare panels.
   4460 		$this->panels = wp_list_sort(
   4461 			$this->panels,
   4462 			array(
   4463 				'priority'        => 'ASC',
   4464 				'instance_number' => 'ASC',
   4465 			),
   4466 			'ASC',
   4467 			true
   4468 		);
   4469 		$panels       = array();
   4470 
   4471 		foreach ( $this->panels as $panel ) {
   4472 			if ( ! $panel->check_capabilities() ) {
   4473 				continue;
   4474 			}
   4475 
   4476 			$panel->sections      = wp_list_sort(
   4477 				$panel->sections,
   4478 				array(
   4479 					'priority'        => 'ASC',
   4480 					'instance_number' => 'ASC',
   4481 				),
   4482 				'ASC',
   4483 				true
   4484 			);
   4485 			$panels[ $panel->id ] = $panel;
   4486 		}
   4487 		$this->panels = $panels;
   4488 
   4489 		// Sort panels and top-level sections together.
   4490 		$this->containers = array_merge( $this->panels, $this->sections );
   4491 		$this->containers = wp_list_sort(
   4492 			$this->containers,
   4493 			array(
   4494 				'priority'        => 'ASC',
   4495 				'instance_number' => 'ASC',
   4496 			),
   4497 			'ASC',
   4498 			true
   4499 		);
   4500 	}
   4501 
   4502 	/**
   4503 	 * Enqueue scripts for customize controls.
   4504 	 *
   4505 	 * @since 3.4.0
   4506 	 */
   4507 	public function enqueue_control_scripts() {
   4508 		foreach ( $this->controls as $control ) {
   4509 			$control->enqueue();
   4510 		}
   4511 
   4512 		if ( ! is_multisite() && ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) || current_user_can( 'delete_themes' ) ) ) {
   4513 			wp_enqueue_script( 'updates' );
   4514 			wp_localize_script(
   4515 				'updates',
   4516 				'_wpUpdatesItemCounts',
   4517 				array(
   4518 					'totals' => wp_get_update_data(),
   4519 				)
   4520 			);
   4521 		}
   4522 	}
   4523 
   4524 	/**
   4525 	 * Determine whether the user agent is iOS.
   4526 	 *
   4527 	 * @since 4.4.0
   4528 	 *
   4529 	 * @return bool Whether the user agent is iOS.
   4530 	 */
   4531 	public function is_ios() {
   4532 		return wp_is_mobile() && preg_match( '/iPad|iPod|iPhone/', $_SERVER['HTTP_USER_AGENT'] );
   4533 	}
   4534 
   4535 	/**
   4536 	 * Get the template string for the Customizer pane document title.
   4537 	 *
   4538 	 * @since 4.4.0
   4539 	 *
   4540 	 * @return string The template string for the document title.
   4541 	 */
   4542 	public function get_document_title_template() {
   4543 		if ( $this->is_theme_active() ) {
   4544 			/* translators: %s: Document title from the preview. */
   4545 			$document_title_tmpl = __( 'Customize: %s' );
   4546 		} else {
   4547 			/* translators: %s: Document title from the preview. */
   4548 			$document_title_tmpl = __( 'Live Preview: %s' );
   4549 		}
   4550 		$document_title_tmpl = html_entity_decode( $document_title_tmpl, ENT_QUOTES, 'UTF-8' ); // Because exported to JS and assigned to document.title.
   4551 		return $document_title_tmpl;
   4552 	}
   4553 
   4554 	/**
   4555 	 * Set the initial URL to be previewed.
   4556 	 *
   4557 	 * URL is validated.
   4558 	 *
   4559 	 * @since 4.4.0
   4560 	 *
   4561 	 * @param string $preview_url URL to be previewed.
   4562 	 */
   4563 	public function set_preview_url( $preview_url ) {
   4564 		$preview_url       = esc_url_raw( $preview_url );
   4565 		$this->preview_url = wp_validate_redirect( $preview_url, home_url( '/' ) );
   4566 	}
   4567 
   4568 	/**
   4569 	 * Get the initial URL to be previewed.
   4570 	 *
   4571 	 * @since 4.4.0
   4572 	 *
   4573 	 * @return string URL being previewed.
   4574 	 */
   4575 	public function get_preview_url() {
   4576 		if ( empty( $this->preview_url ) ) {
   4577 			$preview_url = home_url( '/' );
   4578 		} else {
   4579 			$preview_url = $this->preview_url;
   4580 		}
   4581 		return $preview_url;
   4582 	}
   4583 
   4584 	/**
   4585 	 * Determines whether the admin and the frontend are on different domains.
   4586 	 *
   4587 	 * @since 4.7.0
   4588 	 *
   4589 	 * @return bool Whether cross-domain.
   4590 	 */
   4591 	public function is_cross_domain() {
   4592 		$admin_origin = wp_parse_url( admin_url() );
   4593 		$home_origin  = wp_parse_url( home_url() );
   4594 		$cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );
   4595 		return $cross_domain;
   4596 	}
   4597 
   4598 	/**
   4599 	 * Get URLs allowed to be previewed.
   4600 	 *
   4601 	 * If the front end and the admin are served from the same domain, load the
   4602 	 * preview over ssl if the Customizer is being loaded over ssl. This avoids
   4603 	 * insecure content warnings. This is not attempted if the admin and front end
   4604 	 * are on different domains to avoid the case where the front end doesn't have
   4605 	 * ssl certs. Domain mapping plugins can allow other urls in these conditions
   4606 	 * using the customize_allowed_urls filter.
   4607 	 *
   4608 	 * @since 4.7.0
   4609 	 *
   4610 	 * @return array Allowed URLs.
   4611 	 */
   4612 	public function get_allowed_urls() {
   4613 		$allowed_urls = array( home_url( '/' ) );
   4614 
   4615 		if ( is_ssl() && ! $this->is_cross_domain() ) {
   4616 			$allowed_urls[] = home_url( '/', 'https' );
   4617 		}
   4618 
   4619 		/**
   4620 		 * Filters the list of URLs allowed to be clicked and followed in the Customizer preview.
   4621 		 *
   4622 		 * @since 3.4.0
   4623 		 *
   4624 		 * @param string[] $allowed_urls An array of allowed URLs.
   4625 		 */
   4626 		$allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) );
   4627 
   4628 		return $allowed_urls;
   4629 	}
   4630 
   4631 	/**
   4632 	 * Get messenger channel.
   4633 	 *
   4634 	 * @since 4.7.0
   4635 	 *
   4636 	 * @return string Messenger channel.
   4637 	 */
   4638 	public function get_messenger_channel() {
   4639 		return $this->messenger_channel;
   4640 	}
   4641 
   4642 	/**
   4643 	 * Set URL to link the user to when closing the Customizer.
   4644 	 *
   4645 	 * URL is validated.
   4646 	 *
   4647 	 * @since 4.4.0
   4648 	 *
   4649 	 * @param string $return_url URL for return link.
   4650 	 */
   4651 	public function set_return_url( $return_url ) {
   4652 		$return_url       = esc_url_raw( $return_url );
   4653 		$return_url       = remove_query_arg( wp_removable_query_args(), $return_url );
   4654 		$return_url       = wp_validate_redirect( $return_url );
   4655 		$this->return_url = $return_url;
   4656 	}
   4657 
   4658 	/**
   4659 	 * Get URL to link the user to when closing the Customizer.
   4660 	 *
   4661 	 * @since 4.4.0
   4662 	 *
   4663 	 * @global array $_registered_pages
   4664 	 *
   4665 	 * @return string URL for link to close Customizer.
   4666 	 */
   4667 	public function get_return_url() {
   4668 		global $_registered_pages;
   4669 
   4670 		$referer                    = wp_get_referer();
   4671 		$excluded_referer_basenames = array( 'customize.php', 'wp-login.php' );
   4672 
   4673 		if ( $this->return_url ) {
   4674 			$return_url = $this->return_url;
   4675 		} elseif ( $referer && ! in_array( wp_basename( parse_url( $referer, PHP_URL_PATH ) ), $excluded_referer_basenames, true ) ) {
   4676 			$return_url = $referer;
   4677 		} elseif ( $this->preview_url ) {
   4678 			$return_url = $this->preview_url;
   4679 		} else {
   4680 			$return_url = home_url( '/' );
   4681 		}
   4682 
   4683 		$return_url_basename = wp_basename( parse_url( $this->return_url, PHP_URL_PATH ) );
   4684 		$return_url_query    = parse_url( $this->return_url, PHP_URL_QUERY );
   4685 
   4686 		if ( 'themes.php' === $return_url_basename && $return_url_query ) {
   4687 			parse_str( $return_url_query, $query_vars );
   4688 
   4689 			/*
   4690 			 * If the return URL is a page added by a theme to the Appearance menu via add_submenu_page(),
   4691 			 * verify that belongs to the active theme, otherwise fall back to the Themes screen.
   4692 			 */
   4693 			if ( isset( $query_vars['page'] ) && ! isset( $_registered_pages[ "appearance_page_{$query_vars['page']}" ] ) ) {
   4694 				$return_url = admin_url( 'themes.php' );
   4695 			}
   4696 		}
   4697 
   4698 		return $return_url;
   4699 	}
   4700 
   4701 	/**
   4702 	 * Set the autofocused constructs.
   4703 	 *
   4704 	 * @since 4.4.0
   4705 	 *
   4706 	 * @param array $autofocus {
   4707 	 *     Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
   4708 	 *
   4709 	 *     @type string $control ID for control to be autofocused.
   4710 	 *     @type string $section ID for section to be autofocused.
   4711 	 *     @type string $panel   ID for panel to be autofocused.
   4712 	 * }
   4713 	 */
   4714 	public function set_autofocus( $autofocus ) {
   4715 		$this->autofocus = array_filter( wp_array_slice_assoc( $autofocus, array( 'panel', 'section', 'control' ) ), 'is_string' );
   4716 	}
   4717 
   4718 	/**
   4719 	 * Get the autofocused constructs.
   4720 	 *
   4721 	 * @since 4.4.0
   4722 	 *
   4723 	 * @return array {
   4724 	 *     Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
   4725 	 *
   4726 	 *     @type string $control ID for control to be autofocused.
   4727 	 *     @type string $section ID for section to be autofocused.
   4728 	 *     @type string $panel   ID for panel to be autofocused.
   4729 	 * }
   4730 	 */
   4731 	public function get_autofocus() {
   4732 		return $this->autofocus;
   4733 	}
   4734 
   4735 	/**
   4736 	 * Get nonces for the Customizer.
   4737 	 *
   4738 	 * @since 4.5.0
   4739 	 *
   4740 	 * @return array Nonces.
   4741 	 */
   4742 	public function get_nonces() {
   4743 		$nonces = array(
   4744 			'save'                     => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
   4745 			'preview'                  => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
   4746 			'switch_themes'            => wp_create_nonce( 'switch_themes' ),
   4747 			'dismiss_autosave_or_lock' => wp_create_nonce( 'customize_dismiss_autosave_or_lock' ),
   4748 			'override_lock'            => wp_create_nonce( 'customize_override_changeset_lock' ),
   4749 			'trash'                    => wp_create_nonce( 'trash_customize_changeset' ),
   4750 		);
   4751 
   4752 		/**
   4753 		 * Filters nonces for Customizer.
   4754 		 *
   4755 		 * @since 4.2.0
   4756 		 *
   4757 		 * @param string[]             $nonces Array of refreshed nonces for save and
   4758 		 *                                     preview actions.
   4759 		 * @param WP_Customize_Manager $this   WP_Customize_Manager instance.
   4760 		 */
   4761 		$nonces = apply_filters( 'customize_refresh_nonces', $nonces, $this );
   4762 
   4763 		return $nonces;
   4764 	}
   4765 
   4766 	/**
   4767 	 * Print JavaScript settings for parent window.
   4768 	 *
   4769 	 * @since 4.4.0
   4770 	 */
   4771 	public function customize_pane_settings() {
   4772 
   4773 		$login_url = add_query_arg(
   4774 			array(
   4775 				'interim-login'   => 1,
   4776 				'customize-login' => 1,
   4777 			),
   4778 			wp_login_url()
   4779 		);
   4780 
   4781 		// Ensure dirty flags are set for modified settings.
   4782 		foreach ( array_keys( $this->unsanitized_post_values() ) as $setting_id ) {
   4783 			$setting = $this->get_setting( $setting_id );
   4784 			if ( $setting ) {
   4785 				$setting->dirty = true;
   4786 			}
   4787 		}
   4788 
   4789 		$autosave_revision_post  = null;
   4790 		$autosave_autodraft_post = null;
   4791 		$changeset_post_id       = $this->changeset_post_id();
   4792 		if ( ! $this->saved_starter_content_changeset && ! $this->autosaved() ) {
   4793 			if ( $changeset_post_id ) {
   4794 				if ( is_user_logged_in() ) {
   4795 					$autosave_revision_post = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
   4796 				}
   4797 			} else {
   4798 				$autosave_autodraft_posts = $this->get_changeset_posts(
   4799 					array(
   4800 						'posts_per_page'            => 1,
   4801 						'post_status'               => 'auto-draft',
   4802 						'exclude_restore_dismissed' => true,
   4803 					)
   4804 				);
   4805 				if ( ! empty( $autosave_autodraft_posts ) ) {
   4806 					$autosave_autodraft_post = array_shift( $autosave_autodraft_posts );
   4807 				}
   4808 			}
   4809 		}
   4810 
   4811 		$current_user_can_publish = current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts );
   4812 
   4813 		// @todo Include all of the status labels here from script-loader.php, and then allow it to be filtered.
   4814 		$status_choices = array();
   4815 		if ( $current_user_can_publish ) {
   4816 			$status_choices[] = array(
   4817 				'status' => 'publish',
   4818 				'label'  => __( 'Publish' ),
   4819 			);
   4820 		}
   4821 		$status_choices[] = array(
   4822 			'status' => 'draft',
   4823 			'label'  => __( 'Save Draft' ),
   4824 		);
   4825 		if ( $current_user_can_publish ) {
   4826 			$status_choices[] = array(
   4827 				'status' => 'future',
   4828 				'label'  => _x( 'Schedule', 'customizer changeset action/button label' ),
   4829 			);
   4830 		}
   4831 
   4832 		// Prepare Customizer settings to pass to JavaScript.
   4833 		$changeset_post = null;
   4834 		if ( $changeset_post_id ) {
   4835 			$changeset_post = get_post( $changeset_post_id );
   4836 		}
   4837 
   4838 		// Determine initial date to be at present or future, not past.
   4839 		$current_time = current_time( 'mysql', false );
   4840 		$initial_date = $current_time;
   4841 		if ( $changeset_post ) {
   4842 			$initial_date = get_the_time( 'Y-m-d H:i:s', $changeset_post->ID );
   4843 			if ( $initial_date < $current_time ) {
   4844 				$initial_date = $current_time;
   4845 			}
   4846 		}
   4847 
   4848 		$lock_user_id = false;
   4849 		if ( $this->changeset_post_id() ) {
   4850 			$lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
   4851 		}
   4852 
   4853 		$settings = array(
   4854 			'changeset'              => array(
   4855 				'uuid'                  => $this->changeset_uuid(),
   4856 				'branching'             => $this->branching(),
   4857 				'autosaved'             => $this->autosaved(),
   4858 				'hasAutosaveRevision'   => ! empty( $autosave_revision_post ),
   4859 				'latestAutoDraftUuid'   => $autosave_autodraft_post ? $autosave_autodraft_post->post_name : null,
   4860 				'status'                => $changeset_post ? $changeset_post->post_status : '',
   4861 				'currentUserCanPublish' => $current_user_can_publish,
   4862 				'publishDate'           => $initial_date,
   4863 				'statusChoices'         => $status_choices,
   4864 				'lockUser'              => $lock_user_id ? $this->get_lock_user_data( $lock_user_id ) : null,
   4865 			),
   4866 			'initialServerDate'      => $current_time,
   4867 			'dateFormat'             => get_option( 'date_format' ),
   4868 			'timeFormat'             => get_option( 'time_format' ),
   4869 			'initialServerTimestamp' => floor( microtime( true ) * 1000 ),
   4870 			'initialClientTimestamp' => -1, // To be set with JS below.
   4871 			'timeouts'               => array(
   4872 				'windowRefresh'           => 250,
   4873 				'changesetAutoSave'       => AUTOSAVE_INTERVAL * 1000,
   4874 				'keepAliveCheck'          => 2500,
   4875 				'reflowPaneContents'      => 100,
   4876 				'previewFrameSensitivity' => 2000,
   4877 			),
   4878 			'theme'                  => array(
   4879 				'stylesheet'  => $this->get_stylesheet(),
   4880 				'active'      => $this->is_theme_active(),
   4881 				'_canInstall' => current_user_can( 'install_themes' ),
   4882 			),
   4883 			'url'                    => array(
   4884 				'preview'       => esc_url_raw( $this->get_preview_url() ),
   4885 				'return'        => esc_url_raw( $this->get_return_url() ),
   4886 				'parent'        => esc_url_raw( admin_url() ),
   4887 				'activated'     => esc_url_raw( home_url( '/' ) ),
   4888 				'ajax'          => esc_url_raw( admin_url( 'admin-ajax.php', 'relative' ) ),
   4889 				'allowed'       => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
   4890 				'isCrossDomain' => $this->is_cross_domain(),
   4891 				'home'          => esc_url_raw( home_url( '/' ) ),
   4892 				'login'         => esc_url_raw( $login_url ),
   4893 			),
   4894 			'browser'                => array(
   4895 				'mobile' => wp_is_mobile(),
   4896 				'ios'    => $this->is_ios(),
   4897 			),
   4898 			'panels'                 => array(),
   4899 			'sections'               => array(),
   4900 			'nonce'                  => $this->get_nonces(),
   4901 			'autofocus'              => $this->get_autofocus(),
   4902 			'documentTitleTmpl'      => $this->get_document_title_template(),
   4903 			'previewableDevices'     => $this->get_previewable_devices(),
   4904 			'l10n'                   => array(
   4905 				'confirmDeleteTheme'   => __( 'Are you sure you want to delete this theme?' ),
   4906 				/* translators: %d: Number of theme search results, which cannot currently consider singular vs. plural forms. */
   4907 				'themeSearchResults'   => __( '%d themes found' ),
   4908 				/* translators: %d: Number of themes being displayed, which cannot currently consider singular vs. plural forms. */
   4909 				'announceThemeCount'   => __( 'Displaying %d themes' ),
   4910 				/* translators: %s: Theme name. */
   4911 				'announceThemeDetails' => __( 'Showing details for theme: %s' ),
   4912 			),
   4913 		);
   4914 
   4915 		// Temporarily disable installation in Customizer. See #42184.
   4916 		$filesystem_method = get_filesystem_method();
   4917 		ob_start();
   4918 		$filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
   4919 		ob_end_clean();
   4920 		if ( 'direct' !== $filesystem_method && ! $filesystem_credentials_are_stored ) {
   4921 			$settings['theme']['_filesystemCredentialsNeeded'] = true;
   4922 		}
   4923 
   4924 		// Prepare Customize Section objects to pass to JavaScript.
   4925 		foreach ( $this->sections() as $id => $section ) {
   4926 			if ( $section->check_capabilities() ) {
   4927 				$settings['sections'][ $id ] = $section->json();
   4928 			}
   4929 		}
   4930 
   4931 		// Prepare Customize Panel objects to pass to JavaScript.
   4932 		foreach ( $this->panels() as $panel_id => $panel ) {
   4933 			if ( $panel->check_capabilities() ) {
   4934 				$settings['panels'][ $panel_id ] = $panel->json();
   4935 				foreach ( $panel->sections as $section_id => $section ) {
   4936 					if ( $section->check_capabilities() ) {
   4937 						$settings['sections'][ $section_id ] = $section->json();
   4938 					}
   4939 				}
   4940 			}
   4941 		}
   4942 
   4943 		?>
   4944 		<script type="text/javascript">
   4945 			var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
   4946 			_wpCustomizeSettings.initialClientTimestamp = _.now();
   4947 			_wpCustomizeSettings.controls = {};
   4948 			_wpCustomizeSettings.settings = {};
   4949 			<?php
   4950 
   4951 			// Serialize settings one by one to improve memory usage.
   4952 			echo "(function ( s ){\n";
   4953 			foreach ( $this->settings() as $setting ) {
   4954 				if ( $setting->check_capabilities() ) {
   4955 					printf(
   4956 						"s[%s] = %s;\n",
   4957 						wp_json_encode( $setting->id ),
   4958 						wp_json_encode( $setting->json() )
   4959 					);
   4960 				}
   4961 			}
   4962 			echo "})( _wpCustomizeSettings.settings );\n";
   4963 
   4964 			// Serialize controls one by one to improve memory usage.
   4965 			echo "(function ( c ){\n";
   4966 			foreach ( $this->controls() as $control ) {
   4967 				if ( $control->check_capabilities() ) {
   4968 					printf(
   4969 						"c[%s] = %s;\n",
   4970 						wp_json_encode( $control->id ),
   4971 						wp_json_encode( $control->json() )
   4972 					);
   4973 				}
   4974 			}
   4975 			echo "})( _wpCustomizeSettings.controls );\n";
   4976 			?>
   4977 		</script>
   4978 		<?php
   4979 	}
   4980 
   4981 	/**
   4982 	 * Returns a list of devices to allow previewing.
   4983 	 *
   4984 	 * @since 4.5.0
   4985 	 *
   4986 	 * @return array List of devices with labels and default setting.
   4987 	 */
   4988 	public function get_previewable_devices() {
   4989 		$devices = array(
   4990 			'desktop' => array(
   4991 				'label'   => __( 'Enter desktop preview mode' ),
   4992 				'default' => true,
   4993 			),
   4994 			'tablet'  => array(
   4995 				'label' => __( 'Enter tablet preview mode' ),
   4996 			),
   4997 			'mobile'  => array(
   4998 				'label' => __( 'Enter mobile preview mode' ),
   4999 			),
   5000 		);
   5001 
   5002 		/**
   5003 		 * Filters the available devices to allow previewing in the Customizer.
   5004 		 *
   5005 		 * @since 4.5.0
   5006 		 *
   5007 		 * @see WP_Customize_Manager::get_previewable_devices()
   5008 		 *
   5009 		 * @param array $devices List of devices with labels and default setting.
   5010 		 */
   5011 		$devices = apply_filters( 'customize_previewable_devices', $devices );
   5012 
   5013 		return $devices;
   5014 	}
   5015 
   5016 	/**
   5017 	 * Register some default controls.
   5018 	 *
   5019 	 * @since 3.4.0
   5020 	 */
   5021 	public function register_controls() {
   5022 
   5023 		/* Themes (controls are loaded via ajax) */
   5024 
   5025 		$this->add_panel(
   5026 			new WP_Customize_Themes_Panel(
   5027 				$this,
   5028 				'themes',
   5029 				array(
   5030 					'title'       => $this->theme()->display( 'Name' ),
   5031 					'description' => (
   5032 					'<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>' .
   5033 					'<p>' . __( 'While previewing a new theme, you can continue to tailor things like widgets and menus, and explore theme-specific options.' ) . '</p>'
   5034 					),
   5035 					'capability'  => 'switch_themes',
   5036 					'priority'    => 0,
   5037 				)
   5038 			)
   5039 		);
   5040 
   5041 		$this->add_section(
   5042 			new WP_Customize_Themes_Section(
   5043 				$this,
   5044 				'installed_themes',
   5045 				array(
   5046 					'title'      => __( 'Installed themes' ),
   5047 					'action'     => 'installed',
   5048 					'capability' => 'switch_themes',
   5049 					'panel'      => 'themes',
   5050 					'priority'   => 0,
   5051 				)
   5052 			)
   5053 		);
   5054 
   5055 		if ( ! is_multisite() ) {
   5056 			$this->add_section(
   5057 				new WP_Customize_Themes_Section(
   5058 					$this,
   5059 					'wporg_themes',
   5060 					array(
   5061 						'title'       => __( 'WordPress.org themes' ),
   5062 						'action'      => 'wporg',
   5063 						'filter_type' => 'remote',
   5064 						'capability'  => 'install_themes',
   5065 						'panel'       => 'themes',
   5066 						'priority'    => 5,
   5067 					)
   5068 				)
   5069 			);
   5070 		}
   5071 
   5072 		// Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
   5073 		$this->add_setting(
   5074 			new WP_Customize_Filter_Setting(
   5075 				$this,
   5076 				'active_theme',
   5077 				array(
   5078 					'capability' => 'switch_themes',
   5079 				)
   5080 			)
   5081 		);
   5082 
   5083 		/* Site Identity */
   5084 
   5085 		$this->add_section(
   5086 			'title_tagline',
   5087 			array(
   5088 				'title'    => __( 'Site Identity' ),
   5089 				'priority' => 20,
   5090 			)
   5091 		);
   5092 
   5093 		$this->add_setting(
   5094 			'blogname',
   5095 			array(
   5096 				'default'    => get_option( 'blogname' ),
   5097 				'type'       => 'option',
   5098 				'capability' => 'manage_options',
   5099 			)
   5100 		);
   5101 
   5102 		$this->add_control(
   5103 			'blogname',
   5104 			array(
   5105 				'label'   => __( 'Site Title' ),
   5106 				'section' => 'title_tagline',
   5107 			)
   5108 		);
   5109 
   5110 		$this->add_setting(
   5111 			'blogdescription',
   5112 			array(
   5113 				'default'    => get_option( 'blogdescription' ),
   5114 				'type'       => 'option',
   5115 				'capability' => 'manage_options',
   5116 			)
   5117 		);
   5118 
   5119 		$this->add_control(
   5120 			'blogdescription',
   5121 			array(
   5122 				'label'   => __( 'Tagline' ),
   5123 				'section' => 'title_tagline',
   5124 			)
   5125 		);
   5126 
   5127 		// Add a setting to hide header text if the theme doesn't support custom headers.
   5128 		if ( ! current_theme_supports( 'custom-header', 'header-text' ) ) {
   5129 			$this->add_setting(
   5130 				'header_text',
   5131 				array(
   5132 					'theme_supports'    => array( 'custom-logo', 'header-text' ),
   5133 					'default'           => 1,
   5134 					'sanitize_callback' => 'absint',
   5135 				)
   5136 			);
   5137 
   5138 			$this->add_control(
   5139 				'header_text',
   5140 				array(
   5141 					'label'    => __( 'Display Site Title and Tagline' ),
   5142 					'section'  => 'title_tagline',
   5143 					'settings' => 'header_text',
   5144 					'type'     => 'checkbox',
   5145 				)
   5146 			);
   5147 		}
   5148 
   5149 		$this->add_setting(
   5150 			'site_icon',
   5151 			array(
   5152 				'type'       => 'option',
   5153 				'capability' => 'manage_options',
   5154 				'transport'  => 'postMessage', // Previewed with JS in the Customizer controls window.
   5155 			)
   5156 		);
   5157 
   5158 		$this->add_control(
   5159 			new WP_Customize_Site_Icon_Control(
   5160 				$this,
   5161 				'site_icon',
   5162 				array(
   5163 					'label'       => __( 'Site Icon' ),
   5164 					'description' => sprintf(
   5165 						'<p>' . __( 'Site Icons are what you see in browser tabs, bookmark bars, and within the WordPress mobile apps. Upload one here!' ) . '</p>' .
   5166 						/* translators: %s: Site icon size in pixels. */
   5167 						'<p>' . __( 'Site Icons should be square and at least %s pixels.' ) . '</p>',
   5168 						'<strong>512 &times; 512</strong>'
   5169 					),
   5170 					'section'     => 'title_tagline',
   5171 					'priority'    => 60,
   5172 					'height'      => 512,
   5173 					'width'       => 512,
   5174 				)
   5175 			)
   5176 		);
   5177 
   5178 		$this->add_setting(
   5179 			'custom_logo',
   5180 			array(
   5181 				'theme_supports' => array( 'custom-logo' ),
   5182 				'transport'      => 'postMessage',
   5183 			)
   5184 		);
   5185 
   5186 		$custom_logo_args = get_theme_support( 'custom-logo' );
   5187 		$this->add_control(
   5188 			new WP_Customize_Cropped_Image_Control(
   5189 				$this,
   5190 				'custom_logo',
   5191 				array(
   5192 					'label'         => __( 'Logo' ),
   5193 					'section'       => 'title_tagline',
   5194 					'priority'      => 8,
   5195 					'height'        => isset( $custom_logo_args[0]['height'] ) ? $custom_logo_args[0]['height'] : null,
   5196 					'width'         => isset( $custom_logo_args[0]['width'] ) ? $custom_logo_args[0]['width'] : null,
   5197 					'flex_height'   => isset( $custom_logo_args[0]['flex-height'] ) ? $custom_logo_args[0]['flex-height'] : null,
   5198 					'flex_width'    => isset( $custom_logo_args[0]['flex-width'] ) ? $custom_logo_args[0]['flex-width'] : null,
   5199 					'button_labels' => array(
   5200 						'select'       => __( 'Select logo' ),
   5201 						'change'       => __( 'Change logo' ),
   5202 						'remove'       => __( 'Remove' ),
   5203 						'default'      => __( 'Default' ),
   5204 						'placeholder'  => __( 'No logo selected' ),
   5205 						'frame_title'  => __( 'Select logo' ),
   5206 						'frame_button' => __( 'Choose logo' ),
   5207 					),
   5208 				)
   5209 			)
   5210 		);
   5211 
   5212 		$this->selective_refresh->add_partial(
   5213 			'custom_logo',
   5214 			array(
   5215 				'settings'            => array( 'custom_logo' ),
   5216 				'selector'            => '.custom-logo-link',
   5217 				'render_callback'     => array( $this, '_render_custom_logo_partial' ),
   5218 				'container_inclusive' => true,
   5219 			)
   5220 		);
   5221 
   5222 		/* Colors */
   5223 
   5224 		$this->add_section(
   5225 			'colors',
   5226 			array(
   5227 				'title'    => __( 'Colors' ),
   5228 				'priority' => 40,
   5229 			)
   5230 		);
   5231 
   5232 		$this->add_setting(
   5233 			'header_textcolor',
   5234 			array(
   5235 				'theme_supports'       => array( 'custom-header', 'header-text' ),
   5236 				'default'              => get_theme_support( 'custom-header', 'default-text-color' ),
   5237 
   5238 				'sanitize_callback'    => array( $this, '_sanitize_header_textcolor' ),
   5239 				'sanitize_js_callback' => 'maybe_hash_hex_color',
   5240 			)
   5241 		);
   5242 
   5243 		// Input type: checkbox.
   5244 		// With custom value.
   5245 		$this->add_control(
   5246 			'display_header_text',
   5247 			array(
   5248 				'settings' => 'header_textcolor',
   5249 				'label'    => __( 'Display Site Title and Tagline' ),
   5250 				'section'  => 'title_tagline',
   5251 				'type'     => 'checkbox',
   5252 				'priority' => 40,
   5253 			)
   5254 		);
   5255 
   5256 		$this->add_control(
   5257 			new WP_Customize_Color_Control(
   5258 				$this,
   5259 				'header_textcolor',
   5260 				array(
   5261 					'label'   => __( 'Header Text Color' ),
   5262 					'section' => 'colors',
   5263 				)
   5264 			)
   5265 		);
   5266 
   5267 		// Input type: color.
   5268 		// With sanitize_callback.
   5269 		$this->add_setting(
   5270 			'background_color',
   5271 			array(
   5272 				'default'              => get_theme_support( 'custom-background', 'default-color' ),
   5273 				'theme_supports'       => 'custom-background',
   5274 
   5275 				'sanitize_callback'    => 'sanitize_hex_color_no_hash',
   5276 				'sanitize_js_callback' => 'maybe_hash_hex_color',
   5277 			)
   5278 		);
   5279 
   5280 		$this->add_control(
   5281 			new WP_Customize_Color_Control(
   5282 				$this,
   5283 				'background_color',
   5284 				array(
   5285 					'label'   => __( 'Background Color' ),
   5286 					'section' => 'colors',
   5287 				)
   5288 			)
   5289 		);
   5290 
   5291 		/* Custom Header */
   5292 
   5293 		if ( current_theme_supports( 'custom-header', 'video' ) ) {
   5294 			$title       = __( 'Header Media' );
   5295 			$description = '<p>' . __( 'If you add a video, the image will be used as a fallback while the video loads.' ) . '</p>';
   5296 
   5297 			$width  = absint( get_theme_support( 'custom-header', 'width' ) );
   5298 			$height = absint( get_theme_support( 'custom-header', 'height' ) );
   5299 			if ( $width && $height ) {
   5300 				$control_description = sprintf(
   5301 					/* translators: 1: .mp4, 2: Header size in pixels. */
   5302 					__( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends dimensions of %2$s pixels.' ),
   5303 					'<code>.mp4</code>',
   5304 					sprintf( '<strong>%s &times; %s</strong>', $width, $height )
   5305 				);
   5306 			} elseif ( $width ) {
   5307 				$control_description = sprintf(
   5308 					/* translators: 1: .mp4, 2: Header width in pixels. */
   5309 					__( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a width of %2$s pixels.' ),
   5310 					'<code>.mp4</code>',
   5311 					sprintf( '<strong>%s</strong>', $width )
   5312 				);
   5313 			} else {
   5314 				$control_description = sprintf(
   5315 					/* translators: 1: .mp4, 2: Header height in pixels. */
   5316 					__( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a height of %2$s pixels.' ),
   5317 					'<code>.mp4</code>',
   5318 					sprintf( '<strong>%s</strong>', $height )
   5319 				);
   5320 			}
   5321 		} else {
   5322 			$title               = __( 'Header Image' );
   5323 			$description         = '';
   5324 			$control_description = '';
   5325 		}
   5326 
   5327 		$this->add_section(
   5328 			'header_image',
   5329 			array(
   5330 				'title'          => $title,
   5331 				'description'    => $description,
   5332 				'theme_supports' => 'custom-header',
   5333 				'priority'       => 60,
   5334 			)
   5335 		);
   5336 
   5337 		$this->add_setting(
   5338 			'header_video',
   5339 			array(
   5340 				'theme_supports'    => array( 'custom-header', 'video' ),
   5341 				'transport'         => 'postMessage',
   5342 				'sanitize_callback' => 'absint',
   5343 				'validate_callback' => array( $this, '_validate_header_video' ),
   5344 			)
   5345 		);
   5346 
   5347 		$this->add_setting(
   5348 			'external_header_video',
   5349 			array(
   5350 				'theme_supports'    => array( 'custom-header', 'video' ),
   5351 				'transport'         => 'postMessage',
   5352 				'sanitize_callback' => array( $this, '_sanitize_external_header_video' ),
   5353 				'validate_callback' => array( $this, '_validate_external_header_video' ),
   5354 			)
   5355 		);
   5356 
   5357 		$this->add_setting(
   5358 			new WP_Customize_Filter_Setting(
   5359 				$this,
   5360 				'header_image',
   5361 				array(
   5362 					'default'        => sprintf( get_theme_support( 'custom-header', 'default-image' ), get_template_directory_uri(), get_stylesheet_directory_uri() ),
   5363 					'theme_supports' => 'custom-header',
   5364 				)
   5365 			)
   5366 		);
   5367 
   5368 		$this->add_setting(
   5369 			new WP_Customize_Header_Image_Setting(
   5370 				$this,
   5371 				'header_image_data',
   5372 				array(
   5373 					'theme_supports' => 'custom-header',
   5374 				)
   5375 			)
   5376 		);
   5377 
   5378 		/*
   5379 		 * Switch image settings to postMessage when video support is enabled since
   5380 		 * it entails that the_custom_header_markup() will be used, and thus selective
   5381 		 * refresh can be utilized.
   5382 		 */
   5383 		if ( current_theme_supports( 'custom-header', 'video' ) ) {
   5384 			$this->get_setting( 'header_image' )->transport      = 'postMessage';
   5385 			$this->get_setting( 'header_image_data' )->transport = 'postMessage';
   5386 		}
   5387 
   5388 		$this->add_control(
   5389 			new WP_Customize_Media_Control(
   5390 				$this,
   5391 				'header_video',
   5392 				array(
   5393 					'theme_supports'  => array( 'custom-header', 'video' ),
   5394 					'label'           => __( 'Header Video' ),
   5395 					'description'     => $control_description,
   5396 					'section'         => 'header_image',
   5397 					'mime_type'       => 'video',
   5398 					'active_callback' => 'is_header_video_active',
   5399 				)
   5400 			)
   5401 		);
   5402 
   5403 		$this->add_control(
   5404 			'external_header_video',
   5405 			array(
   5406 				'theme_supports'  => array( 'custom-header', 'video' ),
   5407 				'type'            => 'url',
   5408 				'description'     => __( 'Or, enter a YouTube URL:' ),
   5409 				'section'         => 'header_image',
   5410 				'active_callback' => 'is_header_video_active',
   5411 			)
   5412 		);
   5413 
   5414 		$this->add_control( new WP_Customize_Header_Image_Control( $this ) );
   5415 
   5416 		$this->selective_refresh->add_partial(
   5417 			'custom_header',
   5418 			array(
   5419 				'selector'            => '#wp-custom-header',
   5420 				'render_callback'     => 'the_custom_header_markup',
   5421 				'settings'            => array( 'header_video', 'external_header_video', 'header_image' ), // The image is used as a video fallback here.
   5422 				'container_inclusive' => true,
   5423 			)
   5424 		);
   5425 
   5426 		/* Custom Background */
   5427 
   5428 		$this->add_section(
   5429 			'background_image',
   5430 			array(
   5431 				'title'          => __( 'Background Image' ),
   5432 				'theme_supports' => 'custom-background',
   5433 				'priority'       => 80,
   5434 			)
   5435 		);
   5436 
   5437 		$this->add_setting(
   5438 			'background_image',
   5439 			array(
   5440 				'default'           => get_theme_support( 'custom-background', 'default-image' ),
   5441 				'theme_supports'    => 'custom-background',
   5442 				'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
   5443 			)
   5444 		);
   5445 
   5446 		$this->add_setting(
   5447 			new WP_Customize_Background_Image_Setting(
   5448 				$this,
   5449 				'background_image_thumb',
   5450 				array(
   5451 					'theme_supports'    => 'custom-background',
   5452 					'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
   5453 				)
   5454 			)
   5455 		);
   5456 
   5457 		$this->add_control( new WP_Customize_Background_Image_Control( $this ) );
   5458 
   5459 		$this->add_setting(
   5460 			'background_preset',
   5461 			array(
   5462 				'default'           => get_theme_support( 'custom-background', 'default-preset' ),
   5463 				'theme_supports'    => 'custom-background',
   5464 				'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
   5465 			)
   5466 		);
   5467 
   5468 		$this->add_control(
   5469 			'background_preset',
   5470 			array(
   5471 				'label'   => _x( 'Preset', 'Background Preset' ),
   5472 				'section' => 'background_image',
   5473 				'type'    => 'select',
   5474 				'choices' => array(
   5475 					'default' => _x( 'Default', 'Default Preset' ),
   5476 					'fill'    => __( 'Fill Screen' ),
   5477 					'fit'     => __( 'Fit to Screen' ),
   5478 					'repeat'  => _x( 'Repeat', 'Repeat Image' ),
   5479 					'custom'  => _x( 'Custom', 'Custom Preset' ),
   5480 				),
   5481 			)
   5482 		);
   5483 
   5484 		$this->add_setting(
   5485 			'background_position_x',
   5486 			array(
   5487 				'default'           => get_theme_support( 'custom-background', 'default-position-x' ),
   5488 				'theme_supports'    => 'custom-background',
   5489 				'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
   5490 			)
   5491 		);
   5492 
   5493 		$this->add_setting(
   5494 			'background_position_y',
   5495 			array(
   5496 				'default'           => get_theme_support( 'custom-background', 'default-position-y' ),
   5497 				'theme_supports'    => 'custom-background',
   5498 				'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
   5499 			)
   5500 		);
   5501 
   5502 		$this->add_control(
   5503 			new WP_Customize_Background_Position_Control(
   5504 				$this,
   5505 				'background_position',
   5506 				array(
   5507 					'label'    => __( 'Image Position' ),
   5508 					'section'  => 'background_image',
   5509 					'settings' => array(
   5510 						'x' => 'background_position_x',
   5511 						'y' => 'background_position_y',
   5512 					),
   5513 				)
   5514 			)
   5515 		);
   5516 
   5517 		$this->add_setting(
   5518 			'background_size',
   5519 			array(
   5520 				'default'           => get_theme_support( 'custom-background', 'default-size' ),
   5521 				'theme_supports'    => 'custom-background',
   5522 				'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
   5523 			)
   5524 		);
   5525 
   5526 		$this->add_control(
   5527 			'background_size',
   5528 			array(
   5529 				'label'   => __( 'Image Size' ),
   5530 				'section' => 'background_image',
   5531 				'type'    => 'select',
   5532 				'choices' => array(
   5533 					'auto'    => _x( 'Original', 'Original Size' ),
   5534 					'contain' => __( 'Fit to Screen' ),
   5535 					'cover'   => __( 'Fill Screen' ),
   5536 				),
   5537 			)
   5538 		);
   5539 
   5540 		$this->add_setting(
   5541 			'background_repeat',
   5542 			array(
   5543 				'default'           => get_theme_support( 'custom-background', 'default-repeat' ),
   5544 				'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
   5545 				'theme_supports'    => 'custom-background',
   5546 			)
   5547 		);
   5548 
   5549 		$this->add_control(
   5550 			'background_repeat',
   5551 			array(
   5552 				'label'   => __( 'Repeat Background Image' ),
   5553 				'section' => 'background_image',
   5554 				'type'    => 'checkbox',
   5555 			)
   5556 		);
   5557 
   5558 		$this->add_setting(
   5559 			'background_attachment',
   5560 			array(
   5561 				'default'           => get_theme_support( 'custom-background', 'default-attachment' ),
   5562 				'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
   5563 				'theme_supports'    => 'custom-background',
   5564 			)
   5565 		);
   5566 
   5567 		$this->add_control(
   5568 			'background_attachment',
   5569 			array(
   5570 				'label'   => __( 'Scroll with Page' ),
   5571 				'section' => 'background_image',
   5572 				'type'    => 'checkbox',
   5573 			)
   5574 		);
   5575 
   5576 		// If the theme is using the default background callback, we can update
   5577 		// the background CSS using postMessage.
   5578 		if ( get_theme_support( 'custom-background', 'wp-head-callback' ) === '_custom_background_cb' ) {
   5579 			foreach ( array( 'color', 'image', 'preset', 'position_x', 'position_y', 'size', 'repeat', 'attachment' ) as $prop ) {
   5580 				$this->get_setting( 'background_' . $prop )->transport = 'postMessage';
   5581 			}
   5582 		}
   5583 
   5584 		/*
   5585 		 * Static Front Page
   5586 		 * See also https://core.trac.wordpress.org/ticket/19627 which introduces the static-front-page theme_support.
   5587 		 * The following replicates behavior from options-reading.php.
   5588 		 */
   5589 
   5590 		$this->add_section(
   5591 			'static_front_page',
   5592 			array(
   5593 				'title'           => __( 'Homepage Settings' ),
   5594 				'priority'        => 120,
   5595 				'description'     => __( 'You can choose what&#8217;s displayed on the homepage of your site. It can be posts in reverse chronological order (classic blog), or a fixed/static page. To set a static homepage, you first need to create two Pages. One will become the homepage, and the other will be where your posts are displayed.' ),
   5596 				'active_callback' => array( $this, 'has_published_pages' ),
   5597 			)
   5598 		);
   5599 
   5600 		$this->add_setting(
   5601 			'show_on_front',
   5602 			array(
   5603 				'default'    => get_option( 'show_on_front' ),
   5604 				'capability' => 'manage_options',
   5605 				'type'       => 'option',
   5606 			)
   5607 		);
   5608 
   5609 		$this->add_control(
   5610 			'show_on_front',
   5611 			array(
   5612 				'label'   => __( 'Your homepage displays' ),
   5613 				'section' => 'static_front_page',
   5614 				'type'    => 'radio',
   5615 				'choices' => array(
   5616 					'posts' => __( 'Your latest posts' ),
   5617 					'page'  => __( 'A static page' ),
   5618 				),
   5619 			)
   5620 		);
   5621 
   5622 		$this->add_setting(
   5623 			'page_on_front',
   5624 			array(
   5625 				'type'       => 'option',
   5626 				'capability' => 'manage_options',
   5627 			)
   5628 		);
   5629 
   5630 		$this->add_control(
   5631 			'page_on_front',
   5632 			array(
   5633 				'label'          => __( 'Homepage' ),
   5634 				'section'        => 'static_front_page',
   5635 				'type'           => 'dropdown-pages',
   5636 				'allow_addition' => true,
   5637 			)
   5638 		);
   5639 
   5640 		$this->add_setting(
   5641 			'page_for_posts',
   5642 			array(
   5643 				'type'       => 'option',
   5644 				'capability' => 'manage_options',
   5645 			)
   5646 		);
   5647 
   5648 		$this->add_control(
   5649 			'page_for_posts',
   5650 			array(
   5651 				'label'          => __( 'Posts page' ),
   5652 				'section'        => 'static_front_page',
   5653 				'type'           => 'dropdown-pages',
   5654 				'allow_addition' => true,
   5655 			)
   5656 		);
   5657 
   5658 		/* Custom CSS */
   5659 		$section_description  = '<p>';
   5660 		$section_description .= __( 'Add your own CSS code here to customize the appearance and layout of your site.' );
   5661 		$section_description .= sprintf(
   5662 			' <a href="%1$s" class="external-link" target="_blank">%2$s<span class="screen-reader-text"> %3$s</span></a>',
   5663 			esc_url( __( 'https://codex.wordpress.org/CSS' ) ),
   5664 			__( 'Learn more about CSS' ),
   5665 			/* translators: Accessibility text. */
   5666 			__( '(opens in a new tab)' )
   5667 		);
   5668 		$section_description .= '</p>';
   5669 
   5670 		$section_description .= '<p id="editor-keyboard-trap-help-1">' . __( 'When using a keyboard to navigate:' ) . '</p>';
   5671 		$section_description .= '<ul>';
   5672 		$section_description .= '<li id="editor-keyboard-trap-help-2">' . __( 'In the editing area, the Tab key enters a tab character.' ) . '</li>';
   5673 		$section_description .= '<li id="editor-keyboard-trap-help-3">' . __( 'To move away from this area, press the Esc key followed by the Tab key.' ) . '</li>';
   5674 		$section_description .= '<li id="editor-keyboard-trap-help-4">' . __( 'Screen reader users: when in forms mode, you may need to press the Esc key twice.' ) . '</li>';
   5675 		$section_description .= '</ul>';
   5676 
   5677 		if ( 'false' !== wp_get_current_user()->syntax_highlighting ) {
   5678 			$section_description .= '<p>';
   5679 			$section_description .= sprintf(
   5680 				/* translators: 1: Link to user profile, 2: Additional link attributes, 3: Accessibility text. */
   5681 				__( 'The edit field automatically highlights code syntax. You can disable this in your <a href="%1$s" %2$s>user profile%3$s</a> to work in plain text mode.' ),
   5682 				esc_url( get_edit_profile_url() ),
   5683 				'class="external-link" target="_blank"',
   5684 				sprintf(
   5685 					'<span class="screen-reader-text"> %s</span>',
   5686 					/* translators: Accessibility text. */
   5687 					__( '(opens in a new tab)' )
   5688 				)
   5689 			);
   5690 			$section_description .= '</p>';
   5691 		}
   5692 
   5693 		$section_description .= '<p class="section-description-buttons">';
   5694 		$section_description .= '<button type="button" class="button-link section-description-close">' . __( 'Close' ) . '</button>';
   5695 		$section_description .= '</p>';
   5696 
   5697 		$this->add_section(
   5698 			'custom_css',
   5699 			array(
   5700 				'title'              => __( 'Additional CSS' ),
   5701 				'priority'           => 200,
   5702 				'description_hidden' => true,
   5703 				'description'        => $section_description,
   5704 			)
   5705 		);
   5706 
   5707 		$custom_css_setting = new WP_Customize_Custom_CSS_Setting(
   5708 			$this,
   5709 			sprintf( 'custom_css[%s]', get_stylesheet() ),
   5710 			array(
   5711 				'capability' => 'edit_css',
   5712 				'default'    => '',
   5713 			)
   5714 		);
   5715 		$this->add_setting( $custom_css_setting );
   5716 
   5717 		$this->add_control(
   5718 			new WP_Customize_Code_Editor_Control(
   5719 				$this,
   5720 				'custom_css',
   5721 				array(
   5722 					'label'       => __( 'CSS code' ),
   5723 					'section'     => 'custom_css',
   5724 					'settings'    => array( 'default' => $custom_css_setting->id ),
   5725 					'code_type'   => 'text/css',
   5726 					'input_attrs' => array(
   5727 						'aria-describedby' => 'editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4',
   5728 					),
   5729 				)
   5730 			)
   5731 		);
   5732 	}
   5733 
   5734 	/**
   5735 	 * Return whether there are published pages.
   5736 	 *
   5737 	 * Used as active callback for static front page section and controls.
   5738 	 *
   5739 	 * @since 4.7.0
   5740 	 *
   5741 	 * @return bool Whether there are published (or to be published) pages.
   5742 	 */
   5743 	public function has_published_pages() {
   5744 
   5745 		$setting = $this->get_setting( 'nav_menus_created_posts' );
   5746 		if ( $setting ) {
   5747 			foreach ( $setting->value() as $post_id ) {
   5748 				if ( 'page' === get_post_type( $post_id ) ) {
   5749 					return true;
   5750 				}
   5751 			}
   5752 		}
   5753 		return 0 !== count( get_pages() );
   5754 	}
   5755 
   5756 	/**
   5757 	 * Add settings from the POST data that were not added with code, e.g. dynamically-created settings for Widgets
   5758 	 *
   5759 	 * @since 4.2.0
   5760 	 *
   5761 	 * @see add_dynamic_settings()
   5762 	 */
   5763 	public function register_dynamic_settings() {
   5764 		$setting_ids = array_keys( $this->unsanitized_post_values() );
   5765 		$this->add_dynamic_settings( $setting_ids );
   5766 	}
   5767 
   5768 	/**
   5769 	 * Load themes into the theme browsing/installation UI.
   5770 	 *
   5771 	 * @since 4.9.0
   5772 	 */
   5773 	public function handle_load_themes_request() {
   5774 		check_ajax_referer( 'switch_themes', 'nonce' );
   5775 
   5776 		if ( ! current_user_can( 'switch_themes' ) ) {
   5777 			wp_die( -1 );
   5778 		}
   5779 
   5780 		if ( empty( $_POST['theme_action'] ) ) {
   5781 			wp_send_json_error( 'missing_theme_action' );
   5782 		}
   5783 		$theme_action = sanitize_key( $_POST['theme_action'] );
   5784 		$themes       = array();
   5785 		$args         = array();
   5786 
   5787 		// Define query filters based on user input.
   5788 		if ( ! array_key_exists( 'search', $_POST ) ) {
   5789 			$args['search'] = '';
   5790 		} else {
   5791 			$args['search'] = sanitize_text_field( wp_unslash( $_POST['search'] ) );
   5792 		}
   5793 
   5794 		if ( ! array_key_exists( 'tags', $_POST ) ) {
   5795 			$args['tag'] = '';
   5796 		} else {
   5797 			$args['tag'] = array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['tags'] ) );
   5798 		}
   5799 
   5800 		if ( ! array_key_exists( 'page', $_POST ) ) {
   5801 			$args['page'] = 1;
   5802 		} else {
   5803 			$args['page'] = absint( $_POST['page'] );
   5804 		}
   5805 
   5806 		require_once ABSPATH . 'wp-admin/includes/theme.php';
   5807 
   5808 		if ( 'installed' === $theme_action ) {
   5809 
   5810 			// Load all installed themes from wp_prepare_themes_for_js().
   5811 			$themes = array( 'themes' => wp_prepare_themes_for_js() );
   5812 			foreach ( $themes['themes'] as &$theme ) {
   5813 				$theme['type']   = 'installed';
   5814 				$theme['active'] = ( isset( $_POST['customized_theme'] ) && $_POST['customized_theme'] === $theme['id'] );
   5815 			}
   5816 		} elseif ( 'wporg' === $theme_action ) {
   5817 
   5818 			// Load WordPress.org themes from the .org API and normalize data to match installed theme objects.
   5819 			if ( ! current_user_can( 'install_themes' ) ) {
   5820 				wp_die( -1 );
   5821 			}
   5822 
   5823 			// Arguments for all queries.
   5824 			$wporg_args = array(
   5825 				'per_page' => 100,
   5826 				'fields'   => array(
   5827 					'reviews_url' => true, // Explicitly request the reviews URL to be linked from the customizer.
   5828 				),
   5829 			);
   5830 
   5831 			$args = array_merge( $wporg_args, $args );
   5832 
   5833 			if ( '' === $args['search'] && '' === $args['tag'] ) {
   5834 				$args['browse'] = 'new'; // Sort by latest themes by default.
   5835 			}
   5836 
   5837 			// Load themes from the .org API.
   5838 			$themes = themes_api( 'query_themes', $args );
   5839 			if ( is_wp_error( $themes ) ) {
   5840 				wp_send_json_error();
   5841 			}
   5842 
   5843 			// This list matches the allowed tags in wp-admin/includes/theme-install.php.
   5844 			$themes_allowedtags                     = array_fill_keys(
   5845 				array( 'a', 'abbr', 'acronym', 'code', 'pre', 'em', 'strong', 'div', 'p', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img' ),
   5846 				array()
   5847 			);
   5848 			$themes_allowedtags['a']                = array_fill_keys( array( 'href', 'title', 'target' ), true );
   5849 			$themes_allowedtags['acronym']['title'] = true;
   5850 			$themes_allowedtags['abbr']['title']    = true;
   5851 			$themes_allowedtags['img']              = array_fill_keys( array( 'src', 'class', 'alt' ), true );
   5852 
   5853 			// Prepare a list of installed themes to check against before the loop.
   5854 			$installed_themes = array();
   5855 			$wp_themes        = wp_get_themes();
   5856 			foreach ( $wp_themes as $theme ) {
   5857 				$installed_themes[] = $theme->get_stylesheet();
   5858 			}
   5859 			$update_php = network_admin_url( 'update.php?action=install-theme' );
   5860 
   5861 			// Set up properties for themes available on WordPress.org.
   5862 			foreach ( $themes->themes as &$theme ) {
   5863 				$theme->install_url = add_query_arg(
   5864 					array(
   5865 						'theme'    => $theme->slug,
   5866 						'_wpnonce' => wp_create_nonce( 'install-theme_' . $theme->slug ),
   5867 					),
   5868 					$update_php
   5869 				);
   5870 
   5871 				$theme->name        = wp_kses( $theme->name, $themes_allowedtags );
   5872 				$theme->version     = wp_kses( $theme->version, $themes_allowedtags );
   5873 				$theme->description = wp_kses( $theme->description, $themes_allowedtags );
   5874 				$theme->stars       = wp_star_rating(
   5875 					array(
   5876 						'rating' => $theme->rating,
   5877 						'type'   => 'percent',
   5878 						'number' => $theme->num_ratings,
   5879 						'echo'   => false,
   5880 					)
   5881 				);
   5882 				$theme->num_ratings = number_format_i18n( $theme->num_ratings );
   5883 				$theme->preview_url = set_url_scheme( $theme->preview_url );
   5884 
   5885 				// Handle themes that are already installed as installed themes.
   5886 				if ( in_array( $theme->slug, $installed_themes, true ) ) {
   5887 					$theme->type = 'installed';
   5888 				} else {
   5889 					$theme->type = $theme_action;
   5890 				}
   5891 
   5892 				// Set active based on customized theme.
   5893 				$theme->active = ( isset( $_POST['customized_theme'] ) && $_POST['customized_theme'] === $theme->slug );
   5894 
   5895 				// Map available theme properties to installed theme properties.
   5896 				$theme->id            = $theme->slug;
   5897 				$theme->screenshot    = array( $theme->screenshot_url );
   5898 				$theme->authorAndUri  = wp_kses( $theme->author['display_name'], $themes_allowedtags );
   5899 				$theme->compatibleWP  = is_wp_version_compatible( $theme->requires ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName
   5900 				$theme->compatiblePHP = is_php_version_compatible( $theme->requires_php ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName
   5901 
   5902 				if ( isset( $theme->parent ) ) {
   5903 					$theme->parent = $theme->parent['slug'];
   5904 				} else {
   5905 					$theme->parent = false;
   5906 				}
   5907 				unset( $theme->slug );
   5908 				unset( $theme->screenshot_url );
   5909 				unset( $theme->author );
   5910 			} // End foreach().
   5911 		} // End if().
   5912 
   5913 		/**
   5914 		 * Filters the theme data loaded in the customizer.
   5915 		 *
   5916 		 * This allows theme data to be loading from an external source,
   5917 		 * or modification of data loaded from `wp_prepare_themes_for_js()`
   5918 		 * or WordPress.org via `themes_api()`.
   5919 		 *
   5920 		 * @since 4.9.0
   5921 		 *
   5922 		 * @see wp_prepare_themes_for_js()
   5923 		 * @see themes_api()
   5924 		 * @see WP_Customize_Manager::__construct()
   5925 		 *
   5926 		 * @param array                $themes  Nested array of theme data.
   5927 		 * @param array                $args    List of arguments, such as page, search term, and tags to query for.
   5928 		 * @param WP_Customize_Manager $manager Instance of Customize manager.
   5929 		 */
   5930 		$themes = apply_filters( 'customize_load_themes', $themes, $args, $this );
   5931 
   5932 		wp_send_json_success( $themes );
   5933 	}
   5934 
   5935 
   5936 	/**
   5937 	 * Callback for validating the header_textcolor value.
   5938 	 *
   5939 	 * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
   5940 	 * Returns default text color if hex color is empty.
   5941 	 *
   5942 	 * @since 3.4.0
   5943 	 *
   5944 	 * @param string $color
   5945 	 * @return mixed
   5946 	 */
   5947 	public function _sanitize_header_textcolor( $color ) {
   5948 		if ( 'blank' === $color ) {
   5949 			return 'blank';
   5950 		}
   5951 
   5952 		$color = sanitize_hex_color_no_hash( $color );
   5953 		if ( empty( $color ) ) {
   5954 			$color = get_theme_support( 'custom-header', 'default-text-color' );
   5955 		}
   5956 
   5957 		return $color;
   5958 	}
   5959 
   5960 	/**
   5961 	 * Callback for validating a background setting value.
   5962 	 *
   5963 	 * @since 4.7.0
   5964 	 *
   5965 	 * @param string               $value   Repeat value.
   5966 	 * @param WP_Customize_Setting $setting Setting.
   5967 	 * @return string|WP_Error Background value or validation error.
   5968 	 */
   5969 	public function _sanitize_background_setting( $value, $setting ) {
   5970 		if ( 'background_repeat' === $setting->id ) {
   5971 			if ( ! in_array( $value, array( 'repeat-x', 'repeat-y', 'repeat', 'no-repeat' ), true ) ) {
   5972 				return new WP_Error( 'invalid_value', __( 'Invalid value for background repeat.' ) );
   5973 			}
   5974 		} elseif ( 'background_attachment' === $setting->id ) {
   5975 			if ( ! in_array( $value, array( 'fixed', 'scroll' ), true ) ) {
   5976 				return new WP_Error( 'invalid_value', __( 'Invalid value for background attachment.' ) );
   5977 			}
   5978 		} elseif ( 'background_position_x' === $setting->id ) {
   5979 			if ( ! in_array( $value, array( 'left', 'center', 'right' ), true ) ) {
   5980 				return new WP_Error( 'invalid_value', __( 'Invalid value for background position X.' ) );
   5981 			}
   5982 		} elseif ( 'background_position_y' === $setting->id ) {
   5983 			if ( ! in_array( $value, array( 'top', 'center', 'bottom' ), true ) ) {
   5984 				return new WP_Error( 'invalid_value', __( 'Invalid value for background position Y.' ) );
   5985 			}
   5986 		} elseif ( 'background_size' === $setting->id ) {
   5987 			if ( ! in_array( $value, array( 'auto', 'contain', 'cover' ), true ) ) {
   5988 				return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
   5989 			}
   5990 		} elseif ( 'background_preset' === $setting->id ) {
   5991 			if ( ! in_array( $value, array( 'default', 'fill', 'fit', 'repeat', 'custom' ), true ) ) {
   5992 				return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
   5993 			}
   5994 		} elseif ( 'background_image' === $setting->id || 'background_image_thumb' === $setting->id ) {
   5995 			$value = empty( $value ) ? '' : esc_url_raw( $value );
   5996 		} else {
   5997 			return new WP_Error( 'unrecognized_setting', __( 'Unrecognized background setting.' ) );
   5998 		}
   5999 		return $value;
   6000 	}
   6001 
   6002 	/**
   6003 	 * Export header video settings to facilitate selective refresh.
   6004 	 *
   6005 	 * @since 4.7.0
   6006 	 *
   6007 	 * @param array                          $response          Response.
   6008 	 * @param WP_Customize_Selective_Refresh $selective_refresh Selective refresh component.
   6009 	 * @param array                          $partials          Array of partials.
   6010 	 * @return array
   6011 	 */
   6012 	public function export_header_video_settings( $response, $selective_refresh, $partials ) {
   6013 		if ( isset( $partials['custom_header'] ) ) {
   6014 			$response['custom_header_settings'] = get_header_video_settings();
   6015 		}
   6016 
   6017 		return $response;
   6018 	}
   6019 
   6020 	/**
   6021 	 * Callback for validating the header_video value.
   6022 	 *
   6023 	 * Ensures that the selected video is less than 8MB and provides an error message.
   6024 	 *
   6025 	 * @since 4.7.0
   6026 	 *
   6027 	 * @param WP_Error $validity
   6028 	 * @param mixed    $value
   6029 	 * @return mixed
   6030 	 */
   6031 	public function _validate_header_video( $validity, $value ) {
   6032 		$video = get_attached_file( absint( $value ) );
   6033 		if ( $video ) {
   6034 			$size = filesize( $video );
   6035 			if ( $size > 8 * MB_IN_BYTES ) {
   6036 				$validity->add(
   6037 					'size_too_large',
   6038 					__( 'This video file is too large to use as a header video. Try a shorter video or optimize the compression settings and re-upload a file that is less than 8MB. Or, upload your video to YouTube and link it with the option below.' )
   6039 				);
   6040 			}
   6041 			if ( '.mp4' !== substr( $video, -4 ) && '.mov' !== substr( $video, -4 ) ) { // Check for .mp4 or .mov format, which (assuming h.264 encoding) are the only cross-browser-supported formats.
   6042 				$validity->add(
   6043 					'invalid_file_type',
   6044 					sprintf(
   6045 						/* translators: 1: .mp4, 2: .mov */
   6046 						__( 'Only %1$s or %2$s files may be used for header video. Please convert your video file and try again, or, upload your video to YouTube and link it with the option below.' ),
   6047 						'<code>.mp4</code>',
   6048 						'<code>.mov</code>'
   6049 					)
   6050 				);
   6051 			}
   6052 		}
   6053 		return $validity;
   6054 	}
   6055 
   6056 	/**
   6057 	 * Callback for validating the external_header_video value.
   6058 	 *
   6059 	 * Ensures that the provided URL is supported.
   6060 	 *
   6061 	 * @since 4.7.0
   6062 	 *
   6063 	 * @param WP_Error $validity
   6064 	 * @param mixed    $value
   6065 	 * @return mixed
   6066 	 */
   6067 	public function _validate_external_header_video( $validity, $value ) {
   6068 		$video = esc_url_raw( $value );
   6069 		if ( $video ) {
   6070 			if ( ! preg_match( '#^https?://(?:www\.)?(?:youtube\.com/watch|youtu\.be/)#', $video ) ) {
   6071 				$validity->add( 'invalid_url', __( 'Please enter a valid YouTube URL.' ) );
   6072 			}
   6073 		}
   6074 		return $validity;
   6075 	}
   6076 
   6077 	/**
   6078 	 * Callback for sanitizing the external_header_video value.
   6079 	 *
   6080 	 * @since 4.7.1
   6081 	 *
   6082 	 * @param string $value URL.
   6083 	 * @return string Sanitized URL.
   6084 	 */
   6085 	public function _sanitize_external_header_video( $value ) {
   6086 		return esc_url_raw( trim( $value ) );
   6087 	}
   6088 
   6089 	/**
   6090 	 * Callback for rendering the custom logo, used in the custom_logo partial.
   6091 	 *
   6092 	 * This method exists because the partial object and context data are passed
   6093 	 * into a partial's render_callback so we cannot use get_custom_logo() as
   6094 	 * the render_callback directly since it expects a blog ID as the first
   6095 	 * argument. When WP no longer supports PHP 5.3, this method can be removed
   6096 	 * in favor of an anonymous function.
   6097 	 *
   6098 	 * @see WP_Customize_Manager::register_controls()
   6099 	 *
   6100 	 * @since 4.5.0
   6101 	 *
   6102 	 * @return string Custom logo.
   6103 	 */
   6104 	public function _render_custom_logo_partial() {
   6105 		return get_custom_logo();
   6106 	}
   6107 }