PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / 4.2.10
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization v4.2.10
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 / rest.php

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

1,128 lines 31.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Optimole Rest related actions.
4 *
5 * @package Optimole/Inc
6 * @copyright Copyright (c) 2017, Marius Cristea
7 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
8 */
9
10 use Optimole\Sdk\Resource\ImageProperty\ResizeTypeProperty;
11 use Optimole\Sdk\ValueObject\Position;
12 use OptimoleWP\PageProfiler\Profile;
13
14 /**
15 * Class Optml_Rest
16 *
17 * @codeCoverageIgnore
18 */
19 class Optml_Rest {
20
21 /**
22 * Rest api namespace.
23 *
24 * @var string Namespace.
25 */
26 private $namespace;
27
28 /**
29 * Upload conflicts api.
30 *
31 * @var array{
32 * service_routes: RestRouteMap,
33 * image_routes: RestRouteMap,
34 * media_cloud_routes: RestRouteMap,
35 * conflict_routes: SimpleRouteMap,
36 * cache_routes: SimpleRouteMap,
37 * dam_routes: RestRouteMap,
38 * notification_dismiss_routes: RestRouteMap,
39 * optimization_routes: RestRouteMap
40 * }
41 */
42 public static $rest_routes = [
43 'service_routes' => [
44 'update_option' => 'POST',
45 'request_update' => 'GET',
46 'connect' => [
47 'POST',
48 'args' => [
49 'api_key' => [
50 'type' => 'string',
51 'required' => true,
52 ],
53 ],
54 ],
55 'select_application' => [
56 'POST',
57 'args' => [
58 'api_key' => [
59 'type' => 'string',
60 'required' => true,
61 ],
62 'application' => [
63 'type' => 'string',
64 'required' => true,
65 ],
66 ],
67 ],
68 'register_service' => [
69 'POST',
70 'args' => [
71 'email' => [
72 'type' => 'string',
73 'required' => true,
74 ],
75 ],
76 ],
77 'disconnect' => 'GET',
78 ],
79 'image_routes' => [
80 'poll_optimized_images' => 'GET',
81 'get_sample_rate' => 'POST',
82 'upload_onboard_images' => [
83 'POST',
84 'args' => [
85 'offset' => [
86 'type' => 'number',
87 'required' => false,
88 'default' => 0,
89 ],
90 ],
91 ],
92 ],
93 'media_cloud_routes' => [
94 'number_of_images_and_pages' => 'POST',
95 'clear_offload_errors' => 'GET',
96 'get_offload_conflicts' => 'GET',
97 'move_image' => [
98 'POST',
99 'args' => [
100 'id' => [
101 'type' => 'number',
102 'required' => true,
103 ],
104 'action' => [
105 'type' => 'string',
106 'required' => true,
107 ],
108 ],
109 'permission_callback' => [ __CLASS__, 'can_move_image' ],
110 ],
111 ],
112 'conflict_routes' => [
113 'poll_conflicts' => 'GET',
114 'dismiss_conflict' => 'POST',
115 ],
116 'cache_routes' => [
117 'clear_cache_request' => 'POST',
118 ],
119 'dam_routes' => [
120 'insert_images' => [
121 'POST',
122 'args' => [
123 'images' => [
124 'type' => 'array',
125 'required' => true,
126 ],
127 ],
128 'permission_callback' => 'upload_files',
129 ],
130 ],
131 'notification_dismiss_routes' => [
132 'dismiss_notice' => [
133 'POST',
134 'args' => [
135 'key' => [
136 'type' => 'string',
137 'required' => true,
138 ],
139 ],
140 ],
141 ],
142 'optimization_routes' => [
143 'optimizations' => [
144 'POST',
145 'args' => [
146 'd' => [
147 'type' => 'integer',
148 'required' => true,
149 ],
150 'a' => [
151 'type' => 'array',
152 'required' => true,
153 ],
154 'u' => [
155 'type' => 'string',
156 'required' => true,
157 ],
158 ],
159 'permission_callback' => '__return_true',
160 ],
161 ],
162 ];
163
164 /**
165 * Optml_Rest constructor.
166 *
167 * @return void
168 */
169 public function __construct() {
170 $this->namespace = OPTML_NAMESPACE . '/v1';
171
172 add_action( 'rest_api_init', [ $this, 'register' ] );
173 }
174
175 /**
176 * Method to register a specific REST route.
177 *
178 * @param string $route The route name.
179 * @param string $method The route access method: GET, POST, POST_PUT_PATCH.
180 * @param array $args Optional arguments for route parameters.
181 * @param string|callable $permission_callback Permission callback function or capability.
182 *
183 * @phpstan-param RestArgs $args Optional arguments for route parameters.
184 *
185 * @throws \InvalidArgumentException If method is invalid.
186 *
187 * @return void
188 */
189 private function register_route( $route, $method = 'GET', $args = [], $permission_callback = 'manage_options' ) {
190 if ( empty( $route ) ) {
191 return;
192 }
193
194 $method_map = [
195 'GET' => \WP_REST_Server::READABLE,
196 'POST' => \WP_REST_Server::CREATABLE,
197 'POST_PUT_PATCH' => \WP_REST_Server::EDITABLE,
198 ];
199
200 if ( ! isset( $method_map[ $method ] ) ) {
201 _doing_it_wrong(
202 __METHOD__,
203 sprintf( 'Invalid REST method: %s', esc_html( $method ) ),
204 '1.0.0'
205 );
206 return;
207 }
208
209 $permission = is_callable( $permission_callback )
210 ? $permission_callback
211 : function () use ( $permission_callback ) {
212 if ( ! is_string( $permission_callback ) ) {
213 return false;
214 }
215 return current_user_can( $permission_callback );
216 };
217
218 $params = [
219 'methods' => $method_map[ $method ],
220 'permission_callback' => $permission,
221 'callback' => [ $this, $route ],
222 ];
223
224 if ( ! empty( $args ) ) {
225 $params['args'] = $args;
226 }
227
228 register_rest_route(
229 $this->namespace,
230 '/' . $route,
231 [ $params ]
232 );
233 }
234
235 /**
236 * Register rest routes.
237 *
238 * @return void
239 */
240 public function register() {
241
242 $this->register_service_routes();
243
244 $this->register_image_routes();
245 $this->register_conflict_routes();
246 $this->register_cache_routes();
247 $this->register_media_offload_routes();
248 $this->register_dam_routes();
249 $this->register_notification_routes();
250 $this->register_optimization_routes();
251 }
252
253 /**
254 * Method to register service specific routes.
255 *
256 * @return void
257 */
258 public function register_service_routes() {
259 foreach ( self::$rest_routes['service_routes'] as $route => $details ) {
260 if ( is_array( $details ) ) {
261 $this->register_route( $route, $details[0], $details['args'] );
262 } else {
263 $this->register_route( $route, $details );
264 }
265 }
266 }
267
268 /**
269 * Method to register image specific routes.
270 *
271 * @return void
272 */
273 public function register_image_routes() {
274 foreach ( self::$rest_routes['image_routes'] as $route => $details ) {
275 if ( is_array( $details ) ) {
276 $this->register_route( $route, $details[0], $details['args'] );
277 } else {
278 $this->register_route( $route, $details );
279 }
280 }
281 }
282 /**
283 * Method to register media offload specific routes.
284 *
285 * @return void
286 */
287 public function register_media_offload_routes() {
288 foreach ( self::$rest_routes['media_cloud_routes'] as $route => $details ) {
289
290 $permission = isset( $details['permission_callback'] ) ? $details['permission_callback'] : 'manage_options';
291 $args = isset( $details['args'] ) ? $details['args'] : [];
292 $this->register_route( $route, is_array( $details ) ? $details[0] : $details, $args, $permission );
293 }
294 }
295
296
297 /**
298 * Method to register conflicts specific routes.
299 *
300 * @return void
301 */
302 public function register_conflict_routes() {
303 foreach ( self::$rest_routes['conflict_routes'] as $route => $details ) {
304 $this->register_route( $route, $details );
305 }
306 }
307
308 /**
309 * Method to register cache specific routes.
310 *
311 * @return void
312 */
313 public function register_cache_routes() {
314 foreach ( self::$rest_routes['cache_routes'] as $route => $details ) {
315 $this->register_route( $route, $details );
316 }
317 }
318
319 /**
320 * Register DAM routes.
321 *
322 * @return void
323 */
324 public function register_dam_routes() {
325 foreach ( self::$rest_routes['dam_routes'] as $route => $details ) {
326 $permission = isset( $details['permission_callback'] ) ? $details['permission_callback'] : 'manage_options';
327 $args = isset( $details['args'] ) ? $details['args'] : [];
328 $this->register_route( $route, $details[0], $args, $permission );
329 }
330 }
331
332 /**
333 * Register notification dismiss routes.
334 *
335 * @return void
336 */
337 public function register_notification_routes() {
338 foreach ( self::$rest_routes['notification_dismiss_routes'] as $route => $details ) {
339 $this->register_route( $route, $details[0], isset( $details['args'] ) ? $details['args'] : [] );
340 }
341 }
342
343 /**
344 * Clear Cache request.
345 *
346 * @param WP_REST_Request $request clear cache rest request.
347 * @phpstan-param WP_REST_Request<array{type?: string}> $request
348 *
349 * @return WP_Error|WP_REST_Response
350 */
351 public function clear_cache_request( WP_REST_Request $request ) {
352 $settings = new Optml_Settings();
353 $type = $request->get_param( 'type' );
354 $response = $settings->clear_cache( $type );
355
356 if ( is_wp_error( $response ) ) {
357 wp_send_json_error( $response->get_error_message() );
358 }
359
360 return $this->response( $response, '200' );
361 }
362
363 /**
364 * Connect to optimole service.
365 *
366 * @param WP_REST_Request $request connect rest request.
367 * @phpstan-param WP_REST_Request<array{api_key: string, application?: string}> $request
368 *
369 * @return WP_Error|WP_REST_Response
370 */
371 public function connect( WP_REST_Request $request ) {
372 $api_key = $request->get_param( 'api_key' );
373 $original_request = $request;
374 $request = new Optml_Api();
375 $data = $request->connect( $api_key );
376
377 if ( $data === false || is_wp_error( $data ) ) {
378
379 $extra = '';
380 if ( is_wp_error( $data ) ) {
381 if ( $data->get_error_code() === 'domain_not_accessible' ) {
382 return $this->response( $data->get_error_message(), 400 );
383 }
384 /**
385 * Error from api.
386 *
387 * @var WP_Error $data Error object.
388 */
389 $extra = sprintf( /* translators: Error details */ __( '. ERROR details: %s', 'optimole-wp' ), $data->get_error_message() );
390 }
391 return $this->response( __( 'Can not connect to Optimole service', 'optimole-wp' ) . $extra, 400 );
392 }
393 $settings = new Optml_Settings();
394 $settings->update( 'api_key', $api_key );
395
396 if ( isset( $data['extra_visits'] ) ) {
397 $settings->update_frontend_banner_from_remote( $data['extra_visits'] );
398 }
399
400 if ( $data['app_count'] === 1 ) {
401 return $this->select_application( $original_request );
402 }
403 return $this->response( $data );
404 }
405
406 /**
407 * Select application.
408 *
409 * @param WP_REST_Request $request Rest request.
410 * @phpstan-param WP_REST_Request<array{api_key: string, application?: string}> $request
411 *
412 * @return WP_REST_Response
413 */
414 public function select_application( WP_REST_Request $request ) {
415 $api_key = $request->get_param( 'api_key' );
416 $application = $request->get_param( 'application' );
417 $request = new Optml_Api();
418 $data = $request->get_user_data( $api_key, $application );
419 if ( $data === false || is_wp_error( $data ) ) {
420 $extra = '';
421 if ( is_wp_error( $data ) ) {
422 /**
423 * Error from api.
424 *
425 * @var WP_Error $data Error object.
426 */
427 $extra = sprintf( /* translators: Error details */ __( '. ERROR details: %s', 'optimole-wp' ), $data->get_error_message() );
428 }
429 wp_send_json_error( __( 'Can not connect to Optimole service', 'optimole-wp' ) . $extra );
430 }
431 $settings = new Optml_Settings();
432 $settings->update( 'service_data', $data );
433 return $this->response( $data );
434 }
435
436 /**
437 * Wrapper for api response.
438 *
439 * @param mixed $data data from api.
440 * @param string|int $code Response code.
441 *
442 * @return WP_REST_Response
443 */
444 private function response( $data, $code = 'success' ) {
445 return new WP_REST_Response( [ 'data' => $data, 'code' => $code ], 200 );
446 }
447
448 /**
449 * Connect to optimole service.
450 *
451 * @param WP_REST_Request $request connect rest request.
452 * @phpstan-param WP_REST_Request<array{email: string, auto_connect?: string}> $request
453 *
454 * @return WP_Error|WP_REST_Response
455 */
456 public function register_service( WP_REST_Request $request ) {
457 $email = $request->get_param( 'email' );
458 $api = new Optml_Api();
459
460 $user = $api->create_account( $email );
461
462 $auto_connect = $request->get_param( 'auto_connect' );
463
464 if ( ! empty( $auto_connect ) && $auto_connect === 'true' ) {
465 delete_option( Optml_Settings::OPTML_USER_EMAIL );
466 }
467 if ( $user === false || is_wp_error( $user ) ) {
468 if ( $user->get_error_code() === 'domain_not_accessible' ) {
469 return new WP_REST_Response(
470 [
471 'data' => null,
472 'message' => $user->get_error_message(),
473 'code' => 'domain_not_accessible',
474 ],
475 200
476 );
477 }
478
479 return new WP_REST_Response(
480 [
481 'data' => null,
482 'message' => __( 'Error creating account.', 'optimole-wp' ) . ' ' . $user->get_error_message(),
483 'code' => 'error',
484 ],
485 200
486 );
487 }
488 if ( $user === 'email_registered' ) {
489 return new WP_REST_Response(
490 [
491 'data' => null,
492 'message' => __( 'Error: This email is already registered. Please choose another one.', 'optimole-wp' ),
493 'code' => 'email_registered',
494 ],
495 200
496 );
497
498 }
499
500 if ( $user === 'site_exists' ) {
501 return new WP_REST_Response(
502 [
503 'data' => null,
504 'message' => sprintf( /* translators: 1 start anchor tag to register page, 2 is ending anchor tag. */ __( 'Error: This site has been previously registered. You can login to your account from %1$shere%2$s', 'optimole-wp' ), '<a href="' . esc_url( tsdk_translate_link( 'https://dashboard.optimole.com/login', 'query' ) ) . '" target="_blank"> ', '</a>' ),
505 'code' => 'site_exists',
506 ],
507 200
508 );
509 }
510
511 $user_data = $user['res'];
512
513 $settings = new Optml_Settings();
514 $settings->update( 'api_key', $user_data['api_key'] );
515
516 if ( $user_data['app_count'] === 1 ) {
517 $settings->update( 'service_data', $user_data );
518 }
519
520 if ( isset( $user_data['extra_visits'] ) ) {
521 $settings->update_frontend_banner_from_remote( $user_data['extra_visits'] );
522 }
523
524 return $this->response( $user_data );
525 }
526
527 /**
528 * Return image samples.
529 *
530 * @param WP_REST_Request $request Rest request.
531 * @phpstan-param WP_REST_Request<array{quality?: int, force?: string}> $request
532 *
533 * @return WP_REST_Response Image urls.
534 */
535 public function get_sample_rate( WP_REST_Request $request ) {
536
537 add_filter( 'optml_dont_replace_url', '__return_true' );
538 $image_sample = get_transient( 'optimole_sample_image' );
539 if ( $image_sample === false || $request->get_param( 'force' ) === 'yes' ) {
540 $image_sample = $this->fetch_sample_image();
541 set_transient( 'optimole_sample_image', $image_sample );
542 }
543 $image = [ 'id' => $image_sample['id'] ];
544
545 $image['original'] = $image_sample['url'];
546
547 remove_filter( 'optml_dont_replace_url', '__return_true' );
548
549 $image['optimized'] = apply_filters(
550 'optml_replace_image',
551 $image['original'],
552 [
553 'width' => $image_sample['width'],
554 'height' => $image_sample['height'],
555 'quality' => $request->get_param( 'quality' ),
556 ]
557 );
558
559 $optimized = wp_remote_get(
560 $image['optimized'],
561 [
562 'timeout' => 10,
563 'headers' => [
564 'Accept' => 'text/html,application/xhtml+xml,image/webp,image/apng ',
565 ],
566 ]
567 );
568
569 $original = wp_remote_get( $image['original'] );
570
571 $image['optimized_size'] = (int) wp_remote_retrieve_header( $optimized, 'content-length' );
572 $image['original_size'] = (int) wp_remote_retrieve_header( $original, 'content-length' );
573
574 return $this->response( $image );
575 }
576
577 /**
578 * Crawl & upload initial load.
579 *
580 * @param WP_REST_Request $request Rest request.
581 * @phpstan-param WP_REST_Request<array{offset?: int}> $request
582 *
583 * @return WP_REST_Response If there are more posts left to receive.
584 */
585 public function upload_onboard_images( WP_REST_Request $request ) {
586 $offset = absint( $request->get_param( 'offset' ) );
587
588 // Arguments for get_posts function
589 $args = [
590 'post_type' => 'attachment',
591 'post_mime_type' => 'image',
592 'post_status' => 'inherit',
593 'posts_per_page' => 100,
594 'offset' => $offset,
595 'fields' => 'ids',
596 'no_found_rows' => true,
597 'update_post_meta_cache' => false,
598 'update_post_term_cache' => false,
599 'orderby' => [
600 'parent' => 'DESC',
601 ],
602 ];
603
604 // Initialize an array to store the image URLs
605 $image_urls = [];
606
607 add_filter( 'optml_dont_replace_url', '__return_true' );
608
609 // If site has a logo, get the logo url if offset is 0, ie first request
610 if ( $offset === 0 ) {
611 $custom_logo_id = get_theme_mod( 'custom_logo' );
612
613 if ( $custom_logo_id ) {
614 $image_urls[] = wp_get_attachment_url( $custom_logo_id );
615 }
616 }
617
618 $query = new \WP_Query( $args );
619
620 if ( $query->have_posts() ) {
621 $image_ids = $query->get_posts();
622 $image_urls = array_map( 'wp_get_attachment_url', $image_ids );
623 }
624 remove_filter( 'optml_dont_replace_url', '__return_true' );
625
626 if ( count( $image_urls ) === 0 ) {
627 return $this->response( true );
628 }
629
630 $api = new Optml_Api();
631 $api->call_onboard_api( $image_urls );
632
633 // Check if image_url array has at least 100 items, if not, we have reached the end of the images
634 return $this->response( count( $image_urls ) < 100 ? true : false );
635 }
636
637 /**
638 * Return sample image data.
639 *
640 * @return array{url: string, width: string|int, height: string|int, id: int} Image data.
641 */
642 private function fetch_sample_image() {
643 $accepted_mimes = [ 'image/jpeg' ];
644 $args = [
645 'post_type' => 'attachment',
646 'post_status' => 'any',
647 'number' => '5',
648 'no_found_rows' => true,
649 'fields' => 'ids',
650 'post_mime_type' => $accepted_mimes,
651 'post_parent__not_in' => [ 0 ],
652 ];
653 $image_result = new WP_Query( $args );
654 if ( empty( $image_result->posts ) ) {
655 $rand_id = rand( 1, 3 );
656 $original_image_url = OPTML_URL . 'assets/img/' . $rand_id . '.jpg';
657
658 return [
659 'url' => $original_image_url,
660 'width' => '700',
661 'height' => '465',
662 'id' => - 1,
663 ];
664 }
665 $attachment_id = $image_result->posts[ array_rand( $image_result->posts, 1 ) ];
666
667 $original_image_url = wp_get_attachment_image_url( $attachment_id, 'full' );
668
669 $metadata = wp_get_attachment_metadata( $attachment_id );
670
671 $width = 'auto';
672 $height = 'auto';
673 $size = 'full';
674 if ( isset( $metadata['sizes'] ) && isset( $metadata['sizes'][ $size ] ) ) {
675 $width = $metadata['sizes'][ $size ]['width'];
676 $height = $metadata['sizes'][ $size ]['height'];
677 }
678
679 return [
680 'url' => $original_image_url,
681 'id' => $attachment_id,
682 'width' => $width,
683 'height' => $height,
684 ];
685 }
686
687 /**
688 * Disconnect from optimole service.
689 *
690 * @param WP_REST_Request $request disconnect rest request.
691 *
692 * @return void
693 */
694 public function disconnect( WP_REST_Request $request ) {
695 $settings = new Optml_Settings();
696 $settings->reset();
697 wp_send_json_success( 'Disconnected' );
698 }
699
700 /**
701 * Get optimized images from API.
702 *
703 * @param WP_REST_Request $request rest request.
704 * @phpstan-param WP_REST_Request<array{api_key?: string}> $request
705 *
706 * @return WP_REST_Response
707 */
708 public function poll_optimized_images( WP_REST_Request $request ) {
709 $api_key = $request->get_param( 'api_key' );
710 $request = new Optml_Api();
711 $images = $request->get_optimized_images( $api_key );
712 if ( ! isset( $images['list'] ) || empty( $images['list'] ) ) {
713 return $this->response( [] );
714 }
715
716 $final_images = array_splice( $images['list'], 0, 10 );
717
718 foreach ( $final_images as $index => $value ) {
719 $final_images[ $index ]['url'] = Optml_Media_Offload::instance()->get_media_optimized_url(
720 $value['url'],
721 strtolower( $value['key'] ),
722 140,
723 140,
724 [
725 'type' => ResizeTypeProperty::FILL,
726 'enlarge' => false,
727 'gravity' => Position::CENTER,
728 ]
729 );
730 unset( $final_images[ $index ]['key'] );
731 }
732
733 return $this->response( $final_images );
734 }
735
736
737 /**
738 * Get conflicts from API.
739 *
740 * @param WP_REST_Request $request rest request.
741 *
742 * @return WP_REST_Response
743 */
744 public function poll_conflicts( WP_REST_Request $request ) {
745 $conflicts_to_register = apply_filters( 'optml_register_conflicts', [] );
746 $manager = new Optml_Conflict_Manager( $conflicts_to_register );
747
748 return $this->response(
749 [
750 'count' => $manager->get_conflict_count(),
751 'conflicts' => $manager->get_conflict_list(),
752 ]
753 );
754 }
755
756 /**
757 * Dismiss conflict.
758 *
759 * @param WP_REST_Request $request rest request.
760 * @phpstan-param WP_REST_Request<array{conflictID?: string}> $request
761 *
762 * @return WP_REST_Response
763 */
764 public function dismiss_conflict( WP_REST_Request $request ) {
765 $conflict_id = $request->get_param( 'conflictID' );
766 $conflicts_to_register = apply_filters( 'optml_register_conflicts', [] );
767 $manager = new Optml_Conflict_Manager( $conflicts_to_register );
768 $manager->dismiss_conflict( $conflict_id );
769
770 return $this->response(
771 [
772 'count' => $manager->get_conflict_count(),
773 'conflicts' => $manager->get_conflict_list(),
774 ]
775 );
776 }
777
778 /**
779 * Request stats update for app.
780 *
781 * @param WP_REST_Request $request rest request.
782 *
783 * @return WP_REST_Response
784 */
785 public function request_update( WP_REST_Request $request ) {
786 do_action( 'optml_daily_sync' );
787 $settings = new Optml_Settings();
788 $service_data = $settings->get( 'service_data' );
789 if ( empty( $service_data ) ) {
790 return $this->response( '', 'disconnected' );
791 }
792 return $this->response( $service_data );
793 }
794
795 /**
796 * Update options method.
797 *
798 * @param WP_REST_Request $request option update rest request.
799 * @phpstan-param WP_REST_Request<array{settings?: array<string, mixed>}> $request
800 *
801 * @return WP_REST_Response
802 */
803 public function update_option( WP_REST_Request $request ) {
804 $new_settings = $request->get_param( 'settings' );
805
806 if ( empty( $new_settings ) ) {
807 wp_send_json_error( 'No option key set.' );
808 }
809
810 $settings = new Optml_Settings();
811 $sanitized = $settings->parse_settings( $new_settings );
812
813 return $this->response( $sanitized );
814 }
815
816 /**
817 * Get total number of images.
818 *
819 * @param WP_REST_Request $request rest request object.
820 * @phpstan-param WP_REST_Request<array{action?: string, refresh?: bool}> $request
821 *
822 * @return WP_REST_Response
823 */
824 public function number_of_images_and_pages( WP_REST_Request $request ) {
825 $action = 'offload_images';
826 $refresh = false;
827
828 if ( ! empty( $request->get_param( 'action' ) ) ) {
829 $action = $request->get_param( 'action' );
830 }
831
832 if ( ! empty( $request->get_param( 'refresh' ) ) ) {
833 $refresh = $request->get_param( 'refresh' );
834 }
835
836 return $this->response( Optml_Media_Offload::move_images( $action, $refresh ) );
837 }
838
839 /**
840 * Clear the offload errors from previous offload attempts.
841 *
842 * @param WP_REST_Request $request Rest request object.
843 *
844 * @return WP_REST_Response
845 */
846 public function clear_offload_errors( WP_REST_Request $request ) {
847 $action = $request->get_param( 'action' );
848
849 if ( 'rollback_images' === $action ) {
850 $delete_count = Optml_Media_Offload::clear_rollback_errors_meta();
851 } else {
852 $delete_count = Optml_Media_Offload::clear_offload_errors_meta();
853 }
854
855 return $this->response( [ 'success' => $delete_count ] );
856 }
857
858 /**
859 * Get conflicts list.
860 *
861 * @param WP_REST_Request $request rest request object.
862 *
863 * @return WP_REST_Response
864 */
865 public function get_offload_conflicts( WP_REST_Request $request ) {
866 $request = new Optml_Api();
867 $decoded_list = $request->get_offload_conflicts();
868 $active_conflicts = [];
869 if ( isset( $decoded_list['plugins'] ) ) {
870 foreach ( $decoded_list['plugins'] as $slug ) {
871 if ( is_plugin_active( $slug ) ) {
872 $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $slug );
873 if ( ! empty( $plugin_data['Name'] ) ) {
874 $active_conflicts[] = $plugin_data['Name'];
875 }
876 }
877 }
878 }
879
880 return $this->response( $active_conflicts );
881 }
882
883 /**
884 * Insert images request.
885 *
886 * @param WP_REST_Request $request insert images rest request.
887 * @phpstan-param WP_REST_Request<array{images?: array<mixed>}> $request
888 *
889 * @return WP_REST_Response
890 */
891 public function insert_images( WP_REST_Request $request ) {
892 $images = $request->get_param( 'images' );
893
894 if ( ! is_array( $images ) || empty( $images ) ) {
895 return $this->response( [ 'error' => 'No images found' ] );
896 }
897
898 $insert = Optml_Main::instance()->dam->insert_attachments( $images );
899
900 return $this->response( $insert );
901 }
902
903 /**
904 * Dismiss a notification (set the notification key to 'yes').
905 *
906 * @param WP_REST_Request $request the incoming request.
907 * @phpstan-param WP_REST_Request<array{key?: string}> $request
908 *
909 * @return WP_REST_Response
910 */
911 public function dismiss_notice( WP_REST_Request $request ) {
912 $key = $request->get_param( 'key' );
913
914 if ( empty( $key ) ) {
915 return $this->response( [ 'error' => 'Invalid key' ], 'error' );
916 }
917
918 $result = update_option( $key, 'yes' );
919
920 if ( ! $result ) {
921 return $this->response( [ 'error' => 'Could not dismiss notice' ], 'error' );
922 }
923
924 return $this->response( [ 'success' => 'Notice dismissed' ] );
925 }
926
927 /**
928 * Store optimization data.
929 *
930 * @param WP_REST_Request $request Rest request.
931 * @phpstan-param WP_REST_Request<array{d: int, a: array<mixed>, u: string, t?: int, h?: string, pu?: string, b?: array<string, array<string, list<string>>>, l?: array{i?: string, s?: string, u?: list<string>}, m?: array<int, array{w: int, h: int}>, s?: array<int, list<array{w: int, h: int, d: int, s: string, b: int}>>, c?: array<int, bool>}> $request
932 *
933 * @return WP_REST_Response
934 */
935 public function optimizations( WP_REST_Request $request ) {
936 $settings = new Optml_Settings();
937 if ( ! Optml_Manager::should_load_profiler() ) {
938 return $this->response( 'Optimization is not enabled', 'error' );
939 }
940 $time = $request->get_param( 't' );
941 $hmac = $request->get_param( 'h' );
942 $current_url = $request->get_param( 'pu' );
943 if ( empty( $time ) || empty( $hmac ) ) {
944 return $this->response( 'Missing required parameters', 'error' );
945 }
946
947 $device_type = (int) $request->get_param( 'd' );
948 $above_fold_images = $request->get_param( 'a' );
949 $url = $request->get_param( 'u' );
950 if ( $time < time() - 300 ) {
951 return $this->response( 'Invalid Signature.', 'error' );
952 }
953 if ( wp_hash( $url . $time . $current_url, 'nonce' ) !== $hmac ) {
954 return $this->response( 'Invalid Signature.', 'error' );
955 }
956 $bg_selectors = $request->get_param( 'b' );
957 $lcp_data = $request->get_param( 'l' );
958 $crop_status = $request->get_param( 'c' );
959 $origin = $request->get_header( 'origin' );
960 if ( empty( $origin ) || ! is_allowed_http_origin( $origin ) ) {
961 return $this->response( 'Invalid origin', 'error' );
962 }
963 if ( empty( $device_type ) || empty( $url ) || ! is_array( $above_fold_images ) ) {
964 return $this->response( 'Missing required parameters', 'error' );
965 }
966 // save just the first 6 images, see https://github.com/Codeinwp/optimole-service/issues/1588#issuecomment-3357110865
967 $above_fold_images = array_slice( $above_fold_images, 0, 6 );
968 if ( count( $bg_selectors ) > 100 ) {
969 return $this->response( 'Background selectors limit exceeded', 'error' );
970 }
971 if ( $url === Profile::PLACEHOLDER ) {
972 return $this->response( 'Missing profile parameters', 'error' );
973 }
974
975 $current_selectors = array_values( Optml_Lazyload_Replacer::get_background_lazyload_selectors() );
976 $sanitized_selectors = [];
977 foreach ( $bg_selectors as $selector => $above_fold_bg_selectors ) {
978 if ( ! in_array( $selector, $current_selectors, true ) ) {
979 return $this->response( 'Invalid background selector', 'error' );
980 }
981 if ( count( $above_fold_bg_selectors ) > 100 ) {
982 return $this->response( 'Above fold background selectors limit exceeded', 'error' );
983 }
984 $selector = strip_tags( $selector );
985 $sanitized_selectors[ $selector ] = [];
986 foreach ( $above_fold_bg_selectors as $above_fold_bg_selector => $bg_urls ) {
987 if ( count( $bg_urls ) > 3 ) {
988 return $this->response( 'Background URLs limit exceeded', 'error' );
989 }
990 $sanitized_selectors[ $selector ][ strip_tags( $above_fold_bg_selector ) ] = array_filter(
991 array_map( 'sanitize_url', array_values( $bg_urls ) ),
992 function ( $url ) {
993 // we ignore urls that are not from our service
994 return strpos( $url, Optml_Config::$service_url ) === 0;
995 }
996 );
997 }
998 }
999 $missing_dimensions = $request->get_param( 'm' );
1000 $sanitized_missing_dimensions = [];
1001 if ( ! empty( $missing_dimensions ) ) {
1002 foreach ( $missing_dimensions as $id => $dimension ) {
1003 $sanitized_missing_dimensions[ intval( $id ) ] = [ 'w' => intval( $dimension['w'] ), 'h' => intval( $dimension['h'] ) ];
1004 }
1005 }
1006 $missing_srcsets = $request->get_param( 's' );
1007 $sanitized_missing_srcsets = [];
1008 if ( ! empty( $missing_srcsets ) ) {
1009 foreach ( $missing_srcsets as $id => $srcset ) {
1010 foreach ( $srcset as $size ) {
1011 if ( ! isset( $size['w'], $size['h'], $size['d'], $size['s'], $size['b'] ) ) {
1012 continue;
1013 }
1014 $sanitized_missing_srcsets[ intval( $id ) ][ intval( $size['w'] ) ] = [ 'h' => intval( $size['h'] ), 'w' => intval( $size['w'] ), 'd' => intval( $size['d'] ), 's' => sanitize_text_field( $size['s'] ), 'b' => intval( $size['b'] ) ];
1015 }
1016 }
1017 }
1018 $sanitized_crop_status = [];
1019 if ( ! empty( $crop_status ) ) {
1020 foreach ( $crop_status as $id => $crop ) {
1021 $sanitized_crop_status[ intval( $id ) ] = (bool) $crop;
1022 }
1023 }
1024 $sanitized_lcp_data = [];
1025 if ( ! empty( $lcp_data ) ) {
1026 $sanitized_lcp_data['imageId'] = sanitize_text_field( $lcp_data['i'] ?? '' );
1027 $sanitized_lcp_data['bgSelector'] = sanitize_text_field( $lcp_data['s'] ?? '' );
1028 if ( count( $lcp_data['u'] ?? [] ) > 3 ) {
1029 return $this->response( 'LCP Background URLs limit exceeded', 'error' );
1030 }
1031 $sanitized_lcp_data['bgUrls'] = array_filter(
1032 array_map( 'sanitize_url', array_values( $lcp_data['u'] ?? [] ) ),
1033 function ( $url ) {
1034 // we ignore urls that are not from our service
1035 return strpos( $url, Optml_Config::$service_url ) === 0;
1036 }
1037 );
1038 $sanitized_lcp_data['type'] = empty( $sanitized_lcp_data['imageId'] ) ? 'bg' : 'img';
1039 }
1040
1041 if ( OPTML_DEBUG ) {
1042 do_action( 'optml_log', 'Storing profile data: ' . $url . ' - ' . $device_type . ' - ' . print_r( $above_fold_images, true ) . print_r( $sanitized_selectors, true ) . print_r( $sanitized_lcp_data, true ) . print_r( $sanitized_missing_dimensions, true ) . print_r( $sanitized_missing_srcsets, true ) . print_r( $sanitized_crop_status, true ) );
1043 }
1044 $profile = new Profile();
1045 $profile->store( $url, $device_type, $above_fold_images, $sanitized_selectors, $sanitized_lcp_data, $sanitized_missing_dimensions, $sanitized_missing_srcsets, $sanitized_crop_status );
1046
1047 if ( $profile->exists_all( $url ) ) {
1048 /**
1049 * Clear cache when storing profile data.
1050 *
1051 * @var string $url The url to clear the cache for.
1052 */
1053 do_action( 'optml_clear_cache', $current_url );
1054 }
1055 return $this->response( 'Optimization data stored successfully' );
1056 }
1057
1058 /**
1059 * Method to register above fold data routes.
1060 *
1061 * @return void
1062 */
1063 public function register_optimization_routes() {
1064 foreach ( self::$rest_routes['optimization_routes'] as $route => $details ) {
1065 $this->register_route( $route, $details[0], $details['args'], $details['permission_callback'] );
1066 }
1067 }
1068
1069 /**
1070 * Move image.
1071 *
1072 * @param WP_REST_Request $request Rest request.
1073 * @phpstan-param WP_REST_Request<array{id: int, action: string, status?: string}> $request
1074 *
1075 * @return WP_REST_Response
1076 */
1077 public function move_image( WP_REST_Request $request ) {
1078 $id = $request->get_param( 'id' );
1079 $action = $request->get_param( 'action' );
1080
1081 if ( $request->get_param( 'status' ) === 'start' ) {
1082 try {
1083 Optml_Media_Offload::instance()->move_single_image( $action === 'offload_image' ? 'offload_images' : 'rollback_images', $id );
1084 } catch ( Exception $e ) {
1085 return $this->response( strip_tags( $e->getMessage() ), 'error' );
1086 }
1087 }
1088
1089 $meta = wp_get_attachment_metadata( $id );
1090 $file = $meta['file'];
1091 $result = 'not_moved';
1092 if ( $action === 'offload_image' ) {
1093 $result = Optml_Media_Offload::is_uploaded_image( $file ) ? 'moved' : 'not_moved';
1094 }
1095
1096 if ( $action === 'rollback_image' ) {
1097 $result = ! Optml_Media_Offload::is_uploaded_image( $file ) ? 'moved' : 'not_moved';
1098 }
1099
1100 return $this->response( [ $id,$action ], $result );
1101 }
1102
1103 /**
1104 * Check if user can move image to/from cloud.
1105 *
1106 * @param WP_REST_Request $request Rest request.
1107 * @phpstan-param WP_REST_Request<array{id: int, action: string}> $request Rest request.
1108 * @return bool True if user can move image, false otherwise.
1109 */
1110 public static function can_move_image( WP_REST_Request $request ) {
1111
1112 if ( ! current_user_can( 'upload_files' ) ) {
1113 return false;
1114 }
1115
1116 $id = $request->get_param( 'id' );
1117
1118 if (
1119 get_post_type( $id ) !== 'attachment' ||
1120 ! current_user_can( 'edit_post', $id )
1121 ) {
1122 return false;
1123 }
1124
1125 return true;
1126 }
1127 }
1128