| @@ -13,9 +13,9 @@ | ||
| 13 | 13 | public function getManagers($attributes = []) |
| 14 | 14 | { |
| 15 | 15 | $limit = Arr::get($attributes, 'per_page', 10); |
| 16 | 16 | $page = Arr::get($attributes, 'page', 1); |
| 17 | - $offset = $page == 1 ? 0 : ($page - 1) * $limit; | |
| 17 | + $offset = 1 == $page ? 0 : ($page - 1) * $limit; | |
| 18 | 18 | |
| 19 | 19 | $query = new \WP_User_Query([ |
| 20 | 20 | 'meta_key' => '_fluent_forms_has_role', |
| 21 | 21 | 'meta_value' => 1, |
| @@ -54,36 +54,51 @@ | ||
| 54 | 54 | 'permissions' => Acl::getReadablePermissions(), |
| 55 | 55 | 'forms' => $forms, |
| 56 | 56 | ]); |
| 57 | 57 | } |
| 58 | - | |
| 58 | + | |
| 59 | 59 | public function addManager($attributes = []) |
| 60 | 60 | { |
| 61 | 61 | $manager = Arr::get($attributes, 'manager'); |
| 62 | - | |
| 62 | + | |
| 63 | 63 | $this->validate($manager); |
| 64 | - | |
| 64 | + | |
| 65 | 65 | $permissions = Arr::get($manager, 'permissions', []); |
| 66 | - | |
| 66 | + | |
| 67 | + // Defense in depth: only a full WordPress administrator may grant the | |
| 68 | + // crown-jewel `fluentform_full_access` permission. A holder of that ACL | |
| 69 | + // permission is not necessarily a WP admin (managers can be delegated), | |
| 70 | + // so without this a compromised full-access session — e.g. a script | |
| 71 | + // running via XSS in a privileged session — could mint new full-access | |
| 72 | + // managers and escalate. Lesser permission grants are unaffected. | |
| 73 | + if (in_array('fluentform_full_access', (array) $permissions, true) && !current_user_can('manage_options')) { | |
| 74 | + $permissions = array_values(array_filter((array) $permissions, function ($permission) { | |
| 75 | + return 'fluentform_full_access' !== $permission; | |
| 76 | + })); | |
| 77 | + } | |
| 78 | + | |
| 67 | 79 | $user = get_user_by('email', $manager['email']); |
| 68 | - | |
| 80 | + | |
| 69 | 81 | if (!$user) { |
| 70 | 82 | throw new ValidationException('', 0, null, ['message' => 'Please Provide Valid Email']); |
| 71 | 83 | } |
| 72 | - | |
| 84 | + | |
| 73 | 85 | Acl::attachPermissions($user, $permissions); |
| 74 | - | |
| 86 | + | |
| 75 | 87 | update_user_meta($user->ID, '_fluent_forms_has_role', 1); |
| 76 | - | |
| 88 | + | |
| 77 | 89 | $hasSpecificFormsPermission = 'yes' === Arr::get($manager, 'has_specific_forms_permission'); |
| 78 | 90 | $allowedForms = array_values(array_filter(array_map('intval', (array) Arr::get($manager, 'forms', [])))); |
| 79 | 91 | |
| 92 | + // The UI always sends this flag, so an absent one is a partial payload, not a request to clear. | |
| 93 | + $restrictionWasSubmitted = null !== Arr::get($manager, 'has_specific_forms_permission'); | |
| 94 | + | |
| 80 | 95 | // Keep an empty selection unrestricted so the manager UI's |
| 81 | 96 | // "leave blank for all forms" behavior matches the saved ACL state. |
| 82 | 97 | if ($hasSpecificFormsPermission && $allowedForms) { |
| 83 | 98 | FormManagerService::updateHasSpecificFormsPermission($user->ID, 'yes'); |
| 84 | 99 | FormManagerService::addUserAllowedForms($allowedForms, $user->ID); |
| 85 | - } else { | |
| 100 | + } elseif ($restrictionWasSubmitted) { | |
| 86 | 101 | FormManagerService::updateHasSpecificFormsPermission($user->ID, 'no'); |
| 87 | 102 | FormManagerService::deleteUserAllowedForms($user->ID); |
| 88 | 103 | } |
| 89 | 104 | |
| @@ -91,46 +106,60 @@ | ||
| 91 | 106 | 'id' => $user->ID, |
| 92 | 107 | 'first_name' => $user->first_name, |
| 93 | 108 | 'last_name' => $user->last_name, |
| 94 | 109 | 'email' => $user->user_email, |
| 95 | - 'permissions' => Acl::getUserPermissions($user) | |
| 110 | + 'permissions' => Acl::getUserPermissions($user), | |
| 96 | 111 | ]; |
| 97 | - | |
| 112 | + | |
| 98 | 113 | return ([ |
| 99 | 114 | 'message' => __('Manager has been saved.', 'fluentform'), |
| 100 | - 'manager' => $updatedUser | |
| 115 | + 'manager' => $updatedUser, | |
| 101 | 116 | ]); |
| 102 | 117 | } |
| 103 | - | |
| 118 | + | |
| 104 | 119 | public function removeManager($attributes = []) |
| 105 | 120 | { |
| 106 | 121 | $userID = intval(Arr::get($attributes, 'id')); |
| 107 | 122 | $user = get_user_by('ID', $userID); |
| 108 | - | |
| 123 | + | |
| 109 | 124 | if (!$user) { |
| 110 | 125 | return ([ |
| 111 | 126 | 'message' => __('Associate user could not be found', 'fluentform'), |
| 112 | 127 | ]); |
| 113 | 128 | } |
| 114 | - | |
| 129 | + | |
| 130 | + // Removal deletes _fluent_forms_has_role, which is what suppresses the role fallback. | |
| 131 | + if (get_current_user_id() === $user->ID) { | |
| 132 | + return ([ | |
| 133 | + 'message' => __('You cannot remove yourself as a manager.', 'fluentform'), | |
| 134 | + ]); | |
| 135 | + } | |
| 136 | + | |
| 137 | + // Mirrors the grant-side guard at :73. | |
| 138 | + if (in_array('fluentform_full_access', (array) Acl::getUserPermissions($user), true) && !current_user_can('manage_options')) { | |
| 139 | + return ([ | |
| 140 | + 'message' => __('You do not have permission to remove a full access manager.', 'fluentform'), | |
| 141 | + ]); | |
| 142 | + } | |
| 143 | + | |
| 115 | 144 | Acl::attachPermissions($user, []); |
| 116 | - | |
| 145 | + | |
| 117 | 146 | delete_user_meta($user->ID, '_fluent_forms_has_role'); |
| 118 | - | |
| 147 | + | |
| 119 | 148 | $deletedUser = [ |
| 120 | 149 | 'id' => $user->ID, |
| 121 | 150 | 'first_name' => $user->first_name, |
| 122 | 151 | 'last_name' => $user->last_name, |
| 123 | 152 | 'email' => $user->user_email, |
| 124 | - 'permissions' => Acl::getUserPermissions($user) | |
| 153 | + 'permissions' => Acl::getUserPermissions($user), | |
| 125 | 154 | ]; |
| 126 | - | |
| 155 | + | |
| 127 | 156 | return ([ |
| 128 | 157 | 'message' => __('Manager has been removed.', 'fluentform'), |
| 129 | - 'manager' => $deletedUser | |
| 158 | + 'manager' => $deletedUser, | |
| 130 | 159 | ]); |
| 131 | 160 | } |
| 132 | - | |
| 161 | + | |
| 133 | 162 | private function validate($manager) |
| 134 | 163 | { |
| 135 | 164 | $rules = [ |
| 136 | 165 | 'permissions' => 'required', |
| @@ -135,21 +164,21 @@ | ||
| 135 | 164 | $rules = [ |
| 136 | 165 | 'permissions' => 'required', |
| 137 | 166 | 'email' => 'required|email', |
| 138 | 167 | ]; |
| 139 | - | |
| 168 | + | |
| 140 | 169 | $validatorInstance = new Validator(); |
| 141 | 170 | $validator = $validatorInstance->make($manager, $rules); |
| 142 | - | |
| 171 | + | |
| 143 | 172 | $errors = null; |
| 144 | - | |
| 173 | + | |
| 145 | 174 | if ($validator->validate()->fails()) { |
| 146 | 175 | $errors = $validator->errors(); |
| 147 | 176 | } |
| 148 | - | |
| 177 | + | |
| 149 | 178 | if (!isset($errors['email'])) { |
| 150 | 179 | $user = get_user_by('email', $manager['email']); |
| 151 | - | |
| 180 | + | |
| 152 | 181 | if (!$user) { |
| 153 | 182 | $errors['email'] = [ |
| 154 | 183 | 'no_user' => __('We could not found any user with this email.', 'fluentform'), |
| 155 | 184 | ]; |
| @@ -154,12 +183,12 @@ | ||
| 154 | 183 | 'no_user' => __('We could not found any user with this email.', 'fluentform'), |
| 155 | 184 | ]; |
| 156 | 185 | } |
| 157 | 186 | } |
| 158 | - | |
| 187 | + | |
| 159 | 188 | if (!isset($errors['permissions'])) { |
| 160 | 189 | $message = $this->dependencyValidate($manager['permissions']); |
| 161 | - | |
| 190 | + | |
| 162 | 191 | if ($message) { |
| 163 | 192 | $errors['permissions'] = [ |
| 164 | 193 | 'dependency' => $message, |
| 165 | 194 | ]; |
| @@ -164,12 +193,12 @@ | ||
| 164 | 193 | 'dependency' => $message, |
| 165 | 194 | ]; |
| 166 | 195 | } |
| 167 | 196 | } |
| 168 | - | |
| 197 | + | |
| 169 | 198 | if ($errors) { |
| 170 | 199 | // Escape all error messages before throwing |
| 171 | - $escapedErrors = array_map(function($errorMessages) { | |
| 200 | + $escapedErrors = array_map(function ($errorMessages) { | |
| 172 | 201 | if (is_array($errorMessages)) { |
| 173 | 202 | return array_map('esc_html', $errorMessages); |
| 174 | 203 | } |
| 175 | 204 | return esc_html($errorMessages); |
| @@ -180,24 +209,24 @@ | ||
| 180 | 209 | 'errors' => $escapedErrors, |
| 181 | 210 | ]); |
| 182 | 211 | } |
| 183 | 212 | } |
| 184 | - | |
| 213 | + | |
| 185 | 214 | private function dependencyValidate($permissions) |
| 186 | 215 | { |
| 187 | 216 | $allPermissions = Acl::getReadablePermissions(); |
| 188 | - | |
| 217 | + | |
| 189 | 218 | foreach ($permissions as $permission) { |
| 190 | 219 | $depends = Arr::get($allPermissions, $permission . '.depends', []); |
| 191 | - | |
| 220 | + | |
| 192 | 221 | if ($depends && $more = array_values(array_diff($depends, $permissions))) { |
| 193 | 222 | $message = $allPermissions[$permission]['title'] . ' requires permission: '; |
| 194 | - | |
| 223 | + | |
| 195 | 224 | foreach ($more as $i => $p) { |
| 196 | 225 | $joiner = $i ? ', ' : ''; |
| 197 | 226 | $message = $message . $joiner . $allPermissions[$p]['title']; |
| 198 | 227 | } |
| 199 | - | |
| 228 | + | |
| 200 | 229 | return $message; |
| 201 | 230 | } |
| 202 | 231 | } |
| 203 | 232 | } |