@@ -1089,6 +1089,171 @@ function test_save_changeset_post_without_theme_activation() {
10891089 $ this ->assertCount ( 3 , wp_get_post_revisions ( $ manager ->changeset_post_id () ) );
10901090 }
10911091
1092+ /**
1093+ * Test saving changeset post without Kses or other content_save_pre filters mutating content.
1094+ *
1095+ * @covers WP_Customize_Manager::save_changeset_post()
1096+ */
1097+ public function test_save_changeset_post_without_kses_corrupting_json () {
1098+ global $ wp_customize ;
1099+ $ lesser_admin_user_id = self ::factory ()->user ->create ( array ( 'role ' => 'administrator ' ) );
1100+
1101+ $ uuid = wp_generate_uuid4 ();
1102+ $ wp_customize = new WP_Customize_Manager (
1103+ array (
1104+ 'changeset_uuid ' => $ uuid ,
1105+ )
1106+ );
1107+
1108+ add_filter ( 'map_meta_cap ' , array ( $ this , 'filter_map_meta_cap_to_disallow_unfiltered_html ' ), 10 , 2 );
1109+ kses_init ();
1110+ add_filter ( 'content_save_pre ' , 'capital_P_dangit ' );
1111+ add_post_type_support ( 'customize_changeset ' , 'revisions ' );
1112+
1113+ $ options = array (
1114+ 'custom_html_1 ' => '<script>document.write(" Wordpress 1")</script> ' ,
1115+ 'custom_html_2 ' => '<script>document.write(" Wordpress 2")</script> ' ,
1116+ 'custom_html_3 ' => '<script>document.write(" Wordpress 3")</script> ' ,
1117+ );
1118+
1119+ // Populate setting as user who can bypass content_save_pre filter.
1120+ wp_set_current_user ( self ::$ admin_user_id );
1121+ $ wp_customize = $ this ->get_manager_for_testing_json_corruption_protection ( $ uuid );
1122+ $ wp_customize ->set_post_value ( 'custom_html_1 ' , $ options ['custom_html_1 ' ] );
1123+ $ wp_customize ->save_changeset_post (
1124+ array (
1125+ 'status ' => 'draft ' ,
1126+ )
1127+ );
1128+
1129+ // Populate setting as user who cannot bypass content_save_pre filter.
1130+ wp_set_current_user ( $ lesser_admin_user_id );
1131+ $ wp_customize = $ this ->get_manager_for_testing_json_corruption_protection ( $ uuid );
1132+ $ wp_customize ->set_post_value ( 'custom_html_2 ' , $ options ['custom_html_2 ' ] );
1133+ $ wp_customize ->save_changeset_post (
1134+ array (
1135+ 'autosave ' => true ,
1136+ )
1137+ );
1138+
1139+ /*
1140+ * Ensure that the unsanitized value (the "POST data") is preserved in the autosave revision.
1141+ * The value is sent through the sanitize function when it is read from the changeset.
1142+ */
1143+ $ autosave_revision = wp_get_post_autosave ( $ wp_customize ->changeset_post_id (), get_current_user_id () );
1144+ $ saved_data = json_decode ( $ autosave_revision ->post_content , true );
1145+ $ this ->assertEquals ( $ options ['custom_html_1 ' ], $ saved_data ['custom_html_1 ' ]['value ' ] );
1146+ $ this ->assertEquals ( $ options ['custom_html_2 ' ], $ saved_data ['custom_html_2 ' ]['value ' ] );
1147+
1148+ // Update post to discard autosave.
1149+ $ wp_customize ->save_changeset_post (
1150+ array (
1151+ 'status ' => 'draft ' ,
1152+ )
1153+ );
1154+
1155+ /*
1156+ * Ensure that the unsanitized value (the "POST data") is preserved in the post content.
1157+ * The value is sent through the sanitize function when it is read from the changeset.
1158+ */
1159+ $ wp_customize = $ this ->get_manager_for_testing_json_corruption_protection ( $ uuid );
1160+ $ saved_data = json_decode ( get_post ( $ wp_customize ->changeset_post_id () )->post_content , true );
1161+ $ this ->assertEquals ( $ options ['custom_html_1 ' ], $ saved_data ['custom_html_1 ' ]['value ' ] );
1162+ $ this ->assertEquals ( $ options ['custom_html_2 ' ], $ saved_data ['custom_html_2 ' ]['value ' ] );
1163+
1164+ /*
1165+ * Ensure that the unsanitized value (the "POST data") is preserved in the revisions' content.
1166+ * The value is sent through the sanitize function when it is read from the changeset.
1167+ */
1168+ $ revisions = wp_get_post_revisions ( $ wp_customize ->changeset_post_id () );
1169+ $ revision = array_shift ( $ revisions );
1170+ $ saved_data = json_decode ( $ revision ->post_content , true );
1171+ $ this ->assertEquals ( $ options ['custom_html_1 ' ], $ saved_data ['custom_html_1 ' ]['value ' ] );
1172+ $ this ->assertEquals ( $ options ['custom_html_2 ' ], $ saved_data ['custom_html_2 ' ]['value ' ] );
1173+
1174+ /*
1175+ * Now when publishing the changeset, the unsanitized values will be read from the changeset
1176+ * and sanitized according to the capabilities of the users who originally updated each
1177+ * setting in the changeset to begin with.
1178+ */
1179+ wp_set_current_user ( $ lesser_admin_user_id );
1180+ $ wp_customize = $ this ->get_manager_for_testing_json_corruption_protection ( $ uuid );
1181+ $ wp_customize ->set_post_value ( 'custom_html_3 ' , $ options ['custom_html_3 ' ] );
1182+ $ wp_customize ->save_changeset_post (
1183+ array (
1184+ 'status ' => 'publish ' ,
1185+ )
1186+ );
1187+
1188+ // User saved as one who can bypass content_save_pre filter.
1189+ $ this ->assertContains ( '<script> ' , get_option ( 'custom_html_1 ' ) );
1190+ $ this ->assertContains ( 'Wordpress ' , get_option ( 'custom_html_1 ' ) ); // phpcs:ignore WordPress.WP.CapitalPDangit.Misspelled
1191+
1192+ // User saved as one who cannot bypass content_save_pre filter.
1193+ $ this ->assertNotContains ( '<script> ' , get_option ( 'custom_html_2 ' ) );
1194+ $ this ->assertContains ( 'WordPress ' , get_option ( 'custom_html_2 ' ) );
1195+
1196+ // User saved as one who also cannot bypass content_save_pre filter.
1197+ $ this ->assertNotContains ( '<script> ' , get_option ( 'custom_html_3 ' ) );
1198+ $ this ->assertContains ( 'WordPress ' , get_option ( 'custom_html_3 ' ) );
1199+ }
1200+
1201+ /**
1202+ * Get a manager for testing JSON corruption protection.
1203+ *
1204+ * @param string $uuid UUID.
1205+ * @return WP_Customize_Manager Manager.
1206+ */
1207+ private function get_manager_for_testing_json_corruption_protection ( $ uuid ) {
1208+ global $ wp_customize ;
1209+ $ wp_customize = new WP_Customize_Manager (
1210+ array (
1211+ 'changeset_uuid ' => $ uuid ,
1212+ )
1213+ );
1214+ for ( $ i = 0 ; $ i < 5 ; $ i ++ ) {
1215+ $ wp_customize ->add_setting (
1216+ sprintf ( 'custom_html_%d ' , $ i ),
1217+ array (
1218+ 'type ' => 'option ' ,
1219+ 'sanitize_callback ' => array ( $ this , 'apply_content_save_pre_filters_if_not_main_admin_user ' ),
1220+ )
1221+ );
1222+ }
1223+ return $ wp_customize ;
1224+ }
1225+
1226+ /**
1227+ * Sanitize content with Kses if the current user is not the main admin.
1228+ *
1229+ * @since 5.4.1
1230+ *
1231+ * @param string $content Content to sanitize.
1232+ * @return string Sanitized content.
1233+ */
1234+ public function apply_content_save_pre_filters_if_not_main_admin_user ( $ content ) {
1235+ if ( get_current_user_id () !== self ::$ admin_user_id ) {
1236+ $ content = apply_filters ( 'content_save_pre ' , $ content );
1237+ }
1238+ return $ content ;
1239+ }
1240+
1241+ /**
1242+ * Filter map_meta_cap to disallow unfiltered_html.
1243+ *
1244+ * @since 5.4.1
1245+ *
1246+ * @param array $caps User's capabilities.
1247+ * @param string $cap Requested cap.
1248+ * @return array Caps.
1249+ */
1250+ public function filter_map_meta_cap_to_disallow_unfiltered_html ( $ caps , $ cap ) {
1251+ if ( 'unfiltered_html ' === $ cap ) {
1252+ $ caps = array ( 'do_not_allow ' );
1253+ }
1254+ return $ caps ;
1255+ }
1256+
10921257 /**
10931258 * Call count for customize_changeset_save_data filter.
10941259 *
0 commit comments