FS_Api

Class FS_Api.

Defined (1)

The class is defined in the following location(s).

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