]> BookStack Code Mirror - bookstack/blob - app/Uploads/Controllers/AttachmentApiController.php
b47d6ff8dd12df5f02febb4950529b7f919e81bf
[bookstack] / app / Uploads / Controllers / AttachmentApiController.php
1 <?php
2
3 namespace BookStack\Uploads\Controllers;
4
5 use BookStack\Entities\Queries\PageQueries;
6 use BookStack\Exceptions\FileUploadException;
7 use BookStack\Http\ApiController;
8 use BookStack\Permissions\Permission;
9 use BookStack\Uploads\Attachment;
10 use BookStack\Uploads\AttachmentService;
11 use Exception;
12 use Illuminate\Contracts\Filesystem\FileNotFoundException;
13 use Illuminate\Http\Request;
14 use Illuminate\Validation\ValidationException;
15
16 class AttachmentApiController extends ApiController
17 {
18     public function __construct(
19         protected AttachmentService $attachmentService,
20         protected PageQueries $pageQueries,
21     ) {
22     }
23
24     /**
25      * Get a listing of attachments visible to the user.
26      * The external property indicates whether the attachment is simple a link.
27      * A false value for the external property would indicate a file upload.
28      */
29     public function list()
30     {
31         return $this->apiListingResponse(Attachment::visible(), [
32             'id', 'name', 'extension', 'uploaded_to', 'external', 'order', 'created_at', 'updated_at', 'created_by', 'updated_by',
33         ]);
34     }
35
36     /**
37      * Create a new attachment in the system.
38      * An uploaded_to value must be provided containing an ID of the page
39      * that this upload will be related to.
40      *
41      * If you're uploading a file the POST data should be provided via
42      * a multipart/form-data type request instead of JSON.
43      *
44      * @throws ValidationException
45      * @throws FileUploadException
46      */
47     public function create(Request $request)
48     {
49         $this->checkPermission(Permission::AttachmentCreateAll);
50         $requestData = $this->validate($request, $this->rules()['create']);
51
52         $pageId = $request->get('uploaded_to');
53         $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
54         $this->checkOwnablePermission(Permission::PageUpdate, $page);
55
56         if ($request->hasFile('file')) {
57             $uploadedFile = $request->file('file');
58             $attachment = $this->attachmentService->saveNewUpload($uploadedFile, $page->id);
59         } else {
60             $attachment = $this->attachmentService->saveNewFromLink(
61                 $requestData['name'],
62                 $requestData['link'],
63                 $page->id
64             );
65         }
66
67         $this->attachmentService->updateFile($attachment, $requestData);
68
69         return response()->json($attachment);
70     }
71
72     /**
73      * Get the details & content of a single attachment of the given ID.
74      * The attachment link or file content is provided via a 'content' property.
75      * For files the content will be base64 encoded.
76      *
77      * @throws FileNotFoundException
78      */
79     public function read(string $id)
80     {
81         /** @var Attachment $attachment */
82         $attachment = Attachment::visible()
83             ->with(['createdBy', 'updatedBy'])
84             ->findOrFail($id);
85
86         $attachment->setAttribute('links', [
87             'html'     => $attachment->htmlLink(),
88             'markdown' => $attachment->markdownLink(),
89         ]);
90
91         // Simply return a JSON response of the attachment for link-based attachments
92         if ($attachment->external) {
93             $attachment->setAttribute('content', $attachment->path);
94
95             return response()->json($attachment);
96         }
97
98         // Build and split our core JSON, at point of content.
99         $splitter = 'CONTENT_SPLIT_LOCATION_' . time() . '_' . rand(1, 40000);
100         $attachment->setAttribute('content', $splitter);
101         $json = $attachment->toJson();
102         $jsonParts = explode($splitter, $json);
103         // Get a stream for the file data from storage
104         $stream = $this->attachmentService->streamAttachmentFromStorage($attachment);
105
106         return response()->stream(function () use ($jsonParts, $stream) {
107             // Output the pre-content JSON data
108             echo $jsonParts[0];
109
110             // Stream out our attachment data as base64 content
111             stream_filter_append($stream, 'convert.base64-encode', STREAM_FILTER_READ);
112             fpassthru($stream);
113             fclose($stream);
114
115             // Output our post-content JSON data
116             echo $jsonParts[1];
117         }, 200, ['Content-Type' => 'application/json']);
118     }
119
120     /**
121      * Update the details of a single attachment.
122      * As per the create endpoint, if a file is being provided as the attachment content
123      * the request should be formatted as a multipart/form-data request instead of JSON.
124      *
125      * @throws ValidationException
126      * @throws FileUploadException
127      */
128     public function update(Request $request, string $id)
129     {
130         $requestData = $this->validate($request, $this->rules()['update']);
131         /** @var Attachment $attachment */
132         $attachment = Attachment::visible()->findOrFail($id);
133
134         $page = $attachment->page;
135         if ($requestData['uploaded_to'] ?? false) {
136             $pageId = $request->get('uploaded_to');
137             $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
138             $attachment->uploaded_to = $requestData['uploaded_to'];
139         }
140
141         $this->checkOwnablePermission(Permission::PageView, $page);
142         $this->checkOwnablePermission(Permission::PageUpdate, $page);
143         $this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment);
144
145         if ($request->hasFile('file')) {
146             $uploadedFile = $request->file('file');
147             $attachment = $this->attachmentService->saveUpdatedUpload($uploadedFile, $attachment);
148         }
149
150         $this->attachmentService->updateFile($attachment, $requestData);
151
152         return response()->json($attachment);
153     }
154
155     /**
156      * Delete an attachment of the given ID.
157      *
158      * @throws Exception
159      */
160     public function delete(string $id)
161     {
162         /** @var Attachment $attachment */
163         $attachment = Attachment::visible()->findOrFail($id);
164         $this->checkOwnablePermission(Permission::AttachmentDelete, $attachment);
165
166         $this->attachmentService->deleteFile($attachment);
167
168         return response('', 204);
169     }
170
171     protected function rules(): array
172     {
173         return [
174             'create' => [
175                 'name'        => ['required', 'string', 'min:1', 'max:255'],
176                 'uploaded_to' => ['required', 'integer', 'exists:pages,id'],
177                 'file'        => array_merge(['required_without:link'], $this->attachmentService->getFileValidationRules()),
178                 'link'        => ['required_without:file', 'string', 'min:1', 'max:2000', 'safe_url'],
179             ],
180             'update' => [
181                 'name'        => ['string', 'min:1', 'max:255'],
182                 'uploaded_to' => ['integer', 'exists:pages,id'],
183                 'file'        => $this->attachmentService->getFileValidationRules(),
184                 'link'        => ['string', 'min:1', 'max:2000', 'safe_url'],
185             ],
186         ];
187     }
188 }