PluginProbe
Gutenberg / 10.1.0
Gutenberg v10.1.0
24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 All 403 releases
← All changes | lib/class-wp-rest-url-details-controller.php +27 -354 12.6.0 → 10.1.0 View file →
@@ -18,9 +18,9 @@
18 18 /**
19 19 * Constructs the controller.
20 20 */
21 21 public function __construct() {
22 - $this->namespace = 'wp-block-editor/v1';
22 + $this->namespace = '__experimental';
23 23 $this->rest_base = 'url-details';
24 24 }
25 25
26 26 /**
@@ -56,8 +56,9 @@
56 56 *
57 57 * @return array the schema.
58 58 */
59 59 public function get_item_schema() {
60 +
60 61 if ( $this->schema ) {
61 62 return $this->add_additional_fields_schema( $this->schema );
62 63 }
63 64
@@ -65,34 +66,14 @@
65 66 '$schema' => 'http://json-schema.org/draft-04/schema#',
66 67 'title' => 'url-details',
67 68 'type' => 'object',
68 69 'properties' => array(
69 - 'title' => array(
70 - 'description' => __( 'The contents of the <title> element from the URL.', 'gutenberg' ),
70 + 'title' => array(
71 + 'description' => __( 'The contents of the <title> tag from the URL.', 'gutenberg' ),
71 72 'type' => 'string',
72 73 'context' => array( 'view', 'edit', 'embed' ),
73 74 'readonly' => true,
74 75 ),
75 - 'icon' => array(
76 - 'description' => __( 'The favicon image link of the <link rel="icon"> element from the URL.', 'gutenberg' ),
77 - 'type' => 'string',
78 - 'format' => 'uri',
79 - 'context' => array( 'view', 'edit', 'embed' ),
80 - 'readonly' => true,
81 - ),
82 - 'description' => array(
83 - 'description' => __( 'The content of the <meta name="description"> element from the URL.', 'gutenberg' ),
84 - 'type' => 'string',
85 - 'context' => array( 'view', 'edit', 'embed' ),
86 - 'readonly' => true,
87 - ),
88 - 'image' => array(
89 - 'description' => __( 'The OG image link of the <meta property="og:image"> or <meta property="og:image:url"> element from the URL.', 'gutenberg' ),
90 - 'type' => 'string',
91 - 'format' => 'uri',
92 - 'context' => array( 'view', 'edit', 'embed' ),
93 - 'readonly' => true,
94 - ),
95 76 ),
96 77 );
97 78
98 79 $this->schema = $schema;
@@ -104,11 +85,12 @@
104 85 * Retrieves the contents of the <title> tag from the HTML
105 86 * response.
106 87 *
107 88 * @param WP_REST_REQUEST $request Full details about the request.
108 - * @return WP_REST_Response|WP_Error The parsed details as a response object, or an error.
89 + * @return WP_REST_Response|WP_Error The parsed details as a response object or an error.
109 90 */
110 91 public function parse_url_details( $request ) {
92 +
111 93 $url = untrailingslashit( $request['url'] );
112 94
113 95 if ( empty( $url ) ) {
114 96 return new WP_Error( 'rest_invalid_url', __( 'Invalid URL', 'gutenberg' ), array( 'status' => 404 ) );
@@ -133,17 +115,11 @@
133 115 // Cache the valid response.
134 116 $this->set_cache( $cache_key, $remote_url_response );
135 117 }
136 118
137 - $html_head = $this->get_document_head( $remote_url_response );
138 - $meta_elements = $this->get_meta_with_content_elements( $html_head );
139 -
140 119 $data = $this->add_additional_fields_to_object(
141 120 array(
142 - 'title' => $this->get_title( $html_head ),
143 - 'icon' => $this->get_icon( $html_head, $url ),
144 - 'description' => $this->get_description( $meta_elements ),
145 - 'image' => $this->get_image( $meta_elements, $url ),
121 + 'title' => $this->get_title( $remote_url_response ),
146 122 ),
147 123 $request
148 124 );
149 125
@@ -163,9 +139,9 @@
163 139
164 140 /**
165 141 * Checks whether a given request has permission to read remote urls.
166 142 *
167 - * @return WP_Error|bool True if the request has access, or WP_Error object.
143 + * @return WP_Error|bool True if the request has access, WP_Error object otherwise.
168 144 */
169 145 public function permissions_check() {
170 146 if ( current_user_can( 'edit_posts' ) ) {
171 147 return true;
@@ -187,24 +163,14 @@
187 163 /**
188 164 * Retrieves the document title from a remote URL.
189 165 *
190 166 * @param string $url The website url whose HTML we want to access.
191 - * @return string|WP_Error The HTTP response from the remote URL, or an error.
167 + * @return array|WP_Error the HTTP response from the remote URL or error.
192 168 */
193 169 private function get_remote_url( $url ) {
194 170
195 - // Provide a modified UA string to workaround web properties which block WordPress "Pingbacks".
196 - // Why? The UA string used for pingback requests contains `WordPress/` which is very similar
197 - // to that used as the default UA string by the WP HTTP API. Therefore requests from this
198 - // REST endpoint are being unintentionally blocked as they are misidentified as pingback requests.
199 - // By slightly modifying the UA string, but still retaining the "WordPress" identification (via "WP")
200 - // we are able to work around this issue.
201 - // Example UA string: `WP-URLDetails/5.9-alpha-51389 (+http://localhost:8888)`.
202 - $modified_user_agent = 'WP-URLDetails/' . get_bloginfo( 'version' ) . ' (+' . get_bloginfo( 'url' ) . ')';
203 -
204 171 $args = array(
205 172 'limit_response_size' => 150 * KB_IN_BYTES,
206 - 'user-agent' => $modified_user_agent,
207 173 );
208 174
209 175 /**
210 176 * Filters the HTTP request args for URL data retrieval.
@@ -237,141 +203,24 @@
237 203
238 204 /**
239 205 * Parses the <title> contents from the provided HTML
240 206 *
241 - * @param string $html The HTML from the remote website at URL.
242 - * @return string The title tag contents on success, or an empty string.
207 + * @param string $html the HTML from the remote website at URL.
208 + * @return string the title tag contents (maybe empty).
243 209 */
244 210 private function get_title( $html ) {
245 - $pattern = '#<title[^>]*>(.*?)<\s*/\s*title>#is';
246 - preg_match( $pattern, $html, $match_title );
211 + preg_match( '|<title>([^<]*?)</title>|is', $html, $match_title );
247 212
248 - $title = ! empty( $match_title[1] ) && is_string( $match_title[1] ) ? trim( $match_title[1] ) : '';
213 + $title = isset( $match_title[1] ) ? trim( $match_title[1] ) : '';
249 214
250 - if ( empty( $title ) ) {
251 - return '';
252 - }
253 -
254 - return $this->prepare_metadata_for_output( $title );
215 + return $title;
255 216 }
256 217
257 218 /**
258 - * Parses the site icon from the provided HTML
259 - *
260 - * @param string $html The HTML from the remote website at URL.
261 - * @param string $url The target website URL.
262 - * @return string The icon URI on success, or an empty string.
263 - */
264 - private function get_icon( $html, $url ) {
265 - // Grab the icon's link element.
266 - $pattern = '#<link\s[^>]*rel=(?:[\"\']??)\s*(?:icon|shortcut icon|icon shortcut)\s*(?:[\"\']??)[^>]*\/?>#isU';
267 - preg_match( $pattern, $html, $element );
268 - $element = ! empty( $element[0] ) && is_string( $element[0] ) ? trim( $element[0] ) : '';
269 - if ( empty( $element ) ) {
270 - return '';
271 - }
272 -
273 - // Get the icon's href value.
274 - $pattern = '#href=([\"\']??)([^\" >]*?)\\1[^>]*#isU';
275 - preg_match( $pattern, $element, $icon );
276 - $icon = ! empty( $icon[2] ) && is_string( $icon[2] ) ? trim( $icon[2] ) : '';
277 - if ( empty( $icon ) ) {
278 - return '';
279 - }
280 -
281 - // If the icon is a data URL, return it.
282 - $parsed_icon = parse_url( $icon );
283 - if ( isset( $parsed_icon['schema'] ) && 'data' === $parsed_icon['scheme'] ) {
284 - return $icon;
285 - }
286 -
287 - // Attempt to convert relative URLs to absolute.
288 - $parsed_url = parse_url( $url );
289 - $root_url = $parsed_url['scheme'] . '://' . $parsed_url['host'] . '/';
290 - $icon = WP_Http::make_absolute_url( $icon, $root_url );
291 -
292 - return $icon;
293 - }
294 -
295 - /**
296 - * Parses the meta description from the provided HTML.
297 - *
298 - * @param array $meta_elements {
299 - * A multi-dimensional indexed array on success, or empty array.
300 - *
301 - * @type string[] 0 Meta elements with a content attribute.
302 - * @type string[] 1 Content attribute's opening quotation mark.
303 - * @type string[] 2 Content attribute's value for each meta element.
304 - * }
305 - * @return string The meta description contents on success, or an empty string.
306 - */
307 - private function get_description( $meta_elements ) {
308 - // Bail out if there are no meta elements.
309 - if ( empty( $meta_elements[0] ) ) {
310 - return '';
311 - }
312 -
313 - $description = $this->get_metadata_from_meta_element( $meta_elements, 'name', '(?:description|og:description)' );
314 -
315 - // Bail out if description not found.
316 - if ( '' === $description ) {
317 - return '';
318 - }
319 -
320 - return $this->prepare_metadata_for_output( $description );
321 - }
322 -
323 - /**
324 - * Parses the Open Graph Image from the provided HTML.
325 - *
326 - * See: https://ogp.me/.
327 - *
328 - * @param array $meta_elements {
329 - * A multi-dimensional indexed array on success, or empty array.
330 - *
331 - * @type string[] 0 Meta elements with a content attribute.
332 - * @type string[] 1 Content attribute's opening quotation mark.
333 - * @type string[] 2 Content attribute's value for each meta element.
334 - * }
335 - * @param string $url The target website URL.
336 - * @return string The OG image on success, or empty string.
337 - */
338 - private function get_image( $meta_elements, $url ) {
339 - $image = $this->get_metadata_from_meta_element( $meta_elements, 'property', '(?:og:image|og:image:url)' );
340 -
341 - // Bail out if image not found.
342 - if ( '' === $image ) {
343 - return '';
344 - }
345 -
346 - // Attempt to convert relative URLs to absolute.
347 - $parsed_url = parse_url( $url );
348 - $root_url = $parsed_url['scheme'] . '://' . $parsed_url['host'] . '/';
349 - $image = WP_Http::make_absolute_url( $image, $root_url );
350 -
351 - return $image;
352 - }
353 -
354 - /**
355 - * Prepare the metadata by:
356 - *
357 - * - stripping all HTML tags and tag entities
358 - * - converting non-tag entities into characters.
359 - *
360 - * @param string $metadata The metadata content to prepare.
361 - * @return string The prepared metadata.
362 - */
363 - private function prepare_metadata_for_output( $metadata ) {
364 - $metadata = html_entity_decode( $metadata, ENT_QUOTES, get_bloginfo( 'charset' ) );
365 - $metadata = wp_strip_all_tags( $metadata );
366 - return $metadata;
367 - }
368 -
369 - /**
370 219 * Utility function to build cache key for a given URL.
371 220 *
372 - * @param string $url The URL for which to build a cache key.
373 - * @return string The cache key.
221 + * @param string $url the URL for which to build a cache key.
222 + * @return string the cache key.
374 223 */
375 224 private function build_cache_key_for_url( $url ) {
376 225 return 'g_url_details_response_' . md5( $url );
377 226 }
@@ -378,23 +227,27 @@
378 227
379 228 /**
380 229 * Utility function to retrieve a value from the cache at a given key.
381 230 *
382 - * @param string $key The cache key.
383 - * @return mixed The value from the cache.
231 + * @param string $key the cache key.
232 + * @return string the value from the cache.
384 233 */
385 234 private function get_cache( $key ) {
386 - return get_site_transient( $key );
235 + return get_transient( $key );
387 236 }
388 237
389 238 /**
390 239 * Utility function to cache a given data set at a given cache key.
391 240 *
392 - * @param string $key The cache key under which to store the value.
393 - * @param string $data The data to be stored at the given cache key.
394 - * @return bool True when transient set, or false.
241 + * @param string $key the cache key under which to store the value.
242 + * @param string $data the data to be stored at the given cache key.
243 + * @return void
395 244 */
396 245 private function set_cache( $key, $data = '' ) {
246 + if ( ! is_array( $data ) ) {
247 + return;
248 + }
249 +
397 250 $ttl = HOUR_IN_SECONDS;
398 251
399 252 /**
400 253 * Filters the cache expiration.
@@ -405,187 +258,7 @@
405 258 * @param int $ttl the time until cache expiration in seconds.
406 259 */
407 260 $cache_expiration = apply_filters( 'rest_url_details_cache_expiration', $ttl );
408 261
409 - return set_site_transient( $key, $data, $cache_expiration );
410 - }
411 -
412 - /**
413 - * Retrieves the `<head>` section.
414 - *
415 - * @param string $html The string of HTML to parse.
416 - * @return string The `<head>..</head>` section on success, or original HTML.
417 - */
418 - private function get_document_head( $html ) {
419 - $head_html = $html;
420 -
421 - // Find the opening `<head>` tag.
422 - $head_start = strpos( $html, '<head' );
423 - if ( false === $head_start ) {
424 - // Didn't find it. Return the original HTML.
425 - return $html;
426 - }
427 -
428 - // Find the closing `</head>` tag.
429 - $head_end = strpos( $head_html, '</head>' );
430 - if ( false === $head_end ) {
431 - // Didn't find it. Find the opening `<body>` tag.
432 - $head_end = strpos( $head_html, '<body' );
433 -
434 - // Didn't find it. Return the original HTML.
435 - if ( false === $head_end ) {
436 - return $html;
437 - }
438 - }
439 -
440 - // Extract the HTML from opening tag to the closing tag. Then add the closing tag.
441 - $head_html = substr( $head_html, $head_start, $head_end );
442 - $head_html .= '</head>';
443 -
444 - return $head_html;
445 - }
446 -
447 - /**
448 - * Gets all the <meta> elements that have a `content` attribute.
449 - *
450 - * @param string $html The string of HTML to be parsed.
451 - * @return array {
452 - * A multi-dimensional indexed array on success, or empty array.
453 - *
454 - * @type string[] 0 Meta elements with a content attribute.
455 - * @type string[] 1 Content attribute's opening quotation mark.
456 - * @type string[] 2 Content attribute's value for each meta element.
457 - * }
458 - */
459 - private function get_meta_with_content_elements( $html ) {
460 - /*
461 - * Parse all meta elements with a content attribute.
462 - *
463 - * Why first search for the content attribute rather than directly searching for name=description element?
464 - * tl;dr The content attribute's value will be truncated when it contains a > symbol.
465 - *
466 - * The content attribute's value (i.e. the description to get) can have HTML in it and be well-formed as
467 - * it's a string to the browser. Imagine what happens when attempting to match for the name=description
468 - * first. Hmm, if a > or /> symbol is in the content attribute's value, then it terminates the match
469 - * as the element's closing symbol. But wait, it's in the content attribute and is not the end of the
470 - * element. This is a limitation of using regex. It can't determine "wait a minute this is inside of quotation".
471 - * If this happens, what gets matched is not the entire element or all of the content.
472 - *
473 - * Why not search for the name=description and then content="(.*)"?
474 - * The attribute order could be opposite. Plus, additional attributes may exist including being between
475 - * the name and content attributes.
476 - *
477 - * Why not lookahead?
478 - * Lookahead is not constrained to stay within the element. The first <meta it finds may not include
479 - * the name or content, but rather could be from a different element downstream.
480 - */
481 - $pattern = '#<meta\s' .
482 -
483 - /*
484 - * Allows for additional attributes before the content attribute.
485 - * Searches for anything other than > symbol.
486 - */
487 - '[^>]*' .
488 -
489 - /*
490 - * Find the content attribute. When found, capture its value (.*).
491 - *
492 - * Allows for (a) single or double quotes and (b) whitespace in the value.
493 - *
494 - * Why capture the opening quotation mark, i.e. (["\']), and then backreference,
495 - * i.e \1, for the closing quotation mark?
496 - * To ensure the closing quotation mark matches the opening one. Why? Attribute values
497 - * can contain quotation marks, such as an apostrophe in the content.
498 - */
499 - 'content=(["\']??)(.*)\1' .
500 -
501 - /*
502 - * Allows for additional attributes after the content attribute.
503 - * Searches for anything other than > symbol.
504 - */
505 - '[^>]*' .
506 -
507 - /*
508 - * \/?> searches for the closing > symbol, which can be in either /> or > format.
509 - * # ends the pattern.
510 - */
511 - '\/?>#' .
512 -
513 - /*
514 - * These are the options:
515 - * - i : case insensitive
516 - * - s : allows newline characters for the . match (needed for multiline elements)
517 - * - U means non-greedy matching
518 - */
519 - 'isU';
520 -
521 - preg_match_all( $pattern, $html, $elements );
522 -
523 - return $elements;
524 - }
525 -
526 - /**
527 - * Gets the metadata from a target meta element.
528 - *
529 - * @param array $meta_elements {
530 - * A multi-dimensional indexed array on success, or empty array.
531 - *
532 - * @type string[] 0 Meta elements with a content attribute.
533 - * @type string[] 1 Content attribute's opening quotation mark.
534 - * @type string[] 2 Content attribute's value for each meta element.
535 - * }
536 - * @param string $attr Attribute that identifies the element with the target metadata.
537 - * @param string $attr_value The attribute's value that identifies the element with the target metadata.
538 - * @return string The metadata on success, or an empty string.
539 - */
540 - private function get_metadata_from_meta_element( $meta_elements, $attr, $attr_value ) {
541 - // Bail out if there are no meta elements.
542 - if ( empty( $meta_elements[0] ) ) {
543 - return '';
544 - }
545 -
546 - $metadata = '';
547 - $pattern = '#' .
548 -
549 - /*
550 - * Target this attribute and value to find the metadata element.
551 - *
552 - * Allows for (a) no, single, double quotes and (b) whitespace in the value.
553 - *
554 - * Why capture the opening quotation mark, i.e. (["\']), and then backreference,
555 - * i.e \1, for the closing quotation mark?
556 - * To ensure the closing quotation mark matches the opening one. Why? Attribute values
557 - * can contain quotation marks, such as an apostrophe in the content.
558 - */
559 - $attr . '=([\"\']??)\s*' . $attr_value . '\s*\1' .
560 -
561 - /*
562 - * These are the options:
563 - * - i : case insensitive
564 - * - s : allows newline characters for the . match (needed for multiline elements)
565 - * - U means non-greedy matching
566 - */
567 - '#isU';
568 -
569 - // Find the metdata element.
570 - foreach ( $meta_elements[0] as $index => $element ) {
571 - preg_match( $pattern, $element, $match );
572 -
573 - // This is not the metadata element. Skip it.
574 - if ( empty( $match ) ) {
575 - continue;
576 - }
577 -
578 - /*
579 - * Found the metadata element.
580 - * Get the metadata from its matching content array.
581 - */
582 - if ( isset( $meta_elements[2][ $index ] ) && is_string( $meta_elements[2][ $index ] ) ) {
583 - $metadata = trim( $meta_elements[2][ $index ] );
584 - }
585 -
586 - break;
587 - }
588 -
589 - return $metadata;
262 + return set_transient( $key, $data, $cache_expiration );
590 263 }
591 264 }