From 92b7dd3335a6572debeacfb5faa82c63a5e67888 Mon Sep 17 00:00:00 2001 From: Marvin Borner Date: Fri, 8 Jun 2018 20:03:25 +0200 Subject: Some minor fixes --- .../admin/src/Controller/UserController.php | 3032 ++++++++++---------- 1 file changed, 1516 insertions(+), 1516 deletions(-) (limited to 'main/app/sprinkles/admin/src/Controller/UserController.php') diff --git a/main/app/sprinkles/admin/src/Controller/UserController.php b/main/app/sprinkles/admin/src/Controller/UserController.php index 45f9919..d66f2f1 100644 --- a/main/app/sprinkles/admin/src/Controller/UserController.php +++ b/main/app/sprinkles/admin/src/Controller/UserController.php @@ -1,1516 +1,1516 @@ -getParsedBody(); - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled page - if (!$authorizer->checkAccess($currentUser, 'create_user')) { - throw new ForbiddenException(); - } - - /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ - $ms = $this->ci->alerts; - - // Load the request schema - $schema = new RequestSchema('schema://requests/user/create.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - $error = FALSE; - - // Validate request data - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - $error = TRUE; - } - - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - // Check if username or email already exists - if ($classMapper->staticMethod('user', 'findUnique', $data['user_name'], 'user_name')) { - $ms->addMessageTranslated('danger', 'USERNAME.IN_USE', $data); - $error = TRUE; - } - - if ($classMapper->staticMethod('user', 'findUnique', $data['email'], 'email')) { - $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data); - $error = TRUE; - } - - if ($error) { - return $response->withStatus(400); - } - - /** @var UserFrosting\Config\Config $config */ - $config = $this->ci->config; - - // If currentUser does not have permission to set the group, but they try to set it to something other than their own group, - // throw an exception. - if (!$authorizer->checkAccess($currentUser, 'create_user_field', [ - 'fields' => ['group'] - ])) { - if (isset($data['group_id']) && $data['group_id'] != $currentUser->group_id) { - throw new ForbiddenException(); - } - } - - // In any case, set the group id if not otherwise set - if (!isset($data['group_id'])) { - $data['group_id'] = $currentUser->group_id; - } - - $data['flag_verified'] = 1; - // Set password as empty on initial creation. We will then send email so new user can set it themselves via a verification token - $data['password'] = ''; - - // All checks passed! log events/activities, create user, and send verification email (if required) - // Begin transaction - DB will be rolled back if an exception occurs - Capsule::transaction(function () use ($classMapper, $data, $ms, $config, $currentUser) { - // Create the user - $user = $classMapper->createInstance('user', $data); - - // Store new user to database - $user->save(); - - // Create activity record - $this->ci->userActivityLogger->info("User {$currentUser->user_name} created a new account for {$user->user_name}.", [ - 'type' => 'account_create', - 'user_id' => $currentUser->id - ]); - - // Load default roles - $defaultRoleSlugs = $classMapper->staticMethod('role', 'getDefaultSlugs'); - $defaultRoles = $classMapper->staticMethod('role', 'whereIn', 'slug', $defaultRoleSlugs)->get(); - $defaultRoleIds = $defaultRoles->pluck('id')->all(); - - // Attach default roles - $user->roles()->attach($defaultRoleIds); - - // Try to generate a new password request - $passwordRequest = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.create']); - - // Create and send welcome email with password set link - $message = new TwigMailMessage($this->ci->view, 'mail/password-create.html.twig'); - - $message->from($config['address_book.admin']) - ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) - ->addParams([ - 'user' => $user, - 'create_password_expiration' => $config['password_reset.timeouts.create'] / 3600 . ' hours', - 'token' => $passwordRequest->getToken() - ]); - - $this->ci->mailer->send($message); - - $ms->addMessageTranslated('success', 'USER.CREATED', $data); - }); - - return $response->withStatus(200); - } - - /** - * Processes the request to send a user a password reset email. - * - * Processes the request from the user update form, checking that: - * 1. The target user's new email address, if specified, is not already in use; - * 2. The logged-in user has the necessary permissions to update the posted field(s); - * 3. We're not trying to disable the master account; - * 4. The submitted data is valid. - * This route requires authentication. - * Request type: POST - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - public function createPasswordReset($request, $response, $args) { - // Get the username from the URL - $user = $this->getUserFromParams($args); - - if (!$user) { - throw new NotFoundException($request, $response); - } - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled resource - check that currentUser has permission to edit "password" for this user - if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ - 'user' => $user, - 'fields' => ['password'] - ])) { - throw new ForbiddenException(); - } - - /** @var UserFrosting\Config\Config $config */ - $config = $this->ci->config; - - /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ - $ms = $this->ci->alerts; - - // Begin transaction - DB will be rolled back if an exception occurs - Capsule::transaction(function () use ($user, $config) { - - // Create a password reset and shoot off an email - $passwordReset = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.reset']); - - // Create and send welcome email with password set link - $message = new TwigMailMessage($this->ci->view, 'mail/password-reset.html.twig'); - - $message->from($config['address_book.admin']) - ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) - ->addParams([ - 'user' => $user, - 'token' => $passwordReset->getToken(), - 'request_date' => Carbon::now()->format('Y-m-d H:i:s') - ]); - - $this->ci->mailer->send($message); - }); - - $ms->addMessageTranslated('success', 'PASSWORD.FORGET.REQUEST_SENT', [ - 'email' => $user->email - ]); - return $response->withStatus(200); - } - - - /** - * Sets the users public key - * Request type: POST - * @throws NotFoundException - * @throws BadRequestException - * @throws ForbiddenException - */ - public function setPublicKey($request, $response, $args) { - $requestedUser = $this->getUserFromParams($args); - - if (!$requestedUser) { - throw new NotFoundException($request, $response); - } - - $PublicKey = $request->getParsedBody()["PublicKey"]; - - if ($this->ci->currentUser->id === $requestedUser->id && (Capsule::table('public_keys') - ->where('user_id', "=", $requestedUser->id) - ->exists()) === FALSE) { - Capsule::table('public_keys') - ->insert(['user_id' => $requestedUser->id, 'key' => substr(substr($PublicKey, 100), 0, -40)]); - return $response->withStatus(200); - } else if ($this->ci->currentUser->id === $requestedUser->id) { - Capsule::table('public_keys') - ->where('user_id', $requestedUser->id) - ->update(['key' => substr(substr($PublicKey, 100), 0, -40)]); - return $response->withStatus(200); - } else { - throw new ForbiddenException(); - } - } - - /** - * Processes the request to delete an existing user. - * - * Deletes the specified user, removing any existing associations. - * Before doing so, checks that: - * 1. You are not trying to delete the master account; - * 2. You have permission to delete the target user's account. - * This route requires authentication (and should generally be limited to admins or the root user). - * Request type: DELETE - * @throws BadRequestException - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - public function delete($request, $response, $args) { - $user = $this->getUserFromParams($args); - - // If the user doesn't exist, return 404 - if (!$user) { - throw new NotFoundException($request, $response); - } - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled page - if (!$authorizer->checkAccess($currentUser, 'delete_user', [ - 'user' => $user - ])) { - throw new ForbiddenException(); - } - - /** @var UserFrosting\Config\Config $config */ - $config = $this->ci->config; - - // Check that we are not deleting the master account - // Need to use loose comparison for now, because some DBs return `id` as a string - if ($user->id == $config['reserved_user_ids.master']) { - $e = new BadRequestException(); - $e->addUserMessage('DELETE_MASTER'); - throw $e; - } - - $userName = $user->user_name; - - // Begin transaction - DB will be rolled back if an exception occurs - Capsule::transaction(function () use ($user, $userName, $currentUser) { - $user->delete(); - unset($user); - - // Create activity record - $this->ci->userActivityLogger->info("User {$currentUser->user_name} deleted the account for {$userName}.", [ - 'type' => 'account_delete', - 'user_id' => $currentUser->id - ]); - }); - - /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ - $ms = $this->ci->alerts; - - $ms->addMessageTranslated('success', 'DELETION_SUCCESSFUL', [ - 'user_name' => $userName - ]); - - return $response->withStatus(200); - } - - /** - * Returns activity history for a single user. - * - * This page requires authentication. - * Request type: GET - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - public function getActivities($request, $response, $args) { - $user = $this->getUserFromParams($args); - - // If the user doesn't exist, return 404 - if (!$user) { - throw new NotFoundException($request, $response); - } - - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - // GET parameters - $params = $request->getQueryParams(); - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled page - if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ - 'user' => $user, - 'property' => 'activities' - ])) { - throw new ForbiddenException(); - } - - $sprunje = $classMapper->createInstance('activity_sprunje', $classMapper, $params); - - $sprunje->extendQuery(function ($query) use ($user) { - return $query->where('user_id', $user->id); - }); - - // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. - // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). - return $sprunje->toResponse($response); - } - - /** - * Returns info for a single user. - * - * This page requires authentication. - * Request type: GET - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - public function getInfo($request, $response, $args) { - $user = $this->getUserFromParams($args); - - // If the user doesn't exist, return 404 - if (!$user) { - throw new NotFoundException($request, $response); - } - - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - // Join user's most recent activity - $user = $classMapper->createInstance('user') - ->where('user_name', $user->user_name) - ->joinLastActivity() - ->with('lastActivity', 'group') - ->first(); - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled page - if (!$authorizer->checkAccess($currentUser, 'uri_user', [ - 'user' => $user - ])) { - throw new ForbiddenException(); - } - - $result = $user->toArray(); - $result["avatar"] = $user->avatar; - - // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. - // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). - return $response->withJson($result, 200, JSON_PRETTY_PRINT); - } - - /** - * Returns a list of Users - * - * Generates a list of users, optionally paginated, sorted and/or filtered. - * This page requires authentication. - * Request type: GET - * @throws ForbiddenException - */ - public function getList($request, $response, $args) { - // GET parameters - $params = $request->getQueryParams(); - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled page - if (!$authorizer->checkAccess($currentUser, 'uri_users')) { - throw new ForbiddenException(); - } - - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - $sprunje = $classMapper->createInstance('user_sprunje', $classMapper, $params); - - // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. - // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). - return $sprunje->toResponse($response); - } - - /** - * Renders the modal form to confirm user deletion. - * - * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages. - * This page requires authentication. - * Request type: GET - * @throws BadRequestException - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - public function getModalConfirmDelete($request, $response, $args) { - // GET parameters - $params = $request->getQueryParams(); - - $user = $this->getUserFromParams($params); - - // If the user doesn't exist, return 404 - if (!$user) { - throw new NotFoundException($request, $response); - } - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled page - if (!$authorizer->checkAccess($currentUser, 'delete_user', [ - 'user' => $user - ])) { - throw new ForbiddenException(); - } - - /** @var UserFrosting\Config\Config $config */ - $config = $this->ci->config; - - // Check that we are not deleting the master account - // Need to use loose comparison for now, because some DBs return `id` as a string - if ($user->id == $config['reserved_user_ids.master']) { - $e = new BadRequestException(); - $e->addUserMessage('DELETE_MASTER'); - throw $e; - } - - return $this->ci->view->render($response, 'modals/confirm-delete-user.html.twig', [ - 'user' => $user, - 'form' => [ - 'action' => "api/users/u/{$user->user_name}", - ] - ]); - } - - /** - * Renders the modal form for creating a new user. - * - * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages. - * If the currently logged-in user has permission to modify user group membership, then the group toggle will be displayed. - * Otherwise, the user will be added to the default group and receive the default roles automatically. - * This page requires authentication. - * Request type: GET - * @throws ForbiddenException - */ - public function getModalCreate($request, $response, $args) { - // GET parameters - $params = $request->getQueryParams(); - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - /** @var UserFrosting\I18n\MessageTranslator $translator */ - $translator = $this->ci->translator; - - // Access-controlled page - if (!$authorizer->checkAccess($currentUser, 'create_user')) { - throw new ForbiddenException(); - } - - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var UserFrosting\Config\Config $config */ - $config = $this->ci->config; - - // Determine form fields to hide/disable - $fields = [ - 'hidden' => ['theme'], - 'disabled' => [] - ]; - - // Get a list of all locales - $locales = $config->getDefined('site.locales.available'); - - // Determine if currentUser has permission to modify the group. If so, show the 'group' dropdown. - // Otherwise, set to the currentUser's group and disable the dropdown. - if ($authorizer->checkAccess($currentUser, 'create_user_field', [ - 'fields' => ['group'] - ])) { - // Get a list of all groups - $groups = $classMapper->staticMethod('group', 'all'); - } else { - // Get the current user's group - $groups = $currentUser->group()->get(); - $fields['disabled'][] = 'group'; - } - - // Create a dummy user to prepopulate fields - $data = [ - 'group_id' => $currentUser->group_id, - 'locale' => $config['site.registration.user_defaults.locale'], - 'theme' => '' - ]; - - $user = $classMapper->createInstance('user', $data); - - // Load validation rules - $schema = new RequestSchema('schema://requests/user/create.yaml'); - $validator = new JqueryValidationAdapter($schema, $this->ci->translator); - - return $this->ci->view->render($response, 'modals/user.html.twig', [ - 'user' => $user, - 'groups' => $groups, - 'locales' => $locales, - 'form' => [ - 'action' => 'api/users', - 'method' => 'POST', - 'fields' => $fields, - 'submit_text' => $translator->translate('CREATE') - ], - 'page' => [ - 'validators' => $validator->rules('json', FALSE) - ] - ]); - } - - /** - * Renders the modal form for editing an existing user. - * - * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages. - * This page requires authentication. - * Request type: GET - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - public function getModalEdit($request, $response, $args) { - // GET parameters - $params = $request->getQueryParams(); - - $user = $this->getUserFromParams($params); - - // If the user doesn't exist, return 404 - if (!$user) { - throw new NotFoundException($request, $response); - } - - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - // Get the user to edit - $user = $classMapper->staticMethod('user', 'where', 'user_name', $user->user_name) - ->with('group') - ->first(); - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled resource - check that currentUser has permission to edit basic fields "name", "email", "locale" for this user - $fieldNames = ['name', 'email', 'locale']; - if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ - 'user' => $user, - 'fields' => $fieldNames - ])) { - throw new ForbiddenException(); - } - - // Get a list of all groups - $groups = $classMapper->staticMethod('group', 'all'); - - /** @var UserFrosting\Config\Config $config */ - $config = $this->ci->config; - - // Get a list of all locales - $locales = $config->getDefined('site.locales.available'); - - // Generate form - $fields = [ - 'hidden' => ['theme'], - 'disabled' => ['user_name'] - ]; - - // Disable group field if currentUser doesn't have permission to modify group - if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ - 'user' => $user, - 'fields' => ['group'] - ])) { - $fields['disabled'][] = 'group'; - } - - // Load validation rules - $schema = new RequestSchema('schema://requests/user/edit-info.yaml'); - $validator = new JqueryValidationAdapter($schema, $this->ci->translator); - - $translator = $this->ci->translator; - - return $this->ci->view->render($response, 'modals/user.html.twig', [ - 'user' => $user, - 'groups' => $groups, - 'locales' => $locales, - 'form' => [ - 'action' => "api/users/u/{$user->user_name}", - 'method' => 'PUT', - 'fields' => $fields, - 'submit_text' => $translator->translate('UPDATE') - ], - 'page' => [ - 'validators' => $validator->rules('json', FALSE) - ] - ]); - } - - /** - * Renders the modal form for editing a user's password. - * - * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages. - * This page requires authentication. - * Request type: GET - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - public function getModalEditPassword($request, $response, $args) { - // GET parameters - $params = $request->getQueryParams(); - - $user = $this->getUserFromParams($params); - - // If the user doesn't exist, return 404 - if (!$user) { - throw new NotFoundException($request, $response); - } - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled resource - check that currentUser has permission to edit "password" field for this user - if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ - 'user' => $user, - 'fields' => ['password'] - ])) { - throw new ForbiddenException(); - } - - // Load validation rules - $schema = new RequestSchema('schema://requests/user/edit-password.yaml'); - $validator = new JqueryValidationAdapter($schema, $this->ci->translator); - - return $this->ci->view->render($response, 'modals/user-set-password.html.twig', [ - 'user' => $user, - 'page' => [ - 'validators' => $validator->rules('json', FALSE) - ] - ]); - } - - /** - * Renders the modal form for editing a user's roles. - * - * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages. - * This page requires authentication. - * Request type: GET - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - public function getModalEditRoles($request, $response, $args) { - // GET parameters - $params = $request->getQueryParams(); - - $user = $this->getUserFromParams($params); - - // If the user doesn't exist, return 404 - if (!$user) { - throw new NotFoundException($request, $response); - } - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled resource - check that currentUser has permission to edit "roles" field for this user - if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ - 'user' => $user, - 'fields' => ['roles'] - ])) { - throw new ForbiddenException(); - } - - return $this->ci->view->render($response, 'modals/user-manage-roles.html.twig', [ - 'user' => $user - ]); - } - - /** - * Returns a list of effective Permissions for a specified User. - * - * Generates a list of permissions, optionally paginated, sorted and/or filtered. - * This page requires authentication. - * Request type: GET - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - public function getPermissions($request, $response, $args) { - $user = $this->getUserFromParams($args); - - // If the user doesn't exist, return 404 - if (!$user) { - throw new NotFoundException($request, $response); - } - - // GET parameters - $params = $request->getQueryParams(); - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled page - if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ - 'user' => $user, - 'property' => 'permissions' - ])) { - throw new ForbiddenException(); - } - - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - $params['user_id'] = $user->id; - $sprunje = $classMapper->createInstance('user_permission_sprunje', $classMapper, $params); - - $response = $sprunje->toResponse($response); - - // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. - // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). - return $response; - } - - /** - * Returns roles associated with a single user. - * - * This page requires authentication. - * Request type: GET - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - public function getRoles($request, $response, $args) { - $user = $this->getUserFromParams($args); - - // If the user doesn't exist, return 404 - if (!$user) { - throw new NotFoundException($request, $response); - } - - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - // GET parameters - $params = $request->getQueryParams(); - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled page - if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ - 'user' => $user, - 'property' => 'roles' - ])) { - throw new ForbiddenException(); - } - - $sprunje = $classMapper->createInstance('role_sprunje', $classMapper, $params); - $sprunje->extendQuery(function ($query) use ($user) { - return $query->forUser($user->id); - }); - - // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. - // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). - return $sprunje->toResponse($response); - } - - /** - * Renders a page displaying a user's information, in read-only mode. - * - * This checks that the currently logged-in user has permission to view the requested user's info. - * It checks each field individually, showing only those that you have permission to view. - * This will also try to show buttons for activating, disabling/enabling, deleting, and editing the user. - * This page requires authentication. - * Request type: GET - * @throws ForbiddenException - * @throws BadRequestException - */ - public function pageInfo($request, $response, $args) { - $user = $this->getUserFromParams($args); - - // If the user no longer exists, forward to main user listing page - if (!$user) { - $usersPage = $this->ci->router->pathFor('uri_users'); - return $response->withRedirect($usersPage, 404); - } - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled page - if (!$authorizer->checkAccess($currentUser, 'uri_user', [ - 'user' => $user - ])) { - throw new ForbiddenException(); - } - - /** @var UserFrosting\Config\Config $config */ - $config = $this->ci->config; - - // Get a list of all locales - $locales = $config->getDefined('site.locales.available'); - - // Determine fields that currentUser is authorized to view - $fieldNames = ['user_name', 'name', 'email', 'locale', 'group', 'roles']; - - // Generate form - $fields = [ - // Always hide these - 'hidden' => ['theme'] - ]; - - // Determine which fields should be hidden - foreach ($fieldNames as $field) { - if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ - 'user' => $user, - 'property' => $field - ])) { - $fields['hidden'][] = $field; - } - } - - // Determine buttons to display - $editButtons = [ - 'hidden' => [] - ]; - - if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ - 'user' => $user, - 'fields' => ['name', 'email', 'locale'] - ])) { - $editButtons['hidden'][] = 'edit'; - } - - if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ - 'user' => $user, - 'fields' => ['flag_enabled'] - ])) { - $editButtons['hidden'][] = 'enable'; - } - - if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ - 'user' => $user, - 'fields' => ['flag_verified'] - ])) { - $editButtons['hidden'][] = 'activate'; - } - - if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ - 'user' => $user, - 'fields' => ['password'] - ])) { - $editButtons['hidden'][] = 'password'; - } - - if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ - 'user' => $user, - 'fields' => ['roles'] - ])) { - $editButtons['hidden'][] = 'roles'; - } - - if (!$authorizer->checkAccess($currentUser, 'delete_user', [ - 'user' => $user - ])) { - $editButtons['hidden'][] = 'delete'; - } - - // Determine widgets to display - $widgets = [ - 'hidden' => [] - ]; - - if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ - 'user' => $user, - 'property' => 'permissions' - ])) { - $widgets['hidden'][] = 'permissions'; - } - - if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ - 'user' => $user, - 'property' => 'activities' - ])) { - $widgets['hidden'][] = 'activities'; - } - - return $this->ci->view->render($response, 'pages/user.html.twig', [ - 'user' => $user, - 'locales' => $locales, - 'fields' => $fields, - 'tools' => $editButtons, - 'widgets' => $widgets - ]); - } - - /** - * Renders the user listing page. - * - * This page renders a table of users, with dropdown menus for admin actions for each user. - * Actions typically include: edit user details, activate user, enable/disable user, delete user. - * This page requires authentication. - * Request type: GET - * @throws ForbiddenException - */ - public function pageList($request, $response, $args) { - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled page - if (!$authorizer->checkAccess($currentUser, 'uri_users')) { - throw new ForbiddenException(); - } - - return $this->ci->view->render($response, 'pages/users.html.twig'); - } - - /** - * Gets the users public key - * Request type: GET - * @throws NotFoundException - * @throws BadRequestException - */ - public function getPublicKey($request, $response, $args) { - $requestedUser = $this->getUserFromParams($args); - - if (!$requestedUser) { - throw new NotFoundException($request, $response); - } - - if ((Capsule::table('public_keys') - ->where('user_id', "=", $requestedUser->id) - ->exists())) { - - $RawPublicKey = Capsule::table('public_keys') - ->where('user_id', "=", $requestedUser->id) - ->value('key'); - $PublicKey = "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: OpenPGP.js v3.0.9\nComment: https://openpgpjs.org\n\n" . $RawPublicKey . "\n-----END PGP PUBLIC KEY BLOCK-----"; - - $ContentType = explode(',', $request->getHeaderLine('Accept'))[0]; - switch ($ContentType) { - case 'application/json': - $response->write(json_encode(array('user_id' => $requestedUser->id, 'PublicKey' => $PublicKey))); - break; - case 'text/html': - $response->write("
" . $PublicKey); - break; - default: - $response->write($PublicKey); - } - return $response->withStatus(200); - } else { - throw new NotFoundException($request, $response); - } - } - - /** - * Gets the users which are following the requested user - * Request type: GET - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - public function getFollowers($request, $response, $args) { - $user = $this->getUserFromParams($args); - - // If the user doesn't exist, return 404 - if (!$user) { - throw new NotFoundException($request, $response); - } - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled page - if (!$authorizer->checkAccess($currentUser, 'uri_user', [ - 'user' => $user - ])) { - throw new ForbiddenException(); - } - - $UsersFollowers = Capsule::table('user_follow') - ->where('user_id', "=", $user->id) - ->join("users", "users.id", "=", "user_follow.followed_by_id") - ->select("user_follow.followed_by_id as id", "users.user_name as username") - ->get(); - - $result = $UsersFollowers->toArray(); - - return $response->withJson($result, 200, JSON_PRETTY_PRINT); - } - - /** - * Get users which the user follows - * Request type: GET - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - public function getFollows($request, $response, $args) { - $user = $this->getUserFromParams($args); - - // If the user doesn't exist, return 404 - if (!$user) { - throw new NotFoundException($request, $response); - } - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled page - if (($user->id == $currentUser->id) || (!$authorizer->checkAccess($currentUser, 'uri_user', [ - 'user' => $user - ]))) { - throw new ForbiddenException(); - } - - $UsersFollows = Capsule::table('user_follow') - ->where('followed_by_id', "=", $user->id) - ->join("users", "users.id", "=", "user_follow.user_id") - ->select("user_follow.user_id as id", "users.user_name as username") - ->get(); - - $result = $UsersFollows->toArray(); - - return $response->withJson($result, 200, JSON_PRETTY_PRINT); - } - - /** - * Get users which the user follows and which are following the user - * Request type: GET - * @throws NotFoundException - * @throws ForbiddenException - * @throws BadRequestException - */ - public function getFriends($request, $response, $args) { - $user = $this->getUserFromParams($args); - - // If the user doesn't exist, return 404 - if (!$user) { - throw new NotFoundException($request, $response); - } - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled page - if (!$authorizer->checkAccess($currentUser, 'uri_user', [ - 'user' => $user - ])) { - throw new ForbiddenException(); - } - - $UsersFriends = Capsule::select("SELECT id FROM (SELECT user_id AS id FROM user_follow WHERE followed_by_id = $user->id UNION ALL SELECT followed_by_id FROM user_follow WHERE user_id = $user->id) t GROUP BY id HAVING COUNT(id) > 1"); - - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - foreach ($UsersFriends as $Key => $UsersFriendId) { // NOT THAT EFFICIENT... - $UsersFriendInformation = $classMapper->createInstance('user')// raw select doesnt work with instance - ->where('id', $UsersFriendId->id) - ->get(); - - $UsersFriends[$Key]->id = $UsersFriendInformation[0]->id; - $UsersFriends[$Key]->username = $UsersFriendInformation[0]->user_name; - $UsersFriends[$Key]->avatar = $UsersFriendInformation[0]->avatar; - $UsersFriends[$Key]->full_name = $UsersFriendInformation[0]->full_name; - } - - $result = $UsersFriends; - - if (sizeof($result) > 0) { // USER HAS FRIENDS - return $response->withJson($result, 200, JSON_PRETTY_PRINT); - } else { - throw new NotFoundException($request, $response); - } - } - - - /** - * Processes the request to update an existing user's basic details (first_name, last_name, email, locale, group_id) - * - * Processes the request from the user update form, checking that: - * 1. The target user's new email address, if specified, is not already in use; - * 2. The logged-in user has the necessary permissions to update the putted field(s); - * 3. The submitted data is valid. - * This route requires authentication. - * Request type: PUT - * @throws NotFoundException - * @throws ForbiddenException - * @throws BadRequestException - * @throws BadRequestException - */ - public function updateInfo($request, $response, $args) { - // Get the username from the URL - $user = $this->getUserFromParams($args); - - if (!$user) { - throw new NotFoundException($request, $response); - } - - /** @var UserFrosting\Config\Config $config */ - $config = $this->ci->config; - - // Get PUT parameters - $params = $request->getParsedBody(); - - /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ - $ms = $this->ci->alerts; - - // Load the request schema - $schema = new RequestSchema('schema://requests/user/edit-info.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - $error = FALSE; - - // Validate request data - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - $error = TRUE; - } - - // Determine targeted fields - $fieldNames = []; - foreach ($data as $name => $value) { - if ($name == 'first_name' || $name == 'last_name') { - $fieldNames[] = 'name'; - } else if ($name == 'group_id') { - $fieldNames[] = 'group'; - } else { - $fieldNames[] = $name; - } - } - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled resource - check that currentUser has permission to edit submitted fields for this user - if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ - 'user' => $user, - 'fields' => array_values(array_unique($fieldNames)) - ])) { - throw new ForbiddenException(); - } - - // Only the master account can edit the master account! - if ( - ($user->id == $config['reserved_user_ids.master']) && - ($currentUser->id != $config['reserved_user_ids.master']) - ) { - throw new ForbiddenException(); - } - - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - // Check if email already exists - if ( - isset($data['email']) && - $data['email'] != $user->email && - $classMapper->staticMethod('user', 'findUnique', $data['email'], 'email') - ) { - $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data); - $error = TRUE; - } - - if ($error) { - return $response->withStatus(400); - } - - // Begin transaction - DB will be rolled back if an exception occurs - Capsule::transaction(function () use ($data, $user, $currentUser) { - // Update the user and generate success messages - foreach ($data as $name => $value) { - if ($value != $user->$name) { - $user->$name = $value; - } - } - - $user->save(); - - // Create activity record - $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated basic account info for user {$user->user_name}.", [ - 'type' => 'account_update_info', - 'user_id' => $currentUser->id - ]); - }); - - $ms->addMessageTranslated('success', 'DETAILS_UPDATED', [ - 'user_name' => $user->user_name - ]); - return $response->withStatus(200); - } - - /** - * Processes the request to update a specific field for an existing user. - * - * Supports editing all user fields, including password, enabled/disabled status and verification status. - * Processes the request from the user update form, checking that: - * 1. The logged-in user has the necessary permissions to update the putted field(s); - * 2. We're not trying to disable the master account; - * 3. The submitted data is valid. - * This route requires authentication. - * Request type: PUT - * @throws ForbiddenException - * @throws BadRequestException - * @throws BadRequestException - * @throws BadRequestException - * @throws BadRequestException - * @throws BadRequestException - * @throws NotFoundException - * @throws BadRequestException - */ - public function updateField($request, $response, $args) { - // Get the username from the URL - $user = $this->getUserFromParams($args); - - if (!$user) { - throw new NotFoundException($request, $response); - } - - // Get key->value pair from URL and request body - $fieldName = $args['field']; - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ - $authorizer = $this->ci->authorizer; - - /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled resource - check that currentUser has permission to edit the specified field for this user - if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ - 'user' => $user, - 'fields' => [$fieldName] - ])) { - throw new ForbiddenException(); - } - - /** @var UserFrosting\Config\Config $config */ - $config = $this->ci->config; - - // Only the master account can edit the master account! - if ( - ($user->id == $config['reserved_user_ids.master']) && - ($currentUser->id != $config['reserved_user_ids.master']) - ) { - throw new ForbiddenException(); - } - - // Get PUT parameters: value - $put = $request->getParsedBody(); - - if (!isset($put['value'])) { - throw new BadRequestException(); - } - - // Create and validate key -> value pair - $params = [ - $fieldName => $put['value'] - ]; - - // Load the request schema - $schema = new RequestSchema('schema://requests/user/edit-field.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - // Validate, and throw exception on validation errors. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - // encapsulate the communication of error messages from ServerSideValidator to the BadRequestException - $e = new BadRequestException(); - foreach ($validator->errors() as $idx => $field) { - foreach ($field as $eidx => $error) { - $e->addUserMessage($error); - } - } - throw $e; - } - - // Get validated and transformed value - $fieldValue = $data[$fieldName]; - - /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ - $ms = $this->ci->alerts; - - // Special checks and transformations for certain fields - if ($fieldName == 'flag_enabled') { - // Check that we are not disabling the master account - if ( - ($user->id == $config['reserved_user_ids.master']) && - ($fieldValue == '0') - ) { - $e = new BadRequestException(); - $e->addUserMessage('DISABLE_MASTER'); - throw $e; - } else if ( - ($user->id == $currentUser->id) && - ($fieldValue == '0') - ) { - $e = new BadRequestException(); - $e->addUserMessage('DISABLE_SELF'); - throw $e; - } - } else if ($fieldName == 'password') { - $fieldValue = Password::hash($fieldValue); - } - - // Begin transaction - DB will be rolled back if an exception occurs - Capsule::transaction(function () use ($fieldName, $fieldValue, $user, $currentUser) { - if ($fieldName == 'roles') { - $newRoles = collect($fieldValue)->pluck('role_id')->all(); - $user->roles()->sync($newRoles); - } else { - $user->$fieldName = $fieldValue; - $user->save(); - } - - // Create activity record - $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated property '$fieldName' for user {$user->user_name}.", [ - 'type' => 'account_update_field', - 'user_id' => $currentUser->id - ]); - }); - - // Add success messages - if ($fieldName == 'flag_enabled') { - if ($fieldValue == '1') { - $ms->addMessageTranslated('success', 'ENABLE_SUCCESSFUL', [ - 'user_name' => $user->user_name - ]); - } else { - $ms->addMessageTranslated('success', 'DISABLE_SUCCESSFUL', [ - 'user_name' => $user->user_name - ]); - } - } else if ($fieldName == 'flag_verified') { - $ms->addMessageTranslated('success', 'MANUALLY_ACTIVATED', [ - 'user_name' => $user->user_name - ]); - } else { - $ms->addMessageTranslated('success', 'DETAILS_UPDATED', [ - 'user_name' => $user->user_name - ]); - } - - return $response->withStatus(200); - } - - protected function getUserFromParams($params) { - // Load the request schema - $schema = new RequestSchema('schema://requests/user/get-by-username.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - // Validate, and throw exception on validation errors. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $e = new BadRequestException(); - foreach ($validator->errors() as $idx => $field) { - foreach ($field as $eidx => $error) { - $e->addUserMessage($error); - } - } - throw $e; - } - - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - // Get the user to delete - $user = $classMapper->staticMethod('user', 'where', 'user_name', $data['user_name']) - ->first(); - - return $user; - } -} +getParsedBody(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'create_user')) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ + $ms = $this->ci->alerts; + + // Load the request schema + $schema = new RequestSchema('schema://requests/user/create.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + $error = FALSE; + + // Validate request data + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + $error = TRUE; + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Check if username or email already exists + if ($classMapper->staticMethod('user', 'findUnique', $data['user_name'], 'user_name')) { + $ms->addMessageTranslated('danger', 'USERNAME.IN_USE', $data); + $error = TRUE; + } + + if ($classMapper->staticMethod('user', 'findUnique', $data['email'], 'email')) { + $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data); + $error = TRUE; + } + + if ($error) { + return $response->withStatus(400); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // If currentUser does not have permission to set the group, but they try to set it to something other than their own group, + // throw an exception. + if (!$authorizer->checkAccess($currentUser, 'create_user_field', [ + 'fields' => ['group'] + ])) { + if (isset($data['group_id']) && $data['group_id'] != $currentUser->group_id) { + throw new ForbiddenException(); + } + } + + // In any case, set the group id if not otherwise set + if (!isset($data['group_id'])) { + $data['group_id'] = $currentUser->group_id; + } + + $data['flag_verified'] = 1; + // Set password as empty on initial creation. We will then send email so new user can set it themselves via a verification token + $data['password'] = ''; + + // All checks passed! log events/activities, create user, and send verification email (if required) + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($classMapper, $data, $ms, $config, $currentUser) { + // Create the user + $user = $classMapper->createInstance('user', $data); + + // Store new user to database + $user->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} created a new account for {$user->user_name}.", [ + 'type' => 'account_create', + 'user_id' => $currentUser->id + ]); + + // Load default roles + $defaultRoleSlugs = $classMapper->staticMethod('role', 'getDefaultSlugs'); + $defaultRoles = $classMapper->staticMethod('role', 'whereIn', 'slug', $defaultRoleSlugs)->get(); + $defaultRoleIds = $defaultRoles->pluck('id')->all(); + + // Attach default roles + $user->roles()->attach($defaultRoleIds); + + // Try to generate a new password request + $passwordRequest = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.create']); + + // Create and send welcome email with password set link + $message = new TwigMailMessage($this->ci->view, 'mail/password-create.html.twig'); + + $message->from($config['address_book.admin']) + ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) + ->addParams([ + 'user' => $user, + 'create_password_expiration' => $config['password_reset.timeouts.create'] / 3600 . ' hours', + 'token' => $passwordRequest->getToken() + ]); + + $this->ci->mailer->send($message); + + $ms->addMessageTranslated('success', 'USER.CREATED', $data); + }); + + return $response->withStatus(200); + } + + /** + * Processes the request to send a user a password reset email. + * + * Processes the request from the user update form, checking that: + * 1. The target user's new email address, if specified, is not already in use; + * 2. The logged-in user has the necessary permissions to update the posted field(s); + * 3. We're not trying to disable the master account; + * 4. The submitted data is valid. + * This route requires authentication. + * Request type: POST + * @throws ForbiddenException + * @throws NotFoundException + * @throws BadRequestException + */ + public function createPasswordReset($request, $response, $args) { + // Get the username from the URL + $user = $this->getUserFromParams($args); + + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled resource - check that currentUser has permission to edit "password" for this user + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['password'] + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ + $ms = $this->ci->alerts; + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($user, $config) { + + // Create a password reset and shoot off an email + $passwordReset = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.reset']); + + // Create and send welcome email with password set link + $message = new TwigMailMessage($this->ci->view, 'mail/password-reset.html.twig'); + + $message->from($config['address_book.admin']) + ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) + ->addParams([ + 'user' => $user, + 'token' => $passwordReset->getToken(), + 'request_date' => Carbon::now()->format('Y-m-d H:i:s') + ]); + + $this->ci->mailer->send($message); + }); + + $ms->addMessageTranslated('success', 'PASSWORD.FORGET.REQUEST_SENT', [ + 'email' => $user->email + ]); + return $response->withStatus(200); + } + + + /** + * Sets the users public key + * Request type: POST + * @throws NotFoundException + * @throws BadRequestException + * @throws ForbiddenException + */ + public function setPublicKey($request, $response, $args) { + $requestedUser = $this->getUserFromParams($args); + + if (!$requestedUser) { + throw new NotFoundException($request, $response); + } + + $PublicKey = $request->getParsedBody()["PublicKey"]; + + if ($this->ci->currentUser->id === $requestedUser->id && (Capsule::table('public_keys') + ->where('user_id', "=", $requestedUser->id) + ->exists()) === FALSE) { + Capsule::table('public_keys') + ->insert(['user_id' => $requestedUser->id, 'key' => substr(substr($PublicKey, 100), 0, -40)]); + return $response->withStatus(200); + } else if ($this->ci->currentUser->id === $requestedUser->id) { + Capsule::table('public_keys') + ->where('user_id', $requestedUser->id) + ->update(['key' => substr(substr($PublicKey, 100), 0, -40)]); + return $response->withStatus(200); + } else { + throw new ForbiddenException(); + } + } + + /** + * Processes the request to delete an existing user. + * + * Deletes the specified user, removing any existing associations. + * Before doing so, checks that: + * 1. You are not trying to delete the master account; + * 2. You have permission to delete the target user's account. + * This route requires authentication (and should generally be limited to admins or the root user). + * Request type: DELETE + * @throws BadRequestException + * @throws ForbiddenException + * @throws NotFoundException + * @throws BadRequestException + */ + public function delete($request, $response, $args) { + $user = $this->getUserFromParams($args); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'delete_user', [ + 'user' => $user + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Check that we are not deleting the master account + // Need to use loose comparison for now, because some DBs return `id` as a string + if ($user->id == $config['reserved_user_ids.master']) { + $e = new BadRequestException(); + $e->addUserMessage('DELETE_MASTER'); + throw $e; + } + + $userName = $user->user_name; + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($user, $userName, $currentUser) { + $user->delete(); + unset($user); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} deleted the account for {$userName}.", [ + 'type' => 'account_delete', + 'user_id' => $currentUser->id + ]); + }); + + /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ + $ms = $this->ci->alerts; + + $ms->addMessageTranslated('success', 'DELETION_SUCCESSFUL', [ + 'user_name' => $userName + ]); + + return $response->withStatus(200); + } + + /** + * Returns activity history for a single user. + * + * This page requires authentication. + * Request type: GET + * @throws ForbiddenException + * @throws NotFoundException + * @throws BadRequestException + */ + public function getActivities($request, $response, $args) { + $user = $this->getUserFromParams($args); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ + 'user' => $user, + 'property' => 'activities' + ])) { + throw new ForbiddenException(); + } + + $sprunje = $classMapper->createInstance('activity_sprunje', $classMapper, $params); + + $sprunje->extendQuery(function ($query) use ($user) { + return $query->where('user_id', $user->id); + }); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $sprunje->toResponse($response); + } + + /** + * Returns info for a single user. + * + * This page requires authentication. + * Request type: GET + * @throws ForbiddenException + * @throws NotFoundException + * @throws BadRequestException + */ + public function getInfo($request, $response, $args) { + $user = $this->getUserFromParams($args); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Join user's most recent activity + $user = $classMapper->createInstance('user') + ->where('user_name', $user->user_name) + ->joinLastActivity() + ->with('lastActivity', 'group') + ->first(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_user', [ + 'user' => $user + ])) { + throw new ForbiddenException(); + } + + $result = $user->toArray(); + $result["avatar"] = $user->avatar; + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $response->withJson($result, 200, JSON_PRETTY_PRINT); + } + + /** + * Returns a list of Users + * + * Generates a list of users, optionally paginated, sorted and/or filtered. + * This page requires authentication. + * Request type: GET + * @throws ForbiddenException + */ + public function getList($request, $response, $args) { + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_users')) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $sprunje = $classMapper->createInstance('user_sprunje', $classMapper, $params); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $sprunje->toResponse($response); + } + + /** + * Renders the modal form to confirm user deletion. + * + * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages. + * This page requires authentication. + * Request type: GET + * @throws BadRequestException + * @throws ForbiddenException + * @throws NotFoundException + * @throws BadRequestException + */ + public function getModalConfirmDelete($request, $response, $args) { + // GET parameters + $params = $request->getQueryParams(); + + $user = $this->getUserFromParams($params); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'delete_user', [ + 'user' => $user + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Check that we are not deleting the master account + // Need to use loose comparison for now, because some DBs return `id` as a string + if ($user->id == $config['reserved_user_ids.master']) { + $e = new BadRequestException(); + $e->addUserMessage('DELETE_MASTER'); + throw $e; + } + + return $this->ci->view->render($response, 'modals/confirm-delete-user.html.twig', [ + 'user' => $user, + 'form' => [ + 'action' => "api/users/u/{$user->user_name}", + ] + ]); + } + + /** + * Renders the modal form for creating a new user. + * + * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages. + * If the currently logged-in user has permission to modify user group membership, then the group toggle will be displayed. + * Otherwise, the user will be added to the default group and receive the default roles automatically. + * This page requires authentication. + * Request type: GET + * @throws ForbiddenException + */ + public function getModalCreate($request, $response, $args) { + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + /** @var UserFrosting\I18n\MessageTranslator $translator */ + $translator = $this->ci->translator; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'create_user')) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Determine form fields to hide/disable + $fields = [ + 'hidden' => ['theme'], + 'disabled' => [] + ]; + + // Get a list of all locales + $locales = $config->getDefined('site.locales.available'); + + // Determine if currentUser has permission to modify the group. If so, show the 'group' dropdown. + // Otherwise, set to the currentUser's group and disable the dropdown. + if ($authorizer->checkAccess($currentUser, 'create_user_field', [ + 'fields' => ['group'] + ])) { + // Get a list of all groups + $groups = $classMapper->staticMethod('group', 'all'); + } else { + // Get the current user's group + $groups = $currentUser->group()->get(); + $fields['disabled'][] = 'group'; + } + + // Create a dummy user to prepopulate fields + $data = [ + 'group_id' => $currentUser->group_id, + 'locale' => $config['site.registration.user_defaults.locale'], + 'theme' => '' + ]; + + $user = $classMapper->createInstance('user', $data); + + // Load validation rules + $schema = new RequestSchema('schema://requests/user/create.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'modals/user.html.twig', [ + 'user' => $user, + 'groups' => $groups, + 'locales' => $locales, + 'form' => [ + 'action' => 'api/users', + 'method' => 'POST', + 'fields' => $fields, + 'submit_text' => $translator->translate('CREATE') + ], + 'page' => [ + 'validators' => $validator->rules('json', FALSE) + ] + ]); + } + + /** + * Renders the modal form for editing an existing user. + * + * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages. + * This page requires authentication. + * Request type: GET + * @throws ForbiddenException + * @throws NotFoundException + * @throws BadRequestException + */ + public function getModalEdit($request, $response, $args) { + // GET parameters + $params = $request->getQueryParams(); + + $user = $this->getUserFromParams($params); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Get the user to edit + $user = $classMapper->staticMethod('user', 'where', 'user_name', $user->user_name) + ->with('group') + ->first(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled resource - check that currentUser has permission to edit basic fields "name", "email", "locale" for this user + $fieldNames = ['name', 'email', 'locale']; + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => $fieldNames + ])) { + throw new ForbiddenException(); + } + + // Get a list of all groups + $groups = $classMapper->staticMethod('group', 'all'); + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Get a list of all locales + $locales = $config->getDefined('site.locales.available'); + + // Generate form + $fields = [ + 'hidden' => ['theme'], + 'disabled' => ['user_name'] + ]; + + // Disable group field if currentUser doesn't have permission to modify group + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['group'] + ])) { + $fields['disabled'][] = 'group'; + } + + // Load validation rules + $schema = new RequestSchema('schema://requests/user/edit-info.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + $translator = $this->ci->translator; + + return $this->ci->view->render($response, 'modals/user.html.twig', [ + 'user' => $user, + 'groups' => $groups, + 'locales' => $locales, + 'form' => [ + 'action' => "api/users/u/{$user->user_name}", + 'method' => 'PUT', + 'fields' => $fields, + 'submit_text' => $translator->translate('UPDATE') + ], + 'page' => [ + 'validators' => $validator->rules('json', FALSE) + ] + ]); + } + + /** + * Renders the modal form for editing a user's password. + * + * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages. + * This page requires authentication. + * Request type: GET + * @throws ForbiddenException + * @throws NotFoundException + * @throws BadRequestException + */ + public function getModalEditPassword($request, $response, $args) { + // GET parameters + $params = $request->getQueryParams(); + + $user = $this->getUserFromParams($params); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled resource - check that currentUser has permission to edit "password" field for this user + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['password'] + ])) { + throw new ForbiddenException(); + } + + // Load validation rules + $schema = new RequestSchema('schema://requests/user/edit-password.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'modals/user-set-password.html.twig', [ + 'user' => $user, + 'page' => [ + 'validators' => $validator->rules('json', FALSE) + ] + ]); + } + + /** + * Renders the modal form for editing a user's roles. + * + * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages. + * This page requires authentication. + * Request type: GET + * @throws ForbiddenException + * @throws NotFoundException + * @throws BadRequestException + */ + public function getModalEditRoles($request, $response, $args) { + // GET parameters + $params = $request->getQueryParams(); + + $user = $this->getUserFromParams($params); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled resource - check that currentUser has permission to edit "roles" field for this user + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['roles'] + ])) { + throw new ForbiddenException(); + } + + return $this->ci->view->render($response, 'modals/user-manage-roles.html.twig', [ + 'user' => $user + ]); + } + + /** + * Returns a list of effective Permissions for a specified User. + * + * Generates a list of permissions, optionally paginated, sorted and/or filtered. + * This page requires authentication. + * Request type: GET + * @throws ForbiddenException + * @throws NotFoundException + * @throws BadRequestException + */ + public function getPermissions($request, $response, $args) { + $user = $this->getUserFromParams($args); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ + 'user' => $user, + 'property' => 'permissions' + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $params['user_id'] = $user->id; + $sprunje = $classMapper->createInstance('user_permission_sprunje', $classMapper, $params); + + $response = $sprunje->toResponse($response); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $response; + } + + /** + * Returns roles associated with a single user. + * + * This page requires authentication. + * Request type: GET + * @throws ForbiddenException + * @throws NotFoundException + * @throws BadRequestException + */ + public function getRoles($request, $response, $args) { + $user = $this->getUserFromParams($args); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ + 'user' => $user, + 'property' => 'roles' + ])) { + throw new ForbiddenException(); + } + + $sprunje = $classMapper->createInstance('role_sprunje', $classMapper, $params); + $sprunje->extendQuery(function ($query) use ($user) { + return $query->forUser($user->id); + }); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $sprunje->toResponse($response); + } + + /** + * Renders a page displaying a user's information, in read-only mode. + * + * This checks that the currently logged-in user has permission to view the requested user's info. + * It checks each field individually, showing only those that you have permission to view. + * This will also try to show buttons for activating, disabling/enabling, deleting, and editing the user. + * This page requires authentication. + * Request type: GET + * @throws ForbiddenException + * @throws BadRequestException + */ + public function pageInfo($request, $response, $args) { + $user = $this->getUserFromParams($args); + + // If the user no longer exists, forward to main user listing page + if (!$user) { + $usersPage = $this->ci->router->pathFor('uri_users'); + return $response->withRedirect($usersPage, 404); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_user', [ + 'user' => $user + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Get a list of all locales + $locales = $config->getDefined('site.locales.available'); + + // Determine fields that currentUser is authorized to view + $fieldNames = ['user_name', 'name', 'email', 'locale', 'group', 'roles']; + + // Generate form + $fields = [ + // Always hide these + 'hidden' => ['theme'] + ]; + + // Determine which fields should be hidden + foreach ($fieldNames as $field) { + if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ + 'user' => $user, + 'property' => $field + ])) { + $fields['hidden'][] = $field; + } + } + + // Determine buttons to display + $editButtons = [ + 'hidden' => [] + ]; + + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['name', 'email', 'locale'] + ])) { + $editButtons['hidden'][] = 'edit'; + } + + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['flag_enabled'] + ])) { + $editButtons['hidden'][] = 'enable'; + } + + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['flag_verified'] + ])) { + $editButtons['hidden'][] = 'activate'; + } + + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['password'] + ])) { + $editButtons['hidden'][] = 'password'; + } + + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['roles'] + ])) { + $editButtons['hidden'][] = 'roles'; + } + + if (!$authorizer->checkAccess($currentUser, 'delete_user', [ + 'user' => $user + ])) { + $editButtons['hidden'][] = 'delete'; + } + + // Determine widgets to display + $widgets = [ + 'hidden' => [] + ]; + + if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ + 'user' => $user, + 'property' => 'permissions' + ])) { + $widgets['hidden'][] = 'permissions'; + } + + if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ + 'user' => $user, + 'property' => 'activities' + ])) { + $widgets['hidden'][] = 'activities'; + } + + return $this->ci->view->render($response, 'pages/user.html.twig', [ + 'user' => $user, + 'locales' => $locales, + 'fields' => $fields, + 'tools' => $editButtons, + 'widgets' => $widgets + ]); + } + + /** + * Renders the user listing page. + * + * This page renders a table of users, with dropdown menus for admin actions for each user. + * Actions typically include: edit user details, activate user, enable/disable user, delete user. + * This page requires authentication. + * Request type: GET + * @throws ForbiddenException + */ + public function pageList($request, $response, $args) { + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_users')) { + throw new ForbiddenException(); + } + + return $this->ci->view->render($response, 'pages/users.html.twig'); + } + + /** + * Gets the users public key + * Request type: GET + * @throws NotFoundException + * @throws BadRequestException + */ + public function getPublicKey($request, $response, $args) { + $requestedUser = $this->getUserFromParams($args); + + if (!$requestedUser) { + throw new NotFoundException($request, $response); + } + + if ((Capsule::table('public_keys') + ->where('user_id', "=", $requestedUser->id) + ->exists())) { + + $RawPublicKey = Capsule::table('public_keys') + ->where('user_id', "=", $requestedUser->id) + ->value('key'); + $PublicKey = "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: OpenPGP.js v3.0.9\nComment: https://openpgpjs.org\n\n" . $RawPublicKey . "\n-----END PGP PUBLIC KEY BLOCK-----"; + + $ContentType = explode(',', $request->getHeaderLine('Accept'))[0]; + switch ($ContentType) { + case 'application/json': + $response->write(json_encode(array('user_id' => $requestedUser->id, 'PublicKey' => $PublicKey))); + break; + case 'text/html': + $response->write("" . $PublicKey); + break; + default: + $response->write($PublicKey); + } + return $response->withStatus(200); + } else { + throw new NotFoundException($request, $response); + } + } + + /** + * Gets the users which are following the requested user + * Request type: GET + * @throws ForbiddenException + * @throws NotFoundException + * @throws BadRequestException + */ + public function getFollowers($request, $response, $args) { + $user = $this->getUserFromParams($args); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_user', [ + 'user' => $user + ])) { + throw new ForbiddenException(); + } + + $UsersFollowers = Capsule::table('user_follow') + ->where('user_id', "=", $user->id) + ->join("users", "users.id", "=", "user_follow.followed_by_id") + ->select("user_follow.followed_by_id as id", "users.user_name as username") + ->get(); + + $result = $UsersFollowers->toArray(); + + return $response->withJson($result, 200, JSON_PRETTY_PRINT); + } + + /** + * Get users which the user follows + * Request type: GET + * @throws ForbiddenException + * @throws NotFoundException + * @throws BadRequestException + */ + public function getFollows($request, $response, $args) { + $user = $this->getUserFromParams($args); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (($user->id == $currentUser->id) || (!$authorizer->checkAccess($currentUser, 'uri_user', [ + 'user' => $user + ]))) { + throw new ForbiddenException(); + } + + $UsersFollows = Capsule::table('user_follow') + ->where('followed_by_id', "=", $user->id) + ->join("users", "users.id", "=", "user_follow.user_id") + ->select("user_follow.user_id as id", "users.user_name as username") + ->get(); + + $result = $UsersFollows->toArray(); + + return $response->withJson($result, 200, JSON_PRETTY_PRINT); + } + + /** + * Get users which the user follows and which are following the user + * Request type: GET + * @throws NotFoundException + * @throws ForbiddenException + * @throws BadRequestException + */ + public function getFriends($request, $response, $args) { + $user = $this->getUserFromParams($args); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_user', [ + 'user' => $user + ])) { + throw new ForbiddenException(); + } + + $UsersFriends = Capsule::select("SELECT id FROM (SELECT user_id AS id FROM user_follow WHERE followed_by_id = $user->id UNION ALL SELECT followed_by_id FROM user_follow WHERE user_id = $user->id) t GROUP BY id HAVING COUNT(id) > 1"); + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + foreach ($UsersFriends as $Key => $UsersFriendId) { // NOT THAT EFFICIENT... + $UsersFriendInformation = $classMapper->createInstance('user')// raw select doesnt work with instance + ->where('id', $UsersFriendId->id) + ->get(); + + $UsersFriends[$Key]->id = $UsersFriendInformation[0]->id; + $UsersFriends[$Key]->username = $UsersFriendInformation[0]->user_name; + $UsersFriends[$Key]->avatar = $UsersFriendInformation[0]->avatar; + $UsersFriends[$Key]->full_name = $UsersFriendInformation[0]->full_name; + } + + $result = $UsersFriends; + + if (sizeof($result) > 0) { // USER HAS FRIENDS + return $response->withJson($result, 200, JSON_PRETTY_PRINT); + } else { + throw new NotFoundException($request, $response); + } + } + + + /** + * Processes the request to update an existing user's basic details (first_name, last_name, email, locale, group_id) + * + * Processes the request from the user update form, checking that: + * 1. The target user's new email address, if specified, is not already in use; + * 2. The logged-in user has the necessary permissions to update the putted field(s); + * 3. The submitted data is valid. + * This route requires authentication. + * Request type: PUT + * @throws NotFoundException + * @throws ForbiddenException + * @throws BadRequestException + * @throws BadRequestException + */ + public function updateInfo($request, $response, $args) { + // Get the username from the URL + $user = $this->getUserFromParams($args); + + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Get PUT parameters + $params = $request->getParsedBody(); + + /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ + $ms = $this->ci->alerts; + + // Load the request schema + $schema = new RequestSchema('schema://requests/user/edit-info.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + $error = FALSE; + + // Validate request data + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + $error = TRUE; + } + + // Determine targeted fields + $fieldNames = []; + foreach ($data as $name => $value) { + if ($name == 'first_name' || $name == 'last_name') { + $fieldNames[] = 'name'; + } else if ($name == 'group_id') { + $fieldNames[] = 'group'; + } else { + $fieldNames[] = $name; + } + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled resource - check that currentUser has permission to edit submitted fields for this user + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => array_values(array_unique($fieldNames)) + ])) { + throw new ForbiddenException(); + } + + // Only the master account can edit the master account! + if ( + ($user->id == $config['reserved_user_ids.master']) && + ($currentUser->id != $config['reserved_user_ids.master']) + ) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Check if email already exists + if ( + isset($data['email']) && + $data['email'] != $user->email && + $classMapper->staticMethod('user', 'findUnique', $data['email'], 'email') + ) { + $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data); + $error = TRUE; + } + + if ($error) { + return $response->withStatus(400); + } + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($data, $user, $currentUser) { + // Update the user and generate success messages + foreach ($data as $name => $value) { + if ($value != $user->$name) { + $user->$name = $value; + } + } + + $user->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated basic account info for user {$user->user_name}.", [ + 'type' => 'account_update_info', + 'user_id' => $currentUser->id + ]); + }); + + $ms->addMessageTranslated('success', 'DETAILS_UPDATED', [ + 'user_name' => $user->user_name + ]); + return $response->withStatus(200); + } + + /** + * Processes the request to update a specific field for an existing user. + * + * Supports editing all user fields, including password, enabled/disabled status and verification status. + * Processes the request from the user update form, checking that: + * 1. The logged-in user has the necessary permissions to update the putted field(s); + * 2. We're not trying to disable the master account; + * 3. The submitted data is valid. + * This route requires authentication. + * Request type: PUT + * @throws ForbiddenException + * @throws BadRequestException + * @throws BadRequestException + * @throws BadRequestException + * @throws BadRequestException + * @throws BadRequestException + * @throws NotFoundException + * @throws BadRequestException + */ + public function updateField($request, $response, $args) { + // Get the username from the URL + $user = $this->getUserFromParams($args); + + if (!$user) { + throw new NotFoundException($request, $response); + } + + // Get key->value pair from URL and request body + $fieldName = $args['field']; + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled resource - check that currentUser has permission to edit the specified field for this user + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => [$fieldName] + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Only the master account can edit the master account! + if ( + ($user->id == $config['reserved_user_ids.master']) && + ($currentUser->id != $config['reserved_user_ids.master']) + ) { + throw new ForbiddenException(); + } + + // Get PUT parameters: value + $put = $request->getParsedBody(); + + if (!isset($put['value'])) { + throw new BadRequestException(); + } + + // Create and validate key -> value pair + $params = [ + $fieldName => $put['value'] + ]; + + // Load the request schema + $schema = new RequestSchema('schema://requests/user/edit-field.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and throw exception on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + // encapsulate the communication of error messages from ServerSideValidator to the BadRequestException + $e = new BadRequestException(); + foreach ($validator->errors() as $idx => $field) { + foreach ($field as $eidx => $error) { + $e->addUserMessage($error); + } + } + throw $e; + } + + // Get validated and transformed value + $fieldValue = $data[$fieldName]; + + /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ + $ms = $this->ci->alerts; + + // Special checks and transformations for certain fields + if ($fieldName == 'flag_enabled') { + // Check that we are not disabling the master account + if ( + ($user->id == $config['reserved_user_ids.master']) && + ($fieldValue == '0') + ) { + $e = new BadRequestException(); + $e->addUserMessage('DISABLE_MASTER'); + throw $e; + } else if ( + ($user->id == $currentUser->id) && + ($fieldValue == '0') + ) { + $e = new BadRequestException(); + $e->addUserMessage('DISABLE_SELF'); + throw $e; + } + } else if ($fieldName == 'password') { + $fieldValue = Password::hash($fieldValue); + } + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($fieldName, $fieldValue, $user, $currentUser) { + if ($fieldName == 'roles') { + $newRoles = collect($fieldValue)->pluck('role_id')->all(); + $user->roles()->sync($newRoles); + } else { + $user->$fieldName = $fieldValue; + $user->save(); + } + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated property '$fieldName' for user {$user->user_name}.", [ + 'type' => 'account_update_field', + 'user_id' => $currentUser->id + ]); + }); + + // Add success messages + if ($fieldName == 'flag_enabled') { + if ($fieldValue == '1') { + $ms->addMessageTranslated('success', 'ENABLE_SUCCESSFUL', [ + 'user_name' => $user->user_name + ]); + } else { + $ms->addMessageTranslated('success', 'DISABLE_SUCCESSFUL', [ + 'user_name' => $user->user_name + ]); + } + } else if ($fieldName == 'flag_verified') { + $ms->addMessageTranslated('success', 'MANUALLY_ACTIVATED', [ + 'user_name' => $user->user_name + ]); + } else { + $ms->addMessageTranslated('success', 'DETAILS_UPDATED', [ + 'user_name' => $user->user_name + ]); + } + + return $response->withStatus(200); + } + + protected function getUserFromParams($params) { + // Load the request schema + $schema = new RequestSchema('schema://requests/user/get-by-username.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and throw exception on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $e = new BadRequestException(); + foreach ($validator->errors() as $idx => $field) { + foreach ($field as $eidx => $error) { + $e->addUserMessage($error); + } + } + throw $e; + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Get the user to delete + $user = $classMapper->staticMethod('user', 'where', 'user_name', $data['user_name']) + ->first(); + + return $user; + } +} -- cgit v1.2.3