]> BookStack Code Mirror - bookstack/blob - app/Http/Controllers/Controller.php
Updated attachment download responses to stream from filesystem
[bookstack] / app / Http / Controllers / Controller.php
1 <?php
2
3 namespace BookStack\Http\Controllers;
4
5 use BookStack\Exceptions\NotifyException;
6 use BookStack\Facades\Activity;
7 use BookStack\Interfaces\Loggable;
8 use BookStack\Model;
9 use BookStack\Util\WebSafeMimeSniffer;
10 use Illuminate\Foundation\Bus\DispatchesJobs;
11 use Illuminate\Foundation\Validation\ValidatesRequests;
12 use Illuminate\Http\JsonResponse;
13 use Illuminate\Http\Response;
14 use Illuminate\Routing\Controller as BaseController;
15 use Symfony\Component\HttpFoundation\StreamedResponse;
16
17 abstract class Controller extends BaseController
18 {
19     use DispatchesJobs;
20     use ValidatesRequests;
21
22     /**
23      * Check if the current user is signed in.
24      */
25     protected function isSignedIn(): bool
26     {
27         return auth()->check();
28     }
29
30     /**
31      * Stops the application and shows a permission error if
32      * the application is in demo mode.
33      */
34     protected function preventAccessInDemoMode()
35     {
36         if (config('app.env') === 'demo') {
37             $this->showPermissionError();
38         }
39     }
40
41     /**
42      * Adds the page title into the view.
43      */
44     public function setPageTitle(string $title)
45     {
46         view()->share('pageTitle', $title);
47     }
48
49     /**
50      * On a permission error redirect to home and display.
51      * the error as a notification.
52      *
53      * @return never
54      */
55     protected function showPermissionError()
56     {
57         $message = request()->wantsJson() ? trans('errors.permissionJson') : trans('errors.permission');
58
59         throw new NotifyException($message, '/', 403);
60     }
61
62     /**
63      * Checks that the current user has the given permission otherwise throw an exception.
64      */
65     protected function checkPermission(string $permission): void
66     {
67         if (!user() || !user()->can($permission)) {
68             $this->showPermissionError();
69         }
70     }
71
72     /**
73      * Check the current user's permissions against an ownable item otherwise throw an exception.
74      */
75     protected function checkOwnablePermission(string $permission, Model $ownable): void
76     {
77         if (!userCan($permission, $ownable)) {
78             $this->showPermissionError();
79         }
80     }
81
82     /**
83      * Check if a user has a permission or bypass the permission
84      * check if the given callback resolves true.
85      */
86     protected function checkPermissionOr(string $permission, callable $callback): void
87     {
88         if ($callback() !== true) {
89             $this->checkPermission($permission);
90         }
91     }
92
93     /**
94      * Check if the current user has a permission or bypass if the provided user
95      * id matches the current user.
96      */
97     protected function checkPermissionOrCurrentUser(string $permission, int $userId): void
98     {
99         $this->checkPermissionOr($permission, function () use ($userId) {
100             return $userId === user()->id;
101         });
102     }
103
104     /**
105      * Send back a json error message.
106      */
107     protected function jsonError(string $messageText = '', int $statusCode = 500): JsonResponse
108     {
109         return response()->json(['message' => $messageText, 'status' => 'error'], $statusCode);
110     }
111
112     /**
113      * Create a response that forces a download in the browser.
114      */
115     protected function downloadResponse(string $content, string $fileName): Response
116     {
117         return response()->make($content, 200, [
118             'Content-Type'           => 'application/octet-stream',
119             'Content-Disposition'    => 'attachment; filename="' . $fileName . '"',
120             'X-Content-Type-Options' => 'nosniff',
121         ]);
122     }
123
124     /**
125      * Create a response that forces a download, from a given stream of content.
126      */
127     protected function streamedDownloadResponse($stream, string $fileName): StreamedResponse
128     {
129         return response()->stream(function() use ($stream) {
130             fpassthru($stream);
131             fclose($stream);
132         }, 200, [
133             'Content-Type'           => 'application/octet-stream',
134             'Content-Disposition'    => 'attachment; filename="' . $fileName . '"',
135             'X-Content-Type-Options' => 'nosniff',
136         ]);
137     }
138
139     /**
140      * Create a file download response that provides the file with a content-type
141      * correct for the file, in a way so the browser can show the content in browser.
142      */
143     protected function inlineDownloadResponse(string $content, string $fileName): Response
144     {
145         $mime = (new WebSafeMimeSniffer())->sniff($content);
146
147         return response()->make($content, 200, [
148             'Content-Type'           => $mime,
149             'Content-Disposition'    => 'inline; filename="' . $fileName . '"',
150             'X-Content-Type-Options' => 'nosniff',
151         ]);
152     }
153
154     /**
155      * Create a file download response that provides the file with a content-type
156      * correct for the file, in a way so the browser can show the content in browser,
157      * for a given content stream.
158      */
159     protected function streamedInlineDownloadResponse($stream, string $fileName): StreamedResponse
160     {
161         $sniffContent = fread($stream, 1000);
162         $mime = (new WebSafeMimeSniffer())->sniff($sniffContent);
163
164         return response()->stream(function() use ($sniffContent, $stream) {
165            echo $sniffContent;
166            fpassthru($stream);
167            fclose($stream);
168         }, 200, [
169             'Content-Type'           => $mime,
170             'Content-Disposition'    => 'inline; filename="' . $fileName . '"',
171             'X-Content-Type-Options' => 'nosniff',
172         ]);
173     }
174
175     /**
176      * Show a positive, successful notification to the user on next view load.
177      */
178     protected function showSuccessNotification(string $message): void
179     {
180         session()->flash('success', $message);
181     }
182
183     /**
184      * Show a warning notification to the user on next view load.
185      */
186     protected function showWarningNotification(string $message): void
187     {
188         session()->flash('warning', $message);
189     }
190
191     /**
192      * Show an error notification to the user on next view load.
193      */
194     protected function showErrorNotification(string $message): void
195     {
196         session()->flash('error', $message);
197     }
198
199     /**
200      * Log an activity in the system.
201      *
202      * @param string|Loggable $detail
203      */
204     protected function logActivity(string $type, $detail = ''): void
205     {
206         Activity::add($type, $detail);
207     }
208
209     /**
210      * Get the validation rules for image files.
211      */
212     protected function getImageValidationRules(): array
213     {
214         return ['image_extension', 'mimes:jpeg,png,gif,webp', 'max:' . (config('app.upload_limit') * 1000)];
215     }
216 }