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