PageRenderTime 26ms CodeModel.GetById 8ms RepoModel.GetById 0ms app.codeStats 0ms

/app/models/media_proxy.php

https://github.com/ratbird/hope
PHP | 247 lines | 147 code | 38 blank | 62 comment | 23 complexity | bfe034efbb75c6a172f149c0cf18bc09 MD5 | raw file
Possible License(s): LGPL-2.1, CC-BY-SA-3.0, MIT, BSD-3-Clause, GPL-2.0
  1. <?php
  2. # Lifter010: TODO
  3. /**
  4. * media_proxy.php - media proxy cache model
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU General Public License as
  8. * published by the Free Software Foundation; either version 2 of
  9. * the License, or (at your option) any later version.
  10. *
  11. * @author Elmar Ludwig
  12. * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
  13. * @category Stud.IP
  14. */
  15. require_once 'lib/datei.inc.php';
  16. /**
  17. * Special Exception class for proxy errors. The exception message
  18. * must be HTTP/1.1 response status line.
  19. */
  20. class MediaProxyException extends Exception {
  21. }
  22. /**
  23. * Model class for the Stud.IP media proxy.
  24. */
  25. class MediaProxy
  26. {
  27. const GC_PROBABILITY = 2;
  28. private $cache_path;
  29. private $cache_lifetime;
  30. private $cache_maxlength;
  31. /**
  32. * Initalize a new MediaProxy instance.
  33. */
  34. public function __construct()
  35. {
  36. $config = Config::GetInstance();
  37. $this->cache_path = $config->getValue('MEDIA_CACHE_PATH');
  38. $this->cache_lifetime = $config->getValue('MEDIA_CACHE_LIFETIME');
  39. $this->cache_maxlength = $config->getValue('MEDIA_CACHE_MAX_LENGTH');
  40. if (mt_rand(0, 99) < self::GC_PROBABILITY) {
  41. $this->garbageCollect();
  42. }
  43. }
  44. /**
  45. * Retrieve meta data about a (possibly) cached media resource.
  46. *
  47. * @return array meta data of resource or NULL (not cached)
  48. */
  49. public function getMetaData($url)
  50. {
  51. $id = md5($url);
  52. $query = "SELECT id, type, UNIX_TIMESTAMP(chdate) AS chdate,
  53. UNIX_TIMESTAMP(expires) AS expires
  54. FROM media_cache
  55. WHERE id = ?";
  56. $statement = DBManager::get()->prepare($query);
  57. $statement->execute(array($id));
  58. if ($row = $statement->fetch()) {
  59. if ($row['expires'] > time()) {
  60. return $row;
  61. } else {
  62. $this->removeCacheEntries(array($id));
  63. }
  64. }
  65. return NULL;
  66. }
  67. /**
  68. * Read URL and send data to the browser (similar to readfile()).
  69. * Will cache the sent data if possible. An optional timestamp can
  70. * be specified if the browser supplied an If-Modified-Since header.
  71. *
  72. * @param string $url URL to send
  73. * @param int $modified_since test if resource is modified
  74. */
  75. public function readURL($url, $modified_since = NULL)
  76. {
  77. $metadata = $this->getMetaData($url);
  78. $cachefile = $this->getCacheFile(md5($url));
  79. if (!$metadata) {
  80. return $this->cacheURL($url);
  81. }
  82. if (isset($modified_since) && $metadata['chdate'] <= $modified_since) {
  83. throw new MediaProxyException('HTTP/1.0 304 Not Modified');
  84. }
  85. $type = $metadata['type'];
  86. $chdate = $metadata['chdate'];
  87. $expires = $metadata['expires'];
  88. if (file_exists($cachefile)) {
  89. $this->sendHeaders($type, filesize($cachefile), $chdate, $expires);
  90. readfile($cachefile);
  91. } else {
  92. $this->sendHeaders($type, NULL, $chdate, $expires);
  93. $this->sendData($url, true);
  94. }
  95. }
  96. /**
  97. * Send the appropriate HTTP response headers to the client.
  98. */
  99. private function sendHeaders($type, $length, $chdate, $expires)
  100. {
  101. if (isset($length)) {
  102. header("Content-Length: $length");
  103. }
  104. header("Content-Type: $type");
  105. header("Last-Modified: " . gmdate(DATE_RFC1123, $chdate));
  106. header("Expires: " . gmdate(DATE_RFC1123, $expires));
  107. header('Pragma: public');
  108. }
  109. /**
  110. * Send the data from the given URL to the client.
  111. *
  112. * @param string $url URL to send
  113. * @param bool $cache should data be cached?
  114. */
  115. private function sendData($url, $cache)
  116. {
  117. $handle = fopen($url, 'rb');
  118. $length = 0;
  119. $data = '';
  120. if ($handle === false) {
  121. throw new MediaProxyException('HTTP/1.1 404 Not Found');
  122. }
  123. while (!feof($handle)) {
  124. $buffer = fread($handle, 65536);
  125. $length += strlen($buffer);
  126. if ($cache) {
  127. if ($length <= $this->cache_maxlength) {
  128. $data .= $buffer;
  129. } else {
  130. $cache = false;
  131. }
  132. }
  133. echo $buffer;
  134. }
  135. fclose($handle);
  136. if ($cache) {
  137. file_put_contents($this->getCacheFile(md5($url)), $data);
  138. }
  139. }
  140. /**
  141. * Read URL, try to cache the data and send it to the browser.
  142. *
  143. * @param string $url URL to send
  144. */
  145. private function cacheURL($url)
  146. {
  147. $response = parse_link($url);
  148. foreach ($response as $key => $value) {
  149. $response[strtolower($key)] = $value;
  150. }
  151. if ($response['response_code'] != 200) {
  152. throw new MediaProxyException($response['response']);
  153. } else if (!isset($response['content-type'])
  154. || !in_array(array_shift(explode('/', $response['content-type'])), words('image audio video'))
  155. || stripos($response['content-type'], 'svg') !== false) {
  156. throw new MediaProxyException('HTTP/1.1 415 Unsupported Media Type');
  157. }
  158. $type = $response['content-type'];
  159. $length = $response['content-length'];
  160. $chdate = $response['last-modified'];
  161. $expires = $response['expires'];
  162. $chdate = isset($chdate) ? strtotime($chdate) : time();
  163. $expires = isset($expires) ? strtotime($expires) : time() + $this->cache_lifetime;
  164. $this->sendHeaders($type, $length, $chdate, $expires);
  165. $this->sendData($url, $length <= $this->cache_maxlength);
  166. $this->addCacheEntry(md5($url), $type, $chdate, $expires);
  167. }
  168. /**
  169. * Remove old files from the media cache.
  170. */
  171. public function garbageCollect()
  172. {
  173. $db = DBManager::get();
  174. $config = Config::GetInstance();
  175. $limit = (int)$config->getValue('MEDIA_CACHE_MAX_FILES');
  176. $result = $db->query("SELECT id FROM media_cache ORDER BY expires DESC LIMIT $limit, 1000");
  177. if ($ids = $result->fetchAll(PDO::FETCH_COLUMN)) {
  178. $this->removeCacheEntries($ids);
  179. }
  180. }
  181. /**
  182. * Get the file system path for a cached resource.
  183. */
  184. private function getCacheFile($id)
  185. {
  186. return $this->cache_path . '/' . $id;
  187. }
  188. /**
  189. * Add a cached resource to the database table.
  190. */
  191. private function addCacheEntry($id, $type, $chdate, $expires)
  192. {
  193. $db = DBManager::get();
  194. $stmt = $db->prepare('INSERT INTO media_cache (id, type, chdate, expires) VALUES (?,?,?,?)');
  195. $stmt->execute(array($id, $type, strftime('%F %T', $chdate), strftime('%F %T', $expires)));
  196. }
  197. /**
  198. * Remove cached resources from the database table.
  199. */
  200. private function removeCacheEntries(array $ids)
  201. {
  202. $db = DBManager::get();
  203. $stmt = $db->prepare("DELETE FROM media_cache WHERE id IN (?)");
  204. $stmt->execute(array($ids ?: ''));
  205. foreach ($ids as $id) {
  206. @unlink($this->getCacheFile($id));
  207. }
  208. }
  209. }