PluginProbe ʕ •ᴥ•ʔ
WP Popular Posts / 7.0.1
WP Popular Posts v7.0.1
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 1 year ago Admin 1 year ago Block 1 year ago Compatibility 1 year ago Container 1 year ago Front 1 year ago Rest 1 year ago Shortcode 1 year ago Traits 1 year ago Widget 1 year ago Bootstrap.php 1 year ago Cache.php 1 year ago Helper.php 1 year ago I18N.php 1 year ago Image.php 1 year ago Output.php 1 year ago Query.php 1 year ago Settings.php 1 year ago Themer.php 1 year ago Translate.php 1 year ago WordPressPopularPosts.php 1 year ago deprecated.php 1 year ago template-tags.php 1 year ago
Image.php
982 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 /** @var wpdb $wpdb */
482 global $wpdb;
483
484 //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
485 $content = $wpdb->get_var(
486 $wpdb->prepare(
487 "SELECT post_content FROM {$wpdb->posts} WHERE ID = %d;",
488 $id
489 )
490 );
491 //phpcs:enable
492
493 if ( $content ) {
494 // at least one image has been found
495 if ( preg_match('/<img[^>]+>/i', $content, $img) ) {
496 // get img src attribute from the first image found
497 preg_match('/(src)="([^"]*)"/i', $img[0], $src_attr);
498
499 if ( isset($src_attr[2]) && ! empty($src_attr[2]) ) {
500 // get img alt attribute from the first image found
501 $alt = '';
502 preg_match('/(alt)="([^"]*)"/i', $img[0], $alt_attr);
503
504 if ( isset($alt_attr[2]) && ! empty($alt_attr[2]) ) {
505 $alt = $alt_attr[2];
506 }
507
508 // image from Media Library
509 $attachment_id = $this->get_attachment_id($src_attr[2]);
510
511 if ( $attachment_id ) {
512 return [
513 'path' => get_attached_file($attachment_id),
514 'alt' => $alt
515 ];
516 } // external image?
517 else {
518 return [
519 'path' => $this->fetch_external_image($id, $src_attr[2]),
520 'alt' => $alt
521 ];
522 }
523 }
524 }
525 }
526 }
527
528 return false;
529 }
530
531 /**
532 * Gets image ALT attribute.
533 *
534 * @since 5.0.0
535 * @access private
536 * @param int $id Post ID
537 * @param string $source Image source
538 * @return string
539 */
540 private function get_alt_attribute(int $id, string $source)
541 {
542 $alt = '';
543
544 // get thumbnail path from the Featured Image
545 if ( 'featured' == $source ) {
546 $thumbnail_id = get_post_thumbnail_id($id);
547
548 if ( $thumbnail_id ) {
549 // image path
550 $alt = get_post_meta($thumbnail_id, '_wp_attachment_image_alt', true);
551 }
552 }
553 // get thumbnail path from first image attachment
554 elseif ( 'first_attachment' == $source ) {
555 $args = [
556 'numberposts' => 1,
557 'order' => 'ASC',
558 'post_parent' => $id,
559 'post_type' => 'attachment',
560 'post_mime_type' => 'image'
561 ];
562 $post_attachments = get_children($args);
563
564 if ( ! empty($post_attachments) ) {
565 $first_img = array_shift($post_attachments);
566 $alt = get_post_meta($first_img->ID, '_wp_attachment_image_alt', true);
567 }
568 }
569 // get thumbnail path from post content
570 elseif ( 'first_image' == $source ) {
571 /** @var wpdb $wpdb */
572 global $wpdb;
573
574 //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
575 $content = $wpdb->get_var(
576 $wpdb->prepare(
577 "SELECT post_content FROM {$wpdb->posts} WHERE ID = %d;",
578 $id
579 )
580 );
581 //phpcs:enable
582
583 if ( $content ) {
584 // at least one image has been found
585 if ( preg_match('/<img[^>]+>/i', $content, $img) ) {
586 // get img alt attribute from the first image found
587 preg_match('/(alt)="([^"]*)"/i', $img[0], $alt_attr);
588
589 if ( isset($alt_attr[2]) && ! empty($alt_attr[2]) ) {
590 $alt = $alt_attr[2];
591 }
592 }
593 }
594 }
595
596 return $alt;
597 }
598
599 /**
600 * Get the Attachment ID for a given image URL.
601 *
602 * @since 3.0.0
603 * @access private
604 * @author Frankie Jarrett
605 * @link http://frankiejarrett.com/get-an-attachment-id-by-url-in-wordpress/
606 * @param string $url
607 * @return int|null
608 */
609 private function get_attachment_id(string $url)
610 {
611 $url = Helper::add_scheme(
612 $url,
613 is_ssl() ? 'https://' : 'http://'
614 );
615
616 // Split the $url into two parts with the wp-content directory as the separator.
617 $parse_url = explode(parse_url(WP_CONTENT_URL, PHP_URL_PATH), $url);
618
619 // Get the host of the current site and the host of the $url, ignoring www.
620 $this_host = str_ireplace('www.', '', parse_url(home_url(), PHP_URL_HOST));
621 $file_host = str_ireplace('www.', '', parse_url($url, PHP_URL_HOST));
622
623 // Return nothing if there aren't any $url parts or if the current host and $url host do not match.
624 if (
625 ! isset($parse_url[1])
626 || empty($parse_url[1])
627 || ($this_host != $file_host)
628 ) {
629 return false;
630 }
631
632 // Now we're going to quickly search the DB for any attachment GUID with a partial path match.
633 // Example: /uploads/2013/05/test-image.jpg
634 global $wpdb;
635
636 $attachment = $wpdb->get_col($wpdb->prepare("SELECT ID FROM {$wpdb->prefix}posts WHERE guid RLIKE %s;", $parse_url[1])); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
637
638 if ( ! $attachment ) {
639 // Maybe it's a resized image, so try to get the full one
640 $parse_url[1] = preg_replace('/-[0-9]{1,4}x[0-9]{1,4}\.(jpg|jpeg|png|gif|bmp)$/i', '.$1', $parse_url[1]);
641 $attachment = $wpdb->get_col($wpdb->prepare("SELECT ID FROM {$wpdb->prefix}posts WHERE guid RLIKE %s;", $parse_url[1])); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
642 }
643
644 // Returns null if no attachment is found.
645 return isset($attachment[0]) ? $attachment[0] : null;
646 }
647
648 /**
649 * Fetchs external images.
650 *
651 * @since 2.3.3
652 * @access private
653 * @param int $id Post ID.
654 * @param string $url Image url.
655 * @return string|bool Image path, or false on failure.
656 */
657 private function fetch_external_image(int $id, string $url)
658 {
659 if ( ! Helper::is_image_url($url) ) {
660 return false;
661 }
662
663 $full_image_path = trailingslashit($this->get_plugin_uploads_dir()['basedir']) . "{$id}_" . sanitize_file_name(rawurldecode(wp_basename($url)));
664
665 // if the file exists already, return URL and path
666 if ( file_exists($full_image_path) ) {
667 return $full_image_path;
668 }
669
670 $url = Helper::add_scheme(
671 $url,
672 is_ssl() ? 'https://' : 'http://'
673 );
674
675 $accepted_status_codes = [200, 301, 302];
676 $response = wp_remote_head($url, ['timeout' => 5, 'sslverify' => false]);
677
678 if (
679 ! is_wp_error($response)
680 && in_array(wp_remote_retrieve_response_code($response), $accepted_status_codes)
681 ) {
682 require_once ABSPATH . 'wp-admin/includes/file.php';
683
684 $url = str_replace('https://', 'http://', $url);
685 $tmp = download_url($url);
686
687 // File was downloaded successfully
688 if ( ! is_wp_error($tmp) ) {
689 // Determine image type
690 if ( function_exists('exif_imagetype') ) {
691 $image_type = exif_imagetype($tmp);
692 } else {
693 $image_type = getimagesize($tmp);
694 $image_type = ( isset($image_type[2]) ) ? $image_type[2] : null;
695 }
696
697 // Valid image, save it
698 if ( in_array($image_type, [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_WEBP, IMAGETYPE_AVIF]) ) {
699 // move file to Uploads
700 if ( @rename($tmp, $full_image_path) ) {
701 // borrowed from WP - set correct file permissions
702 $stat = stat(dirname($full_image_path));
703 $perms = $stat['mode'] & 0000644;
704 @chmod($full_image_path, $perms);
705
706 return $full_image_path;
707 }
708 }
709
710 // Invalid file, remove it
711 @unlink($tmp);
712 }
713 }
714
715 return false;
716 }
717
718 /**
719 * Creates thumbnails.
720 *
721 * @since 3.0.0
722 * @access private
723 * @param string $path Image path
724 * @param string $filename Image filename
725 * @param array $size Image size
726 * @param bool $crop Whether to crop the image or not
727 * @return string|bool Image URL on success, false on error
728 */
729 private function resize(string $path, string $filename, array $size, bool $crop = true)
730 {
731 $image = wp_get_image_editor($path);
732
733 // valid image, create thumbnails
734 if ( ! is_wp_error($image) ) {
735 $original_size = $image->get_size();
736 $sizes = [
737 '1x' => $size
738 ];
739 $thumbnail = '';
740
741 /**
742 * Hook to enable/disable retina support.
743 * @since 5.3.0
744 */
745 $retina_support = apply_filters('wpp_retina_support', true);
746
747 if ( $retina_support ) {
748 // Calculate thumbnail sizes
749 foreach( $this->descriptors as $descriptor ) {
750 $new_size_width = floor($descriptor * $size[0]);
751 $new_size_height = floor($descriptor * $size[1]);
752
753 if (
754 $new_size_width <= $original_size['width']
755 && $new_size_height <= $original_size['height']
756 ) {
757 $sizes[$descriptor . 'x'] = [$new_size_width, $new_size_height];
758 }
759 }
760 }
761
762 $path_parts = null;
763
764 // Generate thumbnails
765 foreach( $sizes as $d => $s ) {
766 if ( '1x' == $d ) {
767 $thumbnail = $this->generate_thumbnail($path, $filename, $s, $crop);
768
769 // Image could not be generated, let's bail early.
770 if ( ! $thumbnail ) {
771 break;
772 }
773 } else {
774 if ( ! $path_parts ) {
775 $path_parts = pathinfo($filename);
776 }
777
778 $filename_with_descriptor = $path_parts['filename'] . "@{$d}." . $path_parts['extension'];
779 $this->generate_thumbnail($path, $filename_with_descriptor, $s, $crop);
780 }
781 }
782
783 return $thumbnail;
784 }
785
786 return false;
787 }
788
789 /**
790 * Creates image.
791 *
792 * @since 5.3.0
793 * @access private
794 * @param string $path Image path
795 * @param string $filename Image filename
796 * @param array $size Image size
797 * @param bool $crop Whether to crop the image or not
798 * @return string|bool Image URL on success, false on error
799 */
800 private function generate_thumbnail(string $path, string $filename, array $size, bool $crop = true)
801 {
802 $image = wp_get_image_editor($path);
803
804 // valid image, create thumbnail
805 if ( ! is_wp_error($image) ) {
806 /**
807 * Hook to change the image compression quality of WPP's thumbnails.
808 * @since 4.2.1
809 */
810 $quality = apply_filters('wpp_thumbnail_compression_quality', null);
811
812 if ( ! ctype_digit( (string) $quality) ) {
813 $quality = null; // Fallback to core's default
814 }
815
816 $image->set_quality($quality);
817 $image->resize($size[0], $size[1], $crop);
818
819 $mime_type = null;
820
821 // .webp thumbnails?
822 if (
823 'webp' === $this->admin_options['tools']['thumbnail']['format']
824 && $image->supports_mime_type('image/webp') // .webp support requires WP 5.8 or higher
825 ) {
826 $filename = substr($filename, 0, strrpos($filename, '.')) . '.webp';
827 $mime_type = 'image/webp';
828 }
829 // .avif thumbnails?
830 elseif (
831 'avif' === $this->admin_options['tools']['thumbnail']['format']
832 && $image->supports_mime_type('image/avif') // .avif support requires WP 6.5 or higher
833 ) {
834 $filename = substr($filename, 0, strrpos($filename, '.')) . '.avif';
835 $mime_type = 'image/avif';
836 }
837
838 $file_path = trailingslashit($this->get_plugin_uploads_dir()['basedir']) . $filename;
839 $new_img = $image->save($file_path, $mime_type);
840
841 if ( ! is_wp_error($new_img) ) {
842 return trailingslashit($this->get_plugin_uploads_dir()['baseurl']) . $filename;
843 }
844 }
845
846 return false;
847 }
848
849 /**
850 * Generates srcset attribute for this image.
851 *
852 * @since 5.3.0
853 * @param string $src
854 * @return string
855 */
856 private function get_srcset(string $src)
857 {
858 /**
859 * Hook to enable/disable retina support.
860 * @since 5.3.0
861 */
862 $retina_support = apply_filters('wpp_retina_support', true);
863
864 if ( ! $retina_support ) {
865 return '';
866 }
867
868 $path_parts = pathinfo($src);
869 $srcset = [$src];
870
871 foreach( $this->descriptors as $descriptor ) {
872 $d = "{$descriptor}x";
873 $filename = $path_parts['filename'] . "@{$d}." . $path_parts['extension'];
874
875 if ( @file_exists(trailingslashit($this->get_plugin_uploads_dir()['basedir']) . $filename) ) {
876 $srcset[] = $path_parts['dirname'] . '/' . $filename . ' ' . $d;
877 }
878 }
879
880 return ( count($srcset) > 1 ) ? ' srcset="' . implode(', ', $srcset) . '" ' : '';
881 }
882
883 /**
884 * Render image tag.
885 *
886 * @since 3.0.0
887 * @access public
888 * @param int $post_id The post/page ID
889 * @param string $src Image URL
890 * @param array $dimension Image's width and height
891 * @param string $class CSS class
892 * @param object $alt Alternative text
893 * @param string $error Error, if the image could not be created
894 * @return string
895 */
896 public function render(int $post_id, string $src, array $size, string $class, string $alt = '', string $error = '')
897 {
898 $img_tag = '';
899
900 if ( $error ) {
901 $img_tag = '<!-- ' . $error . ' --> ';
902 }
903
904 // Make sure we use the right protocol
905 $src = esc_url(is_ssl() ? str_ireplace('http://', 'https://', $src) : $src);
906 // Get srcset, if available
907 $srcset = $this->get_srcset($src);
908
909 $src = 'src="' . $src . '"' . $srcset;
910
911 // Lazy Load attribute, if enabled
912 $lazyload = ( $this->admin_options['tools']['thumbnail']['lazyload'] ) ? ' loading="lazy"' : '';
913
914 $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 . ' />';
915
916 return apply_filters('wpp_render_image', $img_tag, $post_id);
917 }
918
919 /**
920 * Gets list of available thumbnails sizes
921 *
922 * @since 3.2.0
923 * @link http://codex.wordpress.org/Function_Reference/get_intermediate_image_sizes
924 * @param string $size
925 * @return array|bool
926 */
927 public function get_sizes(?string $size)
928 {
929 if ( ! is_array($this->available_sizes) || empty($this->available_sizes) ) {
930 global $_wp_additional_image_sizes;
931
932 $this->available_sizes = [];
933 $get_intermediate_image_sizes = get_intermediate_image_sizes();
934
935 // Create the full array with sizes and crop info
936 foreach( $get_intermediate_image_sizes as $_size ) {
937 if ( in_array($_size, ['thumbnail', 'medium', 'large']) ) {
938 $this->available_sizes[$_size]['width'] = get_option($_size . '_size_w');
939 $this->available_sizes[$_size]['height'] = get_option($_size . '_size_h');
940 $this->available_sizes[$_size]['crop'] = (bool) get_option($_size . '_crop');
941 } elseif ( isset($_wp_additional_image_sizes[$_size]) ) {
942 $this->available_sizes[$_size] = [
943 'width' => $_wp_additional_image_sizes[$_size]['width'],
944 'height' => $_wp_additional_image_sizes[$_size]['height'],
945 'crop' => $_wp_additional_image_sizes[$_size]['crop']
946 ];
947 }
948 }
949 }
950
951 // Get only 1 size if found
952 if ( $size ) {
953 if ( isset($this->available_sizes[$size]) ) {
954 return $this->available_sizes[$size];
955 }
956 return false;
957 }
958
959 return $this->available_sizes;
960 }
961
962 /**
963 * Returns the URL of the default thumbnail image.
964 *
965 * @since 5.0.0
966 * @param int|null
967 * @return string
968 */
969 public function get_default_url(?int $post_ID = null)
970 {
971 if ( has_filter('wpp_default_thumbnail_url') ) {
972 $default_thumbnail_url = apply_filters('wpp_default_thumbnail_url', $this->default_thumbnail, $post_ID);
973
974 if ( $default_thumbnail_url != $this->default_thumbnail && Helper::is_image_url($default_thumbnail_url) ) {
975 return $default_thumbnail_url;
976 }
977 }
978
979 return $this->default_thumbnail;
980 }
981 }
982