diff --git a/api/v1/invitations/InvitationController.php b/api/v1/invitations/InvitationController.php index fc508078dab..6dac52e678a 100644 --- a/api/v1/invitations/InvitationController.php +++ b/api/v1/invitations/InvitationController.php @@ -136,7 +136,7 @@ public function getGroupRoutes(): void Route::post('add/{type}', $this->add(...)) ->name('invitation.add'); - + Route::put('{invitationId}/populate', $this->populate(...)) ->name('invitation.populate') ->whereNumber('invitationId'); @@ -272,9 +272,9 @@ public function add(Request $illuminateRequest): JsonResponse 'inviteeEmail.prohibited' => __('invitation.api.error.initialization.noUserIdAndEmailTogether'), 'userId.prohibited' => __('invitation.api.error.initialization.noUserIdAndEmailTogether') ]; - + $validator = ValidatorFactory::make( - $payload, + $payload, $rules, $messages ); @@ -345,11 +345,13 @@ public function getMany(Request $illuminateRequest): JsonResponse ->when($context, fn($query) => $query->byContextId($context->getId())) ->stillActive(); + $itemsMax = $query->clone()->count(); + // Delegate to the specific handler for additional logic $specificData = $this->selectedHandler->getMany($illuminateRequest, $query); return response()->json([ - 'itemsMax' => $query->count(), + 'itemsMax' => $itemsMax, 'items' => $specificData, ], Response::HTTP_OK); } diff --git a/classes/security/authorization/UserRolesRequiredPolicy.php b/classes/security/authorization/UserRolesRequiredPolicy.php index 23bbd7d1327..f216dee1416 100644 --- a/classes/security/authorization/UserRolesRequiredPolicy.php +++ b/classes/security/authorization/UserRolesRequiredPolicy.php @@ -56,9 +56,11 @@ public function effect(): int $userGroups = UserGroup::withUserIds([$user->getId()]) ->withContextIds($context ? [$context->getId(), Application::SITE_CONTEXT_ID] : [Application::SITE_CONTEXT_ID]) + ->whereHas('userUserGroups', function ($query) use ($user) { + $query->withUserId($user->getId())->withActive(); + }) ->get() ->all(); - $roleIds = array_map(fn ($userGroup) => $userGroup->roleId, $userGroups); $this->addAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES, $roleIds); $this->addAuthorizedContextObject(Application::ASSOC_TYPE_USER_GROUP, $userGroups); diff --git a/classes/user/maps/Schema.php b/classes/user/maps/Schema.php index 0de9ac848d6..313d7bf8dfd 100644 --- a/classes/user/maps/Schema.php +++ b/classes/user/maps/Schema.php @@ -131,7 +131,6 @@ protected function mapByProperties(array $props, User $user, array $auxiliaryDat case 'canLoginAs': $output[$prop] = $this->getPropertyCanLoginAs($user); break; - case 'canMergeUsers': $output[$prop] = $this->getPropertyCanMergeUsers($user); break; @@ -175,22 +174,22 @@ protected function mapByProperties(array $props, User $user, array $auxiliaryDat $output[$prop] = []; foreach ($userGroups as $userGroup) { - $output[$prop][] = [ - 'id' => (int) $userGroup->id, - 'name' => $userGroup->getLocalizedData('name'), - 'abbrev' => $userGroup->getLocalizedData('abbrev'), - 'roleId' => (int) $userGroup->roleId, - 'showTitle' => (bool) $userGroup->showTitle, - 'permitSelfRegistration' => (bool) $userGroup->permitSelfRegistration, - 'permitMetadataEdit' => (bool) $userGroup->permitMetadataEdit, - 'recommendOnly' => (bool) $userGroup->recommendOnly, - 'dateStart' => UserUserGroup::withUserId($user->getId()) - ->withUserGroupIds([$userGroup->id]) - ->pluck('date_start')->first(), - 'dateEnd' => UserUserGroup::withUserId($user->getId()) - ->withUserGroupIds([$userGroup->id]) - ->pluck('date_end')->first(), - ]; + $userUserGroup = UserUserGroup::withUserId($user->getId()) + ->withUserGroupIds([$userGroup->id])->get()->toArray(); + foreach ($userUserGroup as $userUserGroupItem) { + $output[$prop][] = [ + 'id' => (int) $userGroup->id, + 'name' => $userGroup->getLocalizedData('name'), + 'abbrev' => $userGroup->getLocalizedData('abbrev'), + 'roleId' => (int) $userGroup->roleId, + 'showTitle' => (bool) $userGroup->showTitle, + 'permitSelfRegistration' => (bool) $userGroup->permitSelfRegistration, + 'permitMetadataEdit' => (bool) $userGroup->permitMetadataEdit, + 'recommendOnly' => (bool) $userGroup->recommendOnly, + 'dateStart' => $userUserGroupItem['dateStart'], + 'dateEnd' => $userUserGroupItem['dateEnd'], + ]; + } } } break; diff --git a/controllers/grid/settings/user/UserGridHandler.php b/controllers/grid/settings/user/UserGridHandler.php index c839d195ad0..4684178bb45 100644 --- a/controllers/grid/settings/user/UserGridHandler.php +++ b/controllers/grid/settings/user/UserGridHandler.php @@ -92,23 +92,6 @@ public function initialize($request, $args = null) // Basic grid configuration. $this->setTitle('grid.user.currentUsers'); - // Grid actions. - $router = $request->getRouter(); - - $this->addAction( - new LinkAction( - 'addUser', - new AjaxModal( - $router->url($request, null, null, 'addUser', null, null), - __('grid.user.add'), - null, - true - ), - __('grid.user.add'), - 'add_user' - ) - ); - // // Grid columns. // @@ -171,7 +154,7 @@ public function getTemplateVarsFromRow($row): array ->withActive(); }) ->get(); - + $roles = $userGroups->map(fn (UserGroup $userGroup) => $userGroup->getLocalizedData('name'))->join(__('common.commaListSeparator')); return ['label' => $roles]; } @@ -550,18 +533,18 @@ public function removeUser($args, $request) if (!$request->checkCSRF()) { return new JSONMessage(false); } - + $context = $request->getContext(); $user = $request->getUser(); - + // Identify the user Id. $userId = $request->getUserVar('rowId'); - + if ($userId !== null && Validation::getAdministrationLevel($userId, $user->getId(), $context->getId()) === Validation::ADMINISTRATION_PROHIBITED) { // We don't have administrative rights over this user. return new JSONMessage(false, __('grid.user.cannotAdminister')); } - + // Check if this user has any active user group assignments for this context. $activeUserGroupCount = UserGroup::query() ->withContextIds($context->getId()) @@ -570,7 +553,7 @@ public function removeUser($args, $request) ->withActive(); }) ->count(); - + if (!$activeUserGroupCount) { return new JSONMessage(false, __('grid.user.userNoRoles')); } else { @@ -582,7 +565,7 @@ public function removeUser($args, $request) $query->withContextIds($context->getId()); }) ->update(['date_end' => now()]); - + return \PKP\db\DAO::getDataChangedEvent($userId); } } diff --git a/controllers/grid/users/author/AuthorGridRow.php b/controllers/grid/users/author/AuthorGridRow.php index c8ea6433f93..acf2b0a1c8a 100644 --- a/controllers/grid/users/author/AuthorGridRow.php +++ b/controllers/grid/users/author/AuthorGridRow.php @@ -101,24 +101,6 @@ public function initialize($request, $template = null) 'delete' ) ); - - $author = Repo::author()->get((int) $rowId, $this->getPublication()->getId()); - - if ($author && !Repo::user()->getByEmail($author->getEmail(), true)) { - $this->addAction( - new LinkAction( - 'addUser', - new AjaxModal( - $router->url($request, null, null, 'addUser', null, $actionArgs), - __('grid.user.add'), - null, - true - ), - __('grid.user.add'), - 'add_user' - ) - ); - } } } } diff --git a/locale/en/user.po b/locale/en/user.po index 088508bcfc9..23a0de6acf6 100644 --- a/locale/en/user.po +++ b/locale/en/user.po @@ -855,7 +855,7 @@ msgid "orcid.field.unverified.shouldRequest" msgstr "This ORCID has not been verified. Please remove this unverified ORCID and request verification from the user/author directly." msgid "user.removeRole.message" -msgstr "Are you sure want remove this role permanently?" +msgstr "Are you sure you want to permanently remove this role? This action will revoke the user's access to all information and permissions associated with this role." msgid "user.futureRole.notification.message" msgstr "Your role is scheduled to begin on {$roleStartDate}" @@ -874,3 +874,6 @@ msgstr "Enable {$fullName}" msgid "user.bioStatement" msgstr "Bio Statement" + +msgid "user.removeRole.roleRemainMessage" +msgstr "You cannot remove the role. At least one role must be assigned to the user." diff --git a/pages/invitation/InvitationHandler.php b/pages/invitation/InvitationHandler.php index ef4a82af642..9dadf8f57a4 100644 --- a/pages/invitation/InvitationHandler.php +++ b/pages/invitation/InvitationHandler.php @@ -17,18 +17,21 @@ namespace PKP\pages\invitation; use APP\core\Application; +use APP\core\PageRouter; use APP\core\Request; use APP\facades\Repo; use APP\handler\Handler; use APP\template\TemplateManager; use PKP\context\Context; -use PKP\core\PKPApplication; +use PKP\core\PKPRequest; use PKP\facades\Locale; -use PKP\i18n\LocaleMetadata; use PKP\invitation\core\enums\InvitationAction; use PKP\invitation\core\Invitation; use PKP\invitation\stepTypes\SendInvitationStep; -use PKP\user\User; +use PKP\security\authorization\ContextAccessPolicy; +use PKP\security\authorization\PolicySet; +use PKP\security\authorization\RoleBasedHandlerOperationPolicy; +use PKP\security\Role; use PKP\userGroup\relationships\UserUserGroup; class InvitationHandler extends Handler @@ -38,6 +41,34 @@ class InvitationHandler extends Handler public const REPLY_OP_ACCEPT = 'accept'; public const REPLY_OP_DECLINE = 'decline'; + /** + * @see PKPHandler::authorize() + * + * @param PKPRequest $request + * @param array $args + * @param array $roleAssignments + */ + public function authorize($request, &$args, $roleAssignments) + { + /** @var PageRouter */ + $router = $request->getRouter(); + $op = $router->getRequestedOp($request); + $rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES); + $rolePolicy->addPolicy( + new RoleBasedHandlerOperationPolicy( + $request, + [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER], + ['invite', 'editUser'] + ) + ); + $this->addPolicy($rolePolicy); + + if (in_array($op, ['accept', 'decline'])) { + return true; + } + return parent::authorize($request, $args, $roleAssignments); + } + /** * Accept invitation handler */ @@ -155,8 +186,8 @@ public function invite(array $args, Request $request): void $invitationModel = $invitation->invitationModel->toArray(); $invitationMode = 'edit'; - $payload['email']=$invitationModel['email']; - $invitationData = $this->generateInvitationPayload($invitationModel['userId'],$payload,$request->getContext())['invitationPayload']; + $payload['email'] = $invitationModel['email']; + $invitationData = $this->generateInvitationPayload($invitationModel['userId'], $payload, $request->getContext()); $user = $invitationData['user']; $invitationPayload = $invitationData['invitationPayload']; } @@ -206,7 +237,8 @@ public function invite(array $args, Request $request): void 'pageTitleDescription' => $invitation ? __( 'invitation.wizard.viewPageTitleDescription', - ['name' => $invitationPayload['givenName'][Locale::getLocale()]] + ['name' => $invitationPayload['givenName'][Locale::getLocale()] ? + $invitationPayload['givenName'][Locale::getLocale()] : $invitationPayload['inviteeEmail']] ) : __('invitation.wizard.pageTitleDescription'), ]); @@ -228,9 +260,9 @@ public function invite(array $args, Request $request): void public function editUser($args, $request): void { $invitation = null; - if(!empty($args)) { + if (!empty($args)) { $invitationMode = 'editUser'; - $invitationData = $this->generateInvitationPayload($args[0],[],$request->getContext()); + $invitationData = $this->generateInvitationPayload($args[0], [], $request->getContext()); $user = $invitationData['user']; $invitationPayload = $invitationData['invitationPayload']; $templateMgr = TemplateManager::getManager($request); @@ -257,7 +289,7 @@ public function editUser($args, $request): void ]; $steps = new SendInvitationStep(); $templateMgr->setState([ - 'steps' => $steps->getSteps($invitation, $context,$user), + 'steps' => $steps->getSteps($invitation, $context, $user), 'emailTemplatesApiUrl' => $request ->getDispatcher() ->url( @@ -295,7 +327,7 @@ public function editUser($args, $request): void * @param Context $context * @param int $id */ - private function getUserUserGroups(int $id , Context $context): array + private function getUserUserGroups(int $id, Context $context): array { $userGroups = []; $userUserGroups = UserUserGroup::query() @@ -325,11 +357,11 @@ private function getUserUserGroups(int $id , Context $context): array private function generateInvitationPayload($userId, array $payload, Context $context): array { $user = null; - if($userId){ - $user = Repo::user()->get($userId,true); + if ($userId) { + $user = Repo::user()->get($userId, true); } - $invitationPayload =[]; + $invitationPayload = []; $invitationPayload['userId'] = $user ? $user->getId() : $userId; $invitationPayload['inviteeEmail'] = $user ? $user->getEmail() : $payload['email']; $invitationPayload['orcid'] = $user ? $user->getData('orcid') : $payload['orcid']; @@ -341,12 +373,12 @@ private function generateInvitationPayload($userId, array $payload, Context $con $invitationPayload['phone'] = $user?->getPhone(); $invitationPayload['mailingAddress'] = $user?->getMailingAddress(); $invitationPayload['signature'] = $user?->getSignature(null); - $invitationPayload['locales'] = $user? $this->getWorkingLanguages($context,$user->getLocales()) : null; + $invitationPayload['locales'] = $user ? $this->getWorkingLanguages($context, $user->getLocales()) : null; $invitationPayload['reviewInterests'] = $user?->getInterestString(); $invitationPayload['homePageUrl'] = $user?->getUrl(); $invitationPayload['disabled'] = $user?->getData('disabled'); $invitationPayload['userGroupsToAdd'] = !$payload['userGroupsToAdd'] ? [] : $payload['userGroupsToAdd']; - $invitationPayload['currentUserGroups'] = !$userId ? [] : $this->getUserUserGroups($userId,$context); + $invitationPayload['currentUserGroups'] = !$userId ? [] : $this->getUserUserGroups($userId, $context); $invitationPayload['userGroupsToRemove'] = []; $invitationPayload['emailComposer'] = [ 'emailBody' => '', @@ -364,7 +396,7 @@ private function generateInvitationPayload($userId, array $payload, Context $con * @param $userLocales * @return string */ - private function getWorkingLanguages(Context $context,$userLocales): string + private function getWorkingLanguages(Context $context, $userLocales): string { $locales = $context->getSupportedLocaleNames(); return join(__('common.commaListSeparator'), array_map(fn($key) => $locales[$key], $userLocales));