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 +308 -85 1.10.2 → 2.4.5 View file →
@@ -3,20 +3,25 @@
3 3 namespace FluentSupport\App\Modules;
4 4
5 5 use FluentSupport\App\Models\MailBox;
6 6 use FluentSupport\App\Services\Helper;
7 +use FluentSupport\App\Services\Tickets\AgentTicketAccess;
8 +use FluentSupport\Framework\Support\Arr;
7 9
8 10 /**
9 - * PermissionManager class is responsible for getting/settings data related to permission
10 - * @package FluentSupport\App\Modules
11 - *
12 - * @version 1.0.0
11 + * Fluent Support's own agent permissions, stored in user meta.
13 12 */
14 13
15 14 class PermissionManager
16 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 +
17 23 /**
18 - * pluginPermissions method will return the list of permissions support by Fluent Support Plugin
19 24 * @return string[]
20 25 */
21 26 public static function pluginPermissions()
22 27 {
@@ -21,8 +26,9 @@
21 26 public static function pluginPermissions()
22 27 {
23 28 return [
24 29 'fst_view_dashboard',
30 + 'fst_view_tickets',
25 31 'fst_manage_own_tickets',
26 32 'fst_manage_unassigned_tickets',
27 33 'fst_manage_other_tickets',
28 34 'fst_delete_tickets',
@@ -42,9 +48,54 @@
42 48 ];
43 49 }
44 50
45 51 /**
46 - * attachPermissions method will add selected permission to the user
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 + *
47 98 * @param $user
48 99 * @param $permissions
49 100 * @return false|mixed
50 101 */
@@ -62,19 +113,34 @@
62 113 return $user;
63 114 }
64 115
65 116 $allPermissions = self::pluginPermissions();
66 - foreach ($allPermissions as $permission) {
67 - $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';
68 137 }
69 138
70 - $permissions = array_intersect($allPermissions, $permissions);
139 + update_user_meta($user->ID, self::META_KEY, array_values($permissions));
71 140
72 - $filterPermissionConditions = self::filterPermissionConditions();
73 - $permissions = self::filterPermissionsByConditions($permissions, $filterPermissionConditions);
74 -
75 - foreach ($permissions as $permission) {
76 - $user->add_cap($permission);
141 + foreach ($allPermissions as $cap) {
142 + $user->remove_cap($cap);
77 143 }
78 144
79 145 return $user;
80 146 }
@@ -79,17 +145,36 @@
79 145 return $user;
80 146 }
81 147
82 148 /**
83 - * Filters permissions based on specified conditions.
149 + * Remove all Fluent Support permissions from a user.
84 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 + *
85 170 * @param array $permissions The array of permissions to filter.
86 - * @param array $conditions The conditions used for filtering.
171 + * @param array $rules Each key => value pair means: if key is present, remove value.
87 172 * @return array The filtered array of permissions.
88 173 */
89 - public static function filterPermissionsByConditions($permissions, $conditions)
174 + public static function applyExclusionRules($permissions, $rules)
90 175 {
91 - foreach ($conditions as $requiredKey => $removeKey) {
176 + foreach ($rules as $requiredKey => $removeKey) {
92 177 if (in_array($requiredKey, $permissions) && in_array($removeKey, $permissions)) {
93 178 unset($permissions[array_search($removeKey, $permissions)]);
94 179 }
95 180 }
@@ -96,25 +181,49 @@
96 181 return $permissions;
97 182 }
98 183
99 184 /**
100 - * Retrieves the permission filter conditions.
185 + * Get the mutual exclusion rules for permission assignment.
101 186 *
102 - * @return array The array of permission filter conditions.
187 + * @return array Each key => value pair means: if key is present, remove value.
103 188 */
104 - public static function filterPermissionConditions()
189 + public static function getExclusionRules()
105 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.
106 193 return [
107 194 'fst_manage_unassigned_tickets' => 'fst_draft_reply',
108 - 'fst_manage_other_tickets' => 'fst_draft_reply',
109 - 'fst_manage_own_tickets' => 'fst_draft_reply',
110 - 'fst_draft_reply' => 'fst_approve_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'
111 198 ];
112 199 }
113 200
114 201 /**
115 - * getUserPermissions method will get all permissions for a user
116 - * @param false $user
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
117 226 * @return array|string[]
118 227 */
119 228 public static function getUserPermissions($user = false)
120 229 {
@@ -129,17 +238,33 @@
129 238 $pluginPermission = self::pluginPermissions();
130 239
131 240 if ($user->has_cap('manage_options')) {
132 241 $pluginPermission[] = 'administrator';
133 - $pluginPermission = array_values(array_diff($pluginPermission, ['fst_draft_reply'])); //Remove draft reply permission from here
242 + $pluginPermission = array_values(array_diff($pluginPermission, ['fst_draft_reply']));
134 243 return $pluginPermission;
135 244 }
136 245
137 - 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;
138 264 }
139 265
140 266 /**
141 - * currentUserPermissions method will return the permission of logged-in user
142 267 * @param bool $cached
143 268 * @return array|mixed|string[]
144 269 */
145 270 public static function currentUserPermissions($cached = true)
@@ -155,106 +280,154 @@
155 280 return $permissions;
156 281 }
157 282
158 283 /**
159 - * Retrieve the list of restricted business boxes for the current user.
284 + * Capability for the admin menu: 'manage_options' for admins, the user's
285 + * role for agents, or '' to hide the menu.
160 286 *
161 - * This method fetches the list of restricted business boxes from the metadata of the current user's agent profile.
162 - * If business box restrictions are enabled for the user, it returns the list of restricted business boxes; otherwise,
163 - * it returns an empty array.
287 + * @return string
288 + */
289 + public static function getMenuPermission()
290 + {
291 + if (current_user_can('manage_options')) {
292 + return 'manage_options';
293 + }
294 +
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, '');
320 + }
321 +
322 + /**
323 + * Get the mailbox IDs that the current agent is restricted from accessing.
164 324 *
165 - * @return array The list of restricted business boxes for the current user.
325 + * @return array Mailbox IDs the agent cannot access, or empty array if unrestricted.
166 326 */
167 - public static function currentUserRestrictedBusinessBoxes()
327 + public static function getRestrictedMailboxIds()
168 328 {
169 - $agent = Helper::getAgentByUserId();
170 - $restrictions = $agent->getMeta('agent_restrictions');
329 + return (new AgentTicketAccess())->getRestrictedMailboxIds();
171 330
172 - return isset($restrictions['businessBoxRestrictions']) ? $restrictions['restrictedBusinessBoxes'] : [];
331 + }
173 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 + ]);
174 346 }
175 347
176 348 /**
177 - * currentUserCan method will return whether a user has the selected permission or not
178 - * @param $permission
349 + * Whether the current user can reach ticket routes at all, read or write.
350 + *
179 351 * @return bool
180 352 */
181 - public static function currentUserCan($permission)
353 + public static function canAccessTicketRoutes()
182 354 {
183 - if (current_user_can('manage_options')) {
184 - return true;
185 - }
186 -
187 - return current_user_can($permission);
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 + ]);
188 363 }
189 364
190 365 /**
191 - * currentUserTicketsPermissionLevel method will return the permission level for a user in tickets
192 - * @return string
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.
193 371 */
194 - public static function currentUserTicketsPermissionLevel()
372 + private static function resolveTicketVisibility(array $permissions)
195 373 {
196 - $permissions = self::currentUserPermissions();
374 + if (in_array('fst_manage_other_tickets', $permissions)) {
375 + return self::VISIBILITY_ALL;
376 + }
197 377
198 - if (in_array('fst_manage_other_tickets', $permissions)) {
199 - return 'all';
378 + if (in_array('fst_manage_unassigned_tickets', $permissions)) {
379 + return self::VISIBILITY_ASSIGNED_AND_UNASSIGNED;
200 380 }
201 381
202 - if (in_array('fst_draft_reply', $permissions) || in_array('fst_manage_unassigned_tickets', $permissions)) {
203 - return 'own_plus';
382 + if (in_array('fst_manage_own_tickets', $permissions)) {
383 + return self::VISIBILITY_ASSIGNED_ONLY;
204 384 }
205 385
206 - return 'own';
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;
207 392 }
208 393
209 394 /**
210 - * agentTicketPermissionLevel method will return the access level of an agent in tickets
211 - * @param false $userId
212 395 * @return string
213 396 */
214 - public static function agentTicketPermissionLevel($userId = false)
397 + public static function currentTicketVisibility()
215 398 {
216 - 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) {
217 410 $userId = get_current_user_id();
218 411 }
219 412
220 413 $permissions = self::getUserPermissions($userId);
221 414
222 - if (in_array('fst_manage_other_tickets', $permissions)) {
223 - return 'all';
224 - }
225 -
226 - if (in_array('fst_draft_reply', $permissions) || in_array('fst_manage_unassigned_tickets', $permissions)) {
227 - return 'own_plus';
228 - }
229 -
230 - return 'own';
415 + return self::resolveTicketVisibility($permissions);
231 416 }
232 417
233 418 /**
234 - * hasTicketPermission method will return whether the selected user has permission in selected ticket or not
419 + * Whether the current agent can access this ticket.
420 + *
235 421 * @param $ticket
236 422 * @return bool
237 423 */
238 - public static function hasTicketPermission($ticket)
424 + public static function canAccessTicket($ticket)
239 425 {
240 - $permissionLevel = self::currentUserTicketsPermissionLevel();
241 -
242 - if ($permissionLevel == 'all') {
243 - return true;
244 - }
245 -
246 - $agent = Helper::getAgentByUserId();
247 -
248 - if ($ticket->agent_id == $agent->id) {
249 - return true;
250 - }
251 -
252 - return !$ticket->agent_id && $permissionLevel == 'own_plus';
426 + return (new AgentTicketAccess())->currentAgentCanAccess($ticket);
253 427 }
254 428
255 429 /**
256 - * getReadablePermissionGroups method will return the permission group as array
257 430 * @return array[]
258 431 */
259 432 public static function getReadablePermissionGroups()
260 433 {
@@ -271,8 +444,9 @@
271 444 'fst_merge_tickets' => __('Merge Tickets', 'fluent-support'),
272 445 'fst_split_ticket' => __('Split Ticket', 'fluent-support'),
273 446 'fst_draft_reply' => __('Draft Reply', 'fluent-support'),
274 447 'fst_approve_draft_reply' => __('Approve Draft Reply', 'fluent-support'),
448 + 'fst_view_tickets' => __('View Tickets (Read Only)', 'fluent-support'),
275 449 ]
276 450 ],
277 451 [
278 452 'title' => __('Workflow Permissions', 'fluent-support'),
@@ -299,9 +473,58 @@
299 473 ]
300 474 ];
301 475 }
302 476
303 - public static function getBusinessBoxesForRestriction()
477 + public static function getMailboxesForRestriction()
304 478 {
305 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';
306 529 }
307 530 }