PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.8
Booking for Appointments and Events Calendar – Amelia v2.4.8
2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / src / Infrastructure / WP / UserRoles / SuperAdminRoleService.php
ameliabooking / src / Infrastructure / WP / UserRoles Last commit date
SuperAdminRoleService.php 1 week ago UserRoles.php 1 week ago
SuperAdminRoleService.php
166 lines
1 <?php
2
3 namespace AmeliaBooking\Infrastructure\WP\UserRoles;
4
5 use AmeliaBooking\Infrastructure\Licence\Licence;
6 use AmeliaBooking\Infrastructure\Licence\LicenceConstants;
7
8 /**
9 * Centralizes SuperAdmin role checks and role membership changes.
10 *
11 * SuperAdmin is Elite (Developer) only — it exists to protect White Label /
12 * Activation for agency workflows that are not available on lower licences.
13 */
14 class SuperAdminRoleService
15 {
16 public const ROLE = 'wpamelia-super-admin';
17 public const CAPABILITY = 'amelia_super_admin';
18 private const ROLE_LOCK_NAME = 'amelia_super_admin_role_lock';
19 private const BLOCKED_GRANT_ROLES = [
20 'wpamelia-customer',
21 'wpamelia-provider',
22 'wpamelia-manager',
23 ];
24
25 /**
26 * SuperAdmin is only available on Elite (Developer) licence.
27 */
28 public static function isAvailable(): bool
29 {
30 return Licence::hasLicenseAccess(LicenceConstants::DEVELOPER);
31 }
32
33 public function isCurrentUserSuperAdmin(): bool
34 {
35 return self::isAvailable() && current_user_can(self::CAPABILITY);
36 }
37
38 public function canAccessActivationSettings(): bool
39 {
40 // Without Elite there is no SuperAdmin gate — Activation stays open to
41 // users who already have settings access.
42 if (!self::isAvailable()) {
43 return true;
44 }
45
46 if ($this->isCurrentUserSuperAdmin()) {
47 return true;
48 }
49
50 // Bootstrap / recovery: no SuperAdmins yet (first activation, or role
51 // restored after switching back to Elite).
52 return $this->countSuperAdmins() === 0;
53 }
54
55 public function countSuperAdmins(): int
56 {
57 if (!self::isAvailable()) {
58 return 0;
59 }
60
61 return count($this->getSuperAdminWpUsers());
62 }
63
64 public function grant(int $userId): bool
65 {
66 if (!self::isAvailable()) {
67 return true;
68 }
69
70 $user = get_user_by('id', (int)$userId);
71
72 if (!$user) {
73 return false;
74 }
75
76 if (in_array(self::ROLE, (array)$user->roles, true)) {
77 self::removeSecondaryAmeliaRoles((int)$user->ID);
78 return true;
79 }
80
81 return $this->withRoleLock(function () use ($user): bool {
82 $user = get_user_by('id', (int)$user->ID);
83
84 if (!$user) {
85 return false;
86 }
87
88 foreach (self::BLOCKED_GRANT_ROLES as $role) {
89 if (in_array($role, (array)$user->roles, true)) {
90 $user->remove_role($role);
91 }
92 }
93
94 $user = get_user_by('id', (int)$user->ID);
95
96 if (!$user) {
97 return false;
98 }
99
100 if (in_array(self::ROLE, (array)$user->roles, true)) {
101 return true;
102 }
103
104 $user->add_role(self::ROLE);
105
106 return true;
107 });
108 }
109
110 public static function removeSecondaryAmeliaRoles(int $userId): void
111 {
112 $user = get_user_by('id', (int)$userId);
113
114 if (!$user || !self::userHasRole((int)$user->ID)) {
115 return;
116 }
117
118 foreach (self::BLOCKED_GRANT_ROLES as $role) {
119 if (in_array($role, (array)$user->roles, true)) {
120 $user->remove_role($role);
121 }
122 }
123 }
124
125 public static function userHasRole(?int $userId): bool
126 {
127 if (!$userId) {
128 return false;
129 }
130
131 $user = get_user_by('id', (int)$userId);
132
133 return $user && in_array(self::ROLE, (array)$user->roles, true);
134 }
135
136 private function getSuperAdminWpUsers(): array
137 {
138 return get_users([
139 'role__in' => [self::ROLE],
140 'orderby' => 'display_name',
141 'order' => 'ASC',
142 ]);
143 }
144
145 private function withRoleLock(callable $callback): bool
146 {
147 global $wpdb;
148
149 $lockAcquired = (int)$wpdb->get_var(
150 $wpdb->prepare('SELECT GET_LOCK(%s, 5)', self::ROLE_LOCK_NAME)
151 );
152
153 if ($lockAcquired !== 1) {
154 return false;
155 }
156
157 try {
158 return (bool)$callback();
159 } finally {
160 $wpdb->get_var(
161 $wpdb->prepare('SELECT RELEASE_LOCK(%s)', self::ROLE_LOCK_NAME)
162 );
163 }
164 }
165 }
166