Skip to content

Commit ea1907b

Browse files
committed
feat!: support mutliple source directories, remove public_path config, refactor controller
BREAKING CHANGE: The `public_path` config has been replaced by `source_directories` and `default_source_directory`.
1 parent d216263 commit ea1907b

File tree

3 files changed

+83
-22
lines changed

3 files changed

+83
-22
lines changed

config/image-transform-url.php

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,35 @@
33
return [
44
/*
55
|--------------------------------------------------------------------------
6-
| Public Path
6+
| Source Directories
77
|--------------------------------------------------------------------------
88
|
9-
| Here you may configure the public path/prefix where the images are stored.
10-
| If you are storing the images in 'storage/app/public', the typically
11-
| linked public path would be 'storage'.
9+
| Here you may configure the directories from which the image transformer
10+
| is allowed to serve images. For security reasons, it is recommended
11+
| to only allow directories which are already publicly accessible.
12+
|
13+
| Important: The public storage directory should be addressed directly via
14+
| storage('app/public') instead of the public_path('storage') link.
15+
|
16+
*/
17+
18+
'source_directories' => [
19+
'images' => public_path('images'),
20+
'storage' => storage_path('app/public/images'),
21+
],
22+
23+
/*
24+
|--------------------------------------------------------------------------
25+
| Default Source Directory
26+
|--------------------------------------------------------------------------
27+
|
28+
| Below you may configure the default source directory which is used when
29+
| no specific path prefix is provided in the URL. This should be one of
30+
| the keys from the source_directories array.
1231
|
1332
*/
1433

15-
'public_path' => env('IMAGE_TRANSFORM_PUBLIC_PATH', 'images'),
34+
'default_source_directory' => env('IMAGE_TRANSFORM_DEFAULT_SOURCE_DIRECTORY', 'images'),
1635

1736
/*
1837
|--------------------------------------------------------------------------

routes/image.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,16 @@
66
use Illuminate\Support\Facades\Route;
77

88
Route::prefix(config()->string('image-transform-url.route_prefix'))->group(function () {
9-
Route::get('{options}/{path}', ImageTransformerController::class)
9+
// Explicit path prefix route
10+
Route::get('{pathPrefix}/{options}/{path}', [ImageTransformerController::class, 'transformWithPrefix'])
11+
->where('pathPrefix', '[a-zA-Z][a-zA-Z0-9_-]*')
1012
->where('options', '([a-zA-Z]+=-?[a-zA-Z0-9]+,?)+')
1113
->where('path', '.*\..*')
1214
->name('image.transform');
15+
16+
// Default path prefix route
17+
Route::get('{options}/{path}', [ImageTransformerController::class, 'transformDefault'])
18+
->where('options', '([a-zA-Z]+=-?[a-zA-Z0-9]+,?)+')
19+
->where('path', '.*\..*')
20+
->name('image.transform.default');
1321
});

src/Http/Controllers/ImageTransformerController.php

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,25 @@ class ImageTransformerController extends \Illuminate\Routing\Controller
2727
{
2828
use ResolvesOptions;
2929

30-
public function __invoke(Request $request, string $options, string $path)
30+
public function transformWithPrefix(Request $request, string $pathPrefix, string $options, string $path)
3131
{
32-
$pathPrefix = config()->string('image-transform-url.public_path');
33-
34-
$publicPath = realpath(public_path($pathPrefix.'/'.$path));
35-
36-
abort_unless($publicPath, 404);
32+
return $this->handleTransform($request, $pathPrefix, $options, $path);
33+
}
3734

38-
abort_unless(Str::startsWith($publicPath, public_path($pathPrefix)), 404);
35+
public function transformDefault(Request $request, string $options, string $path)
36+
{
37+
return $this->handleTransform($request, null, $options, $path);
38+
}
3939

40-
abort_unless(in_array(File::mimeType($publicPath), AllowedMimeTypes::all(), true), 404);
40+
protected function handleTransform(Request $request, ?string $pathPrefix, string $options, ?string $path = null)
41+
{
42+
$realPath = $this->handlePath($pathPrefix, $path);
4143

4244
$options = $this->parseOptions($options);
4345

4446
// Check cache
4547
if (config()->boolean('image-transform-url.cache.enabled')) {
46-
$cachePath = $this->getCachePath($path, $options);
48+
$cachePath = $this->getCachePath($pathPrefix, $path, $options);
4749

4850
if (File::exists($cachePath)) {
4951
if (Cache::has('image-transform-url:'.$cachePath)) {
@@ -66,7 +68,7 @@ public function __invoke(Request $request, string $options, string $path)
6668
$this->rateLimit($request, $path);
6769
}
6870

69-
$image = Image::read($publicPath);
71+
$image = Image::read($realPath);
7072

7173
if (Arr::hasAny($options, ['width', 'height'])) {
7274
$image->scale(
@@ -108,7 +110,7 @@ public function __invoke(Request $request, string $options, string $path)
108110
}
109111

110112
// We use the mime type instead of the extension to determine the format, because this is more reliable.
111-
$originalMimetype = File::mimeType($publicPath);
113+
$originalMimetype = File::mimeType($realPath);
112114

113115
$format = $this->getStringOptionValue($options, 'format', $originalMimetype);
114116
$quality = $this->getPositiveIntOptionValue($options, 'quality', 100, 100);
@@ -124,9 +126,9 @@ public function __invoke(Request $request, string $options, string $path)
124126
$encoded = $image->encode($encoder);
125127

126128
if (config()->boolean('image-transform-url.cache.enabled')) {
127-
defer(function () use ($path, $options, $encoded) {
129+
defer(function () use ($pathPrefix, $path, $options, $encoded) {
128130

129-
$cachePath = $this->getCachePath($path, $options);
131+
$cachePath = $this->getCachePath($pathPrefix, $path, $options);
130132

131133
$cacheDir = dirname($cachePath);
132134

@@ -149,6 +151,40 @@ public function __invoke(Request $request, string $options, string $path)
149151

150152
}
151153

154+
/**
155+
* Handle the path and ensure it is valid.
156+
*/
157+
protected function handlePath(?string &$pathPrefix, ?string &$path): string
158+
{
159+
if ($path === null) {
160+
$path = $pathPrefix;
161+
$pathPrefix = null;
162+
}
163+
164+
$allowedSourceDirectories = config('image-transform-url.source_directories', []);
165+
166+
if (! $pathPrefix) {
167+
$pathPrefix = config('image-transform-url.default_source_directory') ?? array_key_first($allowedSourceDirectories);
168+
}
169+
170+
abort_unless(array_key_exists($pathPrefix, $allowedSourceDirectories), 404);
171+
172+
$basePath = $allowedSourceDirectories[$pathPrefix];
173+
$requestedPath = $basePath.'/'.$path;
174+
$realPath = realpath($requestedPath);
175+
176+
abort_unless($realPath, 404);
177+
178+
$allowedBasePath = realpath($basePath);
179+
abort_unless($allowedBasePath, 404);
180+
181+
abort_unless(Str::startsWith($realPath, $allowedBasePath), 404);
182+
183+
abort_unless(in_array(File::mimeType($realPath), AllowedMimeTypes::all(), true), 404);
184+
185+
return $realPath;
186+
}
187+
152188
/**
153189
* Rate limit the request.
154190
*/
@@ -202,10 +238,8 @@ protected static function parseOptions(string $options): array
202238
/**
203239
* Get the cache path for the given path and options.
204240
*/
205-
protected static function getCachePath(string $path, array $options): string
241+
protected static function getCachePath(string $pathPrefix, string $path, array $options): string
206242
{
207-
$pathPrefix = config()->string('image-transform-url.public_path');
208-
209243
$optionsHash = md5(json_encode($options));
210244

211245
return Storage::disk(config()->string('image-transform-url.cache.disk'))->path('_cache/image-transform-url/'.$pathPrefix.'/'.$optionsHash.'_'.$path);

0 commit comments

Comments
 (0)