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 / Hooks / Handlers / CustomerPortalHandler.php

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

366 lines 14.5 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 * 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()) {
35 $adminPortalUrl = Helper::getPortalAdminBaseUrl();
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>';
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
76 $this->enqueueScripts();
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>';
80 } else {
81
82 $businessSettings = Helper::getBusinessSettings();
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;
103 }
104 }
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
258 public function enqueueScripts()
259 {
260 $app = App::getInstance();
261
262 $ns = $app->config->get('app.rest_namespace');
263 $v = $app->config->get('app.rest_version');
264 $slug = $app->config->get('app.slug');
265
266 $restInfo = [
267 'base_url' => esc_url_raw(rest_url()),
268 'url' => rest_url($ns . '/' . $v . '/customer-portal'),
269 'nonce' => wp_create_nonce('wp_rest'),
270 'namespace' => $ns,
271 'version' => $v,
272 ];
273
274 $assets = $app['url.assets'];
275
276
277 $i18ns = TranslationStrings::getPortalStrings();
278
279 $i18ns['allowed_files_and_size'] = Helper::getFileUploadMessage();
280
281 $data = [
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(),
287 'customer_ticket_priorities' => Helper::customerTicketPriorities(),
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 ]),
302 ];
303
304 if ($this->isSignedTicketView()) {
305 $data['intended_ticket_hash'] = sanitize_text_field($_REQUEST['support_hash']);
306 $data['view_tickets_url'] = Helper::getPortalBaseUrl() . '/#';
307 } else {
308 add_filter('user_can_richedit', '__return_true');
309 }
310
311 $reCaptchaSettings = ReCaptchaHandler::getSettings();
312 $data['recaptcha'] = ['enabled' => false];
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 */
332 $data = apply_filters('fluent_support/customer_portal_vars', $data);
333
334 if (!empty($data['has_rich_text_editor'])) {
335 wp_tinymce_inline_scripts();
336 wp_enqueue_editor();
337 }
338
339 add_action('wp_head', function () {
340 Vite::injectViteClient();
341 }, 1);
342
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);
350 }
351
352 wp_localize_script('fs_tk_customer_portal', 'fs_customer_portal', $data);
353 }
354
355 protected static function customerStatus()
356 {
357 $user = get_current_user_id();
358
359 if (!$user && isset($_REQUEST['support_hash']) && isset($_REQUEST['ticket_id']) && isset($_REQUEST['fs_view']) && $_REQUEST['fs_view'] == 'ticket') {
360 return true;
361 }
362
363 return Customer::where('user_id', $user)->select(['status'])->first();
364 }
365 }
366