PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.8
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.8
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / inc / class-wpseo-image-utils.php

class-wpseo-image-utils.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 27.8, at inc/class-wpseo-image-utils.php

534 lines 15.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WPSEO plugin file.
4 *
5 * @package WPSEO
6 */
7
8 /**
9 * WPSEO_Image_Utils.
10 */
11 class WPSEO_Image_Utils {
12
13 /**
14 * Find an attachment ID for a given URL.
15 *
16 * @param string $url The URL to find the attachment for.
17 *
18 * @return int The found attachment ID, or 0 if none was found.
19 */
20 public static function get_attachment_by_url( $url ) {
21 /*
22 * As get_attachment_by_url won't work on resized versions of images,
23 * we strip out the size part of an image URL.
24 */
25 $url = preg_replace( '/(.*)-\d+x\d+\.(jpg|png|gif)$/', '$1.$2', $url );
26
27 static $uploads;
28
29 $uploads ??= wp_get_upload_dir();
30
31 // Don't try to do this for external URLs.
32 if ( strpos( $url, $uploads['baseurl'] ) !== 0 ) {
33 return 0;
34 }
35
36 if ( function_exists( 'wpcom_vip_attachment_url_to_postid' ) ) {
37 // @codeCoverageIgnoreStart -- We can't test this properly.
38 return (int) wpcom_vip_attachment_url_to_postid( $url );
39 // @codeCoverageIgnoreEnd -- The rest we _can_ test.
40 }
41
42 return self::attachment_url_to_postid( $url );
43 }
44
45 /**
46 * Implements the attachment_url_to_postid with use of WP Cache.
47 *
48 * @param string $url The attachment URL for which we want to know the Post ID.
49 *
50 * @return int The Post ID belonging to the attachment, 0 if not found.
51 */
52 protected static function attachment_url_to_postid( $url ) {
53 $cache_key = sprintf( 'yoast_attachment_url_post_id_%s', md5( $url ) );
54
55 // Set the ID based on the hashed URL in the cache.
56 $id = wp_cache_get( $cache_key );
57
58 if ( $id === 'not_found' ) {
59 return 0;
60 }
61
62 // ID is found in cache, return.
63 if ( $id !== false ) {
64 return $id;
65 }
66
67 // Note: We use the WP COM version if we can, see above.
68 $id = attachment_url_to_postid( $url );
69
70 if ( empty( $id ) ) {
71 /**
72 * If no ID was found, maybe we're dealing with a scaled big image. So, let's try that.
73 *
74 * @see https://core.trac.wordpress.org/ticket/51058
75 */
76 $id = self::get_scaled_image_id( $url );
77 }
78
79 if ( empty( $id ) ) {
80 wp_cache_set( $cache_key, 'not_found', '', ( 12 * HOUR_IN_SECONDS + wp_rand( 0, ( 4 * HOUR_IN_SECONDS ) ) ) );
81 return 0;
82 }
83
84 // We have the Post ID, but it's not in the cache yet. We do that here and return.
85 wp_cache_set( $cache_key, $id, '', ( 24 * HOUR_IN_SECONDS + wp_rand( 0, ( 12 * HOUR_IN_SECONDS ) ) ) );
86 return $id;
87 }
88
89 /**
90 * Tries getting the ID of a potentially scaled image.
91 *
92 * @param string $url The URL of the image.
93 *
94 * @return int|false The ID of the image or false for failure.
95 */
96 protected static function get_scaled_image_id( $url ) {
97 $path_parts = pathinfo( $url );
98 if ( isset( $path_parts['dirname'], $path_parts['filename'], $path_parts['extension'] ) ) {
99 $scaled_url = trailingslashit( $path_parts['dirname'] ) . $path_parts['filename'] . '-scaled.' . $path_parts['extension'];
100
101 return attachment_url_to_postid( $scaled_url );
102 }
103
104 return false;
105 }
106
107 /**
108 * Retrieves the image data.
109 *
110 * @param array $image Image array with URL and metadata.
111 * @param int $attachment_id Attachment ID.
112 *
113 * @return array|false {
114 * Array of image data
115 *
116 * @type string $alt Image's alt text.
117 * @type string $path Path of image.
118 * @type int $width Width of image.
119 * @type int $height Height of image.
120 * @type string $type Image's MIME type.
121 * @type string $size Image's size.
122 * @type string $url Image's URL.
123 * @type int $filesize The file size in bytes, if already set.
124 * }
125 */
126 public static function get_data( $image, $attachment_id ) {
127 if ( ! is_array( $image ) ) {
128 return false;
129 }
130
131 // Deals with non-set keys and values being null or false.
132 if ( empty( $image['width'] ) || empty( $image['height'] ) ) {
133 return false;
134 }
135
136 $image['id'] = $attachment_id;
137 $image['alt'] = self::get_alt_tag( $attachment_id );
138 $image['pixels'] = ( (int) $image['width'] * (int) $image['height'] );
139
140 if ( ! isset( $image['type'] ) ) {
141 $image['type'] = get_post_mime_type( $attachment_id );
142 }
143
144 /**
145 * Filter: 'wpseo_image_data' - Filter image data.
146 *
147 * Elements with keys not listed in the section will be discarded.
148 *
149 * @param array $image_data {
150 * Array of image data
151 *
152 * @type int id Image's ID as an attachment.
153 * @type string alt Image's alt text.
154 * @type string path Image's path.
155 * @type int width Width of image.
156 * @type int height Height of image.
157 * @type int pixels Number of pixels in the image.
158 * @type string type Image's MIME type.
159 * @type string size Image's size.
160 * @type string url Image's URL.
161 * @type int filesize The file size in bytes, if already set.
162 * }
163 * @param int $attachment_id Attachment ID.
164 */
165 $image = apply_filters( 'wpseo_image_data', $image, $attachment_id );
166
167 // Keep only the keys we need, and nothing else.
168 return array_intersect_key( $image, array_flip( [ 'id', 'alt', 'path', 'width', 'height', 'pixels', 'type', 'size', 'url', 'filesize' ] ) );
169 }
170
171 /**
172 * Checks a size version of an image to see if it's not too heavy.
173 *
174 * @param array $image Image to check the file size of.
175 *
176 * @return bool True when the image is within limits, false if not.
177 */
178 public static function has_usable_file_size( $image ) {
179 if ( ! is_array( $image ) || $image === [] ) {
180 return false;
181 }
182
183 /**
184 * Filter: 'wpseo_image_image_weight_limit' - Determines what the maximum weight
185 * (in bytes) of an image is allowed to be, default is 2 MB.
186 *
187 * @param int $max_bytes The maximum weight (in bytes) of an image.
188 */
189 $max_size = apply_filters( 'wpseo_image_image_weight_limit', 2_097_152 );
190
191 // We cannot check without a path, so assume it's fine.
192 if ( ! isset( $image['path'] ) ) {
193 return true;
194 }
195
196 return ( self::get_file_size( $image ) <= $max_size );
197 }
198
199 /**
200 * Find the right version of an image based on size.
201 *
202 * @param int $attachment_id Attachment ID.
203 * @param string|array $size Size name, or array of width and height in pixels (e.g [800,400]).
204 *
205 * @return array|false Returns an array with image data on success, false on failure.
206 */
207 public static function get_image( $attachment_id, $size ) {
208 $image = false;
209 if ( $size === 'full' ) {
210 $image = self::get_full_size_image_data( $attachment_id );
211 }
212
213 if ( ! $image ) {
214 $image = image_get_intermediate_size( $attachment_id, $size );
215 }
216
217 if ( ! is_array( $image ) ) {
218 $image_src = wp_get_attachment_image_src( $attachment_id, $size );
219 if ( is_array( $image_src ) && isset( $image_src[1] ) && isset( $image_src[2] ) ) {
220 $image = [];
221 $image['url'] = $image_src[0];
222 $image['width'] = $image_src[1];
223 $image['height'] = $image_src[2];
224 $image['size'] = 'full';
225 }
226 }
227
228 if ( ! $image ) {
229 return false;
230 }
231
232 if ( ! isset( $image['size'] ) ) {
233 $image['size'] = $size;
234 }
235
236 return self::get_data( $image, $attachment_id );
237 }
238
239 /**
240 * Returns the image data for the full size image.
241 *
242 * @param int $attachment_id Attachment ID.
243 *
244 * @return array|false Array when there is a full size image. False if not.
245 */
246 protected static function get_full_size_image_data( $attachment_id ) {
247 $image = wp_get_attachment_metadata( $attachment_id );
248 if ( ! is_array( $image ) ) {
249 return false;
250 }
251
252 $image['url'] = wp_get_attachment_image_url( $attachment_id, 'full' );
253 $image['path'] = get_attached_file( $attachment_id );
254 $image['size'] = 'full';
255
256 return $image;
257 }
258
259 /**
260 * Finds the full file path for a given image file.
261 *
262 * @param string $path The relative file path.
263 *
264 * @return string The full file path.
265 */
266 public static function get_absolute_path( $path ) {
267 static $uploads;
268
269 $uploads ??= wp_get_upload_dir();
270
271 // Add the uploads basedir if the path does not start with it.
272 if ( empty( $uploads['error'] ) && strpos( $path, $uploads['basedir'] ) !== 0 ) {
273 return $uploads['basedir'] . DIRECTORY_SEPARATOR . ltrim( $path, DIRECTORY_SEPARATOR );
274 }
275
276 return $path;
277 }
278
279 /**
280 * Get the relative path of the image.
281 *
282 * @param string $img Image URL.
283 *
284 * @return string The expanded image URL.
285 */
286 public static function get_relative_path( $img ) {
287 if ( $img[0] !== '/' ) {
288 return $img;
289 }
290
291 // If it's a relative URL, it's relative to the domain, not necessarily to the WordPress install, we
292 // want to preserve domain name and URL scheme (http / https) though.
293 $parsed_url = wp_parse_url( home_url() );
294 $img = $parsed_url['scheme'] . '://' . $parsed_url['host'] . $img;
295
296 return $img;
297 }
298
299 /**
300 * Get the image file size.
301 *
302 * @param array $image An image array object.
303 *
304 * @return int The file size in bytes.
305 */
306 public static function get_file_size( $image ) {
307 if ( isset( $image['filesize'] ) ) {
308 return $image['filesize'];
309 }
310
311 if ( ! isset( $image['path'] ) ) {
312 return 0;
313 }
314
315 // If the file size for the file is over our limit, we're going to go for a smaller version.
316 if ( function_exists( 'wp_filesize' ) ) {
317 return wp_filesize( self::get_absolute_path( $image['path'] ) );
318 }
319
320 return file_exists( $image['path'] ) ? (int) filesize( $image['path'] ) : 0;
321 }
322
323 /**
324 * Returns the different image variations for consideration.
325 *
326 * @param int $attachment_id The attachment to return the variations for.
327 *
328 * @return array The different variations possible for this attachment ID.
329 */
330 public static function get_variations( $attachment_id ) {
331 $variations = [];
332
333 foreach ( self::get_sizes() as $size ) {
334 $variation = self::get_image( $attachment_id, $size );
335
336 // The get_image function returns false if the size doesn't exist for this attachment.
337 if ( $variation ) {
338 $variations[] = $variation;
339 }
340 }
341
342 return $variations;
343 }
344
345 /**
346 * Check original size of image. If original image is too small, return false, else return true.
347 *
348 * Filters a list of variations by a certain set of usable dimensions.
349 *
350 * @param array $usable_dimensions {
351 * The parameters to check against.
352 *
353 * @type int $min_width Minimum width of image.
354 * @type int $max_width Maximum width of image.
355 * @type int $min_height Minimum height of image.
356 * @type int $max_height Maximum height of image.
357 * }
358 * @param array $variations The variations that should be considered.
359 *
360 * @return array Whether a variation is fit for display or not.
361 */
362 public static function filter_usable_dimensions( $usable_dimensions, $variations ) {
363 $filtered = [];
364
365 foreach ( $variations as $variation ) {
366 $dimensions = $variation;
367
368 if ( self::has_usable_dimensions( $dimensions, $usable_dimensions ) ) {
369 $filtered[] = $variation;
370 }
371 }
372
373 return $filtered;
374 }
375
376 /**
377 * Filters a list of variations by (disk) file size.
378 *
379 * @param array $variations The variations to consider.
380 *
381 * @return array The validations that pass the required file size limits.
382 */
383 public static function filter_usable_file_size( $variations ) {
384 foreach ( $variations as $variation ) {
385 // We return early to prevent measuring the file size of all the variations.
386 if ( self::has_usable_file_size( $variation ) ) {
387 return [ $variation ];
388 }
389 }
390
391 return [];
392 }
393
394 /**
395 * Retrieve the internal WP image file sizes.
396 *
397 * @return array An array of image sizes.
398 */
399 public static function get_sizes() {
400 /**
401 * Filter: 'wpseo_image_sizes' - Determines which image sizes we'll loop through to get an appropriate image.
402 *
403 * @param array<string> $sizes The array of image sizes to loop through.
404 */
405 return apply_filters( 'wpseo_image_sizes', [ 'full', 'large', 'medium_large' ] );
406 }
407
408 /**
409 * Grabs an image alt text.
410 *
411 * @param int $attachment_id The attachment ID.
412 *
413 * @return string The image alt text.
414 */
415 public static function get_alt_tag( $attachment_id ) {
416 return (string) get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
417 }
418
419 /**
420 * Checks whether an img sizes up to the parameters.
421 *
422 * @param array $dimensions The image values.
423 * @param array $usable_dimensions The parameters to check against.
424 *
425 * @return bool True if the image has usable measurements, false if not.
426 */
427 private static function has_usable_dimensions( $dimensions, $usable_dimensions ) {
428 foreach ( [ 'width', 'height' ] as $param ) {
429 $minimum = $usable_dimensions[ 'min_' . $param ];
430 $maximum = $usable_dimensions[ 'max_' . $param ];
431
432 $current = $dimensions[ $param ];
433 if ( ( $current < $minimum ) || ( $current > $maximum ) ) {
434 return false;
435 }
436 }
437
438 return true;
439 }
440
441 /**
442 * Gets the post's first usable content image. Null if none is available.
443 *
444 * @param int|null $post_id The post id.
445 *
446 * @return string|null The image URL.
447 */
448 public static function get_first_usable_content_image_for_post( $post_id = null ) {
449 $post = get_post( $post_id );
450
451 // We know get_post() returns the post or null.
452 if ( ! $post ) {
453 return null;
454 }
455
456 $image_finder = new WPSEO_Content_Images();
457 $images = $image_finder->get_images( $post->ID, $post );
458
459 return self::get_first_image( $images );
460 }
461
462 /**
463 * Gets the term's first usable content image. Null if none is available.
464 *
465 * @param int $term_id The term id.
466 *
467 * @return string|null The image URL.
468 */
469 public static function get_first_content_image_for_term( $term_id ) {
470 $term_description = term_description( $term_id );
471
472 // We know term_description() returns a string which may be empty.
473 if ( $term_description === '' ) {
474 return null;
475 }
476
477 $image_finder = new WPSEO_Content_Images();
478 $images = $image_finder->get_images_from_content( $term_description );
479
480 return self::get_first_image( $images );
481 }
482
483 /**
484 * Retrieves an attachment ID for an image uploaded in the settings.
485 *
486 * Due to self::get_attachment_by_url returning 0 instead of false.
487 * 0 is also a possibility when no ID is available.
488 *
489 * @param string $setting The setting the image is stored in.
490 *
491 * @return int|bool The attachment id, or false or 0 if no ID is available.
492 */
493 public static function get_attachment_id_from_settings( $setting ) {
494 $image_id = WPSEO_Options::get( $setting . '_id', false );
495 if ( $image_id ) {
496 return $image_id;
497 }
498
499 $image = WPSEO_Options::get( $setting, false );
500 if ( $image ) {
501 // There is not an option to put a URL in an image field in the settings anymore, only to upload it through the media manager.
502 // This means an attachment always exists, so doing this is only needed once.
503 $image_id = self::get_attachment_by_url( $image );
504 }
505
506 // Only store a new ID if it is not 0, to prevent an update loop.
507 if ( $image_id ) {
508 WPSEO_Options::set( $setting . '_id', $image_id );
509 }
510
511 return $image_id;
512 }
513
514 /**
515 * Retrieves the first possible image url from an array of images.
516 *
517 * @param array $images The array to extract image url from.
518 *
519 * @return string|null The extracted image url when found, null when not found.
520 */
521 protected static function get_first_image( $images ) {
522 if ( ! is_array( $images ) ) {
523 return null;
524 }
525
526 $images = array_filter( $images );
527 if ( empty( $images ) ) {
528 return null;
529 }
530
531 return reset( $images );
532 }
533 }
534