PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.1.1
Fluent Support – Helpdesk & Customer Support Ticket System v2.1.1
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 1.5.6 All 67 releases
fluent-support / app / Modules / PermissionManager.php

PermissionManager.php in Fluent Support – Helpdesk & Customer Support Ticket System 2.1.1, at app/Modules/PermissionManager.php

579 lines 18.6 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\Modules;
4
5 use FluentSupport\App\Models\MailBox;
6 use FluentSupport\App\Services\Helper;
7 use FluentSupport\Framework\Support\Arr;
8
9 /**
10 * PermissionManager class is responsible for getting/settings data related to permission
11 * @package FluentSupport\App\Modules
12 *
13 * @version 1.0.0
14 */
15
16 class PermissionManager
17 {
18 const META_KEY = '_fluent_support_permissions';
19
20 // Ticket visibility levels returned by resolveTicketVisibility()
21 const VISIBILITY_ALL = 'all_tickets';
22 const VISIBILITY_ASSIGNED_AND_UNASSIGNED = 'assigned_and_unassigned';
23 const VISIBILITY_ASSIGNED_ONLY = 'assigned_only';
24
25 /**
26 * pluginPermissions method will return the list of permissions support by Fluent Support Plugin
27 * @return string[]
28 */
29 public static function pluginPermissions()
30 {
31 return [
32 'fst_view_dashboard',
33 'fst_view_tickets',
34 'fst_manage_own_tickets',
35 'fst_manage_unassigned_tickets',
36 'fst_manage_other_tickets',
37 'fst_delete_tickets',
38 'fst_assign_agents',
39 'fst_manage_settings',
40 'fst_sensitive_data',
41 'fst_manage_workflows',
42 'fst_run_workflows',
43 'fst_view_all_reports',
44 'fst_manage_saved_replies',
45 'fst_view_activity_logs',
46 'fst_merge_tickets',
47 'fst_split_ticket',
48 'fst_agent_today_performance',
49 'fst_draft_reply',
50 'fst_approve_draft_reply'
51 ];
52 }
53
54 /**
55 * Primary permission check. Accepts a single permission string or an array (any match).
56 *
57 * @param string|array $permissions
58 * @return bool
59 */
60 public static function userCan($permissions)
61 {
62 if (current_user_can('manage_options')) {
63 return true;
64 }
65
66 $userPermissions = self::currentUserPermissions();
67
68 if (!$userPermissions) {
69 return false;
70 }
71
72 if (is_string($permissions)) {
73 return in_array($permissions, $userPermissions);
74 }
75
76 if (is_array($permissions)) {
77 foreach ($permissions as $permission) {
78 if (in_array($permission, $userPermissions)) {
79 return true;
80 }
81 }
82 }
83
84 return false;
85 }
86
87 /**
88 * currentUserCan method will return whether a user has the selected permission or not.
89 * Backward-compatible alias for userCan().
90 *
91 * @param $permission
92 * @return bool
93 */
94 public static function currentUserCan($permission)
95 {
96 return self::userCan($permission);
97 }
98
99 /**
100 * attachPermissions method will save selected permissions to user meta.
101 * Also cleans up any legacy fst_* WordPress capabilities.
102 *
103 * @param $user
104 * @param $permissions
105 * @return false|mixed
106 */
107 public static function attachPermissions($user, $permissions)
108 {
109 if (is_numeric($user)) {
110 $user = get_user_by('ID', $user);
111 }
112
113 if (!$user) {
114 return false;
115 }
116
117 if (user_can($user, 'manage_options')) {
118 return $user;
119 }
120
121 $allPermissions = self::pluginPermissions();
122
123 $permissions = array_values(array_intersect($allPermissions, $permissions));
124
125 $exclusionRules = self::getExclusionRules();
126 $permissions = self::applyExclusionRules($permissions, $exclusionRules);
127
128 // Auto-grant fst_view_tickets when any manage, draft, or approve permission is present
129 $manageOrDraftPermissions = [
130 'fst_manage_own_tickets',
131 'fst_manage_unassigned_tickets',
132 'fst_manage_other_tickets',
133 'fst_draft_reply',
134 'fst_approve_draft_reply',
135 ];
136
137 if (!empty(array_intersect($permissions, $manageOrDraftPermissions))
138 && !in_array('fst_view_tickets', $permissions)) {
139 $permissions[] = 'fst_view_tickets';
140 }
141
142 // Store permissions in user meta
143 update_user_meta($user->ID, self::META_KEY, array_values($permissions));
144
145 // Clean up legacy WordPress capabilities
146 foreach ($allPermissions as $cap) {
147 $user->remove_cap($cap);
148 }
149
150 return $user;
151 }
152
153 /**
154 * Clean removal of all Fluent Support permissions for a user.
155 *
156 * @param int $userId
157 * @return void
158 */
159 public static function detachPermissions($userId)
160 {
161 delete_user_meta($userId, self::META_KEY);
162
163 // Clean up any legacy WordPress capabilities
164 $user = get_user_by('ID', $userId);
165 if ($user && !user_can($user, 'manage_options')) {
166 foreach (self::pluginPermissions() as $cap) {
167 $user->remove_cap($cap);
168 }
169 }
170 }
171
172 /**
173 * Remove conflicting permissions based on exclusion rules.
174 *
175 * @param array $permissions The array of permissions to filter.
176 * @param array $rules Each key => value pair means: if key is present, remove value.
177 * @return array The filtered array of permissions.
178 */
179 public static function applyExclusionRules($permissions, $rules)
180 {
181 foreach ($rules as $requiredKey => $removeKey) {
182 if (in_array($requiredKey, $permissions) && in_array($removeKey, $permissions)) {
183 unset($permissions[array_search($removeKey, $permissions)]);
184 }
185 }
186 return $permissions;
187 }
188
189 /**
190 * Get the mutual exclusion rules for permission assignment.
191 *
192 * @return array Each key => value pair means: if key is present, remove value.
193 */
194 public static function getExclusionRules()
195 {
196 // Mutual exclusion rules applied when assigning permissions:
197 // - If agent has any manage_*_tickets permission, remove fst_draft_reply
198 // (draft-only mode is for agents who CANNOT manage tickets)
199 // - If agent has fst_draft_reply, remove fst_approve_draft_reply
200 // (draft-only agents should not approve their own drafts)
201 return [
202 'fst_manage_unassigned_tickets' => 'fst_draft_reply',
203 'fst_manage_other_tickets' => 'fst_draft_reply',
204 'fst_manage_own_tickets' => 'fst_draft_reply',
205 'fst_draft_reply' => 'fst_approve_draft_reply'
206 ];
207 }
208
209 /**
210 * Get raw permissions from user meta.
211 *
212 * @param int|null $userId
213 * @return array
214 */
215 public static function getMetaPermissions($userId = null)
216 {
217 if ($userId === null) {
218 $userId = get_current_user_id();
219 }
220
221 if (!$userId) {
222 return [];
223 }
224
225 $permissions = get_user_meta($userId, self::META_KEY, true);
226
227 return is_array($permissions) ? $permissions : [];
228 }
229
230 /**
231 * getUserPermissions method will get all permissions for a user.
232 * Reads from user meta with legacy wp_capabilities fallback.
233 *
234 * @param false $user
235 * @return array|string[]
236 */
237 public static function getUserPermissions($user = false)
238 {
239 if (is_numeric($user)) {
240 $user = get_user_by('ID', $user);
241 }
242
243 if (!$user) {
244 return [];
245 }
246
247 $pluginPermission = self::pluginPermissions();
248
249 if ($user->has_cap('manage_options')) {
250 $pluginPermission[] = 'administrator';
251 $pluginPermission = array_values(array_diff($pluginPermission, ['fst_draft_reply']));
252 return $pluginPermission;
253 }
254
255 // Read from meta first
256 $permissions = self::getMetaPermissions($user->ID);
257
258 if (!empty($permissions)) {
259 return array_values(array_intersect($permissions, $pluginPermission));
260 }
261
262 // Legacy fallback: read from wp_capabilities and migrate
263 $legacyPermissions = array_values(array_intersect(array_keys($user->allcaps), $pluginPermission));
264
265 if (!empty($legacyPermissions)) {
266 // Migrate to meta
267 update_user_meta($user->ID, self::META_KEY, $legacyPermissions);
268
269 // Clean up legacy caps
270 foreach ($legacyPermissions as $cap) {
271 $user->remove_cap($cap);
272 }
273 }
274
275 return $legacyPermissions;
276 }
277
278 /**
279 * currentUserPermissions method will return the permission of logged-in user
280 * @param bool $cached
281 * @return array|mixed|string[]
282 */
283 public static function currentUserPermissions($cached = true)
284 {
285 static $permissions;
286
287 if ($permissions && $cached) {
288 return $permissions;
289 }
290
291 $permissions = self::getUserPermissions(get_current_user_id());
292
293 return $permissions;
294 }
295
296 /**
297 * Determine the WordPress capability string for menu registration.
298 * Returns 'manage_options' for admins, the user's WP role for agents
299 * with permissions, or empty string to hide the menu.
300 *
301 * @return string
302 */
303 public static function getMenuPermission()
304 {
305 if (current_user_can('manage_options')) {
306 return 'manage_options';
307 }
308
309 $userId = get_current_user_id();
310
311 if (!$userId) {
312 return '';
313 }
314
315 $metaPermissions = self::getMetaPermissions($userId);
316
317 // Legacy fallback: check wp_capabilities for fst_* caps
318 if (empty($metaPermissions)) {
319 $user = get_user_by('ID', $userId);
320 if ($user) {
321 $legacyPermissions = array_intersect(array_keys($user->allcaps), self::pluginPermissions());
322 if (empty($legacyPermissions)) {
323 return '';
324 }
325 } else {
326 return '';
327 }
328 }
329
330 $user = wp_get_current_user();
331 $roles = array_values((array) $user->roles);
332
333 return Arr::get($roles, 0, '');
334 }
335
336 /**
337 * Get the mailbox IDs that the current agent is restricted from accessing.
338 *
339 * @return array Mailbox IDs the agent cannot access, or empty array if unrestricted.
340 */
341 public static function getRestrictedMailboxIds()
342 {
343 $agent = Helper::getAgentByUserId();
344 $restrictions = $agent->getMeta('agent_restrictions');
345
346 // Only enforce mailbox restrictions when the toggle is explicitly enabled
347 if (!empty($restrictions['businessBoxRestrictions']) && !empty($restrictions['restrictedBusinessBoxes'])) {
348 return $restrictions['restrictedBusinessBoxes'];
349 }
350
351 return [];
352
353 }
354
355 /**
356 * Whether the current user can perform mutating ticket actions (reply, close, reopen, assign, etc.).
357 * Draft-only agents return false here — they can view tickets and create drafts but cannot publish.
358 *
359 * @return bool
360 */
361 public static function canManageTickets()
362 {
363 return self::userCan([
364 'fst_manage_own_tickets',
365 'fst_manage_unassigned_tickets',
366 'fst_manage_other_tickets'
367 ]);
368 }
369
370 /**
371 * Whether the current user can access ticket API routes at all (read or write).
372 * Includes manage, merge, draft-only, and view-only agents.
373 *
374 * @return bool
375 */
376 public static function canAccessTicketRoutes()
377 {
378 return self::userCan([
379 'fst_view_tickets',
380 'fst_manage_own_tickets',
381 'fst_manage_unassigned_tickets',
382 'fst_manage_other_tickets',
383 'fst_merge_tickets',
384 'fst_draft_reply'
385 ]);
386 }
387
388 /**
389 * Determine ticket visibility level from a permission set.
390 *
391 * Business rule: fst_view_tickets and fst_draft_reply get full visibility because
392 * read-only and draft agents need to view any ticket, even though they cannot publish.
393 *
394 * @param array $permissions
395 * @return string One of the VISIBILITY_* constants.
396 */
397 private static function resolveTicketVisibility(array $permissions)
398 {
399 // Manage-level permissions take priority for visibility
400 if (in_array('fst_manage_other_tickets', $permissions)) {
401 return self::VISIBILITY_ALL;
402 }
403
404 if (in_array('fst_manage_unassigned_tickets', $permissions)) {
405 return self::VISIBILITY_ASSIGNED_AND_UNASSIGNED;
406 }
407
408 if (in_array('fst_manage_own_tickets', $permissions)) {
409 return self::VISIBILITY_ASSIGNED_ONLY;
410 }
411
412 // Non-manage roles (draft, view-only) can see all tickets but cannot modify
413 if (in_array('fst_draft_reply', $permissions)
414 || in_array('fst_view_tickets', $permissions)) {
415 return self::VISIBILITY_ALL;
416 }
417
418 return self::VISIBILITY_ASSIGNED_ONLY;
419 }
420
421 /**
422 * currentTicketVisibility method will return the permission level for a user in tickets
423 * @return string
424 */
425 public static function currentTicketVisibility()
426 {
427 $permissions = self::currentUserPermissions();
428 return self::resolveTicketVisibility($permissions);
429 }
430
431 /**
432 * getAgentTicketVisibility method will return the access level of an agent in tickets
433 * @param false $userId
434 * @return string
435 */
436 public static function getAgentTicketVisibility($userId = false)
437 {
438 if (!$userId) {
439 $userId = get_current_user_id();
440 }
441
442 $permissions = self::getUserPermissions($userId);
443
444 return self::resolveTicketVisibility($permissions);
445 }
446
447 /**
448 * canAccessTicket method will return whether the selected user has permission in selected ticket or not
449 * @param $ticket
450 * @return bool
451 */
452 public static function canAccessTicket($ticket)
453 {
454 $permissionLevel = self::currentTicketVisibility();
455
456 if ($permissionLevel == self::VISIBILITY_ALL) {
457 return true;
458 }
459
460 $agent = Helper::getAgentByUserId();
461
462 if ($ticket->agent_id == $agent->id) {
463 return true;
464 }
465
466 // Allow access to unassigned tickets for agents with assigned_and_unassigned visibility
467 return !$ticket->agent_id && $permissionLevel == self::VISIBILITY_ASSIGNED_AND_UNASSIGNED;
468 }
469
470 /**
471 * getReadablePermissionGroups method will return the permission group as array
472 * @return array[]
473 */
474 public static function getReadablePermissionGroups()
475 {
476 return [
477 [
478 'title' => __('Tickets Permissions', 'fluent-support'),
479 'permissions' => [
480 'fst_view_dashboard' => __('View Dashboard', 'fluent-support'),
481 'fst_manage_own_tickets' => __('Manage Own Tickets', 'fluent-support'),
482 'fst_manage_unassigned_tickets' => __('Manage Unassigned Tickets', 'fluent-support'),
483 'fst_manage_other_tickets' => __('Manage Others Tickets', 'fluent-support'),
484 'fst_assign_agents' => __('Assign Agents', 'fluent-support'),
485 'fst_delete_tickets' => __('Delete Tickets & Individual Responses', 'fluent-support'),
486 'fst_merge_tickets' => __('Merge Tickets', 'fluent-support'),
487 'fst_split_ticket' => __('Split Ticket', 'fluent-support'),
488 'fst_draft_reply' => __('Draft Reply', 'fluent-support'),
489 'fst_approve_draft_reply' => __('Approve Draft Reply', 'fluent-support'),
490 'fst_view_tickets' => __('View Tickets (Read Only)', 'fluent-support'),
491 ]
492 ],
493 [
494 'title' => __('Workflow Permissions', 'fluent-support'),
495 'permissions' => [
496 'fst_manage_workflows' => __('Manage Workflows', 'fluent-support'),
497 'fst_run_workflows' => __('Run workflows', 'fluent-support'),
498 'fst_manage_saved_replies' => __('Manage Saved Replies', 'fluent-support')
499 ]
500 ],
501 [
502 'title' => __('Settings', 'fluent-support'),
503 'permissions' => [
504 'fst_manage_settings' => __('Manage Overall Settings', 'fluent-support'),
505 'fst_sensitive_data' => __('Access Private Data (Customers, Agents)', 'fluent-support')
506 ]
507 ],
508 [
509 'title' => __('Reporting', 'fluent-support'),
510 'permissions' => [
511 'fst_view_all_reports' => __('View All Reports', 'fluent-support'),
512 'fst_view_activity_logs' => __('View Activity Logs', 'fluent-support'),
513 'fst_agent_today_performance' => __('View Agent Today Performance', 'fluent-support'),
514 ]
515 ]
516 ];
517 }
518
519 public static function getMailboxesForRestriction()
520 {
521 return MailBox::select(['id', 'name'])->get();
522 }
523
524 /*
525 |--------------------------------------------------------------------------
526 | Deprecated Methods
527 |--------------------------------------------------------------------------
528 | These methods are kept for backward compatibility with third-party add-ons.
529 | They delegate to the renamed replacements and will be removed in a future release.
530 */
531
532 /**
533 * @deprecated Use currentTicketVisibility() instead.
534 */
535 public static function currentUserTicketsPermissionLevel()
536 {
537 _deprecated_function(__METHOD__, '2.0.5', 'PermissionManager::currentTicketVisibility()');
538
539 return self::mapVisibilityToLegacy(self::currentTicketVisibility());
540 }
541
542 /**
543 * @deprecated Use getAgentTicketVisibility() instead.
544 */
545 public static function agentTicketPermissionLevel($userId = false)
546 {
547 _deprecated_function(__METHOD__, '2.0.5', 'PermissionManager::getAgentTicketVisibility()');
548
549 return self::mapVisibilityToLegacy(self::getAgentTicketVisibility($userId));
550 }
551
552 /**
553 * @deprecated Use canAccessTicket() instead.
554 */
555 public static function hasTicketPermission($ticket)
556 {
557 _deprecated_function(__METHOD__, '2.0.5', 'PermissionManager::canAccessTicket()');
558
559 return self::canAccessTicket($ticket);
560 }
561
562 /**
563 * Map new VISIBILITY_* constants back to legacy string values.
564 *
565 * @param string $visibility
566 * @return string 'all', 'own_plus', or 'own'
567 */
568 private static function mapVisibilityToLegacy($visibility)
569 {
570 $map = [
571 self::VISIBILITY_ALL => 'all',
572 self::VISIBILITY_ASSIGNED_AND_UNASSIGNED => 'own_plus',
573 self::VISIBILITY_ASSIGNED_ONLY => 'own',
574 ];
575
576 return $map[$visibility] ?? 'own';
577 }
578 }
579