]> git.mxchange.org Git - friendica.git/blob - src/Module/Proxy.php
57ff59256460d4cf73cd73ee6ff73d018e582e7e
[friendica.git] / src / Module / Proxy.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2021, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Module;
23
24 use Friendica\BaseModule;
25 use Friendica\Core\Logger;
26 use Friendica\DI;
27 use Friendica\Model\Photo;
28 use Friendica\Object\Image;
29 use Friendica\Util\HTTPSignature;
30 use Friendica\Util\Proxy as ProxyUtils;
31
32 /**
33  * Module Proxy
34  *
35  * urls:
36  * /proxy/[sub1/[sub2/]]<base64url image url>[.ext][:size]
37  * /proxy?url=<image url>
38  */
39 class Proxy extends BaseModule
40 {
41
42         /**
43          * Initializer method for this class.
44          *
45          * Sets application instance and checks if /proxy/ path is writable.
46          *
47          */
48         public static function rawContent(array $parameters = [])
49         {
50                 if (!local_user()) {
51                         throw new \Friendica\Network\HTTPException\ForbiddenException(DI::l10n()->t('Access denied.'));
52                 }
53
54                 // Set application instance here
55                 $a = DI::app();
56
57                 /*
58                  * Pictures are stored in one of the following ways:
59                  *
60                  * 1. If a folder "proxy" exists and is writeable, then use this for caching
61                  * 2. If a cache path is defined, use this
62                  * 3. If everything else failed, cache into the database
63                  *
64                  * Question: Do we really need these three methods?
65                  */
66                 if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
67                         header('HTTP/1.1 304 Not Modified');
68                         header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
69                         header('Etag: ' . $_SERVER['HTTP_IF_NONE_MATCH']);
70                         header('Expires: ' . gmdate('D, d M Y H:i:s', time() + (31536000)) . ' GMT');
71                         header('Cache-Control: max-age=31536000');
72
73                         if (function_exists('header_remove')) {
74                                 header_remove('Last-Modified');
75                                 header_remove('Expires');
76                                 header_remove('Cache-Control');
77                         }
78
79                         /// @TODO Stop here?
80                         exit();
81                 }
82
83                 if (function_exists('header_remove')) {
84                         header_remove('Pragma');
85                         header_remove('pragma');
86                 }
87
88                 $direct_cache = self::setupDirectCache();
89
90                 $request = self::getRequestInfo();
91
92                 if (empty($request['url'])) {
93                         throw new \Friendica\Network\HTTPException\BadRequestException();
94                 }
95
96                 // Webserver already tried direct cache...
97
98                 // Try to use filecache;
99                 $cachefile = self::responseFromCache($request);
100
101                 // Try to use photo from db
102                 self::responseFromDB($request);
103
104                 //
105                 // If script is here, the requested url has never cached before.
106                 // Let's fetch it, scale it if required, then save it in cache.
107                 //
108
109                 // It shouldn't happen but it does - spaces in URL
110                 $request['url'] = str_replace(' ', '+', $request['url']);
111                 $fetchResult = HTTPSignature::fetchRaw($request['url'], local_user(), ['timeout' => 10]);
112                 $img_str = $fetchResult->getBody();
113
114                 // If there is an error then return a blank image
115                 if ((substr($fetchResult->getReturnCode(), 0, 1) == '4') || empty($img_str)) {
116                         Logger::info('Error fetching image', ['image' => $request['url'], 'return' => $fetchResult->getReturnCode(), 'empty' => empty($img_str)]);
117                         self::responseError();
118                         // stop.
119                 }
120
121                 $tempfile = tempnam(get_temppath(), 'cache');
122                 file_put_contents($tempfile, $img_str);
123                 $mime = mime_content_type($tempfile);
124                 unlink($tempfile);
125
126                 $image = new Image($img_str, $mime);
127                 if (!$image->isValid()) {
128                         Logger::info('The image is invalid', ['image' => $request['url'], 'mime' => $mime]);
129                         self::responseError();
130                         // stop.
131                 }
132
133                 $basepath = $a->getBasePath();
134                 $filepermission = DI::config()->get('system', 'proxy_file_chmod');
135
136                 // Store original image
137                 if ($direct_cache) {
138                         // direct cache , store under ./proxy/
139                         $filename = $basepath . '/proxy/' . ProxyUtils::proxifyUrl($request['url'], true);
140                         file_put_contents($filename, $image->asString());
141                         if (!empty($filepermission)) {
142                                 chmod($filename, $filepermission);
143                         }
144                 } elseif($cachefile !== '') {
145                         // cache file
146                         file_put_contents($cachefile, $image->asString());
147                 } else {
148                         // database
149                         Photo::store($image, 0, 0, $request['urlhash'], $request['url'], '', 100);
150                 }
151
152
153                 // reduce quality - if it isn't a GIF
154                 if ($image->getType() != 'image/gif') {
155                         $image->scaleDown($request['size']);
156                 }
157
158
159                 // Store scaled image
160                 if ($direct_cache && $request['sizetype'] != '') {
161                         $filename = $basepath . '/proxy/' . ProxyUtils::proxifyUrl($request['url'], true) . $request['sizetype'];
162                         file_put_contents($filename, $image->asString());
163                         if (!empty($filepermission)) {
164                                 chmod($filename, $filepermission);
165                         }
166                 }
167
168                 self::responseImageHttpCache($image);
169                 // stop.
170         }
171
172
173         /**
174          * Build info about requested image to be proxied
175          *
176          * @return array
177          *    [
178          *      'url' => requested url,
179          *      'urlhash' => sha1 has of the url prefixed with 'pic:',
180          *      'size' => requested image size (int)
181          *      'sizetype' => requested image size (string): ':micro', ':thumb', ':small', ':medium', ':large'
182          *    ]
183          * @throws \Exception
184          */
185         private static function getRequestInfo()
186         {
187                 $a = DI::app();
188                 $size = ProxyUtils::PIXEL_LARGE;
189                 $sizetype = '';
190
191                 // Look for filename in the arguments
192                 // @TODO: Replace with parameter from router
193                 if (($a->argc > 1) && !isset($_REQUEST['url'])) {
194                         if (isset($a->argv[3])) {
195                                 $url = $a->argv[3];
196                         } elseif (isset($a->argv[2])) {
197                                 $url = $a->argv[2];
198                         } else {
199                                 $url = $a->argv[1];
200                         }
201
202                         /// @TODO: Why? And what about $url in this case?
203                         /// @TODO: Replace with parameter from router
204                         if (isset($a->argv[3]) && ($a->argv[3] == 'thumb')) {
205                                 $size = 200;
206                         }
207
208                         // thumb, small, medium and large.
209                         if (substr($url, -6) == ':micro') {
210                                 $size = ProxyUtils::PIXEL_MICRO;
211                                 $sizetype = ':micro';
212                                 $url = substr($url, 0, -6);
213                         } elseif (substr($url, -6) == ':thumb') {
214                                 $size = ProxyUtils::PIXEL_THUMB;
215                                 $sizetype = ':thumb';
216                                 $url = substr($url, 0, -6);
217                         } elseif (substr($url, -6) == ':small') {
218                                 $size = ProxyUtils::PIXEL_SMALL;
219                                 $url = substr($url, 0, -6);
220                                 $sizetype = ':small';
221                         } elseif (substr($url, -7) == ':medium') {
222                                 $size = ProxyUtils::PIXEL_MEDIUM;
223                                 $url = substr($url, 0, -7);
224                                 $sizetype = ':medium';
225                         } elseif (substr($url, -6) == ':large') {
226                                 $size = ProxyUtils::PIXEL_LARGE;
227                                 $url = substr($url, 0, -6);
228                                 $sizetype = ':large';
229                         }
230
231                         $pos = strrpos($url, '=.');
232                         if ($pos) {
233                                 $url = substr($url, 0, $pos + 1);
234                         }
235
236                         $url = str_replace(['.jpg', '.jpeg', '.gif', '.png'], ['','','',''], $url);
237
238                         $url = base64_decode(strtr($url, '-_', '+/'), true);
239
240                 } else {
241                         $url = $_REQUEST['url'] ?? '';
242                 }
243
244                 return [
245                         'url' => $url,
246                         'urlhash' => 'pic:' . sha1($url),
247                         'size' => $size,
248                         'sizetype' => $sizetype,
249                 ];
250         }
251
252
253         /**
254          * setup ./proxy folder for direct cache
255          *
256          * @return bool  False if direct cache can't be used.
257          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
258          */
259         private static function setupDirectCache()
260         {
261                 $a = DI::app();
262                 $basepath = $a->getBasePath();
263
264                 // If the cache path isn't there, try to create it
265                 if (!is_dir($basepath . '/proxy') && is_writable($basepath)) {
266                         mkdir($basepath . '/proxy');
267                 }
268
269                 // Checking if caching into a folder in the webroot is activated and working
270                 $direct_cache = (is_dir($basepath . '/proxy') && is_writable($basepath . '/proxy'));
271                 // we don't use direct cache if image url is passed in args and not in querystring
272                 $direct_cache = $direct_cache && ($a->argc > 1) && !isset($_REQUEST['url']);
273
274                 return $direct_cache;
275         }
276
277
278         /**
279          * Try to reply with image in cachefile
280          *
281          * @param array $request Array from getRequestInfo
282          *
283          * @return string  Cache file name, empty string if cache is not enabled.
284          *
285          * If cachefile exists, script ends here and this function will never returns
286          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
287          * @throws \ImagickException
288          */
289         private static function responseFromCache(&$request)
290         {
291                 $cachefile = get_cachefile(hash('md5', $request['url']));
292                 if ($cachefile != '' && file_exists($cachefile)) {
293                         $img = new Image(file_get_contents($cachefile), mime_content_type($cachefile));
294                         self::responseImageHttpCache($img);
295                         // stop.
296                 }
297                 return $cachefile;
298         }
299
300         /**
301          * Try to reply with image in database
302          *
303          * @param array $request Array from getRequestInfo
304          *
305          * If the image exists in database, then script ends here and this function will never returns
306          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
307          * @throws \ImagickException
308          */
309         private static function responseFromDB(&$request)
310         {
311                 $photo = Photo::getPhoto($request['urlhash']);
312
313                 if ($photo !== false) {
314                         $img = Photo::getImageForPhoto($photo);
315                         self::responseImageHttpCache($img);
316                         // stop.
317                 }
318         }
319
320         /**
321          * In case of an error just stop. We don't return content to avoid caching problems
322          *
323          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
324          */
325         private static function responseError()
326         {
327                 throw new \Friendica\Network\HTTPException\InternalServerErrorException();
328         }
329
330         /**
331          * Output the image with cache headers
332          *
333          * @param Image $img
334          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
335          */
336         private static function responseImageHttpCache(Image $img)
337         {
338                 if (is_null($img) || !$img->isValid()) {
339                         Logger::info('The cached image is invalid');
340                         self::responseError();
341                         // stop.
342                 }
343                 header('Content-type: ' . $img->getType());
344                 header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
345                 header('Etag: "' . md5($img->asString()) . '"');
346                 header('Expires: ' . gmdate('D, d M Y H:i:s', time() + (31536000)) . ' GMT');
347                 header('Cache-Control: max-age=31536000');
348                 echo $img->asString();
349                 exit();
350         }
351 }