Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
60.75% covered (warning)
60.75%
325 / 535
21.05% covered (danger)
21.05%
4 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_PostImages
60.75% covered (warning)
60.75%
325 / 535
21.05% covered (danger)
21.05%
4 / 19
2669.74
0.00% covered (danger)
0.00%
0 / 1
 from_slideshow
45.65% covered (danger)
45.65%
21 / 46
0.00% covered (danger)
0.00%
0 / 1
40.13
 filter_gallery_urls
80.00% covered (warning)
80.00%
12 / 15
0.00% covered (danger)
0.00%
0 / 1
7.39
 from_gallery
60.38% covered (warning)
60.38%
32 / 53
0.00% covered (danger)
0.00%
0 / 1
34.98
 from_attachment
78.38% covered (warning)
78.38%
29 / 37
0.00% covered (danger)
0.00%
0 / 1
15.98
 from_thumbnail
55.38% covered (warning)
55.38%
36 / 65
0.00% covered (danger)
0.00%
0 / 1
109.93
 from_blocks
92.86% covered (success)
92.86%
26 / 28
0.00% covered (danger)
0.00%
0 / 1
7.02
 get_images_from_block_attributes
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
19
 from_html
85.48% covered (warning)
85.48%
53 / 62
0.00% covered (danger)
0.00%
0 / 1
23.48
 from_blavatar
25.00% covered (danger)
25.00%
5 / 20
0.00% covered (danger)
0.00%
0 / 1
21.19
 from_gravatar
88.46% covered (warning)
88.46%
23 / 26
0.00% covered (danger)
0.00%
0 / 1
5.04
 get_image
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 get_images
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
342
 generate_cropped_srcset
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
90
 fit_image_url
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
72
 get_post_html
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
4.01
 get_attachment_data
90.62% covered (success)
90.62%
29 / 32
0.00% covered (danger)
0.00%
0 / 1
10.08
 get_alt_text
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 determine_thumbnail_size_for_photon
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
7
 get_max_thumbnail_dimension
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Useful for finding an image to display alongside/in representation of a specific post.
4 *
5 * @package automattic/jetpack
6 */
7
8use Automattic\Block_Scanner;
9use Automattic\Jetpack\Image_CDN\Image_CDN_Core;
10
11/**
12 * Useful for finding an image to display alongside/in representation of a specific post.
13 *
14 * Includes a few different methods, all of which return a similar-format array containing
15 * details of any images found. Everything can (should) be called statically, it's just a
16 * function-bucket. You can also call Jetpack_PostImages::get_image() to cycle through all of the methods until
17 * one of them finds something useful.
18 *
19 * This file is included verbatim in Jetpack
20 */
21class Jetpack_PostImages {
22    /**
23     * If a slideshow is embedded within a post, then parse out the images involved and return them
24     *
25     * @param int $post_id Post ID.
26     * @param int $width Image width.
27     * @param int $height Image height.
28     * @return array Images.
29     */
30    public static function from_slideshow( $post_id, $width = 200, $height = 200 ) {
31        $images = array();
32
33        $post = get_post( $post_id );
34        if ( ! $post instanceof WP_Post ) {
35            return $images;
36        }
37
38        if ( ! empty( $post->post_password ) ) {
39            return $images;
40        }
41
42        if ( false === has_shortcode( $post->post_content, 'slideshow' ) ) {
43            return $images; // no slideshow - bail.
44        }
45
46        $permalink = get_permalink( $post->ID );
47
48        // Mechanic: Somebody set us up the bomb.
49        $old_post                  = $GLOBALS['post'] ?? null;
50        $GLOBALS['post']           = $post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
51        $old_shortcodes            = $GLOBALS['shortcode_tags'];
52        $GLOBALS['shortcode_tags'] = array( 'slideshow' => $old_shortcodes['slideshow'] ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
53
54        // Find all the slideshows.
55        preg_match_all( '/' . get_shortcode_regex() . '/sx', $post->post_content, $slideshow_matches, PREG_SET_ORDER );
56
57        ob_start(); // The slideshow shortcode handler calls wp_print_scripts and wp_print_styles... not too happy about that.
58
59        foreach ( $slideshow_matches as $slideshow_match ) {
60            $slideshow = do_shortcode_tag( $slideshow_match );
61            $pos       = stripos( $slideshow, 'jetpack-slideshow' );
62            if ( false === $pos ) { // must be something wrong - or we changed the output format in which case none of the following will work.
63                continue;
64            }
65            $start       = strpos( $slideshow, '[', $pos );
66            $end         = strpos( $slideshow, ']', $start );
67            $post_images = json_decode( wp_specialchars_decode( str_replace( "'", '"', substr( $slideshow, $start, $end - $start + 1 ) ), ENT_QUOTES ) ); // parse via JSON
68            // If the JSON didn't decode don't try and act on it.
69            if ( is_array( $post_images ) ) {
70                foreach ( $post_images as $post_image ) {
71                    $post_image_id = absint( $post_image->id );
72                    if ( ! $post_image_id ) {
73                        continue;
74                    }
75
76                    $meta = wp_get_attachment_metadata( $post_image_id );
77
78                    // Must be larger than 200x200 (or user-specified).
79                    if ( ! isset( $meta['width'] ) || $meta['width'] < $width ) {
80                        continue;
81                    }
82                    if ( ! isset( $meta['height'] ) || $meta['height'] < $height ) {
83                        continue;
84                    }
85
86                    $url = wp_get_attachment_url( $post_image_id );
87
88                    $images[] = array(
89                        'type'       => 'image',
90                        'from'       => 'slideshow',
91                        'src'        => $url,
92                        'src_width'  => $meta['width'],
93                        'src_height' => $meta['height'],
94                        'href'       => $permalink,
95                    );
96                }
97            }
98        }
99        ob_end_clean();
100
101        // Operator: Main screen turn on.
102        $GLOBALS['shortcode_tags'] = $old_shortcodes; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
103        $GLOBALS['post']           = $old_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
104
105        return $images;
106    }
107
108    /**
109     * Filtering out images with broken URL from galleries.
110     *
111     * @param array $galleries Galleries.
112     * @return array $filtered_galleries
113     */
114    public static function filter_gallery_urls( $galleries ) {
115        $filtered_galleries = array();
116        foreach ( $galleries as $this_gallery ) {
117            if ( ! isset( $this_gallery['src'] ) ) {
118                continue;
119            }
120            $ids = isset( $this_gallery['ids'] ) ? explode( ',', $this_gallery['ids'] ) : array();
121            // Make sure 'src' array isn't associative and has no holes.
122            $this_gallery['src'] = array_values( $this_gallery['src'] );
123            foreach ( $this_gallery['src'] as $idx => $src_url ) {
124                if ( filter_var( $src_url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED ) === false ) {
125                    unset( $this_gallery['src'][ $idx ] );
126                    unset( $ids[ $idx ] );
127                }
128            }
129            if ( isset( $this_gallery['ids'] ) ) {
130                $this_gallery['ids'] = implode( ',', $ids );
131            }
132            // Remove any holes we introduced.
133            $this_gallery['src']  = array_values( $this_gallery['src'] );
134            $filtered_galleries[] = $this_gallery;
135        }
136
137        return $filtered_galleries;
138    }
139
140    /**
141     * If a gallery is detected, then get all the images from it.
142     *
143     * @param int $post_id Post ID.
144     * @param int $width Minimum image width to consider.
145     * @param int $height Minimum image height to consider.
146     * @return array Images.
147     */
148    public static function from_gallery( $post_id, $width = 200, $height = 200 ) {
149        $images = array();
150
151        $post = get_post( $post_id );
152        if ( ! $post instanceof WP_Post ) {
153            return $images;
154        }
155
156        if ( ! empty( $post->post_password ) ) {
157            return $images;
158        }
159        add_filter( 'get_post_galleries', array( __CLASS__, 'filter_gallery_urls' ), 999999 );
160
161        $permalink = get_permalink( $post->ID );
162
163        /**
164         *  Juggle global post object because the gallery shortcode uses the
165         *  global object.
166         *
167         *  See core ticket:
168         *  https://core.trac.wordpress.org/ticket/39304
169         */
170        // phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
171        if ( isset( $GLOBALS['post'] ) ) {
172            $juggle_post     = $GLOBALS['post'];
173            $GLOBALS['post'] = $post;
174            $galleries       = get_post_galleries( $post->ID, false );
175            $GLOBALS['post'] = $juggle_post;
176        } else {
177            $GLOBALS['post'] = $post;
178            $galleries       = get_post_galleries( $post->ID, false );
179            unset( $GLOBALS['post'] );
180        }
181        // phpcs:enable WordPress.WP.GlobalVariablesOverride.Prohibited
182
183        foreach ( $galleries as $gallery ) {
184            if ( ! empty( $gallery['ids'] ) ) {
185                $image_ids  = explode( ',', $gallery['ids'] );
186                $image_size = isset( $gallery['size'] ) ? $gallery['size'] : 'thumbnail';
187                foreach ( $image_ids as $image_id ) {
188                    $image = wp_get_attachment_image_src( $image_id, $image_size );
189                    $meta  = wp_get_attachment_metadata( $image_id );
190
191                    if ( isset( $gallery['type'] ) && 'slideshow' === $gallery['type'] ) {
192                        // Must be larger than 200x200 (or user-specified).
193                        if ( ! isset( $meta['width'] ) || $meta['width'] < $width ) {
194                            continue;
195                        }
196                        if ( ! isset( $meta['height'] ) || $meta['height'] < $height ) {
197                            continue;
198                        }
199                    }
200
201                    if ( ! empty( $image[0] ) ) {
202                        list( $raw_src ) = explode( '?', $image[0] ); // pull off any Query string (?w=250).
203                        $raw_src         = wp_specialchars_decode( $raw_src ); // rawify it.
204                        $raw_src         = esc_url_raw( $raw_src ); // clean it.
205                        $images[]        = array(
206                            'type'       => 'image',
207                            'from'       => 'gallery',
208                            'src'        => $raw_src,
209                            'src_width'  => $meta['width'] ?? 0,
210                            'src_height' => $meta['height'] ?? 0,
211                            'href'       => $permalink,
212                            'alt_text'   => self::get_alt_text( $image_id ),
213                        );
214                    }
215                }
216            } elseif ( ! empty( $gallery['src'] ) ) {
217                foreach ( $gallery['src'] as $src ) {
218                    list( $raw_src ) = explode( '?', $src ); // pull off any Query string (?w=250).
219                    $raw_src         = wp_specialchars_decode( $raw_src ); // rawify it.
220                    $raw_src         = esc_url_raw( $raw_src ); // clean it.
221                    $images[]        = array(
222                        'type' => 'image',
223                        'from' => 'gallery',
224                        'src'  => $raw_src,
225                        'href' => $permalink,
226                    );
227                }
228            }
229        }
230
231        return $images;
232    }
233
234    /**
235     * Get attachment images for a specified post and return them. Also make sure
236     * their dimensions are at or above a required minimum.
237     *
238     * @param  int $post_id The post ID to check.
239     * @param  int $width Image width.
240     * @param  int $height Image height.
241     * @return array Containing details of the image, or empty array if none.
242     */
243    public static function from_attachment( $post_id, $width = 200, $height = 200 ) {
244        $images = array();
245
246        $post = get_post( $post_id );
247        if ( ! $post instanceof WP_Post ) {
248            return $images;
249        }
250
251        if ( ! empty( $post->post_password ) ) {
252            return $images;
253        }
254
255        $post_images = get_posts(
256            array(
257                'post_parent'      => $post_id,   // Must be children of post.
258                'numberposts'      => 5,          // No more than 5.
259                'post_type'        => 'attachment', // Must be attachments.
260                'post_mime_type'   => 'image', // Must be images.
261                'suppress_filters' => false,
262            )
263        );
264
265        if ( ! $post_images ) {
266            return $images;
267        }
268
269        $permalink = get_permalink( $post_id );
270
271        foreach ( $post_images as $post_image ) {
272            $current_image = self::get_attachment_data( $post_image->ID, $permalink, $width, $height );
273            if ( false !== $current_image ) {
274                $images[] = $current_image;
275            }
276        }
277
278        /*
279        * We only want to pass back attached images that were actually inserted.
280        * We can load up all the images found in the HTML source and then
281        * compare URLs to see if an image is attached AND inserted.
282        */
283        $html_images     = self::from_html( $post_id );
284        $inserted_images = array();
285
286        foreach ( $html_images as $html_image ) {
287            $src = wp_parse_url( $html_image['src'] );
288            if ( ! $src || empty( $src['path'] ) ) {
289                continue;
290            }
291
292            // strip off any query strings from src.
293            if ( ! empty( $src['scheme'] ) && ! empty( $src['host'] ) ) {
294                $inserted_images[] = $src['scheme'] . '://' . $src['host'] . $src['path'];
295            } elseif ( ! empty( $src['host'] ) ) {
296                $inserted_images[] = set_url_scheme( 'http://' . $src['host'] . $src['path'] );
297            } else {
298                $inserted_images[] = site_url( '/' ) . $src['path'];
299            }
300        }
301        foreach ( $images as $i => $image ) {
302            if ( ! in_array( $image['src'], $inserted_images, true ) ) {
303                unset( $images[ $i ] );
304            }
305        }
306
307        return $images;
308    }
309
310    /**
311     * Check if a Featured Image is set for this post, and return it in a similar
312     * format to the other images?_from_*() methods.
313     *
314     * @param  int $post_id The post ID to check.
315     * @param  int $width Image width.
316     * @param  int $height Image height.
317     * @return array containing details of the Featured Image, or empty array if none.
318     */
319    public static function from_thumbnail( $post_id, $width = 200, $height = 200 ) {
320        $images = array();
321
322        $post = get_post( $post_id );
323        if ( ! $post instanceof WP_Post ) {
324            return $images;
325        }
326
327        if ( ! empty( $post->post_password ) ) {
328            return $images;
329        }
330
331        if ( 'attachment' === get_post_type( $post ) && wp_attachment_is_image( $post ) ) {
332            $thumb = $post_id;
333        } else {
334            $thumb = get_post_thumbnail_id( $post );
335        }
336
337        if ( $thumb ) {
338            $meta = wp_get_attachment_metadata( $thumb );
339            // Must be larger than requested minimums.
340            if ( ! isset( $meta['width'] ) || $meta['width'] < $width ) {
341                return $images;
342            }
343            if ( ! isset( $meta['height'] ) || $meta['height'] < $height ) {
344                return $images;
345            }
346            $max_dimension = self::get_max_thumbnail_dimension();
347            $too_big       = ( ( ! empty( $meta['width'] ) && $meta['width'] > $max_dimension ) || ( ! empty( $meta['height'] ) && $meta['height'] > $max_dimension ) );
348
349            if (
350                $too_big &&
351                (
352                    ( method_exists( 'Jetpack', 'is_module_active' ) && Jetpack::is_module_active( 'photon' ) ) ||
353                    ( defined( 'IS_WPCOM' ) && IS_WPCOM )
354                )
355            ) {
356                $size        = self::determine_thumbnail_size_for_photon( $meta['width'], $meta['height'] );
357                $photon_args = array(
358                    'fit' => $size['width'] . ',' . $size['height'],
359                );
360                $img_src     = array( Image_CDN_Core::cdn_url( wp_get_attachment_url( $thumb ), $photon_args ), $size['width'], $size['height'], true ); // Match the signature of wp_get_attachment_image_src
361            } else {
362                $img_src = wp_get_attachment_image_src( $thumb, 'full' );
363            }
364            if ( ! is_array( $img_src ) ) {
365                // If wp_get_attachment_image_src returns false but we know that there should be an image that could be used.
366                // we try a bit harder and user the data that we have.
367                $thumb_post_data = get_post( $thumb );
368                $img_src         = array( $thumb_post_data->guid ?? null, $meta['width'], $meta['height'] );
369            }
370
371            // Let's try to use the postmeta if we can, since it seems to be
372            // more reliable
373            if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
374                $featured_image = get_post_meta( $post->ID, '_jetpack_featured_image', false );
375                if ( $featured_image ) {
376                    $url = $featured_image[0];
377                } else {
378                    $url = $img_src[0];
379                }
380            } else {
381                $url = $img_src[0];
382            }
383            $images = array(
384                array( // Other methods below all return an array of arrays.
385                    'type'       => 'image',
386                    'from'       => 'thumbnail',
387                    'src'        => $url,
388                    'src_width'  => $img_src[1],
389                    'src_height' => $img_src[2],
390                    'href'       => get_permalink( $thumb ),
391                    'alt_text'   => self::get_alt_text( $thumb ),
392                ),
393            );
394
395        }
396
397        if ( empty( $images ) && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
398            $meta_thumbnail = get_post_meta( $post_id, '_jetpack_post_thumbnail', true );
399            if ( ! empty( $meta_thumbnail ) ) {
400                if ( ! isset( $meta_thumbnail['width'] ) || $meta_thumbnail['width'] < $width ) {
401                    return $images;
402                }
403
404                if ( ! isset( $meta_thumbnail['height'] ) || $meta_thumbnail['height'] < $height ) {
405                    return $images;
406                }
407
408                $images = array(
409                    array( // Other methods below all return an array of arrays.
410                        'type'       => 'image',
411                        'from'       => 'thumbnail',
412                        'src'        => $meta_thumbnail['URL'],
413                        'src_width'  => $meta_thumbnail['width'],
414                        'src_height' => $meta_thumbnail['height'],
415                        'href'       => $meta_thumbnail['URL'],
416                        'alt_text'   => self::get_alt_text( $thumb ),
417                    ),
418                );
419            }
420        }
421
422        return $images;
423    }
424
425    /**
426     * Get images from Gutenberg Image blocks.
427     *
428     * @since 6.9.0
429     * @since 14.8 Updated to use Block_Delimiter for improved performance.
430     * @since 14.9 Updated to use Block_Scanner for improved performance.
431     *
432     * @param mixed $html_or_id The HTML string to parse for images, or a post id.
433     * @param int   $width      Minimum Image width.
434     * @param int   $height     Minimum Image height.
435     */
436    public static function from_blocks( $html_or_id, $width = 200, $height = 200 ) {
437        $images = array();
438
439        $html_info = self::get_post_html( $html_or_id );
440
441        if ( empty( $html_info['html'] ) ) {
442            return $images;
443        }
444
445        $scanner = Block_Scanner::create( $html_info['html'] );
446        if ( ! $scanner ) {
447            return $images;
448        }
449
450        /*
451         * Use Block_Scanner to parse our post content HTML,
452         * and find all the block delimiters for supported blocks,
453         * whether they're parent or nested blocks.
454         */
455        $supported_blocks = array(
456            'core/image',
457            'core/media-text',
458            'core/gallery',
459            'jetpack/tiled-gallery',
460            'jetpack/slideshow',
461            'jetpack/story',
462        );
463
464        while ( $scanner->next_delimiter() ) {
465            $type = $scanner->get_delimiter_type();
466            // Only process opening delimiters for supported block types.
467            if ( Block_Scanner::OPENER !== $type ) {
468                continue;
469            }
470
471            $block_type         = $scanner->get_block_type();
472            $is_supported_block = in_array( $block_type, $supported_blocks, true );
473            if ( ! $is_supported_block ) {
474                continue;
475            }
476
477            $attributes   = $scanner->allocate_and_return_parsed_attributes() ?? array();
478            $block_images = self::get_images_from_block_attributes( $block_type, $attributes, $html_info, $width, $height );
479
480            if ( ! empty( $block_images ) ) {
481                $images = array_merge( $images, $block_images );
482            }
483        }
484
485        /**
486         * Returning a filtered array because get_attachment_data returns false
487         * for unsuccessful attempts.
488         */
489        return array_filter( $images );
490    }
491
492    /**
493     * Extract images from block attributes based on block type.
494     *
495     * @since 14.8
496     *
497     * @param string $block_type Block type name.
498     * @param array  $attributes Block attributes.
499     * @param array  $html_info  Info about the post where the block is found.
500     * @param int    $width      Desired image width.
501     * @param int    $height     Desired image height.
502     *
503     * @return array Array of images found.
504     */
505    private static function get_images_from_block_attributes( $block_type, $attributes, $html_info, $width, $height ) {
506        $images = array();
507
508        switch ( $block_type ) {
509            case 'core/image':
510            case 'core/media-text':
511                $id_key = 'core/image' === $block_type ? 'id' : 'mediaId';
512                if ( ! empty( $attributes[ $id_key ] ) ) {
513                    $image = self::get_attachment_data( $attributes[ $id_key ], $html_info['post_url'], $width, $height );
514                    if ( false !== $image ) {
515                        $images[] = $image;
516                    }
517                }
518                break;
519
520            case 'core/gallery':
521            case 'jetpack/tiled-gallery':
522            case 'jetpack/slideshow':
523                if ( ! empty( $attributes['ids'] ) && is_array( $attributes['ids'] ) ) {
524                    foreach ( $attributes['ids'] as $img_id ) {
525                        $image = self::get_attachment_data( $img_id, $html_info['post_url'], $width, $height );
526                        if ( false !== $image ) {
527                            $images[] = $image;
528                        }
529                    }
530                }
531                break;
532
533            case 'jetpack/story':
534                if ( ! empty( $attributes['mediaFiles'] ) && is_array( $attributes['mediaFiles'] ) ) {
535                    foreach ( $attributes['mediaFiles'] as $media_file ) {
536                        if ( ! empty( $media_file['id'] ) ) {
537                            $image = self::get_attachment_data( $media_file['id'], $html_info['post_url'], $width, $height );
538                            if ( false !== $image ) {
539                                $images[] = $image;
540                            }
541                        }
542                    }
543                }
544                break;
545        }
546
547        return $images;
548    }
549
550    /**
551     * Very raw -- just parse the HTML and pull out any/all img tags and return their src
552     *
553     * @param mixed $html_or_id The HTML string to parse for images, or a post id.
554     * @param int   $width      Minimum Image width.
555     * @param int   $height     Minimum Image height.
556     *
557     * @uses DOMDocument
558     *
559     * @return array containing images
560     */
561    public static function from_html( $html_or_id, $width = 200, $height = 200 ) {
562        $images = array();
563
564        $html_info = self::get_post_html( $html_or_id );
565
566        if ( empty( $html_info['html'] ) ) {
567            return $images;
568        }
569
570        // Do not go any further if DOMDocument is disabled on the server.
571        if ( ! class_exists( 'DOMDocument' ) ) {
572            return $images;
573        }
574
575        // Let's grab all image tags from the HTML.
576        $dom_doc = new DOMDocument();
577
578        // DOMDocument defaults to ISO-8859 because we're loading only the post content, without head tag.
579        // Fix: Enforce encoding with meta tag.
580        $charset = get_option( 'blog_charset' );
581        if ( empty( $charset ) || ! preg_match( '/^[a-zA-Z0-9_-]+$/', $charset ) ) {
582            $charset = 'UTF-8';
583        }
584        $html_prefix = sprintf( '<meta http-equiv="Content-Type" content="text/html; charset=%s">', esc_attr( $charset ) );
585
586        // The @ is not enough to suppress errors when dealing with libxml,
587        // we have to tell it directly how we want to handle errors.
588        libxml_use_internal_errors( true );
589        @$dom_doc->loadHTML( $html_prefix . $html_info['html'] ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
590        libxml_use_internal_errors( false );
591
592        $image_tags = $dom_doc->getElementsByTagName( 'img' );
593
594        // For each image Tag, make sure it can be added to the $images array, and add it.
595        foreach ( $image_tags as $image_tag ) {
596            $img_src = $image_tag->getAttribute( 'src' );
597
598            if ( empty( $img_src ) ) {
599                continue;
600            }
601
602            // Do not grab smiley images that were automatically created by WP when entering text smilies.
603            if ( stripos( $img_src, '/smilies/' ) ) {
604                continue;
605            }
606
607            // Do not grab Gravatar images.
608            if ( stripos( $img_src, 'gravatar.com' ) ) {
609                continue;
610            }
611
612            // First try to get the width and height from the img attributes, but if they are not set, check to see if they are specified in the url. WordPress automatically names files like foo-1024x768.jpg during the upload process
613            $width  = (int) $image_tag->getAttribute( 'width' );
614            $height = (int) $image_tag->getAttribute( 'height' );
615            if ( 0 === $width && 0 === $height ) {
616                preg_match( '/-([0-9]{1,5})x([0-9]{1,5})\.(?:jpg|jpeg|png|gif|webp)$/i', $img_src, $matches );
617                if ( ! empty( $matches[1] ) ) {
618                    $width = (int) $matches[1];
619                }
620                if ( ! empty( $matches[2] ) ) {
621                    $height = (int) $matches[2];
622                }
623            }
624            // If width and height are still 0, try to get the id of the image from the class, e.g. wp-image-1234
625            if ( 0 === $width && 0 === $height ) {
626
627                preg_match( '/wp-image-([0-9]+)/', $image_tag->getAttribute( 'class' ), $matches );
628                if ( ! empty( $matches[1] ) ) {
629                    $attachment_id = $matches[1];
630                    $meta          = wp_get_attachment_metadata( $attachment_id );
631                    $height        = $meta['height'] ?? 0;
632                    $width         = $meta['width'] ?? 0;
633                }
634            }
635
636            $meta = array(
637                'width'    => $width,
638                'height'   => $height,
639                'alt_text' => $image_tag->getAttribute( 'alt' ),
640            );
641
642            /**
643             * Filters the switch to ignore minimum image size requirements. Can be used
644             * to add custom logic to image dimensions, like only enforcing one of the dimensions,
645             * or disabling it entirely.
646             *
647             * @since 6.4.0
648             *
649             * @param bool $ignore Should the image dimensions be ignored?
650             * @param array $meta Array containing image dimensions parsed from the markup.
651             */
652            $ignore_dimensions = apply_filters( 'jetpack_postimages_ignore_minimum_dimensions', false, $meta );
653
654            // Must be larger than 200x200 (or user-specified).
655            if (
656                ! $ignore_dimensions
657                && (
658                    empty( $meta['width'] )
659                    || empty( $meta['height'] )
660                    || $meta['width'] < $width
661                    || $meta['height'] < $height
662                )
663            ) {
664                continue;
665            }
666
667            $image = array(
668                'type'       => 'image',
669                'from'       => 'html',
670                'src'        => $img_src,
671                'src_width'  => $meta['width'],
672                'src_height' => $meta['height'],
673                'href'       => $html_info['post_url'],
674            );
675            if ( ! empty( $meta['alt_text'] ) ) {
676                $image['alt_text'] = $meta['alt_text'];
677            }
678            $images[] = $image;
679        }
680        return $images;
681    }
682
683    /**
684     * Data from blavatar.
685     *
686     * @param    int $post_id The post ID to check.
687     * @param    int $size Size.
688     * @return array containing details of the image, or empty array if none.
689     */
690    public static function from_blavatar( $post_id, $size = 96 ) {
691
692        $permalink = get_permalink( $post_id );
693
694        if ( function_exists( 'blavatar_domain' ) && function_exists( 'blavatar_exists' ) && function_exists( 'blavatar_url' ) ) {
695            $domain = blavatar_domain( $permalink );
696
697            if ( ! blavatar_exists( $domain ) ) {
698                return array();
699            }
700
701            $url = blavatar_url( $domain, 'img', $size );
702        } else {
703            $url = get_site_icon_url( $size );
704            if ( ! $url ) {
705                return array();
706            }
707        }
708
709        return array(
710            array(
711                'type'       => 'image',
712                'from'       => 'blavatar',
713                'src'        => $url,
714                'src_width'  => $size,
715                'src_height' => $size,
716                'href'       => $permalink,
717                'alt_text'   => '',
718            ),
719        );
720    }
721
722    /**
723     * Gets a post image from the author avatar.
724     *
725     * @param int    $post_id The post ID to check.
726     * @param int    $size The size of the avatar to get.
727     * @param string $default The default image to use.
728     * @return array containing details of the image, or empty array if none.
729     */
730    public static function from_gravatar( $post_id, $size = 96, $default = false ) {
731        $post = get_post( $post_id );
732        if ( ! $post instanceof WP_Post ) {
733            return array();
734        }
735
736        $permalink = get_permalink( $post_id );
737
738        if ( function_exists( 'wpcom_get_avatar_url' ) ) {
739            $url = wpcom_get_avatar_url( $post->post_author, $size, $default, true );
740            if ( $url && is_array( $url ) ) {
741                $url = $url[0];
742            }
743        } else {
744            $url = get_avatar_url(
745                $post->post_author,
746                array(
747                    'size'    => $size,
748                    'default' => $default,
749                )
750            );
751        }
752
753        return array(
754            array(
755                'type'       => 'image',
756                'from'       => 'gravatar',
757                'src'        => $url,
758                'src_width'  => $size,
759                'src_height' => $size,
760                'href'       => $permalink,
761                'alt_text'   => '',
762            ),
763        );
764    }
765
766    /**
767     * Run through the different methods that we have available to try to find a single good
768     * display image for this post.
769     *
770     * @param int   $post_id Post ID.
771     * @param array $args Other arguments (currently width and height required for images where possible to determine).
772     * @return array|null containing details of the best image to be used, or null if no image is found.
773     */
774    public static function get_image( $post_id, $args = array() ) {
775        $image = null;
776
777        /**
778         * Fires before we find a single good image for a specific post.
779         *
780         * @since 2.2.0
781         *
782         * @param int $post_id Post ID.
783         */
784        do_action( 'jetpack_postimages_pre_get_image', $post_id );
785        $media = self::get_images( $post_id, $args );
786
787        if ( is_array( $media ) ) {
788            foreach ( $media as $item ) {
789                if ( 'image' === $item['type'] ) {
790                    $image = $item;
791                    break;
792                }
793            }
794        }
795
796        /**
797         * Fires after we find a single good image for a specific post.
798         *
799         * @since 2.2.0
800         *
801         * @param int $post_id Post ID.
802         */
803        do_action( 'jetpack_postimages_post_get_image', $post_id );
804
805        return $image;
806    }
807
808    /**
809     * Get an array containing a collection of possible images for this post, stopping once we hit a method
810     * that returns something useful.
811     *
812     * @param  int   $post_id Post ID.
813     * @param  array $args Optional args, see defaults list for details.
814     * @return array containing images that would be good for representing this post
815     */
816    public static function get_images( $post_id, $args = array() ) {
817        // Figure out which image to attach to this post.
818        $media = array();
819
820        /**
821         * Filters the array of images that would be good for a specific post.
822         * This filter is applied before options ($args) filter the original array.
823         *
824         * @since 2.0.0
825         *
826         * @param array $media Array of images that would be good for a specific post.
827         * @param int $post_id Post ID.
828         * @param array $args Array of options to get images.
829         */
830        $media = apply_filters( 'jetpack_images_pre_get_images', $media, $post_id, $args );
831        if ( $media ) {
832            return $media;
833        }
834
835        $defaults = array(
836            'width'               => 200, // Required minimum width (if possible to determine).
837            'height'              => 200, // Required minimum height (if possible to determine).
838
839            'fallback_to_avatars' => false, // Optionally include Blavatar and Gravatar (in that order) in the image stack.
840            'avatar_size'         => 96, // Used for both Grav and Blav.
841            'gravatar_default'    => false, // Default image to use if we end up with no Gravatar.
842
843            'from_thumbnail'      => true, // Use these flags to specify which methods to use to find an image.
844            'from_slideshow'      => true,
845            'from_gallery'        => true,
846            'from_attachment'     => true,
847            'from_blocks'         => true,
848            'from_html'           => true,
849
850            'html_content'        => '', // HTML string to pass to from_html().
851        );
852        $args     = wp_parse_args( $args, $defaults );
853
854        $media = array();
855        if ( $args['from_thumbnail'] ) {
856            $media = self::from_thumbnail( $post_id, $args['width'], $args['height'] );
857        }
858        if ( ! $media && $args['from_slideshow'] ) {
859            $media = self::from_slideshow( $post_id, $args['width'], $args['height'] );
860        }
861        if ( ! $media && $args['from_gallery'] ) {
862            $media = self::from_gallery( $post_id );
863        }
864        if ( ! $media && $args['from_attachment'] ) {
865            $media = self::from_attachment( $post_id, $args['width'], $args['height'] );
866        }
867        if ( ! $media && $args['from_blocks'] ) {
868            if ( empty( $args['html_content'] ) ) {
869                $media = self::from_blocks( $post_id, $args['width'], $args['height'] ); // Use the post_id, which will load the content.
870            } else {
871                $media = self::from_blocks( $args['html_content'], $args['width'], $args['height'] ); // If html_content is provided, use that.
872            }
873        }
874        if ( ! $media && $args['from_html'] ) {
875            if ( empty( $args['html_content'] ) ) {
876                $media = self::from_html( $post_id, $args['width'], $args['height'] ); // Use the post_id, which will load the content.
877            } else {
878                $media = self::from_html( $args['html_content'], $args['width'], $args['height'] ); // If html_content is provided, use that.
879            }
880        }
881
882        if ( ! $media && $args['fallback_to_avatars'] ) {
883            $media = self::from_blavatar( $post_id, $args['avatar_size'] );
884            if ( ! $media ) {
885                $media = self::from_gravatar( $post_id, $args['avatar_size'], $args['gravatar_default'] );
886            }
887        }
888
889        /**
890         * Filters the array of images that would be good for a specific post.
891         * This filter is applied after options ($args) filter the original array.
892         *
893         * @since 2.0.0
894         *
895         * @param array $media Array of images that would be good for a specific post.
896         * @param int $post_id Post ID.
897         * @param array $args Array of options to get images.
898         */
899        return apply_filters( 'jetpack_images_get_images', $media, $post_id, $args );
900    }
901
902    /**
903     * Takes an image and base pixel dimensions and returns a srcset for the
904     * resized and cropped images, based on a fixed set of multipliers.
905     *
906     * @param  array $image Array containing details of the image.
907     * @param  int   $base_width Base image width (i.e., the width at 1x).
908     * @param  int   $base_height Base image height (i.e., the height at 1x).
909     * @param  bool  $use_widths Whether to generate the srcset with widths instead of multipliers.
910     * @return string The srcset for the image.
911     */
912    public static function generate_cropped_srcset( $image, $base_width, $base_height, $use_widths = false ) {
913        $srcset = '';
914
915        if ( ! is_array( $image ) || empty( $image['src'] ) || empty( $image['src_width'] ) ) {
916            return $srcset;
917        }
918
919        $multipliers   = array( 1, 1.5, 2, 3, 4 );
920        $srcset_values = array();
921        foreach ( $multipliers as $multiplier ) {
922            $srcset_width  = (int) ( $base_width * $multiplier );
923            $srcset_height = (int) ( $base_height * $multiplier );
924            if ( $srcset_width < 1 || $srcset_width > $image['src_width'] ) {
925                break;
926            }
927
928            $srcset_url = self::fit_image_url(
929                $image['src'],
930                $srcset_width,
931                $srcset_height
932            );
933
934            if ( $use_widths ) {
935                $srcset_values[] = "{$srcset_url} {$srcset_width}w";
936            } else {
937                $srcset_values[] = "{$srcset_url} {$multiplier}x";
938            }
939        }
940
941        if ( count( $srcset_values ) > 1 ) {
942            $srcset = implode( ', ', $srcset_values );
943        }
944
945        return $srcset;
946    }
947
948    /**
949     * Takes an image URL and pixel dimensions then returns a URL for the
950     * resized and cropped image.
951     *
952     * @param  string $src Image URL.
953     * @param  int    $width Image width.
954     * @param  int    $height Image height.
955     * @return string Transformed image URL
956     */
957    public static function fit_image_url( $src, $width, $height ) {
958        $width  = (int) $width;
959        $height = (int) $height;
960
961        if ( $width < 1 || $height < 1 ) {
962            return $src;
963        }
964
965        // See if we should bypass WordPress.com SaaS resizing.
966        if ( has_filter( 'jetpack_images_fit_image_url_override' ) ) {
967            /**
968             * Filters the image URL used after dimensions are set by Photon.
969             *
970             * @since 3.3.0
971             *
972             * @param string $src Image URL.
973             * @param int $width Image width.
974             * @param int $width Image height.
975             */
976            return apply_filters( 'jetpack_images_fit_image_url_override', $src, $width, $height );
977        }
978
979        // If WPCOM hosted image use native transformations.
980        $img_host = wp_parse_url( $src, PHP_URL_HOST );
981        if ( $img_host && str_ends_with( $img_host, '.files.wordpress.com' ) ) {
982            return add_query_arg(
983                array(
984                    'w'    => $width,
985                    'h'    => $height,
986                    'crop' => 1,
987                ),
988                set_url_scheme( $src )
989            );
990        }
991
992        // Use image cdn magic.
993        if ( class_exists( Image_CDN_Core::class ) && method_exists( Image_CDN_Core::class, 'cdn_url' ) ) {
994            return Image_CDN_Core::cdn_url( $src, array( 'resize' => "$width,$height" ) );
995        }
996
997        // Arg... no way to resize image using WordPress.com infrastructure!
998        return $src;
999    }
1000
1001    /**
1002     * Get HTML from given post content.
1003     *
1004     * @since 6.9.0
1005     *
1006     * @param mixed $html_or_id The HTML string to parse for images, or a post id.
1007     *
1008     * @return array $html_info {
1009     * @type string $html     Post content.
1010     * @type string $post_url Post URL.
1011     * }
1012     */
1013    public static function get_post_html( $html_or_id ) {
1014        if ( is_numeric( $html_or_id ) ) {
1015            $post = get_post( $html_or_id );
1016            if ( ! $post instanceof WP_Post || ! empty( $post->post_password ) ) {
1017                return '';
1018            }
1019
1020            $html_info = array(
1021                'html'     => $post->post_content, // DO NOT apply the_content filters here, it will cause loops.
1022                'post_url' => get_permalink( $post->ID ),
1023            );
1024        } else {
1025            $html_info = array(
1026                'html'     => $html_or_id,
1027                'post_url' => '',
1028            );
1029        }
1030        return $html_info;
1031    }
1032
1033    /**
1034     * Get info about a WordPress attachment.
1035     *
1036     * @since 6.9.0
1037     *
1038     * @param int    $attachment_id Attachment ID.
1039     * @param string $post_url      URL of the post, if we have one.
1040     * @param int    $width         Minimum Image width.
1041     * @param int    $height        Minimum Image height.
1042     * @return array|bool           Image data or false if unavailable.
1043     */
1044    public static function get_attachment_data( $attachment_id, $post_url, $width, $height ) {
1045        if ( empty( $attachment_id ) ) {
1046            return false;
1047        }
1048
1049        $meta = wp_get_attachment_metadata( $attachment_id );
1050
1051        if ( empty( $meta ) ) {
1052            return false;
1053        }
1054
1055        if ( ! empty( $meta['videopress'] ) ) {
1056            // Use poster image for VideoPress videos.
1057            $url         = $meta['videopress']['poster'];
1058            $meta_width  = $meta['videopress']['width'];
1059            $meta_height = $meta['videopress']['height'];
1060        } elseif ( ! empty( $meta['thumb'] ) ) {
1061            // On WordPress.com, VideoPress videos have a 'thumb' property with the
1062            // poster image filename instead.
1063            $media_url   = wp_get_attachment_url( $attachment_id );
1064            $url         = str_replace( wp_basename( $media_url ), $meta['thumb'], $media_url );
1065            $meta_width  = $meta['width'];
1066            $meta_height = $meta['height'];
1067        } elseif ( wp_attachment_is( 'video', $attachment_id ) ) {
1068            // We don't have thumbnail images for non-VideoPress videos - skip them.
1069            return false;
1070        } else {
1071            if ( ! isset( $meta['width'] ) || ! isset( $meta['height'] ) ) {
1072                return false;
1073            }
1074            $url         = wp_get_attachment_url( $attachment_id );
1075            $meta_width  = $meta['width'];
1076            $meta_height = $meta['height'];
1077        }
1078
1079        if ( $meta_width < $width || $meta_height < $height ) {
1080            return false;
1081        }
1082
1083        return array(
1084            'type'       => 'image',
1085            'from'       => 'attachment',
1086            'src'        => $url,
1087            'src_width'  => $meta_width,
1088            'src_height' => $meta_height,
1089            'href'       => $post_url,
1090            'alt_text'   => self::get_alt_text( $attachment_id ),
1091        );
1092    }
1093
1094    /**
1095     * Get the alt text for an image or other media from the Media Library.
1096     *
1097     * @since 7.1
1098     *
1099     * @param int $attachment_id The Post ID of the media.
1100     * @return string The alt text value or an empty string.
1101     */
1102    public static function get_alt_text( $attachment_id ) {
1103        return (string) get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
1104    }
1105
1106    /**
1107     * Determine the size to use with Photon for a thumbnail image.
1108     * Images larger than the maximum thumbnail dimension in either dimension are resized to maintain aspect ratio.
1109     *
1110     * @since 14.6
1111     * @see https://github.com/Automattic/jetpack/issues/40349
1112     *
1113     * @param int $width Original image width.
1114     * @param int $height Original image height.
1115     * @return array Array containing the width and height to use with Photon (null means auto).
1116     */
1117    public static function determine_thumbnail_size_for_photon( $width, $height ) {
1118        $max_dimension = self::get_max_thumbnail_dimension();
1119
1120        // If neither dimension exceeds max size, return original dimensions.
1121        if ( $width <= $max_dimension && $height <= $max_dimension ) {
1122            return array(
1123                'width'  => $width,
1124                'height' => $height,
1125            );
1126        }
1127
1128        if ( $width >= $height ) {
1129            // For landscape or square images.
1130            $dims = image_resize_dimensions( $width, $height, $max_dimension, 0 ); // Height will be calculated automatically.
1131        } else {
1132            // For portrait images.
1133            $dims = image_resize_dimensions( $width, $height, 0, $max_dimension ); // Width will be calculated automatically.
1134        }
1135
1136        // $dims can be false if the image is virtually the same size as the max dimension, e.g. wp_fuzzy_number_match.
1137        if ( $dims && isset( $dims[4] ) && isset( $dims[5] ) ) {
1138            return array(
1139                'width'  => $dims[4],
1140                'height' => $dims[5],
1141            );
1142        }
1143
1144            return array(
1145                'width'  => $width,
1146                'height' => $height,
1147            );
1148    }
1149
1150    /**
1151     * Function to provide the maximum dimension for a thumbnail image.
1152     * Filterable via the `jetpack_post_images_max_dimension` filter.
1153     *
1154     * @since 14.6
1155     * @see https://github.com/Automattic/jetpack/issues/40349
1156     *
1157     * @return int The maximum dimension for a thumbnail image.
1158     */
1159    public static function get_max_thumbnail_dimension() {
1160        /**
1161         * Filter the maximum dimension allowed for a thumbnail image.
1162         * The default value is 1200 pixels.
1163         *
1164         * @since 14.6
1165         *
1166         * @param int $max_dimension Maximum dimension in pixels.
1167         */
1168        return (int) apply_filters( 'jetpack_post_images_max_thumbnail_dimension', 1200 );
1169    }
1170}