]> BookStack Code Mirror - bookstack/blob - app/Uploads/ImageRepo.php
Modernize third party services' logos
[bookstack] / app / Uploads / ImageRepo.php
1 <?php
2
3 namespace BookStack\Uploads;
4
5 use BookStack\Auth\Permissions\PermissionService;
6 use BookStack\Entities\Models\Page;
7 use BookStack\Exceptions\ImageUploadException;
8 use Exception;
9 use Illuminate\Database\Eloquent\Builder;
10 use Symfony\Component\HttpFoundation\File\UploadedFile;
11
12 class ImageRepo
13 {
14     protected $image;
15     protected $imageService;
16     protected $restrictionService;
17     protected $page;
18
19     protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
20
21     /**
22      * ImageRepo constructor.
23      */
24     public function __construct(
25         Image $image,
26         ImageService $imageService,
27         PermissionService $permissionService,
28         Page $page
29     ) {
30         $this->image = $image;
31         $this->imageService = $imageService;
32         $this->restrictionService = $permissionService;
33         $this->page = $page;
34     }
35
36     /**
37      * Check if the given image extension is supported by BookStack.
38      * The extension must not be altered in this function. This check should provide a guarantee
39      * that the provided extension is safe to use for the image to be saved.
40      */
41     public function imageExtensionSupported(string $extension): bool
42     {
43         return in_array($extension, static::$supportedExtensions);
44     }
45
46     /**
47      * Get an image with the given id.
48      */
49     public function getById($id): Image
50     {
51         return $this->image->findOrFail($id);
52     }
53
54     /**
55      * Execute a paginated query, returning in a standard format.
56      * Also runs the query through the restriction system.
57      */
58     private function returnPaginated($query, $page = 1, $pageSize = 24): array
59     {
60         $images = $query->orderBy('created_at', 'desc')->skip($pageSize * ($page - 1))->take($pageSize + 1)->get();
61         $hasMore = count($images) > $pageSize;
62
63         $returnImages = $images->take($pageSize);
64         $returnImages->each(function ($image) {
65             $this->loadThumbs($image);
66         });
67
68         return [
69             'images'   => $returnImages,
70             'has_more' => $hasMore,
71         ];
72     }
73
74     /**
75      * Fetch a list of images in a paginated format, filtered by image type.
76      * Can be filtered by uploaded to and also by name.
77      */
78     public function getPaginatedByType(
79         string $type,
80         int $page = 0,
81         int $pageSize = 24,
82         int $uploadedTo = null,
83         string $search = null,
84         callable $whereClause = null
85     ): array {
86         $imageQuery = $this->image->newQuery()->where('type', '=', strtolower($type));
87
88         if ($uploadedTo !== null) {
89             $imageQuery = $imageQuery->where('uploaded_to', '=', $uploadedTo);
90         }
91
92         if ($search !== null) {
93             $imageQuery = $imageQuery->where('name', 'LIKE', '%' . $search . '%');
94         }
95
96         // Filter by page access
97         $imageQuery = $this->restrictionService->filterRelatedEntity(Page::class, $imageQuery, 'images', 'uploaded_to');
98
99         if ($whereClause !== null) {
100             $imageQuery = $imageQuery->where($whereClause);
101         }
102
103         return $this->returnPaginated($imageQuery, $page, $pageSize);
104     }
105
106     /**
107      * Get paginated gallery images within a specific page or book.
108      */
109     public function getEntityFiltered(
110         string $type,
111         string $filterType = null,
112         int $page = 0,
113         int $pageSize = 24,
114         int $uploadedTo = null,
115         string $search = null
116     ): array {
117         $contextPage = $this->page->findOrFail($uploadedTo);
118         $parentFilter = null;
119
120         if ($filterType === 'book' || $filterType === 'page') {
121             $parentFilter = function (Builder $query) use ($filterType, $contextPage) {
122                 if ($filterType === 'page') {
123                     $query->where('uploaded_to', '=', $contextPage->id);
124                 } elseif ($filterType === 'book') {
125                     $validPageIds = $contextPage->book->pages()->visible()->get(['id'])->pluck('id')->toArray();
126                     $query->whereIn('uploaded_to', $validPageIds);
127                 }
128             };
129         }
130
131         return $this->getPaginatedByType($type, $page, $pageSize, null, $search, $parentFilter);
132     }
133
134     /**
135      * Save a new image into storage and return the new image.
136      *
137      * @throws ImageUploadException
138      */
139     public function saveNew(UploadedFile $uploadFile, string $type, int $uploadedTo = 0, int $resizeWidth = null, int $resizeHeight = null, bool $keepRatio = true): Image
140     {
141         $image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo, $resizeWidth, $resizeHeight, $keepRatio);
142         $this->loadThumbs($image);
143
144         return $image;
145     }
146
147     /**
148      * Save a new image from an existing image data string.
149      *
150      * @throws ImageUploadException
151      */
152     public function saveNewFromData(string $imageName, string $imageData, string $type, int $uploadedTo = 0)
153     {
154         $image = $this->imageService->saveNew($imageName, $imageData, $type, $uploadedTo);
155         $this->loadThumbs($image);
156
157         return $image;
158     }
159
160     /**
161      * Save a drawing the the database.
162      *
163      * @throws ImageUploadException
164      */
165     public function saveDrawing(string $base64Uri, int $uploadedTo): Image
166     {
167         $name = 'Drawing-' . strval(user()->id) . '-' . strval(time()) . '.png';
168
169         return $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawio', $uploadedTo);
170     }
171
172     /**
173      * Update the details of an image via an array of properties.
174      *
175      * @throws ImageUploadException
176      * @throws Exception
177      */
178     public function updateImageDetails(Image $image, $updateDetails): Image
179     {
180         $image->fill($updateDetails);
181         $image->save();
182         $this->loadThumbs($image);
183
184         return $image;
185     }
186
187     /**
188      * Destroys an Image object along with its revisions, files and thumbnails.
189      *
190      * @throws Exception
191      */
192     public function destroyImage(Image $image = null): bool
193     {
194         if ($image) {
195             $this->imageService->destroy($image);
196         }
197
198         return true;
199     }
200
201     /**
202      * Destroy all images of a certain type.
203      *
204      * @throws Exception
205      */
206     public function destroyByType(string $imageType)
207     {
208         $images = $this->image->where('type', '=', $imageType)->get();
209         foreach ($images as $image) {
210             $this->destroyImage($image);
211         }
212     }
213
214     /**
215      * Load thumbnails onto an image object.
216      *
217      * @throws Exception
218      */
219     public function loadThumbs(Image $image)
220     {
221         $image->thumbs = [
222             'gallery' => $this->getThumbnail($image, 150, 150, false),
223             'display' => $this->getThumbnail($image, 1680, null, true),
224         ];
225     }
226
227     /**
228      * Get the thumbnail for an image.
229      * If $keepRatio is true only the width will be used.
230      * Checks the cache then storage to avoid creating / accessing the filesystem on every check.
231      *
232      * @throws Exception
233      */
234     protected function getThumbnail(Image $image, ?int $width = 220, ?int $height = 220, bool $keepRatio = false): ?string
235     {
236         try {
237             return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
238         } catch (Exception $exception) {
239             return null;
240         }
241     }
242
243     /**
244      * Get the raw image data from an Image.
245      */
246     public function getImageData(Image $image): ?string
247     {
248         try {
249             return $this->imageService->getImageData($image);
250         } catch (Exception $exception) {
251             return null;
252         }
253     }
254
255     /**
256      * Get the user visible pages using the given image.
257      */
258     public function getPagesUsingImage(Image $image): array
259     {
260         $pages = Page::visible()
261             ->where('html', 'like', '%' . $image->url . '%')
262             ->get(['id', 'name', 'slug', 'book_id']);
263
264         foreach ($pages as $page) {
265             $page->url = $page->getUrl();
266         }
267
268         return $pages->all();
269     }
270 }