| @@ -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,14 +114,16 @@ | ||
| 120 | 114 | } |
| 121 | 115 | |
| 122 | 116 | $allPermissions = self::pluginPermissions(); |
| 123 | 117 | |
| 118 | + // Only known permissions. There is no "grant only what you hold" check | |
| 119 | + // here because AgentPolicy limits agent edits to administrators. | |
| 124 | 120 | $permissions = array_values(array_intersect($allPermissions, $permissions)); |
| 125 | 121 | |
| 126 | 122 | $exclusionRules = self::getExclusionRules(); |
| 127 | 123 | $permissions = self::applyExclusionRules($permissions, $exclusionRules); |
| 128 | 124 | |
| 129 | - // Auto-grant fst_view_tickets when any manage, draft, or approve permission is present | |
| 125 | + // Manage, draft and approve all imply fst_view_tickets | |
| 130 | 126 | $manageOrDraftPermissions = [ |
| 131 | 127 | 'fst_manage_own_tickets', |
| 132 | 128 | 'fst_manage_unassigned_tickets', |
| 133 | 129 | 'fst_manage_other_tickets', |
| @@ -139,12 +135,10 @@ | ||
| 139 | 135 | && !in_array('fst_view_tickets', $permissions)) { |
| 140 | 136 | $permissions[] = 'fst_view_tickets'; |
| 141 | 137 | } |
| 142 | 138 | |
| 143 | - // Store permissions in user meta | |
| 144 | 139 | update_user_meta($user->ID, self::META_KEY, array_values($permissions)); |
| 145 | 140 | |
| 146 | - // Clean up legacy WordPress capabilities | |
| 147 | 141 | foreach ($allPermissions as $cap) { |
| 148 | 142 | $user->remove_cap($cap); |
| 149 | 143 | } |
| 150 | 144 | |
| @@ -151,9 +145,9 @@ | ||
| 151 | 145 | return $user; |
| 152 | 146 | } |
| 153 | 147 | |
| 154 | 148 | /** |
| 155 | - * Clean removal of all Fluent Support permissions for a user. | |
| 149 | + * Remove all Fluent Support permissions from a user. | |
| 156 | 150 | * |
| 157 | 151 | * @param int $userId |
| 158 | 152 | * @return void |
| 159 | 153 | */ |
| @@ -160,9 +154,9 @@ | ||
| 160 | 154 | public static function detachPermissions($userId) |
| 161 | 155 | { |
| 162 | 156 | delete_user_meta($userId, self::META_KEY); |
| 163 | 157 | |
| 164 | - // Clean up any legacy WordPress capabilities | |
| 158 | + // Legacy capabilities | |
| 165 | 159 | $user = get_user_by('ID', $userId); |
| 166 | 160 | if ($user && !user_can($user, 'manage_options')) { |
| 167 | 161 | foreach (self::pluginPermissions() as $cap) { |
| 168 | 162 | $user->remove_cap($cap); |
| @@ -193,13 +187,10 @@ | ||
| 193 | 187 | * @return array Each key => value pair means: if key is present, remove value. |
| 194 | 188 | */ |
| 195 | 189 | public static function getExclusionRules() |
| 196 | 190 | { |
| 197 | - // Mutual exclusion rules applied when assigning permissions: | |
| 198 | - // - If agent has any manage_*_tickets permission, remove fst_draft_reply | |
| 199 | - // (draft-only mode is for agents who CANNOT manage tickets) | |
| 200 | - // - If agent has fst_draft_reply, remove fst_approve_draft_reply | |
| 201 | - // (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. | |
| 202 | 193 | return [ |
| 203 | 194 | 'fst_manage_unassigned_tickets' => 'fst_draft_reply', |
| 204 | 195 | 'fst_manage_other_tickets' => 'fst_draft_reply', |
| 205 | 196 | 'fst_manage_own_tickets' => 'fst_draft_reply', |
| @@ -228,12 +219,11 @@ | ||
| 228 | 219 | return is_array($permissions) ? $permissions : []; |
| 229 | 220 | } |
| 230 | 221 | |
| 231 | 222 | /** |
| 232 | - * getUserPermissions method will get all permissions for a user. | |
| 233 | - * Reads from user meta with legacy wp_capabilities fallback. | |
| 223 | + * Reads user meta, migrating legacy fst_* capabilities on first read. | |
| 234 | 224 | * |
| 235 | - * @param false $user | |
| 225 | + * @param int|\WP_User|false $user | |
| 236 | 226 | * @return array|string[] |
| 237 | 227 | */ |
| 238 | 228 | public static function getUserPermissions($user = false) |
| 239 | 229 | { |
| @@ -252,9 +242,8 @@ | ||
| 252 | 242 | $pluginPermission = array_values(array_diff($pluginPermission, ['fst_draft_reply'])); |
| 253 | 243 | return $pluginPermission; |
| 254 | 244 | } |
| 255 | 245 | |
| 256 | - // Read from meta first | |
| 257 | 246 | $permissions = self::getMetaPermissions($user->ID); |
| 258 | 247 | |
| 259 | 248 | if (!empty($permissions)) { |
| 260 | 249 | return array_values(array_intersect($permissions, $pluginPermission)); |
| @@ -259,16 +248,14 @@ | ||
| 259 | 248 | if (!empty($permissions)) { |
| 260 | 249 | return array_values(array_intersect($permissions, $pluginPermission)); |
| 261 | 250 | } |
| 262 | 251 | |
| 263 | - // Legacy fallback: read from wp_capabilities and migrate | |
| 252 | + // Legacy: permissions stored as WP capabilities. Move them to meta. | |
| 264 | 253 | $legacyPermissions = array_values(array_intersect(array_keys($user->allcaps), $pluginPermission)); |
| 265 | 254 | |
| 266 | 255 | if (!empty($legacyPermissions)) { |
| 267 | - // Migrate to meta | |
| 268 | 256 | update_user_meta($user->ID, self::META_KEY, $legacyPermissions); |
| 269 | 257 | |
| 270 | - // Clean up legacy caps | |
| 271 | 258 | foreach ($legacyPermissions as $cap) { |
| 272 | 259 | $user->remove_cap($cap); |
| 273 | 260 | } |
| 274 | 261 | } |
| @@ -276,9 +263,8 @@ | ||
| 276 | 263 | return $legacyPermissions; |
| 277 | 264 | } |
| 278 | 265 | |
| 279 | 266 | /** |
| 280 | - * currentUserPermissions method will return the permission of logged-in user | |
| 281 | 267 | * @param bool $cached |
| 282 | 268 | * @return array|mixed|string[] |
| 283 | 269 | */ |
| 284 | 270 | public static function currentUserPermissions($cached = true) |
| @@ -294,11 +280,10 @@ | ||
| 294 | 280 | return $permissions; |
| 295 | 281 | } |
| 296 | 282 | |
| 297 | 283 | /** |
| 298 | - * Determine the WordPress capability string for menu registration. | |
| 299 | - * Returns 'manage_options' for admins, the user's WP role for agents | |
| 300 | - * 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. | |
| 301 | 286 | * |
| 302 | 287 | * @return string |
| 303 | 288 | */ |
| 304 | 289 | public static function getMenuPermission() |
| @@ -314,9 +299,9 @@ | ||
| 314 | 299 | } |
| 315 | 300 | |
| 316 | 301 | $metaPermissions = self::getMetaPermissions($userId); |
| 317 | 302 | |
| 318 | - // Legacy fallback: check wp_capabilities for fst_* caps | |
| 303 | + // Legacy agents may still have fst_* capabilities instead of meta | |
| 319 | 304 | if (empty($metaPermissions)) { |
| 320 | 305 | $user = get_user_by('ID', $userId); |
| 321 | 306 | if ($user) { |
| 322 | 307 | $legacyPermissions = array_intersect(array_keys($user->allcaps), self::pluginPermissions()); |
| @@ -345,10 +330,10 @@ | ||
| 345 | 330 | |
| 346 | 331 | } |
| 347 | 332 | |
| 348 | 333 | /** |
| 349 | - * Whether the current user can perform mutating ticket actions (reply, close, reopen, assign, etc.). | |
| 350 | - * 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. | |
| 351 | 336 | * |
| 352 | 337 | * @return bool |
| 353 | 338 | */ |
| 354 | 339 | public static function canManageTickets() |
| @@ -360,10 +345,9 @@ | ||
| 360 | 345 | ]); |
| 361 | 346 | } |
| 362 | 347 | |
| 363 | 348 | /** |
| 364 | - * Whether the current user can access ticket API routes at all (read or write). | |
| 365 | - * Includes manage, merge, draft-only, and view-only agents. | |
| 349 | + * Whether the current user can reach ticket routes at all, read or write. | |
| 366 | 350 | * |
| 367 | 351 | * @return bool |
| 368 | 352 | */ |
| 369 | 353 | public static function canAccessTicketRoutes() |
| @@ -378,19 +362,16 @@ | ||
| 378 | 362 | ]); |
| 379 | 363 | } |
| 380 | 364 | |
| 381 | 365 | /** |
| 382 | - * 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. | |
| 383 | 368 | * |
| 384 | - * Business rule: fst_view_tickets and fst_draft_reply get full visibility because | |
| 385 | - * read-only and draft agents need to view any ticket, even though they cannot publish. | |
| 386 | - * | |
| 387 | 369 | * @param array $permissions |
| 388 | 370 | * @return string One of the VISIBILITY_* constants. |
| 389 | 371 | */ |
| 390 | 372 | private static function resolveTicketVisibility(array $permissions) |
| 391 | 373 | { |
| 392 | - // Manage-level permissions take priority for visibility | |
| 393 | 374 | if (in_array('fst_manage_other_tickets', $permissions)) { |
| 394 | 375 | return self::VISIBILITY_ALL; |
| 395 | 376 | } |
| 396 | 377 | |
| @@ -401,9 +382,8 @@ | ||
| 401 | 382 | if (in_array('fst_manage_own_tickets', $permissions)) { |
| 402 | 383 | return self::VISIBILITY_ASSIGNED_ONLY; |
| 403 | 384 | } |
| 404 | 385 | |
| 405 | - // Non-manage roles (draft, view-only) can see all tickets but cannot modify | |
| 406 | 386 | if (in_array('fst_draft_reply', $permissions) |
| 407 | 387 | || in_array('fst_view_tickets', $permissions)) { |
| 408 | 388 | return self::VISIBILITY_ALL; |
| 409 | 389 | } |
| @@ -411,9 +391,8 @@ | ||
| 411 | 391 | return self::VISIBILITY_ASSIGNED_ONLY; |
| 412 | 392 | } |
| 413 | 393 | |
| 414 | 394 | /** |
| 415 | - * currentTicketVisibility method will return the permission level for a user in tickets | |
| 416 | 395 | * @return string |
| 417 | 396 | */ |
| 418 | 397 | public static function currentTicketVisibility() |
| 419 | 398 | { |
| @@ -421,10 +400,9 @@ | ||
| 421 | 400 | return self::resolveTicketVisibility($permissions); |
| 422 | 401 | } |
| 423 | 402 | |
| 424 | 403 | /** |
| 425 | - * getAgentTicketVisibility method will return the access level of an agent in tickets | |
| 426 | - * @param false $userId | |
| 404 | + * @param int|false $userId | |
| 427 | 405 | * @return string |
| 428 | 406 | */ |
| 429 | 407 | public static function getAgentTicketVisibility($userId = false) |
| 430 | 408 | { |
| @@ -437,9 +415,10 @@ | ||
| 437 | 415 | return self::resolveTicketVisibility($permissions); |
| 438 | 416 | } |
| 439 | 417 | |
| 440 | 418 | /** |
| 441 | - * 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 | + * | |
| 442 | 421 | * @param $ticket |
| 443 | 422 | * @return bool |
| 444 | 423 | */ |
| 445 | 424 | public static function canAccessTicket($ticket) |
| @@ -447,9 +426,8 @@ | ||
| 447 | 426 | return (new AgentTicketAccess())->currentAgentCanAccess($ticket); |
| 448 | 427 | } |
| 449 | 428 | |
| 450 | 429 | /** |
| 451 | - * getReadablePermissionGroups method will return the permission group as array | |
| 452 | 430 | * @return array[] |
| 453 | 431 | */ |
| 454 | 432 | public static function getReadablePermissionGroups() |
| 455 | 433 | { |
| @@ -500,15 +478,9 @@ | ||
| 500 | 478 | { |
| 501 | 479 | return MailBox::select(['id', 'name'])->get(); |
| 502 | 480 | } |
| 503 | 481 | |
| 504 | - /* | |
| 505 | - |-------------------------------------------------------------------------- | |
| 506 | - | Deprecated Methods | |
| 507 | - |-------------------------------------------------------------------------- | |
| 508 | - | These methods are kept for backward compatibility with third-party add-ons. | |
| 509 | - | They delegate to the renamed replacements and will be removed in a future release. | |
| 510 | - */ | |
| 482 | + // Deprecated aliases, kept for third-party add-ons. | |
| 511 | 483 | |
| 512 | 484 | /** |
| 513 | 485 | * @deprecated Use currentTicketVisibility() instead. |
| 514 | 486 | */ |