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 / Modules / PermissionManager.php

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

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