3 namespace BookStack\Uploads;
5 use Illuminate\Contracts\Filesystem\Filesystem as StorageDisk;
6 use Illuminate\Filesystem\FilesystemManager;
7 use Illuminate\Support\Str;
8 use League\Flysystem\WhitespacePathNormalizer;
12 public function __construct(
13 protected FilesystemManager $fileSystem,
18 * Get the storage disk for the given image type.
20 public function getDisk(string $imageType = ''): StorageDisk
22 return $this->fileSystem->disk($this->getDiskName($imageType));
26 * Check if local secure image storage (Fetched behind authentication)
27 * is currently active in the instance.
29 public function usingSecureImages(string $imageType = 'gallery'): bool
31 return $this->getDiskName($imageType) === 'local_secure_images';
35 * Check if "local secure restricted" (Fetched behind auth, with permissions enforced)
36 * is currently active in the instance.
38 public function usingSecureRestrictedImages()
40 return config('filesystems.images') === 'local_secure_restricted';
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.
47 public function adjustPathForDisk(string $path, string $imageType = ''): string
49 $path = (new WhitespacePathNormalizer())->normalizePath(str_replace('uploads/images/', '', $path));
51 if ($this->usingSecureImages($imageType)) {
55 return 'uploads/images/' . $path;
59 * Clean up an image file name to be both URL and storage safe.
61 public function cleanImageFileName(string $name): string
63 $name = str_replace(' ', '-', $name);
64 $nameParts = explode('.', $name);
65 $extension = array_pop($nameParts);
66 $name = implode('-', $nameParts);
67 $name = Str::slug($name);
69 if (strlen($name) === 0) {
70 $name = Str::random(10);
73 return $name . '.' . $extension;
77 * Get the name of the storage disk to use.
79 protected function getDiskName(string $imageType): string
81 $storageType = config('filesystems.images');
82 $localSecureInUse = ($storageType === 'local_secure' || $storageType === 'local_secure_restricted');
84 // Ensure system images (App logo) are uploaded to a public space
85 if ($imageType === 'system' && $localSecureInUse) {
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';
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.
103 public function urlToPath(string $url): ?string
105 $url = ltrim(trim($url), '/');
107 // Handle potential relative paths
108 $isRelative = !str_starts_with($url, 'http');
110 if (str_starts_with(strtolower($url), 'uploads/images')) {
111 return trim($url, '/');
117 // Handle local images based on paths on the same domain
118 $potentialHostPaths = [
119 url('uploads/images/'),
120 $this->getPublicUrl('/uploads/images/'),
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)), '/');
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.
137 public function getPublicUrl(string $filePath): string
139 $storageUrl = config('filesystems.url');
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';
149 $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
153 $basePath = $storageUrl ?: url('/');
155 return rtrim($basePath, '/') . $filePath;
159 * Save image data for the given path in the public space, if possible,
160 * for the provided storage mechanism.
162 public function storeInPublicSpace(StorageDisk $storage, string $path, string $data): void
164 $storage->put($path, $data);
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');
176 * Check if the image storage in use is an S3-like (but not likely S3) external system.
178 protected function isS3Like(): bool
180 $usingS3 = strtolower(config('filesystems.images')) === 's3';
181 return $usingS3 && !is_null(config('filesystems.disks.s3.endpoint'));