PluginProbe
Tiered Pricing Table for WooCommerce / 2.2.2
Tiered Pricing Table for WooCommerce v2.2.2
7.1.7 7.1.5 7.1.4 7.1.1 6.5.0 6.4.0 6.1.0 trunk 1.0 1.1 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.2.0 2.2.1 2.2.2 2.2.3 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 All 90 releases
tier-pricing-table / freemius / includes / class-fs-api.php

class-fs-api.php in Tiered Pricing Table for WooCommerce 2.2.2, at freemius/includes/class-fs-api.php

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