PluginProbe ʕ •ᴥ•ʔ
Code Manager / 1.0.20
Code Manager v1.0.20
1.0.48 1.0.47 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.2 1.0.20 1.0.21 1.0.22 1.0.23 1.0.24 1.0.25 1.0.26 1.0.27 1.0.28 1.0.3 1.0.30 1.0.31 1.0.32 1.0.33 1.0.34 1.0.35 1.0.36 1.0.37 1.0.38 1.0.39 1.0.4 1.0.40 1.0.41 1.0.42 1.0.43 1.0.44 1.0.45 1.0.46 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9
code-manager / freemius / includes / class-fs-api.php
code-manager / freemius / includes Last commit date
customizer 3 years ago debug 3 years ago entities 3 years ago managers 3 years ago sdk 3 years ago supplements 3 years ago class-freemius-abstract.php 3 years ago class-freemius.php 3 years ago class-fs-admin-notices.php 3 years ago class-fs-api.php 3 years ago class-fs-lock.php 3 years ago class-fs-logger.php 3 years ago class-fs-options.php 3 years ago class-fs-plugin-updater.php 3 years ago class-fs-security.php 3 years ago class-fs-storage.php 3 years ago class-fs-user-lock.php 3 years ago fs-core-functions.php 3 years ago fs-essential-functions.php 3 years ago fs-plugin-info-dialog.php 3 years ago index.php 3 years ago l10n.php 3 years ago
class-fs-api.php
688 lines
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
688 }