Make WordPress Core


Ignore:
Timestamp:
10/18/2016 08:04:36 PM (9 years ago)
Author:
westonruter
Message:

Customize: Implement customized state persistence with changesets.

Includes infrastructure developed in the Customize Snapshots feature plugin.

See https://make.wordpress.org/core/2016/10/12/customize-changesets-technical-design-decisions/

Props westonruter, valendesigns, utkarshpatel, stubgo, lgedeon, ocean90, ryankienstra, mihai2u, dlh, aaroncampbell, jonathanbardo, jorbin.
See #28721.
See #31089.
Fixes #30937.
Fixes #31517.
Fixes #30028.
Fixes #23225.
Fixes #34142.
Fixes #36485.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-customize-manager.php

    r38765 r38810  
    131131
    132132    /**
    133      * Return value of check_ajax_referer() in customize_preview_init() method.
    134      *
    135      * @since 3.5.0
    136      * @access protected
    137      * @var false|int
    138      */
    139     protected $nonce_tick;
    140 
    141     /**
    142133     * Panel types that may be rendered from JS templates.
    143134     *
     
    194185
    195186    /**
     187     * Messenger channel.
     188     *
     189     * @since 4.7.0
     190     * @access protected
     191     * @var string
     192     */
     193    protected $messenger_channel;
     194
     195    /**
    196196     * Unsanitized values for Customize Settings parsed from $_POST['customized'].
    197197     *
     
    201201
    202202    /**
     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    /**
    203230     * Constructor.
    204231     *
    205232     * @since 3.4.0
    206      */
    207     public function __construct() {
     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
    208272        require_once( ABSPATH . WPINC . '/class-wp-customize-setting.php' );
    209273        require_once( ABSPATH . WPINC . '/class-wp-customize-panel.php' );
     
    272336        }
    273337
    274         add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );
    275 
    276338        add_action( 'setup_theme', array( $this, 'setup_theme' ) );
    277339        add_action( 'wp_loaded',   array( $this, 'wp_loaded' ) );
    278 
    279         // Run wp_redirect_status late to make sure we override the status last.
    280         add_action( 'wp_redirect_status', array( $this, 'wp_redirect_status' ), 1000 );
    281340
    282341        // Do not spawn cron (especially the alternate cron) while running the Customizer.
     
    341400     */
    342401    protected function wp_die( $ajax_message, $message = null ) {
    343         if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) {
     402        if ( $this->doing_ajax() ) {
    344403            wp_die( $ajax_message );
    345404        }
     
    349408        }
    350409
     410        if ( $this->messenger_channel ) {
     411            ob_start();
     412            wp_enqueue_scripts();
     413            wp_print_scripts( array( 'customize-base' ) );
     414
     415            $settings = array(
     416                'messengerArgs' => array(
     417                    'channel' => $this->messenger_channel,
     418                    'url' => wp_customize_url(),
     419                ),
     420                'error' => $ajax_message,
     421            );
     422            ?>
     423            <script>
     424            ( function( api, settings ) {
     425                var preview = new api.Messenger( settings.messengerArgs );
     426                preview.send( 'iframe-loading-error', settings.error );
     427            } )( wp.customize, <?php echo wp_json_encode( $settings ) ?> );
     428            </script>
     429            <?php
     430            $message .= ob_get_clean();
     431        }
     432
    351433        wp_die( $message );
    352434    }
     
    356438     *
    357439     * @since 3.4.0
    358      *
    359      * @return string
     440     * @deprecated 4.7.0
     441     *
     442     * @return callable Die handler.
    360443     */
    361444    public function wp_die_handler() {
     445        _deprecated_function( __METHOD__, '4.7.0' );
     446
    362447        if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) {
    363448            return '_ajax_wp_die_handler';
     
    375460     */
    376461    public function setup_theme() {
    377         send_origin_headers();
    378 
    379         $doing_ajax_or_is_customized = ( $this->doing_ajax() || isset( $_POST['customized'] ) );
    380         if ( is_admin() && ! $doing_ajax_or_is_customized ) {
    381             auth_redirect();
    382         } elseif ( $doing_ajax_or_is_customized && ! is_user_logged_in() ) {
    383             $this->wp_die( 0, __( 'You must be logged in to complete this action.' ) );
    384         }
    385 
    386         show_admin_bar( false );
    387 
    388         if ( ! current_user_can( 'customize' ) ) {
    389             $this->wp_die( -1, __( 'Sorry, you are not allowed to customize this site.' ) );
    390         }
    391 
    392         $this->original_stylesheet = get_stylesheet();
    393 
    394         $this->theme = wp_get_theme( isset( $_REQUEST['theme'] ) ? $_REQUEST['theme'] : null );
     462        global $pagenow;
     463
     464        // Check permissions for customize.php access since this method is called before customize.php can run any code,
     465        if ( 'customize.php' === $pagenow && ! current_user_can( 'customize' ) ) {
     466            if ( ! is_user_logged_in() ) {
     467                auth_redirect();
     468            } else {
     469                wp_die(
     470                    '<h1>' . __( 'Cheatin&#8217; uh?' ) . '</h1>' .
     471                    '<p>' . __( 'Sorry, you are not allowed to customize this site.' ) . '</p>',
     472                    403
     473                );
     474            }
     475            return;
     476        }
     477
     478        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 ) ) {
     479            $this->wp_die( -1, __( 'Invalid changeset UUID' ) );
     480        }
     481
     482        /*
     483         * If unauthenticated then require a valid changeset UUID to load the preview.
     484         * In this way, the UUID serves as a secret key. If the messenger channel is present,
     485         * then send unauthenticated code to prompt re-auth.
     486         */
     487        if ( ! current_user_can( 'customize' ) && ! $this->changeset_post_id() ) {
     488            $this->wp_die( $this->messenger_channel ? 0 : -1, __( 'Non-existent changeset UUID.' ) );
     489        }
     490
     491        if ( ! headers_sent() ) {
     492            send_origin_headers();
     493        }
     494
     495        // Hide the admin bar if we're embedded in the customizer iframe.
     496        if ( $this->messenger_channel ) {
     497            show_admin_bar( false );
     498        }
    395499
    396500        if ( $this->is_theme_active() ) {
     
    508612
    509613    /**
     614     * Get the changeset UUID.
     615     *
     616     * @since 4.7.0
     617     * @access public
     618     *
     619     * @return string UUID.
     620     */
     621    public function changeset_uuid() {
     622        return $this->_changeset_uuid;
     623    }
     624
     625    /**
    510626     * Get the theme being customized.
    511627     *
     
    604720        do_action( 'customize_register', $this );
    605721
    606         if ( $this->is_preview() && ! is_admin() )
     722        /*
     723         * Note that settings must be previewed here even outside the customizer preview
     724         * and also in the customizer pane itself. This is to enable loading an existing
     725         * changeset into the customizer. Previewing the settings only has to be prevented
     726         * in the case of a customize_save action because then update_option()
     727         * may short-circuit because it will detect that there are no changes to
     728         * make.
     729         */
     730        if ( ! $this->doing_ajax( 'customize_save' ) ) {
     731            foreach ( $this->settings as $setting ) {
     732                $setting->preview();
     733            }
     734        }
     735
     736        if ( $this->is_preview() && ! is_admin() ) {
    607737            $this->customize_preview_init();
     738        }
    608739    }
    609740
     
    615746     *
    616747     * @since 3.4.0
    617      *
    618      * @param $status
     748     * @deprecated 4.7.0
     749     *
     750     * @param int $status Status.
    619751     * @return int
    620752     */
    621753    public function wp_redirect_status( $status ) {
    622         if ( $this->is_preview() && ! is_admin() )
     754        _deprecated_function( __FUNCTION__, '4.7.0' );
     755
     756        if ( $this->is_preview() && ! is_admin() ) {
    623757            return 200;
     758        }
    624759
    625760        return $status;
     
    627762
    628763    /**
    629      * Parse the incoming $_POST['customized'] JSON data and store the unsanitized
    630      * settings for subsequent post_value() lookups.
     764     * Find the changeset post ID for a given changeset UUID.
     765     *
     766     * @since 4.7.0
     767     * @access public
     768     *
     769     * @param string $uuid Changeset UUID.
     770     * @return int|null Returns post ID on success and null on failure.
     771     */
     772    public function find_changeset_post_id( $uuid ) {
     773        $cache_group = 'customize_changeset_post';
     774        $changeset_post_id = wp_cache_get( $uuid, $cache_group );
     775        if ( $changeset_post_id && 'customize_changeset' === get_post_type( $changeset_post_id ) ) {
     776            return $changeset_post_id;
     777        }
     778
     779        $changeset_post_query = new WP_Query( array(
     780            'post_type' => 'customize_changeset',
     781            'post_status' => get_post_stati(),
     782            'name' => $uuid,
     783            'number' => 1,
     784            'no_found_rows' => true,
     785            'cache_results' => true,
     786            'update_post_meta_cache' => false,
     787            'update_term_meta_cache' => false,
     788        ) );
     789        if ( ! empty( $changeset_post_query->posts ) ) {
     790            // Note: 'fields'=>'ids' is not being used in order to cache the post object as it will be needed.
     791            $changeset_post_id = $changeset_post_query->posts[0]->ID;
     792            wp_cache_set( $this->_changeset_uuid, $changeset_post_id, $cache_group );
     793            return $changeset_post_id;
     794        }
     795
     796        return null;
     797    }
     798
     799    /**
     800     * Get the changeset post id for the loaded changeset.
     801     *
     802     * @since 4.7.0
     803     * @access public
     804     *
     805     * @return int|null Post ID on success or null if there is no post yet saved.
     806     */
     807    public function changeset_post_id() {
     808        if ( ! isset( $this->_changeset_post_id ) ) {
     809            $post_id = $this->find_changeset_post_id( $this->_changeset_uuid );
     810            if ( ! $post_id ) {
     811                $post_id = false;
     812            }
     813            $this->_changeset_post_id = $post_id;
     814        }
     815        if ( false === $this->_changeset_post_id ) {
     816            return null;
     817        }
     818        return $this->_changeset_post_id;
     819    }
     820
     821    /**
     822     * Get the data stored in a changeset post.
     823     *
     824     * @since 4.7.0
     825     * @access protected
     826     *
     827     * @param int $post_id Changeset post ID.
     828     * @return array|WP_Error Changeset data or WP_Error on error.
     829     */
     830    protected function get_changeset_post_data( $post_id ) {
     831        if ( ! $post_id ) {
     832            return new WP_Error( 'empty_post_id' );
     833        }
     834        $changeset_post = get_post( $post_id );
     835        if ( ! $changeset_post ) {
     836            return new WP_Error( 'missing_post' );
     837        }
     838        if ( 'customize_changeset' !== $changeset_post->post_type ) {
     839            return new WP_Error( 'wrong_post_type' );
     840        }
     841        $changeset_data = json_decode( $changeset_post->post_content, true );
     842        if ( function_exists( 'json_last_error' ) && json_last_error() ) {
     843            return new WP_Error( 'json_parse_error', '', json_last_error() );
     844        }
     845        if ( ! is_array( $changeset_data ) ) {
     846            return new WP_Error( 'expected_array' );
     847        }
     848        return $changeset_data;
     849    }
     850
     851    /**
     852     * Get changeset data.
     853     *
     854     * @since 4.7.0
     855     * @access public
     856     *
     857     * @return array Changeset data.
     858     */
     859    public function changeset_data() {
     860        if ( isset( $this->_changeset_data ) ) {
     861            return $this->_changeset_data;
     862        }
     863        $changeset_post_id = $this->changeset_post_id();
     864        if ( ! $changeset_post_id ) {
     865            $this->_changeset_data = array();
     866        } else {
     867            $data = $this->get_changeset_post_data( $changeset_post_id );
     868            if ( ! is_wp_error( $data ) ) {
     869                $this->_changeset_data = $data;
     870            } else {
     871                $this->_changeset_data = array();
     872            }
     873        }
     874        return $this->_changeset_data;
     875    }
     876
     877    /**
     878     * Get dirty pre-sanitized setting values in the current customized state.
     879     *
     880     * The returned array consists of a merge of three sources:
     881     * 1. If the theme is not currently active, then the base array is any stashed
     882     *    theme mods that were modified previously but never published.
     883     * 2. The values from the current changeset, if it exists.
     884     * 3. If the user can customize, the values parsed from the incoming
     885     *    `$_POST['customized']` JSON data.
     886     * 4. Any programmatically-set post values via `WP_Customize_Manager::set_post_value()`.
     887     *
     888     * The name "unsanitized_post_values" is a carry-over from when the customized
     889     * state was exclusively sourced from `$_POST['customized']`. Nevertheless,
     890     * the value returned will come from the current changeset post and from the
     891     * incoming post data.
    631892     *
    632893     * @since 4.1.1
    633      *
     894     * @since 4.7.0 Added $args param and merging with changeset values and stashed theme mods.
     895     *
     896     * @param array $args {
     897     *     Args.
     898     *
     899     *     @type bool $exclude_changeset Whether the changeset values should also be excluded. Defaults to false.
     900     *     @type bool $exclude_post_data Whether the post input values should also be excluded. Defaults to false when lacking the customize capability.
     901     * }
    634902     * @return array
    635903     */
    636     public function unsanitized_post_values() {
    637         if ( ! isset( $this->_post_values ) ) {
    638             if ( isset( $_POST['customized'] ) ) {
    639                 $this->_post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
    640             }
    641             if ( empty( $this->_post_values ) ) { // if not isset or if JSON error
    642                 $this->_post_values = array();
    643             }
    644         }
    645         if ( empty( $this->_post_values ) ) {
    646             return array();
    647         } else {
    648             return $this->_post_values;
    649         }
    650     }
    651 
    652     /**
    653      * Returns the sanitized value for a given setting from the request's POST data.
     904    public function unsanitized_post_values( $args = array() ) {
     905        $args = array_merge(
     906            array(
     907                'exclude_changeset' => false,
     908                'exclude_post_data' => ! current_user_can( 'customize' ),
     909            ),
     910            $args
     911        );
     912
     913        $values = array();
     914
     915        // Let default values be from the stashed theme mods if doing a theme switch and if no changeset is present.
     916        if ( ! $this->is_theme_active() ) {
     917            $stashed_theme_mods = get_option( 'customize_stashed_theme_mods' );
     918            $stylesheet = $this->get_stylesheet();
     919            if ( isset( $stashed_theme_mods[ $stylesheet ] ) ) {
     920                $values = array_merge( $values, wp_list_pluck( $stashed_theme_mods[ $stylesheet ], 'value' ) );
     921            }
     922        }
     923
     924        if ( ! $args['exclude_changeset'] ) {
     925            foreach ( $this->changeset_data() as $setting_id => $setting_params ) {
     926                if ( ! array_key_exists( 'value', $setting_params ) ) {
     927                    continue;
     928                }
     929                if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) {
     930
     931                    // Ensure that theme mods values are only used if they were saved under the current theme.
     932                    $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
     933                    if ( preg_match( $namespace_pattern, $setting_id, $matches ) && $this->get_stylesheet() === $matches['stylesheet'] ) {
     934                        $values[ $matches['setting_id'] ] = $setting_params['value'];
     935                    }
     936                } else {
     937                    $values[ $setting_id ] = $setting_params['value'];
     938                }
     939            }
     940        }
     941
     942        if ( ! $args['exclude_post_data'] ) {
     943            if ( ! isset( $this->_post_values ) ) {
     944                if ( isset( $_POST['customized'] ) ) {
     945                    $post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
     946                } else {
     947                    $post_values = array();
     948                }
     949                if ( is_array( $post_values ) ) {
     950                    $this->_post_values = $post_values;
     951                } else {
     952                    $this->_post_values = array();
     953                }
     954            }
     955            $values = array_merge( $values, $this->_post_values );
     956        }
     957        return $values;
     958    }
     959
     960    /**
     961     * Returns the sanitized value for a given setting from the current customized state.
     962     *
     963     * The name "post_value" is a carry-over from when the customized state was exclusively
     964     * sourced from `$_POST['customized']`. Nevertheless, the value returned will come
     965     * from the current changeset post and from the incoming post data.
    654966     *
    655967     * @since 3.4.0
     
    685997
    686998    /**
    687      * Override a setting's (unsanitized) value as found in any incoming $_POST['customized'].
     999     * Override a setting's value in the current customized state.
     1000     *
     1001     * The name "post_value" is a carry-over from when the customized state was
     1002     * exclusively sourced from `$_POST['customized']`.
    6881003     *
    6891004     * @since 4.2.0
     
    6941009     */
    6951010    public function set_post_value( $setting_id, $value ) {
    696         $this->unsanitized_post_values();
     1011        $this->unsanitized_post_values(); // Populate _post_values from $_POST['customized'].
    6971012        $this->_post_values[ $setting_id ] = $value;
    6981013
     
    7341049     */
    7351050    public function customize_preview_init() {
    736         $this->nonce_tick = check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'nonce' );
     1051
     1052        /*
     1053         * Now that Customizer previews are loaded into iframes via GET requests
     1054         * and natural URLs with transaction UUIDs added, we need to ensure that
     1055         * the responses are never cached by proxies. In practice, this will not
     1056         * be needed if the user is logged-in anyway. But if anonymous access is
     1057         * allowed then the auth cookies would not be sent and WordPress would
     1058         * not send no-cache headers by default.
     1059         */
     1060        if ( ! headers_sent() ) {
     1061            nocache_headers();
     1062            header( 'X-Robots: noindex, nofollow, noarchive' );
     1063        }
     1064        add_action( 'wp_head', 'wp_no_robots' );
     1065        add_filter( 'wp_headers', array( $this, 'filter_iframe_security_headers' ) );
     1066
     1067        /*
     1068         * If preview is being served inside the customizer preview iframe, and
     1069         * if the user doesn't have customize capability, then it is assumed
     1070         * that the user's session has expired and they need to re-authenticate.
     1071         */
     1072        if ( $this->messenger_channel && ! current_user_can( 'customize' ) ) {
     1073            $this->wp_die( -1, __( 'Unauthorized. You may remove the customize_messenger_channel param to preview as frontend.' ) );
     1074            return;
     1075        }
    7371076
    7381077        $this->prepare_controls();
    7391078
     1079        add_filter( 'wp_redirect', array( $this, 'add_state_query_params' ) );
     1080
    7401081        wp_enqueue_script( 'customize-preview' );
    741         add_action( 'wp', array( $this, 'customize_preview_override_404_status' ) );
    742         add_action( 'wp_head', array( $this, 'customize_preview_base' ) );
    7431082        add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) );
    7441083        add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 );
    745         add_action( 'shutdown', array( $this, 'customize_preview_signature' ), 1000 );
    746         add_filter( 'wp_die_handler', array( $this, 'remove_preview_signature' ) );
    747 
    748         foreach ( $this->settings as $setting ) {
    749             $setting->preview();
    750         }
    7511084
    7521085        /**
     
    7621095
    7631096    /**
     1097     * Filter the X-Frame-Options and Content-Security-Policy headers to ensure frontend can load in customizer.
     1098     *
     1099     * @since 4.7.0
     1100     * @access public
     1101     *
     1102     * @param array $headers Headers.
     1103     * @return array Headers.
     1104     */
     1105    public function filter_iframe_security_headers( $headers ) {
     1106        $customize_url = admin_url( 'customize.php' );
     1107        $headers['X-Frame-Options'] = 'ALLOW-FROM ' . $customize_url;
     1108        $headers['Content-Security-Policy'] = 'frame-ancestors ' . preg_replace( '#^(\w+://[^/]+).+?$#', '$1', $customize_url );
     1109        return $headers;
     1110    }
     1111
     1112    /**
     1113     * Add customize state query params to a given URL if preview is allowed.
     1114     *
     1115     * @since 4.7.0
     1116     * @access public
     1117     * @see wp_redirect()
     1118     * @see WP_Customize_Manager::get_allowed_url()
     1119     *
     1120     * @param string $url URL.
     1121     * @return string URL.
     1122     */
     1123    public function add_state_query_params( $url ) {
     1124        $parsed_original_url = wp_parse_url( $url );
     1125        $is_allowed = false;
     1126        foreach ( $this->get_allowed_urls() as $allowed_url ) {
     1127            $parsed_allowed_url = wp_parse_url( $allowed_url );
     1128            $is_allowed = (
     1129                $parsed_allowed_url['scheme'] === $parsed_original_url['scheme']
     1130                &&
     1131                $parsed_allowed_url['host'] === $parsed_original_url['host']
     1132                &&
     1133                0 === strpos( $parsed_original_url['path'], $parsed_allowed_url['path'] )
     1134            );
     1135            if ( $is_allowed ) {
     1136                break;
     1137            }
     1138        }
     1139
     1140        if ( $is_allowed ) {
     1141            $query_params = array(
     1142                'customize_changeset_uuid' => $this->changeset_uuid(),
     1143            );
     1144            if ( ! $this->is_theme_active() ) {
     1145                $query_params['customize_theme'] = $this->get_stylesheet();
     1146            }
     1147            if ( $this->messenger_channel ) {
     1148                $query_params['customize_messenger_channel'] = $this->messenger_channel;
     1149            }
     1150            $url = add_query_arg( $query_params, $url );
     1151        }
     1152
     1153        return $url;
     1154    }
     1155
     1156    /**
    7641157     * Prevent sending a 404 status when returning the response for the customize
    7651158     * preview, since it causes the jQuery Ajax to fail. Send 200 instead.
    7661159     *
    7671160     * @since 4.0.0
     1161     * @deprecated 4.7.0
    7681162     * @access public
    7691163     */
    7701164    public function customize_preview_override_404_status() {
    771         if ( is_404() ) {
    772             status_header( 200 );
    773         }
     1165        _deprecated_function( __METHOD__, '4.7.0' );
    7741166    }
    7751167
     
    7781170     *
    7791171     * @since 3.4.0
     1172     * @deprecated 4.7.0
    7801173     */
    7811174    public function customize_preview_base() {
    782         ?><base href="<?php echo home_url( '/' ); ?>" /><?php
     1175        _deprecated_function( __METHOD__, '4.7.0' );
    7831176    }
    7841177
     
    8101203                pointer-events: none !important;
    8111204            }
     1205            form.customize-unpreviewable,
     1206            form.customize-unpreviewable input,
     1207            form.customize-unpreviewable select,
     1208            form.customize-unpreviewable button,
     1209            a.customize-unpreviewable,
     1210            area.customize-unpreviewable {
     1211                cursor: not-allowed !important;
     1212            }
    8121213        </style><?php
    8131214    }
     
    8191220     */
    8201221    public function customize_preview_settings() {
    821         $setting_validities = $this->validate_setting_values( $this->unsanitized_post_values() );
     1222        $post_values = $this->unsanitized_post_values( array( 'exclude_changeset' => true ) );
     1223        $setting_validities = $this->validate_setting_values( $post_values );
    8221224        $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities );
    8231225
     1226        // Note that the REQUEST_URI is not passed into home_url() since this breaks subdirectory installs.
     1227        $self_url = empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
     1228        $state_query_params = array(
     1229            'customize_theme',
     1230            'customize_changeset_uuid',
     1231            'customize_messenger_channel',
     1232        );
     1233        $self_url = remove_query_arg( $state_query_params, $self_url );
     1234
     1235        $allowed_urls = $this->get_allowed_urls();
     1236        $allowed_hosts = array();
     1237        foreach ( $allowed_urls as $allowed_url ) {
     1238            $parsed = wp_parse_url( $allowed_url );
     1239            if ( empty( $parsed['host'] ) ) {
     1240                continue;
     1241            }
     1242            $host = $parsed['host'];
     1243            if ( ! empty( $parsed['port'] ) ) {
     1244                $host .= ':' . $parsed['port'];
     1245            }
     1246            $allowed_hosts[] = $host;
     1247        }
    8241248        $settings = array(
     1249            'changeset' => array(
     1250                'uuid' => $this->_changeset_uuid,
     1251            ),
     1252            'timeouts' => array(
     1253                'selectiveRefresh' => 250,
     1254                'keepAliveSend' => 1000,
     1255            ),
    8251256            'theme' => array(
    8261257                'stylesheet' => $this->get_stylesheet(),
     
    8281259            ),
    8291260            'url' => array(
    830                 'self' => empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ),
     1261                'self' => $self_url,
     1262                'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
     1263                'allowedHosts' => array_unique( $allowed_hosts ),
     1264                'isCrossDomain' => $this->is_cross_domain(),
    8311265            ),
    832             'channel' => wp_unslash( $_POST['customize_messenger_channel'] ),
     1266            'channel' => $this->messenger_channel,
    8331267            'activePanels' => array(),
    8341268            'activeSections' => array(),
    8351269            'activeControls' => array(),
    8361270            'settingValidities' => $exported_setting_validities,
    837             'nonce' => $this->get_nonces(),
     1271            'nonce' => current_user_can( 'customize' ) ? $this->get_nonces() : array(),
    8381272            'l10n' => array(
    8391273                'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
     1274                'linkUnpreviewable' => __( 'This link is not live-previewable.' ),
     1275                'formUnpreviewable' => __( 'This form is not live-previewable.' ),
    8401276            ),
    841             '_dirty' => array_keys( $this->unsanitized_post_values() ),
     1277            '_dirty' => array_keys( $post_values ),
    8421278        );
    8431279
     
    8931329     *
    8941330     * @since 3.4.0
     1331     * @deprecated 4.7.0
    8951332     */
    8961333    public function customize_preview_signature() {
    897         echo 'WP_CUSTOMIZER_SIGNATURE';
     1334        _deprecated_function( __METHOD__, '4.7.0' );
    8981335    }
    8991336
     
    9021339     *
    9031340     * @since 3.4.0
     1341     * @deprecated 4.7.0
    9041342     *
    9051343     * @param mixed $return Value passed through for {@see 'wp_die_handler'} filter.
     
    9071345     */
    9081346    public function remove_preview_signature( $return = null ) {
    909         remove_action( 'shutdown', array( $this, 'customize_preview_signature' ), 1000 );
     1347        _deprecated_function( __METHOD__, '4.7.0' );
    9101348
    9111349        return $return;
     
    9941432     *
    9951433     * @param array $setting_values Mapping of setting IDs to values to validate and sanitize.
     1434     * @param array $options {
     1435     *     Options.
     1436     *
     1437     *     @type bool $validate_existence  Whether a setting's existence will be checked.
     1438     *     @type bool $validate_capability Whether the setting capability will be checked.
     1439     * }
    9961440     * @return array Mapping of setting IDs to return value of validate method calls, either `true` or `WP_Error`.
    9971441     */
    998     public function validate_setting_values( $setting_values ) {
     1442    public function validate_setting_values( $setting_values, $options = array() ) {
     1443        $options = wp_parse_args( $options, array(
     1444            'validate_capability' => false,
     1445            'validate_existence' => false,
     1446        ) );
     1447
    9991448        $validities = array();
    10001449        foreach ( $setting_values as $setting_id => $unsanitized_value ) {
    10011450            $setting = $this->get_setting( $setting_id );
    1002             if ( ! $setting || is_null( $unsanitized_value ) ) {
     1451            if ( ! $setting ) {
     1452                if ( $options['validate_existence'] ) {
     1453                    $validities[ $setting_id ] = new WP_Error( 'unrecognized', __( 'Setting does not exist or is unrecognized.' ) );
     1454                }
    10031455                continue;
    10041456            }
    1005             $validity = $setting->validate( $unsanitized_value );
     1457            if ( is_null( $unsanitized_value ) ) {
     1458                continue;
     1459            }
     1460            if ( $options['validate_capability'] && ! current_user_can( $setting->capability ) ) {
     1461                $validity = new WP_Error( 'unauthorized', __( 'Unauthorized to modify setting due to capability.' ) );
     1462            } else {
     1463                $validity = $setting->validate( $unsanitized_value );
     1464            }
    10061465            if ( ! is_wp_error( $validity ) ) {
    10071466                /** This filter is documented in wp-includes/class-wp-customize-setting.php */
     
    10571516
    10581517    /**
    1059      * Switch the theme and trigger the save() method on each setting.
    1060      *
    1061      * @since 3.4.0
     1518     * Handle customize_save WP Ajax request to save/update a changeset.
     1519     *
     1520     * @since 3.4.0
     1521     * @since 4.7.0 The semantics of this method have changed to update a changeset, optionally to also change the status and other attributes.
    10621522     */
    10631523    public function save() {
     1524        if ( ! is_user_logged_in() ) {
     1525            wp_send_json_error( 'unauthenticated' );
     1526        }
     1527
    10641528        if ( ! $this->is_preview() ) {
    10651529            wp_send_json_error( 'not_preview' );
     
    10711535        }
    10721536
     1537        $changeset_post_id = $this->changeset_post_id();
     1538        if ( $changeset_post_id && in_array( get_post_status( $changeset_post_id ), array( 'publish', 'trash' ) ) ) {
     1539            wp_send_json_error( 'changeset_already_published' );
     1540        }
     1541
     1542        if ( empty( $changeset_post_id ) ) {
     1543            if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->create_posts ) ) {
     1544                wp_send_json_error( 'cannot_create_changeset_post' );
     1545            }
     1546        } else {
     1547            if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
     1548                wp_send_json_error( 'cannot_edit_changeset_post' );
     1549            }
     1550        }
     1551
     1552        if ( ! empty( $_POST['customize_changeset_data'] ) ) {
     1553            $input_changeset_data = json_decode( wp_unslash( $_POST['customize_changeset_data'] ), true );
     1554            if ( ! is_array( $input_changeset_data ) ) {
     1555                wp_send_json_error( 'invalid_customize_changeset_data' );
     1556            }
     1557        } else {
     1558            $input_changeset_data = array();
     1559        }
     1560
     1561        // Validate title.
     1562        $changeset_title = null;
     1563        if ( isset( $_POST['customize_changeset_title'] ) ) {
     1564            $changeset_title = sanitize_text_field( wp_unslash( $_POST['customize_changeset_title'] ) );
     1565        }
     1566
     1567        // Validate changeset status param.
     1568        $is_publish = null;
     1569        $changeset_status = null;
     1570        if ( isset( $_POST['customize_changeset_status'] ) ) {
     1571            $changeset_status = wp_unslash( $_POST['customize_changeset_status'] );
     1572            if ( ! get_post_status_object( $changeset_status ) || ! in_array( $changeset_status, array( 'draft', 'pending', 'publish', 'future' ), true ) ) {
     1573                wp_send_json_error( 'bad_customize_changeset_status', 400 );
     1574            }
     1575            $is_publish = ( 'publish' === $changeset_status || 'future' === $changeset_status );
     1576            if ( $is_publish ) {
     1577                if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) {
     1578                    wp_send_json_error( 'changeset_publish_unauthorized', 403 );
     1579                }
     1580                if ( false === has_action( 'transition_post_status', '_wp_customize_publish_changeset' ) ) {
     1581                    wp_send_json_error( 'missing_publish_callback', 500 );
     1582                }
     1583            }
     1584        }
     1585
     1586        /*
     1587         * Validate changeset date param. Date is assumed to be in local time for
     1588         * the WP if in MySQL format (YYYY-MM-DD HH:MM:SS). Otherwise, the date
     1589         * is parsed with strtotime() so that ISO date format may be supplied
     1590         * or a string like "+10 minutes".
     1591         */
     1592        $changeset_date_gmt = null;
     1593        if ( isset( $_POST['customize_changeset_date'] ) ) {
     1594            $changeset_date = wp_unslash( $_POST['customize_changeset_date'] );
     1595            if ( preg_match( '/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/', $changeset_date ) ) {
     1596                $mm = substr( $changeset_date, 5, 2 );
     1597                $jj = substr( $changeset_date, 8, 2 );
     1598                $aa = substr( $changeset_date, 0, 4 );
     1599                $valid_date = wp_checkdate( $mm, $jj, $aa, $changeset_date );
     1600                if ( ! $valid_date ) {
     1601                    wp_send_json_error( 'bad_customize_changeset_date', 400 );
     1602                }
     1603                $changeset_date_gmt = get_gmt_from_date( $changeset_date );
     1604            } else {
     1605                $timestamp = strtotime( $changeset_date );
     1606                if ( ! $timestamp ) {
     1607                    wp_send_json_error( 'bad_customize_changeset_date', 400 );
     1608                }
     1609                $changeset_date_gmt = gmdate( 'Y-m-d H:i:s', $timestamp );
     1610            }
     1611            $now = gmdate( 'Y-m-d H:i:59' );
     1612
     1613            $is_future_dated = ( mysql2date( 'U', $changeset_date_gmt, false ) > mysql2date( 'U', $now, false ) );
     1614            if ( ! $is_future_dated ) {
     1615                wp_send_json_error( 'not_future_date', 400 ); // Only future dates are allowed.
     1616            }
     1617
     1618            if ( ! $this->is_theme_active() && ( 'future' === $changeset_status || $is_future_dated ) ) {
     1619                wp_send_json_error( 'cannot_schedule_theme_switches', 400 ); // This should be allowed in the future, when theme is a regular setting.
     1620            }
     1621            $will_remain_auto_draft = ( ! $changeset_status && ( ! $changeset_post_id || 'auto-draft' === get_post_status( $changeset_post_id ) ) );
     1622            if ( $changeset_date && $will_remain_auto_draft ) {
     1623                wp_send_json_error( 'cannot_supply_date_for_auto_draft_changeset', 400 );
     1624            }
     1625        }
     1626
     1627        $r = $this->save_changeset_post( array(
     1628            'status' => $changeset_status,
     1629            'title' => $changeset_title,
     1630            'date_gmt' => $changeset_date_gmt,
     1631            'data' => $input_changeset_data,
     1632        ) );
     1633        if ( is_wp_error( $r ) ) {
     1634            $response = $r->get_error_data();
     1635        } else {
     1636            $response = $r;
     1637
     1638            // Note that if the changeset status was publish, then it will get set to trash if revisions are not supported.
     1639            $response['changeset_status'] = get_post_status( $this->changeset_post_id() );
     1640            if ( $is_publish && 'trash' === $response['changeset_status'] ) {
     1641                $response['changeset_status'] = 'publish';
     1642            }
     1643
     1644            if ( 'publish' === $response['changeset_status'] ) {
     1645                $response['next_changeset_uuid'] = wp_generate_uuid4();
     1646            }
     1647        }
     1648
     1649        if ( isset( $response['setting_validities'] ) ) {
     1650            $response['setting_validities'] = array_map( array( $this, 'prepare_setting_validity_for_js' ), $response['setting_validities'] );
     1651        }
     1652
     1653        /**
     1654         * Filters response data for a successful customize_save Ajax request.
     1655         *
     1656         * This filter does not apply if there was a nonce or authentication failure.
     1657         *
     1658         * @since 4.2.0
     1659         *
     1660         * @param array                $response Additional information passed back to the 'saved'
     1661         *                                       event on `wp.customize`.
     1662         * @param WP_Customize_Manager $this     WP_Customize_Manager instance.
     1663         */
     1664        $response = apply_filters( 'customize_save_response', $response, $this );
     1665
     1666        if ( is_wp_error( $r ) ) {
     1667            wp_send_json_error( $response );
     1668        } else {
     1669            wp_send_json_success( $response );
     1670        }
     1671    }
     1672
     1673    /**
     1674     * Save the post for the loaded changeset.
     1675     *
     1676     * @since 4.7.0
     1677     * @access public
     1678     *
     1679     * @param array $args {
     1680     *     Args for changeset post.
     1681     *
     1682     *     @type array  $data     Optional additional changeset data. Values will be merged on top of any existing post values.
     1683     *     @type string $status   Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed.
     1684     *     @type string $title    Post title. Optional.
     1685     *     @type string $date_gmt Date in GMT. Optional.
     1686     * }
     1687     *
     1688     * @return array|WP_Error Returns array on success and WP_Error with array data on error.
     1689     */
     1690    function save_changeset_post( $args = array() ) {
     1691
     1692        $args = array_merge(
     1693            array(
     1694                'status' => null,
     1695                'title' => null,
     1696                'data' => array(),
     1697                'date_gmt' => null,
     1698            ),
     1699            $args
     1700        );
     1701
     1702        $changeset_post_id = $this->changeset_post_id();
     1703
     1704        // The request was made via wp.customize.previewer.save().
     1705        $update_transactionally = (bool) $args['status'];
     1706        $allow_revision = (bool) $args['status'];
     1707
     1708        // Amend post values with any supplied data.
     1709        foreach ( $args['data'] as $setting_id => $setting_params ) {
     1710            if ( array_key_exists( 'value', $setting_params ) ) {
     1711                $this->set_post_value( $setting_id, $setting_params['value'] ); // Add to post values so that they can be validated and sanitized.
     1712            }
     1713        }
     1714
     1715        // Note that in addition to post data, this will include any stashed theme mods.
     1716        $post_values = $this->unsanitized_post_values( array(
     1717            'exclude_changeset' => true,
     1718            'exclude_post_data' => false,
     1719        ) );
     1720        $this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value.
     1721
    10731722        /**
    10741723         * Fires before save validation happens.
     
    10851734
    10861735        // Validate settings.
    1087         $setting_validities = $this->validate_setting_values( $this->unsanitized_post_values() );
     1736        $setting_validities = $this->validate_setting_values( $post_values, array(
     1737            'validate_capability' => true,
     1738            'validate_existence' => true,
     1739        ) );
    10881740        $invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) );
    1089         $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities );
    1090         if ( $invalid_setting_count > 0 ) {
     1741
     1742        /*
     1743         * Short-circuit if there are invalid settings the update is transactional.
     1744         * A changeset update is transactional when a status is supplied in the request.
     1745         */
     1746        if ( $update_transactionally && $invalid_setting_count > 0 ) {
    10911747            $response = array(
    1092                 'setting_validities' => $exported_setting_validities,
     1748                'setting_validities' => $setting_validities,
    10931749                'message' => sprintf( _n( 'There is %s invalid setting.', 'There are %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ),
    10941750            );
    1095 
    1096             /** This filter is documented in wp-includes/class-wp-customize-manager.php */
    1097             $response = apply_filters( 'customize_save_response', $response, $this );
    1098             wp_send_json_error( $response );
    1099         }
    1100 
    1101         // Do we have to switch themes?
    1102         if ( ! $this->is_theme_active() ) {
    1103             // Temporarily stop previewing the theme to allow switch_themes()
    1104             // to operate properly.
     1751            return new WP_Error( 'transaction_fail', '', $response );
     1752        }
     1753
     1754        $response = array(
     1755            'setting_validities' => $setting_validities,
     1756        );
     1757
     1758        // Obtain/merge data for changeset.
     1759        $original_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
     1760        $data = $original_changeset_data;
     1761        if ( is_wp_error( $data ) ) {
     1762            $data = array();
     1763        }
     1764
     1765        // Ensure that all post values are included in the changeset data.
     1766        foreach ( $post_values as $setting_id => $post_value ) {
     1767            if ( ! isset( $args['data'][ $setting_id ] ) ) {
     1768                $args['data'][ $setting_id ] = array();
     1769            }
     1770            if ( ! isset( $args['data'][ $setting_id ]['value'] ) ) {
     1771                $args['data'][ $setting_id ]['value'] = $post_value;
     1772            }
     1773        }
     1774
     1775        foreach ( $args['data'] as $setting_id => $setting_params ) {
     1776            $setting = $this->get_setting( $setting_id );
     1777            if ( ! $setting || ! $setting->check_capabilities() ) {
     1778                continue;
     1779            }
     1780
     1781            // Skip updating changeset for invalid setting values.
     1782            if ( isset( $setting_validities[ $setting_id ] ) && is_wp_error( $setting_validities[ $setting_id ] ) ) {
     1783                continue;
     1784            }
     1785
     1786            $changeset_setting_id = $setting_id;
     1787            if ( 'theme_mod' === $setting->type ) {
     1788                $changeset_setting_id = sprintf( '%s::%s', $this->get_stylesheet(), $setting_id );
     1789            }
     1790
     1791            if ( null === $setting_params ) {
     1792                // Remove setting from changeset entirely.
     1793                unset( $data[ $changeset_setting_id ] );
     1794            } else {
     1795                // Merge any additional setting params that have been supplied with the existing params.
     1796                if ( ! isset( $data[ $changeset_setting_id ] ) ) {
     1797                    $data[ $changeset_setting_id ] = array();
     1798                }
     1799                $data[ $changeset_setting_id ] = array_merge(
     1800                    $data[ $changeset_setting_id ],
     1801                    $setting_params,
     1802                    array( 'type' => $setting->type )
     1803                );
     1804            }
     1805        }
     1806
     1807        $filter_context = array(
     1808            'uuid' => $this->changeset_uuid(),
     1809            'title' => $args['title'],
     1810            'status' => $args['status'],
     1811            'date_gmt' => $args['date_gmt'],
     1812            'post_id' => $changeset_post_id,
     1813            'previous_data' => is_wp_error( $original_changeset_data ) ? array() : $original_changeset_data,
     1814            'manager' => $this,
     1815        );
     1816
     1817        /**
     1818         * Filters the settings' data that will be persisted into the changeset.
     1819         *
     1820         * Plugins may amend additional data (such as additional meta for settings) into the changeset with this filter.
     1821         *
     1822         * @since 4.7.0
     1823         *
     1824         * @param array $data Updated changeset data, mapping setting IDs to arrays containing a $value item and optionally other metadata.
     1825         * @param array $context {
     1826         *     Filter context.
     1827         *
     1828         *     @type string               $uuid          Changeset UUID.
     1829         *     @type string               $title         Requested title for the changeset post.
     1830         *     @type string               $status        Requested status for the changeset post.
     1831         *     @type string               $date_gmt      Requested date for the changeset post in MySQL format and GMT timezone.
     1832         *     @type int|false            $post_id       Post ID for the changeset, or false if it doesn't exist yet.
     1833         *     @type array                $previous_data Previous data contained in the changeset.
     1834         *     @type WP_Customize_Manager $manager       Manager instance.
     1835         * }
     1836         */
     1837        $data = apply_filters( 'customize_changeset_save_data', $data, $filter_context );
     1838
     1839        // Switch theme if publishing changes now.
     1840        if ( 'publish' === $args['status'] && ! $this->is_theme_active() ) {
     1841            // Temporarily stop previewing the theme to allow switch_themes() to operate properly.
    11051842            $this->stop_previewing_theme();
    11061843            switch_theme( $this->get_stylesheet() );
     
    11091846        }
    11101847
     1848        // Gather the data for wp_insert_post()/wp_update_post().
     1849        $json_options = 0;
     1850        if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) {
     1851            $json_options |= JSON_UNESCAPED_SLASHES; // Introduced in PHP 5.4. This is only to improve readability as slashes needn't be escaped in storage.
     1852        }
     1853        $json_options |= JSON_PRETTY_PRINT; // Also introduced in PHP 5.4, but WP defines constant for back compat. See WP Trac #30139.
     1854        $post_array = array(
     1855            'post_content' => wp_json_encode( $data, $json_options ),
     1856        );
     1857        if ( $args['title'] ) {
     1858            $post_array['post_title'] = $args['title'];
     1859        }
     1860        if ( $changeset_post_id ) {
     1861            $post_array['ID'] = $changeset_post_id;
     1862        } else {
     1863            $post_array['post_type'] = 'customize_changeset';
     1864            $post_array['post_name'] = $this->changeset_uuid();
     1865            $post_array['post_status'] = 'auto-draft';
     1866        }
     1867        if ( $args['status'] ) {
     1868            $post_array['post_status'] = $args['status'];
     1869        }
     1870        if ( $args['date_gmt'] ) {
     1871            $post_array['post_date_gmt'] = $args['date_gmt'];
     1872            $post_array['post_date'] = get_date_from_gmt( $args['date_gmt'] );
     1873        }
     1874
     1875        $this->store_changeset_revision = $allow_revision;
     1876        add_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ), 5, 3 );
     1877
     1878        // Update the changeset post. The publish_customize_changeset action will cause the settings in the changeset to be saved via WP_Customize_Setting::save().
     1879        $has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) );
     1880        if ( $has_kses ) {
     1881            kses_remove_filters(); // Prevent KSES from corrupting JSON in post_content.
     1882        }
     1883
     1884        // Note that updating a post with publish status will trigger WP_Customize_Manager::publish_changeset_values().
     1885        if ( $changeset_post_id ) {
     1886            $post_array['edit_date'] = true; // Prevent date clearing.
     1887            $r = wp_update_post( wp_slash( $post_array ), true );
     1888        } else {
     1889            $r = wp_insert_post( wp_slash( $post_array ), true );
     1890            if ( ! is_wp_error( $r ) ) {
     1891                $this->_changeset_post_id = $r; // Update cached post ID for the loaded changeset.
     1892            }
     1893        }
     1894        if ( $has_kses ) {
     1895            kses_init_filters();
     1896        }
     1897        $this->_changeset_data = null; // Reset so WP_Customize_Manager::changeset_data() will re-populate with updated contents.
     1898
     1899        remove_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ) );
     1900
     1901        if ( is_wp_error( $r ) ) {
     1902            $response['changeset_post_save_failure'] = $r->get_error_code();
     1903            return new WP_Error( 'changeset_post_save_failure', '', $response );
     1904        }
     1905
     1906        return $response;
     1907    }
     1908
     1909    /**
     1910     * Whether a changeset revision should be made.
     1911     *
     1912     * @since 4.7.0
     1913     * @access private
     1914     * @var bool
     1915     */
     1916    protected $store_changeset_revision;
     1917
     1918    /**
     1919     * Filters whether a changeset has changed to create a new revision.
     1920     *
     1921     * Note that this will not be called while a changeset post remains in auto-draft status.
     1922     *
     1923     * @since 4.7.0
     1924     * @access private
     1925     *
     1926     * @param bool    $post_has_changed Whether the post has changed.
     1927     * @param WP_Post $last_revision    The last revision post object.
     1928     * @param WP_Post $post             The post object.
     1929     *
     1930     * @return bool Whether a revision should be made.
     1931     */
     1932    public function _filter_revision_post_has_changed( $post_has_changed, $last_revision, $post ) {
     1933        unset( $last_revision );
     1934        if ( 'customize_changeset' === $post->post_type ) {
     1935            $post_has_changed = $this->store_changeset_revision;
     1936        }
     1937        return $post_has_changed;
     1938    }
     1939
     1940    /**
     1941     * Publish changeset values.
     1942     *
     1943     * This will the values contained in a changeset, even changesets that do not
     1944     * correspond to current manager instance. This is called by
     1945     * `_wp_customize_publish_changeset()` when a customize_changeset post is
     1946     * transitioned to the `publish` status. As such, this method should not be
     1947     * called directly and instead `wp_publish_post()` should be used.
     1948     *
     1949     * Please note that if the settings in the changeset are for a non-activated
     1950     * theme, the theme must first be switched to (via `switch_theme()`) before
     1951     * invoking this method.
     1952     *
     1953     * @since 4.7.0
     1954     * @access private
     1955     * @see _wp_customize_publish_changeset()
     1956     *
     1957     * @param int $changeset_post_id ID for customize_changeset post. Defaults to the changeset for the current manager instance.
     1958     * @return true|WP_Error True or error info.
     1959     */
     1960    public function _publish_changeset_values( $changeset_post_id ) {
     1961        $publishing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
     1962        if ( is_wp_error( $publishing_changeset_data ) ) {
     1963            return $publishing_changeset_data;
     1964        }
     1965
     1966        $changeset_post = get_post( $changeset_post_id );
     1967
     1968        /*
     1969         * Temporarily override the changeset context so that it will be read
     1970         * in calls to unsanitized_post_values() and so that it will be available
     1971         * on the $wp_customize object passed to hooks during the save logic.
     1972         */
     1973        $previous_changeset_post_id = $this->_changeset_post_id;
     1974        $this->_changeset_post_id   = $changeset_post_id;
     1975        $previous_changeset_uuid    = $this->_changeset_uuid;
     1976        $this->_changeset_uuid      = $changeset_post->post_name;
     1977        $previous_changeset_data    = $this->_changeset_data;
     1978        $this->_changeset_data      = $publishing_changeset_data;
     1979
     1980        // Ensure that other theme mods are stashed.
     1981        $other_theme_mod_settings = array();
     1982        if ( did_action( 'switch_theme' ) ) {
     1983            $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
     1984            $matches = array();
     1985            foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) {
     1986                $is_other_theme_mod = (
     1987                    isset( $setting_params['value'] )
     1988                    &&
     1989                    isset( $setting_params['type'] )
     1990                    &&
     1991                    'theme_mod' === $setting_params['type']
     1992                    &&
     1993                    preg_match( $namespace_pattern, $raw_setting_id, $matches )
     1994                    &&
     1995                    $this->get_stylesheet() !== $matches['stylesheet']
     1996                );
     1997                if ( $is_other_theme_mod ) {
     1998                    if ( ! isset( $other_theme_mod_settings[ $matches['stylesheet'] ] ) ) {
     1999                        $other_theme_mod_settings[ $matches['stylesheet'] ] = array();
     2000                    }
     2001                    $other_theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
     2002                }
     2003            }
     2004        }
     2005
     2006        $changeset_setting_values = $this->unsanitized_post_values( array(
     2007            'exclude_post_data' => true,
     2008            'exclude_changeset' => false,
     2009        ) );
     2010        $changeset_setting_ids = array_keys( $changeset_setting_values );
     2011        $this->add_dynamic_settings( $changeset_setting_ids );
     2012
    11112013        /**
    11122014         * Fires once the theme has switched in the Customizer, but before settings
     
    11152017         * @since 3.4.0
    11162018         *
    1117          * @param WP_Customize_Manager $this WP_Customize_Manager instance.
     2019         * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
    11182020         */
    11192021        do_action( 'customize_save', $this );
    11202022
    1121         foreach ( $this->settings as $setting ) {
    1122             $setting->save();
     2023        /*
     2024         * Ensure that all settings will allow themselves to be saved. Note that
     2025         * this is safe because the setting would have checked the capability
     2026         * when the setting value was written into the changeset. So this is why
     2027         * an additional capability check is not required here.
     2028         */
     2029        $original_setting_capabilities = array();
     2030        foreach ( $changeset_setting_ids as $setting_id ) {
     2031            $setting = $this->get_setting( $setting_id );
     2032            if ( $setting ) {
     2033                $original_setting_capabilities[ $setting->id ] = $setting->capability;
     2034                $setting->capability = 'exist';
     2035            }
     2036        }
     2037
     2038        foreach ( $changeset_setting_ids as $setting_id ) {
     2039            $setting = $this->get_setting( $setting_id );
     2040            if ( $setting ) {
     2041                $setting->save();
     2042            }
     2043        }
     2044
     2045        // Update the stashed theme mod settings, removing the active theme's stashed settings, if activated.
     2046        if ( did_action( 'switch_theme' ) ) {
     2047            $this->update_stashed_theme_mod_settings( $other_theme_mod_settings );
    11232048        }
    11242049
     
    11282053         * @since 3.6.0
    11292054         *
    1130          * @param WP_Customize_Manager $this WP_Customize_Manager instance.
     2055         * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
    11312056         */
    11322057        do_action( 'customize_save_after', $this );
    11332058
    1134         $data = array(
    1135             'setting_validities' => $exported_setting_validities,
    1136         );
    1137 
    1138         /**
    1139          * Filters response data for a successful customize_save Ajax request.
    1140          *
    1141          * This filter does not apply if there was a nonce or authentication failure.
    1142          *
    1143          * @since 4.2.0
    1144          *
    1145          * @param array                $data Additional information passed back to the 'saved'
    1146          *                                   event on `wp.customize`.
    1147          * @param WP_Customize_Manager $this WP_Customize_Manager instance.
    1148          */
    1149         $response = apply_filters( 'customize_save_response', $data, $this );
    1150         wp_send_json_success( $response );
     2059        // Restore original capabilities.
     2060        foreach ( $original_setting_capabilities as $setting_id => $capability ) {
     2061            $setting = $this->get_setting( $setting_id );
     2062            if ( $setting ) {
     2063                $setting->capability = $capability;
     2064            }
     2065        }
     2066
     2067        // Restore original changeset data.
     2068        $this->_changeset_data    = $previous_changeset_data;
     2069        $this->_changeset_post_id = $previous_changeset_post_id;
     2070        $this->_changeset_uuid    = $previous_changeset_uuid;
     2071
     2072        return true;
     2073    }
     2074
     2075    /**
     2076     * Update stashed theme mod settings.
     2077     *
     2078     * @since 4.7.0
     2079     * @access private
     2080     *
     2081     * @param array $inactive_theme_mod_settings Mapping of stylesheet to arrays of theme mod settings.
     2082     * @return array|false Returns array of updated stashed theme mods or false if the update failed or there were no changes.
     2083     */
     2084    protected function update_stashed_theme_mod_settings( $inactive_theme_mod_settings ) {
     2085        $stashed_theme_mod_settings = get_option( 'customize_stashed_theme_mods' );
     2086        if ( empty( $stashed_theme_mod_settings ) ) {
     2087            $stashed_theme_mod_settings = array();
     2088        }
     2089
     2090        // Delete any stashed theme mods for the active theme since since they would have been loaded and saved upon activation.
     2091        unset( $stashed_theme_mod_settings[ $this->get_stylesheet() ] );
     2092
     2093        // Merge inactive theme mods with the stashed theme mod settings.
     2094        foreach ( $inactive_theme_mod_settings as $stylesheet => $theme_mod_settings ) {
     2095            if ( ! isset( $stashed_theme_mod_settings[ $stylesheet ] ) ) {
     2096                $stashed_theme_mod_settings[ $stylesheet ] = array();
     2097            }
     2098
     2099            $stashed_theme_mod_settings[ $stylesheet ] = array_merge(
     2100                $stashed_theme_mod_settings[ $stylesheet ],
     2101                $theme_mod_settings
     2102            );
     2103        }
     2104
     2105        $autoload = false;
     2106        $result = update_option( 'customize_stashed_theme_mods', $stashed_theme_mod_settings, $autoload );
     2107        if ( ! $result ) {
     2108            return false;
     2109        }
     2110        return $stashed_theme_mod_settings;
    11512111    }
    11522112
     
    16922652
    16932653    /**
     2654     * Determines whether the admin and the frontend are on different domains.
     2655     *
     2656     * @since 4.7.0
     2657     * @access public
     2658     *
     2659     * @return bool Whether cross-domain.
     2660     */
     2661    public function is_cross_domain() {
     2662        $admin_origin = wp_parse_url( admin_url() );
     2663        $home_origin = wp_parse_url( home_url() );
     2664        $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );
     2665        return $cross_domain;
     2666    }
     2667
     2668    /**
     2669     * Get URLs allowed to be previewed.
     2670     *
     2671     * If the front end and the admin are served from the same domain, load the
     2672     * preview over ssl if the Customizer is being loaded over ssl. This avoids
     2673     * insecure content warnings. This is not attempted if the admin and front end
     2674     * are on different domains to avoid the case where the front end doesn't have
     2675     * ssl certs. Domain mapping plugins can allow other urls in these conditions
     2676     * using the customize_allowed_urls filter.
     2677     *
     2678     * @since 4.7.0
     2679     * @access public
     2680     *
     2681     * @returns array Allowed URLs.
     2682     */
     2683    public function get_allowed_urls() {
     2684        $allowed_urls = array( home_url( '/' ) );
     2685
     2686        if ( is_ssl() && ! $this->is_cross_domain() ) {
     2687            $allowed_urls[] = home_url( '/', 'https' );
     2688        }
     2689
     2690        /**
     2691         * Filters the list of URLs allowed to be clicked and followed in the Customizer preview.
     2692         *
     2693         * @since 3.4.0
     2694         *
     2695         * @param array $allowed_urls An array of allowed URLs.
     2696         */
     2697        $allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) );
     2698
     2699        return $allowed_urls;
     2700    }
     2701
     2702    /**
     2703     * Get messenger channel.
     2704     *
     2705     * @since 4.7.0
     2706     * @access public
     2707     *
     2708     * @return string Messenger channel.
     2709     */
     2710    public function get_messenger_channel() {
     2711        return $this->messenger_channel;
     2712    }
     2713
     2714    /**
    16942715     * Set URL to link the user to when closing the Customizer.
    16952716     *
     
    18002821     */
    18012822    public function customize_pane_settings() {
    1802         /*
    1803          * If the front end and the admin are served from the same domain, load the
    1804          * preview over ssl if the Customizer is being loaded over ssl. This avoids
    1805          * insecure content warnings. This is not attempted if the admin and front end
    1806          * are on different domains to avoid the case where the front end doesn't have
    1807          * ssl certs. Domain mapping plugins can allow other urls in these conditions
    1808          * using the customize_allowed_urls filter.
    1809          */
    1810 
    1811         $allowed_urls = array( home_url( '/' ) );
    1812         $admin_origin = parse_url( admin_url() );
    1813         $home_origin  = parse_url( home_url() );
    1814         $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );
    1815 
    1816         if ( is_ssl() && ! $cross_domain ) {
    1817             $allowed_urls[] = home_url( '/', 'https' );
    1818         }
    1819 
    1820         /**
    1821          * Filters the list of URLs allowed to be clicked and followed in the Customizer preview.
    1822          *
    1823          * @since 3.4.0
    1824          *
    1825          * @param array $allowed_urls An array of allowed URLs.
    1826          */
    1827         $allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) );
    18282823
    18292824        $login_url = add_query_arg( array(
     
    18322827        ), wp_login_url() );
    18332828
     2829        // Ensure dirty flags are set for modified settings.
     2830        foreach ( array_keys( $this->unsanitized_post_values() ) as $setting_id ) {
     2831            $setting = $this->get_setting( $setting_id );
     2832            if ( $setting ) {
     2833                $setting->dirty = true;
     2834            }
     2835        }
     2836
    18342837        // Prepare Customizer settings to pass to JavaScript.
    18352838        $settings = array(
     2839            'changeset' => array(
     2840                'uuid' => $this->changeset_uuid(),
     2841                'status' => $this->changeset_post_id() ? get_post_status( $this->changeset_post_id() ) : '',
     2842            ),
     2843            'timeouts' => array(
     2844                'windowRefresh' => 250,
     2845                'changesetAutoSave' => AUTOSAVE_INTERVAL * 1000,
     2846                'keepAliveCheck' => 2500,
     2847                'reflowPaneContents' => 100,
     2848                'previewFrameSensitivity' => 2000,
     2849            ),
    18362850            'theme'    => array(
    18372851                'stylesheet' => $this->get_stylesheet(),
     
    18432857                'activated'     => esc_url_raw( home_url( '/' ) ),
    18442858                'ajax'          => esc_url_raw( admin_url( 'admin-ajax.php', 'relative' ) ),
    1845                 'allowed'       => array_map( 'esc_url_raw', $allowed_urls ),
    1846                 'isCrossDomain' => $cross_domain,
     2859                'allowed'       => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
     2860                'isCrossDomain' => $this->is_cross_domain(),
    18472861                'home'          => esc_url_raw( home_url( '/' ) ),
    18482862                'login'         => esc_url_raw( $login_url ),
     
    23383352     */
    23393353    public function register_dynamic_settings() {
    2340         $this->add_dynamic_settings( array_keys( $this->unsanitized_post_values() ) );
     3354        $setting_ids = array_keys( $this->unsanitized_post_values() );
     3355        $this->add_dynamic_settings( $setting_ids );
    23413356    }
    23423357
Note: See TracChangeset for help on using the changeset viewer.