PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.5
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.5
2.4.5 2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 All 69 releases
← All changes | app/Http/Controllers/UploaderController.php +147 -5 2.3.0 → 2.4.5 View file →
@@ -3,8 +3,10 @@
3 3 namespace FluentSupport\App\Http\Controllers;
4 4
5 5 use FluentSupport\App\Models\Attachment;
6 6 use FluentSupport\App\Models\Ticket;
7 +use FluentSupport\App\Modules\PermissionManager;
8 +use FluentSupport\App\Services\CustomerPortalService;
7 9 use FluentSupport\App\Services\EmailNotification\Settings;
8 10 use FluentSupport\App\Services\Helper;
9 11 use FluentSupport\Framework\Http\Request\Request;
10 12 use FluentSupport\App\Services\Includes\UploadService;
@@ -17,8 +19,15 @@
17 19 */
18 20 class UploaderController extends Controller
19 21 {
20 22 /**
23 + * Uploads sent with on_behalf belong to that customer, and their ticket_id is the
24 + * public ticket number. Remote portals check for this constant to know which
25 + * ticket reference to send. Don't remove it.
26 + */
27 + const ON_BEHALF_UPLOADS = true;
28 +
29 + /**
21 30 * uploadTicketFiles method will upload all the attached file in a ticket
22 31 * @param Request $request
23 32 * @return array[]
24 33 * @throws \FluentSupport\Framework\Validator\ValidationException
@@ -26,20 +35,50 @@
26 35 public function uploadTicketFiles(Request $request)
27 36 {
28 37 $settings = (new Settings())->globalBusinessSettings();
29 38 $maxFileSize = floatval($settings['max_file_size']);
39 + $maxFileUpload = intval($settings['max_file_upload']);
30 40 $mimeHeadings = Helper::getAcceptedMimeHeadings();
31 41 $maxSizeBytes = $maxFileSize * 1024;
32 42 $imageType = $request->type ? $request->type : null;
33 43
34 - $this->validateUploadedFiles($request->files(), $maxSizeBytes, $mimeHeadings, $maxFileSize);
35 - $ticketId = $this->resolveTicketId($request);
36 - $person = $this->resolvePerson($ticketId, $request);
44 + $files = $request->files();
37 45
38 - $this->checkPermissionToUploadFile($person);
46 + if ($partsError = $this->rejectUnexpectedFileParts($files)) {
47 + return $partsError;
48 + }
39 49
50 + $onBehalf = (new CustomerPortalService())->sanitizeOnBehalf($request->get('on_behalf', []));
51 +
52 + if ($onBehalf) {
53 + try {
54 + list($ticketId, $person) = $this->resolveOnBehalfUploader($onBehalf, $request);
55 + } catch (\Exception $e) {
56 + return $this->sendError([
57 + 'message' => Helper::getSafeErrorMessage($e),
58 + ]);
59 + }
60 + } else {
61 + $ticketId = $this->resolveTicketId($request);
62 + $person = $this->resolvePerson($ticketId, $request);
63 + }
64 +
65 + if ($permissionError = $this->checkPermissionToUploadFile($person)) {
66 + return $permissionError;
67 + }
68 +
69 + if ($accessError = $this->checkTicketAccess($ticketId)) {
70 + return $accessError;
71 + }
72 +
73 + if ($quotaError = $this->checkAttachmentQuota($files, $person, $ticketId, $maxFileUpload)) {
74 + return $quotaError;
75 + }
76 +
77 + $this->validateUploadedFiles($files, $maxSizeBytes, $mimeHeadings, $maxFileSize);
78 +
40 79 try {
41 - $uploadedFiles = UploadService::handleTempFileUpload($request->files());
80 + $uploadedFiles = UploadService::handleTempFileUpload($files);
42 81 } catch (\Exception $e) {
43 82 return $this->sendError([
44 83 'message' => Helper::getSafeErrorMessage($e),
45 84 ]);
@@ -57,8 +96,73 @@
57 96 'attachments' => $attachmentHashes,
58 97 ];
59 98 }
60 99
100 + /**
101 + * Only the "file" multipart part is validated and processed downstream
102 + * (UploadService/FileSystem::put() loops every top-level part it is given), so
103 + * any other part name must be rejected here rather than silently passed through.
104 + */
105 + private function rejectUnexpectedFileParts($files)
106 + {
107 + $files = (array) $files;
108 + $unexpectedKeys = array_diff(array_keys($files), ['file']);
109 +
110 + if ($unexpectedKeys || empty($files['file'])) {
111 + return $this->sendError([
112 + 'message' => __('Invalid file upload request.', 'fluent-support'),
113 + ]);
114 + }
115 +
116 + return null;
117 + }
118 +
119 + /**
120 + * resolveTicketId() passes an agent's ticket_id through unchecked, so authorize it
121 + * before anything is written. No ticket id is legitimate — the Add Ticket form
122 + * uploads before the ticket exists.
123 + */
124 + private function checkTicketAccess($ticketId)
125 + {
126 + if (!$ticketId || !Helper::getCurrentAgent()) {
127 + return null;
128 + }
129 +
130 + $ticket = Ticket::find($ticketId);
131 +
132 + if (!$ticket || !PermissionManager::canAccessTicket($ticket)) {
133 + return $this->sendError([
134 + 'message' => __('You do not have permission to upload a file to this ticket', 'fluent-support'),
135 + ], 403);
136 + }
137 +
138 + return null;
139 + }
140 +
141 + private function checkAttachmentQuota($files, $person, $ticketId, $maxFileUpload)
142 + {
143 + if ($maxFileUpload <= 0) {
144 + return null;
145 + }
146 +
147 + $newFiles = isset($files['file']) ? $files['file'] : null;
148 + $newFilesCount = is_array($newFiles) ? count($newFiles) : 1;
149 +
150 + $existingCount = Attachment::where('person_id', $person->id)
151 + ->where('ticket_id', $ticketId)
152 + ->where('status', 'in-active')
153 + ->count();
154 +
155 + if (($existingCount + $newFilesCount) > $maxFileUpload) {
156 + return $this->sendError([
157 + // translators: %d is the maximum number of files allowed per ticket
158 + 'message' => sprintf(__('You can upload a maximum of %d files.', 'fluent-support'), $maxFileUpload),
159 + ]);
160 + }
161 +
162 + return null;
163 + }
164 +
61 165 private function validateUploadedFiles($files, $maxSizeBytes, $mimeHeadings, $maxFileSize)
62 166 {
63 167 $validationRules = [
64 168 'file' => 'max:' . $maxSizeBytes . '|mimetypes:' . implode(',', Helper::ticketAcceptedFileMiles()),
@@ -90,8 +194,42 @@
90 194
91 195 return $ticket ? $ticket->id : null;
92 196 }
93 197
198 + /**
199 + * A remote portal (PortalPolicy has allowed on_behalf) uploads as the customer it
200 + * acts for, so the file belongs to that customer and counts toward their quota only.
201 + * ticket_id is the public ticket number, as on every other portal endpoint.
202 + *
203 + * @return array [ticket id or null, customer]
204 + * @throws \Exception
205 + */
206 + private function resolveOnBehalfUploader($onBehalf, Request $request)
207 + {
208 + $portalService = new CustomerPortalService();
209 + $identifier = $request->getSafe('ticket_id', 'intval');
210 +
211 + if (!$identifier) {
212 + return [null, $portalService->resolveCustomer($onBehalf, $request->getIp(), true)];
213 + }
214 +
215 + $ticket = Ticket::wherePublicIdentifier($identifier)->first();
216 +
217 + if (!$ticket) {
218 + throw new \Exception(esc_html__('Sorry, You do not have permission to this support ticket', 'fluent-support'));
219 + }
220 +
221 + // getCustomer() also holds an agent to their own mailbox and ticket access.
222 + $customer = $portalService->getCustomer([
223 + 'on_behalf' => $onBehalf,
224 + 'user_ip' => $request->getIp(),
225 + ], $ticket);
226 +
227 + $portalService->checkCustomerTicketAccess($customer, $ticket, 'response');
228 +
229 + return [$ticket->id, $customer];
230 + }
231 +
94 232 private function resolvePerson($ticketId, Request $request)
95 233 {
96 234 $agent = Helper::getCurrentAgent();
97 235 if ($agent) {
@@ -178,8 +316,12 @@
178 316 public function uploadImage(Request $request)
179 317 {
180 318 $images = $request->files();
181 319 $ticketId = $this->resolveTicketId($request);
320 +
321 + if ($accessError = $this->checkTicketAccess($ticketId)) {
322 + return $accessError;
323 + }
182 324
183 325 $validationError = $this->isValidImageType($images);
184 326 if ($validationError) {
185 327 return $validationError;