• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

timber / timber / 5690057835

pending completion
5690057835

push

github

nlemoine
Merge branch '2.x' of github.com:timber/timber into 2.x-refactor-file-models

# Conflicts:
#	src/Attachment.php
#	src/ExternalImage.php
#	src/FileSize.php
#	src/URLHelper.php

1134 of 1134 new or added lines in 55 files covered. (100.0%)

3923 of 4430 relevant lines covered (88.56%)

59.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.48
/src/ImageHelper.php
1
<?php
2

3
namespace Timber;
4

5
use Timber\Image\Operation;
6

7
/**
8
 * Class ImageHelper
9
 *
10
 * Implements the Twig image filters:
11
 * https://timber.github.io/docs/guides/cookbook-images/#arbitrary-resizing-of-images
12
 * - resize
13
 * - retina
14
 * - letterbox
15
 * - tojpg
16
 *
17
 * Implementation:
18
 * - public static functions provide the methods that are called by the filter
19
 * - most of the work is common to all filters (URL analysis, directory gymnastics, file caching, error management) and done by private static functions
20
 * - the specific part (actual image processing) is delegated to dedicated subclasses of TimberImageOperation
21
 *
22
 * @api
23
 */
24
class ImageHelper
25
{
26
    public const BASE_UPLOADS = 1;
27

28
    public const BASE_CONTENT = 2;
29

30
    public static $home_url;
31

32
    /**
33
     * Inits the object.
34
     */
35
    public static function init()
36
    {
37
        self::$home_url = \get_home_url();
1✔
38
        \add_action('delete_attachment', [__CLASS__, 'delete_attachment']);
1✔
39
        \add_filter('wp_generate_attachment_metadata', [__CLASS__, 'generate_attachment_metadata'], 10, 2);
1✔
40
        \add_filter('upload_dir', [__CLASS__, 'add_relative_upload_dir_key']);
1✔
41
        return true;
1✔
42
    }
43

44
    /**
45
     * Generates a new image with the specified dimensions.
46
     *
47
     * New dimensions are achieved by cropping to maintain ratio.
48
     *
49
     * @api
50
     * @example
51
     * ```twig
52
     * <img src="{{ image.src | resize(300, 200, 'top') }}" />
53
     * ```
54
     * ```html
55
     * <img src="http://example.org/wp-content/uploads/pic-300x200-c-top.jpg" />
56
     * ```
57
     *
58
     * @param string     $src   A URL (absolute or relative) to the original image.
59
     * @param int|string $w     Target width (int) or WordPress image size (WP-set or
60
     *                          user-defined).
61
     * @param int        $h     Optional. Target height (ignored if `$w` is WP image size). If not
62
     *                          set, will ignore and resize based on `$w` only. Default `0`.
63
     * @param string     $crop  Optional. Your choices are `default`, `center`, `top`, `bottom`,
64
     *                          `left`, `right`. Default `default`.
65
     * @param bool       $force Optional. Whether to remove any already existing result file and
66
     *                          force file generation. Default `false`.
67
     * @return string The URL of the resized image.
68
     */
69
    public static function resize($src, $w, $h = 0, $crop = 'default', $force = false)
70
    {
71
        if (!\is_numeric($w) && \is_string($w)) {
53✔
72
            if ($sizes = self::find_wp_dimensions($w)) {
4✔
73
                $w = $sizes['w'];
3✔
74
                $h = $sizes['h'];
3✔
75
            } else {
76
                return $src;
1✔
77
            }
78
        }
79
        $op = new Operation\Resize($w, $h, $crop);
52✔
80
        return self::_operate($src, $op, $force);
52✔
81
    }
82

83
    /**
84
     * Finds the sizes of an image based on a defined image size.
85
     *
86
     * @internal
87
     * @param  string $size The image size to search for can be WordPress-defined ('medium') or
88
     *                      user-defined ('my-awesome-size').
89
     * @return false|array An array with `w` and `h` height key, corresponding to the width and the
90
     *                     height of the image.
91
     */
92
    private static function find_wp_dimensions($size)
93
    {
94
        global $_wp_additional_image_sizes;
95
        if (isset($_wp_additional_image_sizes[$size])) {
4✔
96
            $w = $_wp_additional_image_sizes[$size]['width'];
2✔
97
            $h = $_wp_additional_image_sizes[$size]['height'];
2✔
98
        } elseif (\in_array($size, ['thumbnail', 'medium', 'large'])) {
2✔
99
            $w = \get_option($size . '_size_w');
1✔
100
            $h = \get_option($size . '_size_h');
1✔
101
        }
102
        if (isset($w) && isset($h) && ($w || $h)) {
4✔
103
            return [
3✔
104
                'w' => $w,
3✔
105
                'h' => $h,
3✔
106
            ];
3✔
107
        }
108
        return false;
1✔
109
    }
110

111
    /**
112
     * Generates a new image with increased size, for display on Retina screens.
113
     *
114
     * @api
115
     *
116
     * @param string  $src        URL of the file to read from.
117
     * @param float   $multiplier Optional. Factor the original dimensions should be multiplied
118
     *                            with. Default `2`.
119
     * @param boolean $force      Optional. Whether to remove any already existing result file and
120
     *                            force file generation. Default `false`.
121
     * @return string URL to the new image.
122
     */
123
    public static function retina_resize($src, $multiplier = 2, $force = false)
124
    {
125
        $op = new Operation\Retina($multiplier);
7✔
126
        return self::_operate($src, $op, $force);
7✔
127
    }
128

129
    /**
130
     * Checks to see if the given file is an animated GIF.
131
     *
132
     * @api
133
     *
134
     * @param string $file Local filepath to a file, not a URL.
135
     * @return boolean True if it’s an animated GIF, false if not.
136
     */
137
    public static function is_animated_gif($file)
138
    {
139
        if (\strpos(\strtolower($file), '.gif') === false) {
43✔
140
            //doesn't have .gif, bail
141
            return false;
37✔
142
        }
143
        // Its a gif so test
144
        if (!($fh = @\fopen($file, 'rb'))) {
6✔
145
            return false;
1✔
146
        }
147
        $count = 0;
5✔
148
        // An animated gif contains multiple "frames", with each frame having a
149
        // header made up of:
150
        // * a static 4-byte sequence (\x00\x21\xF9\x04).
151
        // * 4 variable bytes.
152
        // * a static 2-byte sequence (\x00\x2C).
153
        // We read through the file til we reach the end of the file, or we've found.
154
        // at least 2 frame headers.
155
        while (!\feof($fh) && $count < 2) {
5✔
156
            $chunk = \fread($fh, 1024 * 100); //read 100kb at a time
5✔
157
            $count += \preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', $chunk, $matches);
5✔
158
        }
159

160
        \fclose($fh);
5✔
161
        return $count > 1;
5✔
162
    }
163

164
    /**
165
     * Checks if file is an SVG.
166
     *
167
     * @param string $file_path File path to check.
168
     * @return bool True if SVG, false if not SVG or file doesn't exist.
169
     */
170
    public static function is_svg($file_path)
171
    {
172
        if ('' === $file_path || !\file_exists($file_path)) {
75✔
173
            return false;
2✔
174
        }
175

176
        if (\str_ends_with(\strtolower($file_path), '.svg')) {
73✔
177
            return true;
8✔
178
        }
179

180
        /**
181
         * Try reading mime type.
182
         *
183
         * SVG images are not allowed by default in WordPress, so we have to pass a default mime
184
         * type for SVG images.
185
         */
186
        $mime = \wp_check_filetype_and_ext($file_path, PathHelper::basename($file_path), [
65✔
187
            'svg' => 'image/svg+xml',
65✔
188
        ]);
65✔
189

190
        return \in_array($mime['type'], [
65✔
191
            'image/svg+xml',
65✔
192
            'text/html',
65✔
193
            'text/plain',
65✔
194
            'image/svg',
65✔
195
        ]);
65✔
196
    }
197

198
    /**
199
     * Generate a new image with the specified dimensions.
200
     *
201
     * New dimensions are achieved by adding colored bands to maintain ratio.
202
     *
203
     * @api
204
     *
205
     * @param string      $src
206
     * @param int         $w
207
     * @param int         $h
208
     * @param string|bool $color
209
     * @param bool        $force
210
     * @return string
211
     */
212
    public static function letterbox($src, $w, $h, $color = false, $force = false)
213
    {
214
        $op = new Operation\Letterbox($w, $h, $color);
14✔
215
        return self::_operate($src, $op, $force);
14✔
216
    }
217

218
    /**
219
     * Generates a new image by converting the source GIF or PNG into JPG.
220
     *
221
     * @api
222
     *
223
     * @param string $src   A URL or path to the image
224
     *                      (http://example.org/wp-content/uploads/2014/image.jpg) or
225
     *                      (/wp-content/uploads/2014/image.jpg).
226
     * @param string $bghex The hex color to use for transparent zones.
227
     * @return string The URL of the processed image.
228
     */
229
    public static function img_to_jpg($src, $bghex = '#FFFFFF', $force = false)
230
    {
231
        $op = new Operation\ToJpg($bghex);
9✔
232
        return self::_operate($src, $op, $force);
9✔
233
    }
234

235
    /**
236
     * Generates a new image by converting the source into WEBP if supported by the server.
237
     *
238
     * @param string $src     A URL or path to the image
239
     *                        (http://example.org/wp-content/uploads/2014/image.webp) or
240
     *                        (/wp-content/uploads/2014/image.webp).
241
     * @param int    $quality Range from `0` (worst quality, smaller file) to `100` (best quality,
242
     *                        biggest file).
243
     * @param bool   $force   Optional. Whether to remove any already existing result file and
244
     *                        force file generation. Default `false`.
245
     * @return string The URL of the processed image. If webp is not supported, a jpeg image will be
246
     *                        generated.
247
     */
248
    public static function img_to_webp($src, $quality = 80, $force = false)
249
    {
250
        $op = new Operation\ToWebp($quality);
8✔
251
        return self::_operate($src, $op, $force);
8✔
252
    }
253

254
    //-- end of public methods --//
255

256
    /**
257
     * Deletes all resized versions of an image when the source is deleted.
258
     *
259
     * @since 1.5.0
260
     * @param int   $post_id An attachment ID.
261
     */
262
    public static function delete_attachment($post_id)
263
    {
264
        self::_delete_generated_if_image($post_id);
2✔
265
    }
266

267
    /**
268
     * Delete all resized version of an image when its meta data is regenerated.
269
     *
270
     * @since 1.5.0
271
     * @param array $metadata Existing metadata.
272
     * @param int   $post_id  An attachment ID.
273
     * @return array
274
     */
275
    public static function generate_attachment_metadata($metadata, $post_id)
276
    {
277
        self::_delete_generated_if_image($post_id);
59✔
278
        return $metadata;
59✔
279
    }
280

281
    /**
282
     * Adds a 'relative' key to wp_upload_dir() result.
283
     *
284
     * It will contain the relative url to upload dir.
285
     *
286
     * @since 1.5.0
287
     * @param array $arr
288
     * @return array
289
     */
290
    public static function add_relative_upload_dir_key($arr)
291
    {
292
        $arr['relative'] = \str_replace(self::$home_url, '', $arr['baseurl']);
192✔
293
        return $arr;
192✔
294
    }
295

296
    /**
297
     * Checks if attachment is an image before deleting generated files.
298
     *
299
     * @param int $post_id An attachment ID.
300
     */
301
    public static function _delete_generated_if_image($post_id)
302
    {
303
        if (\wp_attachment_is_image($post_id)) {
60✔
304
            $attachment = Timber::get_post($post_id);
51✔
305
            /** @var \Timber\Attachment $attachment */
306
            if ($attachment->file_loc) {
51✔
307
                ImageHelper::delete_generated_files($attachment->file_loc);
51✔
308
            }
309
        }
310
    }
311

312
    /**
313
     * Deletes the auto-generated files for resize and letterboxing created by Timber.
314
     *
315
     * @param string $local_file ex: /var/www/wp-content/uploads/2015/my-pic.jpg
316
     *                           or: http://example.org/wp-content/uploads/2015/my-pic.jpg
317
     */
318
    public static function delete_generated_files($local_file)
319
    {
320
        if (URLHelper::is_absolute($local_file)) {
57✔
321
            $local_file = URLHelper::url_to_file_system($local_file);
1✔
322
        }
323
        $info = PathHelper::pathinfo($local_file);
57✔
324
        $dir = $info['dirname'];
57✔
325
        $ext = $info['extension'];
57✔
326
        $filename = $info['filename'];
57✔
327
        self::process_delete_generated_files($filename, $ext, $dir, '-[0-9999999]*', '-[0-9]*x[0-9]*-c-[a-z]*.');
57✔
328
        self::process_delete_generated_files($filename, $ext, $dir, '-lbox-[0-9999999]*', '-lbox-[0-9]*x[0-9]*-[a-zA-Z0-9]*.');
57✔
329
        self::process_delete_generated_files($filename, 'jpg', $dir, '-tojpg.*');
57✔
330
        self::process_delete_generated_files($filename, 'jpg', $dir, '-tojpg-[0-9999999]*');
57✔
331
    }
332

333
    /**
334
     * Deletes resized versions of the supplied file name.
335
     *
336
     * 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.
337
     *
338
     * Keeping these here so I know what the hell we’re matching
339
     * $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);
340
     * $match = preg_match("/\/srv\/www\/wordpress-develop\/src\/wp-content\/uploads\/2014\/05\/arch-[0-9]*x[0-9]*-c-[a-z]*.jpg/", $filename);
341
     *
342
     * @param string  $filename       ex: my-pic.
343
     * @param string  $ext            ex: jpg.
344
     * @param string  $dir            var/www/wp-content/uploads/2015/.
345
     * @param string  $search_pattern Pattern of files to pluck from.
346
     * @param string  $match_pattern  Pattern of files to go forth and delete.
347
     */
348
    protected static function process_delete_generated_files($filename, $ext, $dir, $search_pattern, $match_pattern = null)
349
    {
350
        $searcher = '/' . $filename . $search_pattern;
57✔
351
        $files = \glob($dir . $searcher);
57✔
352
        if ($files === false || empty($files)) {
57✔
353
            return;
57✔
354
        }
355
        foreach ($files as $found_file) {
55✔
356
            $pattern = '/' . \preg_quote($dir, '/') . '\/' . \preg_quote($filename, '/') . $match_pattern . \preg_quote($ext, '/') . '/';
55✔
357
            $match = \preg_match($pattern, $found_file);
55✔
358
            if (!$match_pattern || $match) {
55✔
359
                \unlink($found_file);
10✔
360
            }
361
        }
362
    }
363

364
    /**
365
     * Determines the filepath corresponding to a given URL.
366
     *
367
     * @param string $url
368
     * @return string
369
     */
370
    public static function get_server_location($url)
371
    {
372
        // if we're already an absolute dir, just return.
373
        if (0 === \strpos($url, ABSPATH)) {
14✔
374
            return $url;
1✔
375
        }
376
        // otherwise, analyze URL then build mapping path
377
        $au = self::analyze_url($url);
13✔
378
        $result = self::_get_file_path($au['base'], $au['subdir'], $au['basename']);
13✔
379
        return $result;
13✔
380
    }
381

382
    /**
383
     * Determines the filepath where a given external file will be stored.
384
     *
385
     * @param string  $file
386
     * @return string
387
     */
388
    public static function get_sideloaded_file_loc($file)
389
    {
390
        $upload = \wp_upload_dir();
15✔
391
        $dir = $upload['path'];
15✔
392
        $filename = $file;
15✔
393
        $file = \parse_url($file);
15✔
394
        $path_parts = PathHelper::pathinfo($file['path']);
15✔
395
        $basename = \md5($filename);
15✔
396
        $ext = 'jpg';
15✔
397
        if (isset($path_parts['extension'])) {
15✔
398
            $ext = $path_parts['extension'];
15✔
399
        }
400
        return $dir . '/' . $basename . '.' . $ext;
15✔
401
    }
402

403
    /**
404
     * Downloads an external image to the server and stores it on the server.
405
     *
406
     * External/sideloaded images are saved in a folder named **external** in the uploads folder. If you want to change
407
     * the folder that is used for your sideloaded images, you can use the
408
     * [`timber/sideload_image/subdir`](https://timber.github.io/docs/v2/hooks/filters/#timber/sideload_image/subdir)
409
     * filter. You can disable this behavior using the same filter.
410
     *
411
     * @param string $file The URL to the original file.
412
     *
413
     * @return string The URL to the downloaded file.
414
     */
415
    public static function sideload_image($file)
416
    {
417
        /**
418
         * Adds a filter to change the upload folder temporarily.
419
         *
420
         * This is necessary so that external images are not downloaded every month in case
421
         * year-month-based folders are used. We need to use the `upload_dir` filter, because we use
422
         * functions like `wp_upload_bits()` which uses `wp_upload_dir()` under the hood.
423
         *
424
         * @ticket 1098
425
         * @link https://github.com/timber/timber/issues/1098
426
         */
427
        \add_filter('upload_dir', [__CLASS__, 'set_sideload_image_upload_dir']);
15✔
428

429
        $loc = self::get_sideloaded_file_loc($file);
15✔
430
        if (\file_exists($loc)) {
15✔
431
            $url = URLHelper::file_system_to_url($loc);
14✔
432

433
            \remove_filter('upload_dir', [__CLASS__, 'set_sideload_image_upload_dir']);
14✔
434

435
            return $url;
14✔
436
        }
437
        // Download file to temp location
438
        if (!\function_exists('download_url')) {
1✔
439
            require_once ABSPATH . '/wp-admin/includes/file.php';
×
440
        }
441
        $tmp = \download_url($file);
1✔
442
        \preg_match('/[^\?]+\.(jpe?g|jpe|gif|png)\b/i', $file, $matches);
1✔
443
        $file_array = [];
1✔
444
        $file_array['name'] = PathHelper::basename($matches[0]);
1✔
445
        $file_array['tmp_name'] = $tmp;
1✔
446
        // If error storing temporarily, do not use
447
        if (\is_wp_error($tmp)) {
1✔
448
            $file_array['tmp_name'] = '';
×
449
        }
450
        // do the validation and storage stuff
451
        $locinfo = PathHelper::pathinfo($loc);
1✔
452
        $file = \wp_upload_bits($locinfo['basename'], null, \file_get_contents($file_array['tmp_name']));
1✔
453
        // delete tmp file
454
        @\unlink($file_array['tmp_name']);
1✔
455

456
        \remove_filter('upload_dir', [__CLASS__, 'set_sideload_image_upload_dir']);
1✔
457

458
        return $file['url'];
1✔
459
    }
460

461
    /**
462
     * Gets upload folder definition for sideloaded images.
463
     *
464
     * Used by ImageHelper::sideload_image().
465
     *
466
     * @internal
467
     * @since 2.0.0
468
     * @see   \Timber\ImageHelper::sideload_image()
469
     *
470
     * @param array $upload Array of information about the upload directory.
471
     *
472
     * @return array         Array of information about the upload directory, modified by this
473
     *                        function.
474
     */
475
    public static function set_sideload_image_upload_dir(array $upload)
476
    {
477
        $subdir = 'external';
15✔
478

479
        /**
480
         * Filters to directory that should be used for sideloaded images.
481
         *
482
         * @since 2.0.0
483
         * @example
484
         * ```php
485
         * // Change the subdirectory used for sideloaded images.
486
         * add_filter( 'timber/sideload_image/subdir', function( $subdir ) {
487
         *     return 'sideloaded';
488
         * } );
489
         *
490
         * // Disable subdirectory used for sideloaded images.
491
         * add_filter( 'timber/sideload_image/subdir', '__return_false' );
492
         * ```
493
         *
494
         * @param string $subdir The subdir name to use for sideloaded images. Return an empty
495
         *                       string or a falsey value in order to not use a subfolder. Default
496
         *                       `external`.
497
         */
498
        $subdir = \apply_filters('timber/sideload_image/subdir', $subdir);
15✔
499

500
        if (!empty($subdir)) {
15✔
501
            // Remove slashes before or after.
502
            $subdir = \trim($subdir, '/');
13✔
503

504
            $upload['subdir'] = '/' . $subdir;
13✔
505
            $upload['path'] = $upload['basedir'] . $upload['subdir'];
13✔
506
            $upload['url'] = $upload['baseurl'] . $upload['subdir'];
13✔
507
        }
508

509
        return $upload;
15✔
510
    }
511

512
    /**
513
     * Takes a URL and breaks it into components.
514
     *
515
     * The components can then be used in the different steps of image processing.
516
     * The image is expected to be either part of a theme, plugin, or an upload.
517
     *
518
     * @param  string $url A URL (absolute or relative) pointing to an image.
519
     * @return array       An array (see keys in code below).
520
     */
521
    public static function analyze_url($url)
522
    {
523
        $result = [
89✔
524
            'url' => $url,
89✔
525
            // the initial url
526
            'absolute' => URLHelper::is_absolute($url),
89✔
527
            // is the url absolute or relative (to home_url)
528
            'base' => 0,
89✔
529
            // is the image in uploads dir, or in content dir (theme or plugin)
530
            'subdir' => '',
89✔
531
            // the path between base (uploads or content) and file
532
            'filename' => '',
89✔
533
            // the filename, without extension
534
            'extension' => '',
89✔
535
            // the file extension
536
            'basename' => '',
89✔
537
            // full file name
89✔
538
        ];
89✔
539
        $upload_dir = \wp_upload_dir();
89✔
540
        $tmp = $url;
89✔
541
        if (\str_starts_with($tmp, ABSPATH) || \str_starts_with($tmp, '/srv/www/')) {
89✔
542
            // we've been given a dir, not an url
543
            $result['absolute'] = true;
28✔
544
            if (\str_starts_with($tmp, $upload_dir['basedir'])) {
28✔
545
                $result['base'] = self::BASE_UPLOADS; // upload based
26✔
546
                $tmp = URLHelper::remove_url_component($tmp, $upload_dir['basedir']);
26✔
547
            }
548
            if (\str_starts_with($tmp, WP_CONTENT_DIR)) {
28✔
549
                $result['base'] = self::BASE_CONTENT; // content based
2✔
550
                $tmp = URLHelper::remove_url_component($tmp, WP_CONTENT_DIR);
28✔
551
            }
552
        } else {
553
            if (!$result['absolute']) {
61✔
554
                $tmp = \untrailingslashit(\network_home_url()) . $tmp;
5✔
555
            }
556
            if (URLHelper::starts_with($tmp, $upload_dir['baseurl'])) {
61✔
557
                $result['base'] = self::BASE_UPLOADS; // upload based
61✔
558
                $tmp = URLHelper::remove_url_component($tmp, $upload_dir['baseurl']);
61✔
559
            } elseif (URLHelper::starts_with($tmp, \content_url())) {
×
560
                $result['base'] = self::BASE_CONTENT; // content-based
×
561
                $tmp = self::theme_url_to_dir($tmp);
×
562
                $tmp = URLHelper::remove_url_component($tmp, WP_CONTENT_DIR);
×
563
            }
564
        }
565
        $parts = PathHelper::pathinfo($tmp);
89✔
566
        $result['subdir'] = ($parts['dirname'] === '/') ? '' : $parts['dirname'];
89✔
567
        $result['filename'] = $parts['filename'];
89✔
568
        $result['extension'] = \strtolower($parts['extension']);
89✔
569
        $result['basename'] = $parts['basename'];
89✔
570
        return $result;
89✔
571
    }
572

573
    /**
574
     * Converts a URL located in a theme directory into the raw file path.
575
     *
576
     * @param string  $src A URL (http://example.org/wp-content/themes/twentysixteen/images/home.jpg).
577
     * @return string Full path to the file in question.
578
     */
579
    public static function theme_url_to_dir($src)
580
    {
581
        $site_root = \trailingslashit(\get_theme_root_uri()) . \get_stylesheet();
×
582
        $tmp = \str_replace($site_root, '', $src);
×
583
        //$tmp = trailingslashit(get_theme_root()).get_stylesheet().$tmp;
584
        $tmp = \get_stylesheet_directory() . $tmp;
×
585
        if (\realpath($tmp)) {
×
586
            return \realpath($tmp);
×
587
        }
588
        return $tmp;
×
589
    }
590

591
    /**
592
     * Checks if uploaded image is located in theme.
593
     *
594
     * @param string $path image path.
595
     * @return bool     If the image is located in the theme directory it returns true.
596
     *                  If not or $path doesn't exits it returns false.
597
     */
598
    protected static function is_in_theme_dir($path)
599
    {
600
        $root = \realpath(\get_stylesheet_directory());
80✔
601

602
        if (false === $root) {
80✔
603
            return false;
×
604
        }
605

606
        if (0 === \strpos($path, (string) $root)) {
80✔
607
            return true;
×
608
        } else {
609
            return false;
80✔
610
        }
611
    }
612

613
    /**
614
     * Builds the public URL of a file based on its different components.
615
     *
616
     * @param  int    $base     One of `self::BASE_UPLOADS`, `self::BASE_CONTENT` to indicate if
617
     *                          file is an upload or a content (theme or plugin).
618
     * @param  string $subdir   Subdirectory in which file is stored, relative to $base root
619
     *                          folder.
620
     * @param  string $filename File name, including extension (but no path).
621
     * @param  bool   $absolute Should the returned URL be absolute (include protocol+host), or
622
     *                          relative.
623
     * @return string           The URL.
624
     */
625
    private static function _get_file_url($base, $subdir, $filename, $absolute)
626
    {
627
        $url = '';
85✔
628
        if (self::BASE_UPLOADS == $base) {
85✔
629
            $upload_dir = \wp_upload_dir();
83✔
630
            $url = $upload_dir['baseurl'];
83✔
631
        }
632
        if (self::BASE_CONTENT == $base) {
85✔
633
            $url = \content_url();
2✔
634
        }
635
        if (!empty($subdir)) {
85✔
636
            $url .= $subdir;
84✔
637
        }
638
        $url = \untrailingslashit($url) . '/' . $filename;
85✔
639
        if (!$absolute) {
85✔
640
            $home = \home_url();
5✔
641
            $home = \apply_filters('timber/image_helper/_get_file_url/home_url', $home);
5✔
642
            $url = \str_replace($home, '', $url);
5✔
643
        }
644
        return $url;
85✔
645
    }
646

647
    /**
648
     * Runs realpath to resolve symbolic links (../, etc). But only if it’s a path and not a URL.
649
     *
650
     * @param  string $path
651
     * @return string The resolved path.
652
     */
653
    protected static function maybe_realpath($path)
654
    {
655
        if (\strstr($path, '../') !== false) {
80✔
656
            return \realpath($path);
×
657
        }
658
        return $path;
80✔
659
    }
660

661
    /**
662
     * Builds the absolute file system location of a file based on its different components.
663
     *
664
     * @param  int    $base     One of `self::BASE_UPLOADS`, `self::BASE_CONTENT` to indicate if
665
     *                          file is an upload or a content (theme or plugin).
666
     * @param  string $subdir   Subdirectory in which file is stored, relative to $base root
667
     *                          folder.
668
     * @param  string $filename File name, including extension (but no path).
669
     * @return string           The file location.
670
     */
671
    private static function _get_file_path($base, $subdir, $filename)
672
    {
673
        if (URLHelper::is_url($subdir)) {
80✔
674
            $subdir = URLHelper::url_to_file_system($subdir);
×
675
        }
676
        $subdir = self::maybe_realpath($subdir);
80✔
677

678
        $path = '';
80✔
679
        if (self::BASE_UPLOADS == $base) {
80✔
680
            //it is in the Uploads directory
681
            $upload_dir = \wp_upload_dir();
78✔
682
            $path = $upload_dir['basedir'];
78✔
683
        } elseif (self::BASE_CONTENT == $base) {
2✔
684
            //it is in the content directory, somewhere else ...
685
            $path = WP_CONTENT_DIR;
2✔
686
        }
687
        if (self::is_in_theme_dir(\trailingslashit($subdir) . $filename)) {
80✔
688
            //this is for weird installs when the theme folder is outside of /wp-content
689
            return \trailingslashit($subdir) . $filename;
×
690
        }
691
        if (!empty($subdir)) {
80✔
692
            $path = \trailingslashit($path) . $subdir;
79✔
693
        }
694
        $path = \trailingslashit($path) . $filename;
80✔
695

696
        return URLHelper::remove_double_slashes($path);
80✔
697
    }
698

699
    /**
700
     * Main method that applies operation to src image:
701
     * 1. break down supplied URL into components
702
     * 2. use components to determine result file and URL
703
     * 3. check if a result file already exists
704
     * 4. otherwise, delegate to supplied TimberImageOperation
705
     *
706
     * @param  string  $src   A URL (absolute or relative) to an image.
707
     * @param  object  $op    Object of class TimberImageOperation.
708
     * @param  boolean $force Optional. Whether to remove any already existing result file and
709
     *                        force file generation. Default `false`.
710
     * @return string URL to the new image - or the source one if error.
711
     */
712
    private static function _operate($src, $op, $force = false)
713
    {
714
        if (empty($src)) {
89✔
715
            return '';
8✔
716
        }
717

718
        $allow_fs_write = \apply_filters('timber/allow_fs_write', true);
82✔
719

720
        if ($allow_fs_write === false) {
82✔
721
            return $src;
2✔
722
        }
723

724
        $external = false;
80✔
725
        // if external image, load it first
726
        if (URLHelper::is_external_content($src)) {
80✔
727
            $src = self::sideload_image($src);
9✔
728
            $external = true;
9✔
729
        }
730

731
        // break down URL into components
732
        $au = self::analyze_url($src);
80✔
733

734
        // build URL and filenames
735
        $new_url = self::_get_file_url(
80✔
736
            $au['base'],
80✔
737
            $au['subdir'],
80✔
738
            $op->filename($au['filename'], $au['extension']),
80✔
739
            $au['absolute']
80✔
740
        );
80✔
741
        $destination_path = self::_get_file_path(
80✔
742
            $au['base'],
80✔
743
            $au['subdir'],
80✔
744
            $op->filename($au['filename'], $au['extension'])
80✔
745
        );
80✔
746
        $source_path = self::_get_file_path(
80✔
747
            $au['base'],
80✔
748
            $au['subdir'],
80✔
749
            $au['basename']
80✔
750
        );
80✔
751

752
        /**
753
         * Filters the URL for the resized version of a `Timber\Image`.
754
         *
755
         * You’ll probably need to use this in combination with `timber/image/new_path`.
756
         *
757
         * @since 1.0.0
758
         *
759
         * @param string $new_url The URL to the resized version of an image.
760
         */
761
        $new_url = \apply_filters('timber/image/new_url', $new_url);
80✔
762

763
        /**
764
         * Filters the destination path for the resized version of a `Timber\Image`.
765
         *
766
         * A possible use case for this would be to store all images generated by Timber in a
767
         * separate directory. You’ll probably need to use this in combination with
768
         * `timber/image/new_url`.
769
         *
770
         * @since 1.0.0
771
         *
772
         * @param string $destination_path Full path to the destination of a resized image.
773
         */
774
        $destination_path = \apply_filters('timber/image/new_path', $destination_path);
80✔
775

776
        // if already exists...
777
        if (\file_exists($source_path) && \file_exists($destination_path)) {
80✔
778
            if ($force || \filemtime($source_path) > \filemtime($destination_path)) {
17✔
779
                // Force operation - warning: will regenerate the image on every pageload, use for testing purposes only!
780
                \unlink($destination_path);
3✔
781
            } else {
782
                // return existing file (caching)
783
                return $new_url;
14✔
784
            }
785
        }
786
        // otherwise generate result file
787
        if ($op->run($source_path, $destination_path)) {
69✔
788
            if (\get_class($op) === 'Timber\Image\Operation\Resize' && $external) {
60✔
789
                $new_url = \strtolower($new_url);
×
790
            }
791
            return $new_url;
60✔
792
        } else {
793
            // in case of error, we return source file itself
794
            return $src;
8✔
795
        }
796
    }
797

798
    // -- the below methods are just used for unit testing the URL generation code
799
    //
800
    /**
801
     * @internal
802
     */
803
    public static function get_letterbox_file_url($url, $w, $h, $color)
804
    {
805
        $au = self::analyze_url($url);
1✔
806
        $op = new Operation\Letterbox($w, $h, $color);
1✔
807
        $new_url = self::_get_file_url(
1✔
808
            $au['base'],
1✔
809
            $au['subdir'],
1✔
810
            $op->filename($au['filename'], $au['extension']),
1✔
811
            $au['absolute']
1✔
812
        );
1✔
813
        return $new_url;
1✔
814
    }
815

816
    /**
817
     * @internal
818
     */
819
    public static function get_letterbox_file_path($url, $w, $h, $color)
820
    {
821
        $au = self::analyze_url($url);
1✔
822
        $op = new Operation\Letterbox($w, $h, $color);
1✔
823
        $new_path = self::_get_file_path(
1✔
824
            $au['base'],
1✔
825
            $au['subdir'],
1✔
826
            $op->filename($au['filename'], $au['extension'])
1✔
827
        );
1✔
828
        return $new_path;
1✔
829
    }
830

831
    /**
832
     * @internal
833
     */
834
    public static function get_resize_file_url($url, $w, $h, $crop)
835
    {
836
        $au = self::analyze_url($url);
4✔
837
        $op = new Operation\Resize($w, $h, $crop);
4✔
838
        $new_url = self::_get_file_url(
4✔
839
            $au['base'],
4✔
840
            $au['subdir'],
4✔
841
            $op->filename($au['filename'], $au['extension']),
4✔
842
            $au['absolute']
4✔
843
        );
4✔
844
        return $new_url;
4✔
845
    }
846

847
    /**
848
     * @internal
849
     */
850
    public static function get_resize_file_path($url, $w, $h, $crop)
851
    {
852
        $au = self::analyze_url($url);
5✔
853
        $op = new Operation\Resize($w, $h, $crop);
5✔
854
        $new_path = self::_get_file_path(
5✔
855
            $au['base'],
5✔
856
            $au['subdir'],
5✔
857
            $op->filename($au['filename'], $au['extension'])
5✔
858
        );
5✔
859
        return $new_path;
5✔
860
    }
861
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc