diff --git a/.travis.yml b/.travis.yml index 1dbd3883..3234f80e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,8 @@ node_js: env: - WP_VERSION=trunk WP_MULTISITE=0 + - WP_VERSION=latest WP_MULTISITE=0 + - WP_VERSION=4.6.1 WP_MULTISITE=0 - WP_VERSION=latest WP_MULTISITE=1 install: diff --git a/composer.json b/composer.json index a0390de4..35ecb40d 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "xwp/wp-customize-snapshots", "description": "Allow Customizer states to be drafted, and previewed with a private URL.", - "version": "0.5.2", + "version": "0.6.0", "type": "wordpress-plugin", "homepage": "https://github.com/xwp/wp-customize-snapshots", "license": "GPL-2.0+", diff --git a/css/customize-snapshots-admin.css b/css/customize-snapshots-admin.css new file mode 100644 index 00000000..07d1468c --- /dev/null +++ b/css/customize-snapshots-admin.css @@ -0,0 +1,12 @@ +details.snapshot-setting-removed summary { + text-decoration: line-through; +} +details:not(.snapshot-setting-removed) .snapshot-toggle-setting-removal { + color: #a00 +} +details:not(.snapshot-setting-removed) .snapshot-toggle-setting-removal:hover { + color: #f00 +} +details .snapshot-toggle-setting-removal { + float: right; +} diff --git a/css/customize-snapshots.css b/css/customize-snapshots.css index 7b25b429..e98c6f3e 100644 --- a/css/customize-snapshots.css +++ b/css/customize-snapshots.css @@ -1,17 +1,18 @@ #snapshot-preview-link, -#snapshot-schedule-button { +#snapshot-expand-button { float: right; margin-top: 13px; margin-right: 4px; color: #656a6f; } -#snapshot-schedule-button { +#snapshot-expand-button { display: block; } -#snapshot-schedule-button:hover, -#snapshot-schedule-button:focus, -#snapshot-schedule-button:active { + +#snapshot-expand-button:hover, +#snapshot-expand-button:focus, +#snapshot-expand-button:active { color: #191e23; } @@ -64,7 +65,7 @@ } } -#snapshot-schedule { +#customize-snapshot { background: #fff !important; border-bottom: 1px solid #ddd; line-height: 1.5; @@ -72,28 +73,30 @@ top: 46px; position: absolute; width: 100%; - box-shadow: 0 5px 0 0 rgba(0,0,0,0.05); - padding:10px; + box-shadow: 0 5px 0 0 rgba(0, 0, 0, 0.05); + padding: 10px; box-sizing: border-box; } -#snapshot-schedule .snapshot-schedule-title { +#customize-snapshot .snapshot-schedule-title { color: #555; } -#snapshot-schedule .snapshot-schedule-title h3 { +#customize-snapshot .snapshot-schedule-title h3 { margin: .2em 2em .75em 0; } -#snapshot-schedule .snapshot-schedule-description { +#customize-snapshot .snapshot-schedule-description { margin-bottom: 0.5em; + margin-top: 0; } -#snapshot-schedule .timezone-info { + +#customize-snapshot .timezone-info { margin-top: 0.5em; font-size: smaller; } -#snapshot-schedule a.snapshot-edit-link { +#customize-snapshot a.snapshot-edit-link { position: absolute; top: 4px; right: 1px; @@ -109,12 +112,12 @@ padding: 10px; } -#snapshot-schedule a.snapshot-edit-link:focus, -#snapshot-schedule a.snapshot-edit-link:hover { +#customize-snapshot a.snapshot-edit-link:focus, +#customize-snapshot a.snapshot-edit-link:hover { color: #0073aa; } -#snapshot-schedule a.snapshot-edit-link:before { +#customize-snapshot a.snapshot-edit-link:before { padding: 4px; position: absolute; top: 5px; @@ -123,7 +126,7 @@ border-radius: 100%; } -#snapshot-schedule a.snapshot-edit-link:focus:before { +#customize-snapshot a.snapshot-edit-link:focus:before { -webkit-box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8); @@ -132,11 +135,11 @@ 0 0 2px 1px rgba(30, 140, 190, .8); } -#snapshot-schedule .accordion-section-title { +#customize-snapshot .accordion-section-title { padding: 10px; } -#snapshot-schedule .reset-time { +#customize-snapshot .reset-time { font-weight: normal; font-size: 80%; display: none; @@ -168,13 +171,13 @@ min-width: 4em; } -.snapshot-schedule-control input[type="number"]{ - width:100%; +.snapshot-schedule-control input[type="number"] { + width: 100%; } .snapshot-schedule-control .time-special-char { - padding-left:2px; - padding-right:2px; + padding-left: 2px; + padding-right: 2px; } .snapshot-schedule-control select { @@ -201,3 +204,147 @@ .select2-container { z-index: 500100 !important; } + +.snapshot-control input[type="text"] { + width: 100%; + line-height: 18px; + margin: 0; +} + +#customize-snapshot .snapshot-schedule-title h3 { + font-size: 16px; +} + +#customize-snapshot .snapshot-controls { + list-style: none; + margin: 0; +} + +.snapshot-future-date-notification.notice { + margin-bottom: 5px; + border-top: 1px solid #eee; + padding: 5px; +} + +/*Status Button*/ +/** @todo Need to add snapshot specific class for all ui classes. **/ + +#snapshot-status-button-wrapper { + float: right; + position: relative; + margin-top: 9px; + height: 28px; +} + +#snapshot-status-button-wrapper .button-primary { + margin-top: 0; + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Chrome/Safari/Opera */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; +} + +.snapshot-status-button-wrapper .button:active{ + -webkit-transform: none; + -moz-transform: none; + -ms-transform: none; + -o-transform: none; + transform: none; /* 4.7 added translateY(1px) */ +} + +.ui-selectmenu-menu { + padding: 0; + margin: 0; + position: absolute; + top: 0; + left: 0; + display: none; +} + +.ui-selectmenu-menu .ui-menu { + overflow: auto; + margin: 0; + margin-top: 2px; + overflow-x: hidden; + background: #FFFFFF; + border-top: 0; + border-radius: 0 0 5px 5px; + box-shadow: 0 1px 4px 0 rgba(33, 38, 34, .12), 0 2px 11px 0 rgba(30, 36, 37, .14) +} + +.ui-selectmenu-menu .ui-menu li { + padding: 5px 10px 5px 5px; + margin: 0; +} + +.ui-selectmenu-menu .ui-menu .ui-state-focus { + background: #0073aa; + color: #FFFFFF; + cursor: pointer; +} + +.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup { + font-size: 1em; + font-weight: bold; + line-height: 1.5; + padding: 2px 0.4em; + margin: 0.5em 0 0 0; + height: auto; + border: 0; +} + +.ui-selectmenu-open { + display: block; +} + +#snapshot-status-button-wrapper .ui-selectmenu-button span.dashicons { + text-indent: 0; + float: right; + border-radius: 0 3px 3px 0; + padding-left: 0; + padding-right: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + font-size: 20px; + width: 20px; +} + +#snapshot-status-button-wrapper .ui-selectmenu-button span.dashicons:before { + display: inline-block; + margin-left: -3px; +} + +@media screen and ( max-width: 640px ) { + #snapshot-status-button-wrapper .ui-selectmenu-button span.dashicons { + font-size: 17px; + } +} + +.ui-selectmenu-button span.ui-selectmenu-text { + border-radius: 3px 0 0 3px; + opacity: 0; +} + +.ui-selectmenu-menu.ui-front { + z-index: 999999; +} + +.snapshot-status-button-overlay.button { + width: calc( 100% - 20px ); + height: 28px; + position: absolute; + top: 0; + left: 0; + z-index: 1; + border-radius: 3px 0 0 3px; + box-shadow: none; +} + +#customize-header-actions .ui-selectmenu-button { + display: inline-block; + outline-width: 2px; + outline-offset: -2px; +} + diff --git a/customize-snapshots.php b/customize-snapshots.php index bf0946fd..ec921121 100644 --- a/customize-snapshots.php +++ b/customize-snapshots.php @@ -3,7 +3,7 @@ * Plugin Name: Customize Snapshots * Plugin URI: https://github.com/xwp/wp-customize-snapshots * Description: Allow Customizer states to be drafted, and previewed with a private URL. - * Version: 0.5.2 + * Version: 0.6.0-rc1 * Author: XWP * Author URI: https://xwp.co/ * License: GPLv2+ diff --git a/dev-lib b/dev-lib index 4eab31dd..85f9cf48 160000 --- a/dev-lib +++ b/dev-lib @@ -1 +1 @@ -Subproject commit 4eab31ddb538f57d8e147657f2fd34437aeda44f +Subproject commit 85f9cf48ec5d2fbdeb3eb26fcee9fc2383f36805 diff --git a/instance.php b/instance.php index d1c8fa65..350fa45c 100644 --- a/instance.php +++ b/instance.php @@ -33,7 +33,12 @@ function get_plugin_instance() { * @return bool Whether previewing settings. */ function is_previewing_settings() { - return get_plugin_instance()->customize_snapshot_manager->is_previewing_settings(); + $manager = get_plugin_instance()->customize_snapshot_manager; + if ( get_plugin_instance()->compat ) { + return $manager->is_previewing_settings(); + } else { + return ( isset( $manager->customize_manager ) && $manager->customize_manager->is_preview() ) || did_action( 'customize_preview_init' ); + } } /** @@ -51,3 +56,19 @@ function current_snapshot_uuid() { return $customize_snapshot_uuid; } } + +/** + * Returns whether it is back compat or not. + * + * @return bool is compat. + */ +function is_back_compat() { + $wp_version = get_bloginfo( 'version' ); + + // Fix in case version contains extra string for example 4.7-src in that case php version_compare fails. + $pos = strpos( $wp_version, '-' ); + if ( false !== $pos ) { + $wp_version = substr( $wp_version, 0, $pos ); + } + return version_compare( $wp_version, '4.7', '<' ); +} diff --git a/js/customize-snapshots-frontend.js b/js/compat/customize-snapshots-frontend.js similarity index 100% rename from js/customize-snapshots-frontend.js rename to js/compat/customize-snapshots-frontend.js diff --git a/js/customize-snapshots-preview.js b/js/compat/customize-snapshots-preview.js similarity index 99% rename from js/customize-snapshots-preview.js rename to js/compat/customize-snapshots-preview.js index 80a8f18e..78aa8fbc 100644 --- a/js/customize-snapshots-preview.js +++ b/js/compat/customize-snapshots-preview.js @@ -194,7 +194,7 @@ var CustomizeSnapshotsPreview = (function( api, $ ) { // Now preventDefault as is done on the normal submit.preview handler in customize-preview.js. event.preventDefault(); - }); + } ); }; return component; diff --git a/js/compat/customize-snapshots.js b/js/compat/customize-snapshots.js new file mode 100644 index 00000000..c519c74b --- /dev/null +++ b/js/compat/customize-snapshots.js @@ -0,0 +1,511 @@ +/* global jQuery, wp, _customizeSnapshotsCompatSettings */ +/* eslint consistent-this: ["error", "snapshot"] */ + +( function( api, $ ) { + 'use strict'; + + api.SnapshotsCompat = api.Snapshots.extend( { + + uuidParam: 'customize_snapshot_uuid', + + initialize: function initialize( snapshotsConfig ) { + var snapshot = this; + + if ( _.isObject( snapshotsConfig ) ) { + _.extend( snapshot.data, snapshotsConfig ); + } + + window._wpCustomizeControlsL10n.save = snapshot.data.i18n.publish; + window._wpCustomizeControlsL10n.saved = snapshot.data.i18n.published; + + api.bind( 'ready', function() { + api.state.create( 'snapshot-exists', snapshot.data.snapshotExists ); + snapshot.extendPreviewerQuery(); + + if ( api.state( 'snapshot-exists' ).get() ) { + api.state( 'saved' ).set( false ); + snapshot.resetSavedStateQuietly(); + } + } ); + + // Make sure that saved state is false so that Published button behaves as expected. + api.bind( 'save', function() { + api.state( 'saved' ).set( false ); + } ); + + api.bind( 'change', function() { + api.state( 'snapshot-saved' ).set( false ); + } ); + + api.bind( 'saved', function( response ) { + var url = window.location.href, + updatedUrl, + urlParts; + + // Update the UUID. + if ( response.new_customize_snapshot_uuid ) { + snapshot.data.uuid = response.new_customize_snapshot_uuid; + snapshot.previewLink.attr( 'target', snapshot.data.uuid ); + } + + api.state( 'snapshot-exists' ).set( false ); + + // Replace the history state with an updated Customizer URL that does not include the Snapshot UUID. + urlParts = url.split( '?' ); + if ( history.replaceState && urlParts[1] ) { + updatedUrl = urlParts[0] + '?' + _.filter( urlParts[1].split( '&' ), function( queryPair ) { + return ! /^(customize_snapshot_uuid)=/.test( queryPair ); + } ).join( '&' ); + updatedUrl = updatedUrl.replace( /\?$/, '' ); + if ( updatedUrl !== url ) { + history.replaceState( {}, document.title, updatedUrl ); + } + } + } ); + + api.Snapshots.prototype.initialize.call( snapshot, snapshotsConfig ); + }, + + /** + * Update button text. + * + * @returns {void} + */ + updateButtonText: function updateButtonText() { + var snapshot = this, date = snapshot.getDateFromInputs(); + if ( snapshot.isFutureDate() && date && snapshot.data.currentUserCanPublish ) { + snapshot.snapshotButton.text( snapshot.data.i18n.scheduleButton ); + } else { + snapshot.snapshotButton.text( api.state( 'snapshot-exists' ).get() ? snapshot.data.i18n.updateButton : snapshot.data.i18n.saveButton ); + } + }, + + /** + * Make the AJAX request to update/save a snapshot. + * + * @param {object} options Options. + * @param {string} options.status The post status for the snapshot. + * @return {void} + */ + sendUpdateSnapshotRequest: function sendUpdateSnapshotRequest( options ) { + var snapshot = this, + spinner = $( '#customize-header-actions' ).find( '.spinner' ), + request, data; + + data = _.extend( + { + status: 'draft' + }, + api.previewer.query(), + options, + { + nonce: api.settings.nonce.snapshot, + customize_snapshot_uuid: snapshot.data.uuid + } + ); + + request = wp.ajax.post( 'customize_update_snapshot', data ); + + spinner.addClass( 'is-active' ); + request.always( function( response ) { + spinner.removeClass( 'is-active' ); + if ( response.edit_link ) { + snapshot.data.editLink = response.edit_link; + } + if ( response.snapshot_publish_date ) { + snapshot.data.publishDate = response.snapshot_publish_date; + } + if ( response.title ) { + snapshot.data.title = response.title; + } + snapshot.updateSnapshotEditControls(); + snapshot.data.dirty = false; + + // @todo Remove privateness from _handleSettingValidities in Core. + if ( api._handleSettingValidities && response.setting_validities ) { + api._handleSettingValidities( { + settingValidities: response.setting_validities, + focusInvalidControl: true + } ); + } + } ); + + request.done( function( response ) { + var url = api.previewer.previewUrl(), + regex = new RegExp( '([?&])customize_snapshot_uuid=.*?(&|$)', 'i' ), + notFound = -1, + separator = url.indexOf( '?' ) !== notFound ? '&' : '?', + customizeUrl = window.location.href, + customizeSeparator = customizeUrl.indexOf( '?' ) !== notFound ? '&' : '?'; + + if ( url.match( regex ) ) { + url = url.replace( regex, '$1customize_snapshot_uuid=' + encodeURIComponent( snapshot.data.uuid ) + '$2' ); + } else { + url = url + separator + 'customize_snapshot_uuid=' + encodeURIComponent( snapshot.data.uuid ); + } + + // Change the save button text to update. + api.state( 'snapshot-exists' ).set( true ); + + // Replace the history state with an updated Customizer URL that includes the Snapshot UUID. + if ( history.replaceState && ! customizeUrl.match( regex ) ) { + customizeUrl += customizeSeparator + 'customize_snapshot_uuid=' + encodeURIComponent( snapshot.data.uuid ); + history.replaceState( {}, document.title, customizeUrl ); + } + + api.state( 'snapshot-saved' ).set( true ); + if ( 'pending' === data.status ) { + api.state( 'snapshot-submitted' ).set( true ); + } + snapshot.resetSavedStateQuietly(); + + // Trigger an event for plugins to use. + api.trigger( 'customize-snapshots-update', { + previewUrl: url, + customizeUrl: customizeUrl, + uuid: snapshot.data.uuid, + response: response + } ); + } ); + + request.fail( function( response ) { + var id = 'snapshot-dialog-error', + snapshotDialogShareError = wp.template( id ), + messages = snapshot.data.i18n.errorMsg, + invalidityCount = 0, + dialogElement; + + if ( response.setting_validities ) { + invalidityCount = _.size( response.setting_validities, function( validity ) { + return true !== validity; + } ); + } + + /* + * Short-circuit if there are setting validation errors, since the error messages + * will be displayed with the controls themselves. Eventually, once we have + * a global notification area in the Customizer, we can eliminate this + * short-circuit and instead display the messages in there. + * See https://core.trac.wordpress.org/ticket/35210 + */ + if ( invalidityCount > 0 ) { + return; + } + + if ( response.errors ) { + messages += ' ' + _.pluck( response.errors, 'message' ).join( ' ' ); + } + + // Insert the snapshot dialog error template. + dialogElement = $( '#' + id ); + if ( ! dialogElement.length ) { + dialogElement = $( snapshotDialogShareError( { + title: snapshot.data.i18n.errorTitle, + message: messages + } ) ); + $( 'body' ).append( dialogElement ); + } + + // Open the dialog. + dialogElement.dialog( { + autoOpen: true, + modal: true + } ); + } ); + + return request; + }, + + /** + * Amend the preview query so we can update the snapshot during `customize_save`. + * + * @return {void} + */ + extendPreviewerQuery: function extendPreviewerQuery() { + var snapshot = this, originalQuery = api.previewer.query; + + api.previewer.query = function() { + var retval = originalQuery.apply( this, arguments ); + if ( api.state( 'snapshot-exists' ).get() ) { + retval.customize_snapshot_uuid = snapshot.data.uuid; + if ( snapshot.snapshotTitle && snapshot.snapshotTitle.val() ) { + retval.title = snapshot.snapshotTitle.val(); + } + } + return retval; + }; + }, + + /** + * Create the snapshot buttons. + * + * @return {void} + */ + addButtons: function addButtons() { + var snapshot = this, + header = $( '#customize-header-actions' ), + templateData = {}, setPreviewLinkHref; + + snapshot.publishButton = header.find( '#save' ); + snapshot.spinner = header.find( '.spinner' ); + snapshot.dirtyScheduleDate = new api.Value(); + + // Save/update button. + if ( api.state( 'snapshot-exists' ).get() ) { + if ( 'future' === snapshot.data.postStatus ) { + templateData.buttonText = snapshot.data.i18n.scheduleButton; + } else { + templateData.buttonText = snapshot.data.i18n.updateButton; + } + } else { + templateData.buttonText = snapshot.data.i18n.saveButton; + } + + snapshot.snapshotButton = $( $.trim( wp.template( 'snapshot-save' )( templateData ) ) ); + + if ( ! snapshot.data.currentUserCanPublish ) { + snapshot.snapshotButton.attr( 'title', api.state( 'snapshot-exists' ).get() ? snapshot.data.i18n.permsMsg.update : snapshot.data.i18n.permsMsg.save ); + } + snapshot.snapshotButton.prop( 'disabled', true ); + + snapshot.snapshotButton.on( 'click', function( event ) { + var status; + event.preventDefault(); + status = snapshot.isFutureDate() ? 'future' : 'draft'; + + snapshot.snapshotButton.prop( 'disabled', true ); + snapshot.updateSnapshot( status ).done( function() { + snapshot.snapshotButton.prop( 'disabled', true ); + } ).fail( function() { + snapshot.snapshotButton.prop( 'disabled', false ); + } ); + } ); + + snapshot.snapshotButton.insertAfter( snapshot.publishButton ); + + // Preview link. + snapshot.previewLink = $( $.trim( wp.template( 'snapshot-preview-link' )() ) ); + snapshot.previewLink.toggle( api.state( 'snapshot-saved' ).get() ); + snapshot.previewLink.attr( 'target', snapshot.data.uuid ); + setPreviewLinkHref = _.debounce( function() { + if ( api.state( 'snapshot-exists' ).get() ) { + snapshot.previewLink.attr( 'href', snapshot.getSnapshotFrontendPreviewUrl() ); + } else { + snapshot.previewLink.attr( 'href', snapshot.frontendPreviewUrl.get() ); + } + } ); + snapshot.frontendPreviewUrl.bind( setPreviewLinkHref ); + setPreviewLinkHref(); + api.state.bind( 'change', setPreviewLinkHref ); + api.bind( 'saved', setPreviewLinkHref ); + snapshot.snapshotButton.after( snapshot.previewLink ); + api.state( 'snapshot-saved' ).bind( function( saved ) { + snapshot.previewLink.toggle( saved ); + } ); + + // Edit button. + snapshot.snapshotExpandButton = $( $.trim( wp.template( 'snapshot-expand-button' )( {} ) ) ); + snapshot.snapshotExpandButton.insertAfter( snapshot.snapshotButton ); + + if ( ! snapshot.data.editLink ) { + snapshot.snapshotExpandButton.hide(); + } + + api.state( 'change', function() { + snapshot.snapshotExpandButton.toggle( api.state( 'snapshot-saved' ).get() && api.state( 'snapshot-exists' ).get() ); + } ); + + api.state( 'snapshot-exists' ).bind( function( exist ) { + snapshot.snapshotExpandButton.toggle( exist ); + } ); + + api.state( 'snapshot-saved' ).bind( function( saved ) { + snapshot.snapshotButton.prop( 'disabled', saved ); + } ); + + api.state( 'saved' ).bind( function( saved ) { + if ( saved ) { + snapshot.snapshotButton.prop( 'disabled', true ); + } + } ); + api.bind( 'change', function() { + snapshot.snapshotButton.prop( 'disabled', false ); + } ); + + api.state( 'snapshot-exists' ).bind( function( exists ) { + var buttonText, permsMsg; + if ( exists ) { + buttonText = snapshot.data.i18n.updateButton; + permsMsg = snapshot.data.i18n.permsMsg.update; + } else { + buttonText = snapshot.data.i18n.saveButton; + permsMsg = snapshot.data.i18n.permsMsg.save; + } + + snapshot.snapshotButton.text( buttonText ); + if ( ! snapshot.data.currentUserCanPublish ) { + snapshot.snapshotButton.attr( 'title', permsMsg ); + } + } ); + + snapshot.editControlSettings.bind( function() { + snapshot.snapshotButton.prop( 'disabled', false ); + snapshot.updateButtonText(); + } ); + snapshot.dirtyScheduleDate.bind( function( dirty ) { + var date; + if ( dirty ) { + date = snapshot.getDateFromInputs(); + if ( ! date || ! snapshot.data.currentUserCanPublish ) { + return; + } + snapshot.snapshotButton.text( snapshot.data.i18n.scheduleButton ); + } else { + snapshot.updateButtonText(); + } + } ); + + // Submit for review button. + if ( ! snapshot.data.currentUserCanPublish ) { + snapshot.addSubmitButton(); + } + + header.addClass( 'button-added' ); + }, + + /** + * Silently update the saved state to be true without triggering the + * changed event so that the AYS beforeunload dialog won't appear + * if no settings have been changed after saving a snapshot. Note + * that it would be better if jQuery's callbacks allowed them to + * disabled and then re-enabled later, for example: + * wp.customize.state.topics.change.disable(); + * wp.customize.state( 'saved' ).set( true ); + * wp.customize.state.topics.change.enable(); + * But unfortunately there is no such enable method. + * + * @return {void} + */ + resetSavedStateQuietly: function resetSavedStateQuietly() { + api.state( 'saved' )._value = true; + }, + + /** + * Toggles date notification. + * + * @return {void}. + */ + toggleDateNotification: function showDateNotification() { + var snapshot = this; + if ( ! _.isEmpty( snapshot.dateNotification ) ) { + snapshot.dateNotification.toggle( ! snapshot.isFutureDate() ); + } + }, + + /** + * Overrides the autoSaveEditBox method used in api.Snapshots + * because we do not auto save in < 4.7. + * + * @inheritdoc + */ + autoSaveEditBox: function autoSaveEditor() { + + }, + + /** + * Renders snapshot schedule and handles it's events. + * + * @returns {void} + */ + editSnapshotUI: function editSnapshotUI() { + var snapshot = this; + api.Snapshots.prototype.editSnapshotUI.call( snapshot ); + + api.state( 'saved' ).bind( function( saved ) { + if ( saved && ! _.isEmpty( snapshot.editContainer ) ) { + snapshot.data.dirty = false; + snapshot.data.publishDate = snapshot.getCurrentTime(); + snapshot.snapshotEditContainerDisplayed.set( false ); + snapshot.updateSnapshotEditControls(); + } + } ); + }, + + /** + * Updates snapshot schedule with `snapshot.data`. + * + * @return {void} + */ + updateSnapshotEditControls: function updateSnapshotEditControls() { + var snapshot = this, parsed, + sliceBegin = 0, + sliceEnd = -2; + + if ( _.isEmpty( snapshot.editContainer ) ) { + return; + } + + if ( snapshot.data.currentUserCanPublish ) { + if ( '0000-00-00 00:00:00' === snapshot.data.publishDate ) { + snapshot.data.publishDate = snapshot.getCurrentTime(); + } + + // Normalize date with seconds removed. + snapshot.data.publishDate = snapshot.data.publishDate.slice( sliceBegin, sliceEnd ) + '00'; + parsed = snapshot.parseDateTime( snapshot.data.publishDate ); + + // Update date controls. + snapshot.schedule.inputs.each( function() { + var input = $( this ), + fieldName = input.data( 'date-input' ); + + $( this ).val( parsed[fieldName] ); + } ); + } + + snapshot.editContainer.find( 'a.snapshot-edit-link' ) + .attr( 'href', snapshot.data.editLink ) + .show(); + if ( ! _.isEmpty( snapshot.data.title ) ) { + snapshot.snapshotTitle.val( snapshot.data.title ); + } + snapshot.populateSetting(); + }, + + /** + * Populate setting value from the inputs. + * + * @returns {void} + */ + populateSetting: function populateSetting() { + var snapshot = this, + date = snapshot.getDateFromInputs(), + scheduled, isDirtyDate, editControlSettings; + + editControlSettings = _.extend( {}, snapshot.editControlSettings.get() ); + + if ( ! date || ! snapshot.data.currentUserCanPublish ) { + editControlSettings.title = snapshot.snapshotTitle.val(); + snapshot.editControlSettings.set( editControlSettings ); + return; + } + + date.setSeconds( 0 ); + scheduled = snapshot.formatDate( date ) !== snapshot.data.publishDate; + + isDirtyDate = scheduled && snapshot.isFutureDate(); + snapshot.dirtyScheduleDate.set( isDirtyDate ); + + editControlSettings.title = snapshot.snapshotTitle.val(); + editControlSettings.date = snapshot.formatDate( date ); + + snapshot.editControlSettings.set( editControlSettings ); + + snapshot.updateCountdown(); + snapshot.editContainer.find( '.reset-time' ).toggle( scheduled ); + } + } ); + + api.snapshotsCompat = new api.SnapshotsCompat( _customizeSnapshotsCompatSettings ); + +} )( wp.customize, jQuery ); diff --git a/js/customize-migrate.js b/js/customize-migrate.js new file mode 100644 index 00000000..aab0bcd9 --- /dev/null +++ b/js/customize-migrate.js @@ -0,0 +1,79 @@ +/* global jQuery, wp */ +(function( $ ) { + 'use strict'; + var component = { + doingAjax: false, + postMigrationCount: 20 + }; + + /** + * Initialize js. + * + * @return {void} + */ + component.init = function() { + $( function() { + component.el = $( '#customize-snapshot-migration' ); + component.bindClick(); + component.spinner = $( '.spinner.customize-snapshot-spinner' ); + component.spinner.css( 'margin', '0' ); + } ); + }; + + /** + * Bind migrate click event. + * + * @return {void} + */ + component.bindClick = function() { + component.el.click( function() { + if ( component.doingAjax ) { + return; + } + component.spinner.css( 'visibility', 'visible' ); + component.doingAjax = true; + component.migrate( component.el.data( 'nonce' ), component.postMigrationCount ); + } ); + }; + + /** + * Initiate migrate ajax request. + * + * @param {String} nonce Nonce. + * @param {Number} limit Limit for migrate posts. + * + * @return {void} + */ + component.migrate = function( nonce, limit ) { + var request, + requestData = { + nonce: nonce, + limit: limit + }; + + request = wp.ajax.post( 'customize_snapshot_migration', requestData ); + + request.done( function( data ) { + var outerDiv = $( 'div.customize-snapshot-migration' ), delay = 100, newLimit; + if ( data.remaining_posts ) { + newLimit = data.remaining_posts > limit ? limit : data.remaining_posts; + _.delay( component.migrate, delay, nonce, newLimit ); + } else { + component.spinner.css( 'visibility', 'hidden' ); + outerDiv.removeClass( 'notice-error' ).addClass( 'notice-success' ).find( 'p' ).html( component.el.data( 'migration-success' ) ); + component.doingAjax = false; + } + } ); + + request.fail( function() { + component.spinner.css( 'visibility', 'initial' ); + component.doingAjax = false; + if ( window.console ) { + window.console.error( 'Migration ajax failed. Click notice to start it again.' ); + } + } ); + }; + + component.init(); + +})( jQuery ); diff --git a/js/customize-snapshots-admin.js b/js/customize-snapshots-admin.js new file mode 100644 index 00000000..617e4671 --- /dev/null +++ b/js/customize-snapshots-admin.js @@ -0,0 +1,114 @@ +/* global jQuery */ +/* exported CustomizeSnapshotsAdmin */ +var CustomizeSnapshotsAdmin = (function( $ ) { + 'use strict'; + var component = {}; + + component.data = { + deleteInputName: 'customize_snapshot_remove_settings[]' + }; + + /** + * Initialize component. + * + * @param {object} args Args. + * @return {void} + */ + component.init = function( args ) { + component.data = _.extend( component.data, args ); + $( function() { + component.deleteSetting(); + } ); + }; + + /** + * Handles snapshot setting delete actions. + * + * @return {void} + */ + component.deleteSetting = function() { + var $linkToRemoveOrRestore = $( '.snapshot-toggle-setting-removal' ), + linkActions = ['remove', 'restore'], + dataSlug = 'cs-action'; + + $linkToRemoveOrRestore.data( dataSlug, linkActions[0] ); + + component.isLinkSetToRemoveSetting = function( $link ) { + return linkActions[ 0 ] === component.getClickedLinkAction( $link ); + }; + + component.isLinkSetToRestoreSetting = function( $link ) { + return linkActions[ 1 ] === component.getClickedLinkAction( $link ); + }; + + component.getClickedLinkAction = function( $link ) { + return $link.data( dataSlug ); + }; + + component.hideSettingAndChangeLinkText = function( $link ) { + var $settingDisplay, settingId; + $settingDisplay = component.getSettingDisplay( $link ); + settingId = component.getSettingId( $link ); + + $link.data( dataSlug, linkActions[ 1 ] ) + .after( component.constructHiddenInputWithValue( settingId ) ); + component.changeLinkText( $link ); + $settingDisplay.removeAttr( 'open' ) + .addClass( 'snapshot-setting-removed' ); + }; + + component.getSettingDisplay = function( $link ) { + return $link.parents( 'details' ); + }; + + component.getSettingId = function( $link ) { + return $link.attr( 'id' ); + }; + + component.constructHiddenInputWithValue = function( settingId ) { + return $( '' ).attr( { + 'name': component.data.deleteInputName, + 'type': 'hidden' + } ) + .val( settingId ); + }; + + component.changeLinkText = function( $link ) { + var oldLinkText, newLinkText; + oldLinkText = $link.text(); + newLinkText = $link.data( 'text-restore' ); + + $link.data( 'text-restore', oldLinkText ) + .text( newLinkText ); + }; + + component.showSettingAndChangeLinkText = function( $link ) { + var $settingDisplay, settingId; + $settingDisplay = component.getSettingDisplay( $link ); + settingId = component.getSettingId( $link ); + + $link.data( dataSlug, linkActions[ 0 ] ); + component.changeLinkText( $link ); + component.removeHiddenInputWithValue( settingId ); + $settingDisplay.removeClass( 'snapshot-setting-removed' ); + }; + + component.removeHiddenInputWithValue = function( settingId ) { + $( 'input[name="' + component.data.deleteInputName + '"][value="' + settingId + '"]' ).remove(); + }; + + $linkToRemoveOrRestore.on( 'click', function( event ) { + var $clickedLink = $( this ); + + event.preventDefault(); + + if ( component.isLinkSetToRemoveSetting( $clickedLink ) ) { + component.hideSettingAndChangeLinkText( $clickedLink ); + } else if ( component.isLinkSetToRestoreSetting( $clickedLink ) ) { + component.showSettingAndChangeLinkText( $clickedLink ); + } + } ); + }; + + return component; +})( jQuery ); diff --git a/js/customize-snapshots.js b/js/customize-snapshots.js index c60a4d30..3b5e9fba 100644 --- a/js/customize-snapshots.js +++ b/js/customize-snapshots.js @@ -1,850 +1,1125 @@ -/* global jQuery, _customizeSnapshots */ +/* global jQuery, wp, _customizeSnapshotsSettings */ +/* eslint no-magic-numbers: [ "error", { "ignore": [0,1,-1] } ], consistent-this: [ "error", "snapshot" ] */ -( function( api, $ ) { +(function( api, $ ) { 'use strict'; - var component, escKeyCode = 27; + var escKeyCode = 27; - if ( ! api.Snapshots ) { - api.Snapshots = {}; - } + api.Snapshots = api.Class.extend( { - component = api.Snapshots; - - component.schedule = {}; - - component.data = { - action: '', - uuid: '', - editLink: '', - publishDate: '', - postStatus: '', - currentUserCanPublish: true, - initialServerDate: '', - initialServerTimestamp: 0, - initialClientTimestamp: 0, - i18n: {}, - dirty: false - }; - - if ( 'undefined' !== typeof _customizeSnapshots ) { - _.extend( component.data, _customizeSnapshots ); - } + data: { + action: '', + uuid: '', + editLink: '', + title: '', + publishDate: '', + postStatus: '', + currentUserCanPublish: true, + initialServerDate: '', + initialServerTimestamp: 0, + initialClientTimestamp: 0, + i18n: {}, + dirty: false + }, - /** - * Inject the functionality. - * - * @return {void} - */ - component.init = function() { - window._wpCustomizeControlsL10n.save = component.data.i18n.publish; - window._wpCustomizeControlsL10n.saved = component.data.i18n.published; - - // Set the initial client timestamp. - component.data.initialClientTimestamp = component.dateValueOf(); - - api.bind( 'ready', function() { - api.state.create( 'snapshot-exists', component.data.snapshotExists ); - api.state.create( 'snapshot-saved', true ); - api.state.create( 'snapshot-submitted', true ); - api.bind( 'change', function() { - api.state( 'snapshot-saved' ).set( false ); - api.state( 'snapshot-submitted' ).set( false ); - } ); - component.frontendPreviewUrl = new api.Value( api.previewer.previewUrl.get() ); - component.frontendPreviewUrl.link( api.previewer.previewUrl ); + uuidParam: 'customize_changeset_uuid', - component.extendPreviewerQuery(); - component.addButtons(); - if ( component.data.currentUserCanPublish ) { - component.addSchedule(); + initialize: function initialize( snapshotsConfig ) { + var snapshot = this; + + snapshot.schedule = {}; + + if ( _.isObject( snapshotsConfig ) ) { + _.extend( snapshot.data, snapshotsConfig ); } - $( '#snapshot-save' ).on( 'click', function( event ) { - var scheduleDate; - event.preventDefault(); - if ( ! _.isEmpty( component.schedule.container ) && component.isFutureDate() ) { - scheduleDate = component.getDateFromInputs(); - component.sendUpdateSnapshotRequest( { - status: 'future', - publish_date: component.formatDate( scheduleDate ) - } ); - } else { - component.sendUpdateSnapshotRequest( { status: 'draft' } ); + // Set the initial client timestamp. + snapshot.data.initialClientTimestamp = snapshot.dateValueOf(); + + api.bind( 'ready', function() { + api.state.create( 'snapshot-exists', false ); + api.state.create( 'snapshot-saved', true ); + api.state.create( 'snapshot-submitted', true ); + + snapshot.data.uuid = snapshot.data.uuid || api.settings.changeset.uuid; + snapshot.data.title = snapshot.data.title || snapshot.data.uuid; + + if ( api.state.has( 'changesetStatus' ) && api.state( 'changesetStatus' ).get() ) { + api.state( 'snapshot-exists' ).set( true ); } + + snapshot.editControlSettings = new api.Value( { + title: snapshot.data.title, + date: snapshot.data.publishDate + } ); + + api.bind( 'change', function() { + api.state( 'snapshot-submitted' ).set( false ); + } ); + + snapshot.frontendPreviewUrl = new api.Value( api.previewer.previewUrl.get() ); + snapshot.frontendPreviewUrl.link( api.previewer.previewUrl ); + + snapshot.addButtons(); + snapshot.editSnapshotUI(); + snapshot.prefilterAjax(); + + api.trigger( 'snapshots-ready', snapshot ); } ); - $( '#snapshot-submit' ).on( 'click', function( event ) { - event.preventDefault(); - component.sendUpdateSnapshotRequest( { status: 'pending' } ); - } ); - if ( api.state( 'snapshot-exists' ).get() ) { - api.state( 'saved' ).set( false ); - component.resetSavedStateQuietly(); + api.bind( 'save', function( request ) { + + request.fail( function( response ) { + var id = '#snapshot-dialog-error', + snapshotDialogPublishError = wp.template( 'snapshot-dialog-error' ); + + if ( response.responseText ) { + + // Insert the dialog error template. + if ( 0 === $( id ).length ) { + $( 'body' ).append( snapshotDialogPublishError( { + title: snapshot.data.i18n.publish, + message: api.state( 'snapshot-exists' ).get() ? snapshot.data.i18n.permsMsg.update : snapshot.data.i18n.permsMsg.save + } ) ); + } + + snapshot.spinner.removeClass( 'is-active' ); + + $( id ).dialog( { + autoOpen: true, + modal: true + } ); + } + } ); + + return request; + } ); + }, + + /** + * Update snapshot. + * + * @param {string} status post status. + * @returns {jQuery.promise} Request or promise. + */ + updateSnapshot: function updateSnapshot( status ) { + var snapshot = this, inputDate, + deferred = new $.Deferred(), + request, + requestData = { + status: status + }; + + if ( snapshot.statusButton && snapshot.statusButton.needConfirm ) { + snapshot.statusButton.disbleButton.set( false ); + snapshot.statusButton.updateButtonText( 'confirm-text' ); + snapshot.statusButton.needConfirm = false; + return deferred.promise(); } - api.trigger( 'snapshots-ready', component ); - } ); + if ( snapshot.snapshotTitle && snapshot.snapshotTitle.val() && 'publish' !== status ) { + requestData.title = snapshot.editControlSettings.get().title; + } - api.bind( 'save', function( request ) { + if ( ! _.isEmpty( snapshot.editContainer ) && snapshot.isFutureDate() && 'publish' !== status ) { + inputDate = snapshot.getDateFromInputs(); + requestData.date = snapshot.formatDate( inputDate ); + } - // Make sure that saved state is false so that Published button behaves as expected. - api.state( 'saved' ).set( false ); + if ( 'future' === status ) { + if ( requestData.date ) { + request = snapshot.sendUpdateSnapshotRequest( requestData ); + } + } else { + request = snapshot.sendUpdateSnapshotRequest( requestData ); + } - request.fail( function( response ) { - var id = 'snapshot-dialog-error', - snapshotDialogPublishError = wp.template( id ); + return request ? request : deferred.promise(); + }, + + /** + * Make the AJAX request to update/save a snapshot. + * + * @param {object} options Options. + * @param {string} options.status The post status for the snapshot. + * @return {object} request. + */ + sendUpdateSnapshotRequest: function sendUpdateSnapshotRequest( options ) { + var snapshot = this, + request, data, publishStatus; + + data = _.extend( + { + status: 'draft' + }, + options + ); + + api.state( 'snapshot-saved' ).set( false ); + snapshot.statusButton.disable( true ); + snapshot.spinner.addClass( 'is-active' ); + + request = api.previewer.save( data ); + + publishStatus = 'publish' === data.status; + + request.always( function( response ) { + snapshot.spinner.removeClass( 'is-active' ); + if ( response.edit_link ) { + snapshot.data.editLink = response.edit_link; + } + if ( response.publish_date ) { + snapshot.data.publishDate = response.publish_date; + } + if ( response.title ) { + snapshot.data.title = response.title; + } - if ( response.responseText ) { + snapshot.data.dirty = false; + } ); - // Insert the dialog error template. - if ( 0 === $( '#' + id ).length ) { - $( 'body' ).append( snapshotDialogPublishError( { - title: component.data.i18n.publish, - message: api.state( 'snapshot-exists' ).get() ? component.data.i18n.permsMsg.update : component.data.i18n.permsMsg.save - } ) ); + request.done( function( response ) { + var url = api.previewer.previewUrl(), + customizeUrl = window.location.href, + savedDelay = 400; + + /*** + * Delay because api.Posts.updateSettingsQuietly updates the settings after save, which triggers + * api change causing the publish button to get enabled again. + */ + _.delay( function() { + api.state( 'snapshot-saved' ).set( true ); + if ( 'pending' === data.status ) { + api.state( 'snapshot-submitted' ).set( true ); } + }, savedDelay ); + + api.state( 'snapshot-exists' ).set( true ); + + snapshot.statusButton.disableSelect.set( publishStatus ); + snapshot.statusButton.disbleButton.set( true ); + snapshot.snapshotExpandButton.toggle( ! publishStatus ); + snapshot.previewLink.toggle( ! publishStatus ); + + snapshot.statusButton.updateButtonText( 'alt-text' ); - $( '#customize-header-actions .spinner' ).removeClass( 'is-active' ); + // Trigger an event for plugins to use. + api.trigger( 'customize-snapshots-update', { + previewUrl: url, + customizeUrl: customizeUrl, + uuid: snapshot.data.uuid, + response: response + } ); + } ); + + request.fail( function( response ) { + var id = '#snapshot-dialog-error', + snapshotDialogShareError = wp.template( 'snapshot-dialog-error' ), + messages = snapshot.data.i18n.errorMsg, + invalidityCount = 0, + dialogElement; - // Open the dialog. - $( '#' + id ).dialog( { - autoOpen: true, - modal: true + snapshot.statusButton.disableSelect.set( false ); + + if ( response.setting_validities ) { + invalidityCount = _.size( response.setting_validities, function( validity ) { + return true !== validity; } ); } - } ); - return request; - } ); - api.bind( 'saved', function( response ) { - var url = window.location.href, - updatedUrl, - urlParts; + /* + * Short-circuit if there are setting validation errors, since the error messages + * will be displayed with the controls themselves. Eventually, once we have + * a global notification area in the Customizer, we can eliminate this + * short-circuit and instead display the messages in there. + * See https://core.trac.wordpress.org/ticket/35210 + */ + if ( invalidityCount > 0 ) { + return; + } - // Update the UUID. - if ( response.new_customize_snapshot_uuid ) { - component.data.uuid = response.new_customize_snapshot_uuid; - component.previewLink.attr( 'target', component.data.uuid ); - } - if ( response.edit_link ) { - component.data.editLink = response.edit_link; - } + if ( response.errors ) { + messages += ' ' + _.pluck( response.errors, 'message' ).join( ' ' ); + } - api.state( 'snapshot-exists' ).set( false ); + // Insert the snapshot dialog error template. + dialogElement = $( id ); + if ( ! dialogElement.length ) { + dialogElement = $( snapshotDialogShareError( { + title: snapshot.data.i18n.errorTitle, + message: messages + } ) ); + $( 'body' ).append( dialogElement ); + } + + // Open the dialog. + $( id ).dialog( { + autoOpen: true, + modal: true + } ); + } ); - // Replace the history state with an updated Customizer URL that does not include the Snapshot UUID. - urlParts = url.split( '?' ); - if ( history.replaceState && urlParts[1] ) { - updatedUrl = urlParts[0] + '?' + _.filter( urlParts[1].split( '&' ), function( queryPair ) { - return ! /^(customize_snapshot_uuid)=/.test( queryPair ); - } ).join( '&' ); - updatedUrl = updatedUrl.replace( /\?$/, '' ); - if ( updatedUrl !== url ) { - history.replaceState( {}, document.title, updatedUrl ); + return request; + }, + + /** + * Create the snapshot buttons. + * + * @return {void} + */ + addButtons: function addButtons() { + var snapshot = this, setPreviewLinkHref; + + snapshot.spinner = $( '#customize-header-actions' ).find( '.spinner' ); + snapshot.publishButton = $( '#save' ); + + snapshot.publishButton.addClass( 'hidden' ); + snapshot.statusButton = snapshot.addStatusButton(); + snapshot.statusButton.disbleButton.set( true ); + + if ( api.state( 'changesetStatus' ).get() ) { + if ( 'auto-draft' === api.state( 'changesetStatus' ).get() ) { + snapshot.statusButton.disable( false ); + } else { + snapshot.statusButton.updateButtonText( 'alt-text' ); } - } - } ); - }; - - /** - * Amend the preview query so we can update the snapshot during `customize_save`. - * - * @return {void} - */ - component.extendPreviewerQuery = function() { - var originalQuery = api.previewer.query; - - api.previewer.query = function() { - var retval = originalQuery.apply( this, arguments ); - if ( api.state( 'snapshot-exists' ).get() ) { - retval.customize_snapshot_uuid = component.data.uuid; - } - return retval; - }; - }; - - /** - * Get the preview URL with the snapshot UUID attached. - * - * @returns {string} URL. - */ - component.getSnapshotFrontendPreviewUrl = function getSnapshotFrontendPreviewUrl() { - var a = document.createElement( 'a' ); - a.href = component.frontendPreviewUrl.get(); - if ( a.search ) { - a.search += '&'; - } - a.search += 'customize_snapshot_uuid=' + component.data.uuid; - return a.href; - }; - - /** - * Create the snapshot buttons. - * - * @return {void} - */ - component.addButtons = function() { - var header = $( '#customize-header-actions' ), - publishButton = header.find( '#save' ), - snapshotButton, scheduleButton, submitButton, data, setPreviewLinkHref, snapshotButtonText; - - // Save/update button. - snapshotButton = wp.template( 'snapshot-save' ); - if ( api.state( 'snapshot-exists' ).get() ) { - if ( 'future' === component.data.postStatus ) { - snapshotButtonText = component.data.i18n.scheduleButton; } else { - snapshotButtonText = component.data.i18n.updateButton; + snapshot.statusButton.disable( true ); } - } else { - snapshotButtonText = component.data.i18n.saveButton; - } - data = { - buttonText: snapshotButtonText - }; - snapshotButton = $( $.trim( snapshotButton( data ) ) ); - if ( ! component.data.currentUserCanPublish ) { - snapshotButton.attr( 'title', api.state( 'snapshot-exists' ).get() ? component.data.i18n.permsMsg.update : component.data.i18n.permsMsg.save ); - } - snapshotButton.prop( 'disabled', true ); - snapshotButton.insertAfter( publishButton ); - // Schedule button. - if ( component.data.currentUserCanPublish ) { - scheduleButton = wp.template( 'snapshot-schedule-button' ); - scheduleButton = $( $.trim( scheduleButton( {} ) ) ); - scheduleButton.insertAfter( snapshotButton ); + // Preview link. + snapshot.previewLink = $( $.trim( wp.template( 'snapshot-preview-link' )() ) ); + snapshot.previewLink.toggle( api.state( 'snapshot-saved' ).get() ); + snapshot.previewLink.attr( 'target', snapshot.data.uuid ); + setPreviewLinkHref = _.debounce( function() { + if ( api.state( 'snapshot-exists' ).get() ) { + snapshot.previewLink.attr( 'href', snapshot.getSnapshotFrontendPreviewUrl() ); + } else { + snapshot.previewLink.attr( 'href', snapshot.frontendPreviewUrl.get() ); + } + } ); + snapshot.frontendPreviewUrl.bind( setPreviewLinkHref ); + setPreviewLinkHref(); + api.state.bind( 'change', setPreviewLinkHref ); + api.bind( 'saved', setPreviewLinkHref ); + snapshot.statusButton.container.after( snapshot.previewLink ); + api.state( 'snapshot-saved' ).bind( function( saved ) { + snapshot.previewLink.toggle( saved ); + } ); - if ( ! component.data.editLink ) { - scheduleButton.hide(); + // Edit button. + snapshot.snapshotExpandButton = $( $.trim( wp.template( 'snapshot-expand-button' )( {} ) ) ); + snapshot.statusButton.container.after( snapshot.snapshotExpandButton ); + + if ( ! snapshot.data.editLink ) { + snapshot.snapshotExpandButton.hide(); + snapshot.previewLink.hide(); } api.state( 'change', function() { - scheduleButton.toggle( api.state( 'snapshot-saved' ).get() && api.state( 'snapshot-exists' ).get() ); + snapshot.snapshotExpandButton.toggle( api.state( 'snapshot-saved' ).get() && api.state( 'snapshot-exists' ).get() ); } ); api.state( 'snapshot-exists' ).bind( function( exist ) { - scheduleButton.toggle( exist ); + snapshot.snapshotExpandButton.toggle( exist ); + snapshot.previewLink.toggle( exist ); } ); - } - api.state( 'snapshot-saved' ).bind( function( saved ) { - snapshotButton.prop( 'disabled', saved ); - } ); + api.bind( 'change', function() { + if ( api.state( 'snapshot-saved' ).get() ) { + snapshot.statusButton.disable( false ); + if ( snapshot.statusButton.button.data( 'confirm-text' ) !== snapshot.statusButton.buttonText.get() ) { + snapshot.statusButton.updateButtonText( 'button-text' ); + } + if ( snapshot.submitButton ) { + snapshot.submitButton.prop( 'disabled', false ); + } + if ( snapshot.saveButton ) { + snapshot.saveButton.prop( 'disabled', false ); + } + api.state( 'snapshot-saved' ).set( false ); + } + } ); - api.state( 'saved' ).bind( function( saved ) { - if ( saved ) { - snapshotButton.prop( 'disabled', true ); - } - } ); - api.bind( 'change', function() { - snapshotButton.prop( 'disabled', false ); - } ); - - api.state( 'snapshot-exists' ).bind( function( exists ) { - var buttonText, permsMsg; - if ( exists ) { - buttonText = component.data.i18n.updateButton; - permsMsg = component.data.i18n.permsMsg.update; - } else { - buttonText = component.data.i18n.saveButton; - permsMsg = component.data.i18n.permsMsg.save; + if ( ! snapshot.data.currentUserCanPublish ) { + snapshot.addSubmitButton(); + snapshot.addSaveButton(); } + }, - snapshotButton.text( buttonText ); - if ( ! component.data.currentUserCanPublish ) { - snapshotButton.attr( 'title', permsMsg ); - } - } ); - - // Preview link. - component.previewLink = $( $.trim( wp.template( 'snapshot-preview-link' )() ) ); - component.previewLink.toggle( api.state( 'snapshot-saved' ).get() ); - component.previewLink.attr( 'target', component.data.uuid ); - setPreviewLinkHref = _.debounce( function() { - if ( api.state( 'snapshot-exists' ).get() ) { - component.previewLink.attr( 'href', component.getSnapshotFrontendPreviewUrl() ); + /** + * Adds Submit Button when user does not have 'customize_publish' permission. + * + * @return {void} + */ + addSubmitButton: function() { + var snapshot = this, disableSubmitButton; + + disableSubmitButton = 'pending' === snapshot.data.postStatus || ! api.state( 'snapshot-exists' ).get(); + + if ( snapshot.statusButton ) { + snapshot.statusButton.container.hide(); } else { - component.previewLink.attr( 'href', component.frontendPreviewUrl.get() ); + snapshot.publishButton.hide(); } - } ); - component.frontendPreviewUrl.bind( setPreviewLinkHref ); - setPreviewLinkHref(); - api.state.bind( 'change', setPreviewLinkHref ); - api.bind( 'saved', setPreviewLinkHref ); - snapshotButton.after( component.previewLink ); - api.state( 'snapshot-saved' ).bind( function( saved ) { - component.previewLink.toggle( saved ); - } ); - - // Submit for review button. - if ( ! component.data.currentUserCanPublish ) { - publishButton.hide(); - submitButton = wp.template( 'snapshot-submit' ); - submitButton = $( $.trim( submitButton( { - buttonText: component.data.i18n.submit + + snapshot.submitButton = $( $.trim( wp.template( 'snapshot-submit' )( { + buttonText: snapshot.data.i18n.submit } ) ) ); - submitButton.prop( 'disabled', ! api.state( 'snapshot-exists' ).get() ); - submitButton.insertBefore( snapshotButton ); + + snapshot.submitButton.prop( 'disabled', disableSubmitButton ); + snapshot.submitButton.insertBefore( snapshot.publishButton ); api.state( 'snapshot-submitted' ).bind( function( submitted ) { - submitButton.prop( 'disabled', submitted ); + snapshot.submitButton.prop( 'disabled', submitted ); } ); - } - header.addClass( 'button-added' ); - }; + snapshot.submitButton.on( 'click', function( event ) { + event.preventDefault(); + snapshot.submitButton.prop( 'disabled', true ); + if ( snapshot.saveButton ) { + snapshot.saveButton.prop( 'disabled', true ); + } + snapshot.updateSnapshot( 'pending' ).fail( function() { + snapshot.submitButton.prop( 'disabled', false ); + } ); + } ); - /** - * Renders snapshot schedule and handles it's events. - * - * @returns {void} - */ - component.addSchedule = function addSchedule() { - var sliceBegin = 0, - sliceEnd = -2, - scheduleButton = $( '#snapshot-schedule-button' ); + snapshot.editControlSettings.bind( function() { + if ( api.state( 'snapshot-saved' ).get() ) { + snapshot.submitButton.prop( 'disabled', false ); + } + } ); + }, - component.scheduleContainerDisplayed = new api.Value(); + /** + * Adds Save Button when user does not have 'customize_publish' permission. + * + * @return {void} + */ + addSaveButton: function() { + var snapshot = this, disableSaveButton, isSaved; - if ( ! component.data.currentUserCanPublish ) { - return; - } + isSaved = _.contains( [ 'future', 'pending', 'draft' ], api.state( 'changesetStatus' ).get() ); + disableSaveButton = isSaved || ! api.state( 'snapshot-exists' ).get(); + + snapshot.saveButton = $( $.trim( wp.template( 'snapshot-save' )( { + buttonText: isSaved ? snapshot.data.i18n.updateButton : snapshot.data.i18n.saveButton + } ) ) ); + + snapshot.saveButton.prop( 'disabled', disableSaveButton ); + snapshot.saveButton.insertBefore( snapshot.publishButton ); + + api.state( 'snapshot-submitted' ).bind( function( submitted ) { + if ( submitted ) { + snapshot.saveButton.prop( 'disabled', true ); + } + } ); + + snapshot.saveButton.on( 'click', function( event ) { + event.preventDefault(); + snapshot.saveButton.prop( 'disabled', true ); + snapshot.submitButton.prop( 'disabled', true ); + snapshot.updateSnapshot( 'draft' ).done( function() { + snapshot.saveButton.prop( 'disabled', true ); + snapshot.submitButton.prop( 'disabled', false ); + snapshot.saveButton.text( snapshot.data.i18n.updateButton ); + } ).fail( function() { + snapshot.saveButton.prop( 'disabled', false ); + snapshot.submitButton.prop( 'disabled', false ); + } ); + } ); + + snapshot.editControlSettings.bind( function() { + if ( api.state( 'snapshot-saved' ).get() ) { + snapshot.saveButton.prop( 'disabled', false ); + } + } ); + }, + + /** + * Renders snapshot schedule and handles it's events. + * + * @returns {void} + */ + editSnapshotUI: function editSnapshotUI() { + var snapshot = this, sliceBegin = 0, + sliceEnd = -2, updateUI; + + snapshot.snapshotEditContainerDisplayed = new api.Value( false ); + + updateUI = function() { + snapshot.populateSetting(); + }; + + // Inject the UI. + if ( _.isEmpty( snapshot.editContainer ) ) { + if ( '0000-00-00 00:00:00' === snapshot.data.publishDate ) { + snapshot.data.publishDate = snapshot.getCurrentTime(); + } - // Inject the UI. - if ( _.isEmpty( component.schedule.container ) ) { - if ( '0000-00-00 00:00:00' === component.data.publishDate ) { - component.data.publishDate = component.getCurrentTime(); + // Normalize date with secs set as zeros removed. + snapshot.data.publishDate = snapshot.data.publishDate.slice( sliceBegin, sliceEnd ) + '00'; + + // Extend the snapshots data object and add the parsed datetime strings. + snapshot.data = _.extend( snapshot.data, snapshot.parseDateTime( snapshot.data.publishDate ) ); + + // Add the template to the DOM. + snapshot.editContainer = $( $.trim( wp.template( 'snapshot-edit-container' )( snapshot.data ) ) ); + snapshot.editContainer.hide().appendTo( $( '#customize-header-actions' ) ); + snapshot.dateNotification = snapshot.editContainer.find( '.snapshot-future-date-notification' ); + snapshot.countdown = snapshot.editContainer.find( '.snapshot-scheduled-countdown' ); + + if ( snapshot.data.currentUserCanPublish ) { + + // Store the date inputs. + snapshot.schedule.inputs = snapshot.editContainer.find( '.date-input' ); + + snapshot.schedule.inputs.on( 'input', updateUI ); + + snapshot.schedule.inputs.on( 'blur', function() { + snapshot.populateInputs(); + updateUI(); + } ); + + snapshot.updateCountdown(); + + snapshot.editContainer.find( '.reset-time a' ).on( 'click', function( event ) { + event.preventDefault(); + snapshot.updateSnapshotEditControls(); + } ); + } + + if ( snapshot.statusButton && 'future' !== snapshot.statusButton.value.get() ) { + snapshot.countdown.hide(); + } + + snapshot.snapshotTitle = snapshot.editContainer.find( '#snapshot-title' ); + snapshot.snapshotTitle.on( 'input', updateUI ); } - // Normalize date with secs set as zeros removed. - component.data.publishDate = component.data.publishDate.slice( sliceBegin, sliceEnd ) + '00'; + // Set up toggling of the schedule container. + snapshot.snapshotEditContainerDisplayed.bind( function( isDisplayed ) { + if ( isDisplayed ) { + snapshot.editContainer.stop().slideDown( 'fast' ).attr( 'aria-expanded', 'true' ); + snapshot.snapshotExpandButton.attr( 'aria-pressed', 'true' ); + snapshot.snapshotExpandButton.prop( 'title', snapshot.data.i18n.collapseSnapshotScheduling ); + snapshot.toggleDateNotification(); + } else { + snapshot.editContainer.stop().slideUp( 'fast' ).attr( 'aria-expanded', 'false' ); + snapshot.snapshotExpandButton.attr( 'aria-pressed', 'false' ); + snapshot.snapshotExpandButton.prop( 'title', snapshot.data.i18n.expandSnapshotScheduling ); + } + } ); - // Extend the components data object and add the parsed datetime strings. - component.data = _.extend( component.data, component.parseDateTime( component.data.publishDate ) ); + snapshot.editControlSettings.bind( function() { + snapshot.toggleDateNotification(); + } ); - // Add the template to the DOM. - component.schedule.container = $( $.trim( wp.template( 'snapshot-schedule' )( component.data ) ) ); - component.schedule.container.hide().appendTo( $( '#customize-header-actions' ) ); + // Toggle schedule container when clicking the button. + snapshot.snapshotExpandButton.on( 'click', function( event ) { + event.preventDefault(); + snapshot.snapshotEditContainerDisplayed.set( ! snapshot.snapshotEditContainerDisplayed.get() ); + } ); - // Store the date inputs. - component.schedule.inputs = component.schedule.container.find( '.date-input' ); + // Collapse the schedule container when Esc is pressed while the button is focused. + snapshot.snapshotExpandButton.on( 'keydown', function( event ) { + if ( escKeyCode === event.which && snapshot.snapshotEditContainerDisplayed.get() ) { + event.stopPropagation(); + event.preventDefault(); + snapshot.snapshotEditContainerDisplayed.set( false ); + } + } ); - component.schedule.inputs.on( 'input', function() { - component.populateSetting(); + // Collapse the schedule container when Esc is pressed inside of the schedule container. + snapshot.editContainer.on( 'keydown', function( event ) { + if ( escKeyCode === event.which && snapshot.snapshotEditContainerDisplayed.get() ) { + event.stopPropagation(); + event.preventDefault(); + snapshot.snapshotEditContainerDisplayed.set( false ); + snapshot.snapshotExpandButton.focus(); + } } ); - component.schedule.inputs.on( 'blur', function() { - component.populateInputs(); - component.populateSetting(); + // Collapse the schedule container interacting outside the schedule container. + $( 'body' ).on( 'mousedown', function( event ) { + var isDisplayed = snapshot.snapshotEditContainerDisplayed.get(), + isTargetEditContainer = snapshot.editContainer.is( event.target ) || 0 !== snapshot.editContainer.has( event.target ).length, + isTargetExpandButton = snapshot.snapshotExpandButton.is( event.target ); + + if ( isDisplayed && ! isTargetEditContainer && ! isTargetExpandButton ) { + snapshot.snapshotEditContainerDisplayed.set( false ); + } } ); - component.updateCountdown(); + snapshot.snapshotEditContainerDisplayed.set( false ); - component.schedule.container.find( '.reset-time a' ).on( 'click', function( event ) { - event.preventDefault(); - component.updateSchedule(); + api.state( 'snapshot-saved' ).bind( function( saved ) { + if ( saved ) { + snapshot.updateSnapshotEditControls(); + } } ); - } - // Set up toggling of the schedule container. - component.scheduleContainerDisplayed.bind( function( isDisplayed ) { - if ( isDisplayed ) { - component.schedule.container.stop().slideDown( 'fast' ).attr( 'aria-expanded', 'true' ); - scheduleButton.attr( 'aria-pressed', 'true' ); - scheduleButton.prop( 'title', component.data.i18n.collapseSnapshotScheduling ); - } else { - component.schedule.container.stop().slideUp( 'fast' ).attr( 'aria-expanded', 'false' ); - scheduleButton.attr( 'aria-pressed', 'false' ); - scheduleButton.prop( 'title', component.data.i18n.expandSnapshotScheduling ); - } - } ); - - // Toggle schedule container when clicking the button. - scheduleButton.on( 'click', function( event ) { - event.preventDefault(); - component.scheduleContainerDisplayed.set( ! component.scheduleContainerDisplayed.get() ); - } ); - - // Collapse the schedule container when Esc is pressed while the button is focused. - scheduleButton.on( 'keydown', function( event ) { - if ( escKeyCode === event.which && component.scheduleContainerDisplayed.get() ) { - event.stopPropagation(); - event.preventDefault(); - component.scheduleContainerDisplayed.set( false ); - } - }); + api.bind( 'change', function() { + snapshot.data.dirty = true; + snapshot.editContainer.find( 'a.snapshot-edit-link' ).hide(); + } ); - // Collapse the schedule container when Esc is pressed inside of the schedule container. - component.schedule.container.on( 'keydown', function( event ) { - if ( escKeyCode === event.which && component.scheduleContainerDisplayed.get() ) { - event.stopPropagation(); - event.preventDefault(); - component.scheduleContainerDisplayed.set( false ); - scheduleButton.focus(); - } - }); + api.state( 'snapshot-exists' ).bind( function( exists ) { + if ( exists && ! _.isEmpty( snapshot.editContainer ) ) { + snapshot.updateSnapshotEditControls(); + } else { + snapshot.snapshotEditContainerDisplayed.set( false ); + } + } ); - // Collapse the schedule container interacting outside the schedule container. - $( 'body' ).on( 'mousedown', function( event ) { - if ( component.scheduleContainerDisplayed.get() && ! $.contains( component.schedule.container[0], event.target ) && ! scheduleButton.is( event.target ) ) { - component.scheduleContainerDisplayed.set( false ); + if ( snapshot.statusButton ) { + snapshot.updateSnapshotEditControls(); } - }); - component.scheduleContainerDisplayed.set( false ); + snapshot.autoSaveEditBox(); + }, + + /** + * Auto save the edit box values. + * + * @return {void} + */ + autoSaveEditBox: function() { + var snapshot = this, update, + delay = 2000, status, isValidChangesetStatus; + + snapshot.updatePending = false; + snapshot.dirtyEditControlValues = false; + + update = _.debounce( function() { + status = snapshot.statusButton.value.get(); + if ( 'publish' === status || ! snapshot.isFutureDate() ) { + snapshot.updatePending = false; + return; + } + snapshot.updatePending = true; + snapshot.updateSnapshot( status ).done( function() { + snapshot.updatePending = snapshot.dirtyEditControlValues; + if ( ! snapshot.updatePending ) { + snapshot.updateSnapshotEditControls(); + } else if ( snapshot.dirtyEditControlValues ) { + update(); + } + snapshot.dirtyEditControlValues = false; + } ).fail( function() { + snapshot.updatePending = false; + } ); + }, delay ); + + snapshot.editControlSettings.bind( function() { + if ( snapshot.isFutureDate() ) { + if ( ! snapshot.updatePending ) { + update(); + } else { + snapshot.dirtyEditControlValues = true; + } + } + } ); + + $( window ).on( 'beforeunload.customize-confirm', function() { + if ( snapshot.updatePending || snapshot.dirtyEditControlValues ) { + return snapshot.data.i18n.aysMsg; + } + return undefined; + } ); + + isValidChangesetStatus = function() { + return _.contains( [ 'future', 'pending', 'draft' ], api.state( 'changesetStatus' ).get() ); + }; + + // @todo Show loader and disable button while auto saving. + api.bind( 'changeset-save', function() { + if ( isValidChangesetStatus() ) { + snapshot.updatePending = true; + snapshot.extendPreviewerQuery(); + } + } ); - api.state( 'snapshot-saved' ).bind( function( saved ) { - if ( saved ) { - component.updateSchedule(); + api.bind( 'changeset-saved', function() { + api.state( 'saved' ).set( true ); // Suppress the AYS dialog. + + if ( isValidChangesetStatus() ) { + snapshot.updatePending = false; + } + }); + }, + + /** + * Toggles date notification. + * + * @return {void}. + */ + toggleDateNotification: function showDateNotification() { + var snapshot = this; + if ( ! _.isEmpty( snapshot.dateNotification ) ) { + snapshot.dateNotification.toggle( ! snapshot.isFutureDate() ); + + if ( 'future' === snapshot.statusButton.value.get() ) { + snapshot.statusButton.disbleButton.set( ! snapshot.isFutureDate() ); + } } - } ); - - api.bind( 'change', function() { - component.data.dirty = true; - component.schedule.container.find( 'a.snapshot-edit-link' ).hide(); - } ); - - api.state( 'saved' ).bind( function( saved ) { - if ( saved && ! _.isEmpty( component.schedule.container ) ) { - component.data.dirty = false; - component.data.publishDate = component.getCurrentTime(); - component.scheduleContainerDisplayed.set( false ); - component.updateSchedule(); + }, + + /** + * Get the preview URL with the snapshot UUID attached. + * + * @returns {string} URL. + */ + getSnapshotFrontendPreviewUrl: function getSnapshotFrontendPreviewUrl() { + var snapshot = this, a = document.createElement( 'a' ); + a.href = snapshot.frontendPreviewUrl.get(); + if ( a.search ) { + a.search += '&'; } - } ); - - api.state( 'snapshot-exists' ).bind( function( exists ) { - if ( exists && ! _.isEmpty( component.schedule.container ) ) { - component.updateSchedule(); - } else { - component.scheduleContainerDisplayed.set( false ); + a.search += snapshot.uuidParam + '=' + snapshot.data.uuid; + return a.href; + }, + + /** + * Updates snapshot schedule with `snapshot.data`. + * + * @return {void} + */ + updateSnapshotEditControls: function updateSnapshotEditControls() { + var snapshot = this, + parsed, + status, + sliceBegin = 0, + sliceEnd = -2; + + if ( _.isEmpty( snapshot.editContainer ) ) { + return; } - } ); - }; - - /** - * Updates snapshot schedule with `component.data`. - * - * @return {void} - */ - component.updateSchedule = function updateSchedule() { - var parsed, - sliceBegin = 0, - sliceEnd = -2; - - if ( _.isEmpty( component.schedule.container ) || ! component.data.currentUserCanPublish ) { - return; - } - if ( '0000-00-00 00:00:00' === component.data.publishDate ) { - component.data.publishDate = component.getCurrentTime(); - } + status = api.state( 'changesetStatus' ).get(); - // Normalize date with seconds removed. - component.data.publishDate = component.data.publishDate.slice( sliceBegin, sliceEnd ) + '00'; - - // Update date controls. - component.schedule.container.find( 'a.snapshot-edit-link' ) - .attr( 'href', component.data.editLink ) - .show(); - parsed = component.parseDateTime( component.data.publishDate ); - - component.schedule.inputs.each( function() { - var input = $( this ), - fieldName = input.data( 'date-input' ); - - $( this ).val( parsed[fieldName] ); - } ); - - component.populateSetting(); - }; - - /** - * Update the scheduled countdown text. - * - * Hides countdown if post_status is not already future. - * Toggles the countdown if there is no remaining time. - * - * @returns {boolean} True if date inputs are valid. - */ - component.updateCountdown = function updateCountdown() { - var countdown = component.schedule.container.find( '.snapshot-scheduled-countdown' ), - countdownTemplate = wp.template( 'snapshot-scheduled-countdown' ), - dateTimeFromInput = component.getDateFromInputs(), - millisecondsDivider = 1000, - remainingTime; - - if ( ! dateTimeFromInput ) { - return false; - } + if ( snapshot.data.currentUserCanPublish ) { + if ( '0000-00-00 00:00:00' === snapshot.data.publishDate || ! status || 'auto-draft' === status ) { + snapshot.data.publishDate = snapshot.getCurrentTime(); + } - remainingTime = dateTimeFromInput.valueOf(); - remainingTime -= component.dateValueOf( component.getCurrentTime() ); - remainingTime = Math.ceil( remainingTime / millisecondsDivider ); - - if ( 0 < remainingTime ) { - countdown.text( countdownTemplate( { - remainingTime: remainingTime - } ) ); - countdown.show(); - } else { - countdown.hide(); - } + // Normalize date with seconds removed. + snapshot.data.publishDate = snapshot.data.publishDate.slice( sliceBegin, sliceEnd ) + '00'; + parsed = snapshot.parseDateTime( snapshot.data.publishDate ); - return true; - }; - - /** - * Silently update the saved state to be true without triggering the - * changed event so that the AYS beforeunload dialog won't appear - * if no settings have been changed after saving a snapshot. Note - * that it would be better if jQuery's callbacks allowed them to - * disabled and then re-enabled later, for example: - * wp.customize.state.topics.change.disable(); - * wp.customize.state( 'saved' ).set( true ); - * wp.customize.state.topics.change.enable(); - * But unfortunately there is no such enable method. - * - * @return {void} - */ - component.resetSavedStateQuietly = function() { - api.state( 'saved' )._value = true; - }; - - /** - * Make the AJAX request to update/save a snapshot. - * - * @param {object} options Options. - * @param {string} options.status The post status for the snapshot. - * @return {void} - */ - component.sendUpdateSnapshotRequest = function( options ) { - var spinner = $( '#customize-header-actions .spinner' ), - request, data; - - data = _.extend( - { - status: 'draft' - }, - api.previewer.query(), - options, - { - nonce: api.settings.nonce.snapshot, - customize_snapshot_uuid: component.data.uuid - } - ); - request = wp.ajax.post( 'customize_update_snapshot', data ); - - spinner.addClass( 'is-active' ); - request.always( function( response ) { - spinner.removeClass( 'is-active' ); - if ( response.edit_link ) { - component.data.editLink = response.edit_link; + // Update date controls. + snapshot.schedule.inputs.each( function() { + var input = $( this ), + fieldName = input.data( 'date-input' ); + $( this ).val( parsed[fieldName] ); + } ); } - if ( response.snapshot_publish_date ) { - component.data.publishDate = response.snapshot_publish_date; + + snapshot.editContainer.find( 'a.snapshot-edit-link' ) + .attr( 'href', snapshot.data.editLink ) + .show(); + if ( ! _.isEmpty( snapshot.data.title ) ) { + snapshot.snapshotTitle.val( snapshot.data.title ); } - component.updateSchedule(); - component.data.dirty = false; - - // @todo Remove privateness from _handleSettingValidities in Core. - if ( api._handleSettingValidities && response.setting_validities ) { - api._handleSettingValidities( { - settingValidities: response.setting_validities, - focusInvalidControl: true - } ); + snapshot.populateSetting(); + }, + + /** + * Update the scheduled countdown text. + * + * Hides countdown if post_status is not already future. + * Toggles the countdown if there is no remaining time. + * + * @returns {boolean} True if date inputs are valid. + */ + updateCountdown: function updateCountdown() { + var snapshot = this, + countdownTemplate = wp.template( 'snapshot-scheduled-countdown' ), + dateTimeFromInput = snapshot.getDateFromInputs(), + millisecondsDivider = 1000, + remainingTime; + + if ( ! dateTimeFromInput ) { + return false; } - } ); - - request.done( function() { - var url = api.previewer.previewUrl(), - regex = new RegExp( '([?&])customize_snapshot_uuid=.*?(&|$)', 'i' ), - notFound = -1, - separator = url.indexOf( '?' ) !== notFound ? '&' : '?', - customizeUrl = window.location.href, - customizeSeparator = customizeUrl.indexOf( '?' ) !== notFound ? '&' : '?'; - - if ( url.match( regex ) ) { - url = url.replace( regex, '$1customize_snapshot_uuid=' + encodeURIComponent( component.data.uuid ) + '$2' ); + + remainingTime = dateTimeFromInput.valueOf(); + remainingTime -= snapshot.dateValueOf( snapshot.getCurrentTime() ); + remainingTime = Math.ceil( remainingTime / millisecondsDivider ); + + if ( 0 < remainingTime ) { + snapshot.countdown.text( countdownTemplate( { + remainingTime: remainingTime + } ) ); + snapshot.countdown.show(); } else { - url = url + separator + 'customize_snapshot_uuid=' + encodeURIComponent( component.data.uuid ); + snapshot.countdown.hide(); + } + + return true; + }, + + /** + * Get date from inputs. + * + * @returns {Date|null} Date created from inputs or null if invalid date. + */ + getDateFromInputs: function getDateFromInputs() { + var snapshot = this, + template = snapshot.editContainer, + monthOffset = 1, + date; + + date = new Date( + parseInt( template.find( '[data-date-input="year"]' ).val(), 10 ), + parseInt( template.find( '[data-date-input="month"]' ).val(), 10 ) - monthOffset, + parseInt( template.find( '[data-date-input="day"]' ).val(), 10 ), + parseInt( template.find( '[data-date-input="hour"]' ).val(), 10 ), + parseInt( template.find( '[data-date-input="minute"]' ).val(), 10 ) + ); + + if ( isNaN( date.valueOf() ) ) { + return null; } - // Change the save button text to update. - api.state( 'snapshot-exists' ).set( true ); + date.setSeconds( 0 ); + + return date; + }, - // Replace the history state with an updated Customizer URL that includes the Snapshot UUID. - if ( history.replaceState && ! customizeUrl.match( regex ) ) { - customizeUrl += customizeSeparator + 'customize_snapshot_uuid=' + encodeURIComponent( component.data.uuid ); - history.replaceState( {}, document.title, customizeUrl ); + /** + * Parse datetime string. + * + * @param {string} datetime Date/Time string. + * @returns {object|null} Returns object containing date components or null if parse error. + */ + parseDateTime: function parseDateTime( datetime ) { + var matches = datetime.match( /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$/ ); + + if ( ! matches ) { + return null; } - api.state( 'snapshot-saved' ).set( true ); - if ( 'pending' === data.status ) { - api.state( 'snapshot-submitted' ).set( true ); + matches.shift(); + + return { + year: matches.shift(), + month: matches.shift(), + day: matches.shift(), + hour: matches.shift(), + minute: matches.shift(), + second: matches.shift() + }; + }, + + /** + * Format a Date Object. Returns 'Y-m-d H:i:s' format. + * + * @props http://stackoverflow.com/questions/10073699/pad-a-number-with-leading-zeros-in-javascript#comment33639551_10073699 + * + * @param {Date} date A Date object. + * @returns {string} A formatted date String. + */ + formatDate: function formatDate( date ) { + var formattedDate, + yearLength = 4, + nonYearLength = 2, + monthOffset = 1; + + formattedDate = ( '0000' + date.getFullYear() ).substr( -yearLength, yearLength ); + formattedDate += '-' + ( '00' + ( date.getMonth() + monthOffset ) ).substr( -nonYearLength, nonYearLength ); + formattedDate += '-' + ( '00' + date.getDate() ).substr( -nonYearLength, nonYearLength ); + formattedDate += ' ' + ( '00' + date.getHours() ).substr( -nonYearLength, nonYearLength ); + formattedDate += ':' + ( '00' + date.getMinutes() ).substr( -nonYearLength, nonYearLength ); + formattedDate += ':' + ( '00' + date.getSeconds() ).substr( -nonYearLength, nonYearLength ); + + return formattedDate; + }, + + /** + * Populate inputs from the setting value, if none of them are currently focused. + * + * @returns {boolean} Whether the inputs were populated. + */ + populateInputs: function populateInputs() { + var snapshot = this, parsed; + + if ( snapshot.schedule.inputs.is( ':focus' ) || '0000-00-00 00:00:00' === snapshot.data.publishDate ) { + return false; } - component.resetSavedStateQuietly(); - - // Trigger an event for plugins to use. - api.trigger( 'customize-snapshots-update', { - previewUrl: url, - customizeUrl: customizeUrl, - uuid: component.data.uuid - } ); - } ); - - request.fail( function( response ) { - var id = 'snapshot-dialog-error', - snapshotDialogShareError = wp.template( id ), - messages = component.data.i18n.errorMsg, - invalidityCount = 0, - dialogElement; - - if ( response.setting_validities ) { - invalidityCount = _.size( response.setting_validities, function( validity ) { - return true !== validity; - } ); + + parsed = snapshot.parseDateTime( snapshot.data.publishDate ); + if ( ! parsed ) { + return false; } - /* - * Short-circuit if there are setting validation errors, since the error messages - * will be displayed with the controls themselves. Eventually, once we have - * a global notification area in the Customizer, we can eliminate this - * short-circuit and instead display the messages in there. - * See https://core.trac.wordpress.org/ticket/35210 - */ - if ( invalidityCount > 0 ) { + snapshot.schedule.inputs.each( function() { + var input = $( this ), + fieldName = input.data( 'date-input' ); + + if ( ! $( this ).is( 'select' ) && '' === $( this ).val() ) { + $( this ).val( parsed[fieldName] ); + } + } ); + return true; + }, + + /** + * Populate setting value from the inputs. + * + * @returns {void} + */ + populateSetting: function populateSetting() { + var snapshot = this, + date = snapshot.getDateFromInputs(), + scheduled, editControlSettings; + + editControlSettings = _.extend( {}, snapshot.editControlSettings.get() ); + + if ( ! date || ! snapshot.data.currentUserCanPublish ) { + editControlSettings.title = snapshot.snapshotTitle.val(); + snapshot.editControlSettings.set( editControlSettings ); return; } - if ( response.errors ) { - messages += ' ' + _.pluck( response.errors, 'message' ).join( ' ' ); - } + date.setSeconds( 0 ); + scheduled = snapshot.formatDate( date ) !== snapshot.data.publishDate; - // Insert the snapshot dialog error template. - dialogElement = $( '#' + id ); - if ( ! dialogElement.length ) { - dialogElement = $( snapshotDialogShareError( { - title: component.data.i18n.errorTitle, - message: messages - } ) ); - $( 'body' ).append( dialogElement ); - } + editControlSettings.title = snapshot.snapshotTitle.val(); + editControlSettings.date = snapshot.formatDate( date ); - // Open the dialog. - dialogElement.dialog( { - autoOpen: true, - modal: true - } ); - } ); - }; - - /** - * Get date from inputs. - * - * @returns {Date|null} Date created from inputs or null if invalid date. - */ - component.getDateFromInputs = function getDateFromInputs() { - var template = component.schedule.container, - monthOffset = 1, - date; - - date = new Date( - parseInt( template.find( '[data-date-input="year"]' ).val(), 10 ), - parseInt( template.find( '[data-date-input="month"]' ).val(), 10 ) - monthOffset, - parseInt( template.find( '[data-date-input="day"]' ).val(), 10 ), - parseInt( template.find( '[data-date-input="hour"]' ).val(), 10 ), - parseInt( template.find( '[data-date-input="minute"]' ).val(), 10 ) - ); - - if ( isNaN( date.valueOf() ) ) { - return null; - } + snapshot.editControlSettings.set( editControlSettings ); - date.setSeconds( 0 ); + if ( 'future' === snapshot.statusButton.value.get() ) { + snapshot.updateCountdown(); + } - return date; - }; + snapshot.editContainer.find( '.reset-time' ).toggle( scheduled ); + }, + + /** + * Check if the schedule date is in the future. + * + * @returns {boolean} True if future date. + */ + isFutureDate: function isFutureDate() { + var snapshot = this, + date = snapshot.getDateFromInputs(), + millisecondsDivider = 1000, + remainingTime; + + if ( ! date ) { + return false; + } - /** - * Parse datetime string. - * - * @param {string} datetime Date/Time string. - * @returns {object|null} Returns object containing date components or null if parse error. - */ - component.parseDateTime = function parseDateTime( datetime ) { - var matches = datetime.match( /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$/ ); + remainingTime = snapshot.dateValueOf( date ); + remainingTime -= snapshot.dateValueOf( snapshot.getCurrentTime() ); + remainingTime = Math.ceil( remainingTime / millisecondsDivider ); + return 0 < remainingTime; + }, + + /** + * Get current date/time in the site's timezone. + * + * Same functionality as the `current_time( 'mysql', false )` function in PHP. + * + * @returns {string} Current datetime string. + */ + getCurrentTime: function getCurrentTime() { + var snapshot = this, + currentDate = new Date( snapshot.data.initialServerDate ), + currentTimestamp = snapshot.dateValueOf(), + timestampDifferential; + + timestampDifferential = currentTimestamp - snapshot.data.initialClientTimestamp; + timestampDifferential += snapshot.data.initialClientTimestamp - snapshot.data.initialServerTimestamp; + currentDate.setTime( currentDate.getTime() + timestampDifferential ); + + return snapshot.formatDate( currentDate ); + }, + + /** + * Get the primitive value of a Date object. + * + * @param {string|Date} dateString The post status for the snapshot. + * @returns {object|string} The primitive value or date object. + */ + dateValueOf: function dateValueOf( dateString ) { + var date; + + if ( 'string' === typeof dateString ) { + date = new Date( dateString ); + } else if ( dateString instanceof Date ) { + date = dateString; + } else { + date = new Date(); + } - if ( ! matches ) { - return null; - } + return date.valueOf(); + }, + + /** + * Amend the preview query so we can update the snapshot during `changeset_save`. + * + * @return {void} + */ + extendPreviewerQuery: function extendPreviewerQuery() { + var snapshot = this, originalQuery = api.previewer.query; + + api.previewer.query = function() { + var retval = originalQuery.apply( this, arguments ); + if ( ! _.isEmpty( snapshot.editControlSettings.get() ) ) { + retval.customize_changeset_title = snapshot.editControlSettings.get().title; + if ( snapshot.isFutureDate() ) { + retval.customize_changeset_date = snapshot.editControlSettings.get().date; + } + } + return retval; + }; + }, + + /** + * Add status button. + * + * @return {object} status button. + */ + addStatusButton: function addStatusButton() { + var snapshot = this, selectMenuButton, statusButton, selectedOption, buttonText, changesetStatus, selectedStatus; + changesetStatus = api.state( 'changesetStatus' ).get(); + statusButton = {}; + + selectedStatus = changesetStatus && 'auto-draft' !== changesetStatus ? changesetStatus : 'publish'; + + statusButton.value = new api.Value( selectedStatus ); + statusButton.disbleButton = new api.Value(); + statusButton.disableSelect = new api.Value(); + statusButton.buttonText = new api.Value(); + statusButton.needConfirm = false; + + statusButton.container = $( $.trim( wp.template( 'snapshot-status-button' )({ + selected: selectedStatus + }) ) ); + statusButton.button = statusButton.container.find( '.snapshot-status-button-overlay' ); + statusButton.select = statusButton.container.find( 'select' ); + statusButton.select.selectmenu({ + width: 'auto', + icons: { + button: 'dashicons dashicons-arrow-down' + }, + change: function( event, ui ) { + statusButton.value.set( ui.item.value ); + }, + select: function() { + if ( statusButton.hiddenButton ) { + statusButton.hiddenButton.text( statusButton.buttonText.get() ); + } + } + }); + + selectMenuButton = statusButton.container.find( '.ui-selectmenu-button' ); + statusButton.hiddenButton = selectMenuButton.find( '.ui-selectmenu-text' ); + statusButton.hiddenButton.addClass( 'button button-primary' ); + + statusButton.dropDown = selectMenuButton.find( '.ui-icon' ); + statusButton.dropDown.addClass( 'button button-primary' ); + + statusButton.updateButtonText = function( dataAttr ) { + buttonText = statusButton.button.data( dataAttr ); + statusButton.button.text( buttonText ); + statusButton.hiddenButton.text( buttonText ); + statusButton.buttonText.set( buttonText ); + }; + + statusButton.value.bind( function( status ) { + selectedOption = statusButton.select.find( 'option:selected' ); + statusButton.button.data( 'alt-text', selectedOption.data( 'alt-text' ) ); + statusButton.button.data( 'button-text', selectedOption.text() ); + statusButton.updateButtonText( 'button-text' ); + + if ( 'publish' === status ) { + snapshot.snapshotExpandButton.hide(); + statusButton.button.data( 'confirm-text', selectedOption.data( 'confirm-text' ) ); + statusButton.button.data( 'publish-text', selectedOption.data( 'publish-text' ) ); + statusButton.needConfirm = true; + } - matches.shift(); - - return { - year: matches.shift(), - month: matches.shift(), - day: matches.shift(), - hour: matches.shift(), - minute: matches.shift(), - second: matches.shift() - }; - }; - - /** - * Format a Date Object. Returns 'Y-m-d H:i:s' format. - * - * @props http://stackoverflow.com/questions/10073699/pad-a-number-with-leading-zeros-in-javascript#comment33639551_10073699 - * - * @param {Date} date A Date object. - * @returns {string} A formatted date String. - */ - component.formatDate = function formatDate( date ) { - var formattedDate, - yearLength = 4, - nonYearLength = 2, - monthOffset = 1; - - formattedDate = ( '0000' + date.getFullYear() ).substr( -yearLength, yearLength ); - formattedDate += '-' + ( '00' + ( date.getMonth() + monthOffset ) ).substr( -nonYearLength, nonYearLength ); - formattedDate += '-' + ( '00' + date.getDate() ).substr( -nonYearLength, nonYearLength ); - formattedDate += ' ' + ( '00' + date.getHours() ).substr( -nonYearLength, nonYearLength ); - formattedDate += ':' + ( '00' + date.getMinutes() ).substr( -nonYearLength, nonYearLength ); - formattedDate += ':' + ( '00' + date.getSeconds() ).substr( -nonYearLength, nonYearLength ); - - return formattedDate; - }; - - /** - * Populate inputs from the setting value, if none of them are currently focused. - * - * @returns {boolean} Whether the inputs were populated. - */ - component.populateInputs = function populateInputs() { - var parsed; - - if ( component.schedule.inputs.is( ':focus' ) || '0000-00-00 00:00:00' === component.data.publishDate ) { - return false; - } + if ( 'future' === status ) { + snapshot.snapshotEditContainerDisplayed.set( true ); + snapshot.snapshotExpandButton.show(); + if ( snapshot.isFutureDate() ) { + snapshot.countdown.show(); + snapshot.updateSnapshot( status ); + } + } else { + snapshot.updateSnapshot( status ); + snapshot.snapshotEditContainerDisplayed.set( false ); + snapshot.countdown.hide(); + } + } ); - parsed = component.parseDateTime( component.data.publishDate ); - if ( ! parsed ) { - return false; - } + statusButton.disbleButton.bind( function( disabled ) { + statusButton.button.prop( 'disabled', disabled ); + } ); - component.schedule.inputs.each( function() { - var input = $( this ), - fieldName = input.data( 'date-input' ); + statusButton.disableSelect.bind( function( disabled ) { + statusButton.select.selectmenu( disabled ? 'disable' : 'enable' ); + statusButton.dropDown.toggleClass( 'disabled', disabled ); + } ); - if ( ! $( this ).is( 'select' ) && '' === $( this ).val() ) { - $( this ).val( parsed[fieldName] ); - } - } ); - return true; - }; - - /** - * Populate setting value from the inputs. - * - * @returns {boolean} Whether the date inputs currently represent a valid date. - */ - component.populateSetting = function populateSetting() { - var date = component.getDateFromInputs(), - save = $( '#snapshot-save' ), - scheduled; - - if ( ! date ) { - return false; - } + statusButton.disable = function( disable ) { + statusButton.disableSelect.set( disable ); + statusButton.disbleButton.set( disable ); + }; - date.setSeconds( 0 ); - scheduled = component.formatDate( date ) !== component.data.publishDate; + statusButton.button.on( 'click', function( event ) { + event.preventDefault(); + snapshot.updateSnapshot( statusButton.value.get() ); + } ); - if ( save.length ) { + snapshot.publishButton.after( statusButton.container ); - // Change update button to schedule. - if ( component.isFutureDate() ) { - save.text( component.data.i18n.scheduleButton ); - } else if ( api.state( 'snapshot-exists' ).get() ) { - save.text( component.data.i18n.updateButton ); - } else { - save.text( component.data.i18n.saveButton ); - } + return statusButton; + }, - if ( scheduled || component.data.dirty ) { - save.prop( 'disabled', false ); - } else { - save.prop( 'disabled', true ); - } - } + /** + * Remove 'customize_changeset_status' if its already set. + * + * @return {void} + */ + prefilterAjax: function prefilterAjax() { + var removeParam, isSameStatus; - component.updateCountdown(); - component.schedule.container.find( '.reset-time' ).toggle( scheduled ); + if ( ! api.state.has( 'changesetStatus' ) ) { + return; + } - return true; - }; + removeParam = function( queryString, parameter ) { + var pars = queryString.split( /[&;]/g ); - /** - * Check if the schedule date is in the future. - * - * @returns {boolean} True if future date. - */ - component.isFutureDate = function isFutureDate() { - var date = component.getDateFromInputs(), - millisecondsDivider = 1000, - remainingTime; + _.each( pars, function( string, index ) { + if ( string && string.lastIndexOf( parameter, 0 ) !== -1 ) { + pars.splice( index, 1 ); + } + } ); - if ( ! date ) { - return false; - } + return pars.join( '&' ); + }; - remainingTime = component.dateValueOf( date ); - remainingTime -= component.dateValueOf( component.getCurrentTime() ); - remainingTime = Math.ceil( remainingTime / millisecondsDivider ); - - return 0 < remainingTime; - }; - - /** - * Get current date/time in the site's timezone. - * - * Same functionality as the `current_time( 'mysql', false )` function in PHP. - * - * @returns {string} Current datetime string. - */ - component.getCurrentTime = function getCurrentTime() { - var currentDate = new Date( component.data.initialServerDate ), - currentTimestamp = component.dateValueOf(), - timestampDifferential; - - timestampDifferential = currentTimestamp - component.data.initialClientTimestamp; - timestampDifferential += component.data.initialClientTimestamp - component.data.initialServerTimestamp; - currentDate.setTime( currentDate.getTime() + timestampDifferential ); - - return component.formatDate( currentDate ); - }; - - /** - * Get the primitive value of a Date object. - * - * @param {string|Date} dateString The post status for the snapshot. - * @returns {object|string} The primitive value or date object. - */ - component.dateValueOf = function( dateString ) { - var date; - - if ( 'string' === typeof dateString ) { - date = new Date( dateString ); - } else if ( dateString instanceof Date ) { - date = dateString; - } else { - date = new Date(); + $.ajaxPrefilter( function( options, originalOptions ) { + if ( ! originalOptions.data ) { + return; + } + isSameStatus = api.state( 'changesetStatus' ).get() === originalOptions.data.customize_changeset_status; + if ( 'customize_save' === originalOptions.data.action && isSameStatus && options.data ) { + options.data = removeParam( options.data, 'customize_changeset_status' ); + } + } ); } + } ); - return date.valueOf(); - }; - - component.init(); + if ( 'undefined' !== typeof _customizeSnapshotsSettings ) { + api.snapshots = new api.Snapshots( _customizeSnapshotsSettings ); + } -} )( wp.customize, jQuery ); +})( wp.customize, jQuery ); diff --git a/php/class-customize-snapshot-back-compat.php b/php/class-customize-snapshot-back-compat.php new file mode 100644 index 00000000..5de40fdd --- /dev/null +++ b/php/class-customize-snapshot-back-compat.php @@ -0,0 +1,321 @@ +snapshot_manager = $snapshot_manager; + $this->data = array(); + + if ( ! Customize_Snapshot_Manager_Back_Compat::is_valid_uuid( $uuid ) ) { + throw new Exception( __( 'You\'ve entered an invalid snapshot UUID.', 'customize-snapshots' ) ); + } + $this->uuid = $uuid; + $post = $this->post(); + if ( $post ) { + $this->data = $this->snapshot_manager->post_type->get_post_content( $post ); + } + parent::__construct( $snapshot_manager ); + } + + /** + * Get the snapshot uuid. + * + * @return string + */ + public function uuid() { + return $this->uuid; + } + + /** + * Get the underlying data for the snapshot. + * + * @return array + */ + public function data() { + return $this->data; + } + + /** + * Get the status of the snapshot. + * + * @return string|null + */ + public function status() { + $post = $this->post(); + return $post ? get_post_status( $post->ID ) : null; + } + + /** + * Get the snapshot post associated with the provided UUID, or null if it does not exist. + * + * @return \WP_Post|null Post or null. + */ + public function post() { + if ( ! $this->post_id ) { + $this->post_id = $this->snapshot_manager->post_type->find_post( $this->uuid ); + } + if ( $this->post_id ) { + return get_post( $this->post_id ); + } else { + return null; + } + } + + /** + * Return the Customizer settings corresponding to the data contained in the snapshot. + * + * @return \WP_Customize_Setting[] + */ + public function settings() { + $settings = array(); + $setting_ids = array_keys( $this->data ); + $this->snapshot_manager->customize_manager->add_dynamic_settings( $setting_ids ); + foreach ( $setting_ids as $setting_id ) { + $setting = $this->snapshot_manager->customize_manager->get_setting( $setting_id ); + if ( $setting ) { + $settings[] = $setting; + } + } + return $settings; + } + + /** + * Prepare snapshot data for saving. + * + * @see WP_Customize_Manager::set_post_value() + * @throws Exception When $settings_data is not an array of arrays. + * + * @param array $settings_data Settings data, mapping setting IDs to arrays containing `value` and optionally additional params. + * @param array $options { + * Additional options. + * + * @type bool $skip_validation Whether to skip validation. Optional, defaults to false. + * } + * @return array { + * Result. + * + * @type null|\WP_Error $error Error object if error. + * @type array $sanitized Sanitized values. + * @type array $validities Setting validities. + * } + */ + public function set( array $settings_data, array $options = array() ) { + $error = new \WP_Error(); + $result = array( + 'errors' => null, + 'sanitized' => array(), + 'validities' => array(), + ); + + $setting_ids = array_keys( $settings_data ); + $customize_manager = $this->snapshot_manager->customize_manager; + $customize_manager->add_dynamic_settings( $setting_ids ); + + // Check for recognized settings and authorized settings. + $unsanitized_values = array(); + $unrecognized_setting_ids = array(); + $unauthorized_setting_ids = array(); + foreach ( $settings_data as $setting_id => $setting_params ) { + if ( ! is_array( $setting_params ) ) { + throw new Exception( '$setting_params not an array' ); + } + $setting = $customize_manager->get_setting( $setting_id ); + if ( ! $setting ) { + $unrecognized_setting_ids[] = $setting_id; + } elseif ( ! current_user_can( $setting->capability ) ) { + $unauthorized_setting_ids[] = $setting_id; + } elseif ( array_key_exists( 'value', $setting_params ) ) { + $unsanitized_values[ $setting_id ] = $setting_params['value']; + } + } + + // Remove values that are unrecognized or unauthorized. + $unsanitized_values = wp_array_slice_assoc( + $unsanitized_values, + array_diff( + array_keys( $unsanitized_values ), + array_merge( $unrecognized_setting_ids, $unauthorized_setting_ids ) + ) + ); + + $invalid_setting_ids = array(); + if ( empty( $options['skip_validation'] ) ) { + // Validate. + if ( method_exists( $customize_manager, 'validate_setting_values' ) ) { + $result['validities'] = $customize_manager->validate_setting_values( $unsanitized_values ); + } else { + // @codeCoverageIgnoreStart + $result['validities'] = array_map( + function( $sanitized ) { + if ( is_null( $sanitized ) ) { + return new \WP_Error( 'invalid_value', __( 'Invalid value', 'customize-snapshots' ) ); + } else { + return true; + } + }, + $unsanitized_values + ); + // @codeCoverageIgnoreEnd + } + $invalid_setting_ids = array_keys( array_filter( $result['validities'], function( $validity ) { + return is_wp_error( $validity ); + } ) ); + } + + // Sanitize. + foreach ( $unsanitized_values as $setting_id => $unsanitized_value ) { + $setting = $customize_manager->get_setting( $setting_id ); + if ( $setting ) { + $result['sanitized'][ $setting_id ] = $setting->sanitize( $unsanitized_value ); + } else { + $unrecognized_setting_ids[] = $setting_id; + } + } + + // Add errors. + if ( ! empty( $unauthorized_setting_ids ) ) { + $error->add( + 'unauthorized_settings', + /* translators: %s is the list of unauthorized setting ids */ + sprintf( __( 'Unauthorized settings: %s', 'customize-snapshots' ), join( ',', $unauthorized_setting_ids ) ), + array( 'setting_ids' => $unauthorized_setting_ids ) + ); + } + if ( ! empty( $unrecognized_setting_ids ) ) { + $error->add( + 'unrecognized_settings', + /* translators: %s is the list of unrecognized setting ids */ + sprintf( __( 'Unrecognized settings: %s', 'customize-snapshots' ), join( ',', $unrecognized_setting_ids ) ), + array( 'setting_ids' => $unrecognized_setting_ids ) + ); + } + if ( 0 !== count( $invalid_setting_ids ) ) { + $code = 'invalid_values'; + $message = __( 'Invalid values', 'customize-snapshots' ); + $error->add( $code, $message, compact( 'invalid_setting_ids' ) ); + } + + if ( ! empty( $error->errors ) ) { + $result['errors'] = $error; + } else { + /* + * Note that somewhat unintuitively the unsanitized post values + * ($unsanitized_values) are stored as opposed to storing the + * sanitized ones ($result['sanitized']). It is still safe to do this + * because they have passed sanitization and validation here. The + * reason why we need to store the raw unsanitized values is so that + * the values can be re-populated into the post values for running + * through the sanitize, validate, and ultimately update logic. + * Once a value has gone through the sanitize logic, it may not be + * suitable for populating into a post value, especially widget + * instances which get exported with a JS value that has the instance + * data encoded, serialized, and hashed to prevent mutation. A + * sanitize filter for a widget instance will convert an encoded + * instance value into a regular instance array, and if this regular + * instance array is placed back into a post value, it will get + * rejected by the sanitize logic for not being an encoded value. + */ + foreach ( $settings_data as $setting_id => $setting_params ) { + if ( ! isset( $this->data[ $setting_id ] ) ) { + $this->data[ $setting_id ] = array(); + } + if ( ! array_key_exists( 'value', $setting_params ) ) { + $setting_params['value'] = null; + } + $this->data[ $setting_id ] = array_merge( $this->data[ $setting_id ], $setting_params ); + } + } + + return $result; + } + + /** + * Persist the data in the snapshot post content. + * + * @param array $args Args. + * @return true|\WP_Error + */ + public function save( array $args ) { + + /** + * Filter the snapshot's data before it's saved to 'post_content'. + * + * @param array $data Customizer snapshot data, with setting IDs mapped to an array + * containing a `value` array item and potentially other metadata. + * @param Customize_Snapshot $this Snapshot object. + */ + $this->data = apply_filters( 'customize_snapshot_save', $this->data, $this ); + + $result = $this->snapshot_manager->post_type->save( array_merge( + $args, + array( + 'uuid' => $this->uuid, + 'data' => $this->data, + 'theme' => $this->snapshot_manager->customize_manager->get_stylesheet(), + ) + ) ); + + if ( ! is_wp_error( $result ) ) { + $this->post_id = $result; + } + + return $result; + } + + /** + * Return whether the snapshot was saved (created/inserted) yet. + * + * @return bool + */ + public function saved() { + return ! is_null( $this->post() ); + } +} diff --git a/php/class-customize-snapshot-command.php b/php/class-customize-snapshot-command.php new file mode 100644 index 00000000..96726c78 --- /dev/null +++ b/php/class-customize-snapshot-command.php @@ -0,0 +1,58 @@ +plugin->compat ) { + \WP_CLI::error( __( 'You\'re using older WordPress version please upgrade 4.7 or above to migrate.', 'customize-snapshots' ) ); + return; + } + if ( $migrate_obj->is_migrated() ) { + \WP_CLI::success( __( 'Already migrated.', 'customize-snapshots' ) ); + return; + } + $dry_mode = isset( $assoc_args['dry-run'] ); + if ( ! $dry_mode ) { + $post_count = $migrate_obj->changeset_migrate(); + \WP_CLI::success( $post_count . ' ' . __( 'posts migrated.', 'customize-snapshots' ) ); + } else { + $ids = $migrate_obj->changeset_migrate( - 1, true ); + \WP_CLI::success( count( $ids ) . ' ' . __( 'posts migrated:', 'customize-snapshots' ) . ' ' . implode( ',', $ids ) ); + } + } +} + +if ( defined( 'WP_CLI' ) && WP_CLI ) { + \WP_CLI::add_command( 'snapshot', __NAMESPACE__ . '\\Customize_Snapshot_Command' ); +} diff --git a/php/class-customize-snapshot-manager-back-compat.php b/php/class-customize-snapshot-manager-back-compat.php new file mode 100644 index 00000000..219be0c4 --- /dev/null +++ b/php/class-customize-snapshot-manager-back-compat.php @@ -0,0 +1,1011 @@ +post_type = new Post_Type_Back_Compat( $this ); + + add_filter( 'customize_refresh_nonces', array( $this, 'filter_customize_refresh_nonces' ) ); + add_action( 'template_redirect', array( $this, 'show_theme_switch_error' ) ); + 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' ) ); + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_scripts' ) ); + add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) ); + add_action( 'customize_save', array( $this, 'check_customize_publish_authorization' ), 10, 0 ); + $this->hooks(); + 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 ); + } + } + + /** + * Get the Customize_Snapshot instance. + * + * @return Customize_Snapshot_Back_Compat + */ + public function snapshot() { + return $this->snapshot; + } + + /** + * Ensure Customizer manager is instantiated. + * + * @global \WP_Customize_Manager $wp_customize + */ + public function ensure_customize_manager() { + global $wp_customize; + if ( empty( $wp_customize ) || ! ( $wp_customize instanceof \WP_Customize_Manager ) ) { + require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' ); + $wp_customize = new \WP_Customize_Manager(); // WPCS: override ok. + } + $this->customize_manager = $wp_customize; + } + + /** + * Include the snapshot nonce in the Customizer nonces. + * + * @param array $nonces Nonces. + * @return array Nonces. + */ + public function filter_customize_refresh_nonces( $nonces ) { + $nonces['snapshot'] = wp_create_nonce( self::AJAX_ACTION ); + return $nonces; + } + + /** + * Enqueue styles & scripts for the Customizer. + * + * @action customize_controls_enqueue_scripts + * @global \WP_Customize_Manager $wp_customize + */ + 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( 'customize-snapshots' ); + wp_enqueue_script( 'customize-snapshots-compat' ); + + if ( $this->snapshot ) { + $post = $this->snapshot->post(); + $this->override_post_date_default_data( $post ); + } + + // Script data array. + $exports = apply_filters( 'customize_snapshots_export_data', array( + 'action' => self::AJAX_ACTION, + 'uuid' => $this->snapshot ? $this->snapshot->uuid() : self::generate_uuid(), + 'editLink' => isset( $post ) ? get_edit_post_link( $post, 'raw' ) : '', + 'publishDate' => isset( $post->post_date ) ? $post->post_date : '', + 'title' => isset( $post->post_title ) ? $post->post_title : '', + 'postStatus' => isset( $post->post_status ) ? $post->post_status : '', + 'currentUserCanPublish' => current_user_can( 'customize_publish' ), + 'initialServerDate' => current_time( 'mysql', false ), + 'initialServerTimestamp' => floor( microtime( true ) * 1000 ), + 'i18n' => array( + 'saveButton' => __( 'Save', 'customize-snapshots' ), + 'updateButton' => __( 'Update', 'customize-snapshots' ), + 'scheduleButton' => __( 'Schedule', 'customize-snapshots' ), + 'submit' => __( 'Submit', 'customize-snapshots' ), + 'submitted' => __( 'Submitted', 'customize-snapshots' ), + 'publish' => __( 'Publish', 'customize-snapshots' ), + 'published' => __( 'Published', 'customize-snapshots' ), + 'permsMsg' => array( + 'save' => __( 'You do not have permission to publish changes, but you can create a snapshot by clicking the "Save" button.', 'customize-snapshots' ), + 'update' => __( 'You do not have permission to publish changes, but you can modify this snapshot by clicking the "Update" button.', 'customize-snapshots' ), + ), + 'errorMsg' => __( 'The snapshot could not be saved.', 'customize-snapshots' ), + 'errorTitle' => __( 'Error', 'customize-snapshots' ), + 'collapseSnapshotScheduling' => __( 'Collapse snapshot scheduling', 'customize-snapshots' ), + 'expandSnapshotScheduling' => __( 'Expand snapshot scheduling', 'customize-snapshots' ), + ), + 'snapshotExists' => ( $this->snapshot && $this->snapshot->saved() ), + ) ); + + wp_localize_script( 'customize-snapshots-compat', '_customizeSnapshotsCompatSettings', $exports ); + } + + /** + * Load snapshot. + */ + public function load_snapshot() { + $this->ensure_customize_manager(); + $this->snapshot = new Customize_Snapshot_Back_Compat( $this, $this->current_snapshot_uuid ); + + if ( ! $this->should_import_and_preview_snapshot( $this->snapshot ) ) { + return; + } + + $this->add_widget_setting_preview_filters(); + $this->add_nav_menu_setting_preview_filters(); + + /* + * Populate post values. + * + * Note we have to defer until setup_theme since the transaction + * can be set beforehand, and wp_magic_quotes() would not have + * been called yet, resulting in a $_POST['customized'] that is + * double-escaped. Note that this happens at priority 1, which + * is immediately after Customize_Snapshot_Manager::store_customized_post_data + * which happens at setup_theme priority 0, so that the initial + * POST data can be preserved. + */ + if ( did_action( 'setup_theme' ) ) { + $this->import_snapshot_data(); + } else { + add_action( 'setup_theme', array( $this, 'import_snapshot_data' ) ); + } + + // 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 ); + } + } + + /** + * Populate post values and $_POST['customized'] wth the snapshot's data. + * + * Plugins used to have to dynamically register settings by inspecting the + * $_POST['customized'] var and manually re-parse and inspect to see if it + * contains settings that wouldn't be registered otherwise. This ensures + * that these plugins will continue to work. + * + * Note that this can't be called prior to the setup_theme action or else + * magic quotes may end up getting added twice. + * + * @see Customize_Snapshot_Manager::should_import_and_preview_snapshot() + */ + public function import_snapshot_data() { + /* + * We don't merge the snapshot data with any existing existing unsanitized + * post values since should_import_and_preview_snapshot returns false if + * there is any existing data in the Customizer state. This is to prevent + * clobbering existing values (or previewing non-snapshotted values on frontend). + * Note that wp.customize.Snapshots.extendPreviewerQuery() will extend the + * previewer data to include the current snapshot UUID. + */ + $snapshot_values = array_filter( + wp_list_pluck( $this->snapshot->data(), 'value' ), + function( $value ) { + return ! is_null( $value ); + } + ); + + // Populate input vars for back-compat. + $_POST['customized'] = wp_slash( wp_json_encode( $snapshot_values ) ); + // @codingStandardsIgnoreStart + $_REQUEST['customized'] = $_POST['customized']; + // @codingStandardsIgnoreEnd + + foreach ( $snapshot_values as $setting_id => $value ) { + $this->customize_manager->set_post_value( $setting_id, $value ); + } + } + + /** + * Determine whether the current snapshot can be previewed. + * + * @param Customize_Snapshot_Back_Compat $snapshot Snapshot to check. + * @return true|\WP_Error Returns true if previewable, or `WP_Error` if cannot. + */ + public function should_import_and_preview_snapshot( Customize_Snapshot_Back_Compat $snapshot ) { + global $pagenow; + + // Ignore if in the admin, but not Admin Ajax or Customizer. + if ( is_admin() && ! in_array( $pagenow, array( 'admin-ajax.php', 'customize.php' ), true ) ) { + return false; + } + + if ( is_wp_error( $this->get_theme_switch_error( $snapshot ) ) ) { + return false; + } + + // Abort if doing customize_save. + if ( $this->doing_customize_save_ajax() ) { + return false; + } + + // Abort if the snapshot was already published. + if ( $snapshot->saved() && 'publish' === get_post_status( $snapshot->post() ) ) { + return false; + } + + /* + * Prevent clobbering existing values (or previewing non-snapshotted values on frontend). + * Note that wp.customize.Snapshots.extendPreviewerQuery() will extend the + * previewer data to include the current snapshot UUID. + */ + if ( $this->customize_manager && count( $this->customize_manager->unsanitized_post_values() ) > 0 ) { + return false; + } + + return true; + } + + /** + * Redirect when preview is not allowed for the current theme. + * + * @param Customize_Snapshot $snapshot Snapshot to check. + * @return \WP_Error|null + */ + public function get_theme_switch_error( Customize_Snapshot $snapshot ) { + + // Loading a snapshot into the context of a theme switch is not supported. + if ( ! $this->is_theme_active() ) { + return new \WP_Error( 'snapshot_theme_switch', __( 'Snapshot cannot be previewed when previewing a theme switch.', 'customize-snapshots' ) ); + } + + $snapshot_post = $snapshot->post(); + if ( ! $snapshot_post ) { + return null; + } + + $snapshot_theme = get_post_meta( $snapshot_post->ID, '_snapshot_theme', true ); + if ( ! empty( $snapshot_theme ) && get_stylesheet() !== $snapshot_theme ) { + return new \WP_Error( 'snapshot_theme_switched', __( 'Snapshot requested was made for a different theme and cannot be previewed with the current theme.', 'customize-snapshots' ) ); + } + + return null; + } + + /** + * 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. + * + * Note that this happens at `wp_loaded` action with priority 11 so that we + * can look at whether the `customize_preview_init` action was done. + */ + public function preview_snapshot_settings() { + if ( $this->is_previewing_settings() ) { + return; + } + $this->previewing_settings = true; + + /* + * Note that we need to preview the settings outside the Customizer preview + * and in the Customizer pane itself so we can load a previous snapshot + * into the Customizer. We have to prevent the previews from being added + * in the case of a customize_save action because then update_option() + * may short-circuit because it will detect that there are no changes to + * make. + */ + foreach ( $this->snapshot->settings() as $setting ) { + $setting->preview(); + $setting->dirty = true; + } + } + + /** + * Add filters for previewing widgets on the frontend. + */ + function add_widget_setting_preview_filters() { + /* + * Add WP_Customize_Widget component hooks which were short-circuited in 4.5 (r36611 for #35895). + * See https://core.trac.wordpress.org/ticket/35895 + */ + if ( isset( $this->customize_manager->widgets ) && ! current_user_can( 'edit_theme_options' ) ) { + $hooks = array( + 'customize_dynamic_setting_args' => array( + 'callback' => array( $this->customize_manager->widgets, 'filter_customize_dynamic_setting_args' ), + 'priority' => 10, + ), + 'widgets_init' => array( + 'callback' => array( $this->customize_manager->widgets, 'register_settings' ), + 'priority' => 95, + ), + 'customize_register' => array( + 'callback' => array( $this->customize_manager->widgets, 'schedule_customize_register' ), + 'priority' => 1, + ), + ); + foreach ( $hooks as $hook_name => $hook_args ) { + // Note that add_action()/has_action() are just aliases for add_filter()/has_filter(). + if ( ! has_filter( $hook_name, $hook_args['callback'] ) ) { + add_filter( $hook_name, $hook_args['callback'], $hook_args['priority'], PHP_INT_MAX ); + } + } + } + + /* + * Disable routine which fails because \WP_Customize_Manager::setup_theme() is + * never called in a frontend preview context, whereby the original_stylesheet + * is never set and so \WP_Customize_Manager::is_theme_active() will thus + * always return true because get_stylesheet() !== null. + * + * The action being removed is responsible for adding an option_sidebar_widgets + * filter \WP_Customize_Widgets::filter_option_sidebars_widgets_for_theme_switch() + * which causes the sidebars_widgets to be overridden with a global variable. + */ + if ( ! is_admin() ) { + remove_action( 'wp_loaded', array( $this->customize_manager->widgets, 'override_sidebars_widgets_for_theme_switch' ) ); + } + } + + /** + * Add filters for previewing nav menus on the frontend. + */ + public function add_nav_menu_setting_preview_filters() { + if ( isset( $this->customize_manager->nav_menus ) && ! current_user_can( 'edit_theme_options' ) ) { + $hooks = array( + 'customize_register' => array( + 'callback' => array( $this->customize_manager->nav_menus, 'customize_register' ), + 'priority' => 11, + ), + 'customize_dynamic_setting_args' => array( + 'callback' => array( $this->customize_manager->nav_menus, 'filter_dynamic_setting_args' ), + 'priority' => 10, + ), + 'customize_dynamic_setting_class' => array( + 'callback' => array( $this->customize_manager->nav_menus, 'filter_dynamic_setting_class' ), + 'priority' => 10, + ), + 'wp_nav_menu_args' => array( + 'callback' => array( $this->customize_manager->nav_menus, 'filter_wp_nav_menu_args' ), + 'priority' => 1000, + ), + 'wp_nav_menu' => array( + 'callback' => array( $this->customize_manager->nav_menus, 'filter_wp_nav_menu' ), + 'priority' => 10, + ), + ); + foreach ( $hooks as $hook_name => $hook_args ) { + // Note that add_action()/has_action() are just aliases for add_filter()/has_filter(). + if ( ! has_filter( $hook_name, $hook_args['callback'] ) ) { + add_filter( $hook_name, $hook_args['callback'], $hook_args['priority'], PHP_INT_MAX ); + } + } + } + + if ( isset( $this->customize_manager->nav_menus ) ) { + add_action( 'customize_register', array( $this, 'preview_early_nav_menus_in_customizer' ), 9 ); + } + } + + /** + * Preview nav menu settings early so that the sections and controls for snapshot values will be added properly. + * + * This must happen at `customize_register` priority prior to 11 which is when `WP_Customize_Nav_Menus::customize_register()` runs. + * This is only relevant when accessing the Customizer app (customize.php), as this is where sections/controls matter. + * + * @see \WP_Customize_Nav_Menus::customize_register() + */ + public function preview_early_nav_menus_in_customizer() { + if ( ! is_admin() ) { + return; + } + $this->customize_manager->add_dynamic_settings( array_keys( $this->snapshot()->data() ) ); + foreach ( $this->snapshot->settings() as $setting ) { + $is_nav_menu_setting = ( + $setting instanceof \WP_Customize_Nav_Menu_Setting + || + $setting instanceof \WP_Customize_Nav_Menu_Item_Setting + || + preg_match( '/^nav_menu_locations\[/', $setting->id ) + ); + if ( $is_nav_menu_setting ) { + $setting->preview(); + + /* + * The following is redundant because it will be done later in + * Customize_Snapshot_Manager::preview_snapshot_settings(). + * Also note that the $setting instance here will likely be + * blown away inside of WP_Customize_Nav_Menus::customize_register(), + * when add_setting is called there. What matters here is that + * preview() is called on the setting _before_ the logic inside + * WP_Customize_Nav_Menus::customize_register() runs, so that + * the nav menu sections will be created. + */ + $setting->dirty = true; + } + } + } + + /** + * Setup previewing of Ajax requests in the Customizer preview. + * + * @global \WP_Customize_Manager $wp_customize + */ + public function setup_preview_ajax_requests() { + global $wp_customize, $pagenow; + + /* + * 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(); + } + + // 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 ); + } + + $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; + } + + /** + * Check whether customize_publish capability is granted in customize_save. + */ + public function check_customize_publish_authorization() { + if ( $this->doing_customize_save_ajax() && ! current_user_can( 'customize_publish' ) ) { + wp_send_json_error( array( + 'error' => 'customize_publish_unauthorized', + ) ); + } + } + + /** + * Show the theme switch error if there is one. + */ + public function show_theme_switch_error() { + if ( empty( $this->snapshot ) ) { + return; + } + $error = $this->get_theme_switch_error( $this->snapshot ); + if ( is_wp_error( $error ) ) { + wp_die( esc_html( $error->get_error_message() ) ); + } + } + + /** + * Publish the snapshot snapshots via AJAX. + * + * Fires at `customize_save_after` to update and publish the snapshot. + * The logic in here is the inverse of save_settings_with_publish_snapshot. + * + * @see Customize_Snapshot_Manager::save_settings_with_publish_snapshot() + * + * @return bool Whether the snapshot was saved successfully. + */ + public function publish_snapshot_with_customize_save_after() { + $that = $this; + + if ( ! $this->snapshot || ! $this->doing_customize_save_ajax() ) { + return false; + } + + // This should never be reached due to Customize_Snapshot_Manager::check_customize_publish_authorization(). + if ( ! current_user_can( 'customize_publish' ) ) { + return false; + } + + $settings_data = array_map( + function( $value ) { + return compact( 'value' ); + }, + $this->customize_manager->unsanitized_post_values() + ); + $result = $this->snapshot->set( $settings_data, array( 'skip_validation' => true ) ); + if ( ! empty( $result['errors'] ) ) { + add_filter( 'customize_save_response', function( $response ) use ( $result, $that ) { + $response['snapshot_errors'] = $that->prepare_errors_for_response( $result['errors'] ); + return $response; + } ); + return false; + } + + if ( ! $this->snapshot->post() || 'publish' !== $this->snapshot->post()->post_status ) { + $args = array( + 'status' => 'publish', + ); + + // Ensure a scheduled Snapshot is published. + if ( $this->snapshot->post() && 'future' === $this->snapshot->post()->post_status ) { + $args['edit_date'] = true; + $args['date_gmt'] = current_time( 'mysql', true ); + } + + if ( isset( $_POST['title'] ) && '' !== trim( $_POST['title'] ) ) { + $args['post_title'] = sanitize_text_field( wp_unslash( $_POST['title'] ) ); + } + + $r = $this->snapshot->save( $args ); + if ( is_wp_error( $r ) ) { + add_filter( 'customize_save_response', function( $response ) use ( $r, $that ) { + $response['snapshot_errors'] = $that->prepare_errors_for_response( $r ); + return $response; + } ); + return false; + } + } + + // Send the new UUID to the client for the next snapshot. + $class = __CLASS__; // For PHP 5.3. + add_filter( 'customize_save_response', function( $data ) use ( $class ) { + $data['new_customize_snapshot_uuid'] = $class::generate_uuid(); + return $data; + } ); + return true; + } + + /** + * Publish snapshot changes when snapshot post is being published. + * + * The logic in here is the inverse of to publish_snapshot_with_customize_save_after. + * + * The meat of the logic that manipulates the post_content and validates the settings + * needs to be done in wp_insert_post_data filter in like a + * filter_insert_post_data_to_validate_published_snapshot method? This would + * have the benefit of reducing one wp_insert_post() call. + * + * @todo Consider using wp_insert_post_data to prevent double calls to wp_insert_post(). + * @see Customize_Snapshot_Manager::publish_snapshot_with_customize_save_after() + * + * @param string $new_status New status. + * @param string $old_status Old status. + * @param \WP_Post $post Post object. + * @return bool Whether the settings were saved. + */ + public function save_settings_with_publish_snapshot( $new_status, $old_status, $post ) { + + // Abort if not transitioning a snapshot post to publish from a non-publish status. + if ( $this->get_post_type() !== $post->post_type || 'publish' !== $new_status || $new_status === $old_status ) { + return false; + } + + $this->ensure_customize_manager(); + + if ( $this->doing_customize_save_ajax() ) { + // Short circuit because customize_save ajax call is changing status. + return false; + } + + if ( ! did_action( 'customize_register' ) ) { + /* + * When running from CLI or Cron, we have to remove the action because + * it will get added with a default priority of 10, after themes and plugins + * have already done add_action( 'customize_register' ), resulting in them + * being called first at the priority 10. So we manually call the + * prerequisite function WP_Customize_Manager::register_controls() and + * remove it from being called when the customize_register action fires. + */ + remove_action( 'customize_register', array( $this->customize_manager, 'register_controls' ) ); + $this->customize_manager->register_controls(); + + /* + * Unfortunate hack to prevent \WP_Customize_Widgets::customize_register() + * from calling preview() on settings. This needs to be cleaned up in core. + * It is important for previewing to be prevented because if an option has + * a filter it will short-circuit when an update is attempted since it + * detects that there is no change to be put into the DB. + * See: https://github.com/xwp/wordpress-develop/blob/e8c58c47db1421a1d0b2afa9ad4b9eb9e1e338e0/src/wp-includes/class-wp-customize-widgets.php#L208-L217 + */ + if ( ! defined( 'DOING_AJAX' ) ) { + define( 'DOING_AJAX', true ); + } + $_REQUEST['action'] = 'customize_save'; + + /** This action is documented in wp-includes/class-wp-customize-manager.php */ + do_action( 'customize_register', $this->customize_manager ); + + // undefine( 'DOING_AJAX' )... just kidding. This is the end of the unfortunate hack and it should be fixed in Core. + unset( $_REQUEST['action'] ); + } + $snapshot_content = $this->post_type->get_post_content( $post ); + + if ( method_exists( $this->customize_manager, 'validate_setting_values' ) ) { + /** This action is documented in wp-includes/class-wp-customize-manager.php */ + do_action( 'customize_save_validation_before', $this->customize_manager ); + } + + $setting_ids = array_keys( $snapshot_content ); + $this->customize_manager->add_dynamic_settings( $setting_ids ); + + /** This action is documented in wp-includes/class-wp-customize-manager.php */ + do_action( 'customize_save', $this->customize_manager ); + + /** + * Settings to save. + * + * @var \WP_Customize_Setting[] + */ + $settings = array(); + + $publish_error_count = 0; + foreach ( $snapshot_content as $setting_id => &$setting_params ) { + + // Missing value error. + if ( ! isset( $setting_params['value'] ) || is_null( $setting_params['value'] ) ) { + if ( ! is_array( $setting_params ) ) { + if ( ! empty( $setting_params ) ) { + $setting_params = array( 'value' => $setting_params ); + } else { + $setting_params = array(); + } + } + $setting_params['publish_error'] = 'null_value'; + $publish_error_count += 1; + continue; + } + + // Unrecognized setting error. + $this->customize_manager->set_post_value( $setting_id, $setting_params['value'] ); + $setting = $this->customize_manager->get_setting( $setting_id ); + if ( ! ( $setting instanceof \WP_Customize_Setting ) ) { + $setting_params['publish_error'] = 'unrecognized_setting'; + $publish_error_count += 1; + continue; + } + + // Validate setting value. + if ( method_exists( $setting, 'validate' ) ) { + $validity = $setting->validate( $setting_params['value'] ); + if ( is_wp_error( $validity ) ) { + $setting_params['publish_error'] = $validity->get_error_code(); + $publish_error_count += 1; + continue; + } + } + + // Validate sanitized setting value. + $sanitized_value = $setting->sanitize( $setting_params['value'] ); + if ( is_null( $sanitized_value ) || is_wp_error( $sanitized_value ) ) { + $setting_params['publish_error'] = is_wp_error( $sanitized_value ) ? $sanitized_value->get_error_code() : 'invalid_value'; + $publish_error_count += 1; + continue; + } + + $settings[] = $setting; + unset( $setting_params['publish_error'] ); + } // End foreach(). + + // Handle error scenarios. + if ( $publish_error_count > 0 ) { + $update_setting_args = array( + 'ID' => $post->ID, + 'post_content' => Customize_Snapshot_Manager::encode_json( $snapshot_content ), + 'post_status' => 'pending', + ); + wp_update_post( wp_slash( $update_setting_args ) ); + update_post_meta( $post->ID, 'snapshot_error_on_publish', $publish_error_count ); + + add_filter( 'redirect_post_location', function( $location ) { + $location = add_query_arg( 'snapshot_error_on_publish', '1', $location ); + return $location; + } ); + return false; + } + + /* + * Change all setting capabilities temporarily to 'exist' to allow them to + * be saved regardless of current user, such as when WP-Cron is publishing + * the snapshot post if it was scheduled. It is safe to do this because + * a setting can only be written into a snapshot by users who have the + * capability, so after it has been added to a snapshot it is good to commit. + */ + $existing_caps = wp_list_pluck( $settings, 'capability' ); + foreach ( $settings as $setting ) { + $setting->capability = 'exist'; + } + + // Persist the settings in the DB. + foreach ( $settings as $setting ) { + $setting->save(); + } + + // Restore setting capabilities. + foreach ( $existing_caps as $setting_id => $existing_cap ) { + $settings[ $setting_id ]->capability = $existing_cap; + } + + /** This action is documented in wp-includes/class-wp-customize-manager.php */ + do_action( 'customize_save_after', $this->customize_manager ); + + // Remove any previous error on setting. + delete_post_meta( $post->ID, 'snapshot_error_on_publish' ); + + return true; + } + + /** + * Update snapshots via AJAX. + */ + public function handle_update_snapshot_request() { + if ( ! check_ajax_referer( self::AJAX_ACTION, 'nonce', false ) ) { + status_header( 400 ); + wp_send_json_error( 'bad_nonce' ); + } elseif ( ! current_user_can( 'customize' ) ) { + status_header( 403 ); + wp_send_json_error( 'customize_not_allowed' ); + } elseif ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'POST' !== $_SERVER['REQUEST_METHOD'] ) { // WPCS: input var ok. + status_header( 405 ); + wp_send_json_error( 'bad_method' ); + } elseif ( empty( $this->current_snapshot_uuid ) ) { + status_header( 400 ); + wp_send_json_error( 'invalid_customize_snapshot_uuid' ); + } elseif ( 0 === count( $this->customize_manager->unsanitized_post_values() ) ) { + status_header( 400 ); + wp_send_json_error( 'missing_snapshot_customized' ); + } + + if ( isset( $_POST['status'] ) ) { // WPCS: input var ok. + $status = sanitize_key( $_POST['status'] ); + } else { + $status = 'draft'; + } + if ( ! in_array( $status, array( 'draft', 'pending', 'future' ), true ) ) { + status_header( 400 ); + wp_send_json_error( 'bad_status' ); + } + if ( 'future' === $status && ! current_user_can( 'customize_publish' ) ) { + status_header( 400 ); + wp_send_json_error( 'customize_not_allowed' ); + } + $publish_date = isset( $_POST['date'] ) ? $_POST['date'] : ''; + if ( 'future' === $status ) { + $publish_date_obj = new \DateTime( $publish_date ); + $current_date = new \DateTime( current_time( 'mysql' ) ); + if ( empty( $publish_date ) || ! $publish_date_obj || $current_date > $publish_date_obj ) { + status_header( 400 ); + wp_send_json_error( 'bad_schedule_time' ); + } + } + + // Prevent attempting to modify a "locked" snapshot (a published one). + $post = $this->snapshot->post(); + if ( $post && 'publish' === $post->post_status ) { + wp_send_json_error( array( + 'errors' => array( + 'already_published' => array( + 'message' => __( 'The snapshot has already published so it is locked.', 'customize-snapshots' ), + ), + ), + ) ); + } + + /** + * Add any additional checks before saving snapshot. + * + * @param Customize_Snapshot $snapshot Snapshot to be saved. + * @param Customize_Snapshot_Manager $snapshot_manager Snapshot manager. + */ + do_action( 'customize_snapshot_save_before', $this->snapshot, $this ); + + // Set the snapshot UUID. + $post_type = get_post_type_object( $this->get_post_type() ); + $authorized = ( $post ? + current_user_can( $post_type->cap->edit_post, $post->ID ) : + current_user_can( 'customize' ) + ); + if ( ! $authorized ) { + status_header( 403 ); + wp_send_json_error( 'unauthorized' ); + } + + $data = array( + 'errors' => null, + ); + $settings_data = array_map( + function( $value ) { + return compact( 'value' ); + }, + $this->customize_manager->unsanitized_post_values() + ); + $r = $this->snapshot->set( $settings_data ); + if ( method_exists( $this->customize_manager, 'prepare_setting_validity_for_js' ) ) { + $data['setting_validities'] = array_map( + array( $this->customize_manager, 'prepare_setting_validity_for_js' ), + $r['validities'] + ); + } + + if ( $r['errors'] ) { + $data['errors'] = $this->prepare_errors_for_response( $r['errors'] ); + wp_send_json_error( $data ); + } + $args = array( + 'status' => $status, + ); + if ( isset( $_POST['title'] ) && '' !== trim( $_POST['title'] ) ) { + $args['post_title'] = sanitize_text_field( wp_unslash( $_POST['title'] ) ); + } + + if ( isset( $publish_date_obj ) && 'future' === $status ) { + $args['date_gmt'] = get_gmt_from_date( $publish_date_obj->format( 'Y-m-d H:i:s' ) ); + } + $r = $this->snapshot->save( $args ); + + $post = $this->snapshot->post(); + if ( $post ) { + $data['edit_link'] = get_edit_post_link( $post, 'raw' ); + $data['snapshot_publish_date'] = $post->post_date; + $data['title'] = $post->post_title; + } + + if ( is_wp_error( $r ) ) { + $data['errors'] = $this->prepare_errors_for_response( $r ); + wp_send_json_error( $data ); + } + + /** This filter is documented in wp-includes/class-wp-customize-manager.php */ + $data = apply_filters( 'customize_save_response', $data, $this->customize_manager ); + wp_send_json_success( $data ); + } + + /** + * Set up Customizer preview. + */ + public function customize_preview_init() { + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_preview_scripts' ) ); + } + + /** + * Enqueue Customizer preview scripts. + * + * @global \WP_Customize_Manager $wp_customize + */ + public function enqueue_preview_scripts() { + global $wp_customize; + + $handle = 'customize-snapshots-preview'; + wp_enqueue_script( $handle ); + wp_enqueue_style( $handle ); + + $exports = array( + 'home_url' => wp_parse_url( home_url( '/' ) ), + 'rest_api_url' => wp_parse_url( rest_url( '/' ) ), + 'admin_ajax_url' => wp_parse_url( admin_url( 'admin-ajax.php' ) ), + 'initial_dirty_settings' => array_keys( $wp_customize->unsanitized_post_values() ), + ); + wp_add_inline_script( + $handle, + sprintf( 'CustomizeSnapshotsPreview.init( %s )', wp_json_encode( $exports ) ), + 'after' + ); + } + + /** + * Enqueue Customizer frontend scripts. + */ + public function enqueue_frontend_scripts() { + if ( ! $this->snapshot || is_customize_preview() ) { + 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' + ); + } + + /** + * Underscore (JS) templates. + */ + public function render_templates() { + $this->add_edit_box_template(); + ?> + + + + + + + + + + post_type = new Post_Type( $this ); - add_action( 'init', array( $this->post_type, 'register' ) ); - - add_action( 'template_redirect', array( $this, 'show_theme_switch_error' ) ); - + function hooks() { + add_action( 'init', array( $this->post_type, 'init' ) ); 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( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) ); add_action( 'customize_controls_init', array( $this, 'add_snapshot_uuid_to_return_url' ) ); add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_templates' ) ); - add_action( 'customize_save', array( $this, 'check_customize_publish_authorization' ), 10, 0 ); - add_filter( 'customize_refresh_nonces', array( $this, 'filter_customize_refresh_nonces' ) ); add_action( 'admin_bar_menu', array( $this, 'customize_menu' ), 41 ); add_action( 'admin_bar_menu', array( $this, 'remove_all_non_snapshot_admin_bar_links' ), 100000 ); add_action( 'wp_before_admin_bar_render', array( $this, 'print_admin_bar_styles' ) ); add_filter( 'removable_query_args', array( $this, 'filter_removable_query_args' ) ); - 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. + * Init. */ - 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; - } + function init() { + $this->post_type = new Post_Type( $this ); + $this->hooks(); + add_filter( 'customize_save_response', array( $this, 'add_snapshot_var_to_customize_save' ), 10, 2 ); + if ( $this->read_current_snapshot_uuid() ) { + $this->load_snapshot(); } - $this->current_snapshot_uuid = null; - return false; } /** - * Load snapshot. + * Load Snapshot. */ public function load_snapshot() { $this->ensure_customize_manager(); - $this->snapshot = new Customize_Snapshot( $this, $this->current_snapshot_uuid ); - - if ( ! $this->should_import_and_preview_snapshot( $this->snapshot ) ) { - return; - } - - $this->add_widget_setting_preview_filters(); - $this->add_nav_menu_setting_preview_filters(); - - /* - * Populate post values. - * - * Note we have to defer until setup_theme since the transaction - * can be set beforehand, and wp_magic_quotes() would not have - * been called yet, resulting in a $_POST['customized'] that is - * double-escaped. Note that this happens at priority 1, which - * is immediately after Customize_Snapshot_Manager::store_customized_post_data - * which happens at setup_theme priority 0, so that the initial - * POST data can be preserved. - */ - if ( did_action( 'setup_theme' ) ) { - $this->import_snapshot_data(); - } else { - add_action( 'setup_theme', array( $this, 'import_snapshot_data' ) ); - } - - // 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 ); - } + $this->snapshot = new Customize_Snapshot( $this ); } /** - * Setup previewing of Ajax requests in the Customizer preview. + * Add extra changeset variable to publish date. * - * @global \WP_Customize_Manager $wp_customize + * @param array $response Ajax response. + * @param \WP_Customize_Manager $customize_manager customize manager object. + * + * @return array Response. */ - public function setup_preview_ajax_requests() { - global $wp_customize, $pagenow; - - /* - * 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(); - } - - // 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 ); - } - - $wp_customize->remove_preview_signature(); + public function add_snapshot_var_to_customize_save( $response, $customize_manager ) { + $changeset_post = get_post( $customize_manager->changeset_post_id() ); + $response['edit_link'] = $this->snapshot->get_edit_link( $changeset_post->ID ); + $response['publish_date'] = $changeset_post->post_date; + $response['title'] = $changeset_post->post_title; + return $response; } /** - * Attempt to convert the current request environment into another environment. - * - * @global \WP $wp + * Read the current snapshot UUID from the request. * - * @return bool Whether the override was applied. + * @returns bool Whether a valid snapshot was read. */ - 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; + public function read_current_snapshot_uuid() { + $customize_arg = $this->get_customize_uuid_param(); + $frontend_arg = $this->get_front_uuid_param(); + if ( isset( $_REQUEST[ $customize_arg ] ) ) { + $uuid = $_REQUEST[ $customize_arg ]; // WPCS: input var ok. + } elseif ( isset( $_REQUEST[ $frontend_arg ] ) ) { + $uuid = $_REQUEST[ $frontend_arg ]; // WPCS: input var ok. } - // 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; + if ( isset( $uuid ) ) { + $uuid = sanitize_key( wp_unslash( $uuid ) ); + if ( static::is_valid_uuid( $uuid ) ) { + $this->current_snapshot_uuid = $uuid; + return true; + } } - + $this->current_snapshot_uuid = null; return false; } @@ -266,7 +186,11 @@ public function ensure_customize_manager() { global $wp_customize; if ( empty( $wp_customize ) || ! ( $wp_customize instanceof \WP_Customize_Manager ) ) { require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' ); - $wp_customize = new \WP_Customize_Manager(); // WPCS: override ok. + if ( null !== $this->current_snapshot_uuid ) { + $wp_customize = new \WP_Customize_Manager( array( 'changeset_uuid' => $this->current_snapshot_uuid ) ); // WPCS: override ok. + } else { + $wp_customize = new \WP_Customize_Manager(); // WPCS: override ok. + } } $this->customize_manager = $wp_customize; @@ -276,6 +200,8 @@ public function ensure_customize_manager() { * Is previewing another theme. * * @return bool Whether theme is active. + * + * @todo move to back compat? */ public function is_theme_active() { if ( empty( $this->customize_manager ) ) { @@ -284,268 +210,12 @@ public function is_theme_active() { return $this->customize_manager->get_stylesheet() === $this->original_stylesheet; } - /** - * Determine whether the current snapshot can be previewed. - * - * @param Customize_Snapshot $snapshot Snapshot to check. - * @return true|\WP_Error Returns true if previewable, or `WP_Error` if cannot. - */ - public function should_import_and_preview_snapshot( Customize_Snapshot $snapshot ) { - global $pagenow; - - // Ignore if in the admin, but not Admin Ajax or Customizer. - if ( is_admin() && ! in_array( $pagenow, array( 'admin-ajax.php', 'customize.php' ), true ) ) { - return false; - } - - if ( is_wp_error( $this->get_theme_switch_error( $snapshot ) ) ) { - return false; - } - - // Abort if doing customize_save. - if ( $this->doing_customize_save_ajax() ) { - return false; - } - - // Abort if the snapshot was already published. - if ( $snapshot->saved() && 'publish' === get_post_status( $snapshot->post() ) ) { - return false; - } - - /* - * Prevent clobbering existing values (or previewing non-snapshotted values on frontend). - * Note that wp.customize.Snapshots.extendPreviewerQuery() will extend the - * previewer data to include the current snapshot UUID. - */ - if ( $this->customize_manager && count( $this->customize_manager->unsanitized_post_values() ) > 0 ) { - return false; - } - - return true; - } - - /** - * Populate post values and $_POST['customized'] wth the snapshot's data. - * - * Plugins used to have to dynamically register settings by inspecting the - * $_POST['customized'] var and manually re-parse and inspect to see if it - * contains settings that wouldn't be registered otherwise. This ensures - * that these plugins will continue to work. - * - * Note that this can't be called prior to the setup_theme action or else - * magic quotes may end up getting added twice. - * - * @see Customize_Snapshot_Manager::should_import_and_preview_snapshot() - */ - public function import_snapshot_data() { - /* - * We don't merge the snapshot data with any existing existing unsanitized - * post values since should_import_and_preview_snapshot returns false if - * there is any existing data in the Customizer state. This is to prevent - * clobbering existing values (or previewing non-snapshotted values on frontend). - * Note that wp.customize.Snapshots.extendPreviewerQuery() will extend the - * previewer data to include the current snapshot UUID. - */ - $snapshot_values = array_filter( - wp_list_pluck( $this->snapshot->data(), 'value' ), - function( $value ) { - return ! is_null( $value ); - } - ); - - // Populate input vars for back-compat. - $_POST['customized'] = wp_slash( wp_json_encode( $snapshot_values ) ); - // @codingStandardsIgnoreStart - $_REQUEST['customized'] = $_POST['customized']; - // @codingStandardsIgnoreEnd - - foreach ( $snapshot_values as $setting_id => $value ) { - $this->customize_manager->set_post_value( $setting_id, $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. - * - * Note that this happens at `wp_loaded` action with priority 11 so that we - * can look at whether the `customize_preview_init` action was done. - */ - public function preview_snapshot_settings() { - if ( $this->is_previewing_settings() ) { - return; - } - $this->previewing_settings = true; - - /* - * Note that we need to preview the settings outside the Customizer preview - * and in the Customizer pane itself so we can load a previous snapshot - * into the Customizer. We have to prevent the previews from being added - * in the case of a customize_save action because then update_option() - * may short-circuit because it will detect that there are no changes to - * make. - */ - foreach ( $this->snapshot->settings() as $setting ) { - $setting->preview(); - $setting->dirty = true; - } - } - - /** - * Add filters for previewing widgets on the frontend. - */ - function add_widget_setting_preview_filters() { - /* - * Add WP_Customize_Widget component hooks which were short-circuited in 4.5 (r36611 for #35895). - * See https://core.trac.wordpress.org/ticket/35895 - */ - if ( isset( $this->customize_manager->widgets ) && ! current_user_can( 'edit_theme_options' ) ) { - $hooks = array( - 'customize_dynamic_setting_args' => array( - 'callback' => array( $this->customize_manager->widgets, 'filter_customize_dynamic_setting_args' ), - 'priority' => 10, - ), - 'widgets_init' => array( - 'callback' => array( $this->customize_manager->widgets, 'register_settings' ), - 'priority' => 95, - ), - 'customize_register' => array( - 'callback' => array( $this->customize_manager->widgets, 'schedule_customize_register' ), - 'priority' => 1, - ), - ); - foreach ( $hooks as $hook_name => $hook_args ) { - // Note that add_action()/has_action() are just aliases for add_filter()/has_filter(). - if ( ! has_filter( $hook_name, $hook_args['callback'] ) ) { - add_filter( $hook_name, $hook_args['callback'], $hook_args['priority'], PHP_INT_MAX ); - } - } - } - - /* - * Disable routine which fails because \WP_Customize_Manager::setup_theme() is - * never called in a frontend preview context, whereby the original_stylesheet - * is never set and so \WP_Customize_Manager::is_theme_active() will thus - * always return true because get_stylesheet() !== null. - * - * The action being removed is responsible for adding an option_sidebar_widgets - * filter \WP_Customize_Widgets::filter_option_sidebars_widgets_for_theme_switch() - * which causes the sidebars_widgets to be overridden with a global variable. - */ - if ( ! is_admin() ) { - remove_action( 'wp_loaded', array( $this->customize_manager->widgets, 'override_sidebars_widgets_for_theme_switch' ) ); - } - } - - /** - * Add filters for previewing nav menus on the frontend. - */ - public function add_nav_menu_setting_preview_filters() { - if ( isset( $this->customize_manager->nav_menus ) && ! current_user_can( 'edit_theme_options' ) ) { - $hooks = array( - 'customize_register' => array( - 'callback' => array( $this->customize_manager->nav_menus, 'customize_register' ), - 'priority' => 11, - ), - 'customize_dynamic_setting_args' => array( - 'callback' => array( $this->customize_manager->nav_menus, 'filter_dynamic_setting_args' ), - 'priority' => 10, - ), - 'customize_dynamic_setting_class' => array( - 'callback' => array( $this->customize_manager->nav_menus, 'filter_dynamic_setting_class' ), - 'priority' => 10, - ), - 'wp_nav_menu_args' => array( - 'callback' => array( $this->customize_manager->nav_menus, 'filter_wp_nav_menu_args' ), - 'priority' => 1000, - ), - 'wp_nav_menu' => array( - 'callback' => array( $this->customize_manager->nav_menus, 'filter_wp_nav_menu' ), - 'priority' => 10, - ), - ); - foreach ( $hooks as $hook_name => $hook_args ) { - // Note that add_action()/has_action() are just aliases for add_filter()/has_filter(). - if ( ! has_filter( $hook_name, $hook_args['callback'] ) ) { - add_filter( $hook_name, $hook_args['callback'], $hook_args['priority'], PHP_INT_MAX ); - } - } - } - - if ( isset( $this->customize_manager->nav_menus ) ) { - add_action( 'customize_register', array( $this, 'preview_early_nav_menus_in_customizer' ), 9 ); - } - } - - /** - * Preview nav menu settings early so that the sections and controls for snapshot values will be added properly. - * - * This must happen at `customize_register` priority prior to 11 which is when `WP_Customize_Nav_Menus::customize_register()` runs. - * This is only relevant when accessing the Customizer app (customize.php), as this is where sections/controls matter. - * - * @see \WP_Customize_Nav_Menus::customize_register() - */ - public function preview_early_nav_menus_in_customizer() { - if ( ! is_admin() ) { - return; - } - $this->customize_manager->add_dynamic_settings( array_keys( $this->snapshot()->data() ) ); - foreach ( $this->snapshot->settings() as $setting ) { - $is_nav_menu_setting = ( - $setting instanceof \WP_Customize_Nav_Menu_Setting - || - $setting instanceof \WP_Customize_Nav_Menu_Item_Setting - || - preg_match( '/^nav_menu_locations\[/', $setting->id ) - ); - if ( $is_nav_menu_setting ) { - $setting->preview(); - - /* - * The following is redundant because it will be done later in - * Customize_Snapshot_Manager::preview_snapshot_settings(). - * Also note that the $setting instance here will likely be - * blown away inside of WP_Customize_Nav_Menus::customize_register(), - * when add_setting is called there. What matters here is that - * preview() is called on the setting _before_ the logic inside - * WP_Customize_Nav_Menus::customize_register() runs, so that - * the nav menu sections will be created. - */ - $setting->dirty = true; - } - } - } - /** * Add snapshot UUID the Customizer return URL. * * If the Customizer was loaded with a snapshot UUID, let the return URL include this snapshot. + * + * @todo move to back compat? */ public function add_snapshot_uuid_to_return_url() { $should_add_snapshot_uuid = ( @@ -556,64 +226,15 @@ public function add_snapshot_uuid_to_return_url() { false === strpos( $this->customize_manager->get_return_url(), '/wp-admin/' ) ); if ( $should_add_snapshot_uuid ) { + $args_name = $this->get_front_uuid_param(); $args = array( - 'customize_snapshot_uuid' => $this->current_snapshot_uuid, + $args_name => $this->current_snapshot_uuid, ); $return_url = add_query_arg( array_map( 'rawurlencode', $args ), $this->customize_manager->get_return_url() ); $this->customize_manager->set_return_url( $return_url ); } } - /** - * Show the theme switch error if there is one. - */ - public function show_theme_switch_error() { - if ( empty( $this->snapshot ) ) { - return; - } - $error = $this->get_theme_switch_error( $this->snapshot ); - if ( is_wp_error( $error ) ) { - wp_die( esc_html( $error->get_error_message() ) ); - } - } - - /** - * Redirect when preview is not allowed for the current theme. - * - * @param Customize_Snapshot $snapshot Snapshot to check. - * @return \WP_Error|null - */ - public function get_theme_switch_error( Customize_Snapshot $snapshot ) { - - // Loading a snapshot into the context of a theme switch is not supported. - if ( ! $this->is_theme_active() ) { - return new \WP_Error( 'snapshot_theme_switch', __( 'Snapshot cannot be previewed when previewing a theme switch.', 'customize-snapshots' ) ); - } - - $snapshot_post = $snapshot->post(); - if ( ! $snapshot_post ) { - return null; - } - - $snapshot_theme = get_post_meta( $snapshot_post->ID, '_snapshot_theme', true ); - if ( ! empty( $snapshot_theme ) && get_stylesheet() !== $snapshot_theme ) { - return new \WP_Error( 'snapshot_theme_switched', __( 'Snapshot requested was made for a different theme and cannot be previewed with the current theme.', 'customize-snapshots' ) ); - } - - return null; - } - - /** - * Check whether customize_publish capability is granted in customize_save. - */ - public function check_customize_publish_authorization() { - if ( $this->doing_customize_save_ajax() && ! current_user_can( 'customize_publish' ) ) { - wp_send_json_error( array( - 'error' => 'customize_publish_unauthorized', - ) ); - } - } - /** * Encode JSON with pretty formatting. * @@ -622,9 +243,9 @@ public function check_customize_publish_authorization() { */ static public function encode_json( $value ) { $flags = 0; - if ( defined( '\JSON_PRETTY_PRINT' ) ) { - $flags |= \JSON_PRETTY_PRINT; - } + + $flags |= \JSON_PRETTY_PRINT; + if ( defined( '\JSON_UNESCAPED_SLASHES' ) ) { $flags |= \JSON_UNESCAPED_SLASHES; } @@ -646,17 +267,23 @@ public function enqueue_controls_scripts() { wp_enqueue_style( 'customize-snapshots' ); wp_enqueue_script( 'customize-snapshots' ); + + $post = null; + if ( $this->snapshot ) { - $post = $this->snapshot->post(); - $this->override_post_date_default_data( $post ); + $post_id = $this->customize_manager->changeset_post_id(); + $post = get_post( $post_id ); + if ( $post instanceof \WP_Post ) { + $this->override_post_date_default_data( $post ); + $edit_link = $this->snapshot->get_edit_link( $post ); + } } // Script data array. $exports = apply_filters( 'customize_snapshots_export_data', array( - 'action' => self::AJAX_ACTION, - 'uuid' => $this->snapshot ? $this->snapshot->uuid() : self::generate_uuid(), - 'editLink' => isset( $post ) ? get_edit_post_link( $post, 'raw' ) : '', + 'editLink' => isset( $edit_link ) ? $edit_link : '', 'publishDate' => isset( $post->post_date ) ? $post->post_date : '', + 'title' => isset( $post->post_title ) ? $post->post_title : '', 'postStatus' => isset( $post->post_status ) ? $post->post_status : '', 'currentUserCanPublish' => current_user_can( 'customize_publish' ), 'initialServerDate' => current_time( 'mysql', false ), @@ -664,96 +291,46 @@ public function enqueue_controls_scripts() { 'i18n' => array( 'saveButton' => __( 'Save', 'customize-snapshots' ), 'updateButton' => __( 'Update', 'customize-snapshots' ), - 'scheduleButton' => __( 'Schedule', 'customize-snapshots' ), 'submit' => __( 'Submit', 'customize-snapshots' ), 'submitted' => __( 'Submitted', 'customize-snapshots' ), - 'publish' => __( 'Publish', 'customize-snapshots' ), - 'published' => __( 'Published', 'customize-snapshots' ), 'permsMsg' => array( 'save' => __( 'You do not have permission to publish changes, but you can create a snapshot by clicking the "Save" button.', 'customize-snapshots' ), 'update' => __( 'You do not have permission to publish changes, but you can modify this snapshot by clicking the "Update" button.', 'customize-snapshots' ), ), + 'aysMsg' => __( 'Changes that you made may not be saved.', 'customize-snapshots' ), 'errorMsg' => __( 'The snapshot could not be saved.', 'customize-snapshots' ), 'errorTitle' => __( 'Error', 'customize-snapshots' ), 'collapseSnapshotScheduling' => __( 'Collapse snapshot scheduling', 'customize-snapshots' ), 'expandSnapshotScheduling' => __( 'Expand snapshot scheduling', 'customize-snapshots' ), ), - 'snapshotExists' => ( $this->snapshot && $this->snapshot->saved() ), ) ); - // Export data to JS. - wp_scripts()->add_data( - $this->plugin->slug, - 'data', - sprintf( 'var _customizeSnapshots = %s;', wp_json_encode( $exports ) ) - ); - } - - /** - * Set up Customizer preview. - */ - public function customize_preview_init() { - add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_preview_scripts' ) ); + wp_localize_script( 'customize-snapshots', '_customizeSnapshotsSettings', $exports ); } /** - * Enqueue Customizer preview scripts. + * Enqueue admin scripts. * - * @global \WP_Customize_Manager $wp_customize - */ - public function enqueue_preview_scripts() { - global $wp_customize; - - $handle = 'customize-snapshots-preview'; - wp_enqueue_script( $handle ); - wp_enqueue_style( $handle ); - - $exports = array( - 'home_url' => wp_parse_url( home_url( '/' ) ), - 'rest_api_url' => wp_parse_url( rest_url( '/' ) ), - 'admin_ajax_url' => wp_parse_url( admin_url( 'admin-ajax.php' ) ), - 'initial_dirty_settings' => array_keys( $wp_customize->unsanitized_post_values() ), - ); - wp_add_inline_script( - $handle, - sprintf( 'CustomizeSnapshotsPreview.init( %s )', wp_json_encode( $exports ) ), - 'after' - ); - } - - /** - * Enqueue Customizer frontend scripts. - */ - public function enqueue_frontend_scripts() { - if ( ! $this->snapshot || is_customize_preview() ) { - return; - } - $handle = 'customize-snapshots-frontend'; - wp_enqueue_script( $handle ); - - $exports = array( - 'uuid' => $this->snapshot ? $this->snapshot->uuid() : null, - 'home_url' => wp_parse_url( home_url( '/' ) ), - 'l10n' => array( - 'restoreSessionPrompt' => __( 'It seems you may have inadvertently navigated away from previewing a customized state. Would you like to restore the snapshot context?', 'customize-snapshots' ), - ), - ); - wp_add_inline_script( - $handle, - sprintf( 'CustomizeSnapshotsFrontend.init( %s )', wp_json_encode( $exports ) ), - 'after' - ); - } - - /** - * Include the snapshot nonce in the Customizer nonces. + * These files control the behavior and styling of links to remove settings. + * Published snapshots can't be edited, so these files are not needed on those pages. * - * @param array $nonces Nonces. - * @return array Nonces. + * @param String $hook Current page in admin. */ - public function filter_customize_refresh_nonces( $nonces ) { - $nonces['snapshot'] = wp_create_nonce( self::AJAX_ACTION ); - return $nonces; + public function enqueue_admin_scripts( $hook ) { + global $post; + $handle = 'customize-snapshots-admin'; + if ( ( 'post.php' === $hook ) && isset( $post->post_type ) && ( $this->get_post_type() === $post->post_type ) && ( 'publish' !== $post->post_status ) ) { + wp_enqueue_script( $handle ); + wp_enqueue_style( $handle ); + $exports = array( + 'deleteInputName' => $this->get_post_type() . '_remove_settings[]', + ); + wp_add_inline_script( + $handle, + sprintf( 'CustomizeSnapshotsAdmin.init( %s )', wp_json_encode( $exports ) ), + 'after' + ); + } } /** @@ -765,74 +342,6 @@ public function snapshot() { return $this->snapshot; } - /** - * Publish the snapshot snapshots via AJAX. - * - * Fires at `customize_save_after` to update and publish the snapshot. - * The logic in here is the inverse of save_settings_with_publish_snapshot. - * - * @see Customize_Snapshot_Manager::save_settings_with_publish_snapshot() - * - * @return bool Whether the snapshot was saved successfully. - */ - public function publish_snapshot_with_customize_save_after() { - $that = $this; - - if ( ! $this->snapshot || ! $this->doing_customize_save_ajax() ) { - return false; - } - - // This should never be reached due to Customize_Snapshot_Manager::check_customize_publish_authorization(). - if ( ! current_user_can( 'customize_publish' ) ) { - return false; - } - - $settings_data = array_map( - function( $value ) { - return compact( 'value' ); - }, - $this->customize_manager->unsanitized_post_values() - ); - $result = $this->snapshot->set( $settings_data ); - if ( ! empty( $result['errors'] ) ) { - add_filter( 'customize_save_response', function( $response ) use ( $result, $that ) { - $response['snapshot_errors'] = $that->prepare_errors_for_response( $result['errors'] ); - return $response; - } ); - return false; - } - - if ( ! $this->snapshot->post() || 'publish' !== $this->snapshot->post()->post_status ) { - $args = array( - 'status' => 'publish', - ); - - // Ensure a scheduled Snapshot is published. - if ( $this->snapshot->post() && 'future' === $this->snapshot->post()->post_status ) { - $args['edit_date'] = true; - $args['post_date'] = current_time( 'mysql', false ); - $args['post_date_gmt'] = current_time( 'mysql', true ); - } - - $r = $this->snapshot->save( $args ); - if ( is_wp_error( $r ) ) { - add_filter( 'customize_save_response', function( $response ) use ( $r, $that ) { - $response['snapshot_errors'] = $that->prepare_errors_for_response( $r ); - return $response; - } ); - return false; - } - } - - // Send the new UUID to the client for the next snapshot. - $class = __CLASS__; // For PHP 5.3. - add_filter( 'customize_save_response', function( $data ) use ( $class ) { - $data['new_customize_snapshot_uuid'] = $class::generate_uuid(); - return $data; - } ); - return true; - } - /** * Prepare snapshot post content for publishing. * @@ -847,7 +356,7 @@ public function prepare_snapshot_post_content_for_publish( $data ) { $is_publishing_snapshot = ( isset( $data['post_type'] ) && - Post_Type::SLUG === $data['post_type'] + $this->get_post_type() === $data['post_type'] && 'publish' === $data['post_status'] && @@ -890,300 +399,6 @@ public function filter_removable_query_args( $query_args ) { return $query_args; } - /** - * Publish snapshot changes when snapshot post is being published. - * - * The logic in here is the inverse of to publish_snapshot_with_customize_save_after. - * - * The meat of the logic that manipulates the post_content and validates the settings - * needs to be done in wp_insert_post_data filter in like a - * filter_insert_post_data_to_validate_published_snapshot method? This would - * have the benefit of reducing one wp_insert_post() call. - * - * @todo Consider using wp_insert_post_data to prevent double calls to wp_insert_post(). - * @see Customize_Snapshot_Manager::publish_snapshot_with_customize_save_after() - * - * @param string $new_status New status. - * @param string $old_status Old status. - * @param \WP_Post $post Post object. - * @return bool Whether the settings were saved. - */ - public function save_settings_with_publish_snapshot( $new_status, $old_status, $post ) { - - // Abort if not transitioning a snapshot post to publish from a non-publish status. - if ( Post_Type::SLUG !== $post->post_type || 'publish' !== $new_status || $new_status === $old_status ) { - return false; - } - - $this->ensure_customize_manager(); - - if ( $this->doing_customize_save_ajax() ) { - // Short circuit because customize_save ajax call is changing status. - return false; - } - - if ( ! did_action( 'customize_register' ) ) { - /* - * When running from CLI or Cron, we have to remove the action because - * it will get added with a default priority of 10, after themes and plugins - * have already done add_action( 'customize_register' ), resulting in them - * being called first at the priority 10. So we manually call the - * prerequisite function WP_Customize_Manager::register_controls() and - * remove it from being called when the customize_register action fires. - */ - remove_action( 'customize_register', array( $this->customize_manager, 'register_controls' ) ); - $this->customize_manager->register_controls(); - - /* - * Unfortunate hack to prevent \WP_Customize_Widgets::customize_register() - * from calling preview() on settings. This needs to be cleaned up in core. - * It is important for previewing to be prevented because if an option has - * a filter it will short-circuit when an update is attempted since it - * detects that there is no change to be put into the DB. - * See: https://github.com/xwp/wordpress-develop/blob/e8c58c47db1421a1d0b2afa9ad4b9eb9e1e338e0/src/wp-includes/class-wp-customize-widgets.php#L208-L217 - */ - if ( ! defined( 'DOING_AJAX' ) ) { - define( 'DOING_AJAX', true ); - } - $_REQUEST['action'] = 'customize_save'; - - /** This action is documented in wp-includes/class-wp-customize-manager.php */ - do_action( 'customize_register', $this->customize_manager ); - - // undefine( 'DOING_AJAX' )... just kidding. This is the end of the unfortunate hack and it should be fixed in Core. - unset( $_REQUEST['action'] ); - } - $snapshot_content = $this->post_type->get_post_content( $post ); - - if ( method_exists( $this->customize_manager, 'validate_setting_values' ) ) { - /** This action is documented in wp-includes/class-wp-customize-manager.php */ - do_action( 'customize_save_validation_before', $this->customize_manager ); - } - - $setting_ids = array_keys( $snapshot_content ); - $this->customize_manager->add_dynamic_settings( $setting_ids ); - - /** This action is documented in wp-includes/class-wp-customize-manager.php */ - do_action( 'customize_save', $this->customize_manager ); - - /** - * Settings to save. - * - * @var \WP_Customize_Setting[] - */ - $settings = array(); - - $publish_error_count = 0; - foreach ( $snapshot_content as $setting_id => &$setting_params ) { - - // Missing value error. - if ( ! isset( $setting_params['value'] ) || is_null( $setting_params['value'] ) ) { - if ( ! is_array( $setting_params ) ) { - if ( ! empty( $setting_params ) ) { - $setting_params = array( 'value' => $setting_params ); - } else { - $setting_params = array(); - } - } - $setting_params['publish_error'] = 'null_value'; - $publish_error_count += 1; - continue; - } - - // Unrecognized setting error. - $this->customize_manager->set_post_value( $setting_id, $setting_params['value'] ); - $setting = $this->customize_manager->get_setting( $setting_id ); - if ( ! ( $setting instanceof \WP_Customize_Setting ) ) { - $setting_params['publish_error'] = 'unrecognized_setting'; - $publish_error_count += 1; - continue; - } - - // Validate setting value. - if ( method_exists( $setting, 'validate' ) ) { - $validity = $setting->validate( $setting_params['value'] ); - if ( is_wp_error( $validity ) ) { - $setting_params['publish_error'] = $validity->get_error_code(); - $publish_error_count += 1; - continue; - } - } - - // Validate sanitized setting value. - $sanitized_value = $setting->sanitize( $setting_params['value'] ); - if ( is_null( $sanitized_value ) || is_wp_error( $sanitized_value ) ) { - $setting_params['publish_error'] = is_wp_error( $sanitized_value ) ? $sanitized_value->get_error_code() : 'invalid_value'; - $publish_error_count += 1; - continue; - } - - $settings[] = $setting; - unset( $setting_params['publish_error'] ); - } - - // Handle error scenarios. - if ( $publish_error_count > 0 ) { - $update_setting_args = array( - 'ID' => $post->ID, - 'post_content' => Customize_Snapshot_Manager::encode_json( $snapshot_content ), - 'post_status' => 'pending', - ); - wp_update_post( wp_slash( $update_setting_args ) ); - update_post_meta( $post->ID, 'snapshot_error_on_publish', $publish_error_count ); - - add_filter( 'redirect_post_location', function( $location ) { - $location = add_query_arg( 'snapshot_error_on_publish', '1', $location ); - return $location; - } ); - return false; - } - - /* - * Change all setting capabilities temporarily to 'exist' to allow them to - * be saved regardless of current user, such as when WP-Cron is publishing - * the snapshot post if it was scheduled. It is safe to do this because - * a setting can only be written into a snapshot by users who have the - * capability, so after it has been added to a snapshot it is good to commit. - */ - $existing_caps = wp_list_pluck( $settings, 'capability' ); - foreach ( $settings as $setting ) { - $setting->capability = 'exist'; - } - - // Persist the settings in the DB. - foreach ( $settings as $setting ) { - $setting->save(); - } - - // Restore setting capabilities. - foreach ( $existing_caps as $setting_id => $existing_cap ) { - $settings[ $setting_id ]->capability = $existing_cap; - } - - /** This action is documented in wp-includes/class-wp-customize-manager.php */ - do_action( 'customize_save_after', $this->customize_manager ); - - // Remove any previous error on setting. - delete_post_meta( $post->ID, 'snapshot_error_on_publish' ); - - return true; - } - - /** - * Update snapshots via AJAX. - */ - public function handle_update_snapshot_request() { - if ( ! check_ajax_referer( self::AJAX_ACTION, 'nonce', false ) ) { - status_header( 400 ); - wp_send_json_error( 'bad_nonce' ); - } elseif ( ! current_user_can( 'customize' ) ) { - status_header( 403 ); - wp_send_json_error( 'customize_not_allowed' ); - } elseif ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'POST' !== $_SERVER['REQUEST_METHOD'] ) { // WPCS: input var ok. - status_header( 405 ); - wp_send_json_error( 'bad_method' ); - } elseif ( empty( $this->current_snapshot_uuid ) ) { - status_header( 400 ); - wp_send_json_error( 'invalid_customize_snapshot_uuid' ); - } elseif ( 0 === count( $this->customize_manager->unsanitized_post_values() ) ) { - status_header( 400 ); - wp_send_json_error( 'missing_snapshot_customized' ); - } - - if ( isset( $_POST['status'] ) ) { // WPCS: input var ok. - $status = sanitize_key( $_POST['status'] ); - } else { - $status = 'draft'; - } - if ( ! in_array( $status, array( 'draft', 'pending', 'future' ), true ) ) { - status_header( 400 ); - wp_send_json_error( 'bad_status' ); - } - if ( 'future' === $status && ! current_user_can( 'customize_publish' ) ) { - status_header( 400 ); - wp_send_json_error( 'customize_not_allowed' ); - } - $publish_date = isset( $_POST['publish_date'] ) ? $_POST['publish_date'] : ''; - if ( 'future' === $status ) { - $publish_date_obj = new \DateTime( $publish_date ); - $current_date = new \DateTime(); - if ( empty( $publish_date ) || ! $publish_date_obj || $publish_date > $current_date ) { - status_header( 400 ); - wp_send_json_error( 'bad_schedule_time' ); - } - } - - // Prevent attempting to modify a "locked" snapshot (a published one). - $post = $this->snapshot->post(); - if ( $post && 'publish' === $post->post_status ) { - wp_send_json_error( array( - 'errors' => array( - 'already_published' => array( - 'message' => __( 'The snapshot has already published so it is locked.', 'customize-snapshots' ), - ), - ), - ) ); - } - - // Set the snapshot UUID. - $post_type = get_post_type_object( Post_Type::SLUG ); - $authorized = ( $post ? - current_user_can( $post_type->cap->edit_post, $post->ID ) : - current_user_can( 'customize' ) - ); - if ( ! $authorized ) { - status_header( 403 ); - wp_send_json_error( 'unauthorized' ); - } - - $data = array( - 'errors' => null, - ); - $settings_data = array_map( - function( $value ) { - return compact( 'value' ); - }, - $this->customize_manager->unsanitized_post_values() - ); - $r = $this->snapshot->set( $settings_data ); - if ( method_exists( $this->customize_manager, 'prepare_setting_validity_for_js' ) ) { - $data['setting_validities'] = array_map( - array( $this->customize_manager, 'prepare_setting_validity_for_js' ), - $r['validities'] - ); - } - - if ( $r['errors'] ) { - $data['errors'] = $this->prepare_errors_for_response( $r['errors'] ); - wp_send_json_error( $data ); - } - $args = array( - 'status' => $status, - ); - $args['edit_date'] = current_time( 'mysql' ); - - if ( isset( $publish_date_obj ) && 'future' === $status ) { - $args['post_date'] = $publish_date_obj->format( 'Y-m-d H:i:s' ); - $args['post_date_gmt'] = '0000-00-00 00:00:00'; - } else { - $args['post_date_gmt'] = $args['post_date'] = '0000-00-00 00:00:00'; - } - $r = $this->snapshot->save( $args ); - - $post = $this->snapshot->post(); - if ( $post ) { - $data['edit_link'] = get_edit_post_link( $post, 'raw' ); - $data['snapshot_publish_date'] = $post->post_date; - } - - if ( is_wp_error( $r ) ) { - $data['errors'] = $this->prepare_errors_for_response( $r ); - wp_send_json_error( $data ); - } - - wp_send_json_success( $data ); - } - /** * Prepare a WP_Error for sending to JS. * @@ -1280,12 +495,12 @@ public function replace_customize_link( $wp_admin_bar ) { return; } - // Remove customize_snapshot_uuuid query param from url param to be previewed in Customizer. + // Remove customize_snapshot_uuid 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'] ); + $preview_url_query_params['url'] = remove_query_arg( array( $this->get_front_uuid_param() ), $preview_url_query_params['url'] ); $customize_node->href = preg_replace( '/(?<=\?).*?(?=#|$)/', build_query( $preview_url_query_params ), @@ -1295,7 +510,7 @@ public function replace_customize_link( $wp_admin_bar ) { // 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 ), + array( $this->get_customize_uuid_param() => $this->current_snapshot_uuid ), $customize_node->href ); @@ -1335,7 +550,7 @@ public function add_post_edit_screen_link( $wp_admin_bar ) { $wp_admin_bar->add_menu( array( 'id' => 'inspect-customize-snapshot', 'title' => __( 'Inspect Snapshot', 'customize-snapshots' ), - 'href' => get_edit_post_link( $post->ID, 'raw' ), + 'href' => $this->snapshot->get_edit_link( $post ), 'meta' => array( 'class' => 'ab-item ab-customize-snapshots-item', ), @@ -1354,7 +569,7 @@ public function add_snapshot_exit_link( $wp_admin_bar ) { $wp_admin_bar->add_menu( array( 'id' => 'exit-customize-snapshot', 'title' => __( 'Exit Snapshot Preview', 'customize-snapshots' ), - 'href' => remove_query_arg( 'customize_snapshot_uuid' ), + 'href' => remove_query_arg( $this->get_front_uuid_param() ), 'meta' => array( 'class' => 'ab-item ab-customize-snapshots-item', ), @@ -1395,9 +610,7 @@ public function remove_all_non_snapshot_admin_bar_links( $wp_admin_bar ) { * Underscore (JS) templates for dialog windows. */ public function render_templates() { - $data = $this->get_month_choices(); - - $description = __( 'Schedule your changes to publish (go live) at a future date.', 'customize-snapshots' ); + $this->add_edit_box_template(); ?> - + + + + + + + + + -
+ /** + * Add edit box template. + */ + public function add_edit_box_template() { + $data = $this->get_month_choices(); + ?> + - - - - - - post_type ) . '::SLUG' ); + } + + /** + * Get Frontend UUID param. + * + * @return string param. + */ + public function get_front_uuid_param() { + return constant( get_class( $this->post_type ) . '::FRONT_UUID_PARAM_NAME' ); + } + + /** + * Get customize uuid param name. + * + * @return string customize param name. + */ + public function get_customize_uuid_param() { + return constant( get_class( $this->post_type ) . '::CUSTOMIZE_UUID_PARAM_NAME' ); + } } diff --git a/php/class-customize-snapshot.php b/php/class-customize-snapshot.php index b8e6387d..357607e7 100644 --- a/php/class-customize-snapshot.php +++ b/php/class-customize-snapshot.php @@ -11,7 +11,9 @@ * Customize Snapshot Class * * Implements snapshots for Customizer settings + * This is dummy class main functionality is merged in core class \WP_Customize_Manager in 4.7 * + * @see https://core.trac.wordpress.org/changeset/38810 * @package CustomizeSnapshots */ class Customize_Snapshot { @@ -20,34 +22,10 @@ class Customize_Snapshot { * Customize_Snapshot_Manager instance. * * @access protected - * @var Customize_Snapshot_Manager + * @var Customize_Snapshot_Manager|Customize_Snapshot_Manager_Back_Compat */ protected $snapshot_manager; - /** - * Unique identifier. - * - * @access protected - * @var string - */ - protected $uuid; - - /** - * Store the snapshot data. - * - * @access protected - * @var array - */ - protected $data = array(); - - /** - * Post id for the current snapshot. - * - * @access protected - * @var \WP_Post|null - */ - protected $post_id = null; - /** * Initial loader. * @@ -55,30 +33,10 @@ class Customize_Snapshot { * * @throws Exception If the UUID is invalid. * - * @param Customize_Snapshot_Manager $snapshot_manager Customize snapshot bootstrap instance. - * @param string $uuid Snapshot unique identifier. + * @param Customize_Snapshot_Manager $snapshot_manager Customize snapshot bootstrap instance. */ - public function __construct( Customize_Snapshot_Manager $snapshot_manager, $uuid ) { + public function __construct( $snapshot_manager ) { $this->snapshot_manager = $snapshot_manager; - $this->data = array(); - - if ( ! Customize_Snapshot_Manager::is_valid_uuid( $uuid ) ) { - throw new Exception( __( 'You\'ve entered an invalid snapshot UUID.', 'customize-snapshots' ) ); - } - $this->uuid = $uuid; - $post = $this->post(); - if ( $post ) { - $this->data = $this->snapshot_manager->post_type->get_post_content( $post ); - } - } - - /** - * Get the snapshot uuid. - * - * @return string - */ - public function uuid() { - return $this->uuid; } /** @@ -87,232 +45,39 @@ public function uuid() { * @return \WP_Post|null Post or null. */ public function post() { - if ( ! $this->post_id ) { - $this->post_id = $this->snapshot_manager->post_type->find_post( $this->uuid ); - } - if ( $this->post_id ) { - return get_post( $this->post_id ); - } else { - return null; + $post_id = $this->snapshot_manager->customize_manager->changeset_post_id(); + if ( $post_id ) { + return get_post( $post_id ); } + return null; } /** - * Get the underlying data for the snapshot. - * - * @return array - */ - public function data() { - return $this->data; - } - - /** - * Return the Customizer settings corresponding to the data contained in the snapshot. - * - * @return \WP_Customize_Setting[] - */ - public function settings() { - $settings = array(); - $setting_ids = array_keys( $this->data ); - $this->snapshot_manager->customize_manager->add_dynamic_settings( $setting_ids ); - foreach ( $setting_ids as $setting_id ) { - $setting = $this->snapshot_manager->customize_manager->get_setting( $setting_id ); - if ( $setting ) { - $settings[] = $setting; - } - } - return $settings; - } - - /** - * Get the status of the snapshot. + * Get the snapshot uuid. * - * @return string|null + * @return string */ - public function status() { - $post = $this->post(); - return $post ? get_post_status( $post->ID ) : null; + public function uuid() { + return $this->snapshot_manager->customize_manager->changeset_uuid(); } /** - * Prepare snapshot data for saving. - * - * @see WP_Customize_Manager::set_post_value() - * @throws Exception When $settings_data is not an array of arrays. + * Get edit post link. * - * @param array $settings_data Settings data, mapping setting IDs to arrays containing `value` and optionally additional params. - * @return array { - * Result. + * @param int|\WP_Post $post_id Post. * - * @type null|\WP_Error $error Error object if error. - * @type array $sanitized Sanitized values. - * @type array $validities Setting validities. - * } + * @return null|string Post edit link. */ - public function set( array $settings_data ) { - $error = new \WP_Error(); - $result = array( - 'errors' => null, - 'sanitized' => array(), - 'validities' => array(), - ); - - $setting_ids = array_keys( $settings_data ); - $customize_manager = $this->snapshot_manager->customize_manager; - $customize_manager->add_dynamic_settings( $setting_ids ); - - // Check for recognized settings and authorized settings. - $unsanitized_values = array(); - $unrecognized_setting_ids = array(); - $unauthorized_setting_ids = array(); - foreach ( $settings_data as $setting_id => $setting_params ) { - if ( ! is_array( $setting_params ) ) { - throw new Exception( '$setting_params not an array' ); - } - $setting = $customize_manager->get_setting( $setting_id ); - if ( ! $setting ) { - $unrecognized_setting_ids[] = $setting_id; - } elseif ( ! current_user_can( $setting->capability ) ) { - $unauthorized_setting_ids[] = $setting_id; - } elseif ( array_key_exists( 'value', $setting_params ) ) { - $unsanitized_values[ $setting_id ] = $setting_params['value']; - } - } - - // Remove values that are unrecognized or unauthorized. - $unsanitized_values = wp_array_slice_assoc( - $unsanitized_values, - array_diff( - array_keys( $unsanitized_values ), - array_merge( $unrecognized_setting_ids, $unauthorized_setting_ids ) - ) - ); - - // Validate. - if ( method_exists( $customize_manager, 'validate_setting_values' ) ) { - $result['validities'] = $customize_manager->validate_setting_values( $unsanitized_values ); - } else { - // @codeCoverageIgnoreStart - $result['validities'] = array_map( - function( $sanitized ) { - if ( is_null( $sanitized ) ) { - return new \WP_Error( 'invalid_value', __( 'Invalid value', 'customize-snapshots' ) ); - } else { - return true; - } - }, - $unsanitized_values - ); - // @codeCoverageIgnoreEnd - } - $invalid_setting_ids = array_keys( array_filter( $result['validities'], function( $validity ) { - return is_wp_error( $validity ); - } ) ); - - // Sanitize. - foreach ( $unsanitized_values as $setting_id => $unsanitized_value ) { - $setting = $customize_manager->get_setting( $setting_id ); - if ( $setting ) { - $result['sanitized'][ $setting_id ] = $setting->sanitize( $unsanitized_value ); - } else { - $unrecognized_setting_ids[] = $setting_id; - } - } - - // Add errors. - if ( ! empty( $unauthorized_setting_ids ) ) { - $error->add( - 'unauthorized_settings', - sprintf( __( 'Unauthorized settings: %s', 'customize-snapshots' ), join( ',', $unauthorized_setting_ids ) ), - array( 'setting_ids' => $unauthorized_setting_ids ) - ); - } - if ( ! empty( $unrecognized_setting_ids ) ) { - $error->add( - 'unrecognized_settings', - sprintf( __( 'Unrecognized settings: %s', 'customize-snapshots' ), join( ',', $unrecognized_setting_ids ) ), - array( 'setting_ids' => $unrecognized_setting_ids ) - ); + public function get_edit_link( $post_id ) { + $has_filter = has_filter( 'get_edit_post_link', '__return_empty_string' ); + if ( $has_filter ) { + remove_filter( 'get_edit_post_link', '__return_empty_string' ); } - if ( 0 !== count( $invalid_setting_ids ) ) { - $code = 'invalid_values'; - $message = __( 'Invalid values', 'customize-snapshots' ); - $error->add( $code, $message, compact( 'invalid_setting_ids' ) ); + $link = get_edit_post_link( $post_id, 'raw' ); + if ( $has_filter ) { + add_filter( 'get_edit_post_link', '__return_empty_string' ); } - - if ( ! empty( $error->errors ) ) { - $result['errors'] = $error; - } else { - /* - * Note that somewhat unintuitively the unsanitized post values - * ($unsanitized_values) are stored as opposed to storing the - * sanitized ones ($result['sanitized']). It is still safe to do this - * because they have passed sanitization and validation here. The - * reason why we need to store the raw unsanitized values is so that - * the values can be re-populated into the post values for running - * through the sanitize, validate, and ultimately update logic. - * Once a value has gone through the sanitize logic, it may not be - * suitable for populating into a post value, especially widget - * instances which get exported with a JS value that has the instance - * data encoded, serialized, and hashed to prevent mutation. A - * sanitize filter for a widget instance will convert an encoded - * instance value into a regular instance array, and if this regular - * instance array is placed back into a post value, it will get - * rejected by the sanitize logic for not being an encoded value. - */ - foreach ( $settings_data as $setting_id => $setting_params ) { - if ( ! isset( $this->data[ $setting_id ] ) ) { - $this->data[ $setting_id ] = array(); - } - if ( ! array_key_exists( 'value', $setting_params ) ) { - $setting_params['value'] = null; - } - $this->data[ $setting_id ] = array_merge( $this->data[ $setting_id ], $setting_params ); - } - } - - return $result; + return $link; } - /** - * Return whether the snapshot was saved (created/inserted) yet. - * - * @return bool - */ - public function saved() { - return ! is_null( $this->post() ); - } - - /** - * Persist the data in the snapshot post content. - * - * @param array $args Args. - * @return true|\WP_Error - */ - public function save( array $args ) { - - /** - * Filter the snapshot's data before it's saved to 'post_content'. - * - * @param array $data Customizer snapshot data, with setting IDs mapped to an array - * containing a `value` array item and potentially other metadata. - * @param Customize_Snapshot $this Snapshot object. - */ - $this->data = apply_filters( 'customize_snapshot_save', $this->data, $this ); - - $result = $this->snapshot_manager->post_type->save( array_merge( - $args, - array( - 'uuid' => $this->uuid, - 'data' => $this->data, - 'theme' => $this->snapshot_manager->customize_manager->get_stylesheet(), - ) - ) ); - - if ( ! is_wp_error( $result ) ) { - $this->post_id = $result; - } - - return $result; - } } diff --git a/php/class-migrate.php b/php/class-migrate.php new file mode 100644 index 00000000..bfe28f71 --- /dev/null +++ b/php/class-migrate.php @@ -0,0 +1,216 @@ +plugin = $plugin; + if ( ! $plugin->compat && is_admin() && is_super_admin() ) { + $this->maybe_migrate(); + } + } + + /** + * Is already migrated or not. + * + * @return bool status of migration. + */ + public function is_migrated() { + $snapshot_migrate_option = get_option( self::KEY ); + return ! empty( $snapshot_migrate_option ); + } + + /** + * Migrate if wp version is 4.7 and above. + */ + public function maybe_migrate() { + if ( ! $this->is_migrated() ) { + add_action( 'admin_notices', array( $this, 'show_migration_notice' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_script' ) ); + add_action( 'wp_ajax_customize_snapshot_migration', array( $this, 'handle_migrate_changeset_request' ) ); + } + } + + /** + * Migrate 20 posts at a time. + */ + public function handle_migrate_changeset_request() { + check_ajax_referer( 'customize-snapshot-migration', 'nonce' ); + $limit = isset( $_REQUEST['limit'] ) ? absint( $_REQUEST['limit'] ) : 20; + $found_posts = $this->changeset_migrate( $limit ); + $remaining_post = ( $found_posts < $limit ) ? 0 : $found_posts - $limit; + $data = array( + 'remaining_posts' => $remaining_post, + ); + if ( ! $remaining_post ) { + update_option( self::KEY, 1 ); + } + wp_send_json_success( $data ); + } + + /** + * Print migration javascript script. + */ + public function enqueue_script() { + wp_enqueue_script( 'customize-snapshot-migrate' ); + } + + /** + * Show admin notice to migrate. + */ + public function show_migration_notice() { + ?> +
+

%s %s ', esc_html__( 'Click', 'customize-snapshots' ), esc_html__( 'Customize snapshot migration complete!', 'customize-snapshots' ), esc_html__( 'here', 'customize-snapshots' ), esc_html__( 'to start migration.', 'customize-snapshots' ) ); ?> +

+
+ 'customize_snapshot', + 'no_found_rows' => false, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'post_status' => array_keys( get_post_stati() ), + 'posts_per_page' => $limit, + 'fields' => 'ids', // We will use get_post() to fetch each posts. + ); + + if ( -1 === $limit ) { + $arg['no_found_rows'] = true; + } + + $query->query( $arg ); + if ( $dry_run ) { + return $query->posts; + } + + if ( ! empty( $query->posts ) ) { + $has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) ); + if ( $has_kses ) { + kses_remove_filters(); // Prevent KSES from corrupting JSON in post_content. + } + if ( ! class_exists( '\WP_Customize_Manager' ) ) { + require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' ); + } + foreach ( $query->posts as $id ) { + $this->migrate_post( $id ); + } + if ( $has_kses ) { + kses_init_filters(); + } + } + if ( -1 === $limit ) { + update_option( self::KEY, 1 ); + return count( $query->posts ); + } else { + return $query->found_posts; + } + } + + /** + * Migrate a post. + * + * @param int $id Post ID. + * @return int|\WP_Error maybe updated. + * @global \WP_Customize_Manager $wp_customize + */ + public function migrate_post( $id ) { + global $wp_customize; + + $post = get_post( $id ); + + // Get data. + $data = json_decode( $post->post_content, true ); + if ( json_last_error() || ! is_array( $data ) ) { + $data = array(); + } + + // Get manager instance. + $manager = new \WP_Customize_Manager(); + $original_manager = $wp_customize; + $wp_customize = $manager; // Export to global since some filters (like widget_customizer_setting_args) lack as $wp_customize context and need global. + + // Validate data. + foreach ( $data as $setting_id => $setting_params ) { + // Amend post values with any supplied data. + if ( array_key_exists( 'value', $setting_params ) ) { + $manager->set_post_value( $setting_id, $setting_params['value'] ); // Add to post values so that they can be validated and sanitized. + } + } + do_action( 'customize_register', $manager ); + + // Note that in addition to post data, this will include any stashed theme mods. + $post_values = $manager->unsanitized_post_values( array( + 'exclude_changeset' => true, + 'exclude_post_data' => false, + ) ); + + // Update data as new changeset. + $manager->add_dynamic_settings( array_keys( $post_values ) ); + $theme = get_post_meta( $id, '_snapshot_theme', true ); + $post_data = array(); + foreach ( $post_values as $setting_id => $setting_value ) { + $setting = $manager->get_setting( $setting_id ); + + if ( $setting && 'theme_mod' === $setting->type ) { + $prefixed_setting_id = $theme . '::' . $setting->id; + } else { + $prefixed_setting_id = $setting_id; + } + $post_data[ $prefixed_setting_id ] = array( + 'value' => $setting_value, + 'user_id' => $post->post_author, + ); + if ( $setting instanceof \WP_Customize_Setting ) { + $post_data[ $prefixed_setting_id ]['type'] = $setting->type; + } + } + $maybe_updated = wp_update_post( wp_slash( array( + 'ID' => $post->ID, + 'post_type' => 'customize_changeset', + 'post_content' => Customize_Snapshot_Manager::encode_json( $post_data ), + ) ), true ); + + $wp_customize = $original_manager; // Restore previous manager. + + return $maybe_updated; + } +} diff --git a/php/class-plugin.php b/php/class-plugin.php index 69dfae0a..86959f05 100644 --- a/php/class-plugin.php +++ b/php/class-plugin.php @@ -17,7 +17,7 @@ class Plugin extends Plugin_Base { * * @todo Rename this to just `$manager` and let the class be `Manager`. * - * @var Customize_Snapshot_Manager + * @var Customize_Snapshot_Manager|Customize_Snapshot_Manager_Back_Compat */ public $customize_snapshot_manager; @@ -28,21 +28,46 @@ class Plugin extends Plugin_Base { */ public $version; + /** + * Is old version of WordPress. + * + * @var boolean + */ + public $compat; + + /** + * Migration handler. + * + * @var Migrate + */ + public $migrate; + /** * Plugin constructor. */ public function __construct() { - // Parse plugin version. - if ( preg_match( '/Version:\s*(\S+)/', file_get_contents( dirname( __FILE__ ) . '/../customize-snapshots.php' ), $matches ) ) { + if ( preg_match( '/Version:\s*(\S+)/', file_get_contents( __DIR__ . '/../customize-snapshots.php' ), $matches ) ) { $this->version = $matches[1]; } - + $this->compat = is_back_compat(); load_plugin_textdomain( 'customize-snapshots' ); - + $this->param_back_compat(); parent::__construct(); } + /** + * Init migration. + * + * @action init + */ + public function init_migration() { + $this->migrate = new Migrate( $this ); + if ( defined( 'WP_CLI' ) && WP_CLI ) { + require_once( __DIR__ . '/class-customize-snapshot-command.php' ); + } + } + /** * Initiate the plugin resources. * @@ -54,7 +79,11 @@ public function __construct() { * @action after_setup_theme, 8 */ public function init() { - $this->customize_snapshot_manager = new Customize_Snapshot_Manager( $this ); + if ( $this->compat ) { + $this->customize_snapshot_manager = new Customize_Snapshot_Manager_Back_Compat( $this ); + } else { + $this->customize_snapshot_manager = new Customize_Snapshot_Manager( $this ); + } $this->customize_snapshot_manager->init(); } @@ -66,20 +95,38 @@ public function init() { * @param \WP_Scripts $wp_scripts Instance of \WP_Scripts. */ public function register_scripts( \WP_Scripts $wp_scripts ) { - $min = ( SCRIPT_DEBUG ? '' : '.min' ); + $is_git_repo = file_exists( dirname( __DIR__ ) . '/.git' ); + $min = ( SCRIPT_DEBUG || $is_git_repo ? '' : '.min' ); $handle = 'customize-snapshots'; $src = $this->dir_url . 'js/customize-snapshots' . $min . '.js'; - $deps = array( 'jquery', 'jquery-ui-dialog', 'wp-util', 'customize-controls' ); + $deps = array( 'jquery', 'jquery-ui-dialog', 'jquery-ui-selectmenu', '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 ); + if ( $this->compat ) { + $handle = 'customize-snapshots-compat'; + $src = $this->dir_url . 'js/compat/customize-snapshots' . $min . '.js'; + $deps = array( 'customize-snapshots' ); + $wp_scripts->add( $handle, $src, $deps ); + + $handle = 'customize-snapshots-preview'; + $src = $this->dir_url . 'js/compat/customize-snapshots-preview' . $min . '.js'; + $deps = array( 'customize-preview' ); + $wp_scripts->add( $handle, $src, $deps ); + + $handle = 'customize-snapshots-frontend'; + $src = $this->dir_url . 'js/compat/customize-snapshots-frontend' . $min . '.js'; + $deps = array( 'jquery', 'underscore' ); + $wp_scripts->add( $handle, $src, $deps ); + } else { + $handle = 'customize-snapshot-migrate'; + $src = $this->dir_url . 'js/customize-migrate' . $min . '.js'; + $deps = array( 'jquery', 'wp-util' ); + $wp_scripts->add( $handle, $src, $deps ); + } - $handle = 'customize-snapshots-frontend'; - $src = $this->dir_url . 'js/customize-snapshots-frontend' . $min . '.js'; + $handle = 'customize-snapshots-admin'; + $src = $this->dir_url . 'js/customize-snapshots-admin' . $min . '.js'; $deps = array( 'jquery', 'underscore' ); $wp_scripts->add( $handle, $src, $deps ); } @@ -92,7 +139,8 @@ public function register_scripts( \WP_Scripts $wp_scripts ) { * @param \WP_Styles $wp_styles Instance of \WP_Styles. */ public function register_styles( \WP_Styles $wp_styles ) { - $min = ( SCRIPT_DEBUG ? '' : '.min' ); + $is_git_repo = file_exists( dirname( __DIR__ ) . '/.git' ); + $min = ( SCRIPT_DEBUG || $is_git_repo ? '' : '.min' ); $handle = 'customize-snapshots'; $src = $this->dir_url . 'css/customize-snapshots' . $min . '.css'; @@ -103,5 +151,20 @@ public function register_styles( \WP_Styles $wp_styles ) { $src = $this->dir_url . 'css/customize-snapshots-preview' . $min . '.css'; $deps = array( 'customize-preview' ); $wp_styles->add( $handle, $src, $deps ); + + $handle = 'customize-snapshots-admin'; + $src = $this->dir_url . 'css/customize-snapshots-admin' . $min . '.css'; + $wp_styles->add( $handle, $src ); + } + + /** + * Continue allowing support of param customize_snapshot_uuid in 4.7+. + */ + public function param_back_compat() { + if ( isset( $_REQUEST['customize_snapshot_uuid'] ) && ! $this->compat ) { + $_REQUEST['customize_changeset_uuid'] = $_REQUEST['customize_snapshot_uuid']; + $_GET['customize_changeset_uuid'] = $_REQUEST['customize_snapshot_uuid']; + $_POST['customize_changeset_uuid'] = $_REQUEST['customize_snapshot_uuid']; + } } } diff --git a/php/class-post-type-back-compat.php b/php/class-post-type-back-compat.php new file mode 100644 index 00000000..9d5cae3e --- /dev/null +++ b/php/class-post-type-back-compat.php @@ -0,0 +1,238 @@ + _x( 'Snapshots', 'post type general name', 'customize-snapshots' ), + 'singular_name' => _x( 'Snapshot', 'post type singular name', 'customize-snapshots' ), + 'menu_name' => _x( 'Snapshots', 'admin menu', 'customize-snapshots' ), + 'name_admin_bar' => _x( 'Snapshot', 'add new on admin bar', 'customize-snapshots' ), + 'add_new' => _x( 'Add New', 'Customize Snapshot', 'customize-snapshots' ), + 'add_new_item' => __( 'Add New Snapshot', 'customize-snapshots' ), + 'new_item' => __( 'New Snapshot', 'customize-snapshots' ), + 'edit_item' => __( 'Inspect Snapshot', 'customize-snapshots' ), + 'view_item' => __( 'View Snapshot', 'customize-snapshots' ), + 'all_items' => __( 'All Snapshots', 'customize-snapshots' ), + 'search_items' => __( 'Search Snapshots', 'customize-snapshots' ), + 'not_found' => __( 'No snapshots found.', 'customize-snapshots' ), + 'not_found_in_trash' => __( 'No snapshots found in Trash.', 'customize-snapshots' ), + ); + + $args = array( + 'labels' => $labels, + 'description' => __( 'Customize Snapshots.', 'customize-snapshots' ), + 'public' => true, + 'publicly_queryable' => false, + 'query_var' => false, + 'exclude_from_search' => true, + 'show_ui' => true, + 'show_in_nav_menus' => false, + 'show_in_menu' => true, + 'show_in_admin_bar' => false, + 'map_meta_cap' => true, + 'hierarchical' => false, + 'delete_with_user' => false, + 'menu_position' => null, + 'supports' => array( 'title', 'author', 'revisions' ), + 'capability_type' => static::SLUG, + 'capabilities' => array( + 'create_posts' => 'do_not_allow', + ), + 'rewrite' => false, + 'show_in_customizer' => false, // Prevent inception. + 'show_in_rest' => true, + 'rest_base' => 'customize_snapshots', + 'rest_controller_class' => __NAMESPACE__ . '\\Snapshot_REST_API_Controller', + 'customize_snapshot_post_type_obj' => $this, + 'menu_icon' => 'dashicons-camera', + 'register_meta_box_cb' => array( $this, 'setup_metaboxes' ), + ); + + register_post_type( static::SLUG, $args ); + + // Call parent hooks. + $this->hooks(); + + // 4.6.x and post-type specific hooks. + add_action( 'admin_notices', array( $this, 'show_publish_error_admin_notice' ) ); + add_filter( 'display_post_states', array( $this, 'display_post_states' ), 10, 2 ); + add_action( 'admin_footer-edit.php', array( $this, 'snapshot_merge_print_script' ) ); + add_action( 'load-edit.php', array( $this, 'handle_snapshot_merge_workaround' ) ); + add_filter( 'post_type_link', array( $this, 'filter_post_type_link' ), 10, 2 ); + add_filter( 'wp_insert_post_data', array( $this, 'preserve_post_name_in_insert_data' ), 10, 2 ); + } + + /** + * Insert script for adding merge snapshot bulk action polyfill. + */ + public function snapshot_merge_print_script() { + global $post_type; + if ( static::SLUG === $post_type ) { + ?> + + current_action(); + if ( 'merge_snapshot' !== $action || ( isset( $_REQUEST['post_type'] ) && static::SLUG !== wp_unslash( $_REQUEST['post_type'] ) ) ) { + return; + } + check_admin_referer( 'bulk-posts' ); + $post_ids = array_map( 'intval', $_REQUEST['post'] ); + if ( empty( $post_ids ) ) { + return; + } + $redirect_url = $this->handle_snapshot_merge( wp_get_referer(), 'merge_snapshot', $post_ids ); + if ( ! empty( $redirect_url ) ) { + wp_safe_redirect( $redirect_url ); + exit; + } + } + + /** + * Find a snapshot post by UUID. + * + * @param string $uuid UUID. + * @return int|null Post ID or null if not found. + */ + public function find_post( $uuid ) { + add_action( 'pre_get_posts', array( $this, '_override_wp_query_is_single' ) ); + $query = new \WP_Query( array( + 'name' => $uuid, + 'posts_per_page' => 1, + 'post_type' => static::SLUG, + 'post_status' => get_post_stati(), + 'no_found_rows' => true, + 'ignore_sticky_posts' => true, + 'cache_results' => false, + ) ); + $posts = $query->posts; + remove_action( 'pre_get_posts', array( $this, '_override_wp_query_is_single' ) ); + + $post = array_shift( $posts ); + if ( $post ) { + return $post->ID; + } else { + return null; + } + } + + /** + * Preserve the post_name when submitting a snapshot for review. + * + * @see wp_insert_post() + * @link https://github.com/xwp/wordpress-develop/blob/831a186108983ade4d647124d4e56e09aa254704/src/wp-includes/post.php#L3134-L3137 + * + * @param array $post_data Post data. + * @param array $original_post_data Original post data. + * @return array Post data. + */ + public function preserve_post_name_in_insert_data( $post_data, $original_post_data ) { + if ( empty( $post_data['post_type'] ) || static::SLUG !== $post_data['post_type'] ) { + return $post_data; + } + if ( empty( $post_data['post_name'] ) && 'pending' === $post_data['post_status'] ) { + $post_data['post_name'] = $original_post_data['post_name']; + } + return $post_data; + } + + /** + * Display snapshot save error on post list table. + * + * @param array $states Display states. + * @param \WP_Post $post Post object. + * + * @return mixed + */ + public function display_post_states( $states, $post ) { + if ( static::SLUG !== $post->post_type ) { + return $states; + } + $maybe_error = get_post_meta( $post->ID, 'snapshot_error_on_publish', true ); + if ( $maybe_error ) { + $states['snapshot_error'] = __( 'Error on publish', 'customize-snapshots' ); + } + return $states; + } + + /** + * Show an admin notice when publishing fails and the post gets kicked back to pending. + */ + public function show_publish_error_admin_notice() { + if ( ! function_exists( 'get_current_screen' ) ) { + return; + } + $current_screen = get_current_screen(); + if ( ! $current_screen || static::SLUG !== $current_screen->id || 'post' !== $current_screen->base ) { + return; + } + if ( ! isset( $_REQUEST['snapshot_error_on_publish'] ) ) { + return; + } + ?> +
+

+
+ _x( 'Snapshots', 'post type general name', 'customize-snapshots' ), - 'singular_name' => _x( 'Snapshot', 'post type singular name', 'customize-snapshots' ), - 'menu_name' => _x( 'Snapshots', 'admin menu', 'customize-snapshots' ), - 'name_admin_bar' => _x( 'Snapshot', 'add new on admin bar', 'customize-snapshots' ), - 'add_new' => _x( 'Add New', 'Customize Snapshot', 'customize-snapshots' ), - 'add_new_item' => __( 'Add New Snapshot', 'customize-snapshots' ), - 'new_item' => __( 'New Snapshot', 'customize-snapshots' ), - 'edit_item' => __( 'Inspect Snapshot', 'customize-snapshots' ), - 'view_item' => __( 'View Snapshot', 'customize-snapshots' ), - 'all_items' => __( 'All Snapshots', 'customize-snapshots' ), - 'search_items' => __( 'Search Snapshots', 'customize-snapshots' ), - 'not_found' => __( 'No snapshots found.', 'customize-snapshots' ), - 'not_found_in_trash' => __( 'No snapshots found in Trash.', 'customize-snapshots' ), - ); - - $args = array( - 'labels' => $labels, - 'description' => __( 'Customize Snapshots.', 'customize-snapshots' ), - 'public' => true, - 'publicly_queryable' => false, - 'query_var' => false, - 'exclude_from_search' => true, - 'show_ui' => true, - 'show_in_nav_menus' => false, - 'show_in_menu' => true, - 'show_in_admin_bar' => false, - 'map_meta_cap' => true, - 'hierarchical' => false, - 'delete_with_user' => false, - 'menu_position' => null, - 'supports' => array( 'author', 'revisions' ), - 'capability_type' => static::SLUG, - 'capabilities' => array( - 'create_posts' => 'do_not_allow', - ), - 'rewrite' => false, - 'show_in_customizer' => false, // Prevent inception. - 'show_in_rest' => true, - 'rest_base' => 'customize_snapshots', - 'rest_controller_class' => __NAMESPACE__ . '\\Snapshot_REST_API_Controller', - 'customize_snapshot_post_type_obj' => $this, - 'menu_icon' => 'dashicons-camera', - 'register_meta_box_cb' => array( $this, 'setup_metaboxes' ), - ); - - register_post_type( static::SLUG, $args ); - - add_filter( 'post_type_link', array( $this, 'filter_post_type_link' ), 10, 2 ); + public function hooks() { 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 ); - add_filter( 'wp_insert_post_data', array( $this, 'preserve_post_name_in_insert_data' ), 10, 2 ); add_filter( 'user_has_cap', array( $this, 'filter_user_has_cap' ), 10, 2 ); - add_filter( 'display_post_states', array( $this, 'display_post_states' ), 10, 2 ); - add_action( 'admin_notices', array( $this, 'show_publish_error_admin_notice' ) ); add_action( 'post_submitbox_minor_actions', array( $this, 'hide_disabled_publishing_actions' ) ); + add_filter( 'content_save_pre', array( $this, 'filter_out_settings_if_removed_in_metabox' ), 10 ); add_action( 'admin_print_scripts-revision.php', array( $this, 'disable_revision_ui_for_published_posts' ) ); + add_action( 'admin_notices', array( $this, 'admin_show_merge_error' ) ); + } + + /** + * Register post type. + * Child class should override this to call it's own init. + */ + public function init() { + $this->extend_changeset_post_type_object(); + $this->hooks(); + + add_filter( 'post_link', array( $this, 'filter_post_type_link' ), 10, 2 ); + add_action( 'add_meta_boxes_' . static::SLUG, array( $this, 'setup_metaboxes' ), 10, 1 ); + add_action( 'admin_menu',array( $this, 'add_admin_menu_item' ) ); + add_filter( 'map_meta_cap', array( $this, 'remap_customize_meta_cap' ), 5, 4 ); + add_filter( 'bulk_actions-edit-' . static::SLUG, array( $this, 'add_snapshot_bulk_actions' ) ); + add_filter( 'handle_bulk_actions-edit-' . static::SLUG, array( $this, 'handle_snapshot_merge' ), 10, 3 ); + add_action( 'admin_print_styles-edit.php', array( $this, 'hide_add_new_changeset_button' ) ); + } + + /** + * Extend changeset post_type object. + */ + public function extend_changeset_post_type_object() { + $post_type_obj = get_post_type_object( static::SLUG ); + add_post_type_support( static::SLUG, 'revisions' ); + $post_type_obj->show_ui = true; + $post_type_obj->show_in_menu = true; + $post_type_obj->_edit_link = 'post.php?post=%d'; + $arg = array( + 'capability_type' => Post_Type::SLUG, + 'map_meta_cap' => true, + 'capabilities' => array( + 'publish_posts' => 'customize_publish', + ), + ); + $arg = (object) $arg; + $post_type_obj->cap = get_post_type_capabilities( $arg ); + $post_type_obj->show_in_customizer = false; + $post_type_obj->customize_snapshot_post_type_obj = $this; + $post_type_obj->show_in_rest = true; + $post_type_obj->rest_base = 'customize_changesets'; + $post_type_obj->rest_controller_class = __NAMESPACE__ . '\\Snapshot_REST_API_Controller'; + } + + /** + * Add admin menu item. + */ + public function add_admin_menu_item() { + $post_type_object = get_post_type_object( static::SLUG ); + $capability = $post_type_object->cap->edit_posts; + $page_title = $post_type_object->labels->name; + $menu_title = $post_type_object->labels->name; + $menu_slug = 'edit.php?post_type=' . static::SLUG; + add_theme_page( $page_title, $menu_title, $capability, $menu_slug ); } /** @@ -125,9 +142,9 @@ public function register() { * @return string URL. */ public function filter_post_type_link( $url, $post ) { - if ( self::SLUG === $post->post_type ) { + if ( static::SLUG === $post->post_type ) { $url = add_query_arg( - array( 'customize_snapshot_uuid' => $post->post_name ), + array( static::FRONT_UUID_PARAM_NAME => $post->post_name ), home_url( '/' ) ); } @@ -253,7 +270,7 @@ public function filter_post_row_actions( $actions, $post ) { $post_type_obj = get_post_type_object( static::SLUG ); if ( 'publish' !== $post->post_status && current_user_can( $post_type_obj->cap->edit_post, $post->ID ) ) { $args = array( - 'customize_snapshot_uuid' => $post->post_name, + static::CUSTOMIZE_UUID_PARAM_NAME => $post->post_name, ); $customize_url = add_query_arg( array_map( 'rawurlencode', $args ), wp_customize_url() ); $actions = array_merge( @@ -268,14 +285,12 @@ public function filter_post_row_actions( $actions, $post ) { 'front-view' => sprintf( '%s', esc_url( get_permalink( $post->ID ) ), - esc_html__( 'Preview Snapshot', 'customize-snapshots' ) + esc_html__( 'Preview', 'customize-snapshots' ) ), ), $actions ); } else { - unset( $actions['inline hide-if-no-js'] ); - if ( isset( $actions['edit'] ) ) { $actions['edit'] = sprintf( '%s', @@ -286,27 +301,10 @@ public function filter_post_row_actions( $actions, $post ) { ); } } - return $actions; - } - /** - * Preserve the post_name when submitting a snapshot for review. - * - * @see wp_insert_post() - * @link https://github.com/xwp/wordpress-develop/blob/831a186108983ade4d647124d4e56e09aa254704/src/wp-includes/post.php#L3134-L3137 - * - * @param array $post_data Post data. - * @param array $original_post_data Original post data. - * @return array Post data. - */ - public function preserve_post_name_in_insert_data( $post_data, $original_post_data ) { - if ( empty( $post_data['post_type'] ) || static::SLUG !== $post_data['post_type'] ) { - return $post_data; - } - if ( empty( $post_data['post_name'] ) && 'pending' === $post_data['post_status'] ) { - $post_data['post_name'] = $original_post_data['post_name']; - } - return $post_data; + unset( $actions['inline hide-if-no-js'] ); + + return $actions; } /** @@ -325,12 +323,13 @@ public function render_data_metabox( $post ) { $snapshot_theme = get_post_meta( $post->ID, '_snapshot_theme', true ); if ( ! empty( $snapshot_theme ) && get_stylesheet() !== $snapshot_theme ) { echo '

'; + /* translators: 1 is the theme the snapshot was created for */ echo sprintf( esc_html__( 'This snapshot was made when a different theme was active (%1$s), so currently it cannot be edited.', 'customize-snapshots' ), esc_html( $snapshot_theme ) ); echo '

'; } elseif ( 'publish' !== $post->post_status ) { echo '

'; $args = array( - 'customize_snapshot_uuid' => $post->post_name, + static::CUSTOMIZE_UUID_PARAM_NAME => $post->post_name, ); $customize_url = add_query_arg( array_map( 'rawurlencode', $args ), wp_customize_url() ); echo sprintf( @@ -351,6 +350,7 @@ public function render_data_metabox( $post ) { echo '


'; ksort( $snapshot_content ); + wp_nonce_field( static::SLUG . '_settings', static::SLUG ); echo ''; } @@ -422,25 +423,8 @@ public function render_data_metabox( $post ) { * @return int|null Post ID or null if not found. */ public function find_post( $uuid ) { - add_action( 'pre_get_posts', array( $this, '_override_wp_query_is_single' ) ); - $query = new \WP_Query( array( - 'name' => $uuid, - 'posts_per_page' => 1, - 'post_type' => static::SLUG, - 'post_status' => get_post_stati(), - 'no_found_rows' => true, - 'ignore_sticky_posts' => true, - 'cache_results' => false, - ) ); - $posts = $query->posts; - remove_action( 'pre_get_posts', array( $this, '_override_wp_query_is_single' ) ); - - $post = array_shift( $posts ); - if ( $post ) { - return $post->ID; - } else { - return null; - } + $this->snapshot_manager->ensure_customize_manager(); + return $this->snapshot_manager->customize_manager->find_changeset_post_id( $uuid ); } /** @@ -490,6 +474,9 @@ public function get_post_content( \WP_Post $post ) { * Persist the data in the snapshot post content. * * @param array $args Args. + * + * @internal For saving changesets use \WP_Customize_Manager::save_changeset_post(). + * * @return int|\WP_Error Post ID for snapshot or WP_Error instance. */ public function save( array $args ) { @@ -501,18 +488,13 @@ public function save( array $args ) { $post_arr = array( 'post_name' => $args['uuid'], - 'post_title' => $args['uuid'], + 'post_title' => ! empty( $args['post_title'] ) ? $args['post_title'] : $args['uuid'], 'post_type' => static::SLUG, 'meta_input' => array( '_snapshot_version' => $this->snapshot_manager->plugin->version, ), ); if ( ! empty( $args['status'] ) ) { - if ( isset( $args['post_date'], $args['edit_date'], $args['post_date_gmt'] ) ) { - $post_arr['post_date'] = $args['post_date']; - $post_arr['edit_date'] = $args['edit_date']; - $post_arr['post_date_gmt'] = $args['post_date_gmt']; - } if ( ! get_post_status_object( $args['status'] ) ) { return new \WP_Error( 'bad_status' ); } @@ -551,10 +533,12 @@ public function save( array $args ) { } if ( ! empty( $args['date_gmt'] ) ) { $post_arr['post_date_gmt'] = $args['date_gmt']; + $post_arr['post_date'] = get_date_from_gmt( $args['date_gmt'] ); } $this->suspend_kses(); if ( $is_update ) { + $post_arr['edit_date'] = true; $r = wp_update_post( wp_slash( $post_arr ), true ); } else { $r = wp_insert_post( wp_slash( $post_arr ), true ); @@ -564,6 +548,26 @@ public function save( array $args ) { return $r; } + /** + * Re-map customize meta cap to edit_theme_options primitive cap. + * + * @param array $caps All caps. + * @param string $cap Requested cap. + * + * @return array All caps. + */ + public function remap_customize_meta_cap( $caps, $cap ) { + $post_type_obj = get_post_type_object( static::SLUG ); + if ( isset( $post_type_obj->cap->$cap ) && 'customize' === $post_type_obj->cap->$cap ) { + foreach ( $caps as &$required_cap ) { + if ( 'customize' === $required_cap ) { + $required_cap = 'edit_theme_options'; + } + } + } + return $caps; + } + /** * Add the customize_publish capability to users who can edit_theme_options by default. * @@ -599,51 +603,11 @@ public function filter_user_has_cap( $allcaps, $caps ) { return $allcaps; } - /** - * Display snapshot save error on post list table. - * - * @param array $states Display states. - * @param \WP_Post $post Post object. - * - * @return mixed - */ - public function display_post_states( $states, $post ) { - if ( static::SLUG !== $post->post_type ) { - return $states; - } - $maybe_error = get_post_meta( $post->ID, 'snapshot_error_on_publish', true ); - if ( $maybe_error ) { - $states['snapshot_error'] = __( 'Error on publish', 'customize-snapshots' ); - } - return $states; - } - - /** - * Show an admin notice when publishing fails and the post gets kicked back to pending. - */ - public function show_publish_error_admin_notice() { - if ( ! function_exists( 'get_current_screen' ) ) { - return; - } - $current_screen = get_current_screen(); - if ( ! $current_screen || 'customize_snapshot' !== $current_screen->id || 'post' !== $current_screen->base ) { - return; - } - if ( ! isset( $_REQUEST['snapshot_error_on_publish'] ) ) { - return; - } - ?> -
-

-
- @@ -661,7 +625,7 @@ public function disable_revision_ui_for_published_posts() { * @param \WP_Post $post Current post. */ public function hide_disabled_publishing_actions( $post ) { - if ( 'publish' !== $post->post_status || self::SLUG !== $post->post_type ) { + if ( 'publish' !== $post->post_status || static::SLUG !== $post->post_type ) { return; } ?> @@ -675,4 +639,194 @@ public function hide_disabled_publishing_actions( $post ) { + + 1 ) ) : add_query_arg( array( 'merge-error' => 1 ), $redirect_to ); + } + $post_id = $this->merge_snapshots( $posts ); + $redirect_to = get_edit_post_link( $post_id, 'raw' ); + if ( $redirect_to ) { + wp_safe_redirect( $redirect_to ); + exit; + } + return $post_id; + } + + /** + * Merge two or more snapshots + * + * @param array $post_ids post id array. + * + * @return int Changeset post id. + */ + public function merge_snapshots( $post_ids ) { + $posts = array_map( 'get_post', $post_ids ); + usort( $posts, function( $a, $b ) { + $compare_a = $a->post_modified; + $compare_b = $b->post_modified; + if ( '0000-00-00 00:00:00' === $compare_a ) { + $compare_a = $a->post_date; + } + if ( '0000-00-00 00:00:00' === $compare_b ) { + $compare_b = $b->post_date; + } + return strtotime( $compare_a ) - strtotime( $compare_b ); + } ); + + $snapshot_post_data = array(); + foreach ( $posts as $post ) { + $snapshot_post_data[] = array( + 'data' => $this->get_post_content( $post ), + 'uuid' => $post->post_name, + ); + } + $snapshots_data = wp_list_pluck( $snapshot_post_data, 'data' ); + $data_size = count( $snapshots_data ); + $conflict_keys = array(); + + /* + * This iterates all $snapshots_data and extract conflict keys + */ + for ( $i = 0; $i < $data_size; $i ++ ) { + $copy_snapshots_data = $snapshots_data; + $current_keys = array_keys( $snapshots_data[ $i ] ); + unset( $copy_snapshots_data[ $i ] ); + $temp_other_keys = array_keys( call_user_func_array( 'array_merge', $copy_snapshots_data ) ); + $common_keys = array_intersect( $temp_other_keys, $current_keys ); + $conflict_keys = array_merge( $conflict_keys, $common_keys ); + } + $conflict_keys = array_flip( $conflict_keys ); + $merged_snapshot_data = call_user_func_array( 'array_merge', $snapshots_data ); + + foreach ( $conflict_keys as $key => $i ) { + $original_values = array(); + foreach ( $snapshot_post_data as $post_data ) { + if ( isset( $post_data['data'][ $key ] ) ) { + $original_values[] = array( + 'uuid' => $post_data['uuid'], + 'value' => $post_data['data'][ $key ]['value'], + ); + } + } + $merged_snapshot_data[ $key ]['merge_conflict'] = $original_values; + } + $post_id = $this->save( array( + 'uuid' => Customize_Snapshot_Manager::generate_uuid(), + 'status' => 'draft', + 'data' => $merged_snapshot_data, + 'date_gmt' => gmdate( 'Y-m-d H:i:s' ), + ) ); + return $post_id; + } + + /** + * Show admin notice in case of merge error + */ + public function admin_show_merge_error() { + if ( ! isset( $_REQUEST['merge-error'] ) ) { + return; + } + $error = array( + 1 => __( 'At-least two snapshot required for merge.', 'customize-snapshots' ), + ); + $error_code = intval( $_REQUEST['merge-error'] ); + if ( ! isset( $error[ $error_code ] ) ) { + return; + } + printf( '

%s

', esc_html( $error[ $error_code ] ) ); + } + + /** + * Filter settings out of post content, if they were removed in the meta box. + * + * In each snapshot's edit page, there are JavaScript-controlled links to remove each setting. + * On clicking a setting, the JS sets a hidden input field with that setting's ID. + * And these settings appear in $_REQUEST as the array 'customize_snapshot_remove_settings.' + * So look for these removed settings in that array, on saving. + * And possibly filter out those settings from the post content. + * + * @param String $content Post content to filter. + * @return String $content Post content, possibly filtered. + */ + public function filter_out_settings_if_removed_in_metabox( $content ) { + global $post; + $key_for_settings = static::SLUG . '_remove_settings'; + $post_type_object = get_post_type_object( static::SLUG ); + + $should_filter_content = ( + isset( $post->post_status ) + && + ( 'publish' !== $post->post_status ) + && + current_user_can( $post_type_object->cap->edit_post, $post->ID ) + && + ( static::SLUG === $post->post_type ) + && + ! empty( $_REQUEST[ $key_for_settings ] ) + && + is_array( $_REQUEST[ $key_for_settings ] ) + && + isset( $_REQUEST[ static::SLUG ] ) + && + wp_verify_nonce( $_REQUEST[ static::SLUG ], static::SLUG . '_settings' ) + && + ! ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) + ); + + if ( ! $should_filter_content ) { + return $content; + } + + $setting_ids_to_unset = $_REQUEST[ $key_for_settings ]; + $data = json_decode( wp_unslash( $content ), true ); + foreach ( $setting_ids_to_unset as $setting_id ) { + unset( $data[ $setting_id ] ); + } + $content = Customize_Snapshot_Manager::encode_json( $data ); + + return $content; + } } diff --git a/php/class-snapshot-rest-api-controller.php b/php/class-snapshot-rest-api-controller.php index f12d40cd..f04ea024 100644 --- a/php/class-snapshot-rest-api-controller.php +++ b/php/class-snapshot-rest-api-controller.php @@ -141,7 +141,7 @@ public function get_items_permissions_check( $request ) { * @return boolean Can we read it? */ public function check_read_permission( $post ) { - $post_type_obj = get_post_type_object( 'customize_snapshot' ); + $post_type_obj = get_post_type_object( $this->snapshot_post_type->snapshot_manager->get_post_type() ); if ( ! current_user_can( $post_type_obj->cap->edit_post, $post->ID ) ) { return false; } @@ -161,6 +161,17 @@ public function prepare_item_for_response( $post, $request ) { return $response; } + /** + * Creates a snapshot/changeset post. + * + * @param \WP_REST_Request $request Full details about the request. + * @return \WP_Error WP_Error object. + */ + public function create_item( $request ) { + unset( $request ); + return new \WP_Error( 'rest_cannot_create', __( 'Now allowed to create post', 'customize-snapshots' ), array( 'status' => rest_authorization_required_code() ) ); + } + /** * Update one item from the collection. * @@ -169,6 +180,7 @@ public function prepare_item_for_response( $post, $request ) { */ public function update_item( $request ) { unset( $request ); + /* translators: %s is the method name */ return new \WP_Error( 'invalid-method', sprintf( __( "Method '%s' not yet implemented.", 'customize-snapshots' ), __METHOD__ ), array( 'status' => 405 ) ); } @@ -180,6 +192,7 @@ public function update_item( $request ) { */ public function delete_item( $request ) { unset( $request ); + /* translators: %s is the method name */ return new \WP_Error( 'invalid-method', sprintf( __( "Method '%s' not yet implemented.", 'customize-snapshots' ), __METHOD__ ), array( 'status' => 405 ) ); } } diff --git a/phpcs.ruleset.xml b/phpcs.ruleset.xml index e7a89f20..8aefe6fb 100644 --- a/phpcs.ruleset.xml +++ b/phpcs.ruleset.xml @@ -13,6 +13,16 @@ 0 + + + 0 + + + 0 + + + */tests/* + */node_modules/* */vendor/* diff --git a/readme.md b/readme.md index 81f05728..968fbbc7 100644 --- a/readme.md +++ b/readme.md @@ -4,11 +4,11 @@ ![Banner](wp-assets/banner-1544x500.png) Allow Customizer states to be drafted, and previewed with a private URL. -**Contributors:** [westonruter](https://profiles.wordpress.org/westonruter), [valendesigns](https://profiles.wordpress.org/valendesigns), [xwp](https://profiles.wordpress.org/xwp), [newscorpau](https://profiles.wordpress.org/newscorpau) -**Tags:** [customizer](https://wordpress.org/plugins/tags/customizer), [customize](https://wordpress.org/plugins/tags/customize), [snapshots](https://wordpress.org/plugins/tags/snapshots) +**Contributors:** [xwp](https://profiles.wordpress.org/xwp), [westonruter](https://profiles.wordpress.org/westonruter), [valendesigns](https://profiles.wordpress.org/valendesigns), [utkarshpatel](https://profiles.wordpress.org/utkarshpatel), [sayedwp](https://profiles.wordpress.org/sayedwp), [newscorpau](https://profiles.wordpress.org/newscorpau) +**Tags:** [customizer](https://wordpress.org/plugins/tags/customizer), [customize](https://wordpress.org/plugins/tags/customize), [changesets](https://wordpress.org/plugins/tags/changesets) **Requires at least:** 4.5.3 -**Tested up to:** 4.6 -**Stable tag:** 0.5.2 +**Tested up to:** 4.7 +**Stable tag:** 0.6.0-rc1 **License:** [GPLv2 or later](http://www.gnu.org/licenses/gpl-2.0.html) [![Build Status](https://travis-ci.org/xwp/wp-customize-snapshots.svg?branch=master)](https://travis-ci.org/xwp/wp-customize-snapshots) [![Coverage Status](https://coveralls.io/repos/xwp/wp-customize-snapshots/badge.svg?branch=master)](https://coveralls.io/github/xwp/wp-customize-snapshots) [![Built with Grunt](https://cdn.gruntjs.com/builtwith.svg)](http://gruntjs.com) [![devDependency Status](https://david-dm.org/xwp/wp-customize-snapshots/dev-status.svg)](https://david-dm.org/xwp/wp-customize-snapshots#info=devDependencies) @@ -87,6 +87,13 @@ if ( ! $bypass_object_cache ) { ## Changelog ## +### 0.6.0 RC1 - 2017-02-06 ### +See [issues and PRs in milestone](https://github.com/xwp/wp-customize-snapshots/milestone/4?closed=1). + +See full commit log: [`0.5.2...0.6.0-rc1`](https://github.com/xwp/wp-customize-snapshots/compare/0.5.2...0.6.0-rc1) + +Props: Utkarsh Patel (@PatelUtkarsh), Sayed Taqui (@sayedwp), Weston Ruter (@westonruter), Ryan Kienstra (@kienstra), Luke Gedeon (@lgedeon). + ### 0.5.2 - 2016-08-17 ### * Fixed: Prevent enqueueing frontend JS in the customizer preview. This was erroneously causing the customize_snapshot_uuid param to get injected into links in the preview. See [#80](https://github.com/xwp/wp-customize-snapshots/pull/80). * Fixed: Ensure that Update button gets disabled and reset to Save once changes have been published. See [#83](https://github.com/xwp/wp-customize-snapshots/pull/83). diff --git a/readme.txt b/readme.txt index b842ff7a..fa928b2f 100644 --- a/readme.txt +++ b/readme.txt @@ -1,11 +1,11 @@ === Customize Snapshots === -Contributors: westonruter, valendesigns, xwp, newscorpau +Contributors: xwp, westonruter, valendesigns, utkarshpatel, sayedwp, newscorpau Requires at least: 4.5.3 -Tested up to: 4.6 -Stable tag: 0.5.2 +Tested up to: 4.7 +Stable tag: 0.6.0-rc1 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html -Tags: customizer, customize, snapshots +Tags: customizer, customize, changesets Allow Customizer states to be drafted, and previewed with a private URL. @@ -64,6 +64,14 @@ if ( ! $bypass_object_cache ) { == Changelog == += 0.6.0 RC1 - 2017-02-06 = + +See [issues and PRs in milestone](https://github.com/xwp/wp-customize-snapshots/milestone/4?closed=1). + +See full commit log: [`0.5.2...0.6.0-rc1`](https://github.com/xwp/wp-customize-snapshots/compare/0.5.2...0.6.0-rc1) + +Props: Utkarsh Patel (@PatelUtkarsh), Sayed Taqui (@sayedwp), Weston Ruter (@westonruter), Ryan Kienstra (@kienstra), Luke Gedeon (@lgedeon). + = 0.5.2 - 2016-08-17 = * Fixed: Prevent enqueueing frontend JS in the customizer preview. This was erroneously causing the customize_snapshot_uuid param to get injected into links in the preview. See [#80](https://github.com/xwp/wp-customize-snapshots/pull/80). diff --git a/tests/php/test-class-ajax-customize-snapshot-manager.php b/tests/php/test-class-ajax-customize-snapshot-manager-back-compat.php similarity index 80% rename from tests/php/test-class-ajax-customize-snapshot-manager.php rename to tests/php/test-class-ajax-customize-snapshot-manager-back-compat.php index 6a424de0..d33c9957 100644 --- a/tests/php/test-class-ajax-customize-snapshot-manager.php +++ b/tests/php/test-class-ajax-customize-snapshot-manager-back-compat.php @@ -1,6 +1,6 @@ compat ) { + $this->markTestSkipped( 'WordPress Version 4.6.x or below is required for this test-case.' ); + } 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(); + if ( $this->plugin->compat ) { + $this->post_type_slug = Post_Type_Back_Compat::SLUG; + } else { + $this->post_type_slug = Post_Type::SLUG; + } } /** @@ -124,6 +161,9 @@ function add_setting() { function tearDown() { $this->plugin->customize_snapshot_manager->customize_manager = null; $this->manager = null; + $this->actioned_snapshot = null; + $this->actioned_snapshot_manager = null; + $this->filtered_customizer = null; unset( $GLOBALS['wp_customize'] ); unset( $GLOBALS['wp_scripts'] ); unset( $_SERVER['REQUEST_METHOD'] ); @@ -258,6 +298,8 @@ function test_ajax_update_snapshot_post_check() { * * @param string $role The role we're checking caps against. * @param array $expected_results Expected results. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager_Back_Compat::check_customize_publish_authorization() */ function test_ajax_update_snapshot_cap_check( $role, $expected_results ) { $this->set_current_user( $role ); @@ -277,8 +319,10 @@ function test_ajax_update_snapshot_cap_check( $role, $expected_results ) { if ( $response['success'] ) { $this->assertNotEmpty( $response['data']['edit_link'] ); $this->assertNotEmpty( $response['data']['snapshot_publish_date'] ); + $this->assertNotEmpty( $response['data']['title'] ); unset( $response['data']['edit_link'] ); unset( $response['data']['snapshot_publish_date'] ); + unset( $response['data']['title'] ); } $this->assertSame( $expected_results, $response ); } @@ -427,18 +471,20 @@ function test_ajax_update_snapshot_schedule() { unset( $GLOBALS['wp_customize'] ); remove_all_actions( 'wp_ajax_' . Customize_Snapshot_Manager::AJAX_ACTION ); - $post_type_obj = get_post_type_object( Post_Type::SLUG ); + $post_type_obj = get_post_type_object( $this->post_type_slug ); $setting_key = 'anyonecanedit'; $tomorrow = date( 'Y-m-d H:i:s', time() + 86400 ); $this->set_current_user( 'administrator' ); $this->assertTrue( current_user_can( $post_type_obj->cap->publish_posts ) ); + $title = 'Hello World! \o/'; $this->set_input_vars( array( 'action' => Customize_Snapshot_Manager::AJAX_ACTION, + 'title' => $title, 'nonce' => wp_create_nonce( Customize_Snapshot_Manager::AJAX_ACTION ), 'customize_snapshot_uuid' => self::UUID, 'customized' => wp_json_encode( array( $setting_key => 'Hello' ) ), 'status' => 'future', - 'publish_date' => $tomorrow, // Tomorrow. + 'date' => $tomorrow, // Tomorrow. ) ); $this->plugin = new Plugin(); @@ -454,6 +500,7 @@ function test_ajax_update_snapshot_schedule() { 'setting_validities' => array( $setting_key => true ), 'edit_link' => get_edit_post_link( $post_id, 'raw' ), 'snapshot_publish_date' => $tomorrow, + 'title' => $title, ), ); require_once ABSPATH . WPINC . '/class-wp-customize-manager.php'; @@ -464,6 +511,7 @@ function test_ajax_update_snapshot_schedule() { $response = json_decode( $this->_last_response, true ); $this->assertSame( $expected_results, $response ); $this->assertEquals( 'future', get_post_status( $post_id ) ); + $this->assertEquals( $title, get_the_title( $post_id ) ); } /** @@ -475,7 +523,7 @@ function test_ajax_update_snapshot_ok_for_draft_and_pending_but_not_future() { unset( $GLOBALS['wp_customize'] ); remove_all_actions( 'wp_ajax_' . Customize_Snapshot_Manager::AJAX_ACTION ); - $post_type_obj = get_post_type_object( Post_Type::SLUG ); + $post_type_obj = get_post_type_object( $this->post_type_slug ); $setting_key = 'anyonecanedit'; add_filter( 'user_has_cap', function( $allcaps, $caps, $args ) { $allcaps['customize'] = true; @@ -526,4 +574,53 @@ function test_ajax_update_snapshot_ok_for_draft_and_pending_but_not_future() { ); $this->assertSame( $expected_results, $response ); } + + /** + * Test actions and filters to make sure they are passing correct params. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::handle_update_snapshot_request() + */ + function test_handle_update_snapshot_request_actions_and_filters() { + unset( $GLOBALS['wp_customize'] ); + remove_all_actions( 'wp_ajax_' . Customize_Snapshot_Manager::AJAX_ACTION ); + + add_filter( 'user_has_cap', function( $allcaps, $caps, $args ) { + $allcaps['customize'] = true; + if ( ! empty( $allcaps['edit_posts'] ) && ! empty( $args ) && 'customize' === $args[0] ) { + $allcaps = array_merge( $allcaps, array_fill_keys( $caps, true ) ); + } + return $allcaps; + }, 10, 3 ); + $this->set_current_user( 'contributor' ); + $post_vars = array( + 'action' => Customize_Snapshot_Manager::AJAX_ACTION, + 'nonce' => wp_create_nonce( Customize_Snapshot_Manager::AJAX_ACTION ), + 'customize_snapshot_uuid' => self::UUID, + ); + + $this->plugin = new Plugin(); + $this->plugin->init(); + $this->add_setting(); + + $that = $this; // For PHP 5.3. + add_action( 'customize_snapshot_save_before', function( $test_snapshot, $test_snapshot_manager ) use ( $that ) { + $that->actioned_snapshot = $test_snapshot; + $that->actioned_snapshot_manager = $test_snapshot_manager; + }, 10, 2 ); + add_filter( 'customize_save_response', function( $data, $test_customizer ) use ( $that ) { + $that->filtered_customizer = $test_customizer; + return $data; + }, 10, 2 ); + + $this->set_input_vars( $post_vars ); + $this->make_ajax_call( Customize_Snapshot_Manager_Back_Compat::AJAX_ACTION ); + + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->ensure_customize_manager(); + $manager->init(); + + $this->assertEquals( $manager->snapshot(), $this->actioned_snapshot ); + $this->assertEquals( $manager, $this->actioned_snapshot_manager ); + $this->assertEquals( $manager->customize_manager, $this->filtered_customizer ); + } } diff --git a/tests/php/test-class-customize-snapshot-back-compat.php b/tests/php/test-class-customize-snapshot-back-compat.php new file mode 100644 index 00000000..e0da98ce --- /dev/null +++ b/tests/php/test-class-customize-snapshot-back-compat.php @@ -0,0 +1,363 @@ +plugin = get_plugin_instance(); + if ( ! $this->plugin->compat ) { + $this->markTestSkipped( 'WordPress Version 4.6.x or below is required for this test-case.' ); + } + require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' ); + $GLOBALS['wp_customize'] = new \WP_Customize_Manager(); // WPCS: override ok. + $this->snapshot_manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $this->wp_customize = $GLOBALS['wp_customize']; + wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); + $this->wp_customize->add_setting( 'foo', array( 'default' => 'foo_default' ) ); + $this->wp_customize->add_setting( 'bar', array( 'default' => 'bar_default' ) ); + $this->foo = $this->wp_customize->get_setting( 'foo' ); + $this->bar = $this->wp_customize->get_setting( 'bar' ); + } + + /** + * Bootstrap the customizer. + */ + public static function setUpBeforeClass() { + $args = array( + 'labels' => array( + 'name' => __( 'Customize Snapshots', 'customize-snapshots' ), + 'singular_name' => __( 'Customize Snapshot', 'customize-snapshots' ), + ), + 'public' => false, + 'capability_type' => 'post', + 'map_meta_cap' => true, + 'hierarchical' => false, + 'rewrite' => false, + 'delete_with_user' => false, + 'supports' => array( 'title', 'author', 'revisions' ), + ); + register_post_type( self::POST_TYPE, $args ); + } + + /** + * Tear down after class. + */ + public static function tearDownAfterClass() { + _unregister_post_type( self::POST_TYPE ); + } + + /** + * Tear down. + */ + function tearDown() { + $this->wp_customize = null; + unset( $GLOBALS['wp_customize'] ); + unset( $GLOBALS['wp_scripts'] ); + $this->filtered_snapshot = null; + parent::tearDown(); + } + + /** + * Test constructor. + * + * @see Customize_Snapshot::__construct() + */ + function test_construct() { + $manager = $this->snapshot_manager; + $manager->init(); + $data = array( 'foo' => array( 'value' => 'bar' ) ); + $manager->post_type->save( array( + 'uuid' => self::UUID, + 'data' => $data, + ) ); + $snapshot = new Customize_Snapshot_Back_Compat( $manager, self::UUID ); + $this->assertEquals( $data, $snapshot->data() ); + } + + /** + * Test UUID. + * + * @see Customize_Snapshot::uuid() + */ + function test_uuid() { + $_REQUEST['customize_snapshot_uuid'] = self::UUID; + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->init(); + $this->assertEquals( self::UUID, $manager->snapshot()->uuid() ); + } + + /** + * Test bad UUID. + * + * @see Customize_Snapshot::uuid() + */ + function test_uuid_throws_exception() { + try { + new Customize_Snapshot_Back_Compat( $this->snapshot_manager, '1234-invalid-UUID' ); + } catch ( \Exception $e ) { + $this->assertContains( 'You\'ve entered an invalid snapshot UUID.', $e->getMessage() ); + return; + } + $this->fail( 'An expected exception has not been raised.' ); + } + + /** + * Test data. + * + * @see Customize_Snapshot::data() + */ + function test_data() { + $_REQUEST['customize_snapshot_uuid'] = self::UUID; + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->init(); + + $manager->snapshot()->set( array( 'foo' => array( 'value' => 'foo_default' ) ) ); + $this->assertNotEmpty( $manager->snapshot()->data() ); + $manager->snapshot()->set( array( 'foo' => array( 'value' => 'foo_custom' ) ) ); + $expected = array( + 'foo' => array( + 'value' => 'foo_custom', + ), + ); + $this->assertEquals( $expected, $manager->snapshot()->data() ); + } + + /** + * Test snapshot settings. + * + * @see Customize_Snapshot::settings() + */ + function test_settings() { + $_REQUEST['customize_snapshot_uuid'] = self::UUID; + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->init(); + + $this->assertEmpty( $manager->snapshot()->settings() ); + $manager->snapshot()->set( array( 'foo' => array( 'value' => 'foo_default' ) ) ); + $this->assertNotEmpty( $manager->snapshot()->settings() ); + } + + /** + * Test status. + * + * @see Customize_Snapshot::settings() + */ + function test_status() { + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->init(); + + $snapshot = new Customize_Snapshot_Back_Compat( $manager, self::UUID ); + $this->assertNull( $snapshot->status() ); + + $data = array( 'foo' => array( 'value' => 'bar' ) ); + $manager->post_type->save( array( + 'uuid' => self::UUID, + 'data' => $data, + 'status' => 'draft', + ) ); + + $this->assertEquals( 'draft', $snapshot->status() ); + $manager->post_type->save( array( + 'uuid' => self::UUID, + 'status' => 'publish', + ) ); + $this->assertEquals( 'publish', $snapshot->status() ); + } + + /** + * Test set. + * + * @see Customize_Snapshot::set() + */ + function test_set() { + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->init(); + + $this->bar->capability = 'do_not_allow'; + add_filter( 'customize_sanitize_foo', 'strtoupper' ); + + $snapshot = new Customize_Snapshot_Back_Compat( $manager, self::UUID ); + $result = $snapshot->set( array( + 'foo' => array( 'value' => 'ok' ), + 'bar' => array( 'value' => 'unauthorized' ), + 'baz' => array( 'value' => 'unrecognized' ), + ) ); + + $this->assertArrayHasKey( 'errors', $result ); + $this->assertInstanceOf( 'WP_Error', $result['errors'] ); + $wp_error = $result['errors']; + $this->assertArrayHasKey( 'unauthorized_settings', $wp_error->errors ); + $this->assertArrayHasKey( 'unrecognized_settings', $wp_error->errors ); + + $this->assertArrayHasKey( 'sanitized', $result ); + $this->assertArrayHasKey( 'foo', $result['sanitized'] ); + $this->assertArrayNotHasKey( 'bar', $result['sanitized'] ); + $this->assertArrayNotHasKey( 'baz', $result['sanitized'] ); + $this->assertEquals( 'OK', $result['sanitized']['foo'] ); + + $this->assertArrayHasKey( 'validities', $result ); + $this->assertArrayHasKey( 'foo', $result['validities'] ); + $this->assertTrue( $result['validities']['foo'] ); + + $this->assertEmpty( $snapshot->data() ); + + // Success with populated value. + $result = $snapshot->set( array( 'foo' => array( 'value' => 'ok' ) ) ); + $this->assertNull( $result['errors'] ); + $resultant_data = $snapshot->data(); + $this->assertEquals( 'ok', $resultant_data['foo']['value'] ); + } + + /** + * Test set with varying setting params. + * + * @see Customize_Snapshot::set() + */ + function test_set_with_varying_setting_params() { + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->init(); + $snapshot = new Customize_Snapshot_Back_Compat( $manager, self::UUID ); + + $result = $snapshot->set( array( 'foo' => array( 'value' => 'ok' ) ) ); + $this->assertNull( $result['errors'] ); + $resultant_data = $snapshot->data(); + $this->assertEquals( 'ok', $resultant_data['foo']['value'] ); + + // Check setting a param without a value, ensuring that foo still remains but snapshot is amended. + $result = $snapshot->set( array( 'bar' => array( 'extra' => 'ok' ) ) ); + $this->assertNull( $result['errors'] ); + $resultant_data = $snapshot->data(); + $this->assertEquals( 'ok', $resultant_data['foo']['value'] ); + $this->assertArrayHasKey( 'extra', $resultant_data['bar'] ); + $this->assertNull( $resultant_data['bar']['value'] ); + } + + /** + * Test set with a non-array param. + * + * @see Customize_Snapshot::set() + * @expectedException Exception + */ + function test_set_with_non_array_params() { + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->ensure_customize_manager(); + $manager->init(); + $snapshot = new Customize_Snapshot_Back_Compat( $manager, self::UUID ); + $snapshot->set( array( 'foo' => 'bad' ) ); + } + + /** + * Test saved. + * + * @see Customize_Snapshot::saved() + */ + function test_saved() { + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->init(); + + $snapshot = new Customize_Snapshot_Back_Compat( $manager, self::UUID ); + $this->assertFalse( $snapshot->saved() ); + + $manager->post_type->save( array( + 'uuid' => self::UUID, + 'data' => array( 'foo' => array( 'value' => 'bar' ) ), + ) ); + } + + /** + * Snapshot object passed in customize_snapshot_save filter. + * + * @var Customize_Snapshot + */ + public $filtered_snapshot; + + /** + * Test that the snapshot object is passed as the second filter param. + * + * @see Customize_Snapshot::save() + */ + function test_filter_customize_snapshot_save() { + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->ensure_customize_manager(); + $manager->init(); + + $snapshot = new Customize_Snapshot_Back_Compat( $manager, self::UUID ); + + $that = $this; // For PHP 5.3. + add_filter( 'customize_snapshot_save', function( $data, $test_snapshot ) use ( $that ) { + $that->filtered_snapshot = $test_snapshot; + return $data; + }, 10, 2 ); + + $snapshot->save( array( + 'uuid' => self::UUID, + 'data' => array( 'foo' => array( 'value' => 'bar' ) ), + ) ); + + $this->assertEquals( $snapshot, $this->filtered_snapshot ); + } +} diff --git a/tests/php/test-class-customize-snapshot-manager-back-compat.php b/tests/php/test-class-customize-snapshot-manager-back-compat.php new file mode 100644 index 00000000..6592c6a7 --- /dev/null +++ b/tests/php/test-class-customize-snapshot-manager-back-compat.php @@ -0,0 +1,746 @@ +plugin = get_plugin_instance(); + if ( ! $this->plugin->compat ) { + $this->markTestSkipped( 'WordPress Version 4.6.x or below is required for this test-case.' ); + } + require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' ); + $GLOBALS['wp_customize'] = new \WP_Customize_Manager(); // WPCS: global override ok. + $this->wp_customize = $GLOBALS['wp_customize']; + + $this->wp_customize->add_setting( 'foo', array( 'default' => 'foo_default' ) ); + $this->wp_customize->add_setting( 'bar', array( 'default' => 'bar_default' ) ); + + $this->manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $this->manager->init(); + $this->user_id = $this->factory()->user->create( array( 'role' => 'administrator' ) ); + + remove_action( 'after_setup_theme', 'twentyfifteen_setup' ); + remove_action( 'after_setup_theme', 'twentysixteen_setup' ); + remove_all_actions( 'send_headers' ); // Prevent X-hacker header in VIP Quickstart. + + // For why these hooks have to be removed, see . + $this->css_concat_init_priority = has_action( 'init', 'css_concat_init' ); + if ( $this->css_concat_init_priority ) { + remove_action( 'init', 'css_concat_init', $this->css_concat_init_priority ); + } + $this->js_concat_init_priority = has_action( 'init', 'js_concat_init' ); + if ( $this->js_concat_init_priority ) { + remove_action( 'init', 'js_concat_init', $this->js_concat_init_priority ); + } + } + + /** + * Clean up global scope. + */ + function clean_up_global_scope() { + unset( $GLOBALS['wp_scripts'] ); + unset( $GLOBALS['wp_styles'] ); + unset( $_REQUEST['customize_snapshot_uuid'] ); + unset( $_REQUEST['wp_customize_preview_ajax'] ); + parent::clean_up_global_scope(); + } + + /** + * Tear down. + */ + function tearDown() { + $this->wp_customize = null; + $this->manager = null; + unset( $GLOBALS['wp_customize'] ); + unset( $GLOBALS['screen'] ); + $_REQUEST = array(); + parent::tearDown(); + } + + /** + * Set wp_customize query param. + */ + function do_customize_on() { + $_REQUEST['wp_customize'] = 'on'; + } + + /** + * Do Customize boot actions. + * + * @param bool $on Whether to turn on Customizer. + */ + function do_customize_boot_actions( $on = false ) { + $_SERVER['REQUEST_METHOD'] = 'POST'; + do_action( 'setup_theme' ); + $_REQUEST['nonce'] = wp_create_nonce( 'preview-customize_' . $this->wp_customize->theme()->get_stylesheet() ); + do_action( 'after_setup_theme' ); + do_action( 'init' ); + do_action( 'wp_loaded' ); + do_action( 'wp', $GLOBALS['wp'] ); + if ( $on ) { + $this->do_customize_on(); + } + } + + /** + * Tests load_snapshot. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::init() + * @covers \CustomizeSnapshots\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_Back_Compat( $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' ) ) ); + } + + /** + * Test constructor with Customizer. + * + * @see Customize_Snapshot_Manager_Back_Compat::__construct() + */ + function test_construct_with_customize() { + wp_set_current_user( $this->user_id ); + $this->do_customize_boot_actions( true ); + $this->assertTrue( is_customize_preview() ); + $_REQUEST['customize_snapshot_uuid'] = self::UUID; + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->init(); + $this->assertEquals( $manager->current_snapshot_uuid, self::UUID ); + $this->assertInstanceOf( 'CustomizeSnapshots\Post_Type_Back_Compat', $manager->post_type ); + $this->assertInstanceOf( 'CustomizeSnapshots\Customize_Snapshot_Back_Compat', $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_controls_scripts' ) ) ); + } + + /** + * Test init. + * + * @see Customize_Snapshot_Manager_Back_Compat::init() + */ + function test_init() { + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->init(); + $this->assertEquals( 10, has_filter( 'customize_refresh_nonces', array( $manager, 'filter_customize_refresh_nonces' ) ) ); + $this->assertEquals( 10, has_action( 'template_redirect', array( $manager, 'show_theme_switch_error' ) ) ); + $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' ) ) ); + $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_save', array( $manager, 'check_customize_publish_authorization' ) ) ); + } + + /* + * For Customize_Snapshot_Manager_Back_Compat::Customize_Snapshot_Manager_Back_Compat(), see Test_Ajax_Customize_Snapshot_Manager_Back_Compat::test_ajax_update_snapshot_cap_check(). + */ + + /** + * Test customize preview init. + * + * @see Customize_Snapshot_Manager::customize_preview_init() + */ + function test_customize_preview_init() { + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $this->assertFalse( has_action( 'wp_enqueue_scripts', array( $manager, 'enqueue_preview_scripts' ) ) ); + $manager->customize_preview_init(); + $this->assertEquals( 10, has_action( 'wp_enqueue_scripts', array( $manager, 'enqueue_preview_scripts' ) ) ); + } + + /** + * Test enqueue preview scripts. + * + * @see Customize_Snapshot_Manager::enqueue_preview_scripts() + */ + function test_enqueue_preview_scripts() { + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->ensure_customize_manager(); + $manager->init(); + $handle = 'customize-snapshots-preview'; + $this->assertFalse( wp_scripts()->query( $handle, 'enqueued' ) ); + $this->assertFalse( wp_styles()->query( $handle, 'enqueued' ) ); + $manager->enqueue_preview_scripts(); + $this->assertTrue( wp_scripts()->query( $handle, 'enqueued' ) ); + $this->assertTrue( wp_styles()->query( $handle, 'enqueued' ) ); + + $after = wp_scripts()->get_data( $handle, 'after' ); + $this->assertNotEmpty( $after ); + $this->assertContains( 'CustomizeSnapshotsPreview', join( '', $after ) ); + } + + /** + * 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_Back_Compat( $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_Back_Compat( $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' ) ); + } + + /** + * Test filter_customize_refresh_nonces. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager_Back_Compat::filter_customize_refresh_nonces() + */ + function test_filter_customize_refresh_nonces() { + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $this->assertArrayHasKey( 'snapshot', $manager->filter_customize_refresh_nonces( array() ) ); + } + + /** + * Tests show_theme_switch_error. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager_Back_Compat::show_theme_switch_error() + */ + function test_show_theme_switch_error() { + $this->markTestIncomplete(); + } + + /** + * Test publish snapshot with customize_save_after. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager_Back_Compat::publish_snapshot_with_customize_save_after() + */ + function test_publish_snapshot_with_customize_save_after() { + wp_set_current_user( $this->user_id ); + $this->do_customize_boot_actions( true ); + $_POST = array( + 'nonce' => wp_create_nonce( 'save-customize_' . $this->wp_customize->get_stylesheet() ), + 'customize_snapshot_uuid' => self::UUID, + 'customized' => '{"foo":"foo_default","bar":"bar_default"}', + ); + $_REQUEST['action'] = 'customize_save'; + $_REQUEST['customize_snapshot_uuid'] = self::UUID; + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->init(); + $this->assertEmpty( $manager->snapshot()->post() ); + $manager->publish_snapshot_with_customize_save_after(); + $this->assertNotEmpty( $manager->snapshot()->post() ); + + $this->markTestIncomplete( 'Need to test when snapshot->save() returns errors, and when snapshot post save fails.' ); + } + + /** + * Test save_settings_with_publish_snapshot. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager_Back_Compat::save_settings_with_publish_snapshot() + */ + public function test_save_settings_with_publish_snapshot() { + $post_type = $this->manager->post_type; + $data = array( + 'blogdescription' => array( 'value' => 'Snapshot blog' ), + 'unknown_setting_foo' => array( 'value' => 'bar' ), + 'null_value_baz' => array( 'value' => null ), + 'foo' => array( 'value' => 'foo' ), + ); + $validate_data = array( + 'blogdescription' => array( 'value' => 'Snapshot blog' ), + 'unknown_setting_foo' => array( + 'value' => 'bar', + 'publish_error' => 'unrecognized_setting', + ), + 'null_value_baz' => array( + 'value' => null, + 'publish_error' => 'null_value', + ), + 'foo' => array( + 'value' => 'foo', + ), + ); + + if ( method_exists( 'WP_Customize_Setting', 'validate' ) ) { + $validate_data['foo']['publish_error'] = 'you_shell_not_pass'; + add_filter( 'customize_validate_foo', function( $validity ) { + $validity->add( 'you_shell_not_pass', 'Testing invalid setting while publishing snapshot' ); + return $validity; + }, 10, 1 ); + } + + $post_id = $post_type->save( array( + 'uuid' => self::UUID, + 'data' => $data, + 'status' => 'draft', + ) ); + + // Test invalid settings. + $post = get_post( $post_id ); + $this->manager->save_settings_with_publish_snapshot( 'publish', 'draft', $post ); + $post = get_post( $post_id ); + $this->assertEquals( $validate_data, json_decode( wp_unslash( $post->post_content ), true ) ); + $this->assertEquals( 'pending', $post->post_status ); + + // Test valid settings. + unset( $data['unknown_setting_foo'], $data['null_value_baz'], $data['foo'] ); + $post_id = $post_type->save( array( + 'uuid' => self::UUID, + 'data' => $data, + 'status' => 'publish', + ) ); + $this->assertEquals( 'publish', get_post_status( $post_id ) ); + $this->assertEquals( 'Snapshot blog', get_bloginfo( 'description' ) ); + } + + /* + * For Customize_Snapshot_Manager::handle_update_snapshot_request(), see Test_Ajax_Customize_Snapshot_Manager_Back_Compat. + */ + + /** + * Tests ensure_customize_manager. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::ensure_customize_manager() + */ + public function test_ensure_customize_manager() { + global $wp_customize; + $wp_customize = null; // WPCS: global override ok. + $manager = new Customize_Snapshot_Manager_Back_Compat( $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 ); + } + + /** + * Test enqueue controls scripts. + * + * @see Customize_Snapshot_Manager::enqueue_controls_scripts() + */ + function test_enqueue_controls_scripts() { + $this->plugin->register_scripts( wp_scripts() ); + $this->plugin->register_styles( wp_styles() ); + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->init(); + $manager->enqueue_controls_scripts(); + $this->assertTrue( wp_script_is( 'customize-snapshots-compat', 'enqueued' ) ); + $this->assertTrue( wp_style_is( 'customize-snapshots', 'enqueued' ) ); + } + + /** + * Tests import_snapshot_data. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::import_snapshot_data() + */ + public function test_import_snapshot_data() { + global $wp_actions; + $_REQUEST['customize_snapshot_uuid'] = self::UUID; + $this->manager->post_type->save( array( + 'uuid' => self::UUID, + 'data' => array( + 'blogname' => array( 'value' => 'Hello' ), + 'blogdescription' => array( 'value' => null ), + ), + 'status' => 'draft', + ) ); + + // Prevent init from calling import_snapshot_data straight away. + unset( $wp_actions['setup_theme'] ); + + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->init(); + $manager->ensure_customize_manager(); + do_action( 'customize_register', $manager->customize_manager ); + + $this->assertArrayNotHasKey( 'customized', $_POST ); + $this->assertArrayNotHasKey( 'customized', $_REQUEST ); + $this->assertArrayNotHasKey( 'blogname', $manager->customize_manager->unsanitized_post_values() ); + $this->assertArrayNotHasKey( 'blogdescription', $manager->customize_manager->unsanitized_post_values() ); + $manager->import_snapshot_data(); + $this->assertArrayHasKey( 'customized', $_POST ); + $this->assertArrayHasKey( 'customized', $_REQUEST ); + $this->assertArrayHasKey( 'blogname', $manager->customize_manager->unsanitized_post_values() ); + $this->assertArrayNotHasKey( 'blogdescription', $manager->customize_manager->unsanitized_post_values() ); + } + + /** + * Tests should_import_and_preview_snapshot. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::should_import_and_preview_snapshot() + */ + public function test_should_import_and_preview_snapshot() { + 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_Back_Compat( $manager, self::UUID ); + + // Not if admin. + set_current_screen( 'posts' ); + $pagenow = 'posts.php'; // WPCS: global override ok. + $this->assertTrue( is_admin() ); + $this->assertFalse( $manager->should_import_and_preview_snapshot( $snapshot ) ); + + // Not if theme switch error. + set_current_screen( 'customize' ); + $pagenow = 'customize.php'; // WPCS: global override ok. + update_post_meta( $post_id, '_snapshot_theme', 'Foo' ); + $this->assertFalse( $manager->should_import_and_preview_snapshot( $snapshot ) ); + delete_post_meta( $post_id, '_snapshot_theme' ); + + // Not if customize_save. + $_REQUEST['action'] = 'customize_save'; + $this->assertFalse( $manager->should_import_and_preview_snapshot( $snapshot ) ); + unset( $_REQUEST['action'] ); + + // Not if published snapshot. + $manager->post_type->save( array( + 'uuid' => self::UUID, + 'status' => 'publish', + ) ); + $this->assertFalse( $manager->should_import_and_preview_snapshot( $snapshot ) ); + $manager->post_type->save( array( + 'uuid' => self::UUID, + 'status' => 'draft', + ) ); + + // Not if unsanitized post values is not empty. + $manager->customize_manager = new \WP_Customize_Manager(); + $wp_customize = $manager->customize_manager; // WPCS: global override ok. + $wp_customize->set_post_value( 'name', 'value' ); + $this->assertNotEmpty( $manager->customize_manager->unsanitized_post_values() ); + $this->assertFalse( $manager->should_import_and_preview_snapshot( $snapshot ) ); + + // OK. + $manager->customize_manager = new \WP_Customize_Manager(); + $wp_customize = $manager->customize_manager; // WPCS: global override ok. + $this->assertTrue( $manager->should_import_and_preview_snapshot( $snapshot ) ); + } + + /** + * Tests get_theme_switch_error. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::get_theme_switch_error() + */ + function test_get_theme_switch_error() { + $this->markTestIncomplete(); + } + + /** + * Tests is_previewing_settings. + * + * @covers \CustomizeSnapshots\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_Back_Compat( $this->plugin ); + $manager->init(); + $manager->preview_snapshot_settings(); + $this->assertTrue( $manager->is_previewing_settings() ); + } + + /** + * Tests is_previewing_settings. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::is_previewing_settings() + */ + public function test_is_previewing_settings_via_preview_init() { + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $this->assertFalse( $manager->is_previewing_settings() ); + do_action( 'customize_preview_init' ); + $this->assertTrue( $manager->is_previewing_settings() ); + } + + /** + * Tests preview_snapshot_settings. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::preview_snapshot_settings() + */ + public function test_preview_snapshot_settings() { + global $wp_actions; + $_REQUEST['customize_snapshot_uuid'] = self::UUID; + $this->manager->post_type->save( array( + 'uuid' => self::UUID, + 'data' => array( + 'blogname' => array( 'value' => 'Hello' ), + ), + 'status' => 'draft', + ) ); + + // Prevent init from calling preview_snapshot_settings straight away. + unset( $wp_actions['wp_loaded'] ); + + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->init(); + $manager->ensure_customize_manager(); + do_action( 'customize_register', $manager->customize_manager ); + $this->assertFalse( $manager->is_previewing_settings() ); + $this->assertFalse( $manager->customize_manager->get_setting( 'blogname' )->dirty ); + $this->assertNotEquals( 'Hello', get_option( 'blogname' ) ); + $manager->preview_snapshot_settings(); + $this->assertEquals( 'Hello', get_option( 'blogname' ) ); + $this->assertTrue( $manager->customize_manager->get_setting( 'blogname' )->dirty ); + } + + /** + * Tests add_widget_setting_preview_filters. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::add_widget_setting_preview_filters() + */ + public function test_add_widget_setting_preview_filters() { + $this->markTestIncomplete(); + } + + /** + * Tests add_nav_menu_setting_preview_filters. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::add_nav_menu_setting_preview_filters() + */ + public function test_add_nav_menu_setting_preview_filters() { + $this->markTestIncomplete(); + } + + /** + * Tests preview_early_nav_menus_in_customizer. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::preview_early_nav_menus_in_customizer() + */ + public function test_preview_early_nav_menus_in_customizer() { + global $pagenow; + $pagenow = 'customize.php'; // WPCS: Global override ok. + set_current_screen( 'customize' ); + + $menu_id = -123; + $setting_id = sprintf( 'nav_menu[%d]', $menu_id ); + + $_REQUEST['customize_snapshot_uuid'] = self::UUID; + $this->manager->post_type->save( array( + 'uuid' => self::UUID, + 'data' => array( + $setting_id => array( + 'value' => array( + 'name' => 'Bar', + ), + ), + ), + 'status' => 'draft', + ) ); + + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->init(); + do_action( 'customize_register', $manager->customize_manager ); + + $setting = $manager->customize_manager->get_setting( $setting_id ); + $this->assertInstanceOf( 'WP_Customize_Nav_Menu_Setting', $setting ); + $nav_menu = wp_get_nav_menu_object( $menu_id ); + $this->assertEquals( 'Bar', $nav_menu->name ); + + $this->assertInstanceOf( 'WP_Customize_Nav_Menu_Section', $manager->customize_manager->get_section( $setting_id ) ); + } + + /** + * Tests setup_preview_ajax_requests. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::init() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::setup_preview_ajax_requests() + */ + public function test_setup_preview_ajax_requests() { + wp_set_current_user( $this->user_id ); + $_REQUEST['wp_customize_preview_ajax'] = 'true'; + $_POST['customized'] = wp_slash( wp_json_encode( array( 'blogname' => 'Foo' ) ) ); + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $this->do_customize_boot_actions( true ); + $this->assertTrue( is_customize_preview() ); + $manager->init(); + $this->assertEquals( 12, has_action( 'wp_loaded', array( $manager, 'setup_preview_ajax_requests' ) ) ); + do_action( 'wp_loaded' ); + + $this->assertFalse( has_action( 'shutdown', array( $this->wp_customize, 'customize_preview_signature' ) ) ); + $this->assertEquals( 5, has_action( 'parse_request', array( $manager, 'override_request_method' ) ) ); + } + + + /** + * Tests setup_preview_ajax_requests for admin_ajax. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::init() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::setup_preview_ajax_requests() + */ + public function test_setup_preview_ajax_requests_for_admin_ajax() { + global $pagenow; + wp_set_current_user( $this->user_id ); + + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'GET'; + $pagenow = 'admin-ajax.php'; // WPCS: Global override ok. + set_current_screen( 'admin-ajax' ); + $this->assertTrue( is_admin() ); + + $_REQUEST['wp_customize_preview_ajax'] = 'true'; + $_POST['customized'] = wp_slash( wp_json_encode( array( 'blogname' => 'Foo' ) ) ); + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->init(); + do_action( 'admin_init' ); + $this->do_customize_boot_actions( true ); + $this->assertTrue( is_customize_preview() ); + $this->assertFalse( has_action( 'shutdown', array( $this->wp_customize, 'customize_preview_signature' ) ) ); + $this->assertFalse( has_action( 'parse_request', array( $manager, 'override_request_method' ) ) ); + $this->assertEquals( 'GET', $_SERVER['REQUEST_METHOD'] ); + $this->assertEquals( 'Foo', get_option( 'blogname' ) ); + } + + /** + * Tests override_request_method. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::override_request_method() + */ + public function test_override_request_method() { + global $wp; + + $manager = new Customize_Snapshot_Manager_Back_Compat( $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 ); + + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'PUT'; + $this->assertFalse( $manager->override_request_method() ); + } + + /** + * Test customize menu. + * + * @see Customize_Snapshot_Manager::customize_menu() + */ + public function test_customize_menu() { + set_current_screen( 'front' ); + $preview_url = home_url( '/' ); + + $_REQUEST['customize_snapshot_uuid'] = self::UUID; + $manager = new Customize_Snapshot_Manager_Back_Compat( $this->plugin ); + $manager->init(); + + require_once( ABSPATH . WPINC . '/class-wp-admin-bar.php' ); + $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 ) ); + $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'] ); + } + +} diff --git a/tests/php/test-class-customize-snapshot-manager.php b/tests/php/test-class-customize-snapshot-manager.php index 393ebebf..8d9c3c5f 100644 --- a/tests/php/test-class-customize-snapshot-manager.php +++ b/tests/php/test-class-customize-snapshot-manager.php @@ -61,12 +61,24 @@ class Test_Customize_Snapshot_Manager extends \WP_UnitTestCase { */ protected $js_concat_init_priority; + /** + * Frontend UUID + * + * @var string + */ + public $front_param; + /** * Set up. */ function setUp() { parent::setUp(); $this->plugin = get_plugin_instance(); + if ( $this->plugin->compat ) { + $this->front_param = 'customize_snapshot_uuid'; + } else { + $this->front_param = 'customize_changeset_uuid'; + } require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' ); $GLOBALS['wp_customize'] = new \WP_Customize_Manager(); // WPCS: global override ok. $this->wp_customize = $GLOBALS['wp_customize']; @@ -74,7 +86,7 @@ function setUp() { $this->wp_customize->add_setting( 'foo', array( 'default' => 'foo_default' ) ); $this->wp_customize->add_setting( 'bar', array( 'default' => 'bar_default' ) ); - $this->manager = new Customize_Snapshot_Manager( $this->plugin ); + $this->manager = $this->get_snapshot_manager_instance( $this->plugin ); $this->manager->init(); $this->user_id = $this->factory()->user->create( array( 'role' => 'administrator' ) ); @@ -99,7 +111,7 @@ function setUp() { function clean_up_global_scope() { unset( $GLOBALS['wp_scripts'] ); unset( $GLOBALS['wp_styles'] ); - unset( $_REQUEST['customize_snapshot_uuid'] ); + unset( $_REQUEST[ $this->front_param ] ); unset( $_REQUEST['wp_customize_preview_ajax'] ); parent::clean_up_global_scope(); } @@ -141,12 +153,37 @@ function do_customize_boot_actions( $on = false ) { } } + /** + * Get snapshot manager instance according to WP version. + * + * @param Plugin $plugin Plugin object. + * + * @return Customize_Snapshot_Manager|Customize_Snapshot_Manager_Back_Compat Manager new instace. + */ + function get_snapshot_manager_instance( $plugin ) { + if ( $this->plugin->compat ) { + return new Customize_Snapshot_Manager_Back_Compat( $plugin ); + } else { + return new Customize_Snapshot_Manager( $plugin ); + } + } + + /** + * Mark test incomplete as it is only for new versions. + */ + public function mark_incompatible() { + if ( $this->plugin->compat ) { + $this->markTestSkipped( 'This unit-test require WP version 4.7 or up.' ); + } + } + /** * Test constructor. * * @see Customize_Snapshot_Manager::__construct() */ function test_construct_without_customize() { + $this->mark_incompatible(); $this->assertInstanceOf( 'CustomizeSnapshots\Customize_Snapshot_Manager', $this->manager ); $this->assertInstanceOf( 'CustomizeSnapshots\Plugin', $this->manager->plugin ); $this->assertNull( $this->manager->current_snapshot_uuid ); @@ -158,18 +195,18 @@ function test_construct_without_customize() { * @see Customize_Snapshot_Manager::__construct() */ function test_construct_with_customize() { + $this->mark_incompatible(); wp_set_current_user( $this->user_id ); $this->do_customize_boot_actions( true ); $this->assertTrue( is_customize_preview() ); - $_REQUEST['customize_snapshot_uuid'] = self::UUID; - $manager = new Customize_Snapshot_Manager( $this->plugin ); + $_REQUEST[ $this->front_param ] = self::UUID; + $manager = $this->get_snapshot_manager_instance( $this->plugin ); $manager->init(); $this->assertEquals( $manager->current_snapshot_uuid, self::UUID ); $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_controls_scripts' ) ) ); - $this->assertEquals( 10, has_action( 'wp_ajax_customize_update_snapshot', array( $manager, 'handle_update_snapshot_request' ) ) ); } /** @@ -181,192 +218,126 @@ function test_construct_with_customize_bootstrapped() { wp_set_current_user( $this->user_id ); $this->do_customize_boot_actions( true ); unset( $GLOBALS['wp_customize'] ); - $_REQUEST['customize_snapshot_uuid'] = self::UUID; - $manager = new Customize_Snapshot_Manager( $this->plugin ); + $_REQUEST[ $this->front_param ] = self::UUID; + $manager = $this->get_snapshot_manager_instance( $this->plugin ); $manager->ensure_customize_manager(); $this->assertInstanceOf( 'WP_Customize_Manager', $GLOBALS['wp_customize'] ); } /** - * Tests init hooks. + * Test common hooks. * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::init() + * @see \CustomizeSnapshots\Customize_Snapshot_Manager::hooks() */ - public function test_init_hooks() { - $manager = new Customize_Snapshot_Manager( $this->plugin ); + function test_hooks() { + $manager = $this->get_snapshot_manager_instance( $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( 'init', array( $manager->post_type, 'init' ) ) ); $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( 'admin_enqueue_scripts', array( $manager, 'enqueue_admin_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( 'removable_query_args', array( $manager, 'filter_removable_query_args' ) ) ); - $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 CustomizeSnapshots\Customize_Snapshot_Manager::init() - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::read_current_snapshot_uuid() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::init() */ - 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 ); + public function test_init_hooks() { + $this->mark_incompatible(); + $manager = $this->get_snapshot_manager_instance( $this->plugin ); $manager->init(); - $this->assertEquals( self::UUID, $manager->current_snapshot_uuid ); + $this->assertEquals( 10, has_filter( 'customize_save_response', array( $manager, 'add_snapshot_var_to_customize_save' ) ) ); + $this->assertInstanceOf( __NAMESPACE__ . '\Post_Type', $manager->post_type ); } /** * Tests load_snapshot. * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::init() - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::load_snapshot() + * @covers \CustomizeSnapshots\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', - ) ); + $this->mark_incompatible(); $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' ) ) ); + $this->assertNull( $manager->snapshot ); + $this->assertNull( $manager->customize_manager ); + $manager->load_snapshot(); + $this->assertInstanceOf( __NAMESPACE__ . '\\Customize_Snapshot', $manager->snapshot ); + $this->assertInstanceOf( '\WP_Customize_Manager', $manager->customize_manager ); } - /** - * Tests setup_preview_ajax_requests. + * Tests add_snapshot_var_to_customize_save. * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::init() - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::setup_preview_ajax_requests() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::add_snapshot_var_to_customize_save() */ - public function test_setup_preview_ajax_requests() { - wp_set_current_user( $this->user_id ); - $_REQUEST['wp_customize_preview_ajax'] = 'true'; - $_POST['customized'] = wp_slash( wp_json_encode( array( 'blogname' => 'Foo' ) ) ); + public function test_add_snapshot_var_to_customize_save() { + $this->mark_incompatible(); + global $wp_customize; + $uuid = wp_generate_uuid4(); + get_plugin_instance()->customize_snapshot_manager->post_type->save( array( + 'uuid' => $uuid, + 'data' => array(), + 'status' => 'draft', + ) ); $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' ) ) ); + $wp_customize = null; // WPCS: global override ok. + $manager->current_snapshot_uuid = $uuid; + $manager->load_snapshot(); + $data = $manager->add_snapshot_var_to_customize_save( array(), $manager->customize_manager ); + $this->assertArrayHasKey( 'edit_link', $data ); + $this->assertArrayHasKey( 'publish_date', $data ); + $this->assertArrayHasKey( 'title', $data ); } - /** - * Tests setup_preview_ajax_requests for admin_ajax. + * Tests enqueue_admin_scripts. * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::init() - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::setup_preview_ajax_requests() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::enqueue_admin_scripts() */ - public function test_setup_preview_ajax_requests_for_admin_ajax() { - global $pagenow; - wp_set_current_user( $this->user_id ); - - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'GET'; - $pagenow = 'admin-ajax.php'; // WPCS: Global override ok. - set_current_screen( 'admin-ajax' ); - $this->assertTrue( is_admin() ); - - $_REQUEST['wp_customize_preview_ajax'] = 'true'; - $_POST['customized'] = wp_slash( wp_json_encode( array( 'blogname' => 'Foo' ) ) ); - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $manager->init(); - do_action( 'admin_init' ); - $this->do_customize_boot_actions( true ); - $this->assertTrue( is_customize_preview() ); - $this->assertFalse( has_action( 'shutdown', array( $this->wp_customize, 'customize_preview_signature' ) ) ); - $this->assertFalse( has_action( 'parse_request', array( $manager, 'override_request_method' ) ) ); - $this->assertEquals( 'GET', $_SERVER['REQUEST_METHOD'] ); - $this->assertEquals( 'Foo', get_option( 'blogname' ) ); + public function test_enqueue_admin_scripts() { + $this->markTestIncomplete(); } /** - * Tests override_request_method. + * Tests init hooks. * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::override_request_method() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::init() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::read_current_snapshot_uuid() */ - public function test_override_request_method() { - global $wp; - - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $this->assertFalse( $manager->override_request_method() ); - - $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'GET'; - $wp->query_vars['rest_route'] = '/wp/v1/foo'; - $this->assertFalse( $manager->override_request_method() ); - unset( $wp->query_vars['rest_route'] ); + public function test_read_current_snapshot_uuid() { + $manager = $this->get_snapshot_manager_instance( $this->plugin ); + $manager->init(); - $_SERVER['REQUEST_METHOD'] = 'GET'; - $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'GET'; - $this->assertFalse( $manager->override_request_method() ); + $this->assertFalse( $manager->read_current_snapshot_uuid() ); + $this->assertNull( $manager->current_snapshot_uuid ); - $_SERVER['REQUEST_METHOD'] = 'GET'; - $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'BAD'; - $this->assertFalse( $manager->override_request_method() ); + $_REQUEST[ $manager->get_customize_uuid_param() ] = 'bad'; + $this->assertFalse( $manager->read_current_snapshot_uuid() ); + $this->assertNull( $manager->current_snapshot_uuid ); - $_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 ); + $_REQUEST[ $manager->get_customize_uuid_param() ] = self::UUID; + $this->assertTrue( $manager->read_current_snapshot_uuid() ); + $this->assertEquals( self::UUID, $manager->current_snapshot_uuid ); - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'PUT'; - $this->assertFalse( $manager->override_request_method() ); + $_REQUEST[ $manager->get_customize_uuid_param() ] = self::UUID; + $manager = $this->get_snapshot_manager_instance( $this->plugin ); + $manager->init(); + $this->assertEquals( self::UUID, $manager->current_snapshot_uuid ); } /** * Tests doing_customize_save_ajax. * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::doing_customize_save_ajax() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::doing_customize_save_ajax() */ public function test_doing_customize_save_ajax() { - $manager = new Customize_Snapshot_Manager( $this->plugin ); + $manager = $this->get_snapshot_manager_instance( $this->plugin ); $this->assertFalse( $manager->doing_customize_save_ajax() ); $_REQUEST['action'] = 'foo'; @@ -379,12 +350,12 @@ public function test_doing_customize_save_ajax() { /** * Tests ensure_customize_manager. * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::ensure_customize_manager() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::ensure_customize_manager() */ public function test_ensure_customize_manager() { global $wp_customize; $wp_customize = null; // WPCS: global override ok. - $manager = new Customize_Snapshot_Manager( $this->plugin ); + $manager = $this->get_snapshot_manager_instance( $this->plugin ); $this->assertEmpty( $manager->customize_manager ); $manager->ensure_customize_manager(); $this->assertInstanceOf( 'WP_Customize_Manager', $manager->customize_manager ); @@ -394,227 +365,18 @@ public function test_ensure_customize_manager() { /** * Tests is_theme_active. * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::is_theme_active() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::is_theme_active() */ public function test_is_theme_active() { global $wp_customize; $wp_customize = null; // WPCS: global override ok. - $manager = new Customize_Snapshot_Manager( $this->plugin ); + $manager = $this->get_snapshot_manager_instance( $this->plugin ); $this->assertTrue( $manager->is_theme_active() ); $manager->ensure_customize_manager(); $this->assertTrue( $manager->is_theme_active() ); } - /** - * Tests should_import_and_preview_snapshot. - * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::should_import_and_preview_snapshot() - */ - public function test_should_import_and_preview_snapshot() { - global $pagenow, $wp_customize; - $_REQUEST['customize_snapshot_uuid'] = self::UUID; - $manager = $this->plugin->customize_snapshot_manager; - $post_id = $manager->post_type->save( array( - 'uuid' => self::UUID, - 'data' => array( 'blogname' => array( 'value' => 'Foo' ) ), - ) ); - $snapshot = new Customize_Snapshot( $manager, self::UUID ); - - // Not if admin. - set_current_screen( 'posts' ); - $pagenow = 'posts.php'; // WPCS: global override ok. - $this->assertTrue( is_admin() ); - $this->assertFalse( $manager->should_import_and_preview_snapshot( $snapshot ) ); - - // Not if theme switch error. - set_current_screen( 'customize' ); - $pagenow = 'customize.php'; // WPCS: global override ok. - update_post_meta( $post_id, '_snapshot_theme', 'Foo' ); - $this->assertFalse( $manager->should_import_and_preview_snapshot( $snapshot ) ); - delete_post_meta( $post_id, '_snapshot_theme' ); - - // Not if customize_save. - $_REQUEST['action'] = 'customize_save'; - $this->assertFalse( $manager->should_import_and_preview_snapshot( $snapshot ) ); - unset( $_REQUEST['action'] ); - - // Not if published snapshot. - $manager->post_type->save( array( - 'uuid' => self::UUID, - 'status' => 'publish', - ) ); - $this->assertFalse( $manager->should_import_and_preview_snapshot( $snapshot ) ); - $manager->post_type->save( array( - 'uuid' => self::UUID, - 'status' => 'draft', - ) ); - - // Not if unsanitized post values is not empty. - $manager->customize_manager = new \WP_Customize_Manager(); - $wp_customize = $manager->customize_manager; // WPCS: global override ok. - $wp_customize->set_post_value( 'name', 'value' ); - $this->assertNotEmpty( $manager->customize_manager->unsanitized_post_values() ); - $this->assertFalse( $manager->should_import_and_preview_snapshot( $snapshot ) ); - - // OK. - $manager->customize_manager = new \WP_Customize_Manager(); - $wp_customize = $manager->customize_manager; // WPCS: global override ok. - $this->assertTrue( $manager->should_import_and_preview_snapshot( $snapshot ) ); - } - - /** - * Tests is_previewing_settings. - * - * @covers CustomizeSnapshots\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 CustomizeSnapshots\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. - * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::preview_snapshot_settings() - */ - public function test_preview_snapshot_settings() { - global $wp_actions; - $_REQUEST['customize_snapshot_uuid'] = self::UUID; - $this->manager->post_type->save( array( - 'uuid' => self::UUID, - 'data' => array( - 'blogname' => array( 'value' => 'Hello' ), - ), - 'status' => 'draft', - ) ); - - // Prevent init from calling preview_snapshot_settings straight away. - unset( $wp_actions['wp_loaded'] ); - - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $manager->init(); - $manager->ensure_customize_manager(); - do_action( 'customize_register', $manager->customize_manager ); - $this->assertFalse( $manager->is_previewing_settings() ); - $this->assertFalse( $manager->customize_manager->get_setting( 'blogname' )->dirty ); - $this->assertNotEquals( 'Hello', get_option( 'blogname' ) ); - $manager->preview_snapshot_settings(); - $this->assertEquals( 'Hello', get_option( 'blogname' ) ); - $this->assertTrue( $manager->customize_manager->get_setting( 'blogname' )->dirty ); - } - - /** - * Tests import_snapshot_data. - * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::import_snapshot_data() - */ - public function test_import_snapshot_data() { - global $wp_actions; - $_REQUEST['customize_snapshot_uuid'] = self::UUID; - $this->manager->post_type->save( array( - 'uuid' => self::UUID, - 'data' => array( - 'blogname' => array( 'value' => 'Hello' ), - 'blogdescription' => array( 'value' => null ), - ), - 'status' => 'draft', - ) ); - - // Prevent init from calling import_snapshot_data straight away. - unset( $wp_actions['setup_theme'] ); - - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $manager->init(); - $manager->ensure_customize_manager(); - do_action( 'customize_register', $manager->customize_manager ); - - $this->assertArrayNotHasKey( 'customized', $_POST ); - $this->assertArrayNotHasKey( 'customized', $_REQUEST ); - $this->assertArrayNotHasKey( 'blogname', $manager->customize_manager->unsanitized_post_values() ); - $this->assertArrayNotHasKey( 'blogdescription', $manager->customize_manager->unsanitized_post_values() ); - $manager->import_snapshot_data(); - $this->assertArrayHasKey( 'customized', $_POST ); - $this->assertArrayHasKey( 'customized', $_REQUEST ); - $this->assertArrayHasKey( 'blogname', $manager->customize_manager->unsanitized_post_values() ); - $this->assertArrayNotHasKey( 'blogdescription', $manager->customize_manager->unsanitized_post_values() ); - } - - /** - * Tests add_widget_setting_preview_filters. - * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::add_widget_setting_preview_filters() - */ - public function test_add_widget_setting_preview_filters() { - $this->markTestIncomplete(); - } - - /** - * Tests add_nav_menu_setting_preview_filters. - * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::add_nav_menu_setting_preview_filters() - */ - public function test_add_nav_menu_setting_preview_filters() { - $this->markTestIncomplete(); - } - - /** - * Tests preview_early_nav_menus_in_customizer. - * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::preview_early_nav_menus_in_customizer() - */ - public function test_preview_early_nav_menus_in_customizer() { - global $pagenow; - $pagenow = 'customize.php'; // WPCS: Global override ok. - set_current_screen( 'customize' ); - - $menu_id = -123; - $setting_id = sprintf( 'nav_menu[%d]', $menu_id ); - - $_REQUEST['customize_snapshot_uuid'] = self::UUID; - $this->manager->post_type->save( array( - 'uuid' => self::UUID, - 'data' => array( - $setting_id => array( - 'value' => array( - 'name' => 'Bar', - ), - ), - ), - 'status' => 'draft', - ) ); - - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $manager->init(); - do_action( 'customize_register', $manager->customize_manager ); - - $setting = $manager->customize_manager->get_setting( $setting_id ); - $this->assertInstanceOf( 'WP_Customize_Nav_Menu_Setting', $setting ); - $nav_menu = wp_get_nav_menu_object( $menu_id ); - $this->assertEquals( 'Bar', $nav_menu->name ); - - $this->assertInstanceOf( 'WP_Customize_Nav_Menu_Section', $manager->customize_manager->get_section( $setting_id ) ); - } - /** * Test add snapshot uuid to return url. * @@ -623,43 +385,16 @@ public function test_preview_early_nav_menus_in_customizer() { public function test_add_snapshot_uuid_to_return_url() { global $wp_version; if ( version_compare( $wp_version, '4.4-beta', '>=' ) ) { - $_REQUEST['customize_snapshot_uuid'] = self::UUID; - $manager = new Customize_Snapshot_Manager( $this->plugin ); + $_REQUEST[ $this->front_param ] = self::UUID; + $manager = $this->get_snapshot_manager_instance( $this->plugin ); $manager->init(); $manager->ensure_customize_manager(); - $this->assertNotContains( 'customize_snapshot_uuid', $manager->customize_manager->get_return_url() ); + $this->assertNotContains( $this->front_param, $manager->customize_manager->get_return_url() ); $manager->add_snapshot_uuid_to_return_url(); - $this->assertContains( 'customize_snapshot_uuid', $manager->customize_manager->get_return_url() ); + $this->assertContains( $this->front_param, $manager->customize_manager->get_return_url() ); } } - /** - * Tests show_theme_switch_error. - * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::show_theme_switch_error() - */ - function test_show_theme_switch_error() { - $this->markTestIncomplete(); - } - - /** - * Tests get_theme_switch_error. - * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::get_theme_switch_error() - */ - function test_get_theme_switch_error() { - $this->markTestIncomplete(); - } - - /** - * Tests check_customize_publish_authorization. - * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::check_customize_publish_authorization() - */ - function test_check_customize_publish_authorization() { - $this->markTestIncomplete(); - } - /** * Test encode JSON. * @@ -681,121 +416,31 @@ function test_encode_json() { 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 = $this->get_snapshot_manager_instance( $this->plugin ); $manager->init(); $manager->enqueue_controls_scripts(); $this->assertTrue( wp_script_is( 'customize-snapshots', 'enqueued' ) ); $this->assertTrue( wp_style_is( 'customize-snapshots', 'enqueued' ) ); } - /** - * Test customize preview init. - * - * @see Customize_Snapshot_Manager::customize_preview_init() - */ - function test_customize_preview_init() { - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $this->assertFalse( has_action( 'wp_enqueue_scripts', array( $manager, 'enqueue_preview_scripts' ) ) ); - $manager->customize_preview_init(); - $this->assertEquals( 10, has_action( 'wp_enqueue_scripts', array( $manager, 'enqueue_preview_scripts' ) ) ); - } - - /** - * Test enqueue preview scripts. - * - * @see Customize_Snapshot_Manager::enqueue_preview_scripts() - */ - function test_enqueue_preview_scripts() { - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $manager->ensure_customize_manager(); - $manager->init(); - $handle = 'customize-snapshots-preview'; - $this->assertFalse( wp_scripts()->query( $handle, 'enqueued' ) ); - $this->assertFalse( wp_styles()->query( $handle, 'enqueued' ) ); - $manager->enqueue_preview_scripts(); - $this->assertTrue( wp_scripts()->query( $handle, 'enqueued' ) ); - $this->assertTrue( wp_styles()->query( $handle, 'enqueued' ) ); - - $after = wp_scripts()->get_data( $handle, 'after' ); - $this->assertNotEmpty( $after ); - $this->assertContains( 'CustomizeSnapshotsPreview', join( '', $after ) ); - } - - /** - * 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' ) ); - } - - /** - * Test filter_customize_refresh_nonces. - * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::filter_customize_refresh_nonces() - */ - function test_filter_customize_refresh_nonces() { - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $this->assertArrayHasKey( 'snapshot', $manager->filter_customize_refresh_nonces( array() ) ); - } - /** * Test snapshot method. * * @see Customize_Snapshot_Manager::snapshot() */ function test_snapshot() { - $_REQUEST['customize_snapshot_uuid'] = self::UUID; - $manager = new Customize_Snapshot_Manager( $this->plugin ); + $_REQUEST[ $this->front_param ] = self::UUID; + $manager = $this->get_snapshot_manager_instance( $this->plugin ); $manager->init(); $this->assertInstanceOf( 'CustomizeSnapshots\Customize_Snapshot', $manager->snapshot() ); } - /** - * Test publish snapshot with customize_save_after. - * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::publish_snapshot_with_customize_save_after() - */ - function test_publish_snapshot_with_customize_save_after() { - wp_set_current_user( $this->user_id ); - $this->do_customize_boot_actions( true ); - $_POST = array( - 'nonce' => wp_create_nonce( 'save-customize_' . $this->wp_customize->get_stylesheet() ), - 'customize_snapshot_uuid' => self::UUID, - 'customized' => '{"foo":"foo_default","bar":"bar_default"}', - ); - $_REQUEST['action'] = 'customize_save'; - $_REQUEST['customize_snapshot_uuid'] = self::UUID; - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $manager->init(); - $this->assertEmpty( $manager->snapshot()->post() ); - $manager->publish_snapshot_with_customize_save_after(); - $this->assertNotEmpty( $manager->snapshot()->post() ); - - $this->markTestIncomplete( 'Need to test when snapshot->save() returns errors, and when snapshot post save fails.' ); - } - /** * Test prepare_snapshot_post_content_for_publish. * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::prepare_snapshot_post_content_for_publish() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::prepare_snapshot_post_content_for_publish() */ public function test_prepare_snapshot_post_content_for_publish() { - $snapshot_manager = get_plugin_instance()->customize_snapshot_manager; $data = array( 'blogdescription' => array( 'value' => 'Snapshot blog' ), 'foo' => array( @@ -813,7 +458,7 @@ public function test_prepare_snapshot_post_content_for_publish() { 'baz' => array( 'value' => null ), ); $data_without_errors = $this->manager->prepare_snapshot_post_content_for_publish( array( - 'post_type' => Post_Type::SLUG, + 'post_type' => $this->manager->get_post_type(), 'post_content' => Customize_Snapshot_Manager::encode_json( $data ), 'post_status' => 'publish', ) ); @@ -826,78 +471,14 @@ public function test_prepare_snapshot_post_content_for_publish() { * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::filter_removable_query_args() */ public function test_filter_removable_query_args() { - $manager = new Customize_Snapshot_Manager( $this->plugin ); + $manager = $this->get_snapshot_manager_instance( $this->plugin ); $this->assertContains( 'snapshot_error_on_publish', $manager->filter_removable_query_args( array() ) ); } - /** - * Test save_settings_with_publish_snapshot. - * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::save_settings_with_publish_snapshot() - */ - public function test_save_settings_with_publish_snapshot() { - $post_type = $this->manager->post_type; - $data = array( - 'blogdescription' => array( 'value' => 'Snapshot blog' ), - 'unknown_setting_foo' => array( 'value' => 'bar' ), - 'null_value_baz' => array( 'value' => null ), - 'foo' => array( 'value' => 'foo' ), - ); - $validate_data = array( - 'blogdescription' => array( 'value' => 'Snapshot blog' ), - 'unknown_setting_foo' => array( - 'value' => 'bar', - 'publish_error' => 'unrecognized_setting', - ), - 'null_value_baz' => array( - 'value' => null, - 'publish_error' => 'null_value', - ), - 'foo' => array( - 'value' => 'foo', - ), - ); - - if ( method_exists( 'WP_Customize_Setting', 'validate' ) ) { - $validate_data['foo']['publish_error'] = 'you_shell_not_pass'; - add_filter( 'customize_validate_foo', function( $validity ) { - $validity->add( 'you_shell_not_pass', 'Testing invalid setting while publishing snapshot' ); - return $validity; - }, 10, 1 ); - } - - $post_id = $post_type->save( array( - 'uuid' => self::UUID, - 'data' => $data, - 'status' => 'draft', - ) ); - - // Test invalid settings. - $post = get_post( $post_id ); - $this->manager->save_settings_with_publish_snapshot( 'publish', 'draft', $post ); - $post = get_post( $post_id ); - $this->assertEquals( $validate_data, json_decode( wp_unslash( $post->post_content ), true ) ); - $this->assertEquals( 'pending', $post->post_status ); - - // Test valid settings. - unset( $data['unknown_setting_foo'], $data['null_value_baz'], $data['foo'] ); - $post_id = $post_type->save( array( - 'uuid' => self::UUID, - 'data' => $data, - 'status' => 'publish', - ) ); - $this->assertEquals( 'publish', get_post_status( $post_id ) ); - $this->assertEquals( 'Snapshot blog', get_bloginfo( 'description' ) ); - } - - /* - * For Customize_Snapshot_Manager::handle_update_snapshot_request(), see Test_Ajax_Customize_Snapshot_Manager. - */ - /** * Test prepare_errors_for_response. * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::prepare_errors_for_response() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::prepare_errors_for_response() */ public function test_prepare_errors_for_response() { $this->markTestIncomplete(); @@ -906,7 +487,7 @@ public function test_prepare_errors_for_response() { /** * Tests generate_uuid. * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::generate_uuid() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::generate_uuid() */ public function test_generate_uuid() { $this->markTestIncomplete(); @@ -915,42 +496,12 @@ public function test_generate_uuid() { /** * Tests is_valid_uuid. * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::is_valid_uuid() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::is_valid_uuid() */ public function test_is_valid_uuid() { $this->markTestIncomplete(); } - /** - * Test customize menu. - * - * @see Customize_Snapshot_Manager::customize_menu() - */ - public function test_customize_menu() { - set_current_screen( 'front' ); - $preview_url = home_url( '/' ); - - $_REQUEST['customize_snapshot_uuid'] = self::UUID; - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $manager->init(); - - require_once( ABSPATH . WPINC . '/class-wp-admin-bar.php' ); - $wp_admin_bar = new \WP_Admin_Bar(); // 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 ) ); - $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'] ); - } - /** * Test customize menu return. * @@ -971,10 +522,10 @@ public function test_customize_menu_return() { /** * Tests print_admin_bar_styles. * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::print_admin_bar_styles() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::print_admin_bar_styles() */ public function test_print_admin_bar_styles() { - $manager = new Customize_Snapshot_Manager( $this->plugin ); + $manager = $this->get_snapshot_manager_instance( $this->plugin ); $manager->init(); ob_start(); $manager->print_admin_bar_styles(); @@ -982,56 +533,13 @@ public function test_print_admin_bar_styles() { $this->assertContains( 'go_to( home_url( '?customize_snapshot_uuid=' . self::UUID ) ); - $_REQUEST['customize_snapshot_uuid'] = self::UUID; - - $manager = new Customize_Snapshot_Manager( $this->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(); - $wp_admin_bar->add_menus(); - 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(); - $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 ); - } - /** * Test misc admin bar extensions. * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::add_post_edit_screen_link() - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::add_snapshot_exit_link() - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::add_resume_snapshot_link() - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::remove_all_non_snapshot_admin_bar_links() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::add_post_edit_screen_link() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::add_snapshot_exit_link() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::add_resume_snapshot_link() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::remove_all_non_snapshot_admin_bar_links() */ public function test_add_post_edit_and_exit_links() { global $wp_admin_bar; @@ -1049,7 +557,7 @@ public function test_add_post_edit_and_exit_links() { ) ); remove_all_actions( 'admin_bar_menu' ); - $manager = new Customize_Snapshot_Manager( $this->plugin ); + $manager = $this->get_snapshot_manager_instance( $this->plugin ); $manager->init(); $wp_admin_bar = new \WP_Admin_Bar(); // WPCS: Override OK. $wp_admin_bar->initialize(); @@ -1060,10 +568,14 @@ public function test_add_post_edit_and_exit_links() { $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; + $this->go_to( home_url( '?' . $this->front_param . '=' . self::UUID ) ); remove_all_actions( 'admin_bar_menu' ); - $manager = new Customize_Snapshot_Manager( $this->plugin ); + $manager = $this->get_snapshot_manager_instance( $this->plugin ); + $_REQUEST[ $this->front_param ] = self::UUID; + if ( ! $this->plugin->compat ) { + global $wp_customize; + $wp_customize = null; // WPCS: Override OK. + } $manager->init(); $wp_admin_bar = new \WP_Admin_Bar(); // WPCS: Override OK. $wp_admin_bar->initialize(); @@ -1078,18 +590,17 @@ public function test_add_post_edit_and_exit_links() { /** * Test render templates. * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::render_templates() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::render_templates() */ public function test_render_templates() { ob_start(); $this->manager->render_templates(); $templates = ob_get_contents(); ob_end_clean(); - $this->assertContains( 'tmpl-snapshot-save', $templates ); $this->assertContains( 'tmpl-snapshot-dialog-error', $templates ); $this->assertContains( 'tmpl-snapshot-preview-link', $templates ); - $this->assertContains( 'tmpl-snapshot-schedule-button', $templates ); - $this->assertContains( 'tmpl-snapshot-schedule', $templates ); + $this->assertContains( 'tmpl-snapshot-expand-button', $templates ); + $this->assertContains( 'tmpl-snapshot-edit-container', $templates ); $this->assertContains( 'tmpl-snapshot-scheduled-countdown', $templates ); $this->assertContains( 'tmpl-snapshot-submit', $templates ); } @@ -1097,7 +608,7 @@ public function test_render_templates() { /** * Test format_gmt_offset * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::format_gmt_offset() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::format_gmt_offset() */ public function test_format_gmt_offset() { $offset = $this->manager->format_gmt_offset( 7.0 ); @@ -1107,7 +618,7 @@ public function test_format_gmt_offset() { /** * Test month choices * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::get_month_choices() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::get_month_choices() */ public function test_get_month_choices() { $data = $this->manager->get_month_choices(); @@ -1118,7 +629,7 @@ public function test_get_month_choices() { /** * Test override post date if empty. * - * @covers CustomizeSnapshots\Customize_Snapshot_Manager::override_post_date_default_data() + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::override_post_date_default_data() */ public function test_override_post_date_default_data() { $post_id = $this->factory()->post->create(); @@ -1130,4 +641,92 @@ public function test_override_post_date_default_data() { $this->assertNotEquals( $post->post_modified, '0000-00-00 00:00:00' ); $this->assertNotEquals( $post->post_modified_gmt, '0000-00-00 00:00:00' ); } + + /** + * Tests get_post_type. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::get_post_type() + */ + public function test_get_post_type() { + $plugin = get_plugin_instance(); + if ( $plugin->compat ) { + $this->assertEquals( $this->manager->get_post_type(), Post_Type_Back_Compat::SLUG ); + } else { + $this->assertEquals( $this->manager->get_post_type(), Post_Type::SLUG ); + } + } + + /** + * Tests get_front_uuid_param. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::get_front_uuid_param() + */ + public function test_get_front_uuid_param() { + $plugin = get_plugin_instance(); + if ( $plugin->compat ) { + $this->assertEquals( $this->manager->get_front_uuid_param(), Post_Type_Back_Compat::FRONT_UUID_PARAM_NAME ); + } else { + $this->assertEquals( $this->manager->get_front_uuid_param(), Post_Type::FRONT_UUID_PARAM_NAME ); + } + } + + /** + * Tests get_customize_uuid_param. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::get_customize_uuid_param() + */ + public function test_get_customize_uuid_param() { + $plugin = get_plugin_instance(); + if ( $plugin->compat ) { + $this->assertEquals( $this->manager->get_customize_uuid_param(), Post_Type_Back_Compat::CUSTOMIZE_UUID_PARAM_NAME ); + } else { + $this->assertEquals( $this->manager->get_customize_uuid_param(), Post_Type::CUSTOMIZE_UUID_PARAM_NAME ); + } + } + + /** + * Test replace_customize_link. + * + * @covers \CustomizeSnapshots\Customize_Snapshot_Manager::replace_customize_link() + */ + public function test_replace_customize_link() { + global $wp_admin_bar; + set_current_screen( 'front' ); + + $front_param = $this->manager->get_front_uuid_param(); + $customize_param = $this->manager->get_customize_uuid_param(); + + require_once ABSPATH . WPINC . '/class-wp-admin-bar.php'; + remove_all_actions( 'admin_bar_menu' ); + $this->go_to( home_url( '?' . $front_param . '=' . self::UUID ) ); + $_REQUEST[ $front_param ] = self::UUID; + + $manager = $this->get_snapshot_manager_instance( $this->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(); + $wp_admin_bar->add_menus(); + 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(); + $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_param, $query_params ); + $this->assertEquals( self::UUID, $query_params[ $customize_param ] ); + $this->assertArrayHasKey( 'url', $query_params ); + $parsed_preview_url = wp_parse_url( $query_params['url'] ); + $this->assertArrayNotHasKey( 'query', $parsed_preview_url ); + } } diff --git a/tests/php/test-class-customize-snapshot.php b/tests/php/test-class-customize-snapshot.php index 652d08dd..fe934942 100644 --- a/tests/php/test-class-customize-snapshot.php +++ b/tests/php/test-class-customize-snapshot.php @@ -63,48 +63,18 @@ class Test_Customize_Snapshot extends \WP_UnitTestCase { */ protected $bar; - /** - * Bootstrap the customizer. - */ - public static function setUpBeforeClass() { - $args = array( - 'labels' => array( - 'name' => __( 'Customize Snapshots', 'customize-snapshots' ), - 'singular_name' => __( 'Customize Snapshot', 'customize-snapshots' ), - ), - 'public' => false, - 'capability_type' => 'post', - 'map_meta_cap' => true, - 'hierarchical' => false, - 'rewrite' => false, - 'delete_with_user' => false, - 'supports' => array( 'title', 'author', 'revisions' ), - ); - register_post_type( self::POST_TYPE, $args ); - } - - /** - * Tear down after class. - */ - public static function tearDownAfterClass() { - _unregister_post_type( self::POST_TYPE ); - } - /** * Set up. */ function setUp() { parent::setUp(); $this->plugin = get_plugin_instance(); + $this->mark_incompatible(); require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' ); - $GLOBALS['wp_customize'] = new \WP_Customize_Manager(); // WPCS: override ok. + $GLOBALS['wp_customize'] = new \WP_Customize_Manager( array( 'changeset_uuid' => self::UUID ) ); // WPCS: override ok. $this->snapshot_manager = new Customize_Snapshot_Manager( $this->plugin ); + $this->snapshot_manager->post_type = new Post_Type( $this->snapshot_manager ); $this->wp_customize = $GLOBALS['wp_customize']; - wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); - $this->wp_customize->add_setting( 'foo', array( 'default' => 'foo_default' ) ); - $this->wp_customize->add_setting( 'bar', array( 'default' => 'bar_default' ) ); - $this->foo = $this->wp_customize->get_setting( 'foo' ); - $this->bar = $this->wp_customize->get_setting( 'bar' ); } /** @@ -114,247 +84,72 @@ function tearDown() { $this->wp_customize = null; unset( $GLOBALS['wp_customize'] ); unset( $GLOBALS['wp_scripts'] ); - $this->filtered_snapshot = null; parent::tearDown(); } - /** - * Test constructor. - * - * @see Customize_Snapshot::__construct() - */ - function test_construct() { - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $manager->init(); - $data = array( 'foo' => array( 'value' => 'bar' ) ); - $manager->post_type->save( array( - 'uuid' => self::UUID, - 'data' => $data, - ) ); - $snapshot = new Customize_Snapshot( $manager, self::UUID ); - $this->assertEquals( $data, $snapshot->data() ); - } - /** * Test UUID. * - * @see Customize_Snapshot::uuid() + * @covers \CustomizeSnapshots\Customize_Snapshot::uuid() */ function test_uuid() { - $_REQUEST['customize_snapshot_uuid'] = self::UUID; + $_REQUEST['customize_changeset_uuid'] = self::UUID; $manager = new Customize_Snapshot_Manager( $this->plugin ); $manager->init(); $this->assertEquals( self::UUID, $manager->snapshot()->uuid() ); } /** - * Test bad UUID. + * Test get_edit_link. * - * @see Customize_Snapshot::uuid() + * @covers \CustomizeSnapshots\Customize_Snapshot::get_edit_link() */ - function test_uuid_throws_exception() { - try { - new Customize_Snapshot( $this->snapshot_manager, '1234-invalid-UUID' ); - } catch ( \Exception $e ) { - $this->assertContains( 'You\'ve entered an invalid snapshot UUID.', $e->getMessage() ); - return; + function test_get_edit_link() { + wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); + $post_id = $this->snapshot_manager->post_type->save( array( + 'uuid' => self::UUID, + 'status' => 'draft', + 'data' => array(), + ) ); + $has_filter = has_filter( 'get_edit_post_link', '__return_empty_string' ); + if ( ! $has_filter ) { + add_filter( 'get_edit_post_link', '__return_empty_string' ); } - $this->fail( 'An expected exception has not been raised.' ); - } - - /** - * Test data. - * - * @see Customize_Snapshot::data() - */ - function test_data() { - $_REQUEST['customize_snapshot_uuid'] = self::UUID; - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $manager->init(); - - $manager->snapshot()->set( array( 'foo' => array( 'value' => 'foo_default' ) ) ); - $this->assertNotEmpty( $manager->snapshot()->data() ); - $manager->snapshot()->set( array( 'foo' => array( 'value' => 'foo_custom' ) ) ); - $expected = array( - 'foo' => array( - 'value' => 'foo_custom', - ), - ); - $this->assertEquals( $expected, $manager->snapshot()->data() ); - } - - /** - * Test snapshot settings. - * - * @see Customize_Snapshot::settings() - */ - function test_settings() { - $_REQUEST['customize_snapshot_uuid'] = self::UUID; - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $manager->init(); - - $this->assertEmpty( $manager->snapshot()->settings() ); - $manager->snapshot()->set( array( 'foo' => array( 'value' => 'foo_default' ) ) ); - $this->assertNotEmpty( $manager->snapshot()->settings() ); + $snapshot = new Customize_Snapshot( $this->snapshot_manager ); + $link = $snapshot->get_edit_link( $post_id ); + $this->assertContains( 'post=' . $post_id, $link ); } /** - * Test status. + * Test post * - * @see Customize_Snapshot::settings() + * @covers \CustomizeSnapshots\Customize_Snapshot::post() */ - function test_status() { - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $manager->init(); - - $snapshot = new Customize_Snapshot( $manager, self::UUID ); - $this->assertNull( $snapshot->status() ); - - $data = array( 'foo' => array( 'value' => 'bar' ) ); - $manager->post_type->save( array( + function test_post() { + $post_id = $this->snapshot_manager->post_type->save( array( 'uuid' => self::UUID, - 'data' => $data, 'status' => 'draft', + 'data' => array(), ) ); - - $this->assertEquals( 'draft', $snapshot->status() ); - $manager->post_type->save( array( - 'uuid' => self::UUID, - 'status' => 'publish', + $this->snapshot_manager->customize_manager = new \WP_Customize_Manager( array( + 'changeset_uuid' => self::UUID, ) ); - $this->assertEquals( 'publish', $snapshot->status() ); - } - - /** - * Test set. - * - * @see Customize_Snapshot::set() - */ - function test_set() { - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $manager->init(); - - $this->bar->capability = 'do_not_allow'; - add_filter( 'customize_sanitize_foo', 'strtoupper' ); - - $snapshot = new Customize_Snapshot( $manager, self::UUID ); - $result = $snapshot->set( array( - 'foo' => array( 'value' => 'ok' ), - 'bar' => array( 'value' => 'unauthorized' ), - 'baz' => array( 'value' => 'unrecognized' ), + $snapshot = new Customize_Snapshot( $this->snapshot_manager ); + $snapshot_post = $snapshot->post(); + $this->assertEquals( $post_id, $snapshot_post->ID ); + $this->snapshot_manager->customize_manager = new \WP_Customize_Manager( array( + 'changeset_uuid' => wp_generate_uuid4(), ) ); - - $this->assertArrayHasKey( 'errors', $result ); - $this->assertInstanceOf( 'WP_Error', $result['errors'] ); - $wp_error = $result['errors']; - $this->assertArrayHasKey( 'unauthorized_settings', $wp_error->errors ); - $this->assertArrayHasKey( 'unrecognized_settings', $wp_error->errors ); - - $this->assertArrayHasKey( 'sanitized', $result ); - $this->assertArrayHasKey( 'foo', $result['sanitized'] ); - $this->assertArrayNotHasKey( 'bar', $result['sanitized'] ); - $this->assertArrayNotHasKey( 'baz', $result['sanitized'] ); - $this->assertEquals( 'OK', $result['sanitized']['foo'] ); - - $this->assertArrayHasKey( 'validities', $result ); - $this->assertArrayHasKey( 'foo', $result['validities'] ); - $this->assertTrue( $result['validities']['foo'] ); - - $this->assertEmpty( $snapshot->data() ); - - // Success with populated value. - $result = $snapshot->set( array( 'foo' => array( 'value' => 'ok' ) ) ); - $this->assertNull( $result['errors'] ); - $resultant_data = $snapshot->data(); - $this->assertEquals( 'ok', $resultant_data['foo']['value'] ); + $snapshot_post = $snapshot->post(); + $this->assertNull( $snapshot_post ); } /** - * Test set with varying setting params. - * - * @see Customize_Snapshot::set() - */ - function test_set_with_varying_setting_params() { - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $manager->init(); - $snapshot = new Customize_Snapshot( $manager, self::UUID ); - - $result = $snapshot->set( array( 'foo' => array( 'value' => 'ok' ) ) ); - $this->assertNull( $result['errors'] ); - $resultant_data = $snapshot->data(); - $this->assertEquals( 'ok', $resultant_data['foo']['value'] ); - - // Check setting a param without a value, ensuring that foo still remains but snapshot is amended. - $result = $snapshot->set( array( 'bar' => array( 'extra' => 'ok' ) ) ); - $this->assertNull( $result['errors'] ); - $resultant_data = $snapshot->data(); - $this->assertEquals( 'ok', $resultant_data['foo']['value'] ); - $this->assertArrayHasKey( 'extra', $resultant_data['bar'] ); - $this->assertNull( $resultant_data['bar']['value'] ); - } - - /** - * Test set with a non-array param. - * - * @see Customize_Snapshot::set() - * @expectedException Exception - */ - function test_set_with_non_array_params() { - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $manager->ensure_customize_manager(); - $manager->init(); - $snapshot = new Customize_Snapshot( $manager, self::UUID ); - $snapshot->set( array( 'foo' => 'bad' ) ); - } - - /** - * Test saved. - * - * @see Customize_Snapshot::saved() + * Mark test incomplete as it is only for new versions. */ - function test_saved() { - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $manager->init(); - - $snapshot = new Customize_Snapshot( $manager, self::UUID ); - $this->assertFalse( $snapshot->saved() ); - - $manager->post_type->save( array( - 'uuid' => self::UUID, - 'data' => array( 'foo' => array( 'value' => 'bar' ) ), - ) ); - } - - /** - * Snapshot object passed in customize_snapshot_save filter. - * - * @var Customize_Snapshot - */ - public $filtered_snapshot; - - /** - * Test that the snapshot object is passed as the second filter param. - * - * @see Customize_Snapshot::save() - */ - function test_filter_customize_snapshot_save() { - $manager = new Customize_Snapshot_Manager( $this->plugin ); - $manager->ensure_customize_manager(); - $manager->init(); - - $snapshot = new Customize_Snapshot( $manager, self::UUID ); - - $that = $this; // For PHP 5.3. - add_filter( 'customize_snapshot_save', function( $data, $test_snapshot ) use ( $that ) { - $that->filtered_snapshot = $test_snapshot; - return $data; - }, 10, 2 ); - - $snapshot->save( array( - 'uuid' => self::UUID, - 'data' => array( 'foo' => array( 'value' => 'bar' ) ), - ) ); - - $this->assertEquals( $snapshot, $this->filtered_snapshot ); + public function mark_incompatible() { + if ( $this->plugin->compat ) { + $this->markTestSkipped( 'This unit-test require WP version 4.7 or up.' ); + } } } diff --git a/tests/php/test-class-migrate.php b/tests/php/test-class-migrate.php new file mode 100644 index 00000000..8e5d8244 --- /dev/null +++ b/tests/php/test-class-migrate.php @@ -0,0 +1,230 @@ +plugin = get_plugin_instance(); + $this->mark_incompatible(); + $this->snapshot_manager = new Customize_Snapshot_Manager( $this->plugin ); + $this->snapshot_manager->post_type = new Post_Type( $this->snapshot_manager ); + } + + /** + * Mark test incomplete as it is only for new versions. + */ + public function mark_incompatible() { + if ( $this->plugin->compat ) { + $this->markTestSkipped( 'This unit-test require WP version 4.7 or up.' ); + } + } + + /** + * Tear down. + */ + function tearDown() { + $this->wp_customize = null; + unset( $GLOBALS['wp_customize'] ); + unset( $GLOBALS['wp_scripts'] ); + update_option( Migrate::KEY, 0 ); + parent::tearDown(); + } + + /** + * Test Migrate constructor. + * + * @see Migrate::__construct() + */ + function test_construct() { + $class_name = 'CustomizeSnapshots\Migrate'; + $mock = $this->getMockBuilder( $class_name ) + ->disableOriginalConstructor() + ->getMock(); + $mock->expects( $this->once() ) + ->method( 'maybe_migrate' ); + $reflected_class = new \ReflectionClass( $class_name ); + $constructor = $reflected_class->getConstructor(); + $constructor->invoke( $mock, $this->plugin ); + set_current_screen( 'index' ); + $constructor->invoke( $mock, $this->plugin ); + $user_id = $this->factory()->user->create( array( 'role' => 'administrator' ) ); + if ( is_multisite() ) { + grant_super_admin( $user_id ); + } + wp_set_current_user( $user_id ); + $constructor->invoke( $mock, $this->plugin ); + } + + /** + * Test is_migrated. + * + * @see Migrate::is_migrated() + */ + function test_is_migrated() { + $migrate = new Migrate( $this->plugin ); + update_option( Migrate::KEY, 0 ); + $this->assertFalse( $migrate->is_migrated() ); + update_option( Migrate::KEY, 1 ); + $this->assertTrue( $migrate->is_migrated() ); + } + + /** + * Test maybe_migrate. + * + * @see Migrate::maybe_migrate() + */ + function test_maybe_migrate() { + $migrate = new Migrate( $this->plugin ); + $migrate->maybe_migrate(); + $this->assertEquals( 10, has_action( 'admin_notices', array( $migrate, 'show_migration_notice' ) ) ); + $this->assertEquals( 10, has_action( 'admin_enqueue_scripts', array( $migrate, 'enqueue_script' ) ) ); + $this->assertEquals( 10, has_action( 'wp_ajax_customize_snapshot_migration', array( $migrate, 'handle_migrate_changeset_request' ) ) ); + } + + /** + * Test show_migration_notice. + * + * @see Migrate::show_migration_notice() + */ + function test_show_migration_notice() { + $migrate = new Migrate( $this->plugin ); + ob_start(); + $migrate->show_migration_notice(); + $data = ob_get_clean(); + $this->assertContains( 'customize-snapshot-migration', $data ); + $this->assertContains( 'customize-snapshot-migration', $data ); + $this->assertContains( 'data-nonce', $data ); + $this->assertContains( 'data-migration-success', $data ); + $this->assertContains( 'customize-snapshot-spinner', $data ); + } + + /** + * Test changeset_migrate. + * + * @see Migrate::changeset_migrate() + */ + function test_changeset_migrate() { + $old_post_type_obj = new Post_Type_Back_Compat( $this->snapshot_manager ); + $post_id = $old_post_type_obj->save( array( + 'uuid' => wp_generate_uuid4(), + 'status' => 'draft', + 'data' => array(), + ) ); + $migrate = new Migrate( $this->plugin ); + $posts_count = $migrate->changeset_migrate( -1, true ); + $this->assertEquals( $post_id, array_shift( $posts_count ) ); + + $migrate_obj = $this->getMockBuilder( 'CustomizeSnapshots\Migrate' ) + ->setMethods( array( 'migrate_post' ) ) + ->setConstructorArgs( array( $this->plugin ) ) + ->getMock(); + $migrate_obj->expects( $this->once() ) + ->method( 'migrate_post' ) + ->will( $this->returnValue( null ) ); + $migrate_obj->changeset_migrate( -1 ); + } + + /** + * Test migrate_post. + * + * @see Migrate::migrate_post() + */ + function test_migrate_post() { + $admin_user_id = $this->factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_user_id ); + $old_post_type_obj = new Post_Type_Back_Compat( $this->snapshot_manager ); + $snapshot_post_id = $old_post_type_obj->save( array( + 'uuid' => wp_generate_uuid4(), + 'status' => 'draft', + 'data' => array( + 'foo' => array( + 'value' => 'bar', + ), + ), + ) ); + add_post_meta( $snapshot_post_id, '_snapshot_theme', 'foo_theme' ); + require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' ); + $wp_customize = new \WP_Customize_Manager( array( 'changeset_uuid' => self::UUID ) ); + + $wp_customize->add_setting( 'foo', array( 'default' => 'foo_default' ) ); + $this->action_customize_register_for_dynamic_settings(); + + $migrate = new Migrate( $this->plugin ); + + $has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) ); + if ( $has_kses ) { + kses_remove_filters(); // Prevent KSES from corrupting JSON in post_content. + } + $migrate->migrate_post( $snapshot_post_id ); + if ( $has_kses ) { + kses_init_filters(); + } + + $changeset_post = get_post( $snapshot_post_id ); + $this->assertEquals( Post_Type::SLUG, $changeset_post->post_type ); + $data = json_decode( $changeset_post->post_content, true ); + $expected = array( + 'foo_theme::foo' => array( + 'value' => 'bar', + 'user_id' => (string) $admin_user_id, + 'type' => 'theme_mod', + ), + ); + $this->assertSame( $expected, $data ); + } + + /** + * Add filter for dynamic setting. + */ + function action_customize_register_for_dynamic_settings() { + add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_customize_dynamic_setting_args_for_test_dynamic_settings' ), 10, 2 ); + } + + /** + * To support dynamic setting. + * + * @param array $setting_args Setting args. + * @param string $setting_id Setting ID. + * @return array + */ + function filter_customize_dynamic_setting_args_for_test_dynamic_settings( $setting_args, $setting_id ) { + if ( in_array( $setting_id, array( 'foo' ), true ) ) { + $setting_args = array( 'default' => "dynamic_{$setting_id}_default" ); + } + return $setting_args; + } +} diff --git a/tests/php/test-class-plugin-base.php b/tests/php/test-class-plugin-base.php index 7c38c7a3..00c32156 100644 --- a/tests/php/test-class-plugin-base.php +++ b/tests/php/test-class-plugin-base.php @@ -74,7 +74,7 @@ public function test_trigger_warning() { * @see Plugin_Base::add_doc_hooks() */ public function test_add_doc_hooks() { - $object = new Test_Doc_hooks(); + $object = new Test_Doc_Hooks(); $this->assertFalse( has_action( 'init', array( $object, 'init_action' ) ) ); $this->assertFalse( has_action( 'the_content', array( $object, 'the_content_filter' ) ) ); $this->plugin->add_doc_hooks( $object ); @@ -115,7 +115,7 @@ public function test_add_doc_hooks_error() { * @see Plugin_Base::remove_doc_hooks() */ public function test_remove_doc_hooks() { - $object = new Test_Doc_hooks(); + $object = new Test_Doc_Hooks(); $this->plugin->add_doc_hooks( $object ); $this->assertEquals( 10, has_action( 'init', array( $object, 'init_action' ) ) ); $this->assertEquals( 10, has_action( 'the_content', array( $object, 'the_content_filter' ) ) ); @@ -126,9 +126,9 @@ public function test_remove_doc_hooks() { } /** - * Class Test_Doc_hooks + * Class Test_Doc_Hooks */ -class Test_Doc_hooks { +class Test_Doc_Hooks { /** * Load this on the init action hook. diff --git a/tests/php/test-class-post-type-back-compat.php b/tests/php/test-class-post-type-back-compat.php new file mode 100644 index 00000000..bd02b336 --- /dev/null +++ b/tests/php/test-class-post-type-back-compat.php @@ -0,0 +1,285 @@ +plugin = get_plugin_instance(); + if ( ! $this->plugin->compat ) { + $this->markTestSkipped( 'WordPress Version 4.6.x or below is required for this test-case.' ); + } + $GLOBALS['wp_customize'] = null; // WPCS: Global override ok. + unregister_post_type( Post_Type_Back_Compat::SLUG ); + } + + /** + * Test register post type. + * + * @see Post_Type::init() + */ + public function test_init() { + $this->assertFalse( post_type_exists( Post_Type_Back_Compat::SLUG ) ); + $post_type_obj = new Post_Type_Back_Compat( $this->plugin->customize_snapshot_manager ); + $this->plugin->customize_snapshot_manager->init(); + $post_type_obj->init(); + $this->assertTrue( post_type_exists( Post_Type_Back_Compat::SLUG ) ); + + $this->assertEquals( 10, has_action( 'admin_notices', array( $post_type_obj, 'show_publish_error_admin_notice' ) ) ); + $this->assertEquals( 10, has_filter( 'display_post_states', array( $post_type_obj, 'display_post_states' ) ) ); + $this->assertEquals( 10, has_action( 'admin_footer-edit.php', array( $post_type_obj, 'snapshot_merge_print_script' ) ) ); + $this->assertEquals( 10, has_action( 'load-edit.php', array( $post_type_obj, 'handle_snapshot_merge_workaround' ) ) ); + $this->assertEquals( 10, has_filter( 'post_type_link', array( $post_type_obj, 'filter_post_type_link' ) ) ); + $this->assertEquals( 10, has_filter( 'wp_insert_post_data', array( $post_type_obj, 'preserve_post_name_in_insert_data' ) ) ); + } + + /** + * Tests show_publish_error_admin_notice. + * + * @covers \CustomizeSnapshots\Post_Type_Back_Compat::show_publish_error_admin_notice() + */ + public function test_show_publish_error_admin_notice() { + global $current_screen, $post; + wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); + $post_type_obj = new Post_Type_Back_Compat( $this->plugin->customize_snapshot_manager ); + $post_type_obj->init(); + $post_id = $post_type_obj->save( array( + 'uuid' => self::UUID, + 'data' => array(), + ) ); + + ob_start(); + $post_type_obj->show_publish_error_admin_notice(); + $this->assertEmpty( ob_get_clean() ); + + $current_screen = \WP_Screen::get( 'customize_snapshot' ); // WPCS: Override ok. + $current_screen->id = 'customize_snapshot'; + $current_screen->base = 'edit'; + ob_start(); + $post_type_obj->show_publish_error_admin_notice(); + $this->assertEmpty( ob_get_clean() ); + + $current_screen->base = 'post'; + ob_start(); + $post_type_obj->show_publish_error_admin_notice(); + $this->assertEmpty( ob_get_clean() ); + + $_REQUEST['snapshot_error_on_publish'] = '1'; + wp_update_post( array( 'ID' => $post_id, 'post_status' => 'pending' ) ); + $post = get_post( $post_id ); // WPCS: override ok. + ob_start(); + $post_type_obj->show_publish_error_admin_notice(); + $this->assertContains( 'notice-error', ob_get_clean() ); + } + + /** + * Tests display_post_states. + * + * @covers \CustomizeSnapshots\Post_Type_Back_Compat::display_post_states() + */ + public function test_display_post_states() { + $post_type_obj = new Post_Type_Back_Compat( $this->plugin->customize_snapshot_manager ); + + $post_id = $post_type_obj->save( array( + 'uuid' => self::UUID, + 'data' => array( 'foo' => array( 'value' => 'bar' ) ), + ) ); + $states = $post_type_obj->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_obj->display_post_states( array(), get_post( $post_id ) ); + $this->assertArrayHasKey( 'snapshot_error', $states ); + } + + /** + * Test snapshot_merge_print_script + * + * @see Post_Type_Back_Compat::snapshot_merge_print_script() + */ + public function test_snapshot_merge_print_script() { + global $post_type; + $post_type = Post_Type_Back_Compat::SLUG; // WPCS: global override ok. + $post_type_obj = new Post_Type_Back_Compat( $this->plugin->customize_snapshot_manager ); + ob_start(); + $post_type_obj->snapshot_merge_print_script(); + $script_content = ob_get_clean(); + + $this->assertContains( 'select[name="action"]', $script_content ); + $this->assertContains( 'select[name="action2"]', $script_content ); + $this->assertContains( 'merge_snapshot', $script_content ); + $this->assertContains( 'text/javascript', $script_content ); + } + + /** + * Test handle_snapshot_bulk_actions_workaround + * + * @see Post_Type_Back_Compat::handle_snapshot_merge_workaround() + */ + public function test_handle_snapshot_bulk_actions_workaround() { + $GLOBALS['hook_suffix'] = 'posts-' . Post_Type_Back_Compat::SLUG; // WPCS: global override ok. + $_POST['action'] = $_REQUEST['action'] = $_GET['action'] = 'merge_snapshot'; + $_POST['post_type'] = $_REQUEST['post_type'] = $_GET['post_type'] = Post_Type_Back_Compat::SLUG; + $_POST['post'] = $_REQUEST['post'] = $_GET['post'] = array( 1, 2 ); + $_POST['_wpnonce'] = $_REQUEST['_wpnonce'] = $_GET['_wpnonce'] = wp_create_nonce( 'bulk-posts' ); + $_POST['_wp_http_referer'] = $_REQUEST['_wp_http_referer'] = $_GET['_wp_http_referer'] = admin_url(); + $post_type_obj = $this->getMockBuilder( 'CustomizeSnapshots\Post_Type_Back_Compat' ) + ->setConstructorArgs( array( $this->plugin->customize_snapshot_manager ) ) + ->setMethods( array( 'handle_snapshot_merge' ) ) + ->getMock(); + $post_type_obj->expects( $this->once() ) + ->method( 'handle_snapshot_merge' ) + ->will( $this->returnValue( null ) ); + $post_type_obj->handle_snapshot_merge_workaround(); + } + + /** + * Tests preservation of the post_name when submitting a snapshot for review. + * + * @see Post_Type_Back_Compat::preserve_post_name_in_insert_data() + */ + public function test_preserve_post_name_in_insert_data() { + $post_type_obj = new Post_Type_Back_Compat( $this->plugin->customize_snapshot_manager ); + $post_type_obj->init(); + + $post_data = array( + 'post_name' => '', + 'post_type' => 'no', + 'post_status' => 'pending', + ); + $original_post_data = array( + 'post_type' => 'no', + 'post_name' => '!original!', + 'post_status' => 'pending', + ); + $filtered_post_data = $post_type_obj->preserve_post_name_in_insert_data( $post_data, $original_post_data ); + $this->assertEquals( $post_data, $filtered_post_data ); + + $post_data['post_type'] = Post_Type_Back_Compat::SLUG; + $original_post_data['post_type'] = Post_Type_Back_Compat::SLUG; + + $filtered_post_data = $post_type_obj->preserve_post_name_in_insert_data( $post_data, $original_post_data ); + $this->assertEquals( $original_post_data['post_name'], $filtered_post_data['post_name'] ); + } + + /** + * Snapshot publish. + * + * @see Post_Type::save() + */ + function test_publish_snapshot() { + $admin_user_id = $this->factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_user_id ); + $post_type = get_plugin_instance()->customize_snapshot_manager->post_type; + $post_type->init(); + $tag_line = 'Snapshot blog'; + + $data = array( + 'blogdescription' => array( + 'value' => $tag_line, + ), + 'foo' => array( + 'value' => 'bar', + ), + 'baz' => array( + 'value' => null, + ), + ); + + $validated_content = array( + 'blogdescription' => array( + 'value' => $tag_line, + ), + 'foo' => array( + 'value' => 'bar', + 'publish_error' => 'unrecognized_setting', + ), + 'baz' => array( + 'value' => null, + 'publish_error' => 'null_value', + ), + ); + + /* + * Ensure that directly updating a post succeeds with invalid settings + * works because the post is a draft. Note that if using + * Customize_Snapshot::set() this would fail because it does validation. + */ + $post_id = $post_type->save( array( + 'uuid' => Customize_Snapshot_Manager::generate_uuid(), + 'data' => $data, + 'status' => 'draft', + ) ); + wp_update_post( array( 'ID' => $post_id, 'post_status' => 'draft' ) ); + $content = $post_type->get_post_content( get_post( $post_id ) ); + $this->assertEquals( $data, $content ); + + /* + * Ensure that attempting to publish a snapshot with invalid settings + * will get the publish_errors added as well as kick it back to pending. + */ + remove_all_filters( 'redirect_post_location' ); + $post_id = $post_type->save( array( + 'uuid' => Customize_Snapshot_Manager::generate_uuid(), + 'data' => $data, + 'status' => 'draft', + ) ); + wp_publish_post( $post_id ); + $snapshot_post = get_post( $post_id ); + $content = $post_type->get_post_content( $snapshot_post ); + $this->assertEquals( 'pending', $snapshot_post->post_status ); + $this->assertEquals( $validated_content, $content ); + $this->assertContains( + 'snapshot_error_on_publish=1', + apply_filters( 'redirect_post_location', get_edit_post_link( $snapshot_post->ID ), $snapshot_post->ID ) + ); + + /* + * Remove invalid settings and now attempt publish. + */ + remove_all_filters( 'redirect_post_location' ); + unset( $data['foo'] ); + unset( $data['baz'] ); + $post_id = $post_type->save( array( + 'uuid' => Customize_Snapshot_Manager::generate_uuid(), + 'data' => $data, + 'status' => 'draft', + ) ); + wp_publish_post( $post_id ); + $snapshot_post = get_post( $post_id ); + $content = $post_type->get_post_content( $snapshot_post ); + $this->assertEquals( 'publish', $snapshot_post->post_status ); + $this->assertEquals( $data, $content ); + $this->assertEquals( $tag_line, get_bloginfo( 'description' ) ); + $this->assertNotContains( + 'snapshot_error_on_publish=1', + apply_filters( 'redirect_post_location', get_edit_post_link( $snapshot_post->ID ), $snapshot_post->ID ) + ); + } +} diff --git a/tests/php/test-class-post-type.php b/tests/php/test-class-post-type.php index a1e50f4e..b49a05ca 100644 --- a/tests/php/test-class-post-type.php +++ b/tests/php/test-class-post-type.php @@ -10,7 +10,7 @@ /** * Class Test_Post_type */ -class Test_Post_type extends \WP_UnitTestCase { +class Test_Post_Type extends \WP_UnitTestCase { /** * Plugin. @@ -26,6 +26,13 @@ class Test_Post_type extends \WP_UnitTestCase { */ const UUID = '65aee1ff-af47-47df-9e14-9c69b3017cd3'; + /** + * Post type slug. + * + * @var string + */ + public $post_type_slug; + /** * Set up. */ @@ -33,40 +40,131 @@ function setUp() { parent::setUp(); $GLOBALS['wp_customize'] = null; // WPCS: Global override ok. $this->plugin = get_plugin_instance(); - unregister_post_type( Post_Type::SLUG ); + if ( $this->plugin->compat ) { + $this->post_type_slug = Post_Type_Back_Compat::SLUG; + } else { + $this->post_type_slug = Post_Type::SLUG; + } + } + + /** + * Get plugin instance accoding to WP version. + * + * @param Customize_Snapshot_Manager|Customize_Snapshot_Manager_Back_Compat $manager Manager. + * + * @return Post_Type|Post_Type_Back_Compat Post type object. + */ + public function get_new_post_type_instance( $manager ) { + if ( $this->plugin->compat ) { + return new Post_Type_Back_Compat( $manager ); + } else { + return new Post_Type( $manager ); + } + } + + /** + * Mark test incomplete as it is only for new versions. + */ + public function mark_incompatible() { + if ( $this->plugin->compat ) { + $this->markTestSkipped( 'This unit-test require WP version 4.7 or up.' ); + } } /** * Test register post type. * - * @see Post_Type::register() + * @see Post_Type::init() */ - public function test_register() { - $this->assertFalse( post_type_exists( Post_Type::SLUG ) ); - $post_type = new Post_Type( $this->plugin->customize_snapshot_manager ); + public function test_init() { + $this->mark_incompatible(); + $post_type_obj = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); $this->plugin->customize_snapshot_manager->init(); - $post_type->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_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' ) ) ); - $this->assertEquals( 10, has_filter( 'wp_insert_post_data', array( $post_type, 'preserve_post_name_in_insert_data' ) ) ); - $this->assertEquals( 10, has_filter( 'user_has_cap', array( $post_type, 'filter_user_has_cap' ) ) ); - $this->assertEquals( 10, has_action( 'transition_post_status', array( $post_type->snapshot_manager, 'save_settings_with_publish_snapshot' ) ) ); - $this->assertEquals( 10, has_filter( 'wp_insert_post_data', array( $post_type->snapshot_manager, 'prepare_snapshot_post_content_for_publish' ) ) ); - $this->assertEquals( 10, has_action( 'display_post_states', array( $post_type, 'display_post_states' ) ) ); + $post_type_obj->init(); + + $this->assertEquals( 10, has_filter( 'post_link', array( $post_type_obj, 'filter_post_type_link' ) ) ); + $this->assertEquals( 10, has_action( 'add_meta_boxes_' . Post_Type::SLUG, array( $post_type_obj, 'setup_metaboxes' ) ) ); + $this->assertEquals( 10, has_action( 'admin_menu', array( $post_type_obj, 'add_admin_menu_item' ) ) ); + $this->assertEquals( 5, has_filter( 'map_meta_cap', array( $post_type_obj, 'remap_customize_meta_cap' ) ) ); + $this->assertEquals( 10, has_filter( 'bulk_actions-edit-' . Post_Type::SLUG, array( $post_type_obj, 'add_snapshot_bulk_actions' ) ) ); + $this->assertEquals( 10, has_filter( 'handle_bulk_actions-edit-' . Post_Type::SLUG, array( $post_type_obj, 'handle_snapshot_merge' ) ) ); + $this->assertEquals( 10, has_action( 'admin_print_styles-edit.php', array( $post_type_obj, 'hide_add_new_changeset_button' ) ) ); + } + + /** + * Test common hooks + * + * @see Post_Type::hooks() + */ + public function test_hooks() { + $post_type_obj = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); + $post_type_obj->hooks(); + + $this->assertEquals( 100, has_action( 'add_meta_boxes_' . $this->post_type_slug, array( $post_type_obj, 'remove_slug_metabox' ) ) ); + $this->assertEquals( 10, has_action( 'load-revision.php', array( $post_type_obj, 'suspend_kses_for_snapshot_revision_restore' ) ) ); + $this->assertEquals( 10, has_filter( 'get_the_excerpt', array( $post_type_obj, 'filter_snapshot_excerpt' ) ) ); + $this->assertEquals( 10, has_filter( 'post_row_actions', array( $post_type_obj, 'filter_post_row_actions' ) ) ); + $this->assertEquals( 10, has_filter( 'user_has_cap', array( $post_type_obj, 'filter_user_has_cap' ) ) ); + $this->assertEquals( 10, has_action( 'post_submitbox_minor_actions', array( $post_type_obj, 'hide_disabled_publishing_actions' ) ) ); + $this->assertEquals( 10, has_filter( 'content_save_pre', array( $post_type_obj, 'filter_out_settings_if_removed_in_metabox' ) ) ); + $this->assertEquals( 10, has_action( 'admin_print_scripts-revision.php', array( $post_type_obj, 'disable_revision_ui_for_published_posts' ) ) ); + $this->assertEquals( 10, has_action( 'admin_notices', array( $post_type_obj, 'admin_show_merge_error' ) ) ); + } + + /** + * Test extend_changeset_post_type_object + * + * @covers \CustomizeSnapshots\Post_Type::extend_changeset_post_type_object() + */ + public function test_extend_changeset_post_type_object() { + global $_wp_post_type_features; + $this->mark_incompatible(); + $post_type_obj = get_post_type_object( Post_Type::SLUG ); + $this->assertArrayHasKey( 'revisions', $_wp_post_type_features[ Post_Type::SLUG ] ); + $this->assertTrue( $post_type_obj->show_ui ); + $this->assertTrue( $post_type_obj->show_in_menu ); + $this->assertEquals( 'post.php?post=%d', $post_type_obj->_edit_link ); + $this->assertEquals( 'customize_publish', $post_type_obj->cap->publish_posts ); + $caps = (array) $post_type_obj->cap; + foreach ( $caps as $key => $value ) { + if ( in_array( $key, array( 'read', 'publish_posts' ), true ) ) { + continue; + } else { + $this->assertTrue( 0 < strpos( $value, Post_Type::SLUG ) ); + } + } + $this->assertFalse( $post_type_obj->show_in_customizer ); + $this->assertInstanceOf( __NAMESPACE__ . '\\Post_Type', $post_type_obj->customize_snapshot_post_type_obj ); + $this->assertTrue( $post_type_obj->show_in_rest ); + $this->assertEquals( 'customize_changesets', $post_type_obj->rest_base ); + $this->assertEquals( __NAMESPACE__ . '\\Snapshot_REST_API_Controller', $post_type_obj->rest_controller_class ); + } + + /** + * Test add_admin_menu_item. + * + * @covers \CustomizeSnapshots\Post_Type::add_admin_menu_item() + */ + public function test_add_admin_menu_item() { + $this->mark_incompatible(); + global $submenu; + $admin_user_id = $this->factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_user_id ); + $post_type_obj = new Post_Type( $this->plugin->customize_snapshot_manager ); + $post_type_obj->add_admin_menu_item(); + $menu_slug = 'edit.php?post_type=' . Post_Type::SLUG; + $this->assertArrayHasKey( 'themes.php', $submenu ); + $this->assertArrayHasKey( 0, $submenu['themes.php'] ); + $this->assertTrue( in_array( $menu_slug, $submenu['themes.php'][0], true ) ); } /** * Test filter_post_type_link. * - * @covers CustomizeSnapshots\Post_Type::filter_post_type_link() + * @covers \CustomizeSnapshots\Post_Type::filter_post_type_link() */ function test_filter_post_type_link() { - $post_type = new Post_Type( $this->plugin->customize_snapshot_manager ); + $post_type = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); $post_id = $post_type->save( array( 'uuid' => self::UUID, @@ -74,15 +172,15 @@ function test_filter_post_type_link() { 'blogname' => array( 'value' => 'Hello' ), ), ) ); - + $param = $this->plugin->customize_snapshot_manager->get_front_uuid_param(); $this->assertContains( - 'customize_snapshot_uuid=' . self::UUID, + $param . '=' . self::UUID, $post_type->filter_post_type_link( '', get_post( $post_id ) ) ); remove_all_filters( 'post_type_link' ); - $post_type->register(); - $this->assertContains( 'customize_snapshot_uuid=' . self::UUID, get_permalink( $post_id ) ); + $post_type->init(); + $this->assertContains( $param . '=' . self::UUID, get_permalink( $post_id ) ); } /** @@ -96,7 +194,7 @@ function test_suspend_restore_kses() { kses_init_filters(); } - $post_type = new Post_Type( $this->plugin->customize_snapshot_manager ); + $post_type = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); $post_type->suspend_kses(); $this->assertFalse( has_filter( 'content_save_pre', 'wp_filter_post_kses' ) ); $post_type->restore_kses(); @@ -115,18 +213,18 @@ function test_suspend_restore_kses() { * @see Post_Type::remove_metaboxes() */ public function test_setup_metaboxes() { - set_current_screen( Post_Type::SLUG ); + set_current_screen( $this->post_type_slug ); global $wp_meta_boxes; - $post_type = new Post_Type( $this->plugin->customize_snapshot_manager ); - $post_type->register(); + $post_type = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); + $post_type->init(); - $post_id = $this->factory()->post->create( array( 'post_type' => Post_Type::SLUG, 'post_status' => 'draft' ) ); + $post_id = $this->factory()->post->create( array( 'post_type' => $this->post_type_slug, 'post_status' => 'draft' ) ); $wp_meta_boxes = array(); // WPCS: global override ok. - $metabox_id = Post_Type::SLUG; - $this->assertFalse( ! empty( $wp_meta_boxes[ Post_Type::SLUG ]['normal']['high'][ $metabox_id ] ) ); - do_action( 'add_meta_boxes_' . Post_Type::SLUG, $post_id ); - $this->assertTrue( ! empty( $wp_meta_boxes[ Post_Type::SLUG ]['normal']['high'][ $metabox_id ] ) ); + $metabox_id = $this->post_type_slug; + $this->assertFalse( ! empty( $wp_meta_boxes[ $this->post_type_slug ]['normal']['high'][ $metabox_id ] ) ); + do_action( 'add_meta_boxes_' . $this->post_type_slug, $post_id ); + $this->assertTrue( ! empty( $wp_meta_boxes[ $this->post_type_slug ]['normal']['high'][ $metabox_id ] ) ); } /* Note: Code coverage ignored on Post_Type::remove_publish_metabox(). */ @@ -141,8 +239,8 @@ public function test_setup_metaboxes() { public function test_filter_snapshot_excerpt() { global $post; - $post_type = new Post_Type( $this->plugin->customize_snapshot_manager ); - $post_type->register(); + $post_type = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); + $post_type->init(); $post_id = $post_type->save( array( 'uuid' => self::UUID, 'data' => array( @@ -167,8 +265,8 @@ public function test_filter_post_row_actions() { $admin_user_id = $this->factory()->user->create( array( 'role' => 'administrator' ) ); $subscriber_user_id = $this->factory()->user->create( array( 'role' => 'subscriber' ) ); - $post_type = new Post_Type( $this->plugin->customize_snapshot_manager ); - $post_type->register(); + $post_type = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); + $post_type->init(); $data = array( 'blogdescription' => array( 'value' => 'Another Snapshot Test' ), ); @@ -188,7 +286,7 @@ public function test_filter_post_row_actions() { wp_set_current_user( $admin_user_id ); $filtered_actions = apply_filters( 'post_row_actions', $original_actions, get_post( $post_id ) ); - $this->assertArrayHasKey( 'inline hide-if-no-js', $filtered_actions ); + $this->assertArrayNotHasKey( 'inline hide-if-no-js', $filtered_actions ); $this->assertArrayHasKey( 'customize', $filtered_actions ); $this->assertArrayHasKey( 'front-view', $filtered_actions ); @@ -225,43 +323,14 @@ public function test_filter_post_row_actions() { $this->assertContains( 'plugin->customize_snapshot_manager ); - $post_type->register(); - - $post_data = array( - 'post_name' => '', - 'post_type' => 'no', - 'post_status' => 'pending', - ); - $original_post_data = array( - 'post_type' => 'no', - 'post_name' => '!original!', - 'post_status' => 'pending', - ); - $filtered_post_data = $post_type->preserve_post_name_in_insert_data( $post_data, $original_post_data ); - $this->assertEquals( $post_data, $filtered_post_data ); - - $post_data['post_type'] = Post_Type::SLUG; - $original_post_data['post_type'] = Post_Type::SLUG; - - $filtered_post_data = $post_type->preserve_post_name_in_insert_data( $post_data, $original_post_data ); - $this->assertEquals( $original_post_data['post_name'], $filtered_post_data['post_name'] ); - } - /** * Test rendering the metabox. * * @see Post_Type::render_data_metabox() */ public function test_render_data_metabox() { - $post_type = new Post_Type( $this->plugin->customize_snapshot_manager ); - $post_type->register(); + $post_type = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); + $post_type->init(); $data = array( 'knoa8sdhpasidg0apbdpahcas' => array( 'value' => 'a09sad0as9hdgw22dutacs' ), 'n0nee8fa9s7ap9sdga9sdas9c' => array( 'value' => 'lasdbaosd81vvajgcaf22k' ), @@ -345,10 +414,11 @@ function filter_customize_snapshot_value_preview( $preview, $context ) { * Find a snapshot post by UUID. * * @see Post_Type::find_post() + * @see Post_Type_Back_Compat::find_post() */ public function test_find_post() { - $post_type = new Post_Type( $this->plugin->customize_snapshot_manager ); - $post_type->register(); + $post_type = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); + $post_type->init(); $data = array( 'foo' => array( 'value' => 'bar', @@ -382,12 +452,12 @@ public function test_find_post() { /** * Test getting the snapshot array out of the post_content. * - * @covers CustomizeSnapshots\Post_Type::get_post_content() + * @covers \CustomizeSnapshots\Post_Type::get_post_content() * @expectedException \PHPUnit_Framework_Error_Warning */ public function test_get_post_content() { - $post_type = new Post_Type( $this->plugin->customize_snapshot_manager ); - $post_type->register(); + $post_type = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); + $post_type->init(); // Bad post type. $page_post_id = $this->factory()->post->create( array( 'post_type' => 'page' ) ); @@ -407,6 +477,9 @@ public function test_get_post_content() { 'status' => 'publish', ) ); $snapshot_post = get_post( $post_id ); + if ( ! $this->plugin->compat ) { + unset( $data['foo']['publish_error'] ); + } $this->assertEquals( $data, $post_type->get_post_content( $snapshot_post ) ); // Revision. @@ -424,7 +497,7 @@ public function test_get_post_content() { $this->assertEquals( 'baz', $content['foo']['value'] ); // Bad post data. - $bad_post_id = $this->factory()->post->create( array( 'post_type' => Post_Type::SLUG, 'post_content' => 'BADJSON' ) ); + $bad_post_id = $this->factory()->post->create( array( 'post_type' => $this->post_type_slug, 'post_content' => 'BADJSON' ) ); $bad_post = get_post( $bad_post_id ); $content = $post_type->get_post_content( $bad_post ); $this->assertEquals( array(), $content ); @@ -436,8 +509,8 @@ public function test_get_post_content() { * @see Post_Type::save() */ public function test_save() { - $post_type = new Post_Type( $this->plugin->customize_snapshot_manager ); - $post_type->register(); + $post_type = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); + $post_type->init(); // Error: missing_valid_uuid. $r = $post_type->save( array( 'id' => 'nouuid' ) ); @@ -492,7 +565,11 @@ public function test_save() { 'theme' => get_stylesheet(), ) ); $this->assertInternalType( 'int', $r ); - $this->assertEquals( $data, $post_type->get_post_content( get_post( $r ) ) ); + $expected = $data; + if ( ! $this->plugin->compat ) { + unset( $expected['foo']['publish_error'] ); + } + $this->assertEquals( $expected, $post_type->get_post_content( get_post( $r ) ) ); $this->assertEquals( get_stylesheet(), get_post_meta( $r, '_snapshot_theme', true ) ); $this->assertEquals( $this->plugin->version, get_post_meta( $r, '_snapshot_version', true ) ); @@ -517,101 +594,6 @@ public function test_save() { $this->assertEquals( 'future', get_post_status( $post_id ) ); } - /** - * Snapshot publish. - * - * @see Post_Type::save() - */ - function test_publish_snapshot() { - $admin_user_id = $this->factory()->user->create( array( 'role' => 'administrator' ) ); - wp_set_current_user( $admin_user_id ); - $post_type = get_plugin_instance()->customize_snapshot_manager->post_type; - $post_type->register(); - $tag_line = 'Snapshot blog'; - - $data = array( - 'blogdescription' => array( - 'value' => $tag_line, - ), - 'foo' => array( - 'value' => 'bar', - ), - 'baz' => array( - 'value' => null, - ), - ); - - $validated_content = array( - 'blogdescription' => array( - 'value' => $tag_line, - ), - 'foo' => array( - 'value' => 'bar', - 'publish_error' => 'unrecognized_setting', - ), - 'baz' => array( - 'value' => null, - 'publish_error' => 'null_value', - ), - ); - - /* - * Ensure that directly updating a post succeeds with invalid settings - * works because the post is a draft. Note that if using - * Customize_Snapshot::set() this would fail because it does validation. - */ - $post_id = $post_type->save( array( - 'uuid' => Customize_Snapshot_Manager::generate_uuid(), - 'data' => $data, - 'status' => 'draft', - ) ); - wp_update_post( array( 'ID' => $post_id, 'post_status' => 'draft' ) ); - $content = $post_type->get_post_content( get_post( $post_id ) ); - $this->assertEquals( $data, $content ); - - /* - * Ensure that attempting to publish a snapshot with invalid settings - * will get the publish_errors added as well as kick it back to pending. - */ - remove_all_filters( 'redirect_post_location' ); - $post_id = $post_type->save( array( - 'uuid' => Customize_Snapshot_Manager::generate_uuid(), - 'data' => $data, - 'status' => 'draft', - ) ); - wp_publish_post( $post_id ); - $snapshot_post = get_post( $post_id ); - $content = $post_type->get_post_content( $snapshot_post ); - $this->assertEquals( 'pending', $snapshot_post->post_status ); - $this->assertEquals( $validated_content, $content ); - $this->assertContains( - 'snapshot_error_on_publish=1', - apply_filters( 'redirect_post_location', get_edit_post_link( $snapshot_post->ID ), $snapshot_post->ID ) - ); - - /* - * Remove invalid settings and now attempt publish. - */ - remove_all_filters( 'redirect_post_location' ); - unset( $data['foo'] ); - unset( $data['baz'] ); - $post_id = $post_type->save( array( - 'uuid' => Customize_Snapshot_Manager::generate_uuid(), - 'data' => $data, - 'status' => 'draft', - ) ); - wp_publish_post( $post_id ); - $snapshot_post = get_post( $post_id ); - $content = $post_type->get_post_content( $snapshot_post ); - $this->assertEquals( 'publish', $snapshot_post->post_status ); - $this->assertEquals( $data, $content ); - $this->assertEquals( $tag_line, get_bloginfo( 'description' ) ); - $this->assertNotContains( - 'snapshot_error_on_publish=1', - apply_filters( 'redirect_post_location', get_edit_post_link( $snapshot_post->ID ), $snapshot_post->ID ) - ); - } - /** * Test granting customize capability. * @@ -620,8 +602,8 @@ function test_publish_snapshot() { function test_filter_user_has_cap() { remove_all_filters( 'user_has_cap' ); - $post_type = new Post_Type( $this->plugin->customize_snapshot_manager ); - $post_type->register(); + $post_type = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); + $post_type->init(); $post_id = $post_type->save( array( 'uuid' => self::UUID, @@ -634,72 +616,13 @@ function test_filter_user_has_cap() { $this->assertTrue( current_user_can( 'edit_post', $post_id ) ); } - /** - * Tests display_post_states. - * - * @covers CustomizeSnapshots\Post_Type::display_post_states() - */ - public function test_display_post_states() { - $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 ); - } - - /** - * Tests show_publish_error_admin_notice. - * - * @covers CustomizeSnapshots\Post_Type::show_publish_error_admin_notice() - */ - public function test_show_publish_error_admin_notice() { - global $current_screen, $post; - wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); - $post_type = new Post_Type( $this->plugin->customize_snapshot_manager ); - $post_type->register(); - $post_id = $post_type->save( array( - 'uuid' => self::UUID, - 'data' => array(), - ) ); - - ob_start(); - $post_type->show_publish_error_admin_notice(); - $this->assertEmpty( ob_get_clean() ); - - $current_screen = \WP_Screen::get( 'customize_snapshot' ); // WPCS: Override ok. - $current_screen->id = 'customize_snapshot'; - $current_screen->base = 'edit'; - ob_start(); - $post_type->show_publish_error_admin_notice(); - $this->assertEmpty( ob_get_clean() ); - - $current_screen->base = 'post'; - ob_start(); - $post_type->show_publish_error_admin_notice(); - $this->assertEmpty( ob_get_clean() ); - - $_REQUEST['snapshot_error_on_publish'] = '1'; - wp_update_post( array( 'ID' => $post_id, 'post_status' => 'pending' ) ); - $post = get_post( $post_id ); // WPCS: override ok. - ob_start(); - $post_type->show_publish_error_admin_notice(); - $this->assertContains( 'notice-error', ob_get_clean() ); - } - /** * Tests disable_revision_ui_for_published_posts. * - * @covers CustomizeSnapshots\Post_Type::disable_revision_ui_for_published_posts() + * @covers \CustomizeSnapshots\Post_Type::disable_revision_ui_for_published_posts() */ public function test_disable_revision_ui_for_published_posts() { - $post_type = new Post_Type( $this->plugin->customize_snapshot_manager ); + $post_type = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); $post_id = $post_type->save( array( 'uuid' => self::UUID, 'data' => array(), @@ -725,10 +648,10 @@ public function test_disable_revision_ui_for_published_posts() { /** * Tests hide_disabled_publishing_actions. * - * @covers CustomizeSnapshots\Post_Type::hide_disabled_publishing_actions() + * @covers \CustomizeSnapshots\Post_Type::hide_disabled_publishing_actions() */ public function test_hide_disabled_publishing_actions() { - $post_type = new Post_Type( $this->plugin->customize_snapshot_manager ); + $post_type = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); $post_id = $post_type->save( array( 'uuid' => self::UUID, 'data' => array(), @@ -749,4 +672,195 @@ public function test_hide_disabled_publishing_actions() { $this->assertNotEmpty( $output ); $this->assertContains( 'misc-pub-post-status', $output ); } + + /** + * Tests add_snapshot_bulk_actions + * + * @see Post_Type::add_snapshot_bulk_actions() + */ + public function test_add_snapshot_bulk_actions() { + $post_type = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); + $data = $post_type->add_snapshot_bulk_actions( array() ); + $this->assertArrayHasKey( 'merge_snapshot', $data ); + } + + /** + * Test handle_snapshot_bulk_actions + * + * @see Post_Type::handle_snapshot_merge() + */ + public function test_handle_snapshot_merge() { + $ids = $this->factory()->post->create_many( 2 ); + $posts = array_map( 'get_post', $ids ); + $post_type_obj = $this->getMockBuilder( 'CustomizeSnapshots\Post_Type' ) + ->setConstructorArgs( array( $this->plugin->customize_snapshot_manager ) ) + ->setMethods( array( 'merge_snapshots' ) ) + ->getMock(); + $post_type_obj->expects( $this->once() ) + ->method( 'merge_snapshots' ) + ->with( $posts ) + ->will( $this->returnValue( null ) ); + $post_type_obj->handle_snapshot_merge( '', 'merge_snapshot', $ids ); + } + + /** + * Test merge_snapshots + * + * @see Post_Type::merge_snapshots() + */ + public function test_merge_snapshots() { + $post_type = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); + $date1 = gmdate( 'Y-m-d H:i:s' ); + $post_1 = $post_type->save( array( + 'uuid' => Customize_Snapshot_Manager::generate_uuid(), + 'status' => 'draft', + 'data' => array( + 'foo' => array( + 'value' => 'bar', + ), + ), + 'date_gmt' => $date1, + ) ); + $value = array( + 'foo' => array( + 'value' => 'baz', + ), + 'baz' => array( + 'value' => 'zab', + ), + ); + $date2 = gmdate( 'Y-m-d H:i:s', ( time() + 60 ) ); + $post_2 = $post_type->save( array( + 'uuid' => Customize_Snapshot_Manager::generate_uuid(), + 'status' => 'draft', + 'data' => $value, + 'date_gmt' => $date2, + ) ); + + $merged_post_id = $post_type->merge_snapshots( array( $post_1, $post_2 ) ); + $merged_post = get_post( $merged_post_id ); + $value['foo']['merge_conflict'] = array( + array( + 'uuid' => get_post( $post_1 )->post_name, + 'value' => 'bar', + ), + array( + 'uuid' => get_post( $post_2 )->post_name, + 'value' => 'baz', + ), + ); + $this->assertSame( $value, $post_type->get_post_content( $merged_post ) ); + + $date3 = gmdate( 'Y-m-d H:i:s', ( time() + 120 ) ); + + $value_3 = array( + 'baz' => array( + 'value' => 'z', + ), + ); + $post_3 = $post_type->save( array( + 'uuid' => Customize_Snapshot_Manager::generate_uuid(), + 'status' => 'draft', + 'data' => $value_3, + 'date_gmt' => $date3, + ) ); + $post_3 = get_post( $post_3 ); + $merge_result_post = get_post( $post_type->merge_snapshots( array( $post_1, $post_2, $post_3 ) ) ); + $value['baz']['value'] = 'z'; + $value['baz']['merge_conflict'] = array( + array( + 'uuid' => get_post( $post_2 )->post_name, + 'value' => 'zab', + ), + array( + 'uuid' => $post_3->post_name, + 'value' => 'z', + ), + ); + $this->assertSame( $value, $post_type->get_post_content( $merge_result_post ) ); + } + + /** + * Test admin_show_merge_error + * + * @see Post_Type::admin_show_merge_error() + */ + public function test_admin_show_merge_error() { + $post_type_obj = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); + ob_start(); + $post_type_obj->admin_show_merge_error(); + $notice_content = ob_get_clean(); + $this->assertEmpty( $notice_content ); + ob_start(); + $_POST['merge-error'] = $_REQUEST['merge-error'] = $_GET['merge-error'] = 1; + $post_type_obj->admin_show_merge_error(); + $notice_content = ob_get_clean(); + $this->assertContains( 'notice-error', $notice_content ); + $_POST['merge-error'] = $_REQUEST['merge-error'] = $_GET['merge-error'] = 5; + ob_start(); + $post_type_obj->admin_show_merge_error(); + $notice_content = ob_get_clean(); + $this->assertEmpty( $notice_content ); + } + + /** + * Test filter_out_settings_if_removed_in_metabox. + * + * @covers \CustomizeSnapshots\Post_Type::filter_out_settings_if_removed_in_metabox() + */ + public function test_filter_out_settings_if_removed_in_metabox() { + global $post; + $post_type_obj = $this->get_new_post_type_instance( $this->plugin->customize_snapshot_manager ); + $post_type_obj->init(); + $admin_user_id = $this->factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_user_id ); + $post_id = $post_type_obj->save( array( + 'uuid' => self::UUID, + 'data' => array( + 'foo' => array( + 'value' => 'foo_value', + ), + 'bar' => array( + 'value' => 'bar_value', + ), + ), + 'status' => 'draft', + ) ); + $post = get_post( $post_id ); // WPCS: override ok. + $nonce_key = $this->post_type_slug; + $key_for_settings = $this->post_type_slug . '_remove_settings'; + $_REQUEST[ $nonce_key ] = $_POST[ $nonce_key ] = wp_create_nonce( $this->post_type_slug . '_settings' ); + $_REQUEST[ $key_for_settings ] = $_POST[ $key_for_settings ] = array( 'foo' ); + $content = $post_type_obj->filter_out_settings_if_removed_in_metabox( $post->post_content ); + $data = json_decode( $content, true ); + $this->assertArrayNotHasKey( 'foo', $data ); + } + + /** + * Test remap_customize_meta_cap + * + * @covers \CustomizeSnapshots\Post_Type::remap_customize_meta_cap() + */ + public function test_remap_customize_meta_cap() { + $this->mark_incompatible(); + $this->markTestIncomplete(); + } + + /** + * Test hide_add_new_changeset_button + * + * @covers \CustomizeSnapshots\Post_Type::hide_add_new_changeset_button() + */ + public function test_hide_add_new_changeset_button() { + $this->mark_incompatible(); + $post_type_obj = new Post_Type( $this->plugin->customize_snapshot_manager ); + global $typenow; + $typenow = Post_Type::SLUG; // WPCS: Global override ok. + ob_start(); + $post_type_obj->hide_add_new_changeset_button(); + $content = ob_get_clean(); + $this->assertContains( 'a.page-title-action', $content ); + $this->assertContains( 'display: none;', $content ); + } + } diff --git a/tests/php/test-class-snapshot-ajax.php b/tests/php/test-class-snapshot-ajax.php new file mode 100644 index 00000000..c5dc1114 --- /dev/null +++ b/tests/php/test-class-snapshot-ajax.php @@ -0,0 +1,84 @@ +plugin = get_plugin_instance(); + if ( $this->plugin->compat ) { + $this->markTestSkipped( 'This unit-test require WP version 4.7 or up.' ); + } + parent::setUp(); + } + /** + * Test handle_migrate_changeset_request. + * + * @see Migrate::handle_migrate_changeset_request() + */ + function test_handle_migrate_changeset_request() { + remove_all_actions( 'wp_ajax_customize_snapshot_migration' ); + delete_option( Migrate::KEY ); + $migrate_obj = $this->getMockBuilder( 'CustomizeSnapshots\Migrate' ) + ->setMethods( array( 'changeset_migrate' ) ) + ->setConstructorArgs( array( $this->plugin ) ) + ->getMock(); + $migrate_obj->expects( $this->once() ) + ->method( 'changeset_migrate' ) + ->with( 1, false ) + ->will( $this->returnValue( 92 ) ); + $migrate_obj->maybe_migrate(); + $this->set_input_vars(array( + 'nonce' => wp_create_nonce( 'customize-snapshot-migration' ), + 'limit' => 1, + )); + $this->make_ajax_call( 'customize_snapshot_migration' ); + $response = json_decode( $this->_last_response, true ); + $this->assertTrue( $response['success'] ); + $this->assertArrayHasKey( 'remaining_posts', $response['data'] ); + $this->assertEquals( 91, $response['data']['remaining_posts'] ); + } + + /** + * Helper to keep it DRY + * + * @param string $action Action. + */ + protected function make_ajax_call( $action ) { + try { + $this->_handleAjax( $action ); + } catch ( \WPAjaxDieContinueException $e ) { + unset( $e ); + } + } + + /** + * Set input vars. + * + * @param array $vars Input vars. + * @param string $method Request method. + */ + public function set_input_vars( array $vars = array(), $method = 'POST' ) { + $_GET = $_POST = $_REQUEST = wp_slash( $vars ); + $_SERVER['REQUEST_METHOD'] = $method; + } + +} diff --git a/tests/php/test-class-snapshot-rest-api-controller.php b/tests/php/test-class-snapshot-rest-api-controller.php index 5d1c6608..0b51c1e7 100644 --- a/tests/php/test-class-snapshot-rest-api-controller.php +++ b/tests/php/test-class-snapshot-rest-api-controller.php @@ -33,6 +33,13 @@ class Test_Snapshot_REST_API_Controller extends \WP_Test_REST_TestCase { */ public $snapshot_by_status = array(); + /** + * End point. + * + * @var string + */ + public $end_point; + /** * Set up. */ @@ -40,7 +47,13 @@ function setUp() { parent::setUp(); $this->plugin = get_plugin_instance(); - $this->plugin->customize_snapshot_manager->post_type->register(); + if ( $this->plugin->compat ) { + $this->end_point = 'customize_snapshots'; + } else { + $this->end_point = 'customize_changesets'; + } + + $this->plugin->customize_snapshot_manager->post_type->init(); $snapshot_data = array( array( @@ -79,12 +92,12 @@ function setUp() { } /** - * Test unauthenticated requests for /wp/v2/customize_snapshots + * Test unauthenticated requests for /wp/v2/$end_point */ function test_get_collection_unauthenticated() { wp_set_current_user( 0 ); $this->assertFalse( current_user_can( 'customize' ) ); - $request = new \WP_REST_Request( 'GET', '/wp/v2/customize_snapshots' ); + $request = new \WP_REST_Request( 'GET', '/wp/v2/' . $this->end_point ); $response = $this->server->dispatch( $request ); $this->assertErrorResponse( 'rest_customize_unauthorized', $response ); } @@ -95,18 +108,18 @@ function test_get_collection_unauthenticated() { function test_get_collection_unauthorized() { wp_set_current_user( $this->factory()->user->create( array( 'role' => 'contributor' ) ) ); $this->assertFalse( current_user_can( 'customize' ) ); - $request = new \WP_REST_Request( 'GET', '/wp/v2/customize_snapshots' ); + $request = new \WP_REST_Request( 'GET', '/wp/v2/' . $this->end_point ); $response = $this->server->dispatch( $request ); $this->assertErrorResponse( 'rest_customize_unauthorized', $response ); } /** - * Test unauthorized requests for /wp/v2/customize_snapshots + * Test unauthorized requests for /wp/v2/$end_point */ function test_get_collection_authorized() { wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); $this->assertTrue( current_user_can( 'customize' ) ); - $request = new \WP_REST_Request( 'GET', '/wp/v2/customize_snapshots' ); + $request = new \WP_REST_Request( 'GET', '/wp/v2/' . $this->end_point ); $response = $this->server->dispatch( $request ); $this->assertEquals( 200, $response->get_status() ); } @@ -116,7 +129,7 @@ function test_get_collection_authorized() { */ function test_get_collection_published() { wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); - $request = new \WP_REST_Request( 'GET', '/wp/v2/customize_snapshots' ); + $request = new \WP_REST_Request( 'GET', '/wp/v2/' . $this->end_point ); $request->set_param( 'context', 'edit' ); $response = $this->server->dispatch( $request ); $this->assertEquals( 200, $response->get_status() ); @@ -127,7 +140,7 @@ function test_get_collection_published() { $this->assertArrayHasKey( 'blogname', $items[0]['content'] ); $this->assertArrayHasKey( 'value', $items[0]['content']['blogname'] ); - $request = new \WP_REST_Request( 'GET', '/wp/v2/customize_snapshots' ); + $request = new \WP_REST_Request( 'GET', '/wp/v2/' . $this->end_point ); $request->set_param( 'context', 'edit' ); $request->set_url_params( array( 'status' => 'publish' ) ); $response = $this->server->dispatch( $request ); @@ -139,7 +152,7 @@ function test_get_collection_published() { */ function test_get_collection_any() { wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); - $request = new \WP_REST_Request( 'GET', '/wp/v2/customize_snapshots' ); + $request = new \WP_REST_Request( 'GET', '/wp/v2/' . $this->end_point ); $request->set_param( 'context', 'edit' ); $request->set_param( 'status', 'any' ); $response = $this->server->dispatch( $request ); @@ -158,7 +171,7 @@ function test_get_collection_any() { */ function test_get_collection_by_author() { wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); - $request = new \WP_REST_Request( 'GET', '/wp/v2/customize_snapshots' ); + $request = new \WP_REST_Request( 'GET', '/wp/v2/' . $this->end_point ); $request->set_param( 'context', 'edit' ); $request->set_param( 'status', 'any' ); $response = $this->server->dispatch( $request ); @@ -170,7 +183,7 @@ function test_get_collection_by_author() { $item_author_mapping[ $item['author'] ] = $item['slug']; } - $request = new \WP_REST_Request( 'GET', '/wp/v2/customize_snapshots' ); + $request = new \WP_REST_Request( 'GET', '/wp/v2/' . $this->end_point ); $request->set_param( 'context', 'edit' ); $request->set_param( 'status', 'any' ); $request->set_param( 'author', $item_authors[0] ); @@ -179,7 +192,7 @@ function test_get_collection_by_author() { $this->assertCount( 1, $items ); $this->assertEquals( $items[0]['slug'], $item_author_mapping[ $item_authors[0] ] ); - $request = new \WP_REST_Request( 'GET', '/wp/v2/customize_snapshots' ); + $request = new \WP_REST_Request( 'GET', '/wp/v2/' . $this->end_point ); $request->set_param( 'context', 'edit' ); $request->set_param( 'status', 'any' ); $request->set_param( 'author', get_user_by( 'id', $item_authors[0] )->user_nicename ); @@ -188,7 +201,7 @@ function test_get_collection_by_author() { $this->assertCount( 1, $items ); $this->assertEquals( $items[0]['slug'], $item_author_mapping[ $item_authors[0] ] ); - $request = new \WP_REST_Request( 'GET', '/wp/v2/customize_snapshots' ); + $request = new \WP_REST_Request( 'GET', '/wp/v2/' . $this->end_point ); $request->set_param( 'context', 'edit' ); $request->set_param( 'status', 'any' ); $request->set_param( 'author', join( ',', $item_authors ) ); @@ -203,7 +216,7 @@ function test_get_collection_by_author() { function test_get_item_published() { wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); $post = get_post( $this->snapshot_by_status['publish'] ); - $request = new \WP_REST_Request( 'GET', '/wp/v2/customize_snapshots/' . $post->ID ); + $request = new \WP_REST_Request( 'GET', '/wp/v2/' . $this->end_point . '/' . $post->ID ); $request->set_param( 'context', 'edit' ); $response = $this->server->dispatch( $request ); $this->assertEquals( 200, $response->get_status() ); @@ -219,7 +232,7 @@ function test_get_item_published() { */ function test_create_item() { wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); - $request = new \WP_REST_Request( 'POST', '/wp/v2/customize_snapshots' ); + $request = new \WP_REST_Request( 'POST', '/wp/v2/' . $this->end_point ); $request->set_param( 'content', array( 'blogname' => array( 'value' => 'test' ) ) ); $request->set_param( 'slug', Customize_Snapshot_Manager::generate_uuid() ); $response = $this->server->dispatch( $request ); @@ -232,7 +245,7 @@ function test_create_item() { function test_update_item() { wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); $post = get_post( $this->snapshot_by_status['publish'] ); - $request = new \WP_REST_Request( 'PUT', '/wp/v2/customize_snapshots/' . $post->ID ); + $request = new \WP_REST_Request( 'PUT', '/wp/v2/' . $this->end_point . '/' . $post->ID ); $request->set_param( 'content', array( 'blogname' => array( 'value' => 'test' ) ) ); $response = $this->server->dispatch( $request ); $this->assertErrorResponse( 'invalid-method', $response ); @@ -244,7 +257,7 @@ function test_update_item() { function test_delete_item() { wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); $post = get_post( $this->snapshot_by_status['publish'] ); - $request = new \WP_REST_Request( 'DELETE', '/wp/v2/customize_snapshots/' . $post->ID ); + $request = new \WP_REST_Request( 'DELETE', '/wp/v2/' . $this->end_point . '/' . $post->ID ); $response = $this->server->dispatch( $request ); $this->assertErrorResponse( 'invalid-method', $response ); } diff --git a/tests/test-customize-snapshots.php b/tests/test-customize-snapshots.php index 0b326566..cbbfb1a0 100644 --- a/tests/test-customize-snapshots.php +++ b/tests/test-customize-snapshots.php @@ -12,12 +12,31 @@ */ class Test_Customize_Snapshots extends \WP_UnitTestCase { + /** + * Frontend UUID + * + * @var string + */ + public $front_param; + + /** + * Set up. + */ + function setUp() { + $plugin = get_plugin_instance(); + if ( $plugin->compat ) { + $this->front_param = 'customize_snapshot_uuid'; + } else { + $this->front_param = 'changeset_uuid'; + } + } + /** * Clean up global scope. */ function clean_up_global_scope() { global $customize_snapshots_plugin; - unset( $_REQUEST['customize_snapshot_uuid'] ); + unset( $_REQUEST[ $this->front_param ] ); parent::clean_up_global_scope(); $customize_snapshots_plugin = new Plugin(); $customize_snapshots_plugin->init(); @@ -50,6 +69,9 @@ function test_customize_snapshots_php_version_text() { * @see is_previewing_settings() */ public function test_is_previewing_settings() { + if ( ! get_plugin_instance()->compat ) { + $this->markTestIncomplete( 'WordPress Version 4.6.x or below is required for this test-case.' ); + } $this->assertFalse( is_previewing_settings() ); do_action( 'customize_preview_init' ); $this->assertTrue( is_previewing_settings() ); @@ -62,9 +84,8 @@ public function test_is_previewing_settings() { */ 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; + $_REQUEST[ $this->front_param ] = $uuid; $customize_snapshots_plugin = new Plugin(); $customize_snapshots_plugin->init(); $this->assertEquals( $uuid, current_snapshot_uuid() );