PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 2.2.2
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v2.2.2
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.2.2, at inc/classes/class-imagify.php

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