/freemius/includes/class-fs-api.php

  1. <?php 
  2. /** 
  3. * @package Freemius 
  4. * @copyright Copyright (c) 2015, Freemius, Inc. 
  5. * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License 
  6. * @since 1.0.4 
  7. */ 
  8.  
  9. if ( ! defined( 'ABSPATH' ) ) { 
  10. exit; 
  11.  
  12. /** 
  13. * Class FS_Api 
  14. * 
  15. * Wraps Freemius API SDK to handle: 
  16. * 1. Clock sync. 
  17. * 2. Fallback to HTTP when HTTPS fails. 
  18. * 3. Adds caching layer to GET requests. 
  19. * 4. Adds consistency for failed requests by using last cached version. 
  20. */ 
  21. class FS_Api { 
  22. /** 
  23. * @var FS_Api[] 
  24. */ 
  25. private static $_instances = array(); 
  26.  
  27. /** 
  28. * @var FS_Option_Manager Freemius options, options-manager. 
  29. */ 
  30. private static $_options; 
  31.  
  32. /** 
  33. * @var FS_Cache_Manager API Caching layer 
  34. */ 
  35. private static $_cache; 
  36.  
  37. /** 
  38. * @var int Clock diff in seconds between current server to API server. 
  39. */ 
  40. private static $_clock_diff; 
  41.  
  42. /** 
  43. * @var Freemius_Api 
  44. */ 
  45. private $_api; 
  46.  
  47. /** 
  48. * @var string 
  49. */ 
  50. private $_slug; 
  51.  
  52. /** 
  53. * @var FS_Logger 
  54. * @since 1.0.4 
  55. */ 
  56. private $_logger; 
  57.  
  58. /** 
  59. * @param string $slug 
  60. * @param string $scope 'app', 'developer', 'user' or 'install'. 
  61. * @param number $id Element's id. 
  62. * @param string $public_key Public key. 
  63. * @param bool $is_sandbox 
  64. * @param bool|string $secret_key Element's secret key. 
  65. * 
  66. * @return FS_Api 
  67. */ 
  68. static function instance( $slug, $scope, $id, $public_key, $is_sandbox, $secret_key = false ) { 
  69. $identifier = md5( $slug . $scope . $id . $public_key . ( is_string( $secret_key ) ? $secret_key : '' ) . json_encode( $is_sandbox ) ); 
  70.  
  71. if ( ! isset( self::$_instances[ $identifier ] ) ) { 
  72. self::_init(); 
  73.  
  74. self::$_instances[ $identifier ] = new FS_Api( $slug, $scope, $id, $public_key, $secret_key, $is_sandbox ); 
  75.  
  76. return self::$_instances[ $identifier ]; 
  77.  
  78. private static function _init() { 
  79. if ( isset( self::$_options ) ) { 
  80. return; 
  81.  
  82. if ( ! class_exists( 'Freemius_Api' ) ) { 
  83. require_once( WP_FS__DIR_SDK . '/Freemius.php' ); 
  84.  
  85. self::$_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true ); 
  86. self::$_cache = FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME ); 
  87.  
  88. self::$_clock_diff = self::$_options->get_option( 'api_clock_diff', 0 ); 
  89. Freemius_Api::SetClockDiff( self::$_clock_diff ); 
  90.  
  91. if ( self::$_options->get_option( 'api_force_http', false ) ) { 
  92. Freemius_Api::SetHttp(); 
  93.  
  94. /** 
  95. * @param string $slug 
  96. * @param string $scope 'app', 'developer', 'user' or 'install'. 
  97. * @param number $id Element's id. 
  98. * @param string $public_key Public key. 
  99. * @param bool|string $secret_key Element's secret key. 
  100. * @param bool $is_sandbox 
  101. */ 
  102. private function __construct( $slug, $scope, $id, $public_key, $secret_key, $is_sandbox ) { 
  103. $this->_api = new Freemius_Api( $scope, $id, $public_key, $secret_key, $is_sandbox ); 
  104.  
  105. $this->_slug = $slug; 
  106. $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $slug . '_api', WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK ); 
  107.  
  108. /** 
  109. * Find clock diff between server and API server, and store the diff locally. 
  110. * 
  111. * @param bool|int $diff 
  112. * 
  113. * @return bool|int False if clock diff didn't change, otherwise returns the clock diff in seconds. 
  114. */ 
  115. private function _sync_clock_diff( $diff = false ) { 
  116. $this->_logger->entrance(); 
  117.  
  118. // Sync clock and store. 
  119. $new_clock_diff = ( false === $diff ) ? 
  120. Freemius_Api::FindClockDiff() : 
  121. $diff; 
  122.  
  123. if ( $new_clock_diff === self::$_clock_diff ) { 
  124. return false; 
  125.  
  126. self::$_clock_diff = $new_clock_diff; 
  127.  
  128. // Update API clock's diff. 
  129. Freemius_Api::SetClockDiff( self::$_clock_diff ); 
  130.  
  131. // Store new clock diff in storage. 
  132. self::$_options->set_option( 'api_clock_diff', self::$_clock_diff, true ); 
  133.  
  134. return $new_clock_diff; 
  135.  
  136. /** 
  137. * Override API call to enable retry with servers' clock auto sync method. 
  138. * 
  139. * @param string $path 
  140. * @param string $method 
  141. * @param array $params 
  142. * @param bool $retry Is in retry or first call attempt. 
  143. * 
  144. * @return array|mixed|string|void 
  145. */ 
  146. private function _call( $path, $method = 'GET', $params = array(), $retry = false ) { 
  147. $this->_logger->entrance( $method . ':' . $path ); 
  148.  
  149. if ( self::is_temporary_down() ) { 
  150. $result = $this->get_temporary_unavailable_error(); 
  151. } else { 
  152. $result = $this->_api->Api( $path, $method, $params ); 
  153.  
  154. if ( null !== $result && 
  155. isset( $result->error ) && 
  156. isset( $result->error->code ) && 
  157. 'request_expired' === $result->error->code 
  158. ) { 
  159. if ( ! $retry ) { 
  160. $diff = isset( $result->error->timestamp ) ? 
  161. ( time() - strtotime( $result->error->timestamp ) ) : 
  162. false; 
  163.  
  164. // Try to sync clock diff. 
  165. if ( false !== $this->_sync_clock_diff( $diff ) ) { 
  166. // Retry call with new synced clock. 
  167. return $this->_call( $path, $method, $params, true ); 
  168.  
  169. if ( $this->_logger->is_on() && self::is_api_error( $result ) ) { 
  170. // Log API errors. 
  171. $this->_logger->api_error( $result ); 
  172.  
  173. return $result; 
  174.  
  175. /** 
  176. * Override API call to wrap it in servers' clock sync method. 
  177. * 
  178. * @param string $path 
  179. * @param string $method 
  180. * @param array $params 
  181. * 
  182. * @return array|mixed|string|void 
  183. * @throws Freemius_Exception 
  184. */ 
  185. function call( $path, $method = 'GET', $params = array() ) { 
  186. return $this->_call( $path, $method, $params ); 
  187.  
  188. /** 
  189. * Get API request URL signed via query string. 
  190. * 
  191. * @param string $path 
  192. * 
  193. * @return string 
  194. */ 
  195. function get_signed_url( $path ) { 
  196. return $this->_api->GetSignedUrl( $path ); 
  197.  
  198. /** 
  199. * @param string $path 
  200. * @param bool $flush 
  201. * @param int $expiration (optional) Time until expiration in seconds from now, defaults to 24 hours 
  202. * 
  203. * @return stdClass|mixed 
  204. */ 
  205. function get( $path = '/', $flush = false, $expiration = WP_FS__TIME_24_HOURS_IN_SEC ) { 
  206. $this->_logger->entrance( $path ); 
  207.  
  208. $cache_key = $this->get_cache_key( $path ); 
  209.  
  210. // Always flush during development. 
  211. if ( WP_FS__DEV_MODE || $this->_api->IsSandbox() ) { 
  212. $flush = true; 
  213.  
  214. $cached_result = self::$_cache->get( $cache_key ); 
  215.  
  216. if ( $flush || ! self::$_cache->has_valid( $cache_key ) ) { 
  217. $result = $this->call( $path ); 
  218.  
  219. if ( ! is_object( $result ) || isset( $result->error ) ) { 
  220. // Api returned an error. 
  221. if ( is_object( $cached_result ) && 
  222. ! isset( $cached_result ) 
  223. ) { 
  224. // If there was an error during a newer data fetch,  
  225. // fallback to older data version. 
  226. $result = $cached_result; 
  227.  
  228. if ( $this->_logger->is_on() ) { 
  229. $this->_logger->warn( 'Fallback to cached API result: ' . var_export( $cached_result, true ) ); 
  230. } else { 
  231. // If no older data version, return result without 
  232. // caching the error. 
  233. return $result; 
  234.  
  235. self::$_cache->set( $cache_key, $result, $expiration ); 
  236.  
  237. $cached_result = $result; 
  238. } else { 
  239. $this->_logger->log( 'Using cached API result.' ); 
  240.  
  241. return $cached_result; 
  242.  
  243. /** 
  244. * Check if there's a cached version of the API request. 
  245. * 
  246. * @author Vova Feldman (@svovaf) 
  247. * @since 1.2.1 
  248. * 
  249. * @param string $path 
  250. * @param string $method 
  251. * @param array $params 
  252. * 
  253. * @return bool 
  254. */ 
  255. function is_cached( $path, $method = 'GET', $params = array() ) { 
  256. $cache_key = $this->get_cache_key( $path, $method, $params ); 
  257.  
  258. return self::$_cache->has_valid( $cache_key ); 
  259.  
  260. /** 
  261. * Invalidate a cached version of the API request. 
  262. * 
  263. * @author Vova Feldman (@svovaf) 
  264. * @since 1.2.1.5 
  265. * 
  266. * @param string $path 
  267. * @param string $method 
  268. * @param array $params 
  269. */ 
  270. function purge_cache( $path, $method = 'GET', $params = array() ) { 
  271. $this->_logger->entrance( "{$method}:{$path}" ); 
  272.  
  273. $cache_key = $this->get_cache_key( $path, $method, $params ); 
  274.  
  275. self::$_cache->purge( $cache_key ); 
  276.  
  277. /** 
  278. * @param string $path 
  279. * @param string $method 
  280. * @param array $params 
  281. * 
  282. * @return string 
  283. * @throws \Freemius_Exception 
  284. */ 
  285. private function get_cache_key( $path, $method = 'GET', $params = array() ) { 
  286. $canonized = $this->_api->CanonizePath( $path ); 
  287. // $exploded = explode('/', $canonized); 
  288. // return $method . '_' . array_pop($exploded) . '_' . md5($canonized . json_encode($params)); 
  289. return strtolower( $method . ':' . $canonized ) . ( ! empty( $params ) ? '#' . md5( json_encode( $params ) ) : '' ); 
  290.  
  291. /** 
  292. * Test API connectivity. 
  293. * 
  294. * @author Vova Feldman (@svovaf) 
  295. * @since 1.0.9 If fails, try to fallback to HTTP. 
  296. * @since 1.1.6 Added a 5-min caching mechanism, to prevent from overloading the server if the API if 
  297. * temporary down. 
  298. * 
  299. * @return bool True if successful connectivity to the API. 
  300. */ 
  301. static function test() { 
  302. self::_init(); 
  303.  
  304. $cache_key = 'ping_test'; 
  305.  
  306. $test = self::$_cache->get_valid( $cache_key, null ); 
  307.  
  308. if ( is_null( $test ) ) { 
  309. $test = Freemius_Api::Test(); 
  310.  
  311. if ( false === $test && Freemius_Api::IsHttps() ) { 
  312. // Fallback to HTTP, since HTTPS fails. 
  313. Freemius_Api::SetHttp(); 
  314.  
  315. self::$_options->set_option( 'api_force_http', true, true ); 
  316.  
  317. $test = Freemius_Api::Test(); 
  318.  
  319. if ( false === $test ) { 
  320. /** 
  321. * API connectivity test fail also in HTTP request, therefore,  
  322. * fallback to HTTPS to keep connection secure. 
  323. * 
  324. * @since 1.1.6 
  325. */ 
  326. self::$_options->set_option( 'api_force_http', false, true ); 
  327.  
  328. self::$_cache->set( $cache_key, $test, WP_FS__TIME_5_MIN_IN_SEC ); 
  329.  
  330. return $test; 
  331.  
  332. /** 
  333. * Check if API is temporary down. 
  334. * 
  335. * @author Vova Feldman (@svovaf) 
  336. * @since 1.1.6 
  337. * 
  338. * @return bool 
  339. */ 
  340. static function is_temporary_down() { 
  341. self::_init(); 
  342.  
  343. $test = self::$_cache->get_valid( 'ping_test', null ); 
  344.  
  345. return ( false === $test ); 
  346.  
  347. /** 
  348. * @author Vova Feldman (@svovaf) 
  349. * @since 1.1.6 
  350. * 
  351. * @return object 
  352. */ 
  353. private function get_temporary_unavailable_error() { 
  354. return (object) array( 
  355. 'error' => (object) array( 
  356. 'type' => 'TemporaryUnavailable',  
  357. 'message' => 'API is temporary unavailable, please retry in ' . ( self::$_cache->get_record_expiration( 'ping_test' ) - WP_FS__SCRIPT_START_TIME ) . ' sec.',  
  358. 'code' => 'temporary_unavailable',  
  359. 'http' => 503 
  360. ); 
  361.  
  362. /** 
  363. * Ping API for connectivity test, and return result object. 
  364. * 
  365. * @author Vova Feldman (@svovaf) 
  366. * @since 1.0.9 
  367. * 
  368. * @param null|string $unique_anonymous_id 
  369. * @param array $params 
  370. * 
  371. * @return object 
  372. */ 
  373. function ping( $unique_anonymous_id = null, $params = array() ) { 
  374. $this->_logger->entrance(); 
  375.  
  376. if ( self::is_temporary_down() ) { 
  377. return $this->get_temporary_unavailable_error(); 
  378.  
  379. $pong = is_null( $unique_anonymous_id ) ? 
  380. Freemius_Api::Ping() : 
  381. $this->_call( 'ping.json?' . http_build_query( array_merge( 
  382. array( 'uid' => $unique_anonymous_id ),  
  383. $params 
  384. ) ) ); 
  385.  
  386. if ( $this->is_valid_ping( $pong ) ) { 
  387. return $pong; 
  388.  
  389. if ( self::should_try_with_http( $pong ) ) { 
  390. // Fallback to HTTP, since HTTPS fails. 
  391. Freemius_Api::SetHttp(); 
  392.  
  393. self::$_options->set_option( 'api_force_http', true, true ); 
  394.  
  395. $pong = is_null( $unique_anonymous_id ) ? 
  396. Freemius_Api::Ping() : 
  397. $this->_call( 'ping.json?' . http_build_query( array_merge( 
  398. array( 'uid' => $unique_anonymous_id ),  
  399. $params 
  400. ) ) ); 
  401.  
  402. if ( ! $this->is_valid_ping( $pong ) ) { 
  403. self::$_options->set_option( 'api_force_http', false, true ); 
  404.  
  405. return $pong; 
  406.  
  407. /** 
  408. * Check if based on the API result we should try 
  409. * to re-run the same request with HTTP instead of HTTPS. 
  410. * 
  411. * @author Vova Feldman (@svovaf) 
  412. * @since 1.1.6 
  413. * 
  414. * @param $result 
  415. * 
  416. * @return bool 
  417. */ 
  418. private static function should_try_with_http( $result ) { 
  419. if ( ! Freemius_Api::IsHttps() ) { 
  420. return false; 
  421.  
  422. return ( ! is_object( $result ) || 
  423. ! isset( $result->error ) || 
  424. ! isset( $result->error->code ) || 
  425. ! in_array( $result->error->code, array( 
  426. 'curl_missing',  
  427. 'cloudflare_ddos_protection',  
  428. 'maintenance_mode',  
  429. 'squid_cache_block',  
  430. 'too_many_requests',  
  431. ) ) ); 
  432.  
  433.  
  434. /** 
  435. * Check if valid ping request result. 
  436. * 
  437. * @author Vova Feldman (@svovaf) 
  438. * @since 1.1.1 
  439. * 
  440. * @param mixed $pong 
  441. * 
  442. * @return bool 
  443. */ 
  444. function is_valid_ping( $pong ) { 
  445. return Freemius_Api::Test( $pong ); 
  446.  
  447. function get_url( $path = '' ) { 
  448. return Freemius_Api::GetUrl( $path, $this->_api->IsSandbox() ); 
  449.  
  450. /** 
  451. * Clear API cache. 
  452. * 
  453. * @author Vova Feldman (@svovaf) 
  454. * @since 1.0.9 
  455. */ 
  456. static function clear_cache() { 
  457. self::_init(); 
  458.  
  459. self::$_cache = FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME ); 
  460. self::$_cache->clear(); 
  461.  
  462. #---------------------------------------------------------------------------------- 
  463. #region Error Handling 
  464. #---------------------------------------------------------------------------------- 
  465.  
  466. /** 
  467. * @author Vova Feldman (@svovaf) 
  468. * @since 1.2.1.5 
  469. * 
  470. * @param mixed $result 
  471. * 
  472. * @return bool Is API result contains an error. 
  473. */ 
  474. static function is_api_error( $result ) { 
  475. return ( is_object( $result ) && isset( $result->error ) ) || 
  476. is_string( $result ); 
  477.  
  478. /** 
  479. * Checks if given API result is a non-empty and not an error object. 
  480. * 
  481. * @author Vova Feldman (@svovaf) 
  482. * @since 1.2.1.5 
  483. * 
  484. * @param mixed $result 
  485. * @param string|null $required_property Optional property we want to verify that is set. 
  486. * 
  487. * @return bool 
  488. */ 
  489. static function is_api_result_object( $result, $required_property = null ) { 
  490. return ( 
  491. is_object( $result ) && 
  492. ! isset( $result->error ) && 
  493. ( empty( $required_property ) || isset( $result->{$required_property} ) ) 
  494. ); 
  495.  
  496. /** 
  497. * Checks if given API result is a non-empty entity object with non-empty ID. 
  498. * 
  499. * @author Vova Feldman (@svovaf) 
  500. * @since 1.2.1.5 
  501. * 
  502. * @param mixed $result 
  503. * 
  504. * @return bool 
  505. */ 
  506. static function is_api_result_entity( $result ) { 
  507. return self::is_api_result_object( $result, 'id' ) && 
  508. FS_Entity::is_valid_id( $result->id ); 
  509.  
  510. #endregion 
.