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


Generated: Sun Jan 22 01:00:06 2017 Cross-referenced by PHPXref 0.7.1