Make WordPress Core

Ticket #38705: 38705.0.diff

File 38705.0.diff, 20.2 KB (added by westonruter, 9 years ago)

https://github.com/xwp/wordpress-develop/pull/196

  • src/wp-includes/class-wp-customize-manager.php

    diff --git src/wp-includes/class-wp-customize-manager.php src/wp-includes/class-wp-customize-manager.php
    index 5a9770a..797a07d 100644
    final class WP_Customize_Manager { 
    18271827         *     @type string $status   Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed.
    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         *
    18321833         * @return array|WP_Error Returns array on success and WP_Error with array data on error.
    final class WP_Customize_Manager { 
    18391840                                'title' => null,
    18401841                                'data' => array(),
    18411842                                'date_gmt' => null,
     1843                                'user_id' => get_current_user_id(),
    18421844                        ),
    18431845                        $args
    18441846                );
    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().
    18491855                $update_transactionally = (bool) $args['status'];
    final class WP_Customize_Manager { 
    18631869                ) );
    18641870                $this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value.
    18651871
     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 );
     1902
    18661903                /**
    18671904                 * Fires before save validation happens.
    18681905                 *
    final class WP_Customize_Manager { 
    19431980                                $data[ $changeset_setting_id ] = array_merge(
    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                        }
    19491989                }
    final class WP_Customize_Manager { 
    21212161                $previous_changeset_data    = $this->_changeset_data;
    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                }
    21492198
    final class WP_Customize_Manager { 
    21732222                $original_setting_capabilities = array();
    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';
    21792228                        }
    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                }
    21932259
  • tests/phpunit/tests/customize/manager.php

    diff --git tests/phpunit/tests/customize/manager.php tests/phpunit/tests/customize/manager.php
    index a28d834..e4c7f93 100644
    class Tests_WP_Customize_Manager extends WP_UnitTestCase { 
    416416         * @covers WP_Customize_Manager::save_changeset_post()
    417417         */
    418418        function test_save_changeset_post_without_theme_activation() {
     419                global $wp_customize;
    419420                wp_set_current_user( self::$admin_user_id );
    420421
    421422                $did_action = array(
    class Tests_WP_Customize_Manager extends WP_UnitTestCase { 
    428429                $manager = new WP_Customize_Manager( array(
    429430                        'changeset_uuid' => $uuid,
    430431                ) );
     432                $wp_customize = $manager;
    431433                $manager->register_controls();
    432434                $manager->set_post_value( 'blogname', 'Changeset Title' );
    433435                $manager->set_post_value( 'blogdescription', 'Changeset Tagline' );
    434436
     437                $pre_saved_data = array(
     438                        'blogname' => array(
     439                                'value' => 'Overridden Changeset Title',
     440                        ),
     441                        'blogdescription' => array(
     442                                'custom' => 'something',
     443                        ),
     444                );
    435445                $r = $manager->save_changeset_post( array(
    436446                        'status' => 'auto-draft',
    437447                        'title' => 'Auto Draft',
    438448                        'date_gmt' => '2010-01-01 00:00:00',
    439                         'data' => array(
    440                                 'blogname' => array(
    441                                         'value' => 'Overridden Changeset Title',
    442                                 ),
    443                                 'blogdescription' => array(
    444                                         'custom' => 'something',
    445                                 ),
    446                         ),
     449                        'data' => $pre_saved_data,
    447450                ) );
    448451                $this->assertInternalType( 'array', $r );
    449452
    class Tests_WP_Customize_Manager extends WP_UnitTestCase { 
    453456                $this->assertNotNull( $post_id );
    454457                $saved_data = json_decode( get_post( $post_id )->post_content, true );
    455458                $this->assertEquals( $manager->unsanitized_post_values(), wp_list_pluck( $saved_data, 'value' ) );
    456                 $this->assertEquals( 'Overridden Changeset Title', $saved_data['blogname']['value'] );
    457                 $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                }
    458467                $this->assertEquals( 'Auto Draft', get_post( $post_id )->post_title );
    459468                $this->assertEquals( 'auto-draft', get_post( $post_id )->post_status );
    460469                $this->assertEquals( '2010-01-01 00:00:00', get_post( $post_id )->post_date_gmt );
    class Tests_WP_Customize_Manager extends WP_UnitTestCase { 
    510519                $manager = new WP_Customize_Manager( array(
    511520                        'changeset_uuid' => $uuid,
    512521                ) );
     522                $wp_customize = $manager;
    513523                $manager->register_controls(); // That is, register settings.
    514524                $r = $manager->save_changeset_post( array(
    515525                        'status' => null,
    class Tests_WP_Customize_Manager extends WP_UnitTestCase { 
    545555
    546556                // Publish the changeset.
    547557                $manager = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
    548                 $manager->register_controls();
    549                 $GLOBALS['wp_customize'] = $manager;
     558                $wp_customize = $manager;
     559                do_action( 'customize_register', $wp_customize );
     560                $original_capabilities = wp_list_pluck( $manager->settings(), 'capability' );
    550561                $r = $manager->save_changeset_post( array(
    551562                        'status' => 'publish',
    552563                        'data' => array(
    class Tests_WP_Customize_Manager extends WP_UnitTestCase { 
    558569                $this->assertInternalType( 'array', $r );
    559570                $this->assertEquals( 'Do it live \o/', get_option( 'blogname' ) );
    560571                $this->assertEquals( 'trash', get_post_status( $post_id ) ); // Auto-trashed.
     572                $this->assertEquals( $original_capabilities, wp_list_pluck( $manager->settings(), 'capability' ) );
    561573
    562574                // Test revisions.
    563575                add_post_type_support( 'customize_changeset', 'revisions' );
    564576                $uuid = wp_generate_uuid4();
    565577                $manager = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
    566                 $manager->register_controls();
    567                 $GLOBALS['wp_customize'] = $manager;
     578                $wp_customize = $manager;
     579                do_action( 'customize_register', $manager );
    568580
    569581                $manager->set_post_value( 'blogname', 'Hello Surface' );
    570582                $manager->save_changeset_post( array( 'status' => 'auto-draft' ) );
    class Tests_WP_Customize_Manager extends WP_UnitTestCase { 
    626638         * @covers WP_Customize_Manager::update_stashed_theme_mod_settings()
    627639         */
    628640        function test_save_changeset_post_with_theme_activation() {
     641                global $wp_customize;
    629642                wp_set_current_user( self::$admin_user_id );
    630643
    631644                $preview_theme = $this->get_inactive_core_theme();
    class Tests_WP_Customize_Manager extends WP_UnitTestCase { 
    642655                        'changeset_uuid' => $uuid,
    643656                        'theme' => $preview_theme,
    644657                ) );
    645                 $manager->register_controls();
    646                 $GLOBALS['wp_customize'] = $manager;
     658                $wp_customize = $manager;
     659                do_action( 'customize_register', $manager );
    647660
    648661                $manager->set_post_value( 'blogname', 'Hello Preview Theme' );
    649662                $post_values = $manager->unsanitized_post_values();
    class Tests_WP_Customize_Manager extends WP_UnitTestCase { 
    655668        }
    656669
    657670        /**
     671         * Test saving changesets with varying users and capabilities.
     672         *
     673         * @covers WP_Customize_Manager::save_changeset_post()
     674         */
     675        function test_save_changeset_post_with_varying_users() {
     676                global $wp_customize;
     677
     678                add_theme_support( 'custom-background' );
     679                wp_set_current_user( self::$admin_user_id );
     680                $other_admin_user_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
     681
     682                $uuid = wp_generate_uuid4();
     683                $manager = new WP_Customize_Manager( array(
     684                        'changeset_uuid' => $uuid,
     685                ) );
     686                $wp_customize = $manager;
     687                do_action( 'customize_register', $manager );
     688                $manager->add_setting( 'scratchpad', array(
     689                        'type' => 'option',
     690                        'capability' => 'exist',
     691                ) );
     692
     693                // Create initial set of
     694                $r = $manager->save_changeset_post( array(
     695                        'status' => 'auto-draft',
     696                        'data' => array(
     697                                'blogname' => array(
     698                                        'value' => 'Admin 1 Title',
     699                                ),
     700                                'scratchpad' => array(
     701                                        'value' => 'Admin 1 Scratch',
     702                                ),
     703                                'background_color' => array(
     704                                        'value' => '#000000',
     705                                ),
     706                        ),
     707                ) );
     708                $this->assertInternalType( 'array', $r );
     709                $this->assertEquals(
     710                        array_fill_keys( array( 'blogname', 'scratchpad', 'background_color' ), true ),
     711                        $r['setting_validities']
     712                );
     713                $post_id = $manager->find_changeset_post_id( $uuid );
     714                $data = json_decode( get_post( $post_id )->post_content, true );
     715                $this->assertEquals( self::$admin_user_id, $data['blogname']['user_id'] );
     716                $this->assertEquals( self::$admin_user_id, $data['scratchpad']['user_id'] );
     717                $this->assertEquals( self::$admin_user_id, $data[ $this->manager->get_stylesheet() . '::background_color' ]['user_id'] );
     718
     719                // Attempt to save just one setting under a different user.
     720                wp_set_current_user( $other_admin_user_id );
     721                $r = $manager->save_changeset_post( array(
     722                        'status' => 'auto-draft',
     723                        'data' => array(
     724                                'blogname' => array(
     725                                        'value' => 'Admin 2 Title',
     726                                ),
     727                                'background_color' => array(
     728                                        'value' => '#FFFFFF',
     729                                ),
     730                        ),
     731                ) );
     732                $this->assertInternalType( 'array', $r );
     733                $this->assertEquals(
     734                        array_fill_keys( array( 'blogname', 'background_color' ), true ),
     735                        $r['setting_validities']
     736                );
     737                $data = json_decode( get_post( $post_id )->post_content, true );
     738                $this->assertEquals( 'Admin 2 Title', $data['blogname']['value'] );
     739                $this->assertEquals( $other_admin_user_id, $data['blogname']['user_id'] );
     740                $this->assertEquals( 'Admin 1 Scratch', $data['scratchpad']['value'] );
     741                $this->assertEquals( self::$admin_user_id, $data['scratchpad']['user_id'] );
     742                $this->assertEquals( '#FFFFFF', $data[ $this->manager->get_stylesheet() . '::background_color' ]['value'] );
     743                $this->assertEquals( $other_admin_user_id, $data[ $this->manager->get_stylesheet() . '::background_color' ]['user_id'] );
     744
     745                // Attempt to save now as under-privileged user.
     746                $r = $manager->save_changeset_post( array(
     747                        'status' => 'auto-draft',
     748                        'data' => array(
     749                                'scratchpad' => array(
     750                                        'value' => 'Subscriber Scratch',
     751                                ),
     752                        ),
     753                        'user_id' => self::$subscriber_user_id,
     754                ) );
     755                $this->assertInternalType( 'array', $r );
     756                $this->assertEquals(
     757                        array_fill_keys( array( 'scratchpad' ), true ),
     758                        $r['setting_validities']
     759                );
     760                $data = json_decode( get_post( $post_id )->post_content, true );
     761                $this->assertEquals( $other_admin_user_id, $data['blogname']['user_id'] );
     762                $this->assertEquals( self::$subscriber_user_id, $data['scratchpad']['user_id'] );
     763                $this->assertEquals( $other_admin_user_id, $data[ $this->manager->get_stylesheet() . '::background_color' ]['user_id'] );
     764
     765                // Manually update the changeset so that the user_id context is not included.
     766                $data = json_decode( get_post( $post_id )->post_content, true );
     767                $data['blogdescription']['value'] = 'Programmatically-supplied Tagline';
     768                wp_update_post( wp_slash( array( 'ID' => $post_id, 'post_content' => wp_json_encode( $data ) ) ) );
     769
     770                // Ensure the modifying user set as the current user when each is saved, simulating WP Cron envronment.
     771                wp_set_current_user( 0 );
     772                $save_counts = array();
     773                foreach ( array_keys( $data ) as $setting_id ) {
     774                        $setting_id = preg_replace( '/^.+::/', '', $setting_id );
     775                        $save_counts[ $setting_id ] = did_action( sprintf( 'customize_save_%s', $setting_id ) );
     776                }
     777                $this->filtered_setting_current_user_ids = array();
     778                foreach ( $manager->settings() as $setting ) {
     779                        add_filter( sprintf( 'customize_sanitize_%s', $setting->id ), array( $this, 'filter_customize_setting_to_log_current_user' ), 10, 2 );
     780                }
     781                wp_update_post( array( 'ID' => $post_id, 'post_status' => 'publish' ) );
     782                foreach ( array_keys( $data ) as $setting_id ) {
     783                        $setting_id = preg_replace( '/^.+::/', '', $setting_id );
     784                        $this->assertEquals( $save_counts[ $setting_id ] + 1, did_action( sprintf( 'customize_save_%s', $setting_id ) ), $setting_id );
     785                }
     786                $this->assertEqualSets( array( 'blogname', 'blogdescription', 'background_color', 'scratchpad' ), array_keys( $this->filtered_setting_current_user_ids ) );
     787                $this->assertEquals( $other_admin_user_id, $this->filtered_setting_current_user_ids['blogname'] );
     788                $this->assertEquals( 0, $this->filtered_setting_current_user_ids['blogdescription'] );
     789                $this->assertEquals( self::$subscriber_user_id, $this->filtered_setting_current_user_ids['scratchpad'] );
     790                $this->assertEquals( $other_admin_user_id, $this->filtered_setting_current_user_ids['background_color'] );
     791                $this->assertEquals( 'Subscriber Scratch', get_option( 'scratchpad' ) );
     792        }
     793
     794        /**
     795         * Test writing changesets and publishing with users who can unfiltered_html and those who cannot.
     796         *
     797         * @covers WP_Customize_Manager::save_changeset_post()
     798         */
     799        function test_save_changeset_post_with_varying_unfiltered_html_cap() {
     800                global $wp_customize;
     801                grant_super_admin( self::$admin_user_id );
     802                $this->assertTrue( user_can( self::$admin_user_id, 'unfiltered_html' ) );
     803                $this->assertFalse( user_can( self::$subscriber_user_id, 'unfiltered_html' ) );
     804                wp_set_current_user( 0 );
     805                $setting_args = array(
     806                        'type' => 'option',
     807                        'capability' => 'exist',
     808                        'sanitize_callback' => array( $this, 'filter_sanitize_scratchpad' ),
     809                );
     810
     811                // Attempt scratchpad with user who has unfiltered_html.
     812                $wp_customize = new WP_Customize_Manager();
     813                $wp_customize->add_setting( 'scratchpad', $setting_args );
     814                $wp_customize->set_post_value( 'scratchpad', 'Unfiltered<script>evil</script>' );
     815                $wp_customize->save_changeset_post( array(
     816                        'status' => 'auto-draft',
     817                        'user_id' => self::$admin_user_id,
     818                ) );
     819                $wp_customize->save_changeset_post( array( 'status' => 'publish' ) );
     820                $this->assertEquals( 'Unfiltered<script>evil</script>', get_option( 'scratchpad' ) );
     821
     822                // Attempt scratchpad with user who doesn't have unfiltered_html.
     823                $wp_customize = new WP_Customize_Manager();
     824                $wp_customize->add_setting( 'scratchpad', $setting_args );
     825                $wp_customize->set_post_value( 'scratchpad', 'Unfiltered<script>evil</script>' );
     826                $wp_customize->save_changeset_post( array(
     827                        'status' => 'auto-draft',
     828                        'user_id' => self::$subscriber_user_id,
     829                ) );
     830                $wp_customize->save_changeset_post( array( 'status' => 'publish' ) );
     831                $this->assertEquals( 'Unfilteredevil', get_option( 'scratchpad' ) );
     832        }
     833
     834        /**
     835         * Sanitize scratchpad as if it is post_content so kses filters apply.
     836         *
     837         * @param string $value Value.
     838         * @return string Value.
     839         */
     840        function filter_sanitize_scratchpad( $value ) {
     841                return apply_filters( 'content_save_pre', $value );
     842        }
     843
     844        /**
     845         * Current user when settings are filtered.
     846         *
     847         * @var array
     848         */
     849        protected $filtered_setting_current_user_ids = array();
     850
     851        /**
     852         * Filter setting to capture the current user when the filter applies.
     853         *
     854         * @param mixed                $value   Setting value.
     855         * @param WP_Customize_Setting $setting Setting.
     856         *
     857         * @return mixed Value.
     858         */
     859        function filter_customize_setting_to_log_current_user( $value, $setting ) {
     860                $this->filtered_setting_current_user_ids[ $setting->id ] = get_current_user_id();
     861                return $value;
     862        }
     863
     864        /**
    658865         * Test WP_Customize_Manager::is_cross_domain().
    659866         *
    660867         * @ticket 30937