PluginProbe
StreamCast – bring live radio to your site with a sleek player / 2.2.3
StreamCast – bring live radio to your site with a sleek player v2.2.3
2.4.5 2.4.4 trunk 1.0 1.1 2.0.0 2.1.10 2.1.2 2.1.4 2.1.5 2.1.8 2.2.1 2.2.3 2.2.4 2.2.5 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.8 2.3.9 All 29 releases
streamcast / freemius / includes / class-fs-api.php

class-fs-api.php in StreamCast – bring live radio to your site with a sleek player 2.2.3, at freemius/includes/class-fs-api.php

722 lines 22.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 self::$_cache->set( $cache_key, $result, $expiration );
359
360 $cached_result = $result;
361 } else {
362 $this->_logger->log( 'Using cached API result.' );
363 }
364
365 return $cached_result;
366 }
367
368 /**
369 * @todo Remove this method after migrating Freemius::safe_remote_post() to FS_Api::call().
370 *
371 * @author Leo Fajardo (@leorw)
372 * @since 2.5.4
373 *
374 * @param string $url
375 * @param array $remote_args
376 *
377 * @return array|WP_Error The response array or a WP_Error on failure.
378 */
379 static function remote_request( $url, $remote_args ) {
380 if ( ! class_exists( 'Freemius_Api_WordPress' ) ) {
381 require_once WP_FS__DIR_SDK . '/FreemiusWordPress.php';
382 }
383
384 if ( method_exists( 'Freemius_Api_WordPress', 'RemoteRequest' ) ) {
385 return Freemius_Api_WordPress::RemoteRequest( $url, $remote_args );
386 }
387
388 // 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.
389 $response = wp_remote_request( $url, $remote_args );
390
391 if (
392 is_array( $response ) &&
393 (
394 empty( $response['headers'] ) ||
395 empty( $response['headers']['x-api-server'] )
396 )
397 ) {
398 // 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).
399 $response = new WP_Error( 'api_blocked', htmlentities( $response['body'] ) );
400 }
401
402 return $response;
403 }
404
405 /**
406 * Check if there's a cached version of the API request.
407 *
408 * @author Vova Feldman (@svovaf)
409 * @since 1.2.1
410 *
411 * @param string $path
412 * @param string $method
413 * @param array $params
414 *
415 * @return bool
416 */
417 function is_cached( $path, $method = 'GET', $params = array() ) {
418 $cache_key = $this->get_cache_key( $path, $method, $params );
419
420 return self::$_cache->has_valid( $cache_key );
421 }
422
423 /**
424 * Invalidate a cached version of the API request.
425 *
426 * @author Vova Feldman (@svovaf)
427 * @since 1.2.1.5
428 *
429 * @param string $path
430 * @param string $method
431 * @param array $params
432 */
433 function purge_cache( $path, $method = 'GET', $params = array() ) {
434 $this->_logger->entrance( "{$method}:{$path}" );
435
436 $cache_key = $this->get_cache_key( $path, $method, $params );
437
438 self::$_cache->purge( $cache_key );
439 }
440
441 /**
442 * Invalidate a cached version of the API request.
443 *
444 * @author Vova Feldman (@svovaf)
445 * @since 2.0.0
446 *
447 * @param string $path
448 * @param int $expiration
449 * @param string $method
450 * @param array $params
451 */
452 function update_cache_expiration( $path, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $method = 'GET', $params = array() ) {
453 $this->_logger->entrance( "{$method}:{$path}:{$expiration}" );
454
455 $cache_key = $this->get_cache_key( $path, $method, $params );
456
457 self::$_cache->update_expiration( $cache_key, $expiration );
458 }
459
460 /**
461 * @param string $path
462 * @param string $method
463 * @param array $params
464 *
465 * @return string
466 * @throws \Freemius_Exception
467 */
468 private function get_cache_key( $path, $method = 'GET', $params = array() ) {
469 $canonized = $this->_api->CanonizePath( $path );
470 // $exploded = explode('/', $canonized);
471 // return $method . '_' . array_pop($exploded) . '_' . md5($canonized . json_encode($params));
472 return strtolower( $method . ':' . $canonized ) . ( ! empty( $params ) ? '#' . md5( json_encode( $params ) ) : '' );
473 }
474
475 /**
476 * @author Leo Fajardo (@leorw)
477 * @since 2.5.4
478 *
479 * @param bool $is_http
480 */
481 private function toggle_force_http( $is_http ) {
482 self::$_options->set_option( 'api_force_http', $is_http, true );
483
484 if ( $is_http ) {
485 Freemius_Api_WordPress::SetHttp();
486 } else if ( method_exists( 'Freemius_Api_WordPress', 'SetHttps' ) ) {
487 Freemius_Api_WordPress::SetHttps();
488 }
489 }
490
491 /**
492 * @author Leo Fajardo (@leorw)
493 * @since 2.5.4
494 *
495 * @param mixed $response
496 *
497 * @return bool
498 */
499 static function is_blocked( $response ) {
500 return (
501 self::is_api_error_object( $response, true ) &&
502 isset( $response->error->code ) &&
503 'api_blocked' === $response->error->code
504 );
505 }
506
507 /**
508 * Check if API is temporary down.
509 *
510 * @author Vova Feldman (@svovaf)
511 * @since 1.1.6
512 *
513 * @return bool
514 */
515 static function is_temporary_down() {
516 self::_init();
517
518 $test = self::$_cache->get_valid( 'ping_test', null );
519
520 return ( false === $test );
521 }
522
523 /**
524 * @author Vova Feldman (@svovaf)
525 * @since 1.1.6
526 *
527 * @return object
528 */
529 private function get_temporary_unavailable_error() {
530 return (object) array(
531 'error' => (object) array(
532 'type' => 'TemporaryUnavailable',
533 'message' => 'API is temporary unavailable, please retry in ' . ( self::$_cache->get_record_expiration( 'ping_test' ) - WP_FS__SCRIPT_START_TIME ) . ' sec.',
534 'code' => 'temporary_unavailable',
535 'http' => 503
536 )
537 );
538 }
539
540 /**
541 * Check if based on the API result we should try
542 * to re-run the same request with HTTP instead of HTTPS.
543 *
544 * @author Vova Feldman (@svovaf)
545 * @since 1.1.6
546 *
547 * @param $result
548 *
549 * @return bool
550 */
551 private static function should_try_with_http( $result ) {
552 if ( ! Freemius_Api_WordPress::IsHttps() ) {
553 return false;
554 }
555
556 return ( ! is_object( $result ) ||
557 ! isset( $result->error ) ||
558 ! isset( $result->error->code ) ||
559 ! in_array( $result->error->code, array(
560 'curl_missing',
561 'cloudflare_ddos_protection',
562 'maintenance_mode',
563 'squid_cache_block',
564 'too_many_requests',
565 ) ) );
566
567 }
568
569 function get_url( $path = '' ) {
570 return Freemius_Api_WordPress::GetUrl( $path, $this->_api->IsSandbox() );
571 }
572
573 /**
574 * Clear API cache.
575 *
576 * @author Vova Feldman (@svovaf)
577 * @since 1.0.9
578 */
579 static function clear_cache() {
580 self::_init();
581
582 self::$_cache = FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME );
583 self::$_cache->clear();
584 }
585
586 /**
587 * @author Leo Fajardo (@leorw)
588 * @since 2.5.4
589 */
590 static function clear_force_http_flag() {
591 self::$_options->unset_option( 'api_force_http' );
592 }
593
594 #----------------------------------------------------------------------------------
595 #region Error Handling
596 #----------------------------------------------------------------------------------
597
598 /**
599 * @author Vova Feldman (@svovaf)
600 * @since 1.2.1.5
601 *
602 * @param mixed $result
603 *
604 * @return bool Is API result contains an error.
605 */
606 static function is_api_error( $result ) {
607 return ( is_object( $result ) && isset( $result->error ) ) ||
608 is_string( $result );
609 }
610
611 /**
612 * @author Vova Feldman (@svovaf)
613 * @since 2.0.0
614 *
615 * @param mixed $result
616 * @param bool $ignore_message
617 *
618 * @return bool Is API result contains an error.
619 */
620 static function is_api_error_object( $result, $ignore_message = false ) {
621 return (
622 is_object( $result ) &&
623 isset( $result->error ) &&
624 ( $ignore_message || isset( $result->error->message ) )
625 );
626 }
627
628 /**
629 * @author Leo Fajardo (@leorw)
630 * @since 2.5.4
631 *
632 * @param WP_Error|object|string $response
633 *
634 * @return bool
635 */
636 static function is_ssl_error_response( $response ) {
637 $http_error = null;
638
639 if ( $response instanceof WP_Error ) {
640 if (
641 isset( $response->errors ) &&
642 isset( $response->errors['http_request_failed'] )
643 ) {
644 $http_error = strtolower( $response->errors['http_request_failed'][0] );
645 }
646 } else if (
647 self::is_api_error_object( $response ) &&
648 ! empty( $response->error->message )
649 ) {
650 $http_error = $response->error->message;
651 }
652
653 return (
654 ! empty( $http_error ) &&
655 (
656 false !== strpos( $http_error, 'curl error 35' ) ||
657 (
658 false === strpos( $http_error, '</html>' ) &&
659 false !== strpos( $http_error, 'ssl' )
660 )
661 )
662 );
663 }
664
665 /**
666 * Checks if given API result is a non-empty and not an error object.
667 *
668 * @author Vova Feldman (@svovaf)
669 * @since 1.2.1.5
670 *
671 * @param mixed $result
672 * @param string|null $required_property Optional property we want to verify that is set.
673 *
674 * @return bool
675 */
676 static function is_api_result_object( $result, $required_property = null ) {
677 return (
678 is_object( $result ) &&
679 ! isset( $result->error ) &&
680 ( empty( $required_property ) || isset( $result->{$required_property} ) )
681 );
682 }
683
684 /**
685 * Checks if given API result is a non-empty entity object with non-empty ID.
686 *
687 * @author Vova Feldman (@svovaf)
688 * @since 1.2.1.5
689 *
690 * @param mixed $result
691 *
692 * @return bool
693 */
694 static function is_api_result_entity( $result ) {
695 return self::is_api_result_object( $result, 'id' ) &&
696 FS_Entity::is_valid_id( $result->id );
697 }
698
699 /**
700 * Get API result error code. If failed to get code, returns an empty string.
701 *
702 * @author Vova Feldman (@svovaf)
703 * @since 2.0.0
704 *
705 * @param mixed $result
706 *
707 * @return string
708 */
709 static function get_error_code( $result ) {
710 if ( is_object( $result ) &&
711 isset( $result->error ) &&
712 is_object( $result->error ) &&
713 ! empty( $result->error->code )
714 ) {
715 return $result->error->code;
716 }
717
718 return '';
719 }
720
721 #endregion
722 }