Ticket #30937: 30937.diff
File 30937.diff, 145.5 KB (added by , 8 years ago) |
---|
-
tests/qunit/fixtures/customize-settings.js
147 147 'mobile': { 148 148 'label': 'Enter mobile preview mode' 149 149 } 150 }, 151 'changeset': { 152 'status': '', 153 'uuid': '0c674ff4-c159-4e7a-beb4-cb830ae73979' 150 154 } 151 155 }; 152 156 window._wpCustomizeControlsL10n = {}; -
tests/phpunit/tests/functions.php
880 880 $this->assertSame( false, $raised_limit ); 881 881 $this->assertEquals( WP_MAX_MEMORY_LIMIT, $ini_limit_after ); 882 882 } 883 884 /** 885 * Tests wp_generate_uuid4(). 886 * 887 * @covers wp_generate_uuid4() 888 * @ticket 38164 889 */ 890 function test_wp_generate_uuid4() { 891 $uuids = array(); 892 for ( $i = 0; $i < 20; $i += 1 ) { 893 $uuid = wp_generate_uuid4(); 894 $this->assertRegExp( '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $uuid ); 895 $uuids[] = $uuid; 896 } 897 898 $unique_uuids = array_unique( $uuids ); 899 $this->assertEquals( $uuids, $unique_uuids ); 900 } 883 901 } -
tests/phpunit/tests/customize/manager.php
27 27 public $undefined; 28 28 29 29 /** 30 * Admin user ID. 31 * 32 * @var int 33 */ 34 protected static $admin_user_id; 35 36 /** 37 * Set up before class. 38 * 39 * @param WP_UnitTest_Factory $factory Factory. 40 */ 41 public static function wpSetUpBeforeClass( $factory ) { 42 self::$admin_user_id = $factory->user->create( array( 'role' => 'administrator' ) ); 43 } 44 45 /** 30 46 * Set up test. 31 47 */ 32 48 function setUp() { … … 91 107 * @ticket 30988 92 108 */ 93 109 function test_unsanitized_post_values() { 110 wp_set_current_user( self::$admin_user_id ); 94 111 $manager = $this->manager; 95 112 96 113 $customized = array( … … 108 125 * @ticket 30988 109 126 */ 110 127 function test_post_value() { 128 wp_set_current_user( self::$admin_user_id ); 111 129 $posted_settings = array( 112 130 'foo' => 'OOF', 113 131 ); … … 131 149 * @ticket 34893 132 150 */ 133 151 function test_invalid_post_value() { 152 wp_set_current_user( self::$admin_user_id ); 134 153 $default_value = 'foo_default'; 135 154 $setting = $this->manager->add_setting( 'foo', array( 136 155 'validate_callback' => array( $this, 'filter_customize_validate_foo' ), … … 196 215 * @ticket 37247 197 216 */ 198 217 function test_post_value_validation_sanitization_order() { 218 wp_set_current_user( self::$admin_user_id ); 199 219 $default_value = '0'; 200 220 $setting = $this->manager->add_setting( 'numeric', array( 201 221 'validate_callback' => array( $this, 'filter_customize_validate_numeric' ), … … 240 260 * @see WP_Customize_Manager::validate_setting_values() 241 261 */ 242 262 function test_validate_setting_values() { 263 wp_set_current_user( self::$admin_user_id ); 243 264 $setting = $this->manager->add_setting( 'foo', array( 244 265 'validate_callback' => array( $this, 'filter_customize_validate_foo' ), 245 266 'sanitize_callback' => array( $this, 'filter_customize_sanitize_foo' ), … … 284 305 * @ticket 37247 285 306 */ 286 307 function test_validate_setting_values_validation_sanitization_order() { 308 wp_set_current_user( self::$admin_user_id ); 287 309 $setting = $this->manager->add_setting( 'numeric', array( 288 310 'validate_callback' => array( $this, 'filter_customize_validate_numeric' ), 289 311 'sanitize_callback' => array( $this, 'filter_customize_sanitize_numeric' ), … … 325 347 * @see WP_Customize_Manager::set_post_value() 326 348 */ 327 349 function test_set_post_value() { 350 wp_set_current_user( self::$admin_user_id ); 328 351 $this->manager->add_setting( 'foo', array( 329 352 'sanitize_callback' => array( $this, 'sanitize_foo_for_test_set_post_value' ), 330 353 ) ); … … 429 452 } 430 453 $this->assertFalse( $this->manager->has_published_pages() ); 431 454 432 wp_set_current_user( $this->factory()->user->create( array( 'role' => 'editor' ) ));455 wp_set_current_user( self::$admin_user_id ); 433 456 $this->manager->nav_menus->customize_register(); 434 457 $setting_id = 'nav_menus_created_posts'; 435 458 $setting = $this->manager->get_setting( $setting_id ); … … 448 471 * @ticket 30936 449 472 */ 450 473 function test_register_dynamic_settings() { 474 wp_set_current_user( self::$admin_user_id ); 451 475 $posted_settings = array( 452 476 'foo' => 'OOF', 453 477 'bar' => 'RAB', … … 547 571 wp_set_current_user( self::factory()->user->create( array( 'role' => 'author' ) ) ); 548 572 $this->assertEquals( home_url( '/' ), $this->manager->get_return_url() ); 549 573 550 wp_set_current_user( self:: factory()->user->create( array( 'role' => 'administrator' ) ));574 wp_set_current_user( self::$admin_user_id ); 551 575 $this->assertTrue( current_user_can( 'edit_theme_options' ) ); 552 576 $this->assertEquals( home_url( '/' ), $this->manager->get_return_url() ); 553 577 … … 640 664 * @see WP_Customize_Manager::customize_pane_settings() 641 665 */ 642 666 function test_customize_pane_settings() { 643 wp_set_current_user( self:: factory()->user->create( array( 'role' => 'administrator' ) ));667 wp_set_current_user( self::$admin_user_id ); 644 668 $this->manager->register_controls(); 645 669 $this->manager->prepare_controls(); 646 670 $autofocus = array( 'control' => 'blogname' ); … … 662 686 $data = json_decode( $json, true ); 663 687 $this->assertNotEmpty( $data ); 664 688 665 $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices' ), array_keys( $data ) );689 $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'changeset' ), array_keys( $data ) ); 666 690 $this->assertEquals( $autofocus, $data['autofocus'] ); 667 691 $this->assertArrayHasKey( 'save', $data['nonce'] ); 668 692 $this->assertArrayHasKey( 'preview', $data['nonce'] ); … … 674 698 * @see WP_Customize_Manager::customize_preview_settings() 675 699 */ 676 700 function test_customize_preview_settings() { 677 wp_set_current_user( self:: factory()->user->create( array( 'role' => 'administrator' ) ));701 wp_set_current_user( self::$admin_user_id ); 678 702 $this->manager->register_controls(); 679 703 $this->manager->prepare_controls(); 680 704 $this->manager->set_post_value( 'foo', 'bar' ); … … 693 717 $this->assertArrayHasKey( 'activePanels', $settings ); 694 718 $this->assertArrayHasKey( 'activeSections', $settings ); 695 719 $this->assertArrayHasKey( 'activeControls', $settings ); 696 $this->assertArrayHasKey( 'settingValidities', $settings );697 720 $this->assertArrayHasKey( 'nonce', $settings ); 698 721 $this->assertArrayHasKey( '_dirty', $settings ); 699 722 … … 770 793 $manager = new WP_Customize_Manager(); 771 794 $manager->register_controls(); 772 795 $section_id = 'foo-section'; 773 wp_set_current_user( self:: factory()->user->create( array( 'role' => 'administrator' ) ));796 wp_set_current_user( self::$admin_user_id ); 774 797 $manager->add_section( $section_id, array( 775 798 'title' => 'Section', 776 799 'priority' => 1, … … 801 824 */ 802 825 function test_add_section_return_instance() { 803 826 $manager = new WP_Customize_Manager(); 804 wp_set_current_user( self:: factory()->user->create( array( 'role' => 'administrator' ) ));827 wp_set_current_user( self::$admin_user_id ); 805 828 806 829 $section_id = 'foo-section'; 807 830 $result_section = $manager->add_section( $section_id, array( … … 828 851 */ 829 852 function test_add_setting_return_instance() { 830 853 $manager = new WP_Customize_Manager(); 831 wp_set_current_user( self:: factory()->user->create( array( 'role' => 'administrator' ) ));854 wp_set_current_user( self::$admin_user_id ); 832 855 833 856 $setting_id = 'foo-setting'; 834 857 $result_setting = $manager->add_setting( $setting_id ); … … 899 922 */ 900 923 function test_add_panel_return_instance() { 901 924 $manager = new WP_Customize_Manager(); 902 wp_set_current_user( self:: factory()->user->create( array( 'role' => 'administrator' ) ));925 wp_set_current_user( self::$admin_user_id ); 903 926 904 927 $panel_id = 'foo-panel'; 905 928 $result_panel = $manager->add_panel( $panel_id, array( … … 926 949 function test_add_control_return_instance() { 927 950 $manager = new WP_Customize_Manager(); 928 951 $section_id = 'foo-section'; 929 wp_set_current_user( self:: factory()->user->create( array( 'role' => 'administrator' ) ));952 wp_set_current_user( self::$admin_user_id ); 930 953 $manager->add_section( $section_id, array( 931 954 'title' => 'Section', 932 955 'priority' => 1, -
tests/phpunit/tests/customize/setting.php
97 97 * @see WP_Customize_Setting::value() 98 98 */ 99 99 function test_preview_standard_types_non_multidimensional() { 100 wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); 100 101 $_POST['customized'] = wp_slash( wp_json_encode( $this->post_data_overrides ) ); 101 102 102 103 // Try non-multidimensional settings. … … 175 176 * @see WP_Customize_Setting::value() 176 177 */ 177 178 function test_preview_standard_types_multidimensional() { 179 wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); 178 180 $_POST['customized'] = wp_slash( wp_json_encode( $this->post_data_overrides ) ); 179 181 180 182 foreach ( $this->standard_type_configs as $type => $type_options ) { … … 314 316 * @see WP_Customize_Setting::preview() 315 317 */ 316 318 function test_preview_custom_type() { 319 wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); 317 320 $type = 'custom_type'; 318 321 $post_data_overrides = array( 319 322 "unset_{$type}_with_post_value" => "unset_{$type}_without_post_value\\o/", … … 478 481 * @ticket 31428 479 482 */ 480 483 function test_is_current_blog_previewed() { 484 wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); 481 485 $type = 'option'; 482 486 $name = 'blogname'; 483 487 $post_value = rand_str(); … … 502 506 $this->markTestSkipped( 'Cannot test WP_Customize_Setting::is_current_blog_previewed() with switch_to_blog() if not on multisite.' ); 503 507 } 504 508 509 wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); 505 510 $type = 'option'; 506 511 $name = 'blogdescription'; 507 512 $post_value = rand_str(); … … 647 652 * @ticket 37294 648 653 */ 649 654 public function test_multidimensional_value_when_previewed() { 655 wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); 650 656 WP_Customize_Setting::reset_aggregated_multidimensionals(); 651 657 652 658 $initial_value = 456; -
tests/phpunit/tests/customize/selective-refresh-ajax.php
140 140 } 141 141 142 142 /** 143 * Make sure that the Customizer "signature" is not included in partial render responses.144 *145 * @see WP_Customize_Selective_Refresh::handle_render_partials_request()146 */147 function test_handle_render_partials_request_removes_customize_signature() {148 $this->setup_valid_render_partials_request_environment();149 $this->assertTrue( is_customize_preview() );150 $this->assertEquals( 1000, has_action( 'shutdown', array( $this->wp_customize, 'customize_preview_signature' ) ) );151 ob_start();152 try {153 $this->selective_refresh->handle_render_partials_request();154 } catch ( WPDieException $e ) {155 unset( $e );156 }157 ob_end_clean();158 $this->assertFalse( has_action( 'shutdown', array( $this->wp_customize, 'customize_preview_signature' ) ) );159 }160 161 /**162 143 * Test WP_Customize_Selective_Refresh::handle_render_partials_request() for an unrecognized partial. 163 144 * 164 145 * @see WP_Customize_Selective_Refresh::handle_render_partials_request() -
tests/phpunit/tests/post.php
1240 1240 $this->assertEquals( 0, get_post( $page_id )->post_parent ); 1241 1241 } 1242 1242 1243 /** 1244 * Test ensuring that the post_name (UUID) is preserved when wp_insert_post()/wp_update_post() is called. 1245 * 1246 * @see _wp_customize_changeset_filter_insert_post_data() 1247 * @ticket 30937 1248 */ 1249 function test_wp_insert_post_for_customize_changeset_should_not_drop_post_name() { 1250 1251 $this->assertEquals( 10, has_filter( 'wp_insert_post_data', '_wp_customize_changeset_filter_insert_post_data' ) ); 1252 1253 $changeset_data = array( 1254 'blogname' => array( 1255 'value' => 'Hello World', 1256 ), 1257 ); 1258 1259 wp_set_current_user( $this->factory()->user->create( array( 'role' => 'contributor' ) ) ); 1260 1261 $uuid = wp_generate_uuid4(); 1262 $post_id = wp_insert_post( array( 1263 'post_type' => 'customize_changeset', 1264 'post_name' => strtoupper( $uuid ), 1265 'post_content' => wp_json_encode( $changeset_data ), 1266 ) ); 1267 $this->assertEquals( $uuid, get_post( $post_id )->post_name, 'Expected lower-case UUID4 to be inserted.' ); 1268 $this->assertEquals( $changeset_data, json_decode( get_post( $post_id )->post_content, true ) ); 1269 1270 $changeset_data['blogname']['value'] = 'Hola Mundo'; 1271 wp_update_post( array( 1272 'ID' => $post_id, 1273 'post_status' => 'draft', 1274 'post_content' => wp_json_encode( $changeset_data ), 1275 ) ); 1276 $this->assertEquals( $uuid, get_post( $post_id )->post_name, 'Expected post_name to not have been dropped for drafts.' ); 1277 $this->assertEquals( $changeset_data, json_decode( get_post( $post_id )->post_content, true ) ); 1278 1279 $changeset_data['blogname']['value'] = 'Hallo Welt'; 1280 wp_update_post( array( 1281 'ID' => $post_id, 1282 'post_status' => 'pending', 1283 'post_content' => wp_json_encode( $changeset_data ), 1284 ) ); 1285 $this->assertEquals( $uuid, get_post( $post_id )->post_name, 'Expected post_name to not have been dropped for pending.' ); 1286 $this->assertEquals( $changeset_data, json_decode( get_post( $post_id )->post_content, true ) ); 1287 } 1288 1243 1289 } -
src/wp-includes/admin-bar.php
366 366 * @since 4.3.0 367 367 * 368 368 * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance. 369 * @global WP_Customize_Manager $wp_customize 369 370 */ 370 371 function wp_admin_bar_customize_menu( $wp_admin_bar ) { 372 global $wp_customize; 373 371 374 // Don't show for users who can't access the customizer or when in the admin. 372 375 if ( ! current_user_can( 'customize' ) || is_admin() ) { 373 376 return; 374 377 } 375 378 379 // Don't show if the user cannot edit a given customize_changeset post currently being previewed. 380 if ( is_customize_preview() && $wp_customize->changeset_post_id() && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $wp_customize->changeset_post_id() ) ) { 381 return; 382 } 383 376 384 $current_url = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; 385 if ( is_customize_preview() && $wp_customize->changeset_uuid() ) { 386 $current_url = remove_query_arg( 'customize_changeset_uuid', $current_url ); 387 } 388 377 389 $customize_url = add_query_arg( 'url', urlencode( $current_url ), wp_customize_url() ); 390 if ( is_customize_preview() ) { 391 $customize_url = add_query_arg( array( 'changeset_uuid' => $wp_customize->changeset_uuid() ), $customize_url ); 392 } 378 393 379 394 $wp_admin_bar->add_menu( array( 380 395 'id' => 'customize', -
src/wp-includes/class-wp-customize-nav-menus.php
48 48 $this->previewed_menus = array(); 49 49 $this->manager = $manager; 50 50 51 // Skip useless hooks when the user can't manage nav menus anyway. 51 // See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L469-L499 52 add_action( 'customize_register', array( $this, 'customize_register' ), 11 ); 53 add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_dynamic_setting_args' ), 10, 2 ); 54 add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_dynamic_setting_class' ), 10, 3 ); 55 56 // Skip remaining hooks when the user can't manage nav menus anyway. 52 57 if ( ! current_user_can( 'edit_theme_options' ) ) { 53 58 return; 54 59 } … … 58 63 add_action( 'wp_ajax_search-available-menu-items-customizer', array( $this, 'ajax_search_available_items' ) ); 59 64 add_action( 'wp_ajax_customize-nav-menus-insert-auto-draft', array( $this, 'ajax_insert_auto_draft_post' ) ); 60 65 add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); 61 add_action( 'customize_register', array( $this, 'customize_register' ), 11 );62 add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_dynamic_setting_args' ), 10, 2 );63 add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_dynamic_setting_class' ), 10, 3 );64 66 add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) ); 65 67 add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) ); 66 68 add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) ); … … 486 488 */ 487 489 public function customize_register() { 488 490 491 /* 492 * Preview settings for nav menus early so that the sections and controls will be added properly. 493 * See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L506-L543 494 */ 495 $nav_menus_setting_ids = array(); 496 foreach ( array_keys( $this->manager->unsanitized_post_values() ) as $setting_id ) { 497 if ( preg_match( '/^(nav_menu_locations|nav_menu|nav_menu_item)\[/', $setting_id ) ) { 498 $nav_menus_setting_ids[] = $setting_id; 499 } 500 } 501 foreach ( $nav_menus_setting_ids as $setting_id ) { 502 $setting = $this->manager->get_setting( $setting_id ); 503 if ( $setting ) { 504 $setting->preview(); 505 } 506 } 507 489 508 // Require JS-rendered control types. 490 509 $this->manager->register_panel_type( 'WP_Customize_Nav_Menus_Panel' ); 491 510 $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Control' ); -
src/wp-includes/post.php
111 111 'query_var' => false, 112 112 ) ); 113 113 114 register_post_type( 'customize_changeset', array( 115 'labels' => array( 116 'name' => _x( 'Changesets', 'post type general name' ), 117 'singular_name' => _x( 'Changeset', 'post type singular name' ), 118 'menu_name' => _x( 'Changesets', 'admin menu' ), 119 'name_admin_bar' => _x( 'Changeset', 'add new on admin bar' ), 120 'add_new' => _x( 'Add New', 'Customize Changeset' ), 121 'add_new_item' => __( 'Add New Changeset' ), 122 'new_item' => __( 'New Changeset' ), 123 'edit_item' => __( 'Edit Changeset' ), 124 'view_item' => __( 'View Changeset' ), 125 'all_items' => __( 'All Changesets' ), 126 'search_items' => __( 'Search Changesets' ), 127 'not_found' => __( 'No changesets found.' ), 128 'not_found_in_trash' => __( 'No changesets found in Trash.' ), 129 ), 130 'public' => false, 131 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ 132 'map_meta_cap' => true, 133 'hierarchical' => false, 134 'rewrite' => false, 135 'query_var' => false, 136 'can_export' => false, 137 'delete_with_user' => false, 138 'supports' => array( 'title', 'author' ), 139 'capability_type' => 'customize_changeset', 140 'capabilities' => array( 141 'create_posts' => 'customize', 142 'delete_others_posts' => 'customize', 143 'delete_post' => 'customize', 144 'delete_posts' => 'customize', 145 'delete_private_posts' => 'customize', 146 'delete_published_posts' => 'customize', 147 'edit_others_posts' => 'customize', 148 'edit_post' => 'customize', 149 'edit_posts' => 'customize', 150 'edit_private_posts' => 'customize', 151 'edit_published_posts' => 'do_not_allow', 152 'publish_posts' => 'customize', 153 'read' => 'read', 154 'read_post' => 'customize', 155 'read_private_posts' => 'customize', 156 ), 157 ) ); 158 114 159 register_post_status( 'publish', array( 115 160 'label' => _x( 'Published', 'post status' ), 116 161 'public' => true, -
src/wp-includes/default-filters.php
75 75 76 76 // Slugs 77 77 add_filter( 'pre_term_slug', 'sanitize_title' ); 78 add_filter( 'wp_insert_post_data', '_wp_customize_changeset_filter_insert_post_data', 10, 2 ); 78 79 79 80 // Keys 80 81 foreach ( array( 'pre_post_type', 'pre_post_status', 'pre_post_comment_status', 'pre_post_ping_status' ) as $filter ) { … … 382 383 add_action( 'wp_loaded', '_custom_header_background_just_in_time' ); 383 384 add_action( 'wp_head', '_custom_logo_header_styles' ); 384 385 add_action( 'plugins_loaded', '_wp_customize_include' ); 386 add_action( 'publish_customize_changeset', '_wp_customize_publish_changeset', 10, 2 ); 385 387 add_action( 'admin_enqueue_scripts', '_wp_customize_loader_settings' ); 386 388 add_action( 'delete_attachment', '_delete_attachment_theme_mod' ); 387 389 -
src/wp-includes/customize/class-wp-customize-selective-refresh.php
307 307 return; 308 308 } 309 309 310 $this->manager->remove_preview_signature();311 312 310 /* 313 311 * Note that is_customize_preview() returning true will entail that the 314 312 * user passed the 'customize' capability check and the nonce check, since -
src/wp-includes/customize/class-wp-customize-theme-control.php
63 63 */ 64 64 public function content_template() { 65 65 $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); 66 $active_url = esc_url( remove_query_arg( ' theme', $current_url ) );67 $preview_url = esc_url( add_query_arg( ' theme', '__THEME__', $current_url ) ); // Token because esc_url() strips curly braces.66 $active_url = esc_url( remove_query_arg( 'customize_theme', $current_url ) ); 67 $preview_url = esc_url( add_query_arg( 'customize_theme', '__THEME__', $current_url ) ); // Token because esc_url() strips curly braces. 68 68 $preview_url = str_replace( '__THEME__', '{{ data.theme.id }}', $preview_url ); 69 69 ?> 70 70 <# if ( data.theme.isActiveTheme ) { #> -
src/wp-includes/script-loader.php
447 447 448 448 $scripts->add( 'customize-base', "/wp-includes/js/customize-base$suffix.js", array( 'jquery', 'json2', 'underscore' ), false, 1 ); 449 449 $scripts->add( 'customize-loader', "/wp-includes/js/customize-loader$suffix.js", array( 'customize-base' ), false, 1 ); 450 $scripts->add( 'customize-preview', "/wp-includes/js/customize-preview$suffix.js", array( ' customize-base' ), false, 1 );450 $scripts->add( 'customize-preview', "/wp-includes/js/customize-preview$suffix.js", array( 'wp-a11y', 'customize-base' ), false, 1 ); 451 451 $scripts->add( 'customize-models', "/wp-includes/js/customize-models.js", array( 'underscore', 'backbone' ), false, 1 ); 452 452 $scripts->add( 'customize-views', "/wp-includes/js/customize-views.js", array( 'jquery', 'underscore', 'imgareaselect', 'customize-models', 'media-editor', 'media-views' ), false, 1 ); 453 453 $scripts->add( 'customize-controls', "/wp-admin/js/customize-controls$suffix.js", array( 'customize-base', 'wp-a11y', 'wp-util' ), false, 1 ); -
src/wp-includes/class-wp-customize-manager.php
130 130 protected $controls = array(); 131 131 132 132 /** 133 * Return value of check_ajax_referer() in customize_preview_init() method.134 *135 * @since 3.5.0136 * @access protected137 * @var false|int138 */139 protected $nonce_tick;140 141 /**142 133 * Panel types that may be rendered from JS templates. 143 134 * 144 135 * @since 4.3.0 … … 193 184 protected $autofocus = array(); 194 185 195 186 /** 187 * Changeset UUID. 188 * 189 * @since 4.7.0 190 * @access protected 191 * @var string 192 */ 193 protected $changeset_uuid; 194 195 /** 196 * Messenger channel. 197 * 198 * @since 4.7.0 199 * @access protected 200 * @var string 201 */ 202 protected $messenger_channel; 203 204 /** 196 205 * Unsanitized values for Customize Settings parsed from $_POST['customized']. 197 206 * 198 207 * @var array … … 200 209 private $_post_values; 201 210 202 211 /** 212 * Changeset post ID. 213 * 214 * @var int|false 215 */ 216 private $_changeset_post_id; 217 218 /** 219 * Changeset data loaded from a customize_changeset post. 220 * 221 * @var array 222 */ 223 private $_changeset_data; 224 225 /** 203 226 * Constructor. 204 227 * 205 228 * @since 3.4.0 229 * @since 4.7.0 Added $args param. 230 * 231 * @param array $args { 232 * Args. 233 * 234 * @type string $changeset_uuid Changeset UUID, the post_name for the customize_changeset post containing the customized state. Defaults to new UUID. 235 * @type string $theme Theme to be previewed (for theme switch). Defaults to customize_theme or theme query params. 236 * @type string $messenger_channel Messenger channel. Defaults to customize_messenger_channel query param. 237 * } 206 238 */ 207 public function __construct() { 239 public function __construct( $args = array() ) { 240 241 $args = array_merge( 242 array_fill_keys( array( 'changeset_uuid', 'theme', 'messenger_channel' ), null ), 243 $args 244 ); 245 246 if ( ! isset( $args['changeset_uuid'] ) ) { 247 $args['changeset_uuid'] = wp_generate_uuid4(); 248 } 249 250 // The theme and messenger_channel should be supplied via $args, but they are also looked at in the $_REQUEST global here for back-compat. 251 if ( ! isset( $args['theme'] ) ) { 252 if ( isset( $_REQUEST['customize_theme'] ) ) { 253 $args['theme'] = wp_unslash( $_REQUEST['customize_theme'] ); 254 } elseif ( isset( $_REQUEST['theme'] ) ) { // Deprecated. 255 $args['theme'] = wp_unslash( $_REQUEST['theme'] ); 256 } 257 } 258 if ( ! isset( $args['messenger_channel'] ) && isset( $_REQUEST['customize_messenger_channel'] ) ) { 259 $args['messenger_channel'] = sanitize_key( wp_unslash( $_REQUEST['customize_messenger_channel'] ) ); 260 } 261 262 $this->original_stylesheet = get_stylesheet(); 263 $this->theme = wp_get_theme( $args['theme'] ); 264 foreach ( array( 'changeset_uuid', 'messenger_channel' ) as $key ) { 265 $this->$key = $args[ $key ]; 266 } 267 208 268 require_once( ABSPATH . WPINC . '/class-wp-customize-setting.php' ); 209 269 require_once( ABSPATH . WPINC . '/class-wp-customize-panel.php' ); 210 270 require_once( ABSPATH . WPINC . '/class-wp-customize-section.php' ); … … 271 331 $this->nav_menus = new WP_Customize_Nav_Menus( $this ); 272 332 } 273 333 274 add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );275 276 334 add_action( 'setup_theme', array( $this, 'setup_theme' ) ); 277 335 add_action( 'wp_loaded', array( $this, 'wp_loaded' ) ); 278 336 279 // Run wp_redirect_status late to make sure we override the status last.280 add_action( 'wp_redirect_status', array( $this, 'wp_redirect_status' ), 1000 );281 282 337 // Do not spawn cron (especially the alternate cron) while running the Customizer. 283 338 remove_action( 'init', 'wp_cron' ); 284 339 … … 340 395 * @param mixed $message UI message 341 396 */ 342 397 protected function wp_die( $ajax_message, $message = null ) { 343 if ( $this->doing_ajax() || isset( $_POST['customized'] )) {398 if ( $this->doing_ajax() ) { 344 399 wp_die( $ajax_message ); 345 400 } 346 401 … … 348 403 $message = __( 'Cheatin’ uh?' ); 349 404 } 350 405 406 if ( $this->messenger_channel ) { 407 header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) ); 408 echo '<!DOCTYPE html><html>'; 409 wp_print_scripts( array( 'customize-base' ) ); 410 411 $settings = array( 412 'messengerArgs' => array( 413 'channel' => $this->messenger_channel, 414 'url' => wp_customize_url(), 415 ), 416 'error' => $ajax_message, 417 ); 418 ?> 419 <script> 420 ( function( api, settings ) { 421 var preview = new api.Messenger( settings.messengerArgs ); 422 preview.send( 'iframe-loading-error', settings.error ); 423 } )( wp.customize, <?php echo wp_json_encode( $settings ) ?> ); 424 </script> 425 <?php 426 echo "<p>$message</p>"; 427 echo '</html>'; 428 die(); // @todo Not testable. 429 } 430 351 431 wp_die( $message ); 352 432 } 353 433 … … 355 435 * Return the Ajax wp_die() handler if it's a customized request. 356 436 * 357 437 * @since 3.4.0 438 * @deprecated 4.7.0 358 439 * 359 * @return string440 * @return callable Die handler. 360 441 */ 361 442 public function wp_die_handler() { 443 _deprecated_function( __METHOD__, '4.7.0' ); 444 362 445 if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) { 363 446 return '_ajax_wp_die_handler'; 364 447 } … … 374 457 * @since 3.4.0 375 458 */ 376 459 public function setup_theme() { 377 send_origin_headers();460 global $pagenow; 378 461 379 $doing_ajax_or_is_customized = ( $this->doing_ajax() || isset( $_POST['customized'] ) ); 380 if ( is_admin() && ! $doing_ajax_or_is_customized ) { 381 auth_redirect(); 382 } elseif ( $doing_ajax_or_is_customized && ! is_user_logged_in() ) { 383 $this->wp_die( 0, __( 'You must be logged in to complete this action.' ) ); 462 // Check permissions for customize.php access since this method is called before customize.php can run any code, 463 if ( 'customize.php' === $pagenow && ! current_user_can( 'customize' ) ) { 464 if ( ! is_user_logged_in() ) { 465 auth_redirect(); 466 } else { 467 wp_die( 468 '<h1>' . __( 'Cheatin’ uh?' ) . '</h1>' . 469 '<p>' . __( 'Sorry, you are not allowed to customize this site.' ) . '</p>', 470 403 471 ); 472 } 473 return; 384 474 } 385 475 386 show_admin_bar( false ); 476 if ( ! preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $this->changeset_uuid ) ) { 477 $this->wp_die( -1, __( 'Invalid changeset UUID' ) ); 478 } 387 479 388 if ( ! current_user_can( 'customize' ) ) { 389 $this->wp_die( -1, __( 'Sorry, you are not allowed to customize this site.' ) ); 480 // If unauthenticated then require a valid changeset UUID to load the preview. In this way, the UUID serves as a secret key. 481 if ( ! current_user_can( 'customize' ) && ! $this->changeset_post_id() ) { 482 $this->wp_die( -1, __( 'Non-existent changeset UUID.' ) ); 390 483 } 391 484 392 $this->original_stylesheet = get_stylesheet();485 send_origin_headers(); 393 486 394 $this->theme = wp_get_theme( isset( $_REQUEST['theme'] ) ? $_REQUEST['theme'] : null ); 487 // Hide the admin bar if we're embedded in the customizer iframe. 488 if ( $this->messenger_channel ) { 489 show_admin_bar( false ); 490 } 395 491 396 492 if ( $this->is_theme_active() ) { 397 493 // Once the theme is loaded, we'll validate it. … … 507 603 } 508 604 509 605 /** 606 * Get the changeset UUID. 607 * 608 * @since 4.7.0 609 * @access public 610 * 611 * @return string UUID. 612 */ 613 public function changeset_uuid() { 614 return $this->changeset_uuid; 615 } 616 617 /** 510 618 * Get the theme being customized. 511 619 * 512 620 * @since 3.4.0 … … 603 711 */ 604 712 do_action( 'customize_register', $this ); 605 713 606 if ( $this->is_preview() && ! is_admin() ) 714 /* 715 * Note that settings must be previewed here even outside the customizer preview 716 * and also in the customizer pane itself. This is to enable loading an existing 717 * changeset into the customizer. Previewing the settings only has to be prevented 718 * in the case of a customize_save action because then update_option() 719 * may short-circuit because it will detect that there are no changes to 720 * make. 721 */ 722 if ( ! $this->doing_ajax( 'customize_save' ) ) { 723 foreach ( $this->settings as $setting ) { 724 $setting->preview(); 725 } 726 } 727 728 if ( $this->is_preview() && ! is_admin() ) { 607 729 $this->customize_preview_init(); 730 } 608 731 } 609 732 610 733 /** … … 614 737 * Instead, the JS will sniff out the location header. 615 738 * 616 739 * @since 3.4.0 740 * @deprecated 4.7.0 617 741 * 618 * @param $status742 * @param int $status Status. 619 743 * @return int 620 744 */ 621 745 public function wp_redirect_status( $status ) { 622 if ( $this->is_preview() && ! is_admin() ) 746 _deprecated_function( __FUNCTION__, '4.7.0' ); 747 748 if ( $this->is_preview() && ! is_admin() ) { 623 749 return 200; 750 } 624 751 625 752 return $status; 626 753 } 627 754 628 755 /** 756 * Get the changeset post. 757 * 758 * @todo Add persistent object caching for query. 759 * 760 * @since 4.7.0 761 * @access public 762 * 763 * @return int|false 764 */ 765 public function changeset_post_id() { 766 if ( isset( $this->_changeset_post_id ) ) { 767 return $this->_changeset_post_id; 768 } 769 $changeset_post_query = new WP_Query( array( 770 'post_type' => 'customize_changeset', 771 'post_status' => get_post_stati(), 772 'name' => $this->changeset_uuid, 773 'number' => 1, 774 'no_found_rows' => true, 775 'update_post_meta_cache' => false, 776 'update_term_meta_cache' => false, 777 ) ); 778 if ( ! empty( $changeset_post_query->posts ) ) { 779 $this->_changeset_post_id = $changeset_post_query->posts[0]->ID; 780 } else { 781 $this->_changeset_post_id = false; 782 } 783 return $this->_changeset_post_id; 784 } 785 786 /** 787 * Get changeset data. 788 * 789 * @since 4.7.0 790 * 791 * @return array Changeset data. 792 */ 793 public function changeset_data() { 794 if ( isset( $this->_changeset_data ) ) { 795 return $this->_changeset_data; 796 } 797 $changeset_post_id = $this->changeset_post_id(); 798 if ( ! $changeset_post_id ) { 799 $this->_changeset_data = array(); 800 } else { 801 $changeset_post = get_post( $changeset_post_id ); 802 if ( $changeset_post ) { 803 $changeset = json_decode( $changeset_post->post_content, true ); 804 if ( is_array( $changeset ) ) { 805 $this->_changeset_data = $changeset; 806 } else { 807 $this->_changeset_data = array(); 808 } 809 } 810 } 811 return $this->_changeset_data; 812 } 813 814 /** 629 815 * Parse the incoming $_POST['customized'] JSON data and store the unsanitized 630 816 * settings for subsequent post_value() lookups. 631 817 * 632 818 * @since 4.1.1 819 * @since 4.7.0 Added $args param. 633 820 * 821 * @param array $args { 822 * Args. 823 * 824 * @type bool $exclude_changeset Whether or not the changeset values should also be included. This is excluded obtaining values for updating a snapshot. 825 * @type bool $exclude_post_data Whether or not the post input values should also be included. When lacking a valid nonce, this should be excluded. 826 * } 634 827 * @return array 635 828 */ 636 public function unsanitized_post_values() { 637 if ( ! isset( $this->_post_values ) ) { 638 if ( isset( $_POST['customized'] ) ) { 639 $this->_post_values = json_decode( wp_unslash( $_POST['customized'] ), true ); 829 public function unsanitized_post_values( $args = array() ) { 830 $args = array_merge( 831 array( 832 'exclude_changeset' => false, 833 'exclude_post_data' => ! current_user_can( 'customize' ), 834 ), 835 $args 836 ); 837 838 $values = array(); 839 840 // Let default values be from the stashed theme mods if doing a theme switch and if no changeset is present. 841 if ( ! $this->is_theme_active() && ! $this->changeset_post_id() ) { 842 $stashed_theme_mods = get_option( 'customize_stashed_theme_mods' ); 843 $stylesheet = $this->get_stylesheet(); 844 if ( isset( $stashed_theme_mods[ $stylesheet ] ) ) { 845 $values = array_merge( $values, wp_list_pluck( $stashed_theme_mods[ $stylesheet ], 'value' ) ); 640 846 } 641 if ( empty( $this->_post_values ) ) { // if not isset or if JSON error 642 $this->_post_values = array(); 847 } 848 849 if ( ! $args['exclude_changeset'] ) { 850 foreach ( $this->changeset_data() as $setting_id => $setting_params ) { 851 if ( ! array_key_exists( 'value', $setting_params ) ) { 852 continue; 853 } 854 if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) { 855 856 // Ensure that theme mods values are only used if they were saved under the current theme. 857 $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/'; 858 if ( preg_match( $namespace_pattern, $setting_id, $matches ) && $this->get_stylesheet() === $matches['stylesheet'] ) { 859 $values[ $matches['setting_id'] ] = $setting_params['value']; 860 } 861 } else { 862 $values[ $setting_id ] = $setting_params['value']; 863 } 643 864 } 644 865 } 645 if ( empty( $this->_post_values ) ) { 646 return array(); 647 } else { 648 return $this->_post_values; 866 867 if ( ! $args['exclude_post_data'] ) { 868 if ( ! isset( $this->_post_values ) ) { 869 if ( isset( $_POST['customized'] ) ) { 870 $post_values = json_decode( wp_unslash( $_POST['customized'] ), true ); 871 } else { 872 $post_values = array(); 873 } 874 if ( is_array( $post_values ) ) { 875 $this->_post_values = $post_values; 876 } else { 877 $this->_post_values = array(); 878 } 879 } 880 $values = array_merge( $values, $this->_post_values ); 649 881 } 882 return $values; 650 883 } 651 884 652 885 /** … … 733 966 * @since 3.4.0 734 967 */ 735 968 public function customize_preview_init() { 736 $this->nonce_tick = check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'nonce' );737 969 738 $this->prepare_controls(); 970 /* 971 * Now that Customizer previews are loaded into iframes via GET requests 972 * and natural URLs with transaction UUIDs added, we need to ensure that 973 * the responses are never cached by proxies. In practice, this will not 974 * be needed if the user is logged-in anyway. But if anonymous access is 975 * allowed then the auth cookies would not be sent and WordPress would 976 * not send no-cache headers by default. 977 */ 978 nocache_headers(); 979 if ( ! headers_sent() ) { 980 header( 'X-Robots: noindex, nofollow, noarchive' ); 981 } 982 add_action( 'wp_head', 'wp_no_robots' ); 983 add_filter( 'wp_headers', array( $this, 'filter_iframe_security_headers' ) ); 984 985 /* 986 * If preview is being served inside the customizer preview iframe, and 987 * if the user doesn't have customize capability, then it is assumed 988 * that the user's session has expired and they need to re-authenticate. 989 */ 990 if ( $this->messenger_channel && ! current_user_can( 'customize' ) ) { 991 $this->wp_die( -1, __( 'Unauthorized. You may remove the customize_messenger_channel param to preview as frontend.' ) ); 992 return; 993 } 994 995 if ( current_user_can( 'customize' ) ) { 996 $this->prepare_controls(); 997 } 739 998 740 999 wp_enqueue_script( 'customize-preview' ); 741 add_action( 'wp', array( $this, 'customize_preview_override_404_status' ) );742 add_action( 'wp_head', array( $this, 'customize_preview_base' ) );743 1000 add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) ); 744 1001 add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 ); 745 add_action( 'shutdown', array( $this, 'customize_preview_signature' ), 1000 );746 add_filter( 'wp_die_handler', array( $this, 'remove_preview_signature' ) );747 748 foreach ( $this->settings as $setting ) {749 $setting->preview();750 }751 1002 752 1003 /** 753 1004 * Fires once the Customizer preview has initialized and JavaScript … … 761 1012 } 762 1013 763 1014 /** 1015 * Filter the X-Frame-Options and Content-Security-Policy headers to ensure frontend can load in customizer. 1016 * 1017 * @since 4.7.0 1018 * @access public 1019 * 1020 * @param array $headers Headers. 1021 * @return array Headers. 1022 */ 1023 public function filter_iframe_security_headers( $headers ) { 1024 $customize_url = admin_url( 'customize.php' ); 1025 $headers['X-Frame-Options'] = 'ALLOW-FROM ' . $customize_url; 1026 $headers['Content-Security-Policy'] = 'frame-ancestors ' . preg_replace( '#^(\w+://[^/]+).+?$#', '$1', $customize_url ); 1027 return $headers; 1028 } 1029 1030 /** 764 1031 * Prevent sending a 404 status when returning the response for the customize 765 1032 * preview, since it causes the jQuery Ajax to fail. Send 200 instead. 766 1033 * 767 1034 * @since 4.0.0 1035 * @deprecated 4.7.0 768 1036 * @access public 769 1037 */ 770 1038 public function customize_preview_override_404_status() { 771 if ( is_404() ) { 772 status_header( 200 ); 773 } 1039 _deprecated_function( __METHOD__, '4.7.0' ); 774 1040 } 775 1041 776 1042 /** 777 1043 * Print base element for preview frame. 778 1044 * 779 1045 * @since 3.4.0 1046 * @deprecated 4.7.0 780 1047 */ 781 1048 public function customize_preview_base() { 782 ?><base href="<?php echo home_url( '/' ); ?>" /><?php1049 _deprecated_function( __METHOD__, '4.7.0' ); 783 1050 } 784 1051 785 1052 /** … … 809 1076 body.wp-customizer-unloading * { 810 1077 pointer-events: none !important; 811 1078 } 1079 form.customize-unpreviewable, 1080 form.customize-unpreviewable input, 1081 form.customize-unpreviewable select, 1082 form.customize-unpreviewable button, 1083 a.customize-unpreviewable, 1084 area.customize-unpreviewable { 1085 cursor: not-allowed !important; 1086 } 812 1087 </style><?php 813 1088 } 814 1089 … … 818 1093 * @since 3.4.0 819 1094 */ 820 1095 public function customize_preview_settings() { 821 $setting_validities = $this->validate_setting_values( $this->unsanitized_post_values() ); 822 $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities ); 1096 $setting_values = $this->unsanitized_post_values(); 1097 1098 $self_url = home_url( empty( $_SERVER['REQUEST_URI'] ) ? '/' : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ); 1099 $state_query_params = array( 1100 'customize_theme', 1101 'customize_changeset_uuid', 1102 'customize_messenger_channel', 1103 ); 1104 $self_url = remove_query_arg( $state_query_params, $self_url ); 823 1105 1106 $allowed_urls = $this->get_allowed_urls(); 1107 $allowed_hosts = array(); 1108 foreach ( $allowed_urls as $allowed_url ) { 1109 $parsed = wp_parse_url( $allowed_url ); 1110 if ( empty( $parsed['host'] ) ) { 1111 continue; 1112 } 1113 $host = $parsed['host']; 1114 if ( ! empty( $parsed['port'] ) ) { 1115 $host .= ':' . $parsed['port']; 1116 } 1117 $allowed_hosts[] = $host; 1118 } 824 1119 $settings = array( 1120 'changeset' => array( 1121 'uuid' => $this->changeset_uuid, 1122 'stateQueryParams' => $state_query_params, // @todo Additional persisted query vars may need to be indiated via a customized_persisted_query_vars param. 1123 ), 825 1124 'theme' => array( 826 1125 'stylesheet' => $this->get_stylesheet(), 827 1126 'active' => $this->is_theme_active(), 828 1127 ), 829 1128 'url' => array( 830 'self' => empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ), 1129 'self' => $self_url, 1130 'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ), 1131 'allowedHosts' => array_unique( $allowed_hosts ), 1132 'isCrossDomain' => $this->is_cross_domain(), 831 1133 ), 832 'channel' => wp_unslash( $_POST['customize_messenger_channel'] ),1134 'channel' => $this->messenger_channel, 833 1135 'activePanels' => array(), 834 1136 'activeSections' => array(), 835 1137 'activeControls' => array(), 836 'settingValidities' => $exported_setting_validities, 837 'nonce' => $this->get_nonces(), 1138 'nonce' => current_user_can( 'customize' ) ? $this->get_nonces() : array(), 838 1139 'l10n' => array( 839 1140 'shiftClickToEdit' => __( 'Shift-click to edit this element.' ), 1141 'linkUnpreviewable' => __( 'This link is not live-previewable.' ), 1142 'formUnpreviewable' => __( 'This form is not live-previewable.' ), 840 1143 ), 841 '_dirty' => array_keys( $ this->unsanitized_post_values()),1144 '_dirty' => array_keys( $setting_values ), 842 1145 ); 843 1146 844 1147 foreach ( $this->panels as $panel_id => $panel ) { … … 892 1195 * Prints a signature so we can ensure the Customizer was properly executed. 893 1196 * 894 1197 * @since 3.4.0 1198 * @deprecated 4.7.0 895 1199 */ 896 1200 public function customize_preview_signature() { 897 echo 'WP_CUSTOMIZER_SIGNATURE';1201 _deprecated_function( __METHOD__, '4.7.0' ); 898 1202 } 899 1203 900 1204 /** 901 1205 * Removes the signature in case we experience a case where the Customizer was not properly executed. 902 1206 * 903 1207 * @since 3.4.0 1208 * @deprecated 4.7.0 904 1209 * 905 1210 * @param mixed $return Value passed through for {@see 'wp_die_handler'} filter. 906 1211 * @return mixed Value passed through for {@see 'wp_die_handler'} filter. 907 1212 */ 908 1213 public function remove_preview_signature( $return = null ) { 909 remove_action( 'shutdown', array( $this, 'customize_preview_signature' ), 1000);1214 _deprecated_function( __METHOD__, '4.7.0' ); 910 1215 911 1216 return $return; 912 1217 } … … 993 1298 * @see WP_Customize_Setting::validate() 994 1299 * 995 1300 * @param array $setting_values Mapping of setting IDs to values to validate and sanitize. 1301 * @param array $options { 1302 * Options. 1303 * 1304 * @var bool $validate_existence Whether a setting's existence will be checked. 1305 * @var bool $validate_capability Whether the setting capability will be checked. 1306 * } 996 1307 * @return array Mapping of setting IDs to return value of validate method calls, either `true` or `WP_Error`. 997 1308 */ 998 public function validate_setting_values( $setting_values ) { 1309 public function validate_setting_values( $setting_values, $options = array() ) { 1310 $options = wp_parse_args( $options, array( 1311 'validate_capability' => false, 1312 'validate_existence' => false, 1313 ) ); 1314 999 1315 $validities = array(); 1000 1316 foreach ( $setting_values as $setting_id => $unsanitized_value ) { 1001 1317 $setting = $this->get_setting( $setting_id ); 1002 if ( ! $setting || is_null( $unsanitized_value ) ) { 1318 if ( ! $setting ) { 1319 if ( $options['validate_existence'] ) { 1320 $validities[ $setting_id ] = new WP_Error( 'unrecognized_setting', __( 'Setting does not exist or is unrecognized.' ) ); 1321 } 1322 continue; 1323 } 1324 if ( is_null( $unsanitized_value ) ) { 1325 // @todo Should this be skipped or be invalid? 1003 1326 continue; 1004 1327 } 1005 $validity = $setting->validate( $unsanitized_value ); 1328 if ( $options['validate_capability'] && ! current_user_can( $setting->capability ) ) { 1329 $validity = new WP_Error( 'unauthorized', __( 'Unauthorized.' ) ); 1330 } else { 1331 $validity = $setting->validate( $unsanitized_value ); 1332 } 1006 1333 if ( ! is_wp_error( $validity ) ) { 1007 1334 $value = $setting->sanitize( $unsanitized_value ); 1008 1335 if ( is_null( $value ) ) { … … 1049 1376 } 1050 1377 1051 1378 /** 1052 * S witch the theme and trigger the save() method on each setting.1379 * Save (update) changeset. 1053 1380 * 1054 1381 * @since 3.4.0 1382 * @since 4.7.0 The semantics of this method have changed to update a changeset, optionally to also change the status. 1055 1383 */ 1056 1384 public function save() { 1385 if ( ! is_user_logged_in() ) { 1386 wp_send_json_error( 'unauthenticated' ); 1387 } 1388 1057 1389 if ( ! $this->is_preview() ) { 1058 1390 wp_send_json_error( 'not_preview' ); 1059 1391 } … … 1063 1395 wp_send_json_error( 'invalid_nonce' ); 1064 1396 } 1065 1397 1398 $changeset_post_id = $this->changeset_post_id(); 1399 if ( $changeset_post_id && 'publish' === get_post_status( $changeset_post_id ) ) { 1400 wp_send_json_error( 'changeset_already_published' ); 1401 } 1402 1403 if ( empty( $changeset_post_id ) ) { 1404 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->create_posts ) ) { 1405 wp_send_json_error( 'cannot_create_changeset_post' ); 1406 } 1407 } else { 1408 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) { 1409 wp_send_json_error( 'cannot_edit_changeset_post' ); 1410 } 1411 } 1412 1413 if ( ! empty( $_POST['customize_changeset_data'] ) ) { 1414 $input_changeset_data = json_decode( wp_unslash( $_POST['customize_changeset_data'] ), true ); 1415 if ( ! is_array( $input_changeset_data ) ) { 1416 wp_send_json_error( 'invalid_customize_changeset_data' ); 1417 } 1418 foreach ( $input_changeset_data as $setting_id => $setting_params ) { 1419 if ( array_key_exists( 'value', $setting_params ) ) { 1420 $this->set_post_value( $setting_id, $setting_params['value'] ); // Add to post values so that they can be validated and sanitized. 1421 } 1422 } 1423 $this->add_dynamic_settings( array_keys( $input_changeset_data ) ); // Ensure settings get created even if they lack an input value. 1424 } else { 1425 $input_changeset_data = array(); 1426 } 1427 1428 // Validate title. 1429 $changeset_title = null; 1430 if ( isset( $_POST['customize_changeset_title'] ) ) { 1431 $changeset_title = sanitize_text_field( wp_unslash( $_POST['customize_changeset_title'] ) ); 1432 } 1433 1434 // Validate changeset status param. 1435 $changeset_status = null; 1436 if ( isset( $_POST['customize_changeset_status'] ) ) { 1437 $changeset_status = wp_unslash( $_POST['customize_changeset_status'] ); 1438 if ( ! get_post_status_object( $changeset_status ) || ! in_array( $changeset_status, array( 'draft', 'pending', 'publish', 'future' ), true ) ) { 1439 wp_send_json_error( 'bad_customize_changeset_status', 400 ); 1440 } 1441 $is_publish = ( 'publish' === $changeset_status || 'future' === $changeset_status ); 1442 if ( $is_publish ) { 1443 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) { 1444 wp_send_json_error( 'changeset_publish_unauthorized', 403 ); 1445 } 1446 if ( false === has_action( 'publish_customize_changeset', '_wp_customize_publish_changeset' ) ) { 1447 wp_send_json_error( 'missing_publish_callback', 500 ); 1448 } 1449 } 1450 } 1451 1452 // Validate changeset date param. Date is assumed to be in local time for the WP. 1453 $changeset_date = null; 1454 if ( isset( $_POST['customize_changeset_date'] ) ) { 1455 $changeset_date = wp_unslash( $_POST['customize_changeset_date'] ); 1456 if ( ! preg_match( '/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/', $changeset_date ) ) { 1457 wp_send_json_error( 'bad_customize_changeset_date', 400 ); 1458 } 1459 if ( ( 'publish' === $changeset_status || 'future' === $changeset_status ) && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) { 1460 wp_send_json_error( 'changeset_publish_unauthorized', 403 ); 1461 } 1462 $changeset_date_gmt = get_gmt_from_date( $changeset_date ); 1463 $now = gmdate( 'Y-m-d H:i:59' ); 1464 $is_future_dated = ( mysql2date( 'U', $changeset_date_gmt, false ) > mysql2date( 'U', $now, false ) ); 1465 if ( ! $this->is_theme_active() && ( 'future' === $changeset_status || $is_future_dated ) ) { 1466 wp_send_json_error( 'cannot_schedule_theme_switches', 400 ); // @todo This should be allowed in the future, when theme is a regular setting. 1467 } 1468 $will_remain_auto_draft = ( ! $changeset_status && ( ! $changeset_post_id || 'auto-draft' === get_post_status( $changeset_post_id ) ) ); 1469 if ( $changeset_date && $will_remain_auto_draft ) { 1470 wp_send_json_error( 'cannot_supply_date_for_auto_draft_changeset', 400 ); 1471 } 1472 } 1473 1066 1474 /** 1067 1475 * Fires before save validation happens. 1068 1476 * … … 1077 1485 do_action( 'customize_save_validation_before', $this ); 1078 1486 1079 1487 // Validate settings. 1080 $setting_validities = $this->validate_setting_values( $this->unsanitized_post_values() ); 1488 $post_values = $this->unsanitized_post_values( array( 1489 'exclude_changeset' => true, 1490 'exclude_post_data' => false, 1491 ) ); 1492 $this->add_dynamic_settings( array_keys( $post_values ) ); 1493 $setting_validities = $this->validate_setting_values( $post_values, array( 1494 'validate_capability' => true, 1495 'validate_existence' => true, 1496 ) ); 1081 1497 $invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) ); 1082 1498 $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities ); 1083 if ( $invalid_setting_count > 0 ) { 1499 1500 /* 1501 * Short-circuit if there are invalid settings and attempting to transition 1502 * the changeset to a new status (i.e. publish). When the changeset_status 1503 * is set then the request is considered transactional, where if any of the 1504 * setting are invalid then none of them will be saved. 1505 * 1506 * @todo Consider a separate 'transactional' argument which defaults to true if the $changeset_status is set. 1507 */ 1508 if ( $changeset_status && $invalid_setting_count > 0 ) { 1084 1509 $response = array( 1085 1510 'setting_validities' => $exported_setting_validities, 1086 1511 'message' => sprintf( _n( 'There is %s invalid setting.', 'There are %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ), … … 1091 1516 wp_send_json_error( $response ); 1092 1517 } 1093 1518 1094 // Do we have to switch themes? 1095 if ( ! $this->is_theme_active() ) { 1096 // Temporarily stop previewing the theme to allow switch_themes() 1097 // to operate properly. 1519 $response = array( 1520 'setting_validities' => $exported_setting_validities, 1521 ); 1522 1523 // Obtain/merge data for changeset. 1524 $updated_setting_ids = array(); 1525 $original_changeset_data = $this->changeset_data(); 1526 $data = $original_changeset_data; 1527 1528 foreach ( $input_changeset_data as $setting_id => $setting_params ) { 1529 $setting = $this->get_setting( $setting_id ); 1530 if ( ! $setting || ! $setting->check_capabilities() ) { 1531 continue; 1532 } 1533 1534 // Skip updating changeset for invalid setting values. 1535 if ( isset( $setting_validities[ $setting_id ] ) && is_wp_error( $setting_validities[ $setting_id ] ) ) { 1536 continue; 1537 } 1538 1539 $changeset_setting_id = $setting_id; 1540 if ( 'theme_mod' === $setting->type ) { 1541 $changeset_setting_id = sprintf( '%s::%s', $this->get_stylesheet(), $setting_id ); 1542 } 1543 1544 if ( null === $setting_params ) { 1545 // Remove setting from changeset entirely. 1546 unset( $data[ $changeset_setting_id ] ); 1547 } else { 1548 // Merge any additional setting params that have been supplied with the existing params. 1549 if ( ! isset( $data[ $changeset_setting_id ] ) ) { 1550 $data[ $changeset_setting_id ] = array(); 1551 } 1552 $data[ $changeset_setting_id ] = array_merge( 1553 $data[ $changeset_setting_id ], 1554 $setting_params, 1555 array( 'type' => $setting->type ) 1556 ); 1557 } 1558 $updated_setting_ids[] = $setting_id; 1559 } // End foreach(). 1560 1561 $filter_context = array( 1562 'uuid' => $this->changeset_uuid(), 1563 'status' => $changeset_status, 1564 'date' => $changeset_date, 1565 'post_id' => $this->changeset_post_id(), 1566 'previous_data' => $original_changeset_data, 1567 'manager' => $this, 1568 ); 1569 1570 /** 1571 * Filters the settings' data that will be persisted into the changeset. 1572 * 1573 * Plugins may amend additional data (such as additional meta for settings) into the changeset with this filter. 1574 * 1575 * @param array $data Updated changeset data, mapping setting IDs to arrays containing a $value item and optionally other metadata. 1576 * @param array $context { 1577 * Filter context. 1578 * 1579 * @type string $uuid Changeset UUID. 1580 * @type string $title Requested title for the changeset post. 1581 * @type string $status Requested status for the changeset post. 1582 * @type string $date Requested date for the changeset post. 1583 * @type int|false $post_id Post ID for the changeset, or false if it doesn't exist yet. 1584 * @type array $previous_data Previous data contained in the changeset. 1585 * @type WP_Customize_Manager $manager Manager instance. 1586 * } 1587 */ 1588 $data = apply_filters( 'customize_changeset_save', $data, $filter_context ); 1589 1590 // Switch theme if publishing changes now. 1591 if ( 'publish' === $changeset_status && ! $this->is_theme_active() ) { 1592 // Temporarily stop previewing the theme to allow switch_themes() to operate properly. 1098 1593 $this->stop_previewing_theme(); 1099 1594 switch_theme( $this->get_stylesheet() ); 1100 1595 update_option( 'theme_switched_via_customizer', true ); 1101 1596 $this->start_previewing_theme(); 1102 1597 } 1103 1598 1599 // Gather the data for wp_insert_post()/wp_update_post(). 1600 $json_options = 0; 1601 if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) { 1602 $json_options |= JSON_UNESCAPED_SLASHES; // Introduced in PHP 5.4. 1603 } 1604 $json_options |= JSON_PRETTY_PRINT; // Also introduced in PHP 5.4, but WP defines constant for back compat. See WP Trac #30139. 1605 $post_array = array( 1606 'post_content' => wp_json_encode( $data, $json_options ), 1607 ); 1608 if ( $changeset_title ) { 1609 $post_array['post_title'] = $changeset_title; 1610 } 1611 if ( $changeset_post_id ) { 1612 $post_array['ID'] = $changeset_post_id; 1613 } else { 1614 $post_array['post_type'] = 'customize_changeset'; 1615 $post_array['post_name'] = $this->changeset_uuid(); 1616 $post_array['post_status'] = 'auto-draft'; 1617 } 1618 if ( $changeset_status ) { 1619 $post_array['post_status'] = $changeset_status; 1620 } 1621 if ( $changeset_date ) { 1622 $post_array['post_date'] = $changeset_date; 1623 } 1624 1625 // @todo Consider a separate 'revision' param which makes this explicit, versus looking at whether the status is provided. 1626 $this->store_changeset_revision = ! empty( $changeset_status ); 1627 add_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ), 5, 3 ); 1628 1629 // @todo Add create revision arg for wp.customize.previewer.save() which defaults to true if status is provided. 1630 // Update the changeset post. The publish_customize_changeset action will cause the settings in the changeset to be saved via WP_Customize_Setting::save(). 1631 $has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) ); 1632 if ( $has_kses ) { 1633 kses_remove_filters(); // Prevent KSES from corrupting JSON in post_content. 1634 } 1635 if ( $changeset_post_id ) { 1636 $r = wp_update_post( wp_slash( $post_array ), true ); 1637 } else { 1638 $r = wp_insert_post( wp_slash( $post_array ), true ); 1639 if ( ! is_wp_error( $changeset_post_id ) ) { 1640 $changeset_post_id = $r; 1641 $this->_changeset_post_id = $changeset_post_id; 1642 } 1643 } 1644 if ( $has_kses ) { 1645 kses_init_filters(); 1646 } 1647 1648 remove_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ) ); 1649 1650 if ( is_wp_error( $r ) ) { 1651 $response['snapshot_save_failure'] = $r->get_error_code(); 1652 1653 /** This filter is documented in wp-includes/class-wp-customize-manager.php */ 1654 $response = apply_filters( 'customize_save_response', $response, $this ); 1655 wp_send_json_error( $response ); 1656 } 1657 1658 // Note that if the changeset status was publish, then it will get get set to trash if revisions are not supported. 1659 if ( 'publish' === $changeset_status ) { 1660 $response['changeset_status'] = $changeset_status; 1661 } else { 1662 $response['changeset_status'] = get_post_status( $changeset_post_id ); 1663 } 1664 1665 if ( 'publish' === $response['changeset_status'] ) { 1666 $response['next_changeset_uuid'] = wp_generate_uuid4(); 1667 } 1668 1669 $response['changeset_updated_settings'] = $updated_setting_ids; 1670 1671 /** 1672 * Filters response data for a successful customize_save Ajax request. 1673 * 1674 * This filter does not apply if there was a nonce or authentication failure. 1675 * 1676 * @since 4.2.0 1677 * 1678 * @param array $response Additional information passed back to the 'saved' 1679 * event on `wp.customize`. 1680 * @param WP_Customize_Manager $this WP_Customize_Manager instance. 1681 */ 1682 $response = apply_filters( 'customize_save_response', $response, $this ); 1683 wp_send_json_success( $response ); 1684 } 1685 1686 /** 1687 * Whether a changeset revision should be made. 1688 * 1689 * @since 4.7.0 1690 * @access private 1691 * @var bool 1692 */ 1693 protected $store_changeset_revision; 1694 1695 /** 1696 * Filters whether a changeset has changed to create a new revision. 1697 * 1698 * Note that this will not be called while a changeset post remains in auto-draft status. 1699 * 1700 * @since 4.7.0 1701 * @access private 1702 * 1703 * @param bool $post_has_changed Whether the post has changed. 1704 * @param WP_Post $last_revision The last revision post object. 1705 * @param WP_Post $post The post object. 1706 * 1707 * @return bool Whether a revision should be made. 1708 */ 1709 public function _filter_revision_post_has_changed( $post_has_changed, $last_revision, $post ) { 1710 unset( $last_revision ); 1711 if ( 'customize_changeset' === $post->post_type ) { 1712 $post_has_changed = $this->store_changeset_revision; 1713 } 1714 return $post_has_changed; 1715 } 1716 1717 /** 1718 * Publish changeset values. 1719 * 1720 * This will the values contained in a changeset, even changesets that do not 1721 * correspond to current manager instance. This is called by 1722 * `_wp_customize_publish_changeset()` when a customize_changeset post is 1723 * transitioned to the `publish` status. 1724 * 1725 * Please note that if the settings in the changeset are for a non-activated 1726 * theme, the theme must first be switched to (via `switch_theme()`) before 1727 * invoking this method. 1728 * 1729 * @since 4.7.0 1730 * @see _wp_customize_publish_changeset() 1731 * 1732 * @param int $changeset_post_id ID for customize_changeset post. Defaults to the changeset for the current manager instance. 1733 */ 1734 public function publish_changeset_values( $changeset_post_id = null ) { 1735 1736 if ( empty( $changeset_post_id ) ) { 1737 $changeset_post_id = $this->changeset_post_id(); 1738 } 1739 if ( empty( $changeset_post_id ) ) { 1740 return; 1741 } 1742 $changeset_post = get_post( $changeset_post_id ); 1743 if ( ! $changeset_post ) { 1744 return; 1745 } 1746 1747 $changeset_data = json_decode( $changeset_post->post_content, true ); 1748 if ( ! is_array( $changeset_data ) ) { 1749 return; 1750 } 1751 1752 $other_theme_mod_settings = array(); 1753 $active_theme_setting_values = array(); 1754 foreach ( $changeset_data as $raw_setting_id => $setting_params ) { 1755 1756 if ( ! isset( $setting_params['value'] ) ) { 1757 continue; 1758 } 1759 1760 // @todo There is some duplication of logic here with what is in the save method. 1761 if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) { 1762 1763 // Ensure that theme mods values are only used if they were saved under the current theme. 1764 $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/'; 1765 if ( preg_match( $namespace_pattern, $raw_setting_id, $matches ) ) { 1766 if ( $this->get_stylesheet() === $matches['stylesheet'] ) { 1767 $active_theme_setting_values[ $matches['setting_id'] ] = $setting_params['value']; 1768 } else { 1769 if ( ! isset( $other_theme_mod_settings[ $matches['stylesheet'] ] ) ) { 1770 $other_theme_mod_settings[ $matches['stylesheet'] ] = array(); 1771 } 1772 $other_theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params; 1773 } 1774 } 1775 } else { 1776 $active_theme_setting_values[ $raw_setting_id ] = $setting_params['value']; 1777 } 1778 } 1779 1780 $this->add_dynamic_settings( array_keys( $active_theme_setting_values ) ); 1781 1104 1782 /** 1105 1783 * Fires once the theme has switched in the Customizer, but before settings 1106 1784 * have been saved. 1107 1785 * 1108 1786 * @since 3.4.0 1109 1787 * 1110 * @param WP_Customize_Manager $ thisWP_Customize_Manager instance.1788 * @param WP_Customize_Manager $manager WP_Customize_Manager instance. 1111 1789 */ 1112 1790 do_action( 'customize_save', $this ); 1113 1791 1114 foreach ( $this->settings as $setting ) { 1115 $setting->save(); 1792 /* 1793 * Ensure that all settings will allow themselves to be saved. Note that 1794 * this is safe because the setting would have checked the capability 1795 * when the setting value was written into the changeset. So this is why 1796 * an additional check is not required here. 1797 */ 1798 $original_setting_capabilities = array(); 1799 foreach ( $this->settings() as $setting ) { 1800 $original_setting_capabilities[ $setting->id ] = $setting->capability; 1801 $setting->capability = 'exist'; 1802 } 1803 1804 // Ensure post data are all set before iterating to save. 1805 foreach ( $active_theme_setting_values as $setting_id => $setting_params ) { 1806 if ( isset( $setting_params['value'] ) ) { 1807 $this->set_post_value( $setting_id, $setting_params['value'] ); 1808 } 1809 } 1810 1811 foreach ( $active_theme_setting_values as $setting_id => $setting_params ) { 1812 $setting = $this->get_setting( $setting_id ); 1813 if ( $setting ) { 1814 $setting->save(); 1815 } 1816 } 1817 1818 // Update the stashed theme mod settings, removing the active theme's stashed settings, if activated. 1819 if ( did_action( 'switch_theme' ) ) { 1820 $this->update_stashed_theme_mod_settings( $other_theme_mod_settings ); 1116 1821 } 1117 1822 1118 1823 /** … … 1120 1825 * 1121 1826 * @since 3.6.0 1122 1827 * 1123 * @param WP_Customize_Manager $ thisWP_Customize_Manager instance.1828 * @param WP_Customize_Manager $manager WP_Customize_Manager instance. 1124 1829 */ 1125 1830 do_action( 'customize_save_after', $this ); 1126 1831 1127 $data = array( 1128 'setting_validities' => $exported_setting_validities, 1129 ); 1832 foreach ( $this->settings() as $setting ) { 1833 if ( isset( $original_setting_capabilities[ $setting->id ] ) ) { 1834 $setting->capability = $original_setting_capabilities[ $setting->id ]; 1835 } 1836 } 1837 } 1130 1838 1131 /** 1132 * Filters response data for a successful customize_save Ajax request. 1133 * 1134 * This filter does not apply if there was a nonce or authentication failure. 1135 * 1136 * @since 4.2.0 1137 * 1138 * @param array $data Additional information passed back to the 'saved' 1139 * event on `wp.customize`. 1140 * @param WP_Customize_Manager $this WP_Customize_Manager instance. 1141 */ 1142 $response = apply_filters( 'customize_save_response', $data, $this ); 1143 wp_send_json_success( $response ); 1839 /** 1840 * Update stashed theme mod settings. 1841 * 1842 * @since 4.7.0 1843 * @access private 1844 * 1845 * @param array $inactive_theme_mod_settings Mapping of stylesheet to arrays of theme mod settings. 1846 * @return array|false Returns array of updated stashed theme mods or false if the update failed or there were no changes. 1847 */ 1848 protected function update_stashed_theme_mod_settings( $inactive_theme_mod_settings ) { 1849 $stashed_theme_mod_settings = get_option( 'customize_stashed_theme_mods' ); 1850 if ( empty( $stashed_theme_mod_settings ) ) { 1851 $stashed_theme_mod_settings = array(); 1852 } 1853 1854 // Delete any stashed theme mods for the active theme since since they would have been loaded and saved upon activation. 1855 unset( $stashed_theme_mod_settings[ $this->get_stylesheet() ] ); 1856 1857 // Merge inactive theme mods with the stashed theme mod settings. 1858 foreach ( $inactive_theme_mod_settings as $stylesheet => $theme_mod_settings ) { 1859 if ( ! isset( $stashed_theme_mod_settings[ $stylesheet ] ) ) { 1860 $stashed_theme_mod_settings[ $stylesheet ] = array(); 1861 } 1862 1863 $stashed_theme_mod_settings[ $stylesheet ] = array_merge( 1864 $stashed_theme_mod_settings[ $stylesheet ], 1865 $theme_mod_settings 1866 ); 1867 } 1868 1869 $autoload = false; 1870 $result = update_option( 'customize_stashed_theme_mods', $stashed_theme_mod_settings, $autoload ); 1871 if ( ! $result ) { 1872 return false; 1873 } 1874 return $stashed_theme_mod_settings; 1144 1875 } 1145 1876 1146 1877 /** … … 1684 2415 } 1685 2416 1686 2417 /** 2418 * Determines whether the admin and the frontend are on different domains. 2419 * 2420 * @since 4.7.0 2421 * @access public 2422 * 2423 * @return bool Whether cross-domain. 2424 */ 2425 public function is_cross_domain() { 2426 $admin_origin = wp_parse_url( admin_url() ); 2427 $home_origin = wp_parse_url( home_url() ); 2428 $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) ); 2429 return $cross_domain; 2430 } 2431 2432 /** 2433 * Get URLs allowed to be previewed. 2434 * 2435 * If the front end and the admin are served from the same domain, load the 2436 * preview over ssl if the Customizer is being loaded over ssl. This avoids 2437 * insecure content warnings. This is not attempted if the admin and front end 2438 * are on different domains to avoid the case where the front end doesn't have 2439 * ssl certs. Domain mapping plugins can allow other urls in these conditions 2440 * using the customize_allowed_urls filter. 2441 * 2442 * @since 4.7.0 2443 * @access public 2444 * 2445 * @returns array Allowed URLs. 2446 */ 2447 public function get_allowed_urls() { 2448 $allowed_urls = array( home_url( '/' ) ); 2449 2450 if ( is_ssl() && ! $this->is_cross_domain() ) { 2451 $allowed_urls[] = home_url( '/', 'https' ); 2452 } 2453 2454 /** 2455 * Filters the list of URLs allowed to be clicked and followed in the Customizer preview. 2456 * 2457 * @since 3.4.0 2458 * 2459 * @param array $allowed_urls An array of allowed URLs. 2460 */ 2461 $allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) ); 2462 2463 return $allowed_urls; 2464 } 2465 2466 /** 1687 2467 * Set URL to link the user to when closing the Customizer. 1688 2468 * 1689 2469 * URL is validated. … … 1792 2572 * @since 4.4.0 1793 2573 */ 1794 2574 public function customize_pane_settings() { 1795 /*1796 * If the front end and the admin are served from the same domain, load the1797 * preview over ssl if the Customizer is being loaded over ssl. This avoids1798 * insecure content warnings. This is not attempted if the admin and front end1799 * are on different domains to avoid the case where the front end doesn't have1800 * ssl certs. Domain mapping plugins can allow other urls in these conditions1801 * using the customize_allowed_urls filter.1802 */1803 1804 $allowed_urls = array( home_url( '/' ) );1805 $admin_origin = parse_url( admin_url() );1806 $home_origin = parse_url( home_url() );1807 $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );1808 1809 if ( is_ssl() && ! $cross_domain ) {1810 $allowed_urls[] = home_url( '/', 'https' );1811 }1812 1813 /**1814 * Filters the list of URLs allowed to be clicked and followed in the Customizer preview.1815 *1816 * @since 3.4.01817 *1818 * @param array $allowed_urls An array of allowed URLs.1819 */1820 $allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) );1821 2575 1822 2576 $login_url = add_query_arg( array( 1823 2577 'interim-login' => 1, 1824 2578 'customize-login' => 1, 1825 2579 ), wp_login_url() ); 1826 2580 2581 // Ensure dirty flags are set. 2582 foreach ( array_keys( $this->unsanitized_post_values() ) as $setting_id ) { 2583 $setting = $this->get_setting( $setting_id ); 2584 if ( $setting ) { 2585 $setting->dirty = true; 2586 } 2587 } 2588 1827 2589 // Prepare Customizer settings to pass to JavaScript. 1828 2590 $settings = array( 2591 'changeset' => array( 2592 'uuid' => $this->changeset_uuid, 2593 'status' => $this->changeset_post_id() ? get_post_status( $this->changeset_post_id() ) : '', 2594 ), 1829 2595 'theme' => array( 1830 2596 'stylesheet' => $this->get_stylesheet(), 1831 2597 'active' => $this->is_theme_active(), … … 1835 2601 'parent' => esc_url_raw( admin_url() ), 1836 2602 'activated' => esc_url_raw( home_url( '/' ) ), 1837 2603 'ajax' => esc_url_raw( admin_url( 'admin-ajax.php', 'relative' ) ), 1838 'allowed' => array_map( 'esc_url_raw', $ allowed_urls),1839 'isCrossDomain' => $ cross_domain,2604 'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ), 2605 'isCrossDomain' => $this->is_cross_domain(), 1840 2606 'home' => esc_url_raw( home_url( '/' ) ), 1841 2607 'login' => esc_url_raw( $login_url ), 1842 2608 ), … … 2330 3096 * @see add_dynamic_settings() 2331 3097 */ 2332 3098 public function register_dynamic_settings() { 2333 $this->add_dynamic_settings( array_keys( $this->unsanitized_post_values() ) ); 3099 $setting_ids = array_keys( $this->unsanitized_post_values() ); 3100 $this->add_dynamic_settings( $setting_ids ); 2334 3101 } 2335 3102 2336 3103 /** -
src/wp-includes/js/customize-loader.js
132 132 targetWindow: this.iframe[0].contentWindow 133 133 }); 134 134 135 // Expose the changeset UUID on the parent window's URL so that the customized state can survive a refresh. 136 if ( history.replaceState ) { 137 this.messenger.bind( 'changeset-uuid', function( changesetUuid ) { 138 var urlParser; 139 urlParser = document.createElement( 'a' ); 140 urlParser.href = location.href; 141 urlParser.search = urlParser.search.replace( /(\?|&)changeset_uuid=[^&]+(?=$|&)/, '$1' ); 142 if ( urlParser.search.length > 1 ) { 143 urlParser.search += '&'; 144 } 145 urlParser.search += 'changeset_uuid=' + changesetUuid; 146 history.replaceState( { customize: urlParser.href }, '', urlParser.href ); 147 } ); 148 } 149 135 150 // Wait for the connection from the iframe before sending any postMessage events. 136 151 this.messenger.bind( 'ready', function() { 137 152 Loader.messenger.send( 'back' ); -
src/wp-includes/js/customize-base.js
637 637 /** 638 638 * Initialize Messenger. 639 639 * 640 * @param {object} params 641 * {string} .urlThe URL to communicate with.642 * {window} .targetWindowThe window instance to communicate with. Default window.parent.643 * {string} .channelIf provided, will send the channel with each message and only accept messages a matching channel.644 * @param {object} options 640 * @param {object} params - Parameters to configure the messenger. 641 * {string} params.url - The URL to communicate with. 642 * {window} params.targetWindow - The window instance to communicate with. Default window.parent. 643 * {string} params.channel - If provided, will send the channel with each message and only accept messages a matching channel. 644 * @param {object} options - Extend any instance parameter or method with this object. 645 645 */ 646 646 initialize: function( params, options ) { 647 var defaultTarget; 648 647 649 // Target the parent frame by default, but only if a parent frame exists. 648 var defaultTarget = window.parent== window ? null : window.parent;650 defaultTarget = window.parent === window ? null : window.parent; 649 651 650 652 $.extend( this, options || {} ); 651 653 652 654 this.add( 'channel', params.channel ); 653 655 this.add( 'url', params.url || '' ); 654 656 this.add( 'origin', this.url() ).link( this.url ).setter( function( to ) { 655 return to.replace( /([^:]+:\/\/[^\/]+).*/, '$1' ); 657 var urlParser = document.createElement( 'a' ); 658 urlParser.href = to; 659 return urlParser.protocol + '//' + urlParser.host; 656 660 }); 657 661 658 662 // first add with no value … … 807 811 return result; 808 812 }; 809 813 814 /** 815 * Utility function namespace 816 */ 817 api.utils = {}; 818 819 /** 820 * Parse query string. 821 * 822 * @param {string} queryString Query string. 823 * @returns {object} Parsed query string. 824 */ 825 api.utils.parseQueryString = function parseQueryString( queryString ) { 826 var queryParams = {}; 827 _.each( queryString.split( '&' ), function( pair ) { 828 var parts = pair.split( '=', 2 ); 829 if ( parts[0] ) { 830 queryParams[ decodeURIComponent( parts[0] ) ] = _.isUndefined( parts[1] ) ? null : decodeURIComponent( parts[1] ); 831 } 832 } ); 833 return queryParams; 834 }; 835 810 836 // Expose the API publicly on window.wp.customize 811 837 exports.customize = api; 812 838 })( wp, jQuery ); -
src/wp-includes/js/customize-selective-refresh.js
485 485 return { 486 486 wp_customize: 'on', 487 487 nonce: api.settings.nonce.preview, 488 theme: api.settings.theme.stylesheet, 489 customized: JSON.stringify( dirtyCustomized ) 488 customize_theme: api.settings.theme.stylesheet, 489 customized: JSON.stringify( dirtyCustomized ), 490 customize_changeset_uuid: api.settings.changeset.uuid 490 491 }; 491 492 }; 492 493 -
src/wp-includes/js/customize-preview.js
37 37 * @param {object} options - Extend any instance parameter or method with this object. 38 38 */ 39 39 initialize: function( params, options ) { 40 var self = this;40 var preview = this, urlParser = document.createElement( 'a' ); 41 41 42 api.Messenger.prototype.initialize.call( this, params, options );42 api.Messenger.prototype.initialize.call( preview, params, options ); 43 43 44 this.body = $( document.body ); 45 this.body.on( 'click.preview', 'a', function( event ) { 44 urlParser.href = preview.origin(); 45 preview.add( 'scheme', urlParser.protocol.replace( /:$/, '' ) ); 46 47 preview.body = $( document.body ); 48 preview.body.on( 'click.preview', 'a', function( event ) { 46 49 var link, isInternalJumpLink; 47 50 link = $( this ); 51 52 // No-op if the anchor is not a link. 53 if ( _.isUndefined( link.attr( 'href' ) ) ) { 54 return; 55 } 56 48 57 isInternalJumpLink = ( '#' === link.attr( 'href' ).substr( 0, 1 ) ); 58 59 // Allow internal jump links to behave normally without preventing default. 60 if ( isInternalJumpLink ) { 61 return; 62 } 63 64 // Prevent initiating navigating from click and instead rely on sending url message to pane below. 49 65 event.preventDefault(); 50 66 51 if ( isInternalJumpLink && '#' !== link.attr( 'href' ) ) {52 $( link.attr( 'href' ) ).each( function() {53 this.scrollIntoView();54 } );67 // If the link is not previewable, prevent the browser from navigating to it. 68 if ( ! api.isLinkPreviewable( link[0] ) ) { 69 wp.a11y.speak( api.settings.l10n.linkUnpreviewable ); 70 return; 55 71 } 56 72 57 73 /* … … 59 75 * nav menu items can just result on focusing on the corresponding 60 76 * control instead of also navigating to the URL linked to. 61 77 */ 62 if ( event.shiftKey || isInternalJumpLink) {78 if ( event.shiftKey ) { 63 79 return; 64 80 } 65 self.send( 'scroll', 0 );66 self.send( 'url', link.prop( 'href' ) );67 });68 81 69 // You cannot submit forms. 70 this.body.on( 'submit.preview', 'form', function( event ) { 71 var urlParser; 82 // Note: It's not relevant to send scroll because sending url message will cause iframe src change instead of refresh. 83 preview.send( 'url', link.prop( 'href' ) ); 84 } ); 85 86 preview.body.on( 'submit.preview', 'form', function( event ) { 87 var urlParser = document.createElement( 'a' ); 88 urlParser.href = this.action; 89 90 // If the link is not previewable, prevent the browser from navigating to it. 91 if ( ! api.isLinkPreviewable( urlParser ) ) { 92 wp.a11y.speak( api.settings.l10n.formUnpreviewable ); 93 return; 94 } 72 95 73 96 /* 74 97 * If the default wasn't prevented already (in which case the form … … 82 105 * external site in the preview. 83 106 */ 84 107 if ( ! event.isDefaultPrevented() && 'GET' === this.method.toUpperCase() ) { 85 urlParser = document.createElement( 'a' ); 86 urlParser.href = this.action; 87 if ( urlParser.search.substr( 1 ).length > 1 ) { 108 if ( urlParser.search.length > 1 ) { 88 109 urlParser.search += '&'; 89 110 } 90 111 urlParser.search += $( this ).serialize(); 91 112 api.preview.send( 'url', urlParser.href ); 92 113 } 93 114 115 // Prevent default since navigation should be done via sending url message or via JS submit handler. 94 116 event.preventDefault(); 95 117 }); 96 118 97 this.window = $( window );98 this.window.on( 'scroll.preview', debounce( function() {99 self.send( 'scroll', self.window.scrollTop() );100 }, 200 ) );119 preview.window = $( window ); 120 preview.window.on( 'scroll.preview', debounce( function() { 121 preview.send( 'scroll', preview.window.scrollTop() ); 122 }, 200 ) ); 101 123 102 this.bind( 'scroll', function( distance ) {103 self.window.scrollTop( distance );124 preview.bind( 'scroll', function( distance ) { 125 preview.window.scrollTop( distance ); 104 126 }); 105 127 } 106 128 }); 107 129 130 /** 131 * Inject the changeset UUID into links in the document. 132 * 133 * @returns {void} 134 */ 135 api.addLinkPreviewing = function addLinkPreviewing() { 136 var linkSelectors = 'a[href], area'; 137 138 // Inject links into initial document. 139 $( document.body ).find( linkSelectors ).each( function() { 140 api.prepareLinkPreview( this ); 141 } ); 142 143 // Inject links for new elements added to the page. 144 if ( 'undefined' !== typeof MutationObserver ) { 145 api.mutationObserver = new MutationObserver( function( mutations ) { 146 _.each( mutations, function( mutation ) { 147 $( mutation.target ).find( linkSelectors ).each( function() { 148 api.prepareLinkPreview( this ); 149 } ); 150 } ); 151 } ); 152 api.mutationObserver.observe( document.documentElement, { 153 childList: true, 154 subtree: true 155 } ); 156 } else { 157 158 // If mutation observers aren't available, fallback to just-in-time injection. 159 $( document.documentElement ).on( 'click focus mouseover', linkSelectors, function() { 160 api.prepareLinkPreview( this ); 161 } ); 162 } 163 }; 164 165 /** 166 * Should the supplied link is previewable. 167 * 168 * @param {HTMLAnchorElement|HTMLAreaElement} element Link element. 169 * @param {string} element.search Query string. 170 * @param {string} element.pathname Path. 171 * @param {string} element.hostname Hostname. 172 * @param {object} [options] 173 * @param {object} [options.allowAdminAjax=false] Allow admin-ajax.php requests. 174 * @returns {boolean} Is appropriate for changeset link. 175 */ 176 api.isLinkPreviewable = function isLinkPreviewable( element, options ) { 177 var hasMatchingHost, urlParser, args; 178 179 args = _.extend( {}, { allowAdminAjax: false }, options || {} ); 180 181 if ( 'javascript:' === element.protocol ) { // jshint ignore:line 182 return true; 183 } 184 185 // Only web URLs can be previewed. 186 if ( 'https:' !== element.protocol && 'http:' !== element.protocol ) { 187 return false; 188 } 189 190 urlParser = document.createElement( 'a' ); 191 hasMatchingHost = ! _.isUndefined( _.find( api.settings.url.allowed, function( allowedUrl ) { 192 urlParser.href = allowedUrl; 193 if ( urlParser.host === element.host && urlParser.protocol === element.protocol ) { 194 return true; 195 } 196 return false; 197 } ) ); 198 if ( ! hasMatchingHost ) { 199 return false; 200 } 201 202 // Skip wp login and signup pages. 203 if ( /\/wp-(login|signup)\.php$/.test( element.pathname ) ) { 204 return false; 205 } 206 207 // Allow links to admin ajax as faux frontend URLs. 208 if ( /\/wp-admin\/admin-ajax\.php$/.test( element.pathname ) ) { 209 return args.allowAdminAjax; 210 } 211 212 // Disallow links to admin, includes, and content. 213 if ( /\/wp-(admin|includes|content)(\/|$)/.test( element.pathname ) ) { 214 return false; 215 } 216 217 return true; 218 }; 219 220 /** 221 * Inject the customize_changeset_uuid query param into links on the frontend. 222 * 223 * @param {HTMLAnchorElement|HTMLAreaElement} element Link element. 224 * @param {object} element.search Query string. 225 * @returns {void} 226 */ 227 api.prepareLinkPreview = function prepareLinkPreview( element ) { 228 var queryParams; 229 230 // Skip links in admin bar. 231 if ( $( element ).closest( '#wpadminbar' ).length ) { 232 return; 233 } 234 235 // Make sure links in preview use HTTPS if parent frame uses HTTPS. 236 if ( 'https' === api.preview.scheme.get() && 'http:' === element.protocol && -1 !== api.settings.url.allowedHosts.indexOf( element.host ) ) { 237 element.protocol = 'https:'; 238 } 239 240 if ( ! api.isLinkPreviewable( element ) ) { 241 $( element ).addClass( 'customize-unpreviewable' ); 242 return; 243 } 244 $( element ).removeClass( 'customize-unpreviewable' ); 245 246 queryParams = api.utils.parseQueryString( element.search.substring( 1 ) ); 247 queryParams.customize_changeset_uuid = api.settings.changeset.uuid; 248 if ( ! api.settings.theme.active ) { 249 queryParams.customize_theme = api.settings.theme.stylesheet; 250 } 251 if ( api.settings.channel ) { 252 queryParams.customize_messenger_channel = api.settings.channel; 253 } 254 element.search = $.param( queryParams ); 255 256 // Prevent links from breaking out of preview iframe. 257 if ( api.settings.channel ) { 258 element.target = '_self'; 259 } 260 }; 261 262 /** 263 * Inject the changeset UUID into Ajax requests. 264 * 265 * @access private 266 * @return {void} 267 */ 268 api.addRequestPreviewing = function addRequestPreviewing() { 269 $.ajaxPrefilter( function prefilterAjax( options ) { 270 var urlParser, queryParams; 271 urlParser = document.createElement( 'a' ); 272 urlParser.href = options.url; 273 274 // Abort if the request is not for this site. 275 if ( ! api.isLinkPreviewable( urlParser, { allowAdminAjax: true } ) ) { 276 return; 277 } 278 279 queryParams = api.utils.parseQueryString( urlParser.search.substring( 1 ) ); 280 queryParams.customize_changeset_uuid = api.settings.changeset.uuid; 281 if ( ! api.settings.theme.active ) { 282 queryParams.customize_theme = api.settings.theme.stylesheet; 283 } 284 if ( api.settings.channel ) { 285 queryParams.customize_messenger_channel = api.settings.channel; 286 } 287 urlParser.search = $.param( queryParams ); 288 289 options.url = urlParser.href; 290 } ); 291 }; 292 293 /** 294 * Inject changeset UUID into forms, allowing preview to persist through submissions. 295 * 296 * @access private 297 * @returns {void} 298 */ 299 api.addFormPreviewing = function addFormPreviewing() { 300 301 // Inject inputs for forms in initial document. 302 $( document.body ).find( 'form' ).each( function() { 303 api.prepareFormPreview( this ); 304 } ); 305 306 // Inject inputs for new forms added to the page. 307 if ( 'undefined' !== typeof MutationObserver ) { 308 api.mutationObserver = new MutationObserver( function( mutations ) { 309 _.each( mutations, function( mutation ) { 310 $( mutation.target ).find( 'form' ).each( function() { 311 api.prepareFormPreview( this ); 312 } ); 313 } ); 314 } ); 315 api.mutationObserver.observe( document.documentElement, { 316 childList: true, 317 subtree: true 318 } ); 319 } 320 }; 321 322 /** 323 * Inject changeset into form inputs. 324 * 325 * @param {HTMLFormElement} form Form. 326 * @returns {void} 327 */ 328 api.prepareFormPreview = function prepareFormPreview( form ) { 329 var urlParser, stateParams = {}; 330 331 if ( ! form.action ) { 332 form.action = location.href; 333 } 334 335 urlParser = document.createElement( 'a' ); 336 urlParser.href = form.action; 337 338 // Make sure forms in preview use HTTPS if parent frame uses HTTPS. 339 if ( 'https' === api.preview.scheme.get() && 'http:' === urlParser.protocol && -1 !== api.settings.url.allowedHosts.indexOf( urlParser.host ) ) { 340 urlParser.protocol = 'https:'; 341 form.action = urlParser.href; 342 } 343 344 if ( ! api.isLinkPreviewable( urlParser ) ) { 345 $( form ).addClass( 'customize-unpreviewable' ); 346 return; 347 } 348 $( form ).removeClass( 'customize-unpreviewable' ); 349 350 stateParams.customize_changeset_uuid = api.settings.changeset.uuid; 351 if ( ! api.settings.theme.active ) { 352 stateParams.customize_theme = api.settings.theme.stylesheet; 353 } 354 if ( api.settings.channel ) { 355 stateParams.customize_messenger_channel = api.settings.channel; 356 } 357 358 _.each( stateParams, function( value, name ) { 359 var input = $( form ).find( 'input[name="' + name + '"]' ); 360 if ( input.length ) { 361 input.val( value ); 362 } else { 363 $( form ).prepend( $( '<input>', { 364 type: 'hidden', 365 name: name, 366 value: value 367 } ) ); 368 } 369 } ); 370 371 // Prevent links from breaking out of preview iframe. 372 if ( api.settings.channel ) { 373 form.target = '_self'; 374 } 375 }; 376 377 /** 378 * Watch current URL and send keep-alive (heartbeat) messages to the parent. 379 * 380 * Keep the customizer pane notified that the preview is still alive 381 * and that the user hasn't navigated to a non-customized URL. 382 * These messages also keep the customizer updated on the current URL 383 * for JS-driven sites that use history.pushState()/history.replaceState(). 384 * 385 * @returns {void} 386 */ 387 api.keepAliveCurrentUrl = function keepAliveCurrentUrl() { 388 var currentUrl, urlParser, queryParams, needsParamRestoration = false; 389 390 urlParser = document.createElement( 'a' ); 391 urlParser.href = location.href; 392 queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) ); 393 394 if ( history.replaceState ) { 395 needsParamRestoration = ! queryParams.customize_changeset_uuid || ( ! api.settings.theme.active && ! queryParams.customize_theme ) || ( api.settings.channel && ! queryParams.customize_messenger_channel ); 396 } 397 398 // Scrub the URL of any customized state query params. 399 _.each( api.settings.changeset.stateQueryParams, function( name ) { 400 delete queryParams[ name ]; 401 } ); 402 if ( _.isEmpty( queryParams ) ) { 403 urlParser.search = ''; 404 } else { 405 urlParser.search = '?' + $.param( queryParams ); 406 } 407 urlParser.hash = ''; 408 currentUrl = urlParser.href; 409 410 // Ensure that the customized state params remain in the URL. 411 if ( needsParamRestoration ) { 412 urlParser.href = location.href; 413 queryParams.customize_changeset_uuid = api.settings.changeset.uuid; 414 if ( ! api.settings.theme.active ) { 415 queryParams.customize_changeset_uuid = api.settings.theme.stylesheet; 416 } 417 if ( api.settings.theme.channel ) { 418 queryParams.customize_messenger_channel = api.settings.channel; 419 } 420 urlParser.search = $.param( queryParams ); 421 history.replaceState( {}, '', urlParser.href ); // @todo This is going to clobber any state in any JS app. The state needs to be captured. 422 } 423 424 if ( api.settings.url.self !== currentUrl ) { 425 api.settings.url.self = currentUrl; 426 api.preview.send( 'ready', { 427 currentUrl: api.settings.url.self, 428 activePanels: api.settings.activePanels, 429 activeSections: api.settings.activeSections, 430 activeControls: api.settings.activeControls 431 } ); 432 } else { 433 api.preview.send( 'keep-alive' ); 434 } 435 }; 436 108 437 $( function() { 109 438 var bg, setValue; 110 439 … … 118 447 channel: api.settings.channel 119 448 }); 120 449 450 api.addLinkPreviewing(); 451 api.addRequestPreviewing(); 452 api.addFormPreviewing(); 453 121 454 /** 122 455 * Create/update a setting value. 123 456 * … … 171 504 api.preview.send( 'nonce', api.settings.nonce ); 172 505 173 506 api.preview.send( 'documentTitle', document.title ); 507 508 // Send scroll in case of loading via non-refresh. 509 api.preview.send( 'scroll', $( window ).scrollTop() ); 174 510 }); 175 511 176 512 api.preview.bind( 'saved', function( response ) { 513 var urlParser; 514 515 if ( response.next_changeset_uuid ) { 516 api.settings.changeset.uuid = response.next_changeset_uuid; 517 518 // Update UUIDs in links and forms. 519 $( document.body ).find( 'a[href], area' ).each( function() { 520 api.prepareLinkPreview( this ); 521 } ); 522 $( document.body ).find( 'form' ).each( function() { 523 api.prepareFormPreview( this ); 524 } ); 525 526 // Replace the UUID in the URL. 527 urlParser = document.createElement( 'a' ); 528 urlParser.href = location.href; 529 urlParser.search = urlParser.search.replace( /(\?|&)customize_changeset_uuid=[^&]+(&|$)/, '$1' ); 530 if ( urlParser.search.length > 1 ) { 531 urlParser.search += '&'; 532 } 533 urlParser.search += 'customize_changeset_uuid=' + response.next_changeset_uuid; 534 535 if ( history.replaceState ) { 536 history.replaceState( {}, document.title, urlParser.href ); // @todo This is going to clobber any state in any JS app. The state needs to be captured. 537 } 538 } 539 177 540 api.trigger( 'saved', response ); 178 541 } ); 179 542 … … 191 554 * Send a message to the parent customize frame with a list of which 192 555 * containers and controls are active. 193 556 */ 557 194 558 api.preview.send( 'ready', { 559 currentUrl: api.settings.url.self, 195 560 activePanels: api.settings.activePanels, 196 561 activeSections: api.settings.activeSections, 197 activeControls: api.settings.activeControls, 198 settingValidities: api.settings.settingValidities 562 activeControls: api.settings.activeControls 199 563 } ); 200 564 565 // Send ready when URL changes via JS. 566 setInterval( api.keepAliveCurrentUrl, 1000 ); 567 201 568 // Display a loading indicator when preview is reloading, and remove on failure. 202 569 api.preview.bind( 'loading-initiated', function () { 203 570 $( 'body' ).addClass( 'wp-customizer-unloading' ); -
src/wp-includes/js/customize-preview-nav-menus.js
106 106 * @returns {boolean} 107 107 */ 108 108 isRelatedSetting: function( setting, newValue, oldValue ) { 109 var partial = this, navMenuLocationSetting, navMenuId, isNavMenuItemSetting ;109 var partial = this, navMenuLocationSetting, navMenuId, isNavMenuItemSetting, _newValue, _oldValue, urlParser; 110 110 if ( _.isString( setting ) ) { 111 111 setting = api( setting ); 112 112 } … … 123 123 */ 124 124 isNavMenuItemSetting = /^nav_menu_item\[/.test( setting.id ); 125 125 if ( isNavMenuItemSetting && _.isObject( newValue ) && _.isObject( oldValue ) ) { 126 delete newValue.type_label; 127 delete oldValue.type_label; 128 if ( _.isEqual( oldValue, newValue ) ) { 126 _newValue = _.clone( newValue ); 127 _oldValue = _.clone( oldValue ); 128 delete _newValue.type_label; 129 delete _oldValue.type_label; 130 131 // Normalize URL scheme when parent frame is HTTPS to prevent selective refresh upon initial page load. 132 if ( 'https' === api.preview.scheme.get() ) { 133 urlParser = document.createElement( 'a' ); 134 urlParser.href = _newValue.url; 135 urlParser.protocol = 'https:'; 136 _newValue.url = urlParser.href; 137 urlParser.href = _oldValue.url; 138 urlParser.protocol = 'https:'; 139 _oldValue.url = urlParser.href; 140 } 141 142 if ( _.isEqual( _oldValue, _newValue ) ) { 129 143 return false; 130 144 } 131 145 } -
src/wp-includes/theme.php
2066 2066 * Includes and instantiates the WP_Customize_Manager class. 2067 2067 * 2068 2068 * Loads the Customizer at plugins_loaded when accessing the customize.php admin 2069 * page or when any request includes a wp_customize=on param , either as a GET2070 * query var or as POST data. This param is a signal for whether to bootstrap2071 * the Customizer whenWordPress is loading, especially in the Customizer preview2069 * page or when any request includes a wp_customize=on param or a customize_changeset 2070 * param (a UUID). This param is a signal for whether to bootstrap the Customizer when 2071 * WordPress is loading, especially in the Customizer preview 2072 2072 * or when making Customizer Ajax requests for widgets or menus. 2073 2073 * 2074 2074 * @since 3.4.0 … … 2076 2076 * @global WP_Customize_Manager $wp_customize 2077 2077 */ 2078 2078 function _wp_customize_include() { 2079 if ( ! ( ( isset( $_REQUEST['wp_customize'] ) && 'on' == $_REQUEST['wp_customize'] ) 2080 || ( is_admin() && 'customize.php' == basename( $_SERVER['PHP_SELF'] ) ) 2081 ) ) { 2079 2080 $is_customize_admin_page = ( is_admin() && 'customize.php' == basename( $_SERVER['PHP_SELF'] ) ); 2081 $should_include = ( 2082 $is_customize_admin_page 2083 || 2084 ( isset( $_REQUEST['wp_customize'] ) && 'on' == $_REQUEST['wp_customize'] ) 2085 || 2086 ! empty( $_REQUEST['customize_changeset_uuid'] ) 2087 ); 2088 2089 if ( ! $should_include ) { 2082 2090 return; 2083 2091 } 2084 2092 2085 require_once ABSPATH . WPINC . '/class-wp-customize-manager.php'; 2086 $GLOBALS['wp_customize'] = new WP_Customize_Manager(); 2093 $theme = null; 2094 $changeset_uuid = null; 2095 $messenger_channel = null; 2096 2097 if ( $is_customize_admin_page && isset( $_REQUEST['changeset_uuid'] ) ) { 2098 $changeset_uuid = sanitize_key( wp_unslash( $_REQUEST['changeset_uuid'] ) ); 2099 } elseif ( ! empty( $_REQUEST['customize_changeset_uuid'] ) ) { 2100 $changeset_uuid = sanitize_key( wp_unslash( $_REQUEST['customize_changeset_uuid'] ) ); 2101 } 2102 2103 if ( $is_customize_admin_page && isset( $_REQUEST['theme'] ) ) { 2104 $theme = wp_unslash( $_REQUEST['theme'] ); 2105 } elseif ( isset( $_REQUEST['customize_theme'] ) ) { 2106 $theme = wp_unslash( $_REQUEST['customize_theme'] ); 2107 } 2108 if ( isset( $_REQUEST['customize_messenger_channel'] ) ) { 2109 $messenger_channel = sanitize_key( wp_unslash( $_REQUEST['customize_messenger_channel'] ) ); 2110 } 2111 2112 require_once ABSPATH . WPINC . '/class-wp-customize-manager.php'; 2113 $GLOBALS['wp_customize'] = new WP_Customize_Manager( compact( 'changeset_uuid', 'theme', 'messenger_channel' ) ); 2114 } 2115 2116 /** 2117 * Publish a snapshot's changes. 2118 * 2119 * @param int $changeset_post_id Changeset post ID. 2120 * @param WP_Post $changeset_post Changeset post object. 2121 */ 2122 function _wp_customize_publish_changeset( $changeset_post_id, $changeset_post ) { 2123 global $wp_customize; 2124 if ( empty( $wp_customize ) ) { 2125 require_once ABSPATH . WPINC . '/class-wp-customize-manager.php'; 2126 $wp_customize = new WP_Customize_Manager( $changeset_post->post_name ); 2127 } 2128 if ( ! did_action( 'customize_register' ) ) { 2129 /** This filter is documented in /wp-includes/class-wp-customize-manager.php */ 2130 do_action( 'customize_register', $wp_customize ); 2131 } 2132 $wp_customize->publish_changeset_values( $changeset_post_id ) ; 2133 2134 /* 2135 * Trash the changeset post if revisions are not enabled. Unpublished 2136 * changesets by default get garbage collected due to the auto-draft status. 2137 * When a changeset post is published, however, it would no longer get cleaned 2138 * out. Ths is a problem when the changeset posts are never displayed anywhere, 2139 * since they would just be endlessly piling up. So here we use the revisions 2140 * feature to indicate whether or not a published changeset should get trashed 2141 * and thus garbage collected. 2142 * 2143 * @todo There could be a better way to indicate that published changeset posts should be garbage-collected. 2144 */ 2145 if ( ! wp_revisions_enabled( $changeset_post ) ) { 2146 wp_trash_post( $changeset_post->ID ); 2147 } 2148 } 2149 2150 /** 2151 * Filters changeset post data upon insert to ensure post_name is intact. 2152 * 2153 * This is needed to prevent the post_name from being dropped when the post is 2154 * transitioned into pending status by a contributor. 2155 * 2156 * @since 4.7.0 2157 * @see wp_insert_post() 2158 * 2159 * @param array $post_data An array of slashed post data. 2160 * @param array $supplied_post_data An array of sanitized, but otherwise unmodified post data. 2161 * @returns array Filtered data. 2162 */ 2163 function _wp_customize_changeset_filter_insert_post_data( $post_data, $supplied_post_data ) { 2164 if ( isset( $post_data['post_type'] ) && 'customize_changeset' === $post_data['post_type'] ) { 2165 2166 // Prevent post_name from being dropped, such as when contributor saves a changeset post as pending. 2167 if ( empty( $post_data['post_name'] ) && ! empty( $supplied_post_data['post_name'] ) ) { 2168 $post_data['post_name'] = $supplied_post_data['post_name']; 2169 } 2170 2171 // @todo Let the post_name be immutable by setting $post_data['post_name'] to get_post( $post_data['ID'] )->post_name? 2172 // @todo Otherwise, supply a UUID via wp_generate_uuid4() if it is empty? 2173 } 2174 return $post_data; 2087 2175 } 2088 2176 2089 2177 /** -
src/wp-includes/functions.php
5529 5529 5530 5530 return false; 5531 5531 } 5532 5533 /** 5534 * Generate a random UUID (version 4). 5535 * 5536 * @since 4.7.0 5537 * 5538 * @return string UUID. 5539 */ 5540 function wp_generate_uuid4() { 5541 return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', 5542 mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), 5543 mt_rand( 0, 0xffff ), 5544 mt_rand( 0, 0x0fff ) | 0x4000, 5545 mt_rand( 0, 0x3fff ) | 0x8000, 5546 mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ) 5547 ); 5548 } -
src/wp-includes/class-wp-customize-widgets.php
93 93 public function __construct( $manager ) { 94 94 $this->manager = $manager; 95 95 96 // Skip useless hooks when the user can't manage widgets anyway. 96 // See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L420-L449 97 add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_customize_dynamic_setting_args' ), 10, 2 ); 98 add_action( 'widgets_init', array( $this, 'register_settings' ), 95 ); 99 add_action( 'customize_register', array( $this, 'schedule_customize_register' ), 1 ); 100 101 // Skip remaining hooks when the user can't manage widgets anyway. 97 102 if ( ! current_user_can( 'edit_theme_options' ) ) { 98 103 return; 99 104 } 100 105 101 add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_customize_dynamic_setting_args' ), 10, 2 );102 add_action( 'widgets_init', array( $this, 'register_settings' ), 95 );103 106 add_action( 'wp_loaded', array( $this, 'override_sidebars_widgets_for_theme_switch' ) ); 104 107 add_action( 'customize_controls_init', array( $this, 'customize_controls_init' ) ); 105 add_action( 'customize_register', array( $this, 'schedule_customize_register' ), 1 );106 108 add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); 107 109 add_action( 'customize_controls_print_styles', array( $this, 'print_styles' ) ); 108 110 add_action( 'customize_controls_print_scripts', array( $this, 'print_scripts' ) ); … … 276 278 277 279 $this->old_sidebars_widgets = wp_get_sidebars_widgets(); 278 280 add_filter( 'customize_value_old_sidebars_widgets_data', array( $this, 'filter_customize_value_old_sidebars_widgets_data' ) ); 281 $this->manager->set_post_value( 'old_sidebars_widgets_data', $this->old_sidebars_widgets ); // Override any value cached in changeset. 279 282 280 283 // retrieve_widgets() looks at the global $sidebars_widgets 281 284 $sidebars_widgets = $this->old_sidebars_widgets; -
src/wp-admin/export.php
254 254 </li> 255 255 </ul> 256 256 257 <?php foreach ( get_post_types( array( ' _builtin' => false, 'can_export' => true ), 'objects' ) as $post_type ) :?>257 <?php foreach ( get_post_types( array( 'can_export' => true ), 'objects' ) as $post_type ) : if ( $post_type->_builtin && 'customize_changeset' !== $post_type->name ) { continue; } ?> 258 258 <p><label><input type="radio" name="content" value="<?php echo esc_attr( $post_type->name ); ?>" /> <?php echo esc_html( $post_type->label ); ?></label></p> 259 259 <?php endforeach; ?> 260 260 -
src/wp-admin/js/customize-controls.js
22 22 */ 23 23 api.Setting = api.Value.extend({ 24 24 initialize: function( id, value, options ) { 25 api.Value.prototype.initialize.call( this, value, options ); 25 var setting = this; 26 api.Value.prototype.initialize.call( setting, value, options ); 26 27 27 this.id = id;28 this.transport = this.transport || 'refresh';29 this._dirty = options.dirty || false;30 this.notifications = new api.Values({ defaultConstructor: api.Notification });28 setting.id = id; 29 setting.transport = setting.transport || 'refresh'; 30 setting._dirty = options.dirty || false; 31 setting.notifications = new api.Values({ defaultConstructor: api.Notification }); 31 32 32 // Whenever the setting's value changes, refresh the preview. 33 this.bind( this.preview ); 33 setting.bind( setting.handleChange ); 34 }, 35 36 /** 37 * Handle setting change. 38 * 39 * @since 4.7.0 40 * 41 * @param {mixed} value Value. 42 * @returns {void} 43 */ 44 handleChange: function( value ) { 45 var setting = this, promise, changes = {}; 46 changes[ setting.id ] = { 47 value: value 48 }; 49 promise = api.requestChangesetUpdate( changes ); 50 setting.previewer.addPendingChangesetUpdateRequest( promise ); 51 setting.preview(); 34 52 }, 35 53 36 54 /** … … 65 83 }); 66 84 67 85 /** 68 * Utility function namespace 86 * Timeout ID for the current debounced call to requestChangesetUpdate. 87 * 88 * @type {int|null} 89 * @private 90 */ 91 api._updateChangesetTimeoutId = null; 92 93 /** 94 * Staging for changeset changes added by calls to requestChangesetUpdate. 95 * 96 * @since 4.7.0 97 * @type {object|null} 98 * @private 99 */ 100 api._pendingUpdateChanges = null; 101 102 /** 103 * Current jqXHR made from a call to requestChangesetUpdate. 104 * 105 * @type {jQuery.ajax|null} 106 * @private 107 */ 108 api._currentUpdateRequest = null; 109 110 /** 111 * Deferred returned by debounced calls to requestChangesetUpdate. 112 * 113 * @type {jQuery.Deferred|null} 114 * @private 69 115 */ 70 api.utils = {}; 116 api._pendingChangesetUpdateRequestDeferred = null; 117 118 /** 119 * Request updates to the changeset. 120 * 121 * This is implemented in the same way as wp.customize.selectiveRefresh.requestPartial() 122 * in that it will combine multiple calls (debounce) into a single request. 123 * 124 * @param {object} changes Mapping of setting IDs to setting params each normally including a value property, or mapping to null. 125 * @returns {jQuery.Promise} 126 */ 127 api.requestChangesetUpdate = function requestChangesetUpdate( changes ) { 128 var currentDeferred, nextDeferred; 129 130 // Make sure the first request to update the changes will include any initially-dirty settings. 131 if ( null === api._pendingUpdateChanges ) { 132 api._pendingUpdateChanges = {}; 133 api.each( function( setting ) { 134 if ( setting._dirty ) { 135 api._pendingUpdateChanges[ setting.id ] = { value: setting.get() }; 136 } 137 } ); 138 } 139 140 if ( api._pendingChangesetUpdateRequestDeferred ) { 141 currentDeferred = api._pendingChangesetUpdateRequestDeferred; 142 } else { 143 currentDeferred = new $.Deferred(); 144 api._pendingChangesetUpdateRequestDeferred = currentDeferred; 145 146 // Make sure that publishing a changeset waits for all changeset update requests to complete. 147 api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 ); 148 currentDeferred.always( function() { 149 api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 ); 150 } ); 151 } 152 153 // Store the changes in a object containing all of the pending settings for the next request. 154 _.each( changes, function( settingParams, settingId ) { 155 if ( null === settingParams ) { 156 157 // When null is passed as the change, the result will be the removal of the setting from the changeset. A revert. 158 api._pendingUpdateChanges[ settingId ] = null; 159 } else if ( _.isObject( settingParams ) ) { 160 if ( _.isUndefined( api._pendingUpdateChanges[ settingId ] ) ) { 161 api._pendingUpdateChanges[ settingId ] = {}; 162 } 163 _.extend( api._pendingUpdateChanges[ settingId ], settingParams ); 164 } else { 165 throw new Error( 'Unexpected change for ' + settingId ); 166 } 167 } ); 168 169 /* 170 * If there is a changeset update request currently being made, wait until it completes. 171 * No need to pass along the changes because they're already on _pendingUpdateChanges. 172 * Return a new promise that will resolve/reject with the next requests's response. 173 */ 174 if ( api._currentUpdateRequest ) { 175 nextDeferred = $.Deferred(); 176 api._currentUpdateRequest.always( function oncePendingUpdateRequestCompletes() { 177 api.requestChangesetUpdate( {} ).then( 178 function doneNextRequest( data ) { 179 nextDeferred.resolve( data ); 180 }, 181 function failNextRequest( data ) { 182 nextDeferred.reject( data ); 183 } 184 ); 185 } ); 186 return nextDeferred.promise(); 187 } 188 189 // Reset the timeout for the debounced call. 190 if ( api._updateChangesetTimeoutId ) { 191 clearTimeout( api._updateChangesetTimeoutId ); 192 } 193 194 api._updateChangesetTimeoutId = setTimeout( function requestAjaxChangesetUpdate() { 195 var pendingChanges, requestDeferred, request; 196 197 pendingChanges = _.clone( api._pendingUpdateChanges ); 198 api._pendingUpdateChanges = {}; 199 200 // Allow plugins to attach additional params to the settings. 201 api.trigger( 'changeset-save', pendingChanges ); 202 203 requestDeferred = api._pendingChangesetUpdateRequestDeferred; 204 api._pendingChangesetUpdateRequestDeferred = null; 205 206 request = wp.ajax.post( 'customize_save', { 207 wp_customize: 'on', 208 customize_theme: api.settings.theme.stylesheet, 209 nonce: api.settings.nonce.save, 210 customize_changeset_uuid: api.settings.changeset.uuid, 211 customize_changeset_data: JSON.stringify( pendingChanges ) 212 } ); 213 api._currentUpdateRequest = request; 214 215 request.done( function requestChangesetUpdateDone( data ) { 216 api.state( 'changesetStatus' ).set( data.changeset_status ); 217 requestDeferred.resolve( data ); 218 219 api.trigger( 'changeset-saved', data ); 220 api.previewer.send( 'changeset-saved', data ); 221 } ); 222 request.fail( function requestChangesetUpdateFail( data ) { 223 224 api.trigger( 'changeset-error', data ); 225 226 // Make the changes pending again, merging the changes sent in the request with any new pending changes. 227 _.each( pendingChanges, function( settingParams, settingId ) { 228 if ( _.isObject( api._pendingUpdateChanges[ settingId ] ) ) { 229 api._pendingUpdateChanges[ settingId ] = _.extend( 230 settingParams, 231 api._pendingUpdateChanges[ settingId ] 232 ); 233 } else if ( null !== api._pendingUpdateChanges[ settingId ] ) { 234 api._pendingUpdateChanges[ settingId ] = settingParams; 235 } 236 } ); 237 238 requestDeferred.reject( data ); 239 } ); 240 request.always( function( data ) { 241 242 // Allow another request to be made. 243 api._currentUpdateRequest = null; 244 245 if ( data.setting_validities ) { 246 api._handleSettingValidities( { 247 settingValidities: data.setting_validities 248 } ); 249 } 250 } ); 251 }, api.previewer.refreshBuffer ); // @todo Let this come from api.settings.updateChangesetBuffer. 252 253 return currentDeferred.promise(); 254 }; 71 255 72 256 /** 73 257 * Watch all changes to Value properties, and bubble changes to parent Values instance … … 1216 1400 }, 1217 1401 1218 1402 /** 1403 * Get the url to preview a given theme switch. 1404 * 1405 * @param {string} themeId Theme ID. 1406 * @returns {string} Customize URL. 1407 */ 1408 getThemePreviewUrl: function( themeId ) { 1409 var urlParser = document.createElement( 'a' ); 1410 urlParser.href = location.href; 1411 urlParser.search = urlParser.search.replace( /(\?|&)theme=[^&]+(&|$)/, '$1' ); 1412 if ( urlParser.search.length > 1 ) { 1413 urlParser.search += '&'; 1414 } 1415 urlParser.search += 'theme=' + themeId; 1416 return urlParser.href; 1417 }, 1418 1419 /** 1219 1420 * Render & show the theme details for a given theme model. 1220 1421 * 1221 1422 * @since 4.2.0 … … 1232 1433 $( 'body' ).addClass( 'modal-open' ); 1233 1434 section.containFocus( section.overlay ); 1234 1435 section.updateLimits(); 1436 section.overlay.find( '.inactive-theme > a' ).prop( 'href', section.getThemePreviewUrl( theme.id ) ); 1235 1437 callback(); 1236 1438 }, 1237 1439 … … 2227 2429 wp.ajax.post( 'custom-background-add', { 2228 2430 nonce: _wpCustomizeBackground.nonces.add, 2229 2431 wp_customize: 'on', 2230 theme: api.settings.theme.stylesheet,2432 customize_theme: api.settings.theme.stylesheet, 2231 2433 attachment_id: this.params.attachment.id 2232 2434 } ); 2233 2435 } … … 2869 3071 2870 3072 // Bind details view trigger. 2871 3073 control.container.on( 'click keydown touchend', '.theme', function( event ) { 3074 var previewUrl; 3075 2872 3076 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { 2873 3077 return; 2874 3078 } … … 2883 3087 return; 2884 3088 } 2885 3089 2886 var previewUrl = $( this ).data( 'previewUrl');3090 previewUrl = api.ThemesSection.prototype.getThemePreviewUrl( control.params.theme.id ); 2887 3091 2888 3092 $( '.wp-full-overlay' ).addClass( 'customize-loading' ); 2889 3093 3094 // Remove AYS if the changes can persist via the changeset UUID in the URL. 3095 if ( history.replaceState && 0 === api.state( 'processing' ).get() ) { 3096 $( window ).off( 'beforeunload.customize-confirm' ); 3097 } 3098 2890 3099 window.parent.location = previewUrl; 2891 3100 }); 2892 3101 … … 2954 3163 * Initialize the PreviewFrame. 2955 3164 * 2956 3165 * @param {object} params.container 2957 * @param {object} params.signature2958 3166 * @param {object} params.previewUrl 2959 3167 * @param {object} params.query 2960 3168 * @param {object} options … … 2969 3177 deferred.promise( this ); 2970 3178 2971 3179 this.container = params.container; 2972 this.signature = params.signature;2973 3180 2974 3181 $.extend( params, { channel: api.PreviewFrame.uuid() }); 2975 3182 … … 2989 3196 * the request. 2990 3197 */ 2991 3198 run: function( deferred ) { 2992 var self= this,3199 var previewFrame = this, 2993 3200 loaded = false, 2994 ready = false; 3201 ready = false, 3202 readyData = null, 3203 urlParser, 3204 params; 2995 3205 2996 if ( this._ready ) {2997 this.unbind( 'ready', this._ready );3206 if ( previewFrame._ready ) { 3207 previewFrame.unbind( 'ready', previewFrame._ready ); 2998 3208 } 2999 3209 3000 this._ready = function() {3210 previewFrame._ready = function( data ) { 3001 3211 ready = true; 3002 3003 if ( loaded ) { 3004 deferred.resolveWith( self ); 3005 } 3006 }; 3007 3008 this.bind( 'ready', this._ready ); 3009 3010 this.bind( 'ready', function ( data ) { 3011 3012 this.container.addClass( 'iframe-ready' ); 3013 3212 readyData = data; 3213 previewFrame.container.addClass( 'iframe-ready' ); 3014 3214 if ( ! data ) { 3015 3215 return; 3016 3216 } 3017 3217 3018 /* 3019 * Walk over all panels, sections, and controls and set their 3020 * respective active states to true if the preview explicitly 3021 * indicates as such. 3022 */ 3023 var constructs = { 3024 panel: data.activePanels, 3025 section: data.activeSections, 3026 control: data.activeControls 3027 }; 3028 _( constructs ).each( function ( activeConstructs, type ) { 3029 api[ type ].each( function ( construct, id ) { 3030 var isDynamicallyCreated = _.isUndefined( api.settings[ type + 's' ][ id ] ); 3031 3032 /* 3033 * If the construct was created statically in PHP (not dynamically in JS) 3034 * then consider a missing (undefined) value in the activeConstructs to 3035 * mean it should be deactivated (since it is gone). But if it is 3036 * dynamically created then only toggle activation if the value is defined, 3037 * as this means that the construct was also then correspondingly 3038 * created statically in PHP and the active callback is available. 3039 * Otherwise, dynamically-created constructs should normally have 3040 * their active states toggled in JS rather than from PHP. 3041 */ 3042 if ( ! isDynamicallyCreated || ! _.isUndefined( activeConstructs[ id ] ) ) { 3043 if ( activeConstructs[ id ] ) { 3044 construct.activate(); 3045 } else { 3046 construct.deactivate(); 3047 } 3048 } 3049 } ); 3050 } ); 3051 3052 if ( data.settingValidities ) { 3053 api._handleSettingValidities( { 3054 settingValidities: data.settingValidities, 3055 focusInvalidControl: false 3056 } ); 3218 if ( loaded ) { 3219 deferred.resolveWith( previewFrame, [ data ] ); 3057 3220 } 3058 } );3221 }; 3059 3222 3060 this.request = $.ajax( this.previewUrl(), { 3061 type: 'POST', 3062 data: this.query, 3063 xhrFields: { 3064 withCredentials: true 3065 } 3066 } ); 3223 previewFrame.bind( 'ready', previewFrame._ready ); 3067 3224 3068 this.request.fail( function() { 3069 deferred.rejectWith( self, [ 'request failure' ] ); 3070 }); 3225 urlParser = document.createElement( 'a' ); 3226 urlParser.href = this.previewUrl(); 3227 if ( urlParser.search.length > 1 ) { 3228 urlParser.search += '&'; 3229 } 3230 3231 // @todo Logic duplicated. 3232 params = _.clone( this.query ); 3233 delete params.customized; 3234 delete params.wp_customize; 3235 delete params.nonce; 3236 3237 urlParser.search += $.param( params ); 3238 previewFrame.iframe = $( '<iframe />', { 3239 title: api.l10n.previewIframeTitle, 3240 src: urlParser.href 3241 } ); 3242 previewFrame.iframe.attr( 'onmousewheel', '' ); // Workaround for Safari bug. See WP Trac #38149. 3243 previewFrame.iframe.appendTo( previewFrame.container ); 3244 previewFrame.targetWindow( previewFrame.iframe[0].contentWindow ); 3071 3245 3072 this.request.done( function( response ) { 3073 var location = self.request.getResponseHeader('Location'), 3074 signature = self.signature, 3075 index; 3076 3077 // Check if the location response header differs from the current URL. 3078 // If so, the request was redirected; try loading the requested page. 3079 if ( location && location !== self.previewUrl() ) { 3080 deferred.rejectWith( self, [ 'redirect', location ] ); 3081 return; 3082 } 3246 previewFrame.bind( 'iframe-loading-error', function( error ) { 3247 previewFrame.iframe.remove(); 3083 3248 3084 3249 // Check if the user is not logged in. 3085 if ( '0' === response) {3086 self.login( deferred );3250 if ( 0 === error ) { 3251 previewFrame.login( deferred ); 3087 3252 return; 3088 3253 } 3089 3254 3090 3255 // Check for cheaters. 3091 if ( '-1' === response ) { 3092 deferred.rejectWith( self, [ 'cheatin' ] ); 3093 return; 3094 } 3095 3096 // Check for a signature in the request. 3097 index = response.lastIndexOf( signature ); 3098 if ( -1 === index || index < response.lastIndexOf('</html>') ) { 3099 deferred.rejectWith( self, [ 'unsigned' ] ); 3256 if ( -1 === error ) { 3257 deferred.rejectWith( previewFrame, [ 'cheatin' ] ); 3100 3258 return; 3101 3259 } 3102 3260 3103 // Strip the signature from the request. 3104 response = response.slice( 0, index ) + response.slice( index + signature.length ); 3105 3106 // Create the iframe and inject the html content. 3107 self.iframe = $( '<iframe />', { 'title': api.l10n.previewIframeTitle } ).appendTo( self.container ); 3108 self.iframe.attr( 'onmousewheel', '' ); // Workaround for Safari bug. See WP Trac #38149. 3109 3110 // Bind load event after the iframe has been added to the page; 3111 // otherwise it will fire when injected into the DOM. 3112 self.iframe.one( 'load', function() { 3113 loaded = true; 3114 3115 if ( ready ) { 3116 deferred.resolveWith( self ); 3117 } else { 3118 setTimeout( function() { 3119 deferred.rejectWith( self, [ 'ready timeout' ] ); 3120 }, self.sensitivity ); 3121 } 3122 }); 3261 deferred.rejectWith( previewFrame, [ 'request failure' ] ); 3262 } ); 3123 3263 3124 self.targetWindow( self.iframe[0].contentWindow ); 3264 previewFrame.iframe.one( 'load', function() { 3265 loaded = true; 3125 3266 3126 self.targetWindow().document.open(); 3127 self.targetWindow().document.write( response ); 3128 self.targetWindow().document.close(); 3267 if ( ready ) { 3268 deferred.resolveWith( previewFrame, [ readyData ] ); 3269 } else { 3270 setTimeout( function() { 3271 deferred.rejectWith( previewFrame, [ 'ready timeout' ] ); 3272 }, previewFrame.sensitivity ); 3273 } 3129 3274 }); 3130 3275 }, 3131 3276 … … 3164 3309 3165 3310 destroy: function() { 3166 3311 api.Messenger.prototype.destroy.call( this ); 3167 this.request.abort();3312 // @todo this.request.abort(); 3168 3313 3169 3314 if ( this.iframe ) 3170 3315 this.iframe.remove(); … … 3217 3362 * frame to be placed. 3218 3363 * @param {string} params.form 3219 3364 * @param {string} params.previewUrl The URL to preview. 3220 * @param {string} params.signature3221 3365 * @param {object} options 3222 3366 */ 3223 3367 initialize: function( params, options ) { 3224 var self= this,3225 rscheme = /^https?/;3368 var previewer = this, 3369 urlParser = document.createElement( 'a' ); 3226 3370 3227 $.extend( this, options || {} );3228 this.deferred = {3371 $.extend( previewer, options || {} ); 3372 previewer.deferred = { 3229 3373 active: $.Deferred() 3230 3374 }; 3375 previewer.pendingChangesetUpdateRequests = []; 3231 3376 3232 /* 3233 * Wrap this.refresh to prevent it from hammering the servers: 3234 * 3235 * If refresh is called once and no other refresh requests are 3236 * loading, trigger the request immediately. 3237 * 3238 * If refresh is called while another refresh request is loading, 3239 * debounce the refresh requests: 3240 * 1. Stop the loading request (as it is instantly outdated). 3241 * 2. Trigger the new request once refresh hasn't been called for 3242 * self.refreshBuffer milliseconds. 3243 */ 3244 this.refresh = (function( self ) { 3245 var refresh = self.refresh, 3246 callback = function() { 3247 timeout = null; 3248 refresh.call( self ); 3249 }, 3250 timeout; 3251 3252 return function() { 3253 if ( typeof timeout !== 'number' ) { 3254 if ( self.loading ) { 3255 self.abort(); 3256 } else { 3257 return callback(); 3258 } 3259 } 3260 3261 clearTimeout( timeout ); 3262 timeout = setTimeout( callback, self.refreshBuffer ); 3263 }; 3264 })( this ); 3377 // Debounce to prevent hammering server and then wait for any pending update requests. 3378 previewer.refresh = _.debounce( 3379 ( function( originalRefresh ) { 3380 return function() { 3381 var refreshOnceChangesetRequestsComplete = function() { 3382 $.when.apply( $, previewer.pendingChangesetUpdateRequests ).then( function() { 3383 // Check for any newly-pending requests and then wait for them as well. 3384 var pendingCount = _.filter( previewer.pendingChangesetUpdateRequests, function( request ) { 3385 return 'pending' === request.state(); 3386 } ).length; 3387 if ( pendingCount > 0 ) { 3388 refreshOnceChangesetRequestsComplete(); 3389 } else { 3390 originalRefresh.call( previewer ); 3391 } 3392 } ); 3393 }; 3394 refreshOnceChangesetRequestsComplete(); 3395 }; 3396 }( previewer.refresh ) ), 3397 previewer.refreshBuffer 3398 ); 3265 3399 3266 this.container = api.ensure( params.container ); 3267 this.allowedUrls = params.allowedUrls; 3268 this.signature = params.signature; 3400 previewer.container = api.ensure( params.container ); 3401 previewer.allowedUrls = params.allowedUrls; 3269 3402 3270 3403 params.url = window.location.href; 3271 3404 3272 api.Messenger.prototype.initialize.call( this, params );3405 api.Messenger.prototype.initialize.call( previewer, params ); 3273 3406 3274 this.add( 'scheme', this.origin() ).link( this.origin ).setter( function( to ) { 3275 var match = to.match( rscheme ); 3276 return match ? match[0] : ''; 3277 }); 3407 urlParser.href = previewer.origin(); 3408 previewer.add( 'scheme', urlParser.protocol.replace( /:$/, '' ) ); 3278 3409 3279 3410 // Limit the URL to internal, front-end links. 3280 3411 // … … 3284 3415 // are on different domains to avoid the case where the front end doesn't have 3285 3416 // ssl certs. 3286 3417 3287 this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {3288 var result, urlParser ;3418 previewer.add( 'previewUrl', params.previewUrl ).setter( function( to ) { 3419 var result, urlParser, newPreviewUrl, schemeMatchingPreviewUrl, queryParams; 3289 3420 urlParser = document.createElement( 'a' ); 3290 3421 urlParser.href = to; 3291 3422 … … 3294 3425 return null; 3295 3426 } 3296 3427 3428 // Remove state query params. 3429 if ( urlParser.search.length > 1 ) { 3430 queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) ); 3431 delete queryParams.customize_changeset_uuid; 3432 delete queryParams.customize_theme; 3433 delete queryParams.customize_messenger_channel; 3434 if ( _.isEmpty( queryParams ) ) { 3435 urlParser.search = ''; 3436 } else { 3437 urlParser.search = $.param( queryParams ); 3438 } 3439 } 3440 3441 newPreviewUrl = urlParser.href; 3442 urlParser.protocol = previewer.scheme.get() + ':'; 3443 schemeMatchingPreviewUrl = urlParser.href; 3444 3297 3445 // Attempt to match the URL to the control frame's scheme 3298 3446 // and check if it's allowed. If not, try the original URL. 3299 $.each( [ to.replace( rscheme, self.scheme() ), to], function( i, url ) {3300 $.each( self.allowedUrls, function( i, allowed ) {3447 $.each( [ schemeMatchingPreviewUrl, newPreviewUrl ], function( i, url ) { 3448 $.each( previewer.allowedUrls, function( i, allowed ) { 3301 3449 var path; 3302 3450 3303 3451 allowed = allowed.replace( /\/+$/, '' ); … … 3308 3456 return false; 3309 3457 } 3310 3458 }); 3311 if ( result ) 3459 if ( result ) { 3312 3460 return false; 3461 } 3313 3462 }); 3314 3463 3315 3464 // If we found a matching result, return it. If not, bail. 3316 3465 return result ? result : null; 3317 3466 }); 3318 3467 3319 // Refresh the preview when the URL is changed (but not yet). 3320 this.previewUrl.bind( this.refresh ); 3468 // Change preview iframe URL when the previewUrl changes. 3469 previewer.setIframeSrc = _.bind( previewer.setIframeSrc, previewer ); 3470 previewer.setIframeSrcToPreviewUrl = _.bind( previewer.setIframeSrcToPreviewUrl, previewer ); 3471 previewer.bind( 'ready', previewer.ready ); 3472 3473 // Start listening for keep-alive messages when iframe first loads. 3474 previewer.deferred.active.done( _.bind( previewer.keepPreviewAlive, previewer ) ); 3321 3475 3322 this.scroll = 0; 3323 this.bind( 'scroll', function( distance ) { 3324 this.scroll = distance; 3476 previewer.bind( 'synced', function() { 3477 previewer.send( 'active' ); 3478 } ); 3479 3480 previewer.scroll = 0; 3481 previewer.bind( 'scroll', function( distance ) { 3482 previewer.scroll = distance; 3325 3483 }); 3326 3484 3327 3485 // Update the URL when the iframe sends a URL message. 3328 this.bind( 'url', this.previewUrl );3486 previewer.bind( 'url', previewer.previewUrl ); 3329 3487 3330 3488 // Update the document title when the preview changes. 3331 this.bind( 'documentTitle', function ( title ) {3489 previewer.bind( 'documentTitle', function ( title ) { 3332 3490 api.setDocumentTitle( title ); 3333 3491 } ); 3334 3492 }, 3335 3493 3336 3494 /** 3495 * Handle changes to previewUrl to updating the iframe url once any pending processing has completed. 3496 * 3497 * This will call setIframeSrc once the the customizer processing state has turned to 0, 3498 * or if it is 0 upon invocation. This ensures that the changeset will have been written 3499 * before the iframe attempts to load so that the changes will be available when the 3500 * request is made. Also, if there is already such a deferred setting of the iframe src 3501 * queued up, the function will short-circuit and let the existing pending call handle 3502 * the updating of the iframe src. 3503 * 3504 * @since 4.7.0 3505 * @access private 3506 * 3507 * @returns {void} 3508 */ 3509 setIframeSrcToPreviewUrl: ( function() { 3510 var onceProcessed; 3511 3512 return function setIframeSrcToPreviewUrl() { 3513 var previewer = this, processing; 3514 3515 // Short-circuit if we're still waiting for processing to complete. 3516 if ( onceProcessed ) { 3517 return; 3518 } 3519 3520 processing = api.state( 'processing' ); 3521 if ( 0 === processing.get() ) { 3522 previewer.setIframeSrc( previewer.previewUrl.get() ); 3523 } else { 3524 onceProcessed = function( processingCount ) { 3525 if ( 0 === processingCount ) { 3526 processing.unbind( onceProcessed ); 3527 onceProcessed = null; 3528 previewer.setIframeSrc( previewer.previewUrl.get() ); 3529 } 3530 }; 3531 processing.bind( onceProcessed ); 3532 } 3533 }; 3534 } )(), 3535 3536 /** 3537 * Handle the preview receiving the ready message. 3538 * 3539 * @since 4.7.0 3540 * 3541 * @param {object} data - Data from preview. 3542 * @param {string} data.currentUrl - Current URL. 3543 * @param {object} data.activePanels - Active panels. 3544 * @param {object} data.activeSections Active sections. 3545 * @param {object} data.activeControls Active controls. 3546 * @returns {void} 3547 */ 3548 ready: function( data ) { 3549 var previewer = this, synced = {}, constructs; 3550 3551 synced.settings = api.get(); 3552 if ( 'resolved' !== previewer.deferred.active.state() || previewer.loading ) { 3553 synced.scroll = previewer.scroll; 3554 } 3555 previewer.send( 'sync', synced ); 3556 3557 // Set the previewUrl without causing the url to set the iframe. 3558 if ( data.currentUrl ) { 3559 previewer.previewUrl.unbind( previewer.setIframeSrcToPreviewUrl ); 3560 previewer.previewUrl.set( data.currentUrl ); 3561 previewer.previewUrl.bind( previewer.setIframeSrcToPreviewUrl ); 3562 } 3563 3564 /* 3565 * Walk over all panels, sections, and controls and set their 3566 * respective active states to true if the preview explicitly 3567 * indicates as such. 3568 */ 3569 constructs = { 3570 panel: data.activePanels, 3571 section: data.activeSections, 3572 control: data.activeControls 3573 }; 3574 _( constructs ).each( function ( activeConstructs, type ) { 3575 api[ type ].each( function ( construct, id ) { 3576 var isDynamicallyCreated = _.isUndefined( api.settings[ type + 's' ][ id ] ); 3577 3578 /* 3579 * If the construct was created statically in PHP (not dynamically in JS) 3580 * then consider a missing (undefined) value in the activeConstructs to 3581 * mean it should be deactivated (since it is gone). But if it is 3582 * dynamically created then only toggle activation if the value is defined, 3583 * as this means that the construct was also then correspondingly 3584 * created statically in PHP and the active callback is available. 3585 * Otherwise, dynamically-created constructs should normally have 3586 * their active states toggled in JS rather than from PHP. 3587 */ 3588 if ( ! isDynamicallyCreated || ! _.isUndefined( activeConstructs[ id ] ) ) { 3589 if ( activeConstructs[ id ] ) { 3590 construct.activate(); 3591 } else { 3592 construct.deactivate(); 3593 } 3594 } 3595 } ); 3596 } ); 3597 3598 if ( data.settingValidities ) { 3599 api._handleSettingValidities( { 3600 settingValidities: data.settingValidities, 3601 focusInvalidControl: false 3602 } ); 3603 } 3604 }, 3605 3606 /** 3607 * Keep the preview alive by listening for ready and keep-alive messages. 3608 * 3609 * If a message is not received in the allotted time then the iframe will be set back to the last known valid URL. 3610 * 3611 * @since 4.7.0 3612 * 3613 * @returns {void} 3614 */ 3615 keepPreviewAlive: function keepPreviewAlive() { 3616 var previewer = this, heartbeatTick, timeoutId, handleMissingHeartbeat, scheduleHeartbeatCheck, lastKnownUrl; 3617 3618 lastKnownUrl = previewer.previewUrl.get(); 3619 previewer.previewUrl.bind( function( url ) { 3620 lastKnownUrl = url; 3621 } ); 3622 3623 // @todo What if a page load takes more than 3 seconds? 3624 scheduleHeartbeatCheck = function() { 3625 timeoutId = setTimeout( handleMissingHeartbeat, 3000 ); // @todo Let interval be a parameter. 3626 }; 3627 heartbeatTick = function() { 3628 clearTimeout( timeoutId ); 3629 scheduleHeartbeatCheck(); 3630 }; 3631 handleMissingHeartbeat = function() { 3632 3633 // @todo Instead of changing the URL if the iframe, there should be a notification to prompt the user to return to a customize preview. 3634 previewer.setIframeSrc( lastKnownUrl ); 3635 }; 3636 scheduleHeartbeatCheck(); 3637 3638 previewer.bind( 'ready', heartbeatTick ); 3639 previewer.bind( 'keep-alive', heartbeatTick ); 3640 }, 3641 3642 /** 3643 * Set the iframe's src URL. 3644 * 3645 * @since 4.7.0 3646 * 3647 * @param {string} url URL. 3648 * @returns {void} 3649 */ 3650 setIframeSrc: function( url ) { 3651 var previewer = this, urlParser, params, oldParams, newParams; 3652 3653 urlParser = document.createElement( 'a' ); 3654 3655 // @todo Duplication. 3656 params = previewer.query(); 3657 delete params.customized; 3658 delete params.wp_customize; 3659 delete params.nonce; 3660 params.customize_messenger_channel = previewer.channel(); 3661 3662 urlParser.href = url; 3663 oldParams = api.utils.parseQueryString( urlParser.search.substring( 1 ) ); 3664 newParams = _.extend( {}, oldParams, params ); 3665 3666 // @todo We may need to add a new param for customize_persistent_query_params as _.keys( newParams ). 3667 urlParser.search = $.param( newParams ); 3668 previewer.preview.iframe.prop( 'src', urlParser.href ); 3669 }, 3670 3671 /** 3672 * Add pending changeset update request promise. 3673 * 3674 * @since 4.7.0 3675 * 3676 * @param {jQuery.Promise} promise Promise. 3677 * @returns {void} 3678 */ 3679 addPendingChangesetUpdateRequest: function( promise ) { 3680 var previewer = this; 3681 previewer.pendingChangesetUpdateRequests.push( promise ); 3682 promise.always( function() { 3683 var i = _.indexOf( previewer.pendingChangesetUpdateRequests, promise ); 3684 if ( -1 !== i ) { 3685 previewer.pendingChangesetUpdateRequests.splice( i, 1 ); 3686 } 3687 } ); 3688 }, 3689 3690 /** 3337 3691 * Query string data sent with each preview request. 3338 3692 * 3339 3693 * @abstract … … 3348 3702 }, 3349 3703 3350 3704 /** 3351 * Refresh the preview .3705 * Refresh the preview seamlessly. 3352 3706 */ 3353 3707 refresh: function() { 3354 var self= this;3708 var previewer = this; 3355 3709 3356 3710 // Display loading indicator 3357 this.send( 'loading-initiated' );3711 previewer.send( 'loading-initiated' ); 3358 3712 3359 this.abort();3713 previewer.abort(); 3360 3714 3361 this.loading = new api.PreviewFrame({ 3362 url: this.url(), 3363 previewUrl: this.previewUrl(), 3364 query: this.query() || {}, 3365 container: this.container, 3366 signature: this.signature 3367 }); 3368 3369 this.loading.done( function() { 3370 // 'this' is the loading frame 3371 this.bind( 'synced', function() { 3372 if ( self.preview ) 3373 self.preview.destroy(); 3374 self.preview = this; 3375 delete self.loading; 3715 previewer.loading = new api.PreviewFrame({ 3716 url: previewer.url(), 3717 previewUrl: previewer.previewUrl(), 3718 query: previewer.query() || {}, 3719 container: previewer.container 3720 }); 3376 3721 3377 self.targetWindow( this.targetWindow() );3378 self.channel( this.channel() );3722 previewer.loading.done( function( readyData ) { 3723 var loadingFrame = this, previousPreview, onceSynced; 3379 3724 3380 self.deferred.active.resolve(); 3381 self.send( 'active' ); 3382 }); 3725 previousPreview = previewer.preview; 3726 previewer.preview = loadingFrame; 3727 previewer.targetWindow( loadingFrame.targetWindow() ); 3728 previewer.channel( loadingFrame.channel() ); 3383 3729 3384 this.send( 'sync', { 3385 scroll: self.scroll, 3386 settings: api.get() 3387 }); 3730 onceSynced = function() { 3731 loadingFrame.unbind( 'synced', onceSynced ); 3732 if ( previousPreview ) { 3733 previousPreview.destroy(); 3734 } 3735 previewer.deferred.active.resolve(); 3736 delete previewer.loading; 3737 }; 3738 loadingFrame.bind( 'synced', onceSynced ); 3739 3740 // This event will be received directly by the previewer in normal navigation; this is only needed for seamless refresh. 3741 previewer.trigger( 'ready', readyData ); 3388 3742 }); 3389 3743 3390 this.loading.fail( function( reason, location ) { 3391 self.send( 'loading-failed' ); 3392 if ( 'redirect' === reason && location ) { 3393 self.previewUrl( location ); 3394 } 3744 previewer.loading.fail( function( reason ) { 3745 previewer.send( 'loading-failed' ); 3395 3746 3396 3747 if ( 'logged out' === reason ) { 3397 if ( self.preview ) {3398 self.preview.destroy();3399 delete self.preview;3748 if ( previewer.preview ) { 3749 previewer.preview.destroy(); 3750 delete previewer.preview; 3400 3751 } 3401 3752 3402 self.login().done( self.refresh );3753 previewer.login().done( previewer.refresh ); 3403 3754 } 3404 3755 3405 3756 if ( 'cheatin' === reason ) { 3406 self.cheatin();3757 previewer.cheatin(); 3407 3758 } 3408 3759 }); 3409 3760 }, … … 3463 3814 3464 3815 request = wp.ajax.post( 'customize_refresh_nonces', { 3465 3816 wp_customize: 'on', 3466 theme: api.settings.theme.stylesheet3817 customize_theme: api.settings.theme.stylesheet 3467 3818 }); 3468 3819 3469 3820 request.done( function( response ) { … … 3722 4073 container: '#customize-preview', 3723 4074 form: '#customize-controls', 3724 4075 previewUrl: api.settings.url.preview, 3725 allowedUrls: api.settings.url.allowed, 3726 signature: 'WP_CUSTOMIZER_SIGNATURE' 4076 allowedUrls: api.settings.url.allowed 3727 4077 }, { 3728 4078 3729 4079 nonce: api.settings.nonce, … … 3743 4093 3744 4094 return { 3745 4095 wp_customize: 'on', 3746 theme:api.settings.theme.stylesheet,4096 customize_theme: api.settings.theme.stylesheet, 3747 4097 customized: JSON.stringify( dirtyCustomized ), 3748 nonce: this.nonce.preview 4098 nonce: this.nonce.preview, 4099 customize_changeset_uuid: api.settings.changeset.uuid 3749 4100 }; 3750 4101 }, 3751 4102 3752 save: function() { 3753 var self = this, 4103 /** 4104 * Save (and publish) the customizer changeset. 4105 * 4106 * @param {object} [args] Args. 4107 * @param {string} [args.status=publish] Status. 4108 * @param {string} [args.date] Date, in local time. 4109 * @param {string} [args.title] Title 4110 * 4111 * @returns {jQuery.promise} 4112 */ 4113 save: function( args ) { 4114 var previewer = this, 4115 deferred = $.Deferred(), 4116 changesetStatus = 'publish', 3754 4117 processing = api.state( 'processing' ), 3755 4118 submitWhenDoneProcessing, 3756 4119 submit, … … 3758 4121 invalidSettings = [], 3759 4122 invalidControls; 3760 4123 3761 body.addClass( 'saving' ); 4124 if ( args && args.status ) { 4125 changesetStatus = args.status; 4126 } 4127 4128 if ( api.state( 'saving' ).get() ) { 4129 deferred.reject( 'already_saving' ); 4130 deferred.promise(); 4131 } 4132 4133 api.state( 'changesetStatus' ).set( changesetStatus ); 4134 api.state( 'saving' ).set( true ); 3762 4135 3763 4136 function captureSettingModifiedDuringSave( setting ) { 3764 4137 modifiedWhileSaving[ setting.id ] = true; … … 3766 4139 api.bind( 'change', captureSettingModifiedDuringSave ); 3767 4140 3768 4141 submit = function () { 3769 var request, query ;4142 var request, query, settingInvalidities = {}; 3770 4143 3771 4144 /* 3772 4145 * Block saving if there are any settings that are marked as … … 3775 4148 */ 3776 4149 api.each( function( setting ) { 3777 4150 setting.notifications.each( function( notification ) { 3778 if ( 'error' === notification.type && ! notification.fromServer ) { 4151 if ( 'error' === notification.type && ! notification.fromServer ) { // @todo Eliminate the fromServer now that changeset is updated with each change? 3779 4152 invalidSettings.push( setting.id ); 4153 if ( ! settingInvalidities[ setting.id ] ) { 4154 settingInvalidities[ setting.id ] = {}; 4155 } 4156 settingInvalidities[ setting.id ][ notification.code ] = notification; 3780 4157 } 3781 4158 } ); 3782 4159 } ); 3783 4160 invalidControls = api.findControlsForSettings( invalidSettings ); 3784 4161 if ( ! _.isEmpty( invalidControls ) ) { 3785 4162 _.values( invalidControls )[0][0].focus(); 3786 body.removeClass( 'saving' );3787 4163 api.unbind( 'change', captureSettingModifiedDuringSave ); 3788 return; 4164 deferred.rejectWith( previewer, [ 4165 { setting_invalidities: settingInvalidities } 4166 ] ); 4167 api.state( 'saving' ).set( false ); 4168 return deferred.promise(); 3789 4169 } 3790 4170 3791 query = $.extend( self.query(), { 3792 nonce: self.nonce.save 4171 query = $.extend( previewer.query(), { 4172 nonce: previewer.nonce.save, 4173 customize_changeset_status: api.state( 'changesetStatus' ).get() 3793 4174 } ); 4175 if ( args && args.date ) { 4176 query.customize_changeset_date = args.date; 4177 } 4178 if ( args && args.title ) { 4179 query.customize_changeset_title = args.title; 4180 } 4181 4182 /* 4183 * Note that the dirty customized values will have already been set in the 4184 * changeset and so technically query.customized could be deleted. However, 4185 * it is remaining here to make sure that any settings that got updated 4186 * quietly which may have not triggered an update request will also get 4187 * included in the values that get saved to the changeset. This will ensure 4188 * that values that get injected via the saved event will be included in 4189 * the changeset. This also ensures that setting values that were invalid 4190 * will get re-validated, perhaps in the case of settings that are invalid 4191 * due to dependencies on other settings. 4192 */ 3794 4193 request = wp.ajax.post( 'customize_save', query ); 3795 4194 3796 4195 // Disable save button during the save request. … … 3799 4198 api.trigger( 'save', request ); 3800 4199 3801 4200 request.always( function () { 3802 body.removeClass( 'saving');4201 api.state( 'saving' ).set( false ); 3803 4202 saveBtn.prop( 'disabled', false ); 3804 4203 api.unbind( 'change', captureSettingModifiedDuringSave ); 3805 4204 } ); 3806 4205 3807 4206 request.fail( function ( response ) { 4207 3808 4208 if ( '0' === response ) { 3809 4209 response = 'not_logged_in'; 3810 4210 } else if ( '-1' === response ) { … … 3813 4213 } 3814 4214 3815 4215 if ( 'invalid_nonce' === response ) { 3816 self.cheatin();4216 previewer.cheatin(); 3817 4217 } else if ( 'not_logged_in' === response ) { 3818 self.preview.iframe.hide();3819 self.login().done( function() {3820 self.save();3821 self.preview.iframe.show();4218 previewer.preview.iframe.hide(); 4219 previewer.login().done( function() { 4220 previewer.save(); 4221 previewer.preview.iframe.show(); 3822 4222 } ); 3823 4223 } 3824 4224 … … 3829 4229 } ); 3830 4230 } 3831 4231 4232 deferred.rejectWith( previewer, [ response ] ); 3832 4233 api.trigger( 'error', response ); 3833 4234 } ); 3834 4235 … … 3841 4242 } 3842 4243 } ); 3843 4244 3844 api.previewer.send( 'saved', response ); 4245 previewer.send( 'saved', response ); 4246 4247 api.state( 'changesetStatus' ).set( response.changeset_status ); 4248 if ( 'publish' === response.changeset_status ) { 4249 api.state( 'changesetStatus' ).set( '' ); 4250 api.settings.changeset.uuid = response.next_changeset_uuid; 4251 parent.send( 'changeset-uuid', api.settings.changeset.uuid ); 4252 } 3845 4253 3846 4254 if ( response.setting_validities ) { 3847 4255 api._handleSettingValidities( { … … 3850 4258 } ); 3851 4259 } 3852 4260 4261 deferred.resolveWith( previewer, [ response ] ); 3853 4262 api.trigger( 'saved', response ); 3854 4263 3855 4264 // Restore the global dirty state if any settings were modified during save. … … 3871 4280 api.state.bind( 'change', submitWhenDoneProcessing ); 3872 4281 } 3873 4282 4283 return deferred.promise(); 3874 4284 } 3875 4285 }); 3876 4286 … … 3963 4373 values.bind( 'remove', debouncedReflowPaneContents ); 3964 4374 } ); 3965 4375 3966 // Check if preview url is valid and load the preview frame.3967 if ( api.previewer.previewUrl() ) {3968 api.previewer.refresh();3969 } else {3970 api.previewer.previewUrl( api.settings.url.home );3971 }3972 3973 4376 // Save and activated states 3974 4377 (function() { 3975 4378 var state = new api.Values(), 3976 4379 saved = state.create( 'saved' ), 4380 saving = state.create( 'saving' ), 3977 4381 activated = state.create( 'activated' ), 3978 4382 processing = state.create( 'processing' ), 3979 paneVisible = state.create( 'paneVisible' ); 4383 paneVisible = state.create( 'paneVisible' ), 4384 changesetStatus = state.create( 'changesetStatus' ); 3980 4385 3981 4386 state.bind( 'change', function() { 4387 var canSave; 4388 3982 4389 if ( ! activated() ) { 3983 saveBtn.val( api.l10n.activate ) .prop( 'disabled', false );4390 saveBtn.val( api.l10n.activate ); 3984 4391 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel ); 3985 4392 3986 } else if ( saved() ) {3987 saveBtn.val( api.l10n.saved ) .prop( 'disabled', true );4393 } else if ( '' === changesetStatus.get() ) { 4394 saveBtn.val( api.l10n.saved ); 3988 4395 closeBtn.find( '.screen-reader-text' ).text( api.l10n.close ); 3989 4396 3990 4397 } else { 3991 saveBtn.val( api.l10n.save ) .prop( 'disabled', false );4398 saveBtn.val( api.l10n.save ); 3992 4399 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel ); 3993 4400 } 4401 4402 /* 4403 * Save (publish) button should be enabled if saving is not currently happening, 4404 * and if the theme is not active or the changeset exists but is not published. 4405 */ 4406 canSave = ! saving() && ( ! activated() || ( '' !== changesetStatus() && 'publish' !== changesetStatus() ) ); 4407 4408 saveBtn.prop( 'disabled', ! canSave ); 3994 4409 }); 3995 4410 3996 4411 // Set default states. 3997 4412 saved( true ); 4413 saving( false ); 3998 4414 activated( api.settings.theme.active ); 3999 4415 processing( 0 ); 4000 4416 paneVisible( true ); 4417 changesetStatus( api.settings.changeset.status ); 4001 4418 4002 4419 api.bind( 'change', function() { 4003 4420 state('saved').set( false ); 4004 4421 }); 4005 4422 4006 api.bind( 'saved', function() { 4423 saving.bind( function( isSaving ) { 4424 body.toggleClass( 'saving', isSaving ); 4425 } ); 4426 4427 api.bind( 'saved', function( response ) { 4007 4428 state('saved').set( true ); 4008 state('activated').set( true ); 4429 if ( 'publish' === response.changeset_status ) { 4430 state( 'activated' ).set( true ); 4431 } 4009 4432 }); 4010 4433 4011 4434 activated.bind( function( to ) { … … 4014 4437 } 4015 4438 }); 4016 4439 4440 changesetStatus.bind( function( newStatus, oldStatus ) { 4441 var urlParser; 4442 4443 if ( ! history.replaceState ) { 4444 return; 4445 } 4446 4447 // Abort if not a transition between existing and non-existing. 4448 if ( newStatus && oldStatus ) { 4449 return; 4450 } 4451 4452 urlParser = document.createElement( 'a' ); 4453 urlParser.href = location.href; 4454 urlParser.search = urlParser.search.replace( /(\?|&)changeset_uuid=[^&]+(&|$)/, '$1' ); 4455 urlParser.search = urlParser.search.replace( /&+$/, '' ); 4456 if ( '' !== newStatus ) { 4457 if ( urlParser.search.length > 1 ) { 4458 urlParser.search += '&'; 4459 } 4460 urlParser.search += 'changeset_uuid=' + api.settings.changeset.uuid; 4461 } 4462 history.replaceState( {}, document.title, urlParser.href ); 4463 } ); 4464 4017 4465 // Expose states to the API. 4018 4466 api.state = state; 4019 4467 }()); 4020 4468 4469 // Check if preview url is valid and load the preview frame. 4470 if ( api.previewer.previewUrl() ) { 4471 api.previewer.refresh(); 4472 } else { 4473 api.previewer.previewUrl( api.settings.url.home ); 4474 } 4475 4021 4476 // Button bindings. 4022 4477 saveBtn.click( function( event ) { 4023 4478 api.previewer.save(); … … 4169 4624 }); 4170 4625 4171 4626 // Prompt user with AYS dialog if leaving the Customizer with unsaved changes 4172 $( window ).on( 'beforeunload ', function () {4627 $( window ).on( 'beforeunload.customize-confirm', function () { 4173 4628 if ( ! api.state( 'saved' )() ) { 4174 4629 setTimeout( function() { 4175 4630 overlay.removeClass( 'customize-loading' ); … … 4190 4645 parent.send( 'title', newTitle ); 4191 4646 }); 4192 4647 4648 parent.send( 'changeset-uuid', api.settings.changeset.uuid ); 4649 4193 4650 // Initialize the connection with the parent frame. 4194 4651 parent.send( 'ready' ); 4195 4652 -
src/wp-admin/js/customize-widgets.js
1154 1154 params.action = 'update-widget'; 1155 1155 params.wp_customize = 'on'; 1156 1156 params.nonce = api.settings.nonce['update-widget']; 1157 params. theme = api.settings.theme.stylesheet;1157 params.customize_theme = api.settings.theme.stylesheet; 1158 1158 params.customized = wp.customize.previewer.query().customized; 1159 1159 1160 1160 data = $.param( params ); -
src/wp-admin/customize.php
20 20 ); 21 21 } 22 22 23 /** 24 * @global WP_Scripts $wp_scripts 25 * @global WP_Customize_Manager $wp_customize 26 */ 27 global $wp_scripts, $wp_customize; 28 29 if ( $wp_customize->changeset_post_id() ) { 30 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $wp_customize->changeset_post_id() ) ) { 31 wp_die( 32 '<h1>' . __( 'Cheatin’ uh?' ) . '</h1>' . 33 '<p>' . __( 'Sorry, you are not allowed to edit this changeset.' ) . '</p>', 34 403 35 ); 36 } 37 if ( in_array( get_post_status( $wp_customize->changeset_post_id() ), array( 'publish', 'trash' ), true ) ) { 38 wp_die( 39 '<h1>' . __( 'Cheatin’ uh?' ) . '</h1>' . 40 '<p>' . __( 'This changeset has already been published and cannot be further modified.' ) . '</p>', 41 403 42 ); 43 } 44 } 45 46 23 47 wp_reset_vars( array( 'url', 'return', 'autofocus' ) ); 24 48 if ( ! empty( $url ) ) { 25 49 $wp_customize->set_preview_url( wp_unslash( $url ) ); … … 31 55 $wp_customize->set_autofocus( wp_unslash( $autofocus ) ); 32 56 } 33 57 34 /**35 * @global WP_Scripts $wp_scripts36 * @global WP_Customize_Manager $wp_customize37 */38 global $wp_scripts, $wp_customize;39 40 58 $registered = $wp_scripts->registered; 41 59 $wp_scripts = new WP_Scripts; 42 60 $wp_scripts->registered = $registered; … … 115 133 <div id="customize-header-actions" class="wp-full-overlay-header"> 116 134 <?php 117 135 $save_text = $wp_customize->is_theme_active() ? __( 'Save & Publish' ) : __( 'Save & Activate' ); 118 submit_button( $save_text, 'primary save', 'save', false ); 136 $save_attrs = array(); 137 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) { 138 $save_attrs['style'] = 'display: none'; 139 } 140 submit_button( $save_text, 'primary save', 'save', false, $save_attrs ); 119 141 ?> 120 142 <span class="spinner"></span> 121 143 <button type="button" class="customize-controls-preview-toggle">