Passed
Branch master (99f1ef)
by Michael
04:35
created

thumb.php (46 issues)

1
<?php
2
3
declare(strict_types=1);
4
/**
5
 * TimThumb by Ben Gillbanks and Mark Maunder
6
 * Based on work done by Tim McDaniels and Darren Hoyt
7
 * http://code.google.com/p/timthumb/
8
 *
9
 * GNU General Public License, version 2
10
 * https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
11
 *
12
 * Examples and documentation available on the project homepage
13
 * http://www.binarymoon.co.uk/projects/timthumb/
14
 *
15
 * $Rev$
16
 */
17
18
/*
19
 * --- TimThumb CONFIGURATION ---
20
 * To edit the configs it is best to create a file called timthumb-config.php
21
 * and define variables you want to customize in there. It will automatically be
22
 * loaded by timthumb. This will save you having to re-edit these variables
23
 * everytime you download a new version
24
*/
25
26
use Xmf\Request;
27
28
require_once __DIR__ . '/header.php';
29
30
define('VERSION', '2.8.14');                                                                        // Version of this script
31
//Load a config file if it exists. Otherwise, use the values below
32
if (file_exists(__DIR__ . '/timthumb-config.php')) {
33
    require_once __DIR__ . '/timthumb-config.php';
34
}
35
if (!defined('DEBUG_ON')) {
36
    define('DEBUG_ON', false);
37
}                                // Enable debug logging to web server error log (STDERR)
38
if (!defined('DEBUG_LEVEL')) {
39
    define('DEBUG_LEVEL', 1);
40
}                                // Debug level 1 is less noisy and 3 is the most noisy
41
if (!defined('MEMORY_LIMIT')) {
42
    define('MEMORY_LIMIT', '30M');
43
}                            // Set PHP memory limit
44
if (!defined('BLOCK_EXTERNAL_LEECHERS')) {
45
    define('BLOCK_EXTERNAL_LEECHERS', false);
46
}                // If the image or webshot is being loaded on an external site, display a red "No Hotlinking" gif.
47
if (!defined('DISPLAY_ERROR_MESSAGES')) {
48
    define('DISPLAY_ERROR_MESSAGES', true);
49
}                // Display error messages. Set to false to turn off errors (good for production websites)
50
//Image fetching and caching
51
if (!defined('ALLOW_EXTERNAL')) {
52
    define('ALLOW_EXTERNAL', true);
53
}                        // Allow image fetching from external websites. Will check against ALLOWED_SITES if ALLOW_ALL_EXTERNAL_SITES is false
54
if (!defined('ALLOW_ALL_EXTERNAL_SITES')) {
55
    define('ALLOW_ALL_EXTERNAL_SITES', false);
56
}                // Less secure.
57
if (!defined('FILE_CACHE_ENABLED')) {
58
    define('FILE_CACHE_ENABLED', true);
59
}                    // Should we store resized/modified images on disk to speed things up?
60
if (!defined('FILE_CACHE_TIME_BETWEEN_CLEANS')) {
61
    define('FILE_CACHE_TIME_BETWEEN_CLEANS', 86400);
62
}    // How often the cache is cleaned
63
64
if (!defined('FILE_CACHE_MAX_FILE_AGE')) {
65
    define('FILE_CACHE_MAX_FILE_AGE', 86400);
66
}                // How old does a file have to be to be deleted from the cache
67
if (!defined('FILE_CACHE_SUFFIX')) {
68
    define('FILE_CACHE_SUFFIX', '.timthumb.txt');
69
}            // What to put at the end of all files in the cache directory so we can identify them
70
if (!defined('FILE_CACHE_PREFIX')) {
71
    define('FILE_CACHE_PREFIX', 'timthumb');
72
}                // What to put at the beg of all files in the cache directory so we can identify them
73
if (!defined('FILE_CACHE_DIRECTORY')) {
74
    define('FILE_CACHE_DIRECTORY', '../../cache');
75
} // Directory where images are cached. Left blank it will use the system temporary directory (which is better for security)
76
if (!defined('MAX_FILE_SIZE')) {
77
    define('MAX_FILE_SIZE', 10485760);
78
}                        // 10 Megs is 10485760. This is the max internal or external file size that we'll process.
79
if (!defined('CURL_TIMEOUT')) {
80
    define('CURL_TIMEOUT', 20);
81
}                            // Timeout duration for Curl. This only applies if you have Curl installed and aren't using PHP's default URL fetching mechanism.
82
if (!defined('WAIT_BETWEEN_FETCH_ERRORS')) {
83
    define('WAIT_BETWEEN_FETCH_ERRORS', 3600);
84
}                // Time to wait between errors fetching remote file
85
86
//Browser caching
87
if (!defined('BROWSER_CACHE_MAX_AGE')) {
88
    define('BROWSER_CACHE_MAX_AGE', 864000);
89
}                // Time to cache in the browser
90
if (!defined('BROWSER_CACHE_DISABLE')) {
91
    define('BROWSER_CACHE_DISABLE', false);
92
}                // Use for testing if you want to disable all browser caching
93
94
//Image size and defaults
95
if (!defined('MAX_WIDTH')) {
96
    define('MAX_WIDTH', 1500);
97
}                                // Maximum image width
98
if (!defined('MAX_HEIGHT')) {
99
    define('MAX_HEIGHT', 1500);
100
}                            // Maximum image height
101
if (!defined('NOT_FOUND_IMAGE')) {
102
    define('NOT_FOUND_IMAGE', '');
103
}                            // Image to serve if any 404 occurs
104
if (!defined('ERROR_IMAGE')) {
105
    define('ERROR_IMAGE', '');
106
}                                // Image to serve if an error occurs instead of showing error message
107
if (!defined('PNG_IS_TRANSPARENT')) {
108
    define('PNG_IS_TRANSPARENT', false);
109
}                    // Define if a png image should have a transparent background color. Use False value if you want to display a custom coloured canvas_colour
110
if (!defined('DEFAULT_Q')) {
111
    define('DEFAULT_Q', 90);
112
}                                // Default image quality. Allows overrid in timthumb-config.php
113
if (!defined('DEFAULT_ZC')) {
114
    define('DEFAULT_ZC', 1);
115
}                                // Default zoom/crop setting. Allows overrid in timthumb-config.php
116
if (!defined('DEFAULT_F')) {
117
    define('DEFAULT_F', '');
118
}                                // Default image filters. Allows overrid in timthumb-config.php
119
if (!defined('DEFAULT_S')) {
120
    define('DEFAULT_S', 0);
121
}                                // Default sharpen value. Allows overrid in timthumb-config.php
122
if (!defined('DEFAULT_CC')) {
123
    define('DEFAULT_CC', 'ffffff');
124
}                        // Default canvas colour. Allows overrid in timthumb-config.php
125
if (!defined('DEFAULT_WIDTH')) {
126
    define('DEFAULT_WIDTH', 100);
127
}                            // Default thumbnail width. Allows overrid in timthumb-config.php
128
if (!defined('DEFAULT_HEIGHT')) {
129
    define('DEFAULT_HEIGHT', 100);
130
}                            // Default thumbnail height. Allows overrid in timthumb-config.php
131
132
/**
133
 * Additional Parameters:
134
 * LOCAL_FILE_BASE_DIRECTORY = Override the DOCUMENT_ROOT. This is best used in timthumb-config.php
135
 */
136
137
//Image compression is enabled if either of these point to valid paths
138
139
//These are now disabled by default because the file sizes of PNGs (and GIFs) are much smaller than we used to generate.
140
//They only work for PNGs. GIFs and JPEGs are not affected.
141
if (!defined('OPTIPNG_ENABLED')) {
142
    define('OPTIPNG_ENABLED', false);
143
}
144
if (!defined('OPTIPNG_PATH')) {
145
    define('OPTIPNG_PATH', '/usr/bin/optipng');
146
} //This will run first because it gives better compression than pngcrush.
147
if (!defined('PNGCRUSH_ENABLED')) {
148
    define('PNGCRUSH_ENABLED', false);
149
}
150
if (!defined('PNGCRUSH_PATH')) {
151
    define('PNGCRUSH_PATH', '/usr/bin/pngcrush');
152
} //This will only run if OPTIPNG_PATH is not set or is not valid
153
154
/*
155
    -------====Website Screenshots configuration - BETA====-------
156
157
    If you just want image thumbnails and don't want website screenshots, you can safely leave this as is.
158
159
    If you would like to get website screenshots set up, you will need root access to your own server.
160
161
    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.
162
    Enable BLOCK_EXTERNAL_LEECHERS so that your site doesn't generate thumbnails for the whole Internet.
163
164
    Instructions to get website screenshots enabled on Ubuntu Linux:
165
166
    1. Install Xvfb with the following command: sudo apt-get install subversion libqt4-webkit libqt4-dev g++ xvfb
167
    2. Go to a directory where you can download some code
168
    3. Check-out the latest version of CutyCapt with the following command: svn co https://cutycapt.svn.sourceforge.net/svnroot/cutycapt
169
    4. Compile CutyCapt by doing: cd cutycapt/CutyCapt
170
    5. qmake
171
    6. make
172
    7. cp CutyCapt /usr/local/bin/
173
    8. Test it by running: xvfb-run --server-args="-screen 0, 1024x768x24" CutyCapt --url="http://markmaunder.com/" --out=test.png
174
    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:
175
    10. http://yoursite.com/path/to/timthumb.php?src=http://markmaunder.com/&webshot=1
176
177
    Notes on performance:
178
    The first time a webshot loads, it will take a few seconds.
179
    From then on it uses the regular timthumb caching mechanism with the configurable options above
180
    and loading will be very fast.
181
182
    --ADVANCED USERS ONLY--
183
    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.
184
    nohup Xvfb :100 -ac -nolisten tcp -screen 0, 1024x768x24 > /dev/null 2>&1 &
185
    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.
186
    You will need to take responsibility for keeping Xvfb running in case it crashes. (It seems pretty stable)
187
    You will also need to take responsibility for server security if you're running Xvfb as root.
188
189
190
*/
191
if (!defined('WEBSHOT_ENABLED')) {
192
    define('WEBSHOT_ENABLED', false);
193
} //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.
194
if (!defined('WEBSHOT_CUTYCAPT')) {
195
    define('WEBSHOT_CUTYCAPT', '/usr/local/bin/CutyCapt');
196
} //The path to CutyCapt.
197
if (!defined('WEBSHOT_XVFB')) {
198
    define('WEBSHOT_XVFB', '/usr/bin/xvfb-run');
199
}        //The path to the Xvfb server
200
if (!defined('WEBSHOT_SCREEN_X')) {
201
    define('WEBSHOT_SCREEN_X', '1024');
202
}            //1024 works ok
203
if (!defined('WEBSHOT_SCREEN_Y')) {
204
    define('WEBSHOT_SCREEN_Y', '768');
205
}            //768 works ok
206
if (!defined('WEBSHOT_COLOR_DEPTH')) {
207
    define('WEBSHOT_COLOR_DEPTH', '24');
208
}            //I haven't tested anything besides 24
209
if (!defined('WEBSHOT_IMAGE_FORMAT')) {
210
    define('WEBSHOT_IMAGE_FORMAT', 'png');
211
}            //png is about 2.5 times the size of jpg but is a LOT better quality
212
if (!defined('WEBSHOT_TIMEOUT')) {
213
    define('WEBSHOT_TIMEOUT', '20');
214
}            //Seconds to wait for a webshot
215
if (!defined('WEBSHOT_USER_AGENT')) {
216
    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');
217
} //I hate to do this, but a non-browser robot user agent might not show what humans see. So we pretend to be Firefox
218
if (!defined('WEBSHOT_JAVASCRIPT_ON')) {
219
    define('WEBSHOT_JAVASCRIPT_ON', true);
220
}            //Setting to false might give you a slight speedup and block ads. But it could cause other issues.
221
if (!defined('WEBSHOT_JAVA_ON')) {
222
    define('WEBSHOT_JAVA_ON', false);
223
}            //Have only tested this as fase
224
if (!defined('WEBSHOT_PLUGINS_ON')) {
225
    define('WEBSHOT_PLUGINS_ON', true);
226
}            //Enable flash and other plugins
227
if (!defined('WEBSHOT_PROXY')) {
228
    define('WEBSHOT_PROXY', '');
229
}                //In case you're behind a proxy server.
230
if (!defined('WEBSHOT_XVFB_RUNNING')) {
231
    define('WEBSHOT_XVFB_RUNNING', false);
232
}            //ADVANCED: Enable this if you've got Xvfb running in the background.
233
234
// 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.
235
if (!isset($allowedSites)) {
236
    $allowedSites = [
237
        'flickr.com',
238
        'staticflickr.com',
239
        'picasa.com',
240
        'img.youtube.com',
241
        'upload.wikimedia.org',
242
        'photobucket.com',
243
        'imgur.com',
244
        'imageshack.us',
245
        'tinypic.com',
246
    ];
247
}
248
// -------------------------------------------------------------
249
// -------------- STOP EDITING CONFIGURATION HERE --------------
250
// -------------------------------------------------------------
251
252
Timthumb::start();
253
254
/**
255
 * Class timthumb
256
 */
257
class Timthumb
258
{
259
    protected        $src                      = '';
260
    protected        $is404                    = false;
261
    protected        $docRoot                  = '';
262
    protected        $lastURLError             = false;
263
    protected        $localImage               = '';
264
    protected        $localImageMTime          = 0.0;
265
    protected        $url                      = false;
266
    protected        $myHost                   = '';
267
    protected        $isURL                    = false;
268
    protected        $cachefile                = '';
269
    protected        $errors                   = [];
270
    protected        $toDeletes                = [];
271
    protected        $cacheDirectory           = '';
272
    protected        $startTime                = 0.0;
273
    protected        $lastBenchTime            = 0.0;
274
    protected        $cropTop                  = false;
275
    protected        $salt                     = '';
276
    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.
277
    protected        $filePrependSecurityBlock = "<?php exit('Execution denied!'); //"; //Designed to have three letter mime type, space, question mark and greater than symbol appended. 6 bytes total.
278
    protected static $curlDataWritten          = 0;
279
    protected static $curlFH                   = false;
280
281
    public static function start()
282
    {
283
        $tim = new self();
284
        $tim->handleErrors();
285
        $tim->securityChecks();
286
        if ($tim->tryBrowserCache()) {
287
            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...
288
        }
289
        $tim->handleErrors();
290
        if (FILE_CACHE_ENABLED && $tim->tryServerCache()) {
291
            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...
292
        }
293
        $tim->handleErrors();
294
        $tim->run();
295
        $tim->handleErrors();
296
        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...
297
    }
298
299
    public function __construct()
300
    {
301
        global $allowedSites;
302
        $this->startTime = microtime(true);
303
        date_default_timezone_set('UTC');
304
        $this->debug(1, 'Starting new request from ' . $this->getIP() . ' to ' . Request::getString('REQUEST_URI', '', 'SERVER'));
305
        $this->calcDocRoot();
306
        //On windows systems I'm assuming fileinode returns an empty string or a number that doesn't change. Check this.
307
        $this->salt = @filemtime(__FILE__) . '-' . @fileinode(__FILE__);
308
        $this->debug(3, 'Salt is: ' . $this->salt);
309
        if (FILE_CACHE_DIRECTORY) {
310
            if (!is_dir(FILE_CACHE_DIRECTORY)) {
311
                if (!mkdir($concurrentDirectory = FILE_CACHE_DIRECTORY) && !is_dir($concurrentDirectory)) {
312
                    throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
313
                }
314
                if (!is_dir(FILE_CACHE_DIRECTORY)) {
315
                    $this->error('Could not create the file cache directory.');
316
317
                    return false;
318
                }
319
            }
320
            $this->cacheDirectory = FILE_CACHE_DIRECTORY;
321
            if (!touch($this->cacheDirectory . '/index.html')) {
322
                $this->error('Could not create the index.html file - to fix this create an empty file named index.html file in the cache directory.');
323
            }
324
        } else {
325
            $this->cacheDirectory = sys_get_temp_dir();
326
        }
327
        //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.
328
        $this->cleanCache();
329
330
        $this->myHost = preg_replace('/^www\./i', '', \Xmf\Request::getString('HTTP_HOST', '', 'SERVER'));
331
        $this->src    = $this->param('src');
332
        $this->url    = parse_url($this->src);
333
        $this->src    = preg_replace('/https?:\/\/(?:www\.)?' . $this->myHost . '/i', '', $this->src);
334
335
        if (mb_strlen($this->src) <= 3) {
336
            $this->error('No image specified');
337
338
            return false;
339
        }
340
        if (BLOCK_EXTERNAL_LEECHERS && array_key_exists('HTTP_REFERER', $_SERVER) && (!preg_match('/^https?:\/\/(?:www\.)?' . $this->myHost . '(?:$|\/)/i', \Xmf\Request::getString('HTTP_REFERER', '', 'SERVER')))) {
341
            // base64 encoded red image that says 'no hotlinkers'
342
            // nothing to worry about! :)
343
            $imgData = base64_decode("R0lGODlhUAAMAIAAAP8AAP///yH5BAAHAP8ALAAAAABQAAwAAAJpjI+py+0Po5y0OgAMjjv01YUZ\nOGplhWXfNa6JCLnWkXplrcBmW+spbwvaVr/cDyg7IoFC2KbYVC2NQ5MQ4ZNao9Ynzjl9ScNYpneb\nDULB3RP6JuPuaGfuuV4fumf8PuvqFyhYtjdoeFgAADs=", true);
344
            header('Content-Type: image/gif');
345
            header('Content-Length: ' . mb_strlen($imgData));
346
            header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
347
            header('Pragma: no-cache');
348
            header('Expires: ' . gmdate('D, d M Y H:i:s', time()));
349
            echo $imgData;
350
351
            return false;
352
        }
353
        if (preg_match('/^https?:\/\/[^\/]+/i', $this->src)) {
354
            $this->debug(2, 'Is a request for an external URL: ' . $this->src);
355
            $this->isURL = true;
356
        } else {
357
            $this->debug(2, 'Is a request for an internal file: ' . $this->src);
358
        }
359
        if ($this->isURL && (!ALLOW_EXTERNAL)) {
360
            $this->error('You are not allowed to fetch images from an external website.');
361
362
            return false;
363
        }
364
        if ($this->isURL) {
365
            if (ALLOW_ALL_EXTERNAL_SITES) {
366
                $this->debug(2, 'Fetching from all external sites is enabled.');
367
            } else {
368
                $this->debug(2, 'Fetching only from selected external sites is enabled.');
369
                $allowed = false;
370
                foreach ($allowedSites as $site) {
371
                    if ((mb_strtolower($this->url['host']) === mb_strtolower($site)) || (mb_strtolower(mb_substr($this->url['host'], -mb_strlen($site) - 1)) === mb_strtolower(".$site"))) {
372
                        $this->debug(3, "URL hostname {$this->url['host']} matches $site so allowing.");
373
                        $allowed = true;
374
                    }
375
                }
376
                if (!$allowed) {
377
                    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.');
378
                }
379
            }
380
        }
381
382
        $cachePrefix = ($this->isURL ? '_ext_' : '_int_');
383
        if ($this->isURL) {
384
            $arr = explode('&', $_SERVER['QUERY_STRING']);
385
            asort($arr);
386
            $this->cachefile = $this->cacheDirectory . '/' . FILE_CACHE_PREFIX . $cachePrefix . md5($this->salt . implode('', $arr) . $this->fileCacheVersion) . FILE_CACHE_SUFFIX;
387
        } else {
388
            $this->localImage = $this->getLocalImagePath($this->src);
389
            if (!$this->localImage) {
390
                $this->debug(1, "Could not find the local image: {$this->localImage}");
391
                $this->error('Could not find the internal image you specified.');
392
                $this->set404();
393
394
                return false;
395
            }
396
            $this->debug(1, "Local image path is {$this->localImage}");
397
            $this->localImageMTime = @filemtime($this->localImage);
398
            //We include the mtime of the local file in case in changes on disk.
399
            $this->cachefile = $this->cacheDirectory . '/' . FILE_CACHE_PREFIX . $cachePrefix . md5($this->salt . $this->localImageMTime . $_SERVER['QUERY_STRING'] . $this->fileCacheVersion) . FILE_CACHE_SUFFIX;
400
        }
401
        $this->debug(2, 'Cache file is: ' . $this->cachefile);
402
403
        return true;
404
    }
405
406
    public function __destruct()
407
    {
408
        foreach ($this->toDeletes as $del) {
409
            $this->debug(2, "Deleting temp file $del");
410
            @unlink($del);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). 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

410
            /** @scrutinizer ignore-unhandled */ @unlink($del);

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...
411
        }
412
    }
413
414
    /**
415
     * @return bool
416
     */
417
    public function run()
418
    {
419
        if ($this->isURL) {
420
            if (!ALLOW_EXTERNAL) {
421
                $this->debug(1, 'Got a request for an external image but ALLOW_EXTERNAL is disabled so returning error msg.');
422
                $this->error('You are not allowed to fetch images from an external website.');
423
424
                return false;
425
            }
426
            $this->debug(3, 'Got request for external image. Starting serveExternalImage.');
427
            if ($this->param('webshot')) {
428
                if (WEBSHOT_ENABLED) {
429
                    $this->debug(3, 'webshot param is set, so we\'re going to take a webshot.');
430
                    $this->serveWebshot();
431
                } else {
432
                    $this->error('You added the webshot parameter but webshots are disabled on this server. You need to set WEBSHOT_ENABLED === true to enable webshots.');
433
                }
434
            } else {
435
                $this->debug(3, 'webshot is NOT set so we\'re going to try to fetch a regular image.');
436
                $this->serveExternalImage();
437
            }
438
        } else {
439
            $this->debug(3, 'Got request for internal image. Starting serveInternalImage()');
440
            $this->serveInternalImage();
441
        }
442
443
        return true;
444
    }
445
446
    /**
447
     * @return bool
448
     */
449
    protected function handleErrors()
450
    {
451
        if ($this->haveErrors()) {
452
            if (NOT_FOUND_IMAGE && $this->is404()) {
453
                if ($this->serveImg(NOT_FOUND_IMAGE)) {
454
                    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...
455
                }
456
                $this->error('Additionally, the 404 image that is configured could not be found or there was an error serving it.');
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
                }
462
                $this->error('Additionally, the error image that is configured could not be found or there was an error serving it.');
463
            }
464
            $this->serveErrors();
465
            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...
466
        }
467
468
        return false;
469
    }
470
471
    /**
472
     * @return bool
473
     */
474
    protected function tryBrowserCache()
475
    {
476
        if (BROWSER_CACHE_DISABLE) {
477
            $this->debug(3, 'Browser caching is disabled');
478
479
            return false;
480
        }
481
        if (\Xmf\Request::hasVar('HTTP_IF_MODIFIED_SINCE', 'SERVER')) {
482
            $this->debug(3, 'Got a conditional get');
483
            $mtime = false;
484
            //We've already checked if the real file exists in the constructor
485
            if (!is_file($this->cachefile)) {
486
                //If we don't have something cached, regenerate the cached image.
487
                return false;
488
            }
489
            if ($this->localImageMTime) {
490
                $mtime = $this->localImageMTime;
491
                $this->debug(3, "Local real file's modification time is $mtime");
492
            } elseif (is_file($this->cachefile)) {
493
                //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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mtime of type false|integer is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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) {
509
                //Real file or cache file has been modified since last request, so force refetch.
510
                $this->debug(3, 'File has been modified since last fetch.');
511
512
                return false;
513
            }
514
            //Otherwise serve a 304
515
            $this->debug(3, 'File has not been modified since last get, so serving a 304.');
516
            header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified');
517
            $this->debug(1, 'Returning 304 not modified');
518
519
            return true;
520
        }
521
522
        return false;
523
    }
524
525
    /**
526
     * @return bool
527
     */
528
    protected function tryServerCache()
529
    {
530
        $this->debug(3, 'Trying server cache');
531
        if (is_file($this->cachefile)) {
532
            $this->debug(3, "Cachefile {$this->cachefile} exists");
533
            if ($this->isURL) {
534
                $this->debug(3, 'This is an external request, so checking if the cachefile is empty which means the request failed previously.');
535
                if (filesize($this->cachefile) < 1) {
536
                    $this->debug(3, 'Found an empty cachefile indicating a failed earlier request. Checking how old it is.');
537
                    //Fetching error occured previously
538
                    if (time() - @filemtime($this->cachefile) > WAIT_BETWEEN_FETCH_ERRORS) {
539
                        $this->debug(3, 'File is older than ' . WAIT_BETWEEN_FETCH_ERRORS . ' seconds. Deleting and returning false so app can try and load file.');
540
                        @unlink($this->cachefile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). 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

540
                        /** @scrutinizer ignore-unhandled */ @unlink($this->cachefile);

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...
541
542
                        return false; //to indicate we didn't serve from cache and app should try and load
543
                    }
544
                    $this->debug(3, 'Empty cachefile is still fresh so returning message saying we had an error fetching this image from remote host.');
545
                    $this->set404();
546
                    $this->error('An error occured fetching image.');
547
548
                    return false;
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
            }
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
        return null;
566
    }
567
568
    /**
569
     * @param $err
570
     *
571
     * @return bool
572
     */
573
    protected function error($err)
574
    {
575
        $this->debug(3, "Adding error message: $err");
576
        $this->errors[] = $err;
577
578
        return false;
579
    }
580
581
    /**
582
     * @return bool
583
     */
584
    protected function haveErrors()
585
    {
586
        return count($this->errors) > 0;
587
    }
588
589
    protected function serveErrors()
590
    {
591
        header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
592
        if (!DISPLAY_ERROR_MESSAGES) {
593
            return;
594
        }
595
        $html = '<ul>';
596
        foreach ($this->errors as $err) {
597
            $html .= '<li>' . htmlentities($err, ENT_QUOTES | ENT_HTML5) . '</li>';
598
        }
599
        $html .= '</ul>';
600
        echo '<h1>A TimThumb error has occured</h1>The following error(s) occured:<br>' . $html . '<br>';
601
        echo '<br>Query String : ' . htmlentities($_SERVER['QUERY_STRING'], ENT_QUOTES);
602
        echo '<br>TimThumb version : ' . VERSION . '</pre>';
603
    }
604
605
    /**
606
     * @return bool
607
     */
608
    protected function serveInternalImage()
609
    {
610
        $this->debug(3, "Local image path is $this->localImage");
611
        if (!$this->localImage) {
612
            $this->sanityFail('localImage not set after verifying it earlier in the code.');
613
614
            return false;
615
        }
616
        $fileSize = filesize($this->localImage);
617
        if ($fileSize > MAX_FILE_SIZE) {
618
            $this->error('The file you specified is greater than the maximum allowed file size.');
619
620
            return false;
621
        }
622
        if ($fileSize <= 0) {
623
            $this->error('The file you specified is <= 0 bytes.');
624
625
            return false;
626
        }
627
        $this->debug(3, 'Calling processImageAndWriteToCache() for local image.');
628
        if ($this->processImageAndWriteToCache($this->localImage)) {
629
            $this->serveCacheFile();
630
631
            return true;
632
        }
633
634
        return false;
635
    }
636
637
    /**
638
     * @return bool|void
639
     */
640
    protected function cleanCache()
641
    {
642
        if (FILE_CACHE_TIME_BETWEEN_CLEANS < 0) {
643
            return null;
644
        }
645
        $this->debug(3, 'cleanCache() called');
646
        $lastCleanFile = $this->cacheDirectory . '/timthumb_cacheLastCleanTime.touch';
647
648
        //If this is a new timthumb installation we need to create the file
649
        if (!is_file($lastCleanFile)) {
650
            $this->debug(1, "File tracking last clean doesn't exist. Creating $lastCleanFile");
651
            if (!touch($lastCleanFile)) {
652
                $this->error('Could not create cache clean timestamp file.');
653
            }
654
655
            return null;
656
        }
657
        if (@filemtime($lastCleanFile) < (time() - FILE_CACHE_TIME_BETWEEN_CLEANS)) {
658
            //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, GLOB_NOSORT);
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);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). 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

670
                        /** @scrutinizer ignore-unhandled */ @unlink($file);

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...
671
                    }
672
                }
673
            }
674
675
            return true;
676
        }
677
        $this->debug(3, 'Cache was cleaned less than ' . FILE_CACHE_TIME_BETWEEN_CLEANS . ' seconds ago so no cleaning needed.');
678
679
        return false;
680
    }
681
682
    /**
683
     * @param $localImage
684
     *
685
     * @return bool
686
     */
687
    protected function processImageAndWriteToCache($localImage)
688
    {
689
        $sData    = getimagesize($localImage);
690
        $origType = $sData[2];
691
        $mimeType = $sData['mime'];
692
693
        $this->debug(3, "Mime type of image is $mimeType");
694
        if (!preg_match('/^image\/(?:gif|jpg|jpeg|png)$/i', $mimeType)) {
695
            return $this->error('The image being resized is not a valid gif, jpg or png.');
696
        }
697
698
        if (!function_exists('imagecreatetruecolor')) {
699
            return $this->error('GD Library Error: imagecreatetruecolor does not exist - please contact your webhost and ask them to install the GD library');
700
        }
701
702
        if (defined('IMG_FILTER_NEGATE') && function_exists('imagefilter')) {
703
            $imageFilters = [
704
                1  => [IMG_FILTER_NEGATE, 0],
705
                2  => [IMG_FILTER_GRAYSCALE, 0],
706
                3  => [IMG_FILTER_BRIGHTNESS, 1],
707
                4  => [IMG_FILTER_CONTRAST, 1],
708
                5  => [IMG_FILTER_COLORIZE, 4],
709
                6  => [IMG_FILTER_EDGEDETECT, 0],
710
                7  => [IMG_FILTER_EMBOSS, 0],
711
                8  => [IMG_FILTER_GAUSSIAN_BLUR, 0],
712
                9  => [IMG_FILTER_SELECTIVE_BLUR, 0],
713
                10 => [IMG_FILTER_MEAN_REMOVAL, 0],
714
                11 => [IMG_FILTER_SMOOTH, 0],
715
            ];
716
        }
717
718
        // get standard input properties
719
        $newWidth     = (int)abs((int)$this->param('w', 0));
720
        $newHeight    = (int)abs((int)$this->param('h', 0));
721
        $zoomCrop     = (int)$this->param('zc', DEFAULT_ZC);
722
        $quality      = (int)abs((int)$this->param('q', DEFAULT_Q));
723
        $align        = $this->cropTop ? 't' : $this->param('a', 'c');
724
        $filters      = $this->param('f', DEFAULT_F);
725
        $sharpen      = (bool)$this->param('s', DEFAULT_S);
726
        $canvas_color = $this->param('cc', DEFAULT_CC);
727
        $canvas_trans = (bool)$this->param('ct', '1');
728
729
        // set default width and height if neither are set already
730
        if (0 == $newWidth && 0 == $newHeight) {
731
            $newWidth  = DEFAULT_WIDTH;
732
            $newHeight = DEFAULT_HEIGHT;
733
        }
734
735
        // ensure size limits can not be abused
736
        $newWidth  = min($newWidth, MAX_WIDTH);
737
        $newHeight = min($newHeight, MAX_HEIGHT);
738
739
        // set memory limit to be able to have enough space to resize larger images
740
        $this->setMemoryLimit();
741
742
        // open the existing image
743
        $image = $this->openImage($mimeType, $localImage);
744
        if (false === $image) {
0 ignored issues
show
The condition false === $image is always false.
Loading history...
745
            return $this->error('Unable to open image.');
746
        }
747
748
        // Get original width and height
749
        $width    = imagesx($image);
750
        $height   = imagesy($image);
751
        $origin_x = 0;
752
        $origin_y = 0;
753
754
        // generate new w/h if not provided
755
        if ($newWidth && !$newHeight) {
756
            $newHeight = floor($height * ($newWidth / $width));
757
        } elseif ($newHeight && !$newWidth) {
758
            $newWidth = floor($width * ($newHeight / $height));
759
        }
760
761
        // scale down and add borders
762
        if (3 == $zoomCrop) {
763
            $final_height = $height * ($newWidth / $width);
764
765
            if ($final_height > $newHeight) {
766
                $newWidth = $width * ($newHeight / $height);
767
            } else {
768
                $newHeight = $final_height;
769
            }
770
        }
771
772
        // create a new true color image
773
        $canvas = imagecreatetruecolor((int)$newWidth, (int)$newHeight);
774
        imagealphablending($canvas, false);
0 ignored issues
show
It seems like $canvas can also be of type false; however, parameter $image of imagealphablending() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

774
        imagealphablending(/** @scrutinizer ignore-type */ $canvas, false);
Loading history...
775
776
        if (3 == mb_strlen($canvas_color)) {
777
            //if is 3-char notation, edit string into 6-char notation
778
            $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);
779
        } elseif (6 != mb_strlen($canvas_color)) {
780
            $canvas_color = DEFAULT_CC; // on error return default canvas color
781
        }
782
783
        $canvas_color_R = hexdec(mb_substr($canvas_color, 0, 2));
784
        $canvas_color_G = hexdec(mb_substr($canvas_color, 2, 2));
785
        $canvas_color_B = hexdec(mb_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);
0 ignored issues
show
It seems like $canvas can also be of type false; however, parameter $image of imagecolorallocatealpha() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

791
            $color = imagecolorallocatealpha(/** @scrutinizer ignore-type */ $canvas, $canvas_color_R, $canvas_color_G, $canvas_color_B, 127);
Loading history...
It seems like $canvas_color_R can also be of type double; however, parameter $red of imagecolorallocatealpha() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

791
            $color = imagecolorallocatealpha($canvas, /** @scrutinizer ignore-type */ $canvas_color_R, $canvas_color_G, $canvas_color_B, 127);
Loading history...
It seems like $canvas_color_B can also be of type double; however, parameter $blue of imagecolorallocatealpha() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

791
            $color = imagecolorallocatealpha($canvas, $canvas_color_R, $canvas_color_G, /** @scrutinizer ignore-type */ $canvas_color_B, 127);
Loading history...
It seems like $canvas_color_G can also be of type double; however, parameter $green of imagecolorallocatealpha() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

791
            $color = imagecolorallocatealpha($canvas, $canvas_color_R, /** @scrutinizer ignore-type */ $canvas_color_G, $canvas_color_B, 127);
Loading history...
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);
0 ignored issues
show
It seems like $canvas can also be of type false; however, parameter $image of imagefill() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

797
        imagefill(/** @scrutinizer ignore-type */ $canvas, 0, 0, $color);
Loading history...
798
        // scale down and add borders
799
        if (2 == $zoomCrop) {
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);
0 ignored issues
show
It seems like $canvas can also be of type false; however, parameter $image of imagesavealpha() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

813
        imagesavealpha(/** @scrutinizer ignore-type */ $canvas, true);
Loading history...
814
815
        if ($zoomCrop > 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 !== mb_strpos($align, 't')) {
835
                    $src_y = 0;
836
                }
837
                if (false !== mb_strpos($align, 'b')) {
838
                    $src_y = $height - $src_h;
839
                }
840
                if (false !== mb_strpos($align, 'l')) {
841
                    $src_x = 0;
842
                }
843
                if (false !== mb_strpos($align, 'r')) {
844
                    $src_x = $width - $src_w;
845
                }
846
            }
847
848
            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);
0 ignored issues
show
It seems like $canvas can also be of type false; however, parameter $dst_image of imagecopyresampled() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

848
            imagecopyresampled(/** @scrutinizer ignore-type */ $canvas, $image, (int)$origin_x, (int)$origin_y, (int)$src_x, (int)$src_y, (int)$newWidth, (int)$newHeight, (int)$src_w, (int)$src_h);
Loading history...
849
        } else {
850
            // copy and resize part of an image with resampling
851
            imagecopyresampled($canvas, $image, 0, 0, 0, 0, (int)$newWidth, (int)$newHeight, (int)$width, (int)$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] = (int)$filterSettings[$i];
863
                        } else {
864
                            $filterSettings[$i] = null;
865
                        }
866
                    }
867
868
                    switch ($imageFilters[$filterSettings[0]][1]) {
869
                        case 1:
870
871
                            imagefilter($canvas, $imageFilters[$filterSettings[0]][0], $filterSettings[1]);
0 ignored issues
show
It seems like $canvas can also be of type false; however, parameter $image of imagefilter() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

871
                            imagefilter(/** @scrutinizer ignore-type */ $canvas, $imageFilters[$filterSettings[0]][0], $filterSettings[1]);
Loading history...
872
873
                            break;
874
                        case 2:
875
                            imagefilter($canvas, $imageFilters[$filterSettings[0]][0], $filterSettings[1], $filterSettings[2]);
876
877
                            break;
878
                        case 3:
879
                            imagefilter($canvas, $imageFilters[$filterSettings[0]][0], $filterSettings[1], $filterSettings[2], $filterSettings[3]);
880
881
                            break;
882
                        case 4:
883
                            imagefilter($canvas, $imageFilters[$filterSettings[0]][0], $filterSettings[1], $filterSettings[2], $filterSettings[3], $filterSettings[4]);
884
885
                            break;
886
                        default:
887
                            imagefilter($canvas, $imageFilters[$filterSettings[0]][0]);
888
                            break;
889
                    }
890
                }
891
            }
892
        }
893
894
        // sharpen image
895
        if ($sharpen && function_exists('imageconvolution')) {
896
            $sharpenMatrix = [
897
                [-1, -1, -1],
898
                [-1, 16, -1],
899
                [-1, -1, -1],
900
            ];
901
902
            $divisor = 8;
903
            $offset  = 0;
904
905
            imageconvolution($canvas, $sharpenMatrix, $divisor, $offset);
0 ignored issues
show
It seems like $canvas can also be of type false; however, parameter $image of imageconvolution() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

905
            imageconvolution(/** @scrutinizer ignore-type */ $canvas, $sharpenMatrix, $divisor, $offset);
Loading history...
906
        }
907
        //Straight from Wordpress core code. Reduces filesize by up to 70% for PNG's
908
        if ((IMAGETYPE_PNG == $origType || IMAGETYPE_GIF == $origType) && function_exists('imageistruecolor') && !imageistruecolor($image) && imagecolortransparent($image) > 0) {
909
            imagetruecolortopalette($canvas, false, imagecolorstotal($image));
0 ignored issues
show
It seems like $canvas can also be of type false; however, parameter $image of imagetruecolortopalette() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

909
            imagetruecolortopalette(/** @scrutinizer ignore-type */ $canvas, false, imagecolorstotal($image));
Loading history...
910
        }
911
912
        $imgType  = '';
913
        $tempfile = tempnam($this->cacheDirectory, 'timthumb_tmpimg_');
914
        if (preg_match('/^image\/(?:jpg|jpeg)$/i', $mimeType)) {
915
            $imgType = 'jpg';
916
            imagejpeg($canvas, $tempfile, $quality);
0 ignored issues
show
It seems like $canvas can also be of type false; however, parameter $image of imagejpeg() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

916
            imagejpeg(/** @scrutinizer ignore-type */ $canvas, $tempfile, $quality);
Loading history...
917
        } elseif (preg_match('/^image\/png$/i', $mimeType)) {
918
            $imgType = 'png';
919
            imagepng($canvas, $tempfile, floor($quality * 0.09));
0 ignored issues
show
floor($quality * 0.09) of type double is incompatible with the type integer expected by parameter $quality of imagepng(). ( Ignorable by Annotation )

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

919
            imagepng($canvas, $tempfile, /** @scrutinizer ignore-type */ floor($quality * 0.09));
Loading history...
It seems like $canvas can also be of type false; however, parameter $image of imagepng() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

919
            imagepng(/** @scrutinizer ignore-type */ $canvas, $tempfile, floor($quality * 0.09));
Loading history...
920
        } elseif (preg_match('/^image\/gif$/i', $mimeType)) {
921
            $imgType = 'gif';
922
            imagegif($canvas, $tempfile);
0 ignored issues
show
It seems like $canvas can also be of type false; however, parameter $image of imagegif() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

922
            imagegif(/** @scrutinizer ignore-type */ $canvas, $tempfile);
Loading history...
923
        } else {
924
            return $this->sanityFail('Could not match mime type after verifying it previously.');
925
        }
926
927
        if ('png' === $imgType && OPTIPNG_ENABLED && OPTIPNG_PATH && @is_file(OPTIPNG_PATH)) {
928
            $exec = OPTIPNG_PATH;
0 ignored issues
show
The assignment to $exec is dead and can be removed.
Loading history...
929
            $this->debug(3, "optipng'ing $tempfile");
930
            $presize = filesize($tempfile);
931
            $out     = shell_exec('$exec -o1 $tempfile'); //you can use up to -o7 but it really slows things down
0 ignored issues
show
The assignment to $out is dead and can be removed.
Loading history...
932
            clearstatcache();
933
            $aftersize = filesize($tempfile);
934
            $sizeDrop  = $presize - $aftersize;
935
            if ($sizeDrop > 0) {
936
                $this->debug(1, "optipng reduced size by $sizeDrop");
937
            } elseif ($sizeDrop < 0) {
938
                $this->debug(1, "optipng increased size! Difference was: $sizeDrop");
939
            } else {
940
                $this->debug(1, 'optipng did not change image size.');
941
            }
942
        } elseif ('png' === $imgType && PNGCRUSH_ENABLED && PNGCRUSH_PATH && @is_file(PNGCRUSH_PATH)) {
943
            $exec      = PNGCRUSH_PATH;
944
            $tempfile2 = tempnam($this->cacheDirectory, 'timthumb_tmpimg_');
945
            $this->debug(3, "pngcrush'ing $tempfile to $tempfile2");
946
            $out   = shell_exec('$exec $tempfile $tempfile2');
947
            $todel = '';
948
            if (is_file($tempfile2)) {
949
                $sizeDrop = filesize($tempfile) - filesize($tempfile2);
950
                if ($sizeDrop > 0) {
951
                    $this->debug(1, "pngcrush was succesful and gave a $sizeDrop byte size reduction");
952
                    $todel    = $tempfile;
953
                    $tempfile = $tempfile2;
954
                } else {
955
                    $this->debug(1, "pngcrush did not reduce file size. Difference was $sizeDrop bytes.");
956
                    $todel = $tempfile2;
957
                }
958
            } else {
959
                $this->debug(3, "pngcrush failed with output: $out");
960
                $todel = $tempfile2;
961
            }
962
            @unlink($todel);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). 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

962
            /** @scrutinizer ignore-unhandled */ @unlink($todel);

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...
963
        }
964
965
        $this->debug(3, 'Rewriting image with security header.');
966
        $tempfile4 = tempnam($this->cacheDirectory, 'timthumb_tmpimg_');
967
        $context   = stream_context_create();
968
        $fp        = fopen($tempfile, 'rb', false, $context);
969
        file_put_contents($tempfile4, $this->filePrependSecurityBlock . $imgType . ' ?' . '>'); //6 extra bytes, first 3 being image type
970
        file_put_contents($tempfile4, $fp, FILE_APPEND);
971
        fclose($fp);
0 ignored issues
show
It seems like $fp can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

971
        fclose(/** @scrutinizer ignore-type */ $fp);
Loading history...
972
        @unlink($tempfile);
973
        $this->debug(3, 'Locking and replacing cache file.');
974
        $lockFile = $this->cachefile . '.lock';
975
        $fh       = fopen($lockFile, 'wb');
976
        if (!$fh) {
0 ignored issues
show
$fh is of type false|resource, thus it always evaluated to false.
Loading history...
977
            return $this->error('Could not open the lockfile for writing an image.');
978
        }
979
        if (flock($fh, LOCK_EX)) {
980
            @unlink($this->cachefile); //rename generally overwrites, but doing this in case of platform specific quirks. File might not exist yet.
981
            rename($tempfile4, $this->cachefile);
982
            flock($fh, LOCK_UN);
983
            fclose($fh);
984
            @unlink($lockFile);
985
        } else {
986
            fclose($fh);
987
            @unlink($lockFile);
988
            @unlink($tempfile4);
989
990
            return $this->error('Could not get a lock for writing.');
991
        }
992
        $this->debug(3, 'Done image replace with security header. Cleaning up and running cleanCache()');
993
        imagedestroy($canvas);
994
        imagedestroy($image);
995
996
        return true;
997
    }
998
999
    protected function calcDocRoot()
1000
    {
1001
        $docRoot = @$_SERVER['DOCUMENT_ROOT'];
1002
        if (defined('LOCAL_FILE_BASE_DIRECTORY')) {
1003
            $docRoot = LOCAL_FILE_BASE_DIRECTORY;
0 ignored issues
show
The constant LOCAL_FILE_BASE_DIRECTORY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1004
        }
1005
        if (!isset($docRoot)) {
1006
            $this->debug(3, 'DOCUMENT_ROOT is not set. This is probably windows. Starting search 1.');
1007
            if (\Xmf\Request::hasVar('SCRIPT_FILENAME', 'SERVER')) {
1008
                $docRoot = str_replace('\\', '/', mb_substr($_SERVER['SCRIPT_FILENAME'], 0, 0 - mb_strlen($_SERVER['SCRIPT_NAME'])));
1009
                $this->debug(3, "Generated docRoot using SCRIPT_FILENAME and SCRIPT_NAME as: $docRoot");
1010
            }
1011
        }
1012
        if (!isset($docRoot)) {
1013
            $this->debug(3, 'DOCUMENT_ROOT still is not set. Starting search 2.');
1014
            if (\Xmf\Request::hasVar('PATH_TRANSLATED', 'SERVER')) {
1015
                $docRoot = str_replace('\\', '/', mb_substr(str_replace('\\\\', '\\', $_SERVER['PATH_TRANSLATED']), 0, 0 - mb_strlen($_SERVER['SCRIPT_NAME'])));
1016
                $this->debug(3, "Generated docRoot using PATH_TRANSLATED and SCRIPT_NAME as: $docRoot");
1017
            }
1018
        }
1019
        if ($docRoot && '/' !== $_SERVER['DOCUMENT_ROOT']) {
1020
            $docRoot = preg_replace('/\/$/', '', $docRoot);
1021
        }
1022
        $this->debug(3, 'Doc root is: ' . $docRoot);
1023
        $this->docRoot = $docRoot;
1024
    }
1025
1026
    /**
1027
     * @param $src
1028
     *
1029
     * @return bool|string
1030
     */
1031
    protected function getLocalImagePath($src)
1032
    {
1033
        $src = ltrim($src, '/'); //strip off the leading '/'
1034
        if (!$this->docRoot) {
1035
            $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.');
1036
            //We don't support serving images outside the current dir if we don't have a doc root for security reasons.
1037
            $file = preg_replace('/^.*?([^\/\\\\]+)$/', '$1', $src); //strip off any path info and just leave the filename.
1038
            if (is_file($file)) {
1039
                return $this->realpath($file);
1040
            }
1041
1042
            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.");
1043
        }
1044
        if (!is_dir($this->docRoot)) {
1045
            $this->error("Server path does not exist. Ensure variable \$_SERVER['DOCUMENT_ROOT'] is set correctly");
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 === mb_stripos($real, $this->docRoot)) {
1055
                return $real;
1056
            }
1057
            $this->debug(1, 'Security block: The file specified occurs outside the document root.');
1058
            //allow search to continue
1059
        }
1060
        //Check absolute paths and then verify the real path is under doc root
1061
        $absolute = $this->realpath('/' . $src);
1062
        if ($absolute && file_exists($absolute)) {
1063
            //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 === mb_stripos($absolute, $this->docRoot)) {
1069
                return $absolute;
1070
            }
1071
            $this->debug(1, 'Security block: The file specified occurs outside the document root.');
1072
            //and continue search
1073
        }
1074
1075
        $base = $this->docRoot;
1076
1077
        // account for Windows directory structure
1078
        if (false !== mb_strpos($_SERVER['SCRIPT_FILENAME'], ':')) {
1079
            $subDirectories = explode('\\', str_replace($this->docRoot, '', $_SERVER['SCRIPT_FILENAME']));
1080
        } else {
1081
            $subDirectories = explode('/', str_replace($this->docRoot, '', $_SERVER['SCRIPT_FILENAME']));
1082
        }
1083
1084
        foreach ($subDirectories as $sub) {
1085
            $base .= $sub . '/';
1086
            $this->debug(3, 'Trying file as: ' . $base . $src);
1087
            if (file_exists($base . $src)) {
1088
                $this->debug(3, 'Found file as: ' . $base . $src);
1089
                $real = $this->realpath($base . $src);
1090
                if (0 === mb_stripos($real, $this->realpath($this->docRoot))) {
1091
                    return $real;
1092
                }
1093
                $this->debug(1, 'Security block: The file specified occurs outside the document root.');
1094
                //And continue search
1095
            }
1096
        }
1097
1098
        return false;
1099
    }
1100
1101
    /**
1102
     * @param $path
1103
     *
1104
     * @return string
1105
     */
1106
    protected function realpath($path)
1107
    {
1108
        //try to remove any relative paths
1109
        $removeRelatives = '/\w+\/\.\.\//';
1110
        while (preg_match($removeRelatives, $path)) {
1111
            $path = preg_replace($removeRelatives, '', $path);
1112
        }
1113
        //if any remain use PHP realpath to strip them out, otherwise return $path
1114
        //if using realpath, any symlinks will also be resolved
1115
        return preg_match('#^\.\./|/\.\./#', $path) ? realpath($path) : $path;
1116
    }
1117
1118
    /**
1119
     * @param $name
1120
     */
1121
    protected function toDelete($name)
1122
    {
1123
        $this->debug(3, "Scheduling file $name to delete on destruct.");
1124
        $this->toDeletes[] = $name;
1125
    }
1126
1127
    /**
1128
     * @return bool
1129
     */
1130
    protected function serveWebshot()
1131
    {
1132
        $this->debug(3, 'Starting serveWebshot');
1133
        $instr = 'Please follow the instructions at http://code.google.com/p/timthumb/ to set your server up for taking website screenshots.';
1134
        if (!is_file(WEBSHOT_CUTYCAPT)) {
1135
            return $this->error("CutyCapt is not installed. $instr");
1136
        }
1137
        if (!is_file(WEBSHOT_XVFB)) {
1138
            return $this->error("Xvfb is not installed. $instr");
1139
        }
1140
        $cuty      = WEBSHOT_CUTYCAPT;
1141
        $xv        = WEBSHOT_XVFB;
1142
        $screenX   = WEBSHOT_SCREEN_X;
1143
        $screenY   = WEBSHOT_SCREEN_Y;
1144
        $colDepth  = WEBSHOT_COLOR_DEPTH;
1145
        $format    = WEBSHOT_IMAGE_FORMAT;
1146
        $timeout   = WEBSHOT_TIMEOUT * 1000;
1147
        $ua        = WEBSHOT_USER_AGENT;
1148
        $jsOn      = WEBSHOT_JAVASCRIPT_ON ? 'on' : 'off';
1149
        $javaOn    = WEBSHOT_JAVA_ON ? 'on' : 'off';
1150
        $pluginsOn = WEBSHOT_PLUGINS_ON ? 'on' : 'off';
1151
        $proxy     = WEBSHOT_PROXY ? ' --http-proxy=' . WEBSHOT_PROXY : '';
1152
        $tempfile  = tempnam($this->cacheDirectory, 'timthumb_webshot');
1153
        $url       = $this->src;
1154
        if (!preg_match('/^https?:\/\/[a-zA-Z0-9\.\-]+/i', $url)) {
1155
            return $this->error('Invalid URL supplied.');
1156
        }
1157
        $url = preg_replace('/[^A-Za-z0-9\-\.\_:\/\?\&\+\;\=]+/', '', $url); //RFC 3986 plus ()$ chars to prevent exploit below. Plus the following are also removed: @*!~#[]',
1158
        // 2014 update by Mark Maunder: This exploit: http://cxsecurity.com/issue/WLB-2014060134
1159
        // uses the $(command) shell execution syntax to execute arbitrary shell commands as the web server user.
1160
        // So we're now filtering out the characters: '$', '(' and ')' in the above regex to avoid this.
1161
        // We are also filtering out chars rarely used in URLs but legal accoring to the URL RFC which might be exploitable. These include: @*!~#[]',
1162
        // 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.
1163
        if (WEBSHOT_XVFB_RUNNING) {
1164
            putenv('DISPLAY=:100.0');
1165
            $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";
1166
        } else {
1167
            $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";
1168
        }
1169
        $this->debug(3, "Executing command: $command");
1170
        $out = shell_exec('$command');
1171
        $this->debug(3, "Received output: $out");
1172
        if (!is_file($tempfile)) {
1173
            $this->set404();
1174
1175
            return $this->error('The command to create a thumbnail failed.');
1176
        }
1177
        $this->cropTop = true;
1178
        if ($this->processImageAndWriteToCache($tempfile)) {
1179
            $this->debug(3, 'Image processed successfully. Serving from cache');
1180
1181
            return $this->serveCacheFile();
1182
        }
1183
1184
        return false;
1185
    }
1186
1187
    /**
1188
     * @return bool
1189
     */
1190
    protected function serveExternalImage()
1191
    {
1192
        if (!preg_match('/^https?:\/\/[a-zA-Z0-9\-\.]+/i', $this->src)) {
1193
            $this->error('Invalid URL supplied.');
1194
1195
            return false;
1196
        }
1197
        $tempfile = tempnam($this->cacheDirectory, 'timthumb');
1198
        $this->debug(3, "Fetching external image into temporary file $tempfile");
1199
        $this->toDelete($tempfile);
1200
        #fetch file here
1201
        if (!$this->getURL($this->src, $tempfile)) {
1202
            @unlink($this->cachefile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). 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

1202
            /** @scrutinizer ignore-unhandled */ @unlink($this->cachefile);

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...
1203
            touch($this->cachefile);
1204
            $this->debug(3, 'Error fetching URL: ' . $this->lastURLError);
1205
            $this->error('Error reading the URL you specified from remote host.' . $this->lastURLError);
1206
1207
            return false;
1208
        }
1209
1210
        $mimeType = $this->getMimeType($tempfile);
1211
        if (!preg_match("/^image\/(?:jpg|jpeg|gif|png)$/i", $mimeType)) {
1212
            $this->debug(3, "Remote file has invalid mime type: $mimeType");
1213
            @unlink($this->cachefile);
1214
            touch($this->cachefile);
1215
            $this->error("The remote file is not a valid image. Mimetype = '" . $mimeType . "'" . $tempfile);
1216
1217
            return false;
1218
        }
1219
        if ($this->processImageAndWriteToCache($tempfile)) {
1220
            $this->debug(3, 'Image processed successfully. Serving from cache');
1221
1222
            return $this->serveCacheFile();
1223
        }
1224
1225
        return false;
1226
    }
1227
1228
    /**
1229
     * @param $h
1230
     * @param $d
1231
     *
1232
     * @return int
1233
     */
1234
    public static function curlWrite($h, $d)
0 ignored issues
show
The parameter $h is not used and could be removed. ( Ignorable by Annotation )

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

1234
    public static function curlWrite(/** @scrutinizer ignore-unused */ $h, $d)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1235
    {
1236
        fwrite(self::$curlFH, $d);
0 ignored issues
show
self::curlFH of type boolean is incompatible with the type resource expected by parameter $handle of fwrite(). ( Ignorable by Annotation )

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

1236
        fwrite(/** @scrutinizer ignore-type */ self::$curlFH, $d);
Loading history...
1237
        self::$curlDataWritten += mb_strlen($d);
1238
        if (self::$curlDataWritten > MAX_FILE_SIZE) {
1239
            return 0;
1240
        }
1241
1242
        return mb_strlen($d);
1243
    }
1244
1245
    /**
1246
     * @return bool
1247
     */
1248
    protected function serveCacheFile()
1249
    {
1250
        $this->debug(3, "Serving {$this->cachefile}");
1251
        if (!is_file($this->cachefile)) {
1252
            $this->error("serveCacheFile called in timthumb but we couldn't find the cached file.");
1253
1254
            return false;
1255
        }
1256
        $fp = fopen($this->cachefile, 'rb');
1257
        if (!$fp) {
0 ignored issues
show
$fp is of type false|resource, thus it always evaluated to false.
Loading history...
1258
            return $this->error('Could not open cachefile.');
1259
        }
1260
        fseek($fp, mb_strlen($this->filePrependSecurityBlock), SEEK_SET);
1261
        $imgType = fread($fp, 3);
1262
        fseek($fp, 3, SEEK_CUR);
1263
        if (ftell($fp) != mb_strlen($this->filePrependSecurityBlock) + 6) {
1264
            @unlink($this->cachefile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). 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

1264
            /** @scrutinizer ignore-unhandled */ @unlink($this->cachefile);

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...
1265
1266
            return $this->error('The cached image file seems to be corrupt.');
1267
        }
1268
        $imageDataSize = filesize($this->cachefile) - (mb_strlen($this->filePrependSecurityBlock) + 6);
1269
        $this->sendImageHeaders($imgType, $imageDataSize);
1270
        $bytesSent = @fpassthru($fp);
1271
        fclose($fp);
1272
        if ($bytesSent > 0) {
1273
            return true;
1274
        }
1275
        $content = file_get_contents($this->cachefile);
1276
        if (false !== $content) {
1277
            $content = mb_substr($content, mb_strlen($this->filePrependSecurityBlock) + 6);
1278
            echo $content;
1279
            $this->debug(3, 'Served using file_get_contents and echo');
1280
1281
            return true;
1282
        }
1283
        $this->error('Cache file could not be loaded.');
1284
1285
        return false;
1286
    }
1287
1288
    /**
1289
     * @param $mimeType
1290
     * @param $dataSize
1291
     *
1292
     * @return bool
1293
     */
1294
    protected function sendImageHeaders($mimeType, $dataSize)
1295
    {
1296
        if (!preg_match('/^image\//i', $mimeType)) {
1297
            $mimeType = 'image/' . $mimeType;
1298
        }
1299
        if ('image/jpg' === mb_strtolower($mimeType)) {
1300
            $mimeType = 'image/jpeg';
1301
        }
1302
        $gmdateExpires   = gmdate('D, d M Y H:i:s', strtotime('now +10 days')) . ' GMT';
1303
        $gmdate_modified = gmdate('D, d M Y H:i:s') . ' GMT';
1304
        // send content headers then display image
1305
        header('Content-Type: ' . $mimeType);
1306
        header('Accept-Ranges: none'); //Changed this because we don't accept range requests
1307
        header('Last-Modified: ' . $gmdate_modified);
1308
        header('Content-Length: ' . $dataSize);
1309
        if (BROWSER_CACHE_DISABLE) {
1310
            $this->debug(3, 'Browser cache is disabled so setting non-caching headers.');
1311
            header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
1312
            header('Pragma: no-cache');
1313
            header('Expires: ' . gmdate('D, d M Y H:i:s', time()));
1314
        } else {
1315
            $this->debug(3, 'Browser caching is enabled');
1316
            header('Cache-Control: max-age=' . BROWSER_CACHE_MAX_AGE . ', must-revalidate');
1317
            header('Expires: ' . $gmdateExpires);
1318
        }
1319
1320
        return true;
1321
    }
1322
1323
    protected function securityChecks()
1324
    {
1325
    }
1326
1327
    /**
1328
     * @param        $property
1329
     * @param string $default
1330
     *
1331
     * @return string
1332
     */
1333
    protected function param($property, $default = '')
1334
    {
1335
        if (isset($_GET[$property])) {
1336
            return Request::getString($property, '', 'GET');
1337
        }
1338
1339
        return $default;
1340
    }
1341
1342
    /**
1343
     * @param $mimeType
1344
     * @param $src
1345
     *
1346
     * @return resource
1347
     */
1348
    protected function openImage($mimeType, $src)
1349
    {
1350
        $image = '';
1351
        switch ($mimeType) {
1352
            case 'image/jpeg':
1353
1354
                $image = imagecreatefromjpeg($src);
1355
1356
                break;
1357
            case 'image/png':
1358
                $image = imagecreatefrompng($src);
1359
                imagealphablending($image, true);
0 ignored issues
show
It seems like $image can also be of type false; however, parameter $image of imagealphablending() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

1359
                imagealphablending(/** @scrutinizer ignore-type */ $image, true);
Loading history...
1360
                imagesavealpha($image, true);
0 ignored issues
show
It seems like $image can also be of type false; however, parameter $image of imagesavealpha() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

1360
                imagesavealpha(/** @scrutinizer ignore-type */ $image, true);
Loading history...
1361
1362
                break;
1363
            case 'image/gif':
1364
                $image = imagecreatefromgif($src);
1365
1366
                break;
1367
            default:
1368
                $this->error('Unrecognised mimeType');
1369
        }
1370
1371
        return $image;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $image also could return the type false|string which is incompatible with the documented return type resource.
Loading history...
1372
    }
1373
1374
    /**
1375
     * @return string
1376
     */
1377
    protected function getIP()
1378
    {
1379
        $rem = @$_SERVER['REMOTE_ADDR'];
1380
        $ff  = @$_SERVER['HTTP_X_FORWARDED_FOR'];
1381
        $ci  = @$_SERVER['HTTP_CLIENT_IP'];
1382
        if (preg_match('/^(?:192\.168|172\.16|10\.|127\.)/', $rem)) {
1383
            if ($ff) {
1384
                return $ff;
1385
            }
1386
            if ($ci) {
1387
                return $ci;
1388
            }
1389
1390
            return $rem;
1391
        }
1392
        if ($rem) {
1393
            return $rem;
1394
        }
1395
        if ($ff) {
1396
            return $ff;
1397
        }
1398
        if ($ci) {
1399
            return $ci;
1400
        }
1401
1402
        return 'UNKNOWN';
1403
    }
1404
1405
    /**
1406
     * @param $level
1407
     * @param $msg
1408
     */
1409
    protected function debug($level, $msg)
1410
    {
1411
        if (DEBUG_ON && $level <= DEBUG_LEVEL) {
1412
            $execTime = sprintf('%.6f', microtime(true) - $this->startTime);
1413
            $tick     = sprintf('%.6f', 0);
1414
            if ($this->lastBenchTime > 0) {
1415
                $tick = sprintf('%.6f', microtime(true) - $this->lastBenchTime);
1416
            }
1417
            $this->lastBenchTime = microtime(true);
1418
            error_log('TimThumb Debug line ' . __LINE__ . " [$execTime : $tick]: $msg");
1419
        }
1420
    }
1421
1422
    /**
1423
     * @param $msg
1424
     *
1425
     * @return bool
1426
     */
1427
    protected function sanityFail($msg)
1428
    {
1429
        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");
1430
    }
1431
1432
    /**
1433
     * @param $file
1434
     *
1435
     * @return string
1436
     */
1437
    protected function getMimeType($file)
1438
    {
1439
        $info = getimagesize($file);
1440
        if (is_array($info) && $info['mime']) {
1441
            return $info['mime'];
1442
        }
1443
1444
        return '';
1445
    }
1446
1447
    protected function setMemoryLimit()
1448
    {
1449
        $inimem   = ini_get('memory_limit');
1450
        $inibytes = self::returnBytes($inimem);
1451
        $ourbytes = self::returnBytes(MEMORY_LIMIT);
1452
        if ($inibytes < $ourbytes) {
1453
            ini_set('memory_limit', MEMORY_LIMIT);
1454
            $this->debug(3, "Increased memory from $inimem to " . MEMORY_LIMIT);
1455
        } else {
1456
            $this->debug(3, 'Not adjusting memory size because the current setting is ' . $inimem . ' and our size of ' . MEMORY_LIMIT . ' is smaller.');
1457
        }
1458
    }
1459
1460
    /**
1461
     * @param $sizeString
1462
     *
1463
     * @return int
1464
     */
1465
    protected static function returnBytes($sizeString)
1466
    {
1467
        switch (mb_substr($sizeString, -1)) {
1468
            case 'M':
1469
1470
            case 'm':
1471
1472
                return (int)$sizeString * 1048576;
1473
            case 'K':
1474
            case 'k':
1475
1476
                return (int)$sizeString * 1024;
1477
            case 'G':
1478
            case 'g':
1479
1480
                return (int)$sizeString * 1073741824;
1481
            default:
1482
                return $sizeString;
1483
        }
1484
    }
1485
1486
    /**
1487
     * @param $url
1488
     * @param $tempfile
1489
     *
1490
     * @return bool
1491
     */
1492
    protected function getURL($url, $tempfile)
1493
    {
1494
        $this->lastURLError = false;
1495
        $url                = preg_replace('/ /', '%20', $url);
1496
        if (function_exists('curl_init')) {
1497
            $this->debug(3, 'Curl is installed so using it to fetch URL.');
1498
            self::$curlFH = fopen($tempfile, 'wb');
1499
            if (!self::$curlFH) {
1500
                $this->error("Could not open $tempfile for writing.");
1501
1502
                return false;
1503
            }
1504
            self::$curlDataWritten = 0;
1505
            $this->debug(3, "Fetching url with curl: $url");
1506
            $curl = curl_init($url);
1507
            curl_setopt($curl, CURLOPT_TIMEOUT, CURL_TIMEOUT);
0 ignored issues
show
It seems like $curl can also be of type false; however, parameter $ch of curl_setopt() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

1507
            curl_setopt(/** @scrutinizer ignore-type */ $curl, CURLOPT_TIMEOUT, CURL_TIMEOUT);
Loading history...
1508
            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');
1509
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
1510
            curl_setopt($curl, CURLOPT_HEADER, 0);
1511
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true); //was false before
1512
            curl_setopt($curl, CURLOPT_WRITEFUNCTION, 'timthumb::curlWrite');
1513
            @curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for curl_setopt(). 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

1513
            /** @scrutinizer ignore-unhandled */ @curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);

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...
1514
            @curl_setopt($curl, CURLOPT_MAXREDIRS, 10);
1515
1516
            $curlResult = curl_exec($curl);
0 ignored issues
show
It seems like $curl can also be of type false; however, parameter $ch of curl_exec() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

1516
            $curlResult = curl_exec(/** @scrutinizer ignore-type */ $curl);
Loading history...
1517
            fclose(self::$curlFH);
1518
            $httpStatus = curl_getinfo($curl, CURLINFO_HTTP_CODE);
0 ignored issues
show
It seems like $curl can also be of type false; however, parameter $ch of curl_getinfo() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

1518
            $httpStatus = curl_getinfo(/** @scrutinizer ignore-type */ $curl, CURLINFO_HTTP_CODE);
Loading history...
1519
            if (404 == $httpStatus) {
1520
                $this->set404();
1521
            }
1522
            if (302 == $httpStatus) {
1523
                $this->error('External Image is Redirecting. Try alternate image url');
1524
1525
                return false;
1526
            }
1527
            if ($curlResult) {
1528
                curl_close($curl);
0 ignored issues
show
It seems like $curl can also be of type false; however, parameter $ch of curl_close() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

1528
                curl_close(/** @scrutinizer ignore-type */ $curl);
Loading history...
1529
1530
                return true;
1531
            }
1532
            $this->lastURLError = curl_error($curl);
0 ignored issues
show
It seems like $curl can also be of type false; however, parameter $ch of curl_error() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

1532
            $this->lastURLError = curl_error(/** @scrutinizer ignore-type */ $curl);
Loading history...
1533
            curl_close($curl);
1534
1535
            return false;
1536
        }
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 !== mb_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
     * @param $file
1561
     *
1562
     * @return bool
1563
     */
1564
    protected function serveImg($file)
1565
    {
1566
        $s = getimagesize($file);
1567
        if (!($s && $s['mime'])) {
1568
            return false;
1569
        }
1570
        header('Content-Type: ' . $s['mime']);
1571
        header('Content-Length: ' . filesize($file));
1572
        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
1573
        header('Pragma: no-cache');
1574
        $bytes = @readfile($file);
1575
        if ($bytes > 0) {
1576
            return true;
1577
        }
1578
        $content = @file_get_contents($file);
1579
        if (false !== $content) {
1580
            echo $content;
1581
1582
            return true;
1583
        }
1584
1585
        return false;
1586
    }
1587
1588
    protected function set404()
1589
    {
1590
        $this->is404 = true;
1591
    }
1592
1593
    /**
1594
     * @return bool
1595
     */
1596
    protected function is404()
1597
    {
1598
        return $this->is404;
1599
    }
1600
}
1601