PageRenderTime 43ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/models/datasources/apis_source.php

https://github.com/mb2o/CakePHP-Api-Datasources
PHP | 456 lines | 244 code | 45 blank | 167 comment | 56 complexity | 9dedd180d5ece2cda9951a2433c4a926 MD5 | raw file
  1. <?php
  2. /**
  3. * Apis DataSource
  4. *
  5. * [Short Description]
  6. *
  7. * @package default
  8. * @author Dean Sofer
  9. **/
  10. class ApisSource extends DataSource {
  11. /**
  12. * The description of this data source
  13. *
  14. * @var string
  15. */
  16. public $description = 'Apis DataSource';
  17. /**
  18. * Holds the datasource configuration
  19. *
  20. * @var array
  21. */
  22. public $config = array();
  23. // TODO: Relocate to a dedicated schema file
  24. public $_schema = array();
  25. /**
  26. * Instance of CakePHP core HttpSocket class
  27. *
  28. * @var HttpSocket
  29. */
  30. public $Http = null;
  31. /**
  32. * Request Logs
  33. *
  34. * @var array
  35. * @access private
  36. */
  37. private $__requestLog = array();
  38. /**
  39. * Request Log limit per entry in bytes
  40. *
  41. * @var integer
  42. * @access protected
  43. */
  44. protected $_logLimitBytes = 5000;
  45. /**
  46. * Holds a configuration map
  47. *
  48. * @var array
  49. */
  50. public $map = array();
  51. /**
  52. * API options
  53. * @var array
  54. */
  55. public $options = array(
  56. 'format' => 'json',
  57. 'ps' => '&', // param separator
  58. 'kvs' => '=', // key-value separator
  59. );
  60. /**
  61. * Loads HttpSocket class
  62. *
  63. * @param array $config
  64. * @param HttpSocket $Http
  65. */
  66. public function __construct($config, $Http = null) {
  67. parent::__construct($config);
  68. // Store the API configuration map
  69. $name = pluginSplit($config['driver']);
  70. if (Configure::load($name[0] . '.' . Inflector::underscore($name[1]))) {
  71. $this->map = Configure::read('Apis.' . $name[1]);
  72. }
  73. // Store the HttpSocket reference
  74. if (!$Http) {
  75. if (isset($config['method']) && ($config['method'] = 'OAuth' || !empty($this->map['oauth']['version']))) {
  76. App::import('Lib', 'HttpSocketOauth.HttpSocketOauth');
  77. $Http = new HttpSocketOauth();
  78. } else {
  79. App::import('Core', 'HttpSocket');
  80. $Http = new HttpSocket();
  81. }
  82. }
  83. $this->Http = $Http;
  84. }
  85. /**
  86. * Sends HttpSocket requests. Builds your uri and formats the response too.
  87. *
  88. * @param string $params
  89. * @param array $options
  90. * method: get, post, delete, put
  91. * data: either in string form: "option1=foo&option2=bar" or as a keyed array: array('option1' => 'foo', 'option2' => 'bar')
  92. * @return array $response
  93. * @author Dean Sofer
  94. */
  95. public function request(&$model) {
  96. if (is_object($model)) {
  97. $request = $model->request;
  98. } elseif (is_array($model)) {
  99. $request = $model;
  100. } elseif (is_string($model)) {
  101. $request = array('uri' => $model);
  102. }
  103. if (isset($this->config['method']) && $this->config['method'] == 'OAuth') {
  104. $request = $this->addOauth($model, $request);
  105. } elseif (isset($this->config['method']) && $this->config['method'] == 'OAuthV2') {
  106. $request = $this->addOauthV2($model, $request);
  107. }
  108. if (empty($request['uri']['host'])) {
  109. $request['uri']['host'] = $this->map['hosts']['rest'];
  110. }
  111. if (empty($request['uri']['scheme']) && !empty($this->map['oauth']['scheme'])) {
  112. $request['uri']['scheme'] = $this->map['oauth']['scheme'];
  113. }
  114. // Remove unwanted elements from request array
  115. $request = array_intersect_key($request, $this->Http->request);
  116. if (!empty($this->tokens)) {
  117. $request['uri']['path'] = $this->swapTokens($model, $request['uri']['path'], $this->tokens);
  118. }
  119. if (method_exists($this, 'beforeRequest')) {
  120. $request = $this->beforeRequest(&$model, $request);
  121. }
  122. $timerStart = microtime(true);
  123. // Issues request
  124. $response = $this->Http->request($request);
  125. $timerEnd = microtime(true);
  126. // Log the request in the query log
  127. if(Configure::read('debug')) {
  128. $logText = '';
  129. foreach(array('request','response') as $logPart) {
  130. $logTextForThisPart = $this->Http->{$logPart}['raw'];
  131. if($logPart == 'response') {
  132. $logTextForThisPart = $logTextForThisPart['response'];
  133. }
  134. if(strlen($logTextForThisPart) > $this->_logLimitBytes) {
  135. $logTextForThisPart = substr($logTextForThisPart, 0, $this->_logLimitBytes).' [ ... truncated ...]';
  136. }
  137. $logText .= '---'.strtoupper($logPart)."---\n".$logTextForThisPart."\n\n";
  138. }
  139. $took = round(($timerEnd - $timerStart)/1000);
  140. $newLog = array(
  141. 'query' => $logText,
  142. 'error' => '',
  143. 'affected' => '',
  144. 'numRows' => '',
  145. 'took' => $took,
  146. );
  147. $this->__requestLog[] = $newLog;
  148. }
  149. $response = $this->decode($response);
  150. if (is_object($model)) {
  151. $model->response = $response;
  152. }
  153. // Check response status code for success or failure
  154. if (substr($this->Http->response['status']['code'], 0, 1) != 2) {
  155. if (is_object($model) && method_exists($model, 'onError')) {
  156. $model->onError();
  157. }
  158. return false;
  159. }
  160. return $response;
  161. }
  162. /**
  163. * Supplements a request array with oauth credentials
  164. *
  165. * @param object $model
  166. * @param array $request
  167. * @return array $request
  168. */
  169. public function addOauth(&$model, $request) {
  170. if (!empty($this->config['oauth_token']) && !empty($this->config['oauth_token_secret'])) {
  171. $request['auth']['method'] = 'OAuth';
  172. $request['auth']['oauth_consumer_key'] = $this->config['login'];
  173. $request['auth']['oauth_consumer_secret'] = $this->config['password'];
  174. if (isset($this->config['oauth_token'])) {
  175. $request['auth']['oauth_token'] = $this->config['oauth_token'];
  176. }
  177. if (isset($this->config['oauth_token_secret'])) {
  178. $request['auth']['oauth_token_secret'] = $this->config['oauth_token_secret'];
  179. }
  180. }
  181. return $request;
  182. }
  183. /**
  184. * Supplements a request array with oauth credentials
  185. *
  186. * @param object $model
  187. * @param array $request
  188. * @return array $request
  189. */
  190. public function addOauthV2(&$model, $request) {
  191. if (!empty($this->config['access_token'])) {
  192. $request['auth']['method'] = 'OAuth';
  193. $request['auth']['oauth_version'] = '2.0';
  194. $request['auth']['client_id'] = $this->config['login'];
  195. $request['auth']['client_secret'] = $this->config['password'];
  196. if (isset($this->config['access_token'])) {
  197. $request['auth']['access_token'] = $this->config['access_token'];
  198. }
  199. }
  200. return $request;
  201. }
  202. /**
  203. * Decodes the response based on the content type
  204. *
  205. * @param string $response
  206. * @return void
  207. * @author Dean Sofer
  208. */
  209. public function decode($response) {
  210. // Get content type header
  211. $contentType = $this->Http->response['header']['Content-Type'];
  212. // Extract content type from content type header
  213. if (preg_match('/^([a-z0-9\/\+]+);\s*charset=([a-z0-9\-]+)/i', $contentType, $matches)) {
  214. $contentType = $matches[1];
  215. $charset = $matches[2];
  216. }
  217. // Decode response according to content type
  218. switch ($contentType) {
  219. case 'application/xml':
  220. case 'application/atom+xml':
  221. case 'application/rss+xml':
  222. // If making multiple requests that return xml, I found that using the
  223. // same Xml object with Xml::load() to load new responses did not work,
  224. // consequently it is necessary to create a whole new instance of the
  225. // Xml class. This can use a lot of memory so we have to manually
  226. // garbage collect the Xml object when we've finished with it, i.e. got
  227. // it to transform the xml string response into a php array.
  228. App::import('Core', 'Xml');
  229. $Xml = new Xml($response);
  230. $response = $Xml->toArray(false); // Send false to get separate elements
  231. $Xml->__destruct();
  232. $Xml = null;
  233. unset($Xml);
  234. break;
  235. case 'application/json':
  236. case 'text/javascript':
  237. $response = json_decode($response, true);
  238. break;
  239. }
  240. return $response;
  241. }
  242. public function listSources() {
  243. return array_keys($this->_schema);
  244. }
  245. /**
  246. * Iterates through the tokens (passed or request items) and replaces them into the url
  247. *
  248. * @param string $url
  249. * @param array $tokens optional
  250. * @return string $url
  251. * @author Dean Sofer
  252. */
  253. public function swapTokens(&$model, $url, $tokens = array()) {
  254. $formattedTokens = array();
  255. foreach ($tokens as $token => $value) {
  256. $formattedTokens[':'.$token] = $value;
  257. }
  258. $url = strtr($url, $formattedTokens);
  259. return $url;
  260. }
  261. /**
  262. * Generates a conditions section of the url
  263. *
  264. * @param array $params permitted conditions
  265. * @param array $queryData passed conditions in key => value form
  266. * @return string
  267. * @author Dean Sofer
  268. */
  269. public function buildQuery($params = array(), $data = array()) {
  270. $query = array();
  271. foreach ($params as $param) {
  272. if (!empty($data[$param]) && $this->options['kvs']) {
  273. $query[] = $param . $this->options['kvs'] . $data[$param];
  274. } elseif (!empty($data[$param])) {
  275. $query[] = $data[$param];
  276. }
  277. }
  278. return implode($this->options['ps'], $query);
  279. }
  280. /**
  281. * Tries iterating through the config map of REST commmands to decide which command to use
  282. *
  283. * @param object $model
  284. * @param string $action
  285. * @param string $section
  286. * @param array $params
  287. * @return boolean $found
  288. * @author Dean Sofer
  289. */
  290. public function scanMap(&$model, $action, $section, $params = array()) {
  291. if (!isset($this->map[$action][$section])) {
  292. $this->log('Section ' . $section . ' not found in Apis Driver Configuration Map - ' . get_class($this));
  293. return false;
  294. }
  295. $map = $this->map[$action][$section];
  296. foreach ($map as $path => $conditions) {
  297. $optional = (isset($conditions['optional'])) ? $conditions['optional'] : array();
  298. unset($conditions['optional']);
  299. if (array_intersect(array_keys($params), $conditions) == $conditions) {
  300. return array($path, $conditions, $optional);
  301. }
  302. }
  303. return false;
  304. }
  305. /**
  306. * Play nice with the DebugKit
  307. *
  308. * @param boolean sorted ignored
  309. * @param boolean clear will clear the log if set to true (default)
  310. * @return array of log requested
  311. */
  312. public function getLog($sorted = false, $clear = true){
  313. $log = $this->__requestLog;
  314. if($clear){
  315. $this->__requestLog = array();
  316. }
  317. return array('log' => $log, 'count' => count($log), 'time' => 'Unknown');
  318. }
  319. /**
  320. * Just-In-Time callback for any last-minute request modifications
  321. *
  322. * @param object $model
  323. * @param array $request
  324. * @return array $request
  325. * @author Dean Sofer
  326. */
  327. public function beforeRequest(&$model, $request) {
  328. return $request;
  329. }
  330. /**
  331. * Uses standard find conditions. Use find('all', $params). Since you cannot pull specific fields,
  332. * we will instead use 'fields' to specify what table to pull from.
  333. *
  334. * @param string $model The model being read.
  335. * @param string $queryData An array of query data used to find the data you want
  336. * @return mixed
  337. * @access public
  338. */
  339. public function read(&$model, $queryData = array()) {
  340. if (!isset($model->request)) {
  341. $model->request = array();
  342. }
  343. $model->request = array_merge(array('method' => 'GET'), $model->request);
  344. if (empty($model->request['uri']['path']) && !empty($queryData['path'])) {
  345. $model->request['uri']['path'] = $queryData['path'];
  346. } elseif (!empty($this->map['read']) && is_string($queryData['fields'])) {
  347. if (!isset($queryData['conditions'])) {
  348. $queryData['conditions'] = array();
  349. }
  350. $scan = $this->scanMap($model, 'read', $queryData['fields'], $queryData['conditions']);
  351. if ($scan) {
  352. $model->request['uri']['path'] = $scan[0];
  353. $model->request['uri']['query'] = array();
  354. $usedConditions = array_intersect(array_keys($queryData['conditions']), array_merge($scan[1], $scan[2]));
  355. foreach ($usedConditions as $condition) {
  356. $model->request['uri']['query'][$condition] = $queryData['conditions'][$condition];
  357. }
  358. } else {
  359. return false;
  360. }
  361. }
  362. return $this->request($model);
  363. }
  364. /**
  365. * Sets method = POST in request if not already set
  366. *
  367. * @param AppModel $model
  368. * @param array $fields Unused
  369. * @param array $values Unused
  370. */
  371. public function create(&$model, $fields = null, $values = null) {
  372. if (!isset($model->request)) {
  373. $model->request = array();
  374. }
  375. if (empty($model->request['body']) && !empty($fields) && !empty($values)) {
  376. $model->request['body'] = array_combine($fields, $values);
  377. }
  378. $model->request = array_merge(array('method' => 'POST'), $model->request);
  379. return $this->request($model);
  380. }
  381. /**
  382. * Sets method = PUT in request if not already set
  383. *
  384. * @param AppModel $model
  385. * @param array $fields Unused
  386. * @param array $values Unused
  387. */
  388. public function update(&$model, $fields = null, $values = null) {
  389. if (!isset($model->request)) {
  390. $model->request = array();
  391. }
  392. if (empty($model->request['body']) && !empty($fields) && !empty($values)) {
  393. $model->request['body'] = array_combine($fields, $values);
  394. }
  395. $model->request = array_merge(array('method' => 'PUT'), $model->request);
  396. return $this->request($model);
  397. }
  398. /**
  399. * Sets method = DELETE in request if not already set
  400. *
  401. * @param AppModel $model
  402. * @param mixed $id Unused
  403. */
  404. public function delete(&$model, $id = null) {
  405. if (!isset($model->request)) {
  406. $model->request = array();
  407. }
  408. $model->request = array_merge(array('method' => 'DELETE'), $model->request);
  409. return $this->request($model);
  410. }
  411. }