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 -687 2.5.0trunk View file →
@@ -1,688 +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 - * @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 $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(), $retry = false ) {
197 - $this->_logger->entrance( $method . ':' . $path );
198 -
199 - if ( self::is_temporary_down() ) {
200 - $result = $this->get_temporary_unavailable_error();
201 - } else {
202 - /**
203 - * @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.
204 - */
205 - if ( ! empty( $this->_sdk_version ) ) {
206 - if ( false === strpos( $path, 'sdk_version=' ) &&
207 - ! isset( $params['sdk_version'] )
208 - ) {
209 - // 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.
210 - $path = add_query_arg( 'sdk_version', $this->_sdk_version, $path );
211 - }
212 - }
213 -
214 - /**
215 - * @since 2.5.0 Include the site's URL, if available, in all API requests that are going through the API manager.
216 - */
217 - if ( ! empty( $this->_url ) ) {
218 - if ( false === strpos( $path, 'url=' ) &&
219 - ! isset( $params['url'] )
220 - ) {
221 - $path = add_query_arg( 'url', $this->_url, $path );
222 - }
223 - }
224 -
225 - $result = $this->_api->Api( $path, $method, $params );
226 -
227 - if ( null !== $result &&
228 - isset( $result->error ) &&
229 - isset( $result->error->code ) &&
230 - 'request_expired' === $result->error->code
231 - ) {
232 - if ( ! $retry ) {
233 - $diff = isset( $result->error->timestamp ) ?
234 - ( time() - strtotime( $result->error->timestamp ) ) :
235 - false;
236 -
237 - // Try to sync clock diff.
238 - if ( false !== $this->_sync_clock_diff( $diff ) ) {
239 - // Retry call with new synced clock.
240 - return $this->_call( $path, $method, $params, true );
241 - }
242 - }
243 - }
244 - }
245 -
246 - if ( $this->_logger->is_on() && self::is_api_error( $result ) ) {
247 - // Log API errors.
248 - $this->_logger->api_error( $result );
249 - }
250 -
251 - return $result;
252 - }
253 -
254 - /**
255 - * Override API call to wrap it in servers' clock sync method.
256 - *
257 - * @param string $path
258 - * @param string $method
259 - * @param array $params
260 - *
261 - * @return array|mixed|string|void
262 - * @throws Freemius_Exception
263 - */
264 - function call( $path, $method = 'GET', $params = array() ) {
265 - return $this->_call( $path, $method, $params );
266 - }
267 -
268 - /**
269 - * Get API request URL signed via query string.
270 - *
271 - * @param string $path
272 - *
273 - * @return string
274 - */
275 - function get_signed_url( $path ) {
276 - return $this->_api->GetSignedUrl( $path );
277 - }
278 -
279 - /**
280 - * @param string $path
281 - * @param bool $flush
282 - * @param int $expiration (optional) Time until expiration in seconds from now, defaults to 24 hours
283 - *
284 - * @return stdClass|mixed
285 - */
286 - function get( $path = '/', $flush = false, $expiration = WP_FS__TIME_24_HOURS_IN_SEC ) {
287 - $this->_logger->entrance( $path );
288 -
289 - $cache_key = $this->get_cache_key( $path );
290 -
291 - // Always flush during development.
292 - if ( WP_FS__DEV_MODE || $this->_api->IsSandbox() ) {
293 - $flush = true;
294 - }
295 -
296 - $cached_result = self::$_cache->get( $cache_key );
297 -
298 - if ( $flush || ! self::$_cache->has_valid( $cache_key, $expiration ) ) {
299 - $result = $this->call( $path );
300 -
301 - if ( ! is_object( $result ) || isset( $result->error ) ) {
302 - // Api returned an error.
303 - if ( is_object( $cached_result ) &&
304 - ! isset( $cached_result->error )
305 - ) {
306 - // If there was an error during a newer data fetch,
307 - // fallback to older data version.
308 - $result = $cached_result;
309 -
310 - if ( $this->_logger->is_on() ) {
311 - $this->_logger->warn( 'Fallback to cached API result: ' . var_export( $cached_result, true ) );
312 - }
313 - } else {
314 - if ( is_object( $result ) && isset( $result->error->http ) && 404 == $result->error->http ) {
315 - /**
316 - * If the response code is 404, cache the result for half of the `$expiration`.
317 - *
318 - * @author Leo Fajardo (@leorw)
319 - * @since 2.2.4
320 - */
321 - $expiration /= 2;
322 - } else {
323 - // If no older data version and the response code is not 404, return result without
324 - // caching the error.
325 - return $result;
326 - }
327 - }
328 - }
329 -
330 - self::$_cache->set( $cache_key, $result, $expiration );
331 -
332 - $cached_result = $result;
333 - } else {
334 - $this->_logger->log( 'Using cached API result.' );
335 - }
336 -
337 - return $cached_result;
338 - }
339 -
340 - /**
341 - * Check if there's a cached version of the API request.
342 - *
343 - * @author Vova Feldman (@svovaf)
344 - * @since 1.2.1
345 - *
346 - * @param string $path
347 - * @param string $method
348 - * @param array $params
349 - *
350 - * @return bool
351 - */
352 - function is_cached( $path, $method = 'GET', $params = array() ) {
353 - $cache_key = $this->get_cache_key( $path, $method, $params );
354 -
355 - return self::$_cache->has_valid( $cache_key );
356 - }
357 -
358 - /**
359 - * Invalidate a cached version of the API request.
360 - *
361 - * @author Vova Feldman (@svovaf)
362 - * @since 1.2.1.5
363 - *
364 - * @param string $path
365 - * @param string $method
366 - * @param array $params
367 - */
368 - function purge_cache( $path, $method = 'GET', $params = array() ) {
369 - $this->_logger->entrance( "{$method}:{$path}" );
370 -
371 - $cache_key = $this->get_cache_key( $path, $method, $params );
372 -
373 - self::$_cache->purge( $cache_key );
374 - }
375 -
376 - /**
377 - * Invalidate a cached version of the API request.
378 - *
379 - * @author Vova Feldman (@svovaf)
380 - * @since 2.0.0
381 - *
382 - * @param string $path
383 - * @param int $expiration
384 - * @param string $method
385 - * @param array $params
386 - */
387 - function update_cache_expiration( $path, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $method = 'GET', $params = array() ) {
388 - $this->_logger->entrance( "{$method}:{$path}:{$expiration}" );
389 -
390 - $cache_key = $this->get_cache_key( $path, $method, $params );
391 -
392 - self::$_cache->update_expiration( $cache_key, $expiration );
393 - }
394 -
395 - /**
396 - * @param string $path
397 - * @param string $method
398 - * @param array $params
399 - *
400 - * @return string
401 - * @throws \Freemius_Exception
402 - */
403 - private function get_cache_key( $path, $method = 'GET', $params = array() ) {
404 - $canonized = $this->_api->CanonizePath( $path );
405 -// $exploded = explode('/', $canonized);
406 -// return $method . '_' . array_pop($exploded) . '_' . md5($canonized . json_encode($params));
407 - return strtolower( $method . ':' . $canonized ) . ( ! empty( $params ) ? '#' . md5( json_encode( $params ) ) : '' );
408 - }
409 -
410 - /**
411 - * Test API connectivity.
412 - *
413 - * @author Vova Feldman (@svovaf)
414 - * @since 1.0.9 If fails, try to fallback to HTTP.
415 - * @since 1.1.6 Added a 5-min caching mechanism, to prevent from overloading the server if the API if
416 - * temporary down.
417 - *
418 - * @return bool True if successful connectivity to the API.
419 - */
420 - static function test() {
421 - self::_init();
422 -
423 - $cache_key = 'ping_test';
424 -
425 - $test = self::$_cache->get_valid( $cache_key, null );
426 -
427 - if ( is_null( $test ) ) {
428 - $test = Freemius_Api_WordPress::Test();
429 -
430 - if ( false === $test && Freemius_Api_WordPress::IsHttps() ) {
431 - // Fallback to HTTP, since HTTPS fails.
432 - Freemius_Api_WordPress::SetHttp();
433 -
434 - self::$_options->set_option( 'api_force_http', true, true );
435 -
436 - $test = Freemius_Api_WordPress::Test();
437 -
438 - if ( false === $test ) {
439 - /**
440 - * API connectivity test fail also in HTTP request, therefore,
441 - * fallback to HTTPS to keep connection secure.
442 - *
443 - * @since 1.1.6
444 - */
445 - self::$_options->set_option( 'api_force_http', false, true );
446 - }
447 - }
448 -
449 - self::$_cache->set( $cache_key, $test, WP_FS__TIME_5_MIN_IN_SEC );
450 - }
451 -
452 - return $test;
453 - }
454 -
455 - /**
456 - * Check if API is temporary down.
457 - *
458 - * @author Vova Feldman (@svovaf)
459 - * @since 1.1.6
460 - *
461 - * @return bool
462 - */
463 - static function is_temporary_down() {
464 - self::_init();
465 -
466 - $test = self::$_cache->get_valid( 'ping_test', null );
467 -
468 - return ( false === $test );
469 - }
470 -
471 - /**
472 - * @author Vova Feldman (@svovaf)
473 - * @since 1.1.6
474 - *
475 - * @return object
476 - */
477 - private function get_temporary_unavailable_error() {
478 - return (object) array(
479 - 'error' => (object) array(
480 - 'type' => 'TemporaryUnavailable',
481 - 'message' => 'API is temporary unavailable, please retry in ' . ( self::$_cache->get_record_expiration( 'ping_test' ) - WP_FS__SCRIPT_START_TIME ) . ' sec.',
482 - 'code' => 'temporary_unavailable',
483 - 'http' => 503
484 - )
485 - );
486 - }
487 -
488 - /**
489 - * Ping API for connectivity test, and return result object.
490 - *
491 - * @author Vova Feldman (@svovaf)
492 - * @since 1.0.9
493 - *
494 - * @param null|string $unique_anonymous_id
495 - * @param array $params
496 - *
497 - * @return object
498 - */
499 - function ping( $unique_anonymous_id = null, $params = array() ) {
500 - $this->_logger->entrance();
501 -
502 - if ( self::is_temporary_down() ) {
503 - return $this->get_temporary_unavailable_error();
504 - }
505 -
506 - $pong = is_null( $unique_anonymous_id ) ?
507 - Freemius_Api_WordPress::Ping() :
508 - $this->_call( 'ping.json?' . http_build_query( array_merge(
509 - array( 'uid' => $unique_anonymous_id ),
510 - $params
511 - ) ) );
512 -
513 - if ( $this->is_valid_ping( $pong ) ) {
514 - return $pong;
515 - }
516 -
517 - if ( self::should_try_with_http( $pong ) ) {
518 - // Fallback to HTTP, since HTTPS fails.
519 - Freemius_Api_WordPress::SetHttp();
520 -
521 - self::$_options->set_option( 'api_force_http', true, true );
522 -
523 - $pong = is_null( $unique_anonymous_id ) ?
524 - Freemius_Api_WordPress::Ping() :
525 - $this->_call( 'ping.json?' . http_build_query( array_merge(
526 - array( 'uid' => $unique_anonymous_id ),
527 - $params
528 - ) ) );
529 -
530 - if ( ! $this->is_valid_ping( $pong ) ) {
531 - self::$_options->set_option( 'api_force_http', false, true );
532 - }
533 - }
534 -
535 - return $pong;
536 - }
537 -
538 - /**
539 - * Check if based on the API result we should try
540 - * to re-run the same request with HTTP instead of HTTPS.
541 - *
542 - * @author Vova Feldman (@svovaf)
543 - * @since 1.1.6
544 - *
545 - * @param $result
546 - *
547 - * @return bool
548 - */
549 - private static function should_try_with_http( $result ) {
550 - if ( ! Freemius_Api_WordPress::IsHttps() ) {
551 - return false;
552 - }
553 -
554 - return ( ! is_object( $result ) ||
555 - ! isset( $result->error ) ||
556 - ! isset( $result->error->code ) ||
557 - ! in_array( $result->error->code, array(
558 - 'curl_missing',
559 - 'cloudflare_ddos_protection',
560 - 'maintenance_mode',
561 - 'squid_cache_block',
562 - 'too_many_requests',
563 - ) ) );
564 -
565 - }
566 -
567 - /**
568 - * Check if valid ping request result.
569 - *
570 - * @author Vova Feldman (@svovaf)
571 - * @since 1.1.1
572 - *
573 - * @param mixed $pong
574 - *
575 - * @return bool
576 - */
577 - function is_valid_ping( $pong ) {
578 - return Freemius_Api_WordPress::Test( $pong );
579 - }
580 -
581 - function get_url( $path = '' ) {
582 - return Freemius_Api_WordPress::GetUrl( $path, $this->_api->IsSandbox() );
583 - }
584 -
585 - /**
586 - * Clear API cache.
587 - *
588 - * @author Vova Feldman (@svovaf)
589 - * @since 1.0.9
590 - */
591 - static function clear_cache() {
592 - self::_init();
593 -
594 - self::$_cache = FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME );
595 - self::$_cache->clear();
596 - }
597 -
598 - #----------------------------------------------------------------------------------
599 - #region Error Handling
600 - #----------------------------------------------------------------------------------
601 -
602 - /**
603 - * @author Vova Feldman (@svovaf)
604 - * @since 1.2.1.5
605 - *
606 - * @param mixed $result
607 - *
608 - * @return bool Is API result contains an error.
609 - */
610 - static function is_api_error( $result ) {
611 - return ( is_object( $result ) && isset( $result->error ) ) ||
612 - is_string( $result );
613 - }
614 -
615 - /**
616 - * @author Vova Feldman (@svovaf)
617 - * @since 2.0.0
618 - *
619 - * @param mixed $result
620 - *
621 - * @return bool Is API result contains an error.
622 - */
623 - static function is_api_error_object( $result ) {
624 - return (
625 - is_object( $result ) &&
626 - isset( $result->error ) &&
627 - isset( $result->error->message )
628 - );
629 - }
630 -
631 - /**
632 - * Checks if given API result is a non-empty and not an error object.
633 - *
634 - * @author Vova Feldman (@svovaf)
635 - * @since 1.2.1.5
636 - *
637 - * @param mixed $result
638 - * @param string|null $required_property Optional property we want to verify that is set.
639 - *
640 - * @return bool
641 - */
642 - static function is_api_result_object( $result, $required_property = null ) {
643 - return (
644 - is_object( $result ) &&
645 - ! isset( $result->error ) &&
646 - ( empty( $required_property ) || isset( $result->{$required_property} ) )
647 - );
648 - }
649 -
650 - /**
651 - * Checks if given API result is a non-empty entity object with non-empty ID.
652 - *
653 - * @author Vova Feldman (@svovaf)
654 - * @since 1.2.1.5
655 - *
656 - * @param mixed $result
657 - *
658 - * @return bool
659 - */
660 - static function is_api_result_entity( $result ) {
661 - return self::is_api_result_object( $result, 'id' ) &&
662 - FS_Entity::is_valid_id( $result->id );
663 - }
664 -
665 - /**
666 - * Get API result error code. If failed to get code, returns an empty string.
667 - *
668 - * @author Vova Feldman (@svovaf)
669 - * @since 2.0.0
670 - *
671 - * @param mixed $result
672 - *
673 - * @return string
674 - */
675 - static function get_error_code( $result ) {
676 - if ( is_object( $result ) &&
677 - isset( $result->error ) &&
678 - is_object( $result->error ) &&
679 - ! empty( $result->error->code )
680 - ) {
681 - return $result->error->code;
682 - }
683 -
684 - return '';
685 - }
686 -
687 - #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
688 724 }