Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

377 lines
11 KiB

  1. <?php
  2. // Based upon "Privacy Image Cache" by Tobias Hößl <https://github.com/CatoTH/>
  3. define('PROXY_DEFAULT_TIME', 86400); // 1 Day
  4. define('PROXY_SIZE_MICRO', 'micro');
  5. define('PROXY_SIZE_THUMB', 'thumb');
  6. define('PROXY_SIZE_SMALL', 'small');
  7. define('PROXY_SIZE_MEDIUM', 'medium');
  8. define('PROXY_SIZE_LARGE', 'large');
  9. require_once 'include/security.php';
  10. require_once 'include/Photo.php';
  11. function proxy_init(App $a) {
  12. // Pictures are stored in one of the following ways:
  13. // 1. If a folder "proxy" exists and is writeable, then use this for caching
  14. // 2. If a cache path is defined, use this
  15. // 3. If everything else failed, cache into the database
  16. //
  17. // Question: Do we really need these three methods?
  18. if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
  19. header('HTTP/1.1 304 Not Modified');
  20. header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
  21. header('Etag: ' . $_SERVER['HTTP_IF_NONE_MATCH']);
  22. header('Expires: ' . gmdate('D, d M Y H:i:s', time() + (31536000)) . ' GMT');
  23. header('Cache-Control: max-age=31536000');
  24. if (function_exists('header_remove')) {
  25. header_remove('Last-Modified');
  26. header_remove('Expires');
  27. header_remove('Cache-Control');
  28. }
  29. exit;
  30. }
  31. if (function_exists('header_remove')) {
  32. header_remove('Pragma');
  33. header_remove('pragma');
  34. }
  35. $thumb = false;
  36. $size = 1024;
  37. $sizetype = '';
  38. $basepath = $a->get_basepath();
  39. // If the cache path isn't there, try to create it
  40. if (!is_dir($basepath . '/proxy') AND is_writable($basepath)) {
  41. mkdir($basepath . '/proxy');
  42. }
  43. // Checking if caching into a folder in the webroot is activated and working
  44. $direct_cache = (is_dir($basepath . '/proxy') AND is_writable($basepath . '/proxy'));
  45. // Look for filename in the arguments
  46. if ((isset($a->argv[1]) OR isset($a->argv[2]) OR isset($a->argv[3])) AND !isset($_REQUEST['url'])) {
  47. if (isset($a->argv[3])) {
  48. $url = $a->argv[3];
  49. } elseif (isset($a->argv[2])) {
  50. $url = $a->argv[2];
  51. } else {
  52. $url = $a->argv[1];
  53. }
  54. if (isset($a->argv[3]) AND ($a->argv[3] == 'thumb')) {
  55. $size = 200;
  56. }
  57. // thumb, small, medium and large.
  58. if (substr($url, -6) == ':micro') {
  59. $size = 48;
  60. $sizetype = ':micro';
  61. $url = substr($url, 0, -6);
  62. } elseif (substr($url, -6) == ':thumb') {
  63. $size = 80;
  64. $sizetype = ':thumb';
  65. $url = substr($url, 0, -6);
  66. } elseif (substr($url, -6) == ':small') {
  67. $size = 175;
  68. $url = substr($url, 0, -6);
  69. $sizetype = ':small';
  70. } elseif (substr($url, -7) == ':medium') {
  71. $size = 600;
  72. $url = substr($url, 0, -7);
  73. $sizetype = ':medium';
  74. } elseif (substr($url, -6) == ':large') {
  75. $size = 1024;
  76. $url = substr($url, 0, -6);
  77. $sizetype = ':large';
  78. }
  79. $pos = strrpos($url, '=.');
  80. if ($pos) {
  81. $url = substr($url, 0, $pos + 1);
  82. }
  83. $url = str_replace(array('.jpg', '.jpeg', '.gif', '.png'), array('','','',''), $url);
  84. $url = base64_decode(strtr($url, '-_', '+/'), true);
  85. if ($url) {
  86. $_REQUEST['url'] = $url;
  87. }
  88. } else {
  89. $direct_cache = false;
  90. }
  91. if (!$direct_cache) {
  92. $urlhash = 'pic:' . sha1($_REQUEST['url']);
  93. $cachefile = get_cachefile(hash('md5', $_REQUEST['url']));
  94. if ($cachefile != '' AND file_exists($cachefile)) {
  95. $img_str = file_get_contents($cachefile);
  96. $mime = image_type_to_mime_type(exif_imagetype($cachefile));
  97. header('Content-type: ' . $mime);
  98. header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
  99. header('Etag: "' . md5($img_str) . '"');
  100. header('Expires: ' . gmdate('D, d M Y H:i:s', time() + (31536000)) . ' GMT');
  101. header('Cache-Control: max-age=31536000');
  102. // reduce quality - if it isn't a GIF
  103. if ($mime != 'image/gif') {
  104. $img = new Photo($img_str, $mime);
  105. if ($img->is_valid()) {
  106. $img_str = $img->imageString();
  107. }
  108. }
  109. echo $img_str;
  110. killme();
  111. }
  112. } else {
  113. $cachefile = '';
  114. }
  115. $valid = true;
  116. $r = array();
  117. if (!$direct_cache AND ($cachefile == '')) {
  118. $r = qu("SELECT * FROM `photo` WHERE `resource-id` = '%s' LIMIT 1", $urlhash);
  119. if (dbm::is_result($r)) {
  120. $img_str = $r[0]['data'];
  121. $mime = $r[0]['desc'];
  122. if ($mime == '') {
  123. $mime = 'image/jpeg';
  124. }
  125. }
  126. }
  127. if (!dbm::is_result($r)) {
  128. // It shouldn't happen but it does - spaces in URL
  129. $_REQUEST['url'] = str_replace(' ', '+', $_REQUEST['url']);
  130. $redirects = 0;
  131. $img_str = fetch_url($_REQUEST['url'], true, $redirects, 10);
  132. $tempfile = tempnam(get_temppath(), 'cache');
  133. file_put_contents($tempfile, $img_str);
  134. $mime = image_type_to_mime_type(exif_imagetype($tempfile));
  135. unlink($tempfile);
  136. // If there is an error then return a blank image
  137. if ((substr($a->get_curl_code(), 0, 1) == '4') OR (!$img_str)) {
  138. $img_str = file_get_contents('images/blank.png');
  139. $mime = 'image/png';
  140. $cachefile = ''; // Clear the cachefile so that the dummy isn't stored
  141. $valid = false;
  142. $img = new Photo($img_str, 'image/png');
  143. if ($img->is_valid()) {
  144. $img->scaleImage(10);
  145. $img_str = $img->imageString();
  146. }
  147. } elseif ($mime != 'image/jpeg' AND !$direct_cache AND $cachefile == '') {
  148. $image = @imagecreatefromstring($img_str);
  149. if ($image === FALSE) {
  150. die();
  151. }
  152. q("INSERT INTO `photo`
  153. ( `uid`, `contact-id`, `guid`, `resource-id`, `created`, `edited`, `filename`, `album`, `height`, `width`, `desc`, `data`, `scale`, `profile`, `allow_cid`, `allow_gid`, `deny_cid`, `deny_gid` )
  154. VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', '%s', %d, %d, '%s', '%s', '%s', '%s' )",
  155. 0, 0, get_guid(), dbesc($urlhash),
  156. dbesc(datetime_convert()),
  157. dbesc(datetime_convert()),
  158. dbesc(basename(dbesc($_REQUEST['url']))),
  159. dbesc(''),
  160. intval(imagesy($image)),
  161. intval(imagesx($image)),
  162. $mime,
  163. dbesc($img_str),
  164. 100,
  165. intval(0),
  166. dbesc(''), dbesc(''), dbesc(''), dbesc('')
  167. );
  168. } else {
  169. $img = new Photo($img_str, $mime);
  170. if ($img->is_valid() AND !$direct_cache AND ($cachefile == '')) {
  171. $img->store(0, 0, $urlhash, $_REQUEST['url'], '', 100);
  172. }
  173. }
  174. }
  175. $img_str_orig = $img_str;
  176. // reduce quality - if it isn't a GIF
  177. if ($mime != 'image/gif') {
  178. $img = new Photo($img_str, $mime);
  179. if ($img->is_valid()) {
  180. $img->scaleImage($size);
  181. $img_str = $img->imageString();
  182. }
  183. }
  184. // If there is a real existing directory then put the cache file there
  185. // advantage: real file access is really fast
  186. // Otherwise write in cachefile
  187. if ($valid AND $direct_cache) {
  188. file_put_contents($basepath . '/proxy/' . proxy_url($_REQUEST['url'], true), $img_str_orig);
  189. if ($sizetype != '') {
  190. file_put_contents($basepath . '/proxy/' . proxy_url($_REQUEST['url'], true) . $sizetype, $img_str);
  191. }
  192. } elseif ($cachefile != '') {
  193. file_put_contents($cachefile, $img_str_orig);
  194. }
  195. header('Content-type: ' . $mime);
  196. // Only output the cache headers when the file is valid
  197. if ($valid) {
  198. header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
  199. header('Etag: "' . md5($img_str) . '"');
  200. header('Expires: ' . gmdate('D, d M Y H:i:s', time() + (31536000)) . ' GMT');
  201. header('Cache-Control: max-age=31536000');
  202. }
  203. echo $img_str;
  204. killme();
  205. }
  206. /**
  207. * @brief Transform a remote URL into a local one
  208. *
  209. * This function only performs the URL replacement on http URL and if the
  210. * provided URL isn't local, "the isn't deactivated" (sic) and if the config
  211. * system.proxy_disabled is set to false.
  212. *
  213. * @param string $url The URL to proxyfy
  214. * @param bool $writemode Returns a local path the remote URL should be saved to
  215. * @param string $size One of the PROXY_SIZE_* constants
  216. *
  217. * @return string The proxyfied URL or relative path
  218. */
  219. function proxy_url($url, $writemode = false, $size = '') {
  220. $a = get_app();
  221. if (substr($url, 0, strlen('http')) !== 'http') {
  222. return $url;
  223. }
  224. // Only continue if it isn't a local image and the isn't deactivated
  225. if (proxy_is_local_image($url)) {
  226. $url = str_replace(normalise_link(App::get_baseurl()) . '/', App::get_baseurl() . '/', $url);
  227. return $url;
  228. }
  229. if (get_config('system', 'proxy_disabled')) {
  230. return $url;
  231. }
  232. // Image URL may have encoded ampersands for display which aren't desirable for proxy
  233. $url = html_entity_decode($url, ENT_NOQUOTES, 'utf-8');
  234. // Creating a sub directory to reduce the amount of files in the cache directory
  235. $basepath = $a->get_basepath() . '/proxy';
  236. $shortpath = hash('md5', $url);
  237. $longpath = substr($shortpath, 0, 2);
  238. if (is_dir($basepath) AND $writemode AND !is_dir($basepath . '/' . $longpath)) {
  239. mkdir($basepath . '/' . $longpath);
  240. chmod($basepath . '/' . $longpath, 0777);
  241. }
  242. $longpath .= '/' . strtr(base64_encode($url), '+/', '-_');
  243. // Extract the URL extension
  244. $extension = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
  245. $extensions = array('jpg', 'jpeg', 'gif', 'png');
  246. if (in_array($extension, $extensions)) {
  247. $shortpath .= '.' . $extension;
  248. $longpath .= '.' . $extension;
  249. }
  250. $proxypath = App::get_baseurl() . '/proxy/' . $longpath;
  251. if ($size != '') {
  252. $size = ':' . $size;
  253. }
  254. // Too long files aren't supported by Apache
  255. // Writemode in combination with long files shouldn't be possible
  256. if ((strlen($proxypath) > 250) AND $writemode) {
  257. return $shortpath;
  258. } elseif (strlen($proxypath) > 250) {
  259. return App::get_baseurl() . '/proxy/' . $shortpath . '?url=' . urlencode($url);
  260. } elseif ($writemode) {
  261. return $longpath;
  262. } else {
  263. return $proxypath . $size;
  264. }
  265. }
  266. /**
  267. * @param $url string
  268. * @return boolean
  269. */
  270. function proxy_is_local_image($url) {
  271. if ($url[0] == '/') {
  272. return true;
  273. }
  274. if (strtolower(substr($url, 0, 5)) == 'data:') {
  275. return true;
  276. }
  277. // links normalised - bug #431
  278. $baseurl = normalise_link(App::get_baseurl());
  279. $url = normalise_link($url);
  280. return (substr($url, 0, strlen($baseurl)) == $baseurl);
  281. }
  282. /**
  283. * @brief Return the array of query string parameters from a URL
  284. *
  285. * @param string $url
  286. * @return array Associative array of query string parameters
  287. */
  288. function proxy_parse_query($url) {
  289. $query = parse_url($url, PHP_URL_QUERY);
  290. $query = html_entity_decode($query);
  291. $query_list = explode('&', $query);
  292. $arr = array();
  293. foreach ($query_list as $key_value) {
  294. $key_value_list = explode('=', $key_value);
  295. $arr[$key_value_list[0]] = $key_value_list[1];
  296. }
  297. unset($url, $query_list, $url);
  298. return $arr;
  299. }
  300. function proxy_img_cb($matches) {
  301. // if the picture seems to be from another picture cache then take the original source
  302. $queryvar = proxy_parse_query($matches[2]);
  303. if (($queryvar['url'] != '') AND (substr($queryvar['url'], 0, 4) == 'http')) {
  304. $matches[2] = urldecode($queryvar['url']);
  305. }
  306. // following line changed per bug #431
  307. if (proxy_is_local_image($matches[2])) {
  308. return $matches[1] . $matches[2] . $matches[3];
  309. }
  310. return $matches[1] . proxy_url(htmlspecialchars_decode($matches[2])) . $matches[3];
  311. }
  312. function proxy_parse_html($html) {
  313. $html = str_replace(normalise_link(App::get_baseurl()) . '/', App::get_baseurl() . '/', $html);
  314. return preg_replace_callback('/(<img [^>]*src *= *["\'])([^"\']+)(["\'][^>]*>)/siU', 'proxy_img_cb', $html);
  315. }