WordPress.org

Make WordPress Core

Changeset 39181


Ignore:
Timestamp:
11/09/2016 07:02:53 AM (4 years ago)
Author:
westonruter
Message:

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 #30937.
Fixes #38705.

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-customize-manager.php

    r39175 r39181  
    18281828     *     @type string $title    Post title. Optional.
    18291829     *     @type string $date_gmt Date in GMT. Optional.
     1830     *     @type int    $user_id  ID for user who is saving the changeset. Optional, defaults to the current user ID.
    18301831     * }
    18311832     *
     
    18401841                'data' => array(),
    18411842                'date_gmt' => null,
     1843                'user_id' => get_current_user_id(),
    18421844            ),
    18431845            $args
     
    18451847
    18461848        $changeset_post_id = $this->changeset_post_id();
     1849        $existing_changeset_data = array();
     1850        if ( $changeset_post_id ) {
     1851            $existing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
     1852        }
    18471853
    18481854        // The request was made via wp.customize.previewer.save().
     
    18631869        ) );
    18641870        $this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value.
     1871
     1872        /*
     1873         * Get list of IDs for settings that have values different from what is currently
     1874         * saved in the changeset. By skipping any values that are already the same, the
     1875         * subset of changed settings can be passed into validate_setting_values to prevent
     1876         * an underprivileged modifying a single setting for which they have the capability
     1877         * from being blocked from saving. This also prevents a user from touching of the
     1878         * previous saved settings and overriding the associated user_id if they made no change.
     1879         */
     1880        $changed_setting_ids = array();
     1881        foreach ( $post_values as $setting_id => $setting_value ) {
     1882            $setting = $this->get_setting( $setting_id );
     1883
     1884            if ( $setting && 'theme_mod' === $setting->type ) {
     1885                $prefixed_setting_id = $this->get_stylesheet() . '::' . $setting->id;
     1886            } else {
     1887                $prefixed_setting_id = $setting_id;
     1888            }
     1889
     1890            $is_value_changed = (
     1891                ! isset( $existing_changeset_data[ $prefixed_setting_id ] )
     1892                ||
     1893                ! array_key_exists( 'value', $existing_changeset_data[ $prefixed_setting_id ] )
     1894                ||
     1895                $existing_changeset_data[ $prefixed_setting_id ]['value'] !== $setting_value
     1896            );
     1897            if ( $is_value_changed ) {
     1898                $changed_setting_ids[] = $setting_id;
     1899            }
     1900        }
     1901        $post_values = wp_array_slice_assoc( $post_values, $changed_setting_ids );
    18651902
    18661903        /**
     
    19441981                    $data[ $changeset_setting_id ],
    19451982                    $setting_params,
    1946                     array( 'type' => $setting->type )
     1983                    array(
     1984                        'type' => $setting->type,
     1985                        'user_id' => $args['user_id'],
     1986                    )
    19471987                );
    19481988            }
     
    21222162        $this->_changeset_data      = $publishing_changeset_data;
    21232163
    2124         // Ensure that other theme mods are stashed.
    2125         $other_theme_mod_settings = array();
    2126         if ( did_action( 'switch_theme' ) ) {
    2127             $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
    2128             $matches = array();
    2129             foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) {
    2130                 $is_other_theme_mod = (
    2131                     isset( $setting_params['value'] )
    2132                     &&
    2133                     isset( $setting_params['type'] )
    2134                     &&
    2135                     'theme_mod' === $setting_params['type']
    2136                     &&
    2137                     preg_match( $namespace_pattern, $raw_setting_id, $matches )
    2138                     &&
    2139                     $this->get_stylesheet() !== $matches['stylesheet']
    2140                 );
    2141                 if ( $is_other_theme_mod ) {
    2142                     if ( ! isset( $other_theme_mod_settings[ $matches['stylesheet'] ] ) ) {
    2143                         $other_theme_mod_settings[ $matches['stylesheet'] ] = array();
    2144                     }
    2145                     $other_theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
     2164        // Parse changeset data to identify theme mod settings and user IDs associated with settings to be saved.
     2165        $setting_user_ids = array();
     2166        $theme_mod_settings = array();
     2167        $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
     2168        $matches = array();
     2169        foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) {
     2170            $actual_setting_id = null;
     2171            $is_theme_mod_setting = (
     2172                isset( $setting_params['value'] )
     2173                &&
     2174                isset( $setting_params['type'] )
     2175                &&
     2176                'theme_mod' === $setting_params['type']
     2177                &&
     2178                preg_match( $namespace_pattern, $raw_setting_id, $matches )
     2179            );
     2180            if ( $is_theme_mod_setting ) {
     2181                if ( ! isset( $theme_mod_settings[ $matches['stylesheet'] ] ) ) {
     2182                    $theme_mod_settings[ $matches['stylesheet'] ] = array();
    21462183                }
     2184                $theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
     2185
     2186                if ( $this->get_stylesheet() === $matches['stylesheet'] ) {
     2187                    $actual_setting_id = $matches['setting_id'];
     2188                }
     2189            } else {
     2190                $actual_setting_id = $raw_setting_id;
     2191            }
     2192
     2193            // Keep track of the user IDs for settings actually for this theme.
     2194            if ( $actual_setting_id && isset( $setting_params['user_id'] ) ) {
     2195                $setting_user_ids[ $actual_setting_id ] = $setting_params['user_id'];
    21472196            }
    21482197        }
     
    21742223        foreach ( $changeset_setting_ids as $setting_id ) {
    21752224            $setting = $this->get_setting( $setting_id );
    2176             if ( $setting ) {
     2225            if ( $setting && ! isset( $setting_user_ids[ $setting_id ] ) ) {
    21772226                $original_setting_capabilities[ $setting->id ] = $setting->capability;
    21782227                $setting->capability = 'exist';
     
    21802229        }
    21812230
     2231        $original_user_id = get_current_user_id();
    21822232        foreach ( $changeset_setting_ids as $setting_id ) {
    21832233            $setting = $this->get_setting( $setting_id );
    21842234            if ( $setting ) {
     2235                /*
     2236                 * Set the current user to match the user who saved the value into
     2237                 * the changeset so that any filters that apply during the save
     2238                 * process will respect the original user's capabilities. This
     2239                 * will ensure, for example, that KSES won't strip unsafe HTML
     2240                 * when a scheduled changeset publishes via WP Cron.
     2241                 */
     2242                if ( isset( $setting_user_ids[ $setting_id ] ) ) {
     2243                    wp_set_current_user( $setting_user_ids[ $setting_id ] );
     2244                } else {
     2245                    wp_set_current_user( $original_user_id );
     2246                }
     2247
    21852248                $setting->save();
    21862249            }
    21872250        }
     2251        wp_set_current_user( $original_user_id );
    21882252
    21892253        // Update the stashed theme mod settings, removing the active theme's stashed settings, if activated.
    21902254        if ( did_action( 'switch_theme' ) ) {
     2255            $other_theme_mod_settings = $theme_mod_settings;
     2256            unset( $other_theme_mod_settings[ $this->get_stylesheet() ] );
    21912257            $this->update_stashed_theme_mod_settings( $other_theme_mod_settings );
    21922258        }
  • trunk/src/wp-includes/theme.php

    r39180 r39181  
    25652565    if ( empty( $wp_customize ) ) {
    25662566        require_once ABSPATH . WPINC . '/class-wp-customize-manager.php';
    2567         $wp_customize = new WP_Customize_Manager( $changeset_post->post_name );
     2567        $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $changeset_post->post_name ) );
    25682568    }
    25692569
  • trunk/tests/phpunit/tests/customize/manager.php

    r39180 r39181  
    430430            'changeset_uuid' => $uuid,
    431431        ) );
     432        $wp_customize = $manager;
    432433        $manager->register_controls();
    433434        $manager->set_post_value( 'blogname', 'Changeset Title' );
    434435        $manager->set_post_value( 'blogdescription', 'Changeset Tagline' );
    435436
     437        $pre_saved_data = array(
     438            'blogname' => array(
     439                'value' => 'Overridden Changeset Title',
     440            ),
     441            'blogdescription' => array(
     442                'custom' => 'something',
     443            ),
     444        );
    436445        $r = $manager->save_changeset_post( array(
    437446            'status' => 'auto-draft',
    438447            'title' => 'Auto Draft',
    439448            'date_gmt' => '2010-01-01 00:00:00',
    440             'data' => array(
    441                 'blogname' => array(
    442                     'value' => 'Overridden Changeset Title',
    443                 ),
    444                 'blogdescription' => array(
    445                     'custom' => 'something',
    446                 ),
    447             ),
     449            'data' => $pre_saved_data,
    448450        ) );
    449451        $this->assertInternalType( 'array', $r );
     
    455457        $saved_data = json_decode( get_post( $post_id )->post_content, true );
    456458        $this->assertEquals( $manager->unsanitized_post_values(), wp_list_pluck( $saved_data, 'value' ) );
    457         $this->assertEquals( 'Overridden Changeset Title', $saved_data['blogname']['value'] );
    458         $this->assertEquals( 'something', $saved_data['blogdescription']['custom'] );
     459        $this->assertEquals( $pre_saved_data['blogname']['value'], $saved_data['blogname']['value'] );
     460        $this->assertEquals( $pre_saved_data['blogdescription']['custom'], $saved_data['blogdescription']['custom'] );
     461        foreach ( $saved_data as $setting_id => $setting_params ) {
     462            $this->assertArrayHasKey( 'type', $setting_params );
     463            $this->assertEquals( 'option', $setting_params['type'] );
     464            $this->assertArrayHasKey( 'user_id', $setting_params );
     465            $this->assertEquals( self::$admin_user_id, $setting_params['user_id'] );
     466        }
    459467        $this->assertEquals( 'Auto Draft', get_post( $post_id )->post_title );
    460468        $this->assertEquals( 'auto-draft', get_post( $post_id )->post_status );
     
    512520            'changeset_uuid' => $uuid,
    513521        ) );
     522        $wp_customize = $manager;
    514523        $manager->register_controls(); // That is, register settings.
    515524        $r = $manager->save_changeset_post( array(
     
    564573
    565574        $wp_customize = $manager = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
    566         $manager->register_controls();
     575        do_action( 'customize_register', $wp_customize );
    567576        $manager->add_setting( 'scratchpad', array(
    568577            'type' => 'option',
     
    570579        ) );
    571580        $manager->get_setting( 'blogname' )->capability = 'exist';
     581        $original_capabilities = wp_list_pluck( $manager->settings(), 'capability' );
    572582        wp_set_current_user( self::$subscriber_user_id );
    573583        $r = $manager->save_changeset_post( array(
     
    585595        $this->assertEquals( 'Do it live \o/', get_option( 'blogname' ) );
    586596        $this->assertEquals( 'trash', get_post_status( $post_id ) ); // Auto-trashed.
     597        $this->assertEquals( $original_capabilities, wp_list_pluck( $manager->settings(), 'capability' ) );
    587598        $this->assertContains( '<script>', get_post( $post_id )->post_content );
    588599        $this->assertEquals( $manager->changeset_uuid(), get_post( $post_id )->post_name, 'Expected that the "__trashed" suffix to not be added.' );
     
    599610        $uuid = wp_generate_uuid4();
    600611        $wp_customize = $manager = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
    601         $manager->register_controls();
     612        do_action( 'customize_register', $manager );
    602613
    603614        $manager->set_post_value( 'blogname', 'Hello Surface' );
     
    661672     */
    662673    function test_save_changeset_post_with_theme_activation() {
     674        global $wp_customize;
    663675        wp_set_current_user( self::$admin_user_id );
    664676
     
    677689            'theme' => $preview_theme,
    678690        ) );
    679         $manager->register_controls();
    680         $GLOBALS['wp_customize'] = $manager;
     691        $wp_customize = $manager;
     692        do_action( 'customize_register', $manager );
    681693
    682694        $manager->set_post_value( 'blogname', 'Hello Preview Theme' );
     
    687699        $this->assertEquals( $preview_theme, get_stylesheet() );
    688700        $this->assertEquals( 'Hello Preview Theme', get_option( 'blogname' ) );
     701    }
     702
     703    /**
     704     * Test saving changesets with varying users and capabilities.
     705     *
     706     * @ticket 38705
     707     * @covers WP_Customize_Manager::save_changeset_post()
     708     */
     709    function test_save_changeset_post_with_varying_users() {
     710        global $wp_customize;
     711
     712        add_theme_support( 'custom-background' );
     713        wp_set_current_user( self::$admin_user_id );
     714        $other_admin_user_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
     715
     716        $uuid = wp_generate_uuid4();
     717        $manager = new WP_Customize_Manager( array(
     718            'changeset_uuid' => $uuid,
     719        ) );
     720        $wp_customize = $manager;
     721        do_action( 'customize_register', $manager );
     722        $manager->add_setting( 'scratchpad', array(
     723            'type' => 'option',
     724            'capability' => 'exist',
     725        ) );
     726
     727        // Create initial set of
     728        $r = $manager->save_changeset_post( array(
     729            'status' => 'auto-draft',
     730            'data' => array(
     731                'blogname' => array(
     732                    'value' => 'Admin 1 Title',
     733                ),
     734                'scratchpad' => array(
     735                    'value' => 'Admin 1 Scratch',
     736                ),
     737                'background_color' => array(
     738                    'value' => '#000000',
     739                ),
     740            ),
     741        ) );
     742        $this->assertInternalType( 'array', $r );
     743        $this->assertEquals(
     744            array_fill_keys( array( 'blogname', 'scratchpad', 'background_color' ), true ),
     745            $r['setting_validities']
     746        );
     747        $post_id = $manager->find_changeset_post_id( $uuid );
     748        $data = json_decode( get_post( $post_id )->post_content, true );
     749        $this->assertEquals( self::$admin_user_id, $data['blogname']['user_id'] );
     750        $this->assertEquals( self::$admin_user_id, $data['scratchpad']['user_id'] );
     751        $this->assertEquals( self::$admin_user_id, $data[ $this->manager->get_stylesheet() . '::background_color' ]['user_id'] );
     752
     753        // Attempt to save just one setting under a different user.
     754        wp_set_current_user( $other_admin_user_id );
     755        $r = $manager->save_changeset_post( array(
     756            'status' => 'auto-draft',
     757            'data' => array(
     758                'blogname' => array(
     759                    'value' => 'Admin 2 Title',
     760                ),
     761                'background_color' => array(
     762                    'value' => '#FFFFFF',
     763                ),
     764            ),
     765        ) );
     766        $this->assertInternalType( 'array', $r );
     767        $this->assertEquals(
     768            array_fill_keys( array( 'blogname', 'background_color' ), true ),
     769            $r['setting_validities']
     770        );
     771        $data = json_decode( get_post( $post_id )->post_content, true );
     772        $this->assertEquals( 'Admin 2 Title', $data['blogname']['value'] );
     773        $this->assertEquals( $other_admin_user_id, $data['blogname']['user_id'] );
     774        $this->assertEquals( 'Admin 1 Scratch', $data['scratchpad']['value'] );
     775        $this->assertEquals( self::$admin_user_id, $data['scratchpad']['user_id'] );
     776        $this->assertEquals( '#FFFFFF', $data[ $this->manager->get_stylesheet() . '::background_color' ]['value'] );
     777        $this->assertEquals( $other_admin_user_id, $data[ $this->manager->get_stylesheet() . '::background_color' ]['user_id'] );
     778
     779        // Attempt to save now as under-privileged user.
     780        $r = $manager->save_changeset_post( array(
     781            'status' => 'auto-draft',
     782            'data' => array(
     783                'scratchpad' => array(
     784                    'value' => 'Subscriber Scratch',
     785                ),
     786            ),
     787            'user_id' => self::$subscriber_user_id,
     788        ) );
     789        $this->assertInternalType( 'array', $r );
     790        $this->assertEquals(
     791            array_fill_keys( array( 'scratchpad' ), true ),
     792            $r['setting_validities']
     793        );
     794        $data = json_decode( get_post( $post_id )->post_content, true );
     795        $this->assertEquals( $other_admin_user_id, $data['blogname']['user_id'] );
     796        $this->assertEquals( self::$subscriber_user_id, $data['scratchpad']['user_id'] );
     797        $this->assertEquals( $other_admin_user_id, $data[ $this->manager->get_stylesheet() . '::background_color' ]['user_id'] );
     798
     799        // Manually update the changeset so that the user_id context is not included.
     800        $data = json_decode( get_post( $post_id )->post_content, true );
     801        $data['blogdescription']['value'] = 'Programmatically-supplied Tagline';
     802        wp_update_post( wp_slash( array( 'ID' => $post_id, 'post_content' => wp_json_encode( $data ) ) ) );
     803
     804        // Ensure the modifying user set as the current user when each is saved, simulating WP Cron envronment.
     805        wp_set_current_user( 0 );
     806        $save_counts = array();
     807        foreach ( array_keys( $data ) as $setting_id ) {
     808            $setting_id = preg_replace( '/^.+::/', '', $setting_id );
     809            $save_counts[ $setting_id ] = did_action( sprintf( 'customize_save_%s', $setting_id ) );
     810        }
     811        $this->filtered_setting_current_user_ids = array();
     812        foreach ( $manager->settings() as $setting ) {
     813            add_filter( sprintf( 'customize_sanitize_%s', $setting->id ), array( $this, 'filter_customize_setting_to_log_current_user' ), 10, 2 );
     814        }
     815        wp_update_post( array( 'ID' => $post_id, 'post_status' => 'publish' ) );
     816        foreach ( array_keys( $data ) as $setting_id ) {
     817            $setting_id = preg_replace( '/^.+::/', '', $setting_id );
     818            $this->assertEquals( $save_counts[ $setting_id ] + 1, did_action( sprintf( 'customize_save_%s', $setting_id ) ), $setting_id );
     819        }
     820        $this->assertEqualSets( array( 'blogname', 'blogdescription', 'background_color', 'scratchpad' ), array_keys( $this->filtered_setting_current_user_ids ) );
     821        $this->assertEquals( $other_admin_user_id, $this->filtered_setting_current_user_ids['blogname'] );
     822        $this->assertEquals( 0, $this->filtered_setting_current_user_ids['blogdescription'] );
     823        $this->assertEquals( self::$subscriber_user_id, $this->filtered_setting_current_user_ids['scratchpad'] );
     824        $this->assertEquals( $other_admin_user_id, $this->filtered_setting_current_user_ids['background_color'] );
     825        $this->assertEquals( 'Subscriber Scratch', get_option( 'scratchpad' ) );
     826    }
     827
     828    /**
     829     * Test writing changesets and publishing with users who can unfiltered_html and those who cannot.
     830     *
     831     * @ticket 38705
     832     * @covers WP_Customize_Manager::save_changeset_post()
     833     */
     834    function test_save_changeset_post_with_varying_unfiltered_html_cap() {
     835        global $wp_customize;
     836        grant_super_admin( self::$admin_user_id );
     837        $this->assertTrue( user_can( self::$admin_user_id, 'unfiltered_html' ) );
     838        $this->assertFalse( user_can( self::$subscriber_user_id, 'unfiltered_html' ) );
     839        wp_set_current_user( 0 );
     840        add_action( 'customize_register', array( $this, 'register_scratchpad_setting' ) );
     841
     842        // Attempt scratchpad with user who has unfiltered_html.
     843        update_option( 'scratchpad', '' );
     844        $wp_customize = new WP_Customize_Manager();
     845        do_action( 'customize_register', $wp_customize );
     846        $wp_customize->set_post_value( 'scratchpad', 'Unfiltered<script>evil</script>' );
     847        $wp_customize->save_changeset_post( array(
     848            'status' => 'auto-draft',
     849            'user_id' => self::$admin_user_id,
     850        ) );
     851        $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $wp_customize->changeset_uuid() ) );
     852        do_action( 'customize_register', $wp_customize );
     853        $wp_customize->save_changeset_post( array( 'status' => 'publish' ) );
     854        $this->assertEquals( 'Unfiltered<script>evil</script>', get_option( 'scratchpad' ) );
     855
     856        // Attempt scratchpad with user who doesn't have unfiltered_html.
     857        update_option( 'scratchpad', '' );
     858        $wp_customize = new WP_Customize_Manager();
     859        do_action( 'customize_register', $wp_customize );
     860        $wp_customize->set_post_value( 'scratchpad', 'Unfiltered<script>evil</script>' );
     861        $wp_customize->save_changeset_post( array(
     862            'status' => 'auto-draft',
     863            'user_id' => self::$subscriber_user_id,
     864        ) );
     865        $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $wp_customize->changeset_uuid() ) );
     866        do_action( 'customize_register', $wp_customize );
     867        $wp_customize->save_changeset_post( array( 'status' => 'publish' ) );
     868        $this->assertEquals( 'Unfilteredevil', get_option( 'scratchpad' ) );
     869
     870        // Attempt publishing scratchpad as anonymous user when changeset was set by privileged user.
     871        update_option( 'scratchpad', '' );
     872        $wp_customize = new WP_Customize_Manager();
     873        do_action( 'customize_register', $wp_customize );
     874        $wp_customize->set_post_value( 'scratchpad', 'Unfiltered<script>evil</script>' );
     875        $wp_customize->save_changeset_post( array(
     876            'status' => 'auto-draft',
     877            'user_id' => self::$admin_user_id,
     878        ) );
     879        $changeset_post_id = $wp_customize->changeset_post_id();
     880        wp_set_current_user( 0 );
     881        $wp_customize = null;
     882        unset( $GLOBALS['wp_actions']['customize_register'] );
     883        $this->assertEquals( 'Unfilteredevil', apply_filters( 'content_save_pre', 'Unfiltered<script>evil</script>' ) );
     884        wp_publish_post( $changeset_post_id ); // @todo If wp_update_post() is used here, then kses will corrupt the post_content.
     885        $this->assertEquals( 'Unfiltered<script>evil</script>', get_option( 'scratchpad' ) );
     886    }
     887
     888    /**
     889     * Register scratchpad setting.
     890     *
     891     * @param WP_Customize_Manager $wp_customize Manager.
     892     */
     893    function register_scratchpad_setting( WP_Customize_Manager $wp_customize ) {
     894        $wp_customize->add_setting( 'scratchpad', array(
     895            'type' => 'option',
     896            'capability' => 'exist',
     897            'sanitize_callback' => array( $this, 'filter_sanitize_scratchpad' ),
     898        ) );
     899    }
     900
     901    /**
     902     * Sanitize scratchpad as if it is post_content so kses filters apply.
     903     *
     904     * @param string $value Value.
     905     * @return string Value.
     906     */
     907    function filter_sanitize_scratchpad( $value ) {
     908        return apply_filters( 'content_save_pre', $value );
     909    }
     910
     911    /**
     912     * Current user when settings are filtered.
     913     *
     914     * @var array
     915     */
     916    protected $filtered_setting_current_user_ids = array();
     917
     918    /**
     919     * Filter setting to capture the current user when the filter applies.
     920     *
     921     * @param mixed                $value   Setting value.
     922     * @param WP_Customize_Setting $setting Setting.
     923     * @return mixed Value.
     924     */
     925    function filter_customize_setting_to_log_current_user( $value, $setting ) {
     926        $this->filtered_setting_current_user_ids[ $setting->id ] = get_current_user_id();
     927        return $value;
    689928    }
    690929
Note: See TracChangeset for help on using the changeset viewer.