PluginProbe
BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites / trunk
BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites vtrunk
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
← All changes | freemius/includes/class-fs-api.php +723 -663 2.1.2trunk View file →
@@ -1,664 +1,724 @@
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 + $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
664 724 }