diff --git a/css/customize-snapshots-preview.css b/css/customize-snapshots-preview.css
new file mode 100644
index 00000000..0fc5951f
--- /dev/null
+++ b/css/customize-snapshots-preview.css
@@ -0,0 +1,8 @@
+form[method="post"] button[type="submit"],
+form[method="post"] button[type=""],
+form[method="post"] input[type="submit"] {
+ cursor: not-allowed;
+}
+form[method="post"] button:not([type]) {
+ cursor: not-allowed;
+}
diff --git a/customize-snapshots.php b/customize-snapshots.php
index 79008c40..9ab41359 100644
--- a/customize-snapshots.php
+++ b/customize-snapshots.php
@@ -44,7 +44,7 @@
* Admin notice for incompatible versions of PHP.
*/
function customize_snapshots_php_version_error() {
- printf( '
', customize_snapshots_php_version_text() );
+ printf( '', customize_snapshots_php_version_text() ); // WPCS: XSS OK.
}
/**
diff --git a/instance.php b/instance.php
index 38928caf..d1c8fa65 100644
--- a/instance.php
+++ b/instance.php
@@ -23,3 +23,31 @@ function get_plugin_instance() {
global $customize_snapshots_plugin;
return $customize_snapshots_plugin;
}
+
+/**
+ * Convenience function for whether settings are being previewed.
+ *
+ * @see Customize_Snapshot_Manager::is_previewing_settings()
+ * @see Customize_Snapshot_Manager::preview_snapshot_settings()
+ *
+ * @return bool Whether previewing settings.
+ */
+function is_previewing_settings() {
+ return get_plugin_instance()->customize_snapshot_manager->is_previewing_settings();
+}
+
+/**
+ * Convenience function to get the current snapshot UUID.
+ *
+ * @see Customize_Snapshot_Manager::$current_snapshot_uuid
+ *
+ * @return string|null The current snapshot UUID or null if no snapshot.
+ */
+function current_snapshot_uuid() {
+ $customize_snapshot_uuid = get_plugin_instance()->customize_snapshot_manager->current_snapshot_uuid;
+ if ( empty( $customize_snapshot_uuid ) ) {
+ return null;
+ } else {
+ return $customize_snapshot_uuid;
+ }
+}
diff --git a/js/customize-snapshots-frontend.js b/js/customize-snapshots-frontend.js
new file mode 100644
index 00000000..aa3abbaa
--- /dev/null
+++ b/js/customize-snapshots-frontend.js
@@ -0,0 +1,335 @@
+/* global jQuery, confirm */
+/* exported CustomizeSnapshotsFrontend */
+/* eslint consistent-this: [ "error", "section" ], no-magic-numbers: [ "error", { "ignore": [-1,0,1] } ] */
+/* eslint-disable no-alert */
+
+/*
+ * The code here is derived from the initial Transactions pull request: https://github.com/xwp/wordpress-develop/pull/61
+ * See https://github.com/xwp/wordpress-develop/blob/97fd5019c488a0713d34b517bdbff67c62c48a5d/src/wp-includes/js/customize-preview.js#L98-L111
+ */
+
+var CustomizeSnapshotsFrontend = ( function( $ ) {
+ 'use strict';
+
+ var component = {
+ data: {
+ uuid: '',
+ home_url: {
+ scheme: '',
+ host: '',
+ path: ''
+ },
+ l10n: {
+ restoreSessionPrompt: ''
+ }
+ }
+ };
+
+ /**
+ * Init.
+ *
+ * @param {object} args Args.
+ * @param {string} args.uuid UUID.
+ * @returns {void}
+ */
+ component.init = function init( args ) {
+ _.extend( component.data, args );
+
+ component.hasSessionStorage = 'undefined' !== typeof sessionStorage;
+
+ component.keepSessionAlive();
+ component.rememberSessionSnapshot();
+ component.injectSnapshotIntoLinks();
+ component.handleExitSnapshotSessionLink();
+ component.injectSnapshotIntoAjaxRequests();
+ component.injectSnapshotIntoForms();
+ };
+
+ /**
+ * Prompt to restore session.
+ *
+ * @returns {void}
+ */
+ component.keepSessionAlive = function keepSessionAlive() {
+ var currentSnapshotUuid, urlParser, adminBarItem;
+ if ( ! component.hasSessionStorage ) {
+ return;
+ }
+ currentSnapshotUuid = sessionStorage.getItem( 'customize_snapshot_uuid' );
+ if ( ! currentSnapshotUuid || component.data.uuid ) {
+ return;
+ }
+
+ urlParser = document.createElement( 'a' );
+ urlParser.href = location.href;
+ if ( urlParser.search.length > 1 ) {
+ urlParser.search += '&';
+ }
+ urlParser.search += 'customize_snapshot_uuid=' + encodeURIComponent( sessionStorage.getItem( 'customize_snapshot_uuid' ) );
+
+ $( function() {
+ adminBarItem = $( '#wp-admin-bar-resume-customize-snapshot' );
+ if ( adminBarItem.length ) {
+ adminBarItem.find( '> a' ).prop( 'href', urlParser.href );
+ adminBarItem.show();
+ } else if ( confirm( component.data.l10n.restoreSessionPrompt ) ) {
+ location.replace( urlParser.href );
+ } else {
+ sessionStorage.removeItem( 'customize_snapshot_uuid' );
+ }
+ } );
+ };
+
+ /**
+ * Remember the session's snapshot.
+ *
+ * Persist the snapshot UUID in session storage so that we can prompt to restore the snapshot query param if inadvertently dropped.
+ *
+ * @returns {void}
+ */
+ component.rememberSessionSnapshot = function rememberSessionSnapshot() {
+ if ( ! component.hasSessionStorage || ! component.data.uuid ) {
+ return;
+ }
+ sessionStorage.setItem( 'customize_snapshot_uuid', component.data.uuid );
+ };
+
+ /**
+ * Inject the snapshot UUID into links in the document.
+ *
+ * @returns {void}
+ */
+ component.injectSnapshotIntoLinks = function injectSnapshotIntoLinks() {
+ var linkSelectors = 'a, area';
+
+ if ( ! component.data.uuid ) {
+ return;
+ }
+ $( function() {
+
+ // Inject links into initial document.
+ $( document.body ).find( linkSelectors ).each( function() {
+ component.injectSnapshotLinkParam( this );
+ } );
+
+ // Inject links for new elements added to the page
+ if ( 'undefined' !== typeof MutationObserver ) {
+ component.mutationObserver = new MutationObserver( function( mutations ) {
+ _.each( mutations, function( mutation ) {
+ $( mutation.target ).find( linkSelectors ).each( function() {
+ component.injectSnapshotLinkParam( this );
+ } );
+ } );
+ } );
+ component.mutationObserver.observe( document.documentElement, {
+ childList: true,
+ subtree: true
+ } );
+ } else {
+
+ // If mutation observers aren't available, fallback to just-in-time injection.
+ $( document.documentElement ).on( 'click focus mouseover', linkSelectors, function() {
+ component.injectSnapshotLinkParam( this );
+ } );
+ }
+ } );
+ };
+
+ /**
+ * Is matching base URL (host and path)?
+ *
+ * @param {HTMLAnchorElement} parsedUrl Parsed URL.
+ * @param {string} parsedUrl.hostname Host.
+ * @param {string} parsedUrl.pathname Path.
+ * @returns {boolean} Whether matched.
+ */
+ component.isMatchingBaseUrl = function isMatchingBaseUrl( parsedUrl ) {
+ return parsedUrl.hostname === component.data.home_url.host && 0 === parsedUrl.pathname.indexOf( component.data.home_url.path );
+ };
+
+ /**
+ * Should the supplied link have a snapshot UUID added (or does it have one already)?
+ *
+ * @param {HTMLAnchorElement|HTMLAreaElement} element Link element.
+ * @param {string} element.search Query string.
+ * @param {string} element.pathname Path.
+ * @param {string} element.hostname Hostname.
+ * @returns {boolean} Is appropriate for snapshot link.
+ */
+ component.shouldLinkHaveSnapshotParam = function shouldLinkHaveSnapshotParam( element ) {
+ if ( ! component.isMatchingBaseUrl( element ) ) {
+ return false;
+ }
+
+ // Skip wp login and signup pages.
+ if ( /\/wp-(login|signup)\.php$/.test( element.pathname ) ) {
+ return false;
+ }
+
+ // Allow links to admin ajax as faux frontend URLs.
+ if ( /\/wp-admin\/admin-ajax\.php$/.test( element.pathname ) ) {
+ return true;
+ }
+
+ // Disallow links to admin.
+ if ( /\/wp-admin(\/|$)/.test( element.pathname ) ) {
+ return false;
+ }
+
+ // Skip links in admin bar.
+ if ( $( element ).closest( '#wpadminbar' ).length ) {
+ return false;
+ }
+
+ return true;
+ };
+
+ /**
+ * Return whether the supplied link element has the snapshot query param.
+ *
+ * @param {HTMLAnchorElement|HTMLAreaElement} element Link element.
+ * @param {object} element.search Query string.
+ * @returns {boolean} Whether query param is present.
+ */
+ component.doesLinkHaveSnapshotQueryParam = function( element ) {
+ return /(^|&)customize_snapshot_uuid=/.test( element.search.substr( 1 ) );
+ };
+
+ /**
+ * Inject the customize_snapshot_uuid query param into links on the frontend.
+ *
+ * @param {HTMLAnchorElement|HTMLAreaElement} element Link element.
+ * @param {object} element.search Query string.
+ * @returns {void}
+ */
+ component.injectSnapshotLinkParam = function injectSnapshotLinkParam( element ) {
+ if ( component.doesLinkHaveSnapshotQueryParam( element ) || ! component.shouldLinkHaveSnapshotParam( element ) ) {
+ return;
+ }
+
+ if ( element.search.length > 1 ) {
+ element.search += '&';
+ }
+ element.search += 'customize_snapshot_uuid=' + encodeURIComponent( component.data.uuid );
+ };
+
+ /**
+ * Handle electing to exit from the snapshot session.
+ *
+ * @returns {void}
+ */
+ component.handleExitSnapshotSessionLink = function handleExitSnapshotSessionLink() {
+ $( function() {
+ if ( ! component.hasSessionStorage ) {
+ return;
+ }
+ $( '#wpadminbar' ).on( 'click', '#wp-admin-bar-exit-customize-snapshot', function() {
+ sessionStorage.removeItem( 'customize_snapshot_uuid' );
+ } );
+ } );
+ };
+
+ /**
+ * Inject the snapshot UUID into Ajax requests.
+ *
+ * @return {void}
+ */
+ component.injectSnapshotIntoAjaxRequests = function injectSnapshotIntoAjaxRequests() {
+ $.ajaxPrefilter( component.prefilterAjax );
+ };
+
+ /**
+ * Rewrite Ajax requests to inject Customizer state.
+ *
+ * @param {object} options Options.
+ * @param {string} options.type Type.
+ * @param {string} options.url URL.
+ * @returns {void}
+ */
+ component.prefilterAjax = function prefilterAjax( options ) {
+ var urlParser;
+ if ( ! component.data.uuid ) {
+ return;
+ }
+
+ urlParser = document.createElement( 'a' );
+ urlParser.href = options.url;
+
+ // Abort if the request is not for this site.
+ if ( ! component.isMatchingBaseUrl( urlParser ) ) {
+ return;
+ }
+
+ // Skip if snapshot UUID already in URL.
+ if ( -1 !== urlParser.search.indexOf( 'customize_snapshot_uuid=' + component.data.uuid ) ) {
+ return;
+ }
+
+ if ( urlParser.search.substr( 1 ).length > 0 ) {
+ urlParser.search += '&';
+ }
+ urlParser.search += 'customize_snapshot_uuid=' + component.data.uuid;
+
+ options.url = urlParser.href;
+ };
+
+ /**
+ * Inject snapshot into forms, allowing preview to persist through submissions.
+ *
+ * @returns {void}
+ */
+ component.injectSnapshotIntoForms = function injectSnapshotIntoForms() {
+ if ( ! component.data.uuid ) {
+ return;
+ }
+ $( function() {
+
+ // Inject inputs for forms in initial document.
+ $( document.body ).find( 'form' ).each( function() {
+ component.injectSnapshotFormInput( this );
+ } );
+
+ // Inject inputs for new forms added to the page.
+ if ( 'undefined' !== typeof MutationObserver ) {
+ component.mutationObserver = new MutationObserver( function( mutations ) {
+ _.each( mutations, function( mutation ) {
+ $( mutation.target ).find( 'form' ).each( function() {
+ component.injectSnapshotFormInput( this );
+ } );
+ } );
+ } );
+ component.mutationObserver.observe( document.documentElement, {
+ childList: true,
+ subtree: true
+ } );
+ }
+ } );
+ };
+
+ /**
+ * Inject snapshot into form inputs.
+ *
+ * @param {HTMLFormElement} form Form.
+ * @returns {void}
+ */
+ component.injectSnapshotFormInput = function injectSnapshotFormInput( form ) {
+ var urlParser;
+ if ( $( form ).find( 'input[name=customize_snapshot_uuid]' ).length > 0 ) {
+ return;
+ }
+ urlParser = document.createElement( 'a' );
+ urlParser.href = form.action;
+ if ( ! component.isMatchingBaseUrl( urlParser ) ) {
+ return;
+ }
+
+ $( form ).prepend( $( '', {
+ type: 'hidden',
+ name: 'customize_snapshot_uuid',
+ value: component.data.uuid
+ } ) );
+ };
+
+ return component;
+} )( jQuery );
+
diff --git a/js/customize-snapshots-preview.js b/js/customize-snapshots-preview.js
new file mode 100644
index 00000000..80a8f18e
--- /dev/null
+++ b/js/customize-snapshots-preview.js
@@ -0,0 +1,201 @@
+/* global jQuery, JSON */
+/* exported CustomizeSnapshotsPreview */
+/* eslint consistent-this: [ "error", "section" ], no-magic-numbers: [ "error", { "ignore": [-1,0,1] } ] */
+
+/*
+ * The code here is derived from Customize REST Resources: https://github.com/xwp/wp-customize-rest-resources
+ */
+
+var CustomizeSnapshotsPreview = (function( api, $ ) {
+ 'use strict';
+
+ var component = {
+ data: {
+ home_url: {
+ scheme: '',
+ host: '',
+ path: ''
+ },
+ rest_api_url: {
+ scheme: '',
+ host: '',
+ path: ''
+ },
+ admin_ajax_url: {
+ scheme: '',
+ host: '',
+ path: ''
+ },
+ initial_dirty_settings: []
+ }
+ };
+
+ /**
+ * Init.
+ *
+ * @param {object} args Args.
+ * @param {string} args.uuid UUID.
+ * @returns {void}
+ */
+ component.init = function init( args ) {
+ _.extend( component.data, args );
+
+ component.injectSnapshotIntoAjaxRequests();
+ component.handleFormSubmissions();
+ };
+
+ /**
+ * Get customize query vars.
+ *
+ * @see wp.customize.previewer.query
+ *
+ * @returns {{
+ * customized: string,
+ * nonce: string,
+ * wp_customize: string,
+ * theme: string
+ * }} Query vars.
+ */
+ component.getCustomizeQueryVars = function getCustomizeQueryVars() {
+ var customized = {};
+ api.each( function( setting ) {
+ if ( setting._dirty || -1 !== _.indexOf( component.data.initial_dirty_settings, setting.id ) ) {
+ customized[ setting.id ] = setting.get();
+ }
+ } );
+ return {
+ wp_customize: 'on',
+ theme: api.settings.theme.stylesheet,
+ nonce: api.settings.nonce.preview,
+ customized: JSON.stringify( customized )
+ };
+ };
+
+ /**
+ * Inject the snapshot UUID into Ajax requests.
+ *
+ * @return {void}
+ */
+ component.injectSnapshotIntoAjaxRequests = function injectSnapshotIntoAjaxRequests() {
+ $.ajaxPrefilter( component.prefilterAjax );
+ };
+
+ /**
+ * Rewrite Ajax requests to inject Customizer state.
+ *
+ * This will not work 100% of the time, such as if an Admin Ajax handler is
+ * specifically looking for a $_GET param vs a $_POST param.
+ *
+ * @param {object} options Options.
+ * @param {string} options.type Type.
+ * @param {string} options.url URL.
+ * @param {object} originalOptions Original options.
+ * @param {XMLHttpRequest} xhr XHR.
+ * @returns {void}
+ */
+ component.prefilterAjax = function prefilterAjax( options, originalOptions, xhr ) {
+ var requestMethod, urlParser, queryVars, isMatchingHomeUrl, isMatchingRestUrl, isMatchingAdminAjaxUrl;
+
+ urlParser = document.createElement( 'a' );
+ urlParser.href = options.url;
+
+ isMatchingHomeUrl = urlParser.host === component.data.home_url.host && 0 === urlParser.pathname.indexOf( component.data.home_url.path );
+ isMatchingRestUrl = urlParser.host === component.data.rest_api_url.host && 0 === urlParser.pathname.indexOf( component.data.rest_api_url.path );
+ isMatchingAdminAjaxUrl = urlParser.host === component.data.admin_ajax_url.host && 0 === urlParser.pathname.indexOf( component.data.admin_ajax_url.path );
+
+ if ( ! isMatchingHomeUrl && ! isMatchingRestUrl && ! isMatchingAdminAjaxUrl ) {
+ return;
+ }
+
+ requestMethod = options.type.toUpperCase();
+
+ // Customizer currently requires POST requests, so use override (force Backbone.emulateHTTP).
+ if ( 'POST' !== requestMethod ) {
+ xhr.setRequestHeader( 'X-HTTP-Method-Override', requestMethod );
+ options.type = 'POST';
+ }
+
+ if ( options.data && 'GET' === requestMethod ) {
+ /*
+ * Make sure the query vars for the REST API persist in GET (since
+ * REST API explicitly look at $_GET['filter']).
+ * We have to make sure the REST query vars are added as GET params
+ * when the method is GET as otherwise they won't be parsed properly.
+ * The issue lies in \WP_REST_Request::get_parameter_order() which
+ * only is looking at \WP_REST_Request::$method instead of $_SERVER['REQUEST_METHOD'].
+ * @todo Improve \WP_REST_Request::get_parameter_order() to be more aware of X-HTTP-Method-Override
+ */
+ if ( urlParser.search.substr( 1 ).length > 1 ) {
+ urlParser.search += '&';
+ }
+ urlParser.search += options.data;
+ }
+
+ // Add Customizer post data.
+ if ( options.data ) {
+ options.data += '&';
+ } else {
+ options.data = '';
+ }
+ queryVars = component.getCustomizeQueryVars();
+ queryVars.wp_customize_preview_ajax = 'true';
+ options.data += $.param( queryVars );
+ };
+
+ /**
+ * Handle form submissions.
+ *
+ * This fixes Core ticket {@link https://core.trac.wordpress.org/ticket/20714|#20714: Theme customizer: Impossible to preview a search results page}
+ * Implements todo in {@link https://github.com/xwp/wordpress-develop/blob/4.5.3/src/wp-includes/js/customize-preview.js#L69-L73}
+ *
+ * @returns {void}
+ */
+ component.handleFormSubmissions = function handleFormSubmissions() {
+ $( function() {
+
+ // Defer so that we can be sure that our event handler will come after any other event handlers.
+ _.defer( function() {
+ component.replaceFormSubmitHandler();
+ } );
+ } );
+ };
+
+ /**
+ * Replace form submit handler.
+ *
+ * @returns {void}
+ */
+ component.replaceFormSubmitHandler = function replaceFormSubmitHandler() {
+ var body = $( document.body );
+ body.off( 'submit.preview' );
+ body.on( 'submit.preview', 'form', function( event ) {
+ var urlParser;
+
+ /*
+ * If the default wasn't prevented already (in which case the form
+ * submission is already being handled by JS), and if it has a GET
+ * request method, then take the serialized form data and add it as
+ * a query string to the action URL and send this in a url message
+ * to the Customizer pane so that it will be loaded. If the form's
+ * action points to a non-previewable URL, the the Customizer pane's
+ * previewUrl setter will reject it so that the form submission is
+ * a no-op, which is the same behavior as when clicking a link to an
+ * external site in the preview.
+ */
+ if ( ! event.isDefaultPrevented() && 'GET' === this.method.toUpperCase() ) {
+ urlParser = document.createElement( 'a' );
+ urlParser.href = this.action;
+ if ( urlParser.search.substr( 1 ).length > 1 ) {
+ urlParser.search += '&';
+ }
+ urlParser.search += $( this ).serialize();
+ api.preview.send( 'url', urlParser.href );
+ }
+
+ // Now preventDefault as is done on the normal submit.preview handler in customize-preview.js.
+ event.preventDefault();
+ });
+ };
+
+ return component;
+} )( wp.customize, jQuery );
diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php
index b8bd4ebb..62d83872 100644
--- a/php/class-customize-snapshot-manager.php
+++ b/php/class-customize-snapshot-manager.php
@@ -61,6 +61,13 @@ class Customize_Snapshot_Manager {
*/
public $current_snapshot_uuid;
+ /**
+ * Whether the snapshot settings are being previewed.
+ *
+ * @var bool
+ */
+ protected $previewing_settings = false;
+
/**
* The originally active theme.
*
@@ -92,64 +99,152 @@ function init() {
add_action( 'template_redirect', array( $this, 'show_theme_switch_error' ) );
+ add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_controls_scripts' ) );
+ add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_scripts' ) );
+
add_action( 'customize_controls_init', array( $this, 'add_snapshot_uuid_to_return_url' ) );
- add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_templates' ) );
add_action( 'customize_save', array( $this, 'check_customize_publish_authorization' ), 10, 0 );
add_filter( 'customize_refresh_nonces', array( $this, 'filter_customize_refresh_nonces' ) );
add_action( 'admin_bar_menu', array( $this, 'customize_menu' ), 41 );
+ add_action( 'admin_bar_menu', array( $this, 'remove_all_non_snapshot_admin_bar_links' ), 100000 );
+ add_action( 'wp_before_admin_bar_render', array( $this, 'print_admin_bar_styles' ) );
add_filter( 'wp_insert_post_data', array( $this, 'prepare_snapshot_post_content_for_publish' ) );
add_action( 'customize_save_after', array( $this, 'publish_snapshot_with_customize_save_after' ) );
add_action( 'transition_post_status', array( $this, 'save_settings_with_publish_snapshot' ), 10, 3 );
+ add_action( 'wp_ajax_' . self::AJAX_ACTION, array( $this, 'handle_update_snapshot_request' ) );
+ if ( $this->read_current_snapshot_uuid() ) {
+ $this->load_snapshot();
+ } elseif ( is_customize_preview() && isset( $_REQUEST['wp_customize_preview_ajax'] ) && 'true' === $_REQUEST['wp_customize_preview_ajax'] ) {
+ add_action( 'wp_loaded', array( $this, 'setup_preview_ajax_requests' ), 12 );
+ }
+ }
+
+ /**
+ * Read the current snapshot UUID from the request.
+ *
+ * @returns bool Whether a valid snapshot was read.
+ */
+ public function read_current_snapshot_uuid() {
if ( isset( $_REQUEST['customize_snapshot_uuid'] ) ) { // WPCS: input var ok.
$uuid = sanitize_key( wp_unslash( $_REQUEST['customize_snapshot_uuid'] ) ); // WPCS: input var ok.
if ( static::is_valid_uuid( $uuid ) ) {
$this->current_snapshot_uuid = $uuid;
+ return true;
}
}
+ $this->current_snapshot_uuid = null;
+ return false;
+ }
+
+ /**
+ * Load snapshot.
+ */
+ public function load_snapshot() {
+ $this->ensure_customize_manager();
+ $this->snapshot = new Customize_Snapshot( $this, $this->current_snapshot_uuid );
- if ( $this->current_snapshot_uuid ) {
- $this->ensure_customize_manager();
+ if ( ! $this->should_import_and_preview_snapshot( $this->snapshot ) ) {
+ return;
+ }
+
+ $this->add_widget_setting_preview_filters();
+ $this->add_nav_menu_setting_preview_filters();
+
+ /*
+ * Populate post values.
+ *
+ * Note we have to defer until setup_theme since the transaction
+ * can be set beforehand, and wp_magic_quotes() would not have
+ * been called yet, resulting in a $_POST['customized'] that is
+ * double-escaped. Note that this happens at priority 1, which
+ * is immediately after Customize_Snapshot_Manager::store_customized_post_data
+ * which happens at setup_theme priority 0, so that the initial
+ * POST data can be preserved.
+ */
+ if ( did_action( 'setup_theme' ) ) {
+ $this->import_snapshot_data();
+ } else {
+ add_action( 'setup_theme', array( $this, 'import_snapshot_data' ) );
+ }
- add_action( 'wp_ajax_' . self::AJAX_ACTION, array( $this, 'handle_update_snapshot_request' ) );
+ // Block the robots.
+ add_action( 'wp_head', 'wp_no_robots' );
- $this->snapshot = new Customize_Snapshot( $this, $this->current_snapshot_uuid );
+ // Preview post values.
+ if ( did_action( 'wp_loaded' ) ) {
+ $this->preview_snapshot_settings();
+ } else {
+ add_action( 'wp_loaded', array( $this, 'preview_snapshot_settings' ), 11 );
+ }
+ }
- if ( true === $this->should_import_and_preview_snapshot( $this->snapshot ) ) {
+ /**
+ * Setup previewing of Ajax requests in the Customizer preview.
+ *
+ * @global \WP_Customize_Manager $wp_customize
+ */
+ public function setup_preview_ajax_requests() {
+ global $wp_customize, $pagenow;
- $this->add_widget_setting_preview_filters();
- $this->add_nav_menu_setting_preview_filters();
+ /*
+ * When making admin-ajax requests from the frontend, settings won't be
+ * previewed because is_admin() and the call to preview will be
+ * short-circuited in \WP_Customize_Manager::wp_loaded().
+ */
+ if ( ! did_action( 'customize_preview_init' ) ) {
+ $wp_customize->customize_preview_init();
+ }
- /*
- * Populate post values.
- *
- * Note we have to defer until setup_theme since the transaction
- * can be set beforehand, and wp_magic_quotes() would not have
- * been called yet, resulting in a $_POST['customized'] that is
- * double-escaped. Note that this happens at priority 1, which
- * is immediately after Customize_Snapshot_Manager::store_customized_post_data
- * which happens at setup_theme priority 0, so that the initial
- * POST data can be preserved.
- */
- if ( did_action( 'setup_theme' ) ) {
- $this->import_snapshot_data();
- } else {
- add_action( 'setup_theme', array( $this, 'import_snapshot_data' ) );
- }
+ // Note that using $pagenow is easier to test vs DOING_AJAX.
+ if ( ! empty( $pagenow ) && 'admin-ajax.php' === $pagenow ) {
+ $this->override_request_method();
+ } else {
+ add_action( 'parse_request', array( $this, 'override_request_method' ), 5 );
+ }
- // Block the robots.
- add_action( 'wp_head', 'wp_no_robots' );
+ $wp_customize->remove_preview_signature();
+ }
- // Preview post values.
- if ( did_action( 'wp_loaded' ) ) {
- $this->preview_snapshot_settings();
- } else {
- add_action( 'wp_loaded', array( $this, 'preview_snapshot_settings' ), 11 );
- }
- }
+ /**
+ * Attempt to convert the current request environment into another environment.
+ *
+ * @global \WP $wp
+ *
+ * @return bool Whether the override was applied.
+ */
+ public function override_request_method() {
+ global $wp;
+
+ // Skip of X-HTTP-Method-Override request header is not present.
+ if ( ! isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
+ return false;
+ }
+
+ // Skip if REST API request since it has built-in support for overriding the request method.
+ if ( ! empty( $wp ) && ! empty( $wp->query_vars['rest_route'] ) ) {
+ return false;
+ }
+
+ // Skip if the request method is not GET or POST, or the override is the same as the original.
+ $original_request_method = $_SERVER['REQUEST_METHOD'];
+ $override_request_method = strtoupper( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] );
+ if ( ! in_array( $override_request_method, array( 'GET', 'POST' ), true ) || $original_request_method === $override_request_method ) {
+ return false;
}
+
+ // Convert a POST request into a GET request.
+ if ( 'GET' === $override_request_method && 'POST' === $original_request_method ) {
+ $_SERVER['REQUEST_METHOD'] = $override_request_method;
+ $_GET = array_merge( $_GET, $_POST );
+ $_SERVER['QUERY_STRING'] = build_query( array_map( 'rawurlencode', wp_unslash( $_GET ) ) );
+ return true;
+ }
+
+ return false;
}
/**
@@ -195,6 +290,12 @@ public function is_theme_active() {
* @return true|\WP_Error Returns true if previewable, or `WP_Error` if cannot.
*/
public function should_import_and_preview_snapshot( Customize_Snapshot $snapshot ) {
+ global $pagenow;
+
+ // Ignore if in the admin, but not Admin Ajax or Customizer.
+ if ( is_admin() && ! in_array( $pagenow, array( 'admin-ajax.php', 'customize.php' ), true ) ) {
+ return false;
+ }
if ( is_wp_error( $this->get_theme_switch_error( $snapshot ) ) ) {
return false;
@@ -215,7 +316,7 @@ public function should_import_and_preview_snapshot( Customize_Snapshot $snapshot
* Note that wp.customize.Snapshots.extendPreviewerQuery() will extend the
* previewer data to include the current snapshot UUID.
*/
- if ( count( $this->customize_manager->unsanitized_post_values() ) > 0 ) {
+ if ( $this->customize_manager && count( $this->customize_manager->unsanitized_post_values() ) > 0 ) {
return false;
}
@@ -262,6 +363,33 @@ function( $value ) {
}
}
+ /**
+ * Is previewing settings.
+ *
+ * Plugins and themes may currently only use `is_customize_preview()` to
+ * decide whether or not they can store a value in the object cache. For
+ * example, see `Twenty_Eleven_Ephemera_Widget::widget()`. However, when
+ * viewing a snapshot on the frontend, the `is_customize_preview()` method
+ * will return `false`. Plugins and themes that store values in the object
+ * cache must either skip doing this when `$this->previewing` is `true`,
+ * or include the `$this->current_snapshot_uuid` (`current_snapshot_uuid()`)
+ * in the cache key when it is `true`. Note that if the `customize_preview_init` action
+ * was done, this means that the settings have been previewed in the regular
+ * Customizer preview.
+ *
+ * @see Twenty_Eleven_Ephemera_Widget::widget()
+ * @see WP_Customize_Manager::is_previewing_settings()
+ * @see is_previewing_settings()
+ * @see current_snapshot_uuid()()
+ * @see WP_Customize_Manager::customize_preview_init()
+ * @see Customize_Snapshot_Manager::$previewing_settings
+ *
+ * @return bool Whether previewing settings.
+ */
+ public function is_previewing_settings() {
+ return $this->previewing_settings || did_action( 'customize_preview_init' );
+ }
+
/**
* Preview the snapshot settings.
*
@@ -269,11 +397,10 @@ function( $value ) {
* can look at whether the `customize_preview_init` action was done.
*/
public function preview_snapshot_settings() {
-
- // Short-circuit because if customize_preview_init happened, then all settings have been previewed.
- if ( did_action( 'customize_preview_init' ) ) {
+ if ( $this->is_previewing_settings() ) {
return;
}
+ $this->previewing_settings = true;
/*
* Note that we need to preview the settings outside the Customizer preview
@@ -403,17 +530,6 @@ public function preview_early_nav_menus_in_customizer() {
}
}
- /**
- * Get the current URL.
- *
- * @return string
- */
- public function current_url() {
- $http_host = isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : parse_url( home_url(), PHP_URL_HOST ); // WPCS: input var ok; sanitization ok.
- $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '/'; // WPCS: input var ok; sanitization ok.
- return ( is_ssl() ? 'https://' : 'http://' ) . $http_host . $request_uri;
- }
-
/**
* Add snapshot UUID the Customizer return URL.
*
@@ -436,15 +552,6 @@ public function add_snapshot_uuid_to_return_url() {
}
}
- /**
- * Get the clean version of current URL.
- *
- * @return string
- */
- public function remove_snapshot_uuid_from_current_url() {
- return remove_query_arg( array( 'customize_snapshot_uuid' ), $this->current_url() );
- }
-
/**
* Show the theme switch error if there is one.
*/
@@ -518,15 +625,15 @@ static public function encode_json( $value ) {
* @action customize_controls_enqueue_scripts
* @global \WP_Customize_Manager $wp_customize
*/
- public function enqueue_scripts() {
+ public function enqueue_controls_scripts() {
// Prevent loading the Snapshot interface if the theme is not active.
if ( ! $this->is_theme_active() ) {
return;
}
- wp_enqueue_style( $this->plugin->slug );
- wp_enqueue_script( $this->plugin->slug );
+ wp_enqueue_style( 'customize-snapshots' );
+ wp_enqueue_script( 'customize-snapshots' );
// Script data array.
$exports = apply_filters( 'customize_snapshots_export_data', array(
@@ -559,6 +666,62 @@ public function enqueue_scripts() {
);
}
+ /**
+ * Set up Customizer preview.
+ */
+ public function customize_preview_init() {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_preview_scripts' ) );
+ }
+
+ /**
+ * Enqueue Customizer preview scripts.
+ *
+ * @global \WP_Customize_Manager $wp_customize
+ */
+ public function enqueue_preview_scripts() {
+ global $wp_customize;
+
+ $handle = 'customize-snapshots-preview';
+ wp_enqueue_script( $handle );
+ wp_enqueue_style( $handle );
+
+ $exports = array(
+ 'home_url' => wp_parse_url( home_url( '/' ) ),
+ 'rest_api_url' => wp_parse_url( rest_url( '/' ) ),
+ 'admin_ajax_url' => wp_parse_url( admin_url( 'admin-ajax.php' ) ),
+ 'initial_dirty_settings' => array_keys( $wp_customize->unsanitized_post_values() ),
+ );
+ wp_add_inline_script(
+ $handle,
+ sprintf( 'CustomizeSnapshotsPreview.init( %s )', wp_json_encode( $exports ) ),
+ 'after'
+ );
+ }
+
+ /**
+ * Enqueue Customizer frontend scripts.
+ */
+ public function enqueue_frontend_scripts() {
+ if ( ! $this->snapshot ) {
+ return;
+ }
+ $handle = 'customize-snapshots-frontend';
+ wp_enqueue_script( $handle );
+
+ $exports = array(
+ 'uuid' => $this->snapshot ? $this->snapshot->uuid() : null,
+ 'home_url' => wp_parse_url( home_url( '/' ) ),
+ 'l10n' => array(
+ 'restoreSessionPrompt' => __( 'It seems you may have inadvertently navigated away from previewing a customized state. Would you like to restore the snapshot context?', 'customize-snapshots' ),
+ ),
+ );
+ wp_add_inline_script(
+ $handle,
+ sprintf( 'CustomizeSnapshotsFrontend.init( %s )', wp_json_encode( $exports ) ),
+ 'after'
+ );
+ }
+
/**
* Include the snapshot nonce in the Customizer nonces.
*
@@ -997,9 +1160,90 @@ static public function is_valid_uuid( $uuid ) {
* @param \WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance.
*/
public function customize_menu( $wp_admin_bar ) {
+ add_action( 'wp_before_admin_bar_render', 'wp_customize_support_script' );
$this->replace_customize_link( $wp_admin_bar );
+ $this->add_resume_snapshot_link( $wp_admin_bar );
$this->add_post_edit_screen_link( $wp_admin_bar );
- add_action( 'wp_before_admin_bar_render', 'wp_customize_support_script' );
+ $this->add_snapshot_exit_link( $wp_admin_bar );
+ }
+
+ /**
+ * Print admin bar styles.
+ */
+ public function print_admin_bar_styles() {
+ ?>
+
+ snapshot ) ) {
+ return;
+ }
+
+ $customize_node = $wp_admin_bar->get_node( 'customize' );
+ if ( empty( $customize_node ) ) {
+ return;
+ }
+
+ // Remove customize_snapshot_uuuid query param from url param to be previewed in Customizer.
+ $preview_url_query_params = array();
+ $preview_url_parsed = wp_parse_url( $customize_node->href );
+ parse_str( $preview_url_parsed['query'], $preview_url_query_params );
+ if ( ! empty( $preview_url_query_params['url'] ) ) {
+ $preview_url_query_params['url'] = remove_query_arg( array( 'customize_snapshot_uuid' ), $preview_url_query_params['url'] );
+ $customize_node->href = preg_replace(
+ '/(?<=\?).*?(?=#|$)/',
+ build_query( $preview_url_query_params ),
+ $customize_node->href
+ );
+ }
+
+ // Add customize_snapshot_uuid param as param to customize.php itself.
+ $customize_node->href = add_query_arg(
+ array( 'customize_snapshot_uuid' => $this->current_snapshot_uuid ),
+ $customize_node->href
+ );
+
+ $customize_node->meta['class'] .= ' ab-customize-snapshots-item';
+ $wp_admin_bar->add_menu( (array) $customize_node );
+ }
+
+ /**
+ * Adds a link to resume snapshot previewing.
+ *
+ * @param \WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance.
+ */
+ public function add_resume_snapshot_link( $wp_admin_bar ) {
+ $wp_admin_bar->add_menu( array(
+ 'id' => 'resume-customize-snapshot',
+ 'title' => __( 'Resume Snapshot Preview', 'customize-snapshots' ),
+ 'href' => '#',
+ 'meta' => array(
+ 'class' => 'ab-item ab-customize-snapshots-item',
+ ),
+ ) );
}
/**
@@ -1015,43 +1259,63 @@ public function add_post_edit_screen_link( $wp_admin_bar ) {
if ( ! $post ) {
return;
}
- $wp_admin_bar->add_node( array(
- 'parent' => 'customize',
- 'id' => 'snapshot-view-link',
+ $wp_admin_bar->add_menu( array(
+ 'id' => 'inspect-customize-snapshot',
'title' => __( 'Inspect Snapshot', 'customize-snapshots' ),
'href' => get_edit_post_link( $post->ID, 'raw' ),
+ 'meta' => array(
+ 'class' => 'ab-item ab-customize-snapshots-item',
+ ),
) );
}
/**
- * Replaces the "Customize" link in the Toolbar.
+ * Adds an "Exit Snapshot" link to the Toolbar when in Snapshot mode.
*
* @param \WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance.
*/
- public function replace_customize_link( $wp_admin_bar ) {
- // Don't show for users who can't access the customizer or when in the admin.
- if ( ! current_user_can( 'customize' ) || is_admin() ) {
+ public function add_snapshot_exit_link( $wp_admin_bar ) {
+ if ( ! $this->snapshot ) {
return;
}
+ $wp_admin_bar->add_menu( array(
+ 'id' => 'exit-customize-snapshot',
+ 'title' => __( 'Exit Snapshot Preview', 'customize-snapshots' ),
+ 'href' => remove_query_arg( 'customize_snapshot_uuid' ),
+ 'meta' => array(
+ 'class' => 'ab-item ab-customize-snapshots-item',
+ ),
+ ) );
+ }
- $args = array();
- if ( $this->current_snapshot_uuid ) {
- $args['customize_snapshot_uuid'] = $this->current_snapshot_uuid;
+ /**
+ * Remove all admin bar nodes that have links and which aren't for snapshots.
+ *
+ * @param \WP_Admin_Bar $wp_admin_bar Admin bar.
+ */
+ public function remove_all_non_snapshot_admin_bar_links( $wp_admin_bar ) {
+ if ( empty( $this->snapshot ) ) {
+ return;
}
+ $snapshot_admin_bar_node_ids = array( 'customize', 'exit-customize-snapshot', 'inspect-customize-snapshot' );
+ foreach ( $wp_admin_bar->get_nodes() as $node ) {
+ if ( in_array( $node->id, $snapshot_admin_bar_node_ids, true ) || '#' === substr( $node->href, 0, 1 ) ) {
+ continue;
+ }
- $args['url'] = esc_url_raw( $this->remove_snapshot_uuid_from_current_url() );
- $customize_url = add_query_arg( array_map( 'rawurlencode', $args ), wp_customize_url() );
-
- $wp_admin_bar->add_menu(
- array(
- 'id' => 'customize',
- 'title' => __( 'Customize', 'customize-snapshots' ),
- 'href' => $customize_url,
- 'meta' => array(
- 'class' => 'hide-if-no-customize',
- ),
- )
- );
+ $parsed_link_url = wp_parse_url( $node->href );
+ $parsed_home_url = wp_parse_url( home_url( '/' ) );
+ $is_external_link = (
+ isset( $parsed_link_url['host'] ) && $parsed_link_url['host'] !== $parsed_home_url['host']
+ ||
+ isset( $parsed_link_url['path'] ) && 0 !== strpos( $parsed_link_url['path'], $parsed_home_url['path'] )
+ ||
+ ( ! isset( $parsed_link_url['query'] ) || ! preg_match( '#(^|&)customize_snapshot_uuid=#', $parsed_link_url['query'] ) )
+ );
+ if ( $is_external_link ) {
+ $wp_admin_bar->remove_node( $node->id );
+ }
+ }
}
/**
diff --git a/php/class-plugin.php b/php/class-plugin.php
index a2b543b7..0afb3bf6 100644
--- a/php/class-plugin.php
+++ b/php/class-plugin.php
@@ -65,9 +65,21 @@ public function init() {
*/
public function register_scripts( \WP_Scripts $wp_scripts ) {
$min = ( SCRIPT_DEBUG ? '' : '.min' );
+
+ $handle = 'customize-snapshots';
$src = $this->dir_url . 'js/customize-snapshots' . $min . '.js';
$deps = array( 'jquery', 'jquery-ui-dialog', 'wp-util', 'customize-controls' );
- $wp_scripts->add( $this->slug, $src, $deps );
+ $wp_scripts->add( $handle, $src, $deps );
+
+ $handle = 'customize-snapshots-preview';
+ $src = $this->dir_url . 'js/customize-snapshots-preview' . $min . '.js';
+ $deps = array( 'customize-preview' );
+ $wp_scripts->add( $handle, $src, $deps );
+
+ $handle = 'customize-snapshots-frontend';
+ $src = $this->dir_url . 'js/customize-snapshots-frontend' . $min . '.js';
+ $deps = array( 'jquery', 'underscore' );
+ $wp_scripts->add( $handle, $src, $deps );
}
/**
@@ -79,8 +91,15 @@ public function register_scripts( \WP_Scripts $wp_scripts ) {
*/
public function register_styles( \WP_Styles $wp_styles ) {
$min = ( SCRIPT_DEBUG ? '' : '.min' );
+
+ $handle = 'customize-snapshots';
$src = $this->dir_url . 'css/customize-snapshots' . $min . '.css';
$deps = array( 'wp-jquery-ui-dialog' );
- $wp_styles->add( $this->slug, $src, $deps );
+ $wp_styles->add( $handle, $src, $deps );
+
+ $handle = 'customize-snapshots-preview';
+ $src = $this->dir_url . 'css/customize-snapshots-preview' . $min . '.css';
+ $deps = array( 'customize-preview' );
+ $wp_styles->add( $handle, $src, $deps );
}
}
diff --git a/php/class-post-type.php b/php/class-post-type.php
index 43e45890..ca9af763 100644
--- a/php/class-post-type.php
+++ b/php/class-post-type.php
@@ -105,7 +105,7 @@ public function register() {
register_post_type( static::SLUG, $args );
add_filter( 'post_type_link', array( $this, 'filter_post_type_link' ), 10, 2 );
- add_action( 'add_meta_boxes_' . static::SLUG, array( $this, 'remove_publish_metabox' ), 100 );
+ add_action( 'add_meta_boxes_' . static::SLUG, array( $this, 'remove_slug_metabox' ), 100 );
add_action( 'load-revision.php', array( $this, 'suspend_kses_for_snapshot_revision_restore' ) );
add_filter( 'get_the_excerpt', array( $this, 'filter_snapshot_excerpt' ), 10, 2 );
add_filter( 'post_row_actions', array( $this, 'filter_post_row_actions' ), 10, 2 );
@@ -176,7 +176,7 @@ public function setup_metaboxes() {
*
* @codeCoverageIgnore
*/
- public function remove_publish_metabox() {
+ public function remove_slug_metabox() {
remove_meta_box( 'slugdiv', static::SLUG, 'normal' );
}
@@ -594,20 +594,20 @@ public function filter_user_has_cap( $allcaps, $caps ) {
/**
* Display snapshot save error on post list table.
*
- * @param array $status Display status.
- * @param \WP_Post $post Post object.
+ * @param array $states Display states.
+ * @param \WP_Post $post Post object.
*
* @return mixed
*/
- public function display_post_states( $status, $post ) {
+ public function display_post_states( $states, $post ) {
if ( static::SLUG !== $post->post_type ) {
- return $status;
+ return $states;
}
$maybe_error = get_post_meta( $post->ID, 'snapshot_error_on_publish', true );
if ( $maybe_error ) {
- $status['snapshot_error'] = __( 'Error on publish', 'customize-snapshots' );
+ $states['snapshot_error'] = __( 'Error on publish', 'customize-snapshots' );
}
- return $status;
+ return $states;
}
/**
diff --git a/readme.md b/readme.md
index 4270e025..efcb34c5 100644
--- a/readme.md
+++ b/readme.md
@@ -15,11 +15,41 @@ Allow Customizer states to be drafted, and previewed with a private URL.
## Description ##
-Customize Snapshots save the state of a Customizer session so it can be shared or even publish at a future date. A snapshot can be shared with a private URL to both authenticated and non authenticated users. This means anyone can preview a snapshot's settings on the front-end without loading the Customizer, and authenticated users can load the snapshot into the Customizer and publish or amend the settings at any time.
+Customize Snapshots save the state of a Customizer session so it can be shared or even published at a future date. A snapshot can be shared with a private URL to both authenticated and non authenticated users. This means anyone can preview a snapshot's settings on the front-end without loading the Customizer, and authenticated users can load the snapshot into the Customizer and publish or amend the settings at any time.
+
+Requires PHP 5.3+. **Development of this plugin is done [on GitHub](https://github.com/xwp/wp-customize-snapshots). Pull requests welcome. Please see [issues](https://github.com/xwp/wp-customize-snapshots) reported there before going to the [plugin forum](https://wordpress.org/support/plugin/customize-snapshots).**
+### Persistent Object Caching ###
+Plugins and themes may currently only use `is_customize_preview()` to
+decide whether or not they can store a value in the object cache. For
+example, see `Twenty_Eleven_Ephemera_Widget::widget()`. However, when
+viewing a snapshot on the frontend, the `is_customize_preview()` method
+will return `false`. Plugins and themes that store values in the object
+cache must either skip storing in the object cache when `CustomizeSnapshots\is_previewing_settings()`
+is `true`, or they should include the `CustomizeSnapshots\current_snapshot_uuid()` in the cache key.
+
+Example of bypassing object cache when previewing settings inside the Customizer preview or on the frontend via snapshots:
+
+```php
+if ( function_exists( 'CustomizeSnapshots\is_previewing_settings' ) ) {
+ $bypass_object_cache = CustomizeSnapshots\is_previewing_settings();
+} else {
+ $bypass_object_cache = is_customize_preview();
+}
+$contents = null;
+if ( ! $bypass_object_cache ) {
+ $contents = wp_cache_get( 'something', 'myplugin' );
+}
+if ( ! $contents ) {
+ ob_start();
+ myplugin_do_something();
+ $contents = ob_get_clean();
+ echo $contents;
+}
+if ( ! $bypass_object_cache ) {
+ wp_cache_set( 'something', $contents, 'myplugin', HOUR_IN_SECONDS );
+}
+```
-Requires PHP 5.3+.
-
-**Development of this plugin is done [on GitHub](https://github.com/xwp/wp-customize-snapshots). Pull requests welcome. Please see [issues](https://github.com/xwp/wp-customize-snapshots) reported there before going to the [plugin forum](https://wordpress.org/support/plugin/customize-snapshots).**
## Screenshots ##
diff --git a/readme.txt b/readme.txt
index 97ce5866..94614c87 100644
--- a/readme.txt
+++ b/readme.txt
@@ -11,12 +11,42 @@ Allow Customizer states to be drafted, and previewed with a private URL.
== Description ==
-Customize Snapshots save the state of a Customizer session so it can be shared or even publish at a future date. A snapshot can be shared with a private URL to both authenticated and non authenticated users. This means anyone can preview a snapshot's settings on the front-end without loading the Customizer, and authenticated users can load the snapshot into the Customizer and publish or amend the settings at any time.
-
-Requires PHP 5.3+.
-
-**Development of this plugin is done [on GitHub](https://github.com/xwp/wp-customize-snapshots). Pull requests welcome. Please see [issues](https://github.com/xwp/wp-customize-snapshots) reported there before going to the [plugin forum](https://wordpress.org/support/plugin/customize-snapshots).**
-
+Customize Snapshots save the state of a Customizer session so it can be shared or even published at a future date. A snapshot can be shared with a private URL to both authenticated and non authenticated users. This means anyone can preview a snapshot's settings on the front-end without loading the Customizer, and authenticated users can load the snapshot into the Customizer and publish or amend the settings at any time.
+
+Requires PHP 5.3+. **Development of this plugin is done [on GitHub](https://github.com/xwp/wp-customize-snapshots). Pull requests welcome. Please see [issues](https://github.com/xwp/wp-customize-snapshots) reported there before going to the [plugin forum](https://wordpress.org/support/plugin/customize-snapshots).**
+
+= Persistent Object Caching =
+
+Plugins and themes may currently only use `is_customize_preview()` to
+decide whether or not they can store a value in the object cache. For
+example, see `Twenty_Eleven_Ephemera_Widget::widget()`. However, when
+viewing a snapshot on the frontend, the `is_customize_preview()` method
+will return `false`. Plugins and themes that store values in the object
+cache must either skip storing in the object cache when `CustomizeSnapshots\is_previewing_settings()`
+is `true`, or they should include the `CustomizeSnapshots\current_snapshot_uuid()` in the cache key.
+
+Example of bypassing object cache when previewing settings inside the Customizer preview or on the frontend via snapshots:
+
+
+if ( function_exists( 'CustomizeSnapshots\is_previewing_settings' ) ) {
+ $bypass_object_cache = CustomizeSnapshots\is_previewing_settings();
+} else {
+ $bypass_object_cache = is_customize_preview();
+}
+$contents = null;
+if ( ! $bypass_object_cache ) {
+ $contents = wp_cache_get( 'something', 'myplugin' );
+}
+if ( ! $contents ) {
+ ob_start();
+ myplugin_do_something();
+ $contents = ob_get_clean();
+ echo $contents;
+}
+if ( ! $bypass_object_cache ) {
+ wp_cache_set( 'something', $contents, 'myplugin', HOUR_IN_SECONDS );
+}
+
== Screenshots ==
diff --git a/tests/php/test-class-ajax-customize-snapshot-manager.php b/tests/php/test-class-ajax-customize-snapshot-manager.php
index 660486ba..360be871 100644
--- a/tests/php/test-class-ajax-customize-snapshot-manager.php
+++ b/tests/php/test-class-ajax-customize-snapshot-manager.php
@@ -55,6 +55,7 @@ public function setUp() {
parent::setUp();
remove_all_actions( 'wp_ajax_customize_save' );
+ remove_all_actions( 'wp_ajax_customize_update_snapshot' );
$this->plugin = new Plugin();
$this->set_input_vars();
$this->plugin->init();
diff --git a/tests/php/test-class-customize-snapshot-manager.php b/tests/php/test-class-customize-snapshot-manager.php
index 4d52ffe4..24854795 100644
--- a/tests/php/test-class-customize-snapshot-manager.php
+++ b/tests/php/test-class-customize-snapshot-manager.php
@@ -93,6 +93,17 @@ function setUp() {
}
}
+ /**
+ * Clean up global scope.
+ */
+ function clean_up_global_scope() {
+ unset( $GLOBALS['wp_scripts'] );
+ unset( $GLOBALS['wp_styles'] );
+ unset( $_REQUEST['customize_snapshot_uuid'] );
+ unset( $_REQUEST['wp_customize_preview_ajax'] );
+ parent::clean_up_global_scope();
+ }
+
/**
* Tear down.
*/
@@ -100,7 +111,6 @@ function tearDown() {
$this->wp_customize = null;
$this->manager = null;
unset( $GLOBALS['wp_customize'] );
- unset( $GLOBALS['wp_scripts'] );
unset( $GLOBALS['screen'] );
$_REQUEST = array();
parent::tearDown();
@@ -158,7 +168,7 @@ function test_construct_with_customize() {
$this->assertInstanceOf( 'CustomizeSnapshots\Post_Type', $manager->post_type );
$this->assertInstanceOf( 'CustomizeSnapshots\Customize_Snapshot', $manager->snapshot() );
$this->assertEquals( 0, has_action( 'init', array( $manager, 'create_post_type' ) ) );
- $this->assertEquals( 10, has_action( 'customize_controls_enqueue_scripts', array( $manager, 'enqueue_scripts' ) ) );
+ $this->assertEquals( 10, has_action( 'customize_controls_enqueue_scripts', array( $manager, 'enqueue_controls_scripts' ) ) );
$this->assertEquals( 10, has_action( 'wp_ajax_customize_update_snapshot', array( $manager, 'handle_update_snapshot_request' ) ) );
}
@@ -178,12 +188,141 @@ function test_construct_with_customize_bootstrapped() {
}
/**
- * Tests init.
+ * Tests init hooks.
*
* @covers Customize_Snapshot_Manager::init()
*/
- public function test_init() {
- $this->markTestIncomplete();
+ public function test_init_hooks() {
+ $manager = new Customize_Snapshot_Manager( $this->plugin );
+ $manager->init();
+
+ $this->assertInstanceOf( __NAMESPACE__ . '\Post_Type', $manager->post_type );
+ $this->assertEquals( 10, has_action( 'template_redirect', array( $manager, 'show_theme_switch_error' ) ) );
+ $this->assertEquals( 10, has_action( 'customize_controls_enqueue_scripts', array( $manager, 'enqueue_controls_scripts' ) ) );
+ $this->assertEquals( 10, has_action( 'customize_preview_init', array( $manager, 'customize_preview_init' ) ) );
+ $this->assertEquals( 10, has_action( 'wp_enqueue_scripts', array( $manager, 'enqueue_frontend_scripts' ) ) );
+
+ $this->assertEquals( 10, has_action( 'customize_controls_init', array( $manager, 'add_snapshot_uuid_to_return_url' ) ) );
+ $this->assertEquals( 10, has_action( 'customize_controls_print_footer_scripts', array( $manager, 'render_templates' ) ) );
+ $this->assertEquals( 10, has_action( 'customize_save', array( $manager, 'check_customize_publish_authorization' ) ) );
+ $this->assertEquals( 10, has_filter( 'customize_refresh_nonces', array( $manager, 'filter_customize_refresh_nonces' ) ) );
+ $this->assertEquals( 41, has_action( 'admin_bar_menu', array( $manager, 'customize_menu' ) ) );
+ $this->assertEquals( 100000, has_action( 'admin_bar_menu', array( $manager, 'remove_all_non_snapshot_admin_bar_links' ) ) );
+ $this->assertEquals( 10, has_action( 'wp_before_admin_bar_render', array( $manager, 'print_admin_bar_styles' ) ) );
+
+ $this->assertEquals( 10, has_filter( 'wp_insert_post_data', array( $manager, 'prepare_snapshot_post_content_for_publish' ) ) );
+ $this->assertEquals( 10, has_action( 'customize_save_after', array( $manager, 'publish_snapshot_with_customize_save_after' ) ) );
+ $this->assertEquals( 10, has_action( 'transition_post_status', array( $manager, 'save_settings_with_publish_snapshot' ) ) );
+ $this->assertEquals( 10, has_action( 'wp_ajax_customize_update_snapshot', array( $manager, 'handle_update_snapshot_request' ) ) );
+ }
+
+ /**
+ * Tests init hooks.
+ *
+ * @covers Customize_Snapshot_Manager::init()
+ * @covers Customize_Snapshot_Manager::read_current_snapshot_uuid()
+ */
+ public function test_read_current_snapshot_uuid() {
+ $manager = new Customize_Snapshot_Manager( $this->plugin );
+ $this->assertFalse( $manager->read_current_snapshot_uuid() );
+ $this->assertNull( $manager->current_snapshot_uuid );
+
+ $_REQUEST['customize_snapshot_uuid'] = 'bad';
+ $this->assertFalse( $manager->read_current_snapshot_uuid() );
+ $this->assertNull( $manager->current_snapshot_uuid );
+
+ $_REQUEST['customize_snapshot_uuid'] = self::UUID;
+ $this->assertTrue( $manager->read_current_snapshot_uuid() );
+ $this->assertEquals( self::UUID, $manager->current_snapshot_uuid );
+
+ $_REQUEST['customize_snapshot_uuid'] = self::UUID;
+ $manager = new Customize_Snapshot_Manager( $this->plugin );
+ $manager->init();
+ $this->assertEquals( self::UUID, $manager->current_snapshot_uuid );
+ }
+
+ /**
+ * Tests load_snapshot.
+ *
+ * @covers Customize_Snapshot_Manager::init()
+ * @covers Customize_Snapshot_Manager::load_snapshot()
+ */
+ public function test_load_snapshot() {
+ global $wp_actions;
+ $_REQUEST['customize_snapshot_uuid'] = self::UUID;
+ $this->plugin->customize_snapshot_manager->post_type->save( array(
+ 'uuid' => self::UUID,
+ 'data' => array(
+ 'blogname' => array( 'value' => 'Hello' ),
+ ),
+ 'status' => 'draft',
+ ) );
+ $manager = new Customize_Snapshot_Manager( $this->plugin );
+ unset( $wp_actions['setup_theme'] );
+ unset( $wp_actions['wp_loaded'] );
+ $manager->init();
+ $this->assertNotEmpty( $manager->customize_manager );
+ $this->assertNotEmpty( $manager->snapshot );
+
+ $this->assertEquals( 10, has_action( 'setup_theme', array( $manager, 'import_snapshot_data' ) ) );
+ $this->assertEquals( 10, has_action( 'wp_head', 'wp_no_robots' ) );
+ $this->assertEquals( 11, has_action( 'wp_loaded', array( $manager, 'preview_snapshot_settings' ) ) );
+ }
+
+ /**
+ * Tests setup_preview_ajax_requests.
+ *
+ * @covers Customize_Snapshot_Manager::init()
+ * @covers Customize_Snapshot_Manager::setup_preview_ajax_requests()
+ */
+ public function test_setup_preview_ajax_requests() {
+ wp_set_current_user( $this->user_id );
+ $_REQUEST['wp_customize_preview_ajax'] = 'true';
+ $_POST['customized'] = wp_slash( wp_json_encode( array( 'blogname' => 'Foo' ) ) );
+ $manager = new Customize_Snapshot_Manager( $this->plugin );
+ $this->do_customize_boot_actions( true );
+ $this->assertTrue( is_customize_preview() );
+ $manager->init();
+ $this->assertEquals( 12, has_action( 'wp_loaded', array( $manager, 'setup_preview_ajax_requests' ) ) );
+ do_action( 'wp_loaded' );
+
+ $this->assertFalse( has_action( 'shutdown', array( $this->wp_customize, 'customize_preview_signature' ) ) );
+ $this->assertEquals( 5, has_action( 'parse_request', array( $manager, 'override_request_method' ) ) );
+ }
+
+ /**
+ * Tests override_request_method.
+ *
+ * @covers Customize_Snapshot_Manager::override_request_method()
+ */
+ public function test_override_request_method() {
+ global $wp;
+
+ $manager = new Customize_Snapshot_Manager( $this->plugin );
+ $this->assertFalse( $manager->override_request_method() );
+
+ $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'GET';
+ $wp->query_vars['rest_route'] = '/wp/v1/foo';
+ $this->assertFalse( $manager->override_request_method() );
+ unset( $wp->query_vars['rest_route'] );
+
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+ $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'GET';
+ $this->assertFalse( $manager->override_request_method() );
+
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+ $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'BAD';
+ $this->assertFalse( $manager->override_request_method() );
+
+ $_GET = wp_slash( array( 'foo' => '1' ) );
+ $_POST = wp_slash( array( 'bar' => '2' ) );
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+ $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'GET';
+ $this->assertTrue( $manager->override_request_method() );
+ $this->assertEquals( 'GET', $_SERVER['REQUEST_METHOD'] );
+ $this->assertEquals( 'foo=1&bar=2', $_SERVER['QUERY_STRING'] );
+ $this->assertArrayHasKey( 'foo', $_GET );
+ $this->assertArrayHasKey( 'bar', $_GET );
}
/**
@@ -192,7 +331,14 @@ public function test_init() {
* @covers Customize_Snapshot_Manager::doing_customize_save_ajax()
*/
public function test_doing_customize_save_ajax() {
- $this->markTestIncomplete();
+ $manager = new Customize_Snapshot_Manager( $this->plugin );
+ $this->assertFalse( $manager->doing_customize_save_ajax() );
+
+ $_REQUEST['action'] = 'foo';
+ $this->assertFalse( $manager->doing_customize_save_ajax() );
+
+ $_REQUEST['action'] = 'customize_save';
+ $this->assertTrue( $manager->doing_customize_save_ajax() );
}
/**
@@ -201,7 +347,13 @@ public function test_doing_customize_save_ajax() {
* @covers Customize_Snapshot_Manager::ensure_customize_manager()
*/
public function test_ensure_customize_manager() {
- $this->markTestIncomplete();
+ global $wp_customize;
+ $wp_customize = null; // WPCS: global override ok.
+ $manager = new Customize_Snapshot_Manager( $this->plugin );
+ $this->assertEmpty( $manager->customize_manager );
+ $manager->ensure_customize_manager();
+ $this->assertInstanceOf( 'WP_Customize_Manager', $manager->customize_manager );
+ $this->assertInstanceOf( 'WP_Customize_Manager', $wp_customize );
}
/**
@@ -210,7 +362,13 @@ public function test_ensure_customize_manager() {
* @covers Customize_Snapshot_Manager::is_theme_active()
*/
public function test_is_theme_active() {
- $this->markTestIncomplete();
+ global $wp_customize;
+ $wp_customize = null; // WPCS: global override ok.
+ $manager = new Customize_Snapshot_Manager( $this->plugin );
+ $this->assertTrue( $manager->is_theme_active() );
+
+ $manager->ensure_customize_manager();
+ $this->assertTrue( $manager->is_theme_active() );
}
/**
@@ -219,7 +377,84 @@ public function test_is_theme_active() {
* @covers Customize_Snapshot_Manager::should_import_and_preview_snapshot()
*/
public function test_should_import_and_preview_snapshot() {
- $this->markTestIncomplete();
+ global $pagenow, $wp_customize;
+ $_REQUEST['customize_snapshot_uuid'] = self::UUID;
+ $manager = $this->plugin->customize_snapshot_manager;
+ $post_id = $manager->post_type->save( array(
+ 'uuid' => self::UUID,
+ 'data' => array( 'blogname' => array( 'value' => 'Foo' ) ),
+ ) );
+ $snapshot = new Customize_Snapshot( $manager, self::UUID );
+
+ // Not if admin.
+ set_current_screen( 'posts' );
+ $pagenow = 'posts.php'; // WPCS: global override ok.
+ $this->assertTrue( is_admin() );
+ $this->assertFalse( $manager->should_import_and_preview_snapshot( $snapshot ) );
+
+ // Not if theme switch error.
+ set_current_screen( 'customize' );
+ $pagenow = 'customize.php'; // WPCS: global override ok.
+ update_post_meta( $post_id, '_snapshot_theme', 'Foo' );
+ $this->assertFalse( $manager->should_import_and_preview_snapshot( $snapshot ) );
+ delete_post_meta( $post_id, '_snapshot_theme' );
+
+ // Not if customize_save.
+ $_REQUEST['action'] = 'customize_save';
+ $this->assertFalse( $manager->should_import_and_preview_snapshot( $snapshot ) );
+ unset( $_REQUEST['action'] );
+
+ // Not if published snapshot.
+ $manager->post_type->save( array(
+ 'uuid' => self::UUID,
+ 'status' => 'publish',
+ ) );
+ $this->assertFalse( $manager->should_import_and_preview_snapshot( $snapshot ) );
+ $manager->post_type->save( array(
+ 'uuid' => self::UUID,
+ 'status' => 'draft',
+ ) );
+
+ // Not if unsanitized post values is not empty.
+ $manager->customize_manager = new \WP_Customize_Manager();
+ $wp_customize = $manager->customize_manager; // WPCS: global override ok.
+ $wp_customize->set_post_value( 'name', 'value' );
+ $this->assertNotEmpty( $manager->customize_manager->unsanitized_post_values() );
+ $this->assertFalse( $manager->should_import_and_preview_snapshot( $snapshot ) );
+
+ // OK.
+ $manager->customize_manager = new \WP_Customize_Manager();
+ $wp_customize = $manager->customize_manager; // WPCS: global override ok.
+ $this->assertTrue( $manager->should_import_and_preview_snapshot( $snapshot ) );
+ }
+
+ /**
+ * Tests is_previewing_settings.
+ *
+ * @covers Customize_Snapshot_Manager::is_previewing_settings()
+ */
+ public function test_is_previewing_settings() {
+ $_REQUEST['customize_snapshot_uuid'] = self::UUID;
+ $this->plugin->customize_snapshot_manager->post_type->save( array(
+ 'uuid' => self::UUID,
+ 'data' => array( 'blogname' => array( 'value' => 'Foo' ) ),
+ ) );
+ $manager = new Customize_Snapshot_Manager( $this->plugin );
+ $manager->init();
+ $manager->preview_snapshot_settings();
+ $this->assertTrue( $manager->is_previewing_settings() );
+ }
+
+ /**
+ * Tests is_previewing_settings.
+ *
+ * @covers Customize_Snapshot_Manager::is_previewing_settings()
+ */
+ public function test_is_previewing_settings_via_preview_init() {
+ $manager = new Customize_Snapshot_Manager( $this->plugin );
+ $this->assertFalse( $manager->is_previewing_settings() );
+ do_action( 'customize_preview_init' );
+ $this->assertTrue( $manager->is_previewing_settings() );
}
/**
@@ -285,22 +520,6 @@ public function test_add_snapshot_uuid_to_return_url() {
}
}
- /**
- * Test remove snapshot uuid from current url.
- *
- * @covers Customize_Snapshot_Manager::remove_snapshot_uuid_from_current_url()
- * @covers Customize_Snapshot_Manager::current_url()
- */
- function test_remove_snapshot_uuid_from_current_url() {
- $this->go_to( home_url( '?customize_snapshot_uuid=' . self::UUID ) );
- ob_start();
- $manager = new Customize_Snapshot_Manager( $this->plugin );
- $this->assertContains( 'customize_snapshot_uuid', $manager->current_url() );
- echo $manager->remove_snapshot_uuid_from_current_url(); // WPCS: xss ok.
- $buffer = ob_get_clean();
- $this->assertEquals( home_url( '/' ), $buffer );
- }
-
/**
* Tests show_theme_switch_error.
*
@@ -342,18 +561,40 @@ function test_encode_json() {
}
/**
- * Test enqueue scripts.
+ * Test enqueue controls scripts.
*
- * @see Customize_Snapshot_Manager::enqueue_scripts()
+ * @see Customize_Snapshot_Manager::enqueue_controls_scripts()
*/
- function test_enqueue_scripts() {
+ function test_enqueue_controls_scripts() {
$this->plugin->register_scripts( wp_scripts() );
$this->plugin->register_styles( wp_styles() );
$manager = new Customize_Snapshot_Manager( $this->plugin );
$manager->init();
- $manager->enqueue_scripts();
- $this->assertTrue( wp_script_is( $this->plugin->slug, 'enqueued' ) );
- $this->assertTrue( wp_style_is( $this->plugin->slug, 'enqueued' ) );
+ $manager->enqueue_controls_scripts();
+ $this->assertTrue( wp_script_is( 'customize-snapshots', 'enqueued' ) );
+ $this->assertTrue( wp_style_is( 'customize-snapshots', 'enqueued' ) );
+ }
+
+ /**
+ * Test enqueue frontend scripts.
+ *
+ * @see Customize_Snapshot_Manager::enqueue_frontend_scripts()
+ */
+ function test_enqueue_frontend_scripts() {
+ $this->plugin->register_scripts( wp_scripts() );
+ $this->plugin->register_styles( wp_styles() );
+ $manager = new Customize_Snapshot_Manager( $this->plugin );
+ $manager->init();
+ $this->assertFalse( wp_script_is( 'customize-snapshots-frontend', 'enqueued' ) );
+ $manager->enqueue_frontend_scripts();
+ $this->assertFalse( wp_script_is( 'customize-snapshots-frontend', 'enqueued' ) );
+
+ $_REQUEST['customize_snapshot_uuid'] = self::UUID;
+ $manager = new Customize_Snapshot_Manager( $this->plugin );
+ $manager->init();
+ $this->assertFalse( wp_script_is( 'customize-snapshots-frontend', 'enqueued' ) );
+ $manager->enqueue_frontend_scripts();
+ $this->assertTrue( wp_script_is( 'customize-snapshots-frontend', 'enqueued' ) );
}
/**
@@ -530,21 +771,27 @@ public function test_is_valid_uuid() {
*/
public function test_customize_menu() {
set_current_screen( 'front' );
- $customize_url = admin_url( 'customize.php' ) . '?customize_snapshot_uuid=' . self::UUID . '&url=' . urlencode( esc_url( home_url( '/' ) ) );
+ $preview_url = home_url( '/' );
$_REQUEST['customize_snapshot_uuid'] = self::UUID;
$manager = new Customize_Snapshot_Manager( $this->plugin );
$manager->init();
require_once( ABSPATH . WPINC . '/class-wp-admin-bar.php' );
- $wp_admin_bar = new \WP_Admin_Bar();
+ $wp_admin_bar = new \WP_Admin_Bar(); // WPCS: Override OK.
$this->assertInstanceOf( 'WP_Admin_Bar', $wp_admin_bar );
wp_set_current_user( $this->user_id );
$this->go_to( home_url( '?customize_snapshot_uuid=' . self::UUID ) );
+ $wp_admin_bar->initialize();
+ $wp_admin_bar->add_menus();
do_action_ref_array( 'admin_bar_menu', array( &$wp_admin_bar ) );
- $this->assertEquals( $customize_url, $wp_admin_bar->get_node( 'customize' )->href );
+ $parsed_url = wp_parse_url( $wp_admin_bar->get_node( 'customize' )->href );
+ $query_params = array();
+ wp_parse_str( $parsed_url['query'], $query_params );
+ $this->assertEquals( $preview_url, $query_params['url'] );
+ $this->assertEquals( self::UUID, $query_params['customize_snapshot_uuid'] );
}
/**
@@ -554,7 +801,7 @@ public function test_customize_menu() {
*/
public function test_customize_menu_return() {
require_once( ABSPATH . WPINC . '/class-wp-admin-bar.php' );
- $wp_admin_bar = new \WP_Admin_Bar;
+ $wp_admin_bar = new \WP_Admin_Bar(); // WPCS: Override OK.
$this->assertInstanceOf( 'WP_Admin_Bar', $wp_admin_bar );
wp_set_current_user( $this->factory()->user->create( array( 'role' => 'editor' ) ) );
@@ -565,12 +812,17 @@ public function test_customize_menu_return() {
}
/**
- * Test add_post_edit_screen_link.
+ * Tests print_admin_bar_styles.
*
- * @covers Customize_Snapshot_Manager::add_post_edit_screen_link()
+ * @covers Customize_Snapshot_Manager::print_admin_bar_styles()
*/
- public function test_add_post_edit_screen_link() {
- $this->markTestIncomplete();
+ public function test_print_admin_bar_styles() {
+ $manager = new Customize_Snapshot_Manager( $this->plugin );
+ $manager->init();
+ ob_start();
+ $manager->print_admin_bar_styles();
+ $contents = ob_get_clean();
+ $this->assertContains( '