]> BookStack Code Mirror - bookstack/blob - app/Uploads/ImageStorage.php
Images: Started refactor of image service
[bookstack] / app / Uploads / ImageStorage.php
1 <?php
2
3 namespace BookStack\Uploads;
4
5 use Illuminate\Contracts\Filesystem\Filesystem as StorageDisk;
6 use Illuminate\Filesystem\FilesystemManager;
7 use Illuminate\Support\Str;
8 use League\Flysystem\WhitespacePathNormalizer;
9
10 class ImageStorage
11 {
12     public function __construct(
13         protected FilesystemManager $fileSystem,
14     ) {
15     }
16
17     /**
18      * Get the storage disk for the given image type.
19      */
20     public function getDisk(string $imageType = ''): StorageDisk
21     {
22         return $this->fileSystem->disk($this->getDiskName($imageType));
23     }
24
25     /**
26      * Check if local secure image storage (Fetched behind authentication)
27      * is currently active in the instance.
28      */
29     public function usingSecureImages(string $imageType = 'gallery'): bool
30     {
31         return $this->getDiskName($imageType) === 'local_secure_images';
32     }
33
34     /**
35      * Check if "local secure restricted" (Fetched behind auth, with permissions enforced)
36      * is currently active in the instance.
37      */
38     public function usingSecureRestrictedImages()
39     {
40         return config('filesystems.images') === 'local_secure_restricted';
41     }
42
43     /**
44      * Change the originally provided path to fit any disk-specific requirements.
45      * This also ensures the path is kept to the expected root folders.
46      */
47     public function adjustPathForDisk(string $path, string $imageType = ''): string
48     {
49         $path = (new WhitespacePathNormalizer())->normalizePath(str_replace('uploads/images/', '', $path));
50
51         if ($this->usingSecureImages($imageType)) {
52             return $path;
53         }
54
55         return 'uploads/images/' . $path;
56     }
57
58     /**
59      * Clean up an image file name to be both URL and storage safe.
60      */
61     public function cleanImageFileName(string $name): string
62     {
63         $name = str_replace(' ', '-', $name);
64         $nameParts = explode('.', $name);
65         $extension = array_pop($nameParts);
66         $name = implode('-', $nameParts);
67         $name = Str::slug($name);
68
69         if (strlen($name) === 0) {
70             $name = Str::random(10);
71         }
72
73         return $name . '.' . $extension;
74     }
75
76     /**
77      * Get the name of the storage disk to use.
78      */
79     protected function getDiskName(string $imageType): string
80     {
81         $storageType = config('filesystems.images');
82         $localSecureInUse = ($storageType === 'local_secure' || $storageType === 'local_secure_restricted');
83
84         // Ensure system images (App logo) are uploaded to a public space
85         if ($imageType === 'system' && $localSecureInUse) {
86             return 'local';
87         }
88
89         // Rename local_secure options to get our image specific storage driver which
90         // is scoped to the relevant image directories.
91         if ($localSecureInUse) {
92             return 'local_secure_images';
93         }
94
95         return $storageType;
96     }
97
98     /**
99      * Get a storage path for the given image URL.
100      * Ensures the path will start with "uploads/images".
101      * Returns null if the url cannot be resolved to a local URL.
102      */
103     public function urlToPath(string $url): ?string
104     {
105         $url = ltrim(trim($url), '/');
106
107         // Handle potential relative paths
108         $isRelative = !str_starts_with($url, 'http');
109         if ($isRelative) {
110             if (str_starts_with(strtolower($url), 'uploads/images')) {
111                 return trim($url, '/');
112             }
113
114             return null;
115         }
116
117         // Handle local images based on paths on the same domain
118         $potentialHostPaths = [
119             url('uploads/images/'),
120             $this->getPublicUrl('/uploads/images/'),
121         ];
122
123         foreach ($potentialHostPaths as $potentialBasePath) {
124             $potentialBasePath = strtolower($potentialBasePath);
125             if (str_starts_with(strtolower($url), $potentialBasePath)) {
126                 return 'uploads/images/' . trim(substr($url, strlen($potentialBasePath)), '/');
127             }
128         }
129
130         return null;
131     }
132
133     /**
134      * Gets a public facing url for an image by checking relevant environment variables.
135      * If s3-style store is in use it will default to guessing a public bucket URL.
136      */
137     public function getPublicUrl(string $filePath): string
138     {
139         $storageUrl = config('filesystems.url');
140
141         // Get the standard public s3 url if s3 is set as storage type
142         // Uses the nice, short URL if bucket name has no periods in otherwise the longer
143         // region-based url will be used to prevent http issues.
144         if (!$storageUrl && config('filesystems.images') === 's3') {
145             $storageDetails = config('filesystems.disks.s3');
146             if (!str_contains($storageDetails['bucket'], '.')) {
147                 $storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';
148             } else {
149                 $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
150             }
151         }
152
153         $basePath = $storageUrl ?: url('/');
154
155         return rtrim($basePath, '/') . $filePath;
156     }
157
158     /**
159      * Save image data for the given path in the public space, if possible,
160      * for the provided storage mechanism.
161      */
162     public function storeInPublicSpace(StorageDisk $storage, string $path, string $data): void
163     {
164         $storage->put($path, $data);
165
166         // Set visibility when a non-AWS-s3, s3-like storage option is in use.
167         // Done since this call can break s3-like services but desired for other image stores.
168         // Attempting to set ACL during above put request requires different permissions
169         // hence would technically be a breaking change for actual s3 usage.
170         if (!$this->isS3Like()) {
171             $storage->setVisibility($path, 'public');
172         }
173     }
174
175     /**
176      * Check if the image storage in use is an S3-like (but not likely S3) external system.
177      */
178     protected function isS3Like(): bool
179     {
180         $usingS3 = strtolower(config('filesystems.images')) === 's3';
181         return $usingS3 && !is_null(config('filesystems.disks.s3.endpoint'));
182     }
183 }