Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.60% covered (warning)
85.60%
208 / 243
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Media_Meta_Extractor
85.60% covered (warning)
85.60%
208 / 243
37.50% covered (danger)
37.50%
3 / 8
128.29
0.00% covered (danger)
0.00%
0 / 1
 extract
78.95% covered (warning)
78.95%
15 / 19
0.00% covered (danger)
0.00%
0 / 1
7.46
 extract_from_content
86.47% covered (warning)
86.47%
115 / 133
0.00% covered (danger)
0.00%
0 / 1
66.34
 get_image_fields
70.97% covered (warning)
70.97%
22 / 31
0.00% covered (danger)
0.00%
0 / 1
9.57
 reduce_extracted_images
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
6.09
 extract_images_from_content
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 build_image_struct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 get_images_from_html
92.86% covered (success)
92.86%
26 / 28
0.00% covered (danger)
0.00%
0 / 1
14.07
 get_stripped_content
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Class with methods to extract metadata from a post/page about videos, images, links, mentions embedded
4 * in or attached to the post/page.
5 *
6 * @package automattic/jetpack
7 */
8
9/**
10 * Class with methods to extract metadata from a post/page about videos, images, links, mentions embedded
11 * in or attached to the post/page.
12 *
13 * @todo Additionally, have some filters on number of items in each field
14 */
15class Jetpack_Media_Meta_Extractor {
16
17    // Some consts for what to extract.
18    const ALL        = 255;
19    const LINKS      = 1;
20    const MENTIONS   = 2;
21    const IMAGES     = 4;
22    const SHORTCODES = 8; // Only the keeper shortcodes below.
23    const EMBEDS     = 16;
24    const HASHTAGS   = 32;
25
26    /**
27     * Shortcodes to keep.
28     *
29     * For these, we try to extract some data from the shortcode, rather than just recording its presence (which we do for all)
30     * There should be a function get_{shortcode}_id( $atts ) or static method SomethingShortcode::get_{shortcode}_id( $atts ) for these.
31     *
32     * @var string[]
33     */
34    private static $keeper_shortcodes = array(
35        'audio',
36        'youtube',
37        'vimeo',
38        'hulu',
39        'ted',
40        'video',
41        'wpvideo',
42        'videopress',
43    );
44
45    /**
46     * Gets the specified media and meta info from the given post.
47     * NOTE: If you have the post's HTML content already and don't need image data, use extract_from_content() instead.
48     *
49     * @param int     $blog_id The ID of the blog.
50     * @param int     $post_id The ID of the post.
51     * @param int     $what_to_extract A mask of things to extract, e.g. Jetpack_Media_Meta_Extractor::IMAGES | Jetpack_Media_Meta_Extractor::MENTIONS.
52     * @param boolean $extract_alt_text Should alt_text be extracted, defaults to false.
53     *
54     * @return array|WP_Error a structure containing metadata about the embedded things, or empty array if nothing found, or WP_Error on error.
55     */
56    public static function extract( $blog_id, $post_id, $what_to_extract = self::ALL, $extract_alt_text = false ) {
57
58        // multisite?
59        if ( function_exists( 'switch_to_blog' ) ) {
60            switch_to_blog( $blog_id );
61        }
62
63        $post = get_post( $post_id );
64        if ( ! $post instanceof WP_Post ) {
65            if ( function_exists( 'restore_current_blog' ) ) {
66                restore_current_blog();
67            }
68            return array();
69        }
70        $content  = $post->post_title . "\n\n" . $post->post_content;
71        $char_cnt = strlen( $content );
72
73        // prevent running extraction on really huge amounts of content.
74        if ( $char_cnt > 100000 ) { // about 20k English words.
75            $content = substr( $content, 0, 100000 );
76        }
77
78        $extracted = array();
79
80        // Get images first, we need the full post for that.
81        if ( self::IMAGES & $what_to_extract ) {
82            $extracted = self::get_image_fields( $post, array(), $extract_alt_text );
83
84            // Turn off images so we can safely call extract_from_content() below.
85            $what_to_extract = $what_to_extract - self::IMAGES;
86        }
87
88        if ( function_exists( 'restore_current_blog' ) ) {
89            restore_current_blog();
90        }
91
92        // All of the other things besides images can be extracted from just the content.
93        $extracted = self::extract_from_content( $content, $what_to_extract, $extracted );
94
95        return $extracted;
96    }
97
98    /**
99     * Gets the specified meta info from the given post content.
100     * NOTE: If you want IMAGES, call extract( $blog_id, $post_id, ...) which will give you more/better image extraction
101     * This method will give you an error if you ask for IMAGES.
102     *
103     * @param string $content The HTML post_content of a post.
104     * @param int    $what_to_extract A mask of things to extract, e.g. Jetpack_Media_Meta_Extractor::IMAGES | Jetpack_Media_Meta_Extractor::MENTIONS.
105     * @param array  $already_extracted Previously extracted things, e.g. images from extract(), which can be used for x-referencing here.
106     *
107     * @return array a structure containing metadata about the embedded things, or empty array if nothing found, or WP_Error on error.
108     */
109    public static function extract_from_content( $content, $what_to_extract = self::ALL, $already_extracted = array() ) {
110        $stripped_content = self::get_stripped_content( $content );
111
112        // Maybe start with some previously extracted things (e.g. images from extract().
113        $extracted = $already_extracted;
114
115        // Embedded media objects will have already been converted to shortcodes by pre_kses hooks on save.
116
117        if ( self::IMAGES & $what_to_extract ) {
118            $images    = self::extract_images_from_content( $stripped_content, array() );
119            $extracted = array_merge( $extracted, $images );
120        }
121
122        // ----------------------------------- MENTIONS ------------------------------
123
124        if ( self::MENTIONS & $what_to_extract ) {
125            if ( preg_match_all( '/(^|\s)@(\w+)/u', $stripped_content, $matches ) ) {
126                $mentions             = array_values( array_unique( $matches[2] ) ); // array_unique() retains the keys!
127                $mentions             = array_map( 'strtolower', $mentions );
128                $extracted['mention'] = array( 'name' => $mentions );
129                if ( ! isset( $extracted['has'] ) ) {
130                    $extracted['has'] = array();
131                }
132                $extracted['has']['mention'] = count( $mentions );
133            }
134        }
135
136        // ----------------------------------- HASHTAGS ------------------------------
137        /**
138         * Some hosts may not compile with --enable-unicode-properties and kick a warning:
139         * Warning: preg_match_all() [function.preg-match-all]: Compilation failed: support for \P, \p, and \X has not been compiled
140         * Therefore, we only run this code block on wpcom, not in Jetpack.
141         */
142        if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) && ( self::HASHTAGS & $what_to_extract ) ) {
143            // This regex does not exactly match Twitter's
144            // if there are problems/complaints we should implement this:
145            // https://github.com/twitter/twitter-text/blob/master/java/src/com/twitter/Regex.java .
146            if ( preg_match_all( '/(?:^|\s)#(\w*\p{L}+\w*)/u', $stripped_content, $matches ) ) {
147                $hashtags             = array_values( array_unique( $matches[1] ) ); // array_unique() retains the keys!
148                $hashtags             = array_map( 'strtolower', $hashtags );
149                $extracted['hashtag'] = array( 'name' => $hashtags );
150                if ( ! isset( $extracted['has'] ) ) {
151                    $extracted['has'] = array();
152                }
153                $extracted['has']['hashtag'] = count( $hashtags );
154            }
155        }
156
157        // ----------------------------------- SHORTCODES ------------------------------
158
159        // Always look for shortcodes.
160        // If we don't want them, we'll just remove them, so we don't grab them as links below.
161        $shortcode_pattern = '/' . get_shortcode_regex() . '/s';
162        if ( preg_match_all( $shortcode_pattern, $content, $matches ) ) {
163
164            $shortcode_total_count = 0;
165            $shortcode_type_counts = array();
166            $shortcode_types       = array();
167            $shortcode_details     = array();
168
169            if ( self::SHORTCODES & $what_to_extract ) {
170
171                foreach ( $matches[2] as $key => $shortcode ) {
172                    // Elasticsearch (and probably other things) doesn't deal well with some chars as key names.
173                    $shortcode_name = preg_replace( '/[.,*"\'\/\\\\#+ ]/', '_', $shortcode );
174
175                    $attr = shortcode_parse_atts( $matches[3][ $key ] );
176
177                    ++$shortcode_total_count;
178                    if ( ! isset( $shortcode_type_counts[ $shortcode_name ] ) ) {
179                        $shortcode_type_counts[ $shortcode_name ] = 0;
180                    }
181                    ++$shortcode_type_counts[ $shortcode_name ];
182
183                    // Store (uniquely) presence of all shortcode regardless of whether it's a keeper (for those, get ID below)
184                    // @todo Store number of occurrences?
185                    if ( ! in_array( $shortcode_name, $shortcode_types, true ) ) {
186                        $shortcode_types[] = $shortcode_name;
187                    }
188
189                    // For keeper shortcodes, also store the id/url of the object (e.g. youtube video, TED talk, etc.).
190                    if ( in_array( $shortcode, self::$keeper_shortcodes, true ) ) {
191                        // Clear shortcode ID data left from the last shortcode.
192                        $id = null;
193                        // We'll try to get the salient ID from the function jetpack_shortcode_get_xyz_id().
194                        // If the shortcode is a class, we'll call XyzShortcode::get_xyz_id().
195                        $shortcode_get_id_func   = "jetpack_shortcode_get_{$shortcode}_id";
196                        $shortcode_class_name    = ucfirst( $shortcode ) . 'Shortcode';
197                        $shortcode_get_id_method = "get_{$shortcode}_id";
198                        if ( function_exists( $shortcode_get_id_func ) ) {
199                            $id = call_user_func( $shortcode_get_id_func, $attr );
200                        } elseif ( method_exists( $shortcode_class_name, $shortcode_get_id_method ) ) {
201                            $id = call_user_func( array( $shortcode_class_name, $shortcode_get_id_method ), $attr );
202                        } elseif ( 'video' === $shortcode ) {
203                            $id = $attr['src'] ?? $attr['url'] ?? $attr['mp4'] ?? $attr['m4v'] ?? $attr['webm'] ?? $attr['ogv'] ?? $attr['wmv'] ?? $attr['flv'] ?? null;
204                        } elseif ( 'audio' === $shortcode ) {
205                            preg_match( '#(https?://(?:[^\s"|\']+)\.(?:mp3|ogg|flac|m4a|wav))([ "\'|]|$)#', implode( ' ', $attr ), $audio_matches );
206                            $id = $audio_matches[1] ?? null;
207                        }
208                        if ( ! empty( $id )
209                            && ( ! isset( $shortcode_details[ $shortcode_name ] ) || ! in_array( $id, $shortcode_details[ $shortcode_name ], true ) ) ) {
210                            $shortcode_details[ $shortcode_name ][] = $id;
211                        }
212                    }
213                }
214
215                if ( $shortcode_total_count > 0 ) {
216                    // Add the shortcode info to the $extracted array.
217                    if ( ! isset( $extracted['has'] ) ) {
218                        $extracted['has'] = array();
219                    }
220                    $extracted['has']['shortcode'] = $shortcode_total_count;
221                    $extracted['shortcode']        = array();
222                    foreach ( $shortcode_type_counts as $type => $count ) {
223                        $extracted['shortcode'][ $type ] = array( 'count' => $count );
224                    }
225                    if ( ! empty( $shortcode_types ) ) {
226                        $extracted['shortcode_types'] = $shortcode_types;
227                    }
228                    foreach ( $shortcode_details as $type => $id ) {
229                        $extracted['shortcode'][ $type ]['id'] = $id;
230                    }
231                }
232            }
233
234            // Remove the shortcodes form our copy of $content, so we don't count links in them as links below.
235            $content = preg_replace( $shortcode_pattern, ' ', $content );
236        }
237
238        // ----------------------------------- LINKS ------------------------------
239
240        if ( self::LINKS & $what_to_extract ) {
241
242            // To hold the extracted stuff we find.
243            $links = array();
244
245            // @todo Get the text inside the links?
246
247            // Grab any links, whether in <a href="..." or not, but subtract those from shortcodes and images.
248            // (we treat embed links as just another link).
249            if ( preg_match_all( '#(?:^|\s|"|\')(https?://([^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/))))#', $content, $matches ) ) {
250
251                foreach ( $matches[1] as $link_raw ) {
252                    $url = wp_parse_url( $link_raw );
253
254                    // Data URI links.
255                    if ( ! isset( $url['scheme'] ) || 'data' === $url['scheme'] ) {
256                        continue;
257                    }
258
259                    // Reject invalid URLs.
260                    if ( ! isset( $url['host'] ) ) {
261                        continue;
262                    }
263
264                    // Remove large (and likely invalid) links.
265                    if ( 4096 < strlen( $link_raw ) ) {
266                        continue;
267                    }
268
269                    // Build a simple form of the URL so we can compare it to ones we found in IMAGES or SHORTCODES and exclude those.
270                    $simple_url = $url['scheme'] . '://' . $url['host'] . ( ! empty( $url['path'] ) ? $url['path'] : '' );
271                    if ( isset( $extracted['image']['url'] ) ) {
272                        if ( in_array( $simple_url, (array) $extracted['image']['url'], true ) ) {
273                            continue;
274                        }
275                    }
276
277                    list( $proto, $link_all_but_proto ) = explode( '://', $link_raw ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
278
279                    // Build a reversed hostname.
280                    $host_parts    = array_reverse( explode( '.', $url['host'] ) );
281                    $host_reversed = '';
282                    foreach ( $host_parts as $part ) {
283                        $host_reversed .= ( ! empty( $host_reversed ) ? '.' : '' ) . $part;
284                    }
285
286                    $link_analyzed = '';
287                    if ( ! empty( $url['path'] ) ) {
288                        // The whole path (no query args or fragments).
289                        $path           = substr( $url['path'], 1 ); // strip the leading '/'.
290                        $link_analyzed .= ( ! empty( $link_analyzed ) ? ' ' : '' ) . $path;
291
292                        // The path split by /.
293                        $path_split = explode( '/', $path );
294                        if ( count( $path_split ) > 1 ) {
295                            $link_analyzed .= ' ' . implode( ' ', $path_split );
296                        }
297
298                        // The fragment.
299                        if ( ! empty( $url['fragment'] ) ) {
300                            $link_analyzed .= ( ! empty( $link_analyzed ) ? ' ' : '' ) . $url['fragment'];
301                        }
302                    }
303
304                    $link = array(
305                        'url'           => $link_all_but_proto,
306                        'host_reversed' => $host_reversed,
307                        'host'          => $url['host'],
308                    );
309                    if ( ! in_array( $link, $links, true ) ) {
310                        $links[] = $link;
311                    }
312                }
313            }
314
315            $link_count = count( $links );
316            if ( $link_count ) {
317                $extracted['link'] = $links;
318                if ( ! isset( $extracted['has'] ) ) {
319                    $extracted['has'] = array();
320                }
321                $extracted['has']['link'] = $link_count;
322            }
323        }
324
325        // ----------------------------------- EMBEDS ------------------------------
326
327        // Embeds are just individual links on their own line.
328        if ( self::EMBEDS & $what_to_extract ) {
329
330            if ( ! function_exists( '_wp_oembed_get_object' ) ) {
331                include ABSPATH . WPINC . '/class-oembed.php';
332            }
333
334            // get an oembed object.
335            $oembed = _wp_oembed_get_object();
336
337            // Grab any links on their own lines that may be embeds.
338            if ( preg_match_all( '|^\s*(https?://[^\s"]+)\s*$|im', $content, $matches ) ) {
339
340                // To hold the extracted stuff we find.
341                $embeds = array();
342
343                foreach ( $matches[1] as $link_raw ) {
344                    $url = wp_parse_url( $link_raw );
345
346                    list( $proto, $link_all_but_proto ) = explode( '://', $link_raw ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
347
348                    // Check whether this "link" is really an embed.
349                    foreach ( $oembed->providers as $matchmask => $data ) {
350                        list( $providerurl, $regex ) = $data; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
351
352                        // Turn the asterisk-type provider URLs into regex.
353                        if ( ! $regex ) {
354                            $matchmask = '#' . str_replace( '___wildcard___', '(.+)', preg_quote( str_replace( '*', '___wildcard___', $matchmask ), '#' ) ) . '#i';
355                            $matchmask = preg_replace( '|^#http\\\://|', '#https?\://', $matchmask );
356                        }
357
358                        if ( preg_match( $matchmask, $link_raw ) ) {
359                            $embeds[] = $link_all_but_proto; // @todo Check unique before adding
360
361                            // @todo Try to get ID's for the ones we care about (shortcode_keepers)
362                            break;
363                        }
364                    }
365                }
366
367                if ( ! empty( $embeds ) ) {
368                    if ( ! isset( $extracted['has'] ) ) {
369                        $extracted['has'] = array();
370                    }
371                    $extracted['has']['embed'] = count( $embeds );
372                    $extracted['embed']        = array( 'url' => array() );
373                    foreach ( $embeds as $e ) {
374                        $extracted['embed']['url'][] = $e;
375                    }
376                }
377            }
378        }
379
380        return $extracted;
381    }
382
383    /**
384     * Get image fields for matching images.
385     *
386     * @uses Jetpack_PostImages
387     *
388     * @param WP_Post $post A post object.
389     * @param array   $args Optional args, see defaults list for details.
390     * @param boolean $extract_alt_text Should alt_text be extracted, defaults to false.
391     *
392     * @return array Returns an array of all images meeting the specified criteria in $args.
393     */
394    private static function get_image_fields( $post, $args = array(), $extract_alt_text = false ) {
395
396        if ( ! $post instanceof WP_Post ) {
397            return array();
398        }
399
400        $defaults = array(
401            'width'  => 200, // Required minimum width (if possible to determine).
402            'height' => 200, // Required minimum height (if possible to determine).
403        );
404
405        $args = wp_parse_args( $args, $defaults );
406
407        $image_list                = array();
408        $image_booleans            = array();
409        $image_booleans['gallery'] = 0;
410
411        $from_featured_image = Jetpack_PostImages::from_thumbnail( $post->ID, $args['width'], $args['height'] );
412        if ( ! empty( $from_featured_image ) ) {
413            if ( $extract_alt_text ) {
414                $image_list = array_merge( $image_list, self::reduce_extracted_images( $from_featured_image ) );
415            } else {
416                $srcs       = wp_list_pluck( $from_featured_image, 'src' );
417                $image_list = array_merge( $image_list, $srcs );
418            }
419        }
420
421        $from_slideshow = Jetpack_PostImages::from_slideshow( $post->ID, $args['width'], $args['height'] );
422        if ( ! empty( $from_slideshow ) ) {
423            if ( $extract_alt_text ) {
424                $image_list = array_merge( $image_list, self::reduce_extracted_images( $from_slideshow ) );
425            } else {
426                $srcs       = wp_list_pluck( $from_slideshow, 'src' );
427                $image_list = array_merge( $image_list, $srcs );
428            }
429        }
430
431        $from_gallery = Jetpack_PostImages::from_gallery( $post->ID );
432        if ( ! empty( $from_gallery ) ) {
433            if ( $extract_alt_text ) {
434                $image_list = array_merge( $image_list, self::reduce_extracted_images( $from_gallery ) );
435            } else {
436                $srcs       = wp_list_pluck( $from_gallery, 'src' );
437                $image_list = array_merge( $image_list, $srcs );
438            }
439            ++$image_booleans['gallery']; // @todo This count isn't correct, will only every count 1
440        }
441
442        // @todo Can we check width/height of these efficiently?  Could maybe use query args at least, before we strip them out
443        $image_list = self::get_images_from_html( $post->post_content, $image_list, $extract_alt_text );
444
445        return self::build_image_struct( $image_list, $image_booleans );
446    }
447
448    /**
449     * Given an extracted image array reduce to src,  alt_text, src_width, and src_height.
450     *
451     * @param array $images extracted image array.
452     *
453     * @return array reduced image array
454     */
455    protected static function reduce_extracted_images( $images ) {
456        $ret_images = array();
457        foreach ( $images as $image ) {
458            // skip if src isn't set.
459            if ( empty( $image['src'] ) ) {
460                continue;
461            }
462            $ret_image = array(
463                'url' => $image['src'],
464            );
465            if ( ! empty( $image['src_height'] ) || ! empty( $image['src_width'] ) ) {
466                $ret_image['src_width']  = $image['src_width'] ?? '';
467                $ret_image['src_height'] = $image['src_height'] ?? '';
468            }
469            if ( ! empty( $image['alt_text'] ) ) {
470                $ret_image['alt_text'] = $image['alt_text'];
471            } else {
472                $ret_image = $image['src'];
473            }
474            $ret_images[] = $ret_image;
475        }
476        return $ret_images;
477    }
478
479    /**
480     * Helper function to get images from HTML and return it with the set sturcture.
481     *
482     * @param string $content HTML content.
483     * @param array  $image_list Array of already found images.
484     * @param string $extract_alt_text Whether or not to extract the alt text.
485     *
486     * @return array|array[] Array of images.
487     */
488    public static function extract_images_from_content( $content, $image_list, $extract_alt_text = false ) {
489        $image_list = self::get_images_from_html( $content, $image_list, $extract_alt_text );
490        return self::build_image_struct( $image_list, array() );
491    }
492
493    /**
494     * Produces a set structure for extracted media items.
495     *
496     * @param array $image_list Array of images.
497     * @param array $image_booleans Image booleans.
498     *
499     * @return array|array[]
500     */
501    public static function build_image_struct( $image_list, $image_booleans ) {
502        if ( ! empty( $image_list ) ) {
503            $retval     = array( 'image' => array() );
504            $image_list = array_unique( $image_list, SORT_REGULAR );
505            foreach ( $image_list as $img ) {
506                if ( is_string( $img ) ) {
507                    $retval['image'][] = array( 'url' => $img );
508                } else {
509                    $retval['image'][] = $img;
510                }
511            }
512            $image_booleans['image'] = count( $retval['image'] );
513            $retval['has']           = $image_booleans;
514            return $retval;
515        } else {
516            return array();
517        }
518    }
519
520    /**
521     * Extracts images from html.
522     *
523     * @param string  $html Some markup, possibly containing image tags.
524     * @param array   $images_already_extracted (just an array of image URLs without query strings, no special structure), used for de-duplication.
525     * @param boolean $extract_alt_text Should alt_text be extracted, defaults to false.
526     *
527     * @return array Image URLs extracted from the HTML, stripped of query params and de-duped
528     */
529    public static function get_images_from_html( $html, $images_already_extracted, $extract_alt_text = false ) {
530        $image_list = $images_already_extracted;
531        $from_html  = Jetpack_PostImages::from_html( $html );
532        // early return if no image in html.
533        if ( empty( $from_html ) ) {
534            return $image_list;
535        }
536        // process images.
537        foreach ( $from_html as $extracted_image ) {
538            $image_url = $extracted_image['src'];
539            $length    = strpos( $image_url, '?' );
540            $src       = wp_parse_url( $image_url );
541
542            if ( $src && isset( $src['scheme'] ) && isset( $src['host'] ) && isset( $src['path'] ) ) {
543                // Rebuild the URL without the query string.
544                $queryless = $src['scheme'] . '://' . $src['host'] . $src['path'];
545            } elseif ( $length ) {
546                // If wp_parse_url() didn't work, strip off the query string the old fashioned way.
547                $queryless = substr( $image_url, 0, $length );
548            } else {
549                // Failing that, there was no spoon! Err ... query string!
550                $queryless = $image_url;
551            }
552
553            // Discard URLs that are longer then 4KB, these are likely data URIs or malformed HTML.
554            if ( 4096 < strlen( $queryless ) ) {
555                continue;
556            }
557
558            if ( ! in_array( $queryless, $image_list, true ) ) {
559                $image_to_add = array(
560                    'url' => $queryless,
561                );
562                if ( $extract_alt_text ) {
563                    if ( ! empty( $extracted_image['alt_text'] ) ) {
564                        $image_to_add['alt_text'] = $extracted_image['alt_text'];
565                    }
566                    if ( ! empty( $extracted_image['src_width'] ) || ! empty( $extracted_image['src_height'] ) ) {
567                            $image_to_add['src_width']  = $extracted_image['src_width'];
568                            $image_to_add['src_height'] = $extracted_image['src_height'];
569                    }
570                } else {
571                    $image_to_add = $queryless;
572                }
573                $image_list[] = $image_to_add;
574            }
575        }
576        return $image_list;
577    }
578
579    /**
580     * Strips concents of all tags, shortcodes, and decodes HTML entities.
581     *
582     * @param string $content Original content.
583     *
584     * @return string Cleaned content.
585     */
586    private static function get_stripped_content( $content ) {
587        $clean_content = wp_strip_all_tags( $content );
588        $clean_content = html_entity_decode( $clean_content, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
589        // completely strip shortcodes and any content they enclose.
590        $clean_content = strip_shortcodes( $clean_content );
591        return $clean_content;
592    }
593}