PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / trunk
Fluent Support – Helpdesk & Customer Support Ticket System vtrunk
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
fluent-support / app / Hooks / Handlers / CustomerPortalHandler.php

CustomerPortalHandler.php in Fluent Support – Helpdesk & Customer Support Ticket System trunk, at app/Hooks/Handlers/CustomerPortalHandler.php

377 lines 15.1 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\Hooks\Handlers;
4
5 use FluentSupport\App\App;
6 use FluentSupport\App\Models\Customer;
7 use FluentSupport\App\Models\Product;
8 use FluentSupport\App\Modules\PermissionManager;
9 use FluentSupport\App\Services\Blocks\BlockHelper;
10 use FluentSupport\App\Services\EmailClaimService;
11 use FluentSupport\App\Services\Helper;
12 use FluentSupport\App\Services\TranslationStrings;
13 use FluentSupport\App\Vite;
14 use FluentSupport\Framework\Support\Arr;
15 use FluentSupportPro\App\Services\ProHelper;
16
17 class CustomerPortalHandler
18 {
19 public function renderPortal($args = [])
20 {
21 /**
22 * This hook filter customer portal access permission error message.
23 * If a customer has no access to the portal, then the message will be displayed.
24 * @param string $invalidPermissionMessage
25 * @return string
26 * @since 1.6.0
27 */
28 $invalidPermissionMessage = apply_filters(
29 'fluent_support/customer_portal_invalid_permission_message',
30 esc_html__('You don\'t have permission to access customer support portal', 'fluent-support')
31 );
32
33 $person = Helper::getCurrentCustomer();
34
35 if (!$person && PermissionManager::currentUserPermissions()) {
36 $adminPortalUrl = Helper::getPortalAdminBaseUrl();
37
38 /**
39 * This hook filter is responsible for generating error message
40 * when a support staff try to access customer portal
41 * @param string $agentPermissionErrMessage
42 * @return string
43 * @since 1.6.0
44 */
45 $msg = __('Customer Portal is only accessible by Customers. Looks like you are a support staff', 'fluent-support');
46 $agentPermissionErrMessage = apply_filters(
47 'fluent_support/customer_portal_agent_permission_error_message',
48 $msg
49 );
50 return '<div style="text-align: center;"><h3>' . esc_html($agentPermissionErrMessage) . '</h3><a href="' . esc_url($adminPortalUrl) . '">' . esc_html__('Go to Support Admin Page', 'fluent-support') . '</a></div>';
51 } else if ($this->hasCustomerPortalAccess()) {
52
53 /*
54 * Filter customer portal access settings
55 *
56 * @since v1.0.0
57 *
58 * @param array $canAccess
59 */
60 $canAccess = apply_filters('fluent_support/user_portal_access_config', [
61 'status' => true,
62 'message' => $invalidPermissionMessage
63 ]);
64
65 if (empty($canAccess['status'])) {
66 $invalidPermissionMessage = Arr::get($canAccess, 'message', $invalidPermissionMessage);
67 return '<div id="fluent_support_client_app" style="text-align: center;"><h3 class="fs_customer_restriction">' . esc_html($invalidPermissionMessage) . '</h3></div>';
68 }
69
70 if (!$person) {
71 $this->maybeCreateCustomer();
72 }
73
74 if (isset($args['attributes']) && !empty($args['attributes'])) {
75 BlockHelper::processAttributesAndPrepareStyle($args['attributes']);
76 }
77
78 $this->enqueueScripts();
79
80 return $this->renderEmailClaimNotice($person)
81 . '<div id="fluent_support_client_app"><h3 class="fs_loading_text">' . __('Loading Customer Portal. Please wait...', 'fluent-support') . '</h3></div>';
82 } else {
83
84 $businessSettings = Helper::getBusinessSettings();
85 $loggedInMessage = Arr::get($businessSettings, 'login_message', '');
86
87 $loggedInMessage = str_replace('[fluent_support_portal]', '', $loggedInMessage);
88
89 // Pass portal's show-signup / show-reset-password to auth/login shortcodes
90 // by temporarily overriding defaults via the existing filter.
91 $overrideDefaults = function ($defaults) use ($args) {
92 $defaults['show-signup'] = Arr::get($args, 'show-signup', 'true');
93 $defaults['show-reset-password'] = Arr::get($args, 'show-reset-password', 'true');
94 return $defaults;
95 };
96
97 $loggedInMessage = wp_kses_post($loggedInMessage);
98
99 add_filter('fluent_support/auth_shortcode_defaults', $overrideDefaults);
100 $result = do_shortcode($loggedInMessage);
101
102 remove_filter('fluent_support/auth_shortcode_defaults', $overrideDefaults);
103
104 return $result;
105 }
106 }
107
108 /**
109 * Notice shown above the portal when the customer's support address no
110 * longer matches the address on their WordPress account.
111 *
112 * Rendered server side, outside the element the portal app mounts into, so
113 * it survives the Vue app taking over and needs no asset build.
114 *
115 * @param \FluentSupport\App\Models\Customer|null $customer Already resolved by the caller
116 *
117 * The notice deliberately says nothing about what is waiting on the other
118 * address. Somebody who points their account at an address they do not own
119 * sees this same notice, and a ticket count would tell them whether that
120 * person is a customer here.
121 *
122 * @return string
123 */
124 protected function renderEmailClaimNotice($customer = null)
125 {
126 $html = $this->renderEmailClaimResult();
127
128 // The caller already resolved this record; handing it over keeps the
129 // check free of an extra query on every portal render.
130 $divergence = EmailClaimService::getDivergence($customer);
131
132 if (!$divergence) {
133 return $html;
134 }
135
136 $requestUrl = EmailClaimService::buildRequestUrl();
137
138 if (!$requestUrl) {
139 return $html;
140 }
141
142 $message = sprintf(
143 // translators: 1: address support mail currently goes to, 2: the account's current address
144 __('Support messages are being sent to %1$s, but your account email is now %2$s.', 'fluent-support'),
145 '<strong>' . esc_html($divergence['from']) . '</strong>',
146 '<strong>' . esc_html($divergence['to']) . '</strong>'
147 );
148
149 return $html
150 . '<div class="fs_email_claim_notice" style="border: 1px solid #dcdcde; border-left: 4px solid #2271b1; background: #fff; padding: 12px 16px; margin-bottom: 16px;">'
151 . '<p style="margin: 0 0 8px;">' . wp_kses($message, ['strong' => []]) . '</p>'
152 . '<p style="margin: 0 0 12px; opacity: .8;">' . esc_html__('Confirm the new address to move your support messages and bring across any tickets you opened from it. We will email a link to that address.', 'fluent-support') . '</p>'
153 . '<a class="fs_email_claim_button" style="display: inline-block; background: #2271b1; color: #fff; text-decoration: none; padding: 8px 20px; border-radius: 3px;" href="' . esc_url($requestUrl) . '">'
154 . esc_html__('Send confirmation email', 'fluent-support')
155 . '</a></div>';
156 }
157
158 /**
159 * @return string
160 */
161 protected function renderEmailClaimResult()
162 {
163 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- selects a fixed message to display; performs no action
164 $result = isset($_GET['fs_claim_result']) ? sanitize_key(wp_unslash($_GET['fs_claim_result'])) : '';
165
166 if (!$result) {
167 return '';
168 }
169
170 $user = wp_get_current_user();
171 $accountEmail = $user ? $user->user_email : '';
172
173 $messages = [
174 'sent' => sprintf(
175 // translators: %s is the email address the confirmation was sent to
176 __('Confirmation email sent to %s. Open the link in that inbox to finish.', 'fluent-support'),
177 $accountEmail
178 ),
179 'confirmed' => __('Your support email address has been updated.', 'fluent-support'),
180 'throttled' => __('Too many confirmation emails have been requested for this address. Please try again later.', 'fluent-support'),
181 'expired' => __('That confirmation link has expired. You can request a new one below.', 'fluent-support'),
182 'stale' => __('That confirmation link is no longer valid, usually because it was already used or the address changed again. You can request a new one below.', 'fluent-support'),
183 'wrong_account' => __('That confirmation link belongs to a different account. Sign in as that account and open the link again.', 'fluent-support'),
184 'conflict' => __('Another support profile already uses that address, so it cannot be moved automatically. Please contact support.', 'fluent-support'),
185 'invalid' => __('That confirmation link could not be read. You can request a new one below.', 'fluent-support'),
186 'send_failed' => __('We could not send the confirmation email just now. Please try again in a few minutes.', 'fluent-support')
187 ];
188
189 if (empty($messages[$result])) {
190 return '';
191 }
192
193 $isGood = in_array($result, ['sent', 'confirmed'], true);
194
195 return '<div class="fs_email_claim_result" style="border: 1px solid #dcdcde; border-left: 4px solid ' . ($isGood ? '#00a32a' : '#d63638') . '; background: #fff; padding: 12px 16px; margin-bottom: 16px;">'
196 . '<p style="margin: 0;">' . esc_html($messages[$result]) . '</p></div>';
197 }
198
199 public function hasCustomerPortalAccess()
200 {
201 $userId = get_current_user_id();
202
203 if ($userId) {
204 return true;
205 }
206
207 return $this->isSignedTicketView();
208 }
209
210 protected function isSignedTicketView()
211 {
212 if (!Helper::isPublicSignedTicketEnabled()) {
213 return false;
214 }
215
216 return isset($_REQUEST['fs_view']) && $_REQUEST['fs_view'] == 'ticket' && isset($_REQUEST['support_hash']) && isset($_REQUEST['ticket_id']);
217 }
218
219 private function maybeCreateCustomer()
220 {
221 $userId = get_current_user_id();
222 if (!$userId) {
223 return false;
224 }
225
226 $person = Helper::getCurrentPerson();
227 if ($person) {
228 return $person;
229 }
230
231 $user = get_user_by('ID', $userId);
232
233 $request = App::request();
234
235 $onBehalf = [
236 'user_id' => $user->ID,
237 'email' => $user->user_email,
238 'last_ip_address' => $request->getIp()
239 ];
240
241 $customFields = Helper::getBusinessSettings('custom_registration_form_field');
242
243 if (!empty($customFields)) {
244 $onBehalf = $this->processCustomFields($customFields, $onBehalf);
245 }
246
247 return Customer::maybeCreateCustomer($onBehalf);
248 }
249
250 private function processCustomFields($customFields, $onBehalf)
251 {
252 $userMeta = get_user_meta(get_current_user_id());
253 $customData = [];
254
255 foreach ($customFields as $field) {
256 if (isset($userMeta[$field])) {
257 $customData[$field] = is_array($userMeta[$field]) ? $userMeta[$field][0] : $userMeta[$field];
258 }
259 }
260
261 if ($customData) {
262 $onBehalf = array_merge($onBehalf, $customData);
263 }
264
265 return $onBehalf;
266 }
267
268 public function enqueueScripts()
269 {
270 $app = App::getInstance();
271
272 $ns = $app->config->get('app.rest_namespace');
273 $v = $app->config->get('app.rest_version');
274 $slug = $app->config->get('app.slug');
275
276 $restInfo = [
277 'base_url' => esc_url_raw(rest_url()),
278 'url' => rest_url($ns . '/' . $v . '/customer-portal'),
279 'nonce' => wp_create_nonce('wp_rest'),
280 'namespace' => $ns,
281 'version' => $v,
282 ];
283
284 $assets = $app['url.assets'];
285
286
287 $i18ns = TranslationStrings::getPortalStrings();
288
289 $i18ns['allowed_files_and_size'] = Helper::getFileUploadMessage();
290
291 $data = [
292 'rest' => $restInfo,
293 'nonce' => wp_create_nonce($slug),
294 'ticket_statuses' => Helper::ticketStatuses(),
295 'support_products' => Product::select(['id', 'title'])->orderedByTitle()->get(),
296 'product_field_required' => Helper::isProductRequired(),
297 'customer_ticket_priorities' => Helper::customerTicketPriorities(),
298 'view_tickets_url' => '#/',
299 'i18n' => $i18ns,
300 'fallback_image' => $assets . 'images/icons/file.svg',
301 'has_file_upload' => !!Helper::ticketAcceptedFileMiles(),
302 'has_rich_text_editor' => true,
303 'customer_status' => static::customerStatus()->status ?? static::customerStatus(),
304 'max_file_upload' => Helper::getBusinessSettings('max_file_upload', 3),
305 'agent_feedback_rating' => Helper::getBusinessSettings('agent_feedback_rating', 'no'),
306 'can_view_private_ticket_number' => current_user_can('manage_options') || PermissionManager::currentUserCan([
307 'fst_view_tickets',
308 'fst_manage_own_tickets',
309 'fst_manage_unassigned_tickets',
310 'fst_manage_other_tickets'
311 ]),
312 ];
313
314 if ($this->isSignedTicketView()) {
315 $data['intended_ticket_hash'] = sanitize_text_field($_REQUEST['support_hash']);
316 $data['view_tickets_url'] = Helper::getPortalBaseUrl() . '/#';
317 } else {
318 add_filter('user_can_richedit', '__return_true');
319 }
320
321 $reCaptchaSettings = ReCaptchaHandler::getSettings();
322 $data['recaptcha'] = ['enabled' => false];
323
324 if (ReCaptchaHandler::isRecaptchaApplicable('ticket_form', $reCaptchaSettings)) {
325 $recaptchaVersion = $reCaptchaSettings['reCaptcha_version'] ?? 'recaptcha_v2';
326 $siteKey = $reCaptchaSettings['siteKey'] ?? '';
327
328 $data['recaptcha'] = [
329 'enabled' => true,
330 'version' => $recaptchaVersion,
331 'site_key' => $siteKey,
332 ];
333 }
334
335 /*
336 * Filter customer portal localize javascript data
337 *
338 * @since v1.0.0
339 *
340 * @param array $data
341 */
342 $data = apply_filters('fluent_support/customer_portal_vars', $data);
343
344 if (!empty($data['has_rich_text_editor'])) {
345 wp_tinymce_inline_scripts();
346 wp_enqueue_editor();
347 }
348
349 // Inject Vite HMR client for dev mode
350 add_action('wp_head', function () {
351 Vite::injectViteClient();
352 }, 1);
353
354 wp_enqueue_script('dompurify', $assets . 'libs/purify/purify.min.js', [], '3.4.13');
355 wp_enqueue_script('fs_tk_customer_portal', Vite::getEnqueuePath('portal/js/app.js'), ['jquery'], FLUENT_SUPPORT_VERSION, true);
356
357 if (is_rtl()) {
358 wp_enqueue_style('fs_tk_customer_portal_rtl', $assets . 'portal/css/app-rtl.css', [], FLUENT_SUPPORT_VERSION);
359 } else {
360 wp_enqueue_style('fs_tk_customer_portal', Vite::getEnqueuePath('portal/css/app.css'), [], FLUENT_SUPPORT_VERSION);
361 }
362
363 wp_localize_script('fs_tk_customer_portal', 'fs_customer_portal', $data);
364 }
365
366 protected static function customerStatus()
367 {
368 $user = get_current_user_id();
369
370 if (!$user && isset($_REQUEST['support_hash']) && isset($_REQUEST['ticket_id']) && isset($_REQUEST['fs_view']) && $_REQUEST['fs_view'] == 'ticket') {
371 return true;
372 }
373
374 return Customer::where('user_id', $user)->select(['status'])->first();
375 }
376 }
377