PluginProbe
Buttonizer – Floating Menus, Sticky Buttons, & Popup Builder / 1.5
Buttonizer – Floating Menus, Sticky Buttons, & Popup Builder v1.5
3.6.0 3.5.0 trunk 1.0.10 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.6.1 1.0.7 1.0.8 1.0.9 1.1 1.1.1 1.2 1.3 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.5 1.5.1 All 110 releases
buttonizer-multifunctional-button / freemius / includes / class-fs-api.php

class-fs-api.php in Buttonizer – Floating Menus, Sticky Buttons, & Popup Builder 1.5, at freemius/includes/class-fs-api.php

615 lines 16.8 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 )
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 no older data version, return result without
252 // caching the error.
253 return $result;
254 }
255 }
256
257 self::$_cache->set( $cache_key, $result, $expiration );
258
259 $cached_result = $result;
260 } else {
261 $this->_logger->log( 'Using cached API result.' );
262 }
263
264 return $cached_result;
265 }
266
267 /**
268 * Check if there's a cached version of the API request.
269 *
270 * @author Vova Feldman (@svovaf)
271 * @since 1.2.1
272 *
273 * @param string $path
274 * @param string $method
275 * @param array $params
276 *
277 * @return bool
278 */
279 function is_cached( $path, $method = 'GET', $params = array() ) {
280 $cache_key = $this->get_cache_key( $path, $method, $params );
281
282 return self::$_cache->has_valid( $cache_key );
283 }
284
285 /**
286 * Invalidate a cached version of the API request.
287 *
288 * @author Vova Feldman (@svovaf)
289 * @since 1.2.1.5
290 *
291 * @param string $path
292 * @param string $method
293 * @param array $params
294 */
295 function purge_cache( $path, $method = 'GET', $params = array() ) {
296 $this->_logger->entrance( "{$method}:{$path}" );
297
298 $cache_key = $this->get_cache_key( $path, $method, $params );
299
300 self::$_cache->purge( $cache_key );
301 }
302
303 /**
304 * Invalidate a cached version of the API request.
305 *
306 * @author Vova Feldman (@svovaf)
307 * @since 2.0.0
308 *
309 * @param string $path
310 * @param int $expiration
311 * @param string $method
312 * @param array $params
313 */
314 function update_cache_expiration( $path, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $method = 'GET', $params = array() ) {
315 $this->_logger->entrance( "{$method}:{$path}:{$expiration}" );
316
317 $cache_key = $this->get_cache_key( $path, $method, $params );
318
319 self::$_cache->update_expiration( $cache_key, $expiration );
320 }
321
322 /**
323 * @param string $path
324 * @param string $method
325 * @param array $params
326 *
327 * @return string
328 * @throws \Freemius_Exception
329 */
330 private function get_cache_key( $path, $method = 'GET', $params = array() ) {
331 $canonized = $this->_api->CanonizePath( $path );
332 // $exploded = explode('/', $canonized);
333 // return $method . '_' . array_pop($exploded) . '_' . md5($canonized . json_encode($params));
334 return strtolower( $method . ':' . $canonized ) . ( ! empty( $params ) ? '#' . md5( json_encode( $params ) ) : '' );
335 }
336
337 /**
338 * Test API connectivity.
339 *
340 * @author Vova Feldman (@svovaf)
341 * @since 1.0.9 If fails, try to fallback to HTTP.
342 * @since 1.1.6 Added a 5-min caching mechanism, to prevent from overloading the server if the API if
343 * temporary down.
344 *
345 * @return bool True if successful connectivity to the API.
346 */
347 static function test() {
348 self::_init();
349
350 $cache_key = 'ping_test';
351
352 $test = self::$_cache->get_valid( $cache_key, null );
353
354 if ( is_null( $test ) ) {
355 $test = Freemius_Api_WordPress::Test();
356
357 if ( false === $test && Freemius_Api_WordPress::IsHttps() ) {
358 // Fallback to HTTP, since HTTPS fails.
359 Freemius_Api_WordPress::SetHttp();
360
361 self::$_options->set_option( 'api_force_http', true, true );
362
363 $test = Freemius_Api_WordPress::Test();
364
365 if ( false === $test ) {
366 /**
367 * API connectivity test fail also in HTTP request, therefore,
368 * fallback to HTTPS to keep connection secure.
369 *
370 * @since 1.1.6
371 */
372 self::$_options->set_option( 'api_force_http', false, true );
373 }
374 }
375
376 self::$_cache->set( $cache_key, $test, WP_FS__TIME_5_MIN_IN_SEC );
377 }
378
379 return $test;
380 }
381
382 /**
383 * Check if API is temporary down.
384 *
385 * @author Vova Feldman (@svovaf)
386 * @since 1.1.6
387 *
388 * @return bool
389 */
390 static function is_temporary_down() {
391 self::_init();
392
393 $test = self::$_cache->get_valid( 'ping_test', null );
394
395 return ( false === $test );
396 }
397
398 /**
399 * @author Vova Feldman (@svovaf)
400 * @since 1.1.6
401 *
402 * @return object
403 */
404 private function get_temporary_unavailable_error() {
405 return (object) array(
406 'error' => (object) array(
407 'type' => 'TemporaryUnavailable',
408 'message' => 'API is temporary unavailable, please retry in ' . ( self::$_cache->get_record_expiration( 'ping_test' ) - WP_FS__SCRIPT_START_TIME ) . ' sec.',
409 'code' => 'temporary_unavailable',
410 'http' => 503
411 )
412 );
413 }
414
415 /**
416 * Ping API for connectivity test, and return result object.
417 *
418 * @author Vova Feldman (@svovaf)
419 * @since 1.0.9
420 *
421 * @param null|string $unique_anonymous_id
422 * @param array $params
423 *
424 * @return object
425 */
426 function ping( $unique_anonymous_id = null, $params = array() ) {
427 $this->_logger->entrance();
428
429 if ( self::is_temporary_down() ) {
430 return $this->get_temporary_unavailable_error();
431 }
432
433 $pong = is_null( $unique_anonymous_id ) ?
434 Freemius_Api_WordPress::Ping() :
435 $this->_call( 'ping.json?' . http_build_query( array_merge(
436 array( 'uid' => $unique_anonymous_id ),
437 $params
438 ) ) );
439
440 if ( $this->is_valid_ping( $pong ) ) {
441 return $pong;
442 }
443
444 if ( self::should_try_with_http( $pong ) ) {
445 // Fallback to HTTP, since HTTPS fails.
446 Freemius_Api_WordPress::SetHttp();
447
448 self::$_options->set_option( 'api_force_http', true, true );
449
450 $pong = is_null( $unique_anonymous_id ) ?
451 Freemius_Api_WordPress::Ping() :
452 $this->_call( 'ping.json?' . http_build_query( array_merge(
453 array( 'uid' => $unique_anonymous_id ),
454 $params
455 ) ) );
456
457 if ( ! $this->is_valid_ping( $pong ) ) {
458 self::$_options->set_option( 'api_force_http', false, true );
459 }
460 }
461
462 return $pong;
463 }
464
465 /**
466 * Check if based on the API result we should try
467 * to re-run the same request with HTTP instead of HTTPS.
468 *
469 * @author Vova Feldman (@svovaf)
470 * @since 1.1.6
471 *
472 * @param $result
473 *
474 * @return bool
475 */
476 private static function should_try_with_http( $result ) {
477 if ( ! Freemius_Api_WordPress::IsHttps() ) {
478 return false;
479 }
480
481 return ( ! is_object( $result ) ||
482 ! isset( $result->error ) ||
483 ! isset( $result->error->code ) ||
484 ! in_array( $result->error->code, array(
485 'curl_missing',
486 'cloudflare_ddos_protection',
487 'maintenance_mode',
488 'squid_cache_block',
489 'too_many_requests',
490 ) ) );
491
492 }
493
494 /**
495 * Check if valid ping request result.
496 *
497 * @author Vova Feldman (@svovaf)
498 * @since 1.1.1
499 *
500 * @param mixed $pong
501 *
502 * @return bool
503 */
504 function is_valid_ping( $pong ) {
505 return Freemius_Api_WordPress::Test( $pong );
506 }
507
508 function get_url( $path = '' ) {
509 return Freemius_Api_WordPress::GetUrl( $path, $this->_api->IsSandbox() );
510 }
511
512 /**
513 * Clear API cache.
514 *
515 * @author Vova Feldman (@svovaf)
516 * @since 1.0.9
517 */
518 static function clear_cache() {
519 self::_init();
520
521 self::$_cache = FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME );
522 self::$_cache->clear();
523 }
524
525 #----------------------------------------------------------------------------------
526 #region Error Handling
527 #----------------------------------------------------------------------------------
528
529 /**
530 * @author Vova Feldman (@svovaf)
531 * @since 1.2.1.5
532 *
533 * @param mixed $result
534 *
535 * @return bool Is API result contains an error.
536 */
537 static function is_api_error( $result ) {
538 return ( is_object( $result ) && isset( $result->error ) ) ||
539 is_string( $result );
540 }
541
542 /**
543 * @author Vova Feldman (@svovaf)
544 * @since 2.0.0
545 *
546 * @param mixed $result
547 *
548 * @return bool Is API result contains an error.
549 */
550 static function is_api_error_object( $result ) {
551 return (
552 is_object( $result ) &&
553 isset( $result->error ) &&
554 isset( $result->message )
555 );
556 }
557
558 /**
559 * Checks if given API result is a non-empty and not an error object.
560 *
561 * @author Vova Feldman (@svovaf)
562 * @since 1.2.1.5
563 *
564 * @param mixed $result
565 * @param string|null $required_property Optional property we want to verify that is set.
566 *
567 * @return bool
568 */
569 static function is_api_result_object( $result, $required_property = null ) {
570 return (
571 is_object( $result ) &&
572 ! isset( $result->error ) &&
573 ( empty( $required_property ) || isset( $result->{$required_property} ) )
574 );
575 }
576
577 /**
578 * Checks if given API result is a non-empty entity object with non-empty ID.
579 *
580 * @author Vova Feldman (@svovaf)
581 * @since 1.2.1.5
582 *
583 * @param mixed $result
584 *
585 * @return bool
586 */
587 static function is_api_result_entity( $result ) {
588 return self::is_api_result_object( $result, 'id' ) &&
589 FS_Entity::is_valid_id( $result->id );
590 }
591
592 /**
593 * Get API result error code. If failed to get code, returns an empty string.
594 *
595 * @author Vova Feldman (@svovaf)
596 * @since 2.0.0
597 *
598 * @param mixed $result
599 *
600 * @return string
601 */
602 static function get_error_code( $result ) {
603 if ( is_object( $result ) &&
604 isset( $result->error ) &&
605 is_object( $result->error ) &&
606 ! empty( $result->error->code )
607 ) {
608 return $result->error->code;
609 }
610
611 return '';
612 }
613
614 #endregion
615 }