Ticket #38705: 38705.0.diff
| File 38705.0.diff, 20.2 KB (added by , 9 years ago) |
|---|
-
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 { 1827 1827 * @type string $status Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed. 1828 1828 * @type string $title Post title. Optional. 1829 1829 * @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. 1830 1831 * } 1831 1832 * 1832 1833 * @return array|WP_Error Returns array on success and WP_Error with array data on error. … … final class WP_Customize_Manager { 1839 1840 'title' => null, 1840 1841 'data' => array(), 1841 1842 'date_gmt' => null, 1843 'user_id' => get_current_user_id(), 1842 1844 ), 1843 1845 $args 1844 1846 ); 1845 1847 1846 1848 $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 } 1847 1853 1848 1854 // The request was made via wp.customize.previewer.save(). 1849 1855 $update_transactionally = (bool) $args['status']; … … final class WP_Customize_Manager { 1863 1869 ) ); 1864 1870 $this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value. 1865 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 ); 1902 1866 1903 /** 1867 1904 * Fires before save validation happens. 1868 1905 * … … final class WP_Customize_Manager { 1943 1980 $data[ $changeset_setting_id ] = array_merge( 1944 1981 $data[ $changeset_setting_id ], 1945 1982 $setting_params, 1946 array( 'type' => $setting->type ) 1983 array( 1984 'type' => $setting->type, 1985 'user_id' => $args['user_id'], 1986 ) 1947 1987 ); 1948 1988 } 1949 1989 } … … final class WP_Customize_Manager { 2121 2161 $previous_changeset_data = $this->_changeset_data; 2122 2162 $this->_changeset_data = $publishing_changeset_data; 2123 2163 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(); 2146 2183 } 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']; 2147 2196 } 2148 2197 } 2149 2198 … … final class WP_Customize_Manager { 2173 2222 $original_setting_capabilities = array(); 2174 2223 foreach ( $changeset_setting_ids as $setting_id ) { 2175 2224 $setting = $this->get_setting( $setting_id ); 2176 if ( $setting ) {2225 if ( $setting && ! isset( $setting_user_ids[ $setting_id ] ) ) { 2177 2226 $original_setting_capabilities[ $setting->id ] = $setting->capability; 2178 2227 $setting->capability = 'exist'; 2179 2228 } 2180 2229 } 2181 2230 2231 $original_user_id = get_current_user_id(); 2182 2232 foreach ( $changeset_setting_ids as $setting_id ) { 2183 2233 $setting = $this->get_setting( $setting_id ); 2184 2234 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 2185 2248 $setting->save(); 2186 2249 } 2187 2250 } 2251 wp_set_current_user( $original_user_id ); 2188 2252 2189 2253 // Update the stashed theme mod settings, removing the active theme's stashed settings, if activated. 2190 2254 if ( did_action( 'switch_theme' ) ) { 2255 $other_theme_mod_settings = $theme_mod_settings; 2256 unset( $other_theme_mod_settings[ $this->get_stylesheet() ] ); 2191 2257 $this->update_stashed_theme_mod_settings( $other_theme_mod_settings ); 2192 2258 } 2193 2259 -
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 { 416 416 * @covers WP_Customize_Manager::save_changeset_post() 417 417 */ 418 418 function test_save_changeset_post_without_theme_activation() { 419 global $wp_customize; 419 420 wp_set_current_user( self::$admin_user_id ); 420 421 421 422 $did_action = array( … … class Tests_WP_Customize_Manager extends WP_UnitTestCase { 428 429 $manager = new WP_Customize_Manager( array( 429 430 'changeset_uuid' => $uuid, 430 431 ) ); 432 $wp_customize = $manager; 431 433 $manager->register_controls(); 432 434 $manager->set_post_value( 'blogname', 'Changeset Title' ); 433 435 $manager->set_post_value( 'blogdescription', 'Changeset Tagline' ); 434 436 437 $pre_saved_data = array( 438 'blogname' => array( 439 'value' => 'Overridden Changeset Title', 440 ), 441 'blogdescription' => array( 442 'custom' => 'something', 443 ), 444 ); 435 445 $r = $manager->save_changeset_post( array( 436 446 'status' => 'auto-draft', 437 447 'title' => 'Auto Draft', 438 448 '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, 447 450 ) ); 448 451 $this->assertInternalType( 'array', $r ); 449 452 … … class Tests_WP_Customize_Manager extends WP_UnitTestCase { 453 456 $this->assertNotNull( $post_id ); 454 457 $saved_data = json_decode( get_post( $post_id )->post_content, true ); 455 458 $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 } 458 467 $this->assertEquals( 'Auto Draft', get_post( $post_id )->post_title ); 459 468 $this->assertEquals( 'auto-draft', get_post( $post_id )->post_status ); 460 469 $this->assertEquals( '2010-01-01 00:00:00', get_post( $post_id )->post_date_gmt ); … … class Tests_WP_Customize_Manager extends WP_UnitTestCase { 510 519 $manager = new WP_Customize_Manager( array( 511 520 'changeset_uuid' => $uuid, 512 521 ) ); 522 $wp_customize = $manager; 513 523 $manager->register_controls(); // That is, register settings. 514 524 $r = $manager->save_changeset_post( array( 515 525 'status' => null, … … class Tests_WP_Customize_Manager extends WP_UnitTestCase { 545 555 546 556 // Publish the changeset. 547 557 $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' ); 550 561 $r = $manager->save_changeset_post( array( 551 562 'status' => 'publish', 552 563 'data' => array( … … class Tests_WP_Customize_Manager extends WP_UnitTestCase { 558 569 $this->assertInternalType( 'array', $r ); 559 570 $this->assertEquals( 'Do it live \o/', get_option( 'blogname' ) ); 560 571 $this->assertEquals( 'trash', get_post_status( $post_id ) ); // Auto-trashed. 572 $this->assertEquals( $original_capabilities, wp_list_pluck( $manager->settings(), 'capability' ) ); 561 573 562 574 // Test revisions. 563 575 add_post_type_support( 'customize_changeset', 'revisions' ); 564 576 $uuid = wp_generate_uuid4(); 565 577 $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 ); 568 580 569 581 $manager->set_post_value( 'blogname', 'Hello Surface' ); 570 582 $manager->save_changeset_post( array( 'status' => 'auto-draft' ) ); … … class Tests_WP_Customize_Manager extends WP_UnitTestCase { 626 638 * @covers WP_Customize_Manager::update_stashed_theme_mod_settings() 627 639 */ 628 640 function test_save_changeset_post_with_theme_activation() { 641 global $wp_customize; 629 642 wp_set_current_user( self::$admin_user_id ); 630 643 631 644 $preview_theme = $this->get_inactive_core_theme(); … … class Tests_WP_Customize_Manager extends WP_UnitTestCase { 642 655 'changeset_uuid' => $uuid, 643 656 'theme' => $preview_theme, 644 657 ) ); 645 $ manager->register_controls();646 $GLOBALS['wp_customize'] = $manager;658 $wp_customize = $manager; 659 do_action( 'customize_register', $manager ); 647 660 648 661 $manager->set_post_value( 'blogname', 'Hello Preview Theme' ); 649 662 $post_values = $manager->unsanitized_post_values(); … … class Tests_WP_Customize_Manager extends WP_UnitTestCase { 655 668 } 656 669 657 670 /** 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 /** 658 865 * Test WP_Customize_Manager::is_cross_domain(). 659 866 * 660 867 * @ticket 30937