PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / 4.2.13
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization v4.2.13
4.2.13 4.2.12 4.2.11 4.2.10 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 2.5.5 2.5.6 2.5.7 3.0.0 3.0.1 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.11.0 3.11.1 3.11.2 3.11.3 3.12.0 3.12.1 All 134 releases
optimole-wp / inc / dam.php

dam.php in Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization 4.2.13, at inc/dam.php

757 lines 19.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Dam class.
4 *
5 * Author: Andrei Baicus <andrei@themeisle.com>
6 * Created on: 04/07/2023
7 *
8 * @package \Optimole\Inc
9 * @author Optimole <friends@optimole.com>
10 */
11
12 use Optimole\Sdk\Resource\ImageProperty\GravityProperty;
13 use Optimole\Sdk\Resource\ImageProperty\ResizeTypeProperty;
14 use Optimole\Sdk\ValueObject\Position;
15
16 /**
17 * Class Optml_Dam
18 */
19 class Optml_Dam {
20 use Optml_Dam_Offload_Utils;
21 use Optml_Normalizer;
22
23 /**
24 * Hold the settings object.
25 *
26 * @var Optml_Settings Settings object.
27 */
28 private $settings;
29
30 /**
31 * The dam endpoint.
32 *
33 * @var string
34 */
35 private $dam_endpoint = 'https://dashboard.optimole.com/dam';
36
37 const OM_DAM_IMPORTED_FLAG = 'om-dam-imported';
38 const URL_DAM_FLAG = '/dam:1';
39 const IS_EDIT_FLAG = 'om-dam-edit';
40
41 /**
42 * Optml_Dam constructor.
43 */
44 public function __construct() {
45 $this->settings = Optml_Main::instance()->admin->settings;
46
47 if ( ! $this->settings->is_connected() ) {
48 return;
49 }
50
51 if ( $this->settings->get( 'cloud_images' ) === 'enabled' ) {
52 add_action( 'admin_menu', [ $this, 'add_menu' ] );
53 add_action( 'print_media_templates', [ $this, 'print_media_template' ] );
54 add_action( 'wp_enqueue_media', [ $this, 'enqueue_media_scripts' ] );
55 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_page_scripts' ] );
56 }
57
58 if ( defined( 'OPTML_DAM_ENDPOINT' ) ) {
59 $this->dam_endpoint = constant( 'OPTML_DAM_ENDPOINT' );
60 }
61 if ( $this->settings->get( 'retina_images' ) === 'enabled' ) {
62 add_filter( 'wp_get_attachment_image_attributes', [ Optml_Tag_Replacer::instance(), 'filter_attachment_image_attributes' ], 99, 3 );
63 }
64 add_filter( 'wp_get_attachment_image_src', [ $this, 'alter_attachment_image_src' ], 10, 4 );
65 add_filter( 'wp_get_attachment_metadata', [ $this, 'alter_attachment_metadata' ], 10, 2 );
66 add_filter( 'image_downsize', [ $this, 'catch_downsize' ], 10, 3 );
67 add_filter( 'wp_prepare_attachment_for_js', [ $this, 'alter_attachment_for_js' ], 10, 3 );
68 add_filter( 'get_attached_file', [ $this, 'alter_attached_file_response' ], 10, 2 );
69 add_filter( 'wp_calculate_image_srcset', [ $this, 'disable_dam_images_srcset' ], 1, 5 );
70
71 add_filter(
72 'elementor/image_size/get_attachment_image_html',
73 [
74 $this,
75 'alter_elementor_image_size',
76 ],
77 10,
78 4
79 );
80 }
81
82 /**
83 * Catch image downsize for the DAM imported images.
84 *
85 * @param array $image {
86 * Array of image data.
87 *
88 * @type string $0 Image source URL.
89 * @type int $1 Image width in pixels.
90 * @type int $2 Image height in pixels.
91 * @type bool $3 Whether the image is a resized image.
92 *
93 * @param int $id attachment id.
94 * @param string|int[] $size image size.
95 *
96 * @return array $image.
97 */
98 public function catch_downsize( $image, $id, $size ) {
99 return $this->alter_attachment_image_src( $image, $id, $size, false );
100 }
101
102 /**
103 * Insert attachments.
104 *
105 * @param array $images Images array.
106 *
107 * @return array
108 */
109 public function insert_attachments( $images ) {
110 $ids = [];
111
112 $existing = $this->check_existing_attachments( $images );
113
114 foreach ( $images as $image ) {
115 if ( ! isset( $image['isEdit'] ) && array_key_exists( $image['meta']['resourceS3'], $existing ) ) {
116 $ids[] = $existing[ $image['meta']['resourceS3'] ];
117
118 continue;
119 }
120
121 $id = $this->insert_attachment( $image );
122
123 if ( $id === 0 ) {
124 continue;
125 }
126 $ids[] = $id;
127 }
128
129 return $ids;
130 }
131
132 /**
133 * Insert single attachment
134 *
135 * @param array $image Image data.
136 *
137 * @return int
138 */
139 private function insert_attachment( $image ) {
140 $filename = basename( $image['url'] );
141 $name = pathinfo( $filename, PATHINFO_FILENAME );
142
143 $args = [
144 'post_title' => $name,
145 'post_type' => 'attachment',
146 'post_mime_type' => $image['meta']['mimeType'],
147 'guid' => $image['url'],
148 ];
149
150 $id = wp_insert_attachment( $args );
151
152 if ( $id === 0 ) {
153 return $id;
154 }
155
156 update_post_meta( $id, self::OM_DAM_IMPORTED_FLAG, $image['meta']['resourceS3'] );
157
158 if ( isset( $image['isEdit'] ) ) {
159 update_post_meta( $id, self::IS_EDIT_FLAG, true );
160 }
161
162 $metadata = [];
163
164 $metadata['file'] = '/id:' . $image['meta']['resourceS3'] . '/' . get_home_url() . '/' . $filename;
165 $metadata['mime-type'] = $image['meta']['mimeType'];
166
167 if ( isset( $image['meta']['filesize'] ) ) {
168 $metadata['filesize'] = $image['meta']['fileSize'];
169 }
170
171 if ( isset( $image['meta']['originalWidth'] ) && isset( $image['meta']['originalHeight'] ) ) {
172 $metadata['width'] = $image['meta']['originalWidth'];
173 $metadata['height'] = $image['meta']['originalHeight'];
174 }
175
176 wp_update_attachment_metadata( $id, $metadata );
177
178 return $id;
179 }
180
181 /**
182 * Alter attachment image src for DAM imported images.
183 *
184 * @param array|false $image {
185 * Array of image data.
186 *
187 * @type string $0 Image source URL.
188 * @type int $1 Image width in pixels.
189 * @type int $2 Image height in pixels.
190 * @type bool $3 Whether the image is a resized image.
191 * }
192 *
193 * @param int $attachment_id attachment id.
194 * @param string|int[] $size image size.
195 * @param bool $icon Whether the image should be treated as an icon.
196 *
197 * @return array $image.
198 */
199 public function alter_attachment_image_src( $image, $attachment_id, $size, $icon ) {
200
201 // Skip if not DAM image.
202 if ( ! $this->is_dam_imported_image( $attachment_id ) ) {
203 return $image;
204 }
205 $image_url = wp_get_attachment_url( $attachment_id );
206 list($width, $height) = $this->parse_dimension_from_optimized_url( $image_url );
207
208 // Skip resize in single attachment view on backend.
209 if ( $this->is_attachment_edit_page( $attachment_id ) ) {
210 return [
211 $image_url,
212 $width === 'auto' ? false : $width,
213 $height === 'auto' ? false : $height,
214 false,
215 ];
216 }
217
218 $metadata = wp_get_attachment_metadata( $attachment_id );
219 // Use the original size if the requested size is full.
220 if ( $size === 'full' ) {
221 $image_url = $this->replace_dam_url_args(
222 [
223 'width' => $metadata['width'],
224 'height' => $metadata['height'],
225 'crop' => false,
226 ],
227 $image_url
228 );
229
230 return [
231 $image_url,
232 $metadata['width'],
233 $metadata['height'],
234 false,
235 ];
236 }
237 $sizes = $this->size_to_dimension( $size, $metadata );
238
239 $image_url = $this->replace_dam_url_args(
240 [
241 'width' => $sizes['width'],
242 'height' => $sizes['height'],
243 'crop' => $sizes['resize'] ?? false,
244 ],
245 $image_url
246 );
247 return [
248 $image_url,
249 $sizes['width'],
250 $sizes['height'],
251 $size === 'full', // @phpstan-ignore-line
252 ];
253 }
254
255 /**
256 * Alter the attachment metadata.
257 *
258 * @param array $metadata attachment metadata.
259 * @param int $id attachment ID.
260 *
261 * @return array
262 */
263 public function alter_attachment_metadata( $metadata, $id ) {
264 if ( ! $this->is_dam_imported_image( $id ) ) {
265 return $metadata;
266 }
267
268 return $this->get_altered_metadata_for_remote_images( $metadata, $id );
269 }
270
271 /**
272 * Check if the images are already imported.
273 *
274 * @param array $images List of images to check.
275 *
276 * @return array List of images that are already imported.
277 */
278 private function check_existing_attachments( $images ) {
279 $already_imported = $this->get_dam_imported_attachments( $images );
280
281 // All DAM imports are already in the DB.
282 if ( count( $already_imported ) === count( $images ) ) {
283 return $already_imported;
284 }
285
286 // Get the remaining images.
287 $remaining = array_filter(
288 $images,
289 function ( $image ) use ( $already_imported ) {
290 return ! array_key_exists( $image['meta']['resourceS3'], $already_imported );
291 }
292 );
293
294 // Offloaded images.
295 if ( $this->settings->get( 'offload_media' ) === 'enabled' ) {
296 $offloaded = $this->get_offloaded_attachments( $remaining );
297
298 $already_imported = array_merge( $already_imported, $offloaded );
299 }
300
301 return $already_imported;
302 }
303
304 /**
305 * Check if the images are already imported.
306 *
307 * @param array $images List of images to check.
308 *
309 * @return array
310 */
311 private function get_dam_imported_attachments( $images ) {
312 global $wpdb;
313
314 $s3_ids = [];
315
316 foreach ( $images as $image ) {
317 $s3_ids[] = esc_sql( strval( $image['meta']['resourceS3'] ) );
318 }
319
320 $meta_values_str = "'" . join( "', '", $s3_ids ) . "'";
321
322 // Select all the posts that have the flag and are in the list of images.
323 $found_attachments = $wpdb->get_results(
324 $wpdb->prepare(
325 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- This query cannot use interpolation.
326 "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value IN ( {$meta_values_str} )",
327 self::OM_DAM_IMPORTED_FLAG
328 )
329 );
330
331 if ( empty( $found_attachments ) ) {
332 return [];
333 }
334
335 $map = [];
336
337 // Remap this in a key/value array.
338 // Also ensures that if there are multiple attachments with the same S3 ID, we only get the one.
339 // Shouldn't happen, but just in case.
340 foreach ( $found_attachments as $attachment ) {
341 // Skip edits.
342 if ( ! empty( get_post_meta( $attachment->post_id, self::IS_EDIT_FLAG, true ) ) ) {
343 continue;
344 }
345
346 $map[ $attachment->meta_value ] = (int) $attachment->post_id;
347 }
348
349 return $map;
350 }
351
352 /**
353 * Get the offloaded attachments.
354 *
355 * [ S3 ID => Attachment Post ID ]
356 *
357 * @param array $images List of images to check.
358 *
359 * @return array
360 */
361 private function get_offloaded_attachments( $images ) {
362 global $wpdb;
363
364 $map = [];
365
366 foreach ( $images as $image ) {
367 $like = '%id:' . $image['meta']['resourceS3'] . '%';
368
369 $found_attachments = $wpdb->get_results(
370 $wpdb->prepare(
371 "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value LIKE %s",
372 '_wp_attachment_metadata',
373 $like
374 )
375 );
376
377 if ( empty( $found_attachments ) ) {
378 return [];
379 }
380
381 $map[ $image['meta']['resourceS3'] ] = (int) $found_attachments[0]->post_id;
382 }
383
384 return $map;
385 }
386
387 /**
388 * Adds menu item for DAM.
389 *
390 * @return void
391 */
392 public function add_menu() {
393 if ( defined( 'OPTIOMLE_HIDE_ADMIN_AREA' ) && OPTIOMLE_HIDE_ADMIN_AREA ) {
394 return;
395 }
396
397 add_submenu_page(
398 'optimole',
399 __( 'Cloud Library', 'optimole-wp' ),
400 __( 'Cloud Library', 'optimole-wp' ),
401 'manage_options',
402 'optimole-dam',
403 [ $this, 'render_dashboard_page' ]
404 );
405 }
406
407 /**
408 * Add media template to be used in the media library.
409 *
410 * @return void
411 */
412 public function print_media_template() {
413 ?>
414 <script type="text/html" id="tmpl-optimole-dam">
415 <?php $this->render_dashboard_page(); ?>
416 </script>
417 <?php
418 }
419
420 /**
421 * Render the dashboard page.
422 *
423 * @return void
424 */
425 public function render_dashboard_page() {
426 ?>
427 <style>
428 .notice:not(.optml-notice-optin){
429 display: none !important;
430 }
431 </style>
432 <iframe id="om-dam" style="display: none;" src="<?php echo( $this->build_iframe_url() ); ?>" allow="clipboard-read; clipboard-write"></iframe>
433 <div class="om-dam-loader">
434 <img src="<?php echo esc_url( OPTML_URL . 'assets/img/logo.png' ); ?>" alt="Optimole Logo"
435 class="om-dam-logo">
436 <p><?php echo esc_html__( 'Loading', 'optimole-wp' ); ?>...</p>
437 </div>
438 <?php
439 }
440
441 /**
442 * Build the iFrame URL.
443 *
444 * @return string
445 */
446 public function build_iframe_url() {
447 $api_key = $this->settings->get( 'api_key' );
448 $connected_sites = $this->settings->get( 'cloud_sites' );
449
450 if ( empty( $api_key ) ) {
451 return '';
452 }
453
454 if ( isset( $connected_sites['all'] ) && $connected_sites['all'] === 'true' ) {
455 $connected_sites = [];
456 } else {
457 foreach ( $connected_sites as $site => $status ) {
458 if ( $status !== 'true' ) {
459 unset( $connected_sites[ $site ] );
460 }
461 }
462 }
463
464 $data = [
465 'site' => get_site_url(),
466 'token' => $api_key,
467 'sites' => array_keys( $connected_sites ),
468 ];
469
470 $data = json_encode( $data );
471 $data = rtrim( base64_encode( $data ), '=' );
472
473 return tsdk_translate_link(
474 add_query_arg(
475 [
476 'data' => $data,
477 ],
478 $this->dam_endpoint
479 ),
480 'query'
481 );
482 }
483
484 /**
485 * Enqueue script for generating cloud media tab.
486 *
487 * @return void
488 */
489 public function enqueue_media_scripts() {
490 $asset_file = include OPTML_PATH . 'assets/build/media/media-modal.asset.php';
491
492 wp_register_script(
493 OPTML_NAMESPACE . '-media-modal',
494 OPTML_URL . 'assets/build/media/media-modal.js',
495 $asset_file['dependencies'],
496 $asset_file['version'],
497 true
498 );
499
500 wp_localize_script( OPTML_NAMESPACE . '-media-modal', 'optmlMediaModal', $this->get_localized_vars() );
501 wp_enqueue_script( OPTML_NAMESPACE . '-media-modal' );
502 wp_enqueue_style( OPTML_NAMESPACE . '-media-modal', OPTML_URL . 'assets/build/media/media-modal.css' );
503 }
504
505 /**
506 * Enqueue script for generating admin page.
507 *
508 * @return void
509 */
510 public function enqueue_admin_page_scripts() {
511 $screen = get_current_screen();
512
513 if ( $screen->id !== 'optimole_page_optimole-dam' ) {
514 return;
515 }
516
517 $asset_file = include OPTML_PATH . 'assets/build/media/admin-page.asset.php';
518
519 wp_register_script(
520 OPTML_NAMESPACE . '-admin-page',
521 OPTML_URL . 'assets/build/media/admin-page.js',
522 $asset_file['dependencies'],
523 $asset_file['version'],
524 true
525 );
526
527 wp_localize_script(
528 OPTML_NAMESPACE . '-admin-page',
529 'optmlAdminPage',
530 [
531 'siteUrl' => get_site_url(),
532 ]
533 );
534
535 wp_enqueue_script( OPTML_NAMESPACE . '-admin-page' );
536
537 wp_enqueue_style( OPTML_NAMESPACE . '-admin-page', OPTML_URL . 'assets/build/media/admin-page.css' );
538
539 do_action( 'themeisle_internal_page', OPTML_PRODUCT_SLUG, 'dam' );
540 }
541
542 /**
543 * Get localized variables for the media modal.
544 *
545 * @return array
546 */
547 private function get_localized_vars() {
548 $routes = array_keys( Optml_Rest::$rest_routes['dam_routes'] );
549
550 foreach ( $routes as $route ) {
551 $routes[ $route ] = OPTML_NAMESPACE . '/v1/' . $route;
552 }
553
554 return [
555 'nonce' => wp_create_nonce( 'wp_rest' ),
556 'routes' => $routes,
557 ];
558 }
559
560 /**
561 * Alter the image size for the image widget.
562 *
563 * @param string $html the attachment image HTML string.
564 * @param array $settings Control settings.
565 * @param string $image_size_key Optional. Settings key for image size.
566 * Default is `image`.
567 * @param string $image_key Optional. Settings key for image. Default
568 * is null. If not defined uses image size key
569 * as the image key.
570 *
571 * @return string
572 */
573 public function alter_elementor_image_size( $html, $settings, $image_size_key, $image_key ) {
574 if ( ! isset( $settings['image'] ) ) {
575 return $html;
576 }
577
578 $image = $settings['image'];
579
580 if ( ! isset( $image['id'] ) ) {
581 return $html;
582 }
583
584 if ( ! $this->is_dam_imported_image( $image['id'] ) ) {
585 return $html;
586 }
587
588 if ( ! isset( $settings['image_size'] ) ) {
589 return $html;
590 }
591
592 if ( $settings['image_size'] === 'custom' ) {
593 if ( ! isset( $settings['image_custom_dimension'] ) ) {
594 return $html;
595 }
596
597 $custom_dimensions = $settings['image_custom_dimension'];
598
599 if ( ! isset( $custom_dimensions['width'] ) || ! isset( $custom_dimensions['height'] ) ) {
600 return $html;
601 }
602
603 return $this->replace_dam_url_args( $custom_dimensions, $html );
604 }
605
606 $all_sizes = Optml_App_Replacer::image_sizes();
607
608 if ( ! isset( $all_sizes[ $image_size_key ] ) ) {
609 return $html;
610 }
611
612 return $this->replace_dam_url_args( $all_sizes[ $image_size_key ], $html );
613 }
614
615 /**
616 * Needed as some blocks might use the image sizes.
617 *
618 * @param array $response Array of prepared attachment data. @see wp_prepare_attachment_for_js().
619 * @param WP_Post $attachment Attachment object.
620 * @param array|false $meta Array of attachment meta data, or false if there is none.
621 *
622 * @return array
623 */
624 public function alter_attachment_for_js( $response, $attachment, $meta ) {
625 if ( ! $this->is_dam_imported_image( $attachment->ID ) ) {
626 return $response;
627 }
628
629 $sizes = Optml_App_Replacer::image_sizes();
630
631 $meta = [];
632 if ( isset( $response['width'] ) ) {
633 $meta['width'] = $response['width'];
634 }
635 if ( isset( $response['height'] ) ) {
636 $meta['height'] = $response['height'];
637 }
638 foreach ( $sizes as $size => $args ) {
639 if ( isset( $response['sizes'][ $size ] ) ) {
640 continue;
641 }
642 $args = $this->size_to_dimension( $size, $meta );
643
644 $response['sizes'][ $size ] = array_merge(
645 $args,
646 [
647 'url' => $this->replace_dam_url_args( $args, $response['url'] ),
648 'orientation' => ( $args['height'] > $args['width'] ) ? 'portrait' : 'landscape',
649 ]
650 );
651 }
652 $response['url'] = $this->replace_dam_url_args( $meta, $response['url'] );
653
654 return $response;
655 }
656
657 /**
658 * Replace the image size params in DAM URLs inside a string.
659 *
660 * @param array $args The arguments to replace.
661 * - width: The width of the image.
662 * - height: The height of the image.
663 * - crop: Whether to crop the image.
664 * @param string $subject The string to replace the arguments in.
665 *
666 * @return string
667 */
668 public function replace_dam_url_args( $args, $subject ) {
669 $args = wp_parse_args( $args, [ 'width' => 'auto', 'height' => 'auto', 'dam' => true ] );
670
671 $width = $args['width'];
672 $height = $args['height'];
673 $crop = $args['crop'] ?? $args['resize'] ?? false;
674
675 $gravity = Position::CENTER;
676
677 if ( $this->settings->get( 'resize_smart' ) === 'enabled' ) {
678 $gravity = GravityProperty::SMART;
679 }
680
681 if ( $width === 0 ) {
682 $width = 'auto';
683 }
684
685 if ( $height === 0 ) {
686 $height = 'auto';
687 }
688
689 // Use the proper replacement for the image size.
690 $replacement = '/w:' . $width . '/h:' . $height;
691
692 if ( $crop === true ) {
693 $replacement .= '/g:' . $gravity . '/rt:fill';
694 } elseif ( is_array( $crop ) && ! empty( $crop ) ) {
695
696 $replacement .= '/' . ( new GravityProperty( $crop['gravity'] ) ) .
697 '/' . new ResizeTypeProperty( $crop['type'] ) .
698 ( $crop['enlarge'] ? '/el:1' : '' );
699 }
700
701 $replacement .= '/q:';
702
703 if ( $args['dam'] ) {
704 $replacement = self::URL_DAM_FLAG . $replacement;
705 }
706
707 return preg_replace( '/\/w:(.*)\/h:(.*)\/q:/', $replacement, $subject );
708 }
709
710 /**
711 * Elementor checks if the file exists before requesting a specific image size.
712 *
713 * Needed because otherwise there won't be any width/height on the `img` tags, breaking lazyload.
714 *
715 * Also needed because some
716 *
717 * @param string $file The file path.
718 * @param int $id The attachment ID.
719 *
720 * @return bool|string
721 */
722 public function alter_attached_file_response( $file, $id ) {
723 if ( ! $this->is_dam_imported_image( $id ) ) {
724 return $file;
725 }
726
727 $metadata = wp_get_attachment_metadata( $id );
728
729 if ( isset( $metadata['file'] ) ) {
730 $uploads = wp_get_upload_dir();
731
732 return $uploads['basedir'] . '/' . $metadata['file'];
733 }
734
735 return true;
736 }
737
738 /**
739 * Alter the srcSet for DAM images.
740 *
741 * @param array $sources Initial source array.
742 * @param array $size_array Requested size.
743 * @param string $image_src Image source URL.
744 * @param array $image_meta Image meta data.
745 * @param int $attachment_id Image attachment ID.
746 *
747 * @return array
748 */
749 public function disable_dam_images_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) {
750 if ( ! $this->is_dam_imported_image( $attachment_id ) ) {
751 return $sources;
752 }
753
754 return [];
755 }
756 }
757