Passed
Branch master (7e303a)
by Michael
02:15
created

thumb.php (4 issues)

1
<?php
2
/**
3
 * TimThumb by Ben Gillbanks and Mark Maunder
4
 * Based on work done by Tim McDaniels and Darren Hoyt
5
 * http://code.google.com/p/timthumb/
6
 *
7
 * GNU General Public License, version 2
8
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
9
 *
10
 * Examples and documentation available on the project homepage
11
 * http://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="http://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. http://yoursite.com/path/to/timthumb.php?src=http://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($ALLOWED_SITES)) {
234
    $ALLOWED_SITES = [
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);
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
295
    }
296
297
    /**
298
     *
299
     */
300
    public function __construct()
301
    {
302
        global $ALLOWED_SITES;
303
        $this->startTime = microtime(true);
304
        date_default_timezone_set('UTC');
305
        $this->debug(1, 'Starting new request from ' . $this->getIP() . ' to ' . Request::getString('REQUEST_URI', '', 'SERVER'));
306
        $this->calcDocRoot();
307
        //On windows systems I'm assuming fileinode returns an empty string or a number that doesn't change. Check this.
308
        $this->salt = @filemtime(__FILE__) . '-' . @fileinode(__FILE__);
309
        $this->debug(3, 'Salt is: ' . $this->salt);
310
        if (FILE_CACHE_DIRECTORY) {
311
            if (!is_dir(FILE_CACHE_DIRECTORY)) {
312
                @mkdir(FILE_CACHE_DIRECTORY);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

312
                /** @scrutinizer ignore-unhandled */ @mkdir(FILE_CACHE_DIRECTORY);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
313
                if (!is_dir(FILE_CACHE_DIRECTORY)) {
314
                    $this->error('Could not create the file cache directory.');
315
316
                    return false;
317
                }
318
            }
319
            $this->cacheDirectory = FILE_CACHE_DIRECTORY;
320
            if (!touch($this->cacheDirectory . '/index.html')) {
321
                $this->error('Could not create the index.html file - to fix this create an empty file named index.html file in the cache directory.');
322
            }
323
        } else {
324
            $this->cacheDirectory = sys_get_temp_dir();
325
        }
326
        //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.
327
        $this->cleanCache();
328
329
        $this->myHost = preg_replace('/^www\./i', '', $_SERVER['HTTP_HOST']);
330
        $this->src    = $this->param('src');
331
        $this->url    = parse_url($this->src);
332
        $this->src    = preg_replace('/https?:\/\/(?:www\.)?' . $this->myHost . '/i', '', $this->src);
333
334
        if (strlen($this->src) <= 3) {
335
            $this->error('No image specified');
336
337
            return false;
338
        }
339
        if (BLOCK_EXTERNAL_LEECHERS && array_key_exists('HTTP_REFERER', $_SERVER) && (!preg_match('/^https?:\/\/(?:www\.)?' . $this->myHost . '(?:$|\/)/i', $_SERVER['HTTP_REFERER']))) {
340
            // base64 encoded red image that says 'no hotlinkers'
341
            // nothing to worry about! :)
342
            $imgData = base64_decode("R0lGODlhUAAMAIAAAP8AAP///yH5BAAHAP8ALAAAAABQAAwAAAJpjI+py+0Po5y0OgAMjjv01YUZ\nOGplhWXfNa6JCLnWkXplrcBmW+spbwvaVr/cDyg7IoFC2KbYVC2NQ5MQ4ZNao9Ynzjl9ScNYpneb\nDULB3RP6JuPuaGfuuV4fumf8PuvqFyhYtjdoeFgAADs=");
343
            header('Content-Type: image/gif');
344
            header('Content-Length: ' . strlen($imgData));
345
            header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
346
            header('Pragma: no-cache');
347
            header('Expires: ' . gmdate('D, d M Y H:i:s', time()));
348
            echo $imgData;
349
350
            return false;
351
        }
352
        if (preg_match('/^https?:\/\/[^\/]+/i', $this->src)) {
353
            $this->debug(2, 'Is a request for an external URL: ' . $this->src);
354
            $this->isURL = true;
355
        } else {
356
            $this->debug(2, 'Is a request for an internal file: ' . $this->src);
357
        }
358
        if ($this->isURL && (!ALLOW_EXTERNAL)) {
359
            $this->error('You are not allowed to fetch images from an external website.');
360
361
            return false;
362
        }
363
        if ($this->isURL) {
364
            if (ALLOW_ALL_EXTERNAL_SITES) {
365
                $this->debug(2, 'Fetching from all external sites is enabled.');
366
            } else {
367
                $this->debug(2, 'Fetching only from selected external sites is enabled.');
368
                $allowed = false;
369
                foreach ($ALLOWED_SITES as $site) {
370
                    if ((strtolower($this->url['host']) === strtolower($site)) || (strtolower(substr($this->url['host'], -strlen($site) - 1)) === strtolower(".$site"))) {
371
                        $this->debug(3, "URL hostname {$this->url['host']} matches $site so allowing.");
372
                        $allowed = true;
373
                    }
374
                }
375
                if (!$allowed) {
376
                    return $this->error('You may not fetch images from that site. To enable this site in timthumb, you can either add it to $ALLOWED_SITES and set ALLOW_EXTERNAL=true. Or you can set ALLOW_ALL_EXTERNAL_SITES=true, depending on your security needs.');
377
                }
378
            }
379
        }
380
381
        $cachePrefix = ($this->isURL ? '_ext_' : '_int_');
382
        if ($this->isURL) {
383
            $arr = explode('&', $_SERVER ['QUERY_STRING']);
384
            asort($arr);
385
            $this->cachefile = $this->cacheDirectory . '/' . FILE_CACHE_PREFIX . $cachePrefix . md5($this->salt . implode('', $arr) . $this->fileCacheVersion) . FILE_CACHE_SUFFIX;
386
        } else {
387
            $this->localImage = $this->getLocalImagePath($this->src);
388
            if (!$this->localImage) {
389
                $this->debug(1, "Could not find the local image: {$this->localImage}");
390
                $this->error('Could not find the internal image you specified.');
391
                $this->set404();
392
393
                return false;
394
            }
395
            $this->debug(1, "Local image path is {$this->localImage}");
396
            $this->localImageMTime = @filemtime($this->localImage);
397
            //We include the mtime of the local file in case in changes on disk.
398
            $this->cachefile = $this->cacheDirectory . '/' . FILE_CACHE_PREFIX . $cachePrefix . md5($this->salt . $this->localImageMTime . $_SERVER ['QUERY_STRING'] . $this->fileCacheVersion) . FILE_CACHE_SUFFIX;
399
        }
400
        $this->debug(2, 'Cache file is: ' . $this->cachefile);
401
402
        return true;
403
    }
404
405
    public function __destruct()
406
    {
407
        foreach ($this->toDeletes as $del) {
408
            $this->debug(2, "Deleting temp file $del");
409
            @unlink($del);
410
        }
411
    }
412
413
    /**
414
     * @return bool
415
     */
416
    public function run()
417
    {
418
        if ($this->isURL) {
419
            if (!ALLOW_EXTERNAL) {
420
                $this->debug(1, 'Got a request for an external image but ALLOW_EXTERNAL is disabled so returning error msg.');
421
                $this->error('You are not allowed to fetch images from an external website.');
422
423
                return false;
424
            }
425
            $this->debug(3, 'Got request for external image. Starting serveExternalImage.');
426
            if ($this->param('webshot')) {
427
                if (WEBSHOT_ENABLED) {
428
                    $this->debug(3, 'webshot param is set, so we\'re going to take a webshot.');
429
                    $this->serveWebshot();
430
                } else {
431
                    $this->error('You added the webshot parameter but webshots are disabled on this server. You need to set WEBSHOT_ENABLED === true to enable webshots.');
432
                }
433
            } else {
434
                $this->debug(3, 'webshot is NOT set so we\'re going to try to fetch a regular image.');
435
                $this->serveExternalImage();
436
            }
437
        } else {
438
            $this->debug(3, 'Got request for internal image. Starting serveInternalImage()');
439
            $this->serveInternalImage();
440
        }
441
442
        return true;
443
    }
444
445
    /**
446
     * @return bool
447
     */
448
    protected function handleErrors()
449
    {
450
        if ($this->haveErrors()) {
451
            if (NOT_FOUND_IMAGE && $this->is404()) {
452
                if ($this->serveImg(NOT_FOUND_IMAGE)) {
453
                    exit(0);
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
454
                } else {
455
                    $this->error('Additionally, the 404 image that is configured could not be found or there was an error serving it.');
456
                }
457
            }
458
            if (ERROR_IMAGE) {
459
                if ($this->serveImg(ERROR_IMAGE)) {
460
                    exit(0);
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
461
                } else {
462
                    $this->error('Additionally, the error image that is configured could not be found or there was an error serving it.');
463
                }
464
            }
465
            $this->serveErrors();
466
            exit(0);
467
        }
468
469
        return false;
470
    }
471
472
    /**
473
     * @return bool
474
     */
475
    protected function tryBrowserCache()
476
    {
477
        if (BROWSER_CACHE_DISABLE) {
478
            $this->debug(3, 'Browser caching is disabled');
479
480
            return false;
481
        }
482
       if (\Xmf\Request::hasVar('HTTP_IF_MODIFIED_SINCE', 'SERVER')) {
483
            $this->debug(3, 'Got a conditional get');
484
            $mtime = false;
485
            //We've already checked if the real file exists in the constructor
486
            if (!is_file($this->cachefile)) {
487
                //If we don't have something cached, regenerate the cached image.
488
                return false;
489
            }
490
            if ($this->localImageMTime) {
491
                $mtime = $this->localImageMTime;
492
                $this->debug(3, "Local real file's modification time is $mtime");
493
            } elseif (is_file($this->cachefile)) { //If it's not a local request then use the mtime of the cached file to determine the 304
494
                $mtime = @filemtime($this->cachefile);
495
                $this->debug(3, "Cached file's modification time is $mtime");
496
            }
497
            if (!$mtime) {
498
                return false;
499
            }
500
501
            $iftime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
502
            $this->debug(3, "The conditional get's if-modified-since unixtime is $iftime");
503
            if ($iftime < 1) {
504
                $this->debug(3, 'Got an invalid conditional get modified since time. Returning false.');
505
506
                return false;
507
            }
508
            if ($iftime < $mtime) { //Real file or cache file has been modified since last request, so force refetch.
509
                $this->debug(3, 'File has been modified since last fetch.');
510
511
                return false;
512
            } else { //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
521
        return false;
522
    }
523
524
    /**
525
     * @return bool
526
     */
527
    protected function tryServerCache()
528
    {
529
        $this->debug(3, 'Trying server cache');
530
        if (file_exists($this->cachefile)) {
531
            $this->debug(3, "Cachefile {$this->cachefile} exists");
532
            if ($this->isURL) {
533
                $this->debug(3, 'This is an external request, so checking if the cachefile is empty which means the request failed previously.');
534
                if (filesize($this->cachefile) < 1) {
535
                    $this->debug(3, 'Found an empty cachefile indicating a failed earlier request. Checking how old it is.');
536
                    //Fetching error occured previously
537
                    if (time() - @filemtime($this->cachefile) > WAIT_BETWEEN_FETCH_ERRORS) {
538
                        $this->debug(3, 'File is older than ' . WAIT_BETWEEN_FETCH_ERRORS . ' seconds. Deleting and returning false so app can try and load file.');
539
                        @unlink($this->cachefile);
540
541
                        return false; //to indicate we didn't serve from cache and app should try and load
542
                    } else {
543
                        $this->debug(3, 'Empty cachefile is still fresh so returning message saying we had an error fetching this image from remote host.');
544
                        $this->set404();
545
                        $this->error('An error occured fetching image.');
546
547
                        return false;
548
                    }
549
                }
550
            } else {
551
                $this->debug(3, "Trying to serve cachefile {$this->cachefile}");
552
            }
553
            if ($this->serveCacheFile()) {
554
                $this->debug(3, "Succesfully served cachefile {$this->cachefile}");
555
556
                return true;
557
            } else {
558
                $this->debug(3, "Failed to serve cachefile {$this->cachefile} - Deleting it from cache.");
559
                //Image serving failed. We can't retry at this point, but lets remove it from cache so the next request recreates it
560
                @unlink($this->cachefile);
561
562
                return true;
563
            }
564
        }
565
566
        return null;
567
    }
568
569
    /**
570
     * @param $err
571
     *
572
     * @return bool
573
     */
574
    protected function error($err)
575
    {
576
        $this->debug(3, "Adding error message: $err");
577
        $this->errors[] = $err;
578
579
        return false;
580
    }
581
582
    /**
583
     * @return bool
584
     */
585
    protected function haveErrors()
586
    {
587
        return count($this->errors) > 0;
588
    }
589
590
    protected function serveErrors()
591
    {
592
        header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
593
        if (!DISPLAY_ERROR_MESSAGES) {
594
            return;
595
        }
596
        $html = '<ul>';
597
        foreach ($this->errors as $err) {
598
            $html .= '<li>' . htmlentities($err, ENT_QUOTES | ENT_HTML5) . '</li>';
599
        }
600
        $html .= '</ul>';
601
        echo '<h1>A TimThumb error has occured</h1>The following error(s) occured:<br>' . $html . '<br>';
602
        echo '<br>Query String : ' . htmlentities($_SERVER['QUERY_STRING'], ENT_QUOTES);
603
        echo '<br>TimThumb version : ' . VERSION . '</pre>';
604
    }
605
606
    /**
607
     * @return bool
608
     */
609
    protected function serveInternalImage()
610
    {
611
        $this->debug(3, "Local image path is $this->localImage");
612
        if (!$this->localImage) {
613
            $this->sanityFail('localImage not set after verifying it earlier in the code.');
614
615
            return false;
616
        }
617
        $fileSize = filesize($this->localImage);
618
        if ($fileSize > MAX_FILE_SIZE) {
619
            $this->error('The file you specified is greater than the maximum allowed file size.');
620
621
            return false;
622
        }
623
        if ($fileSize <= 0) {
624
            $this->error('The file you specified is <= 0 bytes.');
625
626
            return false;
627
        }
628
        $this->debug(3, 'Calling processImageAndWriteToCache() for local image.');
629
        if ($this->processImageAndWriteToCache($this->localImage)) {
630
            $this->serveCacheFile();
631
632
            return true;
633
        } else {
634
            return false;
635
        }
636
    }
637
638
    /**
639
     * @return bool|void
640
     */
641
    protected function cleanCache()
642
    {
643
        if (FILE_CACHE_TIME_BETWEEN_CLEANS < 0) {
644
            return null;
645
        }
646
        $this->debug(3, 'cleanCache() called');
647
        $lastCleanFile = $this->cacheDirectory . '/timthumb_cacheLastCleanTime.touch';
648
649
        //If this is a new timthumb installation we need to create the file
650
        if (!is_file($lastCleanFile)) {
651
            $this->debug(1, "File tracking last clean doesn't exist. Creating $lastCleanFile");
652
            if (!touch($lastCleanFile)) {
653
                $this->error('Could not create cache clean timestamp file.');
654
            }
655
656
            return null;
657
        }
658
        if (@filemtime($lastCleanFile) < (time() - FILE_CACHE_TIME_BETWEEN_CLEANS)) { //Cache was last cleaned more than 1 day ago
659
            $this->debug(1, 'Cache was last cleaned more than ' . FILE_CACHE_TIME_BETWEEN_CLEANS . ' seconds ago. Cleaning now.');
660
            // Very slight race condition here, but worst case we'll have 2 or 3 servers cleaning the cache simultaneously once a day.
661
            if (!touch($lastCleanFile)) {
662
                $this->error('Could not create cache clean timestamp file.');
663
            }
664
            $files = glob($this->cacheDirectory . '/*' . FILE_CACHE_SUFFIX);
665
            if ($files) {
666
                $timeAgo = time() - FILE_CACHE_MAX_FILE_AGE;
667
                foreach ($files as $file) {
668
                    if (@filemtime($file) < $timeAgo) {
669
                        $this->debug(3, "Deleting cache file $file older than max age: " . FILE_CACHE_MAX_FILE_AGE . ' seconds');
670
                        @unlink($file);
671
                    }
672
                }
673
            }
674
675
            return true;
676
        } else {
677
            $this->debug(3, 'Cache was cleaned less than ' . FILE_CACHE_TIME_BETWEEN_CLEANS . ' seconds ago so no cleaning needed.');
678
        }
679
680
        return false;
681
    }
682
683
    /**
684
     * @param $localImage
685
     *
686
     * @return bool
687
     */
688
    protected function processImageAndWriteToCache($localImage)
689
    {
690
        $sData    = getimagesize($localImage);
691
        $origType = $sData[2];
692
        $mimeType = $sData['mime'];
693
694
        $this->debug(3, "Mime type of image is $mimeType");
695
        if (!preg_match('/^image\/(?:gif|jpg|jpeg|png)$/i', $mimeType)) {
696
            return $this->error('The image being resized is not a valid gif, jpg or png.');
697
        }
698
699
        if (!function_exists('imagecreatetruecolor')) {
700
            return $this->error('GD Library Error: imagecreatetruecolor does not exist - please contact your webhost and ask them to install the GD library');
701
        }
702
703
        if (defined('IMG_FILTER_NEGATE') && function_exists('imagefilter')) {
704
            $imageFilters = [
705
                1  => [IMG_FILTER_NEGATE, 0],
706
                2  => [IMG_FILTER_GRAYSCALE, 0],
707
                3  => [IMG_FILTER_BRIGHTNESS, 1],
708
                4  => [IMG_FILTER_CONTRAST, 1],
709
                5  => [IMG_FILTER_COLORIZE, 4],
710
                6  => [IMG_FILTER_EDGEDETECT, 0],
711
                7  => [IMG_FILTER_EMBOSS, 0],
712
                8  => [IMG_FILTER_GAUSSIAN_BLUR, 0],
713
                9  => [IMG_FILTER_SELECTIVE_BLUR, 0],
714
                10 => [IMG_FILTER_MEAN_REMOVAL, 0],
715
                11 => [IMG_FILTER_SMOOTH, 0]
716
            ];
717
        }
718
719
        // get standard input properties
720
        $newWidth     = (int)abs($this->param('w', 0));
721
        $newHeight    = (int)abs($this->param('h', 0));
722
        $zoom_crop    = (int)$this->param('zc', DEFAULT_ZC);
723
        $quality      = (int)abs($this->param('q', DEFAULT_Q));
724
        $align        = $this->cropTop ? 't' : $this->param('a', 'c');
725
        $filters      = $this->param('f', DEFAULT_F);
726
        $sharpen      = (bool)$this->param('s', DEFAULT_S);
727
        $canvas_color = $this->param('cc', DEFAULT_CC);
728
        $canvas_trans = (bool)$this->param('ct', '1');
729
730
        // set default width and height if neither are set already
731
        if (0 == $newWidth && 0 == $newHeight) {
732
            $newWidth  = DEFAULT_WIDTH;
733
            $newHeight = DEFAULT_HEIGHT;
734
        }
735
736
        // ensure size limits can not be abused
737
        $newWidth  = min($newWidth, MAX_WIDTH);
738
        $newHeight = min($newHeight, MAX_HEIGHT);
739
740
        // set memory limit to be able to have enough space to resize larger images
741
        $this->setMemoryLimit();
742
743
        // open the existing image
744
        $image = $this->openImage($mimeType, $localImage);
745
        if (false === $image) {
746
            return $this->error('Unable to open image.');
747
        }
748
749
        // Get original width and height
750
        $width    = imagesx($image);
751
        $height   = imagesy($image);
752
        $origin_x = 0;
753
        $origin_y = 0;
754
755
        // generate new w/h if not provided
756
        if ($newWidth && !$newHeight) {
757
            $newHeight = floor($height * ($newWidth / $width));
758
        } elseif ($newHeight && !$newWidth) {
759
            $newWidth = floor($width * ($newHeight / $height));
760
        }
761
762
        // scale down and add borders
763
        if (3 == $zoom_crop) {
764
            $final_height = $height * ($newWidth / $width);
765
766
            if ($final_height > $newHeight) {
767
                $newWidth = $width * ($newHeight / $height);
768
            } else {
769
                $newHeight = $final_height;
770
            }
771
        }
772
773
        // create a new true color image
774
        $canvas = imagecreatetruecolor($newWidth, $newHeight);
775
        imagealphablending($canvas, false);
776
777
        if (3 == strlen($canvas_color)) { //if is 3-char notation, edit string into 6-char notation
778
            $canvas_color = str_repeat(substr($canvas_color, 0, 1), 2) . str_repeat(substr($canvas_color, 1, 1), 2) . str_repeat(substr($canvas_color, 2, 1), 2);
779
        } elseif (6 != strlen($canvas_color)) {
780
            $canvas_color = DEFAULT_CC; // on error return default canvas color
781
        }
782
783
        $canvas_color_R = hexdec(substr($canvas_color, 0, 2));
784
        $canvas_color_G = hexdec(substr($canvas_color, 2, 2));
785
        $canvas_color_B = hexdec(substr($canvas_color, 4, 2));
786
787
        // Create a new transparent color for image
788
        // If is a png and PNG_IS_TRANSPARENT is false then remove the alpha transparency
789
        // (and if is set a canvas color show it in the background)
790
        if (!PNG_IS_TRANSPARENT && $canvas_trans && preg_match('/^image\/png$/i', $mimeType)) {
791
            $color = imagecolorallocatealpha($canvas, $canvas_color_R, $canvas_color_G, $canvas_color_B, 127);
792
        } else {
793
            $color = imagecolorallocatealpha($canvas, $canvas_color_R, $canvas_color_G, $canvas_color_B, 0);
794
        }
795
796
        // Completely fill the background of the new image with allocated color.
797
        imagefill($canvas, 0, 0, $color);
798
        // scale down and add borders
799
        if (2 == $zoom_crop) {
800
            $final_height = $height * ($newWidth / $width);
801
            if ($final_height > $newHeight) {
802
                $origin_x = $newWidth / 2;
803
                $newWidth = $width * ($newHeight / $height);
804
                $origin_x = round($origin_x - ($newWidth / 2));
805
            } else {
806
                $origin_y  = $newHeight / 2;
807
                $newHeight = $final_height;
808
                $origin_y  = round($origin_y - ($newHeight / 2));
809
            }
810
        }
811
812
        // Restore transparency blending
813
        imagesavealpha($canvas, true);
814
815
        if ($zoom_crop > 0) {
816
            $src_x = $src_y = 0;
817
            $src_w = $width;
818
            $src_h = $height;
819
820
            $cmp_x = $width / $newWidth;
821
            $cmp_y = $height / $newHeight;
822
823
            // calculate x or y coordinate and width or height of source
824
            if ($cmp_x > $cmp_y) {
825
                $src_w = round($width / $cmp_x * $cmp_y);
826
                $src_x = round(($width - ($width / $cmp_x * $cmp_y)) / 2);
827
            } elseif ($cmp_y > $cmp_x) {
828
                $src_h = round($height / $cmp_y * $cmp_x);
829
                $src_y = round(($height - ($height / $cmp_y * $cmp_x)) / 2);
830
            }
831
832
            // positional cropping!
833
            if ($align) {
834
                if (false !== strpos($align, 't')) {
835
                    $src_y = 0;
836
                }
837
                if (false !== strpos($align, 'b')) {
838
                    $src_y = $height - $src_h;
839
                }
840
                if (false !== strpos($align, 'l')) {
841
                    $src_x = 0;
842
                }
843
                if (false !== strpos($align, 'r')) {
844
                    $src_x = $width - $src_w;
845
                }
846
            }
847
848
            imagecopyresampled($canvas, $image, $origin_x, $origin_y, $src_x, $src_y, $newWidth, $newHeight, $src_w, $src_h);
849
        } else {
850
            // copy and resize part of an image with resampling
851
            imagecopyresampled($canvas, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
852
        }
853
854
        if (defined('IMG_FILTER_NEGATE') && '' != $filters && function_exists('imagefilter')) {
855
            // apply filters to image
856
            $filterList = explode('|', $filters);
857
            foreach ($filterList as $fl) {
858
                $filterSettings = explode(',', $fl);
859
                if (isset($imageFilters[$filterSettings[0]])) {
860
                    for ($i = 0; $i < 4; ++$i) {
861
                        if (!isset($filterSettings[$i])) {
862
                            $filterSettings[$i] = null;
863
                        } else {
864
                            $filterSettings[$i] = (int)$filterSettings[$i];
865
                        }
866
                    }
867
868
                    switch ($imageFilters[$filterSettings[0]][1]) {
869
                        case 1:
870
                            imagefilter($canvas, $imageFilters[$filterSettings[0]][0], $filterSettings[1]);
871
                            break;
872
873
                        case 2:
874
                            imagefilter($canvas, $imageFilters[$filterSettings[0]][0], $filterSettings[1], $filterSettings[2]);
875
                            break;
876
877
                        case 3:
878
                            imagefilter($canvas, $imageFilters[$filterSettings[0]][0], $filterSettings[1], $filterSettings[2], $filterSettings[3]);
879
                            break;
880
881
                        case 4:
882
                            imagefilter($canvas, $imageFilters[$filterSettings[0]][0], $filterSettings[1], $filterSettings[2], $filterSettings[3], $filterSettings[4]);
883
                            break;
884
885
                        default:
886
                            imagefilter($canvas, $imageFilters[$filterSettings[0]][0]);
887
                            break;
888
                    }
889
                }
890
            }
891
        }
892
893
        // sharpen image
894
        if ($sharpen && function_exists('imageconvolution')) {
895
            $sharpenMatrix = [
896
                [-1, -1, -1],
897
                [-1, 16, -1],
898
                [-1, -1, -1]
899
            ];
900
901
            $divisor = 8;
902
            $offset  = 0;
903
904
            imageconvolution($canvas, $sharpenMatrix, $divisor, $offset);
905
        }
906
        //Straight from Wordpress core code. Reduces filesize by up to 70% for PNG's
907
        if ((IMAGETYPE_PNG == $origType || IMAGETYPE_GIF == $origType) && function_exists('imageistruecolor') && !imageistruecolor($image) && imagecolortransparent($image) > 0) {
908
            imagetruecolortopalette($canvas, false, imagecolorstotal($image));
909
        }
910
911
        $imgType  = '';
912
        $tempfile = tempnam($this->cacheDirectory, 'timthumb_tmpimg_');
913
        if (preg_match('/^image\/(?:jpg|jpeg)$/i', $mimeType)) {
914
            $imgType = 'jpg';
915
            imagejpeg($canvas, $tempfile, $quality);
916
        } elseif (preg_match('/^image\/png$/i', $mimeType)) {
917
            $imgType = 'png';
918
            imagepng($canvas, $tempfile, floor($quality * 0.09));
919
        } elseif (preg_match('/^image\/gif$/i', $mimeType)) {
920
            $imgType = 'gif';
921
            imagegif($canvas, $tempfile);
922
        } else {
923
            return $this->sanityFail('Could not match mime type after verifying it previously.');
924
        }
925
926
        if ('png' === $imgType && OPTIPNG_ENABLED && OPTIPNG_PATH && @is_file(OPTIPNG_PATH)) {
927
            $exec = OPTIPNG_PATH;
928
            $this->debug(3, "optipng'ing $tempfile");
929
            $presize = filesize($tempfile);
930
            $out     = shell_exec('$exec -o1 $tempfile'); //you can use up to -o7 but it really slows things down
931
            clearstatcache();
932
            $aftersize = filesize($tempfile);
933
            $sizeDrop  = $presize - $aftersize;
934
            if ($sizeDrop > 0) {
935
                $this->debug(1, "optipng reduced size by $sizeDrop");
936
            } elseif ($sizeDrop < 0) {
937
                $this->debug(1, "optipng increased size! Difference was: $sizeDrop");
938
            } else {
939
                $this->debug(1, 'optipng did not change image size.');
940
            }
941
        } elseif ('png' === $imgType && PNGCRUSH_ENABLED && PNGCRUSH_PATH && @is_file(PNGCRUSH_PATH)) {
942
            $exec      = PNGCRUSH_PATH;
943
            $tempfile2 = tempnam($this->cacheDirectory, 'timthumb_tmpimg_');
944
            $this->debug(3, "pngcrush'ing $tempfile to $tempfile2");
945
            $out   = shell_exec('$exec $tempfile $tempfile2');
946
            $todel = '';
947
            if (is_file($tempfile2)) {
948
                $sizeDrop = filesize($tempfile) - filesize($tempfile2);
949
                if ($sizeDrop > 0) {
950
                    $this->debug(1, "pngcrush was succesful and gave a $sizeDrop byte size reduction");
951
                    $todel    = $tempfile;
952
                    $tempfile = $tempfile2;
953
                } else {
954
                    $this->debug(1, "pngcrush did not reduce file size. Difference was $sizeDrop bytes.");
955
                    $todel = $tempfile2;
956
                }
957
            } else {
958
                $this->debug(3, "pngcrush failed with output: $out");
959
                $todel = $tempfile2;
960
            }
961
            @unlink($todel);
962
        }
963
964
        $this->debug(3, 'Rewriting image with security header.');
965
        $tempfile4 = tempnam($this->cacheDirectory, 'timthumb_tmpimg_');
966
        $context   = stream_context_create();
967
        $fp        = fopen($tempfile, 'rb', 0, $context);
968
        file_put_contents($tempfile4, $this->filePrependSecurityBlock . $imgType . ' ?' . '>'); //6 extra bytes, first 3 being image type
969
        file_put_contents($tempfile4, $fp, FILE_APPEND);
970
        fclose($fp);
971
        @unlink($tempfile);
972
        $this->debug(3, 'Locking and replacing cache file.');
973
        $lockFile = $this->cachefile . '.lock';
974
        $fh       = fopen($lockFile, 'wb');
975
        if (!$fh) {
976
            return $this->error('Could not open the lockfile for writing an image.');
977
        }
978
        if (flock($fh, LOCK_EX)) {
979
            @unlink($this->cachefile); //rename generally overwrites, but doing this in case of platform specific quirks. File might not exist yet.
980
            rename($tempfile4, $this->cachefile);
981
            flock($fh, LOCK_UN);
982
            fclose($fh);
983
            @unlink($lockFile);
984
        } else {
985
            fclose($fh);
986
            @unlink($lockFile);
987
            @unlink($tempfile4);
988
989
            return $this->error('Could not get a lock for writing.');
990
        }
991
        $this->debug(3, 'Done image replace with security header. Cleaning up and running cleanCache()');
992
        imagedestroy($canvas);
993
        imagedestroy($image);
994
995
        return true;
996
    }
997
998
    protected function calcDocRoot()
999
    {
1000
        $docRoot = @$_SERVER['DOCUMENT_ROOT'];
1001
        if (defined('LOCAL_FILE_BASE_DIRECTORY')) {
1002
            $docRoot = LOCAL_FILE_BASE_DIRECTORY;
1003
        }
1004
        if (!isset($docRoot)) {
1005
            $this->debug(3, 'DOCUMENT_ROOT is not set. This is probably windows. Starting search 1.');
1006
            if (\Xmf\Request::hasVar('SCRIPT_FILENAME', 'SERVER')) {
1007
                $docRoot = str_replace('\\', '/', substr($_SERVER['SCRIPT_FILENAME'], 0, 0 - strlen($_SERVER['PHP_SELF'])));
1008
                $this->debug(3, "Generated docRoot using SCRIPT_FILENAME and PHP_SELF as: $docRoot");
1009
            }
1010
        }
1011
        if (!isset($docRoot)) {
1012
            $this->debug(3, 'DOCUMENT_ROOT still is not set. Starting search 2.');
1013
            if (\Xmf\Request::hasVar('PATH_TRANSLATED', 'SERVER')) {
1014
                $docRoot = str_replace('\\', '/', substr(str_replace('\\\\', '\\', $_SERVER['PATH_TRANSLATED']), 0, 0 - strlen($_SERVER['PHP_SELF'])));
1015
                $this->debug(3, "Generated docRoot using PATH_TRANSLATED and PHP_SELF as: $docRoot");
1016
            }
1017
        }
1018
        if ($docRoot && '/' !== $_SERVER['DOCUMENT_ROOT']) {
1019
            $docRoot = preg_replace('/\/$/', '', $docRoot);
1020
        }
1021
        $this->debug(3, 'Doc root is: ' . $docRoot);
1022
        $this->docRoot = $docRoot;
1023
    }
1024
1025
    /**
1026
     * @param $src
1027
     *
1028
     * @return bool|string
1029
     */
1030
    protected function getLocalImagePath($src)
1031
    {
1032
        $src = ltrim($src, '/'); //strip off the leading '/'
1033
        if (!$this->docRoot) {
1034
            $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.');
1035
            //We don't support serving images outside the current dir if we don't have a doc root for security reasons.
1036
            $file = preg_replace('/^.*?([^\/\\\\]+)$/', '$1', $src); //strip off any path info and just leave the filename.
1037
            if (is_file($file)) {
1038
                return $this->realpath($file);
1039
            }
1040
1041
            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.");
1042
        } else {
1043
            if (!is_dir($this->docRoot)) {
1044
                $this->error("Server path does not exist. Ensure variable \$_SERVER['DOCUMENT_ROOT'] is set correctly");
1045
            }
1046
        }
1047
1048
        //Do not go past this point without docRoot set
1049
1050
        //Try src under docRoot
1051
        if (file_exists($this->docRoot . '/' . $src)) {
1052
            $this->debug(3, 'Found file as ' . $this->docRoot . '/' . $src);
1053
            $real = $this->realpath($this->docRoot . '/' . $src);
1054
            if (0 === stripos($real, $this->docRoot)) {
1055
                return $real;
1056
            } else {
1057
                $this->debug(1, 'Security block: The file specified occurs outside the document root.');
1058
                //allow search to continue
1059
            }
1060
        }
1061
        //Check absolute paths and then verify the real path is under doc root
1062
        $absolute = $this->realpath('/' . $src);
1063
        if ($absolute && file_exists($absolute)) { //realpath does file_exists check, so can probably skip the exists check here
1064
            $this->debug(3, "Found absolute path: $absolute");
1065
            if (!$this->docRoot) {
1066
                $this->sanityFail('docRoot not set when checking absolute path.');
1067
            }
1068
            if (0 === stripos($absolute, $this->docRoot)) {
1069
                return $absolute;
1070
            } else {
1071
                $this->debug(1, 'Security block: The file specified occurs outside the document root.');
1072
                //and continue search
1073
            }
1074
        }
1075
1076
        $base = $this->docRoot;
1077
1078
        // account for Windows directory structure
1079
        if (false !== strpos($_SERVER['SCRIPT_FILENAME'], ':')) {
1080
            $subDirectories = explode('\\', str_replace($this->docRoot, '', $_SERVER['SCRIPT_FILENAME']));
1081
        } else {
1082
            $subDirectories = explode('/', str_replace($this->docRoot, '', $_SERVER['SCRIPT_FILENAME']));
1083
        }
1084
1085
        foreach ($subDirectories as $sub) {
1086
            $base .= $sub . '/';
1087
            $this->debug(3, 'Trying file as: ' . $base . $src);
1088
            if (file_exists($base . $src)) {
1089
                $this->debug(3, 'Found file as: ' . $base . $src);
1090
                $real = $this->realpath($base . $src);
1091
                if (0 === stripos($real, $this->realpath($this->docRoot))) {
1092
                    return $real;
1093
                } else {
1094
                    $this->debug(1, 'Security block: The file specified occurs outside the document root.');
1095
                    //And continue search
1096
                }
1097
            }
1098
        }
1099
1100
        return false;
1101
    }
1102
1103
    /**
1104
     * @param $path
1105
     *
1106
     * @return string
1107
     */
1108
    protected function realpath($path)
1109
    {
1110
        //try to remove any relative paths
1111
        $removeRelatives = '/\w+\/\.\.\//';
1112
        while (preg_match($removeRelatives, $path)) {
1113
            $path = preg_replace($removeRelatives, '', $path);
1114
        }
1115
        //if any remain use PHP realpath to strip them out, otherwise return $path
1116
        //if using realpath, any symlinks will also be resolved
1117
        return preg_match('#^\.\./|/\.\./#', $path) ? realpath($path) : $path;
1118
    }
1119
1120
    /**
1121
     * @param $name
1122
     */
1123
    protected function toDelete($name)
1124
    {
1125
        $this->debug(3, "Scheduling file $name to delete on destruct.");
1126
        $this->toDeletes[] = $name;
1127
    }
1128
1129
    /**
1130
     * @return bool
1131
     */
1132
    protected function serveWebshot()
1133
    {
1134
        $this->debug(3, 'Starting serveWebshot');
1135
        $instr = 'Please follow the instructions at http://code.google.com/p/timthumb/ to set your server up for taking website screenshots.';
1136
        if (!is_file(WEBSHOT_CUTYCAPT)) {
1137
            return $this->error("CutyCapt is not installed. $instr");
1138
        }
1139
        if (!is_file(WEBSHOT_XVFB)) {
1140
            return $this->error("Xvfb is not installed. $instr");
1141
        }
1142
        $cuty      = WEBSHOT_CUTYCAPT;
1143
        $xv        = WEBSHOT_XVFB;
1144
        $screenX   = WEBSHOT_SCREEN_X;
1145
        $screenY   = WEBSHOT_SCREEN_Y;
1146
        $colDepth  = WEBSHOT_COLOR_DEPTH;
1147
        $format    = WEBSHOT_IMAGE_FORMAT;
1148
        $timeout   = WEBSHOT_TIMEOUT * 1000;
1149
        $ua        = WEBSHOT_USER_AGENT;
1150
        $jsOn      = WEBSHOT_JAVASCRIPT_ON ? 'on' : 'off';
1151
        $javaOn    = WEBSHOT_JAVA_ON ? 'on' : 'off';
1152
        $pluginsOn = WEBSHOT_PLUGINS_ON ? 'on' : 'off';
1153
        $proxy     = WEBSHOT_PROXY ? ' --http-proxy=' . WEBSHOT_PROXY : '';
1154
        $tempfile  = tempnam($this->cacheDirectory, 'timthumb_webshot');
1155
        $url       = $this->src;
1156
        if (!preg_match('/^https?:\/\/[a-zA-Z0-9\.\-]+/i', $url)) {
1157
            return $this->error('Invalid URL supplied.');
1158
        }
1159
        $url = preg_replace('/[^A-Za-z0-9\-\.\_:\/\?\&\+\;\=]+/', '', $url); //RFC 3986 plus ()$ chars to prevent exploit below. Plus the following are also removed: @*!~#[]',
1160
        // 2014 update by Mark Maunder: This exploit: http://cxsecurity.com/issue/WLB-2014060134
1161
        // uses the $(command) shell execution syntax to execute arbitrary shell commands as the web server user.
1162
        // So we're now filtering out the characters: '$', '(' and ')' in the above regex to avoid this.
1163
        // We are also filtering out chars rarely used in URLs but legal accoring to the URL RFC which might be exploitable. These include: @*!~#[]',
1164
        // 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.
1165
        if (WEBSHOT_XVFB_RUNNING) {
1166
            putenv('DISPLAY=:100.0');
1167
            $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";
1168
        } else {
1169
            $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";
1170
        }
1171
        $this->debug(3, "Executing command: $command");
1172
        $out = shell_exec('$command');
1173
        $this->debug(3, "Received output: $out");
1174
        if (!is_file($tempfile)) {
1175
            $this->set404();
1176
1177
            return $this->error('The command to create a thumbnail failed.');
1178
        }
1179
        $this->cropTop = true;
1180
        if ($this->processImageAndWriteToCache($tempfile)) {
1181
            $this->debug(3, 'Image processed successfully. Serving from cache');
1182
1183
            return $this->serveCacheFile();
1184
        } else {
1185
            return false;
1186
        }
1187
    }
1188
1189
    /**
1190
     * @return bool
1191
     */
1192
    protected function serveExternalImage()
1193
    {
1194
        if (!preg_match('/^https?:\/\/[a-zA-Z0-9\-\.]+/i', $this->src)) {
1195
            $this->error('Invalid URL supplied.');
1196
1197
            return false;
1198
        }
1199
        $tempfile = tempnam($this->cacheDirectory, 'timthumb');
1200
        $this->debug(3, "Fetching external image into temporary file $tempfile");
1201
        $this->toDelete($tempfile);
1202
        #fetch file here
1203
        if (!$this->getURL($this->src, $tempfile)) {
1204
            @unlink($this->cachefile);
1205
            touch($this->cachefile);
1206
            $this->debug(3, 'Error fetching URL: ' . $this->lastURLError);
1207
            $this->error('Error reading the URL you specified from remote host.' . $this->lastURLError);
1208
1209
            return false;
1210
        }
1211
1212
        $mimeType = $this->getMimeType($tempfile);
1213
        if (!preg_match("/^image\/(?:jpg|jpeg|gif|png)$/i", $mimeType)) {
1214
            $this->debug(3, "Remote file has invalid mime type: $mimeType");
1215
            @unlink($this->cachefile);
1216
            touch($this->cachefile);
1217
            $this->error("The remote file is not a valid image. Mimetype = '" . $mimeType . "'" . $tempfile);
1218
1219
            return false;
1220
        }
1221
        if ($this->processImageAndWriteToCache($tempfile)) {
1222
            $this->debug(3, 'Image processed successfully. Serving from cache');
1223
1224
            return $this->serveCacheFile();
1225
        } else {
1226
            return false;
1227
        }
1228
    }
1229
1230
    /**
1231
     * @param $h
1232
     * @param $d
1233
     *
1234
     * @return int
1235
     */
1236
    public static function curlWrite($h, $d)
1237
    {
1238
        fwrite(self::$curlFH, $d);
1239
        self::$curlDataWritten += strlen($d);
1240
        if (self::$curlDataWritten > MAX_FILE_SIZE) {
1241
            return 0;
1242
        } else {
1243
            return strlen($d);
1244
        }
1245
    }
1246
1247
    /**
1248
     * @return bool
1249
     */
1250
    protected function serveCacheFile()
1251
    {
1252
        $this->debug(3, "Serving {$this->cachefile}");
1253
        if (!is_file($this->cachefile)) {
1254
            $this->error("serveCacheFile called in timthumb but we couldn't find the cached file.");
1255
1256
            return false;
1257
        }
1258
        $fp = fopen($this->cachefile, 'rb');
1259
        if (!$fp) {
1260
            return $this->error('Could not open cachefile.');
1261
        }
1262
        fseek($fp, strlen($this->filePrependSecurityBlock), SEEK_SET);
1263
        $imgType = fread($fp, 3);
1264
        fseek($fp, 3, SEEK_CUR);
1265
        if (ftell($fp) != strlen($this->filePrependSecurityBlock) + 6) {
1266
            @unlink($this->cachefile);
1267
1268
            return $this->error('The cached image file seems to be corrupt.');
1269
        }
1270
        $imageDataSize = filesize($this->cachefile) - (strlen($this->filePrependSecurityBlock) + 6);
1271
        $this->sendImageHeaders($imgType, $imageDataSize);
1272
        $bytesSent = @fpassthru($fp);
1273
        fclose($fp);
1274
        if ($bytesSent > 0) {
1275
            return true;
1276
        }
1277
        $content = file_get_contents($this->cachefile);
1278
        if (false !== $content) {
1279
            $content = substr($content, strlen($this->filePrependSecurityBlock) + 6);
1280
            echo $content;
1281
            $this->debug(3, 'Served using file_get_contents and echo');
1282
1283
            return true;
1284
        } else {
1285
            $this->error('Cache file could not be loaded.');
1286
1287
            return false;
1288
        }
1289
    }
1290
1291
    /**
1292
     * @param $mimeType
1293
     * @param $dataSize
1294
     *
1295
     * @return bool
1296
     */
1297
    protected function sendImageHeaders($mimeType, $dataSize)
1298
    {
1299
        if (!preg_match('/^image\//i', $mimeType)) {
1300
            $mimeType = 'image/' . $mimeType;
1301
        }
1302
        if ('image/jpg' === strtolower($mimeType)) {
1303
            $mimeType = 'image/jpeg';
1304
        }
1305
        $gmdate_expires  = gmdate('D, d M Y H:i:s', strtotime('now +10 days')) . ' GMT';
1306
        $gmdate_modified = gmdate('D, d M Y H:i:s') . ' GMT';
1307
        // send content headers then display image
1308
        header('Content-Type: ' . $mimeType);
1309
        header('Accept-Ranges: none'); //Changed this because we don't accept range requests
1310
        header('Last-Modified: ' . $gmdate_modified);
1311
        header('Content-Length: ' . $dataSize);
1312
        if (BROWSER_CACHE_DISABLE) {
1313
            $this->debug(3, 'Browser cache is disabled so setting non-caching headers.');
1314
            header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
1315
            header('Pragma: no-cache');
1316
            header('Expires: ' . gmdate('D, d M Y H:i:s', time()));
1317
        } else {
1318
            $this->debug(3, 'Browser caching is enabled');
1319
            header('Cache-Control: max-age=' . BROWSER_CACHE_MAX_AGE . ', must-revalidate');
1320
            header('Expires: ' . $gmdate_expires);
1321
        }
1322
1323
        return true;
1324
    }
1325
1326
    protected function securityChecks()
1327
    {
1328
    }
1329
1330
    /**
1331
     * @param        $property
1332
     * @param string $default
1333
     *
1334
     * @return string
1335
     */
1336
    protected function param($property, $default = '')
1337
    {
1338
        if (isset($_GET[$property])) {
1339
            return Request::getString($property, '', 'GET');
1340
        } else {
1341
            return $default;
1342
        }
1343
    }
1344
1345
    /**
1346
     * @param $mimeType
1347
     * @param $src
1348
     *
1349
     * @return resource
1350
     */
1351
    protected function openImage($mimeType, $src)
1352
    {
1353
        $image = '';
1354
        switch ($mimeType) {
1355
            case 'image/jpeg':
1356
                $image = imagecreatefromjpeg($src);
1357
                break;
1358
1359
            case 'image/png':
1360
                $image = imagecreatefrompng($src);
1361
                imagealphablending($image, true);
1362
                imagesavealpha($image, true);
1363
                break;
1364
1365
            case 'image/gif':
1366
                $image = imagecreatefromgif($src);
1367
                break;
1368
1369
            default:
1370
                $this->error('Unrecognised mimeType');
1371
        }
1372
1373
        return $image;
1374
    }
1375
1376
    /**
1377
     * @return string
1378
     */
1379
    protected function getIP()
1380
    {
1381
        $rem = @$_SERVER['REMOTE_ADDR'];
1382
        $ff  = @$_SERVER['HTTP_X_FORWARDED_FOR'];
1383
        $ci  = @$_SERVER['HTTP_CLIENT_IP'];
1384
        if (preg_match('/^(?:192\.168|172\.16|10\.|127\.)/', $rem)) {
1385
            if ($ff) {
1386
                return $ff;
1387
            }
1388
            if ($ci) {
1389
                return $ci;
1390
            }
1391
1392
            return $rem;
1393
        } else {
1394
            if ($rem) {
1395
                return $rem;
1396
            }
1397
            if ($ff) {
1398
                return $ff;
1399
            }
1400
            if ($ci) {
1401
                return $ci;
1402
            }
1403
1404
            return 'UNKNOWN';
1405
        }
1406
    }
1407
1408
    /**
1409
     * @param $level
1410
     * @param $msg
1411
     */
1412
    protected function debug($level, $msg)
1413
    {
1414
        if (DEBUG_ON && $level <= DEBUG_LEVEL) {
1415
            $execTime = sprintf('%.6f', microtime(true) - $this->startTime);
1416
            $tick     = sprintf('%.6f', 0);
1417
            if ($this->lastBenchTime > 0) {
1418
                $tick = sprintf('%.6f', microtime(true) - $this->lastBenchTime);
1419
            }
1420
            $this->lastBenchTime = microtime(true);
1421
            error_log('TimThumb Debug line ' . __LINE__ . " [$execTime : $tick]: $msg");
1422
        }
1423
    }
1424
1425
    /**
1426
     * @param $msg
1427
     *
1428
     * @return bool
1429
     */
1430
    protected function sanityFail($msg)
1431
    {
1432
        return $this->error("There is a problem in the timthumb code. Message: Please report this error at <a href='http://code.google.com/p/timthumb/issues/list'>timthumb's bug tracking page</a>: $msg");
1433
    }
1434
1435
    /**
1436
     * @param $file
1437
     *
1438
     * @return string
1439
     */
1440
    protected function getMimeType($file)
1441
    {
1442
        $info = getimagesize($file);
1443
        if (is_array($info) && $info['mime']) {
1444
            return $info['mime'];
1445
        }
1446
1447
        return '';
1448
    }
1449
1450
    protected function setMemoryLimit()
1451
    {
1452
        $inimem   = ini_get('memory_limit');
1453
        $inibytes = self::returnBytes($inimem);
1454
        $ourbytes = self::returnBytes(MEMORY_LIMIT);
1455
        if ($inibytes < $ourbytes) {
1456
            ini_set('memory_limit', MEMORY_LIMIT);
1457
            $this->debug(3, "Increased memory from $inimem to " . MEMORY_LIMIT);
1458
        } else {
1459
            $this->debug(3, 'Not adjusting memory size because the current setting is ' . $inimem . ' and our size of ' . MEMORY_LIMIT . ' is smaller.');
1460
        }
1461
    }
1462
1463
    /**
1464
     * @param $size_str
1465
     *
1466
     * @return int
1467
     */
1468
    protected static function returnBytes($size_str)
1469
    {
1470
        switch (substr($size_str, -1)) {
1471
            case 'M':
1472
            case 'm':
1473
                return (int)$size_str * 1048576;
1474
            case 'K':
1475
            case 'k':
1476
                return (int)$size_str * 1024;
1477
            case 'G':
1478
            case 'g':
1479
                return (int)$size_str * 1073741824;
1480
            default:
1481
                return $size_str;
1482
        }
1483
    }
1484
1485
    /**
1486
     * @param $url
1487
     * @param $tempfile
1488
     *
1489
     * @return bool
1490
     */
1491
    protected function getURL($url, $tempfile)
1492
    {
1493
        $this->lastURLError = false;
1494
        $url                = preg_replace('/ /', '%20', $url);
1495
        if (function_exists('curl_init')) {
1496
            $this->debug(3, 'Curl is installed so using it to fetch URL.');
1497
            self::$curlFH = fopen($tempfile, 'wb');
1498
            if (!self::$curlFH) {
1499
                $this->error("Could not open $tempfile for writing.");
1500
1501
                return false;
1502
            }
1503
            self::$curlDataWritten = 0;
1504
            $this->debug(3, "Fetching url with curl: $url");
1505
            $curl = curl_init($url);
1506
            curl_setopt($curl, CURLOPT_TIMEOUT, CURL_TIMEOUT);
1507
            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');
1508
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
1509
            curl_setopt($curl, CURLOPT_HEADER, 0);
1510
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true); //was false before
1511
            curl_setopt($curl, CURLOPT_WRITEFUNCTION, 'timthumb::curlWrite');
1512
            @curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
1513
            @curl_setopt($curl, CURLOPT_MAXREDIRS, 10);
1514
1515
            $curlResult = curl_exec($curl);
1516
            fclose(self::$curlFH);
1517
            $httpStatus = curl_getinfo($curl, CURLINFO_HTTP_CODE);
1518
            if (404 == $httpStatus) {
1519
                $this->set404();
1520
            }
1521
            if (302 == $httpStatus) {
1522
                $this->error('External Image is Redirecting. Try alternate image url');
1523
1524
                return false;
1525
            }
1526
            if ($curlResult) {
1527
                curl_close($curl);
1528
1529
                return true;
1530
            } else {
1531
                $this->lastURLError = curl_error($curl);
1532
                curl_close($curl);
1533
1534
                return false;
1535
            }
1536
        } else {
1537
            $img = @file_get_contents($url);
1538
            if (false === $img) {
1539
                $err                = error_get_last();
1540
                $this->lastURLError = $err;
1541
                if (is_array($err) && $err['message']) {
1542
                    $this->lastURLError = $err['message'];
1543
                }
1544
                if (false !== strpos($this->lastURLError, '404')) {
1545
                    $this->set404();
1546
                }
1547
1548
                return false;
1549
            }
1550
            if (!file_put_contents($tempfile, $img)) {
1551
                $this->error("Could not write to $tempfile.");
1552
1553
                return false;
1554
            }
1555
1556
            return true;
1557
        }
1558
    }
1559
1560
    /**
1561
     * @param $file
1562
     *
1563
     * @return bool
1564
     */
1565
    protected function serveImg($file)
1566
    {
1567
        $s = getimagesize($file);
1568
        if (!($s && $s['mime'])) {
1569
            return false;
1570
        }
1571
        header('Content-Type: ' . $s['mime']);
1572
        header('Content-Length: ' . filesize($file));
1573
        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
1574
        header('Pragma: no-cache');
1575
        $bytes = @readfile($file);
1576
        if ($bytes > 0) {
1577
            return true;
1578
        }
1579
        $content = @file_get_contents($file);
1580
        if (false !== $content) {
1581
            echo $content;
1582
1583
            return true;
1584
        }
1585
1586
        return false;
1587
    }
1588
1589
    protected function set404()
1590
    {
1591
        $this->is404 = true;
1592
    }
1593
1594
    /**
1595
     * @return bool
1596
     */
1597
    protected function is404()
1598
    {
1599
        return $this->is404;
1600
    }
1601
}
1602