+ $changeset_data = array();
+ if ( $this->changeset_post_id() ) {
+ $changeset_data = $this->get_changeset_post_data( $this->changeset_post_id() );
+ }
+
+ $sidebars_widgets = isset( $starter_content['widgets'] ) && ! empty( $this->widgets ) ? $starter_content['widgets'] : array();
+ $attachments = isset( $starter_content['attachments'] ) && ! empty( $this->nav_menus ) ? $starter_content['attachments'] : array();
+ $posts = isset( $starter_content['posts'] ) && ! empty( $this->nav_menus ) ? $starter_content['posts'] : array();
+ $options = isset( $starter_content['options'] ) ? $starter_content['options'] : array();
+ $nav_menus = isset( $starter_content['nav_menus'] ) && ! empty( $this->nav_menus ) ? $starter_content['nav_menus'] : array();
+ $theme_mods = isset( $starter_content['theme_mods'] ) ? $starter_content['theme_mods'] : array();
+
+ // Widgets.
+ $max_widget_numbers = array();
+ foreach ( $sidebars_widgets as $sidebar_id => $widgets ) {
+ $sidebar_widget_ids = array();
+ foreach ( $widgets as $widget ) {
+ list( $id_base, $instance ) = $widget;
+
+ if ( ! isset( $max_widget_numbers[ $id_base ] ) ) {
+
+ // When $settings is an array-like object, get an intrinsic array for use with array_keys().
+ $settings = get_option( "widget_{$id_base}", array() );
+ if ( $settings instanceof ArrayObject || $settings instanceof ArrayIterator ) {
+ $settings = $settings->getArrayCopy();
+ }
+
+ // Find the max widget number for this type.
+ $widget_numbers = array_keys( $settings );
+ if ( count( $widget_numbers ) > 0 ) {
+ $widget_numbers[] = 1;
+ $max_widget_numbers[ $id_base ] = call_user_func_array( 'max', $widget_numbers );
+ } else {
+ $max_widget_numbers[ $id_base ] = 1;
+ }
+ }
+ $max_widget_numbers[ $id_base ] += 1;
+
+ $widget_id = sprintf( '%s-%d', $id_base, $max_widget_numbers[ $id_base ] );
+ $setting_id = sprintf( 'widget_%s[%d]', $id_base, $max_widget_numbers[ $id_base ] );
+
+ $setting_value = $this->widgets->sanitize_widget_js_instance( $instance );
+ if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
+ $this->set_post_value( $setting_id, $setting_value );
+ $this->pending_starter_content_settings_ids[] = $setting_id;
+ }
+ $sidebar_widget_ids[] = $widget_id;
+ }
+
+ $setting_id = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
+ if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
+ $this->set_post_value( $setting_id, $sidebar_widget_ids );
+ $this->pending_starter_content_settings_ids[] = $setting_id;
+ }
+ }
+
+ $starter_content_auto_draft_post_ids = array();
+ if ( ! empty( $changeset_data['nav_menus_created_posts']['value'] ) ) {
+ $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, $changeset_data['nav_menus_created_posts']['value'] );
+ }
+
+ // Make an index of all the posts needed and what their slugs are.
+ $needed_posts = array();
+ $attachments = $this->prepare_starter_content_attachments( $attachments );
+ foreach ( $attachments as $attachment ) {
+ $key = 'attachment:' . $attachment['post_name'];
+ $needed_posts[ $key ] = true;
+ }
+ foreach ( array_keys( $posts ) as $post_symbol ) {
+ if ( empty( $posts[ $post_symbol ]['post_name'] ) && empty( $posts[ $post_symbol ]['post_title'] ) ) {
+ unset( $posts[ $post_symbol ] );
+ continue;
+ }
+ if ( empty( $posts[ $post_symbol ]['post_name'] ) ) {
+ $posts[ $post_symbol ]['post_name'] = sanitize_title( $posts[ $post_symbol ]['post_title'] );
+ }
+ if ( empty( $posts[ $post_symbol ]['post_type'] ) ) {
+ $posts[ $post_symbol ]['post_type'] = 'post';
+ }
+ $needed_posts[ $posts[ $post_symbol ]['post_type'] . ':' . $posts[ $post_symbol ]['post_name'] ] = true;
+ }
+ $all_post_slugs = array_merge(
+ wp_list_pluck( $attachments, 'post_name' ),
+ wp_list_pluck( $posts, 'post_name' )
+ );
+
+ // Re-use auto-draft starter content posts referenced in the current customized state.
+ $existing_starter_content_posts = array();
+ if ( ! empty( $starter_content_auto_draft_post_ids ) ) {
+ $existing_posts_query = new WP_Query( array(
+ 'post__in' => $starter_content_auto_draft_post_ids,
+ 'post_status' => 'auto-draft',
+ 'post_type' => 'any',
+ 'posts_per_page' => -1,
+ ) );
+ foreach ( $existing_posts_query->posts as $existing_post ) {
+ $post_name = $existing_post->post_name;
+ if ( empty( $post_name ) ) {
+ $post_name = get_post_meta( $existing_post->ID, '_customize_draft_post_name', true );
+ }
+ $existing_starter_content_posts[ $existing_post->post_type . ':' . $post_name ] = $existing_post;
+ }
+ }
+
+ // Re-use non-auto-draft posts.
+ if ( ! empty( $all_post_slugs ) ) {
+ $existing_posts_query = new WP_Query( array(
+ 'post_name__in' => $all_post_slugs,
+ 'post_status' => array_diff( get_post_stati(), array( 'auto-draft' ) ),
+ 'post_type' => 'any',
+ 'posts_per_page' => -1,
+ ) );
+ foreach ( $existing_posts_query->posts as $existing_post ) {
+ $key = $existing_post->post_type . ':' . $existing_post->post_name;
+ if ( isset( $needed_posts[ $key ] ) && ! isset( $existing_starter_content_posts[ $key ] ) ) {
+ $existing_starter_content_posts[ $key ] = $existing_post;
+ }
+ }
+ }
+
+ // Attachments are technically posts but handled differently.
+ if ( ! empty( $attachments ) ) {
+
+ $attachment_ids = array();
+
+ foreach ( $attachments as $symbol => $attachment ) {
+ $file_array = array(
+ 'name' => $attachment['file_name'],
+ );
+ $file_path = $attachment['file_path'];
+ $attachment_id = null;
+ $attached_file = null;
+ if ( isset( $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ] ) ) {
+ $attachment_post = $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ];
+ $attachment_id = $attachment_post->ID;
+ $attached_file = get_attached_file( $attachment_id );
+ if ( empty( $attached_file ) || ! file_exists( $attached_file ) ) {
+ $attachment_id = null;
+ $attached_file = null;
+ } elseif ( $this->get_stylesheet() !== get_post_meta( $attachment_post->ID, '_starter_content_theme', true ) ) {
+
+ // Re-generate attachment metadata since it was previously generated for a different theme.
+ $metadata = wp_generate_attachment_metadata( $attachment_post->ID, $attached_file );
+ wp_update_attachment_metadata( $attachment_id, $metadata );
+ update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
+ }
+ }
+
+ // Insert the attachment auto-draft because it doesn't yet exist or the attached file is gone.
+ if ( ! $attachment_id ) {
+
+ // Copy file to temp location so that original file won't get deleted from theme after sideloading.
+ $temp_file_name = wp_tempnam( basename( $file_path ) );
+ if ( $temp_file_name && copy( $file_path, $temp_file_name ) ) {
+ $file_array['tmp_name'] = $temp_file_name;
+ }
+ if ( empty( $file_array['tmp_name'] ) ) {
+ continue;
+ }
+
+ $attachment_post_data = array_merge(
+ wp_array_slice_assoc( $attachment, array( 'post_title', 'post_content', 'post_excerpt' ) ),
+ array(
+ 'post_status' => 'auto-draft', // So attachment will be garbage collected in a week if changeset is never published.
+ )
+ );
+
+ // In PHP < 5.6 filesize() returns 0 for the temp files unless we clear the file status cache.
+ // Technically, PHP < 5.6.0 || < 5.5.13 || < 5.4.29 but no need to be so targeted.
+ // See https://bugs.php.net/bug.php?id=65701
+ if ( version_compare( PHP_VERSION, '5.6', '<' ) ) {
+ clearstatcache();
+ }
+
+ $attachment_id = media_handle_sideload( $file_array, 0, null, $attachment_post_data );
+ if ( is_wp_error( $attachment_id ) ) {
+ continue;
+ }
+ update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
+ update_post_meta( $attachment_id, '_customize_draft_post_name', $attachment['post_name'] );
+ }
+
+ $attachment_ids[ $symbol ] = $attachment_id;
+ }
+ $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, array_values( $attachment_ids ) );
+ }
+
+ // Posts & pages.
+ if ( ! empty( $posts ) ) {
+ foreach ( array_keys( $posts ) as $post_symbol ) {
+ if ( empty( $posts[ $post_symbol ]['post_type'] ) || empty( $posts[ $post_symbol ]['post_name'] ) ) {
+ continue;
+ }
+ $post_type = $posts[ $post_symbol ]['post_type'];
+ if ( ! empty( $posts[ $post_symbol ]['post_name'] ) ) {
+ $post_name = $posts[ $post_symbol ]['post_name'];
+ } elseif ( ! empty( $posts[ $post_symbol ]['post_title'] ) ) {
+ $post_name = sanitize_title( $posts[ $post_symbol ]['post_title'] );
+ } else {
+ continue;
+ }
+
+ // Use existing auto-draft post if one already exists with the same type and name.
+ if ( isset( $existing_starter_content_posts[ $post_type . ':' . $post_name ] ) ) {
+ $posts[ $post_symbol ]['ID'] = $existing_starter_content_posts[ $post_type . ':' . $post_name ]->ID;
+ continue;
+ }
+
+ // Translate the featured image symbol.
+ if ( ! empty( $posts[ $post_symbol ]['thumbnail'] )
+ && preg_match( '/^{{(?P<symbol>.+)}}$/', $posts[ $post_symbol ]['thumbnail'], $matches )
+ && isset( $attachment_ids[ $matches['symbol'] ] ) ) {
+ $posts[ $post_symbol ]['meta_input']['_thumbnail_id'] = $attachment_ids[ $matches['symbol'] ];
+ }
+
+ if ( ! empty( $posts[ $post_symbol ]['template'] ) ) {
+ $posts[ $post_symbol ]['meta_input']['_wp_page_template'] = $posts[ $post_symbol ]['template'];
+ }
+
+ $r = $this->nav_menus->insert_auto_draft_post( $posts[ $post_symbol ] );
+ if ( $r instanceof WP_Post ) {
+ $posts[ $post_symbol ]['ID'] = $r->ID;
+ }
+ }
+
+ $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, wp_list_pluck( $posts, 'ID' ) );
+ }
+
+ // The nav_menus_created_posts setting is why nav_menus component is dependency for adding posts.
+ if ( ! empty( $this->nav_menus ) && ! empty( $starter_content_auto_draft_post_ids ) ) {
+ $setting_id = 'nav_menus_created_posts';
+ $this->set_post_value( $setting_id, array_unique( array_values( $starter_content_auto_draft_post_ids ) ) );
+ $this->pending_starter_content_settings_ids[] = $setting_id;
+ }
+
+ // Nav menus.
+ $placeholder_id = -1;
+ $reused_nav_menu_setting_ids = array();
+ foreach ( $nav_menus as $nav_menu_location => $nav_menu ) {
+
+ $nav_menu_term_id = null;
+ $nav_menu_setting_id = null;
+ $matches = array();
+
+ // Look for an existing placeholder menu with starter content to re-use.
+ foreach ( $changeset_data as $setting_id => $setting_params ) {
+ $can_reuse = (
+ ! empty( $setting_params['starter_content'] )
+ &&
+ ! in_array( $setting_id, $reused_nav_menu_setting_ids, true )
+ &&
+ preg_match( '#^nav_menu\[(?P<nav_menu_id>-?\d+)\]$#', $setting_id, $matches )
+ );
+ if ( $can_reuse ) {
+ $nav_menu_term_id = intval( $matches['nav_menu_id'] );
+ $nav_menu_setting_id = $setting_id;
+ $reused_nav_menu_setting_ids[] = $setting_id;
+ break;
+ }
+ }
+
+ if ( ! $nav_menu_term_id ) {
+ while ( isset( $changeset_data[ sprintf( 'nav_menu[%d]', $placeholder_id ) ] ) ) {
+ $placeholder_id--;
+ }
+ $nav_menu_term_id = $placeholder_id;
+ $nav_menu_setting_id = sprintf( 'nav_menu[%d]', $placeholder_id );
+ }
+
+ $this->set_post_value( $nav_menu_setting_id, array(
+ 'name' => isset( $nav_menu['name'] ) ? $nav_menu['name'] : $nav_menu_location,
+ ) );
+ $this->pending_starter_content_settings_ids[] = $nav_menu_setting_id;
+
+ // @todo Add support for menu_item_parent.
+ $position = 0;
+ foreach ( $nav_menu['items'] as $nav_menu_item ) {
+ $nav_menu_item_setting_id = sprintf( 'nav_menu_item[%d]', $placeholder_id-- );
+ if ( ! isset( $nav_menu_item['position'] ) ) {
+ $nav_menu_item['position'] = $position++;
+ }
+ $nav_menu_item['nav_menu_term_id'] = $nav_menu_term_id;
+
+ if ( isset( $nav_menu_item['object_id'] ) ) {
+ if ( 'post_type' === $nav_menu_item['type'] && preg_match( '/^{{(?P<symbol>.+)}}$/', $nav_menu_item['object_id'], $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
+ $nav_menu_item['object_id'] = $posts[ $matches['symbol'] ]['ID'];
+ if ( empty( $nav_menu_item['title'] ) ) {
+ $original_object = get_post( $nav_menu_item['object_id'] );
+ $nav_menu_item['title'] = $original_object->post_title;
+ }
+ } else {
+ continue;
+ }
+ } else {
+ $nav_menu_item['object_id'] = 0;
+ }
+
+ if ( empty( $changeset_data[ $nav_menu_item_setting_id ] ) || ! empty( $changeset_data[ $nav_menu_item_setting_id ]['starter_content'] ) ) {
+ $this->set_post_value( $nav_menu_item_setting_id, $nav_menu_item );
+ $this->pending_starter_content_settings_ids[] = $nav_menu_item_setting_id;
+ }
+ }
+
+ $setting_id = sprintf( 'nav_menu_locations[%s]', $nav_menu_location );
+ if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
+ $this->set_post_value( $setting_id, $nav_menu_term_id );
+ $this->pending_starter_content_settings_ids[] = $setting_id;
+ }
+ }
+
+ // Options.
+ foreach ( $options as $name => $value ) {
+ if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
+ if ( isset( $posts[ $matches['symbol'] ] ) ) {
+ $value = $posts[ $matches['symbol'] ]['ID'];
+ } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
+ $value = $attachment_ids[ $matches['symbol'] ];
+ } else {
+ continue;
+ }
+ }
+
+ if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
+ $this->set_post_value( $name, $value );
+ $this->pending_starter_content_settings_ids[] = $name;
+ }
+ }
+
+ // Theme mods.
+ foreach ( $theme_mods as $name => $value ) {
+ if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
+ if ( isset( $posts[ $matches['symbol'] ] ) ) {
+ $value = $posts[ $matches['symbol'] ]['ID'];
+ } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
+ $value = $attachment_ids[ $matches['symbol'] ];
+ } else {
+ continue;
+ }
+ }
+
+ // Handle header image as special case since setting has a legacy format.
+ if ( 'header_image' === $name ) {
+ $name = 'header_image_data';
+ $metadata = wp_get_attachment_metadata( $value );
+ if ( empty( $metadata ) ) {
+ continue;
+ }
+ $value = array(
+ 'attachment_id' => $value,
+ 'url' => wp_get_attachment_url( $value ),
+ 'height' => $metadata['height'],
+ 'width' => $metadata['width'],
+ );
+ } elseif ( 'background_image' === $name ) {
+ $value = wp_get_attachment_url( $value );
+ }
+
+ if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
+ $this->set_post_value( $name, $value );
+ $this->pending_starter_content_settings_ids[] = $name;
+ }
+ }
+
+ if ( ! empty( $this->pending_starter_content_settings_ids ) ) {
+ if ( did_action( 'customize_register' ) ) {
+ $this->_save_starter_content_changeset();
+ } else {
+ add_action( 'customize_register', array( $this, '_save_starter_content_changeset' ), 1000 );
+ }
+ }
+ }
+
+ /**
+ * Prepare starter content attachments.
+ *
+ * Ensure that the attachments are valid and that they have slugs and file name/path.
+ *
+ * @since 4.7.0
+ * @access private
+ *
+ * @param array $attachments Attachments.
+ * @return array Prepared attachments.
+ */
+ protected function prepare_starter_content_attachments( $attachments ) {
+ $prepared_attachments = array();
+ if ( empty( $attachments ) ) {
+ return $prepared_attachments;
+ }
+
+ // Such is The WordPress Way.
+ require_once( ABSPATH . 'wp-admin/includes/file.php' );
+ require_once( ABSPATH . 'wp-admin/includes/media.php' );
+ require_once( ABSPATH . 'wp-admin/includes/image.php' );
+
+ foreach ( $attachments as $symbol => $attachment ) {
+
+ // A file is required and URLs to files are not currently allowed.
+ if ( empty( $attachment['file'] ) || preg_match( '#^https?://$#', $attachment['file'] ) ) {
+ continue;
+ }
+
+ $file_path = null;
+ if ( file_exists( $attachment['file'] ) ) {
+ $file_path = $attachment['file']; // Could be absolute path to file in plugin.
+ } elseif ( is_child_theme() && file_exists( get_stylesheet_directory() . '/' . $attachment['file'] ) ) {
+ $file_path = get_stylesheet_directory() . '/' . $attachment['file'];
+ } elseif ( file_exists( get_template_directory() . '/' . $attachment['file'] ) ) {
+ $file_path = get_template_directory() . '/' . $attachment['file'];
+ } else {
+ continue;
+ }
+ $file_name = basename( $attachment['file'] );
+
+ // Skip file types that are not recognized.
+ $checked_filetype = wp_check_filetype( $file_name );
+ if ( empty( $checked_filetype['type'] ) ) {
+ continue;
+ }
+
+ // Ensure post_name is set since not automatically derived from post_title for new auto-draft posts.
+ if ( empty( $attachment['post_name'] ) ) {
+ if ( ! empty( $attachment['post_title'] ) ) {
+ $attachment['post_name'] = sanitize_title( $attachment['post_title'] );
+ } else {
+ $attachment['post_name'] = sanitize_title( preg_replace( '/\.\w+$/', '', $file_name ) );
+ }
+ }
+
+ $attachment['file_name'] = $file_name;
+ $attachment['file_path'] = $file_path;
+ $prepared_attachments[ $symbol ] = $attachment;
+ }
+ return $prepared_attachments;
+ }
+
+ /**
+ * Save starter content changeset.
+ *
+ * @since 4.7.0
+ * @access private
+ */
+ public function _save_starter_content_changeset() {
+
+ if ( empty( $this->pending_starter_content_settings_ids ) ) {
+ return;
+ }
+
+ $this->save_changeset_post( array(
+ 'data' => array_fill_keys( $this->pending_starter_content_settings_ids, array( 'starter_content' => true ) ),
+ 'starter_content' => true,
+ ) );
+
+ $this->pending_starter_content_settings_ids = array();
+ }
+
+ /**
+ * Get dirty pre-sanitized setting values in the current customized state.
+ *
+ * The returned array consists of a merge of three sources:
+ * 1. If the theme is not currently active, then the base array is any stashed
+ * theme mods that were modified previously but never published.
+ * 2. The values from the current changeset, if it exists.
+ * 3. If the user can customize, the values parsed from the incoming
+ * `$_POST['customized']` JSON data.
+ * 4. Any programmatically-set post values via `WP_Customize_Manager::set_post_value()`.
+ *
+ * The name "unsanitized_post_values" is a carry-over from when the customized
+ * state was exclusively sourced from `$_POST['customized']`. Nevertheless,
+ * the value returned will come from the current changeset post and from the
+ * incoming post data.
+ *
+ * @since 4.1.1
+ * @since 4.7.0 Added $args param and merging with changeset values and stashed theme mods.
+ *
+ * @param array $args {
+ * Args.
+ *
+ * @type bool $exclude_changeset Whether the changeset values should also be excluded. Defaults to false.
+ * @type bool $exclude_post_data Whether the post input values should also be excluded. Defaults to false when lacking the customize capability.
+ * }
+ * @return array
+ */
+ public function unsanitized_post_values( $args = array() ) {
+ $args = array_merge(
+ array(
+ 'exclude_changeset' => false,
+ 'exclude_post_data' => ! current_user_can( 'customize' ),
+ ),
+ $args
+ );
+
+ $values = array();
+
+ // Let default values be from the stashed theme mods if doing a theme switch and if no changeset is present.
+ if ( ! $this->is_theme_active() ) {
+ $stashed_theme_mods = get_option( 'customize_stashed_theme_mods' );
+ $stylesheet = $this->get_stylesheet();
+ if ( isset( $stashed_theme_mods[ $stylesheet ] ) ) {
+ $values = array_merge( $values, wp_list_pluck( $stashed_theme_mods[ $stylesheet ], 'value' ) );
+ }
+ }
+
+ if ( ! $args['exclude_changeset'] ) {
+ foreach ( $this->changeset_data() as $setting_id => $setting_params ) {
+ if ( ! array_key_exists( 'value', $setting_params ) ) {
+ continue;
+ }
+ if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) {
+
+ // Ensure that theme mods values are only used if they were saved under the current theme.
+ $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
+ if ( preg_match( $namespace_pattern, $setting_id, $matches ) && $this->get_stylesheet() === $matches['stylesheet'] ) {
+ $values[ $matches['setting_id'] ] = $setting_params['value'];
+ }
+ } else {
+ $values[ $setting_id ] = $setting_params['value'];
+ }
+ }
+ }
+
+ if ( ! $args['exclude_post_data'] ) {
+ if ( ! isset( $this->_post_values ) ) {
+ if ( isset( $_POST['customized'] ) ) {
+ $post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
+ } else {
+ $post_values = array();
+ }
+ if ( is_array( $post_values ) ) {
+ $this->_post_values = $post_values;
+ } else {
+ $this->_post_values = array();
+ }
+ }
+ $values = array_merge( $values, $this->_post_values );
+ }
+ return $values;
+ }
+
+ /**
+ * Returns the sanitized value for a given setting from the current customized state.
+ *
+ * The name "post_value" is a carry-over from when the customized state was exclusively
+ * sourced from `$_POST['customized']`. Nevertheless, the value returned will come
+ * from the current changeset post and from the incoming post data.
+ *
+ * @since 3.4.0
+ * @since 4.1.1 Introduced the `$default` parameter.
+ * @since 4.6.0 `$default` is now returned early when the setting post value is invalid.
+ * @access public
+ *
+ * @see WP_REST_Server::dispatch()
+ * @see WP_Rest_Request::sanitize_params()
+ * @see WP_Rest_Request::has_valid_params()
+ *
+ * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object.
+ * @param mixed $default Value returned $setting has no post value (added in 4.2.0)
+ * or the post value is invalid (added in 4.6.0).
+ * @return string|mixed $post_value Sanitized value or the $default provided.
+ */
+ public function post_value( $setting, $default = null ) {
+ $post_values = $this->unsanitized_post_values();
+ if ( ! array_key_exists( $setting->id, $post_values ) ) {
+ return $default;
+ }
+ $value = $post_values[ $setting->id ];
+ $valid = $setting->validate( $value );
+ if ( is_wp_error( $valid ) ) {
+ return $default;
+ }
+ $value = $setting->sanitize( $value );
+ if ( is_null( $value ) || is_wp_error( $value ) ) {
+ return $default;
+ }
+ return $value;
+ }
+
+ /**
+ * Override a setting's value in the current customized state.
+ *
+ * The name "post_value" is a carry-over from when the customized state was
+ * exclusively sourced from `$_POST['customized']`.
+ *
+ * @since 4.2.0
+ * @access public
+ *
+ * @param string $setting_id ID for the WP_Customize_Setting instance.
+ * @param mixed $value Post value.
+ */
+ public function set_post_value( $setting_id, $value ) {
+ $this->unsanitized_post_values(); // Populate _post_values from $_POST['customized'].
+ $this->_post_values[ $setting_id ] = $value;
+
+ /**
+ * Announce when a specific setting's unsanitized post value has been set.
+ *
+ * Fires when the WP_Customize_Manager::set_post_value() method is called.
+ *
+ * The dynamic portion of the hook name, `$setting_id`, refers to the setting ID.
+ *
+ * @since 4.4.0
+ *
+ * @param mixed $value Unsanitized setting post value.
+ * @param WP_Customize_Manager $this WP_Customize_Manager instance.
+ */
+ do_action( "customize_post_value_set_{$setting_id}", $value, $this );
+
+ /**
+ * Announce when any setting's unsanitized post value has been set.
+ *
+ * Fires when the WP_Customize_Manager::set_post_value() method is called.
+ *
+ * This is useful for `WP_Customize_Setting` instances to watch
+ * in order to update a cached previewed value.
+ *
+ * @since 4.4.0
+ *
+ * @param string $setting_id Setting ID.
+ * @param mixed $value Unsanitized setting post value.
+ * @param WP_Customize_Manager $this WP_Customize_Manager instance.
+ */
+ do_action( 'customize_post_value_set', $setting_id, $value, $this );
+ }
+
+ /**
+ * Print JavaScript settings.
+ *
+ * @since 3.4.0
+ */
+ public function customize_preview_init() {
+
+ /*
+ * Now that Customizer previews are loaded into iframes via GET requests
+ * and natural URLs with transaction UUIDs added, we need to ensure that
+ * the responses are never cached by proxies. In practice, this will not
+ * be needed if the user is logged-in anyway. But if anonymous access is
+ * allowed then the auth cookies would not be sent and WordPress would
+ * not send no-cache headers by default.
+ */
+ if ( ! headers_sent() ) {
+ nocache_headers();
+ header( 'X-Robots: noindex, nofollow, noarchive' );
+ }
+ add_action( 'wp_head', 'wp_no_robots' );
+ add_filter( 'wp_headers', array( $this, 'filter_iframe_security_headers' ) );
+
+ /*
+ * If preview is being served inside the customizer preview iframe, and
+ * if the user doesn't have customize capability, then it is assumed
+ * that the user's session has expired and they need to re-authenticate.
+ */
+ if ( $this->messenger_channel && ! current_user_can( 'customize' ) ) {
+ $this->wp_die( -1, __( 'Unauthorized. You may remove the customize_messenger_channel param to preview as frontend.' ) );
+ return;
+ }
+
+ $this->prepare_controls();
+
+ add_filter( 'wp_redirect', array( $this, 'add_state_query_params' ) );
+
+ wp_enqueue_script( 'customize-preview' );
+ add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) );
+ add_action( 'wp_head', array( $this, 'remove_frameless_preview_messenger_channel' ) );
+ add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 );
+ add_filter( 'get_edit_post_link', '__return_empty_string' );
+
+ /**
+ * Fires once the Customizer preview has initialized and JavaScript
+ * settings have been printed.
+ *
+ * @since 3.4.0
+ *
+ * @param WP_Customize_Manager $this WP_Customize_Manager instance.
+ */
+ do_action( 'customize_preview_init', $this );
+ }
+
+ /**
+ * Filter the X-Frame-Options and Content-Security-Policy headers to ensure frontend can load in customizer.
+ *
+ * @since 4.7.0
+ * @access public
+ *
+ * @param array $headers Headers.
+ * @return array Headers.
+ */
+ public function filter_iframe_security_headers( $headers ) {
+ $customize_url = admin_url( 'customize.php' );
+ $headers['X-Frame-Options'] = 'ALLOW-FROM ' . $customize_url;
+ $headers['Content-Security-Policy'] = 'frame-ancestors ' . preg_replace( '#^(\w+://[^/]+).+?$#', '$1', $customize_url );
+ return $headers;
+ }
+
+ /**
+ * Add customize state query params to a given URL if preview is allowed.
+ *
+ * @since 4.7.0
+ * @access public
+ * @see wp_redirect()
+ * @see WP_Customize_Manager::get_allowed_url()
+ *
+ * @param string $url URL.
+ * @return string URL.
+ */
+ public function add_state_query_params( $url ) {
+ $parsed_original_url = wp_parse_url( $url );
+ $is_allowed = false;
+ foreach ( $this->get_allowed_urls() as $allowed_url ) {
+ $parsed_allowed_url = wp_parse_url( $allowed_url );
+ $is_allowed = (
+ $parsed_allowed_url['scheme'] === $parsed_original_url['scheme']
+ &&
+ $parsed_allowed_url['host'] === $parsed_original_url['host']
+ &&
+ 0 === strpos( $parsed_original_url['path'], $parsed_allowed_url['path'] )
+ );
+ if ( $is_allowed ) {
+ break;
+ }
+ }
+
+ if ( $is_allowed ) {
+ $query_params = array(
+ 'customize_changeset_uuid' => $this->changeset_uuid(),
+ );
+ if ( ! $this->is_theme_active() ) {
+ $query_params['customize_theme'] = $this->get_stylesheet();
+ }
+ if ( $this->messenger_channel ) {
+ $query_params['customize_messenger_channel'] = $this->messenger_channel;
+ }
+ $url = add_query_arg( $query_params, $url );
+ }
+
+ return $url;
+ }
+
+ /**
+ * Prevent sending a 404 status when returning the response for the customize
+ * preview, since it causes the jQuery Ajax to fail. Send 200 instead.
+ *
+ * @since 4.0.0
+ * @deprecated 4.7.0
+ * @access public
+ */
+ public function customize_preview_override_404_status() {
+ _deprecated_function( __METHOD__, '4.7.0' );
+ }
+
+ /**
+ * Print base element for preview frame.
+ *
+ * @since 3.4.0
+ * @deprecated 4.7.0
+ */
+ public function customize_preview_base() {
+ _deprecated_function( __METHOD__, '4.7.0' );
+ }
+
+ /**
+ * Print a workaround to handle HTML5 tags in IE < 9.
+ *
+ * @since 3.4.0
+ * @deprecated 4.7.0 Customizer no longer supports IE8, so all supported browsers recognize HTML5.
+ */
+ public function customize_preview_html5() {
+ _deprecated_function( __FUNCTION__, '4.7.0' );
+ }
+
+ /**
+ * Print CSS for loading indicators for the Customizer preview.
+ *
+ * @since 4.2.0
+ * @access public
+ */
+ public function customize_preview_loading_style() {
+ ?><style>
+ body.wp-customizer-unloading {
+ opacity: 0.25;
+ cursor: progress !important;
+ -webkit-transition: opacity 0.5s;
+ transition: opacity 0.5s;
+ }
+ body.wp-customizer-unloading * {
+ pointer-events: none !important;
+ }
+ form.customize-unpreviewable,
+ form.customize-unpreviewable input,
+ form.customize-unpreviewable select,
+ form.customize-unpreviewable button,
+ a.customize-unpreviewable,
+ area.customize-unpreviewable {
+ cursor: not-allowed !important;
+ }
+ </style><?php
+ }
+
+ /**
+ * Remove customize_messenger_channel query parameter from the preview window when it is not in an iframe.
+ *
+ * This ensures that the admin bar will be shown. It also ensures that link navigation will
+ * work as expected since the parent frame is not being sent the URL to navigate to.
+ *
+ * @since 4.7.0
+ * @access public
+ */
+ public function remove_frameless_preview_messenger_channel() {
+ if ( ! $this->messenger_channel ) {
+ return;
+ }
+ ?>
+ <script>
+ ( function() {
+ var urlParser, oldQueryParams, newQueryParams, i;
+ if ( parent !== window ) {
+ return;
+ }
+ urlParser = document.createElement( 'a' );
+ urlParser.href = location.href;
+ oldQueryParams = urlParser.search.substr( 1 ).split( /&/ );
+ newQueryParams = [];
+ for ( i = 0; i < oldQueryParams.length; i += 1 ) {
+ if ( ! /^customize_messenger_channel=/.test( oldQueryParams[ i ] ) ) {
+ newQueryParams.push( oldQueryParams[ i ] );
+ }
+ }
+ urlParser.search = newQueryParams.join( '&' );
+ if ( urlParser.search !== location.search ) {
+ location.replace( urlParser.href );
+ }
+ } )();
+ </script>
+ <?php
+ }
+
+ /**
+ * Print JavaScript settings for preview frame.
+ *
+ * @since 3.4.0
+ */
+ public function customize_preview_settings() {
+ $post_values = $this->unsanitized_post_values( array( 'exclude_changeset' => true ) );
+ $setting_validities = $this->validate_setting_values( $post_values );
+ $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities );
+
+ // Note that the REQUEST_URI is not passed into home_url() since this breaks subdirectory installs.
+ $self_url = empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
+ $state_query_params = array(
+ 'customize_theme',
+ 'customize_changeset_uuid',
+ 'customize_messenger_channel',
+ );
+ $self_url = remove_query_arg( $state_query_params, $self_url );
+
+ $allowed_urls = $this->get_allowed_urls();
+ $allowed_hosts = array();
+ foreach ( $allowed_urls as $allowed_url ) {
+ $parsed = wp_parse_url( $allowed_url );
+ if ( empty( $parsed['host'] ) ) {
+ continue;
+ }
+ $host = $parsed['host'];
+ if ( ! empty( $parsed['port'] ) ) {
+ $host .= ':' . $parsed['port'];
+ }
+ $allowed_hosts[] = $host;
+ }
+
+ $switched_locale = switch_to_locale( get_user_locale() );
+ $l10n = array(
+ 'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
+ 'linkUnpreviewable' => __( 'This link is not live-previewable.' ),
+ 'formUnpreviewable' => __( 'This form is not live-previewable.' ),
+ );
+ if ( $switched_locale ) {
+ restore_previous_locale();
+ }
+
+ $settings = array(
+ 'changeset' => array(
+ 'uuid' => $this->_changeset_uuid,
+ ),
+ 'timeouts' => array(
+ 'selectiveRefresh' => 250,
+ 'keepAliveSend' => 1000,
+ ),
+ 'theme' => array(
+ 'stylesheet' => $this->get_stylesheet(),
+ 'active' => $this->is_theme_active(),
+ ),
+ 'url' => array(
+ 'self' => $self_url,
+ 'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
+ 'allowedHosts' => array_unique( $allowed_hosts ),
+ 'isCrossDomain' => $this->is_cross_domain(),
+ ),
+ 'channel' => $this->messenger_channel,
+ 'activePanels' => array(),
+ 'activeSections' => array(),
+ 'activeControls' => array(),
+ 'settingValidities' => $exported_setting_validities,
+ 'nonce' => current_user_can( 'customize' ) ? $this->get_nonces() : array(),
+ 'l10n' => $l10n,
+ '_dirty' => array_keys( $post_values ),
+ );
+
+ foreach ( $this->panels as $panel_id => $panel ) {
+ if ( $panel->check_capabilities() ) {
+ $settings['activePanels'][ $panel_id ] = $panel->active();
+ foreach ( $panel->sections as $section_id => $section ) {
+ if ( $section->check_capabilities() ) {
+ $settings['activeSections'][ $section_id ] = $section->active();
+ }
+ }
+ }
+ }
+ foreach ( $this->sections as $id => $section ) {
+ if ( $section->check_capabilities() ) {
+ $settings['activeSections'][ $id ] = $section->active();
+ }
+ }
+ foreach ( $this->controls as $id => $control ) {
+ if ( $control->check_capabilities() ) {
+ $settings['activeControls'][ $id ] = $control->active();
+ }