]> BookStack Code Mirror - bookstack/blob - app/Services/ImageService.php
replace GPL diff lib with MIT lib
[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         } catch (Exception $e) {
99             throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
100         }
101
102         $imageDetails = [
103             'name'       => $imageName,
104             'path'       => $fullPath,
105             'url'        => $this->getPublicUrl($fullPath),
106             'type'       => $type,
107             'uploaded_to' => $uploadedTo
108         ];
109
110         if (auth()->user() && auth()->user()->id !== 0) {
111             $userId = auth()->user()->id;
112             $imageDetails['created_by'] = $userId;
113             $imageDetails['updated_by'] = $userId;
114         }
115
116         $image = Image::forceCreate($imageDetails);
117
118         return $image;
119     }
120
121     /**
122      * Get the thumbnail for an image.
123      * If $keepRatio is true only the width will be used.
124      * Checks the cache then storage to avoid creating / accessing the filesystem on every check.
125      *
126      * @param Image $image
127      * @param int $width
128      * @param int $height
129      * @param bool $keepRatio
130      * @return string
131      * @throws Exception
132      * @throws ImageUploadException
133      */
134     public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
135     {
136         $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
137         $thumbFilePath = dirname($image->path) . $thumbDirName . basename($image->path);
138
139         if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) {
140             return $this->getPublicUrl($thumbFilePath);
141         }
142
143         $storage = $this->getStorage();
144
145         if ($storage->exists($thumbFilePath)) {
146             return $this->getPublicUrl($thumbFilePath);
147         }
148
149         try {
150             $thumb = $this->imageTool->make($storage->get($image->path));
151         } catch (Exception $e) {
152             if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
153                 throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
154             } else {
155                 throw $e;
156             }
157         }
158
159         if ($keepRatio) {
160             $thumb->resize($width, null, function ($constraint) {
161                 $constraint->aspectRatio();
162                 $constraint->upsize();
163             });
164         } else {
165             $thumb->fit($width, $height);
166         }
167
168         $thumbData = (string)$thumb->encode();
169         $storage->put($thumbFilePath, $thumbData);
170         $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
171
172         return $this->getPublicUrl($thumbFilePath);
173     }
174
175     /**
176      * Destroys an Image object along with its files and thumbnails.
177      * @param Image $image
178      * @return bool
179      */
180     public function destroyImage(Image $image)
181     {
182         $storage = $this->getStorage();
183
184         $imageFolder = dirname($image->path);
185         $imageFileName = basename($image->path);
186         $allImages = collect($storage->allFiles($imageFolder));
187
188         $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
189             $expectedIndex = strlen($imagePath) - strlen($imageFileName);
190             return strpos($imagePath, $imageFileName) === $expectedIndex;
191         });
192
193         $storage->delete($imagesToDelete->all());
194
195         // Cleanup of empty folders
196         foreach ($storage->directories($imageFolder) as $directory) {
197             if ($this->isFolderEmpty($directory)) $storage->deleteDirectory($directory);
198         }
199         if ($this->isFolderEmpty($imageFolder)) $storage->deleteDirectory($imageFolder);
200
201         $image->delete();
202         return true;
203     }
204
205     /**
206      * Save a gravatar image and set a the profile image for a user.
207      * @param User $user
208      * @param int  $size
209      * @return mixed
210      */
211     public function saveUserGravatar(User $user, $size = 500)
212     {
213         $emailHash = md5(strtolower(trim($user->email)));
214         $url = 'http://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
215         $imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
216         $image = $this->saveNewFromUrl($url, 'user', $imageName);
217         $image->created_by = $user->id;
218         $image->updated_by = $user->id;
219         $image->save();
220         return $image;
221     }
222
223     /**
224      * Get the storage that will be used for storing images.
225      * @return FileSystemInstance
226      */
227     private function getStorage()
228     {
229         if ($this->storageInstance !== null) return $this->storageInstance;
230
231         $storageType = config('filesystems.default');
232         $this->storageInstance = $this->fileSystem->disk($storageType);
233
234         return $this->storageInstance;
235     }
236
237     /**
238      * Check whether or not a folder is empty.
239      * @param $path
240      * @return int
241      */
242     private function isFolderEmpty($path)
243     {
244         $files = $this->getStorage()->files($path);
245         $folders = $this->getStorage()->directories($path);
246         return count($files) === 0 && count($folders) === 0;
247     }
248
249     /**
250      * Gets a public facing url for an image by checking relevant environment variables.
251      * @param $filePath
252      * @return string
253      */
254     private function getPublicUrl($filePath)
255     {
256         if ($this->storageUrl === null) {
257             $storageUrl = config('filesystems.url');
258
259             // Get the standard public s3 url if s3 is set as storage type
260             if ($storageUrl == false && config('filesystems.default') === 's3') {
261                 $storageDetails = config('filesystems.disks.s3');
262                 $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
263             }
264
265             $this->storageUrl = $storageUrl;
266         }
267
268         return ($this->storageUrl == false ? '' : rtrim($this->storageUrl, '/')) . $filePath;
269     }
270
271
272 }