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