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

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