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