diff --git a/environments/plugin-directory/bin/after-start.sh b/environments/plugin-directory/bin/after-start.sh index f5a0a9c0fc..872d1f8e40 100755 --- a/environments/plugin-directory/bin/after-start.sh +++ b/environments/plugin-directory/bin/after-start.sh @@ -22,18 +22,37 @@ $WPENV cli sudo sh -c \ # Set up permalinks. $WP wp rewrite structure '/%postname%/' --hard -# Create pages that exist on wordpress.org/plugins (if they don't already exist). +# Create pages that exist on wordpress.org/plugins. +# +# Idempotent: match by slug and update, otherwise create. after-start.sh runs on +# every `wp-env start`, and a plain `wp post create` would append -2, -3, … to the +# slug each time, leaving the canonical slug pointing at a stale, empty page. echo "Creating pages..." + +# ensure_page
' . $block->name . '',
- $query->posts[0]->post_title
+ '' . esc_html( $block->name ) . '',
+ esc_html( $query->posts[0]->post_title )
),
[ 'block_name' => $block->name, 'slug' => $post->post_name ]
);
@@ -621,7 +624,7 @@ function check_for_standard_block_name() {
__FUNCTION__,
'error',
// translators: %s is the block name.
- sprintf( __( 'Block name %s is invalid. Please use lowercase alphanumeric characters.', 'wporg-plugins' ), '' . $block->name . '' )
+ sprintf( __( 'Block name %s is invalid. Please use lowercase alphanumeric characters.', 'wporg-plugins' ), '' . esc_html( $block->name ) . '' )
);
} else {
$disallowed_ns = array( 'cgb/', 'create-block/', 'example/', 'block/', 'core/', 'gutenberg-examples/' );
@@ -633,8 +636,8 @@ function check_for_standard_block_name() {
sprintf(
// translators: %1$s is the block name, %2$s is the namespace.
__( 'Block %1$s uses namespace %2$s. Please use a unique namespace.', 'wporg-plugins' ),
- '' . $block->name . '',
- '' . $ns . ''
+ '' . esc_html( $block->name ) . '',
+ '' . esc_html( $ns ) . ''
)
);
break;
@@ -665,7 +668,7 @@ function check_for_multiple_namespaces() {
// translators: %1$d is the number of namespaces, %2$s is the list of namespaces.
__( 'Found blocks with %1$d different namespaces: %2$s.', 'wporg-plugins' ),
count( $namespaces ),
- '' . implode( ', ', $namespaces ) . ''
+ '' . esc_html( implode( ', ', $namespaces ) ) . ''
),
$namespaces
);
@@ -732,7 +735,7 @@ function check_for_unique_namespace() {
'error',
// translators: %1$s is the namespace, %2$s is a link to the plugin.
sprintf( __( 'Please use a unique block namespace. Namespace %1$s is already used by %2$s.' ),
- '' . $this->get_namespace( $block ) . '',
+ '' . esc_html( $this->get_namespace( $block ) ) . '',
'' . esc_html( $post->post_title ) . ''
)
);
@@ -745,7 +748,7 @@ function check_for_unique_namespace() {
'warning',
// translators: %1$s is the namespace, %2$s is a link to the plugin.
sprintf( __( 'Please use a unique block namespace. Namespace %1$s is already used by %2$s.' ),
- '' . $this->get_namespace( $block ) . '',
+ '' . esc_html( $this->get_namespace( $block ) ) . '',
'' . esc_html( $post->post_title ) . ''
)
);
@@ -793,7 +796,7 @@ function check_for_block_json() {
__FUNCTION__,
'info',
// translators: %s is the block name.
- sprintf( __( 'Found a block.json file for block %s.', 'wporg-plugins' ), '' . $block_name . '' ),
+ sprintf( __( 'Found a block.json file for block %s.', 'wporg-plugins' ), '' . esc_html( $block_name ) . '' ),
$this->block_json_files[ $block_name ]
);
}
@@ -834,7 +837,7 @@ function( $block ) {
sprintf(
/* translators: %s is a list of block names. */
__( 'More than one top-level block was found: %s', 'wporg-plugins' ),
- implode( ', ', $list )
+ esc_html( implode( ', ', $list ) )
)
);
}
@@ -852,7 +855,7 @@ function check_for_block_script_files() {
__FUNCTION__,
'info',
// translators: %s is the block name.
- sprintf( __( 'Found file %s.', 'wporg-plugins' ), '' . $script . '' ),
+ sprintf( __( 'Found file %s.', 'wporg-plugins' ), '' . esc_html( $script ) . '' ),
compact( 'kind', 'script' )
);
} else {
@@ -868,7 +871,7 @@ function check_for_block_script_files() {
$this->record_result(
__FUNCTION__,
'error',
- sprintf( $message, '' . $script . '' ),
+ sprintf( $message, '' . esc_html( $script ) . '' ),
compact( 'kind', 'script' )
);
}
@@ -912,8 +915,8 @@ function check_block_json_is_valid_json() {
sprintf(
// translators: %1$s is the file name, %2$s is the json error message.
__( 'Error attempting to parse json in %1$s: %2$s', 'wporg-plugins' ),
- '' . $this->relative_filename( $block_json_file ) . '',
- $message
+ '' . esc_html( $this->relative_filename( $block_json_file ) ) . '',
+ esc_html( $message )
),
$this->relative_filename( $block_json_file )
);
@@ -932,7 +935,7 @@ function check_block_json_is_valid() {
__FUNCTION__,
'info',
// translators: %s is the file name.
- sprintf( __( 'JSON file %s is valid.', 'wporg-plugins' ), '' . $this->relative_filename( $block_json_file ) . '' ),
+ sprintf( __( 'JSON file %s is valid.', 'wporg-plugins' ), '' . esc_html( $this->relative_filename( $block_json_file ) ) . '' ),
$this->relative_filename( $block_json_file )
);
continue;
@@ -947,7 +950,7 @@ function check_block_json_is_valid() {
$this->record_result(
__FUNCTION__,
( 'error' === $code ? 'warning' : $code ), // TODO: be smarter about mapping these
- '' . $this->relative_filename( $block_json_file ) . ': ' . $message,
+ '' . esc_html( $this->relative_filename( $block_json_file ) ) . ': ' . wp_kses( $message, array( 'code' => array() ) ),
array(
$this->relative_filename( $block_json_file ),
$result->get_error_data( $code ),
@@ -1073,7 +1076,7 @@ function check_php_function_calls() {
sprintf(
// translators: %s is the function name.
__( 'Found PHP call %s. This may cause problems.', 'wporg-plugins' ),
- '' . $call[0] . '()'
+ '' . esc_html( $call[0] ) . '()'
),
$call
);
@@ -1084,7 +1087,7 @@ function check_php_function_calls() {
sprintf(
// translators: %s is the function name.
__( 'Found PHP call %s. This is likely to prevent your plugin from working as expected.', 'wporg-plugins' ),
- '' . $call[0] . '()'
+ '' . esc_html( $call[0] ) . '()'
),
$call
);
diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/readme/class-parser.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/readme/class-parser.php
index b43dc2d0ed..d886f322d8 100644
--- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/readme/class-parser.php
+++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/readme/class-parser.php
@@ -373,7 +373,7 @@ protected function parse_readme_contents( $contents ) {
$headers['license'] = trim( str_replace( $url[1], '', $headers['license'] ), " -*\t\n\r\n()<>" );
}
- $this->license = $headers['license'];
+ $this->license = $this->sanitize_text( $headers['license'] );
}
if ( ! empty( $headers['license_uri'] ) ) {
$this->license_uri = $headers['license_uri'];
diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/readme/class-validator.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/readme/class-validator.php
index f7c50e8ea0..db422881ae 100644
--- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/readme/class-validator.php
+++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/readme/class-validator.php
@@ -359,7 +359,7 @@ public function translate_code_to_message( $error_code, $data = false ) {
return sprintf(
/* translators: %s: list of tags not supported */
__( 'One or more tags were ignored. The following tags are not permitted: %s', 'wporg-plugins' ),
- '' . implode( ', ', $data ) . ''
+ '' . implode( ', ', array_map( 'esc_html', $data ) ) . ''
);
case 'low_usage_tags':
return sprintf(
diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-block-validator.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-block-validator.php
index 4dd7226c52..bb05e447d5 100644
--- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-block-validator.php
+++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-block-validator.php
@@ -336,19 +336,20 @@ protected static function display_results( $checker ) {
$output .= "{$labels['description']}
\n"; } $output .= "' . implode( '
', (array) $details ) . '
'; - $output .= "{$item->message}
"; + $output .= "{$message}
"; } } // Collapse block.json warnings into one details at the end of warnings list. if ( 'error' === $type && ! empty( $block_json_issues ) ) { - $messages = wp_list_pluck( $block_json_issues, 'message' ); + $messages = array_map( array( __CLASS__, 'sanitize_message' ), wp_list_pluck( $block_json_issues, 'message' ) ); $details = '' . implode( '
', (array) $messages ) . '
'; $output .= sprintf( ', so the name must be escaped.
+ */
+ public function test_invalid_block_name_errors_and_is_escaped() {
+ $checker = $this->checker( array( 'blocks' => array( (object) array( 'name' => 'sample/
' ) ) ) );
+ $checker->check_for_standard_block_name();
+
+ $messages = $this->messages( $checker, 'error', 'check_for_standard_block_name' );
+ $this->assertCount( 1, $messages );
+ $this->assertStringNotContainsString( '
assertStringContainsString( '', $messages[0] );
+ }
+
+ /**
+ * Reserved namespaces from block templates (core/, create-block/, …) are rejected.
+ */
+ public function test_reserved_namespace_errors() {
+ $checker = $this->checker( array( 'blocks' => array( (object) array( 'name' => 'core/my-block' ) ) ) );
+ $checker->check_for_standard_block_name();
+
+ $this->assertCount( 1, $checker->get_results( 'error', 'check_for_standard_block_name' ) );
+ }
+
+ /*
+ * Tests for check_for_multiple_namespaces.
+ */
+
+ /**
+ * Blocks sharing one namespace are fine.
+ */
+ public function test_single_namespace_passes() {
+ $checker = $this->checker(
+ array(
+ 'blocks' => array(
+ (object) array( 'name' => 'sample/one' ),
+ (object) array( 'name' => 'sample/two' ),
+ ),
+ )
+ );
+ $checker->check_for_multiple_namespaces();
+
+ $this->assertEmpty( $checker->get_results( 'error', 'check_for_multiple_namespaces' ) );
+ }
+
+ /**
+ * Blocks spread across namespaces suggest an unrelated suite bundled together.
+ */
+ public function test_multiple_namespaces_error() {
+ $checker = $this->checker(
+ array(
+ 'blocks' => array(
+ (object) array( 'name' => 'sample/one' ),
+ (object) array( 'name' => 'other/two' ),
+ ),
+ )
+ );
+ $checker->check_for_multiple_namespaces();
+
+ $this->assertCount( 1, $checker->get_results( 'error', 'check_for_multiple_namespaces' ) );
+ }
+
+ /*
+ * Tests for check_for_block_json.
+ */
+
+ /**
+ * A block backed by a block.json file is noted.
+ */
+ public function test_block_with_block_json_is_noted() {
+ $checker = $this->checker(
+ array(
+ 'blocks' => array( 'sample/main' => (object) array( 'name' => 'sample/main' ) ),
+ 'block_json_files' => array( 'sample/main' => '/tmp/plugin/block.json' ),
+ )
+ );
+ $checker->check_for_block_json();
+
+ $this->assertCount( 1, $checker->get_results( 'info', 'check_for_block_json' ) );
+ }
+
+ /**
+ * No block.json files anywhere is a warning.
+ */
+ public function test_block_without_block_json_warns() {
+ $checker = $this->checker(
+ array(
+ 'blocks' => array( 'sample/main' => (object) array( 'name' => 'sample/main' ) ),
+ 'block_json_files' => array(),
+ )
+ );
+ $checker->check_for_block_json();
+
+ $this->assertCount( 1, $checker->get_results( 'warning', 'check_for_block_json' ) );
+ }
+
+ /*
+ * Tests for check_for_single_parent.
+ */
+
+ /**
+ * One top-level block (others declaring it as parent) is the expected shape.
+ */
+ public function test_single_top_level_block_passes() {
+ $checker = $this->checker(
+ array(
+ 'blocks' => array(
+ 'sample/list' => (object) array( 'name' => 'sample/list' ),
+ 'sample/list-item' => (object) array(
+ 'name' => 'sample/list-item',
+ 'parent' => array( 'sample/list' ),
+ ),
+ ),
+ )
+ );
+ $checker->check_for_single_parent();
+
+ $this->assertEmpty( $checker->get_results( 'warning', 'check_for_single_parent' ) );
+ }
+
+ /**
+ * Two independent top-level blocks earns a warning.
+ */
+ public function test_multiple_top_level_blocks_warn() {
+ $checker = $this->checker(
+ array(
+ 'blocks' => array(
+ 'sample/one' => (object) array( 'name' => 'sample/one' ),
+ 'sample/two' => (object) array( 'name' => 'sample/two' ),
+ ),
+ )
+ );
+ $checker->check_for_single_parent();
+
+ $this->assertCount( 1, $checker->get_results( 'warning', 'check_for_single_parent' ) );
+ }
+
+ /*
+ * Tests for check_block_json_is_valid and check_block_json_is_valid_json.
+ */
+
+ /**
+ * A block.json that validates is reported as valid.
+ */
+ public function test_valid_block_json_is_noted() {
+ $checker = $this->checker(
+ array(
+ 'path_to_plugin' => '/tmp/plugin',
+ 'block_json_validation' => array( '/tmp/plugin/block.json' => true ),
+ )
+ );
+ $checker->check_block_json_is_valid();
+
+ $this->assertCount( 1, $checker->get_results( 'info', 'check_block_json_is_valid' ) );
+ }
+
+ /**
+ * The file path is built into a Trac link. A quote in the path would otherwise
+ * close the href attribute and turn the rest of the path into live markup.
+ */
+ public function test_block_json_error_path_cannot_break_out_of_href() {
+ $bad_path = '/tmp/plugin/report" onmouseover=alert(1) x="/block.json';
+ $checker = $this->checker(
+ array(
+ 'path_to_plugin' => '/tmp/plugin',
+ 'repo_url' => 'https://plugins.svn.wordpress.org/sample/trunk',
+ 'block_json_validation' => array( $bad_path => new WP_Error( 'error', 'block.json[name] is required.' ) ),
+ )
+ );
+ $checker->check_block_json_is_valid();
+
+ $messages = $this->messages( $checker, 'warning', 'check_block_json_is_valid' );
+ $this->assertCount( 1, $messages );
+
+ /* The quote survives only as an entity, never as attribute syntax. */
+ $this->assertStringNotContainsString( '" onmouseover', $messages[0] );
+ $this->assertStringContainsString( '" onmouseover', $messages[0] );
+
+ /* The only quotes left are the two delimiting the href. */
+ $this->assertSame( 2, substr_count( $messages[0], '"' ) );
+ }
+
+ /**
+ * A JSON parse failure is surfaced as a fatal error.
+ */
+ public function test_block_json_parse_error() {
+ $error = new WP_Error( 'json_parse_error', 'Syntax error' );
+ $checker = $this->checker(
+ array(
+ 'path_to_plugin' => '/tmp/plugin',
+ 'block_json_validation' => array( '/tmp/plugin/block.json' => $error ),
+ )
+ );
+ $checker->check_block_json_is_valid_json();
+
+ $this->assertCount( 1, $checker->get_results( 'error', 'check_block_json_is_valid_json' ) );
+ }
+
+ /*
+ * Tests for check_php_function_calls.
+ */
+
+ /**
+ * Data provider for {@see test_php_function_calls_are_flagged()}.
+ *
+ * @return array
+ */
+ public static function php_call_provider(): array {
+ return array(
+ 'wp_add_inline_script' => array( 'wp_add_inline_script', 'warning' ),
+ 'wp_localize_script' => array( 'wp_localize_script', 'warning' ),
+ 'add_shortcode' => array( 'add_shortcode', 'error' ),
+ 'header' => array( 'header', 'error' ),
+ 'wp_redirect' => array( 'wp_redirect', 'error' ),
+ );
+ }
+
+ /**
+ * Certain PHP calls are flagged as warnings, others as errors.
+ *
+ * @param string $function_name Function name recorded from the plugin's PHP.
+ * @param string $expected Expected result type.
+ */
+ #[DataProvider( 'php_call_provider' )]
+ public function test_php_function_calls_are_flagged( string $function_name, string $expected ) {
+ $checker = $this->checker( array( 'php_function_calls' => array( array( $function_name, 5, 'plugin.php' ) ) ) );
+ $checker->check_php_function_calls();
+
+ $this->assertCount( 1, $checker->get_results( $expected, 'check_php_function_calls' ) );
+ }
+
+ /*
+ * Tests for the Block_Validator sanitize_message render-time backstop.
+ */
+
+ /**
+ * The backstop drops dangerous markup but keeps the formatting the checks emit.
+ */
+ public function test_sanitize_message_allows_only_intended_markup() {
+ $method = new ReflectionMethod( Block_Validator::class, 'sanitize_message' );
+ $method->setAccessible( true );
+
+ $dirty = 'block.json: '
+ . '
x';
+ $clean = $method->invoke( null, $dirty );
+
+ $this->assertStringNotContainsString( '
assertStringNotContainsString( '' ),
+ 'payload containing url' => array( 'License: GPLv2
' ),
+ 'unclosed tag' => array( 'License: GPLv2
assertStringStartsWith( 'GPLv2', $parser->license );
+ $this->assertStringNotContainsString( '<', $parser->license );
+ $this->assertStringNotContainsString( 'onerror', $parser->license );
+ }
}