diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 8aada30959372..69f5fd469ca94 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -3882,13 +3882,29 @@ function wp_ajax_parse_media_shortcode() { $shortcode = wp_unslash( $_POST['shortcode'] ); + // Only process previews for media related shortcodes: + $found_shortcodes = get_shortcode_tags_in_content( $shortcode ); + $media_shortcodes = array( + 'audio', + 'embed', + 'playlist', + 'video', + 'gallery', + ); + + $other_shortcodes = array_diff( $found_shortcodes, $media_shortcodes ); + + if ( ! empty( $other_shortcodes ) ) { + wp_send_json_error(); + } + if ( ! empty( $_POST['post_ID'] ) ) { $post = get_post( (int) $_POST['post_ID'] ); } // The embed shortcode requires a post. if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) { - if ( 'embed' === $shortcode ) { + if ( in_array( 'embed', $found_shortcodes, true ) ) { wp_send_json_error(); } } else { diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index c383a8c58673e..078e3a2daab20 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -2607,6 +2607,7 @@ function gallery_shortcode( $attr ) { $attachments[ $val->ID ] = $_attachments[ $key ]; } } elseif ( ! empty( $atts['exclude'] ) ) { + $post_parent_id = $id; $attachments = get_children( array( 'post_parent' => $id, @@ -2619,6 +2620,7 @@ function gallery_shortcode( $attr ) { ) ); } else { + $post_parent_id = $id; $attachments = get_children( array( 'post_parent' => $id, @@ -2631,6 +2633,17 @@ function gallery_shortcode( $attr ) { ); } + if ( ! empty( $post_parent_id ) ) { + $post_parent = get_post( $post_parent_id ); + + // terminate the shortcode execution if user cannot read the post or password-protected + if ( + ( ! is_post_publicly_viewable( $post_parent->ID ) && ! current_user_can( 'read_post', $post_parent->ID ) ) + || post_password_required( $post_parent ) ) { + return ''; + } + } + if ( empty( $attachments ) ) { return ''; } @@ -2963,6 +2976,15 @@ function wp_playlist_shortcode( $attr ) { $attachments = get_children( $args ); } + if ( ! empty( $args['post_parent'] ) ) { + $post_parent = get_post( $id ); + + // terminate the shortcode execution if user cannot read the post or password-protected + if ( ! current_user_can( 'read_post', $post_parent->ID ) || post_password_required( $post_parent ) ) { + return ''; + } + } + if ( empty( $attachments ) ) { return ''; } diff --git a/src/wp-includes/shortcodes.php b/src/wp-includes/shortcodes.php index 538b6e79daca0..24df21d4da11e 100644 --- a/src/wp-includes/shortcodes.php +++ b/src/wp-includes/shortcodes.php @@ -168,6 +168,44 @@ function has_shortcode( $content, $tag ) { return false; } +/** + * Returns a list of registered shortcode names found in the given content. + * + * Example usage: + * + * get_shortcode_tags_in_content( '[audio src="file.mp3"][/audio] [foo] [gallery ids="1,2,3"]' ); + * // array( 'audio', 'gallery' ) + * + * @since 6.3.2 + * + * @param string $content The content to check. + * @return string[] An array of registered shortcode names found in the content. + */ +function get_shortcode_tags_in_content( $content ) { + if ( false === strpos( $content, '[' ) ) { + return array(); + } + + preg_match_all( '/' . get_shortcode_regex() . '/', $content, $matches, PREG_SET_ORDER ); + if ( empty( $matches ) ) { + return array(); + } + + $tags = array(); + foreach ( $matches as $shortcode ) { + $tags[] = $shortcode[2]; + + if ( ! empty( $shortcode[5] ) ) { + $deep_tags = get_shortcode_tags_in_content( $shortcode[5] ); + if ( ! empty( $deep_tags ) ) { + $tags = array_merge( $tags, $deep_tags ); + } + } + } + + return $tags; +} + /** * Searches content for shortcodes and filter shortcodes through their hooks. * diff --git a/tests/phpunit/tests/ajax/wpAjaxParseMediaShortcode.php b/tests/phpunit/tests/ajax/wpAjaxParseMediaShortcode.php new file mode 100755 index 0000000000000..19c7f20449dfb --- /dev/null +++ b/tests/phpunit/tests/ajax/wpAjaxParseMediaShortcode.php @@ -0,0 +1,85 @@ +attachment->create_object( + get_temp_dir() . 'canola.jpg', + 0, + array( + 'post_mime_type' => 'image/jpeg', + 'post_excerpt' => 'A sample caption', + 'post_name' => 'restapi-client-fixture-attachment', + 'post_title' => 'REST API Client Fixture: Attachment', + 'post_date' => '2017-02-14 00:00:00', + 'post_date_gmt' => '2017-02-14 00:00:00', + 'post_author' => 0, + ) + ); + } + /** + * @dataProvider shortcode_provider + */ + public function test_parse_shortcode( array $payload, $expected ) { + add_shortcode( 'test', array( $this, 'shortcode_test' ) ); + + $_POST = array_merge( + array( + 'action' => 'paser-media-shortcode', + 'type' => '', + ), + $payload + ); + // Make the request. + try { + $this->_handleAjax( 'parse-media-shortcode' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + // Get the response, it is in heartbeat's response. + $response = json_decode( $this->_last_response, true ); + $body = $response['data']['body'] ?? ''; + if ( $body ) { + $this->assertStringNotContainsString( self::SHORTCODE_RETURN_VALUE, $body ); + } + $this->assertSame( $expected['success'], $response['success'] ); + } + + public function shortcode_test() { + return self::SHORTCODE_RETURN_VALUE; + } + + public function shortcode_provider() { + return array( + 'gallery_shortcode_is_allowed' => array( + 'payload' => array( 'shortcode' => '[gallery ids=" ' . self::$media_id . '"]' ), + 'expected' => array( 'success' => true ), + ), + 'gallery_and_custom_test_shortcode_is_not_allowed' => array( + 'payload' => array( 'shortcode' => '[gallery ids=" ' . self::$media_id . '"] [test]' ), + 'expected' => array( 'success' => false ), + ), + 'custom_test_shortcode_is_not_allowed' => array( + 'payload' => array( 'shortcode' => '[test]' ), + 'expected' => array( 'success' => false ), + ), + ); + } +}