PluginProbe
BlockSpare – Gutenberg Blocks, AI Content Generator & Site Builder for News, Magazine & Blogs / 2.6.6
BlockSpare – Gutenberg Blocks, AI Content Generator & Site Builder for News, Magazine & Blogs v2.6.6
5.0.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 All 39 releases
← All changes | freemius/includes/class-fs-api.php +718 -723 5.0.1 → 2.6.6 View file →
@@ -1,724 +1,719 @@
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 - $has_valid_cache = self::$_cache->has_valid( $cache_key, $expiration );
322 - $cached_result = $has_valid_cache ?
323 - self::$_cache->get( $cache_key ) :
324 - null;
325 -
326 - if ( $flush || is_null( $cached_result ) ) {
327 - $result = $this->call( $path );
328 -
329 - if ( ! is_object( $result ) || isset( $result->error ) ) {
330 - // Api returned an error.
331 - if ( is_object( $cached_result ) &&
332 - ! isset( $cached_result->error )
333 - ) {
334 - // If there was an error during a newer data fetch,
335 - // fallback to older data version.
336 - $result = $cached_result;
337 -
338 - if ( $this->_logger->is_on() ) {
339 - $this->_logger->warn( 'Fallback to cached API result: ' . var_export( $cached_result, true ) );
340 - }
341 - } else {
342 - if ( is_object( $result ) && isset( $result->error->http ) && 404 == $result->error->http ) {
343 - /**
344 - * If the response code is 404, cache the result for half of the `$expiration`.
345 - *
346 - * @author Leo Fajardo (@leorw)
347 - * @since 2.2.4
348 - */
349 - $expiration /= 2;
350 - } else {
351 - // If no older data version and the response code is not 404, return result without
352 - // caching the error.
353 - return $result;
354 - }
355 - }
356 - }
357 -
358 - if ( is_numeric( $expiration ) ) {
359 - self::$_cache->set( $cache_key, $result, $expiration );
360 - }
361 -
362 - $cached_result = $result;
363 - } else {
364 - $this->_logger->log( 'Using cached API result.' );
365 - }
366 -
367 - return $cached_result;
368 - }
369 -
370 - /**
371 - * @todo Remove this method after migrating Freemius::safe_remote_post() to FS_Api::call().
372 - *
373 - * @author Leo Fajardo (@leorw)
374 - * @since 2.5.4
375 - *
376 - * @param string $url
377 - * @param array $remote_args
378 - *
379 - * @return array|WP_Error The response array or a WP_Error on failure.
380 - */
381 - static function remote_request( $url, $remote_args ) {
382 - if ( ! class_exists( 'Freemius_Api_WordPress' ) ) {
383 - require_once WP_FS__DIR_SDK . '/FreemiusWordPress.php';
384 - }
385 -
386 - if ( method_exists( 'Freemius_Api_WordPress', 'RemoteRequest' ) ) {
387 - return Freemius_Api_WordPress::RemoteRequest( $url, $remote_args );
388 - }
389 -
390 - // 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.
391 - $response = wp_remote_request( $url, $remote_args );
392 -
393 - if (
394 - is_array( $response ) &&
395 - (
396 - empty( $response['headers'] ) ||
397 - empty( $response['headers']['x-api-server'] )
398 - )
399 - ) {
400 - // 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).
401 - $response = new WP_Error( 'api_blocked', htmlentities( $response['body'] ) );
402 - }
403 -
404 - return $response;
405 - }
406 -
407 - /**
408 - * Check if there's a cached version of the API request.
409 - *
410 - * @author Vova Feldman (@svovaf)
411 - * @since 1.2.1
412 - *
413 - * @param string $path
414 - * @param string $method
415 - * @param array $params
416 - *
417 - * @return bool
418 - */
419 - function is_cached( $path, $method = 'GET', $params = array() ) {
420 - $cache_key = $this->get_cache_key( $path, $method, $params );
421 -
422 - return self::$_cache->has_valid( $cache_key );
423 - }
424 -
425 - /**
426 - * Invalidate a cached version of the API request.
427 - *
428 - * @author Vova Feldman (@svovaf)
429 - * @since 1.2.1.5
430 - *
431 - * @param string $path
432 - * @param string $method
433 - * @param array $params
434 - */
435 - function purge_cache( $path, $method = 'GET', $params = array() ) {
436 - $this->_logger->entrance( "{$method}:{$path}" );
437 -
438 - $cache_key = $this->get_cache_key( $path, $method, $params );
439 -
440 - self::$_cache->purge( $cache_key );
441 - }
442 -
443 - /**
444 - * Invalidate a cached version of the API request.
445 - *
446 - * @author Vova Feldman (@svovaf)
447 - * @since 2.0.0
448 - *
449 - * @param string $path
450 - * @param int $expiration
451 - * @param string $method
452 - * @param array $params
453 - */
454 - function update_cache_expiration( $path, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $method = 'GET', $params = array() ) {
455 - $this->_logger->entrance( "{$method}:{$path}:{$expiration}" );
456 -
457 - $cache_key = $this->get_cache_key( $path, $method, $params );
458 -
459 - self::$_cache->update_expiration( $cache_key, $expiration );
460 - }
461 -
462 - /**
463 - * @param string $path
464 - * @param string $method
465 - * @param array $params
466 - *
467 - * @return string
468 - * @throws \Freemius_Exception
469 - */
470 - private function get_cache_key( $path, $method = 'GET', $params = array() ) {
471 - $canonized = $this->_api->CanonizePath( $path );
472 -// $exploded = explode('/', $canonized);
473 -// return $method . '_' . array_pop($exploded) . '_' . md5($canonized . json_encode($params));
474 - return strtolower( $method . ':' . $canonized ) . ( ! empty( $params ) ? '#' . md5( json_encode( $params ) ) : '' );
475 - }
476 -
477 - /**
478 - * @author Leo Fajardo (@leorw)
479 - * @since 2.5.4
480 - *
481 - * @param bool $is_http
482 - */
483 - private function toggle_force_http( $is_http ) {
484 - self::$_options->set_option( 'api_force_http', $is_http, true );
485 -
486 - if ( $is_http ) {
487 - Freemius_Api_WordPress::SetHttp();
488 - } else if ( method_exists( 'Freemius_Api_WordPress', 'SetHttps' ) ) {
489 - Freemius_Api_WordPress::SetHttps();
490 - }
491 - }
492 -
493 - /**
494 - * @author Leo Fajardo (@leorw)
495 - * @since 2.5.4
496 - *
497 - * @param mixed $response
498 - *
499 - * @return bool
500 - */
501 - static function is_blocked( $response ) {
502 - return (
503 - self::is_api_error_object( $response, true ) &&
504 - isset( $response->error->code ) &&
505 - 'api_blocked' === $response->error->code
506 - );
507 - }
508 -
509 - /**
510 - * Check if API is temporary down.
511 - *
512 - * @author Vova Feldman (@svovaf)
513 - * @since 1.1.6
514 - *
515 - * @return bool
516 - */
517 - static function is_temporary_down() {
518 - self::_init();
519 -
520 - $test = self::$_cache->get_valid( 'ping_test', null );
521 -
522 - return ( false === $test );
523 - }
524 -
525 - /**
526 - * @author Vova Feldman (@svovaf)
527 - * @since 1.1.6
528 - *
529 - * @return object
530 - */
531 - private function get_temporary_unavailable_error() {
532 - return (object) array(
533 - 'error' => (object) array(
534 - 'type' => 'TemporaryUnavailable',
535 - 'message' => 'API is temporary unavailable, please retry in ' . ( self::$_cache->get_record_expiration( 'ping_test' ) - WP_FS__SCRIPT_START_TIME ) . ' sec.',
536 - 'code' => 'temporary_unavailable',
537 - 'http' => 503
538 - )
539 - );
540 - }
541 -
542 - /**
543 - * Check if based on the API result we should try
544 - * to re-run the same request with HTTP instead of HTTPS.
545 - *
546 - * @author Vova Feldman (@svovaf)
547 - * @since 1.1.6
548 - *
549 - * @param $result
550 - *
551 - * @return bool
552 - */
553 - private static function should_try_with_http( $result ) {
554 - if ( ! Freemius_Api_WordPress::IsHttps() ) {
555 - return false;
556 - }
557 -
558 - return ( ! is_object( $result ) ||
559 - ! isset( $result->error ) ||
560 - ! isset( $result->error->code ) ||
561 - ! in_array( $result->error->code, array(
562 - 'curl_missing',
563 - 'cloudflare_ddos_protection',
564 - 'maintenance_mode',
565 - 'squid_cache_block',
566 - 'too_many_requests',
567 - ) ) );
568 -
569 - }
570 -
571 - function get_url( $path = '' ) {
572 - return Freemius_Api_WordPress::GetUrl( $path, $this->_api->IsSandbox() );
573 - }
574 -
575 - /**
576 - * Clear API cache.
577 - *
578 - * @author Vova Feldman (@svovaf)
579 - * @since 1.0.9
580 - */
581 - static function clear_cache() {
582 - self::_init();
583 -
584 - self::$_cache = FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME );
585 - self::$_cache->clear();
586 - }
587 -
588 - /**
589 - * @author Leo Fajardo (@leorw)
590 - * @since 2.5.4
591 - */
592 - static function clear_force_http_flag() {
593 - self::$_options->unset_option( 'api_force_http' );
594 - }
595 -
596 - #----------------------------------------------------------------------------------
597 - #region Error Handling
598 - #----------------------------------------------------------------------------------
599 -
600 - /**
601 - * @author Vova Feldman (@svovaf)
602 - * @since 1.2.1.5
603 - *
604 - * @param mixed $result
605 - *
606 - * @return bool Is API result contains an error.
607 - */
608 - static function is_api_error( $result ) {
609 - return ( is_object( $result ) && isset( $result->error ) ) ||
610 - is_string( $result );
611 - }
612 -
613 - /**
614 - * @author Vova Feldman (@svovaf)
615 - * @since 2.0.0
616 - *
617 - * @param mixed $result
618 - * @param bool $ignore_message
619 - *
620 - * @return bool Is API result contains an error.
621 - */
622 - static function is_api_error_object( $result, $ignore_message = false ) {
623 - return (
624 - is_object( $result ) &&
625 - isset( $result->error ) &&
626 - ( $ignore_message || isset( $result->error->message ) )
627 - );
628 - }
629 -
630 - /**
631 - * @author Leo Fajardo (@leorw)
632 - * @since 2.5.4
633 - *
634 - * @param WP_Error|object|string $response
635 - *
636 - * @return bool
637 - */
638 - static function is_ssl_error_response( $response ) {
639 - $http_error = null;
640 -
641 - if ( $response instanceof WP_Error ) {
642 - if (
643 - isset( $response->errors ) &&
644 - isset( $response->errors['http_request_failed'] )
645 - ) {
646 - $http_error = strtolower( $response->errors['http_request_failed'][0] );
647 - }
648 - } else if (
649 - self::is_api_error_object( $response ) &&
650 - ! empty( $response->error->message )
651 - ) {
652 - $http_error = $response->error->message;
653 - }
654 -
655 - return (
656 - ! empty( $http_error ) &&
657 - (
658 - false !== strpos( $http_error, 'curl error 35' ) ||
659 - (
660 - false === strpos( $http_error, '</html>' ) &&
661 - false !== strpos( $http_error, 'ssl' )
662 - )
663 - )
664 - );
665 - }
666 -
667 - /**
668 - * Checks if given API result is a non-empty and not an error object.
669 - *
670 - * @author Vova Feldman (@svovaf)
671 - * @since 1.2.1.5
672 - *
673 - * @param mixed $result
674 - * @param string|null $required_property Optional property we want to verify that is set.
675 - *
676 - * @return bool
677 - */
678 - static function is_api_result_object( $result, $required_property = null ) {
679 - return (
680 - is_object( $result ) &&
681 - ! isset( $result->error ) &&
682 - ( empty( $required_property ) || isset( $result->{$required_property} ) )
683 - );
684 - }
685 -
686 - /**
687 - * Checks if given API result is a non-empty entity object with non-empty ID.
688 - *
689 - * @author Vova Feldman (@svovaf)
690 - * @since 1.2.1.5
691 - *
692 - * @param mixed $result
693 - *
694 - * @return bool
695 - */
696 - static function is_api_result_entity( $result ) {
697 - return self::is_api_result_object( $result, 'id' ) &&
698 - FS_Entity::is_valid_id( $result->id );
699 - }
700 -
701 - /**
702 - * Get API result error code. If failed to get code, returns an empty string.
703 - *
704 - * @author Vova Feldman (@svovaf)
705 - * @since 2.0.0
706 - *
707 - * @param mixed $result
708 - *
709 - * @return string
710 - */
711 - static function get_error_code( $result ) {
712 - if ( is_object( $result ) &&
713 - isset( $result->error ) &&
714 - is_object( $result->error ) &&
715 - ! empty( $result->error->code )
716 - ) {
717 - return $result->error->code;
718 - }
719 -
720 - return '';
721 - }
722 -
723 - #endregion
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
724 719 }