PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.2.1
Fluent Support – Helpdesk & Customer Support Ticket System v2.2.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.2.1, at app/Modules/PermissionManager.php

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