Issues (311)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

lib/timber-image-helper.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * Implements the Twig image filters:
5
 * https://github.com/jarednova/timber/wiki/Image-cookbook#arbitrary-resizing-of-images
6
 * - resize
7
 * - retina
8
 * - letterbox
9
 * - tojpg
10
 *
11
 * Implementation:
12
 * - public static functions provide the methods that are called by the filter
13
 * - most of the work is common to all filters (URL analysis, directory gymnastics, file caching, error management) and done by private static functions
14
 * - the specific part (actual image processing) is delegated to dedicated subclasses of TimberImageOperation
15
 */
16
class TimberImageHelper {
17
18
	const BASE_UPLOADS = 1;
19
	const BASE_CONTENT = 2;
20
21
	public static function init() {
22
		self::add_constants();
23
		self::add_actions();
24
		self::add_filters();
25
	}
26
27
	/**
28
	 * Generates a new image with the specified dimensions.
29
	 * New dimensions are achieved by cropping to maintain ratio.
30
	 *
31
	 * @api
32
	 * @param string  		$src an URL (absolute or relative) to the original image
33
	 * @param int|string	$w target width(int) or WordPress image size (WP-set or user-defined).
34
	 * @param int     		$h target height (ignored if $w is WP image size). If not set, will ignore and resize based on $w only.
35
	 * @param string  		$crop your choices are 'default', 'center', 'top', 'bottom', 'left', 'right'
36
	 * @param bool    		$force
37
	 * @example
38
	 * ```twig
39
	 * <img src="{{ image.src | resize(300, 200, 'top') }}" />
40
	 * ```
41
	 * ```html
42
	 * <img src="http://example.org/wp-content/uploads/pic-300x200-c-top.jpg" />
43
	 * ```
44
	 * @return string (ex: )
45
	 */
46
	public static function resize( $src, $w, $h = 0, $crop = 'default', $force = false ) {
47
		if ( !is_numeric($w) && is_string($w) ) {
48
			if ( $sizes = self::find_wp_dimensions($w) ) {
49
				$w = $sizes['w'];
50
				$h = $sizes['h'];
51
			} else {
52
				return $src;
53
			}
54
		}
55
		$op = new TimberImageOperationResize($w, $h, $crop);
56
		return self::_operate($src, $op, $force);
57
	}
58
59
	/**
60
	 * Find the sizes of an image based on a defined image size
61
	 * @param  string $size the image size to search for
62
	 *                      can be WordPress-defined ("medium")
63
	 *                      or user-defined ("my-awesome-size")
64
	 * @return false|array {
65
	 *     @type int w
66
	 *     @type int h
67
	 * }
68
	 */
69
	private static function find_wp_dimensions( $size ) {
70
		global $_wp_additional_image_sizes;
71
		if ( isset($_wp_additional_image_sizes[$size]) ) {
72
			$w = $_wp_additional_image_sizes[$size]['width'];
73
			$h = $_wp_additional_image_sizes[$size]['height'];
74
		} else if ( in_array($size, array('thumbnail', 'medium', 'large')) ) {
75
			$w = get_option($size.'_size_w');
76
			$h = get_option($size.'_size_h');
77
		}
78
		if ( isset($w) && isset($h) && ($w || $h) ) {
79
			return array('w' => $w, 'h' => $h);
80
		}
81
		return false;
82
	}
83
84
	/**
85
	 * Generates a new image with increased size, for display on Retina screens.
86
	 *
87
	 * @param string  $src
88
	 * @param float   $multiplier
89
	 * @param boolean $force
90
	 *
91
	 * @return string url to the new image
92
	 */
93
	public static function retina_resize( $src, $multiplier = 2, $force = false ) {
94
		$op = new TimberImageOperationRetina($multiplier);
95
		return self::_operate($src, $op, $force);
96
	}
97
98
	/**
99
	 * checks to see if the given file is an aimated gif
100
	 * @param  string  $file local filepath to a file, not a URL
101
	 * @return boolean true if it's an animated gif, false if not
102
	 */
103
	public static function is_animated_gif( $file ) {
104
		if ( strpos(strtolower($file), '.gif') == -1 ) {
105
			//doesn't have .gif, bail
106
			return false;
107
		}
108
		//its a gif so test
109
		if( !($fh = @fopen($file, 'rb')) ) {
110
		  	return false;
111
		}
112
		$count = 0;
113
		//an animated gif contains multiple "frames", with each frame having a
114
		//header made up of:
115
		// * a static 4-byte sequence (\x00\x21\xF9\x04)
116
		// * 4 variable bytes
117
		// * a static 2-byte sequence (\x00\x2C)
118
119
		// We read through the file til we reach the end of the file, or we've found
120
		// at least 2 frame headers
121
		while(!feof($fh) && $count < 2) {
122
			$chunk = fread($fh, 1024 * 100); //read 100kb at a time
123
			$count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', $chunk, $matches);
124
	    }
125
126
	    fclose($fh);
127
	    return $count > 1;
128
	}
129
130
	/**
131
	 * Generate a new image with the specified dimensions.
132
	 * New dimensions are achieved by adding colored bands to maintain ratio.
133
	 *
134
	 * @param string  $src
135
	 * @param int     $w
136
	 * @param int     $h
137
	 * @param string  $color
138
	 * @param bool    $force
139
	 * @return mixed|null|string
140
	 */
141
	public static function letterbox( $src, $w, $h, $color = '#000000', $force = false ) {
142
		$op = new TimberImageOperationLetterbox($w, $h, $color);
143
		return self::_operate($src, $op, $force);
144
	}
145
146
	/**
147
	 * Generates a new image by converting the source GIF or PNG into JPG
148
	 *
149
	 * @param string  $src   a url or path to the image (http://example.org/wp-content/uploads/2014/image.jpg) or (/wp-content/uploads/2014/image.jpg)
150
	 * @param string  $bghex
151
	 * @return string
152
	 */
153
	public static function img_to_jpg( $src, $bghex = '#FFFFFF', $force = false ) {
154
		$op = new TimberImageOperationToJpg($bghex);
155
		return self::_operate($src, $op, $force);
156
	}
157
158
	/**
159
	 * Deletes all resized versions of an image when the source is deleted
160
	 */
161
	protected static function add_actions() {
162
		add_action( 'delete_attachment', function ( $post_id ) {
163
			$post = get_post( $post_id );
164
			$image_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/jpg' );
165
			if ( in_array( $post->post_mime_type, $image_types ) ) {
166
				$attachment = new TimberImage( $post_id );
167
				if ( $attachment->file_loc ) {
168
					TimberImageHelper::delete_generated_files( $attachment->file_loc );
169
				}
170
			}
171
		} );
172
	}
173
174
	/**
175
	 * Adds a constant defining the path to the content directory relative to the site
176
	 * for example /wp-content or /content
177
	 */
178
	protected static function add_constants() {
179
		if ( !defined( 'WP_CONTENT_SUBDIR' ) ) {
180
			$wp_content_path = str_replace( home_url(), '', WP_CONTENT_URL );
181
			define( 'WP_CONTENT_SUBDIR', $wp_content_path );
182
		}
183
	}
184
185
	/**
186
	 * adds a 'relative' key to wp_upload_dir() result.
187
	 * It will contain the relative url to upload dir.
188
	 * @return void
189
	 */
190
	static function add_filters() {
191
		add_filter( 'upload_dir', function ( $arr ) {
192
			$arr['relative'] = str_replace( home_url(), '', $arr['baseurl'] );
193
			return $arr;
194
		} );
195
	}
196
197
	//-- end of public methods --//
198
	/**
199
	 * Deletes the auto-generated files for resize and letterboxing created by Timber
200
	 * @param string  $local_file   ex: /var/www/wp-content/uploads/2015/my-pic.jpg
201
	 *	                            or: http://example.org/wp-content/uploads/2015/my-pic.jpg
202
	 */
203
	static function delete_generated_files( $local_file ) {
204
		if (TimberURLHelper::is_absolute( $local_file ) ) {
205
			$local_file = TimberURLHelper::url_to_file_system( $local_file );
206
		}
207
		$info = pathinfo( $local_file );
208
		$dir = $info['dirname'];
209
		$ext = $info['extension'];
210
		$filename = $info['filename'];
211
		self::process_delete_generated_files( $filename, $ext, $dir, '-[0-9999999]*', '-[0-9]*x[0-9]*-c-[a-z]*.' );
212
		self::process_delete_generated_files( $filename, $ext, $dir, '-lbox-[0-9999999]*', '-lbox-[0-9]*x[0-9]*-[a-zA-Z0-9]*.' );
213
		self::process_delete_generated_files( $filename, 'jpg', $dir, '-tojpg.*' );
214
		self::process_delete_generated_files( $filename, 'jpg', $dir, '-tojpg-[0-9999999]*' );
215
	}
216
217
	/**
218
	 * Deletes resized versions of the supplied file name.
219
	 * So if passed a value like my-pic.jpg, this function will delete my-pic-500x200-c-left.jpg, my-pic-400x400-c-default.jpg, etc.
220
	 *
221
	 * keeping these here so I know what the hell we're matching
222
	 * $match = preg_match("/\/srv\/www\/wordpress-develop\/src\/wp-content\/uploads\/2014\/05\/$filename-[0-9]*x[0-9]*-c-[a-z]*.jpg/", $found_file);
223
	 * $match = preg_match("/\/srv\/www\/wordpress-develop\/src\/wp-content\/uploads\/2014\/05\/arch-[0-9]*x[0-9]*-c-[a-z]*.jpg/", $filename);
224
	 *
225
	 * @param string 	$filename   ex: my-pic
226
	 * @param string 	$ext ex: jpg
227
	 * @param string 	$dir var/www/wp-content/uploads/2015/
228
	 * @param string 	$search_pattern pattern of files to pluck from
229
	 * @param string 	$match_pattern pattern of files to go forth and delete
230
	 */
231
	protected static function process_delete_generated_files( $filename, $ext, $dir, $search_pattern, $match_pattern = null ) {
232
		$searcher = '/' . $filename . $search_pattern;
233
		foreach ( glob( $dir . $searcher ) as $found_file ) {
234
			$regexdir = str_replace( '/', '\/', $dir );
235
			$pattern = '/' . ( $regexdir ) . '\/' . $filename . $match_pattern . $ext . '/';
236
			$match = preg_match( $pattern, $found_file );
237
			if ( ! $match_pattern || $match ) {
238
				unlink( $found_file );
239
			}
240
		}
241
	}
242
243
244
	/**
245
	 * Determines the filepath corresponding to a given URL
246
	 *
247
	 * @param string  $url
248
	 * @return string
249
	 */
250
	public static function get_server_location( $url ) {
251
		// if we're already an absolute dir, just return
252
		if ( 0 === strpos( $url, ABSPATH ) ) {
253
			return $url;
254
		}
255
		// otherwise, analyze URL then build mapping path
256
		$au = self::analyze_url($url);
257
		$result = self::_get_file_path($au['base'], $au['subdir'], $au['basename']);
258
		return $result;
259
	}
260
261
	/**
262
	 * Determines the filepath where a given external file will be stored.
263
	 *
264
	 * @param string  $file
265
	 * @return string
266
	 */
267
	public static function get_sideloaded_file_loc( $file ) {
268
		$upload = wp_upload_dir();
269
		$dir = $upload['path'];
270
		$filename = $file;
271
		$file = parse_url( $file );
272
		$path_parts = pathinfo( $file['path'] );
273
		$basename = md5( $filename );
274
		$ext = 'jpg';
275
		if ( isset( $path_parts['extension'] ) ) {
276
			$ext = $path_parts['extension'];
277
		}
278
		return $dir . '/' . $basename . '.' . $ext;
279
	}
280
281
	/**
282
	 * downloads an external image to the server and stores it on the server
283
	 *
284
	 * @param string  $file the URL to the original file
285
	 * @return string the URL to the downloaded file
286
	 */
287
	public static function sideload_image( $file ) {
288
		$loc = self::get_sideloaded_file_loc( $file );
289
		if ( file_exists( $loc ) ) {
290
			return TimberURLHelper::preslashit( TimberURLHelper::get_rel_path( $loc ) );
291
		}
292
		// Download file to temp location
293
		if ( !function_exists( 'download_url' ) ) {
294
			require_once ABSPATH . '/wp-admin/includes/file.php';
295
		}
296
		$tmp = download_url( $file );
297
		preg_match( '/[^\?]+\.(jpe?g|jpe|gif|png)\b/i', $file, $matches );
298
		$file_array = array();
299
		$file_array['name'] = basename( $matches[0] );
300
		$file_array['tmp_name'] = $tmp;
301
		// If error storing temporarily, unlink
302
		if ( is_wp_error( $tmp ) ) {
303
			@unlink( $file_array['tmp_name'] );
304
			$file_array['tmp_name'] = '';
305
		}
306
		// do the validation and storage stuff
307
		$locinfo = pathinfo( $loc );
308
		$file = wp_upload_bits( $locinfo['basename'], null, file_get_contents( $file_array['tmp_name'] ) );
309
		return $file['url'];
310
	}
311
312
	/**
313
	 * Takes in an URL and breaks it into components,
314
	 * that will then be used in the different steps of image processing.
315
	 * The image is expected to be either part of a theme, plugin, or an upload.
316
	 *
317
	 * @param  string $url an URL (absolute or relative) pointing to an image
318
	 * @return array       an array (see keys in code below)
319
	 */
320
	private static function analyze_url($url) {
321
		$result = array(
322
			'url' => $url, // the initial url
323
			'absolute' => TimberURLHelper::is_absolute($url), // is the url absolute or relative (to home_url)
324
			'base' => 0, // is the image in uploads dir, or in content dir (theme or plugin)
325
			'subdir' => '', // the path between base (uploads or content) and file
326
			'filename' => '', // the filename, without extension
327
			'extension' => '', // the file extension
328
			'basename' => '', // full file name
329
		);
330
		$upload_dir = wp_upload_dir();
331
		$tmp = $url;
332
		if ( 0 === strpos($tmp, ABSPATH) ) { // we've been given a dir, not an url
333
			$result['absolute'] = true;
334 View Code Duplication
			if ( 0 === strpos($tmp, $upload_dir['basedir']) ) {
335
				$result['base']= self::BASE_UPLOADS; // upload based
336
				$tmp = str_replace($upload_dir['basedir'], '', $tmp);
337
			}
338 View Code Duplication
			if ( 0 === strpos($tmp, WP_CONTENT_DIR) ) {
339
				$result['base']= self::BASE_CONTENT; // content based
340
				$tmp = str_replace(WP_CONTENT_DIR, '', $tmp);
341
			}
342
		} else {
343
			if (!$result['absolute']) {
344
				$tmp = home_url().$tmp;
345
			}
346 View Code Duplication
			if (0 === strpos($tmp, $upload_dir['baseurl'])) {
347
				$result['base']= self::BASE_UPLOADS; // upload based
348
				$tmp = str_replace($upload_dir['baseurl'], '', $tmp);
349
			}
350 View Code Duplication
			if (0 === strpos($tmp, content_url())) {
351
				$result['base']= self::BASE_CONTENT; // content-based
352
				$tmp = str_replace(content_url(), '', $tmp);
353
			}
354
		}
355
		$parts = pathinfo($tmp);
356
		$result['subdir'] = ($parts['dirname'] === '/') ? '' : $parts['dirname'];
357
		$result['filename'] = $parts['filename'];
358
		$result['extension'] = $parts['extension'];
359
		$result['basename'] = $parts['basename'];
360
		// todo filename
361
		return $result;
362
	}
363
364
	/**
365
	 * Builds the public URL of a file based on its different components
366
	 *
367
	 * @param  int    $base     one of self::BASE_UPLOADS, self::BASE_CONTENT to indicate if file is an upload or a content (theme or plugin)
368
	 * @param  string $subdir   subdirectory in which file is stored, relative to $base root folder
369
	 * @param  string $filename file name, including extension (but no path)
370
	 * @param  bool   $absolute should the returned URL be absolute (include protocol+host), or relative
371
	 * @return string           the URL
372
	 */
373
	private static function _get_file_url($base, $subdir, $filename, $absolute) {
374
		$url = '';
375
		if( self::BASE_UPLOADS == $base ) {
376
			$upload_dir = wp_upload_dir();
377
			$url = $upload_dir['baseurl'];
378
		}
379
		if( self::BASE_CONTENT == $base ) {
380
			$url = content_url();
381
		}
382
		if(!empty($subdir)) {
383
			$url .= $subdir;
384
		}
385
		$url .= '/'.$filename;
386
		if(!$absolute) {
387
			$url = str_replace(home_url(), '', $url);
388
		}
389
		// $url = TimberURLHelper::remove_double_slashes( $url);
0 ignored issues
show
Unused Code Comprehensibility introduced by
47% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
390
		return $url;
391
	}
392
393
	/**
394
	 * Builds the absolute file system location of a file based on its different components
395
	 *
396
	 * @param  int    $base     one of self::BASE_UPLOADS, self::BASE_CONTENT to indicate if file is an upload or a content (theme or plugin)
397
	 * @param  string $subdir   subdirectory in which file is stored, relative to $base root folder
398
	 * @param  string $filename file name, including extension (but no path)
399
	 * @return string           the file location
400
	 */
401
	private static function _get_file_path($base, $subdir, $filename) {
402
		$path = '';
403
		if(self::BASE_UPLOADS == $base) {
404
			$upload_dir = wp_upload_dir();
405
			$path = $upload_dir['basedir'];
406
		}
407
		if(self::BASE_CONTENT == $base) {
408
			$path = WP_CONTENT_DIR;
409
		}
410
		if(!empty($subdir)) {
411
			$path .= $subdir;
412
		}
413
		$path .= '/'.$filename;
414
		return $path;
415
	}
416
417
418
	/**
419
	 * Main method that applies operation to src image:
420
	 * 1. break down supplied URL into components
421
	 * 2. use components to determine result file and URL
422
	 * 3. check if a result file already exists
423
	 * 4. otherwise, delegate to supplied TimberImageOperation
424
	 *
425
	 * @param  string  $src   an URL (absolute or relative) to an image
426
	 * @param  object  $op    object of class TimberImageOperation
427
	 * @param  boolean $force if true, remove any already existing result file and forces file generation
428
	 * @return string         URL to the new image - or the source one if error
429
	 *
430
	 */
431
	private static function _operate( $src, $op, $force = false ) {
432
		if ( empty( $src ) ) {
433
			return '';
434
		}
435
		$external = false;
436
437
		// if external image, load it first
438
		if ( TimberURLHelper::is_external_content( $src ) ) {
439
			$src = self::sideload_image( $src );
440
			$external = true;
441
		}
442
		// break down URL into components
443
		$au = self::analyze_url($src);
444
		// build URL and filenames
445
		$new_url = self::_get_file_url(
446
			$au['base'],
447
			$au['subdir'],
448
			$op->filename($au['filename'], $au['extension']),
449
			$au['absolute']
450
		);
451
		$new_server_path = self::_get_file_path(
452
			$au['base'],
453
			$au['subdir'],
454
			$op->filename($au['filename'], $au['extension'])
455
		);
456
		$old_server_path = self::_get_file_path(
457
			$au['base'],
458
			$au['subdir'],
459
			$au['basename']
460
		);
461
		// if already exists...
462
		if ( file_exists( $new_server_path ) ) {
463
			if ( $force ) {
464
				// Force operation - warning: will regenerate the image on every pageload, use for testing purposes only!
465
				unlink( $new_server_path );
466
			} else {
467
				// return existing file (caching)
468
				return $new_url;
469
			}
470
		}
471
		// otherwise generate result file
472
		if($op->run($old_server_path, $new_server_path)) {
473
			if( get_class( $op ) === 'TimberImageOperationResize' && $external ) {
474
				$new_url = strtolower( $new_url );
475
			}
476
			return $new_url;
477
		} else {
478
			// in case of error, we return source file itself
479
			return $src;
480
		}
481
	}
482
483
484
// -- the below methods are just used for unit testing the URL generation code
485
//
486 View Code Duplication
	static function get_letterbox_file_url($url, $w, $h, $color) {
487
		$au = self::analyze_url($url);
488
		$op = new TimberImageOperationLetterbox($w, $h, $color);
489
		$new_url = self::_get_file_url(
490
			$au['base'],
491
			$au['subdir'],
492
			$op->filename($au['filename'], $au['extension']),
493
			$au['absolute']
494
		);
495
		return $new_url;
496
	}
497 View Code Duplication
	public static function get_letterbox_file_path($url, $w, $h, $color ) {
498
		$au = self::analyze_url($url);
499
		$op = new TimberImageOperationLetterbox($w, $h, $color);
500
		$new_path = self::_get_file_path(
501
			$au['base'],
502
			$au['subdir'],
503
			$op->filename($au['filename'], $au['extension'])
504
		);
505
		return $new_path;
506
	}
507 View Code Duplication
	static function get_resize_file_url($url, $w, $h, $crop) {
508
		$au = self::analyze_url($url);
509
		$op = new TimberImageOperationResize($w, $h, $crop);
510
		$new_url = self::_get_file_url(
511
			$au['base'],
512
			$au['subdir'],
513
			$op->filename($au['filename'], $au['extension']),
514
			$au['absolute']
515
		);
516
		return $new_url;
517
	}
518 View Code Duplication
	static function get_resize_file_path($url, $w, $h, $crop) {
519
		$au = self::analyze_url($url);
520
		$op = new TimberImageOperationResize($w, $h, $crop);
521
		$new_path = self::_get_file_path(
522
			$au['base'],
523
			$au['subdir'],
524
			$op->filename($au['filename'], $au['extension'])
525
		);
526
		return $new_path;
527
	}
528
529
530
}
531