Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php
/**
* REST API: WP_REST_Media_Search_Handler class
*
* @package WordPress
* @subpackage REST_API
Comment on lines +2 to +6

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replicated the intention of this PR and gave credit to the author in case this one gets merged first

* @since 6.6.0
* */

if ( class_exists( 'WP_REST_Media_Search_Handler' ) ) {
return;
}

/**
* Core class representing a search handler for attachments in the REST API.
*
* @since 6.6.0
*
* @see WP_REST_Search_Handler
*/
class WP_REST_Media_Search_Handler_Gutenberg_6_6 extends WP_REST_Post_Search_Handler {

/**
* Constructor.
*
* @since 6.6.0
*/
public function __construct() {
parent::__construct();
$this->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;
}
}
118 changes: 118 additions & 0 deletions lib/compat/wordpress-6.6/rest-api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php
/**
* PHP and WordPress configuration compatibility functions for the Gutenberg
* editor plugin changes related to REST API.
*
* @package gutenberg
*/

if ( ! defined( 'ABSPATH' ) ) {
die( 'Silence is golden.' );
}

/**
* Registers additional fields for search result rest api.
*
* @access private* @internal
*
* @param array $result_object Search result object.
* @param string $field_name Current additional field's name (unused).
* @param \WP_REST_Request $request Rest request object.
* @return string Thumbnail for the result object.
*/
function _gutenberg_get_search_result_thumbnail_field( $result_object ) {

$object_id = $result_object['id'];
if ( empty( $object_id ) ) {
return '';
}

$thumbnail = wp_get_attachment_image_src( $object_id, 'thumbnail' );

return $thumbnail[0] ? $thumbnail[0] : '';
}

/**
* Registers additional fields for search result rest api.
*
* @access private* @internal
*
* @param array $result_object Search result object.
* @param string $field_name Current additional field's name (unused).
* @param \WP_REST_Request $request Rest request object.
* @return string Alt text for the result object.
*/
function _gutenberg_get_search_result_alt_text_field( $result_object ) {

$object_id = $result_object['id'];
if ( empty( $object_id ) ) {
return '';
}

$alt_text = get_post_meta( $object_id, '_wp_attachment_image_alt', true );

return $alt_text;
}

/**
* Registers additional fields for search result rest api.
*
* @access private
* @internal
*/
function _gutenberg_register_search_result_additional_fields() {
global $wp_rest_additional_fields;

if ( isset( $wp_rest_additional_fields['search-result']['thumbnail'] ) ) {
return;
}

register_rest_field(
'search-result',
'thumbnail',
array(
'get_callback' => '_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' );
4 changes: 4 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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 );
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
*/
import classnames from 'classnames';

/**
* Internal dependencies
*/
import isImageUrl from './is-url-image';

/**
* WordPress dependencies
*/
Expand Down Expand Up @@ -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 = <img src={ richData?.icon } alt="" />;
} else if ( attachmentImg ) {
//TODO we don't have an alt text for this image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An empty alt tag is acceptable when there is no way to provide an accurate description

icon = (
<img
className="block-editor-link-control__search-item-media-thumbnail"
src={ attachmentImg }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this loads the original sized image, it feels a little overkill

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both this and a thumbnail for the image are on coreStore, but the component shouldn't have to know about core-data. Should we pass that extra info to all instances of the link preview, or should we leave this as is?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How big is the image? If it's huge then I don't think this is acceptable.

I did have a plan to use the value.id to fetch the full data about the entity from the Core Store. In fact I did have a PR about precisely that but I cannot locate it.

This selector would need to be passed in via blockEditorSettings because - as you noted - we cannot consume Core Store directly here.

If we do this I would consider whether it's better to do that bit as a separate PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#50998 this one perhaps?

alt=""
/>
);
} else if ( isEmptyURL ) {
icon = <Icon icon={ info } size={ 32 } />;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -43,7 +50,17 @@ function SearchItemIcon( { isURL, suggestion } ) {
}
}

if ( icon ) {
if ( imageURL ) {
return (
<span className="block-editor-link-control__search-item-icon">
<img
className="block-editor-link-control__search-item-media-thumbnail"
src={ imageURL }
alt={ altText }
/>
</span>
);
} else if ( icon ) {
return (
<Icon
className="block-editor-link-control__search-item-icon"
Expand Down
14 changes: 14 additions & 0 deletions packages/block-editor/src/components/link-control/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,20 @@ $block-editor-link-control-number-of-actions: 1;
img {
width: $grid-unit-20; // favicons often have a source of 32px
}

.block-editor-link-control__search-item-media-thumbnail {
width: $grid-unit-40;
height: $grid-unit-40;
object-fit: cover;
border-radius: $radius-block-ui;

.block-editor-link-control__search-results & {
width: $grid-unit-20;
height: $grid-unit-20;
margin: $grid-unit-05;
}

}
}

&.is-error .block-editor-link-control__search-item-icon {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Internal dependencies
*/
import isImageUrl from '../is-url-image';

describe( 'isImageUrl', () => {
// 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 );
}
);
} );
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) => {
Expand Down Expand Up @@ -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,
};
} );
} );
Expand Down