<?php /** * Copyright (C) 2019 Leipzig University Library * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * @author Gregor Gawol <gawol@ub.uni-leipzig.de> * @author Sebastian Kehr <kehr@ub.uni-leipzig.de> * @license http://opensource.org/licenses/gpl-2.0.php GNU GPLv2 */ namespace fid\Controller; use fid\FormModel\PasswordChangeModel; use fid\FormModel\PasswordResetModel; use fid\FormModel\UserCreateModel; use fid\FormModel\UserInitModel; use fid\FormModel\UserUpdateModel; use fid\FormModel\UsernameChangeModel; use fid\Service\Client; use fid\Service\ClientException; use fid\Service\DataTransferObject\Address; use fid\Service\DataTransferObject\Library; use fid\Service\DataTransferObject\User; use fid\Service\UserNotAuthorizedException; use Symfony\Component\Serializer\SerializerAwareTrait; use VuFind\Auth\Manager as AuthManager; use VuFind\Controller\AbstractBase; use VuFind\Exception\Auth as AuthException; use Zend\Form\Element\MultiCheckbox; use Zend\Form\Element\Radio; use Zend\Form\Element\Select; use Zend\Form\Form; use Zend\Http\PhpEnvironment\Request; use Zend\Http\Response; use Zend\Mvc\Plugin\FlashMessenger\FlashMessenger; use Zend\ServiceManager\ServiceLocatorInterface; use Zend\View\Model\ViewModel; class UserController extends AbstractBase { use SerializerAwareTrait; /** * @var AuthManager */ protected $authManager; /** * @var Client */ protected $client; /** * @var array */ protected $config; /** * RegistrationController constructor. * * @param ServiceLocatorInterface $serviceLocator * @param AuthManager $authManager * @param Client $client * @param array $config */ public function __construct( ServiceLocatorInterface $serviceLocator, AuthManager $authManager, Client $client, array $config ) { parent::__construct($serviceLocator); $this->authManager = $authManager; $this->client = $client; $this->config = $config; } /** * @noinspection PhpUnused * @return ViewModel */ public function initAction() { /** @var Request $request */ $request = $this->getRequest(); $form = $this->serviceLocator->get(UserInitModel::class); $forwarded = $this->params()->fromRoute('forwarded', false); if ($submitted = $this->formWasSubmitted()) { $form->setData($request->getPost()); if (!$forwarded && $form->isValid()) { return $this->init($form); } } $view = $this->createViewModel(); $view->setVariables(compact('form')); $view->setTemplate('fid/user/init'); return $view; } /** * @noinspection PhpUnused * @return Response|ViewModel */ public function createAction() { /** @var Request $request */ $request = $this->getRequest(); $query = $request->getQuery(); $messenger = $this->getMessenger(); if ($credentials = $query->get('logon')) { try { $this->client->logon($credentials); } catch (ClientException $exception) { $message = $exception->getCode() === 401 ? 'fid::user_create_error_expired' : 'fid::user_create_error'; $messenger->addErrorMessage($this->translate($message)); return $this->redirect()->toRoute('fid/user/init'); } $query->offsetUnset('logon'); return $this->redirect()->toRoute('fid/user/create', [], [ 'query' => $query->toArray() ]); } try { $libraries = array_map(function (Library $libary) { return $libary->getLabel(); }, $this->client->requestLibraryList()); } catch (ClientException $exception) { $message = 'fid::user_create_error'; $messenger->addErrorMessage($this->translate($message)); return $this->redirect()->toRoute('fid/user/init'); } /** @var Form $form */ $form = $this->serviceLocator->get(UserCreateModel::class); /** @var Select $homeLibraryElement */ $homeLibraryElement = $form->get('homeLibrary'); $homeLibraryElement->setValueOptions($libraries); /** @var Radio $accessLevelElement */ $accessLevelElement = $form->get('accessLevel'); $accessLevels = $this->config['Security']['access_levels']; $accessLevelValueOptions = array_map(function ($accessLevel) { return "label_access_level_$accessLevel"; }, array_combine($accessLevels, $accessLevels)); $accessLevelElement->setValueOptions($accessLevelValueOptions); if ($this->formWasSubmitted()) { $form->setData($request->getPost()); if ($form->isValid()) { return $this->create($form); } } else { $form->setData($query); } $view = $this->createViewModel(); $view->setVariables(compact('form')); $view->setTemplate('fid/user/create'); return $view; } /** * @noinspection PhpUnused * @return Response|ViewModel */ public function updateAction() { /** @var Request $request */ $request = $this->getRequest(); try { $user = $this->client->requestUserDetails(); $libraries = array_map(function (Library $libary) { return $libary->getLabel(); }, $this->client->requestLibraryList()); } catch (ClientException $exception) { $this->setFollowupUrlToReferer(); $message = $exception->getCode() === 401 ? 'fid::user_update_error_expired' : 'fid::user_update_error'; $this->getMessenger()->addErrorMessage($this->translate($message)); return $this->redirect()->toRoute('myresearch-home'); } /** @var Form $form */ $form = $this->serviceLocator->get(UserUpdateModel::class); /** @var Select $homeLibraryElement */ $homeLibraryElement = $form->get('homeLibrary'); $homeLibraryElement->setValueOptions($libraries); if ($this->formWasSubmitted()) { $form->setData($request->getPost()); if ($form->isValid()) { return $this->update($form); } } $viewModel = $this->createViewModel(); $viewModel->setVariables(compact('form', 'user')); $viewModel->setTemplate('fid/user/update'); return $viewModel; } /** * @noinspection PhpUnused * @return ViewModel */ public function changeUsernameAction() { /** @var Request $request */ $request = $this->getRequest(); $form = $this->serviceLocator->get(UsernameChangeModel::class); $forwarded = $this->params()->fromRoute('forwarded', false); if ($submitted = $this->formWasSubmitted()) { $form->setData($request->getPost()); if (!$forwarded && $form->isValid()) { return $this->changeUsername($form); } } $view = $this->createViewModel(); $view->setVariables(compact('form')); $view->setTemplate('fid/user/username-change'); return $view; } /** * @noinspection PhpUnused * @return Response */ public function updateUsernameAction() { /** @var Request $request */ $request = $this->getRequest(); $query = $request->getQuery(); $messenger = $this->getMessenger(); if ($credentials = $query->get('logon')) { try { $this->client->logon($credentials); } catch (ClientException $exception) { $message = $exception->getCode() === 401 ? 'fid::username_update_error_expired' : 'fid::username_update_error'; $messenger->addErrorMessage($this->translate($message)); return $this->redirect()->toRoute('fid/user/change-username'); } $query->offsetUnset('logon'); return $this->redirect()->toRoute('fid/user/update-username', [], [ 'query' => $query->toArray() ]); } try { $user = $this->client->requestUserDetails(); $user->setUsername($query->get('username')); $this->client->requestUserUsernameUpdate($user); $message = $this->translate('fid::username_update_success'); $this->getMessenger()->addSuccessMessage($message); } catch (ClientException $exception) { $message = $this->translate('fid::username_update_error'); $this->getMessenger()->addErrorMessage($message); } return $this->redirect()->toRoute('myresearch-home'); } public function policyAction() { $viewModel = $this->createViewModel(); $viewModel->setTemplate('fid/user/policy'); if (isset($_REQUEST['lbreferer'])) { $viewModel->setVariable('backUrl', $_REQUEST['lbreferer']); } return $viewModel; } public function termsAction() { $viewModel = $this->createViewModel(); $viewModel->setTemplate('fid/user/terms'); if (isset($_REQUEST['lbreferer'])) { $viewModel->setVariable('backUrl', $_REQUEST['lbreferer']); } return $viewModel; } /** * Reset password action - Allows the reset password form to appear. * * @return ViewModel */ public function resetPasswordAction() { /** @var Form $form */ /** @var Request $request */ $request = $this->getRequest(); $form = $this->serviceLocator->get(PasswordResetModel::class); $forwarded = $this->params()->fromRoute('forwarded', false); if ($submitted = $this->formWasSubmitted()) { $form->setData($request->getPost()); if (!$forwarded && $form->isValid()) { return $this->sendResetPassword($form); } } $view = $this->createViewModel(); $view->setVariables(compact('form')); $view->setTemplate('fid/user/password-reset'); return $view; } /** * Reset password action - Allows the change password form to appear. * * @return Response|ViewModel */ public function changePasswordAction() { /** @var Request $request */ $request = $this->getRequest(); $query = $request->getQuery(); $messenger = $this->getMessenger(); if ($credentials = $query->get('logon')) { try { $this->client->logon($credentials); } catch (ClientException $exception) { $message = $exception->getCode() === 401 ? 'fid::password_change_error_expired' : 'fid::password_change_error'; $messenger->addErrorMessage($this->translate($message)); return $this->redirect()->toRoute('fid/user/reset-password'); } $query->offsetUnset('logon'); return $this->redirect()->toRoute('fid/user/change-password', [], [ 'query' => $query->toArray() ]); } $form = $this->serviceLocator->get(PasswordChangeModel::class); if ($this->formWasSubmitted()) { $form->setData($request->getPost()); if ($form->isValid()) { return $this->changePassword($form); } } else { $form->setData($query); } $view = $this->createViewModel(); $view->setVariables(compact('form')); $view->setTemplate('fid/user/password-change'); return $view; } /** * @param Form $form * * @return mixed|Response */ protected function init(Form $form) { /** @var UserInitModel $model */ $messenger = $this->getMessenger(); $model = $form->getHydrator() ->hydrate($form->getData(), new UserInitModel()); /** @noinspection PhpUndefinedFieldInspection */ $query['lng'] = $this->layout()->userLang; $username = $query['username'] = $model->getUsername(); $firstname = $query['firstname'] = $model->getFirstname(); $lastname = $query['lastname'] = $model->getLastname(); $baseUrl = $this->url()->fromRoute('fid/user/create', [], ['query' => $query, 'force_canonical' => true]); try { $this->client->requestRegistrationLink($baseUrl, $username, $firstname, $lastname); } catch (ClientException $exception) { $message = $exception->getCode() === 400 ? 'fid::user_init_error_username' : 'fid::user_init_error'; $messenger->addErrorMessage($this->translate($message)); return $this->forward()->dispatch(self::class, [ 'action' => 'init', 'forwarded' => true ]); } $message = $this->translate('fid::user_init_success'); $messenger->addSuccessMessage(sprintf($message, $username)); return $this->redirect()->toRoute('myresearch-home'); } protected function create(Form $form) { $messenger = $this->getMessenger(); /** @var UserCreateModel $model */ $model = $form->getHydrator() ->hydrate($form->getData(), new UserCreateModel()); $user = new User(); $user->setUsername($username = $model->getUsername()); $user->setPassword($password = $model->getPassword()); $user->setHomeLibrary($model->getHomeLibrary()); $user->setPermissions([$model->getAccessLevel() => 'requested']); $user->setSalutation($model->getSalutation()); $user->setAcademicTitle($model->getAcademicTitle()); $user->setFirstname($model->getFirstname()); $user->setLastname($model->getLastname()); $user->setYearOfBirth($model->getYearOfBirth()); $user->setCollege($model->getCollege()); $user->setJobTitle($model->getJobTitle()); if ($model->getAddressLine1()) { $address = new Address(); $address->setLine1($model->getAddressLine1()); $address->setLine2($model->getAddressLine2()); $address->setZip($model->getAddressZip()); $address->setCity($model->getAddressCity()); $address->setCountry($model->getAddressCountry()); $user->setAddresses([$address]); } try { $this->client->requestUserCreation($user); $message = $this->translate('fid::user_create_success'); $messenger->addSuccessMessage($message); /** @noinspection PhpParamsInspection */ $this->authManager->create($this->getRequest()); } catch (ClientException $exception) { $message = $this->translate('fid::user_create_error'); $messenger->addErrorMessage($message); } catch (AuthException $e) { $message = $this->translate('fid::user_create_error_autologon'); $messenger->addWarningMessage($message); } return $this->redirect()->toRoute('myresearch-home', [], [ 'query' => ['redirect' => false] ]); } /** * @param Form $form * * @return Response */ protected function update(Form $form,string $redirect = 'myresearch-home') { $messenger = $this->getMessenger(); /** @var UserUpdateModel $model */ $model = $form->getHydrator()->hydrate($form->getData(), new UserUpdateModel()); try { $userId = $model->getUserId(); $user = $this->client->requestUserDetails($userId); $user->setHomeLibrary($model->getHomeLibrary()); $user->setSalutation($model->getSalutation()); $user->setAcademicTitle($model->getAcademicTitle()); $user->setFirstname($model->getFirstname()); $user->setLastname($model->getLastname()); $user->setYearOfBirth($model->getYearOfBirth()); $user->setCollege($model->getCollege()); $user->setJobTitle($model->getJobTitle()); $user->setPermissions($model->getPermissions()); $addresses = $user->getAddresses(); foreach ($model->getAddresses() as $index => $address) { $addr = $addresses[$index] ?? new Address(); $addr->setLine1($address['line1']); $addr->setLine2($address['line2']); $addr->setZip($address['zip']); $addr->setCity($address['city']); $addr->setCountry($address['country']); $addresses[$index] = $addr; } $user->setAddresses($addresses); $this->client->requestUserUpdate($user); $message = $this->translate('fid::user_update_success'); $messenger->addSuccessMessage($message); } catch (ClientException $exception) { if (in_array($exception->getCode(), [403])) { $message = $this->translate('fid::user_update_error_' . $exception->getCode()); } else { $message = $this->translate('fid::user_update_error'); } $messenger->addErrorMessage($message); } $this->client->flushUserList(); return $this->redirect()->toRoute($redirect, [], [ 'query' => ['redirect' => false] ]); } protected function sendResetPassword(Form $form) { /** @var PasswordResetModel $model */ $messenger = $this->getMessenger(); $model = $form->getHydrator()->hydrate( $form->getData(), new PasswordResetModel()); $username = $model->getUsername(); try { /** @noinspection PhpUndefinedFieldInspection */ $query['lng'] = $this->layout()->userLang; $baseUrl = $this->url()->fromRoute('fid/user/change-password', [], ['query' => $query, 'force_canonical' => true]); $this->client->requestPasswordLink($baseUrl, $username); $message = $this->translate('fid::password_reset_success'); $messenger->addSuccessMessage(sprintf($message, $username)); } catch (ClientException $exception) { $message = $exception->getCode() === 400 ? $this->translate('fid::password_reset_error_username') : $this->translate('fid::password_reset_error'); $messenger->addErrorMessage(sprintf($message, $username)); return $this->redirect()->toRoute('fid/user/reset-password'); } return $this->redirect()->toRoute('myresearch-home'); } protected function changePassword(Form $form) { /** @var PasswordChangeModel $model */ $messenger = $this->getMessenger(); $model = $form->getHydrator()->hydrate( $form->getData(), new PasswordChangeModel()); try { $user = $this->client->requestUserDetails(); $user->setPassword($password = $model->getPassword()); $this->client->requestUserPasswordUpdate($user); $message = $this->translate('fid::password_change_success'); $message = sprintf($message, $username = $user->getUsername()); $messenger->addSuccessMessage($message); /** @var Request $request */ $request = clone $this->getRequest(); $params = clone $request->getPost(); $params->set('username', $username); $params->set('password', $password); $request->setPost($params); $this->authManager->create($request); } catch (ClientException $exception) { $message = $this->translate('fid::password_change_error'); $messenger->addErrorMessage($message); } catch (AuthException $e) { $message = $this->translate('fid::password_change_error_autologon'); $messenger->addErrorMessage($message); } return $this->redirect()->toRoute('myresearch-home', [], [ 'query' => ['redirect' => false] ]); } protected function changeUsername(Form $form) { $messenger = $this->getMessenger(); $model = $form->getHydrator()->hydrate( $form->getData(), new UsernameChangeModel()); /** @noinspection PhpUndefinedFieldInspection */ $query['lng'] = $this->layout()->userLang; $username = $query['username'] = $model->getUsername(); $baseUrl = $this->url()->fromRoute('fid/user/update-username', [], ['query' => $query, 'force_canonical' => true]); try { $this->client->requestUsernameLink($baseUrl, $username); } catch (ClientException $exception) { $message = $exception->getCode() === 400 ? 'fid::username_change_error_username' : 'fid::username_change_error'; $messenger->addErrorMessage($this->translate($message)); return $this->forward()->dispatch(self::class, [ 'action' => 'changeUsername', 'forwarded' => true ]); } $message = $this->translate('fid::username_change_success'); $messenger->addSuccessMessage(sprintf($message, $username)); return $this->redirect()->toRoute('myresearch-home'); } protected function getMessenger(): FlashMessenger { /** @noinspection PhpUndefinedMethodInspection */ /** @var FlashMessenger $messenger */ return $this->flashMessenger(); } public function editAction() { // Not logged in? Force user to log in: if (!($user = $this->getUser())) { return $this->forceLogin(); } /** @var Request $request */ $request = $this->getRequest(); $userId = $this->params()->fromRoute('userid'); if (empty($userId)) { // if no user ID is set the call is not valid // unless we are in submission state return $this->redirect()->toRoute('fid/admin/list'); } $editableFields = $this->config['Admin']['editable_user_fields'] ?? []; try { try { $user = $this->client->requestUserDetails($userId); } catch (UserNotAuthorizedException $ex) { // either user does not exist, or we are not allowed to edit it $this->getMessenger()->addErrorMessage($this->translate('fid::user_edit_not_allowed', ['%%userid%%' => $userId])); return $this->redirect()->toRoute('fid/admin/list'); } catch (\Exception $ex) { $this->getMessenger()->addErrorMessage($this->translate('fid::user_read_error',['%%userid%%' => $userId])); return $this->redirect()->toRoute('fid/admin/list'); } $libraries = $this->client->requestLibraryList(); } catch (ClientException $exception) { $this->setFollowupUrlToReferer(); $message = $exception->getCode() === 401 ? 'fid::user_update_error_expired' : 'fid::user_update_error'; $this->getMessenger()->addErrorMessage($this->translate($message)); return $this->redirect()->toRoute('fid/admin/list'); } /** @var Form $form */ $form = $this->serviceLocator->get(UserUpdateModel::class); if (!empty($libraries)) { $userLibrary = $user->getHomeLibrary(); $libraries = array_map(function (Library $libary) use ($editableFields,$userLibrary) { $option = [ 'label' => $libary->getLabel(), 'value' => $id = $libary->getId(), 'selected' => $id == $userLibrary ]; if ($id != $userLibrary && !in_array('HomeLibrary', $editableFields)) { $option['attributes']['disabled'] = 'disabled'; } return $option; }, $libraries); /** @var Select $homeLibraryElement */ $homeLibraryElement = $form->get('homeLibrary'); $homeLibraryElement->setValueOptions($libraries); } if ($permissions = $this->config['Admin']['permission_options'] ?? []) { $userPermissions = $user->getPermissions(); if (!isset(self::$currentPermissions)) { $valid = []; foreach ($permissions as $permission) { $valid[$permission] = $userPermissions[$permission] ?? 'denied'; } self::$currentPermissions = $valid; } /** @var MultiCheckbox $permissionsElement */ $permissionsElement = $form->get('permissions'); $permissions = array_map(function ($permission) use ($editableFields,$userPermissions) { $option = [ 'label' => $this->translate('fid::permission_' . $permission), 'name' => $permission, 'label_attributes' => ['class' => 'permission-label col-sm-11'], 'attributes' => ['class' => 'permission-option col-sm-1'], 'selected' => isset($userPermissions[$permission]) && $userPermissions[$permission] == 'granted', 'value' => $permission . '::' . ($userPermissions[$permission] ?? 'denied'), ]; if (!in_array('Permissions', $editableFields)) { $option['disabled'] = '1'; } return $option; }, $permissions); $permissionsElement->setDisableInArrayValidator(true); $permissionsElement->setValueOptions($permissions); } if ($this->formWasSubmitted()) { $form->setData($request->getPost()); if ($form->isValid()) { return $this->update($form,'fid/admin/list'); } } $viewModel = $this->createViewModel(); $viewModel->setVariables(compact('form', 'user','editableFields')); $viewModel->setTemplate('fid/admin/edit'); return $viewModel; } public static $currentPermissions; public static function combinePermissionsArray($input) { if (empty($input)) { return []; } $output = []; foreach ($input as $line) { list($key,) = explode('::',$line); if (isset(self::$currentPermissions[$key])) { $output[$key] = 'granted'; } } foreach (self::$currentPermissions as $key => $state) { if ($state === 'granted' && !isset($output[$key])) { $output[$key] = 'denied'; } } return array_intersect_key($output,['full_access'=>'0','limited_access'=>1]); } public function listAction() { // Not logged in? Force user to log in: if (!($user = $this->getUser())) { return $this->forceLogin(); } $viewModel = $this->createViewModel(); try { $list = $this->client->requestUserList(); $viewModel->setVariable('list',$list); } catch (UserNotAuthorizedException $exception) { $this->getMessenger()->addErrorMessage('fid::read_user_list_not_allowed'); } catch (ClientException $exception) { $this->getMessenger()->addErrorMessage('fid::read_user_list_error'); } if ($fields = $this->config['Admin']['overview_fields']) { $viewModel->setVariable('fields',$fields); } $viewModel->setTemplate('fid/admin/list'); return $viewModel; } }