| @@ -7,12 +7,9 @@ | ||
| 7 | 7 | use FluentSupport\App\Services\Tickets\AgentTicketAccess; |
| 8 | 8 | use FluentSupport\Framework\Support\Arr; |
| 9 | 9 | |
| 10 | 10 | /** |
| 11 | - * PermissionManager class is responsible for getting/settings data related to permission | |
| 12 | - * @package FluentSupport\App\Modules | |
| 13 | - * | |
| 14 | - * @version 1.0.0 | |
| 11 | + * Fluent Support's own agent permissions, stored in user meta. | |
| 15 | 12 | */ |
| 16 | 13 | |
| 17 | 14 | class PermissionManager |
| 18 | 15 | { |
| @@ -23,9 +20,8 @@ | ||
| 23 | 20 | const VISIBILITY_ASSIGNED_AND_UNASSIGNED = 'assigned_and_unassigned'; |
| 24 | 21 | const VISIBILITY_ASSIGNED_ONLY = 'assigned_only'; |
| 25 | 22 | |
| 26 | 23 | /** |
| 27 | - * pluginPermissions method will return the list of permissions support by Fluent Support Plugin | |
| 28 | 24 | * @return string[] |
| 29 | 25 | */ |
| 30 | 26 | public static function pluginPermissions() |
| 31 | 27 | { |
| @@ -85,10 +81,9 @@ | ||
| 85 | 81 | return false; |
| 86 | 82 | } |
| 87 | 83 | |
| 88 | 84 | /** |
| 89 | - * currentUserCan method will return whether a user has the selected permission or not. | |
| 90 | - * Backward-compatible alias for userCan(). | |
| 85 | + * Alias of userCan(). | |
| 91 | 86 | * |
| 92 | 87 | * @param $permission |
| 93 | 88 | * @return bool |
| 94 | 89 | */ |
| @@ -97,10 +92,9 @@ | ||
| 97 | 92 | return self::userCan($permission); |
| 98 | 93 | } |
| 99 | 94 | |
| 100 | 95 | /** |
| 101 | - * attachPermissions method will save selected permissions to user meta. | |
| 102 | - * Also cleans up any legacy fst_* WordPress capabilities. | |
| 96 | + * Save permissions to user meta and drop legacy fst_* capabilities. | |
| 103 | 97 | * |
| 104 | 98 | * @param $user |
| 105 | 99 | * @param $permissions |
| 106 | 100 | * @return false|mixed |
| @@ -120,18 +114,16 @@ | ||
| 120 | 114 | } |
| 121 | 115 | |
| 122 | 116 | $allPermissions = self::pluginPermissions(); |
| 123 | 117 | |
| 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. | |
| 118 | + // Only known permissions. There is no "grant only what you hold" check | |
| 119 | + // here because AgentPolicy limits agent edits to administrators. | |
| 128 | 120 | $permissions = array_values(array_intersect($allPermissions, $permissions)); |
| 129 | 121 | |
| 130 | 122 | $exclusionRules = self::getExclusionRules(); |
| 131 | 123 | $permissions = self::applyExclusionRules($permissions, $exclusionRules); |
| 132 | 124 | |
| 133 | - // Auto-grant fst_view_tickets when any manage, draft, or approve permission is present | |
| 125 | + // Manage, draft and approve all imply fst_view_tickets | |
| 134 | 126 | $manageOrDraftPermissions = [ |
| 135 | 127 | 'fst_manage_own_tickets', |
| 136 | 128 | 'fst_manage_unassigned_tickets', |
| 137 | 129 | 'fst_manage_other_tickets', |
| @@ -143,12 +135,10 @@ | ||
| 143 | 135 | && !in_array('fst_view_tickets', $permissions)) { |
| 144 | 136 | $permissions[] = 'fst_view_tickets'; |
| 145 | 137 | } |
| 146 | 138 | |
| 147 | - // Store permissions in user meta | |
| 148 | 139 | update_user_meta($user->ID, self::META_KEY, array_values($permissions)); |
| 149 | 140 | |
| 150 | - // Clean up legacy WordPress capabilities | |
| 151 | 141 | foreach ($allPermissions as $cap) { |
| 152 | 142 | $user->remove_cap($cap); |
| 153 | 143 | } |
| 154 | 144 | |
| @@ -155,9 +145,9 @@ | ||
| 155 | 145 | return $user; |
| 156 | 146 | } |
| 157 | 147 | |
| 158 | 148 | /** |
| 159 | - * Clean removal of all Fluent Support permissions for a user. | |
| 149 | + * Remove all Fluent Support permissions from a user. | |
| 160 | 150 | * |
| 161 | 151 | * @param int $userId |
| 162 | 152 | * @return void |
| 163 | 153 | */ |
| @@ -164,9 +154,9 @@ | ||
| 164 | 154 | public static function detachPermissions($userId) |
| 165 | 155 | { |
| 166 | 156 | delete_user_meta($userId, self::META_KEY); |
| 167 | 157 | |
| 168 | - // Clean up any legacy WordPress capabilities | |
| 158 | + // Legacy capabilities | |
| 169 | 159 | $user = get_user_by('ID', $userId); |
| 170 | 160 | if ($user && !user_can($user, 'manage_options')) { |
| 171 | 161 | foreach (self::pluginPermissions() as $cap) { |
| 172 | 162 | $user->remove_cap($cap); |
| @@ -197,13 +187,10 @@ | ||
| 197 | 187 | * @return array Each key => value pair means: if key is present, remove value. |
| 198 | 188 | */ |
| 199 | 189 | public static function getExclusionRules() |
| 200 | 190 | { |
| 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) | |
| 191 | + // Draft-only mode is for agents who can't manage tickets, and a | |
| 192 | + // draft-only agent must not approve their own drafts. | |
| 206 | 193 | return [ |
| 207 | 194 | 'fst_manage_unassigned_tickets' => 'fst_draft_reply', |
| 208 | 195 | 'fst_manage_other_tickets' => 'fst_draft_reply', |
| 209 | 196 | 'fst_manage_own_tickets' => 'fst_draft_reply', |
| @@ -232,12 +219,11 @@ | ||
| 232 | 219 | return is_array($permissions) ? $permissions : []; |
| 233 | 220 | } |
| 234 | 221 | |
| 235 | 222 | /** |
| 236 | - * getUserPermissions method will get all permissions for a user. | |
| 237 | - * Reads from user meta with legacy wp_capabilities fallback. | |
| 223 | + * Reads user meta, migrating legacy fst_* capabilities on first read. | |
| 238 | 224 | * |
| 239 | - * @param false $user | |
| 225 | + * @param int|\WP_User|false $user | |
| 240 | 226 | * @return array|string[] |
| 241 | 227 | */ |
| 242 | 228 | public static function getUserPermissions($user = false) |
| 243 | 229 | { |
| @@ -256,9 +242,8 @@ | ||
| 256 | 242 | $pluginPermission = array_values(array_diff($pluginPermission, ['fst_draft_reply'])); |
| 257 | 243 | return $pluginPermission; |
| 258 | 244 | } |
| 259 | 245 | |
| 260 | - // Read from meta first | |
| 261 | 246 | $permissions = self::getMetaPermissions($user->ID); |
| 262 | 247 | |
| 263 | 248 | if (!empty($permissions)) { |
| 264 | 249 | return array_values(array_intersect($permissions, $pluginPermission)); |
| @@ -263,16 +248,14 @@ | ||
| 263 | 248 | if (!empty($permissions)) { |
| 264 | 249 | return array_values(array_intersect($permissions, $pluginPermission)); |
| 265 | 250 | } |
| 266 | 251 | |
| 267 | - // Legacy fallback: read from wp_capabilities and migrate | |
| 252 | + // Legacy: permissions stored as WP capabilities. Move them to meta. | |
| 268 | 253 | $legacyPermissions = array_values(array_intersect(array_keys($user->allcaps), $pluginPermission)); |
| 269 | 254 | |
| 270 | 255 | if (!empty($legacyPermissions)) { |
| 271 | - // Migrate to meta | |
| 272 | 256 | update_user_meta($user->ID, self::META_KEY, $legacyPermissions); |
| 273 | 257 | |
| 274 | - // Clean up legacy caps | |
| 275 | 258 | foreach ($legacyPermissions as $cap) { |
| 276 | 259 | $user->remove_cap($cap); |
| 277 | 260 | } |
| 278 | 261 | } |
| @@ -280,9 +263,8 @@ | ||
| 280 | 263 | return $legacyPermissions; |
| 281 | 264 | } |
| 282 | 265 | |
| 283 | 266 | /** |
| 284 | - * currentUserPermissions method will return the permission of logged-in user | |
| 285 | 267 | * @param bool $cached |
| 286 | 268 | * @return array|mixed|string[] |
| 287 | 269 | */ |
| 288 | 270 | public static function currentUserPermissions($cached = true) |
| @@ -298,11 +280,10 @@ | ||
| 298 | 280 | return $permissions; |
| 299 | 281 | } |
| 300 | 282 | |
| 301 | 283 | /** |
| 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. | |
| 284 | + * Capability for the admin menu: 'manage_options' for admins, the user's | |
| 285 | + * role for agents, or '' to hide the menu. | |
| 305 | 286 | * |
| 306 | 287 | * @return string |
| 307 | 288 | */ |
| 308 | 289 | public static function getMenuPermission() |
| @@ -318,9 +299,9 @@ | ||
| 318 | 299 | } |
| 319 | 300 | |
| 320 | 301 | $metaPermissions = self::getMetaPermissions($userId); |
| 321 | 302 | |
| 322 | - // Legacy fallback: check wp_capabilities for fst_* caps | |
| 303 | + // Legacy agents may still have fst_* capabilities instead of meta | |
| 323 | 304 | if (empty($metaPermissions)) { |
| 324 | 305 | $user = get_user_by('ID', $userId); |
| 325 | 306 | if ($user) { |
| 326 | 307 | $legacyPermissions = array_intersect(array_keys($user->allcaps), self::pluginPermissions()); |
| @@ -349,10 +330,10 @@ | ||
| 349 | 330 | |
| 350 | 331 | } |
| 351 | 332 | |
| 352 | 333 | /** |
| 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. | |
| 334 | + * Whether the current user can reply, close, assign, etc. False for | |
| 335 | + * draft-only agents. | |
| 355 | 336 | * |
| 356 | 337 | * @return bool |
| 357 | 338 | */ |
| 358 | 339 | public static function canManageTickets() |
| @@ -364,10 +345,9 @@ | ||
| 364 | 345 | ]); |
| 365 | 346 | } |
| 366 | 347 | |
| 367 | 348 | /** |
| 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. | |
| 349 | + * Whether the current user can reach ticket routes at all, read or write. | |
| 370 | 350 | * |
| 371 | 351 | * @return bool |
| 372 | 352 | */ |
| 373 | 353 | public static function canAccessTicketRoutes() |
| @@ -382,19 +362,16 @@ | ||
| 382 | 362 | ]); |
| 383 | 363 | } |
| 384 | 364 | |
| 385 | 365 | /** |
| 386 | - * Determine ticket visibility level from a permission set. | |
| 366 | + * Manage permissions win. Otherwise view-only and draft agents see all | |
| 367 | + * tickets, since they can't modify them. | |
| 387 | 368 | * |
| 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 | 369 | * @param array $permissions |
| 392 | 370 | * @return string One of the VISIBILITY_* constants. |
| 393 | 371 | */ |
| 394 | 372 | private static function resolveTicketVisibility(array $permissions) |
| 395 | 373 | { |
| 396 | - // Manage-level permissions take priority for visibility | |
| 397 | 374 | if (in_array('fst_manage_other_tickets', $permissions)) { |
| 398 | 375 | return self::VISIBILITY_ALL; |
| 399 | 376 | } |
| 400 | 377 | |
| @@ -405,9 +382,8 @@ | ||
| 405 | 382 | if (in_array('fst_manage_own_tickets', $permissions)) { |
| 406 | 383 | return self::VISIBILITY_ASSIGNED_ONLY; |
| 407 | 384 | } |
| 408 | 385 | |
| 409 | - // Non-manage roles (draft, view-only) can see all tickets but cannot modify | |
| 410 | 386 | if (in_array('fst_draft_reply', $permissions) |
| 411 | 387 | || in_array('fst_view_tickets', $permissions)) { |
| 412 | 388 | return self::VISIBILITY_ALL; |
| 413 | 389 | } |
| @@ -415,9 +391,8 @@ | ||
| 415 | 391 | return self::VISIBILITY_ASSIGNED_ONLY; |
| 416 | 392 | } |
| 417 | 393 | |
| 418 | 394 | /** |
| 419 | - * currentTicketVisibility method will return the permission level for a user in tickets | |
| 420 | 395 | * @return string |
| 421 | 396 | */ |
| 422 | 397 | public static function currentTicketVisibility() |
| 423 | 398 | { |
| @@ -425,10 +400,9 @@ | ||
| 425 | 400 | return self::resolveTicketVisibility($permissions); |
| 426 | 401 | } |
| 427 | 402 | |
| 428 | 403 | /** |
| 429 | - * getAgentTicketVisibility method will return the access level of an agent in tickets | |
| 430 | - * @param false $userId | |
| 404 | + * @param int|false $userId | |
| 431 | 405 | * @return string |
| 432 | 406 | */ |
| 433 | 407 | public static function getAgentTicketVisibility($userId = false) |
| 434 | 408 | { |
| @@ -441,9 +415,10 @@ | ||
| 441 | 415 | return self::resolveTicketVisibility($permissions); |
| 442 | 416 | } |
| 443 | 417 | |
| 444 | 418 | /** |
| 445 | - * canAccessTicket method will return whether the selected user has permission in selected ticket or not | |
| 419 | + * Whether the current agent can access this ticket. | |
| 420 | + * | |
| 446 | 421 | * @param $ticket |
| 447 | 422 | * @return bool |
| 448 | 423 | */ |
| 449 | 424 | public static function canAccessTicket($ticket) |
| @@ -451,9 +426,8 @@ | ||
| 451 | 426 | return (new AgentTicketAccess())->currentAgentCanAccess($ticket); |
| 452 | 427 | } |
| 453 | 428 | |
| 454 | 429 | /** |
| 455 | - * getReadablePermissionGroups method will return the permission group as array | |
| 456 | 430 | * @return array[] |
| 457 | 431 | */ |
| 458 | 432 | public static function getReadablePermissionGroups() |
| 459 | 433 | { |
| @@ -504,15 +478,9 @@ | ||
| 504 | 478 | { |
| 505 | 479 | return MailBox::select(['id', 'name'])->get(); |
| 506 | 480 | } |
| 507 | 481 | |
| 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 | - */ | |
| 482 | + // Deprecated aliases, kept for third-party add-ons. | |
| 515 | 483 | |
| 516 | 484 | /** |
| 517 | 485 | * @deprecated Use currentTicketVisibility() instead. |
| 518 | 486 | */ |