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 / rest.php

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

1,129 lines 32.0 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 ( is_wp_error( $images ) || ! is_array( $images ) || 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 $above_fold_images = array_values( array_map( 'intval', array_filter( $above_fold_images, 'is_numeric' ) ) );
969 if ( count( $bg_selectors ) > 100 ) {
970 return $this->response( 'Background selectors limit exceeded', 'error' );
971 }
972 if ( $url === Profile::PLACEHOLDER ) {
973 return $this->response( 'Missing profile parameters', 'error' );
974 }
975
976 $current_selectors = array_values( Optml_Lazyload_Replacer::get_background_lazyload_selectors() );
977 $sanitized_selectors = [];
978 foreach ( $bg_selectors as $selector => $above_fold_bg_selectors ) {
979 if ( ! in_array( $selector, $current_selectors, true ) ) {
980 return $this->response( 'Invalid background selector', 'error' );
981 }
982 if ( count( $above_fold_bg_selectors ) > 100 ) {
983 return $this->response( 'Above fold background selectors limit exceeded', 'error' );
984 }
985 $selector = strip_tags( $selector );
986 $sanitized_selectors[ $selector ] = [];
987 foreach ( $above_fold_bg_selectors as $above_fold_bg_selector => $bg_urls ) {
988 if ( count( $bg_urls ) > 3 ) {
989 return $this->response( 'Background URLs limit exceeded', 'error' );
990 }
991 $sanitized_selectors[ $selector ][ strip_tags( $above_fold_bg_selector ) ] = array_filter(
992 array_map( 'sanitize_url', array_values( $bg_urls ) ),
993 function ( $url ) {
994 // we ignore urls that are not from our service
995 return strpos( $url, Optml_Config::$service_url ) === 0;
996 }
997 );
998 }
999 }
1000 $missing_dimensions = $request->get_param( 'm' );
1001 $sanitized_missing_dimensions = [];
1002 if ( ! empty( $missing_dimensions ) ) {
1003 foreach ( $missing_dimensions as $id => $dimension ) {
1004 $sanitized_missing_dimensions[ intval( $id ) ] = [ 'w' => intval( $dimension['w'] ), 'h' => intval( $dimension['h'] ) ];
1005 }
1006 }
1007 $missing_srcsets = $request->get_param( 's' );
1008 $sanitized_missing_srcsets = [];
1009 if ( ! empty( $missing_srcsets ) ) {
1010 foreach ( $missing_srcsets as $id => $srcset ) {
1011 foreach ( $srcset as $size ) {
1012 if ( ! isset( $size['w'], $size['h'], $size['d'], $size['s'], $size['b'] ) ) {
1013 continue;
1014 }
1015 $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'] ) ];
1016 }
1017 }
1018 }
1019 $sanitized_crop_status = [];
1020 if ( ! empty( $crop_status ) ) {
1021 foreach ( $crop_status as $id => $crop ) {
1022 $sanitized_crop_status[ intval( $id ) ] = (bool) $crop;
1023 }
1024 }
1025 $sanitized_lcp_data = [];
1026 if ( ! empty( $lcp_data ) ) {
1027 $sanitized_lcp_data['imageId'] = sanitize_text_field( $lcp_data['i'] ?? '' );
1028 $sanitized_lcp_data['bgSelector'] = sanitize_text_field( $lcp_data['s'] ?? '' );
1029 if ( count( $lcp_data['u'] ?? [] ) > 3 ) {
1030 return $this->response( 'LCP Background URLs limit exceeded', 'error' );
1031 }
1032 $sanitized_lcp_data['bgUrls'] = array_filter(
1033 array_map( 'sanitize_url', array_values( $lcp_data['u'] ?? [] ) ),
1034 function ( $url ) {
1035 // we ignore urls that are not from our service
1036 return strpos( $url, Optml_Config::$service_url ) === 0;
1037 }
1038 );
1039 $sanitized_lcp_data['type'] = empty( $sanitized_lcp_data['imageId'] ) ? 'bg' : 'img';
1040 }
1041
1042 if ( OPTML_DEBUG ) {
1043 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 ) );
1044 }
1045 $profile = new Profile();
1046 $profile->store( $url, $device_type, $above_fold_images, $sanitized_selectors, $sanitized_lcp_data, $sanitized_missing_dimensions, $sanitized_missing_srcsets, $sanitized_crop_status );
1047
1048 if ( $profile->exists_all( $url ) ) {
1049 /**
1050 * Clear cache when storing profile data.
1051 *
1052 * @var string $url The url to clear the cache for.
1053 */
1054 do_action( 'optml_clear_cache', $current_url );
1055 }
1056 return $this->response( 'Optimization data stored successfully' );
1057 }
1058
1059 /**
1060 * Method to register above fold data routes.
1061 *
1062 * @return void
1063 */
1064 public function register_optimization_routes() {
1065 foreach ( self::$rest_routes['optimization_routes'] as $route => $details ) {
1066 $this->register_route( $route, $details[0], $details['args'], $details['permission_callback'] );
1067 }
1068 }
1069
1070 /**
1071 * Move image.
1072 *
1073 * @param WP_REST_Request $request Rest request.
1074 * @phpstan-param WP_REST_Request<array{id: int, action: string, status?: string}> $request
1075 *
1076 * @return WP_REST_Response
1077 */
1078 public function move_image( WP_REST_Request $request ) {
1079 $id = $request->get_param( 'id' );
1080 $action = $request->get_param( 'action' );
1081
1082 if ( $request->get_param( 'status' ) === 'start' ) {
1083 try {
1084 Optml_Media_Offload::instance()->move_single_image( $action === 'offload_image' ? 'offload_images' : 'rollback_images', $id );
1085 } catch ( Exception $e ) {
1086 return $this->response( strip_tags( $e->getMessage() ), 'error' );
1087 }
1088 }
1089
1090 $meta = wp_get_attachment_metadata( $id );
1091 $file = $meta['file'];
1092 $result = 'not_moved';
1093 if ( $action === 'offload_image' ) {
1094 $result = Optml_Media_Offload::is_uploaded_image( $file ) ? 'moved' : 'not_moved';
1095 }
1096
1097 if ( $action === 'rollback_image' ) {
1098 $result = ! Optml_Media_Offload::is_uploaded_image( $file ) ? 'moved' : 'not_moved';
1099 }
1100
1101 return $this->response( [ $id,$action ], $result );
1102 }
1103
1104 /**
1105 * Check if user can move image to/from cloud.
1106 *
1107 * @param WP_REST_Request $request Rest request.
1108 * @phpstan-param WP_REST_Request<array{id: int, action: string}> $request Rest request.
1109 * @return bool True if user can move image, false otherwise.
1110 */
1111 public static function can_move_image( WP_REST_Request $request ) {
1112
1113 if ( ! current_user_can( 'upload_files' ) ) {
1114 return false;
1115 }
1116
1117 $id = $request->get_param( 'id' );
1118
1119 if (
1120 get_post_type( $id ) !== 'attachment' ||
1121 ! current_user_can( 'edit_post', $id )
1122 ) {
1123 return false;
1124 }
1125
1126 return true;
1127 }
1128 }
1129