PageRenderTime 41ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Pyrus/REST.php

https://github.com/CloCkWeRX/Pyrus
PHP | 322 lines | 191 code | 34 blank | 97 comment | 40 complexity | 6ddb955944cdb6476d67c7f1527c1710 MD5 | raw file
  1. <?php
  2. /**
  3. * PEAR_REST
  4. *
  5. * PHP versions 5
  6. *
  7. * @category Pyrus
  8. * @package Pyrus
  9. * @author Greg Beaver <cellog@php.net>
  10. * @copyright 2010 The PEAR Group
  11. * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
  12. * @link https://github.com/pyrus/Pyrus
  13. */
  14. /**
  15. * Intelligently retrieve data, following hyperlinks if necessary, and re-directing
  16. * as well
  17. *
  18. * @category Pyrus
  19. * @package Pyrus
  20. * @author Greg Beaver <cellog@php.net>
  21. * @copyright 2010 The PEAR Group
  22. * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
  23. * @link https://github.com/pyrus/Pyrus
  24. */
  25. namespace Pyrus;
  26. class REST
  27. {
  28. protected $config;
  29. protected $_options;
  30. function __construct()
  31. {
  32. $this->config = Config::current();
  33. $this->_options = Main::$options;
  34. }
  35. /**
  36. * Retrieve REST data, but always retrieve the local cache if it is available.
  37. *
  38. * This is useful for elements that should never change, such as information on a particular
  39. * release
  40. *
  41. * @param string full URL to this resource
  42. * @param array|false contents of the accept-encoding header
  43. * @param boolean if true, xml will be returned as a string, otherwise, xml will be
  44. * parsed using Pyrus\XMLParser
  45. *
  46. * @return string|array
  47. */
  48. function retrieveCacheFirst($url, $accept = false, $forcestring = false)
  49. {
  50. $cachefile = $this->config->cache_dir . DIRECTORY_SEPARATOR .
  51. md5($url) . 'rest.cachefile';
  52. if (file_exists($cachefile)) {
  53. return unserialize(implode('', file($cachefile)));
  54. }
  55. return $this->retrieveData($url, $accept, $forcestring);
  56. }
  57. /**
  58. * Retrieve a remote REST resource
  59. *
  60. * @param string full URL to this resource
  61. * @param array|false contents of the accept-encoding header
  62. * @param boolean if true, xml will be returned as a string, otherwise, xml will be
  63. * parsed using Pyrus\XMLParser
  64. *
  65. * @return string|array
  66. *
  67. * @throws Pyrus\REST\Exception If the xml cannot be parsed
  68. */
  69. function retrieveData($url, $accept = false, $forcestring = false)
  70. {
  71. $cacheId = $this->getCacheId($url);
  72. if ($ret = $this->useLocalCache($url, $cacheId)) {
  73. return $ret;
  74. }
  75. if (!isset($this->_options['offline'])) {
  76. $trieddownload = true;
  77. try {
  78. $file = $this->downloadHttp($url, $cacheId ? $cacheId['lastChange'] : false, $accept);
  79. } catch (\PEAR2\HTTP\Request\Exception $e) {
  80. $file = $trieddownload = false;
  81. }
  82. } else {
  83. $file = $trieddownload = false;
  84. }
  85. if (!$file) {
  86. $ret = $this->getCache($url);
  87. if ($trieddownload) {
  88. // reset the age of the cache if the server says it was unmodified
  89. $this->saveCache($url, $ret, null, true, $cacheId);
  90. }
  91. return $ret;
  92. }
  93. if (is_array($file)) {
  94. $headers = $file[2];
  95. $lastmodified = $file[1];
  96. $content = $file[0];
  97. } else {
  98. $content = $file;
  99. $lastmodified = false;
  100. $headers = array();
  101. }
  102. if ($forcestring) {
  103. $this->saveCache($url, $content, $lastmodified, false, $cacheId);
  104. return $content;
  105. }
  106. // Default to XML if no content-type is provided
  107. //TODO: Deal with text as well, look at PEAR 1.9/1.8
  108. $ct = isset($headers['content-type']) ? $headers['content-type'] : 'text/xml';
  109. switch ($ct) {
  110. case 'text/xml' :
  111. case 'application/xml' :
  112. $parser = new XMLParser;
  113. try {
  114. $content = $parser->parseString($content);
  115. $content = current($content);
  116. } catch (\Exception $e) {
  117. throw new REST\Exception('Invalid xml downloaded from "' . $url . '"', $e);
  118. }
  119. case 'text/html' :
  120. default :
  121. // use it as a string
  122. }
  123. $this->saveCache($url, $content, $lastmodified, false, $cacheId);
  124. return $content;
  125. }
  126. function useLocalCache($url, $cacheid = null)
  127. {
  128. if ($cacheid === null) {
  129. $cacheidfile = $this->config->cache_dir . DIRECTORY_SEPARATOR .
  130. md5($url) . 'rest.cacheid';
  131. if (!file_exists($cacheidfile)) {
  132. return false;
  133. }
  134. $cacheid = unserialize(implode('', file($cacheidfile)));
  135. }
  136. $cachettl = $this->config->cache_ttl;
  137. // If cache is newer than $cachettl seconds, we use the cache!
  138. if (time() - $cacheid['age'] < $cachettl) {
  139. return $this->getCache($url);
  140. }
  141. return false;
  142. }
  143. function getCacheId($url)
  144. {
  145. $cacheidfile = $this->config->cache_dir . DIRECTORY_SEPARATOR .
  146. md5($url) . 'rest.cacheid';
  147. if (file_exists($cacheidfile)) {
  148. $ret = unserialize(implode('', file($cacheidfile)));
  149. return $ret;
  150. }
  151. return false;
  152. }
  153. /**
  154. * retrieve a url stored in the cache
  155. *
  156. * @param string $url full URL to REST resource
  157. *
  158. * @return string contents of file
  159. *
  160. * @throws Pyrus\REST\Exception if no cache exists
  161. */
  162. function getCache($url)
  163. {
  164. $cachefile = $this->config->cache_dir . DIRECTORY_SEPARATOR .
  165. md5($url) . 'rest.cachefile';
  166. if (file_exists($cachefile)) {
  167. return unserialize(implode('', file($cachefile)));
  168. }
  169. throw new REST\Exception('No cached content available for "' . $url . '"');
  170. }
  171. /**
  172. * @param string $url full URL to REST resource
  173. * @param string $contents original contents of the REST resource
  174. * @param array $lastmodified HTTP Last-Modified and ETag headers
  175. * @param bool $nochange if true, then the cache id file should be
  176. * regenerated to trigger a new time-to-live value
  177. * @param string $cacheid optional filename of the cache file
  178. *
  179. * @return bool Returns true on success, false on error
  180. *
  181. * @throws Pyrus\REST\Exception
  182. */
  183. function saveCache($url, $contents, $lastmodified, $nochange = false, $cacheid = null)
  184. {
  185. $cacheidfile = $this->config->cache_dir . DIRECTORY_SEPARATOR .
  186. md5($url) . 'rest.cacheid';
  187. $cachefile = $this->config->cache_dir . DIRECTORY_SEPARATOR .
  188. md5($url) . 'rest.cachefile';
  189. if ($cacheid === null && $nochange) {
  190. $cacheid = unserialize(implode('', file($cacheidfile)));
  191. }
  192. $fp = @fopen($cacheidfile, 'wb');
  193. if (!$fp) {
  194. $cache_dir = $this->config->cache_dir;
  195. if (is_dir($cache_dir)) {
  196. return false;
  197. }
  198. if (!@mkdir($cache_dir, 0755, true)) {
  199. throw new REST\Exception('Cannot create REST cache directory ' . $cache_dir);
  200. }
  201. $fp = @fopen($cacheidfile, 'wb');
  202. if (!$fp) {
  203. return false;
  204. }
  205. }
  206. if ($nochange) {
  207. fwrite($fp, serialize(array(
  208. 'age' => time(),
  209. 'lastChange' => $cacheid['lastChange'],
  210. )));
  211. fclose($fp);
  212. return true;
  213. } else {
  214. fwrite($fp, serialize(array(
  215. 'age' => time(),
  216. 'lastChange' => $lastmodified,
  217. )));
  218. }
  219. fclose($fp);
  220. $fp = @fopen($cachefile, 'wb');
  221. if (!$fp) {
  222. if (file_exists($cacheidfile)) {
  223. @unlink($cacheidfile);
  224. }
  225. return false;
  226. }
  227. fwrite($fp, serialize($contents));
  228. fclose($fp);
  229. return true;
  230. }
  231. /**
  232. * Efficiently Download a file through HTTP. Returns downloaded file as a string in-memory
  233. * This is best used for small files
  234. *
  235. * If an HTTP proxy has been configured (http_proxy Pyrus\Config
  236. * setting), the proxy will be used.
  237. *
  238. * @param string $url the URL to download
  239. * @param false|string|array $lastmodified header values to check against
  240. * for caching use false to return
  241. * the header values from this download
  242. * @param false|array $accept Accept headers to send
  243. *
  244. * @return string|array Returns the contents of the downloaded file or a PEAR
  245. * error on failure. If the error is caused by
  246. * socket-related errors, the error object will
  247. * have the fsockopen error code available through
  248. * getCode(). If caching is requested, then return the header
  249. * values.
  250. *
  251. * @throws Pyrus\REST\Exception if the url is invalid
  252. * @access public
  253. */
  254. function downloadHttp($url, $lastmodified = null, $accept = false)
  255. {
  256. $info = parse_url($url);
  257. if (!isset($info['scheme']) || !in_array($info['scheme'], array('http', 'https'))) {
  258. throw new REST\Exception('Cannot download non-http URL "' . $url . '"');
  259. }
  260. if (!isset($info['host'])) {
  261. throw new REST\Exception('Cannot download from non-URL "' . $url . '"');
  262. }
  263. $response = Main::download($url);
  264. if ($response->code == 304 && ($lastmodified || ($lastmodified === false))) {
  265. return false;
  266. }
  267. if (isset($response->headers['content-length'])) {
  268. $length = $response->headers['content-length'];
  269. } else {
  270. $length = -1;
  271. }
  272. $data = $response->body;
  273. if ($lastmodified === false || $lastmodified) {
  274. if (isset($response->headers['etag'])) {
  275. $lastmodified = array('ETag' => $response->headers['etag']);
  276. }
  277. if (isset($response->headers['last-modified'])) {
  278. if (is_array($lastmodified)) {
  279. $lastmodified['Last-Modified'] = $response->headers['last-modified'];
  280. } else {
  281. $lastmodified = $response->headers['last-modified'];
  282. }
  283. }
  284. return array($data, $lastmodified, $response->headers);
  285. }
  286. return $data;
  287. }
  288. }