]> BookStack Code Mirror - bookstack/blob - app/Uploads/AttachmentService.php
Prevented possible XSS via link attachments
[bookstack] / app / Uploads / AttachmentService.php
1 <?php namespace BookStack\Uploads;
2
3 use BookStack\Exceptions\FileUploadException;
4 use Exception;
5 use Illuminate\Support\Str;
6 use Symfony\Component\HttpFoundation\File\UploadedFile;
7
8 class AttachmentService extends UploadService
9 {
10
11     /**
12      * Get the storage that will be used for storing files.
13      * @return \Illuminate\Contracts\Filesystem\Filesystem
14      */
15     protected function getStorage()
16     {
17         $storageType = config('filesystems.attachments');
18
19         // Override default location if set to local public to ensure not visible.
20         if ($storageType === 'local') {
21             $storageType = 'local_secure';
22         }
23
24         return $this->fileSystem->disk($storageType);
25     }
26
27     /**
28      * Get an attachment from storage.
29      * @param Attachment $attachment
30      * @return string
31      * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
32      */
33     public function getAttachmentFromStorage(Attachment $attachment)
34     {
35         return $this->getStorage()->get($attachment->path);
36     }
37
38     /**
39      * Store a new attachment upon user upload.
40      * @param UploadedFile $uploadedFile
41      * @param int $page_id
42      * @return Attachment
43      * @throws FileUploadException
44      */
45     public function saveNewUpload(UploadedFile $uploadedFile, $page_id)
46     {
47         $attachmentName = $uploadedFile->getClientOriginalName();
48         $attachmentPath = $this->putFileInStorage($uploadedFile);
49         $largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
50
51         $attachment = Attachment::forceCreate([
52             'name' => $attachmentName,
53             'path' => $attachmentPath,
54             'extension' => $uploadedFile->getClientOriginalExtension(),
55             'uploaded_to' => $page_id,
56             'created_by' => user()->id,
57             'updated_by' => user()->id,
58             'order' => $largestExistingOrder + 1
59         ]);
60
61         return $attachment;
62     }
63
64     /**
65      * Store a upload, saving to a file and deleting any existing uploads
66      * attached to that file.
67      * @param UploadedFile $uploadedFile
68      * @param Attachment $attachment
69      * @return Attachment
70      * @throws FileUploadException
71      */
72     public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment)
73     {
74         if (!$attachment->external) {
75             $this->deleteFileInStorage($attachment);
76         }
77
78         $attachmentName = $uploadedFile->getClientOriginalName();
79         $attachmentPath = $this->putFileInStorage($uploadedFile);
80
81         $attachment->name = $attachmentName;
82         $attachment->path = $attachmentPath;
83         $attachment->external = false;
84         $attachment->extension = $uploadedFile->getClientOriginalExtension();
85         $attachment->save();
86         return $attachment;
87     }
88
89     /**
90      * Save a new File attachment from a given link and name.
91      */
92     public function saveNewFromLink(string $name, string $link, int $page_id): Attachment
93     {
94         $largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
95         return Attachment::forceCreate([
96             'name' => $name,
97             'path' => $link,
98             'external' => true,
99             'extension' => '',
100             'uploaded_to' => $page_id,
101             'created_by' => user()->id,
102             'updated_by' => user()->id,
103             'order' => $largestExistingOrder + 1
104         ]);
105     }
106
107     /**
108      * Updates the ordering for a listing of attached files.
109      */
110     public function updateFileOrderWithinPage(array $attachmentOrder, string $pageId)
111     {
112         foreach ($attachmentOrder as $index => $attachmentId) {
113             Attachment::query()->where('uploaded_to', '=', $pageId)
114                 ->where('id', '=', $attachmentId)
115                 ->update(['order' => $index]);
116         }
117     }
118
119
120     /**
121      * Update the details of a file.
122      */
123     public function updateFile(Attachment $attachment, array $requestData): Attachment
124     {
125         $attachment->name = $requestData['name'];
126
127         if (isset($requestData['link']) && trim($requestData['link']) !== '') {
128             $attachment->path = $requestData['link'];
129             if (!$attachment->external) {
130                 $this->deleteFileInStorage($attachment);
131                 $attachment->external = true;
132             }
133         }
134
135         $attachment->save();
136         return $attachment;
137     }
138
139     /**
140      * Delete a File from the database and storage.
141      * @param Attachment $attachment
142      * @throws Exception
143      */
144     public function deleteFile(Attachment $attachment)
145     {
146         if ($attachment->external) {
147             $attachment->delete();
148             return;
149         }
150         
151         $this->deleteFileInStorage($attachment);
152         $attachment->delete();
153     }
154
155     /**
156      * Delete a file from the filesystem it sits on.
157      * Cleans any empty leftover folders.
158      * @param Attachment $attachment
159      */
160     protected function deleteFileInStorage(Attachment $attachment)
161     {
162         $storage = $this->getStorage();
163         $dirPath = dirname($attachment->path);
164
165         $storage->delete($attachment->path);
166         if (count($storage->allFiles($dirPath)) === 0) {
167             $storage->deleteDirectory($dirPath);
168         }
169     }
170
171     /**
172      * Store a file in storage with the given filename
173      * @param UploadedFile $uploadedFile
174      * @return string
175      * @throws FileUploadException
176      */
177     protected function putFileInStorage(UploadedFile $uploadedFile)
178     {
179         $attachmentData = file_get_contents($uploadedFile->getRealPath());
180
181         $storage = $this->getStorage();
182         $basePath = 'uploads/files/' . Date('Y-m-M') . '/';
183
184         $uploadFileName = Str::random(16) . '.' . $uploadedFile->getClientOriginalExtension();
185         while ($storage->exists($basePath . $uploadFileName)) {
186             $uploadFileName = Str::random(3) . $uploadFileName;
187         }
188
189         $attachmentPath = $basePath . $uploadFileName;
190         try {
191             $storage->put($attachmentPath, $attachmentData);
192         } catch (Exception $e) {
193             throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentPath]));
194         }
195
196         return $attachmentPath;
197     }
198 }