From fe54c3ce621269bbd3b91b1314aece358c4ba0ca Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 6 Aug 2016 17:20:13 -0700 Subject: [PATCH 01/31] Ensure customize_snapshot_uuid gets added to frontend links --- js/customize-snapshots-frontend.js | 68 +++++++++++++++++++ php/class-customize-snapshot-manager.php | 30 ++++++-- php/class-plugin.php | 13 +++- .../test-class-customize-snapshot-manager.php | 6 +- 4 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 js/customize-snapshots-frontend.js diff --git a/js/customize-snapshots-frontend.js b/js/customize-snapshots-frontend.js new file mode 100644 index 00000000..550d33ef --- /dev/null +++ b/js/customize-snapshots-frontend.js @@ -0,0 +1,68 @@ +/* global jQuery */ +/* exported CustomizeSnapshotsFrontend */ +/* eslint consistent-this: [ "error", "section" ], no-magic-numbers: [ "error", { "ignore": [0,1] } ] */ + +// @todo Use session storage to make sure the user stays in the snapshot frontend preview, unless explicitly exited. Warn with confirm if clicking. +// @todo Inject customize_snapshot_uuid into all Ajax requests back to the site. + +var CustomizeSnapshotsFrontend = ( function( $ ) { + 'use strict'; + + var component = { + data: { + uuid: '', + home_url: { + scheme: '', + host: '', + path: '' + } + } + }; + + /** + * Init. + * + * @param {object} args Args. + * @param {string} args.uuid UUID. + * @returns {void} + */ + component.init = function init( args ) { + if ( ! args.uuid ) { + throw new Error( 'Missing UUID' ); + } + + _.extend( component.data, args ); + + // Inject snapshot UUID into links on click. + $( document.documentElement ).on( 'click focus mouseover', 'a, area', function() { + component.injectQueryParam( this ); + } ); + }; + + /** + * 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.injectQueryParam = function injectQueryParam( element ) { + if ( element.hostname !== component.data.home_url.host ) { + return; + } + if ( 0 !== element.pathname.indexOf( component.data.home_url.path ) ) { + return; + } + if ( /(^|&)customize_snapshot_uuid=/.test( element.search.substr( 1 ) ) ) { + return; + } + + if ( element.search.length > 1 ) { + element.search += '&'; + } + element.search += 'customize_snapshot_uuid=' + encodeURIComponent( component.data.uuid ); + }; + + return component; +} )( jQuery ); + diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php index b8bd4ebb..127e170b 100644 --- a/php/class-customize-snapshot-manager.php +++ b/php/class-customize-snapshot-manager.php @@ -92,8 +92,10 @@ 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( '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' ) ); @@ -518,15 +520,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 +561,26 @@ public function enqueue_scripts() { ); } + /** + * Enqueue Customizer frontend scripts. + */ + public function enqueue_frontend_scripts() { + if ( $this->snapshot ) { + $handle = 'customize-snapshots-frontend'; + wp_enqueue_script( $handle ); + + $exports = array( + 'uuid' => $this->snapshot->uuid(), + 'home_url' => wp_parse_url( home_url( '/' ) ), + ); + wp_add_inline_script( + $handle, + sprintf( 'CustomizeSnapshotsFrontend.init( %s )', wp_json_encode( $exports ) ), + 'after' + ); + } + } + /** * Include the snapshot nonce in the Customizer nonces. * diff --git a/php/class-plugin.php b/php/class-plugin.php index a2b543b7..b3b457fc 100644 --- a/php/class-plugin.php +++ b/php/class-plugin.php @@ -65,9 +65,16 @@ 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-frontend'; + $src = $this->dir_url . 'js/customize-snapshots-frontend' . $min . '.js'; + $deps = array( 'jquery', 'underscore' ); + $wp_scripts->add( $handle, $src, $deps ); } /** @@ -79,8 +86,10 @@ 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 ); } } diff --git a/tests/php/test-class-customize-snapshot-manager.php b/tests/php/test-class-customize-snapshot-manager.php index 4d52ffe4..ec3ea097 100644 --- a/tests/php/test-class-customize-snapshot-manager.php +++ b/tests/php/test-class-customize-snapshot-manager.php @@ -158,7 +158,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' ) ) ); } @@ -344,14 +344,14 @@ function test_encode_json() { /** * Test enqueue scripts. * - * @see Customize_Snapshot_Manager::enqueue_scripts() + * @see Customize_Snapshot_Manager::enqueue_controls_scripts() */ function test_enqueue_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(); + $manager->enqueue_controls_scripts(); $this->assertTrue( wp_script_is( $this->plugin->slug, 'enqueued' ) ); $this->assertTrue( wp_style_is( $this->plugin->slug, 'enqueued' ) ); } From c1ce097bc65697523c8752acd5fcbdbeeacee4e1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 6 Aug 2016 17:23:04 -0700 Subject: [PATCH 02/31] Ignore customize_snapshot_uuid param if in the admin --- php/class-customize-snapshot-manager.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php index 127e170b..88ba631d 100644 --- a/php/class-customize-snapshot-manager.php +++ b/php/class-customize-snapshot-manager.php @@ -198,6 +198,11 @@ public function is_theme_active() { */ public function should_import_and_preview_snapshot( Customize_Snapshot $snapshot ) { + // Ignore if in the admin. + if ( is_admin() && ! ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) { + return false; + } + if ( is_wp_error( $this->get_theme_switch_error( $snapshot ) ) ) { return false; } From 8bf8be29fa87aa0379e548a3540413c1ff23dd13 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 6 Aug 2016 18:36:04 -0700 Subject: [PATCH 03/31] Add test for enqueue_frontend_scripts --- .../test-class-customize-snapshot-manager.php | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/tests/php/test-class-customize-snapshot-manager.php b/tests/php/test-class-customize-snapshot-manager.php index ec3ea097..eb2a9529 100644 --- a/tests/php/test-class-customize-snapshot-manager.php +++ b/tests/php/test-class-customize-snapshot-manager.php @@ -93,6 +93,15 @@ function setUp() { } } + /** + * Clean up global scope. + */ + function clean_up_global_scope() { + unset( $GLOBALS['wp_scripts'] ); + unset( $GLOBALS['wp_styles'] ); + parent::clean_up_global_scope(); + } + /** * Tear down. */ @@ -100,7 +109,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(); @@ -342,18 +350,40 @@ function test_encode_json() { } /** - * Test enqueue scripts. + * Test enqueue controls 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_controls_scripts(); - $this->assertTrue( wp_script_is( $this->plugin->slug, 'enqueued' ) ); - $this->assertTrue( wp_style_is( $this->plugin->slug, 'enqueued' ) ); + $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' ) ); } /** From 1c93cfb94a0ba1691267d67313c898afab917842 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 6 Aug 2016 23:34:23 -0700 Subject: [PATCH 04/31] Remember snapshot in session and prompt to restore --- js/customize-snapshots-frontend.js | 70 +++++++++++++++++++++--- php/class-customize-snapshot-manager.php | 7 ++- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/js/customize-snapshots-frontend.js b/js/customize-snapshots-frontend.js index 550d33ef..2d40de79 100644 --- a/js/customize-snapshots-frontend.js +++ b/js/customize-snapshots-frontend.js @@ -1,8 +1,9 @@ -/* global jQuery */ +/* global jQuery, confirm */ /* exported CustomizeSnapshotsFrontend */ /* eslint consistent-this: [ "error", "section" ], no-magic-numbers: [ "error", { "ignore": [0,1] } ] */ +/* eslint-disable no-alert */ -// @todo Use session storage to make sure the user stays in the snapshot frontend preview, unless explicitly exited. Warn with confirm if clicking. +// @todo Allow the session to be explicitly exited. Warn with confirm if clicking a non-snapshotted link (to the admin). // @todo Inject customize_snapshot_uuid into all Ajax requests back to the site. var CustomizeSnapshotsFrontend = ( function( $ ) { @@ -15,6 +16,9 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { scheme: '', host: '', path: '' + }, + l10n: { + restoreSessionPrompt: '' } } }; @@ -27,15 +31,65 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { * @returns {void} */ component.init = function init( args ) { - if ( ! args.uuid ) { - throw new Error( 'Missing UUID' ); + _.extend( component.data, args ); + + component.keepSessionAlive(); + component.rememberSessionSnapshot(); + component.injectSnapshotIntoLinks(); + }; + + /** + * Prompt to restore session. + * + * @returns {void} + */ + component.keepSessionAlive = function keepSessionAlive() { + var currentSnapshotUuid, urlParser; + if ( 'undefined' === typeof sessionStorage ) { + return; + } + currentSnapshotUuid = sessionStorage.getItem( 'customize_snapshot_uuid' ); + if ( ! currentSnapshotUuid || component.data.uuid ) { + return; } + if ( confirm( component.data.l10n.restoreSessionPrompt ) ) { + 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' ) ); + location.replace( urlParser.href ); + } else { + sessionStorage.removeItem( 'customize_snapshot_uuid' ); + } + }; - _.extend( component.data, args ); + /** + * 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 ( 'undefined' === typeof sessionStorage || ! component.data.uuid ) { + return; + } + sessionStorage.setItem( 'customize_snapshot_uuid', component.data.uuid ); + }; - // Inject snapshot UUID into links on click. + /** + * Inject the snapshot UUID into links in the document. + * + * @returns {void} + */ + component.injectSnapshotIntoLinks = function injectSnapshotIntoLinks() { + if ( ! component.data.uuid ) { + return; + } $( document.documentElement ).on( 'click focus mouseover', 'a, area', function() { - component.injectQueryParam( this ); + component.injectLinkQueryParam( this ); } ); }; @@ -46,7 +100,7 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { * @param {object} element.search Query string. * @returns {void} */ - component.injectQueryParam = function injectQueryParam( element ) { + component.injectLinkQueryParam = function injectLinkQueryParam( element ) { if ( element.hostname !== component.data.home_url.host ) { return; } diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php index 88ba631d..f30976da 100644 --- a/php/class-customize-snapshot-manager.php +++ b/php/class-customize-snapshot-manager.php @@ -570,13 +570,16 @@ public function enqueue_controls_scripts() { * Enqueue Customizer frontend scripts. */ public function enqueue_frontend_scripts() { - if ( $this->snapshot ) { + if ( $this->snapshot || current_user_can( 'customize' ) ) { $handle = 'customize-snapshots-frontend'; wp_enqueue_script( $handle ); $exports = array( - 'uuid' => $this->snapshot->uuid(), + '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, From 0d74731f5d0ac8e099ac431fa5b4d2ef5c54dec5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 7 Aug 2016 00:09:15 -0700 Subject: [PATCH 05/31] Confirm when leaving snapshot state --- js/customize-snapshots-frontend.js | 69 +++++++++++++++++++++--- php/class-customize-snapshot-manager.php | 1 + 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/js/customize-snapshots-frontend.js b/js/customize-snapshots-frontend.js index 2d40de79..5fd9524a 100644 --- a/js/customize-snapshots-frontend.js +++ b/js/customize-snapshots-frontend.js @@ -18,7 +18,8 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { path: '' }, l10n: { - restoreSessionPrompt: '' + restoreSessionPrompt: '', + leaveSessionPrompt: '' } } }; @@ -33,6 +34,8 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { component.init = function init( args ) { _.extend( component.data, args ); + component.hasSessionStorage = 'undefined' !== typeof sessionStorage; + component.keepSessionAlive(); component.rememberSessionSnapshot(); component.injectSnapshotIntoLinks(); @@ -45,7 +48,7 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { */ component.keepSessionAlive = function keepSessionAlive() { var currentSnapshotUuid, urlParser; - if ( 'undefined' === typeof sessionStorage ) { + if ( ! component.hasSessionStorage ) { return; } currentSnapshotUuid = sessionStorage.getItem( 'customize_snapshot_uuid' ); @@ -73,7 +76,7 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { * @returns {void} */ component.rememberSessionSnapshot = function rememberSessionSnapshot() { - if ( 'undefined' === typeof sessionStorage || ! component.data.uuid ) { + if ( ! component.hasSessionStorage || ! component.data.uuid ) { return; } sessionStorage.setItem( 'customize_snapshot_uuid', component.data.uuid ); @@ -88,11 +91,64 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { if ( ! component.data.uuid ) { return; } - $( document.documentElement ).on( 'click focus mouseover', 'a, area', function() { + $( document.documentElement ).on( 'click focus mouseover', 'a, area', function( event ) { component.injectLinkQueryParam( this ); + + if ( 'click' === event.type && ! component.isLinkSnapshottable( this ) ) { + if ( confirm( component.data.l10n.leaveSessionPrompt ) ) { + if ( component.hasSessionStorage ) { + sessionStorage.removeItem( 'customize_snapshot_uuid' ); + } + } else { + event.preventDefault(); + } + } } ); }; + /** + * 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.isLinkSnapshottable = function isLinkSnapshottable( element ) { + if ( element.hostname !== component.data.home_url.host ) { + return false; + } + if ( 0 !== element.pathname.indexOf( component.data.home_url.path ) ) { + return false; + } + + if ( /\/wp-(login|signup)\.php$/.test( element.pathname ) ) { + return false; + } + + // @todo The snapshot UUID is getting added here still. + if ( $( element ).parent().is( '#wp-admin-bar-snapshot-view-link' ) ) { + return true; + } + + if ( /(^|&)customize_snapshot_uuid=/.test( element.search.substr( 1 ) ) ) { + return true; + } + + // 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; + } + + return true; + }; + /** * Inject the customize_snapshot_uuid query param into links on the frontend. * @@ -101,10 +157,7 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { * @returns {void} */ component.injectLinkQueryParam = function injectLinkQueryParam( element ) { - if ( element.hostname !== component.data.home_url.host ) { - return; - } - if ( 0 !== element.pathname.indexOf( component.data.home_url.path ) ) { + if ( ! component.isLinkSnapshottable( element ) ) { return; } if ( /(^|&)customize_snapshot_uuid=/.test( element.search.substr( 1 ) ) ) { diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php index f30976da..37adb677 100644 --- a/php/class-customize-snapshot-manager.php +++ b/php/class-customize-snapshot-manager.php @@ -579,6 +579,7 @@ public function enqueue_frontend_scripts() { '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' ), + 'leaveSessionPrompt' => __( 'You\'re about to leave previewing our snapshotted customized state. Would you like to continue?', 'customize-snapshots' ), ), ); wp_add_inline_script( From ae9593d40b90d24865b9facb2de4596a26491083 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 7 Aug 2016 16:01:55 -0700 Subject: [PATCH 06/31] Improve logic for replacing customize admin bar link; move inspect item to top --- php/class-customize-snapshot-manager.php | 83 ++++++++++--------- .../test-class-customize-snapshot-manager.php | 78 ++++++++++++----- 2 files changed, 101 insertions(+), 60 deletions(-) diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php index 37adb677..2ed4f230 100644 --- a/php/class-customize-snapshot-manager.php +++ b/php/class-customize-snapshot-manager.php @@ -115,6 +115,7 @@ function init() { if ( $this->current_snapshot_uuid ) { $this->ensure_customize_manager(); + add_action( 'wp_head', array( $this, 'print_admin_bar_styles' ) ); add_action( 'wp_ajax_' . self::AJAX_ACTION, array( $this, 'handle_update_snapshot_request' ) ); $this->snapshot = new Customize_Snapshot( $this, $this->current_snapshot_uuid ); @@ -410,17 +411,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. * @@ -443,15 +433,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. */ @@ -1028,9 +1009,23 @@ 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_post_edit_screen_link( $wp_admin_bar ); - add_action( 'wp_before_admin_bar_render', 'wp_customize_support_script' ); + } + + /** + * Print admin bar styles. + */ + public function print_admin_bar_styles() { + ?> + + add_node( array( - 'parent' => 'customize', - 'id' => 'snapshot-view-link', + $wp_admin_bar->add_menu( array( + 'id' => 'inspect-snapshot', 'title' => __( 'Inspect Snapshot', 'customize-snapshots' ), 'href' => get_edit_post_link( $post->ID, 'raw' ), + 'meta' => array( + 'class' => 'ab-item ab-snapshot-item', + ), ) ); } @@ -1061,28 +1058,36 @@ public function add_post_edit_screen_link( $wp_admin_bar ) { */ 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() ) { + if ( empty( $this->current_snapshot_uuid ) ) { return; } - $args = array(); - if ( $this->current_snapshot_uuid ) { - $args['customize_snapshot_uuid'] = $this->current_snapshot_uuid; + $customize_node = $wp_admin_bar->get_node( 'customize' ); + if ( empty( $customize_node ) ) { + return; } - $args['url'] = esc_url_raw( $this->remove_snapshot_uuid_from_current_url() ); - $customize_url = add_query_arg( array_map( 'rawurlencode', $args ), wp_customize_url() ); + // 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 + ); + } - $wp_admin_bar->add_menu( - array( - 'id' => 'customize', - 'title' => __( 'Customize', 'customize-snapshots' ), - 'href' => $customize_url, - 'meta' => array( - 'class' => 'hide-if-no-customize', - ), - ) + // 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-snapshot-item'; + $wp_admin_bar->add_menu( (array) $customize_node ); } /** diff --git a/tests/php/test-class-customize-snapshot-manager.php b/tests/php/test-class-customize-snapshot-manager.php index eb2a9529..8c4366aa 100644 --- a/tests/php/test-class-customize-snapshot-manager.php +++ b/tests/php/test-class-customize-snapshot-manager.php @@ -99,6 +99,7 @@ function setUp() { function clean_up_global_scope() { unset( $GLOBALS['wp_scripts'] ); unset( $GLOBALS['wp_styles'] ); + unset( $_REQUEST['customize_snapshot_uuid'] ); parent::clean_up_global_scope(); } @@ -293,22 +294,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. * @@ -560,21 +545,28 @@ 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( '/' ); + $customize_url = admin_url( 'customize.php' ) . '?customize_snapshot_uuid=' . self::UUID . '&url=' . urlencode( $preview_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'] ); } /** @@ -584,7 +576,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' ) ) ); @@ -594,6 +586,19 @@ public function test_customize_menu_return() { $this->assertNull( $wp_admin_bar->get_node( 'customize' ) ); } + /** + * Tests print_admin_bar_styles. + * + * @covers Customize_Snapshot_Manager::print_admin_bar_styles() + */ + public function test_print_admin_bar_styles() { + $manager = new Customize_Snapshot_Manager( $this->plugin ); + ob_start(); + $manager->print_admin_bar_styles(); + $contents = ob_get_clean(); + $this->assertContains( 'markTestIncomplete(); + global $wp_admin_bar; + + require_once ABSPATH . WPINC . '/class-wp-admin-bar.php'; + remove_all_actions( 'admin_bar_menu' ); + $this->go_to( home_url( '?customize_snapshot_uuid=' . self::UUID ) ); + $_REQUEST['customize_snapshot_uuid'] = self::UUID; + + $manager = new Customize_Snapshot_Manager( $this->plugin ); + $manager->init(); + + wp_set_current_user( 0 ); + $wp_admin_bar = new \WP_Admin_Bar(); // WPCS: Override OK. + $wp_admin_bar->initialize(); + $wp_admin_bar->add_menus(); + do_action_ref_array( 'admin_bar_menu', array( &$wp_admin_bar ) ); + $this->assertEmpty( $wp_admin_bar->get_node( 'customize' ) ); + + wp_set_current_user( $this->user_id ); + $wp_admin_bar = new \WP_Admin_Bar(); // WPCS: Override OK. + $wp_admin_bar->initialize(); + $wp_admin_bar->add_menus(); + do_action_ref_array( 'admin_bar_menu', array( &$wp_admin_bar ) ); + $node = $wp_admin_bar->get_node( 'customize' ); + $this->assertTrue( is_object( $node ) ); + $parsed_url = wp_parse_url( $node->href ); + $query_params = array(); + parse_str( $parsed_url['query'], $query_params ); + $this->assertArrayHasKey( 'customize_snapshot_uuid', $query_params ); + $this->assertEquals( self::UUID, $query_params['customize_snapshot_uuid'] ); + $this->assertArrayHasKey( 'url', $query_params ); + $parsed_preview_url = wp_parse_url( $query_params['url'] ); + $this->assertArrayNotHasKey( 'query', $parsed_preview_url ); } /** From 1100a2e4bc7f957e86e88e283fae5a4206294077 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 7 Aug 2016 17:07:24 -0700 Subject: [PATCH 07/31] Add link to exit snapshot frontend preview; remove all other external admin bar links --- js/customize-snapshots-frontend.js | 57 ++++++++--- php/class-customize-snapshot-manager.php | 98 ++++++++++++++----- .../test-class-customize-snapshot-manager.php | 79 +++++++++++++-- 3 files changed, 181 insertions(+), 53 deletions(-) diff --git a/js/customize-snapshots-frontend.js b/js/customize-snapshots-frontend.js index 5fd9524a..3ea50fe2 100644 --- a/js/customize-snapshots-frontend.js +++ b/js/customize-snapshots-frontend.js @@ -3,7 +3,6 @@ /* eslint consistent-this: [ "error", "section" ], no-magic-numbers: [ "error", { "ignore": [0,1] } ] */ /* eslint-disable no-alert */ -// @todo Allow the session to be explicitly exited. Warn with confirm if clicking a non-snapshotted link (to the admin). // @todo Inject customize_snapshot_uuid into all Ajax requests back to the site. var CustomizeSnapshotsFrontend = ( function( $ ) { @@ -39,6 +38,7 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { component.keepSessionAlive(); component.rememberSessionSnapshot(); component.injectSnapshotIntoLinks(); + component.handleExitSnapshotSessionLink(); }; /** @@ -94,7 +94,7 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { $( document.documentElement ).on( 'click focus mouseover', 'a, area', function( event ) { component.injectLinkQueryParam( this ); - if ( 'click' === event.type && ! component.isLinkSnapshottable( this ) ) { + if ( 'click' === event.type && ! component.doesLinkHaveSnapshotQueryParam( this ) && ! $( this ).parent().hasClass( 'ab-customize-snapshots-item' ) ) { if ( confirm( component.data.l10n.leaveSessionPrompt ) ) { if ( component.hasSessionStorage ) { sessionStorage.removeItem( 'customize_snapshot_uuid' ); @@ -115,27 +115,23 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { * @param {string} element.hostname Hostname. * @returns {boolean} Is appropriate for snapshot link. */ - component.isLinkSnapshottable = function isLinkSnapshottable( element ) { + component.shouldLinkHaveSnapshotParam = function shouldLinkHaveSnapshotParam( element ) { + + // Skip links to different hosts. if ( element.hostname !== component.data.home_url.host ) { return false; } + + // Skip links that aren't under the home path. if ( 0 !== element.pathname.indexOf( component.data.home_url.path ) ) { return false; } + // Skip wp login and signup pages. if ( /\/wp-(login|signup)\.php$/.test( element.pathname ) ) { return false; } - // @todo The snapshot UUID is getting added here still. - if ( $( element ).parent().is( '#wp-admin-bar-snapshot-view-link' ) ) { - return true; - } - - if ( /(^|&)customize_snapshot_uuid=/.test( element.search.substr( 1 ) ) ) { - return true; - } - // Allow links to admin ajax as faux frontend URLs. if ( /\/wp-admin\/admin-ajax\.php$/.test( element.pathname ) ) { return true; @@ -146,9 +142,25 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { 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. * @@ -157,10 +169,7 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { * @returns {void} */ component.injectLinkQueryParam = function injectLinkQueryParam( element ) { - if ( ! component.isLinkSnapshottable( element ) ) { - return; - } - if ( /(^|&)customize_snapshot_uuid=/.test( element.search.substr( 1 ) ) ) { + if ( component.doesLinkHaveSnapshotQueryParam( element ) || ! component.shouldLinkHaveSnapshotParam( element ) ) { return; } @@ -170,6 +179,22 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { 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' ); + } ); + } ); + }; + return component; } )( jQuery ); diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php index 2ed4f230..c0762a64 100644 --- a/php/class-customize-snapshot-manager.php +++ b/php/class-customize-snapshot-manager.php @@ -100,6 +100,7 @@ function init() { 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' ), 1000 ); 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' ) ); @@ -1012,53 +1013,37 @@ 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_post_edit_screen_link( $wp_admin_bar ); + $this->add_snapshot_exit_link( $wp_admin_bar ); } /** * Print admin bar styles. */ public function print_admin_bar_styles() { + if ( ! $this->snapshot ) { + return; + } ?> snapshot ) { - return; - } - $post = $this->snapshot->post(); - if ( ! $post ) { - return; - } - $wp_admin_bar->add_menu( array( - 'id' => 'inspect-snapshot', - 'title' => __( 'Inspect Snapshot', 'customize-snapshots' ), - 'href' => get_edit_post_link( $post->ID, 'raw' ), - 'meta' => array( - 'class' => 'ab-item ab-snapshot-item', - ), - ) ); - } - /** * Replaces the "Customize" link in the Toolbar. * * @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 ( empty( $this->current_snapshot_uuid ) ) { + if ( empty( $this->snapshot ) ) { return; } @@ -1086,10 +1071,69 @@ public function replace_customize_link( $wp_admin_bar ) { $customize_node->href ); - $customize_node->meta['class'] .= ' ab-snapshot-item'; + $customize_node->meta['class'] .= ' ab-customize-snapshots-item'; $wp_admin_bar->add_menu( (array) $customize_node ); } + /** + * Adds a "Snapshot in Dashboard" link to the Toolbar when in Snapshot mode. + * + * @param \WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance. + */ + public function add_post_edit_screen_link( $wp_admin_bar ) { + if ( ! $this->snapshot ) { + return; + } + $post = $this->snapshot->post(); + if ( ! $post ) { + return; + } + $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', + ), + ) ); + } + + /** + * 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 add_snapshot_exit_link( $wp_admin_bar ) { + if ( ! $this->snapshot ) { + return; + } + $wp_admin_bar->add_menu( array( + 'id' => 'exit-customize-snapshot', + 'title' => __( 'Exit Snapshot', 'customize-snapshots' ), + 'href' => remove_query_arg( 'customize_snapshot_uuid' ), + 'meta' => array( + 'class' => 'ab-item ab-customize-snapshots-item', + ), + ) ); + } + + /** + * 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 ) ) { + $wp_admin_bar->remove_node( $node->id ); + } + } + } + /** * Underscore (JS) templates for dialog windows. */ diff --git a/tests/php/test-class-customize-snapshot-manager.php b/tests/php/test-class-customize-snapshot-manager.php index 8c4366aa..4cfc2f6a 100644 --- a/tests/php/test-class-customize-snapshot-manager.php +++ b/tests/php/test-class-customize-snapshot-manager.php @@ -546,7 +546,6 @@ public function test_is_valid_uuid() { public function test_customize_menu() { set_current_screen( 'front' ); $preview_url = home_url( '/' ); - $customize_url = admin_url( 'customize.php' ) . '?customize_snapshot_uuid=' . self::UUID . '&url=' . urlencode( $preview_url ); $_REQUEST['customize_snapshot_uuid'] = self::UUID; $manager = new Customize_Snapshot_Manager( $this->plugin ); @@ -593,19 +592,29 @@ public function test_customize_menu_return() { */ 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( 'assertEmpty( $contents ); - /** - * Test add_post_edit_screen_link. - * - * @covers Customize_Snapshot_Manager::add_post_edit_screen_link() - */ - public function test_add_post_edit_screen_link() { - $this->markTestIncomplete(); + $this->manager->post_type->save( array( + 'uuid' => self::UUID, + 'data' => array( + 'blogname' => array( + 'value' => 'Hello', + ), + ), + 'status' => 'draft', + ) ); + $this->go_to( home_url( '?customize_snapshot_uuid=' . self::UUID ) ); + $_REQUEST['customize_snapshot_uuid'] = self::UUID; + $manager = new Customize_Snapshot_Manager( $this->plugin ); + $manager->init(); + ob_start(); + $manager->print_admin_bar_styles(); + $contents = ob_get_clean(); + $this->assertContains( 'plugin ); $manager->init(); + // Ensure customize link remains unknown if user lacks cap. wp_set_current_user( 0 ); $wp_admin_bar = new \WP_Admin_Bar(); // WPCS: Override OK. $wp_admin_bar->initialize(); @@ -631,6 +642,7 @@ public function test_replace_customize_link() { do_action_ref_array( 'admin_bar_menu', array( &$wp_admin_bar ) ); $this->assertEmpty( $wp_admin_bar->get_node( 'customize' ) ); + // Ensure customize link modified. wp_set_current_user( $this->user_id ); $wp_admin_bar = new \WP_Admin_Bar(); // WPCS: Override OK. $wp_admin_bar->initialize(); @@ -648,6 +660,53 @@ public function test_replace_customize_link() { $this->assertArrayNotHasKey( 'query', $parsed_preview_url ); } + /** + * Test add_post_edit_screen_link, add_snapshot_exit_link, and remove_all_non_snapshot_admin_bar_links. + * + * @covers Customize_Snapshot_Manager::add_post_edit_screen_link() + * @covers Customize_Snapshot_Manager::add_snapshot_exit_link() + * @covers Customize_Snapshot_Manager::remove_all_non_snapshot_admin_bar_links() + */ + public function test_add_post_edit_and_exit_links() { + global $wp_admin_bar; + set_current_screen( 'front' ); + require_once ABSPATH . WPINC . '/class-wp-admin-bar.php'; + + $this->manager->post_type->save( array( + 'uuid' => self::UUID, + 'data' => array( + 'blogname' => array( + 'value' => 'Hello', + ), + ), + 'status' => 'draft', + ) ); + + remove_all_actions( 'admin_bar_menu' ); + $manager = new Customize_Snapshot_Manager( $this->plugin ); + $manager->init(); + $wp_admin_bar = new \WP_Admin_Bar(); // WPCS: Override OK. + $wp_admin_bar->initialize(); + $wp_admin_bar->add_menus(); + do_action_ref_array( 'admin_bar_menu', array( &$wp_admin_bar ) ); + $this->assertEmpty( $wp_admin_bar->get_node( 'inspect-customize-snapshot' ) ); + $this->assertEmpty( $wp_admin_bar->get_node( 'exit-customize-snapshot' ) ); + $this->assertNotEmpty( $wp_admin_bar->get_node( 'wporg' ) ); + + $this->go_to( home_url( '?customize_snapshot_uuid=' . self::UUID ) ); + $_REQUEST['customize_snapshot_uuid'] = self::UUID; + remove_all_actions( 'admin_bar_menu' ); + $manager = new Customize_Snapshot_Manager( $this->plugin ); + $manager->init(); + $wp_admin_bar = new \WP_Admin_Bar(); // WPCS: Override OK. + $wp_admin_bar->initialize(); + $wp_admin_bar->add_menus(); + do_action_ref_array( 'admin_bar_menu', array( &$wp_admin_bar ) ); + $this->assertNotEmpty( $wp_admin_bar->get_node( 'inspect-customize-snapshot' ) ); + $this->assertNotEmpty( $wp_admin_bar->get_node( 'exit-customize-snapshot' ) ); + $this->assertEmpty( $wp_admin_bar->get_node( 'wporg' ) ); + } + /** * Test render templates. * From 5742fb5eed628b0271d3429afe2c107dda6fa1a6 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 7 Aug 2016 17:21:29 -0700 Subject: [PATCH 08/31] Bump priority on removal of admin bar links; allow links with snapshots --- php/class-customize-snapshot-manager.php | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php index c0762a64..7fe4b18a 100644 --- a/php/class-customize-snapshot-manager.php +++ b/php/class-customize-snapshot-manager.php @@ -100,7 +100,8 @@ function init() { 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' ), 1000 ); + add_action( 'admin_bar_menu', array( $this, 'remove_all_non_snapshot_admin_bar_links' ), 100000 ); + add_action( 'wp_print_styles', 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' ) ); @@ -116,7 +117,6 @@ function init() { if ( $this->current_snapshot_uuid ) { $this->ensure_customize_manager(); - add_action( 'wp_head', array( $this, 'print_admin_bar_styles' ) ); add_action( 'wp_ajax_' . self::AJAX_ACTION, array( $this, 'handle_update_snapshot_request' ) ); $this->snapshot = new Customize_Snapshot( $this, $this->current_snapshot_uuid ); @@ -1128,7 +1128,20 @@ public function remove_all_non_snapshot_admin_bar_links( $wp_admin_bar ) { } $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 ) ) { + if ( in_array( $node->id, $snapshot_admin_bar_node_ids, true ) || '#' === substr( $node->href, 0, 1 ) ) { + continue; + } + + $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 ); } } From b3707da7d4c54da72bf341d84c35106f4566930e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 7 Aug 2016 17:50:13 -0700 Subject: [PATCH 09/31] Remove confirm to resume snapshot in favor of admin bar link if available --- js/customize-snapshots-frontend.js | 30 ++++++++----- php/class-customize-snapshot-manager.php | 45 ++++++++++++++----- .../test-class-customize-snapshot-manager.php | 23 ++-------- 3 files changed, 56 insertions(+), 42 deletions(-) diff --git a/js/customize-snapshots-frontend.js b/js/customize-snapshots-frontend.js index 3ea50fe2..37a62bfc 100644 --- a/js/customize-snapshots-frontend.js +++ b/js/customize-snapshots-frontend.js @@ -47,7 +47,7 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { * @returns {void} */ component.keepSessionAlive = function keepSessionAlive() { - var currentSnapshotUuid, urlParser; + var currentSnapshotUuid, urlParser, adminBarItem; if ( ! component.hasSessionStorage ) { return; } @@ -55,17 +55,25 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { if ( ! currentSnapshotUuid || component.data.uuid ) { return; } - if ( confirm( component.data.l10n.restoreSessionPrompt ) ) { - 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' ) ); - location.replace( urlParser.href ); - } else { - sessionStorage.removeItem( 'customize_snapshot_uuid' ); + + 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' ); + } + } ); }; /** diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php index 7fe4b18a..931a7dbb 100644 --- a/php/class-customize-snapshot-manager.php +++ b/php/class-customize-snapshot-manager.php @@ -101,7 +101,7 @@ function init() { 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_print_styles', array( $this, 'print_admin_bar_styles' ) ); + 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' ) ); @@ -1012,6 +1012,7 @@ static public function is_valid_uuid( $uuid ) { 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 ); $this->add_snapshot_exit_link( $wp_admin_bar ); } @@ -1020,19 +1021,23 @@ public function customize_menu( $wp_admin_bar ) { * Print admin bar styles. */ public function print_admin_bar_styles() { - if ( ! $this->snapshot ) { - return; - } ?> 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', + ), + ) ); + } + /** * Adds a "Snapshot in Dashboard" link to the Toolbar when in Snapshot mode. * diff --git a/tests/php/test-class-customize-snapshot-manager.php b/tests/php/test-class-customize-snapshot-manager.php index 4cfc2f6a..f2968616 100644 --- a/tests/php/test-class-customize-snapshot-manager.php +++ b/tests/php/test-class-customize-snapshot-manager.php @@ -591,24 +591,6 @@ public function test_customize_menu_return() { * @covers Customize_Snapshot_Manager::print_admin_bar_styles() */ 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->assertEmpty( $contents ); - - $this->manager->post_type->save( array( - 'uuid' => self::UUID, - 'data' => array( - 'blogname' => array( - 'value' => 'Hello', - ), - ), - 'status' => 'draft', - ) ); - $this->go_to( home_url( '?customize_snapshot_uuid=' . self::UUID ) ); - $_REQUEST['customize_snapshot_uuid'] = self::UUID; $manager = new Customize_Snapshot_Manager( $this->plugin ); $manager->init(); ob_start(); @@ -661,10 +643,11 @@ public function test_replace_customize_link() { } /** - * Test add_post_edit_screen_link, add_snapshot_exit_link, and remove_all_non_snapshot_admin_bar_links. + * Test misc admin bar extensions. * * @covers Customize_Snapshot_Manager::add_post_edit_screen_link() * @covers Customize_Snapshot_Manager::add_snapshot_exit_link() + * @covers Customize_Snapshot_Manager::add_resume_snapshot_link() * @covers Customize_Snapshot_Manager::remove_all_non_snapshot_admin_bar_links() */ public function test_add_post_edit_and_exit_links() { @@ -692,6 +675,7 @@ public function test_add_post_edit_and_exit_links() { $this->assertEmpty( $wp_admin_bar->get_node( 'inspect-customize-snapshot' ) ); $this->assertEmpty( $wp_admin_bar->get_node( 'exit-customize-snapshot' ) ); $this->assertNotEmpty( $wp_admin_bar->get_node( 'wporg' ) ); + $this->assertNotEmpty( $wp_admin_bar->get_node( 'resume-customize-snapshot' ) ); $this->go_to( home_url( '?customize_snapshot_uuid=' . self::UUID ) ); $_REQUEST['customize_snapshot_uuid'] = self::UUID; @@ -705,6 +689,7 @@ public function test_add_post_edit_and_exit_links() { $this->assertNotEmpty( $wp_admin_bar->get_node( 'inspect-customize-snapshot' ) ); $this->assertNotEmpty( $wp_admin_bar->get_node( 'exit-customize-snapshot' ) ); $this->assertEmpty( $wp_admin_bar->get_node( 'wporg' ) ); + $this->assertNotEmpty( $wp_admin_bar->get_node( 'resume-customize-snapshot' ) ); } /** From e08b8996cca7ae543c08ce7baf5da4f90f0f3b5c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 7 Aug 2016 17:52:41 -0700 Subject: [PATCH 10/31] Remove nag when clicking on links without snapshot param --- js/customize-snapshots-frontend.js | 10 ---------- php/class-customize-snapshot-manager.php | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/js/customize-snapshots-frontend.js b/js/customize-snapshots-frontend.js index 37a62bfc..f3f357e2 100644 --- a/js/customize-snapshots-frontend.js +++ b/js/customize-snapshots-frontend.js @@ -101,16 +101,6 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { } $( document.documentElement ).on( 'click focus mouseover', 'a, area', function( event ) { component.injectLinkQueryParam( this ); - - if ( 'click' === event.type && ! component.doesLinkHaveSnapshotQueryParam( this ) && ! $( this ).parent().hasClass( 'ab-customize-snapshots-item' ) ) { - if ( confirm( component.data.l10n.leaveSessionPrompt ) ) { - if ( component.hasSessionStorage ) { - sessionStorage.removeItem( 'customize_snapshot_uuid' ); - } - } else { - event.preventDefault(); - } - } } ); }; diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php index 931a7dbb..c82baf7c 100644 --- a/php/class-customize-snapshot-manager.php +++ b/php/class-customize-snapshot-manager.php @@ -1130,7 +1130,7 @@ public function add_snapshot_exit_link( $wp_admin_bar ) { } $wp_admin_bar->add_menu( array( 'id' => 'exit-customize-snapshot', - 'title' => __( 'Exit Snapshot', 'customize-snapshots' ), + 'title' => __( 'Exit Snapshot Preview', 'customize-snapshots' ), 'href' => remove_query_arg( 'customize_snapshot_uuid' ), 'meta' => array( 'class' => 'ab-item ab-customize-snapshots-item', From dd8e944fc31b652903b811d0c9e2869749875c98 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 7 Aug 2016 18:12:38 -0700 Subject: [PATCH 11/31] Use mutation observers to inject query params into links if available --- js/customize-snapshots-frontend.js | 31 ++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/js/customize-snapshots-frontend.js b/js/customize-snapshots-frontend.js index f3f357e2..706ca374 100644 --- a/js/customize-snapshots-frontend.js +++ b/js/customize-snapshots-frontend.js @@ -96,11 +96,38 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { * @returns {void} */ component.injectSnapshotIntoLinks = function injectSnapshotIntoLinks() { + var linkSelectors = 'a, area'; + if ( ! component.data.uuid ) { return; } - $( document.documentElement ).on( 'click focus mouseover', 'a, area', function( event ) { - component.injectLinkQueryParam( this ); + $( function() { + + // Inject links into initial document. + $( document.body ).find( linkSelectors ).each( function() { + component.injectLinkQueryParam( 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.injectLinkQueryParam( 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.injectLinkQueryParam( this ); + } ); + } } ); }; From 74718b4f0ac25f05fea3a02c68339ebc52431ffe Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 7 Aug 2016 21:54:27 -0700 Subject: [PATCH 12/31] Inject customize_snapshot_uuid into frontend requests --- js/customize-snapshots-frontend.js | 70 ++++++++++++++++++++---- php/class-customize-snapshot-manager.php | 2 +- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/js/customize-snapshots-frontend.js b/js/customize-snapshots-frontend.js index 706ca374..a36c1d58 100644 --- a/js/customize-snapshots-frontend.js +++ b/js/customize-snapshots-frontend.js @@ -1,10 +1,8 @@ /* global jQuery, confirm */ /* exported CustomizeSnapshotsFrontend */ -/* eslint consistent-this: [ "error", "section" ], no-magic-numbers: [ "error", { "ignore": [0,1] } ] */ +/* eslint consistent-this: [ "error", "section" ], no-magic-numbers: [ "error", { "ignore": [-1,0,1] } ] */ /* eslint-disable no-alert */ -// @todo Inject customize_snapshot_uuid into all Ajax requests back to the site. - var CustomizeSnapshotsFrontend = ( function( $ ) { 'use strict'; @@ -39,6 +37,7 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { component.rememberSessionSnapshot(); component.injectSnapshotIntoLinks(); component.handleExitSnapshotSessionLink(); + component.injectSnapshotIntoAjaxRequests(); }; /** @@ -131,6 +130,18 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { } ); }; + /** + * 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)? * @@ -141,14 +152,7 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { * @returns {boolean} Is appropriate for snapshot link. */ component.shouldLinkHaveSnapshotParam = function shouldLinkHaveSnapshotParam( element ) { - - // Skip links to different hosts. - if ( element.hostname !== component.data.home_url.host ) { - return false; - } - - // Skip links that aren't under the home path. - if ( 0 !== element.pathname.indexOf( component.data.home_url.path ) ) { + if ( ! component.isMatchingBaseUrl( element ) ) { return false; } @@ -220,6 +224,50 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { } ); }; + /** + * 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; + }; + return component; } )( jQuery ); diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php index c82baf7c..688fe1f3 100644 --- a/php/class-customize-snapshot-manager.php +++ b/php/class-customize-snapshot-manager.php @@ -552,7 +552,7 @@ public function enqueue_controls_scripts() { * Enqueue Customizer frontend scripts. */ public function enqueue_frontend_scripts() { - if ( $this->snapshot || current_user_can( 'customize' ) ) { + if ( $this->snapshot ) { $handle = 'customize-snapshots-frontend'; wp_enqueue_script( $handle ); From c6b5bddedae1fd98887b1956a77764f854df3099 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 7 Aug 2016 21:58:05 -0700 Subject: [PATCH 13/31] Restore snapshot importing on customize.php --- php/class-customize-snapshot-manager.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php index 688fe1f3..7ba0d7e1 100644 --- a/php/class-customize-snapshot-manager.php +++ b/php/class-customize-snapshot-manager.php @@ -199,9 +199,10 @@ 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. - if ( is_admin() && ! ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) { + if ( is_admin() && ! ( defined( 'DOING_AJAX' ) && DOING_AJAX ) && 'customize.php' !== $pagenow ) { return false; } From 32c9c71638aebdc203cd10b9328b0fd2dac57a96 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 7 Aug 2016 22:04:06 -0700 Subject: [PATCH 14/31] Remove obsolete leaveSessionPrompt --- js/customize-snapshots-frontend.js | 3 +-- php/class-customize-snapshot-manager.php | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/js/customize-snapshots-frontend.js b/js/customize-snapshots-frontend.js index a36c1d58..cff69aa6 100644 --- a/js/customize-snapshots-frontend.js +++ b/js/customize-snapshots-frontend.js @@ -15,8 +15,7 @@ var CustomizeSnapshotsFrontend = ( function( $ ) { path: '' }, l10n: { - restoreSessionPrompt: '', - leaveSessionPrompt: '' + restoreSessionPrompt: '' } } }; diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php index 7ba0d7e1..b89f9ee2 100644 --- a/php/class-customize-snapshot-manager.php +++ b/php/class-customize-snapshot-manager.php @@ -562,7 +562,6 @@ public function enqueue_frontend_scripts() { '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' ), - 'leaveSessionPrompt' => __( 'You\'re about to leave previewing our snapshotted customized state. Would you like to continue?', 'customize-snapshots' ), ), ); wp_add_inline_script( From d57ecac087b724d8ef8bc2258a2ae30c6a983484 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 7 Aug 2016 22:50:27 -0700 Subject: [PATCH 15/31] Add stub for customize-snapshots-preview --- js/customize-snapshots-preview.js | 1 + php/class-customize-snapshot-manager.php | 15 +++++++++++++++ php/class-plugin.php | 5 +++++ 3 files changed, 21 insertions(+) create mode 100644 js/customize-snapshots-preview.js diff --git a/js/customize-snapshots-preview.js b/js/customize-snapshots-preview.js new file mode 100644 index 00000000..e0d1f690 --- /dev/null +++ b/js/customize-snapshots-preview.js @@ -0,0 +1 @@ +// @todo Inject customized state into all Ajax requests back to the site? All can be taken from Customize REST Resources. diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php index b89f9ee2..c037f367 100644 --- a/php/class-customize-snapshot-manager.php +++ b/php/class-customize-snapshot-manager.php @@ -93,6 +93,7 @@ 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' ) ); @@ -549,6 +550,20 @@ public function enqueue_controls_scripts() { ); } + /** + * Set up Customizer preview. + */ + public function customize_preview_init() { + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_preview_scripts' ) ); + } + + /** + * Enqueue Customizer preview scripts. + */ + public function enqueue_preview_scripts() { + wp_enqueue_script( 'customize-snapshots-preview' ); + } + /** * Enqueue Customizer frontend scripts. */ diff --git a/php/class-plugin.php b/php/class-plugin.php index b3b457fc..817fe094 100644 --- a/php/class-plugin.php +++ b/php/class-plugin.php @@ -71,6 +71,11 @@ public function register_scripts( \WP_Scripts $wp_scripts ) { $deps = array( 'jquery', 'jquery-ui-dialog', 'wp-util', 'customize-controls' ); $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' ); From 97f366327911191a91c58ff056797603f3af3dd3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 8 Aug 2016 00:40:53 -0700 Subject: [PATCH 16/31] Introduce Customize_Snapshot_Manager::is_previewing_settings() * Add is_previewing_settings() and current_snapshot_uuid() helper functions. * Add example for how to bypass cache appropriately. --- customize-snapshots.php | 2 +- instance.php | 28 +++++++++++++ php/class-customize-snapshot-manager.php | 39 +++++++++++++++-- readme.md | 6 +-- readme.txt | 42 ++++++++++++++++--- .../test-class-customize-snapshot-manager.php | 29 +++++++++++++ tests/test-customize-snapshots.php | 39 +++++++++++++++++ 7 files changed, 171 insertions(+), 14 deletions(-) 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( '

%s

', customize_snapshots_php_version_text() ); + printf( '

%s

', 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/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php index c037f367..5a1fd003 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. * @@ -273,6 +280,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. * @@ -280,11 +314,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 diff --git a/readme.md b/readme.md index 4270e025..70abe0fa 100644 --- a/readme.md +++ b/readme.md @@ -15,11 +15,9 @@ 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).** +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..f92f7a5f 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-customize-snapshot-manager.php b/tests/php/test-class-customize-snapshot-manager.php index f2968616..0cc112ac 100644 --- a/tests/php/test-class-customize-snapshot-manager.php +++ b/tests/php/test-class-customize-snapshot-manager.php @@ -231,6 +231,35 @@ public function test_should_import_and_preview_snapshot() { $this->markTestIncomplete(); } + /** + * 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() ); + } + /** * Tests preview_snapshot_settings. * diff --git a/tests/test-customize-snapshots.php b/tests/test-customize-snapshots.php index 9e4e8fc9..bf391a53 100644 --- a/tests/test-customize-snapshots.php +++ b/tests/test-customize-snapshots.php @@ -5,11 +5,24 @@ * @package CustomizeSnapshots */ +namespace CustomizeSnapshots; + /** * Class Test_Customize_Snapshots */ class Test_Customize_Snapshots extends \WP_UnitTestCase { + /** + * Clean up global scope. + */ + function clean_up_global_scope() { + global $customize_snapshots_plugin; + unset( $_REQUEST['customize_snapshot_uuid'] ); + parent::clean_up_global_scope(); + $customize_snapshots_plugin = new Plugin(); + $customize_snapshots_plugin->init(); + } + /** * Test customize_snapshots_php_version_error. * @@ -30,4 +43,30 @@ function test_customize_snapshots_php_version_error() { function test_customize_snapshots_php_version_text() { $this->assertContains( 'Customize Snapshots plugin error:', customize_snapshots_php_version_text() ); } + + /** + * Tests is_previewing_settings(). + * + * @covers is_previewing_settings() + */ + public function test_is_previewing_settings() { + $this->assertFalse( is_previewing_settings() ); + do_action( 'customize_preview_init' ); + $this->assertTrue( is_previewing_settings() ); + } + + /** + * Tests current_snapshot_uuid(). + * + * @covers current_snapshot_uuid() + */ + public function test_current_snapshot_uuid() { + global $customize_snapshots_plugin; + $this->assertNull( current_snapshot_uuid() ); + $uuid = '65aee1ff-af47-47df-9e14-9c69b3017cd3'; + $_REQUEST['customize_snapshot_uuid'] = $uuid; + $customize_snapshots_plugin = new Plugin(); + $customize_snapshots_plugin->init(); + $this->assertEquals( $uuid, current_snapshot_uuid() ); + } } From e08290ac69d51730ee247982e8ab39a7da3982c9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 8 Aug 2016 00:43:52 -0700 Subject: [PATCH 17/31] Fix readme --- readme.md | 32 ++++++++++++++++++++++++++++++++ readme.txt | 4 ++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 70abe0fa..efcb34c5 100644 --- a/readme.md +++ b/readme.md @@ -18,6 +18,38 @@ Allow Customizer states to be drafted, and previewed with a private URL. 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 ); +} +``` + ## Screenshots ## diff --git a/readme.txt b/readme.txt index f92f7a5f..94614c87 100644 --- a/readme.txt +++ b/readme.txt @@ -15,7 +15,7 @@ Customize Snapshots save the state of a Customizer session so it can be shared o 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 === += 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 @@ -27,7 +27,7 @@ is `true`, or they should include the `CustomizeSnapshots\current_snapshot_uuid( 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 {

From 50b04d32b677c9cdba1aa62b706922ce14478cc2 Mon Sep 17 00:00:00 2001
From: Weston Ruter 
Date: Mon, 8 Aug 2016 16:05:50 -0700
Subject: [PATCH 18/31] Add support for previewing Ajax requests inside of the
 Customizer preview

---
 js/customize-snapshots-preview.js        | 131 +++++++++++++++++++++++
 php/class-customize-snapshot-manager.php |  73 ++++++++++---
 2 files changed, 187 insertions(+), 17 deletions(-)

diff --git a/js/customize-snapshots-preview.js b/js/customize-snapshots-preview.js
index e0d1f690..6dd3c605 100644
--- a/js/customize-snapshots-preview.js
+++ b/js/customize-snapshots-preview.js
@@ -1 +1,132 @@
+/* global jQuery, JSON */
+/* exported CustomizeSnapshotsPreview */
+/* eslint consistent-this: [ "error", "section" ], no-magic-numbers: [ "error", { "ignore": [-1,0,1] } ] */
+/* eslint-disable no-alert */
+
 // @todo Inject customized state into all Ajax requests back to the site? All can be taken from Customize REST Resources.
+
+var CustomizeSnapshotsPreview = (function( api, $ ) {
+	'use strict';
+
+	var component = {
+		data: {
+			home_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();
+	};
+
+	/**
+	 * 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.
+	 * @todo We could rewrite the REQUEST_METHOD and $_POST to $_GET when wp_customize_preview_ajax.
+	 *
+	 * @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;
+
+		urlParser = document.createElement( 'a' );
+		urlParser.href = options.url;
+
+		// @todo Handle admin_ajax and rest_api requests differently?
+		if ( urlParser.host !== component.data.home_url.host || 0 !== urlParser.pathname.indexOf( component.data.home_url.path ) ) {
+			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 += jQuery.param( queryVars );
+	};
+
+	return component;
+} )( wp.customize, jQuery );
diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php
index 5a1fd003..3a83e370 100644
--- a/php/class-customize-snapshot-manager.php
+++ b/php/class-customize-snapshot-manager.php
@@ -161,9 +161,31 @@ function init() {
 					add_action( 'wp_loaded', array( $this, 'preview_snapshot_settings' ), 11 );
 				}
 			}
+		} 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 );
 		}
 	}
 
+	/**
+	 * Setup previewing of Ajax requests in the Customizer preview.
+	 *
+	 * @global \WP_Customize_Manager $wp_customize
+	 */
+	public function setup_preview_ajax_requests() {
+		global $wp_customize;
+
+		/*
+		 * 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();
+		}
+
+		$wp_customize->remove_preview_signature();
+	}
+
 	/**
 	 * Return true if it's a customize_save Ajax request.
 	 *
@@ -592,32 +614,49 @@ public function customize_preview_init() {
 
 	/**
 	 * Enqueue Customizer preview scripts.
+	 *
+	 * @global \WP_Customize_Manager $wp_customize
 	 */
 	public function enqueue_preview_scripts() {
-		wp_enqueue_script( 'customize-snapshots-preview' );
+		global $wp_customize;
+
+		$handle = 'customize-snapshots-preview';
+		wp_enqueue_script( $handle );
+
+		$exports = array(
+			'home_url' => wp_parse_url( home_url( '/' ) ),
+			'initial_dirty_settings' => array_keys( $wp_customize->unsanitized_post_values() ),
+			// @todo Include rest_api_url and admin_ajax_url?
+		);
+		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 ) {
-			$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'
-			);
+		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'
+		);
 	}
 
 	/**

From 1527e86a118fe7ba35ed188c317c28b9730789ad Mon Sep 17 00:00:00 2001
From: Weston Ruter 
Date: Mon, 8 Aug 2016 20:12:12 -0700
Subject: [PATCH 19/31] Ensure that GET requests in Customizer Preview get
 recognized as GET not POST

---
 js/customize-snapshots-frontend.js            |  5 ++
 js/customize-snapshots-preview.js             | 27 +++++++---
 php/class-customize-snapshot-manager.php      | 49 ++++++++++++++++++-
 .../test-class-customize-snapshot-manager.php | 18 +++++++
 4 files changed, 91 insertions(+), 8 deletions(-)

diff --git a/js/customize-snapshots-frontend.js b/js/customize-snapshots-frontend.js
index cff69aa6..184e9964 100644
--- a/js/customize-snapshots-frontend.js
+++ b/js/customize-snapshots-frontend.js
@@ -3,6 +3,11 @@
 /* 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';
 
diff --git a/js/customize-snapshots-preview.js b/js/customize-snapshots-preview.js
index 6dd3c605..0d8dbf5b 100644
--- a/js/customize-snapshots-preview.js
+++ b/js/customize-snapshots-preview.js
@@ -1,9 +1,10 @@
 /* global jQuery, JSON */
 /* exported CustomizeSnapshotsPreview */
 /* eslint consistent-this: [ "error", "section" ], no-magic-numbers: [ "error", { "ignore": [-1,0,1] } ] */
-/* eslint-disable no-alert */
 
-// @todo Inject customized state into all Ajax requests back to the site? All can be taken from Customize REST Resources.
+/*
+ * The code here is derived from Customize REST Resources: https://github.com/xwp/wp-customize-rest-resources
+ */
 
 var CustomizeSnapshotsPreview = (function( api, $ ) {
 	'use strict';
@@ -15,6 +16,16 @@ var CustomizeSnapshotsPreview = (function( api, $ ) {
 				host: '',
 				path: ''
 			},
+			rest_api_url: {
+				scheme: '',
+				host: '',
+				path: ''
+			},
+			admin_ajax_url: {
+				scheme: '',
+				host: '',
+				path: ''
+			},
 			initial_dirty_settings: []
 		}
 	};
@@ -73,7 +84,6 @@ var CustomizeSnapshotsPreview = (function( api, $ ) {
 	 *
 	 * 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.
-	 * @todo We could rewrite the REQUEST_METHOD and $_POST to $_GET when wp_customize_preview_ajax.
 	 *
 	 * @param {object} options Options.
 	 * @param {string} options.type Type.
@@ -83,13 +93,16 @@ var CustomizeSnapshotsPreview = (function( api, $ ) {
 	 * @returns {void}
 	 */
 	component.prefilterAjax = function prefilterAjax( options, originalOptions, xhr ) {
-		var requestMethod, urlParser, queryVars;
+		var requestMethod, urlParser, queryVars, isMatchingHomeUrl, isMatchingRestUrl, isMatchingAdminAjaxUrl;
 
 		urlParser = document.createElement( 'a' );
 		urlParser.href = options.url;
 
-		// @todo Handle admin_ajax and rest_api requests differently?
-		if ( urlParser.host !== component.data.home_url.host || 0 !== urlParser.pathname.indexOf( component.data.home_url.path ) ) {
+		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;
 		}
 
@@ -125,7 +138,7 @@ var CustomizeSnapshotsPreview = (function( api, $ ) {
 		}
 		queryVars = component.getCustomizeQueryVars();
 		queryVars.wp_customize_preview_ajax = 'true';
-		options.data += jQuery.param( queryVars );
+		options.data += $.param( queryVars );
 	};
 
 	return component;
diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php
index 3a83e370..ec936b5d 100644
--- a/php/class-customize-snapshot-manager.php
+++ b/php/class-customize-snapshot-manager.php
@@ -183,9 +183,55 @@ public function setup_preview_ajax_requests() {
 			$wp_customize->customize_preview_init();
 		}
 
+		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
+			 $this->override_request_method();
+		} else {
+			add_action( 'parse_request', array( $this, 'override_request_method' ), 5 );
+		}
+
+		add_action( 'admin_init', array( $this, 'override_request_method' ) );
+
 		$wp_customize->remove_preview_signature();
 	}
 
+	/**
+	 * 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;
+	}
+
 	/**
 	 * Return true if it's a customize_save Ajax request.
 	 *
@@ -625,8 +671,9 @@ public function enqueue_preview_scripts() {
 
 		$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() ),
-			// @todo Include rest_api_url and admin_ajax_url?
 		);
 		wp_add_inline_script(
 			$handle,
diff --git a/tests/php/test-class-customize-snapshot-manager.php b/tests/php/test-class-customize-snapshot-manager.php
index 0cc112ac..361b0b3e 100644
--- a/tests/php/test-class-customize-snapshot-manager.php
+++ b/tests/php/test-class-customize-snapshot-manager.php
@@ -195,6 +195,24 @@ public function test_init() {
 		$this->markTestIncomplete();
 	}
 
+	/**
+	 * Tests setup_preview_ajax_requests.
+	 *
+	 * @covers Customize_Snapshot_Manager::setup_preview_ajax_requests()
+	 */
+	public function test_setup_preview_ajax_requests() {
+		$this->markTestIncomplete();
+	}
+
+	/**
+	 * Tests override_request_method.
+	 *
+	 * @covers Customize_Snapshot_Manager::override_request_method()
+	 */
+	public function test_override_request_method() {
+		$this->markTestIncomplete();
+	}
+
 	/**
 	 * Tests doing_customize_save_ajax.
 	 *

From f2e5a178c573c491862056478888ee7babbe0ccd Mon Sep 17 00:00:00 2001
From: Weston Ruter 
Date: Mon, 8 Aug 2016 23:05:10 -0700
Subject: [PATCH 20/31] Break up init method into smaller methods; add tests

---
 php/class-customize-snapshot-manager.php      |  97 ++++++++------
 ...-class-ajax-customize-snapshot-manager.php |   1 +
 .../test-class-customize-snapshot-manager.php | 122 +++++++++++++++++-
 3 files changed, 174 insertions(+), 46 deletions(-)

diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php
index ec936b5d..14e1d644 100644
--- a/php/class-customize-snapshot-manager.php
+++ b/php/class-customize-snapshot-manager.php
@@ -114,55 +114,71 @@ function init() {
 		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;
+	}
 
-		if ( $this->current_snapshot_uuid ) {
-			$this->ensure_customize_manager();
-
-			add_action( 'wp_ajax_' . self::AJAX_ACTION, array( $this, 'handle_update_snapshot_request' ) );
-
-			$this->snapshot = new Customize_Snapshot( $this, $this->current_snapshot_uuid );
+	/**
+	 * Load snapshot.
+	 */
+	public function load_snapshot() {
+		$this->ensure_customize_manager();
+		$this->snapshot = new Customize_Snapshot( $this, $this->current_snapshot_uuid );
 
-			if ( true === $this->should_import_and_preview_snapshot( $this->snapshot ) ) {
+		if ( ! $this->should_import_and_preview_snapshot( $this->snapshot ) ) {
+			return;
+		}
 
-				$this->add_widget_setting_preview_filters();
-				$this->add_nav_menu_setting_preview_filters();
+		$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' ) );
-				}
+		/*
+		 * 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' ) );
+		}
 
-				// Block the robots.
-				add_action( 'wp_head', 'wp_no_robots' );
+		// Block the robots.
+		add_action( 'wp_head', 'wp_no_robots' );
 
-				// Preview post values.
-				if ( did_action( 'wp_loaded' ) ) {
-					$this->preview_snapshot_settings();
-				} else {
-					add_action( 'wp_loaded', array( $this, 'preview_snapshot_settings' ), 11 );
-				}
-			}
-		} 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 );
+		// Preview post values.
+		if ( did_action( 'wp_loaded' ) ) {
+			$this->preview_snapshot_settings();
+		} else {
+			add_action( 'wp_loaded', array( $this, 'preview_snapshot_settings' ), 11 );
 		}
 	}
 
@@ -172,7 +188,7 @@ function init() {
 	 * @global \WP_Customize_Manager $wp_customize
 	 */
 	public function setup_preview_ajax_requests() {
-		global $wp_customize;
+		global $wp_customize, $pagenow;
 
 		/*
 		 * When making admin-ajax requests from the frontend, settings won't be
@@ -183,14 +199,13 @@ public function setup_preview_ajax_requests() {
 			$wp_customize->customize_preview_init();
 		}
 
-		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
-			 $this->override_request_method();
+		// 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 );
 		}
 
-		add_action( 'admin_init', array( $this, 'override_request_method' ) );
-
 		$wp_customize->remove_preview_signature();
 	}
 
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 361b0b3e..17f7eb85 100644
--- a/tests/php/test-class-customize-snapshot-manager.php
+++ b/tests/php/test-class-customize-snapshot-manager.php
@@ -100,6 +100,7 @@ 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();
 	}
 
@@ -187,21 +188,106 @@ 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() {
-		$this->markTestIncomplete();
+		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' ) ) );
 	}
 
 	/**
@@ -210,7 +296,33 @@ public function test_setup_preview_ajax_requests() {
 	 * @covers Customize_Snapshot_Manager::override_request_method()
 	 */
 	public function test_override_request_method() {
-		$this->markTestIncomplete();
+		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 );
 	}
 
 	/**

From 557aa06a01f622350626ccb5b06291bd711567c0 Mon Sep 17 00:00:00 2001
From: Weston Ruter 
Date: Mon, 8 Aug 2016 23:34:13 -0700
Subject: [PATCH 21/31] Add test for Post_Type::display_post_states()

---
 php/class-post-type.php            | 16 +++++++--------
 tests/php/test-class-post-type.php | 33 ++++++++++++++----------------
 2 files changed, 23 insertions(+), 26 deletions(-)

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/tests/php/test-class-post-type.php b/tests/php/test-class-post-type.php
index db7c1bcd..b2027887 100644
--- a/tests/php/test-class-post-type.php
+++ b/tests/php/test-class-post-type.php
@@ -49,7 +49,7 @@ public function test_register() {
 		$this->assertTrue( post_type_exists( Post_Type::SLUG ) );
 
 		$this->assertEquals( 10, has_filter( 'post_type_link', array( $post_type, 'filter_post_type_link' ) ) );
-		$this->assertEquals( 100, has_action( 'add_meta_boxes_' . Post_Type::SLUG, array( $post_type, 'remove_publish_metabox' ) ) );
+		$this->assertEquals( 100, has_action( 'add_meta_boxes_' . Post_Type::SLUG, array( $post_type, 'remove_slug_metabox' ) ) );
 		$this->assertEquals( 10, has_action( 'load-revision.php', array( $post_type, 'suspend_kses_for_snapshot_revision_restore' ) ) );
 		$this->assertEquals( 10, has_filter( 'get_the_excerpt', array( $post_type, 'filter_snapshot_excerpt' ) ) );
 		$this->assertEquals( 10, has_filter( 'post_row_actions', array( $post_type, 'filter_post_row_actions' ) ) );
@@ -129,23 +129,9 @@ public function test_setup_metaboxes() {
 		$this->assertTrue( ! empty( $wp_meta_boxes[ Post_Type::SLUG ]['normal']['high'][ $metabox_id ] ) );
 	}
 
-	/**
-	 * Tests remove_publish_metabox.
-	 *
-	 * @covers Post_Type::remove_publish_metabox()
-	 */
-	public function test_remove_publish_metabox() {
-		$this->markTestIncomplete();
-	}
+	/* Note: Code coverage ignored on Post_Type::remove_publish_metabox(). */
 
-	/**
-	 * Tests suspend_kses_for_snapshot_revision_restore.
-	 *
-	 * @covers Post_Type::suspend_kses_for_snapshot_revision_restore()
-	 */
-	public function test_suspend_kses_for_snapshot_revision_restore() {
-		$this->markTestIncomplete();
-	}
+	/* Note: Code coverage ignored on Post_Type::suspend_kses_for_snapshot_revision_restore(). */
 
 	/**
 	 * Test include the setting IDs in the excerpt.
@@ -643,7 +629,18 @@ function test_filter_user_has_cap() {
 	 * @covers Post_Type::display_post_states()
 	 */
 	public function test_display_post_states() {
-		$this->markTestIncomplete();
+		$post_type = new Post_Type( $this->plugin->customize_snapshot_manager );
+
+		$post_id = $post_type->save( array(
+			'uuid' => self::UUID,
+			'data' => array( 'foo' => array( 'value' => 'bar' ) ),
+		) );
+		$states = $post_type->display_post_states( array(), get_post( $post_id ) );
+		$this->assertArrayNotHasKey( 'snapshot_error', $states );
+
+		update_post_meta( $post_id, 'snapshot_error_on_publish', true );
+		$states = $post_type->display_post_states( array(), get_post( $post_id ) );
+		$this->assertArrayHasKey( 'snapshot_error', $states );
 	}
 
 	/**

From 751a80f38c8622bb89e907a4836603ea815b2abf Mon Sep 17 00:00:00 2001
From: Weston Ruter 
Date: Mon, 8 Aug 2016 23:36:29 -0700
Subject: [PATCH 22/31] Add test for
 Customize_Snapshot_Manager::doing_customize_save_ajax()

---
 tests/php/test-class-customize-snapshot-manager.php | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/tests/php/test-class-customize-snapshot-manager.php b/tests/php/test-class-customize-snapshot-manager.php
index 17f7eb85..7feede4f 100644
--- a/tests/php/test-class-customize-snapshot-manager.php
+++ b/tests/php/test-class-customize-snapshot-manager.php
@@ -331,7 +331,14 @@ public function test_override_request_method() {
 	 * @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() );
 	}
 
 	/**

From 672c20264c21978f8da95e746cee89ebb21aff5b Mon Sep 17 00:00:00 2001
From: Weston Ruter 
Date: Mon, 8 Aug 2016 23:38:12 -0700
Subject: [PATCH 23/31] Add test for
 Customize_Snapshot_Manager::ensure_customize_manager()

---
 tests/php/test-class-customize-snapshot-manager.php | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/tests/php/test-class-customize-snapshot-manager.php b/tests/php/test-class-customize-snapshot-manager.php
index 7feede4f..81b0a25a 100644
--- a/tests/php/test-class-customize-snapshot-manager.php
+++ b/tests/php/test-class-customize-snapshot-manager.php
@@ -347,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 );
 	}
 
 	/**

From 19a224e82d3771e88d3e7a57f4878ffbb991b7f2 Mon Sep 17 00:00:00 2001
From: Weston Ruter 
Date: Mon, 8 Aug 2016 23:40:12 -0700
Subject: [PATCH 24/31] Add test for
 Customize_Snapshot_Manager::is_theme_active()

---
 tests/php/test-class-customize-snapshot-manager.php | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/tests/php/test-class-customize-snapshot-manager.php b/tests/php/test-class-customize-snapshot-manager.php
index 81b0a25a..b780d53f 100644
--- a/tests/php/test-class-customize-snapshot-manager.php
+++ b/tests/php/test-class-customize-snapshot-manager.php
@@ -362,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() );
 	}
 
 	/**

From be2a9cd3ee6d901e2144bdd712969f8c26c61b2d Mon Sep 17 00:00:00 2001
From: Weston Ruter 
Date: Tue, 9 Aug 2016 00:05:25 -0700
Subject: [PATCH 25/31] Add test for
 Customize_Snapshot_Manager::should_import_and_preview_snapshot()

---
 php/class-customize-snapshot-manager.php      |  6 +--
 .../test-class-customize-snapshot-manager.php | 50 ++++++++++++++++++-
 2 files changed, 52 insertions(+), 4 deletions(-)

diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php
index 14e1d644..7ac97c13 100644
--- a/php/class-customize-snapshot-manager.php
+++ b/php/class-customize-snapshot-manager.php
@@ -292,8 +292,8 @@ public function is_theme_active() {
 	public function should_import_and_preview_snapshot( Customize_Snapshot $snapshot ) {
 		global $pagenow;
 
-		// Ignore if in the admin.
-		if ( is_admin() && ! ( defined( 'DOING_AJAX' ) && DOING_AJAX ) && 'customize.php' !== $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;
 		}
 
@@ -316,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;
 		}
 
diff --git a/tests/php/test-class-customize-snapshot-manager.php b/tests/php/test-class-customize-snapshot-manager.php
index b780d53f..24854795 100644
--- a/tests/php/test-class-customize-snapshot-manager.php
+++ b/tests/php/test-class-customize-snapshot-manager.php
@@ -377,7 +377,55 @@ 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 ) );
 	}
 
 	/**

From 674557087bc5e818c6e2f91edd512ff65850a43a Mon Sep 17 00:00:00 2001
From: Weston Ruter 
Date: Tue, 9 Aug 2016 21:13:21 -0700
Subject: [PATCH 26/31] Add support for previewing GET form submissions in
 Customizer preview

This fixes #20714: Theme customizer: Impossible to preview a search results page 
---
 js/customize-snapshots-preview.js | 65 +++++++++++++++++++++++++++++++
 1 file changed, 65 insertions(+)

diff --git a/js/customize-snapshots-preview.js b/js/customize-snapshots-preview.js
index 0d8dbf5b..f5feeccb 100644
--- a/js/customize-snapshots-preview.js
+++ b/js/customize-snapshots-preview.js
@@ -41,6 +41,7 @@ var CustomizeSnapshotsPreview = (function( api, $ ) {
 		_.extend( component.data, args );
 
 		component.injectSnapshotIntoAjaxRequests();
+		component.handleFormSubmissions();
 	};
 
 	/**
@@ -141,5 +142,69 @@ var CustomizeSnapshotsPreview = (function( api, $ ) {
 		options.data += $.param( queryVars );
 	};
 
+	/**
+	 * Handle form submissions.
+	 *
+	 * Implements todo in https://github.com/xwp/wordpress-develop/blob/4.5.3/src/wp-includes/js/customize-preview.js#L69-L73
+	 */
+	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();
+			} );
+		} );
+	};
+
+	/**
+	 * Check if form is previewable (has a GET method and an action pointing to WP).
+	 *
+	 * @param {HTMLFormElement} form Form.
+	 * @returns {boolean} Is previewable.
+	 */
+	component.isFormPreviewable = function isFormPreviewable( form ) {
+		var method = form.method.toUpperCase(), urlParser;
+		if ( 'GET' !== method ) {
+			return false;
+		}
+		urlParser = document.createElement( 'a' );
+		urlParser.href = form.action;
+		return urlParser.host === component.data.home_url.host && 0 === urlParser.pathname.indexOf( component.data.home_url.path );
+	};
+
+	/**
+	 * Replace form submit handler.
+	 */
+	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 the method is
+			 * GET and its action points to a URL on this site, 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 ( ! event.isDefaultPrevented() && component.isFormPreviewable( this ) ) {
+				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 );

From 005aed463c445c636726e10228643d3120063497 Mon Sep 17 00:00:00 2001
From: Weston Ruter 
Date: Tue, 9 Aug 2016 21:15:43 -0700
Subject: [PATCH 27/31] Add not-allowed cursor to submit buttons for forms with
 post method

---
 css/customize-snapshots-preview.css      | 8 ++++++++
 php/class-customize-snapshot-manager.php | 1 +
 php/class-plugin.php                     | 5 +++++
 3 files changed, 14 insertions(+)
 create mode 100644 css/customize-snapshots-preview.css

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/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php
index 7ac97c13..62d83872 100644
--- a/php/class-customize-snapshot-manager.php
+++ b/php/class-customize-snapshot-manager.php
@@ -683,6 +683,7 @@ public function enqueue_preview_scripts() {
 
 		$handle = 'customize-snapshots-preview';
 		wp_enqueue_script( $handle );
+		wp_enqueue_style( $handle );
 
 		$exports = array(
 			'home_url' => wp_parse_url( home_url( '/' ) ),
diff --git a/php/class-plugin.php b/php/class-plugin.php
index 817fe094..0afb3bf6 100644
--- a/php/class-plugin.php
+++ b/php/class-plugin.php
@@ -96,5 +96,10 @@ public function register_styles( \WP_Styles $wp_styles ) {
 		$src = $this->dir_url . 'css/customize-snapshots' . $min . '.css';
 		$deps = array( 'wp-jquery-ui-dialog' );
 		$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 );
 	}
 }

From e2eb16ee3725baba6a25869e73ec03576c4957d1 Mon Sep 17 00:00:00 2001
From: Weston Ruter 
Date: Tue, 9 Aug 2016 21:34:44 -0700
Subject: [PATCH 28/31] Persist current snapshot in form submissions on
 frontend

---
 js/customize-snapshots-frontend.js | 66 ++++++++++++++++++++++++++++--
 1 file changed, 62 insertions(+), 4 deletions(-)

diff --git a/js/customize-snapshots-frontend.js b/js/customize-snapshots-frontend.js
index 184e9964..dba25d61 100644
--- a/js/customize-snapshots-frontend.js
+++ b/js/customize-snapshots-frontend.js
@@ -42,6 +42,7 @@ var CustomizeSnapshotsFrontend = ( function( $ ) {
 		component.injectSnapshotIntoLinks();
 		component.handleExitSnapshotSessionLink();
 		component.injectSnapshotIntoAjaxRequests();
+		component.injectSnapshotIntoForms();
 	};
 
 	/**
@@ -108,7 +109,7 @@ var CustomizeSnapshotsFrontend = ( function( $ ) {
 
 			// Inject links into initial document.
 			$( document.body ).find( linkSelectors ).each( function() {
-				component.injectLinkQueryParam( this );
+				component.injectSnapshotLinkParam( this );
 			} );
 
 			// Inject links for new elements added to the page
@@ -116,7 +117,7 @@ var CustomizeSnapshotsFrontend = ( function( $ ) {
 				component.mutationObserver = new MutationObserver( function( mutations ) {
 					_.each( mutations, function( mutation ) {
 						$( mutation.target ).find( linkSelectors ).each( function() {
-							component.injectLinkQueryParam( this );
+							component.injectSnapshotLinkParam( this );
 						} );
 					} );
 				} );
@@ -128,7 +129,7 @@ var CustomizeSnapshotsFrontend = ( function( $ ) {
 
 				// If mutation observers aren't available, fallback to just-in-time injection.
 				$( document.documentElement ).on( 'click focus mouseover', linkSelectors, function() {
-					component.injectLinkQueryParam( this );
+					component.injectSnapshotLinkParam( this );
 				} );
 			}
 		} );
@@ -201,7 +202,7 @@ var CustomizeSnapshotsFrontend = ( function( $ ) {
 	 * @param {object} element.search Query string.
 	 * @returns {void}
 	 */
-	component.injectLinkQueryParam = function injectLinkQueryParam( element ) {
+	component.injectSnapshotLinkParam = function injectSnapshotLinkParam( element ) {
 		if ( component.doesLinkHaveSnapshotQueryParam( element ) || ! component.shouldLinkHaveSnapshotParam( element ) ) {
 			return;
 		}
@@ -272,6 +273,63 @@ var CustomizeSnapshotsFrontend = ( function( $ ) {
 		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.querySelector( 'input[name=customize_snapshot_uuid]' ) ) {
+			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 );
 

From 307c5af4404b07c59ae1c86a249799996f82f136 Mon Sep 17 00:00:00 2001
From: Weston Ruter 
Date: Tue, 9 Aug 2016 21:53:21 -0700
Subject: [PATCH 29/31] Re-use the previewUrl value's setter to determine if
 form action is previewable

---
 js/customize-snapshots-preview.js | 20 +++-----------------
 1 file changed, 3 insertions(+), 17 deletions(-)

diff --git a/js/customize-snapshots-preview.js b/js/customize-snapshots-preview.js
index f5feeccb..aa06f23f 100644
--- a/js/customize-snapshots-preview.js
+++ b/js/customize-snapshots-preview.js
@@ -157,24 +157,10 @@ var CustomizeSnapshotsPreview = (function( api, $ ) {
 		} );
 	};
 
-	/**
-	 * Check if form is previewable (has a GET method and an action pointing to WP).
-	 *
-	 * @param {HTMLFormElement} form Form.
-	 * @returns {boolean} Is previewable.
-	 */
-	component.isFormPreviewable = function isFormPreviewable( form ) {
-		var method = form.method.toUpperCase(), urlParser;
-		if ( 'GET' !== method ) {
-			return false;
-		}
-		urlParser = document.createElement( 'a' );
-		urlParser.href = form.action;
-		return urlParser.host === component.data.home_url.host && 0 === urlParser.pathname.indexOf( component.data.home_url.path );
-	};
-
 	/**
 	 * Replace form submit handler.
+	 *
+	 * @returns {void}
 	 */
 	component.replaceFormSubmitHandler = function replaceFormSubmitHandler() {
 		var body = $( document.body );
@@ -190,7 +176,7 @@ var CustomizeSnapshotsPreview = (function( api, $ ) {
 			 * URL and send this in a url message to the Customizer pane so that
 			 * it will be loaded.
 			 */
-			if ( ! event.isDefaultPrevented() && component.isFormPreviewable( this ) ) {
+			if ( ! event.isDefaultPrevented() && 'GET' === this.method.toUpperCase() ) {
 				urlParser = document.createElement( 'a' );
 				urlParser.href = this.action;
 				if ( urlParser.search.substr( 1 ).length > 1 ) {

From 51c0439717396f8cee437b694a38c229eceacbea Mon Sep 17 00:00:00 2001
From: Weston Ruter 
Date: Tue, 9 Aug 2016 21:53:40 -0700
Subject: [PATCH 30/31] Use jQuery instead of straight querySelector

---
 js/customize-snapshots-frontend.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/js/customize-snapshots-frontend.js b/js/customize-snapshots-frontend.js
index dba25d61..aa3abbaa 100644
--- a/js/customize-snapshots-frontend.js
+++ b/js/customize-snapshots-frontend.js
@@ -314,7 +314,7 @@ var CustomizeSnapshotsFrontend = ( function( $ ) {
 	 */
 	component.injectSnapshotFormInput = function injectSnapshotFormInput( form ) {
 		var urlParser;
-		if ( form.querySelector( 'input[name=customize_snapshot_uuid]' ) ) {
+		if ( $( form ).find( 'input[name=customize_snapshot_uuid]' ).length > 0 ) {
 			return;
 		}
 		urlParser = document.createElement( 'a' );

From ae6a1762312e3f69aa5632c89b8f8fa7a7c848e2 Mon Sep 17 00:00:00 2001
From: Weston Ruter 
Date: Tue, 9 Aug 2016 21:59:43 -0700
Subject: [PATCH 31/31] Clarify jsdoc

---
 js/customize-snapshots-preview.js | 19 ++++++++++++-------
 1 file changed, 12 insertions(+), 7 deletions(-)

diff --git a/js/customize-snapshots-preview.js b/js/customize-snapshots-preview.js
index aa06f23f..80a8f18e 100644
--- a/js/customize-snapshots-preview.js
+++ b/js/customize-snapshots-preview.js
@@ -145,7 +145,10 @@ var CustomizeSnapshotsPreview = (function( api, $ ) {
 	/**
 	 * Handle form submissions.
 	 *
-	 * Implements todo in https://github.com/xwp/wordpress-develop/blob/4.5.3/src/wp-includes/js/customize-preview.js#L69-L73
+	 * 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() {
@@ -170,11 +173,14 @@ var CustomizeSnapshotsPreview = (function( api, $ ) {
 
 			/*
 			 * If the default wasn't prevented already (in which case the form
-			 * submission is already being handled by JS), and if the method is
-			 * GET and its action points to a URL on this site, 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.
+			 * 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' );
@@ -183,7 +189,6 @@ var CustomizeSnapshotsPreview = (function( api, $ ) {
 					urlParser.search += '&';
 				}
 				urlParser.search += $( this ).serialize();
-
 				api.preview.send( 'url', urlParser.href );
 			}