<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[39181] trunk: Customize: Store modifying user ID with setting change written into changeset and restore current user when setting is being saved.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/39181">39181</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/39181","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>westonruter</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2016-11-09 07:02:53 +0000 (Wed, 09 Nov 2016)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Customize: Store modifying user ID with setting change written into changeset and restore current user when setting is being saved.

Restoring the current user context when saving a setting ensures filters apply as expected, such as Kses. When a user is not associated with a given setting change, continue to override `capability` to be `exist` when saving. Skip overwriting setting values in a changeset that have not changed, facilitating concurrent editing and amending a changeset by a user with fewer privileges.

See <a href="https://core.trac.wordpress.org/ticket/30937">#30937</a>.
Fixes <a href="https://core.trac.wordpress.org/ticket/38705">#38705</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesclasswpcustomizemanagerphp">trunk/src/wp-includes/class-wp-customize-manager.php</a></li>
<li><a href="#trunksrcwpincludesthemephp">trunk/src/wp-includes/theme.php</a></li>
<li><a href="#trunktestsphpunittestscustomizemanagerphp">trunk/tests/phpunit/tests/customize/manager.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesclasswpcustomizemanagerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-customize-manager.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-manager.php      2016-11-09 05:44:14 UTC (rev 39180)
+++ trunk/src/wp-includes/class-wp-customize-manager.php        2016-11-09 07:02:53 UTC (rev 39181)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1827,6 +1827,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *     @type string $status   Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed.
</span><span class="cx" style="display: block; padding: 0 10px">         *     @type string $title    Post title. Optional.
</span><span class="cx" style="display: block; padding: 0 10px">         *     @type string $date_gmt Date in GMT. Optional.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         *     @type int    $user_id  ID for user who is saving the changeset. Optional, defaults to the current user ID.
</ins><span class="cx" style="display: block; padding: 0 10px">          * }
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array|WP_Error Returns array on success and WP_Error with array data on error.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1839,11 +1840,16 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                'title' => null,
</span><span class="cx" style="display: block; padding: 0 10px">                                'data' => array(),
</span><span class="cx" style="display: block; padding: 0 10px">                                'date_gmt' => null,
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                'user_id' => get_current_user_id(),
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><span class="cx" style="display: block; padding: 0 10px">                        $args
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $changeset_post_id = $this->changeset_post_id();
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $existing_changeset_data = array();
+               if ( $changeset_post_id ) {
+                       $existing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // The request was made via wp.customize.previewer.save().
</span><span class="cx" style="display: block; padding: 0 10px">                $update_transactionally = (bool) $args['status'];
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1863,6 +1869,37 @@
</span><span class="cx" style="display: block; padding: 0 10px">                ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value.
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                /*
+                * Get list of IDs for settings that have values different from what is currently
+                * saved in the changeset. By skipping any values that are already the same, the
+                * subset of changed settings can be passed into validate_setting_values to prevent
+                * an underprivileged modifying a single setting for which they have the capability
+                * from being blocked from saving. This also prevents a user from touching of the
+                * previous saved settings and overriding the associated user_id if they made no change.
+                */
+               $changed_setting_ids = array();
+               foreach ( $post_values as $setting_id => $setting_value ) {
+                       $setting = $this->get_setting( $setting_id );
+
+                       if ( $setting && 'theme_mod' === $setting->type ) {
+                               $prefixed_setting_id = $this->get_stylesheet() . '::' . $setting->id;
+                       } else {
+                               $prefixed_setting_id = $setting_id;
+                       }
+
+                       $is_value_changed = (
+                               ! isset( $existing_changeset_data[ $prefixed_setting_id ] )
+                               ||
+                               ! array_key_exists( 'value', $existing_changeset_data[ $prefixed_setting_id ] )
+                               ||
+                               $existing_changeset_data[ $prefixed_setting_id ]['value'] !== $setting_value
+                       );
+                       if ( $is_value_changed ) {
+                               $changed_setting_ids[] = $setting_id;
+                       }
+               }
+               $post_values = wp_array_slice_assoc( $post_values, $changed_setting_ids );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><span class="cx" style="display: block; padding: 0 10px">                 * Fires before save validation happens.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1943,7 +1980,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                $data[ $changeset_setting_id ] = array_merge(
</span><span class="cx" style="display: block; padding: 0 10px">                                        $data[ $changeset_setting_id ],
</span><span class="cx" style="display: block; padding: 0 10px">                                        $setting_params,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        array( 'type' => $setting->type )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 array(
+                                               'type' => $setting->type,
+                                               'user_id' => $args['user_id'],
+                                       )
</ins><span class="cx" style="display: block; padding: 0 10px">                                 );
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2121,30 +2161,39 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $previous_changeset_data    = $this->_changeset_data;
</span><span class="cx" style="display: block; padding: 0 10px">                $this->_changeset_data      = $publishing_changeset_data;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Ensure that other theme mods are stashed.
-               $other_theme_mod_settings = array();
-               if ( did_action( 'switch_theme' ) ) {
-                       $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
-                       $matches = array();
-                       foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) {
-                               $is_other_theme_mod = (
-                                       isset( $setting_params['value'] )
-                                       &&
-                                       isset( $setting_params['type'] )
-                                       &&
-                                       'theme_mod' === $setting_params['type']
-                                       &&
-                                       preg_match( $namespace_pattern, $raw_setting_id, $matches )
-                                       &&
-                                       $this->get_stylesheet() !== $matches['stylesheet']
-                               );
-                               if ( $is_other_theme_mod ) {
-                                       if ( ! isset( $other_theme_mod_settings[ $matches['stylesheet'] ] ) ) {
-                                               $other_theme_mod_settings[ $matches['stylesheet'] ] = array();
-                                       }
-                                       $other_theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Parse changeset data to identify theme mod settings and user IDs associated with settings to be saved.
+               $setting_user_ids = array();
+               $theme_mod_settings = array();
+               $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
+               $matches = array();
+               foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) {
+                       $actual_setting_id = null;
+                       $is_theme_mod_setting = (
+                               isset( $setting_params['value'] )
+                               &&
+                               isset( $setting_params['type'] )
+                               &&
+                               'theme_mod' === $setting_params['type']
+                               &&
+                               preg_match( $namespace_pattern, $raw_setting_id, $matches )
+                       );
+                       if ( $is_theme_mod_setting ) {
+                               if ( ! isset( $theme_mod_settings[ $matches['stylesheet'] ] ) ) {
+                                       $theme_mod_settings[ $matches['stylesheet'] ] = array();
</ins><span class="cx" style="display: block; padding: 0 10px">                                 }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                $theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
+
+                               if ( $this->get_stylesheet() === $matches['stylesheet'] ) {
+                                       $actual_setting_id = $matches['setting_id'];
+                               }
+                       } else {
+                               $actual_setting_id = $raw_setting_id;
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                       // Keep track of the user IDs for settings actually for this theme.
+                       if ( $actual_setting_id && isset( $setting_params['user_id'] ) ) {
+                               $setting_user_ids[ $actual_setting_id ] = $setting_params['user_id'];
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $changeset_setting_values = $this->unsanitized_post_values( array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2173,21 +2222,38 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $original_setting_capabilities = array();
</span><span class="cx" style="display: block; padding: 0 10px">                foreach ( $changeset_setting_ids as $setting_id ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $setting = $this->get_setting( $setting_id );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( $setting ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( $setting && ! isset( $setting_user_ids[ $setting_id ] ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 $original_setting_capabilities[ $setting->id ] = $setting->capability;
</span><span class="cx" style="display: block; padding: 0 10px">                                $setting->capability = 'exist';
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $original_user_id = get_current_user_id();
</ins><span class="cx" style="display: block; padding: 0 10px">                 foreach ( $changeset_setting_ids as $setting_id ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $setting = $this->get_setting( $setting_id );
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( $setting ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                /*
+                                * Set the current user to match the user who saved the value into
+                                * the changeset so that any filters that apply during the save
+                                * process will respect the original user's capabilities. This
+                                * will ensure, for example, that KSES won't strip unsafe HTML
+                                * when a scheduled changeset publishes via WP Cron.
+                                */
+                               if ( isset( $setting_user_ids[ $setting_id ] ) ) {
+                                       wp_set_current_user( $setting_user_ids[ $setting_id ] );
+                               } else {
+                                       wp_set_current_user( $original_user_id );
+                               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                 $setting->save();
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                wp_set_current_user( $original_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Update the stashed theme mod settings, removing the active theme's stashed settings, if activated.
</span><span class="cx" style="display: block; padding: 0 10px">                if ( did_action( 'switch_theme' ) ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        $other_theme_mod_settings = $theme_mod_settings;
+                       unset( $other_theme_mod_settings[ $this->get_stylesheet() ] );
</ins><span class="cx" style="display: block; padding: 0 10px">                         $this->update_stashed_theme_mod_settings( $other_theme_mod_settings );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunksrcwpincludesthemephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/theme.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/theme.php   2016-11-09 05:44:14 UTC (rev 39180)
+++ trunk/src/wp-includes/theme.php     2016-11-09 07:02:53 UTC (rev 39181)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2564,7 +2564,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        if ( empty( $wp_customize ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                require_once ABSPATH . WPINC . '/class-wp-customize-manager.php';
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $wp_customize = new WP_Customize_Manager( $changeset_post->post_name );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $changeset_post->post_name ) );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        if ( ! did_action( 'customize_register' ) ) {
</span></span></pre></div>
<a id="trunktestsphpunittestscustomizemanagerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/customize/manager.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/customize/manager.php   2016-11-09 05:44:14 UTC (rev 39180)
+++ trunk/tests/phpunit/tests/customize/manager.php     2016-11-09 07:02:53 UTC (rev 39181)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -429,22 +429,24 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_customize = $manager = new WP_Customize_Manager( array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'changeset_uuid' => $uuid,
</span><span class="cx" style="display: block; padding: 0 10px">                ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $wp_customize = $manager;
</ins><span class="cx" style="display: block; padding: 0 10px">                 $manager->register_controls();
</span><span class="cx" style="display: block; padding: 0 10px">                $manager->set_post_value( 'blogname', 'Changeset Title' );
</span><span class="cx" style="display: block; padding: 0 10px">                $manager->set_post_value( 'blogdescription', 'Changeset Tagline' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $pre_saved_data = array(
+                       'blogname' => array(
+                               'value' => 'Overridden Changeset Title',
+                       ),
+                       'blogdescription' => array(
+                               'custom' => 'something',
+                       ),
+               );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $r = $manager->save_changeset_post( array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'status' => 'auto-draft',
</span><span class="cx" style="display: block; padding: 0 10px">                        'title' => 'Auto Draft',
</span><span class="cx" style="display: block; padding: 0 10px">                        'date_gmt' => '2010-01-01 00:00:00',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'data' => array(
-                               'blogname' => array(
-                                       'value' => 'Overridden Changeset Title',
-                               ),
-                               'blogdescription' => array(
-                                       'custom' => 'something',
-                               ),
-                       ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'data' => $pre_saved_data,
</ins><span class="cx" style="display: block; padding: 0 10px">                 ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertInternalType( 'array', $r );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -454,8 +456,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertNotNull( $post_id );
</span><span class="cx" style="display: block; padding: 0 10px">                $saved_data = json_decode( get_post( $post_id )->post_content, true );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $manager->unsanitized_post_values(), wp_list_pluck( $saved_data, 'value' ) );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertEquals( 'Overridden Changeset Title', $saved_data['blogname']['value'] );
-               $this->assertEquals( 'something', $saved_data['blogdescription']['custom'] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertEquals( $pre_saved_data['blogname']['value'], $saved_data['blogname']['value'] );
+               $this->assertEquals( $pre_saved_data['blogdescription']['custom'], $saved_data['blogdescription']['custom'] );
+               foreach ( $saved_data as $setting_id => $setting_params ) {
+                       $this->assertArrayHasKey( 'type', $setting_params );
+                       $this->assertEquals( 'option', $setting_params['type'] );
+                       $this->assertArrayHasKey( 'user_id', $setting_params );
+                       $this->assertEquals( self::$admin_user_id, $setting_params['user_id'] );
+               }
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertEquals( 'Auto Draft', get_post( $post_id )->post_title );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'auto-draft', get_post( $post_id )->post_status );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( '2010-01-01 00:00:00', get_post( $post_id )->post_date_gmt );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -511,6 +519,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_customize = $manager = new WP_Customize_Manager( array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'changeset_uuid' => $uuid,
</span><span class="cx" style="display: block; padding: 0 10px">                ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $wp_customize = $manager;
</ins><span class="cx" style="display: block; padding: 0 10px">                 $manager->register_controls(); // That is, register settings.
</span><span class="cx" style="display: block; padding: 0 10px">                $r = $manager->save_changeset_post( array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'status' => null,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -563,12 +572,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_customize = $manager = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $manager->register_controls();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         do_action( 'customize_register', $wp_customize );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $manager->add_setting( 'scratchpad', array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'type' => 'option',
</span><span class="cx" style="display: block; padding: 0 10px">                        'capability' => 'exist',
</span><span class="cx" style="display: block; padding: 0 10px">                ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $manager->get_setting( 'blogname' )->capability = 'exist';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $original_capabilities = wp_list_pluck( $manager->settings(), 'capability' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 wp_set_current_user( self::$subscriber_user_id );
</span><span class="cx" style="display: block; padding: 0 10px">                $r = $manager->save_changeset_post( array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'status' => 'publish',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -584,6 +594,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertInternalType( 'array', $r );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'Do it live \o/', get_option( 'blogname' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'trash', get_post_status( $post_id ) ); // Auto-trashed.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertEquals( $original_capabilities, wp_list_pluck( $manager->settings(), 'capability' ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertContains( '<script>', get_post( $post_id )->post_content );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $manager->changeset_uuid(), get_post( $post_id )->post_name, 'Expected that the "__trashed" suffix to not be added.' );
</span><span class="cx" style="display: block; padding: 0 10px">                wp_set_current_user( self::$admin_user_id );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -598,7 +609,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                add_post_type_support( 'customize_changeset', 'revisions' );
</span><span class="cx" style="display: block; padding: 0 10px">                $uuid = wp_generate_uuid4();
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_customize = $manager = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $manager->register_controls();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         do_action( 'customize_register', $manager );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $manager->set_post_value( 'blogname', 'Hello Surface' );
</span><span class="cx" style="display: block; padding: 0 10px">                $manager->save_changeset_post( array( 'status' => 'auto-draft' ) );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -660,6 +671,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @covers WP_Customize_Manager::update_stashed_theme_mod_settings()
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_save_changeset_post_with_theme_activation() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                global $wp_customize;
</ins><span class="cx" style="display: block; padding: 0 10px">                 wp_set_current_user( self::$admin_user_id );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $preview_theme = $this->get_inactive_core_theme();
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -676,8 +688,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'changeset_uuid' => $uuid,
</span><span class="cx" style="display: block; padding: 0 10px">                        'theme' => $preview_theme,
</span><span class="cx" style="display: block; padding: 0 10px">                ) );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $manager->register_controls();
-               $GLOBALS['wp_customize'] = $manager;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $wp_customize = $manager;
+               do_action( 'customize_register', $manager );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $manager->set_post_value( 'blogname', 'Hello Preview Theme' );
</span><span class="cx" style="display: block; padding: 0 10px">                $post_values = $manager->unsanitized_post_values();
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -689,6 +701,233 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Test saving changesets with varying users and capabilities.
+        *
+        * @ticket 38705
+        * @covers WP_Customize_Manager::save_changeset_post()
+        */
+       function test_save_changeset_post_with_varying_users() {
+               global $wp_customize;
+
+               add_theme_support( 'custom-background' );
+               wp_set_current_user( self::$admin_user_id );
+               $other_admin_user_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
+
+               $uuid = wp_generate_uuid4();
+               $manager = new WP_Customize_Manager( array(
+                       'changeset_uuid' => $uuid,
+               ) );
+               $wp_customize = $manager;
+               do_action( 'customize_register', $manager );
+               $manager->add_setting( 'scratchpad', array(
+                       'type' => 'option',
+                       'capability' => 'exist',
+               ) );
+
+               // Create initial set of
+               $r = $manager->save_changeset_post( array(
+                       'status' => 'auto-draft',
+                       'data' => array(
+                               'blogname' => array(
+                                       'value' => 'Admin 1 Title',
+                               ),
+                               'scratchpad' => array(
+                                       'value' => 'Admin 1 Scratch',
+                               ),
+                               'background_color' => array(
+                                       'value' => '#000000',
+                               ),
+                       ),
+               ) );
+               $this->assertInternalType( 'array', $r );
+               $this->assertEquals(
+                       array_fill_keys( array( 'blogname', 'scratchpad', 'background_color' ), true ),
+                       $r['setting_validities']
+               );
+               $post_id = $manager->find_changeset_post_id( $uuid );
+               $data = json_decode( get_post( $post_id )->post_content, true );
+               $this->assertEquals( self::$admin_user_id, $data['blogname']['user_id'] );
+               $this->assertEquals( self::$admin_user_id, $data['scratchpad']['user_id'] );
+               $this->assertEquals( self::$admin_user_id, $data[ $this->manager->get_stylesheet() . '::background_color' ]['user_id'] );
+
+               // Attempt to save just one setting under a different user.
+               wp_set_current_user( $other_admin_user_id );
+               $r = $manager->save_changeset_post( array(
+                       'status' => 'auto-draft',
+                       'data' => array(
+                               'blogname' => array(
+                                       'value' => 'Admin 2 Title',
+                               ),
+                               'background_color' => array(
+                                       'value' => '#FFFFFF',
+                               ),
+                       ),
+               ) );
+               $this->assertInternalType( 'array', $r );
+               $this->assertEquals(
+                       array_fill_keys( array( 'blogname', 'background_color' ), true ),
+                       $r['setting_validities']
+               );
+               $data = json_decode( get_post( $post_id )->post_content, true );
+               $this->assertEquals( 'Admin 2 Title', $data['blogname']['value'] );
+               $this->assertEquals( $other_admin_user_id, $data['blogname']['user_id'] );
+               $this->assertEquals( 'Admin 1 Scratch', $data['scratchpad']['value'] );
+               $this->assertEquals( self::$admin_user_id, $data['scratchpad']['user_id'] );
+               $this->assertEquals( '#FFFFFF', $data[ $this->manager->get_stylesheet() . '::background_color' ]['value'] );
+               $this->assertEquals( $other_admin_user_id, $data[ $this->manager->get_stylesheet() . '::background_color' ]['user_id'] );
+
+               // Attempt to save now as under-privileged user.
+               $r = $manager->save_changeset_post( array(
+                       'status' => 'auto-draft',
+                       'data' => array(
+                               'scratchpad' => array(
+                                       'value' => 'Subscriber Scratch',
+                               ),
+                       ),
+                       'user_id' => self::$subscriber_user_id,
+               ) );
+               $this->assertInternalType( 'array', $r );
+               $this->assertEquals(
+                       array_fill_keys( array( 'scratchpad' ), true ),
+                       $r['setting_validities']
+               );
+               $data = json_decode( get_post( $post_id )->post_content, true );
+               $this->assertEquals( $other_admin_user_id, $data['blogname']['user_id'] );
+               $this->assertEquals( self::$subscriber_user_id, $data['scratchpad']['user_id'] );
+               $this->assertEquals( $other_admin_user_id, $data[ $this->manager->get_stylesheet() . '::background_color' ]['user_id'] );
+
+               // Manually update the changeset so that the user_id context is not included.
+               $data = json_decode( get_post( $post_id )->post_content, true );
+               $data['blogdescription']['value'] = 'Programmatically-supplied Tagline';
+               wp_update_post( wp_slash( array( 'ID' => $post_id, 'post_content' => wp_json_encode( $data ) ) ) );
+
+               // Ensure the modifying user set as the current user when each is saved, simulating WP Cron envronment.
+               wp_set_current_user( 0 );
+               $save_counts = array();
+               foreach ( array_keys( $data ) as $setting_id ) {
+                       $setting_id = preg_replace( '/^.+::/', '', $setting_id );
+                       $save_counts[ $setting_id ] = did_action( sprintf( 'customize_save_%s', $setting_id ) );
+               }
+               $this->filtered_setting_current_user_ids = array();
+               foreach ( $manager->settings() as $setting ) {
+                       add_filter( sprintf( 'customize_sanitize_%s', $setting->id ), array( $this, 'filter_customize_setting_to_log_current_user' ), 10, 2 );
+               }
+               wp_update_post( array( 'ID' => $post_id, 'post_status' => 'publish' ) );
+               foreach ( array_keys( $data ) as $setting_id ) {
+                       $setting_id = preg_replace( '/^.+::/', '', $setting_id );
+                       $this->assertEquals( $save_counts[ $setting_id ] + 1, did_action( sprintf( 'customize_save_%s', $setting_id ) ), $setting_id );
+               }
+               $this->assertEqualSets( array( 'blogname', 'blogdescription', 'background_color', 'scratchpad' ), array_keys( $this->filtered_setting_current_user_ids ) );
+               $this->assertEquals( $other_admin_user_id, $this->filtered_setting_current_user_ids['blogname'] );
+               $this->assertEquals( 0, $this->filtered_setting_current_user_ids['blogdescription'] );
+               $this->assertEquals( self::$subscriber_user_id, $this->filtered_setting_current_user_ids['scratchpad'] );
+               $this->assertEquals( $other_admin_user_id, $this->filtered_setting_current_user_ids['background_color'] );
+               $this->assertEquals( 'Subscriber Scratch', get_option( 'scratchpad' ) );
+       }
+
+       /**
+        * Test writing changesets and publishing with users who can unfiltered_html and those who cannot.
+        *
+        * @ticket 38705
+        * @covers WP_Customize_Manager::save_changeset_post()
+        */
+       function test_save_changeset_post_with_varying_unfiltered_html_cap() {
+               global $wp_customize;
+               grant_super_admin( self::$admin_user_id );
+               $this->assertTrue( user_can( self::$admin_user_id, 'unfiltered_html' ) );
+               $this->assertFalse( user_can( self::$subscriber_user_id, 'unfiltered_html' ) );
+               wp_set_current_user( 0 );
+               add_action( 'customize_register', array( $this, 'register_scratchpad_setting' ) );
+
+               // Attempt scratchpad with user who has unfiltered_html.
+               update_option( 'scratchpad', '' );
+               $wp_customize = new WP_Customize_Manager();
+               do_action( 'customize_register', $wp_customize );
+               $wp_customize->set_post_value( 'scratchpad', 'Unfiltered<script>evil</script>' );
+               $wp_customize->save_changeset_post( array(
+                       'status' => 'auto-draft',
+                       'user_id' => self::$admin_user_id,
+               ) );
+               $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $wp_customize->changeset_uuid() ) );
+               do_action( 'customize_register', $wp_customize );
+               $wp_customize->save_changeset_post( array( 'status' => 'publish' ) );
+               $this->assertEquals( 'Unfiltered<script>evil</script>', get_option( 'scratchpad' ) );
+
+               // Attempt scratchpad with user who doesn't have unfiltered_html.
+               update_option( 'scratchpad', '' );
+               $wp_customize = new WP_Customize_Manager();
+               do_action( 'customize_register', $wp_customize );
+               $wp_customize->set_post_value( 'scratchpad', 'Unfiltered<script>evil</script>' );
+               $wp_customize->save_changeset_post( array(
+                       'status' => 'auto-draft',
+                       'user_id' => self::$subscriber_user_id,
+               ) );
+               $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $wp_customize->changeset_uuid() ) );
+               do_action( 'customize_register', $wp_customize );
+               $wp_customize->save_changeset_post( array( 'status' => 'publish' ) );
+               $this->assertEquals( 'Unfilteredevil', get_option( 'scratchpad' ) );
+
+               // Attempt publishing scratchpad as anonymous user when changeset was set by privileged user.
+               update_option( 'scratchpad', '' );
+               $wp_customize = new WP_Customize_Manager();
+               do_action( 'customize_register', $wp_customize );
+               $wp_customize->set_post_value( 'scratchpad', 'Unfiltered<script>evil</script>' );
+               $wp_customize->save_changeset_post( array(
+                       'status' => 'auto-draft',
+                       'user_id' => self::$admin_user_id,
+               ) );
+               $changeset_post_id = $wp_customize->changeset_post_id();
+               wp_set_current_user( 0 );
+               $wp_customize = null;
+               unset( $GLOBALS['wp_actions']['customize_register'] );
+               $this->assertEquals( 'Unfilteredevil', apply_filters( 'content_save_pre', 'Unfiltered<script>evil</script>' ) );
+               wp_publish_post( $changeset_post_id ); // @todo If wp_update_post() is used here, then kses will corrupt the post_content.
+               $this->assertEquals( 'Unfiltered<script>evil</script>', get_option( 'scratchpad' ) );
+       }
+
+       /**
+        * Register scratchpad setting.
+        *
+        * @param WP_Customize_Manager $wp_customize Manager.
+        */
+       function register_scratchpad_setting( WP_Customize_Manager $wp_customize ) {
+               $wp_customize->add_setting( 'scratchpad', array(
+                       'type' => 'option',
+                       'capability' => 'exist',
+                       'sanitize_callback' => array( $this, 'filter_sanitize_scratchpad' ),
+               ) );
+       }
+
+       /**
+        * Sanitize scratchpad as if it is post_content so kses filters apply.
+        *
+        * @param string $value Value.
+        * @return string Value.
+        */
+       function filter_sanitize_scratchpad( $value ) {
+               return apply_filters( 'content_save_pre', $value );
+       }
+
+       /**
+        * Current user when settings are filtered.
+        *
+        * @var array
+        */
+       protected $filtered_setting_current_user_ids = array();
+
+       /**
+        * Filter setting to capture the current user when the filter applies.
+        *
+        * @param mixed                $value   Setting value.
+        * @param WP_Customize_Setting $setting Setting.
+        * @return mixed Value.
+        */
+       function filter_customize_setting_to_log_current_user( $value, $setting ) {
+               $this->filtered_setting_current_user_ids[ $setting->id ] = get_current_user_id();
+               return $value;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Test WP_Customize_Manager::is_cross_domain().
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 30937
</span></span></pre>
</div>
</div>

</body>
</html>