PluginProbe
BlockSpare – Gutenberg Blocks, AI Content Generator & Site Builder for News, Magazine & Blogs / 5.0.1
BlockSpare – Gutenberg Blocks, AI Content Generator & Site Builder for News, Magazine & Blogs v5.0.1
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 +723 -687 2.5.0 → 5.0.1 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 }