]> BookStack Code Mirror - bookstack/blob - app/Services/ImageService.php
Found the source of the issue, not sure how to fix
[bookstack] / app / Services / ImageService.php
1 <?php namespace BookStack\Services;
2
3 use BookStack\Exceptions\ImageUploadException;
4 use BookStack\Image;
5 use BookStack\User;
6 use Exception;
7 use Intervention\Image\Exception\NotSupportedException;
8 use Intervention\Image\ImageManager;
9 use Illuminate\Contracts\Filesystem\Factory as FileSystem;
10 use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
11 use Illuminate\Contracts\Cache\Repository as Cache;
12 use Setting;
13 use Symfony\Component\HttpFoundation\File\UploadedFile;
14
15 class ImageService
16 {
17
18     protected $imageTool;
19     protected $fileSystem;
20     protected $cache;
21
22     /**
23      * @var FileSystemInstance
24      */
25     protected $storageInstance;
26     protected $storageUrl;
27
28     /**
29      * ImageService constructor.
30      * @param $imageTool
31      * @param $fileSystem
32      * @param $cache
33      */
34     public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
35     {
36         $this->imageTool = $imageTool;
37         $this->fileSystem = $fileSystem;
38         $this->cache = $cache;
39     }
40
41     /**
42      * Saves a new image from an upload.
43      * @param UploadedFile $uploadedFile
44      * @param  string      $type
45      * @return mixed
46      */
47     public function saveNewFromUpload(UploadedFile $uploadedFile, $type)
48     {
49         $imageName = $uploadedFile->getClientOriginalName();
50         $imageData = file_get_contents($uploadedFile->getRealPath());
51         return $this->saveNew($imageName, $imageData, $type);
52     }
53
54
55     /**
56      * Gets an image from url and saves it to the database.
57      * @param             $url
58      * @param string      $type
59      * @param bool|string $imageName
60      * @return mixed
61      * @throws \Exception
62      */
63     private function saveNewFromUrl($url, $type, $imageName = false)
64     {
65         $imageName = $imageName ? $imageName : basename($url);
66         $imageData = file_get_contents($url);
67         if($imageData === false) throw new \Exception('Cannot get image from ' . $url);
68         return $this->saveNew($imageName, $imageData, $type);
69     }
70
71     /**
72      * Saves a new image
73      * @param string $imageName
74      * @param string $imageData
75      * @param string $type
76      * @return Image
77      * @throws ImageUploadException
78      */
79     private function saveNew($imageName, $imageData, $type)
80     {
81         $storage = $this->getStorage();
82         $secureUploads = Setting::get('app-secure-images');
83         $imageName = str_replace(' ', '-', $imageName);
84
85         if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
86
87         $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
88         while ($storage->exists($imagePath . $imageName)) {
89             $imageName = str_random(3) . $imageName;
90         }
91         $fullPath = $imagePath . $imageName;
92
93         try {
94             $storage->put($fullPath, $imageData);
95         } catch (Exception $e) {
96             throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
97         }
98
99         $imageDetails = [
100             'name'       => $imageName,
101             'path'       => $fullPath,
102             'url'        => $this->getPublicUrl($fullPath),
103             'type'       => $type
104         ];
105
106         if (auth()->user() && auth()->user()->id !== 0) {
107             $userId = auth()->user()->id;
108             $imageDetails['created_by'] = $userId;
109             $imageDetails['updated_by'] = $userId;
110         }
111
112         $image = Image::forceCreate($imageDetails);
113
114         return $image;
115     }
116
117     /**
118      * Get the thumbnail for an image.
119      * If $keepRatio is true only the width will be used.
120      * Checks the cache then storage to avoid creating / accessing the filesystem on every check.
121      *
122      * @param Image $image
123      * @param int $width
124      * @param int $height
125      * @param bool $keepRatio
126      * @return string
127      * @throws Exception
128      * @throws ImageUploadException
129      */
130     public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
131     {
132         $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
133         $thumbFilePath = dirname($image->path) . $thumbDirName . basename($image->path);
134
135         if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) {
136             return $this->getPublicUrl($thumbFilePath);
137         }
138
139         $storage = $this->getStorage();
140
141         if ($storage->exists($thumbFilePath)) {
142             return $this->getPublicUrl($thumbFilePath);
143         }
144
145         try {
146             $thumb = $this->imageTool->make($storage->get($image->path));
147         } catch (Exception $e) {
148             if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
149                 throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
150             } else {
151                 throw $e;
152             }
153         }
154
155         if ($keepRatio) {
156             $thumb->resize($width, null, function ($constraint) {
157                 $constraint->aspectRatio();
158                 $constraint->upsize();
159             });
160         } else {
161             $thumb->fit($width, $height);
162         }
163
164         $thumbData = (string)$thumb->encode();
165         $storage->put($thumbFilePath, $thumbData);
166         $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
167
168         return $this->getPublicUrl($thumbFilePath);
169     }
170
171     /**
172      * Destroys an Image object along with its files and thumbnails.
173      * @param Image $image
174      * @return bool
175      */
176     public function destroyImage(Image $image)
177     {
178         $storage = $this->getStorage();
179
180         $imageFolder = dirname($image->path);
181         $imageFileName = basename($image->path);
182         $allImages = collect($storage->allFiles($imageFolder));
183
184         $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
185             $expectedIndex = strlen($imagePath) - strlen($imageFileName);
186             return strpos($imagePath, $imageFileName) === $expectedIndex;
187         });
188
189         $storage->delete($imagesToDelete->all());
190
191         // Cleanup of empty folders
192         foreach ($storage->directories($imageFolder) as $directory) {
193             if ($this->isFolderEmpty($directory)) $storage->deleteDirectory($directory);
194         }
195         if ($this->isFolderEmpty($imageFolder)) $storage->deleteDirectory($imageFolder);
196
197         $image->delete();
198         return true;
199     }
200
201     /**
202      * Save a gravatar image and set a the profile image for a user.
203      * @param User $user
204      * @param int  $size
205      * @return mixed
206      */
207     public function saveUserGravatar(User $user, $size = 500)
208     {
209         $emailHash = md5(strtolower(trim($user->email)));
210         $url = 'http://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
211         $imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
212         $image = $this->saveNewFromUrl($url, 'user', $imageName);
213         $image->created_by = $user->id;
214         $image->updated_by = $user->id;
215         $image->save();
216         return $image;
217     }
218
219     /**
220      * Get the storage that will be used for storing images.
221      * @return FileSystemInstance
222      */
223     private function getStorage()
224     {
225         if ($this->storageInstance !== null) return $this->storageInstance;
226
227         $storageType = config('filesystems.default');
228         $this->storageInstance = $this->fileSystem->disk($storageType);
229
230         return $this->storageInstance;
231     }
232
233     /**
234      * Check whether or not a folder is empty.
235      * @param $path
236      * @return int
237      */
238     private function isFolderEmpty($path)
239     {
240         $files = $this->getStorage()->files($path);
241         $folders = $this->getStorage()->directories($path);
242         return count($files) === 0 && count($folders) === 0;
243     }
244
245     /**
246      * Gets a public facing url for an image by checking relevant environment variables.
247      * @param $filePath
248      * @return string
249      */
250     private function getPublicUrl($filePath)
251     {
252         if ($this->storageUrl === null) {
253             $storageUrl = config('filesystems.url');
254
255             // Get the standard public s3 url if s3 is set as storage type
256             if ($storageUrl == false && config('filesystems.default') === 's3') {
257                 $storageDetails = config('filesystems.disks.s3');
258                 $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
259             }
260
261             $this->storageUrl = $storageUrl;
262         }
263
264         return ($this->storageUrl == false ? '' : rtrim($this->storageUrl, '/')) . $filePath;
265     }
266
267
268 }