diff --git a/lib/compat/wordpress-6.6/class-wp-rest-media-search-handler-gutenberg-6-6.php b/lib/compat/wordpress-6.6/class-wp-rest-media-search-handler-gutenberg-6-6.php new file mode 100644 index 00000000000000..53b4dc02ecbc16 --- /dev/null +++ b/lib/compat/wordpress-6.6/class-wp-rest-media-search-handler-gutenberg-6-6.php @@ -0,0 +1,110 @@ +type = 'media'; + $this->subtypes = array(); + } + + /** + * Searches the object type content for a given search request. + * + * @since 6.6.0 + * + * @param WP_REST_Request $request Full REST request. + * @return array Associative array containing an `WP_REST_Search_Handler::RESULT_IDS` containing + * an array of found IDs and `WP_REST_Search_Handler::RESULT_TOTAL` containing the + * total count for the matching search results. + */ + public function search_items( WP_REST_Request $request ) { + + $query_args = array( + 'post_type' => 'attachment', + 'post_status' => 'inherit', + 'paged' => (int) $request['page'], + 'posts_per_page' => (int) $request['per_page'], + ); + + if ( ! empty( $request['search'] ) ) { + $query_args['s'] = $request['search']; + + // Filter query clauses to include filenames. + add_filter( 'wp_allow_query_attachment_by_filename', '__return_true' ); + } + + if ( ! empty( $request['exclude'] ) ) { + $query_args['post__not_in'] = $request['exclude']; + } + + if ( ! empty( $request['include'] ) ) { + $query_args['post__in'] = $request['include']; + } + + /** + * Filters the query arguments for a REST API search request. + * + * Enables adding extra arguments or setting defaults for a media search request. + * + * @since 6.6.0 + * + * @param array $query_args Key value array of query var to query value. + * @param WP_REST_Request $request The request used. + */ + $query_args = apply_filters( 'rest_media_search_query', $query_args, $request ); + + $query = new WP_Query(); + $posts = $query->query( $query_args ); + // Querying the whole post object will warm the object cache, avoiding an extra query per result. + $found_ids = wp_list_pluck( $posts, 'ID' ); + $total = $query->found_posts; + + return array( + self::RESULT_IDS => $found_ids, + self::RESULT_TOTAL => $total, + ); + } + + /** + * Prepares the search result for a given ID. + * + * @since 6.6.0 + * + * @param int $id Item ID. + * @param array $fields Fields to include for the item. + * @return array Associative array containing all fields for the item. + */ + public function prepare_item( $id, array $fields ) { + $data = parent::prepare_item( $id, $fields ); + + if ( isset( $data[ WP_REST_Search_Controller::PROP_SUBTYPE ] ) ) { + unset( $data[ WP_REST_Search_Controller::PROP_SUBTYPE ] ); + } + + return $data; + } +} diff --git a/lib/compat/wordpress-6.6/rest-api.php b/lib/compat/wordpress-6.6/rest-api.php new file mode 100644 index 00000000000000..24040fde2762ed --- /dev/null +++ b/lib/compat/wordpress-6.6/rest-api.php @@ -0,0 +1,118 @@ + '_gutenberg_get_search_result_thumbnail_field', + 'update_callback' => null, + 'schema' => array( + 'description' => __( 'Object human readable subtype.', 'gutenberg' ), + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'embed' ), + ), + ) + ); + + register_rest_field( + 'search-result', + 'alt_text', + array( + 'get_callback' => '_gutenberg_get_search_result_alt_text_field', + 'update_callback' => null, + 'schema' => array( + 'description' => __( 'Object human readable subtype.', 'gutenberg' ), + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'embed' ), + ), + ) + ); +} + +add_action( 'rest_api_init', '_gutenberg_register_search_result_additional_fields' ); + +/** + * Register custom rest media search handler. + * + * @param array $handlers + * + * @return array + */ +function _gutenberg_register_media_search_handler( $handlers ) { + if ( class_exists( 'WP_REST_Media_Search_Handler_Gutenberg_6_6' ) ) { + $handlers[] = new WP_REST_Media_Search_Handler_Gutenberg_6_6(); + } + + return $handlers; +} + +add_action( 'wp_rest_search_handlers', '_gutenberg_register_media_search_handler' ); diff --git a/lib/load.php b/lib/load.php index 3d9213ce6e93a9..363e74f60db93f 100644 --- a/lib/load.php +++ b/lib/load.php @@ -45,6 +45,10 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/compat/wordpress-6.5/class-gutenberg-rest-global-styles-revisions-controller-6-5.php'; require_once __DIR__ . '/compat/wordpress-6.5/rest-api.php'; + // WordPress 6.6 compat. + require_once __DIR__ . '/compat/wordpress-6.6/class-wp-rest-media-search-handler-gutenberg-6-6.php'; + require_once __DIR__ . '/compat/wordpress-6.6/rest-api.php'; + // Plugin specific code. require_once __DIR__ . '/class-wp-rest-global-styles-controller-gutenberg.php'; require_once __DIR__ . '/rest-api.php'; diff --git a/packages/block-editor/src/components/link-control/is-url-image.js b/packages/block-editor/src/components/link-control/is-url-image.js new file mode 100644 index 00000000000000..34c05396633121 --- /dev/null +++ b/packages/block-editor/src/components/link-control/is-url-image.js @@ -0,0 +1,10 @@ +/** + * Checks if the url is an image. + * @param {string} url the url we are checking. + * @return {boolean} true if the url is an image url. + */ +export default function isImageUrl( url ) { + const pattern = + /^(https?:\/\/)([\w-]+(?:\.[\w-]+)*\/)*[\w-]+\.(?:jpg|JPG|jpeg|gif|png|webp)$/i; + return pattern.test( url ); +} diff --git a/packages/block-editor/src/components/link-control/link-preview.js b/packages/block-editor/src/components/link-control/link-preview.js index 17cc851c503a14..c75aae8b078442 100644 --- a/packages/block-editor/src/components/link-control/link-preview.js +++ b/packages/block-editor/src/components/link-control/link-preview.js @@ -3,6 +3,11 @@ */ import classnames from 'classnames'; +/** + * Internal dependencies + */ +import isImageUrl from './is-url-image'; + /** * WordPress dependencies */ @@ -56,8 +61,22 @@ export default function LinkPreview( { let icon; + const attachmentImg = + value.type === 'attachment' && isImageUrl( value?.url ) + ? value.url + : null; + if ( richData?.icon ) { icon = ; + } else if ( attachmentImg ) { + //TODO we don't have an alt text for this image + icon = ( + + ); } else if ( isEmptyURL ) { icon = ; } else { diff --git a/packages/block-editor/src/components/link-control/search-item.js b/packages/block-editor/src/components/link-control/search-item.js index 7fa8ee1803644c..22ab57a5544297 100644 --- a/packages/block-editor/src/components/link-control/search-item.js +++ b/packages/block-editor/src/components/link-control/search-item.js @@ -28,6 +28,13 @@ const ICONS_MAP = { function SearchItemIcon( { isURL, suggestion } ) { let icon = null; + let imageURL = null; + let altText = ''; + + if ( suggestion.kind === 'media' ) { + imageURL = suggestion.thumbnail ? suggestion.thumbnail : null; + altText = suggestion.alt_text; + } if ( isURL ) { icon = globe; @@ -43,7 +50,17 @@ function SearchItemIcon( { isURL, suggestion } ) { } } - if ( icon ) { + if ( imageURL ) { + return ( + + { + + ); + } else if ( icon ) { return ( { + // use .each to test multiple cases + it.each( [ + [ true, 'https://example.com/image.jpg' ], + [ true, 'https://example.com/image.gif' ], + [ true, 'https://example.com/image.png' ], + [ true, 'https://example.com/image.webp' ], + [ true, 'https://example.com/image.jpeg' ], + [ true, 'https://example.com/image.JPG' ], + [ false, 'https://example.com/image.txt' ], + [ false, 'https://example.com/image' ], + [ false, 'https://example.com/image.jpg?query=123' ], + [ false, '' ], + [ false, null ], + [ false, undefined ], + [ false, 123 ], + ] )( + 'returns %s when testing against URL "%s" for a valid image', + ( expected, testString ) => { + expect( isImageUrl( testString ) ).toBe( expected ); + } + ); +} ); diff --git a/packages/core-data/src/fetch/__experimental-fetch-link-suggestions.js b/packages/core-data/src/fetch/__experimental-fetch-link-suggestions.js index 3bba760b20c459..41a14f74531c3a 100644 --- a/packages/core-data/src/fetch/__experimental-fetch-link-suggestions.js +++ b/packages/core-data/src/fetch/__experimental-fetch-link-suggestions.js @@ -179,10 +179,11 @@ const fetchLinkSuggestions = async ( if ( ! type || type === 'attachment' ) { queries.push( apiFetch( { - path: addQueryArgs( '/wp/v2/media', { + path: addQueryArgs( '/wp/v2/search', { search, page, per_page: perPage, + type: 'media', } ), } ) .then( ( results ) => { @@ -229,6 +230,8 @@ const fetchLinkSuggestions = async ( ) || __( '(no title)' ), type: result.subtype || result.type, kind: result?.meta?.kind, + thumbnail: result?.thumbnail, + alt_text: result?.alt_text, }; } ); } );