PluginProbe ʕ •ᴥ•ʔ
WP Popular Posts / 7.3.4
WP Popular Posts v7.3.4
4.0.8 4.0.9 4.1.0 4.1.1 4.1.2 4.2.0 4.2.1 4.2.2 5.0.0 5.0.1 5.0.2 5.1.0 5.2.0 5.2.1 5.2.2 5.2.3 5.2.4 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 6.0.0 6.0.1 6.0.2 6.0.3 6.0.4 6.0.5 6.1.0 6.1.1 6.1.2 6.1.3 6.1.4 6.2.0 6.2.1 6.3.0 6.3.1 6.3.2 6.3.3 6.3.4 6.4.0 6.4.1 6.4.2 7.0.0 7.0.1 7.1.0 7.2.0 7.3.0 7.3.1 7.3.2 7.3.3 7.3.4 7.3.5 7.3.6 7.3.7 7.3.8 7.4.0 trunk 2.3.7 3.0.0 3.0.1 3.0.2 3.0.3 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.2 4.0.3 4.0.5 4.0.6
wordpress-popular-posts / src / Image.php
wordpress-popular-posts / src Last commit date
Activation 8 months ago Admin 8 months ago Block 8 months ago Compatibility 8 months ago Container 8 months ago Front 8 months ago Rest 8 months ago Shortcode 8 months ago Traits 8 months ago Widget 8 months ago Bootstrap.php 8 months ago Cache.php 8 months ago Helper.php 8 months ago Image.php 8 months ago Output.php 8 months ago Query.php 8 months ago Settings.php 8 months ago Themer.php 8 months ago Translate.php 8 months ago Upgrader.php 8 months ago WordPressPopularPosts.php 8 months ago deprecated.php 8 months ago template-tags.php 8 months ago
Image.php
963 lines
1 <?php
2 /**
3 * This class builds/retrieves the thumbnail image of each popular posts.
4 *
5 *
6 * @package WordPressPopularPosts
7 * @author Hector Cabrera <me@cabrerahector.com>
8 */
9
10 namespace WordPressPopularPosts;
11
12 class Image {
13
14 /**
15 * Default thumbnail.
16 *
17 * @since 2.2.0
18 * @var string
19 */
20 private $default_thumbnail = '';
21
22 /**
23 * Plugin uploads directory.
24 *
25 * @since 3.0.4
26 * @var array
27 */
28 private $uploads_dir = [];
29
30 /**
31 * Admin settings.
32 *
33 * @since 5.0.0
34 * @var array
35 */
36 private $admin_options = [];
37
38 /**
39 * Available image sizes.
40 *
41 * @since 5.0.0
42 * @var array
43 */
44 private $available_sizes = [];
45
46 /**
47 * Available image descriptors.
48 *
49 * @since 5.3.0
50 * @var array
51 */
52 private $descriptors = [];
53
54 /**
55 * Construct.
56 *
57 * @since 4.0.0
58 * @param array $admin_options
59 */
60 public function __construct(array $admin_options)
61 {
62 $this->admin_options = $admin_options;
63
64 // Set default thumbnail
65 $this->default_thumbnail = plugins_url('assets/images/no_thumb.jpg', dirname(__FILE__, 1));
66
67 if ( Helper::is_image_url($this->admin_options['tools']['thumbnail']['default']) ) {
68 $this->default_thumbnail = $this->admin_options['tools']['thumbnail']['default'];
69 }
70
71 // Set uploads folder
72 $wp_upload_dir = wp_get_upload_dir();
73 $this->uploads_dir['basedir'] = $wp_upload_dir['basedir'] . '/wordpress-popular-posts';
74 $this->uploads_dir['baseurl'] = $wp_upload_dir['baseurl'] . '/wordpress-popular-posts';
75
76 if ( ! is_dir($this->uploads_dir['basedir']) ) {
77 // Couldn't create the folder, store thumbnails in Uploads
78 if ( ! wp_mkdir_p($this->uploads_dir['basedir']) ) {
79 $this->uploads_dir['basedir'] = $wp_upload_dir['basedir'];
80 $this->uploads_dir['baseurl'] = $wp_upload_dir['baseurl'];
81 }
82 }
83
84 // Set descriptors
85 $this->descriptors = ['1.5', '2', '2.5', '3'];
86 }
87
88 /**
89 * Get WPP's uploads folder.
90 *
91 * @since 4.0.0
92 * @access public
93 * @return array|bool
94 */
95 public function get_plugin_uploads_dir()
96 {
97 if ( is_array($this->uploads_dir) && ! empty($this->uploads_dir) ) {
98 return $this->uploads_dir;
99 }
100 return false;
101 }
102
103 /**
104 * Returns an image.
105 *
106 * @since 5.0.0
107 * @param int $post_id Post ID
108 * @param array $size Image size (width & height)
109 * @param string $source Image source
110 * @param bool|array $crop Whether to crop the image or not, and if so how
111 * @param string $build Whether to build the image or get an existing one
112 * @return string
113 */
114 public function get(int $post_id, array $size, string $source, $crop = true, ?string $build = 'manual') /** @TODO: starting PHP 8.0 $crop can be declared as mixed $crop */
115 {
116 // Bail, $post_id is not an integer
117 if ( ! is_numeric($post_id) ) {
118 return '';
119 }
120
121 $alt = '';
122 $classes = ['wpp-thumbnail', 'wpp_' . $source];
123 $filename = $post_id . '-' . $source . '-' . $size[0] . 'x' . $size[1];
124 $cached = $this->exists($filename);
125
126 // We have a thumbnail already, return it
127 if ( $cached ) {
128 $classes[] = 'wpp_cached_thumb';
129
130 /**
131 * Filters CSS classes assigned to the thumbnail
132 *
133 * @since 5.0.0
134 * @param array CSS classes
135 * @param int The post ID
136 * @return array The new CSS classes
137 */
138 $classes = apply_filters(
139 'wpp_thumbnail_class_attribute',
140 $classes,
141 $post_id
142 );
143
144 /**
145 * Filters ALT attribute assigned to the thumbnail
146 *
147 * @since 5.0.0
148 * @param string Original ALT attribute
149 * @param int The post ID
150 * @return string The new ALT attribute
151 */
152 $alt = apply_filters(
153 'wpp_thumbnail_alt_attribute',
154 $this->get_alt_attribute($post_id, $source),
155 $post_id
156 );
157
158 return $this->render(
159 $post_id,
160 $cached,
161 $size,
162 is_array($classes) ? implode(' ', $classes) : 'wpp-thumbnail wpp_' . $source,
163 is_string($alt) ? $alt : ''
164 );
165 }
166
167 $thumb_url = null;
168
169 // Return image as-is, no need to create a new thumbnail
170 if (
171 ( 'custom_field' == $source && ! $this->admin_options['tools']['thumbnail']['resize'] )
172 || ( 'featured' == $source && 'predefined' == $build )
173 ){
174 // Get custom field image URL
175 if ( 'custom_field' == $source && ! $this->admin_options['tools']['thumbnail']['resize'] ) {
176 $thumb_url = get_post_meta(
177 $post_id,
178 $this->admin_options['tools']['thumbnail']['field'],
179 true
180 );
181
182 if ( ! $thumb_url || ! Helper::is_image_url($thumb_url) ) {
183 // Is this an attachment ID instead of an image URL?
184 if ( Helper::is_number($thumb_url) ) {
185 $thumb_url = wp_get_attachment_image_src($thumb_url, 'full');
186 $thumb_url = is_array($thumb_url) ? $thumb_url[0] : null;
187 } else {
188 $thumb_url = null;
189 }
190 }
191 }
192 // Get Post Thumbnail
193 else {
194 if (
195 current_theme_supports('post-thumbnails')
196 && has_post_thumbnail($post_id)
197 ) {
198 // Find corresponding image size
199 $stock_size = null;
200 $images_sizes = $this->get_sizes(null);
201
202 foreach ( $images_sizes as $name => $attr ) :
203 if (
204 $attr['width'] == $size[0]
205 && $attr['height'] == $size[1]
206 && $attr['crop'] == $crop
207 ) {
208 $stock_size = $name;
209 break;
210 }
211 endforeach;
212
213 // Couldn't find a matching size so let's go with width/height combo instead
214 // (this should never happen but better safe than sorry!)
215 if ( null == $stock_size ) {
216 $stock_size = $size;
217 }
218
219 /**
220 * Filters CSS classes assigned to the thumbnail
221 *
222 * @since 5.0.0
223 * @param array CSS classes
224 * @param int The post ID
225 * @return array The new CSS classes
226 */
227 $classes = apply_filters(
228 'wpp_thumbnail_class_attribute',
229 $classes,
230 $post_id
231 );
232
233 $featured_image = get_the_post_thumbnail(
234 $post_id,
235 $stock_size
236 );
237
238 if ( strpos($featured_image, 'class="') && is_array($classes) && ! empty($classes) ) {
239 $featured_image = str_replace('class="', 'class="' . esc_attr(implode(' ', $classes)) . ' ', $featured_image);
240 }
241
242 if ( $this->admin_options['tools']['thumbnail']['lazyload'] && false == strpos($featured_image, 'loading="lazy"') ) {
243 $featured_image = str_replace('src="', 'loading="lazy" src="', $featured_image);
244 }
245
246 return $featured_image;
247 }
248 }
249 }
250 // Build a new thumbnail and return it
251 else {
252 $file_path = null;
253
254 if ( 'custom_field' == $source && $this->admin_options['tools']['thumbnail']['resize'] ) {
255 $thumb_url = get_post_meta(
256 $post_id,
257 $this->admin_options['tools']['thumbnail']['field'],
258 true
259 );
260
261 if ( ! $thumb_url || ! Helper::is_image_url($thumb_url) ) {
262 // Is this an attachment ID instead of an image URL?
263 // If so, try to fetch the image
264 if ( Helper::is_number($thumb_url) ) {
265 $thumb_url = wp_get_attachment_image_src($thumb_url, 'full');
266 $thumb_url = is_array($thumb_url) ? $thumb_url[0] : null;
267 } else {
268 $thumb_url = null;
269 }
270 }
271
272 if ( $thumb_url && Helper::is_image_url($thumb_url) ) {
273 $file_path = $this->url_to_path($thumb_url, $post_id);
274 }
275 } else {
276 $file_meta = $this->get_file_meta($post_id, $source);
277
278 if ( is_array($file_meta) && isset($file_meta['path']) ) {
279 $alt = isset($file_meta['alt']) ? $file_meta['alt'] : '';
280 $file_path = $file_meta['path'];
281 }
282 }
283
284 if ( $file_path ) {
285 $extension = pathinfo($file_path, PATHINFO_EXTENSION);
286 $thumb_url = $this->resize(
287 $file_path,
288 $filename . '.' . $extension,
289 $size,
290 $crop
291 );
292 }
293 }
294
295 if ( ! $thumb_url ) {
296 $classes[] = 'wpp_def_no_src';
297 $thumb_url = $this->get_default_url($post_id);
298 }
299
300 /**
301 * Filters CSS classes assigned to the thumbnail
302 *
303 * @since 5.0.0
304 * @param array CSS classes
305 * @param int The post ID
306 * @return array The new CSS classes
307 */
308 $classes = apply_filters(
309 'wpp_thumbnail_class_attribute',
310 $classes,
311 $post_id
312 );
313
314 /**
315 * Filters ALT attribute assigned to the thumbnail
316 *
317 * @since 5.0.0
318 * @param string Original ALT attribute
319 * @param int The post ID
320 * @return string The new ALT attribute
321 */
322 $alt = apply_filters(
323 'wpp_thumbnail_alt_attribute',
324 $this->get_alt_attribute($post_id, $source),
325 $post_id
326 );
327
328 return $this->render(
329 $post_id,
330 $thumb_url,
331 $size,
332 is_array($classes) ? implode(' ', $classes) : 'wpp-thumbnail wpp_' . $source,
333 is_string($alt) ? $alt : ''
334 );
335 }
336
337 /**
338 * Checks whether a given thumbnail exists.
339 *
340 * @since 5.0.0
341 * @access private
342 * @param string $filename
343 * @return string|bool Full URL to image
344 */
345 private function exists(string $filename)
346 {
347 // Do we have thumbnail already?
348 $file = $this->resolve(trailingslashit($this->get_plugin_uploads_dir()['basedir']) . $filename);
349
350 if ( $file && is_file($file) ) {
351 $extension = pathinfo($file, PATHINFO_EXTENSION);
352 return trailingslashit($this->get_plugin_uploads_dir()['baseurl']) . $filename . '.' . $extension;
353 }
354
355 return false;
356 }
357
358 /**
359 * Resolves filename.
360 *
361 * @since 5.0.0
362 * @access private
363 * @author Ioan Chiriac
364 * @link https://stackoverflow.com/a/29468093/9131961
365 * @param string $name
366 * @return string|bool Resolved path, or false if not found
367 */
368 private function resolve(string $name)
369 {
370 $info = pathinfo($name);
371
372 // File already contains an extension, return it
373 if ( isset($info['extension']) && ! empty($info['extension']) ) {
374 return $name;
375 }
376
377 $filename = $info['filename'];
378 $len = strlen($filename);
379
380 // open the folder
381 $dh = opendir($info['dirname']);
382
383 if ( ! $dh ) {
384 return false;
385 }
386
387 // scan each file in the folder
388 while ( ($file = readdir($dh)) !== false ) {
389 if ( strncmp($file, $filename, $len) === 0 ) {
390 if ( strlen($name) > $len ) {
391 // if name contains a directory part
392 $name = substr($name, 0, strlen($name) - $len) . $file;
393 } else {
394 // if the name is at the path root
395 $name = $file;
396 }
397
398 closedir($dh);
399 return $name;
400 }
401 }
402
403 // file not found
404 closedir($dh);
405 return false;
406 }
407
408 /**
409 * Retrieves local path to image.
410 *
411 * @since 5.0.0
412 * @access private
413 * @param string $url
414 * @param integer $post_ID
415 * @return string|boolean Path to image, or false if not found
416 */
417 private function url_to_path(string $url, ?int $post_ID)
418 {
419 if ( Helper::is_image_url($url) ) {
420 $attachment_id = $this->get_attachment_id($url);
421
422 // Image is hosted locally
423 if ( $attachment_id ) {
424 return get_attached_file($attachment_id);
425 }
426
427 // Image hosted elsewhere?
428 if ( $post_ID && Helper::is_number($post_ID) ) {
429 return $this->fetch_external_image($post_ID, $url);
430 }
431 }
432
433 return false;
434 }
435
436 /**
437 * Gets image meta.
438 *
439 * @since 5.0.0
440 * @access private
441 * @param int $id Post ID
442 * @param string $source Image source
443 * @return array|bool
444 */
445 private function get_file_meta(int $id, string $source)
446 {
447 // get thumbnail path from the Featured Image
448 if ( 'featured' == $source ) {
449 $thumbnail_id = get_post_thumbnail_id($id);
450
451 if ( $thumbnail_id ) {
452 // image path
453 return [
454 'path' => get_attached_file($thumbnail_id),
455 'alt' => get_post_meta($thumbnail_id, '_wp_attachment_image_alt', true)
456 ];
457 }
458 }
459 // get thumbnail path from first image attachment
460 elseif ( 'first_attachment' == $source ) {
461 $args = [
462 'numberposts' => 1,
463 'order' => 'ASC',
464 'post_parent' => $id,
465 'post_type' => 'attachment',
466 'post_mime_type' => 'image'
467 ];
468 $post_attachments = get_children($args);
469
470 if ( ! empty($post_attachments) ) {
471 $first_img = array_shift($post_attachments);
472
473 return [
474 'path' => get_attached_file($first_img->ID),
475 'alt' => get_post_meta($first_img->ID, '_wp_attachment_image_alt', true)
476 ];
477 }
478 }
479 // get thumbnail path from post content
480 elseif ( 'first_image' == $source ) {
481 $content = Helper::get_post_content($id);
482
483 if ( $content ) {
484 // at least one image has been found
485 if ( preg_match('/<img[^>]+>/i', $content, $img) ) {
486 // get img src attribute from the first image found
487 preg_match('/(src)="([^"]*)"/i', $img[0], $src_attr);
488
489 if ( isset($src_attr[2]) && ! empty($src_attr[2]) ) {
490 // get img alt attribute from the first image found
491 $alt = '';
492 preg_match('/(alt)="([^"]*)"/i', $img[0], $alt_attr);
493
494 if ( isset($alt_attr[2]) && ! empty($alt_attr[2]) ) {
495 $alt = $alt_attr[2];
496 }
497
498 // image from Media Library
499 $attachment_id = $this->get_attachment_id($src_attr[2]);
500
501 if ( $attachment_id ) {
502 return [
503 'path' => get_attached_file($attachment_id),
504 'alt' => $alt
505 ];
506 } // external image?
507 else {
508 return [
509 'path' => $this->fetch_external_image($id, $src_attr[2]),
510 'alt' => $alt
511 ];
512 }
513 }
514 }
515 }
516 }
517
518 return false;
519 }
520
521 /**
522 * Gets image ALT attribute.
523 *
524 * @since 5.0.0
525 * @access private
526 * @param int $id Post ID
527 * @param string $source Image source
528 * @return string
529 */
530 private function get_alt_attribute(int $id, string $source)
531 {
532 $alt = '';
533
534 // get thumbnail path from the Featured Image
535 if ( 'featured' == $source ) {
536 $thumbnail_id = get_post_thumbnail_id($id);
537
538 if ( $thumbnail_id ) {
539 // image path
540 $alt = get_post_meta($thumbnail_id, '_wp_attachment_image_alt', true);
541 }
542 }
543 // get thumbnail path from first image attachment
544 elseif ( 'first_attachment' == $source ) {
545 $args = [
546 'numberposts' => 1,
547 'order' => 'ASC',
548 'post_parent' => $id,
549 'post_type' => 'attachment',
550 'post_mime_type' => 'image'
551 ];
552 $post_attachments = get_children($args);
553
554 if ( ! empty($post_attachments) ) {
555 $first_img = array_shift($post_attachments);
556 $alt = get_post_meta($first_img->ID, '_wp_attachment_image_alt', true);
557 }
558 }
559 // get thumbnail path from post content
560 elseif ( 'first_image' == $source ) {
561 $content = Helper::get_post_content($id);
562
563 if ( $content ) {
564 // at least one image has been found
565 if ( preg_match('/<img[^>]+>/i', $content, $img) ) {
566 // get img alt attribute from the first image found
567 preg_match('/(alt)="([^"]*)"/i', $img[0], $alt_attr);
568
569 if ( isset($alt_attr[2]) && ! empty($alt_attr[2]) ) {
570 $alt = $alt_attr[2];
571 }
572 }
573 }
574 }
575
576 return $alt;
577 }
578
579 /**
580 * Get the Attachment ID for a given image URL.
581 *
582 * @since 3.0.0
583 * @access private
584 * @author Frankie Jarrett
585 * @link http://frankiejarrett.com/get-an-attachment-id-by-url-in-wordpress/
586 * @param string $url
587 * @return int|null
588 */
589 private function get_attachment_id(string $url)
590 {
591 $url = Helper::add_scheme(
592 $url,
593 is_ssl() ? 'https://' : 'http://'
594 );
595
596 // Split the $url into two parts with the wp-content directory as the separator.
597 $parse_url = explode(parse_url(WP_CONTENT_URL, PHP_URL_PATH), $url);
598
599 // Get the host of the current site and the host of the $url, ignoring www.
600 $this_host = str_ireplace('www.', '', parse_url(home_url(), PHP_URL_HOST));
601 $file_host = str_ireplace('www.', '', parse_url($url, PHP_URL_HOST));
602
603 // Return nothing if there aren't any $url parts or if the current host and $url host do not match.
604 if (
605 ! isset($parse_url[1])
606 || empty($parse_url[1])
607 || ($this_host != $file_host)
608 ) {
609 return false;
610 }
611
612 // Now we're going to quickly search the DB for any attachment GUID with a partial path match.
613 // Example: /uploads/2013/05/test-image.jpg
614 global $wpdb;
615 $posts_table = "{$wpdb->prefix}posts";
616
617 $attachment = $wpdb->get_col($wpdb->prepare("SELECT ID FROM %i WHERE guid RLIKE %s;", $posts_table, $parse_url[1])); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
618
619 if ( ! $attachment ) {
620 // Maybe it's a resized image, so try to get the full one
621 $parse_url[1] = preg_replace('/-[0-9]{1,4}x[0-9]{1,4}\.(jpg|jpeg|png|gif|bmp)$/i', '.$1', $parse_url[1]);
622 $attachment = $wpdb->get_col($wpdb->prepare("SELECT ID FROM %i WHERE guid RLIKE %s;", $posts_table, $parse_url[1])); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
623 }
624
625 // Returns null if no attachment is found.
626 return isset($attachment[0]) ? $attachment[0] : null;
627 }
628
629 /**
630 * Fetchs external images.
631 *
632 * @since 2.3.3
633 * @access private
634 * @param int $id Post ID.
635 * @param string $url Image url.
636 * @return string|bool Image path, or false on failure.
637 */
638 private function fetch_external_image(int $id, string $url)
639 {
640 if ( ! Helper::is_image_url($url) ) {
641 return false;
642 }
643
644 $full_image_path = trailingslashit($this->get_plugin_uploads_dir()['basedir']) . "{$id}_" . sanitize_file_name(rawurldecode(wp_basename($url)));
645
646 // if the file exists already, return URL and path
647 if ( file_exists($full_image_path) ) {
648 return $full_image_path;
649 }
650
651 $url = Helper::add_scheme(
652 $url,
653 is_ssl() ? 'https://' : 'http://'
654 );
655
656 $accepted_status_codes = [200, 301, 302];
657 $response = wp_remote_head($url, ['timeout' => 5, 'sslverify' => false]);
658
659 if (
660 ! is_wp_error($response)
661 && in_array(wp_remote_retrieve_response_code($response), $accepted_status_codes)
662 ) {
663 require_once ABSPATH . 'wp-admin/includes/file.php';
664
665 $url = str_replace('https://', 'http://', $url);
666 $tmp = download_url($url);
667
668 // File was downloaded successfully
669 if ( ! is_wp_error($tmp) ) {
670 // Determine image type
671 if ( function_exists('exif_imagetype') ) {
672 $image_type = exif_imagetype($tmp);
673 } else {
674 $image_type = getimagesize($tmp);
675 $image_type = ( isset($image_type[2]) ) ? $image_type[2] : null;
676 }
677
678 // Valid image, save it
679 if ( in_array($image_type, [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_WEBP, IMAGETYPE_AVIF]) ) {
680 // move file to Uploads
681 if ( @rename($tmp, $full_image_path) ) {
682 // borrowed from WP - set correct file permissions
683 $stat = stat(dirname($full_image_path));
684 $perms = $stat['mode'] & 0000644;
685 @chmod($full_image_path, $perms);
686
687 return $full_image_path;
688 }
689 }
690
691 // Invalid file, remove it
692 @unlink($tmp);
693 }
694 }
695
696 return false;
697 }
698
699 /**
700 * Creates thumbnails.
701 *
702 * @since 3.0.0
703 * @access private
704 * @param string $path Image path
705 * @param string $filename Image filename
706 * @param array $size Image size
707 * @param bool $crop Whether to crop the image or not
708 * @return string|bool Image URL on success, false on error
709 */
710 private function resize(string $path, string $filename, array $size, bool $crop = true)
711 {
712 $image = wp_get_image_editor($path);
713
714 // valid image, create thumbnails
715 if ( ! is_wp_error($image) ) {
716 $original_size = $image->get_size();
717 $sizes = [
718 '1x' => $size
719 ];
720 $thumbnail = '';
721
722 /**
723 * Hook to enable/disable retina support.
724 * @since 5.3.0
725 */
726 $retina_support = apply_filters('wpp_retina_support', true);
727
728 if ( $retina_support ) {
729 // Calculate thumbnail sizes
730 foreach( $this->descriptors as $descriptor ) {
731 $new_size_width = floor($descriptor * $size[0]);
732 $new_size_height = floor($descriptor * $size[1]);
733
734 if (
735 $new_size_width <= $original_size['width']
736 && $new_size_height <= $original_size['height']
737 ) {
738 $sizes[$descriptor . 'x'] = [$new_size_width, $new_size_height];
739 }
740 }
741 }
742
743 $path_parts = null;
744
745 // Generate thumbnails
746 foreach( $sizes as $d => $s ) {
747 if ( '1x' == $d ) {
748 $thumbnail = $this->generate_thumbnail($path, $filename, $s, $crop);
749
750 // Image could not be generated, let's bail early.
751 if ( ! $thumbnail ) {
752 break;
753 }
754 } else {
755 if ( ! $path_parts ) {
756 $path_parts = pathinfo($filename);
757 }
758
759 $filename_with_descriptor = $path_parts['filename'] . "@{$d}." . $path_parts['extension'];
760 $this->generate_thumbnail($path, $filename_with_descriptor, $s, $crop);
761 }
762 }
763
764 return $thumbnail;
765 }
766
767 return false;
768 }
769
770 /**
771 * Creates image.
772 *
773 * @since 5.3.0
774 * @access private
775 * @param string $path Image path
776 * @param string $filename Image filename
777 * @param array $size Image size
778 * @param bool $crop Whether to crop the image or not
779 * @return string|bool Image URL on success, false on error
780 */
781 private function generate_thumbnail(string $path, string $filename, array $size, bool $crop = true)
782 {
783 $image = wp_get_image_editor($path);
784
785 // valid image, create thumbnail
786 if ( ! is_wp_error($image) ) {
787 /**
788 * Hook to change the image compression quality of WPP's thumbnails.
789 * @since 4.2.1
790 */
791 $quality = apply_filters('wpp_thumbnail_compression_quality', null);
792
793 if ( $quality && ! ctype_digit( (string) $quality) ) {
794 $quality = null; // Fallback to core's default
795 }
796
797 $image->set_quality($quality);
798 $image->resize($size[0], $size[1], $crop);
799
800 $mime_type = null;
801
802 // .webp thumbnails?
803 if (
804 'webp' === $this->admin_options['tools']['thumbnail']['format']
805 && $image->supports_mime_type('image/webp') // .webp support requires WP 5.8 or higher
806 ) {
807 $filename = substr($filename, 0, strrpos($filename, '.')) . '.webp';
808 $mime_type = 'image/webp';
809 }
810 // .avif thumbnails?
811 elseif (
812 'avif' === $this->admin_options['tools']['thumbnail']['format']
813 && $image->supports_mime_type('image/avif') // .avif support requires WP 6.5 or higher
814 ) {
815 $filename = substr($filename, 0, strrpos($filename, '.')) . '.avif';
816 $mime_type = 'image/avif';
817 }
818
819 $file_path = trailingslashit($this->get_plugin_uploads_dir()['basedir']) . $filename;
820 $new_img = $image->save($file_path, $mime_type);
821
822 if ( ! is_wp_error($new_img) ) {
823 return trailingslashit($this->get_plugin_uploads_dir()['baseurl']) . $filename;
824 }
825 }
826
827 return false;
828 }
829
830 /**
831 * Generates srcset attribute for this image.
832 *
833 * @since 5.3.0
834 * @param string $src
835 * @return string
836 */
837 private function get_srcset(string $src)
838 {
839 /**
840 * Hook to enable/disable retina support.
841 * @since 5.3.0
842 */
843 $retina_support = apply_filters('wpp_retina_support', true);
844
845 if ( ! $retina_support ) {
846 return '';
847 }
848
849 $path_parts = pathinfo($src);
850 $srcset = [$src];
851
852 foreach( $this->descriptors as $descriptor ) {
853 $d = "{$descriptor}x";
854 $filename = $path_parts['filename'] . "@{$d}." . $path_parts['extension'];
855
856 if ( @file_exists(trailingslashit($this->get_plugin_uploads_dir()['basedir']) . $filename) ) {
857 $srcset[] = $path_parts['dirname'] . '/' . $filename . ' ' . $d;
858 }
859 }
860
861 return ( count($srcset) > 1 ) ? ' srcset="' . implode(', ', $srcset) . '" ' : '';
862 }
863
864 /**
865 * Render image tag.
866 *
867 * @since 3.0.0
868 * @access public
869 * @param int $post_id The post/page ID
870 * @param string $src Image URL
871 * @param array $dimension Image's width and height
872 * @param string $class CSS class
873 * @param object $alt Alternative text
874 * @param string $error Error, if the image could not be created
875 * @return string
876 */
877 public function render(int $post_id, string $src, array $size, string $class, string $alt = '', string $error = '')
878 {
879 $img_tag = '';
880
881 if ( $error ) {
882 $img_tag = '<!-- ' . $error . ' --> ';
883 }
884
885 // Make sure we use the right protocol
886 $src = esc_url(is_ssl() ? str_ireplace('http://', 'https://', $src) : $src);
887 // Get srcset, if available
888 $srcset = $this->get_srcset($src);
889
890 $src = 'src="' . $src . '"' . $srcset;
891
892 // Lazy Load attribute, if enabled
893 $lazyload = ( $this->admin_options['tools']['thumbnail']['lazyload'] ) ? ' loading="lazy"' : '';
894
895 $img_tag .= '<img ' . $src . ' width="' . esc_attr($size[0]) . '" height="' . esc_attr($size[1]) . '" alt="' . esc_attr($alt) . '" class="' . esc_attr($class) . '" decoding="async" ' . $lazyload . ' />';
896
897 return apply_filters('wpp_render_image', $img_tag, $post_id);
898 }
899
900 /**
901 * Gets list of available thumbnails sizes
902 *
903 * @since 3.2.0
904 * @link http://codex.wordpress.org/Function_Reference/get_intermediate_image_sizes
905 * @param string $size
906 * @return array|bool
907 */
908 public function get_sizes(?string $size)
909 {
910 if ( ! is_array($this->available_sizes) || empty($this->available_sizes) ) {
911 global $_wp_additional_image_sizes;
912
913 $this->available_sizes = [];
914 $get_intermediate_image_sizes = get_intermediate_image_sizes();
915
916 // Create the full array with sizes and crop info
917 foreach( $get_intermediate_image_sizes as $_size ) {
918 if ( in_array($_size, ['thumbnail', 'medium', 'large']) ) {
919 $this->available_sizes[$_size]['width'] = get_option($_size . '_size_w');
920 $this->available_sizes[$_size]['height'] = get_option($_size . '_size_h');
921 $this->available_sizes[$_size]['crop'] = (bool) get_option($_size . '_crop');
922 } elseif ( isset($_wp_additional_image_sizes[$_size]) ) {
923 $this->available_sizes[$_size] = [
924 'width' => $_wp_additional_image_sizes[$_size]['width'],
925 'height' => $_wp_additional_image_sizes[$_size]['height'],
926 'crop' => $_wp_additional_image_sizes[$_size]['crop']
927 ];
928 }
929 }
930 }
931
932 // Get only 1 size if found
933 if ( $size ) {
934 if ( isset($this->available_sizes[$size]) ) {
935 return $this->available_sizes[$size];
936 }
937 return false;
938 }
939
940 return $this->available_sizes;
941 }
942
943 /**
944 * Returns the URL of the default thumbnail image.
945 *
946 * @since 5.0.0
947 * @param int|null
948 * @return string
949 */
950 public function get_default_url(?int $post_ID = null)
951 {
952 if ( has_filter('wpp_default_thumbnail_url') ) {
953 $default_thumbnail_url = apply_filters('wpp_default_thumbnail_url', $this->default_thumbnail, $post_ID);
954
955 if ( $default_thumbnail_url != $this->default_thumbnail && Helper::is_image_url($default_thumbnail_url) ) {
956 return $default_thumbnail_url;
957 }
958 }
959
960 return $this->default_thumbnail;
961 }
962 }
963