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