<!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>