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/CustomerPortalController.php +183 -76 1.10.3 → 2.4.5 View file →
@@ -2,40 +2,35 @@
2 2
3 3 namespace FluentSupport\App\Http\Controllers;
4 4
5 5 use Exception;
6 +use FluentSupport\App\Hooks\Handlers\ReCaptchaHandler;
7 +use FluentSupport\App\Http\Policies\PortalPolicy;
6 8 use FluentSupport\App\Http\Requests\TicketResponseRequest;
7 9 use FluentSupport\App\Models\Product;
8 10 use FluentSupport\App\Models\Ticket;
9 -use FluentSupport\App\Models\Conversation;
10 11 use FluentSupport\App\Services\CustomerPortalService;
11 12 use FluentSupport\App\Services\Helper;
12 -use FluentSupport\Framework\Request\Request;
13 +use FluentSupport\App\Services\Tickets\AgentTicketAccess;
14 +use FluentSupport\Framework\Http\Request\Request;
15 +use FluentSupport\Framework\Support\Arr;
13 16
14 17 /**
15 - * CustomerPortalController class for REST API
16 - * This class is responsible for getting data for all request related to customer and customer portal
17 - * @package FluentSupport\App\Http\Controllers
18 - *
19 - * @version 1.0.0
18 + * Customer portal endpoints.
20 19 */
21 20 class CustomerPortalController extends Controller
22 21 {
23 22 /**
24 - * getTickets will generate ticket information with customer and agents by customer
25 23 * @param Request $request
26 - * @param CustomerPortalService $customerPortalService
27 - * @return array
28 - * @throws Exception
24 + * @return array|\WP_REST_Response
29 25 */
30 - public function getTickets(Request $request, CustomerPortalService $customerPortalService)
26 + public function getTickets(Request $request)
31 27 {
32 28
33 - $onBehalf = $request->getSafe('on_behalf', 'sanitize_text_field');
29 + $onBehalf = $this->sanitizeOnBehalf($request);
30 +
34 31 $userIP = $request->getIp();
35 -
36 32 $requestedStatus = $request->getSafe('filter_type', 'sanitize_text_field');
37 -
38 33 $ticketOptions = $request->getSafe([
39 34 'search' => 'sanitize_text_field',
40 35 'filters.product_id' => 'intval',
41 36 'sorting.sort_type' => 'sanitize_sql_orderby',
@@ -41,29 +36,102 @@
41 36 'sorting.sort_type' => 'sanitize_sql_orderby',
42 37 'sorting.sort_by' => 'sanitize_sql_orderby'
43 38 ]);
44 39
45 - try {
46 - $customer = $customerPortalService->resolveCustomer($onBehalf, $userIP);
40 + $customer = (new CustomerPortalService)->resolveCustomer($onBehalf, $userIP);
41 +
42 + if (!$customer) {
47 43 return [
48 - 'tickets' => $customerPortalService->getTickets($customer, $requestedStatus, $ticketOptions)
44 + 'tickets' => [
45 + 'data' => [],
46 + 'current_page' => $request->getSafe('page', 'intval', 1),
47 + 'last_page' => 1,
48 + 'per_page' => $request->getSafe('per_page', 'intval', 10),
49 + 'total' => 0,
50 + 'from' => null,
51 + 'to' => null
52 + ]
49 53 ];
50 - } catch (Exception $e) {
54 + }
55 +
56 + if (!$customer->canAccessPortal()) {
51 57 return $this->sendError([
52 - 'message' => $e->getMessage(),
53 - 'error_type' => $e->getCode()
54 - ]);
58 + 'message' => __('Your account is not active. Please contact support.', 'fluent-support'),
59 + 'error_type' => '403'
60 + ], 403);
55 61 }
62 +
63 + $canAccess = apply_filters('fluent_support/can_customer_access_portal', true, $customer);
64 +
65 + if (is_wp_error($canAccess)) {
66 + return $this->sendError([
67 + 'message' => $canAccess->get_error_message(),
68 + 'error_type' => $canAccess->get_error_code()
69 + ], 403);
70 + }
71 +
72 + $statuses = [
73 + 'open' => ['new', 'active', 'on-hold'],
74 + 'all' => [],
75 + 'closed' => ['closed']
76 + ];
77 +
78 + $statusFilter = $statuses[$requestedStatus] ?? $statuses['all'];
79 +
80 + $sortBy = Arr::get($ticketOptions, 'sorting.sort_by', 'created_at');
81 + $sortType = Arr::get($ticketOptions, 'sorting.sort_type', 'desc');
82 +
83 + $ticketsQuery = Ticket::where('customer_id', $customer->id)
84 + ->filterByStatues($statusFilter)
85 + ->searchBy(Arr::get($ticketOptions, 'search'))
86 + ->filterByProductId(Arr::get($ticketOptions, 'filters.product_id'))
87 + ->orderBy($sortBy, $sortType);
88 +
89 + // An agent acting for a customer only sees the tickets they could open themselves.
90 + // Site admins have full access and may not have an agent record at all.
91 + if ($onBehalf && !current_user_can('manage_options')) {
92 + (new AgentTicketAccess())->applyAccessScope($ticketsQuery);
93 + }
94 +
95 + do_action_ref_array('fluent_support/customer_portal/tickets_query', [&$ticketsQuery, $customer, $request]);
96 +
97 + $tickets = $ticketsQuery->paginate($request->getInt('per_page', 10));
98 +
99 + foreach ($tickets as $ticket) {
100 + $ticket->human_date = sprintf(__('%s ago', 'fluent-support'), human_time_diff(strtotime($ticket->created_at), current_time('timestamp')));
101 + $ticket->preview_response = $ticket->getLastResponse();
102 + }
103 +
104 + return [
105 + 'tickets' => CustomerPortalService::hidePrivateTicketFields($tickets)
106 + ];
56 107 }
57 108
58 109 /**
59 - * createTicket method will create ticket submitted by customers
60 110 * @param Request $request
61 - * @return array
62 - * @throws \FluentSupport\Framework\Validator\ValidationException
111 + * @return array | \WP_REST_Response
63 112 */
64 - public function createTicket(Request $request, CustomerPortalService $customerPortalService)
113 + public function createTicket(Request $request)
65 114 {
115 + // A remote portal filing for a customer can't answer a captcha. That takes a user
116 + // allowed to act on_behalf (an admin, or an agent who manages all tickets), checked
117 + // here, never a flag the client sends. Customers in the browser keep the captcha.
118 + $isOnBehalfClient = $this->sanitizeOnBehalf($request) && PortalPolicy::canActOnBehalf();
119 +
120 + if (!$isOnBehalfClient && ReCaptchaHandler::isRecaptchaApplicable('ticket_form')) {
121 + $captchaResponse = $request->getSafe('g-recaptcha-response', 'sanitize_text_field');
122 + $isValidCaptcha = $captchaResponse && ReCaptchaHandler::validateRecaptcha(
123 + $captchaResponse, null, null, 'create_ticket'
124 + );
125 +
126 + if (!$isValidCaptcha) {
127 + return $this->sendError([
128 + 'message' => __('Your recaptcha is not verified', 'fluent-support'),
129 + 'error_type' => '422'
130 + ], 422);
131 + }
132 + }
133 +
66 134 $dataRules = $this->app->applyCustomFilters('custom_field_required_before_ticket_create', [
67 135 'required_fields' => [
68 136 'title' => 'required',
69 137 'content' => 'required'
@@ -84,24 +152,27 @@
84 152 }
85 153 }
86 154
87 155 $defaultData = [
88 - 'ticket_title' => $request->get('title'),
89 - 'ticket_content' => $request->get('content')
156 + 'ticket_title' => $request->getSafe('title', 'sanitize_text_field'),
157 + 'ticket_content' => $request->getSafe('content', 'wp_kses_post')
90 158 ];
91 159
92 160 if ($request->has('product_id')) {
93 - $defaultData['ticket_product_id'] = $request->get('product_id');
161 + $defaultData['ticket_product_id'] = $request->getSafe('product_id', 'intval');
94 162 }
95 163
96 164 if ($request->has('client_priority')) {
97 - $defaultData['ticket_client_priority'] = $request->get('client_priority');
165 + $defaultData['ticket_client_priority'] = $request->getSafe('client_priority', 'sanitize_text_field');
98 166 }
99 167
168 + $customData = $request->get('custom_data', []);
169 + $customData = is_array($customData) ? map_deep($customData, 'sanitize_text_field') : [];
170 +
100 171 $dataRules = $this->app->applyCustomFilters('custom_field_required_by_conditions_before_ticket_create', [
101 172 'required_fields' => $dataRules['required_fields'],
102 173 'error_messages' => $dataRules['error_messages'],
103 - 'custom_data' => $request->get('custom_data', []),
174 + 'custom_data' => $customData,
104 175 'default_data' => $defaultData
105 176 ]);
106 177
107 178 if (!isset($dataRules['required_fields']) && !isset($dataRules['error_messages'])) {
@@ -114,41 +185,69 @@
114 185 $data = $this->validate($request->get(), $dataRules['required_fields'], $dataRules['error_messages']);
115 186
116 187 $data['title'] = sanitize_text_field($data['title']);
117 188 $data['content'] = wp_kses_post($data['content']);
189 + $data['custom_data'] = $customData;
118 190
119 - $onBehalf = $request->getSafe('on_behalf', 'sanitize_text_field');
191 + $onBehalf = $this->sanitizeOnBehalf($request);
120 192 $userIP = $request->getIp();
121 193
194 + if (!empty($onBehalf['last_ip_address'])) {
195 + $userIP = $onBehalf['last_ip_address'];
196 + }
197 +
198 + // An agent acting for a customer may only file tickets into mailboxes they can
199 + // access. Checked before the customer is resolved, which may create one.
200 + if ($onBehalf && !current_user_can('manage_options')) {
201 + $mailboxId = (new CustomerPortalService())->resolveMailboxId($request->getSafe('mailbox_id', 'intval'));
202 +
203 + if ($mailboxId && in_array((int) $mailboxId, (new AgentTicketAccess())->getRestrictedMailboxIds(), true)) {
204 + return $this->sendError([
205 + 'message' => __('You do not have access to this mailbox.', 'fluent-support'),
206 + 'error_type' => '403'
207 + ], 403);
208 + }
209 + }
210 +
122 211 try {
123 - $customer = $customerPortalService->resolveCustomer($onBehalf, $userIP, true);
212 + $customer = (new CustomerPortalService())->resolveCustomer($onBehalf, $userIP, true);
124 213
214 + if (!$customer) {
215 + return $this->sendError([
216 + 'message' => __('Unable to identify. Please make sure you have provided correct information.', 'fluent-support'),
217 + 'error_type' => '403'
218 + ], 403);
219 + }
220 +
125 221 $canCreateTicket = apply_filters('fluent_support/can_customer_create_ticket', true, $customer, $data);
126 222
127 223 if (!$canCreateTicket || is_wp_error($canCreateTicket)) {
128 224 $isWpError = is_wp_error($canCreateTicket);
129 225
130 - $message = ($isWpError) ? $canCreateTicket->get_error_message() : __('Sorry you can not create ticket', 'fluent-support');
226 + $message = ($isWpError) ? $canCreateTicket->get_error_message() : __('Sorry you cannot create ticket', 'fluent-support');
131 227 $errorCode = ($isWpError) ? $canCreateTicket->get_error_code() : 'general_error';
132 228
133 -
134 - throw new \Exception($message, $errorCode);
229 + return $this->sendError([
230 + 'message' => $message,
231 + 'error_type' => $errorCode
232 + ]);
135 233 }
136 234
137 - if ($customer && $messageId = Helper::generateMessageID($customer->email)) {
235 + if ($messageId = Helper::generateMessageID($customer->email)) {
138 236 $data['message_id'] = $messageId;
139 237 }
140 238
141 239 $defaultMailbox = Helper::getDefaultMailBox();
142 - $ticket = $customerPortalService->createTicket($customer, $data, $request->getSafe('mailbox_id', 'intval', $defaultMailbox->id));
240 + $defaultMailboxId = $defaultMailbox ? $defaultMailbox->id : null;
241 + $ticket = (new CustomerPortalService())->createTicket($customer, $data, $request->getSafe('mailbox_id', 'intval', $defaultMailboxId), (bool) $onBehalf);
143 242
144 243 return [
145 244 'message' => __('Ticket has been created successfully', 'fluent-support'),
146 - 'ticket' => $ticket
245 + 'ticket' => CustomerPortalService::hidePrivateTicketFields($ticket)
147 246 ];
148 247 } catch (\Exception $e) {
149 248 return $this->sendError([
150 - 'message' => $e->getMessage(),
249 + 'message' => Helper::getSafeErrorMessage($e),
151 250 'error_type' => $e->getCode()
152 251 ]);
153 252 }
154 253 }
@@ -153,22 +252,21 @@
153 252 }
154 253 }
155 254
156 255 /**
157 - * getTicket method will get the ticket information with customer and agent as well as response in a ticket by ticket id
158 256 * @param Request $request
159 - * @param $ticket_id
160 - * @return array
257 + * @param int $ticket_id
258 + * @return array|\WP_REST_Response
161 259 */
162 - public function getTicket(Request $request, CustomerPortalService $customerPortalService, $ticket_id)
260 + public function getTicket(Request $request, $ticket_id)
163 261 {
164 262 $customerAdditionalData = $this->getCustomerAdditionalData($request);
165 263
166 264 try {
167 - return $customerPortalService->getTicket($customerAdditionalData, $ticket_id);
168 - } catch (Exception $e) {
265 + return (new CustomerPortalService())->getTicket($customerAdditionalData, $ticket_id);
266 + } catch (\Exception $e) {
169 267 return $this->sendError([
170 - 'message' => $e->getMessage(),
268 + 'message' => Helper::getSafeErrorMessage($e),
171 269 'error_type' => $e->getCode()
172 270 ]);
173 271 }
174 272 }
@@ -173,20 +271,19 @@
173 271 }
174 272 }
175 273
176 274 /**
177 - * createResponse method will create response by customer in a ticket by ticket id
178 - * @param Request $request
179 - * @param $ticket_id
275 + * @param TicketResponseRequest $request
276 + * @param int $ticket_id
180 277 * @return array|\WP_REST_Response
181 278 * @throws \FluentSupport\Framework\Validator\ValidationException
182 279 */
183 - public function createResponse(TicketResponseRequest $request, CustomerPortalService $customerPortalService, $ticket_id)
280 + public function createResponse(TicketResponseRequest $request, $ticket_id)
184 281 {
185 282
186 283 $customerAdditionalData = $this->getCustomerAdditionalData($request);
187 284
188 - $ticket = Ticket::findOrFail($ticket_id);
285 + $ticket = Ticket::wherePublicIdentifier($ticket_id)->firstOrFail();
189 286
190 287 $data = $request->sanitize();
191 288
192 289 $canCreateResponse = apply_filters('fluent_support/can_customer_create_response', true, $ticket->customer, $ticket, $data);
@@ -193,17 +290,17 @@
193 290
194 291 if (!$canCreateResponse || is_wp_error($canCreateResponse)) {
195 292 return [
196 293 'type' => 'error',
197 - 'message' => (is_wp_error($canCreateResponse)) ? $canCreateResponse->get_error_message() : __('Sorry you can not create response', 'fluent-support')
294 + 'message' => (is_wp_error($canCreateResponse)) ? $canCreateResponse->get_error_message() : __('Sorry you cannot create response', 'fluent-support')
198 295 ];
199 296 }
200 297
201 298 try {
202 - return $customerPortalService->createResponse($customerAdditionalData, $ticket_id, $data);
203 - } catch (Exception $e) {
299 + return (new CustomerPortalService())->createResponse($customerAdditionalData, $ticket_id, $data);
300 + } catch (\Exception $e) {
204 301 return $this->sendError([
205 - 'message' => $e->getMessage(),
302 + 'message' => Helper::getSafeErrorMessage($e),
206 303 'error_type' => $e->getCode()
207 304 ]);
208 305 }
209 306 }
@@ -208,22 +305,21 @@
208 305 }
209 306 }
210 307
211 308 /**
212 - * This `closeTicket` is responsible for closing ticket by ticket id
213 309 * @param Request $request
214 - * @param $ticket_id
310 + * @param int $ticket_id
215 311 * @return array
216 312 */
217 - public function closeTicket(Request $request, CustomerPortalService $customerPortalService, $ticket_id)
313 + public function closeTicket(Request $request, $ticket_id)
218 314 {
219 315 $customerAdditionalData = $this->getCustomerAdditionalData($request);
220 316
221 317 try {
222 - return $customerPortalService->closeTicket($customerAdditionalData, $ticket_id);
318 + return (new CustomerPortalService())->closeTicket($customerAdditionalData, $ticket_id);
223 319 } catch (Exception $e) {
224 320 return $this->sendError([
225 - 'message' => $e->getMessage(),
321 + 'message' => Helper::getSafeErrorMessage($e),
226 322 'error_type' => $e->getCode()
227 323 ]);
228 324 }
229 325 }
@@ -228,31 +324,33 @@
228 324 }
229 325 }
230 326
231 327 /**
232 - * closeTicket method will re-open a ticket by customer using ticket id
233 328 * @param Request $request
234 - * @param $ticket_id
329 + * @param int $ticket_id
235 330 * @return array
236 331 */
237 - public function reOpenTicket(Request $request, CustomerPortalService $customerPortalService, $ticket_id)
332 + public function reOpenTicket(Request $request, $ticket_id)
238 333 {
239 334 $customerAdditionalData = $this->getCustomerAdditionalData($request);
240 335
241 336 try {
242 - return $customerPortalService->reOpenTicket($customerAdditionalData, $ticket_id);
337 + return (new CustomerPortalService())->reOpenTicket($customerAdditionalData, $ticket_id);
243 338 } catch (Exception $e) {
244 339 return $this->sendError([
245 - 'message' => $e->getMessage(),
340 + 'message' => Helper::getSafeErrorMessage($e),
246 341 'error_type' => $e->getCode()
247 342 ]);
248 343 }
249 344 }
250 345
251 - public function agentFeedbackRating(Request $request, CustomerPortalService $customerPortalService, $ticketId)
346 + public function agentFeedbackRating(Request $request, $ticketId)
252 347 {
253 - // just for validation
254 - $ticket = Ticket::with(['customer'])->findOrFail($ticketId);
348 +
349 + $customerPortalService = new CustomerPortalService();
350 +
351 + // Throws unless the customer can access this ticket.
352 + $ticket = Ticket::with(['customer'])->wherePublicIdentifier($ticketId)->firstOrFail();
255 353 $customerAdditionalData = $this->getCustomerAdditionalData($request);
256 354 $customer = $customerPortalService->getCustomer($customerAdditionalData, $ticket);
257 355 $customerPortalService->checkCustomerTicketAccess($customer, $ticket, 'feedback');
258 356
@@ -259,12 +357,12 @@
259 357 $conversationID = $request->getSafe('conversation_id', 'intval');
260 358 $approvalStatus = $request->getSafe('approval_status', 'sanitize_text_field');
261 359
262 360 try {
263 - return $customerPortalService->addUserFeedback($approvalStatus, $conversationID);
361 + return $customerPortalService->addUserFeedback($approvalStatus, $conversationID, $ticket->id);
264 362 } catch (Exception $e) {
265 363 return $this->sendError([
266 - 'message' => $e->getMessage(),
364 + 'message' => Helper::getSafeErrorMessage($e),
267 365 'error_type' => $e->getCode()
268 366 ]);
269 367 }
270 368 }
@@ -269,14 +367,15 @@
269 367 }
270 368 }
271 369
272 370 /**
273 - * getPublicOptions method will return the list of product and customer priorities
371 + * Products and customer ticket priorities.
372 + *
274 373 * @return array
275 374 */
276 375 public function getPublicOptions()
277 376 {
278 - $products = Product::select(['id', 'title'])->get();
377 + $products = Product::select(['id', 'title'])->orderedByTitle()->get();
279 378
280 379 return [
281 380 'support_products' => $products,
282 381 'customer_ticket_priorities' => Helper::customerTicketPriorities()
@@ -283,9 +382,8 @@
283 382 ];
284 383 }
285 384
286 385 /**
287 - * getCustomFieldsRender method will return the list of custom fields
288 386 * @return array|array[]
289 387 */
290 388 public function getCustomFieldsRender()
291 389 {
@@ -301,9 +399,8 @@
301 399 }
302 400
303 401
304 402 /**
305 - * logout method will logout the customer
306 403 * @return mixed
307 404 */
308 405 public function logout()
309 406 {
@@ -315,13 +412,23 @@
315 412 }
316 413
317 414 private function getCustomerAdditionalData($request)
318 415 {
416 +
319 417 $customerAdditionalData = [
320 418 'intended_ticket_hash' => $request->getSafe('intended_ticket_hash', 'sanitize_text_field'),
321 - 'on_behalf' => $request->getSafe('on_behalf', 'sanitize_text_field'),
419 + 'on_behalf' => $this->sanitizeOnBehalf($request),
322 420 'user_ip' => $request->getIp()
323 421 ];
324 422
325 423 return $customerAdditionalData;
424 + }
425 +
426 + /**
427 + * @param Request $request
428 + * @return array
429 + */
430 + private function sanitizeOnBehalf($request)
431 + {
432 + return (new CustomerPortalService())->sanitizeOnBehalf($request->get('on_behalf', []));
326 433 }
327 434 }