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