PluginProbe
BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites / 2.6.1
BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites v2.6.1
4.1.0 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 2.1.2 2.5.0 2.5.3 2.6.1 All 38 releases
blockspare / freemius / includes / class-fs-api.php

class-fs-api.php in BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites 2.6.1, at freemius/includes/class-fs-api.php

719 lines 21.2 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 array|WP_Error The response array or a WP_Error on failure.
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 is_array( $response ) &&
390 (
391 empty( $response['headers'] ) ||
392 empty( $response['headers']['x-api-server'] )
393 )
394 ) {
395 // 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).
396 $response = new WP_Error( 'api_blocked', htmlentities( $response['body'] ) );
397 }
398
399 return $response;
400 }
401
402 /**
403 * Check if there's a cached version of the API request.
404 *
405 * @author Vova Feldman (@svovaf)
406 * @since 1.2.1
407 *
408 * @param string $path
409 * @param string $method
410 * @param array $params
411 *
412 * @return bool
413 */
414 function is_cached( $path, $method = 'GET', $params = array() ) {
415 $cache_key = $this->get_cache_key( $path, $method, $params );
416
417 return self::$_cache->has_valid( $cache_key );
418 }
419
420 /**
421 * Invalidate a cached version of the API request.
422 *
423 * @author Vova Feldman (@svovaf)
424 * @since 1.2.1.5
425 *
426 * @param string $path
427 * @param string $method
428 * @param array $params
429 */
430 function purge_cache( $path, $method = 'GET', $params = array() ) {
431 $this->_logger->entrance( "{$method}:{$path}" );
432
433 $cache_key = $this->get_cache_key( $path, $method, $params );
434
435 self::$_cache->purge( $cache_key );
436 }
437
438 /**
439 * Invalidate a cached version of the API request.
440 *
441 * @author Vova Feldman (@svovaf)
442 * @since 2.0.0
443 *
444 * @param string $path
445 * @param int $expiration
446 * @param string $method
447 * @param array $params
448 */
449 function update_cache_expiration( $path, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $method = 'GET', $params = array() ) {
450 $this->_logger->entrance( "{$method}:{$path}:{$expiration}" );
451
452 $cache_key = $this->get_cache_key( $path, $method, $params );
453
454 self::$_cache->update_expiration( $cache_key, $expiration );
455 }
456
457 /**
458 * @param string $path
459 * @param string $method
460 * @param array $params
461 *
462 * @return string
463 * @throws \Freemius_Exception
464 */
465 private function get_cache_key( $path, $method = 'GET', $params = array() ) {
466 $canonized = $this->_api->CanonizePath( $path );
467 // $exploded = explode('/', $canonized);
468 // return $method . '_' . array_pop($exploded) . '_' . md5($canonized . json_encode($params));
469 return strtolower( $method . ':' . $canonized ) . ( ! empty( $params ) ? '#' . md5( json_encode( $params ) ) : '' );
470 }
471
472 /**
473 * @author Leo Fajardo (@leorw)
474 * @since 2.5.4
475 *
476 * @param bool $is_http
477 */
478 private function toggle_force_http( $is_http ) {
479 self::$_options->set_option( 'api_force_http', $is_http, true );
480
481 if ( $is_http ) {
482 Freemius_Api_WordPress::SetHttp();
483 } else if ( method_exists( 'Freemius_Api_WordPress', 'SetHttps' ) ) {
484 Freemius_Api_WordPress::SetHttps();
485 }
486 }
487
488 /**
489 * @author Leo Fajardo (@leorw)
490 * @since 2.5.4
491 *
492 * @param mixed $response
493 *
494 * @return bool
495 */
496 static function is_blocked( $response ) {
497 return (
498 self::is_api_error_object( $response, true ) &&
499 isset( $response->error->code ) &&
500 'api_blocked' === $response->error->code
501 );
502 }
503
504 /**
505 * Check if API is temporary down.
506 *
507 * @author Vova Feldman (@svovaf)
508 * @since 1.1.6
509 *
510 * @return bool
511 */
512 static function is_temporary_down() {
513 self::_init();
514
515 $test = self::$_cache->get_valid( 'ping_test', null );
516
517 return ( false === $test );
518 }
519
520 /**
521 * @author Vova Feldman (@svovaf)
522 * @since 1.1.6
523 *
524 * @return object
525 */
526 private function get_temporary_unavailable_error() {
527 return (object) array(
528 'error' => (object) array(
529 'type' => 'TemporaryUnavailable',
530 'message' => 'API is temporary unavailable, please retry in ' . ( self::$_cache->get_record_expiration( 'ping_test' ) - WP_FS__SCRIPT_START_TIME ) . ' sec.',
531 'code' => 'temporary_unavailable',
532 'http' => 503
533 )
534 );
535 }
536
537 /**
538 * Check if based on the API result we should try
539 * to re-run the same request with HTTP instead of HTTPS.
540 *
541 * @author Vova Feldman (@svovaf)
542 * @since 1.1.6
543 *
544 * @param $result
545 *
546 * @return bool
547 */
548 private static function should_try_with_http( $result ) {
549 if ( ! Freemius_Api_WordPress::IsHttps() ) {
550 return false;
551 }
552
553 return ( ! is_object( $result ) ||
554 ! isset( $result->error ) ||
555 ! isset( $result->error->code ) ||
556 ! in_array( $result->error->code, array(
557 'curl_missing',
558 'cloudflare_ddos_protection',
559 'maintenance_mode',
560 'squid_cache_block',
561 'too_many_requests',
562 ) ) );
563
564 }
565
566 function get_url( $path = '' ) {
567 return Freemius_Api_WordPress::GetUrl( $path, $this->_api->IsSandbox() );
568 }
569
570 /**
571 * Clear API cache.
572 *
573 * @author Vova Feldman (@svovaf)
574 * @since 1.0.9
575 */
576 static function clear_cache() {
577 self::_init();
578
579 self::$_cache = FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME );
580 self::$_cache->clear();
581 }
582
583 /**
584 * @author Leo Fajardo (@leorw)
585 * @since 2.5.4
586 */
587 static function clear_force_http_flag() {
588 self::$_options->unset_option( 'api_force_http' );
589 }
590
591 #----------------------------------------------------------------------------------
592 #region Error Handling
593 #----------------------------------------------------------------------------------
594
595 /**
596 * @author Vova Feldman (@svovaf)
597 * @since 1.2.1.5
598 *
599 * @param mixed $result
600 *
601 * @return bool Is API result contains an error.
602 */
603 static function is_api_error( $result ) {
604 return ( is_object( $result ) && isset( $result->error ) ) ||
605 is_string( $result );
606 }
607
608 /**
609 * @author Vova Feldman (@svovaf)
610 * @since 2.0.0
611 *
612 * @param mixed $result
613 * @param bool $ignore_message
614 *
615 * @return bool Is API result contains an error.
616 */
617 static function is_api_error_object( $result, $ignore_message = false ) {
618 return (
619 is_object( $result ) &&
620 isset( $result->error ) &&
621 ( $ignore_message || isset( $result->error->message ) )
622 );
623 }
624
625 /**
626 * @author Leo Fajardo (@leorw)
627 * @since 2.5.4
628 *
629 * @param WP_Error|object|string $response
630 *
631 * @return bool
632 */
633 static function is_ssl_error_response( $response ) {
634 $http_error = null;
635
636 if ( $response instanceof WP_Error ) {
637 if (
638 isset( $response->errors ) &&
639 isset( $response->errors['http_request_failed'] )
640 ) {
641 $http_error = strtolower( $response->errors['http_request_failed'][0] );
642 }
643 } else if (
644 self::is_api_error_object( $response ) &&
645 ! empty( $response->error->message )
646 ) {
647 $http_error = $response->error->message;
648 }
649
650 return (
651 ! empty( $http_error ) &&
652 (
653 false !== strpos( $http_error, 'curl error 35' ) ||
654 (
655 false === strpos( $http_error, '</html>' ) &&
656 false !== strpos( $http_error, 'ssl' )
657 )
658 )
659 );
660 }
661
662 /**
663 * Checks if given API result is a non-empty and not an error object.
664 *
665 * @author Vova Feldman (@svovaf)
666 * @since 1.2.1.5
667 *
668 * @param mixed $result
669 * @param string|null $required_property Optional property we want to verify that is set.
670 *
671 * @return bool
672 */
673 static function is_api_result_object( $result, $required_property = null ) {
674 return (
675 is_object( $result ) &&
676 ! isset( $result->error ) &&
677 ( empty( $required_property ) || isset( $result->{$required_property} ) )
678 );
679 }
680
681 /**
682 * Checks if given API result is a non-empty entity object with non-empty ID.
683 *
684 * @author Vova Feldman (@svovaf)
685 * @since 1.2.1.5
686 *
687 * @param mixed $result
688 *
689 * @return bool
690 */
691 static function is_api_result_entity( $result ) {
692 return self::is_api_result_object( $result, 'id' ) &&
693 FS_Entity::is_valid_id( $result->id );
694 }
695
696 /**
697 * Get API result error code. If failed to get code, returns an empty string.
698 *
699 * @author Vova Feldman (@svovaf)
700 * @since 2.0.0
701 *
702 * @param mixed $result
703 *
704 * @return string
705 */
706 static function get_error_code( $result ) {
707 if ( is_object( $result ) &&
708 isset( $result->error ) &&
709 is_object( $result->error ) &&
710 ! empty( $result->error->code )
711 ) {
712 return $result->error->code;
713 }
714
715 return '';
716 }
717
718 #endregion
719 }