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