ImageHelper   F
last analyzed

Complexity

Total Complexity 90

Size/Duplication

Total Lines 656
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 260
c 2
b 1
f 0
dl 0
loc 656
rs 2
wmc 90

29 Methods

Rating   Name   Duplication   Size   Complexity  
A is_svg() 0 24 5
A delete_generated_files() 0 12 2
A resize() 0 11 4
A is_animated_gif() 0 25 5
A _delete_generated_if_image() 0 5 3
A letterbox() 0 3 1
A delete_attachment() 0 2 1
B find_wp_dimensions() 0 13 7
A img_to_jpg() 0 3 1
A generate_attachment_metadata() 0 3 1
A img_to_webp() 0 3 1
A retina_resize() 0 3 1
A init() 0 6 1
A add_relative_upload_dir_key() 0 3 1
A get_sideloaded_file_loc() 0 12 2
B analyze_url() 0 42 9
A theme_url_to_dir() 0 9 2
A is_in_theme_dir() 0 11 3
A process_delete_generated_files() 0 11 6
A sideload_image() 0 24 4
A get_server_location() 0 9 2
A get_letterbox_file_url() 0 10 1
B _operate() 0 60 11
A get_resize_file_url() 0 10 1
A get_resize_file_path() 0 9 1
A _get_file_path() 0 25 6
A maybe_realpath() 0 5 2
A get_letterbox_file_path() 0 9 1
A _get_file_url() 0 19 5

How to fix   Complexity   

Complex Class

Complex classes like ImageHelper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ImageHelper, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Timber;
4
5
use Timber\Image;
6
use Timber\Image\Operation\ToJpg;
7
use Timber\Image\Operation\ToWebp;
8
use Timber\Image\Operation\Resize;
9
use Timber\Image\Operation\Retina;
10
use Timber\Image\Operation\Letterbox;
11
12
use Timber\URLHelper;
13
use Timber\PathHelper;
14
15
/**
16
 * Implements the Twig image filters:
17
 * https://timber.github.io/docs/guides/cookbook-images/#arbitrary-resizing-of-images
18
 * - resize
19
 * - retina
20
 * - letterbox
21
 * - tojpg
22
 *
23
 * Implementation:
24
 * - public static functions provide the methods that are called by the filter
25
 * - most of the work is common to all filters (URL analysis, directory gymnastics, file caching, error management) and done by private static functions
26
 * - the specific part (actual image processing) is delegated to dedicated subclasses of TimberImageOperation
27
 */
28
class ImageHelper {
29
30
	const BASE_UPLOADS = 1;
31
	const BASE_CONTENT = 2;
32
33
	static $home_url;
34
35
	public static function init() {
36
		self::$home_url = get_home_url();
37
		add_action('delete_attachment', array(__CLASS__, 'delete_attachment'));
38
		add_filter('wp_generate_attachment_metadata', array(__CLASS__, 'generate_attachment_metadata'), 10, 2);
39
		add_filter('upload_dir', array(__CLASS__, 'add_relative_upload_dir_key'), 10, 2);
40
		return true;
41
	}
42
43
	/**
44
	 * Generates a new image with the specified dimensions.
45
	 * New dimensions are achieved by cropping to maintain ratio.
46
	 *
47
	 * @api
48
	 * @param string  		$src an URL (absolute or relative) to the original image
49
	 * @param int|string	$w target width(int) or WordPress image size (WP-set or user-defined).
50
	 * @param int     		$h target height (ignored if $w is WP image size). If not set, will ignore and resize based on $w only.
51
	 * @param string  		$crop your choices are 'default', 'center', 'top', 'bottom', 'left', 'right'
52
	 * @param bool    		$force
53
	 * @example
54
	 * ```twig
55
	 * <img src="{{ image.src | resize(300, 200, 'top') }}" />
56
	 * ```
57
	 * ```html
58
	 * <img src="http://example.org/wp-content/uploads/pic-300x200-c-top.jpg" />
59
	 * ```
60
	 * @return string (ex: )
61
	 */
62
	public static function resize( $src, $w, $h = 0, $crop = 'default', $force = false ) {
63
		if ( !is_numeric($w) && is_string($w) ) {
64
			if ( $sizes = self::find_wp_dimensions($w) ) {
65
				$w = $sizes['w'];
66
				$h = $sizes['h'];
67
			} else {
68
				return $src;
69
			}
70
		}
71
		$op = new Image\Operation\Resize($w, $h, $crop);
72
		return self::_operate($src, $op, $force);
73
	}
74
75
	/**
76
	 * Find the sizes of an image based on a defined image size
77
	 * @param  string $size the image size to search for
78
	 *                      can be WordPress-defined ("medium")
79
	 *                      or user-defined ("my-awesome-size")
80
	 * @return false|array {
81
	 *     @type int w
82
	 *     @type int h
83
	 * }
84
	 */
85
	private static function find_wp_dimensions( $size ) {
86
		global $_wp_additional_image_sizes;
87
		if ( isset($_wp_additional_image_sizes[$size]) ) {
88
			$w = $_wp_additional_image_sizes[$size]['width'];
89
			$h = $_wp_additional_image_sizes[$size]['height'];
90
		} else if ( in_array($size, array('thumbnail', 'medium', 'large')) ) {
91
			$w = get_option($size.'_size_w');
92
			$h = get_option($size.'_size_h');
93
		}
94
		if ( isset($w) && isset($h) && ($w || $h) ) {
95
			return array('w' => $w, 'h' => $h);
96
		}
97
		return false;
98
	}
99
100
	/**
101
	 * Generates a new image with increased size, for display on Retina screens.
102
	 *
103
	 * @param string  $src
104
	 * @param float   $multiplier
105
	 * @param boolean $force
106
	 *
107
	 * @return string url to the new image
108
	 */
109
	public static function retina_resize( $src, $multiplier = 2, $force = false ) {
110
		$op = new Image\Operation\Retina($multiplier);
111
		return self::_operate($src, $op, $force);
112
	}
113
114
	/**
115
	 * checks to see if the given file is an aimated gif
116
	 * @param  string  $file local filepath to a file, not a URL
117
	 * @return boolean true if it's an animated gif, false if not
118
	 */
119
	public static function is_animated_gif( $file ) {
120
		if ( strpos(strtolower($file), '.gif') === false ) {
121
			//doesn't have .gif, bail
122
			return false;
123
		}
124
		//its a gif so test
125
		if ( !($fh = @fopen($file, 'rb')) ) {
126
		  	return false;
127
		}
128
		$count = 0;
129
		//an animated gif contains multiple "frames", with each frame having a
130
		//header made up of:
131
		// * a static 4-byte sequence (\x00\x21\xF9\x04)
132
		// * 4 variable bytes
133
		// * a static 2-byte sequence (\x00\x2C)
134
135
		// We read through the file til we reach the end of the file, or we've found
136
		// at least 2 frame headers
137
		while ( !feof($fh) && $count < 2 ) {
138
			$chunk = fread($fh, 1024 * 100); //read 100kb at a time
139
			$count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', $chunk, $matches);
140
		}
141
142
		fclose($fh);
143
		return $count > 1;
144
	}
145
146
	/**
147
	 * Checks if file is an SVG.
148
	 *
149
	 * @param string $file_path File path to check.
150
	 * @return bool True if SVG, false if not SVG or file doesn't exist.
151
	 */
152
	public static function is_svg( $file_path ) {
153
		if ( ! isset( $file_path ) || '' === $file_path || ! file_exists( $file_path ) ) {
154
			return false;
155
		}
156
157
		if ( TextHelper::ends_with( strtolower($file_path), '.svg' ) ) {
158
			return true;
159
		}
160
161
		/**
162
		 * Try reading mime type.
163
		 *
164
		 * SVG images are not allowed by default in WordPress, so we have to pass a default mime
165
		 * type for SVG images.
166
		 */
167
		$mime = wp_check_filetype_and_ext( $file_path, PathHelper::basename( $file_path ), array(
168
			'svg' => 'image/svg+xml',
169
		) );
170
171
		return in_array( $mime['type'], array(
172
			'image/svg+xml',
173
			'text/html',
174
			'text/plain',
175
			'image/svg',
176
		) );
177
	}
178
179
	/**
180
	 * Generate a new image with the specified dimensions.
181
	 * New dimensions are achieved by adding colored bands to maintain ratio.
182
	 *
183
	 * @param string  $src
184
	 * @param int     $w
185
	 * @param int     $h
186
	 * @param string  $color
187
	 * @param bool    $force
188
	 * @return string
189
	 */
190
	public static function letterbox( $src, $w, $h, $color = false, $force = false ) {
191
		$op = new Letterbox($w, $h, $color);
0 ignored issues
show
Bug introduced by
It seems like $color can also be of type false; however, parameter $color of Timber\Image\Operation\Letterbox::__construct() does only seem to accept string, 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

191
		$op = new Letterbox($w, $h, /** @scrutinizer ignore-type */ $color);
Loading history...
192
		return self::_operate($src, $op, $force);
193
	}
194
195
	/**
196
	 * Generates a new image by converting the source GIF or PNG into JPG
197
	 *
198
	 * @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)
199
	 * @param string  $bghex
200
	 * @return string
201
	 */
202
	public static function img_to_jpg( $src, $bghex = '#FFFFFF', $force = false ) {
203
		$op = new Image\Operation\ToJpg($bghex);
204
		return self::_operate($src, $op, $force);
205
	}
206
207
    /**
208
     * Generates a new image by converting the source into WEBP if supported by the server
209
     *
210
     * @param string  $src      a url or path to the image (http://example.org/wp-content/uploads/2014/image.webp)
211
     *							or (/wp-content/uploads/2014/image.jpg)
212
     *							If webp is not supported, a jpeg image will be generated
213
	 * @param int     $quality  ranges from 0 (worst quality, smaller file) to 100 (best quality, biggest file)
214
     * @param bool    $force
215
     */
216
    public static function img_to_webp( $src, $quality = 80, $force = false ) {
217
        $op = new Image\Operation\ToWebp($quality);
218
        return self::_operate($src, $op, $force);
219
    }
220
221
	//-- end of public methods --//
222
223
	/**
224
	 * Deletes all resized versions of an image when the source is deleted.
225
	 *
226
	 * @since 1.5.0
227
	 * @param int   $post_id an attachment post id
228
	 */
229
	public static function delete_attachment( $post_id ) {
230
		self::_delete_generated_if_image($post_id);
231
	}
232
233
234
	/**
235
	 * Delete all resized version of an image when its meta data is regenerated.
236
	 *
237
	 * @since 1.5.0
238
	 * @param array $metadata
239
	 * @param int   $post_id an attachment post id
240
	 * @return array
241
	 */
242
	public static function generate_attachment_metadata( $metadata, $post_id ) {
243
		self::_delete_generated_if_image($post_id);
244
		return $metadata;
245
	}
246
247
	/**
248
	 * Adds a 'relative' key to wp_upload_dir() result.
249
	 * It will contain the relative url to upload dir.
250
	 *
251
	 * @since 1.5.0
252
	 * @param array $arr
253
	 * @return array
254
	 */
255
	public static function add_relative_upload_dir_key( $arr ) {
256
		$arr['relative'] = str_replace(self::$home_url, '', $arr['baseurl']);
257
		return $arr;
258
	}
259
260
	/**
261
	 * Checks if attachment is an image before deleting generated files
262
	 *
263
	 * @param  int  $post_id   an attachment post id
264
	 *
265
	 */
266
	public static function _delete_generated_if_image( $post_id ) {
267
		if ( wp_attachment_is_image($post_id) ) {
268
			$attachment = new Image($post_id);
269
			if ( $attachment->file_loc ) {
270
				ImageHelper::delete_generated_files($attachment->file_loc);
271
			}
272
		}
273
	}
274
275
276
	/**
277
	 * Deletes the auto-generated files for resize and letterboxing created by Timber
278
	 * @param string  $local_file   ex: /var/www/wp-content/uploads/2015/my-pic.jpg
279
	 *	                            or: http://example.org/wp-content/uploads/2015/my-pic.jpg
280
	 */
281
	static function delete_generated_files( $local_file ) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
282
		if ( URLHelper::is_absolute($local_file) ) {
283
			$local_file = URLHelper::url_to_file_system($local_file);
284
		}
285
		$info = PathHelper::pathinfo($local_file);
286
		$dir = $info['dirname'];
287
		$ext = $info['extension'];
288
		$filename = $info['filename'];
289
		self::process_delete_generated_files($filename, $ext, $dir, '-[0-9999999]*', '-[0-9]*x[0-9]*-c-[a-z]*.');
290
		self::process_delete_generated_files($filename, $ext, $dir, '-lbox-[0-9999999]*', '-lbox-[0-9]*x[0-9]*-[a-zA-Z0-9]*.');
291
		self::process_delete_generated_files($filename, 'jpg', $dir, '-tojpg.*');
292
		self::process_delete_generated_files($filename, 'jpg', $dir, '-tojpg-[0-9999999]*');
293
	}
294
295
	/**
296
	 * Deletes resized versions of the supplied file name.
297
	 * 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.
298
	 *
299
	 * keeping these here so I know what the hell we're matching
300
	 * $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);
301
	 * $match = preg_match("/\/srv\/www\/wordpress-develop\/src\/wp-content\/uploads\/2014\/05\/arch-[0-9]*x[0-9]*-c-[a-z]*.jpg/", $filename);
302
	 *
303
	 * @param string 	$filename   ex: my-pic
304
	 * @param string 	$ext ex: jpg
305
	 * @param string 	$dir var/www/wp-content/uploads/2015/
306
	 * @param string 	$search_pattern pattern of files to pluck from
307
	 * @param string 	$match_pattern pattern of files to go forth and delete
308
	 */
309
	protected static function process_delete_generated_files( $filename, $ext, $dir, $search_pattern, $match_pattern = null ) {
310
		$searcher = '/'.$filename.$search_pattern;
311
		$files = glob($dir.$searcher);
312
		if ( $files === false || empty($files) ) {
313
			return;
314
		}
315
		foreach ( $files as $found_file ) {
316
			$pattern = '/'.preg_quote($dir, '/').'\/'.preg_quote($filename, '/').$match_pattern.preg_quote($ext, '/').'/';
317
			$match = preg_match($pattern, $found_file);
318
			if ( !$match_pattern || $match ) {
319
				unlink($found_file);
320
			}
321
		}
322
	}
323
324
	/**
325
	 * Determines the filepath corresponding to a given URL
326
	 *
327
	 * @param string  $url
328
	 * @return string
329
	 */
330
	public static function get_server_location( $url ) {
331
		// if we're already an absolute dir, just return.
332
		if ( 0 === strpos($url, ABSPATH) ) {
333
			return $url;
334
		}
335
		// otherwise, analyze URL then build mapping path
336
		$au = self::analyze_url($url);
337
		$result = self::_get_file_path($au['base'], $au['subdir'], $au['basename']);
338
		return $result;
339
	}
340
341
	/**
342
	 * Determines the filepath where a given external file will be stored.
343
	 *
344
	 * @param string  $file
345
	 * @return string
346
	 */
347
	public static function get_sideloaded_file_loc( $file ) {
348
		$upload = wp_upload_dir();
349
		$dir = $upload['path'];
350
		$filename = $file;
351
		$file = parse_url($file);
352
		$path_parts = PathHelper::pathinfo($file['path']);
353
		$basename = md5($filename);
354
		$ext = 'jpg';
355
		if ( isset($path_parts['extension']) ) {
356
			$ext = $path_parts['extension'];
357
		}
358
		return $dir.'/'.$basename.'.'.$ext;
359
	}
360
361
	/**
362
	 * downloads an external image to the server and stores it on the server
363
	 *
364
	 * @param string  $file the URL to the original file
365
	 * @return string the URL to the downloaded file
366
	 */
367
	public static function sideload_image( $file ) {
368
		$loc = self::get_sideloaded_file_loc($file);
369
		if ( file_exists($loc) ) {
370
			return URLHelper::file_system_to_url($loc);
371
		}
372
		// Download file to temp location
373
		if ( !function_exists('download_url') ) {
374
			require_once ABSPATH.'/wp-admin/includes/file.php';
375
		}
376
		$tmp = download_url($file);
377
		preg_match('/[^\?]+\.(jpe?g|jpe|gif|png)\b/i', $file, $matches);
378
		$file_array = array();
379
		$file_array['name'] = PathHelper::basename($matches[0]);
380
		$file_array['tmp_name'] = $tmp;
381
		// If error storing temporarily, do not use
382
		if ( is_wp_error($tmp) ) {
383
			$file_array['tmp_name'] = '';
384
		}
385
		// do the validation and storage stuff
386
		$locinfo = PathHelper::pathinfo($loc);
387
		$file = wp_upload_bits($locinfo['basename'], null, file_get_contents($file_array['tmp_name']));
388
		// delete tmp file
389
		@unlink($file_array['tmp_name']);
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

389
		/** @scrutinizer ignore-unhandled */ @unlink($file_array['tmp_name']);

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...
390
		return $file['url'];
391
	}
392
393
	/**
394
	 * Takes in an URL and breaks it into components,
395
	 * that will then be used in the different steps of image processing.
396
	 * The image is expected to be either part of a theme, plugin, or an upload.
397
	 *
398
	 * @param  string $url an URL (absolute or relative) pointing to an image
399
	 * @return array       an array (see keys in code below)
400
	 */
401
	public static function analyze_url( $url ) {
402
		$result = array(
403
			'url' => $url, // the initial url
404
			'absolute' => URLHelper::is_absolute($url), // is the url absolute or relative (to home_url)
405
			'base' => 0, // is the image in uploads dir, or in content dir (theme or plugin)
406
			'subdir' => '', // the path between base (uploads or content) and file
407
			'filename' => '', // the filename, without extension
408
			'extension' => '', // the file extension
409
			'basename' => '', // full file name
410
		);
411
		$upload_dir = wp_upload_dir();
412
		$tmp = $url;
413
		if ( TextHelper::starts_with($tmp, ABSPATH) || TextHelper::starts_with($tmp, '/srv/www/') ) {
414
			// we've been given a dir, not an url
415
			$result['absolute'] = true;
416
			if ( TextHelper::starts_with($tmp, $upload_dir['basedir']) ) {
417
				$result['base'] = self::BASE_UPLOADS; // upload based
418
				$tmp = URLHelper::remove_url_component($tmp, $upload_dir['basedir']);
419
			}
420
			if ( TextHelper::starts_with($tmp, WP_CONTENT_DIR) ) {
421
				$result['base'] = self::BASE_CONTENT; // content based
422
				$tmp = URLHelper::remove_url_component($tmp, WP_CONTENT_DIR);
423
			}
424
		} else {
425
			if ( !$result['absolute'] ) {
426
				$tmp = untrailingslashit(network_home_url()).$tmp;
427
			}
428
			if ( URLHelper::starts_with($tmp, $upload_dir['baseurl']) ) {
429
				$result['base'] = self::BASE_UPLOADS; // upload based
430
				$tmp = URLHelper::remove_url_component($tmp, $upload_dir['baseurl']);
431
			} else if ( URLHelper::starts_with($tmp, content_url()) ) {
432
				$result['base'] = self::BASE_CONTENT; // content-based
433
				$tmp = self::theme_url_to_dir($tmp);
434
				$tmp = URLHelper::remove_url_component($tmp, WP_CONTENT_DIR);
435
			}
436
		}
437
		$parts = PathHelper::pathinfo($tmp);
438
		$result['subdir'] = ($parts['dirname'] === '/') ? '' : $parts['dirname'];
439
		$result['filename'] = $parts['filename'];
440
		$result['extension'] = strtolower($parts['extension']);
441
		$result['basename'] = $parts['basename'];
442
		return $result;
443
	}
444
445
	/**
446
	 * Converts a URL located in a theme directory into the raw file path
447
	 * @param string 	$src a URL (http://example.org/wp-content/themes/twentysixteen/images/home.jpg)
448
	 * @return string full path to the file in question
449
	 */
450
	static function theme_url_to_dir( $src ) 	{
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
451
		$site_root = trailingslashit(get_theme_root_uri()).get_stylesheet();
452
		$tmp = str_replace($site_root, '', $src);
453
		//$tmp = trailingslashit(get_theme_root()).get_stylesheet().$tmp;
454
		$tmp = get_stylesheet_directory().$tmp;
455
		if ( realpath($tmp) ) {
456
			return realpath($tmp);
457
		}
458
		return $tmp;
459
	}
460
461
	/**
462
	 * Checks if uploaded image is located in theme.
463
	 *
464
	 * @param string $path image path.
465
	 * @return bool     If the image is located in the theme directory it returns true.
466
	 *                  If not or $path doesn't exits it returns false.
467
	 */
468
	protected static function is_in_theme_dir( $path ) {
469
		$root = realpath(get_stylesheet_directory());
470
471
		if ( false === $root ) {
472
			return false;
473
		}
474
475
		if ( 0 === strpos($path, (string) $root) ) {
476
			return true;
477
		} else {
478
			return false;
479
		}
480
	}
481
482
	/**
483
	 * Builds the public URL of a file based on its different components
484
	 *
485
	 * @param  int    $base     one of self::BASE_UPLOADS, self::BASE_CONTENT to indicate if file is an upload or a content (theme or plugin)
486
	 * @param  string $subdir   subdirectory in which file is stored, relative to $base root folder
487
	 * @param  string $filename file name, including extension (but no path)
488
	 * @param  bool   $absolute should the returned URL be absolute (include protocol+host), or relative
489
	 * @return string           the URL
490
	 */
491
	private static function _get_file_url( $base, $subdir, $filename, $absolute ) {
492
		$url = '';
493
		if ( self::BASE_UPLOADS == $base ) {
494
			$upload_dir = wp_upload_dir();
495
			$url = $upload_dir['baseurl'];
496
		}
497
		if ( self::BASE_CONTENT == $base ) {
498
			$url = content_url();
499
		}
500
		if ( !empty($subdir) ) {
501
			$url .= $subdir;
502
		}
503
		$url = untrailingslashit($url).'/'.$filename;
504
		if ( !$absolute ) {
505
			$home = home_url();
506
			$home = apply_filters('timber/ImageHelper/_get_file_url/home_url', $home);
507
			$url = str_replace($home, '', $url);
508
		}
509
		return $url;
510
	}
511
512
	/**
513
	 * Runs realpath to resolve symbolic links (../, etc). But only if it's a path and not a URL
514
	 * @param  string $path
515
	 * @return string 			the resolved path
516
	 */
517
	protected static function maybe_realpath( $path ) {
518
		if ( strstr($path, '../') !== false ) {
519
			return realpath($path);
520
		}
521
		return $path;
522
	}
523
524
525
	/**
526
	 * Builds the absolute file system location of a file based on its different components
527
	 *
528
	 * @param  int    $base     one of self::BASE_UPLOADS, self::BASE_CONTENT to indicate if file is an upload or a content (theme or plugin)
529
	 * @param  string $subdir   subdirectory in which file is stored, relative to $base root folder
530
	 * @param  string $filename file name, including extension (but no path)
531
	 * @return string           the file location
532
	 */
533
	private static function _get_file_path( $base, $subdir, $filename ) {
534
		if ( URLHelper::is_url($subdir) ) {
535
			$subdir = URLHelper::url_to_file_system($subdir);
536
		}
537
		$subdir = self::maybe_realpath($subdir);
538
539
		$path = '';
540
		if ( self::BASE_UPLOADS == $base ) {
541
			//it is in the Uploads directory
542
			$upload_dir = wp_upload_dir();
543
			$path = $upload_dir['basedir'];
544
		} else if ( self::BASE_CONTENT == $base ) {
545
			//it is in the content directory, somewhere else ...
546
			$path = WP_CONTENT_DIR;
547
		}
548
		if ( self::is_in_theme_dir(trailingslashit($subdir).$filename) ) {
549
			//this is for weird installs when the theme folder is outside of /wp-content
550
			return trailingslashit($subdir).$filename;
551
		}
552
		if ( !empty($subdir) ) {
553
			$path = trailingslashit($path).$subdir;
554
		}
555
		$path = trailingslashit($path).$filename;
556
557
		return URLHelper::remove_double_slashes($path);
558
	}
559
560
561
	/**
562
	 * Main method that applies operation to src image:
563
	 * 1. break down supplied URL into components
564
	 * 2. use components to determine result file and URL
565
	 * 3. check if a result file already exists
566
	 * 4. otherwise, delegate to supplied TimberImageOperation
567
	 *
568
	 * @param  string  $src   an URL (absolute or relative) to an image
569
	 * @param  object  $op    object of class TimberImageOperation
570
	 * @param  boolean $force if true, remove any already existing result file and forces file generation
571
	 * @return string         URL to the new image - or the source one if error
572
	 *
573
	 */
574
	private static function _operate( $src, $op, $force = false ) {
575
		if ( empty($src) ) {
576
			return '';
577
		}
578
579
		$allow_fs_write = apply_filters('timber/allow_fs_write', true);
580
581
		if ( $allow_fs_write === false ) {
582
			return $src;
583
		}
584
		
585
		$external = false;
586
		// if external image, load it first
587
		if ( URLHelper::is_external_content($src) ) {
588
			$src = self::sideload_image($src);
589
			$external = true;
590
		}
591
592
		// break down URL into components
593
		$au = self::analyze_url($src);
594
595
		// build URL and filenames
596
		$new_url = self::_get_file_url(
597
			$au['base'],
598
			$au['subdir'],
599
			$op->filename($au['filename'], $au['extension']),
600
			$au['absolute']
601
		);
602
		$destination_path = self::_get_file_path(
603
			$au['base'],
604
			$au['subdir'],
605
			$op->filename($au['filename'], $au['extension'])
606
		);
607
		$source_path = self::_get_file_path(
608
			$au['base'],
609
			$au['subdir'],
610
			$au['basename']
611
		);
612
613
		$new_url = apply_filters('timber/image/new_url', $new_url);
614
		$destination_path = apply_filters('timber/image/new_path', $destination_path);
615
		// if already exists...
616
		if ( file_exists($source_path) && file_exists($destination_path) ) {
617
			if ( $force || filemtime($source_path) > filemtime($destination_path) ) {
618
				// Force operation - warning: will regenerate the image on every pageload, use for testing purposes only!
619
				unlink($destination_path);
620
			} else {
621
				// return existing file (caching)
622
				return $new_url;
623
			}
624
		}
625
		// otherwise generate result file
626
		if ( $op->run($source_path, $destination_path) ) {
627
			if ( get_class($op) === 'Timber\Image\Operation\Resize' && $external ) {
628
				$new_url = strtolower($new_url);
629
			}
630
			return $new_url;
631
		} else {
632
			// in case of error, we return source file itself
633
			return $src;
634
		}
635
	}
636
637
638
// -- the below methods are just used for unit testing the URL generation code
639
//
640
	public static function get_letterbox_file_url( $url, $w, $h, $color ) {
641
		$au = self::analyze_url($url);
642
		$op = new Image\Operation\Letterbox($w, $h, $color);
643
		$new_url = self::_get_file_url(
644
			$au['base'],
645
			$au['subdir'],
646
			$op->filename($au['filename'], $au['extension']),
647
			$au['absolute']
648
		);
649
		return $new_url;
650
	}
651
652
	public static function get_letterbox_file_path( $url, $w, $h, $color ) {
653
		$au = self::analyze_url($url);
654
		$op = new Image\Operation\Letterbox($w, $h, $color);
655
		$new_path = self::_get_file_path(
656
			$au['base'],
657
			$au['subdir'],
658
			$op->filename($au['filename'], $au['extension'])
659
		);
660
		return $new_path;
661
	}
662
663
	public static function get_resize_file_url( $url, $w, $h, $crop ) {
664
		$au = self::analyze_url($url);
665
		$op = new Image\Operation\Resize($w, $h, $crop);
666
		$new_url = self::_get_file_url(
667
			$au['base'],
668
			$au['subdir'],
669
			$op->filename($au['filename'], $au['extension']),
670
			$au['absolute']
671
		);
672
		return $new_url;
673
	}
674
675
	public static function get_resize_file_path( $url, $w, $h, $crop ) {
676
		$au = self::analyze_url($url);
677
		$op = new Image\Operation\Resize($w, $h, $crop);
678
		$new_path = self::_get_file_path(
679
			$au['base'],
680
			$au['subdir'],
681
			$op->filename($au['filename'], $au['extension'])
682
		);
683
		return $new_path;
684
	}
685
}
686