PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 2.3.0
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v2.3.0
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / inc / classes / class-imagify.php

class-imagify.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF 2.3.0, at inc/classes/class-imagify.php

663 lines 17.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 use Imagify\Traits\InstanceGetterTrait;
3
4 /**
5 * Imagify.io API for WordPress.
6 */
7 class Imagify {
8 use InstanceGetterTrait;
9
10 /**
11 * The Imagify API endpoint.
12 *
13 * @var string
14 */
15 const API_ENDPOINT = IMAGIFY_APP_API_URL;
16
17 /**
18 * The Imagify API key.
19 *
20 * @var string
21 */
22 private $api_key = '';
23
24 /**
25 * Random key used to store the API key in the request args.
26 *
27 * @var string
28 */
29 private $secure_key = '';
30
31 /**
32 * HTTP headers. Each http call must fill it (even if it's with an empty array).
33 *
34 * @var array
35 */
36 private $headers = [];
37
38 /**
39 * All (default) HTTP headers. They must not be modified once the class is instanciated, or it will affect any following HTTP calls.
40 *
41 * @var array
42 */
43 private $all_headers = [];
44
45 /**
46 * Filesystem object.
47 *
48 * @var object Imagify_Filesystem
49 * @since 1.7.1
50 * @author Grégory Viguier
51 */
52 protected $filesystem;
53
54 /**
55 * Use data fetched from the API.
56 *
57 * @var \stdClass|\WP_Error
58 * @since 1.9.9
59 * @author Grégory Viguier
60 */
61 protected static $user;
62
63 /**
64 * The constructor.
65 */
66 protected function __construct() {
67 if ( ! class_exists( 'Imagify_Filesystem' ) ) {
68 // Dirty patch used when updating from 1.7.
69 include_once IMAGIFY_PATH . 'inc/classes/class-imagify-filesystem.php';
70 }
71
72 $this->api_key = get_imagify_option( 'api_key' );
73 $this->secure_key = $this->generate_secure_key();
74 $this->filesystem = Imagify_Filesystem::get_instance();
75
76 $this->all_headers['Accept'] = 'Accept: application/json';
77 $this->all_headers['Content-Type'] = 'Content-Type: application/json';
78 $this->all_headers['Authorization'] = 'Authorization: token ' . $this->api_key;
79 }
80
81 /**
82 * Get your Imagify account infos.
83 *
84 * @since 1.6.5
85 *
86 * @return object
87 */
88 public function get_user() {
89 if ( empty( $this->api_key ) ) {
90 return new WP_Error( 'api_key_missing', __( 'API key required.', 'imagify' ) );
91 }
92
93 global $wp_current_filter;
94
95 if ( isset( static::$user ) ) {
96 return static::$user;
97 }
98
99 if ( in_array( 'upgrader_post_install', (array) $wp_current_filter, true ) ) {
100 // Dirty patch used when updating from 1.7.
101 static::$user = new WP_Error();
102 return static::$user;
103 }
104
105 $this->headers = $this->all_headers;
106 static::$user = $this->http_call( 'users/me/', [ 'timeout' => 10 ] );
107
108 if ( is_wp_error( static::$user ) ) {
109 return static::$user;
110 }
111
112 $maybe_missing = [
113 'account_type' => 'free',
114 'quota' => 0,
115 'extra_quota' => 0,
116 'extra_quota_consumed' => 0,
117 'consumed_current_month_quota' => 0,
118 ];
119
120 foreach ( $maybe_missing as $name => $value ) {
121 if ( ! isset( static::$user->$name ) ) {
122 static::$user->$name = $value;
123 }
124 }
125
126 return static::$user;
127 }
128
129 /**
130 * Create a user on your Imagify account.
131 *
132 * @since 1.6.5
133 *
134 * @param array $data All user data.
135 * @return object
136 */
137 public function create_user( $data ) {
138 $this->headers = [];
139
140 $data = array_merge(
141 $data,
142 [
143 'from_plugin' => true,
144 'partner' => imagify_get_partner(),
145 ]
146 );
147
148 if ( ! $data['partner'] ) {
149 unset( $data['partner'] );
150 }
151
152 $response = $this->http_call(
153 'users/',
154 [
155 'method' => 'POST',
156 'post_data' => $data,
157 ]
158 );
159
160 if ( ! is_wp_error( $response ) && isset( $data['partner'] ) ) {
161 imagify_delete_partner();
162 }
163
164 return $response;
165 }
166
167 /**
168 * Update an existing user on your Imagify account.
169 *
170 * @since 1.6.5
171 *
172 * @param string $data All user data.
173 * @return object
174 */
175 public function update_user( $data ) {
176 $this->headers = $this->all_headers;
177
178 return $this->http_call(
179 'users/me/',
180 [
181 'method' => 'PUT',
182 'post_data' => $data,
183 'timeout' => 10,
184 ]
185 );
186 }
187
188 /**
189 * Check your Imagify API key status.
190 *
191 * @since 1.6.5
192 *
193 * @param string $data The license key.
194 * @return object
195 */
196 public function get_status( $data ) {
197 static $status = [];
198
199 if ( isset( $status[ $data ] ) ) {
200 return $status[ $data ];
201 }
202
203 $this->headers = [
204 'Authorization' => 'Authorization: token ' . $data,
205 ];
206
207 $uri = 'status/';
208 $partner = imagify_get_partner();
209
210 if ( $partner ) {
211 $uri .= '?partner=' . $partner;
212 }
213
214 $status[ $data ] = $this->http_call( $uri, [ 'timeout' => 10 ] );
215
216 return $status[ $data ];
217 }
218
219 /**
220 * Get the Imagify API version.
221 *
222 * @since 1.6.5
223 *
224 * @return object
225 */
226 public function get_api_version() {
227 static $api_version;
228
229 if ( ! isset( $api_version ) ) {
230 $this->headers = [
231 'Authorization' => $this->all_headers['Authorization'],
232 ];
233
234 $api_version = $this->http_call( 'version/', [ 'timeout' => 5 ] );
235 }
236
237 return $api_version;
238 }
239
240 /**
241 * Get Public Info.
242 *
243 * @since 1.6.5
244 *
245 * @return object
246 */
247 public function get_public_info() {
248 $this->headers = $this->all_headers;
249
250 return $this->http_call( 'public-info' );
251 }
252
253 /**
254 * Optimize an image from its binary content.
255 *
256 * @since 1.6.5
257 * @since 1.6.7 $data['image'] can contain the file path (prefered) or the result of `curl_file_create()`.
258 *
259 * @param string $data All options.
260 * @return object
261 */
262 public function upload_image( $data ) {
263 $this->headers = [
264 'Authorization' => $this->all_headers['Authorization'],
265 ];
266
267 return $this->http_call(
268 'upload/',
269 [
270 'method' => 'POST',
271 'post_data' => $data,
272 ]
273 );
274 }
275
276 /**
277 * Optimize an image from its URL.
278 *
279 * @since 1.6.5
280 *
281 * @param string $data All options. Details here: --.
282 * @return object
283 */
284 public function fetch_image( $data ) {
285 $this->headers = $this->all_headers;
286
287 return $this->http_call(
288 'fetch/',
289 [
290 'method' => 'POST',
291 'post_data' => wp_json_encode( $data ),
292 ]
293 );
294 }
295
296 /**
297 * Get prices for plans.
298 *
299 * @since 1.6.5
300 *
301 * @return object
302 */
303 public function get_plans_prices() {
304 $this->headers = $this->all_headers;
305
306 return $this->http_call( 'pricing/plan/' );
307 }
308
309 /**
310 * Get all prices (Plans included).
311 *
312 * @since 1.6.5
313 *
314 * @return object
315 */
316 public function get_all_prices() {
317 $this->headers = $this->all_headers;
318
319 return $this->http_call( 'pricing/all/' );
320 }
321
322 /**
323 * Get coupon code data.
324 *
325 * @since 1.6.5
326 *
327 * @param string $coupon A coupon code.
328 * @return object
329 */
330 public function check_coupon_code( $coupon ) {
331 $this->headers = $this->all_headers;
332
333 return $this->http_call( 'coupons/' . $coupon . '/' );
334 }
335
336 /**
337 * Get information about current discount.
338 *
339 * @since 1.6.5
340 *
341 * @return object
342 */
343 public function check_discount() {
344 $this->headers = $this->all_headers;
345
346 return $this->http_call( 'pricing/discount/' );
347 }
348
349 /**
350 * Make an HTTP call using curl.
351 *
352 * @since 1.6.5
353 * @since 1.6.7 Use `wp_remote_request()` when possible (when we don't need to send an image).
354 *
355 * @param string $url The URL to call.
356 * @param array $args The request args.
357 * @return object
358 */
359 private function http_call( $url, $args = [] ) {
360 $args = array_merge(
361 [
362 'method' => 'GET',
363 'post_data' => null,
364 'timeout' => 45,
365 ],
366 $args
367 );
368
369 $endpoint = trim( $url, '/' );
370 /**
371 * Filter the timeout value for any request to the API.
372 *
373 * @since 1.6.7
374 * @author Grégory Viguier
375 *
376 * @param int $timeout Timeout value in seconds.
377 * @param string $endpoint The targetted endpoint. It's basically URI without heading nor trailing slash.
378 */
379 $args['timeout'] = apply_filters( 'imagify_api_http_request_timeout', $args['timeout'], $endpoint );
380
381 // We need to send an image: we must use cURL directly.
382 if ( isset( $args['post_data']['image'] ) ) {
383 return $this->curl_http_call( $url, $args );
384 }
385
386 $args = array_merge(
387 [
388 'headers' => [],
389 'body' => $args['post_data'],
390 'sslverify' => apply_filters( 'https_ssl_verify', false ),
391 ],
392 $args
393 );
394
395 unset( $args['post_data'] );
396
397 if ( $this->headers ) {
398 foreach ( $this->headers as $name => $value ) {
399 $value = explode( ':', $value, 2 );
400 $value = end( $value );
401
402 $args['headers'][ $name ] = trim( $value );
403 }
404 }
405
406 if ( ! empty( $args['headers']['Authorization'] ) ) {
407 // Make sure our API has not overwritten by some other plugin.
408 $args[ $this->secure_key ] = preg_replace( '/^token /', '', $args['headers']['Authorization'] );
409
410 if ( ! has_filter( 'http_request_args', [ $this, 'force_api_key_header' ] ) ) {
411 add_filter( 'http_request_args', [ $this, 'force_api_key_header' ], IMAGIFY_INT_MAX + 25, 2 );
412 }
413 }
414
415 $response = wp_remote_request( self::API_ENDPOINT . $url, $args );
416
417 if ( is_wp_error( $response ) ) {
418 return $response;
419 }
420
421 $http_code = wp_remote_retrieve_response_code( $response );
422 $response = wp_remote_retrieve_body( $response );
423
424 return $this->handle_response( $response, $http_code );
425 }
426
427 /**
428 * Make an HTTP call using curl.
429 *
430 * @since 1.6.7
431 * @throws Exception When curl_init() fails.
432 * @author Grégory Viguier
433 *
434 * @param string $url The URL to call.
435 * @param array $args The request arguments.
436 * @return object
437 */
438 private function curl_http_call( $url, $args = [] ) {
439 // Check if curl is available.
440 if ( ! Imagify_Requirements::supports_curl() ) {
441 return new WP_Error( 'curl', 'cURL isn\'t installed on the server.' );
442 }
443
444 /**
445 * Allows to mock Imagify calls to the API.
446 *
447 * @param stdClass|null $response Response from the call.
448 * @param string $url URL from the call.
449 * @param array $args Arguments from the call.
450 */
451 $response = apply_filters( 'pre_imagify_request', null, $url, $args );
452
453 if ( $response ) {
454 return $response;
455 }
456
457 try {
458 $url = self::API_ENDPOINT . $url;
459 $ch = curl_init(); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_init
460
461 if ( false === $ch ) {
462 throw new Exception( 'Could not initialize a new cURL handle' );
463 }
464
465 if ( isset( $args['post_data']['image'] ) && is_string( $args['post_data']['image'] ) && $this->filesystem->exists( $args['post_data']['image'] ) ) {
466 $args['post_data']['image'] = curl_file_create( $args['post_data']['image'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_file_create
467 }
468
469 // Handle proxies.
470 $proxy = new WP_HTTP_Proxy();
471
472 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
473 curl_setopt( $ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
474 curl_setopt( $ch, CURLOPT_PROXY, $proxy->host() ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
475 curl_setopt( $ch, CURLOPT_PROXYPORT, $proxy->port() ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
476
477 if ( $proxy->use_authentication() ) {
478 curl_setopt( $ch, CURLOPT_PROXYAUTH, CURLAUTH_ANY ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
479 curl_setopt( $ch, CURLOPT_PROXYUSERPWD, $proxy->authentication() ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
480 }
481 }
482
483 if ( 'POST' === $args['method'] ) {
484 curl_setopt( $ch, CURLOPT_POST, true ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
485 curl_setopt( $ch, CURLOPT_POSTFIELDS, $args['post_data'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
486 } elseif ( 'PUT' === $args['method'] ) {
487 curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, 'PUT' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
488 curl_setopt( $ch, CURLOPT_POSTFIELDS, $args['post_data'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
489 }
490
491 if ( defined( 'CURLOPT_PROTOCOLS' ) ) {
492 curl_setopt( $ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
493 }
494
495 $user_agent = apply_filters( 'http_headers_useragent', 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ), $url );
496
497 curl_setopt( $ch, CURLOPT_URL, $url ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
498 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
499 curl_setopt( $ch, CURLOPT_HEADER, false ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
500 curl_setopt( $ch, CURLOPT_HTTPHEADER, $this->headers ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
501 curl_setopt( $ch, CURLOPT_TIMEOUT, $args['timeout'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
502 curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, $args['timeout'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
503 curl_setopt( $ch, CURLOPT_USERAGENT, $user_agent ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
504 @curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, false ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.curl_curl_setopt
505 curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
506 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
507
508 /**
509 * Tell which http version to use with cURL during image optimization.
510 *
511 * @since 1.8.4.1
512 * @since 1.9.9 Default value is `false`.
513 * @author Grégory Viguier
514 *
515 * @param $use_version_1_0 bool True to use version 1.0. False for 1.1. Default is false.
516 */
517 if ( apply_filters( 'imagify_curl_http_version_1_0', false ) ) {
518 curl_setopt( $ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
519 } else {
520 curl_setopt( $ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
521 }
522
523 $response = curl_exec( $ch ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_exec
524 $error = curl_error( $ch ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_error
525 $http_code = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_getinfo
526
527 if ( is_resource( $ch ) ) {
528 curl_close( $ch ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_close
529 } else {
530 unset( $ch );
531 }
532 } catch ( Exception $e ) {
533 $args['headers'] = $this->headers;
534 /**
535 * Fires after a failed curl request.
536 *
537 * @since 1.6.9
538 * @author Grégory Viguier
539 *
540 * @param string $url The requested URL.
541 * @param array $args The request arguments.
542 * @param object $e The raised Exception.
543 */
544 do_action( 'imagify_curl_http_response', $url, $args, $e );
545
546 return new WP_Error( 'curl', 'An error occurred (' . $e->getMessage() . ')' );
547 }
548
549 $args['headers'] = $this->headers;
550
551 /**
552 * Fires after a successful curl request.
553 *
554 * @since 1.6.9
555 * @author Grégory Viguier
556 *
557 * @param string $url The requested URL.
558 * @param array $args The request arguments.
559 * @param string $response The request response.
560 * @param int $http_code The request HTTP code.
561 * @param string $error An error message.
562 */
563 do_action( 'imagify_curl_http_response', $url, $args, $response, $http_code, $error );
564
565 return $this->handle_response( $response, $http_code, $error );
566 }
567
568 /**
569 * Handle the request response and maybe trigger an error.
570 *
571 * @since 1.6.7
572 * @author Grégory Viguier
573 *
574 * @param string $response The request response.
575 * @param int $http_code The request HTTP code.
576 * @param string $error An error message.
577 * @return object
578 */
579 private function handle_response( $response, $http_code, $error = '' ) {
580 $response = json_decode( $response );
581
582 if ( 401 === $http_code ) {
583 // Reset the API validity cache if the API key is not valid.
584 Imagify_Requirements::reset_cache( 'api_key_valid' );
585 }
586
587 if ( 200 !== $http_code && ! empty( $response->code ) ) {
588 if ( ! empty( $response->detail ) ) {
589 return new WP_Error( 'error ' . $http_code, $response->detail );
590 }
591 if ( ! empty( $response->image ) ) {
592 $error = (array) $response->image;
593 $error = reset( $error );
594 return new WP_Error( 'error ' . $http_code, $error );
595 }
596 }
597
598 if ( 413 === $http_code ) {
599 return new WP_Error( 'error ' . $http_code, 'Your image is too big to be uploaded on our server.' );
600 }
601
602 if ( 200 !== $http_code ) {
603 $error = trim( (string) $error );
604 $error = '' !== $error ? ' - ' . htmlentities( $error ) : '';
605 return new WP_Error( 'error ' . $http_code, "Our server returned an error ({$http_code}{$error})" );
606 }
607
608 if ( ! is_object( $response ) ) {
609 return new WP_Error( 'invalid response', 'Our server returned an invalid response.', $response );
610 }
611
612 return $response;
613 }
614
615 /**
616 * Generate a random key.
617 * Similar to wp_generate_password() but without filter.
618 *
619 * @since 1.8.4
620 * @see wp_generate_password()
621 * @author Grégory Viguier
622 *
623 * @return string
624 */
625 private function generate_secure_key() {
626 $length = wp_rand( 12, 20 );
627 $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|';
628 $password = '';
629
630 for ( $i = 0; $i < $length; $i++ ) {
631 $password .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 );
632 }
633
634 return $password;
635 }
636
637 /**
638 * Filter the arguments used in an HTTP request, to make sure our API key has not been overwritten by some other plugin.
639 *
640 * @since 1.8.4
641 * @author Grégory Viguier
642 *
643 * @param array $args An array of HTTP request arguments.
644 * @param string $url The request URL.
645 * @return array
646 */
647 public function force_api_key_header( $args, $url ) {
648 if ( strpos( $url, self::API_ENDPOINT ) === false ) {
649 return $args;
650 }
651
652 if ( ! empty( $args['headers']['Authorization'] ) || ! empty( $args[ $this->secure_key ] ) ) {
653 if ( ! empty( $args[ $this->secure_key ] ) ) {
654 $args['headers']['Authorization'] = 'token ' . $args[ $this->secure_key ];
655 } else {
656 $args['headers']['Authorization'] = 'token ' . $this->api_key;
657 }
658 }
659
660 return $args;
661 }
662 }
663