[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

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

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