Changeset 58729
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/interactivity-api/class-wp-interactivity-api.php
r58327 r58729 193 193 * 194 194 * @since 6.5.0 195 * 196 * @deprecated 6.7.0 Client data passing is handled by the {@see "script_module_data_{$module_id}"} filter. 195 197 */ 196 198 public function print_client_interactivity_data() { 199 _deprecated_function( __METHOD__, '6.7.0' ); 200 } 201 202 /** 203 * Set client-side interactivity data. 204 * 205 * Once in the browser, the state will be parsed and used to hydrate the client-side 206 * interactivity stores and the configuration will be available using a `getConfig` utility. 207 * 208 * @since 6.7.0 209 * 210 * @param array $data Data to filter. 211 * @return array Data for the Interactivity API script module. 212 */ 213 public function filter_script_module_interactivity_data( array $data ): array { 197 214 if ( empty( $this->state_data ) && empty( $this->config_data ) ) { 198 return; 199 } 200 201 $interactivity_data = array(); 215 return $data; 216 } 202 217 203 218 $config = array(); … … 208 223 } 209 224 if ( ! empty( $config ) ) { 210 $ interactivity_data['config'] = $config;225 $data['config'] = $config; 211 226 } 212 227 … … 218 233 } 219 234 if ( ! empty( $state ) ) { 220 $interactivity_data['state'] = $state; 221 } 222 223 if ( ! empty( $interactivity_data ) ) { 224 /* 225 * This data will be printed as JSON inside a script tag like this: 226 * <script type="application/json"></script> 227 * 228 * A script tag must be closed by a sequence beginning with `</`. It's impossible to 229 * close a script tag without using `<`. We ensure that `<` is escaped and `/` can 230 * remain unescaped, so `</script>` will be printed as `\u003C/script\u00E3`. 231 * 232 * - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E. 233 * - JSON_UNESCAPED_SLASHES: Don't escape /. 234 * 235 * If the page will use UTF-8 encoding, it's safe to print unescaped unicode: 236 * 237 * - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`). 238 * - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when 239 * JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was 240 * before PHP 7.1 without this constant. Available as of PHP 7.1.0. 241 * 242 * The JSON specification requires encoding in UTF-8, so if the generated HTML page 243 * is not encoded in UTF-8 then it's not safe to include those literals. They must 244 * be escaped to avoid encoding issues. 245 * 246 * @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements. 247 * @see https://www.php.net/manual/en/json.constants.php for details on these constants. 248 * @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing. 249 */ 250 $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS; 251 if ( ! is_utf8_charset() ) { 252 $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES; 253 } 254 255 wp_print_inline_script_tag( 256 wp_json_encode( 257 $interactivity_data, 258 $json_encode_flags 259 ), 260 array( 261 'type' => 'application/json', 262 'id' => 'wp-interactivity-data', 263 ) 264 ); 265 } 235 $data['state'] = $state; 236 } 237 238 return $data; 266 239 } 267 240 … … 330 303 * 331 304 * @since 6.5.0 305 * @since 6.7.0 Use the {@see "script_module_data_{$module_id}"} filter to pass client-side data. 332 306 */ 333 307 public function add_hooks() { 334 308 add_action( 'wp_enqueue_scripts', array( $this, 'register_script_modules' ) ); 335 add_action( 'wp_footer', array( $this, 'print_client_interactivity_data' ) );336 337 309 add_action( 'admin_enqueue_scripts', array( $this, 'register_script_modules' ) ); 338 add_action( 'admin_print_footer_scripts', array( $this, 'print_client_interactivity_data' ) ); 310 311 add_filter( 'script_module_data_@wordpress/interactivity', array( $this, 'filter_script_module_interactivity_data' ) ); 339 312 } 340 313 -
trunk/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php
r58594 r58729 185 185 186 186 /** 187 * Invokes the private `print_client_interactivity` method of188 * WP_Interactivity_API class.189 *190 * @return array|null The content of the JSON object printed on the client-side or null if nothings was printed.191 */192 private function print_client_interactivity_data() {193 $interactivity_data_markup = get_echo( array( $this->interactivity, 'print_client_interactivity_data' ) );194 preg_match( '/<script type="application\/json" id="wp-interactivity-data">.*?(\{.*\}).*?<\/script>/s', $interactivity_data_markup, $interactivity_data_string );195 return isset( $interactivity_data_string[1] ) ? json_decode( $interactivity_data_string[1], true ) : null;196 }197 198 /**199 * Tests that the initial state and config are correctly printed on the200 * client-side.201 *202 * @ticket 60356203 *204 * @covers ::state205 * @covers ::config206 * @covers ::print_client_interactivity_data207 */208 public function test_state_and_config_is_correctly_printed() {209 $this->interactivity->state( 'myPlugin', array( 'a' => 1 ) );210 $this->interactivity->state( 'otherPlugin', array( 'b' => 2 ) );211 $this->interactivity->config( 'myPlugin', array( 'a' => 1 ) );212 $this->interactivity->config( 'otherPlugin', array( 'b' => 2 ) );213 214 $result = $this->print_client_interactivity_data();215 216 $data = array(217 'myPlugin' => array( 'a' => 1 ),218 'otherPlugin' => array( 'b' => 2 ),219 );220 221 $this->assertSame(222 array(223 'config' => $data,224 'state' => $data,225 ),226 $result227 );228 }229 230 /**231 187 * Tests that the wp-interactivity-data script is not printed if both state 232 188 * and config are empty. 233 189 * 234 190 * @ticket 60356 191 * @ticket 61512 192 */ 193 public function test_state_and_config_dont_print_when_empty() { 194 $filter = $this->get_script_data_filter_result(); 195 196 $this->assertSame( array(), $filter->get_args()[0][0] ); 197 } 198 199 /** 200 * Test that the print_client_interactivity_data is deprecated and produces no output. 201 * 202 * @ticket 60356 203 * @ticket 61512 235 204 * 236 205 * @covers ::print_client_interactivity_data 237 */ 238 public function test_state_and_config_dont_print_when_empty() { 239 $result = $this->print_client_interactivity_data(); 240 $this->assertNull( $result ); 241 } 242 243 /** 244 * Tests that the config is not printed if it's empty. 245 * 246 * @ticket 60356 247 * 248 * @covers ::state 249 * @covers ::print_client_interactivity_data 206 * 207 * @expectedDeprecated WP_Interactivity_API::print_client_interactivity_data 250 208 */ 251 209 public function test_config_not_printed_when_empty() { 252 $this->interactivity->state( 'myPlugin', array( 'a' => 1 ) ); 253 $result = $this->print_client_interactivity_data(); 254 $this->assertSame( array( 'state' => array( 'myPlugin' => array( 'a' => 1 ) ) ), $result ); 255 } 256 257 /** 258 * Tests that the state is not printed if it's empty. 259 * 260 * @ticket 60356 261 * 262 * @covers ::config 263 * @covers ::print_client_interactivity_data 210 $this->interactivity->print_client_interactivity_data(); 211 $this->expectOutputString( '' ); 212 } 213 214 /** 215 * Sets up an activity, runs an optional callback, and returns a MockAction for inspection. 216 * 217 * @since 6.7.0 218 * 219 * @param ?Closure $callback Optional. Callback to run to set up interactivity state and config. 220 * @return MockAction 221 */ 222 private function get_script_data_filter_result( ?Closure $callback = null ): MockAction { 223 $this->interactivity->add_hooks(); 224 $this->interactivity->register_script_modules(); 225 wp_enqueue_script_module( '@wordpress/interactivity' ); 226 $filter = new MockAction(); 227 add_filter( 'script_module_data_@wordpress/interactivity', array( $filter, 'filter' ) ); 228 229 if ( $callback ) { 230 $callback(); 231 } 232 233 ob_start(); 234 wp_script_modules()->print_script_module_data(); 235 ob_end_clean(); 236 237 return $filter; 238 } 239 240 /** 241 * Tests that the state is not included in client data if it's empty. 242 * 243 * @ticket 60356 244 * @ticket 61512 264 245 */ 265 246 public function test_state_not_printed_when_empty() { 266 $this->interactivity->config( 'myPlugin', array( 'a' => 1 ) ); 267 $result = $this->print_client_interactivity_data(); 268 $this->assertSame( array( 'config' => array( 'myPlugin' => array( 'a' => 1 ) ) ), $result ); 247 $filter = $this->get_script_data_filter_result( 248 function () { 249 $this->interactivity->config( 'myPlugin', array( 'a' => 1 ) ); 250 } 251 ); 252 253 $this->assertSame( array( 'config' => array( 'myPlugin' => array( 'a' => 1 ) ) ), $filter->get_args()[0][0] ); 269 254 } 270 255 … … 273 258 * 274 259 * @ticket 60761 275 * 276 * @covers ::print_client_interactivity_data 260 * @ticket 61512 277 261 */ 278 262 public function test_state_not_printed_when_empty_array() { 279 $this->interactivity->state( 'pluginWithEmptyState_prune', array() ); 280 $this->interactivity->state( 'pluginWithState_include', array( 'value' => 'excellent' ) ); 281 $printed_script = get_echo( array( $this->interactivity, 'print_client_interactivity_data' ) ); 282 $expected = <<<'SCRIPT_TAG' 283 <script type="application/json" id="wp-interactivity-data"> 284 {"state":{"pluginWithState_include":{"value":"excellent"}}} 285 </script> 286 287 SCRIPT_TAG; 288 289 $this->assertSameIgnoreEOL( $expected, $printed_script ); 263 $filter = $this->get_script_data_filter_result( 264 function () { 265 $this->interactivity->state( 'pluginWithEmptyState_prune', array() ); 266 $this->interactivity->state( 'pluginWithState_include', array( 'value' => 'excellent' ) ); 267 } 268 ); 269 270 $this->assertSame( array( 'state' => array( 'pluginWithState_include' => array( 'value' => 'excellent' ) ) ), $filter->get_args()[0][0] ); 290 271 } 291 272 … … 294 275 * 295 276 * @ticket 60761 296 * 297 * @covers ::print_client_interactivity_data 277 * @ticket 61512 298 278 */ 299 279 public function test_state_not_printed_when_only_empty_arrays() { 300 $this->interactivity->state( 'pluginWithEmptyState_prune', array() ); 301 $printed_script = get_echo( array( $this->interactivity, 'print_client_interactivity_data' ) ); 302 $this->assertSame( '', $printed_script ); 280 $filter = $this->get_script_data_filter_result( 281 function () { 282 $this->interactivity->state( 'pluginWithEmptyState_prune', array() ); 283 } 284 ); 285 286 $this->assertSame( array(), $filter->get_args()[0][0] ); 303 287 } 304 288 … … 307 291 * 308 292 * @ticket 60761 309 * 310 * @covers ::print_client_interactivity_data 293 * @ticket 61512 311 294 */ 312 295 public function test_state_printed_correctly_with_nested_empty_array() { 313 $this->interactivity->state( 'myPlugin', array( 'emptyArray' => array() ) ); 314 $printed_script = get_echo( array( $this->interactivity, 'print_client_interactivity_data' ) ); 315 $expected = <<<'SCRIPT_TAG' 316 <script type="application/json" id="wp-interactivity-data"> 317 {"state":{"myPlugin":{"emptyArray":[]}}} 318 </script> 319 320 SCRIPT_TAG; 321 322 $this->assertSameIgnoreEOL( $expected, $printed_script ); 296 $filter = $this->get_script_data_filter_result( 297 function () { 298 $this->interactivity->state( 'myPlugin', array( 'emptyArray' => array() ) ); 299 } 300 ); 301 302 $this->assertSame( array( 'state' => array( 'myPlugin' => array( 'emptyArray' => array() ) ) ), $filter->get_args()[0][0] ); 323 303 } 324 304 … … 327 307 * 328 308 * @ticket 60761 329 * 330 * @covers ::print_client_interactivity_data 309 * @ticket 61512 331 310 */ 332 311 public function test_config_not_printed_when_empty_array() { 333 $this->interactivity->config( 'pluginWithEmptyConfig_prune', array() ); 334 $this->interactivity->config( 'pluginWithConfig_include', array( 'value' => 'excellent' ) ); 335 $printed_script = get_echo( array( $this->interactivity, 'print_client_interactivity_data' ) ); 336 $expected = <<<'SCRIPT_TAG' 337 <script type="application/json" id="wp-interactivity-data"> 338 {"config":{"pluginWithConfig_include":{"value":"excellent"}}} 339 </script> 340 341 SCRIPT_TAG; 342 343 $this->assertSameIgnoreEOL( $expected, $printed_script ); 312 $filter = $this->get_script_data_filter_result( 313 function () { 314 $this->interactivity->config( 'pluginWithEmptyConfig_prune', array() ); 315 $this->interactivity->config( 'pluginWithConfig_include', array( 'value' => 'excellent' ) ); 316 } 317 ); 318 319 $this->assertSame( array( 'config' => array( 'pluginWithConfig_include' => array( 'value' => 'excellent' ) ) ), $filter->get_args()[0][0] ); 344 320 } 345 321 … … 348 324 * 349 325 * @ticket 60761 350 * 351 * @covers ::print_client_interactivity_data 326 * @ticket 61512 352 327 */ 353 328 public function test_config_not_printed_when_only_empty_arrays() { 354 $this->interactivity->config( 'pluginWithEmptyConfig_prune', array() ); 355 $printed_script = get_echo( array( $this->interactivity, 'print_client_interactivity_data' ) ); 356 $this->assertSame( '', $printed_script ); 329 $filter = $this->get_script_data_filter_result( 330 function () { 331 $this->interactivity->config( 'pluginWithEmptyConfig_prune', array() ); 332 } 333 ); 334 335 $this->assertSame( array(), $filter->get_args()[0][0] ); 357 336 } 358 337 … … 361 340 * 362 341 * @ticket 60761 363 * 364 * @covers ::print_client_interactivity_data 342 * @ticket 61512 365 343 */ 366 344 public function test_config_printed_correctly_with_nested_empty_array() { 367 $this->interactivity->config( 'myPlugin', array( 'emptyArray' => array() ) ); 368 $printed_script = get_echo( array( $this->interactivity, 'print_client_interactivity_data' ) ); 369 $expected = <<<'SCRIPT_TAG' 370 <script type="application/json" id="wp-interactivity-data"> 371 {"config":{"myPlugin":{"emptyArray":[]}}} 372 </script> 373 374 SCRIPT_TAG; 375 376 $this->assertSameIgnoreEOL( $expected, $printed_script ); 377 } 378 379 /** 380 * Tests that special characters in the initial state and configuration are 381 * properly escaped. 382 * 383 * @ticket 60356 384 * @ticket 61170 385 * 386 * @covers ::state 387 * @covers ::config 388 * @covers ::print_client_interactivity_data 389 */ 390 public function test_state_and_config_escape_special_characters() { 391 $this->interactivity->state( 392 'myPlugin', 393 array( 394 'ampersand' => '&', 395 'less-than sign' => '<', 396 'greater-than sign' => '>', 397 'solidus' => '/', 398 'line separator' => "\u{2028}", 399 'paragraph separator' => "\u{2029}", 400 'flag of england' => "\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}", 401 'malicious script closer' => '</script>', 402 'entity-encoded malicious script closer' => '</script>', 403 ) 404 ); 405 $this->interactivity->config( 'myPlugin', array( 'chars' => '&<>/' ) ); 406 407 $interactivity_data_markup = get_echo( array( $this->interactivity, 'print_client_interactivity_data' ) ); 408 preg_match( '~<script type="application/json" id="wp-interactivity-data">\s*(\{.*\})\s*</script>~s', $interactivity_data_markup, $interactivity_data_string ); 409 410 $expected = <<<"JSON" 411 {"config":{"myPlugin":{"chars":"&\\u003C\\u003E/"}},"state":{"myPlugin":{"ampersand":"&","less-than sign":"\\u003C","greater-than sign":"\\u003E","solidus":"/","line separator":"\u{2028}","paragraph separator":"\u{2029}","flag of england":"\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}","malicious script closer":"\\u003C/script\\u003E","entity-encoded malicious script closer":"</script>"}}} 412 JSON; 413 $this->assertSame( $expected, $interactivity_data_string[1] ); 414 } 415 416 /** 417 * Tests that special characters in the initial state and configuration are 418 * properly escaped when the blog_charset is not UTF-8 (unicode compatible). 419 * 420 * This this test, unicode and line terminators should be escaped to their 421 * JSON unicode sequences. 422 * 423 * @ticket 61170 424 * 425 * @covers ::state 426 * @covers ::config 427 * @covers ::print_client_interactivity_data 428 */ 429 public function test_state_and_config_escape_special_characters_non_utf8() { 430 add_filter( 'pre_option_blog_charset', array( $this, 'charset_iso_8859_1' ) ); 431 $this->interactivity->state( 432 'myPlugin', 433 array( 434 'ampersand' => '&', 435 'less-than sign' => '<', 436 'greater-than sign' => '>', 437 'solidus' => '/', 438 'line separator' => "\u{2028}", 439 'paragraph separator' => "\u{2029}", 440 'flag of england' => "\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}", 441 'malicious script closer' => '</script>', 442 'entity-encoded malicious script closer' => '</script>', 443 ) 444 ); 445 $this->interactivity->config( 'myPlugin', array( 'chars' => '&<>/' ) ); 446 447 $interactivity_data_markup = get_echo( array( $this->interactivity, 'print_client_interactivity_data' ) ); 448 preg_match( '~<script type="application/json" id="wp-interactivity-data">\s*(\{.*\})\s*</script>~s', $interactivity_data_markup, $interactivity_data_string ); 449 450 $expected = <<<"JSON" 451 {"config":{"myPlugin":{"chars":"&\\u003C\\u003E/"}},"state":{"myPlugin":{"ampersand":"&","less-than sign":"\\u003C","greater-than sign":"\\u003E","solidus":"/","line separator":"\\u2028","paragraph separator":"\\u2029","flag of england":"\\ud83c\\udff4\\udb40\\udc67\\udb40\\udc62\\udb40\\udc65\\udb40\\udc6e\\udb40\\udc67\\udb40\\udc7f","malicious script closer":"\\u003C/script\\u003E","entity-encoded malicious script closer":"</script>"}}} 452 JSON; 453 $this->assertSame( $expected, $interactivity_data_string[1] ); 345 $filter = $this->get_script_data_filter_result( 346 function () { 347 $this->interactivity->config( 'myPlugin', array( 'emptyArray' => array() ) ); 348 } 349 ); 350 351 $this->assertSame( array( 'config' => array( 'myPlugin' => array( 'emptyArray' => array() ) ) ), $filter->get_args()[0][0] ); 454 352 } 455 353
Note: See TracChangeset
for help on using the changeset viewer.