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