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