diff --git a/src/Module/Proxy.php b/src/Module/Proxy.php index 29e0f4f52f..b975069e7a 100644 --- a/src/Module/Proxy.php +++ b/src/Module/Proxy.php @@ -19,6 +19,10 @@ use Friendica\Util\Proxy as ProxyUtils; /** * @brief Module Proxy + * + * urls: + * /proxy/[sub1/[sub2/]][.ext][:size] + * /proxy?url= */ class Proxy extends BaseModule { @@ -66,21 +70,102 @@ class Proxy extends BaseModule header_remove('pragma'); } - $thumb = false; - $size = 1024; - $sizetype = ''; - $basepath = $a->getBasePath(); + $direct_cache = self::setupDirectCache(); - // If the cache path isn't there, try to create it - if (!is_dir($basepath . '/proxy') && is_writable($basepath)) { - mkdir($basepath . '/proxy'); + $request = self::getRequestInfo(); + + if (empty($request['url'])) { + System::httpExit(400, ['title' => L10n::t('Bad Request.')]); } - // Checking if caching into a folder in the webroot is activated and working - $direct_cache = (is_dir($basepath . '/proxy') && is_writable($basepath . '/proxy')); + // Webserver already tried direct cache... + // Try to use filecache; + $cachefile = self::responseFromCache($request); + + // Try to use photo from db + self::responseFromDB($request); + + + // + // If script is here, the requested url has never cached before. + // Let's fetch it, scale it if required, then save it in cache. + // + + + // It shouldn't happen but it does - spaces in URL + $request['url'] = str_replace(' ', '+', $request['url']); + $redirects = 0; + $fetchResult = Network::fetchUrlFull($request['url'], true, $redirects, 10); + $img_str = $fetchResult->getBody(); + + $tempfile = tempnam(get_temppath(), 'cache'); + file_put_contents($tempfile, $img_str); + $mime = mime_content_type($tempfile); + unlink($tempfile); + + // If there is an error then return a blank image + if ((substr($fetchResult->getReturnCode(), 0, 1) == '4') || (!$img_str)) { + self::responseError($request); + // stop. + } + + $image = new Image($img_str, $mime); + if (!$image->isValid()) { + self::responseError($request); + // stop. + } + + // Store original image + if ($direct_cache) { + // direct cache , store under ./proxy/ + file_put_contents($basepath . '/proxy/' . ProxyUtils::proxifyUrl($request['url'], true), $image->asString()); + } elseif($cachefile !== '') { + // cache file + file_put_contents($cachefile, $image->asString()); + } else { + // database + Photo::store($image, 0, 0, $request['urlhash'], $request['url'], '', 100); + } + + + // reduce quality - if it isn't a GIF + if ($image->getType() != 'image/gif') { + $image->scaleDown($request['size']); + } + + + // Store scaled image + if ($direct_cache && $request['sizetype'] != '') { + file_put_contents($basepath . '/proxy/' . ProxyUtils::proxifyUrl($request['url'], true) . $request['sizetype'], $image->asString()); + } + + self::responseImageHttpCache($image); + // stop. + } + + + /** + * @brief Build info about requested image to be proxied + * + * @return array + * [ + * 'url' => requested url, + * 'urlhash' => sha1 has of the url prefixed with 'pic:', + * 'size' => requested image size (int) + * 'sizetype' => requested image size (string): ':micro', ':thumb', ':small', ':medium', ':large' + * ] + */ + private static function getRequestInfo() + { + $a = self::getApp(); + $url = ''; + $size = 1024; + $sizetype = ''; + + // Look for filename in the arguments - if ((isset($a->argv[1]) || isset($a->argv[2]) || isset($a->argv[3])) && !isset($_REQUEST['url'])) { + if (($a->argc > 1) && !isset($_REQUEST['url'])) { if (isset($a->argv[3])) { $url = $a->argv[3]; } elseif (isset($a->argv[2])) { @@ -89,6 +174,7 @@ class Proxy extends BaseModule $url = $a->argv[1]; } + /// @TODO: Why? And what about $url in this case? if (isset($a->argv[3]) && ($a->argv[3] == 'thumb')) { $size = 200; } @@ -125,148 +211,108 @@ class Proxy extends BaseModule $url = base64_decode(strtr($url, '-_', '+/'), true); - if ($url) { - $_REQUEST['url'] = $url; - } } else { - $direct_cache = false; + $url = defaults($_REQUEST, 'url', ''); + } + + return [ + 'url' => $url, + 'urlhash' => 'pic:' . sha1($url), + 'size' => $size, + 'sizetype' => $sizetype, + ]; + } + + + /** + * @brief setup ./proxy folder for direct cache + * + * @return bool False if direct cache can't be used. + */ + private static function setupDirectCache() + { + $a = self::getApp(); + $basepath = $a->getBasePath(); + + // If the cache path isn't there, try to create it + if (!is_dir($basepath . '/proxy') && is_writable($basepath)) { + mkdir($basepath . '/proxy'); } - if (empty($_REQUEST['url'])) { - System::httpExit(400, ["title" => L10n::t('Bad Request.')]); + // Checking if caching into a folder in the webroot is activated and working + $direct_cache = (is_dir($basepath . '/proxy') && is_writable($basepath . '/proxy')); + // we don't use direct cache if image url is passed in args and not in querystring + $direct_cache = $direct_cache && ($a->argc > 1) && !isset($_REQUEST['url']); + + return $direct_cache; + } + + + /** + * @brief Try to reply with image in cachefile + * + * @param array $request Array from getRequestInfo + * + * @return string Cache file name, empty string if cache is not enabled. + * + * If cachefile exists, script ends here and this function will never returns + */ + private static function responseFromCache(&$request) + { + $cachefile = get_cachefile(hash('md5', $request['url'])); + if ($cachefile != '' && file_exists($cachefile)) { + $img = new Image(file_get_contents($cachefile), mime_content_type($cachefile)); + self::responseImageHttpCache($img); + // stop. } + return $cachefile; + } + + /** + * @brief Try to reply with image in database + * + * @param array $request Array from getRequestInfo + * + * If the image exists in database, then script ends here and this function will never returns + */ + private static function responseFromDB(&$request) { + + $photo = Photo::getPhoto($request['urlhash']); - if (!$direct_cache) { - $urlhash = 'pic:' . sha1($_REQUEST['url']); - - $cachefile = get_cachefile(hash('md5', $_REQUEST['url'])); - if ($cachefile != '' && file_exists($cachefile)) { - $img_str = file_get_contents($cachefile); - $mime = mime_content_type($cachefile); - - header('Content-type: ' . $mime); - header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT'); - header('Etag: "' . md5($img_str) . '"'); - header('Expires: ' . gmdate('D, d M Y H:i:s', time() + (31536000)) . ' GMT'); - header('Cache-Control: max-age=31536000'); - - // reduce quality - if it isn't a GIF - if ($mime != 'image/gif') { - $image = new Image($img_str, $mime); - - if ($image->isValid()) { - $img_str = $image->asString(); - } - } - - echo $img_str; - exit(); - } - } else { - $cachefile = ''; + if ($photo !== false) { + $img = Photo::getImageForPhoto($photo); + self::responseImageHttpCache($img); + // stop. } - - $valid = true; - $photo = null; - - if (!$direct_cache && ($cachefile == '')) { - $photo = DBA::selectFirst('photo', ['data', 'desc'], ['resource-id' => $urlhash]); - - if (DBA::isResult($photo)) { - $img_str = $photo['data']; - $mime = $photo['desc']; - - if ($mime == '') { - $mime = 'image/jpeg'; - } - } - } - - if (!DBA::isResult($photo)) { - // It shouldn't happen but it does - spaces in URL - $_REQUEST['url'] = str_replace(' ', '+', $_REQUEST['url']); - $redirects = 0; - $fetchResult = Network::fetchUrlFull($_REQUEST['url'], true, $redirects, 10); - $img_str = $fetchResult->getBody(); - - $tempfile = tempnam(get_temppath(), 'cache'); - file_put_contents($tempfile, $img_str); - $mime = mime_content_type($tempfile); - unlink($tempfile); - - // If there is an error then return a blank image - if ((substr($fetchResult->getReturnCode(), 0, 1) == '4') || (!$img_str)) { - $img_str = file_get_contents('images/blank.png'); - $mime = 'image/png'; - $cachefile = ''; // Clear the cachefile so that the dummy isn't stored - $valid = false; - $image = new Image($img_str, 'image/png'); - - if ($image->isValid()) { - $image->scaleDown(10); - $img_str = $image->asString(); - } - } elseif ($mime != 'image/jpeg' && !$direct_cache && $cachefile == '') { - $image = @imagecreatefromstring($img_str); - - if ($image === FALSE) { - die(); - } - - $fields = ['uid' => 0, 'contact-id' => 0, 'guid' => System::createGUID(), 'resource-id' => $urlhash, 'created' => DateTimeFormat::utcNow(), 'edited' => DateTimeFormat::utcNow(), - 'filename' => basename($_REQUEST['url']), 'type' => '', 'album' => '', 'height' => imagesy($image), 'width' => imagesx($image), - 'datasize' => 0, 'data' => $img_str, 'scale' => 100, 'profile' => 0, - 'allow_cid' => '', 'allow_gid' => '', 'deny_cid' => '', 'deny_gid' => '', 'desc' => $mime]; - DBA::insert('photo', $fields); - } else { - $image = new Image($img_str, $mime); - - if ($image->isValid() && !$direct_cache && ($cachefile == '')) { - Photo::store($image, 0, 0, $urlhash, $_REQUEST['url'], '', 100); - } - } - } - - $img_str_orig = $img_str; - - // reduce quality - if it isn't a GIF - if ($mime != 'image/gif') { - $image = new Image($img_str, $mime); - - if ($image->isValid()) { - $image->scaleDown($size); - $img_str = $image->asString(); - } - } - - /* - * If there is a real existing directory then put the cache file there - * advantage: real file access is really fast - * Otherwise write in cachefile - */ - if ($valid && $direct_cache) { - file_put_contents($basepath . '/proxy/' . ProxyUtils::proxifyUrl($_REQUEST['url'], true), $img_str_orig); - - if ($sizetype != '') { - file_put_contents($basepath . '/proxy/' . ProxyUtils::proxifyUrl($_REQUEST['url'], true) . $sizetype, $img_str); - } - } elseif ($cachefile != '') { - file_put_contents($cachefile, $img_str_orig); - } - - header('Content-type: ' . $mime); - - // Only output the cache headers when the file is valid - if ($valid) { - header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT'); - header('Etag: "' . md5($img_str) . '"'); - header('Expires: ' . gmdate('D, d M Y H:i:s', time() + (31536000)) . ' GMT'); - header('Cache-Control: max-age=31536000'); - } - - echo $img_str; - + } + + /** + * @brief Output a blank image, without cache headers, in case of errors + * + */ + private static function responseError() { + header('Content-type: ' . $img->getType()); + echo file_get_contents('images/blank.png'); + exit(); + } + + /** + * @brief Output the image with cache headers + * + * @param Image $image + */ + private static function responseImageHttpCache(Image $img) + { + if (is_null($img) || !$img->isValid()) { + self::responseError(); + // stop. + } + header('Content-type: ' . $img->getType()); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT'); + header('Etag: "' . md5($img->asString()) . '"'); + header('Expires: ' . gmdate('D, d M Y H:i:s', time() + (31536000)) . ' GMT'); + header('Cache-Control: max-age=31536000'); + echo $img->asString(); exit(); } - }