1
|
|
|
<?php declare(strict_types=1); |
2
|
|
|
/** |
3
|
|
|
* TimThumb by Ben Gillbanks and Mark Maunder |
4
|
|
|
* Based on work done by Tim McDaniels and Darren Hoyt |
5
|
|
|
* https://code.google.com/p/timthumb/ |
6
|
|
|
* |
7
|
|
|
* GNU General Public License, version 2 |
8
|
|
|
* https://www.gnu.org/licenses/old-licenses/gpl-2.0.html |
9
|
|
|
* |
10
|
|
|
* Examples and documentation available on the project homepage |
11
|
|
|
* https://www.binarymoon.co.uk/projects/timthumb/ |
12
|
|
|
* |
13
|
|
|
* $Rev$ |
14
|
|
|
*/ |
15
|
|
|
|
16
|
|
|
/* |
17
|
|
|
* --- TimThumb CONFIGURATION --- |
18
|
|
|
* To edit the configs it is best to create a file called timthumb-config.php |
19
|
|
|
* and define variables you want to customize in there. It will automatically be |
20
|
|
|
* loaded by timthumb. This will save you having to re-edit these variables |
21
|
|
|
* everytime you download a new version |
22
|
|
|
*/ |
23
|
|
|
|
24
|
|
|
use Xmf\Request; |
|
|
|
|
25
|
|
|
|
26
|
|
|
require_once __DIR__ . '/header.php'; |
27
|
|
|
|
28
|
|
|
define('VERSION', '2.8.14'); // Version of this script |
29
|
|
|
//Load a config file if it exists. Otherwise, use the values below |
30
|
|
|
if (file_exists(__DIR__ . '/timthumb-config.php')) { |
31
|
|
|
require_once __DIR__ . '/timthumb-config.php'; |
32
|
|
|
} |
33
|
|
|
if (!defined('DEBUG_ON')) { |
34
|
|
|
define('DEBUG_ON', false); |
35
|
|
|
} // Enable debug logging to web server error log (STDERR) |
36
|
|
|
if (!defined('DEBUG_LEVEL')) { |
37
|
|
|
define('DEBUG_LEVEL', 1); |
38
|
|
|
} // Debug level 1 is less noisy and 3 is the most noisy |
39
|
|
|
if (!defined('MEMORY_LIMIT')) { |
40
|
|
|
define('MEMORY_LIMIT', '30M'); |
41
|
|
|
} // Set PHP memory limit |
42
|
|
|
if (!defined('BLOCK_EXTERNAL_LEECHERS')) { |
43
|
|
|
define('BLOCK_EXTERNAL_LEECHERS', false); |
44
|
|
|
} // If the image or webshot is being loaded on an external site, display a red "No Hotlinking" gif. |
45
|
|
|
if (!defined('DISPLAY_ERROR_MESSAGES')) { |
46
|
|
|
define('DISPLAY_ERROR_MESSAGES', true); |
47
|
|
|
} // Display error messages. Set to false to turn off errors (good for production websites) |
48
|
|
|
//Image fetching and caching |
49
|
|
|
if (!defined('ALLOW_EXTERNAL')) { |
50
|
|
|
define('ALLOW_EXTERNAL', true); |
51
|
|
|
} // Allow image fetching from external websites. Will check against ALLOWED_SITES if ALLOW_ALL_EXTERNAL_SITES is false |
52
|
|
|
if (!defined('ALLOW_ALL_EXTERNAL_SITES')) { |
53
|
|
|
define('ALLOW_ALL_EXTERNAL_SITES', false); |
54
|
|
|
} // Less secure. |
55
|
|
|
if (!defined('FILE_CACHE_ENABLED')) { |
56
|
|
|
define('FILE_CACHE_ENABLED', true); |
57
|
|
|
} // Should we store resized/modified images on disk to speed things up? |
58
|
|
|
if (!defined('FILE_CACHE_TIME_BETWEEN_CLEANS')) { |
59
|
|
|
define('FILE_CACHE_TIME_BETWEEN_CLEANS', 86400); |
60
|
|
|
} // How often the cache is cleaned |
61
|
|
|
|
62
|
|
|
if (!defined('FILE_CACHE_MAX_FILE_AGE')) { |
63
|
|
|
define('FILE_CACHE_MAX_FILE_AGE', 86400); |
64
|
|
|
} // How old does a file have to be to be deleted from the cache |
65
|
|
|
if (!defined('FILE_CACHE_SUFFIX')) { |
66
|
|
|
define('FILE_CACHE_SUFFIX', '.timthumb.txt'); |
67
|
|
|
} // What to put at the end of all files in the cache directory so we can identify them |
68
|
|
|
if (!defined('FILE_CACHE_PREFIX')) { |
69
|
|
|
define('FILE_CACHE_PREFIX', 'timthumb'); |
70
|
|
|
} // What to put at the beg of all files in the cache directory so we can identify them |
71
|
|
|
if (!defined('FILE_CACHE_DIRECTORY')) { |
72
|
|
|
define('FILE_CACHE_DIRECTORY', '../../cache'); |
73
|
|
|
} // Directory where images are cached. Left blank it will use the system temporary directory (which is better for security) |
74
|
|
|
if (!defined('MAX_FILE_SIZE')) { |
75
|
|
|
define('MAX_FILE_SIZE', 10485760); |
76
|
|
|
} // 10 Megs is 10485760. This is the max internal or external file size that we'll process. |
77
|
|
|
if (!defined('CURL_TIMEOUT')) { |
78
|
|
|
define('CURL_TIMEOUT', 20); |
79
|
|
|
} // Timeout duration for Curl. This only applies if you have Curl installed and aren't using PHP's default URL fetching mechanism. |
80
|
|
|
if (!defined('WAIT_BETWEEN_FETCH_ERRORS')) { |
81
|
|
|
define('WAIT_BETWEEN_FETCH_ERRORS', 3600); |
82
|
|
|
} // Time to wait between errors fetching remote file |
83
|
|
|
|
84
|
|
|
//Browser caching |
85
|
|
|
if (!defined('BROWSER_CACHE_MAX_AGE')) { |
86
|
|
|
define('BROWSER_CACHE_MAX_AGE', 864000); |
87
|
|
|
} // Time to cache in the browser |
88
|
|
|
if (!defined('BROWSER_CACHE_DISABLE')) { |
89
|
|
|
define('BROWSER_CACHE_DISABLE', false); |
90
|
|
|
} // Use for testing if you want to disable all browser caching |
91
|
|
|
|
92
|
|
|
//Image size and defaults |
93
|
|
|
if (!defined('MAX_WIDTH')) { |
94
|
|
|
define('MAX_WIDTH', 1500); |
95
|
|
|
} // Maximum image width |
96
|
|
|
if (!defined('MAX_HEIGHT')) { |
97
|
|
|
define('MAX_HEIGHT', 1500); |
98
|
|
|
} // Maximum image height |
99
|
|
|
if (!defined('NOT_FOUND_IMAGE')) { |
100
|
|
|
define('NOT_FOUND_IMAGE', ''); |
101
|
|
|
} // Image to serve if any 404 occurs |
102
|
|
|
if (!defined('ERROR_IMAGE')) { |
103
|
|
|
define('ERROR_IMAGE', ''); |
104
|
|
|
} // Image to serve if an error occurs instead of showing error message |
105
|
|
|
if (!defined('PNG_IS_TRANSPARENT')) { |
106
|
|
|
define('PNG_IS_TRANSPARENT', false); |
107
|
|
|
} // Define if a png image should have a transparent background color. Use False value if you want to display a custom coloured canvas_colour |
108
|
|
|
if (!defined('DEFAULT_Q')) { |
109
|
|
|
define('DEFAULT_Q', 90); |
110
|
|
|
} // Default image quality. Allows overrid in timthumb-config.php |
111
|
|
|
if (!defined('DEFAULT_ZC')) { |
112
|
|
|
define('DEFAULT_ZC', 1); |
113
|
|
|
} // Default zoom/crop setting. Allows overrid in timthumb-config.php |
114
|
|
|
if (!defined('DEFAULT_F')) { |
115
|
|
|
define('DEFAULT_F', ''); |
116
|
|
|
} // Default image filters. Allows overrid in timthumb-config.php |
117
|
|
|
if (!defined('DEFAULT_S')) { |
118
|
|
|
define('DEFAULT_S', 0); |
119
|
|
|
} // Default sharpen value. Allows overrid in timthumb-config.php |
120
|
|
|
if (!defined('DEFAULT_CC')) { |
121
|
|
|
define('DEFAULT_CC', 'ffffff'); |
122
|
|
|
} // Default canvas colour. Allows overrid in timthumb-config.php |
123
|
|
|
if (!defined('DEFAULT_WIDTH')) { |
124
|
|
|
define('DEFAULT_WIDTH', 100); |
125
|
|
|
} // Default thumbnail width. Allows overrid in timthumb-config.php |
126
|
|
|
if (!defined('DEFAULT_HEIGHT')) { |
127
|
|
|
define('DEFAULT_HEIGHT', 100); |
128
|
|
|
} // Default thumbnail height. Allows overrid in timthumb-config.php |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* Additional Parameters: |
132
|
|
|
* LOCAL_FILE_BASE_DIRECTORY = Override the DOCUMENT_ROOT. This is best used in timthumb-config.php |
133
|
|
|
*/ |
134
|
|
|
|
135
|
|
|
//Image compression is enabled if either of these point to valid paths |
136
|
|
|
|
137
|
|
|
//These are now disabled by default because the file sizes of PNGs (and GIFs) are much smaller than we used to generate. |
138
|
|
|
//They only work for PNGs. GIFs and JPEGs are not affected. |
139
|
|
|
if (!defined('OPTIPNG_ENABLED')) { |
140
|
|
|
define('OPTIPNG_ENABLED', false); |
141
|
|
|
} |
142
|
|
|
if (!defined('OPTIPNG_PATH')) { |
143
|
|
|
define('OPTIPNG_PATH', '/usr/bin/optipng'); |
144
|
|
|
} //This will run first because it gives better compression than pngcrush. |
145
|
|
|
if (!defined('PNGCRUSH_ENABLED')) { |
146
|
|
|
define('PNGCRUSH_ENABLED', false); |
147
|
|
|
} |
148
|
|
|
if (!defined('PNGCRUSH_PATH')) { |
149
|
|
|
define('PNGCRUSH_PATH', '/usr/bin/pngcrush'); |
150
|
|
|
} //This will only run if OPTIPNG_PATH is not set or is not valid |
151
|
|
|
|
152
|
|
|
/* |
153
|
|
|
-------====Website Screenshots configuration - BETA====------- |
154
|
|
|
|
155
|
|
|
If you just want image thumbnails and don't want website screenshots, you can safely leave this as is. |
156
|
|
|
|
157
|
|
|
If you would like to get website screenshots set up, you will need root access to your own server. |
158
|
|
|
|
159
|
|
|
Enable ALLOW_ALL_EXTERNAL_SITES so you can fetch any external web page. This is more secure now that we're using a non-web folder for cache. |
160
|
|
|
Enable BLOCK_EXTERNAL_LEECHERS so that your site doesn't generate thumbnails for the whole Internet. |
161
|
|
|
|
162
|
|
|
Instructions to get website screenshots enabled on Ubuntu Linux: |
163
|
|
|
|
164
|
|
|
1. Install Xvfb with the following command: sudo apt-get install subversion libqt4-webkit libqt4-dev g++ xvfb |
165
|
|
|
2. Go to a directory where you can download some code |
166
|
|
|
3. Check-out the latest version of CutyCapt with the following command: svn co https://cutycapt.svn.sourceforge.net/svnroot/cutycapt |
167
|
|
|
4. Compile CutyCapt by doing: cd cutycapt/CutyCapt |
168
|
|
|
5. qmake |
169
|
|
|
6. make |
170
|
|
|
7. cp CutyCapt /usr/local/bin/ |
171
|
|
|
8. Test it by running: xvfb-run --server-args="-screen 0, 1024x768x24" CutyCapt --url="https://markmaunder.com/" --out=test.png |
172
|
|
|
9. If you get a file called test.png with something in it, it probably worked. Now test the script by accessing it as follows: |
173
|
|
|
10. https://yoursite.com/path/to/timthumb.php?src=https://markmaunder.com/&webshot=1 |
174
|
|
|
|
175
|
|
|
Notes on performance: |
176
|
|
|
The first time a webshot loads, it will take a few seconds. |
177
|
|
|
From then on it uses the regular timthumb caching mechanism with the configurable options above |
178
|
|
|
and loading will be very fast. |
179
|
|
|
|
180
|
|
|
--ADVANCED USERS ONLY-- |
181
|
|
|
If you'd like a slight speedup (about 25%) and you know Linux, you can run the following command which will keep Xvfb running in the background. |
182
|
|
|
nohup Xvfb :100 -ac -nolisten tcp -screen 0, 1024x768x24 > /dev/null 2>&1 & |
183
|
|
|
Then set WEBSHOT_XVFB_RUNNING = true below. This will save your server having to fire off a new Xvfb server and shut it down every time a new shot is generated. |
184
|
|
|
You will need to take responsibility for keeping Xvfb running in case it crashes. (It seems pretty stable) |
185
|
|
|
You will also need to take responsibility for server security if you're running Xvfb as root. |
186
|
|
|
|
187
|
|
|
|
188
|
|
|
*/ |
189
|
|
|
if (!defined('WEBSHOT_ENABLED')) { |
190
|
|
|
define('WEBSHOT_ENABLED', false); |
191
|
|
|
} //Beta feature. Adding webshot=1 to your query string will cause the script to return a browser screenshot rather than try to fetch an image. |
192
|
|
|
if (!defined('WEBSHOT_CUTYCAPT')) { |
193
|
|
|
define('WEBSHOT_CUTYCAPT', '/usr/local/bin/CutyCapt'); |
194
|
|
|
} //The path to CutyCapt. |
195
|
|
|
if (!defined('WEBSHOT_XVFB')) { |
196
|
|
|
define('WEBSHOT_XVFB', '/usr/bin/xvfb-run'); |
197
|
|
|
} //The path to the Xvfb server |
198
|
|
|
if (!defined('WEBSHOT_SCREEN_X')) { |
199
|
|
|
define('WEBSHOT_SCREEN_X', '1024'); |
200
|
|
|
} //1024 works ok |
201
|
|
|
if (!defined('WEBSHOT_SCREEN_Y')) { |
202
|
|
|
define('WEBSHOT_SCREEN_Y', '768'); |
203
|
|
|
} //768 works ok |
204
|
|
|
if (!defined('WEBSHOT_COLOR_DEPTH')) { |
205
|
|
|
define('WEBSHOT_COLOR_DEPTH', '24'); |
206
|
|
|
} //I haven't tested anything besides 24 |
207
|
|
|
if (!defined('WEBSHOT_IMAGE_FORMAT')) { |
208
|
|
|
define('WEBSHOT_IMAGE_FORMAT', 'png'); |
209
|
|
|
} //png is about 2.5 times the size of jpg but is a LOT better quality |
210
|
|
|
if (!defined('WEBSHOT_TIMEOUT')) { |
211
|
|
|
define('WEBSHOT_TIMEOUT', '20'); |
212
|
|
|
} //Seconds to wait for a webshot |
213
|
|
|
if (!defined('WEBSHOT_USER_AGENT')) { |
214
|
|
|
define('WEBSHOT_USER_AGENT', 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.2.18) Gecko/20110614 Firefox/3.6.18'); |
215
|
|
|
} //I hate to do this, but a non-browser robot user agent might not show what humans see. So we pretend to be Firefox |
216
|
|
|
if (!defined('WEBSHOT_JAVASCRIPT_ON')) { |
217
|
|
|
define('WEBSHOT_JAVASCRIPT_ON', true); |
218
|
|
|
} //Setting to false might give you a slight speedup and block ads. But it could cause other issues. |
219
|
|
|
if (!defined('WEBSHOT_JAVA_ON')) { |
220
|
|
|
define('WEBSHOT_JAVA_ON', false); |
221
|
|
|
} //Have only tested this as fase |
222
|
|
|
if (!defined('WEBSHOT_PLUGINS_ON')) { |
223
|
|
|
define('WEBSHOT_PLUGINS_ON', true); |
224
|
|
|
} //Enable flash and other plugins |
225
|
|
|
if (!defined('WEBSHOT_PROXY')) { |
226
|
|
|
define('WEBSHOT_PROXY', ''); |
227
|
|
|
} //In case you're behind a proxy server. |
228
|
|
|
if (!defined('WEBSHOT_XVFB_RUNNING')) { |
229
|
|
|
define('WEBSHOT_XVFB_RUNNING', false); |
230
|
|
|
} //ADVANCED: Enable this if you've got Xvfb running in the background. |
231
|
|
|
|
232
|
|
|
// If ALLOW_EXTERNAL is true and ALLOW_ALL_EXTERNAL_SITES is false, then external images will only be fetched from these domains and their subdomains. |
233
|
|
|
if (!isset($allowedSites)) { |
234
|
|
|
$allowedSites = [ |
235
|
|
|
'flickr.com', |
236
|
|
|
'staticflickr.com', |
237
|
|
|
'picasa.com', |
238
|
|
|
'img.youtube.com', |
239
|
|
|
'upload.wikimedia.org', |
240
|
|
|
'photobucket.com', |
241
|
|
|
'imgur.com', |
242
|
|
|
'imageshack.us', |
243
|
|
|
'tinypic.com', |
244
|
|
|
]; |
245
|
|
|
} |
246
|
|
|
// ------------------------------------------------------------- |
247
|
|
|
// -------------- STOP EDITING CONFIGURATION HERE -------------- |
248
|
|
|
// ------------------------------------------------------------- |
249
|
|
|
|
250
|
|
|
Timthumb::start(); |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Class timthumb |
254
|
|
|
*/ |
255
|
|
|
class Timthumb |
256
|
|
|
{ |
257
|
|
|
protected $src = ''; |
258
|
|
|
protected $is404 = false; |
259
|
|
|
protected $docRoot = ''; |
260
|
|
|
protected $lastURLError = false; |
261
|
|
|
protected $localImage = ''; |
262
|
|
|
protected $localImageMTime = 0.0; |
263
|
|
|
protected $url = false; |
264
|
|
|
protected $myHost = ''; |
265
|
|
|
protected $isURL = false; |
266
|
|
|
protected $cachefile = ''; |
267
|
|
|
protected $errors = []; |
268
|
|
|
protected $toDeletes = []; |
269
|
|
|
protected $cacheDirectory = ''; |
270
|
|
|
protected $startTime = 0.0; |
271
|
|
|
protected $lastBenchTime = 0.0; |
272
|
|
|
protected $cropTop = false; |
273
|
|
|
protected $salt = ''; |
274
|
|
|
protected $fileCacheVersion = 1; //Generally if timthumb.php is modifed (upgraded) then the salt changes and all cache files are recreated. This is a backup mechanism to force regen. |
275
|
|
|
protected $filePrependSecurityBlock = "<?php exit('Execution denied!'); //"; //Designed to have three letter mime type, space, question mark and greater than symbol appended. 6 bytes total. |
276
|
|
|
protected static $curlDataWritten = 0; |
277
|
|
|
protected static $curlFH = false; |
278
|
|
|
|
279
|
|
|
public static function start() |
280
|
|
|
{ |
281
|
|
|
$tim = new self(); |
282
|
|
|
$tim->handleErrors(); |
283
|
|
|
$tim->securityChecks(); |
284
|
|
|
if ($tim->tryBrowserCache()) { |
285
|
|
|
exit(0); |
|
|
|
|
286
|
|
|
} |
287
|
|
|
$tim->handleErrors(); |
288
|
|
|
if (FILE_CACHE_ENABLED && $tim->tryServerCache()) { |
289
|
|
|
exit(0); |
|
|
|
|
290
|
|
|
} |
291
|
|
|
$tim->handleErrors(); |
292
|
|
|
$tim->run(); |
293
|
|
|
$tim->handleErrors(); |
294
|
|
|
exit(0); |
|
|
|
|
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
public function __construct() |
298
|
|
|
{ |
299
|
|
|
global $allowedSites; |
300
|
|
|
$this->startTime = microtime(true); |
301
|
|
|
date_default_timezone_set('UTC'); |
302
|
|
|
$this->debug(1, 'Starting new request from ' . $this->getIP() . ' to ' . Request::getString('REQUEST_URI', '', 'SERVER')); |
303
|
|
|
$this->calcDocRoot(); |
304
|
|
|
//On windows systems I'm assuming fileinode returns an empty string or a number that doesn't change. Check this. |
305
|
|
|
$this->salt = @filemtime(__FILE__) . '-' . @fileinode(__FILE__); |
|
|
|
|
306
|
|
|
$this->debug(3, 'Salt is: ' . $this->salt); |
307
|
|
|
if (FILE_CACHE_DIRECTORY) { |
308
|
|
|
if (!is_dir(FILE_CACHE_DIRECTORY)) { |
309
|
|
|
if (!mkdir($concurrentDirectory = FILE_CACHE_DIRECTORY) && !is_dir($concurrentDirectory)) { |
310
|
|
|
throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory)); |
311
|
|
|
} |
312
|
|
|
if (!is_dir(FILE_CACHE_DIRECTORY)) { |
313
|
|
|
$this->error('Could not create the file cache directory.'); |
314
|
|
|
|
315
|
|
|
return false; |
316
|
|
|
} |
317
|
|
|
} |
318
|
|
|
$this->cacheDirectory = FILE_CACHE_DIRECTORY; |
319
|
|
|
if (!touch($this->cacheDirectory . '/index.html')) { |
320
|
|
|
$this->error('Could not create the index.html file - to fix this create an empty file named index.html file in the cache directory.'); |
321
|
|
|
} |
322
|
|
|
} else { |
323
|
|
|
$this->cacheDirectory = sys_get_temp_dir(); |
324
|
|
|
} |
325
|
|
|
//Clean the cache before we do anything because we don't want the first visitor after FILE_CACHE_TIME_BETWEEN_CLEANS expires to get a stale image. |
326
|
|
|
$this->cleanCache(); |
327
|
|
|
|
328
|
|
|
$this->myHost = preg_replace('/^www\./i', '', \Xmf\Request::getString('HTTP_HOST', '', 'SERVER')); |
329
|
|
|
$this->src = $this->param('src'); |
330
|
|
|
$this->url = parse_url($this->src); |
331
|
|
|
$this->src = preg_replace('/https?:\/\/(?:www\.)?' . $this->myHost . '/i', '', $this->src); |
332
|
|
|
|
333
|
|
|
if (mb_strlen($this->src) <= 3) { |
334
|
|
|
$this->error('No image specified'); |
335
|
|
|
|
336
|
|
|
return false; |
337
|
|
|
} |
338
|
|
|
if (BLOCK_EXTERNAL_LEECHERS && array_key_exists('HTTP_REFERER', $_SERVER) && (!preg_match('/^https?:\/\/(?:www\.)?' . $this->myHost . '(?:$|\/)/i', \Xmf\Request::getString('HTTP_REFERER', '', 'SERVER')))) { |
339
|
|
|
// base64 encoded red image that says 'no hotlinkers' |
340
|
|
|
// nothing to worry about! :) |
341
|
|
|
$imgData = base64_decode("R0lGODlhUAAMAIAAAP8AAP///yH5BAAHAP8ALAAAAABQAAwAAAJpjI+py+0Po5y0OgAMjjv01YUZ\nOGplhWXfNa6JCLnWkXplrcBmW+spbwvaVr/cDyg7IoFC2KbYVC2NQ5MQ4ZNao9Ynzjl9ScNYpneb\nDULB3RP6JuPuaGfuuV4fumf8PuvqFyhYtjdoeFgAADs=", true); |
342
|
|
|
header('Content-Type: image/gif'); |
343
|
|
|
header('Content-Length: ' . mb_strlen($imgData)); |
344
|
|
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); |
345
|
|
|
header('Pragma: no-cache'); |
346
|
|
|
header('Expires: ' . gmdate('D, d M Y H:i:s', time())); |
347
|
|
|
echo $imgData; |
348
|
|
|
|
349
|
|
|
return false; |
350
|
|
|
} |
351
|
|
|
if (preg_match('/^https?:\/\/[^\/]+/i', $this->src)) { |
352
|
|
|
$this->debug(2, 'Is a request for an external URL: ' . $this->src); |
353
|
|
|
$this->isURL = true; |
354
|
|
|
} else { |
355
|
|
|
$this->debug(2, 'Is a request for an internal file: ' . $this->src); |
356
|
|
|
} |
357
|
|
|
if ($this->isURL && (!ALLOW_EXTERNAL)) { |
358
|
|
|
$this->error('You are not allowed to fetch images from an external website.'); |
359
|
|
|
|
360
|
|
|
return false; |
361
|
|
|
} |
362
|
|
|
if ($this->isURL) { |
363
|
|
|
if (ALLOW_ALL_EXTERNAL_SITES) { |
364
|
|
|
$this->debug(2, 'Fetching from all external sites is enabled.'); |
365
|
|
|
} else { |
366
|
|
|
$this->debug(2, 'Fetching only from selected external sites is enabled.'); |
367
|
|
|
$allowed = false; |
368
|
|
|
foreach ($allowedSites as $site) { |
369
|
|
|
if ((mb_strtolower($this->url['host']) === \mb_strtolower($site)) || (mb_strtolower(mb_substr($this->url['host'], -mb_strlen($site) - 1)) === \mb_strtolower(".$site"))) { |
370
|
|
|
$this->debug(3, "URL hostname {$this->url['host']} matches $site so allowing."); |
371
|
|
|
$allowed = true; |
372
|
|
|
} |
373
|
|
|
} |
374
|
|
|
if (!$allowed) { |
375
|
|
|
return $this->error('You may not fetch images from that site. To enable this site in timthumb, you can either add it to $allowedSites and set ALLOW_EXTERNAL=true. Or you can set ALLOW_ALL_EXTERNAL_SITES=true, depending on your security needs.'); |
376
|
|
|
} |
377
|
|
|
} |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
$cachePrefix = ($this->isURL ? '_ext_' : '_int_'); |
381
|
|
|
if ($this->isURL) { |
382
|
|
|
$arr = explode('&', $_SERVER['QUERY_STRING']); |
383
|
|
|
asort($arr); |
384
|
|
|
$this->cachefile = $this->cacheDirectory . '/' . FILE_CACHE_PREFIX . $cachePrefix . md5($this->salt . implode('', $arr) . $this->fileCacheVersion) . FILE_CACHE_SUFFIX; |
385
|
|
|
} else { |
386
|
|
|
$this->localImage = $this->getLocalImagePath($this->src); |
387
|
|
|
if (!$this->localImage) { |
388
|
|
|
$this->debug(1, "Could not find the local image: {$this->localImage}"); |
389
|
|
|
$this->error('Could not find the internal image you specified.'); |
390
|
|
|
$this->set404(); |
391
|
|
|
|
392
|
|
|
return false; |
393
|
|
|
} |
394
|
|
|
$this->debug(1, "Local image path is {$this->localImage}"); |
395
|
|
|
$this->localImageMTime = @filemtime($this->localImage); |
396
|
|
|
//We include the mtime of the local file in case in changes on disk. |
397
|
|
|
$this->cachefile = $this->cacheDirectory . '/' . FILE_CACHE_PREFIX . $cachePrefix . md5($this->salt . $this->localImageMTime . $_SERVER['QUERY_STRING'] . $this->fileCacheVersion) . FILE_CACHE_SUFFIX; |
|
|
|
|
398
|
|
|
} |
399
|
|
|
$this->debug(2, 'Cache file is: ' . $this->cachefile); |
400
|
|
|
|
401
|
|
|
return true; |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
public function __destruct() |
405
|
|
|
{ |
406
|
|
|
foreach ($this->toDeletes as $del) { |
407
|
|
|
$this->debug(2, "Deleting temp file $del"); |
408
|
|
|
@unlink($del); |
|
|
|
|
409
|
|
|
} |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
/** |
413
|
|
|
* @return bool |
414
|
|
|
*/ |
415
|
|
|
public function run() |
416
|
|
|
{ |
417
|
|
|
if ($this->isURL) { |
418
|
|
|
if (!ALLOW_EXTERNAL) { |
419
|
|
|
$this->debug(1, 'Got a request for an external image but ALLOW_EXTERNAL is disabled so returning error msg.'); |
420
|
|
|
$this->error('You are not allowed to fetch images from an external website.'); |
421
|
|
|
|
422
|
|
|
return false; |
423
|
|
|
} |
424
|
|
|
$this->debug(3, 'Got request for external image. Starting serveExternalImage.'); |
425
|
|
|
if ($this->param('webshot')) { |
426
|
|
|
if (WEBSHOT_ENABLED) { |
427
|
|
|
$this->debug(3, 'webshot param is set, so we\'re going to take a webshot.'); |
428
|
|
|
$this->serveWebshot(); |
429
|
|
|
} else { |
430
|
|
|
$this->error('You added the webshot parameter but webshots are disabled on this server. You need to set WEBSHOT_ENABLED === true to enable webshots.'); |
431
|
|
|
} |
432
|
|
|
} else { |
433
|
|
|
$this->debug(3, 'webshot is NOT set so we\'re going to try to fetch a regular image.'); |
434
|
|
|
$this->serveExternalImage(); |
435
|
|
|
} |
436
|
|
|
} else { |
437
|
|
|
$this->debug(3, 'Got request for internal image. Starting serveInternalImage()'); |
438
|
|
|
$this->serveInternalImage(); |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
return true; |
442
|
|
|
} |
443
|
|
|
|
444
|
|
|
/** |
445
|
|
|
* @return bool |
446
|
|
|
*/ |
447
|
|
|
protected function handleErrors() |
448
|
|
|
{ |
449
|
|
|
if ($this->haveErrors()) { |
450
|
|
|
if (NOT_FOUND_IMAGE && $this->is404()) { |
451
|
|
|
if ($this->serveImg(NOT_FOUND_IMAGE)) { |
452
|
|
|
exit(0); |
|
|
|
|
453
|
|
|
} |
454
|
|
|
$this->error('Additionally, the 404 image that is configured could not be found or there was an error serving it.'); |
455
|
|
|
} |
456
|
|
|
if (ERROR_IMAGE) { |
457
|
|
|
if ($this->serveImg(ERROR_IMAGE)) { |
458
|
|
|
exit(0); |
|
|
|
|
459
|
|
|
} |
460
|
|
|
$this->error('Additionally, the error image that is configured could not be found or there was an error serving it.'); |
461
|
|
|
} |
462
|
|
|
$this->serveErrors(); |
463
|
|
|
exit(0); |
|
|
|
|
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
return false; |
467
|
|
|
} |
468
|
|
|
|
469
|
|
|
/** |
470
|
|
|
* @return bool |
471
|
|
|
*/ |
472
|
|
|
protected function tryBrowserCache() |
473
|
|
|
{ |
474
|
|
|
if (BROWSER_CACHE_DISABLE) { |
475
|
|
|
$this->debug(3, 'Browser caching is disabled'); |
476
|
|
|
|
477
|
|
|
return false; |
478
|
|
|
} |
479
|
|
|
if (\Xmf\Request::hasVar('HTTP_IF_MODIFIED_SINCE', 'SERVER')) { |
480
|
|
|
$this->debug(3, 'Got a conditional get'); |
481
|
|
|
$mtime = false; |
482
|
|
|
//We've already checked if the real file exists in the constructor |
483
|
|
|
if (!is_file($this->cachefile)) { |
484
|
|
|
//If we don't have something cached, regenerate the cached image. |
485
|
|
|
return false; |
486
|
|
|
} |
487
|
|
|
if ($this->localImageMTime) { |
488
|
|
|
$mtime = $this->localImageMTime; |
489
|
|
|
$this->debug(3, "Local real file's modification time is $mtime"); |
490
|
|
|
} elseif (\is_file($this->cachefile)) { |
491
|
|
|
//If it's not a local request then use the mtime of the cached file to determine the 304 |
492
|
|
|
$mtime = @filemtime($this->cachefile); |
493
|
|
|
$this->debug(3, "Cached file's modification time is $mtime"); |
494
|
|
|
} |
495
|
|
|
if (false === $mtime) { |
496
|
|
|
return false; |
497
|
|
|
} |
498
|
|
|
|
499
|
|
|
$iftime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']); |
500
|
|
|
$this->debug(3, "The conditional get's if-modified-since unixtime is $iftime"); |
501
|
|
|
if ($iftime < 1) { |
502
|
|
|
$this->debug(3, 'Got an invalid conditional get modified since time. Returning false.'); |
503
|
|
|
|
504
|
|
|
return false; |
505
|
|
|
} |
506
|
|
|
if ($iftime < $mtime) { |
507
|
|
|
//Real file or cache file has been modified since last request, so force refetch. |
508
|
|
|
$this->debug(3, 'File has been modified since last fetch.'); |
509
|
|
|
|
510
|
|
|
return false; |
511
|
|
|
} |
512
|
|
|
//Otherwise serve a 304 |
513
|
|
|
$this->debug(3, 'File has not been modified since last get, so serving a 304.'); |
514
|
|
|
header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified'); |
515
|
|
|
$this->debug(1, 'Returning 304 not modified'); |
516
|
|
|
|
517
|
|
|
return true; |
518
|
|
|
} |
519
|
|
|
|
520
|
|
|
return false; |
521
|
|
|
} |
522
|
|
|
|
523
|
|
|
/** |
524
|
|
|
* @return bool |
525
|
|
|
*/ |
526
|
|
|
protected function tryServerCache() |
527
|
|
|
{ |
528
|
|
|
$this->debug(3, 'Trying server cache'); |
529
|
|
|
if (\is_file($this->cachefile)) { |
530
|
|
|
$this->debug(3, "Cachefile {$this->cachefile} exists"); |
531
|
|
|
if ($this->isURL) { |
532
|
|
|
$this->debug(3, 'This is an external request, so checking if the cachefile is empty which means the request failed previously.'); |
533
|
|
|
if (filesize($this->cachefile) < 1) { |
534
|
|
|
$this->debug(3, 'Found an empty cachefile indicating a failed earlier request. Checking how old it is.'); |
535
|
|
|
//Fetching error occured previously |
536
|
|
|
if (time() - @filemtime($this->cachefile) > WAIT_BETWEEN_FETCH_ERRORS) { |
537
|
|
|
$this->debug(3, 'File is older than ' . WAIT_BETWEEN_FETCH_ERRORS . ' seconds. Deleting and returning false so app can try and load file.'); |
538
|
|
|
@unlink($this->cachefile); |
|
|
|
|
539
|
|
|
|
540
|
|
|
return false; //to indicate we didn't serve from cache and app should try and load |
541
|
|
|
} |
542
|
|
|
$this->debug(3, 'Empty cachefile is still fresh so returning message saying we had an error fetching this image from remote host.'); |
543
|
|
|
$this->set404(); |
544
|
|
|
$this->error('An error occured fetching image.'); |
545
|
|
|
|
546
|
|
|
return false; |
547
|
|
|
} |
548
|
|
|
} else { |
549
|
|
|
$this->debug(3, "Trying to serve cachefile {$this->cachefile}"); |
550
|
|
|
} |
551
|
|
|
if ($this->serveCacheFile()) { |
552
|
|
|
$this->debug(3, "Succesfully served cachefile {$this->cachefile}"); |
553
|
|
|
|
554
|
|
|
return true; |
555
|
|
|
} |
556
|
|
|
$this->debug(3, "Failed to serve cachefile {$this->cachefile} - Deleting it from cache."); |
557
|
|
|
//Image serving failed. We can't retry at this point, but lets remove it from cache so the next request recreates it |
558
|
|
|
@unlink($this->cachefile); |
559
|
|
|
|
560
|
|
|
return true; |
561
|
|
|
} |
562
|
|
|
|
563
|
|
|
return null; |
564
|
|
|
} |
565
|
|
|
|
566
|
|
|
/** |
567
|
|
|
* @param $err |
568
|
|
|
* |
569
|
|
|
* @return bool |
570
|
|
|
*/ |
571
|
|
|
protected function error($err) |
572
|
|
|
{ |
573
|
|
|
$this->debug(3, "Adding error message: $err"); |
574
|
|
|
$this->errors[] = $err; |
575
|
|
|
|
576
|
|
|
return false; |
577
|
|
|
} |
578
|
|
|
|
579
|
|
|
/** |
580
|
|
|
* @return bool |
581
|
|
|
*/ |
582
|
|
|
protected function haveErrors() |
583
|
|
|
{ |
584
|
|
|
return count($this->errors) > 0; |
585
|
|
|
} |
586
|
|
|
|
587
|
|
|
protected function serveErrors() |
588
|
|
|
{ |
589
|
|
|
header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request'); |
590
|
|
|
if (!DISPLAY_ERROR_MESSAGES) { |
591
|
|
|
return; |
592
|
|
|
} |
593
|
|
|
$html = '<ul>'; |
594
|
|
|
foreach ($this->errors as $err) { |
595
|
|
|
$html .= '<li>' . htmlentities($err, ENT_QUOTES | ENT_HTML5) . '</li>'; |
596
|
|
|
} |
597
|
|
|
$html .= '</ul>'; |
598
|
|
|
echo '<h1>A TimThumb error has occured</h1>The following error(s) occured:<br>' . $html . '<br>'; |
599
|
|
|
echo '<br>Query String : ' . htmlentities($_SERVER['QUERY_STRING'], ENT_QUOTES | ENT_HTML5); |
600
|
|
|
echo '<br>TimThumb version : ' . VERSION . '</pre>'; |
601
|
|
|
} |
602
|
|
|
|
603
|
|
|
/** |
604
|
|
|
* @return bool |
605
|
|
|
*/ |
606
|
|
|
protected function serveInternalImage() |
607
|
|
|
{ |
608
|
|
|
$this->debug(3, "Local image path is $this->localImage"); |
609
|
|
|
if (!$this->localImage) { |
610
|
|
|
$this->sanityFail('localImage not set after verifying it earlier in the code.'); |
611
|
|
|
|
612
|
|
|
return false; |
613
|
|
|
} |
614
|
|
|
$fileSize = filesize($this->localImage); |
615
|
|
|
if ($fileSize > MAX_FILE_SIZE) { |
616
|
|
|
$this->error('The file you specified is greater than the maximum allowed file size.'); |
617
|
|
|
|
618
|
|
|
return false; |
619
|
|
|
} |
620
|
|
|
if ($fileSize <= 0) { |
621
|
|
|
$this->error('The file you specified is <= 0 bytes.'); |
622
|
|
|
|
623
|
|
|
return false; |
624
|
|
|
} |
625
|
|
|
$this->debug(3, 'Calling processImageAndWriteToCache() for local image.'); |
626
|
|
|
if ($this->processImageAndWriteToCache($this->localImage)) { |
627
|
|
|
$this->serveCacheFile(); |
628
|
|
|
|
629
|
|
|
return true; |
630
|
|
|
} |
631
|
|
|
|
632
|
|
|
return false; |
633
|
|
|
} |
634
|
|
|
|
635
|
|
|
/** |
636
|
|
|
* @return bool|void |
637
|
|
|
*/ |
638
|
|
|
protected function cleanCache() |
639
|
|
|
{ |
640
|
|
|
if (FILE_CACHE_TIME_BETWEEN_CLEANS < 0) { |
641
|
|
|
return null; |
642
|
|
|
} |
643
|
|
|
$this->debug(3, 'cleanCache() called'); |
644
|
|
|
$lastCleanFile = $this->cacheDirectory . '/timthumb_cacheLastCleanTime.touch'; |
645
|
|
|
|
646
|
|
|
//If this is a new timthumb installation we need to create the file |
647
|
|
|
if (!is_file($lastCleanFile)) { |
648
|
|
|
$this->debug(1, "File tracking last clean doesn't exist. Creating $lastCleanFile"); |
649
|
|
|
if (!touch($lastCleanFile)) { |
650
|
|
|
$this->error('Could not create cache clean timestamp file.'); |
651
|
|
|
} |
652
|
|
|
|
653
|
|
|
return null; |
654
|
|
|
} |
655
|
|
|
if (@filemtime($lastCleanFile) < (time() - FILE_CACHE_TIME_BETWEEN_CLEANS)) { |
656
|
|
|
//Cache was last cleaned more than 1 day ago |
657
|
|
|
$this->debug(1, 'Cache was last cleaned more than ' . FILE_CACHE_TIME_BETWEEN_CLEANS . ' seconds ago. Cleaning now.'); |
658
|
|
|
// Very slight race condition here, but worst case we'll have 2 or 3 servers cleaning the cache simultaneously once a day. |
659
|
|
|
if (!touch($lastCleanFile)) { |
660
|
|
|
$this->error('Could not create cache clean timestamp file.'); |
661
|
|
|
} |
662
|
|
|
$files = glob($this->cacheDirectory . '/*' . FILE_CACHE_SUFFIX, GLOB_NOSORT); |
663
|
|
|
if ($files) { |
|
|
|
|
664
|
|
|
$timeAgo = time() - FILE_CACHE_MAX_FILE_AGE; |
665
|
|
|
foreach ($files as $file) { |
666
|
|
|
if (@filemtime($file) < $timeAgo) { |
667
|
|
|
$this->debug(3, "Deleting cache file $file older than max age: " . FILE_CACHE_MAX_FILE_AGE . ' seconds'); |
668
|
|
|
@unlink($file); |
|
|
|
|
669
|
|
|
} |
670
|
|
|
} |
671
|
|
|
} |
672
|
|
|
|
673
|
|
|
return true; |
674
|
|
|
} |
675
|
|
|
$this->debug(3, 'Cache was cleaned less than ' . FILE_CACHE_TIME_BETWEEN_CLEANS . ' seconds ago so no cleaning needed.'); |
676
|
|
|
|
677
|
|
|
return false; |
678
|
|
|
} |
679
|
|
|
|
680
|
|
|
/** |
681
|
|
|
* @param $localImage |
682
|
|
|
* |
683
|
|
|
* @return bool |
684
|
|
|
*/ |
685
|
|
|
protected function processImageAndWriteToCache($localImage) |
686
|
|
|
{ |
687
|
|
|
$sData = getimagesize($localImage); |
688
|
|
|
$origType = $sData[2]; |
689
|
|
|
$mimeType = $sData['mime']; |
690
|
|
|
|
691
|
|
|
$this->debug(3, "Mime type of image is $mimeType"); |
692
|
|
|
if (!preg_match('/^image\/(?:gif|jpg|jpeg|png)$/i', $mimeType)) { |
693
|
|
|
return $this->error('The image being resized is not a valid gif, jpg or png.'); |
694
|
|
|
} |
695
|
|
|
|
696
|
|
|
if (!function_exists('imagecreatetruecolor')) { |
697
|
|
|
return $this->error('GD Library Error: imagecreatetruecolor does not exist - please contact your webhost and ask them to install the GD library'); |
698
|
|
|
} |
699
|
|
|
|
700
|
|
|
if (defined('IMG_FILTER_NEGATE') && function_exists('imagefilter')) { |
701
|
|
|
$imageFilters = [ |
702
|
|
|
1 => [IMG_FILTER_NEGATE, 0], |
703
|
|
|
2 => [IMG_FILTER_GRAYSCALE, 0], |
704
|
|
|
3 => [IMG_FILTER_BRIGHTNESS, 1], |
705
|
|
|
4 => [IMG_FILTER_CONTRAST, 1], |
706
|
|
|
5 => [IMG_FILTER_COLORIZE, 4], |
707
|
|
|
6 => [IMG_FILTER_EDGEDETECT, 0], |
708
|
|
|
7 => [IMG_FILTER_EMBOSS, 0], |
709
|
|
|
8 => [IMG_FILTER_GAUSSIAN_BLUR, 0], |
710
|
|
|
9 => [IMG_FILTER_SELECTIVE_BLUR, 0], |
711
|
|
|
10 => [IMG_FILTER_MEAN_REMOVAL, 0], |
712
|
|
|
11 => [IMG_FILTER_SMOOTH, 0], |
713
|
|
|
]; |
714
|
|
|
} |
715
|
|
|
|
716
|
|
|
// get standard input properties |
717
|
|
|
$newWidth = (int)abs((int)$this->param('w', 0)); |
718
|
|
|
$newHeight = (int)abs((int)$this->param('h', 0)); |
719
|
|
|
$zoomCrop = (int)$this->param('zc', DEFAULT_ZC); |
720
|
|
|
$quality = (int)abs((int)$this->param('q', DEFAULT_Q)); |
721
|
|
|
$align = $this->cropTop ? 't' : $this->param('a', 'c'); |
722
|
|
|
$filters = $this->param('f', DEFAULT_F); |
723
|
|
|
$sharpen = (bool)$this->param('s', DEFAULT_S); |
724
|
|
|
$canvas_color = $this->param('cc', DEFAULT_CC); |
725
|
|
|
$canvas_trans = (bool)$this->param('ct', '1'); |
726
|
|
|
|
727
|
|
|
// set default width and height if neither are set already |
728
|
|
|
if (0 == $newWidth && 0 == $newHeight) { |
729
|
|
|
$newWidth = DEFAULT_WIDTH; |
730
|
|
|
$newHeight = DEFAULT_HEIGHT; |
731
|
|
|
} |
732
|
|
|
|
733
|
|
|
// ensure size limits can not be abused |
734
|
|
|
$newWidth = min($newWidth, MAX_WIDTH); |
735
|
|
|
$newHeight = min($newHeight, MAX_HEIGHT); |
736
|
|
|
|
737
|
|
|
// set memory limit to be able to have enough space to resize larger images |
738
|
|
|
$this->setMemoryLimit(); |
739
|
|
|
|
740
|
|
|
// open the existing image |
741
|
|
|
$image = $this->openImage($mimeType, $localImage); |
742
|
|
|
if (false === $image) { |
|
|
|
|
743
|
|
|
return $this->error('Unable to open image.'); |
744
|
|
|
} |
745
|
|
|
|
746
|
|
|
// Get original width and height |
747
|
|
|
$width = imagesx($image); |
748
|
|
|
$height = imagesy($image); |
749
|
|
|
$origin_x = 0; |
750
|
|
|
$origin_y = 0; |
751
|
|
|
|
752
|
|
|
// generate new w/h if not provided |
753
|
|
|
if ($newWidth && !$newHeight) { |
754
|
|
|
$newHeight = floor($height * ($newWidth / $width)); |
755
|
|
|
} elseif ($newHeight && !$newWidth) { |
756
|
|
|
$newWidth = floor($width * ($newHeight / $height)); |
757
|
|
|
} |
758
|
|
|
|
759
|
|
|
// scale down and add borders |
760
|
|
|
if (3 == $zoomCrop) { |
761
|
|
|
$final_height = $height * ($newWidth / $width); |
762
|
|
|
|
763
|
|
|
if ($final_height > $newHeight) { |
764
|
|
|
$newWidth = $width * ($newHeight / $height); |
765
|
|
|
} else { |
766
|
|
|
$newHeight = $final_height; |
767
|
|
|
} |
768
|
|
|
} |
769
|
|
|
|
770
|
|
|
// create a new true color image |
771
|
|
|
$canvas = imagecreatetruecolor((int)$newWidth, (int)$newHeight); |
772
|
|
|
imagealphablending($canvas, false); |
773
|
|
|
|
774
|
|
|
if (3 == \mb_strlen($canvas_color)) { |
775
|
|
|
//if is 3-char notation, edit string into 6-char notation |
776
|
|
|
$canvas_color = str_repeat(mb_substr($canvas_color, 0, 1), 2) . str_repeat(mb_substr($canvas_color, 1, 1), 2) . str_repeat(mb_substr($canvas_color, 2, 1), 2); |
777
|
|
|
} elseif (6 != \mb_strlen($canvas_color)) { |
778
|
|
|
$canvas_color = DEFAULT_CC; // on error return default canvas color |
779
|
|
|
} |
780
|
|
|
|
781
|
|
|
$canvas_color_R = hexdec(mb_substr($canvas_color, 0, 2)); |
782
|
|
|
$canvas_color_G = hexdec(mb_substr($canvas_color, 2, 2)); |
783
|
|
|
$canvas_color_B = hexdec(mb_substr($canvas_color, 4, 2)); |
784
|
|
|
|
785
|
|
|
// Create a new transparent color for image |
786
|
|
|
// If is a png and PNG_IS_TRANSPARENT is false then remove the alpha transparency |
787
|
|
|
// (and if is set a canvas color show it in the background) |
788
|
|
|
if (!PNG_IS_TRANSPARENT && $canvas_trans && preg_match('/^image\/png$/i', $mimeType)) { |
789
|
|
|
$color = imagecolorallocatealpha($canvas, $canvas_color_R, $canvas_color_G, $canvas_color_B, 127); |
|
|
|
|
790
|
|
|
} else { |
791
|
|
|
$color = imagecolorallocatealpha($canvas, $canvas_color_R, $canvas_color_G, $canvas_color_B, 0); |
792
|
|
|
} |
793
|
|
|
|
794
|
|
|
// Completely fill the background of the new image with allocated color. |
795
|
|
|
imagefill($canvas, 0, 0, $color); |
796
|
|
|
// scale down and add borders |
797
|
|
|
if (2 == $zoomCrop) { |
798
|
|
|
$final_height = $height * ($newWidth / $width); |
799
|
|
|
if ($final_height > $newHeight) { |
800
|
|
|
$origin_x = $newWidth / 2; |
801
|
|
|
$newWidth = $width * ($newHeight / $height); |
802
|
|
|
$origin_x = round($origin_x - ($newWidth / 2)); |
803
|
|
|
} else { |
804
|
|
|
$origin_y = $newHeight / 2; |
805
|
|
|
$newHeight = $final_height; |
806
|
|
|
$origin_y = round($origin_y - ($newHeight / 2)); |
807
|
|
|
} |
808
|
|
|
} |
809
|
|
|
|
810
|
|
|
// Restore transparency blending |
811
|
|
|
imagesavealpha($canvas, true); |
812
|
|
|
|
813
|
|
|
if ($zoomCrop > 0) { |
814
|
|
|
$src_x = $src_y = 0; |
815
|
|
|
$src_w = $width; |
816
|
|
|
$src_h = $height; |
817
|
|
|
|
818
|
|
|
$cmp_x = $width / $newWidth; |
819
|
|
|
$cmp_y = $height / $newHeight; |
820
|
|
|
|
821
|
|
|
// calculate x or y coordinate and width or height of source |
822
|
|
|
if ($cmp_x > $cmp_y) { |
823
|
|
|
$src_w = round($width / $cmp_x * $cmp_y); |
824
|
|
|
$src_x = round(($width - ($width / $cmp_x * $cmp_y)) / 2); |
825
|
|
|
} elseif ($cmp_y > $cmp_x) { |
826
|
|
|
$src_h = round($height / $cmp_y * $cmp_x); |
827
|
|
|
$src_y = round(($height - ($height / $cmp_y * $cmp_x)) / 2); |
828
|
|
|
} |
829
|
|
|
|
830
|
|
|
// positional cropping! |
831
|
|
|
if ($align) { |
832
|
|
|
if (false !== mb_strpos($align, 't')) { |
833
|
|
|
$src_y = 0; |
834
|
|
|
} |
835
|
|
|
if (false !== mb_strpos($align, 'b')) { |
836
|
|
|
$src_y = $height - $src_h; |
837
|
|
|
} |
838
|
|
|
if (false !== mb_strpos($align, 'l')) { |
839
|
|
|
$src_x = 0; |
840
|
|
|
} |
841
|
|
|
if (false !== mb_strpos($align, 'r')) { |
842
|
|
|
$src_x = $width - $src_w; |
843
|
|
|
} |
844
|
|
|
} |
845
|
|
|
|
846
|
|
|
imagecopyresampled($canvas, $image, (int)$origin_x, (int)$origin_y, (int)$src_x, (int)$src_y, (int)$newWidth, (int)$newHeight, (int)$src_w, (int)$src_h); |
847
|
|
|
} else { |
848
|
|
|
// copy and resize part of an image with resampling |
849
|
|
|
imagecopyresampled($canvas, $image, 0, 0, 0, 0, (int)$newWidth, (int)$newHeight, (int)$width, (int)$height); |
850
|
|
|
} |
851
|
|
|
|
852
|
|
|
if (defined('IMG_FILTER_NEGATE') && '' != $filters && function_exists('imagefilter')) { |
853
|
|
|
// apply filters to image |
854
|
|
|
$filterList = explode('|', $filters); |
855
|
|
|
foreach ($filterList as $fl) { |
856
|
|
|
$filterSettings = explode(',', $fl); |
857
|
|
|
if (isset($imageFilters[$filterSettings[0]])) { |
858
|
|
|
for ($i = 0; $i < 4; ++$i) { |
859
|
|
|
if (isset($filterSettings[$i])) { |
860
|
|
|
$filterSettings[$i] = (int)$filterSettings[$i]; |
861
|
|
|
} else { |
862
|
|
|
$filterSettings[$i] = null; |
863
|
|
|
} |
864
|
|
|
} |
865
|
|
|
|
866
|
|
|
switch ($imageFilters[$filterSettings[0]][1]) { |
867
|
|
|
case 1: |
868
|
|
|
|
869
|
|
|
imagefilter($canvas, $imageFilters[$filterSettings[0]][0], $filterSettings[1]); |
870
|
|
|
|
871
|
|
|
break; |
872
|
|
|
case 2: |
873
|
|
|
imagefilter($canvas, $imageFilters[$filterSettings[0]][0], $filterSettings[1], $filterSettings[2]); |
874
|
|
|
|
875
|
|
|
break; |
876
|
|
|
case 3: |
877
|
|
|
imagefilter($canvas, $imageFilters[$filterSettings[0]][0], $filterSettings[1], $filterSettings[2], $filterSettings[3]); |
878
|
|
|
|
879
|
|
|
break; |
880
|
|
|
case 4: |
881
|
|
|
imagefilter($canvas, $imageFilters[$filterSettings[0]][0], $filterSettings[1], $filterSettings[2], $filterSettings[3], $filterSettings[4]); |
882
|
|
|
|
883
|
|
|
break; |
884
|
|
|
default: |
885
|
|
|
imagefilter($canvas, $imageFilters[$filterSettings[0]][0]); |
886
|
|
|
break; |
887
|
|
|
} |
888
|
|
|
} |
889
|
|
|
} |
890
|
|
|
} |
891
|
|
|
|
892
|
|
|
// sharpen image |
893
|
|
|
if ($sharpen && function_exists('imageconvolution')) { |
894
|
|
|
$sharpenMatrix = [ |
895
|
|
|
[-1, -1, -1], |
896
|
|
|
[-1, 16, -1], |
897
|
|
|
[-1, -1, -1], |
898
|
|
|
]; |
899
|
|
|
|
900
|
|
|
$divisor = 8; |
901
|
|
|
$offset = 0; |
902
|
|
|
|
903
|
|
|
imageconvolution($canvas, $sharpenMatrix, $divisor, $offset); |
904
|
|
|
} |
905
|
|
|
//Straight from Wordpress core code. Reduces filesize by up to 70% for PNG's |
906
|
|
|
if ((IMAGETYPE_PNG == $origType || IMAGETYPE_GIF == $origType) && function_exists('imageistruecolor') && !imageistruecolor($image) && imagecolortransparent($image) > 0) { |
907
|
|
|
imagetruecolortopalette($canvas, false, imagecolorstotal($image)); |
908
|
|
|
} |
909
|
|
|
|
910
|
|
|
$imgType = ''; |
911
|
|
|
$tempfile = tempnam($this->cacheDirectory, 'timthumb_tmpimg_'); |
912
|
|
|
if (preg_match('/^image\/(?:jpg|jpeg)$/i', $mimeType)) { |
913
|
|
|
$imgType = 'jpg'; |
914
|
|
|
imagejpeg($canvas, $tempfile, $quality); |
915
|
|
|
} elseif (preg_match('/^image\/png$/i', $mimeType)) { |
916
|
|
|
$imgType = 'png'; |
917
|
|
|
imagepng($canvas, $tempfile, (int)floor($quality * 0.09)); |
918
|
|
|
} elseif (preg_match('/^image\/gif$/i', $mimeType)) { |
919
|
|
|
$imgType = 'gif'; |
920
|
|
|
imagegif($canvas, $tempfile); |
921
|
|
|
} else { |
922
|
|
|
return $this->sanityFail('Could not match mime type after verifying it previously.'); |
923
|
|
|
} |
924
|
|
|
|
925
|
|
|
if ('png' === $imgType && OPTIPNG_ENABLED && OPTIPNG_PATH && @is_file(OPTIPNG_PATH)) { |
926
|
|
|
$exec = OPTIPNG_PATH; |
|
|
|
|
927
|
|
|
$this->debug(3, "optipng'ing $tempfile"); |
928
|
|
|
$presize = filesize($tempfile); |
929
|
|
|
$out = shell_exec('$exec -o1 $tempfile'); //you can use up to -o7 but it really slows things down |
|
|
|
|
930
|
|
|
clearstatcache(); |
931
|
|
|
$aftersize = filesize($tempfile); |
932
|
|
|
$sizeDrop = $presize - $aftersize; |
933
|
|
|
if ($sizeDrop > 0) { |
934
|
|
|
$this->debug(1, "optipng reduced size by $sizeDrop"); |
935
|
|
|
} elseif ($sizeDrop < 0) { |
936
|
|
|
$this->debug(1, "optipng increased size! Difference was: $sizeDrop"); |
937
|
|
|
} else { |
938
|
|
|
$this->debug(1, 'optipng did not change image size.'); |
939
|
|
|
} |
940
|
|
|
} elseif ('png' === $imgType && PNGCRUSH_ENABLED && PNGCRUSH_PATH && @is_file(PNGCRUSH_PATH)) { |
941
|
|
|
$exec = PNGCRUSH_PATH; |
942
|
|
|
$tempfile2 = tempnam($this->cacheDirectory, 'timthumb_tmpimg_'); |
943
|
|
|
$this->debug(3, "pngcrush'ing $tempfile to $tempfile2"); |
944
|
|
|
$out = shell_exec('$exec $tempfile $tempfile2'); |
945
|
|
|
$todel = ''; |
946
|
|
|
if (\is_file($tempfile2)) { |
947
|
|
|
$sizeDrop = filesize($tempfile) - filesize($tempfile2); |
948
|
|
|
if ($sizeDrop > 0) { |
949
|
|
|
$this->debug(1, "pngcrush was succesful and gave a $sizeDrop byte size reduction"); |
950
|
|
|
$todel = $tempfile; |
951
|
|
|
$tempfile = $tempfile2; |
952
|
|
|
} else { |
953
|
|
|
$this->debug(1, "pngcrush did not reduce file size. Difference was $sizeDrop bytes."); |
954
|
|
|
$todel = $tempfile2; |
955
|
|
|
} |
956
|
|
|
} else { |
957
|
|
|
$this->debug(3, "pngcrush failed with output: $out"); |
958
|
|
|
$todel = $tempfile2; |
959
|
|
|
} |
960
|
|
|
@unlink($todel); |
|
|
|
|
961
|
|
|
} |
962
|
|
|
|
963
|
|
|
$this->debug(3, 'Rewriting image with security header.'); |
964
|
|
|
$tempfile4 = tempnam($this->cacheDirectory, 'timthumb_tmpimg_'); |
965
|
|
|
$context = stream_context_create(); |
966
|
|
|
$fp = fopen($tempfile, 'rb', false, $context); |
967
|
|
|
file_put_contents($tempfile4, $this->filePrependSecurityBlock . $imgType . ' ?' . '>'); //6 extra bytes, first 3 being image type |
968
|
|
|
file_put_contents($tempfile4, $fp, FILE_APPEND); |
969
|
|
|
fclose($fp); |
970
|
|
|
@unlink($tempfile); |
971
|
|
|
$this->debug(3, 'Locking and replacing cache file.'); |
972
|
|
|
$lockFile = $this->cachefile . '.lock'; |
973
|
|
|
$fh = fopen($lockFile, 'wb'); |
974
|
|
|
if (!$fh) { |
|
|
|
|
975
|
|
|
return $this->error('Could not open the lockfile for writing an image.'); |
976
|
|
|
} |
977
|
|
|
if (flock($fh, LOCK_EX)) { |
978
|
|
|
@unlink($this->cachefile); //rename generally overwrites, but doing this in case of platform specific quirks. File might not exist yet. |
979
|
|
|
rename($tempfile4, $this->cachefile); |
980
|
|
|
flock($fh, LOCK_UN); |
981
|
|
|
fclose($fh); |
982
|
|
|
@unlink($lockFile); |
983
|
|
|
} else { |
984
|
|
|
fclose($fh); |
985
|
|
|
@unlink($lockFile); |
986
|
|
|
@unlink($tempfile4); |
987
|
|
|
|
988
|
|
|
return $this->error('Could not get a lock for writing.'); |
989
|
|
|
} |
990
|
|
|
$this->debug(3, 'Done image replace with security header. Cleaning up and running cleanCache()'); |
991
|
|
|
imagedestroy($canvas); |
992
|
|
|
imagedestroy($image); |
993
|
|
|
|
994
|
|
|
return true; |
995
|
|
|
} |
996
|
|
|
|
997
|
|
|
protected function calcDocRoot() |
998
|
|
|
{ |
999
|
|
|
$docRoot = @$_SERVER['DOCUMENT_ROOT']; |
1000
|
|
|
if (defined('LOCAL_FILE_BASE_DIRECTORY')) { |
1001
|
|
|
$docRoot = LOCAL_FILE_BASE_DIRECTORY; |
|
|
|
|
1002
|
|
|
} |
1003
|
|
|
if (!isset($docRoot)) { |
1004
|
|
|
$this->debug(3, 'DOCUMENT_ROOT is not set. This is probably windows. Starting search 1.'); |
1005
|
|
|
if (\Xmf\Request::hasVar('SCRIPT_FILENAME', 'SERVER')) { |
1006
|
|
|
$docRoot = str_replace('\\', '/', mb_substr($_SERVER['SCRIPT_FILENAME'], 0, 0 - mb_strlen($_SERVER['SCRIPT_NAME']))); |
1007
|
|
|
$this->debug(3, "Generated docRoot using SCRIPT_FILENAME and SCRIPT_NAME as: $docRoot"); |
1008
|
|
|
} |
1009
|
|
|
} |
1010
|
|
|
if (!isset($docRoot)) { |
1011
|
|
|
$this->debug(3, 'DOCUMENT_ROOT still is not set. Starting search 2.'); |
1012
|
|
|
if (\Xmf\Request::hasVar('PATH_TRANSLATED', 'SERVER')) { |
1013
|
|
|
$docRoot = str_replace('\\', '/', mb_substr(str_replace('\\\\', '\\', $_SERVER['PATH_TRANSLATED']), 0, 0 - mb_strlen($_SERVER['SCRIPT_NAME']))); |
1014
|
|
|
$this->debug(3, "Generated docRoot using PATH_TRANSLATED and SCRIPT_NAME as: $docRoot"); |
1015
|
|
|
} |
1016
|
|
|
} |
1017
|
|
|
if ($docRoot && '/' !== $_SERVER['DOCUMENT_ROOT']) { |
1018
|
|
|
$docRoot = preg_replace('/\/$/', '', $docRoot); |
1019
|
|
|
} |
1020
|
|
|
$this->debug(3, 'Doc root is: ' . $docRoot); |
1021
|
|
|
$this->docRoot = $docRoot; |
1022
|
|
|
} |
1023
|
|
|
|
1024
|
|
|
/** |
1025
|
|
|
* @param $src |
1026
|
|
|
* |
1027
|
|
|
* @return bool|string |
1028
|
|
|
*/ |
1029
|
|
|
protected function getLocalImagePath($src) |
1030
|
|
|
{ |
1031
|
|
|
$src = ltrim($src, '/'); //strip off the leading '/' |
1032
|
|
|
if (!$this->docRoot) { |
1033
|
|
|
$this->debug(3, 'We have no document root set, so as a last resort, lets check if the image is in the current dir and serve that.'); |
1034
|
|
|
//We don't support serving images outside the current dir if we don't have a doc root for security reasons. |
1035
|
|
|
$file = preg_replace('/^.*?([^\/\\\\]+)$/', '$1', $src); //strip off any path info and just leave the filename. |
1036
|
|
|
if (\is_file($file)) { |
1037
|
|
|
return $this->realpath($file); |
1038
|
|
|
} |
1039
|
|
|
|
1040
|
|
|
return $this->error("Could not find your website document root and the file specified doesn't exist in timthumbs directory. We don't support serving files outside timthumb's directory without a document root for security reasons."); |
1041
|
|
|
} |
1042
|
|
|
if (!is_dir($this->docRoot)) { |
1043
|
|
|
$this->error("Server path does not exist. Ensure variable \$_SERVER['DOCUMENT_ROOT'] is set correctly"); |
1044
|
|
|
} |
1045
|
|
|
|
1046
|
|
|
//Do not go past this point without docRoot set |
1047
|
|
|
|
1048
|
|
|
//Try src under docRoot |
1049
|
|
|
if (file_exists($this->docRoot . '/' . $src)) { |
1050
|
|
|
$this->debug(3, 'Found file as ' . $this->docRoot . '/' . $src); |
1051
|
|
|
$real = $this->realpath($this->docRoot . '/' . $src); |
1052
|
|
|
if (0 === mb_stripos($real, $this->docRoot)) { |
1053
|
|
|
return $real; |
1054
|
|
|
} |
1055
|
|
|
$this->debug(1, 'Security block: The file specified occurs outside the document root.'); |
1056
|
|
|
//allow search to continue |
1057
|
|
|
} |
1058
|
|
|
//Check absolute paths and then verify the real path is under doc root |
1059
|
|
|
$absolute = $this->realpath('/' . $src); |
1060
|
|
|
if ($absolute && file_exists($absolute)) { |
1061
|
|
|
//realpath does file_exists check, so can probably skip the exists check here |
1062
|
|
|
$this->debug(3, "Found absolute path: $absolute"); |
1063
|
|
|
if (!$this->docRoot) { |
1064
|
|
|
$this->sanityFail('docRoot not set when checking absolute path.'); |
1065
|
|
|
} |
1066
|
|
|
if (0 === mb_stripos($absolute, $this->docRoot)) { |
1067
|
|
|
return $absolute; |
1068
|
|
|
} |
1069
|
|
|
$this->debug(1, 'Security block: The file specified occurs outside the document root.'); |
1070
|
|
|
//and continue search |
1071
|
|
|
} |
1072
|
|
|
|
1073
|
|
|
$base = $this->docRoot; |
1074
|
|
|
|
1075
|
|
|
// account for Windows directory structure |
1076
|
|
|
if (false !== mb_strpos($_SERVER['SCRIPT_FILENAME'], ':')) { |
1077
|
|
|
$subDirectories = explode('\\', str_replace($this->docRoot, '', $_SERVER['SCRIPT_FILENAME'])); |
1078
|
|
|
} else { |
1079
|
|
|
$subDirectories = explode('/', str_replace($this->docRoot, '', $_SERVER['SCRIPT_FILENAME'])); |
1080
|
|
|
} |
1081
|
|
|
|
1082
|
|
|
foreach ($subDirectories as $sub) { |
1083
|
|
|
$base .= $sub . '/'; |
1084
|
|
|
$this->debug(3, 'Trying file as: ' . $base . $src); |
1085
|
|
|
if (file_exists($base . $src)) { |
1086
|
|
|
$this->debug(3, 'Found file as: ' . $base . $src); |
1087
|
|
|
$real = $this->realpath($base . $src); |
1088
|
|
|
if (0 === mb_stripos($real, $this->realpath($this->docRoot))) { |
1089
|
|
|
return $real; |
1090
|
|
|
} |
1091
|
|
|
$this->debug(1, 'Security block: The file specified occurs outside the document root.'); |
1092
|
|
|
//And continue search |
1093
|
|
|
} |
1094
|
|
|
} |
1095
|
|
|
|
1096
|
|
|
return false; |
1097
|
|
|
} |
1098
|
|
|
|
1099
|
|
|
/** |
1100
|
|
|
* @param $path |
1101
|
|
|
* |
1102
|
|
|
* @return string |
1103
|
|
|
*/ |
1104
|
|
|
protected function realpath($path) |
1105
|
|
|
{ |
1106
|
|
|
//try to remove any relative paths |
1107
|
|
|
$removeRelatives = '/\w+\/\.\.\//'; |
1108
|
|
|
while (preg_match($removeRelatives, $path)) { |
1109
|
|
|
$path = preg_replace($removeRelatives, '', $path); |
1110
|
|
|
} |
1111
|
|
|
//if any remain use PHP realpath to strip them out, otherwise return $path |
1112
|
|
|
//if using realpath, any symlinks will also be resolved |
1113
|
|
|
return preg_match('#^\.\./|/\.\./#', $path) ? realpath($path) : $path; |
1114
|
|
|
} |
1115
|
|
|
|
1116
|
|
|
/** |
1117
|
|
|
* @param $name |
1118
|
|
|
*/ |
1119
|
|
|
protected function toDelete($name) |
1120
|
|
|
{ |
1121
|
|
|
$this->debug(3, "Scheduling file $name to delete on destruct."); |
1122
|
|
|
$this->toDeletes[] = $name; |
1123
|
|
|
} |
1124
|
|
|
|
1125
|
|
|
/** |
1126
|
|
|
* @return bool |
1127
|
|
|
*/ |
1128
|
|
|
protected function serveWebshot() |
1129
|
|
|
{ |
1130
|
|
|
$this->debug(3, 'Starting serveWebshot'); |
1131
|
|
|
$instr = 'Please follow the instructions at https://code.google.com/p/timthumb/ to set your server up for taking website screenshots.'; |
1132
|
|
|
if (!is_file(WEBSHOT_CUTYCAPT)) { |
1133
|
|
|
return $this->error("CutyCapt is not installed. $instr"); |
1134
|
|
|
} |
1135
|
|
|
if (!is_file(WEBSHOT_XVFB)) { |
1136
|
|
|
return $this->error("Xvfb is not installed. $instr"); |
1137
|
|
|
} |
1138
|
|
|
$cuty = WEBSHOT_CUTYCAPT; |
1139
|
|
|
$xv = WEBSHOT_XVFB; |
1140
|
|
|
$screenX = WEBSHOT_SCREEN_X; |
1141
|
|
|
$screenY = WEBSHOT_SCREEN_Y; |
1142
|
|
|
$colDepth = WEBSHOT_COLOR_DEPTH; |
1143
|
|
|
$format = WEBSHOT_IMAGE_FORMAT; |
1144
|
|
|
$timeout = WEBSHOT_TIMEOUT * 1000; |
1145
|
|
|
$ua = WEBSHOT_USER_AGENT; |
1146
|
|
|
$jsOn = WEBSHOT_JAVASCRIPT_ON ? 'on' : 'off'; |
1147
|
|
|
$javaOn = WEBSHOT_JAVA_ON ? 'on' : 'off'; |
1148
|
|
|
$pluginsOn = WEBSHOT_PLUGINS_ON ? 'on' : 'off'; |
1149
|
|
|
$proxy = WEBSHOT_PROXY ? ' --http-proxy=' . WEBSHOT_PROXY : ''; |
1150
|
|
|
$tempfile = tempnam($this->cacheDirectory, 'timthumb_webshot'); |
1151
|
|
|
$url = $this->src; |
1152
|
|
|
if (!preg_match('/^https?:\/\/[a-zA-Z0-9\.\-]+/i', $url)) { |
1153
|
|
|
return $this->error('Invalid URL supplied.'); |
1154
|
|
|
} |
1155
|
|
|
$url = preg_replace('/[^A-Za-z0-9\-\.\_:\/\?\&\+\;\=]+/', '', $url); //RFC 3986 plus ()$ chars to prevent exploit below. Plus the following are also removed: @*!~#[]', |
1156
|
|
|
// 2014 update by Mark Maunder: This exploit: https://cxsecurity.com/issue/WLB-2014060134 |
1157
|
|
|
// uses the $(command) shell execution syntax to execute arbitrary shell commands as the web server user. |
1158
|
|
|
// So we're now filtering out the characters: '$', '(' and ')' in the above regex to avoid this. |
1159
|
|
|
// We are also filtering out chars rarely used in URLs but legal accoring to the URL RFC which might be exploitable. These include: @*!~#[]', |
1160
|
|
|
// We're doing this because we're passing this URL to the shell and need to make very sure it's not going to execute arbitrary commands. |
1161
|
|
|
if (WEBSHOT_XVFB_RUNNING) { |
1162
|
|
|
putenv('DISPLAY=:100.0'); |
1163
|
|
|
$command = "$cuty $proxy --max-wait=$timeout --user-agent=\"$ua\" --javascript=$jsOn --java=$javaOn --plugins=$pluginsOn --js-can-open-windows=off --url=\"$url\" --out-format=$format --out=$tempfile"; |
1164
|
|
|
} else { |
1165
|
|
|
$command = "$xv --server-args=\"-screen 0, {$screenX}x{$screenY}x{$colDepth}\" $cuty $proxy --max-wait=$timeout --user-agent=\"$ua\" --javascript=$jsOn --java=$javaOn --plugins=$pluginsOn --js-can-open-windows=off --url=\"$url\" --out-format=$format --out=$tempfile"; |
1166
|
|
|
} |
1167
|
|
|
$this->debug(3, "Executing command: $command"); |
1168
|
|
|
$out = shell_exec('$command'); |
1169
|
|
|
$this->debug(3, "Received output: $out"); |
1170
|
|
|
if (!is_file($tempfile)) { |
1171
|
|
|
$this->set404(); |
1172
|
|
|
|
1173
|
|
|
return $this->error('The command to create a thumbnail failed.'); |
1174
|
|
|
} |
1175
|
|
|
$this->cropTop = true; |
1176
|
|
|
if ($this->processImageAndWriteToCache($tempfile)) { |
1177
|
|
|
$this->debug(3, 'Image processed successfully. Serving from cache'); |
1178
|
|
|
|
1179
|
|
|
return $this->serveCacheFile(); |
1180
|
|
|
} |
1181
|
|
|
|
1182
|
|
|
return false; |
1183
|
|
|
} |
1184
|
|
|
|
1185
|
|
|
/** |
1186
|
|
|
* @return bool |
1187
|
|
|
*/ |
1188
|
|
|
protected function serveExternalImage() |
1189
|
|
|
{ |
1190
|
|
|
if (!preg_match('/^https?:\/\/[a-zA-Z0-9\-\.]+/i', $this->src)) { |
1191
|
|
|
$this->error('Invalid URL supplied.'); |
1192
|
|
|
|
1193
|
|
|
return false; |
1194
|
|
|
} |
1195
|
|
|
$tempfile = tempnam($this->cacheDirectory, 'timthumb'); |
1196
|
|
|
$this->debug(3, "Fetching external image into temporary file $tempfile"); |
1197
|
|
|
$this->toDelete($tempfile); |
1198
|
|
|
#fetch file here |
1199
|
|
|
if (!$this->getURL($this->src, $tempfile)) { |
1200
|
|
|
@unlink($this->cachefile); |
|
|
|
|
1201
|
|
|
touch($this->cachefile); |
1202
|
|
|
$this->debug(3, 'Error fetching URL: ' . $this->lastURLError); |
1203
|
|
|
$this->error('Error reading the URL you specified from remote host.' . $this->lastURLError); |
1204
|
|
|
|
1205
|
|
|
return false; |
1206
|
|
|
} |
1207
|
|
|
|
1208
|
|
|
$mimeType = $this->getMimeType($tempfile); |
1209
|
|
|
if (!preg_match('/^image\/(?:jpg|jpeg|gif|png)$/i', $mimeType)) { |
1210
|
|
|
$this->debug(3, "Remote file has invalid mime type: $mimeType"); |
1211
|
|
|
@unlink($this->cachefile); |
1212
|
|
|
touch($this->cachefile); |
1213
|
|
|
$this->error("The remote file is not a valid image. Mimetype = '" . $mimeType . "'" . $tempfile); |
1214
|
|
|
|
1215
|
|
|
return false; |
1216
|
|
|
} |
1217
|
|
|
if ($this->processImageAndWriteToCache($tempfile)) { |
1218
|
|
|
$this->debug(3, 'Image processed successfully. Serving from cache'); |
1219
|
|
|
|
1220
|
|
|
return $this->serveCacheFile(); |
1221
|
|
|
} |
1222
|
|
|
|
1223
|
|
|
return false; |
1224
|
|
|
} |
1225
|
|
|
|
1226
|
|
|
/** |
1227
|
|
|
* @param $h |
1228
|
|
|
* @param $d |
1229
|
|
|
* |
1230
|
|
|
* @return int |
1231
|
|
|
*/ |
1232
|
|
|
public static function curlWrite($h, $d) |
|
|
|
|
1233
|
|
|
{ |
1234
|
|
|
fwrite(self::$curlFH, $d); |
|
|
|
|
1235
|
|
|
self::$curlDataWritten += \mb_strlen($d); |
1236
|
|
|
if (self::$curlDataWritten > MAX_FILE_SIZE) { |
1237
|
|
|
return 0; |
1238
|
|
|
} |
1239
|
|
|
|
1240
|
|
|
return mb_strlen($d); |
1241
|
|
|
} |
1242
|
|
|
|
1243
|
|
|
/** |
1244
|
|
|
* @return bool |
1245
|
|
|
*/ |
1246
|
|
|
protected function serveCacheFile() |
1247
|
|
|
{ |
1248
|
|
|
$this->debug(3, "Serving {$this->cachefile}"); |
1249
|
|
|
if (!is_file($this->cachefile)) { |
1250
|
|
|
$this->error("serveCacheFile called in timthumb but we couldn't find the cached file."); |
1251
|
|
|
|
1252
|
|
|
return false; |
1253
|
|
|
} |
1254
|
|
|
$fp = fopen($this->cachefile, 'rb'); |
1255
|
|
|
if (!$fp) { |
|
|
|
|
1256
|
|
|
return $this->error('Could not open cachefile.'); |
1257
|
|
|
} |
1258
|
|
|
fseek($fp, mb_strlen($this->filePrependSecurityBlock), SEEK_SET); |
1259
|
|
|
$imgType = fread($fp, 3); |
1260
|
|
|
fseek($fp, 3, SEEK_CUR); |
1261
|
|
|
if (ftell($fp) != \mb_strlen($this->filePrependSecurityBlock) + 6) { |
1262
|
|
|
@unlink($this->cachefile); |
|
|
|
|
1263
|
|
|
|
1264
|
|
|
return $this->error('The cached image file seems to be corrupt.'); |
1265
|
|
|
} |
1266
|
|
|
$imageDataSize = filesize($this->cachefile) - (mb_strlen($this->filePrependSecurityBlock) + 6); |
1267
|
|
|
$this->sendImageHeaders($imgType, $imageDataSize); |
1268
|
|
|
$bytesSent = @fpassthru($fp); |
1269
|
|
|
fclose($fp); |
1270
|
|
|
if ($bytesSent > 0) { |
1271
|
|
|
return true; |
1272
|
|
|
} |
1273
|
|
|
$content = file_get_contents($this->cachefile); |
1274
|
|
|
if (false !== $content) { |
1275
|
|
|
$content = mb_substr($content, mb_strlen($this->filePrependSecurityBlock) + 6); |
1276
|
|
|
echo $content; |
1277
|
|
|
$this->debug(3, 'Served using file_get_contents and echo'); |
1278
|
|
|
|
1279
|
|
|
return true; |
1280
|
|
|
} |
1281
|
|
|
$this->error('Cache file could not be loaded.'); |
1282
|
|
|
|
1283
|
|
|
return false; |
1284
|
|
|
} |
1285
|
|
|
|
1286
|
|
|
/** |
1287
|
|
|
* @param $mimeType |
1288
|
|
|
* @param $dataSize |
1289
|
|
|
* |
1290
|
|
|
* @return bool |
1291
|
|
|
*/ |
1292
|
|
|
protected function sendImageHeaders($mimeType, $dataSize) |
1293
|
|
|
{ |
1294
|
|
|
if (!preg_match('/^image\//i', $mimeType)) { |
1295
|
|
|
$mimeType = 'image/' . $mimeType; |
1296
|
|
|
} |
1297
|
|
|
if ('image/jpg' === \mb_strtolower($mimeType)) { |
1298
|
|
|
$mimeType = 'image/jpeg'; |
1299
|
|
|
} |
1300
|
|
|
$gmdateExpires = gmdate('D, d M Y H:i:s', strtotime('now +10 days')) . ' GMT'; |
1301
|
|
|
$gmdate_modified = gmdate('D, d M Y H:i:s') . ' GMT'; |
1302
|
|
|
// send content headers then display image |
1303
|
|
|
header('Content-Type: ' . $mimeType); |
1304
|
|
|
header('Accept-Ranges: none'); //Changed this because we don't accept range requests |
1305
|
|
|
header('Last-Modified: ' . $gmdate_modified); |
1306
|
|
|
header('Content-Length: ' . $dataSize); |
1307
|
|
|
if (BROWSER_CACHE_DISABLE) { |
1308
|
|
|
$this->debug(3, 'Browser cache is disabled so setting non-caching headers.'); |
1309
|
|
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); |
1310
|
|
|
header('Pragma: no-cache'); |
1311
|
|
|
header('Expires: ' . gmdate('D, d M Y H:i:s', time())); |
1312
|
|
|
} else { |
1313
|
|
|
$this->debug(3, 'Browser caching is enabled'); |
1314
|
|
|
header('Cache-Control: max-age=' . BROWSER_CACHE_MAX_AGE . ', must-revalidate'); |
1315
|
|
|
header('Expires: ' . $gmdateExpires); |
1316
|
|
|
} |
1317
|
|
|
|
1318
|
|
|
return true; |
1319
|
|
|
} |
1320
|
|
|
|
1321
|
|
|
protected function securityChecks() |
1322
|
|
|
{ |
1323
|
|
|
} |
1324
|
|
|
|
1325
|
|
|
/** |
1326
|
|
|
* @param $property |
1327
|
|
|
* @param string $default |
1328
|
|
|
* |
1329
|
|
|
* @return string |
1330
|
|
|
*/ |
1331
|
|
|
protected function param($property, $default = '') |
1332
|
|
|
{ |
1333
|
|
|
if (isset($_GET[$property])) { |
1334
|
|
|
return Request::getString($property, '', 'GET'); |
1335
|
|
|
} |
1336
|
|
|
|
1337
|
|
|
return $default; |
1338
|
|
|
} |
1339
|
|
|
|
1340
|
|
|
/** |
1341
|
|
|
* @param $mimeType |
1342
|
|
|
* @param $src |
1343
|
|
|
* |
1344
|
|
|
* @return resource |
1345
|
|
|
*/ |
1346
|
|
|
protected function openImage($mimeType, $src) |
1347
|
|
|
{ |
1348
|
|
|
$image = ''; |
1349
|
|
|
switch ($mimeType) { |
1350
|
|
|
case 'image/jpeg': |
1351
|
|
|
|
1352
|
|
|
$image = imagecreatefromjpeg($src); |
1353
|
|
|
|
1354
|
|
|
break; |
1355
|
|
|
case 'image/png': |
1356
|
|
|
$image = imagecreatefrompng($src); |
1357
|
|
|
imagealphablending($image, true); |
1358
|
|
|
imagesavealpha($image, true); |
1359
|
|
|
|
1360
|
|
|
break; |
1361
|
|
|
case 'image/gif': |
1362
|
|
|
$image = imagecreatefromgif($src); |
1363
|
|
|
|
1364
|
|
|
break; |
1365
|
|
|
default: |
1366
|
|
|
$this->error('Unrecognised mimeType'); |
1367
|
|
|
} |
1368
|
|
|
|
1369
|
|
|
return $image; |
|
|
|
|
1370
|
|
|
} |
1371
|
|
|
|
1372
|
|
|
/** |
1373
|
|
|
* @return string |
1374
|
|
|
*/ |
1375
|
|
|
protected function getIP() |
1376
|
|
|
{ |
1377
|
|
|
$rem = @$_SERVER['REMOTE_ADDR']; |
1378
|
|
|
$ff = @$_SERVER['HTTP_X_FORWARDED_FOR']; |
1379
|
|
|
$ci = @$_SERVER['HTTP_CLIENT_IP']; |
1380
|
|
|
if (preg_match('/^(?:192\.168|172\.16|10\.|127\.)/', $rem)) { |
1381
|
|
|
if ($ff) { |
1382
|
|
|
return $ff; |
1383
|
|
|
} |
1384
|
|
|
if ($ci) { |
1385
|
|
|
return $ci; |
1386
|
|
|
} |
1387
|
|
|
|
1388
|
|
|
return $rem; |
1389
|
|
|
} |
1390
|
|
|
if ($rem) { |
1391
|
|
|
return $rem; |
1392
|
|
|
} |
1393
|
|
|
if ($ff) { |
1394
|
|
|
return $ff; |
1395
|
|
|
} |
1396
|
|
|
if ($ci) { |
1397
|
|
|
return $ci; |
1398
|
|
|
} |
1399
|
|
|
|
1400
|
|
|
return 'UNKNOWN'; |
1401
|
|
|
} |
1402
|
|
|
|
1403
|
|
|
/** |
1404
|
|
|
* @param $level |
1405
|
|
|
* @param $msg |
1406
|
|
|
*/ |
1407
|
|
|
protected function debug($level, $msg) |
1408
|
|
|
{ |
1409
|
|
|
if (DEBUG_ON && $level <= DEBUG_LEVEL) { |
1410
|
|
|
$execTime = sprintf('%.6f', microtime(true) - $this->startTime); |
1411
|
|
|
$tick = sprintf('%.6f', 0); |
1412
|
|
|
if ($this->lastBenchTime > 0) { |
1413
|
|
|
$tick = sprintf('%.6f', microtime(true) - $this->lastBenchTime); |
1414
|
|
|
} |
1415
|
|
|
$this->lastBenchTime = microtime(true); |
1416
|
|
|
error_log('TimThumb Debug line ' . __LINE__ . " [$execTime : $tick]: $msg"); |
1417
|
|
|
} |
1418
|
|
|
} |
1419
|
|
|
|
1420
|
|
|
/** |
1421
|
|
|
* @param $msg |
1422
|
|
|
* |
1423
|
|
|
* @return bool |
1424
|
|
|
*/ |
1425
|
|
|
protected function sanityFail($msg) |
1426
|
|
|
{ |
1427
|
|
|
return $this->error("There is a problem in the timthumb code. Message: Please report this error at <a href='https://code.google.com/p/timthumb/issues/list'>timthumb's bug tracking page</a>: $msg"); |
1428
|
|
|
} |
1429
|
|
|
|
1430
|
|
|
/** |
1431
|
|
|
* @param $file |
1432
|
|
|
* |
1433
|
|
|
* @return string |
1434
|
|
|
*/ |
1435
|
|
|
protected function getMimeType($file) |
1436
|
|
|
{ |
1437
|
|
|
$info = getimagesize($file); |
1438
|
|
|
if (is_array($info) && $info['mime']) { |
1439
|
|
|
return $info['mime']; |
1440
|
|
|
} |
1441
|
|
|
|
1442
|
|
|
return ''; |
1443
|
|
|
} |
1444
|
|
|
|
1445
|
|
|
protected function setMemoryLimit() |
1446
|
|
|
{ |
1447
|
|
|
$inimem = ini_get('memory_limit'); |
1448
|
|
|
$inibytes = self::returnBytes($inimem); |
1449
|
|
|
$ourbytes = self::returnBytes(MEMORY_LIMIT); |
1450
|
|
|
if ($inibytes < $ourbytes) { |
1451
|
|
|
ini_set('memory_limit', MEMORY_LIMIT); |
1452
|
|
|
$this->debug(3, "Increased memory from $inimem to " . MEMORY_LIMIT); |
1453
|
|
|
} else { |
1454
|
|
|
$this->debug(3, 'Not adjusting memory size because the current setting is ' . $inimem . ' and our size of ' . MEMORY_LIMIT . ' is smaller.'); |
1455
|
|
|
} |
1456
|
|
|
} |
1457
|
|
|
|
1458
|
|
|
/** |
1459
|
|
|
* @param $sizeString |
1460
|
|
|
* |
1461
|
|
|
* @return int |
1462
|
|
|
*/ |
1463
|
|
|
protected static function returnBytes($sizeString) |
1464
|
|
|
{ |
1465
|
|
|
switch (mb_substr($sizeString, -1)) { |
1466
|
|
|
case 'M': |
1467
|
|
|
|
1468
|
|
|
case 'm': |
1469
|
|
|
|
1470
|
|
|
return (int)$sizeString * 1048576; |
1471
|
|
|
case 'K': |
1472
|
|
|
case 'k': |
1473
|
|
|
|
1474
|
|
|
return (int)$sizeString * 1024; |
1475
|
|
|
case 'G': |
1476
|
|
|
case 'g': |
1477
|
|
|
|
1478
|
|
|
return (int)$sizeString * 1073741824; |
1479
|
|
|
default: |
1480
|
|
|
return $sizeString; |
1481
|
|
|
} |
1482
|
|
|
} |
1483
|
|
|
|
1484
|
|
|
/** |
1485
|
|
|
* @param $url |
1486
|
|
|
* @param $tempfile |
1487
|
|
|
* |
1488
|
|
|
* @return bool |
1489
|
|
|
*/ |
1490
|
|
|
protected function getURL($url, $tempfile) |
1491
|
|
|
{ |
1492
|
|
|
$this->lastURLError = false; |
1493
|
|
|
$url = preg_replace('/ /', '%20', $url); |
1494
|
|
|
if (function_exists('curl_init')) { |
1495
|
|
|
$this->debug(3, 'Curl is installed so using it to fetch URL.'); |
1496
|
|
|
self::$curlFH = fopen($tempfile, 'wb'); |
1497
|
|
|
if (!self::$curlFH) { |
1498
|
|
|
$this->error("Could not open $tempfile for writing."); |
1499
|
|
|
|
1500
|
|
|
return false; |
1501
|
|
|
} |
1502
|
|
|
self::$curlDataWritten = 0; |
1503
|
|
|
$this->debug(3, "Fetching url with curl: $url"); |
1504
|
|
|
$curl = curl_init($url); |
1505
|
|
|
curl_setopt($curl, CURLOPT_TIMEOUT, CURL_TIMEOUT); |
1506
|
|
|
curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.122 Safari/534.30'); |
1507
|
|
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); |
1508
|
|
|
curl_setopt($curl, CURLOPT_HEADER, 0); |
1509
|
|
|
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true); //was false before |
1510
|
|
|
curl_setopt($curl, CURLOPT_WRITEFUNCTION, 'timthumb::curlWrite'); |
1511
|
|
|
@curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); |
|
|
|
|
1512
|
|
|
@curl_setopt($curl, CURLOPT_MAXREDIRS, 10); |
1513
|
|
|
|
1514
|
|
|
$curlResult = curl_exec($curl); |
1515
|
|
|
fclose(self::$curlFH); |
1516
|
|
|
$httpStatus = curl_getinfo($curl, CURLINFO_HTTP_CODE); |
1517
|
|
|
if (404 == $httpStatus) { |
1518
|
|
|
$this->set404(); |
1519
|
|
|
} |
1520
|
|
|
if (302 == $httpStatus) { |
1521
|
|
|
$this->error('External Image is Redirecting. Try alternate image url'); |
1522
|
|
|
|
1523
|
|
|
return false; |
1524
|
|
|
} |
1525
|
|
|
if ($curlResult) { |
1526
|
|
|
curl_close($curl); |
1527
|
|
|
|
1528
|
|
|
return true; |
1529
|
|
|
} |
1530
|
|
|
$this->lastURLError = curl_error($curl); |
1531
|
|
|
curl_close($curl); |
1532
|
|
|
|
1533
|
|
|
return false; |
1534
|
|
|
} |
1535
|
|
|
$img = @file_get_contents($url); |
1536
|
|
|
if (false === $img) { |
1537
|
|
|
$err = error_get_last(); |
1538
|
|
|
$this->lastURLError = $err; |
1539
|
|
|
if (is_array($err) && $err['message']) { |
1540
|
|
|
$this->lastURLError = $err['message']; |
1541
|
|
|
} |
1542
|
|
|
if (false !== mb_strpos($this->lastURLError, '404')) { |
1543
|
|
|
$this->set404(); |
1544
|
|
|
} |
1545
|
|
|
|
1546
|
|
|
return false; |
1547
|
|
|
} |
1548
|
|
|
if (!file_put_contents($tempfile, $img)) { |
1549
|
|
|
$this->error("Could not write to $tempfile."); |
1550
|
|
|
|
1551
|
|
|
return false; |
1552
|
|
|
} |
1553
|
|
|
|
1554
|
|
|
return true; |
1555
|
|
|
} |
1556
|
|
|
|
1557
|
|
|
/** |
1558
|
|
|
* @param $file |
1559
|
|
|
* |
1560
|
|
|
* @return bool |
1561
|
|
|
*/ |
1562
|
|
|
protected function serveImg($file) |
1563
|
|
|
{ |
1564
|
|
|
$s = getimagesize($file); |
1565
|
|
|
if (!($s && $s['mime'])) { |
|
|
|
|
1566
|
|
|
return false; |
1567
|
|
|
} |
1568
|
|
|
header('Content-Type: ' . $s['mime']); |
1569
|
|
|
header('Content-Length: ' . filesize($file)); |
1570
|
|
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); |
1571
|
|
|
header('Pragma: no-cache'); |
1572
|
|
|
$bytes = @readfile($file); |
1573
|
|
|
if ($bytes > 0) { |
1574
|
|
|
return true; |
1575
|
|
|
} |
1576
|
|
|
$content = @file_get_contents($file); |
1577
|
|
|
if (false !== $content) { |
1578
|
|
|
echo $content; |
1579
|
|
|
|
1580
|
|
|
return true; |
1581
|
|
|
} |
1582
|
|
|
|
1583
|
|
|
return false; |
1584
|
|
|
} |
1585
|
|
|
|
1586
|
|
|
protected function set404() |
1587
|
|
|
{ |
1588
|
|
|
$this->is404 = true; |
1589
|
|
|
} |
1590
|
|
|
|
1591
|
|
|
/** |
1592
|
|
|
* @return bool |
1593
|
|
|
*/ |
1594
|
|
|
protected function is404() |
1595
|
|
|
{ |
1596
|
|
|
return $this->is404; |
1597
|
|
|
} |
1598
|
|
|
} |
1599
|
|
|
|
Let?s assume that you have a directory layout like this:
and let?s assume the following content of
Bar.php
:If both files
OtherDir/Foo.php
andSomeDir/Foo.php
are loaded in the same runtime, you will see a PHP error such as the following:PHP Fatal error: Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php
However, as
OtherDir/Foo.php
does not necessarily have to be loaded and the error is only triggered if it is loaded beforeOtherDir/Bar.php
, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias: