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 --- .../account/src/Controller/AccountController.php | 2542 ++++++++++---------- 1 file changed, 1271 insertions(+), 1271 deletions(-) (limited to 'main/app/sprinkles/account/src/Controller/AccountController.php') diff --git a/main/app/sprinkles/account/src/Controller/AccountController.php b/main/app/sprinkles/account/src/Controller/AccountController.php index 7373923..c4201a7 100644 --- a/main/app/sprinkles/account/src/Controller/AccountController.php +++ b/main/app/sprinkles/account/src/Controller/AccountController.php @@ -1,1271 +1,1271 @@ -ci->alerts; - - // GET parameters - $params = $request->getQueryParams(); - - // Load request schema - $schema = new RequestSchema('schema://requests/check-username.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - // Validate, and halt on validation errors. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - // O: encapsulate the communication of error messages from ServerSideValidator to the BadRequestException - $e = new BadRequestException('Missing or malformed request data!'); - foreach ($validator->errors() as $idx => $field) { - foreach ($field as $eidx => $error) { - $e->addUserMessage($error); - } - } - throw $e; - } - - /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ - $throttler = $this->ci->throttler; - $delay = $throttler->getDelay('check_username_request'); - - // Throttle requests - if ($delay > 0) { - return $response->withStatus(429); - } - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\I18n\MessageTranslator $translator */ - $translator = $this->ci->translator; - - // Log throttleable event - $throttler->logEvent('check_username_request'); - - if ($classMapper->staticMethod('user', 'findUnique', $data['user_name'], 'user_name')) { - $message = $translator->translate('USERNAME.NOT_AVAILABLE', $data); - return $response->write($message)->withStatus(200); - } else { - return $response->write('true')->withStatus(200); - } - } - - /** - * Processes a request to cancel a password reset request. - * - * This is provided so that users can cancel a password reset request, if they made it in error or if it was not initiated by themselves. - * Processes the request from the password reset link, checking that: - * 1. The provided token is associated with an existing user account, who has a pending password reset request. - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function denyResetPassword(Request $request, Response $response, $args) { - // GET parameters - $params = $request->getQueryParams(); - - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - $loginPage = $this->ci->router->pathFor('login'); - - // Load validation rules - $schema = new RequestSchema('schema://requests/deny-password.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - // Validate, and halt on validation errors. Since this is a GET request, we need to redirect on failure - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - // 400 code + redirect is perfectly fine, according to user Dilaz in #laravel - return $response->withRedirect($loginPage, 400); - } - - $passwordReset = $this->ci->repoPasswordReset->cancel($data['token']); - - if (!$passwordReset) { - $ms->addMessageTranslated('danger', 'PASSWORD.FORGET.INVALID'); - return $response->withRedirect($loginPage, 400); - } - - $ms->addMessageTranslated('success', 'PASSWORD.FORGET.REQUEST_CANNED'); - return $response->withRedirect($loginPage); - } - - /** - * Processes a request to email a forgotten password reset link to the user. - * - * Processes the request from the form on the "forgot password" page, checking that: - * 1. The rate limit for this type of request is being observed. - * 2. The provided email address belongs to a registered account; - * 3. The submitted data is valid. - * Note that we have removed the requirement that a password reset request not already be in progress. - * This is because we need to allow users to re-request a reset, even if they lose the first reset email. - * This route is "public access". - * Request type: POST - * @odo require additional user information - * @odo prevent password reset requests for root account? - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function forgotPassword(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - // Get POST parameters - $params = $request->getParsedBody(); - - // Load the request schema - $schema = new RequestSchema('schema://requests/forgot-password.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - return $response->withStatus(400); - } - - // Throttle requests - - /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ - $throttler = $this->ci->throttler; - - $throttleData = [ - 'email' => $data['email'] - ]; - $delay = $throttler->getDelay('password_reset_request', $throttleData); - - if ($delay > 0) { - $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', ['delay' => $delay]); - return $response->withStatus(429); - } - - // All checks passed! log events/activities, update user, and send email - // Begin transaction - DB will be rolled back if an exception occurs - Capsule::transaction(function () use ($classMapper, $data, $throttler, $throttleData, $config) { - - // Log throttleable event - $throttler->logEvent('password_reset_request', $throttleData); - - // Load the user, by email address - $user = $classMapper->staticMethod('user', 'where', 'email', $data['email'])->first(); - - // Check that the email exists. - // If there is no user with that email address, we should still pretend like we succeeded, to prevent account enumeration - if ($user) { - // Try to generate a new password reset request. - // Use timeout for "reset password" - $passwordReset = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.reset']); - - // Create and send email - $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' => $data['email']]); - return $response->withStatus(200); - } - - /** - * Returns a modal containing account terms of service. - * - * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages. - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function getModalAccountTos(Request $request, Response $response, $args) { - return $this->ci->view->render($response, 'modals/tos.html.twig'); - } - - /** - * Generate a random captcha, store it to the session, and return the captcha image. - * - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function imageCaptcha(Request $request, Response $response, $args) { - $captcha = new Captcha($this->ci->session, $this->ci->config['session.keys.captcha']); - $captcha->generateRandomCode(); - - return $response->withStatus(200) - ->withHeader('Content-Type', 'image/png;base64') - ->write($captcha->getImage()); - } - - /** - * Processes an account login request. - * - * Processes the request from the form on the login page, checking that: - * 1. The user is not already logged in. - * 2. The rate limit for this type of request is being observed. - * 3. Email login is enabled, if an email address was used. - * 4. The user account exists. - * 5. The user account is enabled and verified. - * 6. The user entered a valid username/email and password. - * This route, by definition, is "public access". - * Request type: POST - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function login(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ - $authenticator = $this->ci->authenticator; - - // Return 200 success if user is already logged in - if ($authenticator->check()) { - $ms->addMessageTranslated('warning', 'LOGIN.ALREADY_COMPLETE'); - return $response->withStatus(200); - } - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - // Get POST parameters - $params = $request->getParsedBody(); - - // Load the request schema - $schema = new RequestSchema('schema://requests/login.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - return $response->withStatus(400); - } - - // Determine whether we are trying to log in with an email address or a username - $isEmail = filter_var($data['user_name'], FILTER_VALIDATE_EMAIL); - - // Throttle requests - - /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ - $throttler = $this->ci->throttler; - - $userIdentifier = $data['user_name']; - - $throttleData = [ - 'user_identifier' => $userIdentifier - ]; - - $delay = $throttler->getDelay('sign_in_attempt', $throttleData); - if ($delay > 0) { - $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', [ - 'delay' => $delay - ]); - return $response->withStatus(429); - } - - // Log throttleable event - $throttler->logEvent('sign_in_attempt', $throttleData); - - // If credential is an email address, but email login is not enabled, raise an error. - // Note that we do this after logging throttle event, so this error counts towards throttling limit. - if ($isEmail && !$config['site.login.enable_email']) { - $ms->addMessageTranslated('danger', 'USER_OR_PASS_INVALID'); - return $response->withStatus(403); - } - - // Try to authenticate the user. Authenticator will throw an exception on failure. - /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ - $authenticator = $this->ci->authenticator; - - $currentUser = $authenticator->attempt(($isEmail ? 'email' : 'user_name'), $userIdentifier, $data['password'], $data['rememberme']); - - $ms->addMessageTranslated('success', 'WELCOME', $currentUser->export()); - - // Set redirect, if relevant - $redirectOnLogin = $this->ci->get('redirect.onLogin'); - - return $redirectOnLogin($request, $response, $args); - } - - /** - * Log the user out completely, including destroying any "remember me" token. - * - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function logout(Request $request, Response $response, $args) { - // Destroy the session - $this->ci->authenticator->logout(); - - // Return to home page - $config = $this->ci->config; - return $response->withStatus(302)->withHeader('Location', $config['site.uri.public']); - } - - /** - * Render the "forgot password" page. - * - * This creates a simple form to allow users who forgot their password to have a time-limited password reset link emailed to them. - * By default, this is a "public page" (does not require authentication). - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function pageForgotPassword(Request $request, Response $response, $args) { - // Load validation rules - $schema = new RequestSchema('schema://requests/forgot-password.yaml'); - $validator = new JqueryValidationAdapter($schema, $this->ci->translator); - - return $this->ci->view->render($response, 'pages/forgot-password.html.twig', [ - 'page' => [ - 'validators' => [ - 'forgot_password' => $validator->rules('json', FALSE) - ] - ] - ]); - } - - - /** - * Render the account registration page for UserFrosting. - * - * This allows new (non-authenticated) users to create a new account for themselves on your website (if enabled). - * By definition, this is a "public page" (does not require authentication). - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function pageRegister(Request $request, Response $response, $args) { - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - if (!$config['site.registration.enabled']) { - throw new NotFoundException($request, $response); - } - - /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ - $authenticator = $this->ci->authenticator; - - // Redirect if user is already logged in - if ($authenticator->check()) { - $redirect = $this->ci->get('redirect.onAlreadyLoggedIn'); - - return $redirect($request, $response, $args); - } - - // Load validation rules - $schema = new RequestSchema('schema://requests/register.yaml'); - $validatorRegister = new JqueryValidationAdapter($schema, $this->ci->translator); - - return $this->ci->view->render($response, 'pages/register.html.twig', [ - 'page' => [ - 'validators' => [ - 'register' => $validatorRegister->rules('json', FALSE) - ] - ] - ]); - } - - /** - * Render the "resend verification email" page. - * - * This is a form that allows users who lost their account verification link to have the link resent to their email address. - * By default, this is a "public page" (does not require authentication). - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function pageResendVerification(Request $request, Response $response, $args) { - // Load validation rules - $schema = new RequestSchema('schema://requests/resend-verification.yaml'); - $validator = new JqueryValidationAdapter($schema, $this->ci->translator); - - return $this->ci->view->render($response, 'pages/resend-verification.html.twig', [ - 'page' => [ - 'validators' => [ - 'resend_verification' => $validator->rules('json', FALSE) - ] - ] - ]); - } - - /** - * Reset password page. - * - * Renders the new password page for password reset requests. - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function pageResetPassword(Request $request, Response $response, $args) { - // Insert the user's secret token from the link into the password reset form - $params = $request->getQueryParams(); - - // Load validation rules - note this uses the same schema as "set password" - $schema = new RequestSchema('schema://requests/set-password.yaml'); - $validator = new JqueryValidationAdapter($schema, $this->ci->translator); - - return $this->ci->view->render($response, 'pages/reset-password.html.twig', [ - 'page' => [ - 'validators' => [ - 'set_password' => $validator->rules('json', FALSE) - ] - ], - 'token' => isset($params['token']) ? $params['token'] : '', - ]); - } - - /** - * Render the "set password" page. - * - * Renders the page where new users who have had accounts created for them by another user, can set their password. - * By default, this is a "public page" (does not require authentication). - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function pageSetPassword(Request $request, Response $response, $args) { - // Insert the user's secret token from the link into the password set form - $params = $request->getQueryParams(); - - // Load validation rules - $schema = new RequestSchema('schema://requests/set-password.yaml'); - $validator = new JqueryValidationAdapter($schema, $this->ci->translator); - - return $this->ci->view->render($response, 'pages/set-password.html.twig', [ - 'page' => [ - 'validators' => [ - 'set_password' => $validator->rules('json', FALSE) - ] - ], - 'token' => isset($params['token']) ? $params['token'] : '', - ]); - } - - /** - * Account settings page. - * - * Provides a form for users to modify various properties of their account, such as name, email, locale, etc. - * Any fields that the user does not have permission to modify will be automatically disabled. - * This page requires authentication. - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function pageSettings(Request $request, Response $response, $args) { - /** @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, 'uri_account_settings')) { - throw new ForbiddenException(); - } - - // Load validation rules - $schema = new RequestSchema('schema://requests/account-settings.yaml'); - $validatorAccountSettings = new JqueryValidationAdapter($schema, $this->ci->translator); - - $schema = new RequestSchema('schema://requests/profile-settings.yaml'); - $validatorProfileSettings = new JqueryValidationAdapter($schema, $this->ci->translator); - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - // Get a list of all locales - $locales = $config->getDefined('site.locales.available'); - - return $this->ci->view->render($response, 'pages/account-settings.html.twig', [ - 'locales' => $locales, - 'page' => [ - 'validators' => [ - 'account_settings' => $validatorAccountSettings->rules('json', FALSE), - 'profile_settings' => $validatorProfileSettings->rules('json', FALSE) - ], - 'visibility' => ($authorizer->checkAccess($currentUser, 'update_account_settings') ? '' : 'disabled') - ] - ]); - } - - /** - * Render the account sign-in page for UserFrosting. - * - * This allows existing users to sign in. - * By definition, this is a "public page" (does not require authentication). - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function pageSignIn(Request $request, Response $response, $args) { - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ - $authenticator = $this->ci->authenticator; - - // Redirect if user is already logged in - if ($authenticator->check()) { - $redirect = $this->ci->get('redirect.onAlreadyLoggedIn'); - - return $redirect($request, $response, $args); - } - - // Load validation rules - $schema = new RequestSchema('schema://requests/login.yaml'); - $validatorLogin = new JqueryValidationAdapter($schema, $this->ci->translator); - - return $this->ci->view->render($response, 'pages/sign-in.html.twig', [ - 'page' => [ - 'validators' => [ - 'login' => $validatorLogin->rules('json', FALSE) - ] - ] - ]); - } - - /** - * Processes a request to update a user's profile information. - * - * Processes the request from the user profile settings form, checking that: - * 1. They have the necessary permissions to update the posted field(s); - * 2. The submitted data is valid. - * This route requires authentication. - * Request type: POST - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function profile(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ - $authorizer = $this->ci->authorizer; - - /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access control for entire resource - check that the current user has permission to modify themselves - // See recipe "per-field access control" for dynamic fine-grained control over which properties a user can modify. - if (!$authorizer->checkAccess($currentUser, 'update_account_settings')) { - $ms->addMessageTranslated('danger', 'ACCOUNT.ACCESS_DENIED'); - return $response->withStatus(403); - } - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - // POST parameters - $params = $request->getParsedBody(); - - // Load the request schema - $schema = new RequestSchema('schema://requests/profile-settings.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - $error = FALSE; - - // Validate, and halt on validation errors. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - $error = TRUE; - } - - // Check that locale is valid - $locales = $config->getDefined('site.locales.available'); - if (!array_key_exists($data['locale'], $locales)) { - $ms->addMessageTranslated('danger', 'LOCALE.INVALID', $data); - $error = TRUE; - } - - if ($error) { - return $response->withStatus(400); - } - - // Looks good, let's update with new values! - // Note that only fields listed in `profile-settings.yaml` will be permitted in $data, so this prevents the user from updating all columns in the DB - $currentUser->fill($data); - - $currentUser->save(); - - // Create activity record - $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated their profile settings.", [ - 'type' => 'update_profile_settings' - ]); - - $ms->addMessageTranslated('success', 'PROFILE.UPDATED'); - return $response->withStatus(200); - } - - /** - * Processes an new account registration request. - * - * This is throttled to prevent account enumeration, since it needs to divulge when a username/email has been used. - * Processes the request from the form on the registration page, checking that: - * 1. The honeypot was not modified; - * 2. The master account has already been created (during installation); - * 3. Account registration is enabled; - * 4. The user is not already logged in; - * 5. Valid information was entered; - * 6. The captcha, if enabled, is correct; - * 7. The username and email are not already taken. - * Automatically sends an activation link upon success, if account activation is enabled. - * This route is "public access". - * Request type: POST - * Returns the User Object for the user record that was created. - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function register(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - // Get POST parameters: user_name, first_name, last_name, email, password, passwordc, captcha, spiderbro, csrf_token - $params = $request->getParsedBody(); - - // Check the honeypot. 'spiderbro' is not a real field, it is hidden on the main page and must be submitted with its default value for this to be processed. - if (!isset($params['spiderbro']) || $params['spiderbro'] != 'http://') { - throw new SpammyRequestException('Possible spam received:' . print_r($params, TRUE)); - } - - // Security measure: do not allow registering new users until the master account has been created. - if (!$classMapper->staticMethod('user', 'find', $config['reserved_user_ids.master'])) { - $ms->addMessageTranslated('danger', 'ACCOUNT.MASTER_NOT_EXISTS'); - return $response->withStatus(403); - } - - // Check if registration is currently enabled - if (!$config['site.registration.enabled']) { - $ms->addMessageTranslated('danger', 'REGISTRATION.DISABLED'); - return $response->withStatus(403); - } - - /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ - $authenticator = $this->ci->authenticator; - - // Prevent the user from registering if he/she is already logged in - if ($authenticator->check()) { - $ms->addMessageTranslated('danger', 'REGISTRATION.LOGOUT'); - return $response->withStatus(403); - } - - // Load the request schema - $schema = new RequestSchema('schema://requests/register.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\Throttle\Throttler $throttler */ - $throttler = $this->ci->throttler; - $delay = $throttler->getDelay('registration_attempt'); - - // Throttle requests - if ($delay > 0) { - return $response->withStatus(429); - } - - // 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; - } - - // Check captcha, if required - if ($config['site.registration.captcha']) { - $captcha = new Captcha($this->ci->session, $this->ci->config['session.keys.captcha']); - if (!$data['captcha'] || !$captcha->verifyCode($data['captcha'])) { - $ms->addMessageTranslated('danger', 'CAPTCHA.FAIL'); - $error = TRUE; - } - } - - if ($error) { - return $response->withStatus(400); - } - - // Remove captcha, password confirmation from object data after validation - unset($data['captcha']); - unset($data['passwordc']); - - if ($config['site.registration.require_email_verification']) { - $data['flag_verified'] = FALSE; - } else { - $data['flag_verified'] = TRUE; - } - - // Load default group - $groupSlug = $config['site.registration.user_defaults.group']; - $defaultGroup = $classMapper->staticMethod('group', 'where', 'slug', $groupSlug)->first(); - - if (!$defaultGroup) { - $e = new HttpException("Account registration is not working because the default group '$groupSlug' does not exist."); - $e->addUserMessage('REGISTRATION.BROKEN'); - throw $e; - } - - // Set default group - $data['group_id'] = $defaultGroup->id; - - // Set default locale - $data['locale'] = $config['site.registration.user_defaults.locale']; - - // Hash password - $data['password'] = Password::hash($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, $throttler) { - // Log throttleable event - $throttler->logEvent('registration_attempt'); - - // Create the user - $user = $classMapper->createInstance('user', $data); - - // Store new user to database - $user->save(); - - // Create activity record - $this->ci->userActivityLogger->info("User {$user->user_name} registered for a new account.", [ - 'type' => 'sign_up', - 'user_id' => $user->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); - - // Verification email - if ($config['site.registration.require_email_verification']) { - // Try to generate a new verification request - $verification = $this->ci->repoVerification->create($user, $config['verification.timeout']); - - // Create and send verification email - $message = new TwigMailMessage($this->ci->view, 'mail/verify-account.html.twig'); - - $message->from($config['address_book.admin']) - ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) - ->addParams([ - 'user' => $user, - 'token' => $verification->getToken() - ]); - - $this->ci->mailer->send($message); - - $ms->addMessageTranslated('success', 'REGISTRATION.COMPLETE_TYPE2', $user->toArray()); - } else { - // No verification required - $ms->addMessageTranslated('success', 'REGISTRATION.COMPLETE_TYPE1'); - } - }); - - return $response->withStatus(200); - } - - /** - * Processes a request to resend the verification email for a new user account. - * - * Processes the request from the resend verification email form, checking that: - * 1. The rate limit on this type of request is observed; - * 2. The provided email is associated with an existing user account; - * 3. The user account is not already verified; - * 4. The submitted data is valid. - * This route is "public access". - * Request type: POST - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function resendVerification(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - // Get POST parameters - $params = $request->getParsedBody(); - - // Load the request schema - $schema = new RequestSchema('schema://requests/resend-verification.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - return $response->withStatus(400); - } - - // Throttle requests - - /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ - $throttler = $this->ci->throttler; - - $throttleData = [ - 'email' => $data['email'] - ]; - $delay = $throttler->getDelay('verification_request', $throttleData); - - if ($delay > 0) { - $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', ['delay' => $delay]); - return $response->withStatus(429); - } - - // 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, $throttler, $throttleData, $config) { - // Log throttleable event - $throttler->logEvent('verification_request', $throttleData); - - // Load the user, by email address - $user = $classMapper->staticMethod('user', 'where', 'email', $data['email'])->first(); - - // Check that the user exists and is not already verified. - // If there is no user with that email address, or the user exists and is already verified, - // we pretend like we succeeded to prevent account enumeration - if ($user && $user->flag_verified != '1') { - // We're good to go - record user activity and send the email - $verification = $this->ci->repoVerification->create($user, $config['verification.timeout']); - - // Create and send verification email - $message = new TwigMailMessage($this->ci->view, 'mail/resend-verification.html.twig'); - - $message->from($config['address_book.admin']) - ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) - ->addParams([ - 'user' => $user, - 'token' => $verification->getToken() - ]); - - $this->ci->mailer->send($message); - } - }); - - $ms->addMessageTranslated('success', 'ACCOUNT.VERIFICATION.NEW_LINK_SENT', ['email' => $data['email']]); - return $response->withStatus(200); - } - - /** - * Processes a request to set the password for a new or current user. - * - * Processes the request from the password create/reset form, which should have the secret token embedded in it, checking that: - * 1. The provided secret token is associated with an existing user account; - * 2. The user has a password set/reset request in progress; - * 3. The token has not expired; - * 4. The submitted data (new password) is valid. - * This route is "public access". - * Request type: POST - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function setPassword(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - // Get POST parameters - $params = $request->getParsedBody(); - - // Load the request schema - $schema = new RequestSchema('schema://requests/set-password.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - return $response->withStatus(400); - } - - $forgotPasswordPage = $this->ci->router->pathFor('forgot-password'); - - // Ok, try to complete the request with the specified token and new password - $passwordReset = $this->ci->repoPasswordReset->complete($data['token'], [ - 'password' => $data['password'] - ]); - - if (!$passwordReset) { - $ms->addMessageTranslated('danger', 'PASSWORD.FORGET.INVALID', ['url' => $forgotPasswordPage]); - return $response->withStatus(400); - } - - $ms->addMessageTranslated('success', 'PASSWORD.UPDATED'); - - /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ - $authenticator = $this->ci->authenticator; - - // Log out any existing user, and create a new session - if ($authenticator->check()) { - $authenticator->logout(); - } - - // Auto-login the user (without "remember me") - $user = $passwordReset->user; - $authenticator->login($user); - - $ms->addMessageTranslated('success', 'WELCOME', $user->export()); - return $response->withStatus(200); - } - - /** - * Processes a request to update a user's account information. - * - * Processes the request from the user account settings form, checking that: - * 1. The user correctly input their current password; - * 2. They have the necessary permissions to update the posted field(s); - * 3. The submitted data is valid. - * This route requires authentication. - * Request type: POST - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function settings(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ - $authorizer = $this->ci->authorizer; - - /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access control for entire resource - check that the current user has permission to modify themselves - // See recipe "per-field access control" for dynamic fine-grained control over which properties a user can modify. - if (!$authorizer->checkAccess($currentUser, 'update_account_settings')) { - $ms->addMessageTranslated('danger', 'ACCOUNT.ACCESS_DENIED'); - return $response->withStatus(403); - } - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - // POST parameters - $params = $request->getParsedBody(); - - // Load the request schema - $schema = new RequestSchema('schema://requests/account-settings.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - $error = FALSE; - - // Validate, and halt on validation errors. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - $error = TRUE; - } - - // Confirm current password - if (!isset($data['passwordcheck']) || !Password::verify($data['passwordcheck'], $currentUser->password)) { - $ms->addMessageTranslated('danger', 'PASSWORD.INVALID'); - $error = TRUE; - } - - // Remove password check, password confirmation from object data after validation - unset($data['passwordcheck']); - unset($data['passwordc']); - - // If new email was submitted, check that the email address is not in use - if (isset($data['email']) && $data['email'] != $currentUser->email && $classMapper->staticMethod('user', 'findUnique', $data['email'], 'email')) { - $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data); - $error = TRUE; - } - - if ($error) { - return $response->withStatus(400); - } - - // Hash new password, if specified - if (isset($data['password']) && !empty($data['password'])) { - $data['password'] = Password::hash($data['password']); - } else { - // Do not pass to model if no password is specified - unset($data['password']); - } - - // Looks good, let's update with new values! - // Note that only fields listed in `account-settings.yaml` will be permitted in $data, so this prevents the user from updating all columns in the DB - $currentUser->fill($data); - - $currentUser->save(); - - // Create activity record - $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated their account settings.", [ - 'type' => 'update_account_settings' - ]); - - $ms->addMessageTranslated('success', 'ACCOUNT.SETTINGS.UPDATED'); - return $response->withStatus(200); - } - - /** - * Suggest an available username for a specified first/last name. - * - * This route is "public access". - * Request type: GET - * @odo Can this route be abused for account enumeration? If so we should throttle it as well. - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function suggestUsername(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - $suggestion = AccountUtil::randomUniqueUsername($classMapper, 50, 10); - - // 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([ - 'user_name' => $suggestion - ], 200, JSON_PRETTY_PRINT); - } - - /** - * Processes an new email verification request. - * - * Processes the request from the email verification link that was emailed to the user, checking that: - * 1. The token provided matches a user in the database; - * 2. The user account is not already verified; - * This route is "public access". - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function verify(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - $loginPage = $this->ci->router->pathFor('login'); - - // GET parameters - $params = $request->getQueryParams(); - - // Load request schema - $schema = new RequestSchema('schema://requests/account-verify.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - // Validate, and halt on validation errors. This is a GET request, so we redirect on validation error. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - // 400 code + redirect is perfectly fine, according to user Dilaz in #laravel - return $response->withRedirect($loginPage, 400); - } - - $verification = $this->ci->repoVerification->complete($data['token']); - - if (!$verification) { - $ms->addMessageTranslated('danger', 'ACCOUNT.VERIFICATION.TOKEN_NOT_FOUND'); - return $response->withRedirect($loginPage, 400); - } - - $ms->addMessageTranslated('success', 'ACCOUNT.VERIFICATION.COMPLETE'); - - // Forward to login page - return $response->withRedirect($loginPage); - } -} +ci->alerts; + + // GET parameters + $params = $request->getQueryParams(); + + // Load request schema + $schema = new RequestSchema('schema://requests/check-username.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + // O: encapsulate the communication of error messages from ServerSideValidator to the BadRequestException + $e = new BadRequestException('Missing or malformed request data!'); + foreach ($validator->errors() as $idx => $field) { + foreach ($field as $eidx => $error) { + $e->addUserMessage($error); + } + } + throw $e; + } + + /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ + $throttler = $this->ci->throttler; + $delay = $throttler->getDelay('check_username_request'); + + // Throttle requests + if ($delay > 0) { + return $response->withStatus(429); + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\I18n\MessageTranslator $translator */ + $translator = $this->ci->translator; + + // Log throttleable event + $throttler->logEvent('check_username_request'); + + if ($classMapper->staticMethod('user', 'findUnique', $data['user_name'], 'user_name')) { + $message = $translator->translate('USERNAME.NOT_AVAILABLE', $data); + return $response->write($message)->withStatus(200); + } else { + return $response->write('true')->withStatus(200); + } + } + + /** + * Processes a request to cancel a password reset request. + * + * This is provided so that users can cancel a password reset request, if they made it in error or if it was not initiated by themselves. + * Processes the request from the password reset link, checking that: + * 1. The provided token is associated with an existing user account, who has a pending password reset request. + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function denyResetPassword(Request $request, Response $response, $args) { + // GET parameters + $params = $request->getQueryParams(); + + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $loginPage = $this->ci->router->pathFor('login'); + + // Load validation rules + $schema = new RequestSchema('schema://requests/deny-password.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. Since this is a GET request, we need to redirect on failure + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + // 400 code + redirect is perfectly fine, according to user Dilaz in #laravel + return $response->withRedirect($loginPage, 400); + } + + $passwordReset = $this->ci->repoPasswordReset->cancel($data['token']); + + if (!$passwordReset) { + $ms->addMessageTranslated('danger', 'PASSWORD.FORGET.INVALID'); + return $response->withRedirect($loginPage, 400); + } + + $ms->addMessageTranslated('success', 'PASSWORD.FORGET.REQUEST_CANNED'); + return $response->withRedirect($loginPage); + } + + /** + * Processes a request to email a forgotten password reset link to the user. + * + * Processes the request from the form on the "forgot password" page, checking that: + * 1. The rate limit for this type of request is being observed. + * 2. The provided email address belongs to a registered account; + * 3. The submitted data is valid. + * Note that we have removed the requirement that a password reset request not already be in progress. + * This is because we need to allow users to re-request a reset, even if they lose the first reset email. + * This route is "public access". + * Request type: POST + * @odo require additional user information + * @odo prevent password reset requests for root account? + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function forgotPassword(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/forgot-password.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + return $response->withStatus(400); + } + + // Throttle requests + + /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ + $throttler = $this->ci->throttler; + + $throttleData = [ + 'email' => $data['email'] + ]; + $delay = $throttler->getDelay('password_reset_request', $throttleData); + + if ($delay > 0) { + $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', ['delay' => $delay]); + return $response->withStatus(429); + } + + // All checks passed! log events/activities, update user, and send email + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($classMapper, $data, $throttler, $throttleData, $config) { + + // Log throttleable event + $throttler->logEvent('password_reset_request', $throttleData); + + // Load the user, by email address + $user = $classMapper->staticMethod('user', 'where', 'email', $data['email'])->first(); + + // Check that the email exists. + // If there is no user with that email address, we should still pretend like we succeeded, to prevent account enumeration + if ($user) { + // Try to generate a new password reset request. + // Use timeout for "reset password" + $passwordReset = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.reset']); + + // Create and send email + $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' => $data['email']]); + return $response->withStatus(200); + } + + /** + * Returns a modal containing account terms of service. + * + * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages. + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function getModalAccountTos(Request $request, Response $response, $args) { + return $this->ci->view->render($response, 'modals/tos.html.twig'); + } + + /** + * Generate a random captcha, store it to the session, and return the captcha image. + * + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function imageCaptcha(Request $request, Response $response, $args) { + $captcha = new Captcha($this->ci->session, $this->ci->config['session.keys.captcha']); + $captcha->generateRandomCode(); + + return $response->withStatus(200) + ->withHeader('Content-Type', 'image/png;base64') + ->write($captcha->getImage()); + } + + /** + * Processes an account login request. + * + * Processes the request from the form on the login page, checking that: + * 1. The user is not already logged in. + * 2. The rate limit for this type of request is being observed. + * 3. Email login is enabled, if an email address was used. + * 4. The user account exists. + * 5. The user account is enabled and verified. + * 6. The user entered a valid username/email and password. + * This route, by definition, is "public access". + * Request type: POST + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function login(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + // Return 200 success if user is already logged in + if ($authenticator->check()) { + $ms->addMessageTranslated('warning', 'LOGIN.ALREADY_COMPLETE'); + return $response->withStatus(200); + } + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/login.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + return $response->withStatus(400); + } + + // Determine whether we are trying to log in with an email address or a username + $isEmail = filter_var($data['user_name'], FILTER_VALIDATE_EMAIL); + + // Throttle requests + + /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ + $throttler = $this->ci->throttler; + + $userIdentifier = $data['user_name']; + + $throttleData = [ + 'user_identifier' => $userIdentifier + ]; + + $delay = $throttler->getDelay('sign_in_attempt', $throttleData); + if ($delay > 0) { + $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', [ + 'delay' => $delay + ]); + return $response->withStatus(429); + } + + // Log throttleable event + $throttler->logEvent('sign_in_attempt', $throttleData); + + // If credential is an email address, but email login is not enabled, raise an error. + // Note that we do this after logging throttle event, so this error counts towards throttling limit. + if ($isEmail && !$config['site.login.enable_email']) { + $ms->addMessageTranslated('danger', 'USER_OR_PASS_INVALID'); + return $response->withStatus(403); + } + + // Try to authenticate the user. Authenticator will throw an exception on failure. + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + $currentUser = $authenticator->attempt(($isEmail ? 'email' : 'user_name'), $userIdentifier, $data['password'], $data['rememberme']); + + $ms->addMessageTranslated('success', 'WELCOME', $currentUser->export()); + + // Set redirect, if relevant + $redirectOnLogin = $this->ci->get('redirect.onLogin'); + + return $redirectOnLogin($request, $response, $args); + } + + /** + * Log the user out completely, including destroying any "remember me" token. + * + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function logout(Request $request, Response $response, $args) { + // Destroy the session + $this->ci->authenticator->logout(); + + // Return to home page + $config = $this->ci->config; + return $response->withStatus(302)->withHeader('Location', $config['site.uri.public']); + } + + /** + * Render the "forgot password" page. + * + * This creates a simple form to allow users who forgot their password to have a time-limited password reset link emailed to them. + * By default, this is a "public page" (does not require authentication). + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageForgotPassword(Request $request, Response $response, $args) { + // Load validation rules + $schema = new RequestSchema('schema://requests/forgot-password.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/forgot-password.html.twig', [ + 'page' => [ + 'validators' => [ + 'forgot_password' => $validator->rules('json', FALSE) + ] + ] + ]); + } + + + /** + * Render the account registration page for UserFrosting. + * + * This allows new (non-authenticated) users to create a new account for themselves on your website (if enabled). + * By definition, this is a "public page" (does not require authentication). + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageRegister(Request $request, Response $response, $args) { + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + if (!$config['site.registration.enabled']) { + throw new NotFoundException($request, $response); + } + + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + // Redirect if user is already logged in + if ($authenticator->check()) { + $redirect = $this->ci->get('redirect.onAlreadyLoggedIn'); + + return $redirect($request, $response, $args); + } + + // Load validation rules + $schema = new RequestSchema('schema://requests/register.yaml'); + $validatorRegister = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/register.html.twig', [ + 'page' => [ + 'validators' => [ + 'register' => $validatorRegister->rules('json', FALSE) + ] + ] + ]); + } + + /** + * Render the "resend verification email" page. + * + * This is a form that allows users who lost their account verification link to have the link resent to their email address. + * By default, this is a "public page" (does not require authentication). + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageResendVerification(Request $request, Response $response, $args) { + // Load validation rules + $schema = new RequestSchema('schema://requests/resend-verification.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/resend-verification.html.twig', [ + 'page' => [ + 'validators' => [ + 'resend_verification' => $validator->rules('json', FALSE) + ] + ] + ]); + } + + /** + * Reset password page. + * + * Renders the new password page for password reset requests. + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageResetPassword(Request $request, Response $response, $args) { + // Insert the user's secret token from the link into the password reset form + $params = $request->getQueryParams(); + + // Load validation rules - note this uses the same schema as "set password" + $schema = new RequestSchema('schema://requests/set-password.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/reset-password.html.twig', [ + 'page' => [ + 'validators' => [ + 'set_password' => $validator->rules('json', FALSE) + ] + ], + 'token' => isset($params['token']) ? $params['token'] : '', + ]); + } + + /** + * Render the "set password" page. + * + * Renders the page where new users who have had accounts created for them by another user, can set their password. + * By default, this is a "public page" (does not require authentication). + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageSetPassword(Request $request, Response $response, $args) { + // Insert the user's secret token from the link into the password set form + $params = $request->getQueryParams(); + + // Load validation rules + $schema = new RequestSchema('schema://requests/set-password.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/set-password.html.twig', [ + 'page' => [ + 'validators' => [ + 'set_password' => $validator->rules('json', FALSE) + ] + ], + 'token' => isset($params['token']) ? $params['token'] : '', + ]); + } + + /** + * Account settings page. + * + * Provides a form for users to modify various properties of their account, such as name, email, locale, etc. + * Any fields that the user does not have permission to modify will be automatically disabled. + * This page requires authentication. + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageSettings(Request $request, Response $response, $args) { + /** @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, 'uri_account_settings')) { + throw new ForbiddenException(); + } + + // Load validation rules + $schema = new RequestSchema('schema://requests/account-settings.yaml'); + $validatorAccountSettings = new JqueryValidationAdapter($schema, $this->ci->translator); + + $schema = new RequestSchema('schema://requests/profile-settings.yaml'); + $validatorProfileSettings = new JqueryValidationAdapter($schema, $this->ci->translator); + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get a list of all locales + $locales = $config->getDefined('site.locales.available'); + + return $this->ci->view->render($response, 'pages/account-settings.html.twig', [ + 'locales' => $locales, + 'page' => [ + 'validators' => [ + 'account_settings' => $validatorAccountSettings->rules('json', FALSE), + 'profile_settings' => $validatorProfileSettings->rules('json', FALSE) + ], + 'visibility' => ($authorizer->checkAccess($currentUser, 'update_account_settings') ? '' : 'disabled') + ] + ]); + } + + /** + * Render the account sign-in page for UserFrosting. + * + * This allows existing users to sign in. + * By definition, this is a "public page" (does not require authentication). + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageSignIn(Request $request, Response $response, $args) { + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + // Redirect if user is already logged in + if ($authenticator->check()) { + $redirect = $this->ci->get('redirect.onAlreadyLoggedIn'); + + return $redirect($request, $response, $args); + } + + // Load validation rules + $schema = new RequestSchema('schema://requests/login.yaml'); + $validatorLogin = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/sign-in.html.twig', [ + 'page' => [ + 'validators' => [ + 'login' => $validatorLogin->rules('json', FALSE) + ] + ] + ]); + } + + /** + * Processes a request to update a user's profile information. + * + * Processes the request from the user profile settings form, checking that: + * 1. They have the necessary permissions to update the posted field(s); + * 2. The submitted data is valid. + * This route requires authentication. + * Request type: POST + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function profile(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access control for entire resource - check that the current user has permission to modify themselves + // See recipe "per-field access control" for dynamic fine-grained control over which properties a user can modify. + if (!$authorizer->checkAccess($currentUser, 'update_account_settings')) { + $ms->addMessageTranslated('danger', 'ACCOUNT.ACCESS_DENIED'); + return $response->withStatus(403); + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/profile-settings.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + $error = FALSE; + + // Validate, and halt on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + $error = TRUE; + } + + // Check that locale is valid + $locales = $config->getDefined('site.locales.available'); + if (!array_key_exists($data['locale'], $locales)) { + $ms->addMessageTranslated('danger', 'LOCALE.INVALID', $data); + $error = TRUE; + } + + if ($error) { + return $response->withStatus(400); + } + + // Looks good, let's update with new values! + // Note that only fields listed in `profile-settings.yaml` will be permitted in $data, so this prevents the user from updating all columns in the DB + $currentUser->fill($data); + + $currentUser->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated their profile settings.", [ + 'type' => 'update_profile_settings' + ]); + + $ms->addMessageTranslated('success', 'PROFILE.UPDATED'); + return $response->withStatus(200); + } + + /** + * Processes an new account registration request. + * + * This is throttled to prevent account enumeration, since it needs to divulge when a username/email has been used. + * Processes the request from the form on the registration page, checking that: + * 1. The honeypot was not modified; + * 2. The master account has already been created (during installation); + * 3. Account registration is enabled; + * 4. The user is not already logged in; + * 5. Valid information was entered; + * 6. The captcha, if enabled, is correct; + * 7. The username and email are not already taken. + * Automatically sends an activation link upon success, if account activation is enabled. + * This route is "public access". + * Request type: POST + * Returns the User Object for the user record that was created. + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function register(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get POST parameters: user_name, first_name, last_name, email, password, passwordc, captcha, spiderbro, csrf_token + $params = $request->getParsedBody(); + + // Check the honeypot. 'spiderbro' is not a real field, it is hidden on the main page and must be submitted with its default value for this to be processed. + if (!isset($params['spiderbro']) || $params['spiderbro'] != 'http://') { + throw new SpammyRequestException('Possible spam received:' . print_r($params, TRUE)); + } + + // Security measure: do not allow registering new users until the master account has been created. + if (!$classMapper->staticMethod('user', 'find', $config['reserved_user_ids.master'])) { + $ms->addMessageTranslated('danger', 'ACCOUNT.MASTER_NOT_EXISTS'); + return $response->withStatus(403); + } + + // Check if registration is currently enabled + if (!$config['site.registration.enabled']) { + $ms->addMessageTranslated('danger', 'REGISTRATION.DISABLED'); + return $response->withStatus(403); + } + + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + // Prevent the user from registering if he/she is already logged in + if ($authenticator->check()) { + $ms->addMessageTranslated('danger', 'REGISTRATION.LOGOUT'); + return $response->withStatus(403); + } + + // Load the request schema + $schema = new RequestSchema('schema://requests/register.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\Throttle\Throttler $throttler */ + $throttler = $this->ci->throttler; + $delay = $throttler->getDelay('registration_attempt'); + + // Throttle requests + if ($delay > 0) { + return $response->withStatus(429); + } + + // 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; + } + + // Check captcha, if required + if ($config['site.registration.captcha']) { + $captcha = new Captcha($this->ci->session, $this->ci->config['session.keys.captcha']); + if (!$data['captcha'] || !$captcha->verifyCode($data['captcha'])) { + $ms->addMessageTranslated('danger', 'CAPTCHA.FAIL'); + $error = TRUE; + } + } + + if ($error) { + return $response->withStatus(400); + } + + // Remove captcha, password confirmation from object data after validation + unset($data['captcha']); + unset($data['passwordc']); + + if ($config['site.registration.require_email_verification']) { + $data['flag_verified'] = FALSE; + } else { + $data['flag_verified'] = TRUE; + } + + // Load default group + $groupSlug = $config['site.registration.user_defaults.group']; + $defaultGroup = $classMapper->staticMethod('group', 'where', 'slug', $groupSlug)->first(); + + if (!$defaultGroup) { + $e = new HttpException("Account registration is not working because the default group '$groupSlug' does not exist."); + $e->addUserMessage('REGISTRATION.BROKEN'); + throw $e; + } + + // Set default group + $data['group_id'] = $defaultGroup->id; + + // Set default locale + $data['locale'] = $config['site.registration.user_defaults.locale']; + + // Hash password + $data['password'] = Password::hash($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, $throttler) { + // Log throttleable event + $throttler->logEvent('registration_attempt'); + + // Create the user + $user = $classMapper->createInstance('user', $data); + + // Store new user to database + $user->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$user->user_name} registered for a new account.", [ + 'type' => 'sign_up', + 'user_id' => $user->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); + + // Verification email + if ($config['site.registration.require_email_verification']) { + // Try to generate a new verification request + $verification = $this->ci->repoVerification->create($user, $config['verification.timeout']); + + // Create and send verification email + $message = new TwigMailMessage($this->ci->view, 'mail/verify-account.html.twig'); + + $message->from($config['address_book.admin']) + ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) + ->addParams([ + 'user' => $user, + 'token' => $verification->getToken() + ]); + + $this->ci->mailer->send($message); + + $ms->addMessageTranslated('success', 'REGISTRATION.COMPLETE_TYPE2', $user->toArray()); + } else { + // No verification required + $ms->addMessageTranslated('success', 'REGISTRATION.COMPLETE_TYPE1'); + } + }); + + return $response->withStatus(200); + } + + /** + * Processes a request to resend the verification email for a new user account. + * + * Processes the request from the resend verification email form, checking that: + * 1. The rate limit on this type of request is observed; + * 2. The provided email is associated with an existing user account; + * 3. The user account is not already verified; + * 4. The submitted data is valid. + * This route is "public access". + * Request type: POST + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function resendVerification(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/resend-verification.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + return $response->withStatus(400); + } + + // Throttle requests + + /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ + $throttler = $this->ci->throttler; + + $throttleData = [ + 'email' => $data['email'] + ]; + $delay = $throttler->getDelay('verification_request', $throttleData); + + if ($delay > 0) { + $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', ['delay' => $delay]); + return $response->withStatus(429); + } + + // 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, $throttler, $throttleData, $config) { + // Log throttleable event + $throttler->logEvent('verification_request', $throttleData); + + // Load the user, by email address + $user = $classMapper->staticMethod('user', 'where', 'email', $data['email'])->first(); + + // Check that the user exists and is not already verified. + // If there is no user with that email address, or the user exists and is already verified, + // we pretend like we succeeded to prevent account enumeration + if ($user && $user->flag_verified != '1') { + // We're good to go - record user activity and send the email + $verification = $this->ci->repoVerification->create($user, $config['verification.timeout']); + + // Create and send verification email + $message = new TwigMailMessage($this->ci->view, 'mail/resend-verification.html.twig'); + + $message->from($config['address_book.admin']) + ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) + ->addParams([ + 'user' => $user, + 'token' => $verification->getToken() + ]); + + $this->ci->mailer->send($message); + } + }); + + $ms->addMessageTranslated('success', 'ACCOUNT.VERIFICATION.NEW_LINK_SENT', ['email' => $data['email']]); + return $response->withStatus(200); + } + + /** + * Processes a request to set the password for a new or current user. + * + * Processes the request from the password create/reset form, which should have the secret token embedded in it, checking that: + * 1. The provided secret token is associated with an existing user account; + * 2. The user has a password set/reset request in progress; + * 3. The token has not expired; + * 4. The submitted data (new password) is valid. + * This route is "public access". + * Request type: POST + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function setPassword(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/set-password.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + return $response->withStatus(400); + } + + $forgotPasswordPage = $this->ci->router->pathFor('forgot-password'); + + // Ok, try to complete the request with the specified token and new password + $passwordReset = $this->ci->repoPasswordReset->complete($data['token'], [ + 'password' => $data['password'] + ]); + + if (!$passwordReset) { + $ms->addMessageTranslated('danger', 'PASSWORD.FORGET.INVALID', ['url' => $forgotPasswordPage]); + return $response->withStatus(400); + } + + $ms->addMessageTranslated('success', 'PASSWORD.UPDATED'); + + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + // Log out any existing user, and create a new session + if ($authenticator->check()) { + $authenticator->logout(); + } + + // Auto-login the user (without "remember me") + $user = $passwordReset->user; + $authenticator->login($user); + + $ms->addMessageTranslated('success', 'WELCOME', $user->export()); + return $response->withStatus(200); + } + + /** + * Processes a request to update a user's account information. + * + * Processes the request from the user account settings form, checking that: + * 1. The user correctly input their current password; + * 2. They have the necessary permissions to update the posted field(s); + * 3. The submitted data is valid. + * This route requires authentication. + * Request type: POST + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function settings(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access control for entire resource - check that the current user has permission to modify themselves + // See recipe "per-field access control" for dynamic fine-grained control over which properties a user can modify. + if (!$authorizer->checkAccess($currentUser, 'update_account_settings')) { + $ms->addMessageTranslated('danger', 'ACCOUNT.ACCESS_DENIED'); + return $response->withStatus(403); + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/account-settings.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + $error = FALSE; + + // Validate, and halt on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + $error = TRUE; + } + + // Confirm current password + if (!isset($data['passwordcheck']) || !Password::verify($data['passwordcheck'], $currentUser->password)) { + $ms->addMessageTranslated('danger', 'PASSWORD.INVALID'); + $error = TRUE; + } + + // Remove password check, password confirmation from object data after validation + unset($data['passwordcheck']); + unset($data['passwordc']); + + // If new email was submitted, check that the email address is not in use + if (isset($data['email']) && $data['email'] != $currentUser->email && $classMapper->staticMethod('user', 'findUnique', $data['email'], 'email')) { + $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data); + $error = TRUE; + } + + if ($error) { + return $response->withStatus(400); + } + + // Hash new password, if specified + if (isset($data['password']) && !empty($data['password'])) { + $data['password'] = Password::hash($data['password']); + } else { + // Do not pass to model if no password is specified + unset($data['password']); + } + + // Looks good, let's update with new values! + // Note that only fields listed in `account-settings.yaml` will be permitted in $data, so this prevents the user from updating all columns in the DB + $currentUser->fill($data); + + $currentUser->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated their account settings.", [ + 'type' => 'update_account_settings' + ]); + + $ms->addMessageTranslated('success', 'ACCOUNT.SETTINGS.UPDATED'); + return $response->withStatus(200); + } + + /** + * Suggest an available username for a specified first/last name. + * + * This route is "public access". + * Request type: GET + * @odo Can this route be abused for account enumeration? If so we should throttle it as well. + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function suggestUsername(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $suggestion = AccountUtil::randomUniqueUsername($classMapper, 50, 10); + + // 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([ + 'user_name' => $suggestion + ], 200, JSON_PRETTY_PRINT); + } + + /** + * Processes an new email verification request. + * + * Processes the request from the email verification link that was emailed to the user, checking that: + * 1. The token provided matches a user in the database; + * 2. The user account is not already verified; + * This route is "public access". + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function verify(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + $loginPage = $this->ci->router->pathFor('login'); + + // GET parameters + $params = $request->getQueryParams(); + + // Load request schema + $schema = new RequestSchema('schema://requests/account-verify.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. This is a GET request, so we redirect on validation error. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + // 400 code + redirect is perfectly fine, according to user Dilaz in #laravel + return $response->withRedirect($loginPage, 400); + } + + $verification = $this->ci->repoVerification->complete($data['token']); + + if (!$verification) { + $ms->addMessageTranslated('danger', 'ACCOUNT.VERIFICATION.TOKEN_NOT_FOUND'); + return $response->withRedirect($loginPage, 400); + } + + $ms->addMessageTranslated('success', 'ACCOUNT.VERIFICATION.COMPLETE'); + + // Forward to login page + return $response->withRedirect($loginPage); + } +} -- cgit v1.2.3