Changeset 38810
- Timestamp:
- 10/18/2016 08:04:36 PM (8 years ago)
- Location:
- trunk
- Files:
-
- 1 added
- 31 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-admin/customize.php
r38672 r38810 21 21 } 22 22 23 /** 24 * @global WP_Scripts $wp_scripts 25 * @global WP_Customize_Manager $wp_customize 26 */ 27 global $wp_scripts, $wp_customize; 28 29 if ( $wp_customize->changeset_post_id() ) { 30 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $wp_customize->changeset_post_id() ) ) { 31 wp_die( 32 '<h1>' . __( 'Cheatin’ uh?' ) . '</h1>' . 33 '<p>' . __( 'Sorry, you are not allowed to edit this changeset.' ) . '</p>', 34 403 35 ); 36 } 37 if ( in_array( get_post_status( $wp_customize->changeset_post_id() ), array( 'publish', 'trash' ), true ) ) { 38 wp_die( 39 '<h1>' . __( 'Cheatin’ uh?' ) . '</h1>' . 40 '<p>' . __( 'This changeset has already been published and cannot be further modified.' ) . '</p>' . 41 '<p><a href="' . esc_url( remove_query_arg( 'changeset_uuid' ) ) . '">' . __( 'Customize New Changes' ) . '</a></p>', 42 403 43 ); 44 } 45 } 46 47 23 48 wp_reset_vars( array( 'url', 'return', 'autofocus' ) ); 24 49 if ( ! empty( $url ) ) { … … 31 56 $wp_customize->set_autofocus( wp_unslash( $autofocus ) ); 32 57 } 33 34 /**35 * @global WP_Scripts $wp_scripts36 * @global WP_Customize_Manager $wp_customize37 */38 global $wp_scripts, $wp_customize;39 58 40 59 $registered = $wp_scripts->registered; … … 116 135 <?php 117 136 $save_text = $wp_customize->is_theme_active() ? __( 'Save & Publish' ) : __( 'Save & Activate' ); 118 submit_button( $save_text, 'primary save', 'save', false ); 137 $save_attrs = array(); 138 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) { 139 $save_attrs['style'] = 'display: none'; 140 } 141 submit_button( $save_text, 'primary save', 'save', false, $save_attrs ); 119 142 ?> 120 143 <span class="spinner"></span> -
trunk/src/wp-admin/js/customize-controls.js
r38742 r38810 23 23 api.Setting = api.Value.extend({ 24 24 initialize: function( id, value, options ) { 25 api.Value.prototype.initialize.call( this, value, options ); 26 27 this.id = id; 28 this.transport = this.transport || 'refresh'; 29 this._dirty = options.dirty || false; 30 this.notifications = new api.Values({ defaultConstructor: api.Notification }); 25 var setting = this; 26 api.Value.prototype.initialize.call( setting, value, options ); 27 28 setting.id = id; 29 setting.transport = setting.transport || 'refresh'; 30 setting._dirty = options.dirty || false; 31 setting.notifications = new api.Values({ defaultConstructor: api.Notification }); 31 32 32 33 // Whenever the setting's value changes, refresh the preview. 33 this.bind( this.preview );34 setting.bind( setting.preview ); 34 35 }, 35 36 36 37 /** 37 38 * Refresh the preview, respective of the setting's refresh policy. 39 * 40 * If the preview hasn't sent a keep-alive message and is likely 41 * disconnected by having navigated to a non-allowed URL, then the 42 * refresh transport will be forced when postMessage is the transport. 43 * Note that postMessage does not throw an error when the recipient window 44 * fails to match the origin window, so using try/catch around the 45 * previewer.send() call to then fallback to refresh will not work. 46 * 47 * @since 3.4.0 38 48 */ 39 49 preview: function() { 40 switch ( this.transport ) { 41 case 'refresh': 42 return this.previewer.refresh(); 43 case 'postMessage': 44 return this.previewer.send( 'setting', [ this.id, this() ] ); 50 var setting = this, transport; 51 transport = setting.transport; 52 53 if ( 'postMessage' === transport && ! api.state( 'previewerAlive' ).get() ) { 54 transport = 'refresh'; 55 } 56 57 if ( 'postMessage' === transport ) { 58 return setting.previewer.send( 'setting', [ setting.id, setting() ] ); 59 } else if ( 'refresh' === transport ) { 60 return setting.previewer.refresh(); 45 61 } 46 62 }, … … 66 82 67 83 /** 68 * Utility function namespace 84 * Current change count. 85 * 86 * @since 4.7.0 87 * @type {number} 88 * @protected 69 89 */ 70 api.utils = {}; 90 api._latestRevision = 0; 91 92 /** 93 * Last revision that was saved. 94 * 95 * @since 4.7.0 96 * @type {number} 97 * @protected 98 */ 99 api._lastSavedRevision = 0; 100 101 /** 102 * Latest revisions associated with the updated setting. 103 * 104 * @since 4.7.0 105 * @type {object} 106 * @protected 107 */ 108 api._latestSettingRevisions = {}; 109 110 /* 111 * Keep track of the revision associated with each updated setting so that 112 * requestChangesetUpdate knows which dirty settings to include. Also, once 113 * ready is triggered and all initial settings have been added, increment 114 * revision for each newly-created initially-dirty setting so that it will 115 * also be included in changeset update requests. 116 */ 117 api.bind( 'change', function incrementChangedSettingRevision( setting ) { 118 api._latestRevision += 1; 119 api._latestSettingRevisions[ setting.id ] = api._latestRevision; 120 } ); 121 api.bind( 'ready', function() { 122 api.bind( 'add', function incrementCreatedSettingRevision( setting ) { 123 if ( setting._dirty ) { 124 api._latestRevision += 1; 125 api._latestSettingRevisions[ setting.id ] = api._latestRevision; 126 } 127 } ); 128 } ); 129 130 /** 131 * Get the dirty setting values. 132 * 133 * @param {object} [options] Options. 134 * @param {boolean} [options.unsaved=false] Whether only values not saved yet into a changeset will be returned (differential changes). 135 * @returns {object} Dirty setting values. 136 */ 137 api.dirtyValues = function dirtyValues( options ) { 138 var values = {}; 139 api.each( function( setting ) { 140 var settingRevision; 141 142 if ( ! setting._dirty ) { 143 return; 144 } 145 146 settingRevision = api._latestSettingRevisions[ setting.id ]; 147 148 // Skip including settings that have already been included in the changeset, if only requesting unsaved. 149 if ( ( options && options.unsaved ) && ( _.isUndefined( settingRevision ) || settingRevision <= api._lastSavedRevision ) ) { 150 return; 151 } 152 153 values[ setting.id ] = setting.get(); 154 } ); 155 return values; 156 }; 157 158 /** 159 * Request updates to the changeset. 160 * 161 * @param {object} [changes] Mapping of setting IDs to setting params each normally including a value property, or mapping to null. 162 * If not provided, then the changes will still be obtained from unsaved dirty settings. 163 * @returns {jQuery.Promise} 164 */ 165 api.requestChangesetUpdate = function requestChangesetUpdate( changes ) { 166 var deferred, request, submittedChanges = {}, data; 167 deferred = new $.Deferred(); 168 169 if ( changes ) { 170 _.extend( submittedChanges, changes ); 171 } 172 173 // Ensure all revised settings (changes pending save) are also included, but not if marked for deletion in changes. 174 _.each( api.dirtyValues( { unsaved: true } ), function( dirtyValue, settingId ) { 175 if ( ! changes || null !== changes[ settingId ] ) { 176 submittedChanges[ settingId ] = _.extend( 177 {}, 178 submittedChanges[ settingId ] || {}, 179 { value: dirtyValue } 180 ); 181 } 182 } ); 183 184 // Short-circuit when there are no pending changes. 185 if ( _.isEmpty( submittedChanges ) ) { 186 deferred.resolve( {} ); 187 return deferred.promise(); 188 } 189 190 // Make sure that publishing a changeset waits for all changeset update requests to complete. 191 api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 ); 192 deferred.always( function() { 193 api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 ); 194 } ); 195 196 // Allow plugins to attach additional params to the settings. 197 api.trigger( 'changeset-save', submittedChanges ); 198 199 // Ensure that if any plugins add data to save requests by extending query() that they get included here. 200 data = api.previewer.query( { excludeCustomizedSaved: true } ); 201 delete data.customized; // Being sent in customize_changeset_data instead. 202 _.extend( data, { 203 nonce: api.settings.nonce.save, 204 customize_theme: api.settings.theme.stylesheet, 205 customize_changeset_data: JSON.stringify( submittedChanges ) 206 } ); 207 208 request = wp.ajax.post( 'customize_save', data ); 209 210 request.done( function requestChangesetUpdateDone( data ) { 211 var savedChangesetValues = {}; 212 213 // Ensure that all settings updated subsequently will be included in the next changeset update request. 214 api._lastSavedRevision = Math.max( api._latestRevision, api._lastSavedRevision ); 215 216 api.state( 'changesetStatus' ).set( data.changeset_status ); 217 deferred.resolve( data ); 218 api.trigger( 'changeset-saved', data ); 219 220 if ( data.setting_validities ) { 221 _.each( data.setting_validities, function( validity, settingId ) { 222 if ( true === validity && _.isObject( submittedChanges[ settingId ] ) && ! _.isUndefined( submittedChanges[ settingId ].value ) ) { 223 savedChangesetValues[ settingId ] = submittedChanges[ settingId ].value; 224 } 225 } ); 226 } 227 228 api.previewer.send( 'changeset-saved', _.extend( {}, data, { saved_changeset_values: savedChangesetValues } ) ); 229 } ); 230 request.fail( function requestChangesetUpdateFail( data ) { 231 deferred.reject( data ); 232 api.trigger( 'changeset-error', data ); 233 } ); 234 request.always( function( data ) { 235 if ( data.setting_validities ) { 236 api._handleSettingValidities( { 237 settingValidities: data.setting_validities 238 } ); 239 } 240 } ); 241 242 return deferred.promise(); 243 }; 71 244 72 245 /** … … 1217 1390 1218 1391 /** 1392 * Load theme preview. 1393 * 1394 * @since 4.7.0 1395 * 1396 * @param {string} themeId Theme ID. 1397 * @returns {jQuery.promise} Promise. 1398 */ 1399 loadThemePreview: function( themeId ) { 1400 var deferred = $.Deferred(), onceProcessingComplete, overlay, urlParser; 1401 1402 urlParser = document.createElement( 'a' ); 1403 urlParser.href = location.href; 1404 urlParser.search = $.param( _.extend( 1405 api.utils.parseQueryString( urlParser.search.substr( 1 ) ), 1406 { 1407 theme: themeId, 1408 changeset_uuid: api.settings.changeset.uuid 1409 } 1410 ) ); 1411 1412 overlay = $( '.wp-full-overlay' ); 1413 overlay.addClass( 'customize-loading' ); 1414 1415 onceProcessingComplete = function() { 1416 var request; 1417 if ( api.state( 'processing' ).get() > 0 ) { 1418 return; 1419 } 1420 1421 api.state( 'processing' ).unbind( onceProcessingComplete ); 1422 1423 request = api.requestChangesetUpdate(); 1424 request.done( function() { 1425 $( window ).off( 'beforeunload.customize-confirm' ); 1426 window.location.href = urlParser.href; 1427 } ); 1428 request.fail( function() { 1429 overlay.removeClass( 'customize-loading' ); 1430 } ); 1431 }; 1432 1433 if ( 0 === api.state( 'processing' ).get() ) { 1434 onceProcessingComplete(); 1435 } else { 1436 api.state( 'processing' ).bind( onceProcessingComplete ); 1437 } 1438 1439 return deferred.promise(); 1440 }, 1441 1442 /** 1219 1443 * Render & show the theme details for a given theme model. 1220 1444 * … … 1224 1448 */ 1225 1449 showDetails: function ( theme, callback ) { 1226 var section = this ;1450 var section = this, link; 1227 1451 callback = callback || function(){}; 1228 1452 section.currentTheme = theme.id; … … 1233 1457 section.containFocus( section.overlay ); 1234 1458 section.updateLimits(); 1459 1460 link = section.overlay.find( '.inactive-theme > a' ); 1461 1462 link.on( 'click', function( event ) { 1463 event.preventDefault(); 1464 1465 // Short-circuit if request is currently being made. 1466 if ( link.hasClass( 'disabled' ) ) { 1467 return; 1468 } 1469 link.addClass( 'disabled' ); 1470 1471 section.loadThemePreview( theme.id ).fail( function() { 1472 link.removeClass( 'disabled' ); 1473 } ); 1474 } ); 1235 1475 callback(); 1236 1476 }, … … 2228 2468 nonce: _wpCustomizeBackground.nonces.add, 2229 2469 wp_customize: 'on', 2230 theme: api.settings.theme.stylesheet,2470 customize_theme: api.settings.theme.stylesheet, 2231 2471 attachment_id: this.params.attachment.id 2232 2472 } ); … … 2593 2833 // Ensure custom-header-crop Ajax requests bootstrap the Customizer to activate the previewed theme. 2594 2834 wp.media.controller.Cropper.prototype.defaults.doCropArgs.wp_customize = 'on'; 2595 wp.media.controller.Cropper.prototype.defaults.doCropArgs. theme = api.settings.theme.stylesheet;2835 wp.media.controller.Cropper.prototype.defaults.doCropArgs.customize_theme = api.settings.theme.stylesheet; 2596 2836 }, 2597 2837 … … 2884 3124 } 2885 3125 2886 var previewUrl = $( this ).data( 'previewUrl' ); 2887 2888 $( '.wp-full-overlay' ).addClass( 'customize-loading' ); 2889 2890 window.parent.location = previewUrl; 3126 api.section( control.section() ).loadThemePreview( control.params.theme.id ); 2891 3127 }); 2892 3128 … … 2949 3185 */ 2950 3186 api.PreviewFrame = api.Messenger.extend({ 2951 sensitivity: 2000,3187 sensitivity: null, // Will get set to api.settings.timeouts.previewFrameSensitivity. 2952 3188 2953 3189 /** … … 2955 3191 * 2956 3192 * @param {object} params.container 2957 * @param {object} params.signature2958 3193 * @param {object} params.previewUrl 2959 3194 * @param {object} params.query … … 2970 3205 2971 3206 this.container = params.container; 2972 this.signature = params.signature;2973 3207 2974 3208 $.extend( params, { channel: api.PreviewFrame.uuid() }); … … 2990 3224 */ 2991 3225 run: function( deferred ) { 2992 var self= this,3226 var previewFrame = this, 2993 3227 loaded = false, 2994 ready = false; 2995 2996 if ( this._ready ) { 2997 this.unbind( 'ready', this._ready ); 2998 } 2999 3000 this._ready = function() { 3228 ready = false, 3229 readyData = null, 3230 hasPendingChangesetUpdate = '{}' !== previewFrame.query.customized, 3231 urlParser, 3232 params, 3233 form; 3234 3235 if ( previewFrame._ready ) { 3236 previewFrame.unbind( 'ready', previewFrame._ready ); 3237 } 3238 3239 previewFrame._ready = function( data ) { 3001 3240 ready = true; 3002 3003 if ( loaded ) { 3004 deferred.resolveWith( self ); 3005 } 3006 }; 3007 3008 this.bind( 'ready', this._ready ); 3009 3010 this.bind( 'ready', function ( data ) { 3011 3012 this.container.addClass( 'iframe-ready' ); 3013 3241 readyData = data; 3242 previewFrame.container.addClass( 'iframe-ready' ); 3014 3243 if ( ! data ) { 3015 3244 return; 3016 3245 } 3017 3246 3018 /* 3019 * Walk over all panels, sections, and controls and set their 3020 * respective active states to true if the preview explicitly 3021 * indicates as such. 3022 */ 3023 var constructs = { 3024 panel: data.activePanels, 3025 section: data.activeSections, 3026 control: data.activeControls 3027 }; 3028 _( constructs ).each( function ( activeConstructs, type ) { 3029 api[ type ].each( function ( construct, id ) { 3030 var isDynamicallyCreated = _.isUndefined( api.settings[ type + 's' ][ id ] ); 3031 3032 /* 3033 * If the construct was created statically in PHP (not dynamically in JS) 3034 * then consider a missing (undefined) value in the activeConstructs to 3035 * mean it should be deactivated (since it is gone). But if it is 3036 * dynamically created then only toggle activation if the value is defined, 3037 * as this means that the construct was also then correspondingly 3038 * created statically in PHP and the active callback is available. 3039 * Otherwise, dynamically-created constructs should normally have 3040 * their active states toggled in JS rather than from PHP. 3041 */ 3042 if ( ! isDynamicallyCreated || ! _.isUndefined( activeConstructs[ id ] ) ) { 3043 if ( activeConstructs[ id ] ) { 3044 construct.activate(); 3045 } else { 3046 construct.deactivate(); 3047 } 3048 } 3049 } ); 3247 if ( loaded ) { 3248 deferred.resolveWith( previewFrame, [ data ] ); 3249 } 3250 }; 3251 3252 previewFrame.bind( 'ready', previewFrame._ready ); 3253 3254 urlParser = document.createElement( 'a' ); 3255 urlParser.href = previewFrame.previewUrl(); 3256 3257 params = _.extend( 3258 api.utils.parseQueryString( urlParser.search.substr( 1 ) ), 3259 { 3260 customize_changeset_uuid: previewFrame.query.customize_changeset_uuid, 3261 customize_theme: previewFrame.query.customize_theme, 3262 customize_messenger_channel: previewFrame.query.customize_messenger_channel 3263 } 3264 ); 3265 3266 urlParser.search = $.param( params ); 3267 previewFrame.iframe = $( '<iframe />', { 3268 title: api.l10n.previewIframeTitle, 3269 name: 'customize-' + previewFrame.channel() 3270 } ); 3271 previewFrame.iframe.attr( 'onmousewheel', '' ); // Workaround for Safari bug. See WP Trac #38149. 3272 3273 if ( ! hasPendingChangesetUpdate ) { 3274 previewFrame.iframe.attr( 'src', urlParser.href ); 3275 } else { 3276 previewFrame.iframe.attr( 'data-src', urlParser.href ); // For debugging purposes. 3277 } 3278 3279 previewFrame.iframe.appendTo( previewFrame.container ); 3280 previewFrame.targetWindow( previewFrame.iframe[0].contentWindow ); 3281 3282 /* 3283 * Submit customized data in POST request to preview frame window since 3284 * there are setting value changes not yet written to changeset. 3285 */ 3286 if ( hasPendingChangesetUpdate ) { 3287 form = $( '<form>', { 3288 action: urlParser.href, 3289 target: previewFrame.iframe.attr( 'name' ), 3290 method: 'post', 3291 hidden: 'hidden' 3050 3292 } ); 3051 3052 if ( data.settingValidities ) { 3053 api._handleSettingValidities( { 3054 settingValidities: data.settingValidities, 3055 focusInvalidControl: false 3056 } ); 3057 } 3293 form.append( $( '<input>', { 3294 type: 'hidden', 3295 name: '_method', 3296 value: 'GET' 3297 } ) ); 3298 _.each( previewFrame.query, function( value, key ) { 3299 form.append( $( '<input>', { 3300 type: 'hidden', 3301 name: key, 3302 value: value 3303 } ) ); 3304 } ); 3305 previewFrame.container.append( form ); 3306 form.submit(); 3307 form.remove(); // No need to keep the form around after submitted. 3308 } 3309 3310 previewFrame.bind( 'iframe-loading-error', function( error ) { 3311 previewFrame.iframe.remove(); 3312 3313 // Check if the user is not logged in. 3314 if ( 0 === error ) { 3315 previewFrame.login( deferred ); 3316 return; 3317 } 3318 3319 // Check for cheaters. 3320 if ( -1 === error ) { 3321 deferred.rejectWith( previewFrame, [ 'cheatin' ] ); 3322 return; 3323 } 3324 3325 deferred.rejectWith( previewFrame, [ 'request failure' ] ); 3058 3326 } ); 3059 3327 3060 this.request = $.ajax( this.previewUrl(), { 3061 type: 'POST', 3062 data: this.query, 3063 xhrFields: { 3064 withCredentials: true 3065 } 3066 } ); 3067 3068 this.request.fail( function() { 3069 deferred.rejectWith( self, [ 'request failure' ] ); 3070 }); 3071 3072 this.request.done( function( response ) { 3073 var location = self.request.getResponseHeader('Location'), 3074 signature = self.signature, 3075 index; 3076 3077 // Check if the location response header differs from the current URL. 3078 // If so, the request was redirected; try loading the requested page. 3079 if ( location && location !== self.previewUrl() ) { 3080 deferred.rejectWith( self, [ 'redirect', location ] ); 3081 return; 3082 } 3083 3084 // Check if the user is not logged in. 3085 if ( '0' === response ) { 3086 self.login( deferred ); 3087 return; 3088 } 3089 3090 // Check for cheaters. 3091 if ( '-1' === response ) { 3092 deferred.rejectWith( self, [ 'cheatin' ] ); 3093 return; 3094 } 3095 3096 // Check for a signature in the request. 3097 index = response.lastIndexOf( signature ); 3098 if ( -1 === index || index < response.lastIndexOf('</html>') ) { 3099 deferred.rejectWith( self, [ 'unsigned' ] ); 3100 return; 3101 } 3102 3103 // Strip the signature from the request. 3104 response = response.slice( 0, index ) + response.slice( index + signature.length ); 3105 3106 // Create the iframe and inject the html content. 3107 self.iframe = $( '<iframe />', { 'title': api.l10n.previewIframeTitle } ).appendTo( self.container ); 3108 self.iframe.attr( 'onmousewheel', '' ); // Workaround for Safari bug. See WP Trac #38149. 3109 3110 // Bind load event after the iframe has been added to the page; 3111 // otherwise it will fire when injected into the DOM. 3112 self.iframe.one( 'load', function() { 3113 loaded = true; 3114 3115 if ( ready ) { 3116 deferred.resolveWith( self ); 3117 } else { 3118 setTimeout( function() { 3119 deferred.rejectWith( self, [ 'ready timeout' ] ); 3120 }, self.sensitivity ); 3121 } 3122 }); 3123 3124 self.targetWindow( self.iframe[0].contentWindow ); 3125 3126 self.targetWindow().document.open(); 3127 self.targetWindow().document.write( response ); 3128 self.targetWindow().document.close(); 3328 previewFrame.iframe.one( 'load', function() { 3329 loaded = true; 3330 3331 if ( ready ) { 3332 deferred.resolveWith( previewFrame, [ readyData ] ); 3333 } else { 3334 setTimeout( function() { 3335 deferred.rejectWith( previewFrame, [ 'ready timeout' ] ); 3336 }, previewFrame.sensitivity ); 3337 } 3129 3338 }); 3130 3339 }, … … 3165 3374 destroy: function() { 3166 3375 api.Messenger.prototype.destroy.call( this ); 3167 this.request.abort(); 3168 3169 if ( this.iframe ) 3376 3377 if ( this.iframe ) { 3170 3378 this.iframe.remove(); 3171 3172 delete this.request; 3379 } 3380 3173 3381 delete this.iframe; 3174 3382 delete this.targetWindow; … … 3177 3385 3178 3386 (function(){ 3179 var uuid = 0; 3180 /** 3181 * Create a universally unique identifier. 3182 * 3183 * @return {int} 3387 var id = 0; 3388 /** 3389 * Return an incremented ID for a preview messenger channel. 3390 * 3391 * This function is named "uuid" for historical reasons, but it is a 3392 * misnomer as it is not an actual UUID, and it is not universally unique. 3393 * This is not to be confused with `api.settings.changeset.uuid`. 3394 * 3395 * @return {string} 3184 3396 */ 3185 3397 api.PreviewFrame.uuid = function() { 3186 return 'preview-' + uuid++;3398 return 'preview-' + String( id++ ); 3187 3399 }; 3188 3400 }()); … … 3210 3422 */ 3211 3423 api.Previewer = api.Messenger.extend({ 3212 refreshBuffer: 250,3424 refreshBuffer: null, // Will get set to api.settings.timeouts.windowRefresh. 3213 3425 3214 3426 /** … … 3218 3430 * @param {string} params.form 3219 3431 * @param {string} params.previewUrl The URL to preview. 3220 * @param {string} params.signature3221 3432 * @param {object} options 3222 3433 */ 3223 3434 initialize: function( params, options ) { 3224 var self= this,3225 rscheme = /^https?/;3226 3227 $.extend( this, options || {} );3228 this.deferred = {3435 var previewer = this, 3436 urlParser = document.createElement( 'a' ); 3437 3438 $.extend( previewer, options || {} ); 3439 previewer.deferred = { 3229 3440 active: $.Deferred() 3230 3441 }; 3231 3442 3232 /* 3233 * Wrap this.refresh to prevent it from hammering the servers: 3234 * 3235 * If refresh is called once and no other refresh requests are 3236 * loading, trigger the request immediately. 3237 * 3238 * If refresh is called while another refresh request is loading, 3239 * debounce the refresh requests: 3240 * 1. Stop the loading request (as it is instantly outdated). 3241 * 2. Trigger the new request once refresh hasn't been called for 3242 * self.refreshBuffer milliseconds. 3243 */ 3244 this.refresh = (function( self ) { 3245 var refresh = self.refresh, 3246 callback = function() { 3247 timeout = null; 3248 refresh.call( self ); 3249 }, 3250 timeout; 3251 3252 return function() { 3253 if ( typeof timeout !== 'number' ) { 3254 if ( self.loading ) { 3255 self.abort(); 3443 // Debounce to prevent hammering server and then wait for any pending update requests. 3444 previewer.refresh = _.debounce( 3445 ( function( originalRefresh ) { 3446 return function() { 3447 var isProcessingComplete, refreshOnceProcessingComplete; 3448 isProcessingComplete = function() { 3449 return 0 === api.state( 'processing' ).get(); 3450 }; 3451 if ( isProcessingComplete() ) { 3452 originalRefresh.call( previewer ); 3256 3453 } else { 3257 return callback(); 3454 refreshOnceProcessingComplete = function() { 3455 if ( isProcessingComplete() ) { 3456 originalRefresh.call( previewer ); 3457 api.state( 'processing' ).unbind( refreshOnceProcessingComplete ); 3458 } 3459 }; 3460 api.state( 'processing' ).bind( refreshOnceProcessingComplete ); 3258 3461 } 3259 } 3260 3261 clearTimeout( timeout ); 3262 timeout = setTimeout( callback, self.refreshBuffer ); 3263 }; 3264 })( this ); 3265 3266 this.container = api.ensure( params.container ); 3267 this.allowedUrls = params.allowedUrls; 3268 this.signature = params.signature; 3462 }; 3463 }( previewer.refresh ) ), 3464 previewer.refreshBuffer 3465 ); 3466 3467 previewer.container = api.ensure( params.container ); 3468 previewer.allowedUrls = params.allowedUrls; 3269 3469 3270 3470 params.url = window.location.href; 3271 3471 3272 api.Messenger.prototype.initialize.call( this, params ); 3273 3274 this.add( 'scheme', this.origin() ).link( this.origin ).setter( function( to ) { 3275 var match = to.match( rscheme ); 3276 return match ? match[0] : ''; 3277 }); 3472 api.Messenger.prototype.initialize.call( previewer, params ); 3473 3474 urlParser.href = previewer.origin(); 3475 previewer.add( 'scheme', urlParser.protocol.replace( /:$/, '' ) ); 3278 3476 3279 3477 // Limit the URL to internal, front-end links. … … 3285 3483 // ssl certs. 3286 3484 3287 this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {3288 var result, urlParser ;3485 previewer.add( 'previewUrl', params.previewUrl ).setter( function( to ) { 3486 var result, urlParser, newPreviewUrl, schemeMatchingPreviewUrl, queryParams; 3289 3487 urlParser = document.createElement( 'a' ); 3290 3488 urlParser.href = to; … … 3295 3493 } 3296 3494 3495 // Remove state query params. 3496 if ( urlParser.search.length > 1 ) { 3497 queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) ); 3498 delete queryParams.customize_changeset_uuid; 3499 delete queryParams.customize_theme; 3500 delete queryParams.customize_messenger_channel; 3501 if ( _.isEmpty( queryParams ) ) { 3502 urlParser.search = ''; 3503 } else { 3504 urlParser.search = $.param( queryParams ); 3505 } 3506 } 3507 3508 newPreviewUrl = urlParser.href; 3509 urlParser.protocol = previewer.scheme.get() + ':'; 3510 schemeMatchingPreviewUrl = urlParser.href; 3511 3297 3512 // Attempt to match the URL to the control frame's scheme 3298 3513 // and check if it's allowed. If not, try the original URL. 3299 $.each( [ to.replace( rscheme, self.scheme() ), to], function( i, url ) {3300 $.each( self.allowedUrls, function( i, allowed ) {3514 $.each( [ schemeMatchingPreviewUrl, newPreviewUrl ], function( i, url ) { 3515 $.each( previewer.allowedUrls, function( i, allowed ) { 3301 3516 var path; 3302 3517 … … 3309 3524 } 3310 3525 }); 3311 if ( result ) 3526 if ( result ) { 3312 3527 return false; 3528 } 3313 3529 }); 3314 3530 … … 3317 3533 }); 3318 3534 3535 previewer.bind( 'ready', previewer.ready ); 3536 3537 // Start listening for keep-alive messages when iframe first loads. 3538 previewer.deferred.active.done( _.bind( previewer.keepPreviewAlive, previewer ) ); 3539 3540 previewer.bind( 'synced', function() { 3541 previewer.send( 'active' ); 3542 } ); 3543 3319 3544 // Refresh the preview when the URL is changed (but not yet). 3320 this.previewUrl.bind( this.refresh ); 3321 3322 this.scroll = 0; 3323 this.bind( 'scroll', function( distance ) { 3324 this.scroll = distance; 3325 }); 3326 3327 // Update the URL when the iframe sends a URL message. 3328 this.bind( 'url', this.previewUrl ); 3545 previewer.previewUrl.bind( previewer.refresh ); 3546 3547 previewer.scroll = 0; 3548 previewer.bind( 'scroll', function( distance ) { 3549 previewer.scroll = distance; 3550 }); 3551 3552 // Update the URL when the iframe sends a URL message, resetting scroll position. If URL is unchanged, then refresh. 3553 previewer.bind( 'url', function( url ) { 3554 var onUrlChange, urlChanged = false; 3555 previewer.scroll = 0; 3556 onUrlChange = function() { 3557 urlChanged = true; 3558 }; 3559 previewer.previewUrl.bind( onUrlChange ); 3560 previewer.previewUrl.set( url ); 3561 previewer.previewUrl.unbind( onUrlChange ); 3562 if ( ! urlChanged ) { 3563 previewer.refresh(); 3564 } 3565 } ); 3329 3566 3330 3567 // Update the document title when the preview changes. 3331 this.bind( 'documentTitle', function ( title ) {3568 previewer.bind( 'documentTitle', function ( title ) { 3332 3569 api.setDocumentTitle( title ); 3333 3570 } ); 3571 }, 3572 3573 /** 3574 * Handle the preview receiving the ready message. 3575 * 3576 * @since 4.7.0 3577 * 3578 * @param {object} data - Data from preview. 3579 * @param {string} data.currentUrl - Current URL. 3580 * @param {object} data.activePanels - Active panels. 3581 * @param {object} data.activeSections Active sections. 3582 * @param {object} data.activeControls Active controls. 3583 * @returns {void} 3584 */ 3585 ready: function( data ) { 3586 var previewer = this, synced = {}, constructs; 3587 3588 synced.settings = api.get(); 3589 if ( 'resolved' !== previewer.deferred.active.state() || previewer.loading ) { 3590 synced.scroll = previewer.scroll; 3591 } 3592 previewer.send( 'sync', synced ); 3593 3594 // Set the previewUrl without causing the url to set the iframe. 3595 if ( data.currentUrl ) { 3596 previewer.previewUrl.unbind( previewer.refresh ); 3597 previewer.previewUrl.set( data.currentUrl ); 3598 previewer.previewUrl.bind( previewer.refresh ); 3599 } 3600 3601 /* 3602 * Walk over all panels, sections, and controls and set their 3603 * respective active states to true if the preview explicitly 3604 * indicates as such. 3605 */ 3606 constructs = { 3607 panel: data.activePanels, 3608 section: data.activeSections, 3609 control: data.activeControls 3610 }; 3611 _( constructs ).each( function ( activeConstructs, type ) { 3612 api[ type ].each( function ( construct, id ) { 3613 var isDynamicallyCreated = _.isUndefined( api.settings[ type + 's' ][ id ] ); 3614 3615 /* 3616 * If the construct was created statically in PHP (not dynamically in JS) 3617 * then consider a missing (undefined) value in the activeConstructs to 3618 * mean it should be deactivated (since it is gone). But if it is 3619 * dynamically created then only toggle activation if the value is defined, 3620 * as this means that the construct was also then correspondingly 3621 * created statically in PHP and the active callback is available. 3622 * Otherwise, dynamically-created constructs should normally have 3623 * their active states toggled in JS rather than from PHP. 3624 */ 3625 if ( ! isDynamicallyCreated || ! _.isUndefined( activeConstructs[ id ] ) ) { 3626 if ( activeConstructs[ id ] ) { 3627 construct.activate(); 3628 } else { 3629 construct.deactivate(); 3630 } 3631 } 3632 } ); 3633 } ); 3634 3635 if ( data.settingValidities ) { 3636 api._handleSettingValidities( { 3637 settingValidities: data.settingValidities, 3638 focusInvalidControl: false 3639 } ); 3640 } 3641 }, 3642 3643 /** 3644 * Keep the preview alive by listening for ready and keep-alive messages. 3645 * 3646 * If a message is not received in the allotted time then the iframe will be set back to the last known valid URL. 3647 * 3648 * @since 4.7.0 3649 * 3650 * @returns {void} 3651 */ 3652 keepPreviewAlive: function keepPreviewAlive() { 3653 var previewer = this, keepAliveTick, timeoutId, handleMissingKeepAlive, scheduleKeepAliveCheck; 3654 3655 /** 3656 * Schedule a preview keep-alive check. 3657 * 3658 * Note that if a page load takes longer than keepAliveCheck milliseconds, 3659 * the keep-alive messages will still be getting sent from the previous 3660 * URL. 3661 */ 3662 scheduleKeepAliveCheck = function() { 3663 timeoutId = setTimeout( handleMissingKeepAlive, api.settings.timeouts.keepAliveCheck ); 3664 }; 3665 3666 /** 3667 * Set the previewerAlive state to true when receiving a message from the preview. 3668 */ 3669 keepAliveTick = function() { 3670 api.state( 'previewerAlive' ).set( true ); 3671 clearTimeout( timeoutId ); 3672 scheduleKeepAliveCheck(); 3673 }; 3674 3675 /** 3676 * Set the previewerAlive state to false if keepAliveCheck milliseconds have transpired without a message. 3677 * 3678 * This is most likely to happen in the case of a connectivity error, or if the theme causes the browser 3679 * to navigate to a non-allowed URL. Setting this state to false will force settings with a postMessage 3680 * transport to use refresh instead, causing the preview frame also to be replaced with the current 3681 * allowed preview URL. 3682 */ 3683 handleMissingKeepAlive = function() { 3684 api.state( 'previewerAlive' ).set( false ); 3685 }; 3686 scheduleKeepAliveCheck(); 3687 3688 previewer.bind( 'ready', keepAliveTick ); 3689 previewer.bind( 'keep-alive', keepAliveTick ); 3334 3690 }, 3335 3691 … … 3349 3705 3350 3706 /** 3351 * Refresh the preview .3707 * Refresh the preview seamlessly. 3352 3708 */ 3353 3709 refresh: function() { 3354 var self= this;3710 var previewer = this; 3355 3711 3356 3712 // Display loading indicator 3357 this.send( 'loading-initiated' ); 3358 3359 this.abort(); 3360 3361 this.loading = new api.PreviewFrame({ 3362 url: this.url(), 3363 previewUrl: this.previewUrl(), 3364 query: this.query() || {}, 3365 container: this.container, 3366 signature: this.signature 3367 }); 3368 3369 this.loading.done( function() { 3370 // 'this' is the loading frame 3371 this.bind( 'synced', function() { 3372 if ( self.preview ) 3373 self.preview.destroy(); 3374 self.preview = this; 3375 delete self.loading; 3376 3377 self.targetWindow( this.targetWindow() ); 3378 self.channel( this.channel() ); 3379 3380 self.deferred.active.resolve(); 3381 self.send( 'active' ); 3382 }); 3383 3384 this.send( 'sync', { 3385 scroll: self.scroll, 3386 settings: api.get() 3387 }); 3388 }); 3389 3390 this.loading.fail( function( reason, location ) { 3391 self.send( 'loading-failed' ); 3392 if ( 'redirect' === reason && location ) { 3393 self.previewUrl( location ); 3394 } 3713 previewer.send( 'loading-initiated' ); 3714 3715 previewer.abort(); 3716 3717 previewer.loading = new api.PreviewFrame({ 3718 url: previewer.url(), 3719 previewUrl: previewer.previewUrl(), 3720 query: previewer.query( { excludeCustomizedSaved: true } ) || {}, 3721 container: previewer.container 3722 }); 3723 3724 previewer.loading.done( function( readyData ) { 3725 var loadingFrame = this, previousPreview, onceSynced; 3726 3727 previousPreview = previewer.preview; 3728 previewer.preview = loadingFrame; 3729 previewer.targetWindow( loadingFrame.targetWindow() ); 3730 previewer.channel( loadingFrame.channel() ); 3731 3732 onceSynced = function() { 3733 loadingFrame.unbind( 'synced', onceSynced ); 3734 if ( previousPreview ) { 3735 previousPreview.destroy(); 3736 } 3737 previewer.deferred.active.resolve(); 3738 delete previewer.loading; 3739 }; 3740 loadingFrame.bind( 'synced', onceSynced ); 3741 3742 // This event will be received directly by the previewer in normal navigation; this is only needed for seamless refresh. 3743 previewer.trigger( 'ready', readyData ); 3744 }); 3745 3746 previewer.loading.fail( function( reason ) { 3747 previewer.send( 'loading-failed' ); 3395 3748 3396 3749 if ( 'logged out' === reason ) { 3397 if ( self.preview ) {3398 self.preview.destroy();3399 delete self.preview;3750 if ( previewer.preview ) { 3751 previewer.preview.destroy(); 3752 delete previewer.preview; 3400 3753 } 3401 3754 3402 self.login().done( self.refresh );3755 previewer.login().done( previewer.refresh ); 3403 3756 } 3404 3757 3405 3758 if ( 'cheatin' === reason ) { 3406 self.cheatin();3759 previewer.cheatin(); 3407 3760 } 3408 3761 }); … … 3464 3817 request = wp.ajax.post( 'customize_refresh_nonces', { 3465 3818 wp_customize: 'on', 3466 theme: api.settings.theme.stylesheet3819 customize_theme: api.settings.theme.stylesheet 3467 3820 }); 3468 3821 … … 3680 4033 } 3681 4034 4035 if ( null === api.PreviewFrame.prototype.sensitivity ) { 4036 api.PreviewFrame.prototype.sensitivity = api.settings.timeouts.previewFrameSensitivity; 4037 } 4038 if ( null === api.Previewer.prototype.refreshBuffer ) { 4039 api.Previewer.prototype.refreshBuffer = api.settings.timeouts.windowRefresh; 4040 } 4041 3682 4042 var parent, 3683 4043 body = $( document.body ), … … 3723 4083 form: '#customize-controls', 3724 4084 previewUrl: api.settings.url.preview, 3725 allowedUrls: api.settings.url.allowed, 3726 signature: 'WP_CUSTOMIZER_SIGNATURE' 4085 allowedUrls: api.settings.url.allowed 3727 4086 }, { 3728 4087 … … 3732 4091 * Build the query to send along with the Preview request. 3733 4092 * 3734 * @return {object} 4093 * @since 4.7.0 Added options param. 4094 * 4095 * @param {object} [options] Options. 4096 * @param {boolean} [options.excludeCustomizedSaved=false] Exclude saved settings in customized response (values pending writing to changeset). 4097 * @return {object} Query vars. 3735 4098 */ 3736 query: function() { 3737 var dirtyCustomized = {}; 3738 api.each( function ( value, key ) { 3739 if ( value._dirty ) { 3740 dirtyCustomized[ key ] = value(); 3741 } 3742 } ); 3743 3744 return { 4099 query: function( options ) { 4100 var queryVars = { 3745 4101 wp_customize: 'on', 3746 theme:api.settings.theme.stylesheet,3747 customized: JSON.stringify( dirtyCustomized ),3748 nonce: this.nonce.preview4102 customize_theme: api.settings.theme.stylesheet, 4103 nonce: this.nonce.preview, 4104 customize_changeset_uuid: api.settings.changeset.uuid 3749 4105 }; 4106 4107 /* 4108 * Exclude customized data if requested especially for calls to requestChangesetUpdate. 4109 * Changeset updates are differential and so it is a performance waste to send all of 4110 * the dirty settings with each update. 4111 */ 4112 queryVars.customized = JSON.stringify( api.dirtyValues( { 4113 unsaved: options && options.excludeCustomizedSaved 4114 } ) ); 4115 4116 return queryVars; 3750 4117 }, 3751 4118 3752 save: function() { 3753 var self = this, 4119 /** 4120 * Save (and publish) the customizer changeset. 4121 * 4122 * Updates to the changeset are transactional. If any of the settings 4123 * are invalid then none of them will be written into the changeset. 4124 * A revision will be made for the changeset post if revisions support 4125 * has been added to the post type. 4126 * 4127 * @param {object} [args] Args. 4128 * @param {string} [args.status=publish] Status. 4129 * @param {string} [args.date] Date, in local time in MySQL format. 4130 * @param {string} [args.title] Title 4131 * 4132 * @returns {jQuery.promise} 4133 */ 4134 save: function( args ) { 4135 var previewer = this, 4136 deferred = $.Deferred(), 4137 changesetStatus = 'publish', 3754 4138 processing = api.state( 'processing' ), 3755 4139 submitWhenDoneProcessing, … … 3759 4143 invalidControls; 3760 4144 3761 body.addClass( 'saving' ); 4145 if ( args && args.status ) { 4146 changesetStatus = args.status; 4147 } 4148 4149 if ( api.state( 'saving' ).get() ) { 4150 deferred.reject( 'already_saving' ); 4151 deferred.promise(); 4152 } 4153 4154 api.state( 'saving' ).set( true ); 3762 4155 3763 4156 function captureSettingModifiedDuringSave( setting ) { … … 3767 4160 3768 4161 submit = function () { 3769 var request, query ;4162 var request, query, settingInvalidities = {}; 3770 4163 3771 4164 /* … … 3778 4171 if ( 'error' === notification.type && ! notification.fromServer ) { 3779 4172 invalidSettings.push( setting.id ); 4173 if ( ! settingInvalidities[ setting.id ] ) { 4174 settingInvalidities[ setting.id ] = {}; 4175 } 4176 settingInvalidities[ setting.id ][ notification.code ] = notification; 3780 4177 } 3781 4178 } ); … … 3784 4181 if ( ! _.isEmpty( invalidControls ) ) { 3785 4182 _.values( invalidControls )[0][0].focus(); 3786 body.removeClass( 'saving' );3787 4183 api.unbind( 'change', captureSettingModifiedDuringSave ); 3788 return; 4184 deferred.rejectWith( previewer, [ 4185 { setting_invalidities: settingInvalidities } 4186 ] ); 4187 api.state( 'saving' ).set( false ); 4188 return deferred.promise(); 3789 4189 } 3790 4190 3791 query = $.extend( self.query(), { 3792 nonce: self.nonce.save 4191 /* 4192 * Note that excludeCustomizedSaved is intentionally false so that the entire 4193 * set of customized data will be included if bypassed changeset update. 4194 */ 4195 query = $.extend( previewer.query( { excludeCustomizedSaved: false } ), { 4196 nonce: previewer.nonce.save, 4197 customize_changeset_status: changesetStatus 3793 4198 } ); 4199 if ( args && args.date ) { 4200 query.customize_changeset_date = args.date; 4201 } 4202 if ( args && args.title ) { 4203 query.customize_changeset_title = args.title; 4204 } 4205 4206 /* 4207 * Note that the dirty customized values will have already been set in the 4208 * changeset and so technically query.customized could be deleted. However, 4209 * it is remaining here to make sure that any settings that got updated 4210 * quietly which may have not triggered an update request will also get 4211 * included in the values that get saved to the changeset. This will ensure 4212 * that values that get injected via the saved event will be included in 4213 * the changeset. This also ensures that setting values that were invalid 4214 * will get re-validated, perhaps in the case of settings that are invalid 4215 * due to dependencies on other settings. 4216 */ 3794 4217 request = wp.ajax.post( 'customize_save', query ); 3795 4218 … … 3800 4223 3801 4224 request.always( function () { 3802 body.removeClass( 'saving');4225 api.state( 'saving' ).set( false ); 3803 4226 saveBtn.prop( 'disabled', false ); 3804 4227 api.unbind( 'change', captureSettingModifiedDuringSave ); … … 3806 4229 3807 4230 request.fail( function ( response ) { 4231 3808 4232 if ( '0' === response ) { 3809 4233 response = 'not_logged_in'; … … 3814 4238 3815 4239 if ( 'invalid_nonce' === response ) { 3816 self.cheatin();4240 previewer.cheatin(); 3817 4241 } else if ( 'not_logged_in' === response ) { 3818 self.preview.iframe.hide();3819 self.login().done( function() {3820 self.save();3821 self.preview.iframe.show();4242 previewer.preview.iframe.hide(); 4243 previewer.login().done( function() { 4244 previewer.save(); 4245 previewer.preview.iframe.show(); 3822 4246 } ); 3823 4247 } … … 3830 4254 } 3831 4255 4256 deferred.rejectWith( previewer, [ response ] ); 3832 4257 api.trigger( 'error', response ); 3833 4258 } ); … … 3835 4260 request.done( function( response ) { 3836 4261 3837 // Clear setting dirty states, if setting wasn't modified while saving.3838 api.each( function( setting ) { 3839 if ( ! modifiedWhileSaving[ setting.id ] ) {3840 setting._dirty = false;3841 }3842 } );3843 3844 api.previewer.send( 'saved', response );4262 previewer.send( 'saved', response ); 4263 4264 api.state( 'changesetStatus' ).set( response.changeset_status ); 4265 if ( 'publish' === response.changeset_status ) { 4266 api.state( 'changesetStatus' ).set( '' ); 4267 api.settings.changeset.uuid = response.next_changeset_uuid; 4268 parent.send( 'changeset-uuid', api.settings.changeset.uuid ); 4269 } 3845 4270 3846 4271 if ( response.setting_validities ) { … … 3851 4276 } 3852 4277 4278 deferred.resolveWith( previewer, [ response ] ); 3853 4279 api.trigger( 'saved', response ); 3854 4280 … … 3872 4298 } 3873 4299 4300 return deferred.promise(); 3874 4301 } 3875 4302 }); … … 3958 4385 api.bind( 'ready', api.reflowPaneContents ); 3959 4386 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) { 3960 var debouncedReflowPaneContents = _.debounce( api.reflowPaneContents, 100);4387 var debouncedReflowPaneContents = _.debounce( api.reflowPaneContents, api.settings.timeouts.reflowPaneContents ); 3961 4388 values.bind( 'add', debouncedReflowPaneContents ); 3962 4389 values.bind( 'change', debouncedReflowPaneContents ); 3963 4390 values.bind( 'remove', debouncedReflowPaneContents ); 3964 4391 } ); 4392 4393 // Save and activated states 4394 (function() { 4395 var state = new api.Values(), 4396 saved = state.create( 'saved' ), 4397 saving = state.create( 'saving' ), 4398 activated = state.create( 'activated' ), 4399 processing = state.create( 'processing' ), 4400 paneVisible = state.create( 'paneVisible' ), 4401 changesetStatus = state.create( 'changesetStatus' ), 4402 previewerAlive = state.create( 'previewerAlive' ), 4403 populateChangesetUuidParam; 4404 4405 state.bind( 'change', function() { 4406 var canSave; 4407 4408 if ( ! activated() ) { 4409 saveBtn.val( api.l10n.activate ); 4410 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel ); 4411 4412 } else if ( '' === changesetStatus.get() && saved() ) { 4413 saveBtn.val( api.l10n.saved ); 4414 closeBtn.find( '.screen-reader-text' ).text( api.l10n.close ); 4415 4416 } else { 4417 saveBtn.val( api.l10n.save ); 4418 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel ); 4419 } 4420 4421 /* 4422 * Save (publish) button should be enabled if saving is not currently happening, 4423 * and if the theme is not active or the changeset exists but is not published. 4424 */ 4425 canSave = ! saving() && ( ! activated() || ! saved() || ( '' !== changesetStatus() && 'publish' !== changesetStatus() ) ); 4426 4427 saveBtn.prop( 'disabled', ! canSave ); 4428 }); 4429 4430 // Set default states. 4431 saved( true ); 4432 saving( false ); 4433 activated( api.settings.theme.active ); 4434 processing( 0 ); 4435 paneVisible( true ); 4436 previewerAlive( true ); 4437 changesetStatus( api.settings.changeset.status ); 4438 4439 api.bind( 'change', function() { 4440 state('saved').set( false ); 4441 }); 4442 4443 saving.bind( function( isSaving ) { 4444 body.toggleClass( 'saving', isSaving ); 4445 } ); 4446 4447 api.bind( 'saved', function( response ) { 4448 state('saved').set( true ); 4449 if ( 'publish' === response.changeset_status ) { 4450 state( 'activated' ).set( true ); 4451 } 4452 }); 4453 4454 activated.bind( function( to ) { 4455 if ( to ) { 4456 api.trigger( 'activated' ); 4457 } 4458 }); 4459 4460 populateChangesetUuidParam = function( isIncluded ) { 4461 var urlParser, queryParams; 4462 urlParser = document.createElement( 'a' ); 4463 urlParser.href = location.href; 4464 queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) ); 4465 if ( isIncluded ) { 4466 if ( queryParams.changeset_uuid === api.settings.changeset.uuid ) { 4467 return; 4468 } 4469 queryParams.changeset_uuid = api.settings.changeset.uuid; 4470 } else { 4471 if ( ! queryParams.changeset_uuid ) { 4472 return; 4473 } 4474 delete queryParams.changeset_uuid; 4475 } 4476 urlParser.search = $.param( queryParams ); 4477 history.replaceState( {}, document.title, urlParser.href ); 4478 }; 4479 4480 if ( history.replaceState ) { 4481 saved.bind( function( isSaved ) { 4482 if ( ! isSaved ) { 4483 populateChangesetUuidParam( true ); 4484 } 4485 } ); 4486 changesetStatus.bind( function( newStatus ) { 4487 populateChangesetUuidParam( '' !== newStatus ); 4488 } ); 4489 } 4490 4491 // Expose states to the API. 4492 api.state = state; 4493 }()); 3965 4494 3966 4495 // Check if preview url is valid and load the preview frame. … … 3970 4499 api.previewer.previewUrl( api.settings.url.home ); 3971 4500 } 3972 3973 // Save and activated states3974 (function() {3975 var state = new api.Values(),3976 saved = state.create( 'saved' ),3977 activated = state.create( 'activated' ),3978 processing = state.create( 'processing' ),3979 paneVisible = state.create( 'paneVisible' );3980 3981 state.bind( 'change', function() {3982 if ( ! activated() ) {3983 saveBtn.val( api.l10n.activate ).prop( 'disabled', false );3984 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );3985 3986 } else if ( saved() ) {3987 saveBtn.val( api.l10n.saved ).prop( 'disabled', true );3988 closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );3989 3990 } else {3991 saveBtn.val( api.l10n.save ).prop( 'disabled', false );3992 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );3993 }3994 });3995 3996 // Set default states.3997 saved( true );3998 activated( api.settings.theme.active );3999 processing( 0 );4000 paneVisible( true );4001 4002 api.bind( 'change', function() {4003 state('saved').set( false );4004 });4005 4006 api.bind( 'saved', function() {4007 state('saved').set( true );4008 state('activated').set( true );4009 });4010 4011 activated.bind( function( to ) {4012 if ( to ) {4013 api.trigger( 'activated' );4014 }4015 });4016 4017 // Expose states to the API.4018 api.state = state;4019 }());4020 4501 4021 4502 // Button bindings. … … 4170 4651 4171 4652 // Prompt user with AYS dialog if leaving the Customizer with unsaved changes 4172 $( window ).on( 'beforeunload ', function () {4653 $( window ).on( 'beforeunload.customize-confirm', function () { 4173 4654 if ( ! api.state( 'saved' )() ) { 4174 4655 setTimeout( function() { … … 4190 4671 parent.send( 'title', newTitle ); 4191 4672 }); 4673 4674 parent.send( 'changeset-uuid', api.settings.changeset.uuid ); 4192 4675 4193 4676 // Initialize the connection with the parent frame. … … 4293 4776 }); 4294 4777 4778 // Autosave changeset. 4779 ( function() { 4780 var timeoutId, updateChangesetWithReschedule, scheduleChangesetUpdate, updatePending = false; 4781 4782 /** 4783 * Request changeset update and then re-schedule the next changeset update time. 4784 * 4785 * @private 4786 */ 4787 updateChangesetWithReschedule = function() { 4788 if ( ! updatePending ) { 4789 updatePending = true; 4790 api.requestChangesetUpdate().always( function() { 4791 updatePending = false; 4792 } ); 4793 } 4794 scheduleChangesetUpdate(); 4795 }; 4796 4797 /** 4798 * Schedule changeset update. 4799 * 4800 * @private 4801 */ 4802 scheduleChangesetUpdate = function() { 4803 clearTimeout( timeoutId ); 4804 timeoutId = setTimeout( function() { 4805 updateChangesetWithReschedule(); 4806 }, api.settings.timeouts.changesetAutoSave ); 4807 }; 4808 4809 // Start auto-save interval for updating changeset. 4810 scheduleChangesetUpdate(); 4811 4812 // Save changeset when focus removed from window. 4813 $( window ).on( 'blur.wp-customize-changeset-update', function() { 4814 updateChangesetWithReschedule(); 4815 } ); 4816 4817 // Save changeset before unloading window. 4818 $( window ).on( 'beforeunload.wp-customize-changeset-update', function() { 4819 updateChangesetWithReschedule(); 4820 } ); 4821 } ()); 4822 4295 4823 api.trigger( 'ready' ); 4296 4824 }); -
trunk/src/wp-admin/js/customize-widgets.js
r38709 r38810 1155 1155 params.wp_customize = 'on'; 1156 1156 params.nonce = api.settings.nonce['update-widget']; 1157 params. theme = api.settings.theme.stylesheet;1157 params.customize_theme = api.settings.theme.stylesheet; 1158 1158 params.customized = wp.customize.previewer.query().customized; 1159 1159 -
trunk/src/wp-includes/admin-bar.php
r38708 r38810 367 367 * 368 368 * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance. 369 * @global WP_Customize_Manager $wp_customize 369 370 */ 370 371 function wp_admin_bar_customize_menu( $wp_admin_bar ) { 372 global $wp_customize; 373 371 374 // Don't show for users who can't access the customizer or when in the admin. 372 375 if ( ! current_user_can( 'customize' ) || is_admin() ) { … … 374 377 } 375 378 379 // Don't show if the user cannot edit a given customize_changeset post currently being previewed. 380 if ( is_customize_preview() && $wp_customize->changeset_post_id() && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $wp_customize->changeset_post_id() ) ) { 381 return; 382 } 383 376 384 $current_url = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; 385 if ( is_customize_preview() && $wp_customize->changeset_uuid() ) { 386 $current_url = remove_query_arg( 'customize_changeset_uuid', $current_url ); 387 } 388 377 389 $customize_url = add_query_arg( 'url', urlencode( $current_url ), wp_customize_url() ); 390 if ( is_customize_preview() ) { 391 $customize_url = add_query_arg( array( 'changeset_uuid' => $wp_customize->changeset_uuid() ), $customize_url ); 392 } 378 393 379 394 $wp_admin_bar->add_menu( array( -
trunk/src/wp-includes/class-wp-customize-manager.php
r38765 r38810 131 131 132 132 /** 133 * Return value of check_ajax_referer() in customize_preview_init() method.134 *135 * @since 3.5.0136 * @access protected137 * @var false|int138 */139 protected $nonce_tick;140 141 /**142 133 * Panel types that may be rendered from JS templates. 143 134 * … … 194 185 195 186 /** 187 * Messenger channel. 188 * 189 * @since 4.7.0 190 * @access protected 191 * @var string 192 */ 193 protected $messenger_channel; 194 195 /** 196 196 * Unsanitized values for Customize Settings parsed from $_POST['customized']. 197 197 * … … 201 201 202 202 /** 203 * Changeset UUID. 204 * 205 * @since 4.7.0 206 * @access private 207 * @var string 208 */ 209 private $_changeset_uuid; 210 211 /** 212 * Changeset post ID. 213 * 214 * @since 4.7.0 215 * @access private 216 * @var int|false 217 */ 218 private $_changeset_post_id; 219 220 /** 221 * Changeset data loaded from a customize_changeset post. 222 * 223 * @since 4.7.0 224 * @access private 225 * @var array 226 */ 227 private $_changeset_data; 228 229 /** 203 230 * Constructor. 204 231 * 205 232 * @since 3.4.0 206 */ 207 public function __construct() { 233 * @since 4.7.0 Added $args param. 234 * 235 * @param array $args { 236 * Args. 237 * 238 * @type string $changeset_uuid Changeset UUID, the post_name for the customize_changeset post containing the customized state. Defaults to new UUID. 239 * @type string $theme Theme to be previewed (for theme switch). Defaults to customize_theme or theme query params. 240 * @type string $messenger_channel Messenger channel. Defaults to customize_messenger_channel query param. 241 * } 242 */ 243 public function __construct( $args = array() ) { 244 245 $args = array_merge( 246 array_fill_keys( array( 'changeset_uuid', 'theme', 'messenger_channel' ), null ), 247 $args 248 ); 249 250 // Note that the UUID format will be validated in the setup_theme() method. 251 if ( ! isset( $args['changeset_uuid'] ) ) { 252 $args['changeset_uuid'] = wp_generate_uuid4(); 253 } 254 255 // The theme and messenger_channel should be supplied via $args, but they are also looked at in the $_REQUEST global here for back-compat. 256 if ( ! isset( $args['theme'] ) ) { 257 if ( isset( $_REQUEST['customize_theme'] ) ) { 258 $args['theme'] = wp_unslash( $_REQUEST['customize_theme'] ); 259 } elseif ( isset( $_REQUEST['theme'] ) ) { // Deprecated. 260 $args['theme'] = wp_unslash( $_REQUEST['theme'] ); 261 } 262 } 263 if ( ! isset( $args['messenger_channel'] ) && isset( $_REQUEST['customize_messenger_channel'] ) ) { 264 $args['messenger_channel'] = sanitize_key( wp_unslash( $_REQUEST['customize_messenger_channel'] ) ); 265 } 266 267 $this->original_stylesheet = get_stylesheet(); 268 $this->theme = wp_get_theme( $args['theme'] ); 269 $this->messenger_channel = $args['messenger_channel']; 270 $this->_changeset_uuid = $args['changeset_uuid']; 271 208 272 require_once( ABSPATH . WPINC . '/class-wp-customize-setting.php' ); 209 273 require_once( ABSPATH . WPINC . '/class-wp-customize-panel.php' ); … … 272 336 } 273 337 274 add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );275 276 338 add_action( 'setup_theme', array( $this, 'setup_theme' ) ); 277 339 add_action( 'wp_loaded', array( $this, 'wp_loaded' ) ); 278 279 // Run wp_redirect_status late to make sure we override the status last.280 add_action( 'wp_redirect_status', array( $this, 'wp_redirect_status' ), 1000 );281 340 282 341 // Do not spawn cron (especially the alternate cron) while running the Customizer. … … 341 400 */ 342 401 protected function wp_die( $ajax_message, $message = null ) { 343 if ( $this->doing_ajax() || isset( $_POST['customized'] )) {402 if ( $this->doing_ajax() ) { 344 403 wp_die( $ajax_message ); 345 404 } … … 349 408 } 350 409 410 if ( $this->messenger_channel ) { 411 ob_start(); 412 wp_enqueue_scripts(); 413 wp_print_scripts( array( 'customize-base' ) ); 414 415 $settings = array( 416 'messengerArgs' => array( 417 'channel' => $this->messenger_channel, 418 'url' => wp_customize_url(), 419 ), 420 'error' => $ajax_message, 421 ); 422 ?> 423 <script> 424 ( function( api, settings ) { 425 var preview = new api.Messenger( settings.messengerArgs ); 426 preview.send( 'iframe-loading-error', settings.error ); 427 } )( wp.customize, <?php echo wp_json_encode( $settings ) ?> ); 428 </script> 429 <?php 430 $message .= ob_get_clean(); 431 } 432 351 433 wp_die( $message ); 352 434 } … … 356 438 * 357 439 * @since 3.4.0 358 * 359 * @return string 440 * @deprecated 4.7.0 441 * 442 * @return callable Die handler. 360 443 */ 361 444 public function wp_die_handler() { 445 _deprecated_function( __METHOD__, '4.7.0' ); 446 362 447 if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) { 363 448 return '_ajax_wp_die_handler'; … … 375 460 */ 376 461 public function setup_theme() { 377 send_origin_headers(); 378 379 $doing_ajax_or_is_customized = ( $this->doing_ajax() || isset( $_POST['customized'] ) ); 380 if ( is_admin() && ! $doing_ajax_or_is_customized ) { 381 auth_redirect(); 382 } elseif ( $doing_ajax_or_is_customized && ! is_user_logged_in() ) { 383 $this->wp_die( 0, __( 'You must be logged in to complete this action.' ) ); 384 } 385 386 show_admin_bar( false ); 387 388 if ( ! current_user_can( 'customize' ) ) { 389 $this->wp_die( -1, __( 'Sorry, you are not allowed to customize this site.' ) ); 390 } 391 392 $this->original_stylesheet = get_stylesheet(); 393 394 $this->theme = wp_get_theme( isset( $_REQUEST['theme'] ) ? $_REQUEST['theme'] : null ); 462 global $pagenow; 463 464 // Check permissions for customize.php access since this method is called before customize.php can run any code, 465 if ( 'customize.php' === $pagenow && ! current_user_can( 'customize' ) ) { 466 if ( ! is_user_logged_in() ) { 467 auth_redirect(); 468 } else { 469 wp_die( 470 '<h1>' . __( 'Cheatin’ uh?' ) . '</h1>' . 471 '<p>' . __( 'Sorry, you are not allowed to customize this site.' ) . '</p>', 472 403 473 ); 474 } 475 return; 476 } 477 478 if ( ! preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $this->_changeset_uuid ) ) { 479 $this->wp_die( -1, __( 'Invalid changeset UUID' ) ); 480 } 481 482 /* 483 * If unauthenticated then require a valid changeset UUID to load the preview. 484 * In this way, the UUID serves as a secret key. If the messenger channel is present, 485 * then send unauthenticated code to prompt re-auth. 486 */ 487 if ( ! current_user_can( 'customize' ) && ! $this->changeset_post_id() ) { 488 $this->wp_die( $this->messenger_channel ? 0 : -1, __( 'Non-existent changeset UUID.' ) ); 489 } 490 491 if ( ! headers_sent() ) { 492 send_origin_headers(); 493 } 494 495 // Hide the admin bar if we're embedded in the customizer iframe. 496 if ( $this->messenger_channel ) { 497 show_admin_bar( false ); 498 } 395 499 396 500 if ( $this->is_theme_active() ) { … … 508 612 509 613 /** 614 * Get the changeset UUID. 615 * 616 * @since 4.7.0 617 * @access public 618 * 619 * @return string UUID. 620 */ 621 public function changeset_uuid() { 622 return $this->_changeset_uuid; 623 } 624 625 /** 510 626 * Get the theme being customized. 511 627 * … … 604 720 do_action( 'customize_register', $this ); 605 721 606 if ( $this->is_preview() && ! is_admin() ) 722 /* 723 * Note that settings must be previewed here even outside the customizer preview 724 * and also in the customizer pane itself. This is to enable loading an existing 725 * changeset into the customizer. Previewing the settings only has to be prevented 726 * in the case of a customize_save action because then update_option() 727 * may short-circuit because it will detect that there are no changes to 728 * make. 729 */ 730 if ( ! $this->doing_ajax( 'customize_save' ) ) { 731 foreach ( $this->settings as $setting ) { 732 $setting->preview(); 733 } 734 } 735 736 if ( $this->is_preview() && ! is_admin() ) { 607 737 $this->customize_preview_init(); 738 } 608 739 } 609 740 … … 615 746 * 616 747 * @since 3.4.0 617 * 618 * @param $status 748 * @deprecated 4.7.0 749 * 750 * @param int $status Status. 619 751 * @return int 620 752 */ 621 753 public function wp_redirect_status( $status ) { 622 if ( $this->is_preview() && ! is_admin() ) 754 _deprecated_function( __FUNCTION__, '4.7.0' ); 755 756 if ( $this->is_preview() && ! is_admin() ) { 623 757 return 200; 758 } 624 759 625 760 return $status; … … 627 762 628 763 /** 629 * Parse the incoming $_POST['customized'] JSON data and store the unsanitized 630 * settings for subsequent post_value() lookups. 764 * Find the changeset post ID for a given changeset UUID. 765 * 766 * @since 4.7.0 767 * @access public 768 * 769 * @param string $uuid Changeset UUID. 770 * @return int|null Returns post ID on success and null on failure. 771 */ 772 public function find_changeset_post_id( $uuid ) { 773 $cache_group = 'customize_changeset_post'; 774 $changeset_post_id = wp_cache_get( $uuid, $cache_group ); 775 if ( $changeset_post_id && 'customize_changeset' === get_post_type( $changeset_post_id ) ) { 776 return $changeset_post_id; 777 } 778 779 $changeset_post_query = new WP_Query( array( 780 'post_type' => 'customize_changeset', 781 'post_status' => get_post_stati(), 782 'name' => $uuid, 783 'number' => 1, 784 'no_found_rows' => true, 785 'cache_results' => true, 786 'update_post_meta_cache' => false, 787 'update_term_meta_cache' => false, 788 ) ); 789 if ( ! empty( $changeset_post_query->posts ) ) { 790 // Note: 'fields'=>'ids' is not being used in order to cache the post object as it will be needed. 791 $changeset_post_id = $changeset_post_query->posts[0]->ID; 792 wp_cache_set( $this->_changeset_uuid, $changeset_post_id, $cache_group ); 793 return $changeset_post_id; 794 } 795 796 return null; 797 } 798 799 /** 800 * Get the changeset post id for the loaded changeset. 801 * 802 * @since 4.7.0 803 * @access public 804 * 805 * @return int|null Post ID on success or null if there is no post yet saved. 806 */ 807 public function changeset_post_id() { 808 if ( ! isset( $this->_changeset_post_id ) ) { 809 $post_id = $this->find_changeset_post_id( $this->_changeset_uuid ); 810 if ( ! $post_id ) { 811 $post_id = false; 812 } 813 $this->_changeset_post_id = $post_id; 814 } 815 if ( false === $this->_changeset_post_id ) { 816 return null; 817 } 818 return $this->_changeset_post_id; 819 } 820 821 /** 822 * Get the data stored in a changeset post. 823 * 824 * @since 4.7.0 825 * @access protected 826 * 827 * @param int $post_id Changeset post ID. 828 * @return array|WP_Error Changeset data or WP_Error on error. 829 */ 830 protected function get_changeset_post_data( $post_id ) { 831 if ( ! $post_id ) { 832 return new WP_Error( 'empty_post_id' ); 833 } 834 $changeset_post = get_post( $post_id ); 835 if ( ! $changeset_post ) { 836 return new WP_Error( 'missing_post' ); 837 } 838 if ( 'customize_changeset' !== $changeset_post->post_type ) { 839 return new WP_Error( 'wrong_post_type' ); 840 } 841 $changeset_data = json_decode( $changeset_post->post_content, true ); 842 if ( function_exists( 'json_last_error' ) && json_last_error() ) { 843 return new WP_Error( 'json_parse_error', '', json_last_error() ); 844 } 845 if ( ! is_array( $changeset_data ) ) { 846 return new WP_Error( 'expected_array' ); 847 } 848 return $changeset_data; 849 } 850 851 /** 852 * Get changeset data. 853 * 854 * @since 4.7.0 855 * @access public 856 * 857 * @return array Changeset data. 858 */ 859 public function changeset_data() { 860 if ( isset( $this->_changeset_data ) ) { 861 return $this->_changeset_data; 862 } 863 $changeset_post_id = $this->changeset_post_id(); 864 if ( ! $changeset_post_id ) { 865 $this->_changeset_data = array(); 866 } else { 867 $data = $this->get_changeset_post_data( $changeset_post_id ); 868 if ( ! is_wp_error( $data ) ) { 869 $this->_changeset_data = $data; 870 } else { 871 $this->_changeset_data = array(); 872 } 873 } 874 return $this->_changeset_data; 875 } 876 877 /** 878 * Get dirty pre-sanitized setting values in the current customized state. 879 * 880 * The returned array consists of a merge of three sources: 881 * 1. If the theme is not currently active, then the base array is any stashed 882 * theme mods that were modified previously but never published. 883 * 2. The values from the current changeset, if it exists. 884 * 3. If the user can customize, the values parsed from the incoming 885 * `$_POST['customized']` JSON data. 886 * 4. Any programmatically-set post values via `WP_Customize_Manager::set_post_value()`. 887 * 888 * The name "unsanitized_post_values" is a carry-over from when the customized 889 * state was exclusively sourced from `$_POST['customized']`. Nevertheless, 890 * the value returned will come from the current changeset post and from the 891 * incoming post data. 631 892 * 632 893 * @since 4.1.1 633 * 894 * @since 4.7.0 Added $args param and merging with changeset values and stashed theme mods. 895 * 896 * @param array $args { 897 * Args. 898 * 899 * @type bool $exclude_changeset Whether the changeset values should also be excluded. Defaults to false. 900 * @type bool $exclude_post_data Whether the post input values should also be excluded. Defaults to false when lacking the customize capability. 901 * } 634 902 * @return array 635 903 */ 636 public function unsanitized_post_values() { 637 if ( ! isset( $this->_post_values ) ) { 638 if ( isset( $_POST['customized'] ) ) { 639 $this->_post_values = json_decode( wp_unslash( $_POST['customized'] ), true ); 640 } 641 if ( empty( $this->_post_values ) ) { // if not isset or if JSON error 642 $this->_post_values = array(); 643 } 644 } 645 if ( empty( $this->_post_values ) ) { 646 return array(); 647 } else { 648 return $this->_post_values; 649 } 650 } 651 652 /** 653 * Returns the sanitized value for a given setting from the request's POST data. 904 public function unsanitized_post_values( $args = array() ) { 905 $args = array_merge( 906 array( 907 'exclude_changeset' => false, 908 'exclude_post_data' => ! current_user_can( 'customize' ), 909 ), 910 $args 911 ); 912 913 $values = array(); 914 915 // Let default values be from the stashed theme mods if doing a theme switch and if no changeset is present. 916 if ( ! $this->is_theme_active() ) { 917 $stashed_theme_mods = get_option( 'customize_stashed_theme_mods' ); 918 $stylesheet = $this->get_stylesheet(); 919 if ( isset( $stashed_theme_mods[ $stylesheet ] ) ) { 920 $values = array_merge( $values, wp_list_pluck( $stashed_theme_mods[ $stylesheet ], 'value' ) ); 921 } 922 } 923 924 if ( ! $args['exclude_changeset'] ) { 925 foreach ( $this->changeset_data() as $setting_id => $setting_params ) { 926 if ( ! array_key_exists( 'value', $setting_params ) ) { 927 continue; 928 } 929 if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) { 930 931 // Ensure that theme mods values are only used if they were saved under the current theme. 932 $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/'; 933 if ( preg_match( $namespace_pattern, $setting_id, $matches ) && $this->get_stylesheet() === $matches['stylesheet'] ) { 934 $values[ $matches['setting_id'] ] = $setting_params['value']; 935 } 936 } else { 937 $values[ $setting_id ] = $setting_params['value']; 938 } 939 } 940 } 941 942 if ( ! $args['exclude_post_data'] ) { 943 if ( ! isset( $this->_post_values ) ) { 944 if ( isset( $_POST['customized'] ) ) { 945 $post_values = json_decode( wp_unslash( $_POST['customized'] ), true ); 946 } else { 947 $post_values = array(); 948 } 949 if ( is_array( $post_values ) ) { 950 $this->_post_values = $post_values; 951 } else { 952 $this->_post_values = array(); 953 } 954 } 955 $values = array_merge( $values, $this->_post_values ); 956 } 957 return $values; 958 } 959 960 /** 961 * Returns the sanitized value for a given setting from the current customized state. 962 * 963 * The name "post_value" is a carry-over from when the customized state was exclusively 964 * sourced from `$_POST['customized']`. Nevertheless, the value returned will come 965 * from the current changeset post and from the incoming post data. 654 966 * 655 967 * @since 3.4.0 … … 685 997 686 998 /** 687 * Override a setting's (unsanitized) value as found in any incoming $_POST['customized']. 999 * Override a setting's value in the current customized state. 1000 * 1001 * The name "post_value" is a carry-over from when the customized state was 1002 * exclusively sourced from `$_POST['customized']`. 688 1003 * 689 1004 * @since 4.2.0 … … 694 1009 */ 695 1010 public function set_post_value( $setting_id, $value ) { 696 $this->unsanitized_post_values(); 1011 $this->unsanitized_post_values(); // Populate _post_values from $_POST['customized']. 697 1012 $this->_post_values[ $setting_id ] = $value; 698 1013 … … 734 1049 */ 735 1050 public function customize_preview_init() { 736 $this->nonce_tick = check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'nonce' ); 1051 1052 /* 1053 * Now that Customizer previews are loaded into iframes via GET requests 1054 * and natural URLs with transaction UUIDs added, we need to ensure that 1055 * the responses are never cached by proxies. In practice, this will not 1056 * be needed if the user is logged-in anyway. But if anonymous access is 1057 * allowed then the auth cookies would not be sent and WordPress would 1058 * not send no-cache headers by default. 1059 */ 1060 if ( ! headers_sent() ) { 1061 nocache_headers(); 1062 header( 'X-Robots: noindex, nofollow, noarchive' ); 1063 } 1064 add_action( 'wp_head', 'wp_no_robots' ); 1065 add_filter( 'wp_headers', array( $this, 'filter_iframe_security_headers' ) ); 1066 1067 /* 1068 * If preview is being served inside the customizer preview iframe, and 1069 * if the user doesn't have customize capability, then it is assumed 1070 * that the user's session has expired and they need to re-authenticate. 1071 */ 1072 if ( $this->messenger_channel && ! current_user_can( 'customize' ) ) { 1073 $this->wp_die( -1, __( 'Unauthorized. You may remove the customize_messenger_channel param to preview as frontend.' ) ); 1074 return; 1075 } 737 1076 738 1077 $this->prepare_controls(); 739 1078 1079 add_filter( 'wp_redirect', array( $this, 'add_state_query_params' ) ); 1080 740 1081 wp_enqueue_script( 'customize-preview' ); 741 add_action( 'wp', array( $this, 'customize_preview_override_404_status' ) );742 add_action( 'wp_head', array( $this, 'customize_preview_base' ) );743 1082 add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) ); 744 1083 add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 ); 745 add_action( 'shutdown', array( $this, 'customize_preview_signature' ), 1000 );746 add_filter( 'wp_die_handler', array( $this, 'remove_preview_signature' ) );747 748 foreach ( $this->settings as $setting ) {749 $setting->preview();750 }751 1084 752 1085 /** … … 762 1095 763 1096 /** 1097 * Filter the X-Frame-Options and Content-Security-Policy headers to ensure frontend can load in customizer. 1098 * 1099 * @since 4.7.0 1100 * @access public 1101 * 1102 * @param array $headers Headers. 1103 * @return array Headers. 1104 */ 1105 public function filter_iframe_security_headers( $headers ) { 1106 $customize_url = admin_url( 'customize.php' ); 1107 $headers['X-Frame-Options'] = 'ALLOW-FROM ' . $customize_url; 1108 $headers['Content-Security-Policy'] = 'frame-ancestors ' . preg_replace( '#^(\w+://[^/]+).+?$#', '$1', $customize_url ); 1109 return $headers; 1110 } 1111 1112 /** 1113 * Add customize state query params to a given URL if preview is allowed. 1114 * 1115 * @since 4.7.0 1116 * @access public 1117 * @see wp_redirect() 1118 * @see WP_Customize_Manager::get_allowed_url() 1119 * 1120 * @param string $url URL. 1121 * @return string URL. 1122 */ 1123 public function add_state_query_params( $url ) { 1124 $parsed_original_url = wp_parse_url( $url ); 1125 $is_allowed = false; 1126 foreach ( $this->get_allowed_urls() as $allowed_url ) { 1127 $parsed_allowed_url = wp_parse_url( $allowed_url ); 1128 $is_allowed = ( 1129 $parsed_allowed_url['scheme'] === $parsed_original_url['scheme'] 1130 && 1131 $parsed_allowed_url['host'] === $parsed_original_url['host'] 1132 && 1133 0 === strpos( $parsed_original_url['path'], $parsed_allowed_url['path'] ) 1134 ); 1135 if ( $is_allowed ) { 1136 break; 1137 } 1138 } 1139 1140 if ( $is_allowed ) { 1141 $query_params = array( 1142 'customize_changeset_uuid' => $this->changeset_uuid(), 1143 ); 1144 if ( ! $this->is_theme_active() ) { 1145 $query_params['customize_theme'] = $this->get_stylesheet(); 1146 } 1147 if ( $this->messenger_channel ) { 1148 $query_params['customize_messenger_channel'] = $this->messenger_channel; 1149 } 1150 $url = add_query_arg( $query_params, $url ); 1151 } 1152 1153 return $url; 1154 } 1155 1156 /** 764 1157 * Prevent sending a 404 status when returning the response for the customize 765 1158 * preview, since it causes the jQuery Ajax to fail. Send 200 instead. 766 1159 * 767 1160 * @since 4.0.0 1161 * @deprecated 4.7.0 768 1162 * @access public 769 1163 */ 770 1164 public function customize_preview_override_404_status() { 771 if ( is_404() ) { 772 status_header( 200 ); 773 } 1165 _deprecated_function( __METHOD__, '4.7.0' ); 774 1166 } 775 1167 … … 778 1170 * 779 1171 * @since 3.4.0 1172 * @deprecated 4.7.0 780 1173 */ 781 1174 public function customize_preview_base() { 782 ?><base href="<?php echo home_url( '/' ); ?>" /><?php1175 _deprecated_function( __METHOD__, '4.7.0' ); 783 1176 } 784 1177 … … 810 1203 pointer-events: none !important; 811 1204 } 1205 form.customize-unpreviewable, 1206 form.customize-unpreviewable input, 1207 form.customize-unpreviewable select, 1208 form.customize-unpreviewable button, 1209 a.customize-unpreviewable, 1210 area.customize-unpreviewable { 1211 cursor: not-allowed !important; 1212 } 812 1213 </style><?php 813 1214 } … … 819 1220 */ 820 1221 public function customize_preview_settings() { 821 $setting_validities = $this->validate_setting_values( $this->unsanitized_post_values() ); 1222 $post_values = $this->unsanitized_post_values( array( 'exclude_changeset' => true ) ); 1223 $setting_validities = $this->validate_setting_values( $post_values ); 822 1224 $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities ); 823 1225 1226 // Note that the REQUEST_URI is not passed into home_url() since this breaks subdirectory installs. 1227 $self_url = empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ); 1228 $state_query_params = array( 1229 'customize_theme', 1230 'customize_changeset_uuid', 1231 'customize_messenger_channel', 1232 ); 1233 $self_url = remove_query_arg( $state_query_params, $self_url ); 1234 1235 $allowed_urls = $this->get_allowed_urls(); 1236 $allowed_hosts = array(); 1237 foreach ( $allowed_urls as $allowed_url ) { 1238 $parsed = wp_parse_url( $allowed_url ); 1239 if ( empty( $parsed['host'] ) ) { 1240 continue; 1241 } 1242 $host = $parsed['host']; 1243 if ( ! empty( $parsed['port'] ) ) { 1244 $host .= ':' . $parsed['port']; 1245 } 1246 $allowed_hosts[] = $host; 1247 } 824 1248 $settings = array( 1249 'changeset' => array( 1250 'uuid' => $this->_changeset_uuid, 1251 ), 1252 'timeouts' => array( 1253 'selectiveRefresh' => 250, 1254 'keepAliveSend' => 1000, 1255 ), 825 1256 'theme' => array( 826 1257 'stylesheet' => $this->get_stylesheet(), … … 828 1259 ), 829 1260 'url' => array( 830 'self' => empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ), 1261 'self' => $self_url, 1262 'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ), 1263 'allowedHosts' => array_unique( $allowed_hosts ), 1264 'isCrossDomain' => $this->is_cross_domain(), 831 1265 ), 832 'channel' => wp_unslash( $_POST['customize_messenger_channel'] ),1266 'channel' => $this->messenger_channel, 833 1267 'activePanels' => array(), 834 1268 'activeSections' => array(), 835 1269 'activeControls' => array(), 836 1270 'settingValidities' => $exported_setting_validities, 837 'nonce' => $this->get_nonces(),1271 'nonce' => current_user_can( 'customize' ) ? $this->get_nonces() : array(), 838 1272 'l10n' => array( 839 1273 'shiftClickToEdit' => __( 'Shift-click to edit this element.' ), 1274 'linkUnpreviewable' => __( 'This link is not live-previewable.' ), 1275 'formUnpreviewable' => __( 'This form is not live-previewable.' ), 840 1276 ), 841 '_dirty' => array_keys( $ this->unsanitized_post_values()),1277 '_dirty' => array_keys( $post_values ), 842 1278 ); 843 1279 … … 893 1329 * 894 1330 * @since 3.4.0 1331 * @deprecated 4.7.0 895 1332 */ 896 1333 public function customize_preview_signature() { 897 echo 'WP_CUSTOMIZER_SIGNATURE';1334 _deprecated_function( __METHOD__, '4.7.0' ); 898 1335 } 899 1336 … … 902 1339 * 903 1340 * @since 3.4.0 1341 * @deprecated 4.7.0 904 1342 * 905 1343 * @param mixed $return Value passed through for {@see 'wp_die_handler'} filter. … … 907 1345 */ 908 1346 public function remove_preview_signature( $return = null ) { 909 remove_action( 'shutdown', array( $this, 'customize_preview_signature' ), 1000);1347 _deprecated_function( __METHOD__, '4.7.0' ); 910 1348 911 1349 return $return; … … 994 1432 * 995 1433 * @param array $setting_values Mapping of setting IDs to values to validate and sanitize. 1434 * @param array $options { 1435 * Options. 1436 * 1437 * @type bool $validate_existence Whether a setting's existence will be checked. 1438 * @type bool $validate_capability Whether the setting capability will be checked. 1439 * } 996 1440 * @return array Mapping of setting IDs to return value of validate method calls, either `true` or `WP_Error`. 997 1441 */ 998 public function validate_setting_values( $setting_values ) { 1442 public function validate_setting_values( $setting_values, $options = array() ) { 1443 $options = wp_parse_args( $options, array( 1444 'validate_capability' => false, 1445 'validate_existence' => false, 1446 ) ); 1447 999 1448 $validities = array(); 1000 1449 foreach ( $setting_values as $setting_id => $unsanitized_value ) { 1001 1450 $setting = $this->get_setting( $setting_id ); 1002 if ( ! $setting || is_null( $unsanitized_value ) ) { 1451 if ( ! $setting ) { 1452 if ( $options['validate_existence'] ) { 1453 $validities[ $setting_id ] = new WP_Error( 'unrecognized', __( 'Setting does not exist or is unrecognized.' ) ); 1454 } 1003 1455 continue; 1004 1456 } 1005 $validity = $setting->validate( $unsanitized_value ); 1457 if ( is_null( $unsanitized_value ) ) { 1458 continue; 1459 } 1460 if ( $options['validate_capability'] && ! current_user_can( $setting->capability ) ) { 1461 $validity = new WP_Error( 'unauthorized', __( 'Unauthorized to modify setting due to capability.' ) ); 1462 } else { 1463 $validity = $setting->validate( $unsanitized_value ); 1464 } 1006 1465 if ( ! is_wp_error( $validity ) ) { 1007 1466 /** This filter is documented in wp-includes/class-wp-customize-setting.php */ … … 1057 1516 1058 1517 /** 1059 * Switch the theme and trigger the save() method on each setting. 1060 * 1061 * @since 3.4.0 1518 * Handle customize_save WP Ajax request to save/update a changeset. 1519 * 1520 * @since 3.4.0 1521 * @since 4.7.0 The semantics of this method have changed to update a changeset, optionally to also change the status and other attributes. 1062 1522 */ 1063 1523 public function save() { 1524 if ( ! is_user_logged_in() ) { 1525 wp_send_json_error( 'unauthenticated' ); 1526 } 1527 1064 1528 if ( ! $this->is_preview() ) { 1065 1529 wp_send_json_error( 'not_preview' ); … … 1071 1535 } 1072 1536 1537 $changeset_post_id = $this->changeset_post_id(); 1538 if ( $changeset_post_id && in_array( get_post_status( $changeset_post_id ), array( 'publish', 'trash' ) ) ) { 1539 wp_send_json_error( 'changeset_already_published' ); 1540 } 1541 1542 if ( empty( $changeset_post_id ) ) { 1543 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->create_posts ) ) { 1544 wp_send_json_error( 'cannot_create_changeset_post' ); 1545 } 1546 } else { 1547 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) { 1548 wp_send_json_error( 'cannot_edit_changeset_post' ); 1549 } 1550 } 1551 1552 if ( ! empty( $_POST['customize_changeset_data'] ) ) { 1553 $input_changeset_data = json_decode( wp_unslash( $_POST['customize_changeset_data'] ), true ); 1554 if ( ! is_array( $input_changeset_data ) ) { 1555 wp_send_json_error( 'invalid_customize_changeset_data' ); 1556 } 1557 } else { 1558 $input_changeset_data = array(); 1559 } 1560 1561 // Validate title. 1562 $changeset_title = null; 1563 if ( isset( $_POST['customize_changeset_title'] ) ) { 1564 $changeset_title = sanitize_text_field( wp_unslash( $_POST['customize_changeset_title'] ) ); 1565 } 1566 1567 // Validate changeset status param. 1568 $is_publish = null; 1569 $changeset_status = null; 1570 if ( isset( $_POST['customize_changeset_status'] ) ) { 1571 $changeset_status = wp_unslash( $_POST['customize_changeset_status'] ); 1572 if ( ! get_post_status_object( $changeset_status ) || ! in_array( $changeset_status, array( 'draft', 'pending', 'publish', 'future' ), true ) ) { 1573 wp_send_json_error( 'bad_customize_changeset_status', 400 ); 1574 } 1575 $is_publish = ( 'publish' === $changeset_status || 'future' === $changeset_status ); 1576 if ( $is_publish ) { 1577 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) { 1578 wp_send_json_error( 'changeset_publish_unauthorized', 403 ); 1579 } 1580 if ( false === has_action( 'transition_post_status', '_wp_customize_publish_changeset' ) ) { 1581 wp_send_json_error( 'missing_publish_callback', 500 ); 1582 } 1583 } 1584 } 1585 1586 /* 1587 * Validate changeset date param. Date is assumed to be in local time for 1588 * the WP if in MySQL format (YYYY-MM-DD HH:MM:SS). Otherwise, the date 1589 * is parsed with strtotime() so that ISO date format may be supplied 1590 * or a string like "+10 minutes". 1591 */ 1592 $changeset_date_gmt = null; 1593 if ( isset( $_POST['customize_changeset_date'] ) ) { 1594 $changeset_date = wp_unslash( $_POST['customize_changeset_date'] ); 1595 if ( preg_match( '/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/', $changeset_date ) ) { 1596 $mm = substr( $changeset_date, 5, 2 ); 1597 $jj = substr( $changeset_date, 8, 2 ); 1598 $aa = substr( $changeset_date, 0, 4 ); 1599 $valid_date = wp_checkdate( $mm, $jj, $aa, $changeset_date ); 1600 if ( ! $valid_date ) { 1601 wp_send_json_error( 'bad_customize_changeset_date', 400 ); 1602 } 1603 $changeset_date_gmt = get_gmt_from_date( $changeset_date ); 1604 } else { 1605 $timestamp = strtotime( $changeset_date ); 1606 if ( ! $timestamp ) { 1607 wp_send_json_error( 'bad_customize_changeset_date', 400 ); 1608 } 1609 $changeset_date_gmt = gmdate( 'Y-m-d H:i:s', $timestamp ); 1610 } 1611 $now = gmdate( 'Y-m-d H:i:59' ); 1612 1613 $is_future_dated = ( mysql2date( 'U', $changeset_date_gmt, false ) > mysql2date( 'U', $now, false ) ); 1614 if ( ! $is_future_dated ) { 1615 wp_send_json_error( 'not_future_date', 400 ); // Only future dates are allowed. 1616 } 1617 1618 if ( ! $this->is_theme_active() && ( 'future' === $changeset_status || $is_future_dated ) ) { 1619 wp_send_json_error( 'cannot_schedule_theme_switches', 400 ); // This should be allowed in the future, when theme is a regular setting. 1620 } 1621 $will_remain_auto_draft = ( ! $changeset_status && ( ! $changeset_post_id || 'auto-draft' === get_post_status( $changeset_post_id ) ) ); 1622 if ( $changeset_date && $will_remain_auto_draft ) { 1623 wp_send_json_error( 'cannot_supply_date_for_auto_draft_changeset', 400 ); 1624 } 1625 } 1626 1627 $r = $this->save_changeset_post( array( 1628 'status' => $changeset_status, 1629 'title' => $changeset_title, 1630 'date_gmt' => $changeset_date_gmt, 1631 'data' => $input_changeset_data, 1632 ) ); 1633 if ( is_wp_error( $r ) ) { 1634 $response = $r->get_error_data(); 1635 } else { 1636 $response = $r; 1637 1638 // Note that if the changeset status was publish, then it will get set to trash if revisions are not supported. 1639 $response['changeset_status'] = get_post_status( $this->changeset_post_id() ); 1640 if ( $is_publish && 'trash' === $response['changeset_status'] ) { 1641 $response['changeset_status'] = 'publish'; 1642 } 1643 1644 if ( 'publish' === $response['changeset_status'] ) { 1645 $response['next_changeset_uuid'] = wp_generate_uuid4(); 1646 } 1647 } 1648 1649 if ( isset( $response['setting_validities'] ) ) { 1650 $response['setting_validities'] = array_map( array( $this, 'prepare_setting_validity_for_js' ), $response['setting_validities'] ); 1651 } 1652 1653 /** 1654 * Filters response data for a successful customize_save Ajax request. 1655 * 1656 * This filter does not apply if there was a nonce or authentication failure. 1657 * 1658 * @since 4.2.0 1659 * 1660 * @param array $response Additional information passed back to the 'saved' 1661 * event on `wp.customize`. 1662 * @param WP_Customize_Manager $this WP_Customize_Manager instance. 1663 */ 1664 $response = apply_filters( 'customize_save_response', $response, $this ); 1665 1666 if ( is_wp_error( $r ) ) { 1667 wp_send_json_error( $response ); 1668 } else { 1669 wp_send_json_success( $response ); 1670 } 1671 } 1672 1673 /** 1674 * Save the post for the loaded changeset. 1675 * 1676 * @since 4.7.0 1677 * @access public 1678 * 1679 * @param array $args { 1680 * Args for changeset post. 1681 * 1682 * @type array $data Optional additional changeset data. Values will be merged on top of any existing post values. 1683 * @type string $status Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed. 1684 * @type string $title Post title. Optional. 1685 * @type string $date_gmt Date in GMT. Optional. 1686 * } 1687 * 1688 * @return array|WP_Error Returns array on success and WP_Error with array data on error. 1689 */ 1690 function save_changeset_post( $args = array() ) { 1691 1692 $args = array_merge( 1693 array( 1694 'status' => null, 1695 'title' => null, 1696 'data' => array(), 1697 'date_gmt' => null, 1698 ), 1699 $args 1700 ); 1701 1702 $changeset_post_id = $this->changeset_post_id(); 1703 1704 // The request was made via wp.customize.previewer.save(). 1705 $update_transactionally = (bool) $args['status']; 1706 $allow_revision = (bool) $args['status']; 1707 1708 // Amend post values with any supplied data. 1709 foreach ( $args['data'] as $setting_id => $setting_params ) { 1710 if ( array_key_exists( 'value', $setting_params ) ) { 1711 $this->set_post_value( $setting_id, $setting_params['value'] ); // Add to post values so that they can be validated and sanitized. 1712 } 1713 } 1714 1715 // Note that in addition to post data, this will include any stashed theme mods. 1716 $post_values = $this->unsanitized_post_values( array( 1717 'exclude_changeset' => true, 1718 'exclude_post_data' => false, 1719 ) ); 1720 $this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value. 1721 1073 1722 /** 1074 1723 * Fires before save validation happens. … … 1085 1734 1086 1735 // Validate settings. 1087 $setting_validities = $this->validate_setting_values( $this->unsanitized_post_values() ); 1736 $setting_validities = $this->validate_setting_values( $post_values, array( 1737 'validate_capability' => true, 1738 'validate_existence' => true, 1739 ) ); 1088 1740 $invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) ); 1089 $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities ); 1090 if ( $invalid_setting_count > 0 ) { 1741 1742 /* 1743 * Short-circuit if there are invalid settings the update is transactional. 1744 * A changeset update is transactional when a status is supplied in the request. 1745 */ 1746 if ( $update_transactionally && $invalid_setting_count > 0 ) { 1091 1747 $response = array( 1092 'setting_validities' => $ exported_setting_validities,1748 'setting_validities' => $setting_validities, 1093 1749 'message' => sprintf( _n( 'There is %s invalid setting.', 'There are %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ), 1094 1750 ); 1095 1096 /** This filter is documented in wp-includes/class-wp-customize-manager.php */ 1097 $response = apply_filters( 'customize_save_response', $response, $this ); 1098 wp_send_json_error( $response ); 1099 } 1100 1101 // Do we have to switch themes? 1102 if ( ! $this->is_theme_active() ) { 1103 // Temporarily stop previewing the theme to allow switch_themes() 1104 // to operate properly. 1751 return new WP_Error( 'transaction_fail', '', $response ); 1752 } 1753 1754 $response = array( 1755 'setting_validities' => $setting_validities, 1756 ); 1757 1758 // Obtain/merge data for changeset. 1759 $original_changeset_data = $this->get_changeset_post_data( $changeset_post_id ); 1760 $data = $original_changeset_data; 1761 if ( is_wp_error( $data ) ) { 1762 $data = array(); 1763 } 1764 1765 // Ensure that all post values are included in the changeset data. 1766 foreach ( $post_values as $setting_id => $post_value ) { 1767 if ( ! isset( $args['data'][ $setting_id ] ) ) { 1768 $args['data'][ $setting_id ] = array(); 1769 } 1770 if ( ! isset( $args['data'][ $setting_id ]['value'] ) ) { 1771 $args['data'][ $setting_id ]['value'] = $post_value; 1772 } 1773 } 1774 1775 foreach ( $args['data'] as $setting_id => $setting_params ) { 1776 $setting = $this->get_setting( $setting_id ); 1777 if ( ! $setting || ! $setting->check_capabilities() ) { 1778 continue; 1779 } 1780 1781 // Skip updating changeset for invalid setting values. 1782 if ( isset( $setting_validities[ $setting_id ] ) && is_wp_error( $setting_validities[ $setting_id ] ) ) { 1783 continue; 1784 } 1785 1786 $changeset_setting_id = $setting_id; 1787 if ( 'theme_mod' === $setting->type ) { 1788 $changeset_setting_id = sprintf( '%s::%s', $this->get_stylesheet(), $setting_id ); 1789 } 1790 1791 if ( null === $setting_params ) { 1792 // Remove setting from changeset entirely. 1793 unset( $data[ $changeset_setting_id ] ); 1794 } else { 1795 // Merge any additional setting params that have been supplied with the existing params. 1796 if ( ! isset( $data[ $changeset_setting_id ] ) ) { 1797 $data[ $changeset_setting_id ] = array(); 1798 } 1799 $data[ $changeset_setting_id ] = array_merge( 1800 $data[ $changeset_setting_id ], 1801 $setting_params, 1802 array( 'type' => $setting->type ) 1803 ); 1804 } 1805 } 1806 1807 $filter_context = array( 1808 'uuid' => $this->changeset_uuid(), 1809 'title' => $args['title'], 1810 'status' => $args['status'], 1811 'date_gmt' => $args['date_gmt'], 1812 'post_id' => $changeset_post_id, 1813 'previous_data' => is_wp_error( $original_changeset_data ) ? array() : $original_changeset_data, 1814 'manager' => $this, 1815 ); 1816 1817 /** 1818 * Filters the settings' data that will be persisted into the changeset. 1819 * 1820 * Plugins may amend additional data (such as additional meta for settings) into the changeset with this filter. 1821 * 1822 * @since 4.7.0 1823 * 1824 * @param array $data Updated changeset data, mapping setting IDs to arrays containing a $value item and optionally other metadata. 1825 * @param array $context { 1826 * Filter context. 1827 * 1828 * @type string $uuid Changeset UUID. 1829 * @type string $title Requested title for the changeset post. 1830 * @type string $status Requested status for the changeset post. 1831 * @type string $date_gmt Requested date for the changeset post in MySQL format and GMT timezone. 1832 * @type int|false $post_id Post ID for the changeset, or false if it doesn't exist yet. 1833 * @type array $previous_data Previous data contained in the changeset. 1834 * @type WP_Customize_Manager $manager Manager instance. 1835 * } 1836 */ 1837 $data = apply_filters( 'customize_changeset_save_data', $data, $filter_context ); 1838 1839 // Switch theme if publishing changes now. 1840 if ( 'publish' === $args['status'] && ! $this->is_theme_active() ) { 1841 // Temporarily stop previewing the theme to allow switch_themes() to operate properly. 1105 1842 $this->stop_previewing_theme(); 1106 1843 switch_theme( $this->get_stylesheet() ); … … 1109 1846 } 1110 1847 1848 // Gather the data for wp_insert_post()/wp_update_post(). 1849 $json_options = 0; 1850 if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) { 1851 $json_options |= JSON_UNESCAPED_SLASHES; // Introduced in PHP 5.4. This is only to improve readability as slashes needn't be escaped in storage. 1852 } 1853 $json_options |= JSON_PRETTY_PRINT; // Also introduced in PHP 5.4, but WP defines constant for back compat. See WP Trac #30139. 1854 $post_array = array( 1855 'post_content' => wp_json_encode( $data, $json_options ), 1856 ); 1857 if ( $args['title'] ) { 1858 $post_array['post_title'] = $args['title']; 1859 } 1860 if ( $changeset_post_id ) { 1861 $post_array['ID'] = $changeset_post_id; 1862 } else { 1863 $post_array['post_type'] = 'customize_changeset'; 1864 $post_array['post_name'] = $this->changeset_uuid(); 1865 $post_array['post_status'] = 'auto-draft'; 1866 } 1867 if ( $args['status'] ) { 1868 $post_array['post_status'] = $args['status']; 1869 } 1870 if ( $args['date_gmt'] ) { 1871 $post_array['post_date_gmt'] = $args['date_gmt']; 1872 $post_array['post_date'] = get_date_from_gmt( $args['date_gmt'] ); 1873 } 1874 1875 $this->store_changeset_revision = $allow_revision; 1876 add_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ), 5, 3 ); 1877 1878 // Update the changeset post. The publish_customize_changeset action will cause the settings in the changeset to be saved via WP_Customize_Setting::save(). 1879 $has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) ); 1880 if ( $has_kses ) { 1881 kses_remove_filters(); // Prevent KSES from corrupting JSON in post_content. 1882 } 1883 1884 // Note that updating a post with publish status will trigger WP_Customize_Manager::publish_changeset_values(). 1885 if ( $changeset_post_id ) { 1886 $post_array['edit_date'] = true; // Prevent date clearing. 1887 $r = wp_update_post( wp_slash( $post_array ), true ); 1888 } else { 1889 $r = wp_insert_post( wp_slash( $post_array ), true ); 1890 if ( ! is_wp_error( $r ) ) { 1891 $this->_changeset_post_id = $r; // Update cached post ID for the loaded changeset. 1892 } 1893 } 1894 if ( $has_kses ) { 1895 kses_init_filters(); 1896 } 1897 $this->_changeset_data = null; // Reset so WP_Customize_Manager::changeset_data() will re-populate with updated contents. 1898 1899 remove_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ) ); 1900 1901 if ( is_wp_error( $r ) ) { 1902 $response['changeset_post_save_failure'] = $r->get_error_code(); 1903 return new WP_Error( 'changeset_post_save_failure', '', $response ); 1904 } 1905 1906 return $response; 1907 } 1908 1909 /** 1910 * Whether a changeset revision should be made. 1911 * 1912 * @since 4.7.0 1913 * @access private 1914 * @var bool 1915 */ 1916 protected $store_changeset_revision; 1917 1918 /** 1919 * Filters whether a changeset has changed to create a new revision. 1920 * 1921 * Note that this will not be called while a changeset post remains in auto-draft status. 1922 * 1923 * @since 4.7.0 1924 * @access private 1925 * 1926 * @param bool $post_has_changed Whether the post has changed. 1927 * @param WP_Post $last_revision The last revision post object. 1928 * @param WP_Post $post The post object. 1929 * 1930 * @return bool Whether a revision should be made. 1931 */ 1932 public function _filter_revision_post_has_changed( $post_has_changed, $last_revision, $post ) { 1933 unset( $last_revision ); 1934 if ( 'customize_changeset' === $post->post_type ) { 1935 $post_has_changed = $this->store_changeset_revision; 1936 } 1937 return $post_has_changed; 1938 } 1939 1940 /** 1941 * Publish changeset values. 1942 * 1943 * This will the values contained in a changeset, even changesets that do not 1944 * correspond to current manager instance. This is called by 1945 * `_wp_customize_publish_changeset()` when a customize_changeset post is 1946 * transitioned to the `publish` status. As such, this method should not be 1947 * called directly and instead `wp_publish_post()` should be used. 1948 * 1949 * Please note that if the settings in the changeset are for a non-activated 1950 * theme, the theme must first be switched to (via `switch_theme()`) before 1951 * invoking this method. 1952 * 1953 * @since 4.7.0 1954 * @access private 1955 * @see _wp_customize_publish_changeset() 1956 * 1957 * @param int $changeset_post_id ID for customize_changeset post. Defaults to the changeset for the current manager instance. 1958 * @return true|WP_Error True or error info. 1959 */ 1960 public function _publish_changeset_values( $changeset_post_id ) { 1961 $publishing_changeset_data = $this->get_changeset_post_data( $changeset_post_id ); 1962 if ( is_wp_error( $publishing_changeset_data ) ) { 1963 return $publishing_changeset_data; 1964 } 1965 1966 $changeset_post = get_post( $changeset_post_id ); 1967 1968 /* 1969 * Temporarily override the changeset context so that it will be read 1970 * in calls to unsanitized_post_values() and so that it will be available 1971 * on the $wp_customize object passed to hooks during the save logic. 1972 */ 1973 $previous_changeset_post_id = $this->_changeset_post_id; 1974 $this->_changeset_post_id = $changeset_post_id; 1975 $previous_changeset_uuid = $this->_changeset_uuid; 1976 $this->_changeset_uuid = $changeset_post->post_name; 1977 $previous_changeset_data = $this->_changeset_data; 1978 $this->_changeset_data = $publishing_changeset_data; 1979 1980 // Ensure that other theme mods are stashed. 1981 $other_theme_mod_settings = array(); 1982 if ( did_action( 'switch_theme' ) ) { 1983 $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/'; 1984 $matches = array(); 1985 foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) { 1986 $is_other_theme_mod = ( 1987 isset( $setting_params['value'] ) 1988 && 1989 isset( $setting_params['type'] ) 1990 && 1991 'theme_mod' === $setting_params['type'] 1992 && 1993 preg_match( $namespace_pattern, $raw_setting_id, $matches ) 1994 && 1995 $this->get_stylesheet() !== $matches['stylesheet'] 1996 ); 1997 if ( $is_other_theme_mod ) { 1998 if ( ! isset( $other_theme_mod_settings[ $matches['stylesheet'] ] ) ) { 1999 $other_theme_mod_settings[ $matches['stylesheet'] ] = array(); 2000 } 2001 $other_theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params; 2002 } 2003 } 2004 } 2005 2006 $changeset_setting_values = $this->unsanitized_post_values( array( 2007 'exclude_post_data' => true, 2008 'exclude_changeset' => false, 2009 ) ); 2010 $changeset_setting_ids = array_keys( $changeset_setting_values ); 2011 $this->add_dynamic_settings( $changeset_setting_ids ); 2012 1111 2013 /** 1112 2014 * Fires once the theme has switched in the Customizer, but before settings … … 1115 2017 * @since 3.4.0 1116 2018 * 1117 * @param WP_Customize_Manager $ thisWP_Customize_Manager instance.2019 * @param WP_Customize_Manager $manager WP_Customize_Manager instance. 1118 2020 */ 1119 2021 do_action( 'customize_save', $this ); 1120 2022 1121 foreach ( $this->settings as $setting ) { 1122 $setting->save(); 2023 /* 2024 * Ensure that all settings will allow themselves to be saved. Note that 2025 * this is safe because the setting would have checked the capability 2026 * when the setting value was written into the changeset. So this is why 2027 * an additional capability check is not required here. 2028 */ 2029 $original_setting_capabilities = array(); 2030 foreach ( $changeset_setting_ids as $setting_id ) { 2031 $setting = $this->get_setting( $setting_id ); 2032 if ( $setting ) { 2033 $original_setting_capabilities[ $setting->id ] = $setting->capability; 2034 $setting->capability = 'exist'; 2035 } 2036 } 2037 2038 foreach ( $changeset_setting_ids as $setting_id ) { 2039 $setting = $this->get_setting( $setting_id ); 2040 if ( $setting ) { 2041 $setting->save(); 2042 } 2043 } 2044 2045 // Update the stashed theme mod settings, removing the active theme's stashed settings, if activated. 2046 if ( did_action( 'switch_theme' ) ) { 2047 $this->update_stashed_theme_mod_settings( $other_theme_mod_settings ); 1123 2048 } 1124 2049 … … 1128 2053 * @since 3.6.0 1129 2054 * 1130 * @param WP_Customize_Manager $ thisWP_Customize_Manager instance.2055 * @param WP_Customize_Manager $manager WP_Customize_Manager instance. 1131 2056 */ 1132 2057 do_action( 'customize_save_after', $this ); 1133 2058 1134 $data = array( 1135 'setting_validities' => $exported_setting_validities, 1136 ); 1137 1138 /** 1139 * Filters response data for a successful customize_save Ajax request. 1140 * 1141 * This filter does not apply if there was a nonce or authentication failure. 1142 * 1143 * @since 4.2.0 1144 * 1145 * @param array $data Additional information passed back to the 'saved' 1146 * event on `wp.customize`. 1147 * @param WP_Customize_Manager $this WP_Customize_Manager instance. 1148 */ 1149 $response = apply_filters( 'customize_save_response', $data, $this ); 1150 wp_send_json_success( $response ); 2059 // Restore original capabilities. 2060 foreach ( $original_setting_capabilities as $setting_id => $capability ) { 2061 $setting = $this->get_setting( $setting_id ); 2062 if ( $setting ) { 2063 $setting->capability = $capability; 2064 } 2065 } 2066 2067 // Restore original changeset data. 2068 $this->_changeset_data = $previous_changeset_data; 2069 $this->_changeset_post_id = $previous_changeset_post_id; 2070 $this->_changeset_uuid = $previous_changeset_uuid; 2071 2072 return true; 2073 } 2074 2075 /** 2076 * Update stashed theme mod settings. 2077 * 2078 * @since 4.7.0 2079 * @access private 2080 * 2081 * @param array $inactive_theme_mod_settings Mapping of stylesheet to arrays of theme mod settings. 2082 * @return array|false Returns array of updated stashed theme mods or false if the update failed or there were no changes. 2083 */ 2084 protected function update_stashed_theme_mod_settings( $inactive_theme_mod_settings ) { 2085 $stashed_theme_mod_settings = get_option( 'customize_stashed_theme_mods' ); 2086 if ( empty( $stashed_theme_mod_settings ) ) { 2087 $stashed_theme_mod_settings = array(); 2088 } 2089 2090 // Delete any stashed theme mods for the active theme since since they would have been loaded and saved upon activation. 2091 unset( $stashed_theme_mod_settings[ $this->get_stylesheet() ] ); 2092 2093 // Merge inactive theme mods with the stashed theme mod settings. 2094 foreach ( $inactive_theme_mod_settings as $stylesheet => $theme_mod_settings ) { 2095 if ( ! isset( $stashed_theme_mod_settings[ $stylesheet ] ) ) { 2096 $stashed_theme_mod_settings[ $stylesheet ] = array(); 2097 } 2098 2099 $stashed_theme_mod_settings[ $stylesheet ] = array_merge( 2100 $stashed_theme_mod_settings[ $stylesheet ], 2101 $theme_mod_settings 2102 ); 2103 } 2104 2105 $autoload = false; 2106 $result = update_option( 'customize_stashed_theme_mods', $stashed_theme_mod_settings, $autoload ); 2107 if ( ! $result ) { 2108 return false; 2109 } 2110 return $stashed_theme_mod_settings; 1151 2111 } 1152 2112 … … 1692 2652 1693 2653 /** 2654 * Determines whether the admin and the frontend are on different domains. 2655 * 2656 * @since 4.7.0 2657 * @access public 2658 * 2659 * @return bool Whether cross-domain. 2660 */ 2661 public function is_cross_domain() { 2662 $admin_origin = wp_parse_url( admin_url() ); 2663 $home_origin = wp_parse_url( home_url() ); 2664 $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) ); 2665 return $cross_domain; 2666 } 2667 2668 /** 2669 * Get URLs allowed to be previewed. 2670 * 2671 * If the front end and the admin are served from the same domain, load the 2672 * preview over ssl if the Customizer is being loaded over ssl. This avoids 2673 * insecure content warnings. This is not attempted if the admin and front end 2674 * are on different domains to avoid the case where the front end doesn't have 2675 * ssl certs. Domain mapping plugins can allow other urls in these conditions 2676 * using the customize_allowed_urls filter. 2677 * 2678 * @since 4.7.0 2679 * @access public 2680 * 2681 * @returns array Allowed URLs. 2682 */ 2683 public function get_allowed_urls() { 2684 $allowed_urls = array( home_url( '/' ) ); 2685 2686 if ( is_ssl() && ! $this->is_cross_domain() ) { 2687 $allowed_urls[] = home_url( '/', 'https' ); 2688 } 2689 2690 /** 2691 * Filters the list of URLs allowed to be clicked and followed in the Customizer preview. 2692 * 2693 * @since 3.4.0 2694 * 2695 * @param array $allowed_urls An array of allowed URLs. 2696 */ 2697 $allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) ); 2698 2699 return $allowed_urls; 2700 } 2701 2702 /** 2703 * Get messenger channel. 2704 * 2705 * @since 4.7.0 2706 * @access public 2707 * 2708 * @return string Messenger channel. 2709 */ 2710 public function get_messenger_channel() { 2711 return $this->messenger_channel; 2712 } 2713 2714 /** 1694 2715 * Set URL to link the user to when closing the Customizer. 1695 2716 * … … 1800 2821 */ 1801 2822 public function customize_pane_settings() { 1802 /*1803 * If the front end and the admin are served from the same domain, load the1804 * preview over ssl if the Customizer is being loaded over ssl. This avoids1805 * insecure content warnings. This is not attempted if the admin and front end1806 * are on different domains to avoid the case where the front end doesn't have1807 * ssl certs. Domain mapping plugins can allow other urls in these conditions1808 * using the customize_allowed_urls filter.1809 */1810 1811 $allowed_urls = array( home_url( '/' ) );1812 $admin_origin = parse_url( admin_url() );1813 $home_origin = parse_url( home_url() );1814 $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );1815 1816 if ( is_ssl() && ! $cross_domain ) {1817 $allowed_urls[] = home_url( '/', 'https' );1818 }1819 1820 /**1821 * Filters the list of URLs allowed to be clicked and followed in the Customizer preview.1822 *1823 * @since 3.4.01824 *1825 * @param array $allowed_urls An array of allowed URLs.1826 */1827 $allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) );1828 2823 1829 2824 $login_url = add_query_arg( array( … … 1832 2827 ), wp_login_url() ); 1833 2828 2829 // Ensure dirty flags are set for modified settings. 2830 foreach ( array_keys( $this->unsanitized_post_values() ) as $setting_id ) { 2831 $setting = $this->get_setting( $setting_id ); 2832 if ( $setting ) { 2833 $setting->dirty = true; 2834 } 2835 } 2836 1834 2837 // Prepare Customizer settings to pass to JavaScript. 1835 2838 $settings = array( 2839 'changeset' => array( 2840 'uuid' => $this->changeset_uuid(), 2841 'status' => $this->changeset_post_id() ? get_post_status( $this->changeset_post_id() ) : '', 2842 ), 2843 'timeouts' => array( 2844 'windowRefresh' => 250, 2845 'changesetAutoSave' => AUTOSAVE_INTERVAL * 1000, 2846 'keepAliveCheck' => 2500, 2847 'reflowPaneContents' => 100, 2848 'previewFrameSensitivity' => 2000, 2849 ), 1836 2850 'theme' => array( 1837 2851 'stylesheet' => $this->get_stylesheet(), … … 1843 2857 'activated' => esc_url_raw( home_url( '/' ) ), 1844 2858 'ajax' => esc_url_raw( admin_url( 'admin-ajax.php', 'relative' ) ), 1845 'allowed' => array_map( 'esc_url_raw', $ allowed_urls),1846 'isCrossDomain' => $ cross_domain,2859 'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ), 2860 'isCrossDomain' => $this->is_cross_domain(), 1847 2861 'home' => esc_url_raw( home_url( '/' ) ), 1848 2862 'login' => esc_url_raw( $login_url ), … … 2338 3352 */ 2339 3353 public function register_dynamic_settings() { 2340 $this->add_dynamic_settings( array_keys( $this->unsanitized_post_values() ) ); 3354 $setting_ids = array_keys( $this->unsanitized_post_values() ); 3355 $this->add_dynamic_settings( $setting_ids ); 2341 3356 } 2342 3357 -
trunk/src/wp-includes/class-wp-customize-nav-menus.php
r38794 r38810 49 49 $this->manager = $manager; 50 50 51 // Skip useless hooks when the user can't manage nav menus anyway. 51 // See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L469-L499 52 add_action( 'customize_register', array( $this, 'customize_register' ), 11 ); 53 add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_dynamic_setting_args' ), 10, 2 ); 54 add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_dynamic_setting_class' ), 10, 3 ); 55 56 // Skip remaining hooks when the user can't manage nav menus anyway. 52 57 if ( ! current_user_can( 'edit_theme_options' ) ) { 53 58 return; … … 59 64 add_action( 'wp_ajax_customize-nav-menus-insert-auto-draft', array( $this, 'ajax_insert_auto_draft_post' ) ); 60 65 add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); 61 add_action( 'customize_register', array( $this, 'customize_register' ), 11 );62 add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_dynamic_setting_args' ), 10, 2 );63 add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_dynamic_setting_class' ), 10, 3 );64 66 add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) ); 65 67 add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) ); … … 487 489 public function customize_register() { 488 490 491 /* 492 * Preview settings for nav menus early so that the sections and controls will be added properly. 493 * See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L506-L543 494 */ 495 $nav_menus_setting_ids = array(); 496 foreach ( array_keys( $this->manager->unsanitized_post_values() ) as $setting_id ) { 497 if ( preg_match( '/^(nav_menu_locations|nav_menu|nav_menu_item)\[/', $setting_id ) ) { 498 $nav_menus_setting_ids[] = $setting_id; 499 } 500 } 501 foreach ( $nav_menus_setting_ids as $setting_id ) { 502 $setting = $this->manager->get_setting( $setting_id ); 503 if ( $setting ) { 504 $setting->preview(); 505 } 506 } 507 489 508 // Require JS-rendered control types. 490 509 $this->manager->register_panel_type( 'WP_Customize_Nav_Menus_Panel' ); -
trunk/src/wp-includes/class-wp-customize-widgets.php
r38766 r38810 94 94 $this->manager = $manager; 95 95 96 // Skip useless hooks when the user can't manage widgets anyway. 96 // See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L420-L449 97 add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_customize_dynamic_setting_args' ), 10, 2 ); 98 add_action( 'widgets_init', array( $this, 'register_settings' ), 95 ); 99 add_action( 'customize_register', array( $this, 'schedule_customize_register' ), 1 ); 100 101 // Skip remaining hooks when the user can't manage widgets anyway. 97 102 if ( ! current_user_can( 'edit_theme_options' ) ) { 98 103 return; 99 104 } 100 105 101 add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_customize_dynamic_setting_args' ), 10, 2 );102 add_action( 'widgets_init', array( $this, 'register_settings' ), 95 );103 106 add_action( 'wp_loaded', array( $this, 'override_sidebars_widgets_for_theme_switch' ) ); 104 107 add_action( 'customize_controls_init', array( $this, 'customize_controls_init' ) ); 105 add_action( 'customize_register', array( $this, 'schedule_customize_register' ), 1 );106 108 add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); 107 109 add_action( 'customize_controls_print_styles', array( $this, 'print_styles' ) ); … … 277 279 $this->old_sidebars_widgets = wp_get_sidebars_widgets(); 278 280 add_filter( 'customize_value_old_sidebars_widgets_data', array( $this, 'filter_customize_value_old_sidebars_widgets_data' ) ); 281 $this->manager->set_post_value( 'old_sidebars_widgets_data', $this->old_sidebars_widgets ); // Override any value cached in changeset. 279 282 280 283 // retrieve_widgets() looks at the global $sidebars_widgets -
trunk/src/wp-includes/customize/class-wp-customize-selective-refresh.php
r38478 r38810 307 307 return; 308 308 } 309 310 $this->manager->remove_preview_signature();311 309 312 310 /* -
trunk/src/wp-includes/customize/class-wp-customize-theme-control.php
r35389 r38810 64 64 public function content_template() { 65 65 $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); 66 $active_url = esc_url( remove_query_arg( ' theme', $current_url ) );67 $preview_url = esc_url( add_query_arg( ' theme', '__THEME__', $current_url ) ); // Token because esc_url() strips curly braces.66 $active_url = esc_url( remove_query_arg( 'customize_theme', $current_url ) ); 67 $preview_url = esc_url( add_query_arg( 'customize_theme', '__THEME__', $current_url ) ); // Token because esc_url() strips curly braces. 68 68 $preview_url = str_replace( '__THEME__', '{{ data.theme.id }}', $preview_url ); 69 69 ?> -
trunk/src/wp-includes/default-filters.php
r38778 r38810 76 76 // Slugs 77 77 add_filter( 'pre_term_slug', 'sanitize_title' ); 78 add_filter( 'wp_insert_post_data', '_wp_customize_changeset_filter_insert_post_data', 10, 2 ); 78 79 79 80 // Keys … … 383 384 add_action( 'wp_head', '_custom_logo_header_styles' ); 384 385 add_action( 'plugins_loaded', '_wp_customize_include' ); 386 add_action( 'transition_post_status', '_wp_customize_publish_changeset', 10, 3 ); 385 387 add_action( 'admin_enqueue_scripts', '_wp_customize_loader_settings' ); 386 388 add_action( 'delete_attachment', '_delete_attachment_theme_mod' ); -
trunk/src/wp-includes/functions.php
r38809 r38810 5524 5524 return false; 5525 5525 } 5526 5527 /** 5528 * Generate a random UUID (version 4). 5529 * 5530 * @since 4.7.0 5531 * 5532 * @return string UUID. 5533 */ 5534 function wp_generate_uuid4() { 5535 return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', 5536 mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), 5537 mt_rand( 0, 0xffff ), 5538 mt_rand( 0, 0x0fff ) | 0x4000, 5539 mt_rand( 0, 0x3fff ) | 0x8000, 5540 mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ) 5541 ); 5542 } -
trunk/src/wp-includes/functions.wp-scripts.php
r38519 r38810 35 35 */ 36 36 function _wp_scripts_maybe_doing_it_wrong( $function ) { 37 if ( did_action( 'init' ) ) {37 if ( did_action( 'init' ) || did_action( 'admin_enqueue_scripts' ) || did_action( 'wp_enqueue_scripts' ) || did_action( 'login_enqueue_scripts' ) ) { 38 38 return; 39 39 } -
trunk/src/wp-includes/js/customize-base.js
r38513 r38810 638 638 * Initialize Messenger. 639 639 * 640 * @param {object} params 641 * {string} .urlThe URL to communicate with.642 * {window} .targetWindowThe window instance to communicate with. Default window.parent.643 * {string} .channelIf provided, will send the channel with each message and only accept messages a matching channel.644 * @param {object} options 640 * @param {object} params - Parameters to configure the messenger. 641 * {string} params.url - The URL to communicate with. 642 * {window} params.targetWindow - The window instance to communicate with. Default window.parent. 643 * {string} params.channel - If provided, will send the channel with each message and only accept messages a matching channel. 644 * @param {object} options - Extend any instance parameter or method with this object. 645 645 */ 646 646 initialize: function( params, options ) { 647 647 // Target the parent frame by default, but only if a parent frame exists. 648 var defaultTarget = window.parent == window ? null : window.parent;648 var defaultTarget = window.parent === window ? null : window.parent; 649 649 650 650 $.extend( this, options || {} ); … … 653 653 this.add( 'url', params.url || '' ); 654 654 this.add( 'origin', this.url() ).link( this.url ).setter( function( to ) { 655 return to.replace( /([^:]+:\/\/[^\/]+).*/, '$1' ); 655 var urlParser = document.createElement( 'a' ); 656 urlParser.href = to; 657 return urlParser.protocol + '//' + urlParser.hostname; 656 658 }); 657 659 … … 808 810 }; 809 811 812 /** 813 * Utility function namespace 814 */ 815 api.utils = {}; 816 817 /** 818 * Parse query string. 819 * 820 * @since 4.7.0 821 * @access public 822 * 823 * @param {string} queryString Query string. 824 * @returns {object} Parsed query string. 825 */ 826 api.utils.parseQueryString = function parseQueryString( queryString ) { 827 var queryParams = {}; 828 _.each( queryString.split( '&' ), function( pair ) { 829 var parts, key, value; 830 parts = pair.split( '=', 2 ); 831 if ( ! parts[0] ) { 832 return; 833 } 834 key = decodeURIComponent( parts[0].replace( /\+/g, ' ' ) ); 835 key = key.replace( / /g, '_' ); // What PHP does. 836 if ( _.isUndefined( parts[1] ) ) { 837 value = null; 838 } else { 839 value = decodeURIComponent( parts[1].replace( /\+/g, ' ' ) ); 840 } 841 queryParams[ key ] = value; 842 } ); 843 return queryParams; 844 }; 845 810 846 // Expose the API publicly on window.wp.customize 811 847 exports.customize = api; -
trunk/src/wp-includes/js/customize-loader.js
r38520 r38810 132 132 targetWindow: this.iframe[0].contentWindow 133 133 }); 134 135 // Expose the changeset UUID on the parent window's URL so that the customized state can survive a refresh. 136 if ( history.replaceState ) { 137 this.messenger.bind( 'changeset-uuid', function( changesetUuid ) { 138 var urlParser = document.createElement( 'a' ); 139 urlParser.href = location.href; 140 urlParser.search = $.param( _.extend( 141 api.utils.parseQueryString( urlParser.search.substr( 1 ) ), 142 { changeset_uuid: changesetUuid } 143 ) ); 144 history.replaceState( { customize: urlParser.href }, '', urlParser.href ); 145 } ); 146 } 134 147 135 148 // Wait for the connection from the iframe before sending any postMessage events. -
trunk/src/wp-includes/js/customize-preview-nav-menus.js
r36889 r38810 107 107 */ 108 108 isRelatedSetting: function( setting, newValue, oldValue ) { 109 var partial = this, navMenuLocationSetting, navMenuId, isNavMenuItemSetting ;109 var partial = this, navMenuLocationSetting, navMenuId, isNavMenuItemSetting, _newValue, _oldValue, urlParser; 110 110 if ( _.isString( setting ) ) { 111 111 setting = api( setting ); … … 124 124 isNavMenuItemSetting = /^nav_menu_item\[/.test( setting.id ); 125 125 if ( isNavMenuItemSetting && _.isObject( newValue ) && _.isObject( oldValue ) ) { 126 delete newValue.type_label; 127 delete oldValue.type_label; 128 if ( _.isEqual( oldValue, newValue ) ) { 126 _newValue = _.clone( newValue ); 127 _oldValue = _.clone( oldValue ); 128 delete _newValue.type_label; 129 delete _oldValue.type_label; 130 131 // Normalize URL scheme when parent frame is HTTPS to prevent selective refresh upon initial page load. 132 if ( 'https' === api.preview.scheme.get() ) { 133 urlParser = document.createElement( 'a' ); 134 urlParser.href = _newValue.url; 135 urlParser.protocol = 'https:'; 136 _newValue.url = urlParser.href; 137 urlParser.href = _oldValue.url; 138 urlParser.protocol = 'https:'; 139 _oldValue.url = urlParser.href; 140 } 141 142 if ( _.isEqual( _oldValue, _newValue ) ) { 129 143 return false; 130 144 } … … 366 380 var selector = '.menu-item'; 367 381 382 // Skip adding highlights if not in the customizer preview iframe. 383 if ( ! api.settings.channel ) { 384 return; 385 } 386 368 387 // Focus on the menu item control when shift+clicking the menu item. 369 388 $( document ).on( 'click', selector, function( e ) { -
trunk/src/wp-includes/js/customize-preview-widgets.js
r38577 r38810 573 573 selector = this.widgetSelectors.join( ',' ); 574 574 575 // Skip adding highlights if not in the customizer preview iframe. 576 if ( ! api.settings.channel ) { 577 return; 578 } 579 575 580 $( selector ).attr( 'title', this.l10n.widgetTooltip ); 576 581 -
trunk/src/wp-includes/js/customize-preview.js
r38588 r38810 4 4 (function( exports, $ ){ 5 5 var api = wp.customize, 6 debounce; 6 debounce, 7 currentHistoryState = {}; 8 9 /* 10 * Capture the state that is passed into history.replaceState() and history.pushState() 11 * and also which is returned in the popstate event so that when the changeset_uuid 12 * gets updated when transitioning to a new changeset there the current state will 13 * be supplied in the call to history.replaceState(). 14 */ 15 ( function( history ) { 16 var injectUrlWithState; 17 18 if ( ! history.replaceState ) { 19 return; 20 } 21 22 /** 23 * Amend the supplied URL with the customized state. 24 * 25 * @since 4.7.0 26 * @access private 27 * 28 * @param {string} url URL. 29 * @returns {string} URL with customized state. 30 */ 31 injectUrlWithState = function( url ) { 32 var urlParser, queryParams; 33 urlParser = document.createElement( 'a' ); 34 urlParser.href = url; 35 queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) ); 36 37 queryParams.customize_changeset_uuid = api.settings.changeset.uuid; 38 if ( ! api.settings.theme.active ) { 39 queryParams.customize_theme = api.settings.theme.stylesheet; 40 } 41 if ( api.settings.theme.channel ) { 42 queryParams.customize_messenger_channel = api.settings.channel; 43 } 44 urlParser.search = $.param( queryParams ); 45 return url; 46 }; 47 48 history.replaceState = ( function( nativeReplaceState ) { 49 return function historyReplaceState( data, title, url ) { 50 currentHistoryState = data; 51 return nativeReplaceState.call( history, data, title, injectUrlWithState( url ) ); 52 }; 53 } )( history.replaceState ); 54 55 history.pushState = ( function( nativePushState ) { 56 return function historyPushState( data, title, url ) { 57 currentHistoryState = data; 58 return nativePushState.call( history, data, title, injectUrlWithState( url ) ); 59 }; 60 } )( history.pushState ); 61 62 window.addEventListener( 'popstate', function( event ) { 63 currentHistoryState = event.state; 64 } ); 65 66 }( history ) ); 7 67 8 68 /** … … 38 98 */ 39 99 initialize: function( params, options ) { 40 var self = this; 41 42 api.Messenger.prototype.initialize.call( this, params, options ); 43 44 this.body = $( document.body ); 45 this.body.on( 'click.preview', 'a', function( event ) { 100 var preview = this, urlParser = document.createElement( 'a' ); 101 102 api.Messenger.prototype.initialize.call( preview, params, options ); 103 104 urlParser.href = preview.origin(); 105 preview.add( 'scheme', urlParser.protocol.replace( /:$/, '' ) ); 106 107 preview.body = $( document.body ); 108 preview.body.on( 'click.preview', 'a', function( event ) { 46 109 var link, isInternalJumpLink; 47 110 link = $( this ); 111 112 // No-op if the anchor is not a link. 113 if ( _.isUndefined( link.attr( 'href' ) ) ) { 114 return; 115 } 116 48 117 isInternalJumpLink = ( '#' === link.attr( 'href' ).substr( 0, 1 ) ); 118 119 // Allow internal jump links to behave normally without preventing default. 120 if ( isInternalJumpLink ) { 121 return; 122 } 123 124 // If the link is not previewable, prevent the browser from navigating to it. 125 if ( ! api.isLinkPreviewable( link[0] ) ) { 126 wp.a11y.speak( api.settings.l10n.linkUnpreviewable ); 127 event.preventDefault(); 128 return; 129 } 130 131 // If not in an iframe, then allow the link click to proceed normally since the state query params are added. 132 if ( ! api.settings.channel ) { 133 return; 134 } 135 136 // Prevent initiating navigating from click and instead rely on sending url message to pane. 49 137 event.preventDefault(); 50 51 if ( isInternalJumpLink && '#' !== link.attr( 'href' ) ) {52 $( link.attr( 'href' ) ).each( function() {53 this.scrollIntoView();54 } );55 }56 138 57 139 /* … … 60 142 * control instead of also navigating to the URL linked to. 61 143 */ 62 if ( event.shiftKey || isInternalJumpLink) {144 if ( event.shiftKey ) { 63 145 return; 64 146 } 65 self.send( 'scroll', 0 ); 66 self.send( 'url', link.prop( 'href' ) ); 67 }); 68 69 // You cannot submit forms. 70 this.body.on( 'submit.preview', 'form', function( event ) { 71 var urlParser; 147 148 // Note: It's not relevant to send scroll because sending url message will have the same effect. 149 preview.send( 'url', link.prop( 'href' ) ); 150 } ); 151 152 preview.body.on( 'submit.preview', 'form', function( event ) { 153 var urlParser = document.createElement( 'a' ); 154 urlParser.href = this.action; 155 156 // If the link is not previewable, prevent the browser from navigating to it. 157 if ( 'GET' !== this.method.toUpperCase() || ! api.isLinkPreviewable( urlParser ) ) { 158 wp.a11y.speak( api.settings.l10n.formUnpreviewable ); 159 event.preventDefault(); 160 return; 161 } 162 163 // If not in an iframe, then allow the form submission to proceed normally with the state inputs injected. 164 if ( ! api.settings.channel ) { 165 return; 166 } 72 167 73 168 /* … … 82 177 * external site in the preview. 83 178 */ 84 if ( ! event.isDefaultPrevented() && 'GET' === this.method.toUpperCase() ) { 85 urlParser = document.createElement( 'a' ); 86 urlParser.href = this.action; 87 if ( urlParser.search.substr( 1 ).length > 1 ) { 179 if ( ! event.isDefaultPrevented() ) { 180 if ( urlParser.search.length > 1 ) { 88 181 urlParser.search += '&'; 89 182 } … … 92 185 } 93 186 187 // Prevent default since navigation should be done via sending url message or via JS submit handler. 94 188 event.preventDefault(); 95 189 }); 96 190 97 this.window = $( window ); 98 this.window.on( 'scroll.preview', debounce( function() { 99 self.send( 'scroll', self.window.scrollTop() ); 100 }, 200 )); 101 102 this.bind( 'scroll', function( distance ) { 103 self.window.scrollTop( distance ); 104 }); 191 preview.window = $( window ); 192 193 if ( api.settings.channel ) { 194 preview.window.on( 'scroll.preview', debounce( function() { 195 preview.send( 'scroll', preview.window.scrollTop() ); 196 }, 200 ) ); 197 198 preview.bind( 'scroll', function( distance ) { 199 preview.window.scrollTop( distance ); 200 }); 201 } 105 202 } 106 203 }); 204 205 /** 206 * Inject the changeset UUID into links in the document. 207 * 208 * @since 4.7.0 209 * @access protected 210 * 211 * @access private 212 * @returns {void} 213 */ 214 api.addLinkPreviewing = function addLinkPreviewing() { 215 var linkSelectors = 'a[href], area'; 216 217 // Inject links into initial document. 218 $( document.body ).find( linkSelectors ).each( function() { 219 api.prepareLinkPreview( this ); 220 } ); 221 222 // Inject links for new elements added to the page. 223 if ( 'undefined' !== typeof MutationObserver ) { 224 api.mutationObserver = new MutationObserver( function( mutations ) { 225 _.each( mutations, function( mutation ) { 226 $( mutation.target ).find( linkSelectors ).each( function() { 227 api.prepareLinkPreview( this ); 228 } ); 229 } ); 230 } ); 231 api.mutationObserver.observe( document.documentElement, { 232 childList: true, 233 subtree: true 234 } ); 235 } else { 236 237 // If mutation observers aren't available, fallback to just-in-time injection. 238 $( document.documentElement ).on( 'click focus mouseover', linkSelectors, function() { 239 api.prepareLinkPreview( this ); 240 } ); 241 } 242 }; 243 244 /** 245 * Should the supplied link is previewable. 246 * 247 * @since 4.7.0 248 * @access public 249 * 250 * @param {HTMLAnchorElement|HTMLAreaElement} element Link element. 251 * @param {string} element.search Query string. 252 * @param {string} element.pathname Path. 253 * @param {string} element.hostname Hostname. 254 * @param {object} [options] 255 * @param {object} [options.allowAdminAjax=false] Allow admin-ajax.php requests. 256 * @returns {boolean} Is appropriate for changeset link. 257 */ 258 api.isLinkPreviewable = function isLinkPreviewable( element, options ) { 259 var hasMatchingHost, urlParser, args; 260 261 args = _.extend( {}, { allowAdminAjax: false }, options || {} ); 262 263 if ( 'javascript:' === element.protocol ) { // jshint ignore:line 264 return true; 265 } 266 267 // Only web URLs can be previewed. 268 if ( 'https:' !== element.protocol && 'http:' !== element.protocol ) { 269 return false; 270 } 271 272 urlParser = document.createElement( 'a' ); 273 hasMatchingHost = ! _.isUndefined( _.find( api.settings.url.allowed, function( allowedUrl ) { 274 urlParser.href = allowedUrl; 275 if ( urlParser.hostname === element.hostname && urlParser.protocol === element.protocol ) { 276 return true; 277 } 278 return false; 279 } ) ); 280 if ( ! hasMatchingHost ) { 281 return false; 282 } 283 284 // Skip wp login and signup pages. 285 if ( /\/wp-(login|signup)\.php$/.test( element.pathname ) ) { 286 return false; 287 } 288 289 // Allow links to admin ajax as faux frontend URLs. 290 if ( /\/wp-admin\/admin-ajax\.php$/.test( element.pathname ) ) { 291 return args.allowAdminAjax; 292 } 293 294 // Disallow links to admin, includes, and content. 295 if ( /\/wp-(admin|includes|content)(\/|$)/.test( element.pathname ) ) { 296 return false; 297 } 298 299 return true; 300 }; 301 302 /** 303 * Inject the customize_changeset_uuid query param into links on the frontend. 304 * 305 * @since 4.7.0 306 * @access protected 307 * 308 * @param {HTMLAnchorElement|HTMLAreaElement} element Link element. 309 * @param {object} element.search Query string. 310 * @returns {void} 311 */ 312 api.prepareLinkPreview = function prepareLinkPreview( element ) { 313 var queryParams; 314 315 // Skip links in admin bar. 316 if ( $( element ).closest( '#wpadminbar' ).length ) { 317 return; 318 } 319 320 // Ignore links with href="#" or href="#id". 321 if ( '#' === $( element ).attr( 'href' ).substr( 0, 1 ) ) { 322 return; 323 } 324 325 // Make sure links in preview use HTTPS if parent frame uses HTTPS. 326 if ( 'https' === api.preview.scheme.get() && 'http:' === element.protocol && -1 !== api.settings.url.allowedHosts.indexOf( element.hostname ) ) { 327 element.protocol = 'https:'; 328 } 329 330 if ( ! api.isLinkPreviewable( element ) ) { 331 $( element ).addClass( 'customize-unpreviewable' ); 332 return; 333 } 334 $( element ).removeClass( 'customize-unpreviewable' ); 335 336 queryParams = api.utils.parseQueryString( element.search.substring( 1 ) ); 337 queryParams.customize_changeset_uuid = api.settings.changeset.uuid; 338 if ( ! api.settings.theme.active ) { 339 queryParams.customize_theme = api.settings.theme.stylesheet; 340 } 341 if ( api.settings.channel ) { 342 queryParams.customize_messenger_channel = api.settings.channel; 343 } 344 element.search = $.param( queryParams ); 345 346 // Prevent links from breaking out of preview iframe. 347 if ( api.settings.channel ) { 348 element.target = '_self'; 349 } 350 }; 351 352 /** 353 * Inject the changeset UUID into Ajax requests. 354 * 355 * @since 4.7.0 356 * @access protected 357 * 358 * @return {void} 359 */ 360 api.addRequestPreviewing = function addRequestPreviewing() { 361 362 /** 363 * Rewrite Ajax requests to inject customizer state. 364 * 365 * @param {object} options Options. 366 * @param {string} options.type Type. 367 * @param {string} options.url URL. 368 * @param {object} originalOptions Original options. 369 * @param {XMLHttpRequest} xhr XHR. 370 * @returns {void} 371 */ 372 var prefilterAjax = function( options, originalOptions, xhr ) { 373 var urlParser, queryParams, requestMethod, dirtyValues = {}; 374 urlParser = document.createElement( 'a' ); 375 urlParser.href = options.url; 376 377 // Abort if the request is not for this site. 378 if ( ! api.isLinkPreviewable( urlParser, { allowAdminAjax: true } ) ) { 379 return; 380 } 381 queryParams = api.utils.parseQueryString( urlParser.search.substring( 1 ) ); 382 383 // Note that _dirty flag will be cleared with changeset updates. 384 api.each( function( setting ) { 385 if ( setting._dirty ) { 386 dirtyValues[ setting.id ] = setting.get(); 387 } 388 } ); 389 390 if ( ! _.isEmpty( dirtyValues ) ) { 391 requestMethod = options.type.toUpperCase(); 392 393 // Override underlying request method to ensure unsaved changes to changeset can be included (force Backbone.emulateHTTP). 394 if ( 'POST' !== requestMethod ) { 395 xhr.setRequestHeader( 'X-HTTP-Method-Override', requestMethod ); 396 queryParams._method = requestMethod; 397 options.type = 'POST'; 398 } 399 400 // Amend the post data with the customized values. 401 if ( options.data ) { 402 options.data += '&'; 403 } else { 404 options.data = ''; 405 } 406 options.data += $.param( { 407 customized: JSON.stringify( dirtyValues ) 408 } ); 409 } 410 411 // Include customized state query params in URL. 412 queryParams.customize_changeset_uuid = api.settings.changeset.uuid; 413 if ( ! api.settings.theme.active ) { 414 queryParams.customize_theme = api.settings.theme.stylesheet; 415 } 416 urlParser.search = $.param( queryParams ); 417 options.url = urlParser.href; 418 }; 419 420 $.ajaxPrefilter( prefilterAjax ); 421 }; 422 423 /** 424 * Inject changeset UUID into forms, allowing preview to persist through submissions. 425 * 426 * @since 4.7.0 427 * @access protected 428 * 429 * @returns {void} 430 */ 431 api.addFormPreviewing = function addFormPreviewing() { 432 433 // Inject inputs for forms in initial document. 434 $( document.body ).find( 'form' ).each( function() { 435 api.prepareFormPreview( this ); 436 } ); 437 438 // Inject inputs for new forms added to the page. 439 if ( 'undefined' !== typeof MutationObserver ) { 440 api.mutationObserver = new MutationObserver( function( mutations ) { 441 _.each( mutations, function( mutation ) { 442 $( mutation.target ).find( 'form' ).each( function() { 443 api.prepareFormPreview( this ); 444 } ); 445 } ); 446 } ); 447 api.mutationObserver.observe( document.documentElement, { 448 childList: true, 449 subtree: true 450 } ); 451 } 452 }; 453 454 /** 455 * Inject changeset into form inputs. 456 * 457 * @since 4.7.0 458 * @access protected 459 * 460 * @param {HTMLFormElement} form Form. 461 * @returns {void} 462 */ 463 api.prepareFormPreview = function prepareFormPreview( form ) { 464 var urlParser, stateParams = {}; 465 466 if ( ! form.action ) { 467 form.action = location.href; 468 } 469 470 urlParser = document.createElement( 'a' ); 471 urlParser.href = form.action; 472 473 // Make sure forms in preview use HTTPS if parent frame uses HTTPS. 474 if ( 'https' === api.preview.scheme.get() && 'http:' === urlParser.protocol && -1 !== api.settings.url.allowedHosts.indexOf( urlParser.hostname ) ) { 475 urlParser.protocol = 'https:'; 476 form.action = urlParser.href; 477 } 478 479 if ( 'GET' !== form.method.toUpperCase() || ! api.isLinkPreviewable( urlParser ) ) { 480 $( form ).addClass( 'customize-unpreviewable' ); 481 return; 482 } 483 $( form ).removeClass( 'customize-unpreviewable' ); 484 485 stateParams.customize_changeset_uuid = api.settings.changeset.uuid; 486 if ( ! api.settings.theme.active ) { 487 stateParams.customize_theme = api.settings.theme.stylesheet; 488 } 489 if ( api.settings.channel ) { 490 stateParams.customize_messenger_channel = api.settings.channel; 491 } 492 493 _.each( stateParams, function( value, name ) { 494 var input = $( form ).find( 'input[name="' + name + '"]' ); 495 if ( input.length ) { 496 input.val( value ); 497 } else { 498 $( form ).prepend( $( '<input>', { 499 type: 'hidden', 500 name: name, 501 value: value 502 } ) ); 503 } 504 } ); 505 506 // Prevent links from breaking out of preview iframe. 507 if ( api.settings.channel ) { 508 form.target = '_self'; 509 } 510 }; 511 512 /** 513 * Watch current URL and send keep-alive (heartbeat) messages to the parent. 514 * 515 * Keep the customizer pane notified that the preview is still alive 516 * and that the user hasn't navigated to a non-customized URL. 517 * 518 * @since 4.7.0 519 * @access protected 520 */ 521 api.keepAliveCurrentUrl = ( function() { 522 var previousPathName = location.pathname, 523 previousQueryString = location.search.substr( 1 ), 524 previousQueryParams = null, 525 stateQueryParams = [ 'customize_theme', 'customize_changeset_uuid', 'customize_messenger_channel' ]; 526 527 return function keepAliveCurrentUrl() { 528 var urlParser, currentQueryParams; 529 530 // Short-circuit with keep-alive if previous URL is identical (as is normal case). 531 if ( previousQueryString === location.search.substr( 1 ) && previousPathName === location.pathname ) { 532 api.preview.send( 'keep-alive' ); 533 return; 534 } 535 536 urlParser = document.createElement( 'a' ); 537 if ( null === previousQueryParams ) { 538 urlParser.search = previousQueryString; 539 previousQueryParams = api.utils.parseQueryString( previousQueryString ); 540 _.each( stateQueryParams, function( name ) { 541 delete previousQueryParams[ name ]; 542 } ); 543 } 544 545 // Determine if current URL minus customized state params and URL hash. 546 urlParser.href = location.href; 547 currentQueryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) ); 548 _.each( stateQueryParams, function( name ) { 549 delete currentQueryParams[ name ]; 550 } ); 551 552 if ( previousPathName !== location.pathname || ! _.isEqual( previousQueryParams, currentQueryParams ) ) { 553 urlParser.search = $.param( currentQueryParams ); 554 urlParser.hash = ''; 555 api.settings.url.self = urlParser.href; 556 api.preview.send( 'ready', { 557 currentUrl: api.settings.url.self, 558 activePanels: api.settings.activePanels, 559 activeSections: api.settings.activeSections, 560 activeControls: api.settings.activeControls, 561 settingValidities: api.settings.settingValidities 562 } ); 563 } else { 564 api.preview.send( 'keep-alive' ); 565 } 566 previousQueryParams = currentQueryParams; 567 previousQueryString = location.search.substr( 1 ); 568 previousPathName = location.pathname; 569 }; 570 } )(); 107 571 108 572 $( function() { … … 118 582 channel: api.settings.channel 119 583 }); 584 585 api.addLinkPreviewing(); 586 api.addRequestPreviewing(); 587 api.addFormPreviewing(); 120 588 121 589 /** … … 172 640 173 641 api.preview.send( 'documentTitle', document.title ); 642 643 // Send scroll in case of loading via non-refresh. 644 api.preview.send( 'scroll', $( window ).scrollTop() ); 174 645 }); 175 646 176 647 api.preview.bind( 'saved', function( response ) { 648 649 if ( response.next_changeset_uuid ) { 650 api.settings.changeset.uuid = response.next_changeset_uuid; 651 652 // Update UUIDs in links and forms. 653 $( document.body ).find( 'a[href], area' ).each( function() { 654 api.prepareLinkPreview( this ); 655 } ); 656 $( document.body ).find( 'form' ).each( function() { 657 api.prepareFormPreview( this ); 658 } ); 659 660 /* 661 * Replace the UUID in the URL. Note that the wrapped history.replaceState() 662 * will handle injecting the current api.settings.changeset.uuid into the URL, 663 * so this is merely to trigger that logic. 664 */ 665 if ( history.replaceState ) { 666 history.replaceState( currentHistoryState, '', location.href ); 667 } 668 } 669 177 670 api.trigger( 'saved', response ); 178 671 } ); 179 672 180 api.bind( 'saved', function() { 181 api.each( function( setting ) { 182 setting._dirty = false; 673 /* 674 * Clear dirty flag for settings when saved to changeset so that they 675 * won't be needlessly included in selective refresh or ajax requests. 676 */ 677 api.preview.bind( 'changeset-saved', function( data ) { 678 _.each( data.saved_changeset_values, function( value, settingId ) { 679 var setting = api( settingId ); 680 if ( setting && _.isEqual( setting.get(), value ) ) { 681 setting._dirty = false; 682 } 183 683 } ); 184 684 } ); … … 193 693 */ 194 694 api.preview.send( 'ready', { 695 currentUrl: api.settings.url.self, 195 696 activePanels: api.settings.activePanels, 196 697 activeSections: api.settings.activeSections, … … 198 699 settingValidities: api.settings.settingValidities 199 700 } ); 701 702 // Send ready when URL changes via JS. 703 setInterval( api.keepAliveCurrentUrl, api.settings.timeouts.keepAliveSend ); 200 704 201 705 // Display a loading indicator when preview is reloading, and remove on failure. -
trunk/src/wp-includes/js/customize-selective-refresh.js
r38649 r38810 12 12 l10n: { 13 13 shiftClickToEdit: '' 14 }, 15 refreshBuffer: 250 14 } 16 15 }, 17 16 currentRequest: null … … 486 485 wp_customize: 'on', 487 486 nonce: api.settings.nonce.preview, 488 theme: api.settings.theme.stylesheet, 489 customized: JSON.stringify( dirtyCustomized ) 487 customize_theme: api.settings.theme.stylesheet, 488 customized: JSON.stringify( dirtyCustomized ), 489 customize_changeset_uuid: api.settings.changeset.uuid 490 490 }; 491 491 }; … … 669 669 } ); 670 670 }, 671 self.data.refreshBuffer671 api.settings.timeouts.selectiveRefresh 672 672 ); 673 673 -
trunk/src/wp-includes/post.php
r38798 r38810 110 110 'delete_with_user' => false, 111 111 'query_var' => false, 112 ) ); 113 114 register_post_type( 'customize_changeset', array( 115 'labels' => array( 116 'name' => _x( 'Changesets', 'post type general name' ), 117 'singular_name' => _x( 'Changeset', 'post type singular name' ), 118 'menu_name' => _x( 'Changesets', 'admin menu' ), 119 'name_admin_bar' => _x( 'Changeset', 'add new on admin bar' ), 120 'add_new' => _x( 'Add New', 'Customize Changeset' ), 121 'add_new_item' => __( 'Add New Changeset' ), 122 'new_item' => __( 'New Changeset' ), 123 'edit_item' => __( 'Edit Changeset' ), 124 'view_item' => __( 'View Changeset' ), 125 'all_items' => __( 'All Changesets' ), 126 'search_items' => __( 'Search Changesets' ), 127 'not_found' => __( 'No changesets found.' ), 128 'not_found_in_trash' => __( 'No changesets found in Trash.' ), 129 ), 130 'public' => false, 131 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ 132 'map_meta_cap' => true, 133 'hierarchical' => false, 134 'rewrite' => false, 135 'query_var' => false, 136 'can_export' => false, 137 'delete_with_user' => false, 138 'supports' => array( 'title', 'author' ), 139 'capability_type' => 'customize_changeset', 140 'capabilities' => array( 141 'create_posts' => 'customize', 142 'delete_others_posts' => 'customize', 143 'delete_post' => 'customize', 144 'delete_posts' => 'customize', 145 'delete_private_posts' => 'customize', 146 'delete_published_posts' => 'customize', 147 'edit_others_posts' => 'customize', 148 'edit_post' => 'customize', 149 'edit_posts' => 'customize', 150 'edit_private_posts' => 'customize', 151 'edit_published_posts' => 'do_not_allow', 152 'publish_posts' => 'customize', 153 'read' => 'read', 154 'read_post' => 'customize', 155 'read_private_posts' => 'customize', 156 ), 112 157 ) ); 113 158 -
trunk/src/wp-includes/script-loader.php
r38797 r38810 451 451 $scripts->add( 'customize-base', "/wp-includes/js/customize-base$suffix.js", array( 'jquery', 'json2', 'underscore' ), false, 1 ); 452 452 $scripts->add( 'customize-loader', "/wp-includes/js/customize-loader$suffix.js", array( 'customize-base' ), false, 1 ); 453 $scripts->add( 'customize-preview', "/wp-includes/js/customize-preview$suffix.js", array( ' customize-base' ), false, 1 );453 $scripts->add( 'customize-preview', "/wp-includes/js/customize-preview$suffix.js", array( 'wp-a11y', 'customize-base' ), false, 1 ); 454 454 $scripts->add( 'customize-models', "/wp-includes/js/customize-models.js", array( 'underscore', 'backbone' ), false, 1 ); 455 455 $scripts->add( 'customize-views', "/wp-includes/js/customize-views.js", array( 'jquery', 'underscore', 'imgareaselect', 'customize-models', 'media-editor', 'media-views' ), false, 1 ); -
trunk/src/wp-includes/theme.php
r38649 r38810 2067 2067 * 2068 2068 * Loads the Customizer at plugins_loaded when accessing the customize.php admin 2069 * page or when any request includes a wp_customize=on param , either as a GET2070 * query var or as POST data. This param is a signal for whether to bootstrap2071 * the Customizer whenWordPress is loading, especially in the Customizer preview2069 * page or when any request includes a wp_customize=on param or a customize_changeset 2070 * param (a UUID). This param is a signal for whether to bootstrap the Customizer when 2071 * WordPress is loading, especially in the Customizer preview 2072 2072 * or when making Customizer Ajax requests for widgets or menus. 2073 2073 * … … 2077 2077 */ 2078 2078 function _wp_customize_include() { 2079 if ( ! ( ( isset( $_REQUEST['wp_customize'] ) && 'on' == $_REQUEST['wp_customize'] ) 2080 || ( is_admin() && 'customize.php' == basename( $_SERVER['PHP_SELF'] ) ) 2081 ) ) { 2079 2080 $is_customize_admin_page = ( is_admin() && 'customize.php' == basename( $_SERVER['PHP_SELF'] ) ); 2081 $should_include = ( 2082 $is_customize_admin_page 2083 || 2084 ( isset( $_REQUEST['wp_customize'] ) && 'on' == $_REQUEST['wp_customize'] ) 2085 || 2086 ( ! empty( $_GET['customize_changeset_uuid'] ) || ! empty( $_POST['customize_changeset_uuid'] ) ) 2087 ); 2088 2089 if ( ! $should_include ) { 2082 2090 return; 2083 2091 } 2084 2092 2085 require_once ABSPATH . WPINC . '/class-wp-customize-manager.php'; 2086 $GLOBALS['wp_customize'] = new WP_Customize_Manager(); 2093 /* 2094 * Note that wp_unslash() is not being used on the input vars because it is 2095 * called before wp_magic_quotes() gets called. Besides this fact, none of 2096 * the values should contain any characters needing slashes anyway. 2097 */ 2098 $keys = array( 'changeset_uuid', 'customize_changeset_uuid', 'customize_theme', 'theme', 'customize_messenger_channel' ); 2099 $input_vars = array_merge( 2100 wp_array_slice_assoc( $_GET, $keys ), 2101 wp_array_slice_assoc( $_POST, $keys ) 2102 ); 2103 2104 $theme = null; 2105 $changeset_uuid = null; 2106 $messenger_channel = null; 2107 2108 if ( $is_customize_admin_page && isset( $input_vars['changeset_uuid'] ) ) { 2109 $changeset_uuid = sanitize_key( $input_vars['changeset_uuid'] ); 2110 } elseif ( ! empty( $input_vars['customize_changeset_uuid'] ) ) { 2111 $changeset_uuid = sanitize_key( $input_vars['customize_changeset_uuid'] ); 2112 } 2113 2114 // Note that theme will be sanitized via WP_Theme. 2115 if ( $is_customize_admin_page && isset( $input_vars['theme'] ) ) { 2116 $theme = $input_vars['theme']; 2117 } elseif ( isset( $input_vars['customize_theme'] ) ) { 2118 $theme = $input_vars['customize_theme']; 2119 } 2120 if ( isset( $input_vars['customize_messenger_channel'] ) ) { 2121 $messenger_channel = sanitize_key( $input_vars['customize_messenger_channel'] ); 2122 } 2123 2124 require_once ABSPATH . WPINC . '/class-wp-customize-manager.php'; 2125 $GLOBALS['wp_customize'] = new WP_Customize_Manager( compact( 'changeset_uuid', 'theme', 'messenger_channel' ) ); 2126 } 2127 2128 /** 2129 * Publish a snapshot's changes. 2130 * 2131 * @param string $new_status New post status. 2132 * @param string $old_status Old post status. 2133 * @param WP_Post $changeset_post Changeset post object. 2134 */ 2135 function _wp_customize_publish_changeset( $new_status, $old_status, $changeset_post ) { 2136 global $wp_customize; 2137 2138 $is_publishing_changeset = ( 2139 'customize_changeset' === $changeset_post->post_type 2140 && 2141 'publish' === $new_status 2142 && 2143 'publish' !== $old_status 2144 ); 2145 if ( ! $is_publishing_changeset ) { 2146 return; 2147 } 2148 2149 if ( empty( $wp_customize ) ) { 2150 require_once ABSPATH . WPINC . '/class-wp-customize-manager.php'; 2151 $wp_customize = new WP_Customize_Manager( $changeset_post->post_name ); 2152 } 2153 2154 if ( ! did_action( 'customize_register' ) ) { 2155 /* 2156 * When running from CLI or Cron, the customize_register action will need 2157 * to be triggered in order for core, themes, and plugins to register their 2158 * settings. Normally core will add_action( 'customize_register' ) at 2159 * priority 10 to register the core settings, and if any themes/plugins 2160 * also add_action( 'customize_register' ) at the same priority, they 2161 * will have a $wp_customize with those settings registered since they 2162 * call add_action() afterward, normally. However, when manually doing 2163 * the customize_register action after the setup_theme, then the order 2164 * will be reversed for two actions added at priority 10, resulting in 2165 * the core settings no longer being available as expected to themes/plugins. 2166 * So the following manually calls the method that registers the core 2167 * settings up front before doing the action. 2168 */ 2169 remove_action( 'customize_register', array( $wp_customize, 'register_controls' ) ); 2170 $wp_customize->register_controls(); 2171 2172 /** This filter is documented in /wp-includes/class-wp-customize-manager.php */ 2173 do_action( 'customize_register', $wp_customize ); 2174 } 2175 $wp_customize->_publish_changeset_values( $changeset_post->ID ) ; 2176 2177 /* 2178 * Trash the changeset post if revisions are not enabled. Unpublished 2179 * changesets by default get garbage collected due to the auto-draft status. 2180 * When a changeset post is published, however, it would no longer get cleaned 2181 * out. Ths is a problem when the changeset posts are never displayed anywhere, 2182 * since they would just be endlessly piling up. So here we use the revisions 2183 * feature to indicate whether or not a published changeset should get trashed 2184 * and thus garbage collected. 2185 */ 2186 if ( ! wp_revisions_enabled( $changeset_post ) ) { 2187 wp_trash_post( $changeset_post->ID ); 2188 } 2189 } 2190 2191 /** 2192 * Filters changeset post data upon insert to ensure post_name is intact. 2193 * 2194 * This is needed to prevent the post_name from being dropped when the post is 2195 * transitioned into pending status by a contributor. 2196 * 2197 * @since 4.7.0 2198 * @see wp_insert_post() 2199 * 2200 * @param array $post_data An array of slashed post data. 2201 * @param array $supplied_post_data An array of sanitized, but otherwise unmodified post data. 2202 * @returns array Filtered data. 2203 */ 2204 function _wp_customize_changeset_filter_insert_post_data( $post_data, $supplied_post_data ) { 2205 if ( isset( $post_data['post_type'] ) && 'customize_changeset' === $post_data['post_type'] ) { 2206 2207 // Prevent post_name from being dropped, such as when contributor saves a changeset post as pending. 2208 if ( empty( $post_data['post_name'] ) && ! empty( $supplied_post_data['post_name'] ) ) { 2209 $post_data['post_name'] = $supplied_post_data['post_name']; 2210 } 2211 } 2212 return $post_data; 2087 2213 } 2088 2214 -
trunk/tests/phpunit/tests/adminbar.php
r38708 r38810 551 551 $this->assertNull( $node ); 552 552 } 553 554 /** 555 * @ticket 30937 556 * @covers wp_admin_bar_customize_menu() 557 */ 558 public function test_customize_link() { 559 global $wp_customize; 560 require_once ABSPATH . WPINC . '/class-wp-customize-manager.php'; 561 $uuid = wp_generate_uuid4(); 562 $this->go_to( home_url( "/?customize_changeset_uuid=$uuid" ) ); 563 wp_set_current_user( self::$admin_id ); 564 565 $this->factory()->post->create( array( 566 'post_type' => 'customize_changeset', 567 'post_status' => 'auto-draft', 568 'post_name' => $uuid, 569 ) ); 570 $wp_customize = new WP_Customize_Manager( array( 571 'changeset_uuid' => $uuid, 572 ) ); 573 $wp_customize->start_previewing_theme(); 574 575 set_current_screen( 'front' ); 576 $wp_admin_bar = $this->get_standard_admin_bar(); 577 $node = $wp_admin_bar->get_node( 'customize' ); 578 $this->assertNotEmpty( $node ); 579 580 $parsed_url = wp_parse_url( $node->href ); 581 $query_params = array(); 582 wp_parse_str( $parsed_url['query'], $query_params ); 583 $this->assertEquals( $uuid, $query_params['changeset_uuid'] ); 584 $this->assertNotContains( 'changeset_uuid', $query_params['url'] ); 585 } 553 586 } -
trunk/tests/phpunit/tests/customize/manager.php
r38765 r38810 28 28 29 29 /** 30 * Admin user ID. 31 * 32 * @var int 33 */ 34 protected static $admin_user_id; 35 36 /** 37 * Subscriber user ID. 38 * 39 * @var int 40 */ 41 protected static $subscriber_user_id; 42 43 /** 44 * Set up before class. 45 * 46 * @param WP_UnitTest_Factory $factory Factory. 47 */ 48 public static function wpSetUpBeforeClass( $factory ) { 49 self::$subscriber_user_id = $factory->user->create( array( 'role' => 'subscriber' ) ); 50 self::$admin_user_id = $factory->user->create( array( 'role' => 'administrator' ) ); 51 } 52 53 /** 30 54 * Set up test. 31 55 */ … … 43 67 $this->manager = null; 44 68 unset( $GLOBALS['wp_customize'] ); 69 $_REQUEST = array(); 45 70 parent::tearDown(); 46 71 } … … 54 79 $GLOBALS['wp_customize'] = new WP_Customize_Manager(); 55 80 return $GLOBALS['wp_customize']; 81 } 82 83 /** 84 * Test WP_Customize_Manager::__construct(). 85 * 86 * @covers WP_Customize_Manager::__construct() 87 */ 88 function test_constructor() { 89 $uuid = wp_generate_uuid4(); 90 $theme = 'twentyfifteen'; 91 $messenger_channel = 'preview-123'; 92 $wp_customize = new WP_Customize_Manager( array( 93 'changeset_uuid' => $uuid, 94 'theme' => $theme, 95 'messenger_channel' => $messenger_channel, 96 ) ); 97 $this->assertEquals( $uuid, $wp_customize->changeset_uuid() ); 98 $this->assertEquals( $theme, $wp_customize->get_stylesheet() ); 99 $this->assertEquals( $messenger_channel, $wp_customize->get_messenger_channel() ); 100 101 $theme = 'twentyfourteen'; 102 $messenger_channel = 'preview-456'; 103 $_REQUEST['theme'] = $theme; 104 $_REQUEST['customize_messenger_channel'] = $messenger_channel; 105 $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) ); 106 $this->assertEquals( $theme, $wp_customize->get_stylesheet() ); 107 $this->assertEquals( $messenger_channel, $wp_customize->get_messenger_channel() ); 108 109 $theme = 'twentyfourteen'; 110 $_REQUEST['customize_theme'] = $theme; 111 $wp_customize = new WP_Customize_Manager(); 112 $this->assertEquals( $theme, $wp_customize->get_stylesheet() ); 113 $this->assertNotEmpty( $wp_customize->changeset_uuid() ); 114 } 115 116 /** 117 * Test WP_Customize_Manager::setup_theme() for admin screen. 118 * 119 * @covers WP_Customize_Manager::setup_theme() 120 */ 121 function test_setup_theme_in_customize_admin() { 122 global $pagenow, $wp_customize; 123 $pagenow = 'customize.php'; 124 set_current_screen( 'customize' ); 125 126 // Unauthorized. 127 $exception = null; 128 $wp_customize = new WP_Customize_Manager(); 129 wp_set_current_user( self::$subscriber_user_id ); 130 try { 131 $wp_customize->setup_theme(); 132 } catch ( Exception $e ) { 133 $exception = $e; 134 } 135 $this->assertInstanceOf( 'WPDieException', $exception ); 136 $this->assertContains( 'you are not allowed to customize this site', $exception->getMessage() ); 137 138 // Bad changeset. 139 $exception = null; 140 wp_set_current_user( self::$admin_user_id ); 141 $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => 'bad' ) ); 142 try { 143 $wp_customize->setup_theme(); 144 } catch ( Exception $e ) { 145 $exception = $e; 146 } 147 $this->assertInstanceOf( 'WPDieException', $exception ); 148 $this->assertContains( 'Invalid changeset UUID', $exception->getMessage() ); 149 150 $wp_customize = new WP_Customize_Manager(); 151 $wp_customize->setup_theme(); 152 } 153 154 /** 155 * Test WP_Customize_Manager::setup_theme() for frontend. 156 * 157 * @covers WP_Customize_Manager::setup_theme() 158 */ 159 function test_setup_theme_in_frontend() { 160 global $wp_customize, $pagenow, $show_admin_bar; 161 $pagenow = 'front'; 162 set_current_screen( 'front' ); 163 164 wp_set_current_user( 0 ); 165 $exception = null; 166 $wp_customize = new WP_Customize_Manager(); 167 wp_set_current_user( self::$subscriber_user_id ); 168 try { 169 $wp_customize->setup_theme(); 170 } catch ( Exception $e ) { 171 $exception = $e; 172 } 173 $this->assertInstanceOf( 'WPDieException', $exception ); 174 $this->assertContains( 'Non-existent changeset UUID', $exception->getMessage() ); 175 176 wp_set_current_user( self::$admin_user_id ); 177 $wp_customize = new WP_Customize_Manager( array( 'messenger_channel' => 'preview-1' ) ); 178 $wp_customize->setup_theme(); 179 $this->assertFalse( $show_admin_bar ); 180 181 show_admin_bar( true ); 182 wp_set_current_user( self::$admin_user_id ); 183 $wp_customize = new WP_Customize_Manager( array( 'messenger_channel' => null ) ); 184 $wp_customize->setup_theme(); 185 $this->assertTrue( $show_admin_bar ); 186 } 187 188 /** 189 * Test WP_Customize_Manager::changeset_uuid(). 190 * 191 * @ticket 30937 192 * @covers WP_Customize_Manager::changeset_uuid() 193 */ 194 function test_changeset_uuid() { 195 $uuid = wp_generate_uuid4(); 196 $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) ); 197 $this->assertEquals( $uuid, $wp_customize->changeset_uuid() ); 198 } 199 200 /** 201 * Test WP_Customize_Manager::wp_loaded(). 202 * 203 * Ensure that post values are previewed even without being in preview. 204 * 205 * @ticket 30937 206 * @covers WP_Customize_Manager::wp_loaded() 207 */ 208 function test_wp_loaded() { 209 wp_set_current_user( self::$admin_user_id ); 210 $wp_customize = new WP_Customize_Manager(); 211 $title = 'Hello World'; 212 $wp_customize->set_post_value( 'blogname', $title ); 213 $this->assertNotEquals( $title, get_option( 'blogname' ) ); 214 $wp_customize->wp_loaded(); 215 $this->assertFalse( $wp_customize->is_preview() ); 216 $this->assertEquals( $title, $wp_customize->get_setting( 'blogname' )->value() ); 217 $this->assertEquals( $title, get_option( 'blogname' ) ); 218 } 219 220 /** 221 * Test WP_Customize_Manager::find_changeset_post_id(). 222 * 223 * @ticket 30937 224 * @covers WP_Customize_Manager::find_changeset_post_id() 225 */ 226 function test_find_changeset_post_id() { 227 $uuid = wp_generate_uuid4(); 228 $post_id = $this->factory()->post->create( array( 229 'post_name' => $uuid, 230 'post_type' => 'customize_changeset', 231 'post_status' => 'auto-draft', 232 'post_content' => '{}', 233 ) ); 234 235 $wp_customize = new WP_Customize_Manager(); 236 $this->assertNull( $wp_customize->find_changeset_post_id( wp_generate_uuid4() ) ); 237 $this->assertEquals( $post_id, $wp_customize->find_changeset_post_id( $uuid ) ); 238 } 239 240 /** 241 * Test WP_Customize_Manager::changeset_post_id(). 242 * 243 * @ticket 30937 244 * @covers WP_Customize_Manager::changeset_post_id() 245 */ 246 function test_changeset_post_id() { 247 $uuid = wp_generate_uuid4(); 248 $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) ); 249 $this->assertNull( $wp_customize->changeset_post_id() ); 250 251 $uuid = wp_generate_uuid4(); 252 $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) ); 253 $post_id = $this->factory()->post->create( array( 254 'post_name' => $uuid, 255 'post_type' => 'customize_changeset', 256 'post_status' => 'auto-draft', 257 'post_content' => '{}', 258 ) ); 259 $this->assertEquals( $post_id, $wp_customize->changeset_post_id() ); 260 } 261 262 /** 263 * Test WP_Customize_Manager::changeset_data(). 264 * 265 * @ticket 30937 266 * @covers WP_Customize_Manager::changeset_data() 267 */ 268 function test_changeset_data() { 269 $uuid = wp_generate_uuid4(); 270 $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) ); 271 $this->assertEquals( array(), $wp_customize->changeset_data() ); 272 273 $uuid = wp_generate_uuid4(); 274 $data = array( 'blogname' => array( 'value' => 'Hello World' ) ); 275 $this->factory()->post->create( array( 276 'post_name' => $uuid, 277 'post_type' => 'customize_changeset', 278 'post_status' => 'auto-draft', 279 'post_content' => wp_json_encode( $data ), 280 ) ); 281 $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) ); 282 $this->assertEquals( $data, $wp_customize->changeset_data() ); 283 } 284 285 /** 286 * Test WP_Customize_Manager::customize_preview_init(). 287 * 288 * @ticket 30937 289 * @covers WP_Customize_Manager::customize_preview_init() 290 */ 291 function test_customize_preview_init() { 292 293 // Test authorized admin user. 294 wp_set_current_user( self::$admin_user_id ); 295 $did_action_customize_preview_init = did_action( 'customize_preview_init' ); 296 $wp_customize = new WP_Customize_Manager(); 297 $wp_customize->customize_preview_init(); 298 $this->assertEquals( $did_action_customize_preview_init + 1, did_action( 'customize_preview_init' ) ); 299 300 $this->assertEquals( 10, has_action( 'wp_head', 'wp_no_robots' ) ); 301 $this->assertEquals( 10, has_filter( 'wp_headers', array( $wp_customize, 'filter_iframe_security_headers' ) ) ); 302 $this->assertEquals( 10, has_filter( 'wp_redirect', array( $wp_customize, 'add_state_query_params' ) ) ); 303 $this->assertTrue( wp_script_is( 'customize-preview', 'enqueued' ) ); 304 $this->assertEquals( 10, has_action( 'wp_head', array( $wp_customize, 'customize_preview_loading_style' ) ) ); 305 $this->assertEquals( 20, has_action( 'wp_footer', array( $wp_customize, 'customize_preview_settings' ) ) ); 306 307 // Test unauthorized user outside preview (no messenger_channel). 308 wp_set_current_user( self::$subscriber_user_id ); 309 $wp_customize = new WP_Customize_Manager(); 310 $wp_customize->register_controls(); 311 $this->assertNotEmpty( $wp_customize->controls() ); 312 $wp_customize->customize_preview_init(); 313 $this->assertEmpty( $wp_customize->controls() ); 314 315 // Test unauthorized user inside preview (with messenger_channel). 316 wp_set_current_user( self::$subscriber_user_id ); 317 $wp_customize = new WP_Customize_Manager( array( 'messenger_channel' => 'preview-0' ) ); 318 $exception = null; 319 try { 320 $wp_customize->customize_preview_init(); 321 } catch ( WPDieException $e ) { 322 $exception = $e; 323 } 324 $this->assertNotNull( $exception ); 325 $this->assertContains( 'Unauthorized', $exception->getMessage() ); 326 } 327 328 /** 329 * Test WP_Customize_Manager::filter_iframe_security_headers(). 330 * 331 * @ticket 30937 332 * @covers WP_Customize_Manager::filter_iframe_security_headers() 333 */ 334 function test_filter_iframe_security_headers() { 335 $customize_url = admin_url( 'customize.php' ); 336 $wp_customize = new WP_Customize_Manager(); 337 $headers = $wp_customize->filter_iframe_security_headers( array() ); 338 $this->assertArrayHasKey( 'X-Frame-Options', $headers ); 339 $this->assertArrayHasKey( 'Content-Security-Policy', $headers ); 340 $this->assertEquals( "ALLOW-FROM $customize_url", $headers['X-Frame-Options'] ); 341 } 342 343 /** 344 * Test WP_Customize_Manager::add_state_query_params(). 345 * 346 * @ticket 30937 347 * @covers WP_Customize_Manager::add_state_query_params() 348 */ 349 function test_add_state_query_params() { 350 $uuid = wp_generate_uuid4(); 351 $messenger_channel = 'preview-0'; 352 $wp_customize = new WP_Customize_Manager( array( 353 'changeset_uuid' => $uuid, 354 'messenger_channel' => $messenger_channel, 355 ) ); 356 $url = $wp_customize->add_state_query_params( home_url( '/' ) ); 357 $parsed_url = wp_parse_url( $url ); 358 parse_str( $parsed_url['query'], $query_params ); 359 $this->assertArrayHasKey( 'customize_messenger_channel', $query_params ); 360 $this->assertArrayHasKey( 'customize_changeset_uuid', $query_params ); 361 $this->assertArrayNotHasKey( 'customize_theme', $query_params ); 362 $this->assertEquals( $uuid, $query_params['customize_changeset_uuid'] ); 363 $this->assertEquals( $messenger_channel, $query_params['customize_messenger_channel'] ); 364 365 $uuid = wp_generate_uuid4(); 366 $wp_customize = new WP_Customize_Manager( array( 367 'changeset_uuid' => $uuid, 368 'messenger_channel' => null, 369 'theme' => 'twentyfifteen', 370 ) ); 371 $url = $wp_customize->add_state_query_params( home_url( '/' ) ); 372 $parsed_url = wp_parse_url( $url ); 373 parse_str( $parsed_url['query'], $query_params ); 374 $this->assertArrayNotHasKey( 'customize_messenger_channel', $query_params ); 375 $this->assertArrayHasKey( 'customize_changeset_uuid', $query_params ); 376 $this->assertArrayHasKey( 'customize_theme', $query_params ); 377 $this->assertEquals( $uuid, $query_params['customize_changeset_uuid'] ); 378 $this->assertEquals( 'twentyfifteen', $query_params['customize_theme'] ); 379 380 $uuid = wp_generate_uuid4(); 381 $wp_customize = new WP_Customize_Manager( array( 382 'changeset_uuid' => $uuid, 383 'messenger_channel' => null, 384 'theme' => 'twentyfifteen', 385 ) ); 386 $url = $wp_customize->add_state_query_params( 'http://not-allowed.example.com/?q=1' ); 387 $parsed_url = wp_parse_url( $url ); 388 parse_str( $parsed_url['query'], $query_params ); 389 $this->assertArrayNotHasKey( 'customize_messenger_channel', $query_params ); 390 $this->assertArrayNotHasKey( 'customize_changeset_uuid', $query_params ); 391 $this->assertArrayNotHasKey( 'customize_theme', $query_params ); 392 } 393 394 /** 395 * Test WP_Customize_Manager::save_changeset_post(). 396 * 397 * @ticket 30937 398 * @covers WP_Customize_Manager::save_changeset_post() 399 */ 400 function test_save_changeset_post_without_theme_activation() { 401 wp_set_current_user( self::$admin_user_id ); 402 403 $did_action = array( 404 'customize_save_validation_before' => did_action( 'customize_save_validation_before' ), 405 'customize_save' => did_action( 'customize_save' ), 406 'customize_save_after' => did_action( 'customize_save_after' ), 407 ); 408 $uuid = wp_generate_uuid4(); 409 410 $manager = new WP_Customize_Manager( array( 411 'changeset_uuid' => $uuid, 412 ) ); 413 $manager->register_controls(); 414 $manager->set_post_value( 'blogname', 'Changeset Title' ); 415 $manager->set_post_value( 'blogdescription', 'Changeset Tagline' ); 416 417 $r = $manager->save_changeset_post( array( 418 'status' => 'auto-draft', 419 'title' => 'Auto Draft', 420 'date_gmt' => '2010-01-01 00:00:00', 421 'data' => array( 422 'blogname' => array( 423 'value' => 'Overridden Changeset Title', 424 ), 425 'blogdescription' => array( 426 'custom' => 'something', 427 ), 428 ), 429 ) ); 430 $this->assertInternalType( 'array', $r ); 431 432 $this->assertEquals( $did_action['customize_save_validation_before'] + 1, did_action( 'customize_save_validation_before' ) ); 433 434 $post_id = $manager->find_changeset_post_id( $uuid ); 435 $this->assertNotNull( $post_id ); 436 $saved_data = json_decode( get_post( $post_id )->post_content, true ); 437 $this->assertEquals( $manager->unsanitized_post_values(), wp_list_pluck( $saved_data, 'value' ) ); 438 $this->assertEquals( 'Overridden Changeset Title', $saved_data['blogname']['value'] ); 439 $this->assertEquals( 'something', $saved_data['blogdescription']['custom'] ); 440 $this->assertEquals( 'Auto Draft', get_post( $post_id )->post_title ); 441 $this->assertEquals( 'auto-draft', get_post( $post_id )->post_status ); 442 $this->assertEquals( '2010-01-01 00:00:00', get_post( $post_id )->post_date_gmt ); 443 $this->assertNotEquals( 'Changeset Title', get_option( 'blogname' ) ); 444 $this->assertArrayHasKey( 'setting_validities', $r ); 445 446 // Test saving with invalid settings, ensuring transaction blocked. 447 $previous_saved_data = $saved_data; 448 $manager->add_setting( 'foo_unauthorized', array( 449 'capability' => 'do_not_allow', 450 ) ); 451 $manager->add_setting( 'baz_illegal', array( 452 'validate_callback' => array( $this, 'return_illegal_error' ), 453 ) ); 454 $r = $manager->save_changeset_post( array( 455 'status' => 'auto-draft', 456 'data' => array( 457 'blogname' => array( 458 'value' => 'OK', 459 ), 460 'foo_unauthorized' => array( 461 'value' => 'No', 462 ), 463 'bar_unknown' => array( 464 'value' => 'No', 465 ), 466 'baz_illegal' => array( 467 'value' => 'No', 468 ), 469 ), 470 ) ); 471 $this->assertInstanceOf( 'WP_Error', $r ); 472 $this->assertEquals( 'transaction_fail', $r->get_error_code() ); 473 $this->assertInternalType( 'array', $r->get_error_data() ); 474 $this->assertArrayHasKey( 'setting_validities', $r->get_error_data() ); 475 $error_data = $r->get_error_data(); 476 $this->assertArrayHasKey( 'blogname', $error_data['setting_validities'] ); 477 $this->assertTrue( $error_data['setting_validities']['blogname'] ); 478 $this->assertArrayHasKey( 'foo_unauthorized', $error_data['setting_validities'] ); 479 $this->assertInstanceOf( 'WP_Error', $error_data['setting_validities']['foo_unauthorized'] ); 480 $this->assertEquals( 'unauthorized', $error_data['setting_validities']['foo_unauthorized']->get_error_code() ); 481 $this->assertArrayHasKey( 'bar_unknown', $error_data['setting_validities'] ); 482 $this->assertInstanceOf( 'WP_Error', $error_data['setting_validities']['bar_unknown'] ); 483 $this->assertEquals( 'unrecognized', $error_data['setting_validities']['bar_unknown']->get_error_code() ); 484 $this->assertArrayHasKey( 'baz_illegal', $error_data['setting_validities'] ); 485 $this->assertInstanceOf( 'WP_Error', $error_data['setting_validities']['baz_illegal'] ); 486 $this->assertEquals( 'illegal', $error_data['setting_validities']['baz_illegal']->get_error_code() ); 487 488 // Since transactional, ensure no changes have been made. 489 $this->assertEquals( $previous_saved_data, json_decode( get_post( $post_id )->post_content, true ) ); 490 491 // Attempt a non-transactional/incremental update. 492 $manager = new WP_Customize_Manager( array( 493 'changeset_uuid' => $uuid, 494 ) ); 495 $manager->register_controls(); // That is, register settings. 496 $r = $manager->save_changeset_post( array( 497 'status' => null, 498 'data' => array( 499 'blogname' => array( 500 'value' => 'Non-Transactional \o/ <script>unsanitized</script>', 501 ), 502 'bar_unknown' => array( 503 'value' => 'No', 504 ), 505 ), 506 ) ); 507 $this->assertInternalType( 'array', $r ); 508 $this->assertArrayHasKey( 'setting_validities', $r ); 509 $this->assertTrue( $r['setting_validities']['blogname'] ); 510 $this->assertInstanceOf( 'WP_Error', $r['setting_validities']['bar_unknown'] ); 511 $saved_data = json_decode( get_post( $post_id )->post_content, true ); 512 $this->assertNotEquals( $previous_saved_data, $saved_data ); 513 $this->assertEquals( 'Non-Transactional \o/ <script>unsanitized</script>', $saved_data['blogname']['value'] ); 514 515 // Ensure the filter applies. 516 $customize_changeset_save_data_call_count = $this->customize_changeset_save_data_call_count; 517 add_filter( 'customize_changeset_save_data', array( $this, 'filter_customize_changeset_save_data' ), 10, 2 ); 518 $manager->save_changeset_post( array( 519 'status' => null, 520 'data' => array( 521 'blogname' => array( 522 'value' => 'Filtered', 523 ), 524 ), 525 ) ); 526 $this->assertEquals( $customize_changeset_save_data_call_count + 1, $this->customize_changeset_save_data_call_count ); 527 528 // Publish the changeset. 529 $manager = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) ); 530 $manager->register_controls(); 531 $GLOBALS['wp_customize'] = $manager; 532 $r = $manager->save_changeset_post( array( 533 'status' => 'publish', 534 'data' => array( 535 'blogname' => array( 536 'value' => 'Do it live \o/', 537 ), 538 ), 539 ) ); 540 $this->assertInternalType( 'array', $r ); 541 $this->assertEquals( 'Do it live \o/', get_option( 'blogname' ) ); 542 $this->assertEquals( 'trash', get_post_status( $post_id ) ); // Auto-trashed. 543 544 // Test revisions.