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
fluent-support / app / Http / Controllers / CustomerPortalController.php

CustomerPortalController.php in Fluent Support – Helpdesk & Customer Support Ticket System 2.4.5, at app/Http/Controllers/CustomerPortalController.php

435 lines 15.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentSupport\App\Http\Controllers;
4
5 use Exception;
6 use FluentSupport\App\Hooks\Handlers\ReCaptchaHandler;
7 use FluentSupport\App\Http\Policies\PortalPolicy;
8 use FluentSupport\App\Http\Requests\TicketResponseRequest;
9 use FluentSupport\App\Models\Product;
10 use FluentSupport\App\Models\Ticket;
11 use FluentSupport\App\Services\CustomerPortalService;
12 use FluentSupport\App\Services\Helper;
13 use FluentSupport\App\Services\Tickets\AgentTicketAccess;
14 use FluentSupport\Framework\Http\Request\Request;
15 use FluentSupport\Framework\Support\Arr;
16
17 /**
18 * Customer portal endpoints.
19 */
20 class CustomerPortalController extends Controller
21 {
22 /**
23 * @param Request $request
24 * @return array|\WP_REST_Response
25 */
26 public function getTickets(Request $request)
27 {
28
29 $onBehalf = $this->sanitizeOnBehalf($request);
30
31 $userIP = $request->getIp();
32 $requestedStatus = $request->getSafe('filter_type', 'sanitize_text_field');
33 $ticketOptions = $request->getSafe([
34 'search' => 'sanitize_text_field',
35 'filters.product_id' => 'intval',
36 'sorting.sort_type' => 'sanitize_sql_orderby',
37 'sorting.sort_by' => 'sanitize_sql_orderby'
38 ]);
39
40 $customer = (new CustomerPortalService)->resolveCustomer($onBehalf, $userIP);
41
42 if (!$customer) {
43 return [
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 ]
53 ];
54 }
55
56 if (!$customer->canAccessPortal()) {
57 return $this->sendError([
58 'message' => __('Your account is not active. Please contact support.', 'fluent-support'),
59 'error_type' => '403'
60 ], 403);
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 ];
107 }
108
109 /**
110 * @param Request $request
111 * @return array | \WP_REST_Response
112 */
113 public function createTicket(Request $request)
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
134 $dataRules = $this->app->applyCustomFilters('custom_field_required_before_ticket_create', [
135 'required_fields' => [
136 'title' => 'required',
137 'content' => 'required'
138 ],
139 'error_messages' => [
140 'title.required' => __('Title is required', 'fluent-support'),
141 'content.required' => __('Content is required', 'fluent-support')
142 ]
143 ]);
144
145 $settings = Helper::getOption('_ticket_form_settings', []);
146
147 if (!empty($settings['product_required_field']) && $settings['product_required_field'] === 'yes') {
148 $productCount = Product::count();
149 if ($productCount > 0) {
150 $dataRules['required_fields']['product_id'] = 'required';
151 $dataRules['error_messages']['product_id.required'] = __('Product is required', 'fluent-support');
152 }
153 }
154
155 $defaultData = [
156 'ticket_title' => $request->getSafe('title', 'sanitize_text_field'),
157 'ticket_content' => $request->getSafe('content', 'wp_kses_post')
158 ];
159
160 if ($request->has('product_id')) {
161 $defaultData['ticket_product_id'] = $request->getSafe('product_id', 'intval');
162 }
163
164 if ($request->has('client_priority')) {
165 $defaultData['ticket_client_priority'] = $request->getSafe('client_priority', 'sanitize_text_field');
166 }
167
168 $customData = $request->get('custom_data', []);
169 $customData = is_array($customData) ? map_deep($customData, 'sanitize_text_field') : [];
170
171 $dataRules = $this->app->applyCustomFilters('custom_field_required_by_conditions_before_ticket_create', [
172 'required_fields' => $dataRules['required_fields'],
173 'error_messages' => $dataRules['error_messages'],
174 'custom_data' => $customData,
175 'default_data' => $defaultData
176 ]);
177
178 if (!isset($dataRules['required_fields']) && !isset($dataRules['error_messages'])) {
179 return $this->sendError([
180 'message' => __('Invalid form data submitted', 'fluent-support'),
181 'error_type' => '400'
182 ], 400);
183 }
184
185 $data = $this->validate($request->get(), $dataRules['required_fields'], $dataRules['error_messages']);
186
187 $data['title'] = sanitize_text_field($data['title']);
188 $data['content'] = wp_kses_post($data['content']);
189 $data['custom_data'] = $customData;
190
191 $onBehalf = $this->sanitizeOnBehalf($request);
192 $userIP = $request->getIp();
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
211 try {
212 $customer = (new CustomerPortalService())->resolveCustomer($onBehalf, $userIP, true);
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
221 $canCreateTicket = apply_filters('fluent_support/can_customer_create_ticket', true, $customer, $data);
222
223 if (!$canCreateTicket || is_wp_error($canCreateTicket)) {
224 $isWpError = is_wp_error($canCreateTicket);
225
226 $message = ($isWpError) ? $canCreateTicket->get_error_message() : __('Sorry you cannot create ticket', 'fluent-support');
227 $errorCode = ($isWpError) ? $canCreateTicket->get_error_code() : 'general_error';
228
229 return $this->sendError([
230 'message' => $message,
231 'error_type' => $errorCode
232 ]);
233 }
234
235 if ($messageId = Helper::generateMessageID($customer->email)) {
236 $data['message_id'] = $messageId;
237 }
238
239 $defaultMailbox = Helper::getDefaultMailBox();
240 $defaultMailboxId = $defaultMailbox ? $defaultMailbox->id : null;
241 $ticket = (new CustomerPortalService())->createTicket($customer, $data, $request->getSafe('mailbox_id', 'intval', $defaultMailboxId), (bool) $onBehalf);
242
243 return [
244 'message' => __('Ticket has been created successfully', 'fluent-support'),
245 'ticket' => CustomerPortalService::hidePrivateTicketFields($ticket)
246 ];
247 } catch (\Exception $e) {
248 return $this->sendError([
249 'message' => Helper::getSafeErrorMessage($e),
250 'error_type' => $e->getCode()
251 ]);
252 }
253 }
254
255 /**
256 * @param Request $request
257 * @param int $ticket_id
258 * @return array|\WP_REST_Response
259 */
260 public function getTicket(Request $request, $ticket_id)
261 {
262 $customerAdditionalData = $this->getCustomerAdditionalData($request);
263
264 try {
265 return (new CustomerPortalService())->getTicket($customerAdditionalData, $ticket_id);
266 } catch (\Exception $e) {
267 return $this->sendError([
268 'message' => Helper::getSafeErrorMessage($e),
269 'error_type' => $e->getCode()
270 ]);
271 }
272 }
273
274 /**
275 * @param TicketResponseRequest $request
276 * @param int $ticket_id
277 * @return array|\WP_REST_Response
278 * @throws \FluentSupport\Framework\Validator\ValidationException
279 */
280 public function createResponse(TicketResponseRequest $request, $ticket_id)
281 {
282
283 $customerAdditionalData = $this->getCustomerAdditionalData($request);
284
285 $ticket = Ticket::wherePublicIdentifier($ticket_id)->firstOrFail();
286
287 $data = $request->sanitize();
288
289 $canCreateResponse = apply_filters('fluent_support/can_customer_create_response', true, $ticket->customer, $ticket, $data);
290
291 if (!$canCreateResponse || is_wp_error($canCreateResponse)) {
292 return [
293 'type' => 'error',
294 'message' => (is_wp_error($canCreateResponse)) ? $canCreateResponse->get_error_message() : __('Sorry you cannot create response', 'fluent-support')
295 ];
296 }
297
298 try {
299 return (new CustomerPortalService())->createResponse($customerAdditionalData, $ticket_id, $data);
300 } catch (\Exception $e) {
301 return $this->sendError([
302 'message' => Helper::getSafeErrorMessage($e),
303 'error_type' => $e->getCode()
304 ]);
305 }
306 }
307
308 /**
309 * @param Request $request
310 * @param int $ticket_id
311 * @return array
312 */
313 public function closeTicket(Request $request, $ticket_id)
314 {
315 $customerAdditionalData = $this->getCustomerAdditionalData($request);
316
317 try {
318 return (new CustomerPortalService())->closeTicket($customerAdditionalData, $ticket_id);
319 } catch (Exception $e) {
320 return $this->sendError([
321 'message' => Helper::getSafeErrorMessage($e),
322 'error_type' => $e->getCode()
323 ]);
324 }
325 }
326
327 /**
328 * @param Request $request
329 * @param int $ticket_id
330 * @return array
331 */
332 public function reOpenTicket(Request $request, $ticket_id)
333 {
334 $customerAdditionalData = $this->getCustomerAdditionalData($request);
335
336 try {
337 return (new CustomerPortalService())->reOpenTicket($customerAdditionalData, $ticket_id);
338 } catch (Exception $e) {
339 return $this->sendError([
340 'message' => Helper::getSafeErrorMessage($e),
341 'error_type' => $e->getCode()
342 ]);
343 }
344 }
345
346 public function agentFeedbackRating(Request $request, $ticketId)
347 {
348
349 $customerPortalService = new CustomerPortalService();
350
351 // Throws unless the customer can access this ticket.
352 $ticket = Ticket::with(['customer'])->wherePublicIdentifier($ticketId)->firstOrFail();
353 $customerAdditionalData = $this->getCustomerAdditionalData($request);
354 $customer = $customerPortalService->getCustomer($customerAdditionalData, $ticket);
355 $customerPortalService->checkCustomerTicketAccess($customer, $ticket, 'feedback');
356
357 $conversationID = $request->getSafe('conversation_id', 'intval');
358 $approvalStatus = $request->getSafe('approval_status', 'sanitize_text_field');
359
360 try {
361 return $customerPortalService->addUserFeedback($approvalStatus, $conversationID, $ticket->id);
362 } catch (Exception $e) {
363 return $this->sendError([
364 'message' => Helper::getSafeErrorMessage($e),
365 'error_type' => $e->getCode()
366 ]);
367 }
368 }
369
370 /**
371 * Products and customer ticket priorities.
372 *
373 * @return array
374 */
375 public function getPublicOptions()
376 {
377 $products = Product::select(['id', 'title'])->orderedByTitle()->get();
378
379 return [
380 'support_products' => $products,
381 'customer_ticket_priorities' => Helper::customerTicketPriorities()
382 ];
383 }
384
385 /**
386 * @return array|array[]
387 */
388 public function getCustomFieldsRender()
389 {
390 if (!defined('FLUENTSUPPORTPRO')) {
391 return [
392 'custom_fields_rendered' => []
393 ];
394 }
395
396 return [
397 'custom_fields_rendered' => \FluentSupportPro\App\Services\CustomFieldsService::getRenderedPublicFields()
398 ];
399 }
400
401
402 /**
403 * @return mixed
404 */
405 public function logout()
406 {
407 wp_logout();
408
409 return $this->sendSuccess([
410 'message' => __('You have been logged out', 'fluent-support')
411 ]);
412 }
413
414 private function getCustomerAdditionalData($request)
415 {
416
417 $customerAdditionalData = [
418 'intended_ticket_hash' => $request->getSafe('intended_ticket_hash', 'sanitize_text_field'),
419 'on_behalf' => $this->sanitizeOnBehalf($request),
420 'user_ip' => $request->getIp()
421 ];
422
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', []));
433 }
434 }
435