PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.0
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.0
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 1.5.5 All 68 releases
← All changes | app/Http/Controllers/CustomerPortalController.php +167 -55 1.10.22.4.0 View file →
@@ -2,15 +2,16 @@
2 2
3 3 namespace FluentSupport\App\Http\Controllers;
4 4
5 5 use Exception;
6 +use FluentSupport\App\Hooks\Handlers\ReCaptchaHandler;
6 7 use FluentSupport\App\Http\Requests\TicketResponseRequest;
7 8 use FluentSupport\App\Models\Product;
8 9 use FluentSupport\App\Models\Ticket;
9 -use FluentSupport\App\Models\Conversation;
10 10 use FluentSupport\App\Services\CustomerPortalService;
11 11 use FluentSupport\App\Services\Helper;
12 -use FluentSupport\Framework\Request\Request;
12 +use FluentSupport\Framework\Http\Request\Request;
13 +use FluentSupport\Framework\Support\Arr;
13 14
14 15 /**
15 16 * CustomerPortalController class for REST API
16 17 * This class is responsible for getting data for all request related to customer and customer portal
@@ -22,20 +23,17 @@
22 23 {
23 24 /**
24 25 * getTickets will generate ticket information with customer and agents by customer
25 26 * @param Request $request
26 - * @param CustomerPortalService $customerPortalService
27 - * @return array
28 - * @throws Exception
27 + * @return array|\WP_REST_Response
29 28 */
30 - public function getTickets(Request $request, CustomerPortalService $customerPortalService)
29 + public function getTickets(Request $request)
31 30 {
32 31
33 - $onBehalf = $request->getSafe('on_behalf', 'sanitize_text_field');
32 + $onBehalf = $this->sanitizeOnBehalf($request);
33 +
34 34 $userIP = $request->getIp();
35 -
36 35 $requestedStatus = $request->getSafe('filter_type', 'sanitize_text_field');
37 -
38 36 $ticketOptions = $request->getSafe([
39 37 'search' => 'sanitize_text_field',
40 38 'filters.product_id' => 'intval',
41 39 'sorting.sort_type' => 'sanitize_sql_orderby',
@@ -41,29 +39,92 @@
41 39 'sorting.sort_type' => 'sanitize_sql_orderby',
42 40 'sorting.sort_by' => 'sanitize_sql_orderby'
43 41 ]);
44 42
45 - try {
46 - $customer = $customerPortalService->resolveCustomer($onBehalf, $userIP);
43 + $customer = (new CustomerPortalService)->resolveCustomer($onBehalf, $userIP);
44 +
45 + if (!$customer) {
47 46 return [
48 - 'tickets' => $customerPortalService->getTickets($customer, $requestedStatus, $ticketOptions)
47 + 'tickets' => [
48 + 'data' => [],
49 + 'current_page' => $request->getSafe('page', 'intval', 1),
50 + 'last_page' => 1,
51 + 'per_page' => $request->getSafe('per_page', 'intval', 10),
52 + 'total' => 0,
53 + 'from' => null,
54 + 'to' => null
55 + ]
49 56 ];
50 - } catch (Exception $e) {
57 + }
58 +
59 + if (!$customer->canAccessPortal()) {
51 60 return $this->sendError([
52 - 'message' => $e->getMessage(),
53 - 'error_type' => $e->getCode()
54 - ]);
61 + 'message' => __('Your account is not active. Please contact support.', 'fluent-support'),
62 + 'error_type' => '403'
63 + ], 403);
55 64 }
65 +
66 + $canAccess = apply_filters('fluent_support/can_customer_access_portal', true, $customer);
67 +
68 + if (is_wp_error($canAccess)) {
69 + return $this->sendError([
70 + 'message' => $canAccess->get_error_message(),
71 + 'error_type' => $canAccess->get_error_code()
72 + ], 403);
73 + }
74 +
75 + $statuses = [
76 + 'open' => ['new', 'active', 'on-hold'],
77 + 'all' => [],
78 + 'closed' => ['closed']
79 + ];
80 +
81 + $statusFilter = $statuses[$requestedStatus] ?? $statuses['all'];
82 +
83 + $sortBy = Arr::get($ticketOptions, 'sorting.sort_by', 'created_at');
84 + $sortType = Arr::get($ticketOptions, 'sorting.sort_type', 'desc');
85 +
86 + $ticketsQuery = Ticket::where('customer_id', $customer->id)
87 + ->filterByStatues($statusFilter)
88 + ->searchBy(Arr::get($ticketOptions, 'search'))
89 + ->filterByProductId(Arr::get($ticketOptions, 'filters.product_id'))
90 + ->orderBy($sortBy, $sortType);
91 +
92 + do_action_ref_array('fluent_support/customer_portal/tickets_query', [&$ticketsQuery, $customer, $request]);
93 +
94 + $tickets = $ticketsQuery->paginate($request->getInt('per_page', 10));
95 +
96 + foreach ($tickets as $ticket) {
97 + $ticket->human_date = sprintf(__('%s ago', 'fluent-support'), human_time_diff(strtotime($ticket->created_at), current_time('timestamp')));
98 + $ticket->preview_response = $ticket->getLastResponse();
99 + }
100 +
101 + return [
102 + 'tickets' => $tickets
103 + ];
56 104 }
57 105
58 106 /**
59 107 * createTicket method will create ticket submitted by customers
60 108 * @param Request $request
61 - * @return array
62 - * @throws \FluentSupport\Framework\Validator\ValidationException
109 + * @return array | \WP_REST_Response
63 110 */
64 - public function createTicket(Request $request, CustomerPortalService $customerPortalService)
111 + public function createTicket(Request $request)
65 112 {
113 + if (ReCaptchaHandler::isRecaptchaApplicable('ticket_form')) {
114 + $captchaResponse = $request->getSafe('g-recaptcha-response', 'sanitize_text_field');
115 + $isValidCaptcha = $captchaResponse && ReCaptchaHandler::validateRecaptcha(
116 + $captchaResponse, null, null, 'create_ticket'
117 + );
118 +
119 + if (!$isValidCaptcha) {
120 + return $this->sendError([
121 + 'message' => __('Your recaptcha is not verified', 'fluent-support'),
122 + 'error_type' => '422'
123 + ], 422);
124 + }
125 + }
126 +
66 127 $dataRules = $this->app->applyCustomFilters('custom_field_required_before_ticket_create', [
67 128 'required_fields' => [
68 129 'title' => 'required',
69 130 'content' => 'required'
@@ -84,24 +145,27 @@
84 145 }
85 146 }
86 147
87 148 $defaultData = [
88 - 'ticket_title' => $request->get('title'),
89 - 'ticket_content' => $request->get('content')
149 + 'ticket_title' => $request->getSafe('title', 'sanitize_text_field'),
150 + 'ticket_content' => $request->getSafe('content', 'wp_kses_post')
90 151 ];
91 152
92 153 if ($request->has('product_id')) {
93 - $defaultData['ticket_product_id'] = $request->get('product_id');
154 + $defaultData['ticket_product_id'] = $request->getSafe('product_id', 'intval');
94 155 }
95 156
96 157 if ($request->has('client_priority')) {
97 - $defaultData['ticket_client_priority'] = $request->get('client_priority');
158 + $defaultData['ticket_client_priority'] = $request->getSafe('client_priority', 'sanitize_text_field');
98 159 }
99 160
161 + $customData = $request->get('custom_data', []);
162 + $customData = is_array($customData) ? map_deep($customData, 'sanitize_text_field') : [];
163 +
100 164 $dataRules = $this->app->applyCustomFilters('custom_field_required_by_conditions_before_ticket_create', [
101 165 'required_fields' => $dataRules['required_fields'],
102 166 'error_messages' => $dataRules['error_messages'],
103 - 'custom_data' => $request->get('custom_data', []),
167 + 'custom_data' => $customData,
104 168 'default_data' => $defaultData
105 169 ]);
106 170
107 171 if (!isset($dataRules['required_fields']) && !isset($dataRules['error_messages'])) {
@@ -114,33 +178,48 @@
114 178 $data = $this->validate($request->get(), $dataRules['required_fields'], $dataRules['error_messages']);
115 179
116 180 $data['title'] = sanitize_text_field($data['title']);
117 181 $data['content'] = wp_kses_post($data['content']);
182 + $data['custom_data'] = $customData;
118 183
119 - $onBehalf = $request->getSafe('on_behalf', 'sanitize_text_field');
184 + $onBehalf = $this->sanitizeOnBehalf($request);
120 185 $userIP = $request->getIp();
121 186
187 + if (!empty($onBehalf['last_ip_address'])) {
188 + $userIP = $onBehalf['last_ip_address'];
189 + }
190 +
122 191 try {
123 - $customer = $customerPortalService->resolveCustomer($onBehalf, $userIP, true);
192 + $customer = (new CustomerPortalService())->resolveCustomer($onBehalf, $userIP, true);
124 193
194 + if (!$customer) {
195 + return $this->sendError([
196 + 'message' => __('Unable to identify. Please make sure you have provided correct information.', 'fluent-support'),
197 + 'error_type' => '403'
198 + ], 403);
199 + }
200 +
125 201 $canCreateTicket = apply_filters('fluent_support/can_customer_create_ticket', true, $customer, $data);
126 202
127 203 if (!$canCreateTicket || is_wp_error($canCreateTicket)) {
128 204 $isWpError = is_wp_error($canCreateTicket);
129 205
130 - $message = ($isWpError) ? $canCreateTicket->get_error_message() : __('Sorry you can not create ticket', 'fluent-support');
206 + $message = ($isWpError) ? $canCreateTicket->get_error_message() : __('Sorry you cannot create ticket', 'fluent-support');
131 207 $errorCode = ($isWpError) ? $canCreateTicket->get_error_code() : 'general_error';
132 208
133 -
134 - throw new \Exception($message, $errorCode);
209 + return $this->sendError([
210 + 'message' => $message,
211 + 'error_type' => $errorCode
212 + ]);
135 213 }
136 214
137 - if ($customer && $messageId = Helper::generateMessageID($customer->email)) {
215 + if ($messageId = Helper::generateMessageID($customer->email)) {
138 216 $data['message_id'] = $messageId;
139 217 }
140 218
141 219 $defaultMailbox = Helper::getDefaultMailBox();
142 - $ticket = $customerPortalService->createTicket($customer, $data, $request->getSafe('mailbox_id', 'intval', $defaultMailbox->id));
220 + $defaultMailboxId = $defaultMailbox ? $defaultMailbox->id : null;
221 + $ticket = (new CustomerPortalService())->createTicket($customer, $data, $request->getSafe('mailbox_id', 'intval', $defaultMailboxId));
143 222
144 223 return [
145 224 'message' => __('Ticket has been created successfully', 'fluent-support'),
146 225 'ticket' => $ticket
@@ -146,9 +225,9 @@
146 225 'ticket' => $ticket
147 226 ];
148 227 } catch (\Exception $e) {
149 228 return $this->sendError([
150 - 'message' => $e->getMessage(),
229 + 'message' => Helper::getSafeErrorMessage($e),
151 230 'error_type' => $e->getCode()
152 231 ]);
153 232 }
154 233 }
@@ -156,19 +235,19 @@
156 235 /**
157 236 * getTicket method will get the ticket information with customer and agent as well as response in a ticket by ticket id
158 237 * @param Request $request
159 238 * @param $ticket_id
160 - * @return array
239 + * @return array|\WP_REST_Response
161 240 */
162 - public function getTicket(Request $request, CustomerPortalService $customerPortalService, $ticket_id)
241 + public function getTicket(Request $request, $ticket_id)
163 242 {
164 243 $customerAdditionalData = $this->getCustomerAdditionalData($request);
165 244
166 245 try {
167 - return $customerPortalService->getTicket($customerAdditionalData, $ticket_id);
168 - } catch (Exception $e) {
246 + return (new CustomerPortalService())->getTicket($customerAdditionalData, $ticket_id);
247 + } catch (\Exception $e) {
169 248 return $this->sendError([
170 - 'message' => $e->getMessage(),
249 + 'message' => Helper::getSafeErrorMessage($e),
171 250 'error_type' => $e->getCode()
172 251 ]);
173 252 }
174 253 }
@@ -179,14 +258,14 @@
179 258 * @param $ticket_id
180 259 * @return array|\WP_REST_Response
181 260 * @throws \FluentSupport\Framework\Validator\ValidationException
182 261 */
183 - public function createResponse(TicketResponseRequest $request, CustomerPortalService $customerPortalService, $ticket_id)
262 + public function createResponse(TicketResponseRequest $request, $ticket_id)
184 263 {
185 264
186 265 $customerAdditionalData = $this->getCustomerAdditionalData($request);
187 266
188 - $ticket = Ticket::findOrFail($ticket_id);
267 + $ticket = Ticket::wherePublicIdentifier($ticket_id)->firstOrFail();
189 268
190 269 $data = $request->sanitize();
191 270
192 271 $canCreateResponse = apply_filters('fluent_support/can_customer_create_response', true, $ticket->customer, $ticket, $data);
@@ -193,17 +272,17 @@
193 272
194 273 if (!$canCreateResponse || is_wp_error($canCreateResponse)) {
195 274 return [
196 275 'type' => 'error',
197 - 'message' => (is_wp_error($canCreateResponse)) ? $canCreateResponse->get_error_message() : __('Sorry you can not create response', 'fluent-support')
276 + 'message' => (is_wp_error($canCreateResponse)) ? $canCreateResponse->get_error_message() : __('Sorry you cannot create response', 'fluent-support')
198 277 ];
199 278 }
200 279
201 280 try {
202 - return $customerPortalService->createResponse($customerAdditionalData, $ticket_id, $data);
203 - } catch (Exception $e) {
281 + return (new CustomerPortalService())->createResponse($customerAdditionalData, $ticket_id, $data);
282 + } catch (\Exception $e) {
204 283 return $this->sendError([
205 - 'message' => $e->getMessage(),
284 + 'message' => Helper::getSafeErrorMessage($e),
206 285 'error_type' => $e->getCode()
207 286 ]);
208 287 }
209 288 }
@@ -213,17 +292,17 @@
213 292 * @param Request $request
214 293 * @param $ticket_id
215 294 * @return array
216 295 */
217 - public function closeTicket(Request $request, CustomerPortalService $customerPortalService, $ticket_id)
296 + public function closeTicket(Request $request, $ticket_id)
218 297 {
219 298 $customerAdditionalData = $this->getCustomerAdditionalData($request);
220 299
221 300 try {
222 - return $customerPortalService->closeTicket($customerAdditionalData, $ticket_id);
301 + return (new CustomerPortalService())->closeTicket($customerAdditionalData, $ticket_id);
223 302 } catch (Exception $e) {
224 303 return $this->sendError([
225 - 'message' => $e->getMessage(),
304 + 'message' => Helper::getSafeErrorMessage($e),
226 305 'error_type' => $e->getCode()
227 306 ]);
228 307 }
229 308 }
@@ -233,26 +312,29 @@
233 312 * @param Request $request
234 313 * @param $ticket_id
235 314 * @return array
236 315 */
237 - public function reOpenTicket(Request $request, CustomerPortalService $customerPortalService, $ticket_id)
316 + public function reOpenTicket(Request $request, $ticket_id)
238 317 {
239 318 $customerAdditionalData = $this->getCustomerAdditionalData($request);
240 319
241 320 try {
242 - return $customerPortalService->reOpenTicket($customerAdditionalData, $ticket_id);
321 + return (new CustomerPortalService())->reOpenTicket($customerAdditionalData, $ticket_id);
243 322 } catch (Exception $e) {
244 323 return $this->sendError([
245 - 'message' => $e->getMessage(),
324 + 'message' => Helper::getSafeErrorMessage($e),
246 325 'error_type' => $e->getCode()
247 326 ]);
248 327 }
249 328 }
250 329
251 - public function agentFeedbackRating(Request $request, CustomerPortalService $customerPortalService, $ticketId)
330 + public function agentFeedbackRating(Request $request, $ticketId)
252 331 {
332 +
333 + $customerPortalService = new CustomerPortalService();
334 +
253 335 // just for validation
254 - $ticket = Ticket::with(['customer'])->findOrFail($ticketId);
336 + $ticket = Ticket::with(['customer'])->wherePublicIdentifier($ticketId)->firstOrFail();
255 337 $customerAdditionalData = $this->getCustomerAdditionalData($request);
256 338 $customer = $customerPortalService->getCustomer($customerAdditionalData, $ticket);
257 339 $customerPortalService->checkCustomerTicketAccess($customer, $ticket, 'feedback');
258 340
@@ -259,12 +341,12 @@
259 341 $conversationID = $request->getSafe('conversation_id', 'intval');
260 342 $approvalStatus = $request->getSafe('approval_status', 'sanitize_text_field');
261 343
262 344 try {
263 - return $customerPortalService->addUserFeedback($approvalStatus, $conversationID);
345 + return $customerPortalService->addUserFeedback($approvalStatus, $conversationID, $ticket->id);
264 346 } catch (Exception $e) {
265 347 return $this->sendError([
266 - 'message' => $e->getMessage(),
348 + 'message' => Helper::getSafeErrorMessage($e),
267 349 'error_type' => $e->getCode()
268 350 ]);
269 351 }
270 352 }
@@ -274,9 +356,9 @@
274 356 * @return array
275 357 */
276 358 public function getPublicOptions()
277 359 {
278 - $products = Product::select(['id', 'title'])->get();
360 + $products = Product::select(['id', 'title'])->orderedByTitle()->get();
279 361
280 362 return [
281 363 'support_products' => $products,
282 364 'customer_ticket_priorities' => Helper::customerTicketPriorities()
@@ -315,13 +397,43 @@
315 397 }
316 398
317 399 private function getCustomerAdditionalData($request)
318 400 {
401 +
319 402 $customerAdditionalData = [
320 403 'intended_ticket_hash' => $request->getSafe('intended_ticket_hash', 'sanitize_text_field'),
321 - 'on_behalf' => $request->getSafe('on_behalf', 'sanitize_text_field'),
404 + 'on_behalf' => $this->sanitizeOnBehalf($request),
322 405 'user_ip' => $request->getIp()
323 406 ];
324 407
325 408 return $customerAdditionalData;
409 + }
410 +
411 + /**
412 + * Read the `on_behalf` identity payload from the request in a predictable shape.
413 + * Only an array of scalars is accepted; anything else (a scalar, or nested
414 + * arrays) is discarded so callers can never hand a non-string to
415 + * sanitize_text_field(). An empty result makes resolveCustomer() fall back to
416 + * the logged-in user instead of a request-supplied identity.
417 + *
418 + * @param Request $request
419 + * @return array
420 + */
421 + private function sanitizeOnBehalf($request)
422 + {
423 + $onBehalf = $request->get('on_behalf', []);
424 +
425 + if (!is_array($onBehalf)) {
426 + return [];
427 + }
428 +
429 + $sanitized = [];
430 +
431 + foreach ($onBehalf as $key => $value) {
432 + if (is_scalar($value)) {
433 + $sanitized[sanitize_text_field($key)] = sanitize_text_field($value);
434 + }
435 + }
436 +
437 + return $sanitized;
326 438 }
327 439 }