@@ -1240,6 +1240,171 @@ function test_save_changeset_post_without_theme_activation() {
12401240 $ this ->assertCount ( 3 , wp_get_post_revisions ( $ manager ->changeset_post_id () ) );
12411241 }
12421242
1243+ /**
1244+ * Test saving changeset post without Kses or other content_save_pre filters mutating content.
1245+ *
1246+ * @covers WP_Customize_Manager::save_changeset_post()
1247+ */
1248+ public function test_save_changeset_post_without_kses_corrupting_json () {
1249+ global $ wp_customize ;
1250+ $ lesser_admin_user_id = self ::factory ()->user ->create ( array ( 'role ' => 'administrator ' ) );
1251+
1252+ $ uuid = wp_generate_uuid4 ();
1253+ $ wp_customize = new WP_Customize_Manager (
1254+ array (
1255+ 'changeset_uuid ' => $ uuid ,
1256+ )
1257+ );
1258+
1259+ add_filter ( 'map_meta_cap ' , array ( $ this , 'filter_map_meta_cap_to_disallow_unfiltered_html ' ), 10 , 2 );
1260+ kses_init ();
1261+ add_filter ( 'content_save_pre ' , 'capital_P_dangit ' );
1262+ add_post_type_support ( 'customize_changeset ' , 'revisions ' );
1263+
1264+ $ options = array (
1265+ 'custom_html_1 ' => '<script>document.write(" Wordpress 1")</script> ' ,
1266+ 'custom_html_2 ' => '<script>document.write(" Wordpress 2")</script> ' ,
1267+ 'custom_html_3 ' => '<script>document.write(" Wordpress 3")</script> ' ,
1268+ );
1269+
1270+ // Populate setting as user who can bypass content_save_pre filter.
1271+ wp_set_current_user ( self ::$ admin_user_id );
1272+ $ wp_customize = $ this ->get_manager_for_testing_json_corruption_protection ( $ uuid );
1273+ $ wp_customize ->set_post_value ( 'custom_html_1 ' , $ options ['custom_html_1 ' ] );
1274+ $ wp_customize ->save_changeset_post (
1275+ array (
1276+ 'status ' => 'draft ' ,
1277+ )
1278+ );
1279+
1280+ // Populate setting as user who cannot bypass content_save_pre filter.
1281+ wp_set_current_user ( $ lesser_admin_user_id );
1282+ $ wp_customize = $ this ->get_manager_for_testing_json_corruption_protection ( $ uuid );
1283+ $ wp_customize ->set_post_value ( 'custom_html_2 ' , $ options ['custom_html_2 ' ] );
1284+ $ wp_customize ->save_changeset_post (
1285+ array (
1286+ 'autosave ' => true ,
1287+ )
1288+ );
1289+
1290+ /*
1291+ * Ensure that the unsanitized value (the "POST data") is preserved in the autosave revision.
1292+ * The value is sent through the sanitize function when it is read from the changeset.
1293+ */
1294+ $ autosave_revision = wp_get_post_autosave ( $ wp_customize ->changeset_post_id (), get_current_user_id () );
1295+ $ saved_data = json_decode ( $ autosave_revision ->post_content , true );
1296+ $ this ->assertEquals ( $ options ['custom_html_1 ' ], $ saved_data ['custom_html_1 ' ]['value ' ] );
1297+ $ this ->assertEquals ( $ options ['custom_html_2 ' ], $ saved_data ['custom_html_2 ' ]['value ' ] );
1298+
1299+ // Update post to discard autosave.
1300+ $ wp_customize ->save_changeset_post (
1301+ array (
1302+ 'status ' => 'draft ' ,
1303+ )
1304+ );
1305+
1306+ /*
1307+ * Ensure that the unsanitized value (the "POST data") is preserved in the post content.
1308+ * The value is sent through the sanitize function when it is read from the changeset.
1309+ */
1310+ $ wp_customize = $ this ->get_manager_for_testing_json_corruption_protection ( $ uuid );
1311+ $ saved_data = json_decode ( get_post ( $ wp_customize ->changeset_post_id () )->post_content , true );
1312+ $ this ->assertEquals ( $ options ['custom_html_1 ' ], $ saved_data ['custom_html_1 ' ]['value ' ] );
1313+ $ this ->assertEquals ( $ options ['custom_html_2 ' ], $ saved_data ['custom_html_2 ' ]['value ' ] );
1314+
1315+ /*
1316+ * Ensure that the unsanitized value (the "POST data") is preserved in the revisions' content.
1317+ * The value is sent through the sanitize function when it is read from the changeset.
1318+ */
1319+ $ revisions = wp_get_post_revisions ( $ wp_customize ->changeset_post_id () );
1320+ $ revision = array_shift ( $ revisions );
1321+ $ saved_data = json_decode ( $ revision ->post_content , true );
1322+ $ this ->assertEquals ( $ options ['custom_html_1 ' ], $ saved_data ['custom_html_1 ' ]['value ' ] );
1323+ $ this ->assertEquals ( $ options ['custom_html_2 ' ], $ saved_data ['custom_html_2 ' ]['value ' ] );
1324+
1325+ /*
1326+ * Now when publishing the changeset, the unsanitized values will be read from the changeset
1327+ * and sanitized according to the capabilities of the users who originally updated each
1328+ * setting in the changeset to begin with.
1329+ */
1330+ wp_set_current_user ( $ lesser_admin_user_id );
1331+ $ wp_customize = $ this ->get_manager_for_testing_json_corruption_protection ( $ uuid );
1332+ $ wp_customize ->set_post_value ( 'custom_html_3 ' , $ options ['custom_html_3 ' ] );
1333+ $ wp_customize ->save_changeset_post (
1334+ array (
1335+ 'status ' => 'publish ' ,
1336+ )
1337+ );
1338+
1339+ // User saved as one who can bypass content_save_pre filter.
1340+ $ this ->assertContains ( '<script> ' , get_option ( 'custom_html_1 ' ) );
1341+ $ this ->assertContains ( 'Wordpress ' , get_option ( 'custom_html_1 ' ) ); // phpcs:ignore WordPress.WP.CapitalPDangit.Misspelled
1342+
1343+ // User saved as one who cannot bypass content_save_pre filter.
1344+ $ this ->assertNotContains ( '<script> ' , get_option ( 'custom_html_2 ' ) );
1345+ $ this ->assertContains ( 'WordPress ' , get_option ( 'custom_html_2 ' ) );
1346+
1347+ // User saved as one who also cannot bypass content_save_pre filter.
1348+ $ this ->assertNotContains ( '<script> ' , get_option ( 'custom_html_3 ' ) );
1349+ $ this ->assertContains ( 'WordPress ' , get_option ( 'custom_html_3 ' ) );
1350+ }
1351+
1352+ /**
1353+ * Get a manager for testing JSON corruption protection.
1354+ *
1355+ * @param string $uuid UUID.
1356+ * @return WP_Customize_Manager Manager.
1357+ */
1358+ private function get_manager_for_testing_json_corruption_protection ( $ uuid ) {
1359+ global $ wp_customize ;
1360+ $ wp_customize = new WP_Customize_Manager (
1361+ array (
1362+ 'changeset_uuid ' => $ uuid ,
1363+ )
1364+ );
1365+ for ( $ i = 0 ; $ i < 5 ; $ i ++ ) {
1366+ $ wp_customize ->add_setting (
1367+ sprintf ( 'custom_html_%d ' , $ i ),
1368+ array (
1369+ 'type ' => 'option ' ,
1370+ 'sanitize_callback ' => array ( $ this , 'apply_content_save_pre_filters_if_not_main_admin_user ' ),
1371+ )
1372+ );
1373+ }
1374+ return $ wp_customize ;
1375+ }
1376+
1377+ /**
1378+ * Sanitize content with Kses if the current user is not the main admin.
1379+ *
1380+ * @since 5.2.?
1381+ *
1382+ * @param string $content Content to sanitize.
1383+ * @return string Sanitized content.
1384+ */
1385+ public function apply_content_save_pre_filters_if_not_main_admin_user ( $ content ) {
1386+ if ( get_current_user_id () !== self ::$ admin_user_id ) {
1387+ $ content = apply_filters ( 'content_save_pre ' , $ content );
1388+ }
1389+ return $ content ;
1390+ }
1391+
1392+ /**
1393+ * Filter map_meta_cap to disallow unfiltered_html.
1394+ *
1395+ * @since 5.2.?
1396+ *
1397+ * @param array $caps User's capabilities.
1398+ * @param string $cap Requested cap.
1399+ * @return array Caps.
1400+ */
1401+ public function filter_map_meta_cap_to_disallow_unfiltered_html ( $ caps , $ cap ) {
1402+ if ( 'unfiltered_html ' === $ cap ) {
1403+ $ caps = array ( 'do_not_allow ' );
1404+ }
1405+ return $ caps ;
1406+ }
1407+
12431408 /**
12441409 * Call count for customize_changeset_save_data filter.
12451410 *
0 commit comments