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