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

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

531 lines 16.0 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 * Fluent Support's own agent permissions, stored in user meta.
12 */
13
14 class PermissionManager
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 */
26 public static function pluginPermissions()
27 {
28 return [
29 'fst_view_dashboard',
30 'fst_view_tickets',
31 'fst_manage_own_tickets',
32 'fst_manage_unassigned_tickets',
33 'fst_manage_other_tickets',
34 'fst_delete_tickets',
35 'fst_assign_agents',
36 'fst_manage_settings',
37 'fst_sensitive_data',
38 'fst_manage_workflows',
39 'fst_run_workflows',
40 'fst_view_all_reports',
41 'fst_manage_saved_replies',
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'
48 ];
49 }
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 */
102 public static function attachPermissions($user, $permissions)
103 {
104 if (is_numeric($user)) {
105 $user = get_user_by('ID', $user);
106 }
107
108 if (!$user) {
109 return false;
110 }
111
112 if (user_can($user, 'manage_options')) {
113 return $user;
114 }
115
116 $allPermissions = self::pluginPermissions();
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';
137 }
138
139 update_user_meta($user->ID, self::META_KEY, array_values($permissions));
140
141 foreach ($allPermissions as $cap) {
142 $user->remove_cap($cap);
143 }
144
145 return $user;
146 }
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 */
228 public static function getUserPermissions($user = false)
229 {
230 if (is_numeric($user)) {
231 $user = get_user_by('ID', $user);
232 }
233
234 if (!$user) {
235 return [];
236 }
237
238 $pluginPermission = self::pluginPermissions();
239
240 if ($user->has_cap('manage_options')) {
241 $pluginPermission[] = 'administrator';
242 $pluginPermission = array_values(array_diff($pluginPermission, ['fst_draft_reply']));
243 return $pluginPermission;
244 }
245
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;
264 }
265
266 /**
267 * @param bool $cached
268 * @return array|mixed|string[]
269 */
270 public static function currentUserPermissions($cached = true)
271 {
272 static $permissions;
273
274 if ($permissions && $cached) {
275 return $permissions;
276 }
277
278 $permissions = self::getUserPermissions(get_current_user_id());
279
280 return $permissions;
281 }
282
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()
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.
324 *
325 * @return array Mailbox IDs the agent cannot access, or empty array if unrestricted.
326 */
327 public static function getRestrictedMailboxIds()
328 {
329 return (new AgentTicketAccess())->getRestrictedMailboxIds();
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 {
374 if (in_array('fst_manage_other_tickets', $permissions)) {
375 return self::VISIBILITY_ALL;
376 }
377
378 if (in_array('fst_manage_unassigned_tickets', $permissions)) {
379 return self::VISIBILITY_ASSIGNED_AND_UNASSIGNED;
380 }
381
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;
392 }
393
394 /**
395 * @return string
396 */
397 public static function currentTicketVisibility()
398 {
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) {
410 $userId = get_current_user_id();
411 }
412
413 $permissions = self::getUserPermissions($userId);
414
415 return self::resolveTicketVisibility($permissions);
416 }
417
418 /**
419 * Whether the current agent can access this ticket.
420 *
421 * @param $ticket
422 * @return bool
423 */
424 public static function canAccessTicket($ticket)
425 {
426 return (new AgentTicketAccess())->currentAgentCanAccess($ticket);
427 }
428
429 /**
430 * @return array[]
431 */
432 public static function getReadablePermissionGroups()
433 {
434 return [
435 [
436 'title' => __('Tickets Permissions', 'fluent-support'),
437 'permissions' => [
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'),
449 ]
450 ],
451 [
452 'title' => __('Workflow Permissions', 'fluent-support'),
453 'permissions' => [
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')
457 ]
458 ],
459 [
460 'title' => __('Settings', 'fluent-support'),
461 'permissions' => [
462 'fst_manage_settings' => __('Manage Overall Settings', 'fluent-support'),
463 'fst_sensitive_data' => __('Access Private Data (Customers, Agents)', 'fluent-support')
464 ]
465 ],
466 [
467 'title' => __('Reporting', 'fluent-support'),
468 'permissions' => [
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'),
472 ]
473 ]
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';
529 }
530 }
531