PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.2
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.2
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 28.2, at inc/sitemaps/class-sitemap-image-parser.php

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