Make WordPress Core


Ignore:
Timestamp:
10/18/2016 08:04:36 PM (10 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.