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

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