PluginProbe
Adminify – White Label, Admin Menu Editor, Login Customizer / 3.1.2
Adminify – White Label, Admin Menu Editor, Login Customizer v3.1.2
4.3.1 4.3.0 4.2.26 4.2.25 4.2.24 4.2.23 4.2.22 4.2.21 4.2.20 4.2.19 4.2.18 4.2.17 4.2.16 4.2.15 4.2.14 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 4.1.17 All 164 releases
adminify / lib / freemius / includes / class-fs-api.php

class-fs-api.php in Adminify – White Label, Admin Menu Editor, Login Customizer 3.1.2, at lib/freemius/includes/class-fs-api.php

716 lines 21.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Freemius
4 * @copyright Copyright (c) 2015, Freemius, Inc.
5 * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
6 * @since 1.0.4
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 /**
14 * Class FS_Api
15 *
16 * Wraps Freemius API SDK to handle:
17 * 1. Clock sync.
18 * 2. Fallback to HTTP when HTTPS fails.
19 * 3. Adds caching layer to GET requests.
20 * 4. Adds consistency for failed requests by using last cached version.
21 */
22 class FS_Api {
23 /**
24 * @var FS_Api[]
25 */
26 private static $_instances = array();
27
28 /**
29 * @var FS_Option_Manager Freemius options, options-manager.
30 */
31 private static $_options;
32
33 /**
34 * @var FS_Cache_Manager API Caching layer
35 */
36 private static $_cache;
37
38 /**
39 * @var int Clock diff in seconds between current server to API server.
40 */
41 private static $_clock_diff;
42
43 /**
44 * @var Freemius_Api_WordPress
45 */
46 private $_api;
47
48 /**
49 * @var string
50 */
51 private $_slug;
52
53 /**
54 * @var FS_Logger
55 * @since 1.0.4
56 */
57 private $_logger;
58
59 /**
60 * @author Leo Fajardo (@leorw)
61 * @since 2.3.0
62 *
63 * @var string
64 */
65 private $_sdk_version;
66
67 /**
68 * @author Leo Fajardo (@leorw)
69 * @since 2.5.0
70 *
71 * @var string
72 */
73 private $_url;
74
75 /**
76 * @param string $slug
77 * @param string $scope 'app', 'developer', 'user' or 'install'.
78 * @param number $id Element's id.
79 * @param string $public_key Public key.
80 * @param bool $is_sandbox
81 * @param bool|string $secret_key Element's secret key.
82 * @param null|string $sdk_version
83 * @param null|string $url
84 *
85 * @return FS_Api
86 */
87 static function instance(
88 $slug,
89 $scope,
90 $id,
91 $public_key,
92 $is_sandbox,
93 $secret_key = false,
94 $sdk_version = null,
95 $url = null
96 ) {
97 $identifier = md5( $slug . $scope . $id . $public_key . ( is_string( $secret_key ) ? $secret_key : '' ) . json_encode( $is_sandbox ) );
98
99 if ( ! isset( self::$_instances[ $identifier ] ) ) {
100 self::_init();
101
102 self::$_instances[ $identifier ] = new FS_Api( $slug, $scope, $id, $public_key, $secret_key, $is_sandbox, $sdk_version, $url );
103 }
104
105 return self::$_instances[ $identifier ];
106 }
107
108 private static function _init() {
109 if ( isset( self::$_options ) ) {
110 return;
111 }
112
113 if ( ! class_exists( 'Freemius_Api_WordPress' ) ) {
114 require_once WP_FS__DIR_SDK . '/FreemiusWordPress.php';
115 }
116
117 self::$_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
118 self::$_cache = FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME );
119
120 self::$_clock_diff = self::$_options->get_option( 'api_clock_diff', 0 );
121 Freemius_Api_WordPress::SetClockDiff( self::$_clock_diff );
122
123 if ( self::$_options->get_option( 'api_force_http', false ) ) {
124 Freemius_Api_WordPress::SetHttp();
125 }
126 }
127
128 /**
129 * @param string $slug
130 * @param string $scope 'app', 'developer', 'user' or 'install'.
131 * @param number $id Element's id.
132 * @param string $public_key Public key.
133 * @param bool|string $secret_key Element's secret key.
134 * @param bool $is_sandbox
135 * @param null|string $sdk_version
136 * @param null|string $url
137 */
138 private function __construct(
139 $slug,
140 $scope,
141 $id,
142 $public_key,
143 $secret_key,
144 $is_sandbox,
145 $sdk_version,
146 $url
147 ) {
148 $this->_api = new Freemius_Api_WordPress( $scope, $id, $public_key, $secret_key, $is_sandbox );
149
150 $this->_slug = $slug;
151 $this->_sdk_version = $sdk_version;
152 $this->_url = $url;
153 $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $slug . '_api', WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
154 }
155
156 /**
157 * Find clock diff between server and API server, and store the diff locally.
158 *
159 * @param bool|int $diff
160 *
161 * @return bool|int False if clock diff didn't change, otherwise returns the clock diff in seconds.
162 */
163 private function _sync_clock_diff( $diff = false ) {
164 $this->_logger->entrance();
165
166 // Sync clock and store.
167 $new_clock_diff = ( false === $diff ) ?
168 Freemius_Api_WordPress::FindClockDiff() :
169 $diff;
170
171 if ( $new_clock_diff === self::$_clock_diff ) {
172 return false;
173 }
174
175 self::$_clock_diff = $new_clock_diff;
176
177 // Update API clock's diff.
178 Freemius_Api_WordPress::SetClockDiff( self::$_clock_diff );
179
180 // Store new clock diff in storage.
181 self::$_options->set_option( 'api_clock_diff', self::$_clock_diff, true );
182
183 return $new_clock_diff;
184 }
185
186 /**
187 * Override API call to enable retry with servers' clock auto sync method.
188 *
189 * @param string $path
190 * @param string $method
191 * @param array $params
192 * @param bool $in_retry Is in retry or first call attempt.
193 *
194 * @return array|mixed|string|void
195 */
196 private function _call( $path, $method = 'GET', $params = array(), $in_retry = false ) {
197 $this->_logger->entrance( $method . ':' . $path );
198
199 $force_http = ( ! $in_retry && self::$_options->get_option( 'api_force_http', false ) );
200
201 if ( self::is_temporary_down() ) {
202 $result = $this->get_temporary_unavailable_error();
203 } else {
204 /**
205 * @since 2.3.0 Include the SDK version with all API requests that going through the API manager. IMPORTANT: Only pass the SDK version if the caller didn't include it yet.
206 */
207 if ( ! empty( $this->_sdk_version ) ) {
208 if ( false === strpos( $path, 'sdk_version=' ) &&
209 ! isset( $params['sdk_version'] )
210 ) {
211 // Always add the sdk_version param in the querystring. DO NOT INCLUDE IT IN THE BODY PARAMS, OTHERWISE, IT MAY LEAD TO AN UNEXPECTED PARAMS PARSING IN CASES WHERE THE $params IS A REGULAR NON-ASSOCIATIVE ARRAY.
212 $path = add_query_arg( 'sdk_version', $this->_sdk_version, $path );
213 }
214 }
215
216 /**
217 * @since 2.5.0 Include the site's URL, if available, in all API requests that are going through the API manager.
218 */
219 if ( ! empty( $this->_url ) ) {
220 if ( false === strpos( $path, 'url=' ) &&
221 ! isset( $params['url'] )
222 ) {
223 $path = add_query_arg( 'url', $this->_url, $path );
224 }
225 }
226
227 $result = $this->_api->Api( $path, $method, $params );
228
229 if (
230 ! $in_retry &&
231 null !== $result &&
232 isset( $result->error ) &&
233 isset( $result->error->code )
234 ) {
235 $retry = false;
236
237 if ( 'request_expired' === $result->error->code ) {
238 $diff = isset( $result->error->timestamp ) ?
239 ( time() - strtotime( $result->error->timestamp ) ) :
240 false;
241
242 // Try to sync clock diff.
243 if ( false !== $this->_sync_clock_diff( $diff ) ) {
244 // Retry call with new synced clock.
245 $retry = true;
246 }
247 } else if (
248 Freemius_Api_WordPress::IsHttps() &&
249 FS_Api::is_ssl_error_response( $result )
250 ) {
251 $force_http = true;
252 $retry = true;
253 }
254
255 if ( $retry ) {
256 if ( $force_http ) {
257 $this->toggle_force_http( true );
258 }
259
260 $result = $this->_call( $path, $method, $params, true );
261 }
262 }
263 }
264
265 if ( self::is_api_error( $result ) ) {
266 if ( $this->_logger->is_on() ) {
267 // Log API errors.
268 $this->_logger->api_error( $result );
269 }
270
271 if ( $force_http ) {
272 $this->toggle_force_http( false );
273 }
274 }
275
276 return $result;
277 }
278
279 /**
280 * Override API call to wrap it in servers' clock sync method.
281 *
282 * @param string $path
283 * @param string $method
284 * @param array $params
285 *
286 * @return array|mixed|string|void
287 * @throws Freemius_Exception
288 */
289 function call( $path, $method = 'GET', $params = array() ) {
290 return $this->_call( $path, $method, $params );
291 }
292
293 /**
294 * Get API request URL signed via query string.
295 *
296 * @param string $path
297 *
298 * @return string
299 */
300 function get_signed_url( $path ) {
301 return $this->_api->GetSignedUrl( $path );
302 }
303
304 /**
305 * @param string $path
306 * @param bool $flush
307 * @param int $expiration (optional) Time until expiration in seconds from now, defaults to 24 hours
308 *
309 * @return stdClass|mixed
310 */
311 function get( $path = '/', $flush = false, $expiration = WP_FS__TIME_24_HOURS_IN_SEC ) {
312 $this->_logger->entrance( $path );
313
314 $cache_key = $this->get_cache_key( $path );
315
316 // Always flush during development.
317 if ( WP_FS__DEV_MODE || $this->_api->IsSandbox() ) {
318 $flush = true;
319 }
320
321 $cached_result = self::$_cache->get( $cache_key );
322
323 if ( $flush || ! self::$_cache->has_valid( $cache_key, $expiration ) ) {
324 $result = $this->call( $path );
325
326 if ( ! is_object( $result ) || isset( $result->error ) ) {
327 // Api returned an error.
328 if ( is_object( $cached_result ) &&
329 ! isset( $cached_result->error )
330 ) {
331 // If there was an error during a newer data fetch,
332 // fallback to older data version.
333 $result = $cached_result;
334
335 if ( $this->_logger->is_on() ) {
336 $this->_logger->warn( 'Fallback to cached API result: ' . var_export( $cached_result, true ) );
337 }
338 } else {
339 if ( is_object( $result ) && isset( $result->error->http ) && 404 == $result->error->http ) {
340 /**
341 * If the response code is 404, cache the result for half of the `$expiration`.
342 *
343 * @author Leo Fajardo (@leorw)
344 * @since 2.2.4
345 */
346 $expiration /= 2;
347 } else {
348 // If no older data version and the response code is not 404, return result without
349 // caching the error.
350 return $result;
351 }
352 }
353 }
354
355 self::$_cache->set( $cache_key, $result, $expiration );
356
357 $cached_result = $result;
358 } else {
359 $this->_logger->log( 'Using cached API result.' );
360 }
361
362 return $cached_result;
363 }
364
365 /**
366 * @todo Remove this method after migrating Freemius::safe_remote_post() to FS_Api::call().
367 *
368 * @author Leo Fajardo (@leorw)
369 * @since 2.5.4
370 *
371 * @param string $url
372 * @param array $remote_args
373 *
374 * @return mixed
375 */
376 static function remote_request( $url, $remote_args ) {
377 if ( ! class_exists( 'Freemius_Api_WordPress' ) ) {
378 require_once WP_FS__DIR_SDK . '/FreemiusWordPress.php';
379 }
380
381 if ( method_exists( 'Freemius_Api_WordPress', 'RemoteRequest' ) ) {
382 return Freemius_Api_WordPress::RemoteRequest( $url, $remote_args );
383 }
384
385 // The following is for backward compatibility when a modified PHP SDK version is in use and the `Freemius_Api_WordPress:RemoteRequest()` method doesn't exist.
386 $response = wp_remote_request( $url, $remote_args );
387
388 if (
389 empty( $response['headers'] ) ||
390 empty( $response['headers']['x-api-server'] )
391 ) {
392 // API is considered blocked if the response doesn't include the `x-api-server` header. When there's no error but this header doesn't exist, the response is usually not in the expected form (e.g., cannot be JSON-decoded).
393 $response = new WP_Error( 'api_blocked', htmlentities( $response['body'] ) );
394 }
395
396 return $response;
397 }
398
399 /**
400 * Check if there's a cached version of the API request.
401 *
402 * @author Vova Feldman (@svovaf)
403 * @since 1.2.1
404 *
405 * @param string $path
406 * @param string $method
407 * @param array $params
408 *
409 * @return bool
410 */
411 function is_cached( $path, $method = 'GET', $params = array() ) {
412 $cache_key = $this->get_cache_key( $path, $method, $params );
413
414 return self::$_cache->has_valid( $cache_key );
415 }
416
417 /**
418 * Invalidate a cached version of the API request.
419 *
420 * @author Vova Feldman (@svovaf)
421 * @since 1.2.1.5
422 *
423 * @param string $path
424 * @param string $method
425 * @param array $params
426 */
427 function purge_cache( $path, $method = 'GET', $params = array() ) {
428 $this->_logger->entrance( "{$method}:{$path}" );
429
430 $cache_key = $this->get_cache_key( $path, $method, $params );
431
432 self::$_cache->purge( $cache_key );
433 }
434
435 /**
436 * Invalidate a cached version of the API request.
437 *
438 * @author Vova Feldman (@svovaf)
439 * @since 2.0.0
440 *
441 * @param string $path
442 * @param int $expiration
443 * @param string $method
444 * @param array $params
445 */
446 function update_cache_expiration( $path, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $method = 'GET', $params = array() ) {
447 $this->_logger->entrance( "{$method}:{$path}:{$expiration}" );
448
449 $cache_key = $this->get_cache_key( $path, $method, $params );
450
451 self::$_cache->update_expiration( $cache_key, $expiration );
452 }
453
454 /**
455 * @param string $path
456 * @param string $method
457 * @param array $params
458 *
459 * @return string
460 * @throws \Freemius_Exception
461 */
462 private function get_cache_key( $path, $method = 'GET', $params = array() ) {
463 $canonized = $this->_api->CanonizePath( $path );
464 // $exploded = explode('/', $canonized);
465 // return $method . '_' . array_pop($exploded) . '_' . md5($canonized . json_encode($params));
466 return strtolower( $method . ':' . $canonized ) . ( ! empty( $params ) ? '#' . md5( json_encode( $params ) ) : '' );
467 }
468
469 /**
470 * @author Leo Fajardo (@leorw)
471 * @since 2.5.4
472 *
473 * @param bool $is_http
474 */
475 private function toggle_force_http( $is_http ) {
476 self::$_options->set_option( 'api_force_http', $is_http, true );
477
478 if ( $is_http ) {
479 Freemius_Api_WordPress::SetHttp();
480 } else if ( method_exists( 'Freemius_Api_WordPress', 'SetHttps' ) ) {
481 Freemius_Api_WordPress::SetHttps();
482 }
483 }
484
485 /**
486 * @author Leo Fajardo (@leorw)
487 * @since 2.5.4
488 *
489 * @param mixed $response
490 *
491 * @return bool
492 */
493 static function is_blocked( $response ) {
494 return (
495 self::is_api_error_object( $response, true ) &&
496 isset( $response->error->code ) &&
497 'api_blocked' === $response->error->code
498 );
499 }
500
501 /**
502 * Check if API is temporary down.
503 *
504 * @author Vova Feldman (@svovaf)
505 * @since 1.1.6
506 *
507 * @return bool
508 */
509 static function is_temporary_down() {
510 self::_init();
511
512 $test = self::$_cache->get_valid( 'ping_test', null );
513
514 return ( false === $test );
515 }
516
517 /**
518 * @author Vova Feldman (@svovaf)
519 * @since 1.1.6
520 *
521 * @return object
522 */
523 private function get_temporary_unavailable_error() {
524 return (object) array(
525 'error' => (object) array(
526 'type' => 'TemporaryUnavailable',
527 'message' => 'API is temporary unavailable, please retry in ' . ( self::$_cache->get_record_expiration( 'ping_test' ) - WP_FS__SCRIPT_START_TIME ) . ' sec.',
528 'code' => 'temporary_unavailable',
529 'http' => 503
530 )
531 );
532 }
533
534 /**
535 * Check if based on the API result we should try
536 * to re-run the same request with HTTP instead of HTTPS.
537 *
538 * @author Vova Feldman (@svovaf)
539 * @since 1.1.6
540 *
541 * @param $result
542 *
543 * @return bool
544 */
545 private static function should_try_with_http( $result ) {
546 if ( ! Freemius_Api_WordPress::IsHttps() ) {
547 return false;
548 }
549
550 return ( ! is_object( $result ) ||
551 ! isset( $result->error ) ||
552 ! isset( $result->error->code ) ||
553 ! in_array( $result->error->code, array(
554 'curl_missing',
555 'cloudflare_ddos_protection',
556 'maintenance_mode',
557 'squid_cache_block',
558 'too_many_requests',
559 ) ) );
560
561 }
562
563 function get_url( $path = '' ) {
564 return Freemius_Api_WordPress::GetUrl( $path, $this->_api->IsSandbox() );
565 }
566
567 /**
568 * Clear API cache.
569 *
570 * @author Vova Feldman (@svovaf)
571 * @since 1.0.9
572 */
573 static function clear_cache() {
574 self::_init();
575
576 self::$_cache = FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME );
577 self::$_cache->clear();
578 }
579
580 /**
581 * @author Leo Fajardo (@leorw)
582 * @since 2.5.4
583 */
584 static function clear_force_http_flag() {
585 self::$_options->unset_option( 'api_force_http' );
586 }
587
588 #----------------------------------------------------------------------------------
589 #region Error Handling
590 #----------------------------------------------------------------------------------
591
592 /**
593 * @author Vova Feldman (@svovaf)
594 * @since 1.2.1.5
595 *
596 * @param mixed $result
597 *
598 * @return bool Is API result contains an error.
599 */
600 static function is_api_error( $result ) {
601 return ( is_object( $result ) && isset( $result->error ) ) ||
602 is_string( $result );
603 }
604
605 /**
606 * @author Vova Feldman (@svovaf)
607 * @since 2.0.0
608 *
609 * @param mixed $result
610 * @param bool $ignore_message
611 *
612 * @return bool Is API result contains an error.
613 */
614 static function is_api_error_object( $result, $ignore_message = false ) {
615 return (
616 is_object( $result ) &&
617 isset( $result->error ) &&
618 ( $ignore_message || isset( $result->error->message ) )
619 );
620 }
621
622 /**
623 * @author Leo Fajardo (@leorw)
624 * @since 2.5.4
625 *
626 * @param WP_Error|object|string $response
627 *
628 * @return bool
629 */
630 static function is_ssl_error_response( $response ) {
631 $http_error = null;
632
633 if ( $response instanceof WP_Error ) {
634 if (
635 isset( $response->errors ) &&
636 isset( $response->errors['http_request_failed'] )
637 ) {
638 $http_error = strtolower( $response->errors['http_request_failed'][0] );
639 }
640 } else if (
641 self::is_api_error_object( $response ) &&
642 ! empty( $response->error->message )
643 ) {
644 $http_error = $response->error->message;
645 }
646
647 return (
648 ! empty( $http_error ) &&
649 (
650 false !== strpos( $http_error, 'curl error 35' ) ||
651 (
652 false === strpos( $http_error, '</html>' ) &&
653 false !== strpos( $http_error, 'ssl' )
654 )
655 )
656 );
657 }
658
659 /**
660 * Checks if given API result is a non-empty and not an error object.
661 *
662 * @author Vova Feldman (@svovaf)
663 * @since 1.2.1.5
664 *
665 * @param mixed $result
666 * @param string|null $required_property Optional property we want to verify that is set.
667 *
668 * @return bool
669 */
670 static function is_api_result_object( $result, $required_property = null ) {
671 return (
672 is_object( $result ) &&
673 ! isset( $result->error ) &&
674 ( empty( $required_property ) || isset( $result->{$required_property} ) )
675 );
676 }
677
678 /**
679 * Checks if given API result is a non-empty entity object with non-empty ID.
680 *
681 * @author Vova Feldman (@svovaf)
682 * @since 1.2.1.5
683 *
684 * @param mixed $result
685 *
686 * @return bool
687 */
688 static function is_api_result_entity( $result ) {
689 return self::is_api_result_object( $result, 'id' ) &&
690 FS_Entity::is_valid_id( $result->id );
691 }
692
693 /**
694 * Get API result error code. If failed to get code, returns an empty string.
695 *
696 * @author Vova Feldman (@svovaf)
697 * @since 2.0.0
698 *
699 * @param mixed $result
700 *
701 * @return string
702 */
703 static function get_error_code( $result ) {
704 if ( is_object( $result ) &&
705 isset( $result->error ) &&
706 is_object( $result->error ) &&
707 ! empty( $result->error->code )
708 ) {
709 return $result->error->code;
710 }
711
712 return '';
713 }
714
715 #endregion
716 }