PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
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 / sitemaps / class-sitemap-image-parser.php

class-sitemap-image-parser.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI trunk, at inc/sitemaps/class-sitemap-image-parser.php

539 lines 12.5 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\XML_Sitemaps
6 */
7
8 /**
9 * Parses images from the given post.
10 */
11 class WPSEO_Sitemap_Image_Parser {
12
13 /**
14 * Holds the home_url() value to speed up loops.
15 *
16 * @var string
17 */
18 protected $home_url = '';
19
20 /**
21 * Holds site URL hostname.
22 *
23 * @var string
24 */
25 protected $host = '';
26
27 /**
28 * Holds site URL protocol.
29 *
30 * @var string
31 */
32 protected $scheme = 'http';
33
34 /**
35 * Cached set of attachments for multiple posts.
36 *
37 * @var array
38 */
39 protected $attachments = [];
40
41 /**
42 * Holds blog charset value for use in DOM parsing.
43 *
44 * @var string
45 */
46 protected $charset = 'UTF-8';
47
48 /**
49 * Set up URL properties for reuse.
50 */
51 public function __construct() {
52
53 $this->home_url = home_url();
54 $parsed_home = wp_parse_url( $this->home_url );
55
56 if ( ! empty( $parsed_home['host'] ) ) {
57 $this->host = str_replace( 'www.', '', $parsed_home['host'] );
58 }
59
60 if ( ! empty( $parsed_home['scheme'] ) ) {
61 $this->scheme = $parsed_home['scheme'];
62 }
63
64 $this->charset = esc_attr( get_bloginfo( 'charset' ) );
65 }
66
67 /**
68 * Get set of image data sets for the given post.
69 *
70 * @param object $post Post object to get images for.
71 *
72 * @return array
73 */
74 public function get_images( $post ) {
75
76 $images = [];
77
78 if ( ! is_object( $post ) ) {
79 return $images;
80 }
81
82 // Pass the post object rather than its ID, so the post does not get re-fetched from the database.
83 $thumbnail_id = get_post_thumbnail_id( $post );
84
85 if ( $thumbnail_id ) {
86
87 $src = $this->get_absolute_url( $this->image_url( $thumbnail_id ) );
88 $images[] = $this->get_image_item( $post, $src );
89 }
90
91 /**
92 * Filter: 'wpseo_sitemap_content_before_parse_html_images' - Filters the post content
93 * before it is parsed for images.
94 *
95 * @param string $content The raw/unprocessed post content.
96 */
97 $content = apply_filters( 'wpseo_sitemap_content_before_parse_html_images', $post->post_content );
98
99 $unfiltered_images = $this->parse_html_images( $content );
100
101 foreach ( $unfiltered_images as $image ) {
102 $images[] = $this->get_image_item( $post, $image['src'] );
103 }
104
105 foreach ( $this->parse_galleries( $content, $post->ID ) as $attachment ) {
106 $src = $this->get_absolute_url( $this->image_url( $attachment->ID ) );
107 $images[] = $this->get_image_item( $post, $src );
108 }
109
110 if ( $post->post_type === 'attachment' && wp_attachment_is_image( $post ) ) {
111 $src = $this->get_absolute_url( $this->image_url( $post->ID ) );
112 $images[] = $this->get_image_item( $post, $src );
113 }
114
115 foreach ( $images as $key => $image ) {
116
117 if ( empty( $image['src'] ) ) {
118 unset( $images[ $key ] );
119 }
120 }
121
122 /**
123 * Filter images to be included for the post in XML sitemap.
124 *
125 * @param array $images Array of image items.
126 * @param int $post_id ID of the post.
127 */
128 $image_list = apply_filters( 'wpseo_sitemap_urlimages', $images, $post->ID );
129 if ( isset( $image_list ) && is_array( $image_list ) ) {
130 $images = $image_list;
131 }
132
133 return $images;
134 }
135
136 /**
137 * Primes the meta caches of the featured images of the given posts.
138 *
139 * This parser reads each post's featured image file location from the attachment's
140 * meta individually; warming that meta cache in bulk avoids one query per post on
141 * setups without a persistent object cache.
142 *
143 * @param WP_Post[] $posts The posts to prime the featured-image caches for.
144 *
145 * @return void
146 */
147 public function prime_thumbnail_caches( $posts ) {
148
149 $thumbnail_ids = [];
150
151 foreach ( $posts as $post ) {
152 $thumbnail_id = get_post_thumbnail_id( $post );
153
154 if ( $thumbnail_id ) {
155 $thumbnail_ids[] = $thumbnail_id;
156 }
157 }
158
159 if ( ! empty( $thumbnail_ids ) ) {
160 update_meta_cache( 'post', array_unique( $thumbnail_ids ) );
161 }
162 }
163
164 /**
165 * Get the images in the term description.
166 *
167 * @param object $term Term to get images from description for.
168 *
169 * @return array
170 */
171 public function get_term_images( $term ) {
172
173 $images = $this->parse_html_images( $term->description );
174
175 foreach ( $this->parse_galleries( $term->description ) as $attachment ) {
176
177 $images[] = [
178 'src' => $this->get_absolute_url( $this->image_url( $attachment->ID ) ),
179 ];
180 }
181
182 /**
183 * Filter images to be included for the term in XML sitemap.
184 *
185 * @param array $image_list Array of image items.
186 * @param int $term_id ID of the post.
187 */
188 $image_list = apply_filters( 'wpseo_sitemap_urlimages_term', $images, $term->term_id );
189 if ( isset( $image_list ) && is_array( $image_list ) ) {
190 $images = $image_list;
191 }
192
193 return $images;
194 }
195
196 /**
197 * Parse `<img />` tags in content.
198 *
199 * @param string $content Content string to parse.
200 *
201 * @return array
202 */
203 private function parse_html_images( $content ) {
204
205 $images = [];
206
207 if ( ! class_exists( 'DOMDocument' ) ) {
208 return $images;
209 }
210
211 if ( empty( $content ) ) {
212 return $images;
213 }
214
215 // Prevent DOMDocument from bubbling warnings about invalid HTML.
216 libxml_use_internal_errors( true );
217
218 $post_dom = new DOMDocument();
219 $post_dom->loadHTML( '<?xml encoding="' . $this->charset . '">' . $content );
220
221 // Clear the errors, so they don't get kept in memory.
222 libxml_clear_errors();
223
224 /**
225 * Image attribute.
226 *
227 * @var DOMElement $img
228 */
229 foreach ( $post_dom->getElementsByTagName( 'img' ) as $img ) {
230
231 $src = $img->getAttribute( 'src' );
232
233 if ( empty( $src ) ) {
234 continue;
235 }
236
237 $class = $img->getAttribute( 'class' );
238
239 if ( // This detects WP-inserted images, which we need to upsize. R.
240 ! empty( $class )
241 && ( strpos( $class, 'size-full' ) === false )
242 && preg_match( '|wp-image-(?P<id>\d+)|', $class, $matches )
243 && get_post_status( $matches['id'] )
244 ) {
245 $query_params = wp_parse_url( $src, PHP_URL_QUERY );
246 $src = $this->image_url( $matches['id'] );
247
248 if ( $query_params ) {
249 $src .= '?' . $query_params;
250 }
251 }
252
253 $src = $this->get_absolute_url( $src );
254
255 if ( strpos( $src, $this->host ) === false ) {
256 continue;
257 }
258
259 if ( $src !== esc_url( $src, null, 'attribute' ) ) {
260 continue;
261 }
262
263 $images[] = [
264 'src' => $src,
265 ];
266 }
267
268 return $images;
269 }
270
271 /**
272 * Parse gallery shortcodes in a given content.
273 *
274 * @param string $content Content string.
275 * @param int $post_id Optional. ID of post being parsed.
276 *
277 * @return array Set of attachment objects.
278 */
279 protected function parse_galleries( $content, $post_id = 0 ) {
280
281 $attachments = [];
282 $galleries = $this->get_content_galleries( $content );
283
284 foreach ( $galleries as $gallery ) {
285
286 $id = $post_id;
287
288 if ( ! empty( $gallery['id'] ) ) {
289 $id = (int) $gallery['id'];
290 }
291
292 // Forked from core gallery_shortcode() to have exact same logic. R.
293 if ( ! empty( $gallery['ids'] ) ) {
294 $gallery['include'] = $gallery['ids'];
295 }
296
297 $gallery_attachments = $this->get_gallery_attachments( $id, $gallery );
298
299 $attachments = array_merge( $attachments, $gallery_attachments );
300 }
301
302 return array_unique( $attachments, SORT_REGULAR );
303 }
304
305 /**
306 * Retrieves galleries from the passed content.
307 *
308 * Forked from core to skip executing shortcodes for performance.
309 *
310 * @param string $content Content to parse for shortcodes.
311 *
312 * @return array A list of arrays, each containing gallery data.
313 */
314 protected function get_content_galleries( $content ) {
315
316 $galleries = [];
317
318 if ( ! preg_match_all( '/' . get_shortcode_regex( [ 'gallery' ] ) . '/s', $content, $matches, PREG_SET_ORDER ) ) {
319 return $galleries;
320 }
321
322 foreach ( $matches as $shortcode ) {
323
324 $attributes = shortcode_parse_atts( $shortcode[3] );
325
326 if ( $attributes === '' ) { // Valid shortcode without any attributes. R.
327 $attributes = [];
328 }
329
330 $galleries[] = $attributes;
331 }
332
333 return $galleries;
334 }
335
336 /**
337 * Get image item array with filters applied.
338 *
339 * @param WP_Post $post Post object for the context.
340 * @param string $src Image URL.
341 *
342 * @return array
343 */
344 protected function get_image_item( $post, $src ) {
345
346 $image = [];
347
348 /**
349 * Filter image URL to be included in XML sitemap for the post.
350 *
351 * @param string $src Image URL.
352 * @param object $post Post object.
353 */
354 $image['src'] = apply_filters( 'wpseo_xml_sitemap_img_src', $src, $post );
355
356 /**
357 * Filter image data to be included in XML sitemap for the post.
358 *
359 * @param array $image {
360 * Array of image data.
361 *
362 * @type string $src Image URL.
363 * }
364 *
365 * @param object $post Post object.
366 */
367 return apply_filters( 'wpseo_xml_sitemap_img', $image, $post );
368 }
369
370 /**
371 * Get attached image URL with filters applied. Adapted from core for speed.
372 *
373 * @param int $post_id ID of the post.
374 *
375 * @return string
376 */
377 private function image_url( $post_id ) {
378
379 static $uploads;
380
381 if ( empty( $uploads ) ) {
382 $uploads = wp_upload_dir();
383 }
384
385 if ( $uploads['error'] !== false ) {
386 return '';
387 }
388
389 $file = get_post_meta( $post_id, '_wp_attached_file', true );
390
391 if ( empty( $file ) ) {
392 return '';
393 }
394
395 // Check that the upload base exists in the file location.
396 if ( strpos( $file, $uploads['basedir'] ) === 0 ) {
397 $src = str_replace( $uploads['basedir'], $uploads['baseurl'], $file );
398 }
399 elseif ( strpos( $file, 'wp-content/uploads' ) !== false ) {
400 $src = $uploads['baseurl'] . substr( $file, ( strpos( $file, 'wp-content/uploads' ) + 18 ) );
401 }
402 else {
403 // It's a newly uploaded file, therefore $file is relative to the baseurl.
404 $src = $uploads['baseurl'] . '/' . $file;
405 }
406
407 return apply_filters( 'wp_get_attachment_url', $src, $post_id );
408 }
409
410 /**
411 * Make absolute URL for domain or protocol-relative one.
412 *
413 * @param string $src URL to process.
414 *
415 * @return string
416 */
417 protected function get_absolute_url( $src ) {
418
419 if ( empty( $src ) || ! is_string( $src ) ) {
420 return $src;
421 }
422
423 if ( YoastSEO()->helpers->url->is_relative( $src ) === true ) {
424
425 if ( $src[0] !== '/' ) {
426 return $src;
427 }
428
429 // The URL is relative, we'll have to make it absolute.
430 return $this->home_url . $src;
431 }
432
433 if ( strpos( $src, 'http' ) !== 0 ) {
434 // Protocol relative URL, we add the scheme as the standard requires a protocol.
435 return $this->scheme . ':' . $src;
436 }
437
438 return $src;
439 }
440
441 /**
442 * Returns the attachments for a gallery.
443 *
444 * @param int $id The post ID.
445 * @param array $gallery The gallery config.
446 *
447 * @return array The selected attachments.
448 */
449 protected function get_gallery_attachments( $id, $gallery ) {
450
451 // When there are attachments to include.
452 if ( ! empty( $gallery['include'] ) ) {
453 return $this->get_gallery_attachments_for_included( $gallery['include'] );
454 }
455
456 // When $id is empty, just return empty array.
457 if ( empty( $id ) ) {
458 return [];
459 }
460
461 return $this->get_gallery_attachments_for_parent( $id, $gallery );
462 }
463
464 /**
465 * Returns the attachments for the given ID.
466 *
467 * @param int $id The post ID.
468 * @param array $gallery The gallery config.
469 *
470 * @return array The selected attachments.
471 */
472 protected function get_gallery_attachments_for_parent( $id, $gallery ) {
473 $query = [
474 'posts_per_page' => -1,
475 'post_parent' => $id,
476 ];
477
478 // When there are posts that should be excluded from result set.
479 if ( ! empty( $gallery['exclude'] ) ) {
480 $query['post__not_in'] = wp_parse_id_list( $gallery['exclude'] );
481 }
482
483 return $this->get_attachments( $query );
484 }
485
486 /**
487 * Returns an array with attachments for the post IDs that will be included.
488 *
489 * @param array $included_ids Array with IDs to include.
490 *
491 * @return array The found attachments.
492 */
493 protected function get_gallery_attachments_for_included( $included_ids ) {
494 $ids_to_include = wp_parse_id_list( $included_ids );
495 $attachments = $this->get_attachments(
496 [
497 'posts_per_page' => count( $ids_to_include ),
498 'post__in' => $ids_to_include,
499 ],
500 );
501
502 $gallery_attachments = [];
503 foreach ( $attachments as $val ) {
504 $gallery_attachments[ $val->ID ] = $val;
505 }
506
507 return $gallery_attachments;
508 }
509
510 /**
511 * Returns the attachments.
512 *
513 * @param array $args Array with query args.
514 *
515 * @return array The found attachments.
516 */
517 protected function get_attachments( $args ) {
518 $default_args = [
519 'post_status' => 'inherit',
520 'post_type' => 'attachment',
521 'post_mime_type' => 'image',
522
523 // Defaults taken from function get_posts.
524 'orderby' => 'date',
525 'order' => 'DESC',
526 'meta_key' => '',
527 'meta_value' => '',
528 'suppress_filters' => true,
529 'ignore_sticky_posts' => true,
530 'no_found_rows' => true,
531 ];
532
533 $args = wp_parse_args( $args, $default_args );
534
535 $get_attachments = new WP_Query();
536 return $get_attachments->query( $args );
537 }
538 }
539