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 -663 1.3.4 → 2.6.6 View file →
@@ -1,664 +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 - * @param string $slug
69 - * @param string $scope 'app', 'developer', 'user' or 'install'.
70 - * @param number $id Element's id.
71 - * @param string $public_key Public key.
72 - * @param bool $is_sandbox
73 - * @param bool|string $secret_key Element's secret key.
74 - * @param null|string $sdk_version
75 - *
76 - * @return FS_Api
77 - */
78 - static function instance(
79 - $slug,
80 - $scope,
81 - $id,
82 - $public_key,
83 - $is_sandbox,
84 - $secret_key = false,
85 - $sdk_version = null
86 - ) {
87 - $identifier = md5( $slug . $scope . $id . $public_key . ( is_string( $secret_key ) ? $secret_key : '' ) . json_encode( $is_sandbox ) );
88 -
89 - if ( ! isset( self::$_instances[ $identifier ] ) ) {
90 - self::_init();
91 -
92 - self::$_instances[ $identifier ] = new FS_Api( $slug, $scope, $id, $public_key, $secret_key, $is_sandbox, $sdk_version );
93 - }
94 -
95 - return self::$_instances[ $identifier ];
96 - }
97 -
98 - private static function _init() {
99 - if ( isset( self::$_options ) ) {
100 - return;
101 - }
102 -
103 - if ( ! class_exists( 'Freemius_Api_WordPress' ) ) {
104 - require_once WP_FS__DIR_SDK . '/FreemiusWordPress.php';
105 - }
106 -
107 - self::$_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
108 - self::$_cache = FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME );
109 -
110 - self::$_clock_diff = self::$_options->get_option( 'api_clock_diff', 0 );
111 - Freemius_Api_WordPress::SetClockDiff( self::$_clock_diff );
112 -
113 - if ( self::$_options->get_option( 'api_force_http', false ) ) {
114 - Freemius_Api_WordPress::SetHttp();
115 - }
116 - }
117 -
118 - /**
119 - * @param string $slug
120 - * @param string $scope 'app', 'developer', 'user' or 'install'.
121 - * @param number $id Element's id.
122 - * @param string $public_key Public key.
123 - * @param bool|string $secret_key Element's secret key.
124 - * @param bool $is_sandbox
125 - * @param null|string $sdk_version
126 - */
127 - private function __construct(
128 - $slug,
129 - $scope,
130 - $id,
131 - $public_key,
132 - $secret_key,
133 - $is_sandbox,
134 - $sdk_version
135 - ) {
136 - $this->_api = new Freemius_Api_WordPress( $scope, $id, $public_key, $secret_key, $is_sandbox );
137 -
138 - $this->_slug = $slug;
139 - $this->_sdk_version = $sdk_version;
140 - $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $slug . '_api', WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
141 - }
142 -
143 - /**
144 - * Find clock diff between server and API server, and store the diff locally.
145 - *
146 - * @param bool|int $diff
147 - *
148 - * @return bool|int False if clock diff didn't change, otherwise returns the clock diff in seconds.
149 - */
150 - private function _sync_clock_diff( $diff = false ) {
151 - $this->_logger->entrance();
152 -
153 - // Sync clock and store.
154 - $new_clock_diff = ( false === $diff ) ?
155 - Freemius_Api_WordPress::FindClockDiff() :
156 - $diff;
157 -
158 - if ( $new_clock_diff === self::$_clock_diff ) {
159 - return false;
160 - }
161 -
162 - self::$_clock_diff = $new_clock_diff;
163 -
164 - // Update API clock's diff.
165 - Freemius_Api_WordPress::SetClockDiff( self::$_clock_diff );
166 -
167 - // Store new clock diff in storage.
168 - self::$_options->set_option( 'api_clock_diff', self::$_clock_diff, true );
169 -
170 - return $new_clock_diff;
171 - }
172 -
173 - /**
174 - * Override API call to enable retry with servers' clock auto sync method.
175 - *
176 - * @param string $path
177 - * @param string $method
178 - * @param array $params
179 - * @param bool $retry Is in retry or first call attempt.
180 - *
181 - * @return array|mixed|string|void
182 - */
183 - private function _call( $path, $method = 'GET', $params = array(), $retry = false ) {
184 - $this->_logger->entrance( $method . ':' . $path );
185 -
186 - if ( self::is_temporary_down() ) {
187 - $result = $this->get_temporary_unavailable_error();
188 - } else {
189 - /**
190 - * @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.
191 - */
192 - if ( ! empty( $this->_sdk_version ) ) {
193 - if ( false === strpos( $path, 'sdk_version=' ) &&
194 - ! isset( $params['sdk_version'] )
195 - ) {
196 - // 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.
197 - $path = add_query_arg( 'sdk_version', $this->_sdk_version, $path );
198 - }
199 - }
200 -
201 - $result = $this->_api->Api( $path, $method, $params );
202 -
203 - if ( null !== $result &&
204 - isset( $result->error ) &&
205 - isset( $result->error->code ) &&
206 - 'request_expired' === $result->error->code
207 - ) {
208 - if ( ! $retry ) {
209 - $diff = isset( $result->error->timestamp ) ?
210 - ( time() - strtotime( $result->error->timestamp ) ) :
211 - false;
212 -
213 - // Try to sync clock diff.
214 - if ( false !== $this->_sync_clock_diff( $diff ) ) {
215 - // Retry call with new synced clock.
216 - return $this->_call( $path, $method, $params, true );
217 - }
218 - }
219 - }
220 - }
221 -
222 - if ( $this->_logger->is_on() && self::is_api_error( $result ) ) {
223 - // Log API errors.
224 - $this->_logger->api_error( $result );
225 - }
226 -
227 - return $result;
228 - }
229 -
230 - /**
231 - * Override API call to wrap it in servers' clock sync method.
232 - *
233 - * @param string $path
234 - * @param string $method
235 - * @param array $params
236 - *
237 - * @return array|mixed|string|void
238 - * @throws Freemius_Exception
239 - */
240 - function call( $path, $method = 'GET', $params = array() ) {
241 - return $this->_call( $path, $method, $params );
242 - }
243 -
244 - /**
245 - * Get API request URL signed via query string.
246 - *
247 - * @param string $path
248 - *
249 - * @return string
250 - */
251 - function get_signed_url( $path ) {
252 - return $this->_api->GetSignedUrl( $path );
253 - }
254 -
255 - /**
256 - * @param string $path
257 - * @param bool $flush
258 - * @param int $expiration (optional) Time until expiration in seconds from now, defaults to 24 hours
259 - *
260 - * @return stdClass|mixed
261 - */
262 - function get( $path = '/', $flush = false, $expiration = WP_FS__TIME_24_HOURS_IN_SEC ) {
263 - $this->_logger->entrance( $path );
264 -
265 - $cache_key = $this->get_cache_key( $path );
266 -
267 - // Always flush during development.
268 - if ( WP_FS__DEV_MODE || $this->_api->IsSandbox() ) {
269 - $flush = true;
270 - }
271 -
272 - $cached_result = self::$_cache->get( $cache_key );
273 -
274 - if ( $flush || ! self::$_cache->has_valid( $cache_key, $expiration ) ) {
275 - $result = $this->call( $path );
276 -
277 - if ( ! is_object( $result ) || isset( $result->error ) ) {
278 - // Api returned an error.
279 - if ( is_object( $cached_result ) &&
280 - ! isset( $cached_result->error )
281 - ) {
282 - // If there was an error during a newer data fetch,
283 - // fallback to older data version.
284 - $result = $cached_result;
285 -
286 - if ( $this->_logger->is_on() ) {
287 - $this->_logger->warn( 'Fallback to cached API result: ' . var_export( $cached_result, true ) );
288 - }
289 - } else {
290 - if ( is_object( $result ) && isset( $result->error->http ) && 404 == $result->error->http ) {
291 - /**
292 - * If the response code is 404, cache the result for half of the `$expiration`.
293 - *
294 - * @author Leo Fajardo (@leorw)
295 - * @since 2.2.4
296 - */
297 - $expiration /= 2;
298 - } else {
299 - // If no older data version and the response code is not 404, return result without
300 - // caching the error.
301 - return $result;
302 - }
303 - }
304 - }
305 -
306 - self::$_cache->set( $cache_key, $result, $expiration );
307 -
308 - $cached_result = $result;
309 - } else {
310 - $this->_logger->log( 'Using cached API result.' );
311 - }
312 -
313 - return $cached_result;
314 - }
315 -
316 - /**
317 - * Check if there's a cached version of the API request.
318 - *
319 - * @author Vova Feldman (@svovaf)
320 - * @since 1.2.1
321 - *
322 - * @param string $path
323 - * @param string $method
324 - * @param array $params
325 - *
326 - * @return bool
327 - */
328 - function is_cached( $path, $method = 'GET', $params = array() ) {
329 - $cache_key = $this->get_cache_key( $path, $method, $params );
330 -
331 - return self::$_cache->has_valid( $cache_key );
332 - }
333 -
334 - /**
335 - * Invalidate a cached version of the API request.
336 - *
337 - * @author Vova Feldman (@svovaf)
338 - * @since 1.2.1.5
339 - *
340 - * @param string $path
341 - * @param string $method
342 - * @param array $params
343 - */
344 - function purge_cache( $path, $method = 'GET', $params = array() ) {
345 - $this->_logger->entrance( "{$method}:{$path}" );
346 -
347 - $cache_key = $this->get_cache_key( $path, $method, $params );
348 -
349 - self::$_cache->purge( $cache_key );
350 - }
351 -
352 - /**
353 - * Invalidate a cached version of the API request.
354 - *
355 - * @author Vova Feldman (@svovaf)
356 - * @since 2.0.0
357 - *
358 - * @param string $path
359 - * @param int $expiration
360 - * @param string $method
361 - * @param array $params
362 - */
363 - function update_cache_expiration( $path, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $method = 'GET', $params = array() ) {
364 - $this->_logger->entrance( "{$method}:{$path}:{$expiration}" );
365 -
366 - $cache_key = $this->get_cache_key( $path, $method, $params );
367 -
368 - self::$_cache->update_expiration( $cache_key, $expiration );
369 - }
370 -
371 - /**
372 - * @param string $path
373 - * @param string $method
374 - * @param array $params
375 - *
376 - * @return string
377 - * @throws \Freemius_Exception
378 - */
379 - private function get_cache_key( $path, $method = 'GET', $params = array() ) {
380 - $canonized = $this->_api->CanonizePath( $path );
381 -// $exploded = explode('/', $canonized);
382 -// return $method . '_' . array_pop($exploded) . '_' . md5($canonized . json_encode($params));
383 - return strtolower( $method . ':' . $canonized ) . ( ! empty( $params ) ? '#' . md5( json_encode( $params ) ) : '' );
384 - }
385 -
386 - /**
387 - * Test API connectivity.
388 - *
389 - * @author Vova Feldman (@svovaf)
390 - * @since 1.0.9 If fails, try to fallback to HTTP.
391 - * @since 1.1.6 Added a 5-min caching mechanism, to prevent from overloading the server if the API if
392 - * temporary down.
393 - *
394 - * @return bool True if successful connectivity to the API.
395 - */
396 - static function test() {
397 - self::_init();
398 -
399 - $cache_key = 'ping_test';
400 -
401 - $test = self::$_cache->get_valid( $cache_key, null );
402 -
403 - if ( is_null( $test ) ) {
404 - $test = Freemius_Api_WordPress::Test();
405 -
406 - if ( false === $test && Freemius_Api_WordPress::IsHttps() ) {
407 - // Fallback to HTTP, since HTTPS fails.
408 - Freemius_Api_WordPress::SetHttp();
409 -
410 - self::$_options->set_option( 'api_force_http', true, true );
411 -
412 - $test = Freemius_Api_WordPress::Test();
413 -
414 - if ( false === $test ) {
415 - /**
416 - * API connectivity test fail also in HTTP request, therefore,
417 - * fallback to HTTPS to keep connection secure.
418 - *
419 - * @since 1.1.6
420 - */
421 - self::$_options->set_option( 'api_force_http', false, true );
422 - }
423 - }
424 -
425 - self::$_cache->set( $cache_key, $test, WP_FS__TIME_5_MIN_IN_SEC );
426 - }
427 -
428 - return $test;
429 - }
430 -
431 - /**
432 - * Check if API is temporary down.
433 - *
434 - * @author Vova Feldman (@svovaf)
435 - * @since 1.1.6
436 - *
437 - * @return bool
438 - */
439 - static function is_temporary_down() {
440 - self::_init();
441 -
442 - $test = self::$_cache->get_valid( 'ping_test', null );
443 -
444 - return ( false === $test );
445 - }
446 -
447 - /**
448 - * @author Vova Feldman (@svovaf)
449 - * @since 1.1.6
450 - *
451 - * @return object
452 - */
453 - private function get_temporary_unavailable_error() {
454 - return (object) array(
455 - 'error' => (object) array(
456 - 'type' => 'TemporaryUnavailable',
457 - 'message' => 'API is temporary unavailable, please retry in ' . ( self::$_cache->get_record_expiration( 'ping_test' ) - WP_FS__SCRIPT_START_TIME ) . ' sec.',
458 - 'code' => 'temporary_unavailable',
459 - 'http' => 503
460 - )
461 - );
462 - }
463 -
464 - /**
465 - * Ping API for connectivity test, and return result object.
466 - *
467 - * @author Vova Feldman (@svovaf)
468 - * @since 1.0.9
469 - *
470 - * @param null|string $unique_anonymous_id
471 - * @param array $params
472 - *
473 - * @return object
474 - */
475 - function ping( $unique_anonymous_id = null, $params = array() ) {
476 - $this->_logger->entrance();
477 -
478 - if ( self::is_temporary_down() ) {
479 - return $this->get_temporary_unavailable_error();
480 - }
481 -
482 - $pong = is_null( $unique_anonymous_id ) ?
483 - Freemius_Api_WordPress::Ping() :
484 - $this->_call( 'ping.json?' . http_build_query( array_merge(
485 - array( 'uid' => $unique_anonymous_id ),
486 - $params
487 - ) ) );
488 -
489 - if ( $this->is_valid_ping( $pong ) ) {
490 - return $pong;
491 - }
492 -
493 - if ( self::should_try_with_http( $pong ) ) {
494 - // Fallback to HTTP, since HTTPS fails.
495 - Freemius_Api_WordPress::SetHttp();
496 -
497 - self::$_options->set_option( 'api_force_http', true, true );
498 -
499 - $pong = is_null( $unique_anonymous_id ) ?
500 - Freemius_Api_WordPress::Ping() :
501 - $this->_call( 'ping.json?' . http_build_query( array_merge(
502 - array( 'uid' => $unique_anonymous_id ),
503 - $params
504 - ) ) );
505 -
506 - if ( ! $this->is_valid_ping( $pong ) ) {
507 - self::$_options->set_option( 'api_force_http', false, true );
508 - }
509 - }
510 -
511 - return $pong;
512 - }
513 -
514 - /**
515 - * Check if based on the API result we should try
516 - * to re-run the same request with HTTP instead of HTTPS.
517 - *
518 - * @author Vova Feldman (@svovaf)
519 - * @since 1.1.6
520 - *
521 - * @param $result
522 - *
523 - * @return bool
524 - */
525 - private static function should_try_with_http( $result ) {
526 - if ( ! Freemius_Api_WordPress::IsHttps() ) {
527 - return false;
528 - }
529 -
530 - return ( ! is_object( $result ) ||
531 - ! isset( $result->error ) ||
532 - ! isset( $result->error->code ) ||
533 - ! in_array( $result->error->code, array(
534 - 'curl_missing',
535 - 'cloudflare_ddos_protection',
536 - 'maintenance_mode',
537 - 'squid_cache_block',
538 - 'too_many_requests',
539 - ) ) );
540 -
541 - }
542 -
543 - /**
544 - * Check if valid ping request result.
545 - *
546 - * @author Vova Feldman (@svovaf)
547 - * @since 1.1.1
548 - *
549 - * @param mixed $pong
550 - *
551 - * @return bool
552 - */
553 - function is_valid_ping( $pong ) {
554 - return Freemius_Api_WordPress::Test( $pong );
555 - }
556 -
557 - function get_url( $path = '' ) {
558 - return Freemius_Api_WordPress::GetUrl( $path, $this->_api->IsSandbox() );
559 - }
560 -
561 - /**
562 - * Clear API cache.
563 - *
564 - * @author Vova Feldman (@svovaf)
565 - * @since 1.0.9
566 - */
567 - static function clear_cache() {
568 - self::_init();
569 -
570 - self::$_cache = FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME );
571 - self::$_cache->clear();
572 - }
573 -
574 - #----------------------------------------------------------------------------------
575 - #region Error Handling
576 - #----------------------------------------------------------------------------------
577 -
578 - /**
579 - * @author Vova Feldman (@svovaf)
580 - * @since 1.2.1.5
581 - *
582 - * @param mixed $result
583 - *
584 - * @return bool Is API result contains an error.
585 - */
586 - static function is_api_error( $result ) {
587 - return ( is_object( $result ) && isset( $result->error ) ) ||
588 - is_string( $result );
589 - }
590 -
591 - /**
592 - * @author Vova Feldman (@svovaf)
593 - * @since 2.0.0
594 - *
595 - * @param mixed $result
596 - *
597 - * @return bool Is API result contains an error.
598 - */
599 - static function is_api_error_object( $result ) {
600 - return (
601 - is_object( $result ) &&
602 - isset( $result->error ) &&
603 - isset( $result->error->message )
604 - );
605 - }
606 -
607 - /**
608 - * Checks if given API result is a non-empty and not an error object.
609 - *
610 - * @author Vova Feldman (@svovaf)
611 - * @since 1.2.1.5
612 - *
613 - * @param mixed $result
614 - * @param string|null $required_property Optional property we want to verify that is set.
615 - *
616 - * @return bool
617 - */
618 - static function is_api_result_object( $result, $required_property = null ) {
619 - return (
620 - is_object( $result ) &&
621 - ! isset( $result->error ) &&
622 - ( empty( $required_property ) || isset( $result->{$required_property} ) )
623 - );
624 - }
625 -
626 - /**
627 - * Checks if given API result is a non-empty entity object with non-empty ID.
628 - *
629 - * @author Vova Feldman (@svovaf)
630 - * @since 1.2.1.5
631 - *
632 - * @param mixed $result
633 - *
634 - * @return bool
635 - */
636 - static function is_api_result_entity( $result ) {
637 - return self::is_api_result_object( $result, 'id' ) &&
638 - FS_Entity::is_valid_id( $result->id );
639 - }
640 -
641 - /**
642 - * Get API result error code. If failed to get code, returns an empty string.
643 - *
644 - * @author Vova Feldman (@svovaf)
645 - * @since 2.0.0
646 - *
647 - * @param mixed $result
648 - *
649 - * @return string
650 - */
651 - static function get_error_code( $result ) {
652 - if ( is_object( $result ) &&
653 - isset( $result->error ) &&
654 - is_object( $result->error ) &&
655 - ! empty( $result->error->code )
656 - ) {
657 - return $result->error->code;
658 - }
659 -
660 - return '';
661 - }
662 -
663 - #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
664 719 }