diff --git a/lib/Alchemy/Phrasea/Account/CollectionRequestMapper.php b/lib/Alchemy/Phrasea/Account/CollectionRequestMapper.php
new file mode 100644
index 0000000000..5f14f65c3a
--- /dev/null
+++ b/lib/Alchemy/Phrasea/Account/CollectionRequestMapper.php
@@ -0,0 +1,61 @@
+app = $app;
+ $this->registrationManager = $registrationManager;
+ }
+
+ public function getUserRequests(User $user)
+ {
+ $databoxStatuses = $this->registrationManager->getRegistrationSummary($user);
+
+ $demands = array();
+
+ foreach ($databoxStatuses as $databoxId => $data) {
+ foreach ($data['registrations']['by-type']['pending'] as $collectionId => $waiting) {
+ $demands[] = $this->mapCollectionStatus($databoxId, $collectionId, "pending");
+ }
+
+ foreach ($data['registrations']['by-type']['rejected'] as $collectionId => $waiting) {
+ $demands[] = $this->mapCollectionStatus($databoxId, $collectionId, "rejected");
+ }
+
+ foreach ($data['registrations']['by-type']['accepted'] as $collectionId => $waiting) {
+ $demands[] = $this->mapCollectionStatus($databoxId, $collectionId, "accepted");
+ }
+ }
+
+ return $demands;
+ }
+
+ private function mapCollectionStatus($databoxId, $collectionId, $status)
+ {
+ $baseId = \phrasea::baseFromColl($databoxId, $collectionId, $this->app);
+
+ return array(
+ "databox_id" => $databoxId,
+ "base_id" => $baseId,
+ "collection_id" => $collectionId,
+ "status" => $status
+ );
+ }
+}
diff --git a/lib/Alchemy/Phrasea/Authentication/RecoveryService.php b/lib/Alchemy/Phrasea/Authentication/RecoveryService.php
new file mode 100644
index 0000000000..9b7d660deb
--- /dev/null
+++ b/lib/Alchemy/Phrasea/Authentication/RecoveryService.php
@@ -0,0 +1,150 @@
+application = $application;
+ $this->mailer = $mailer;
+ $this->tokenManipulator = $tokenManipulator;
+ $this->tokenRepository = $tokenRepository;
+ $this->userManipulator = $userManipulator;
+ $this->userRepository = $userRepository;
+ $this->urlGenerator = $urlGenerator;
+ }
+
+ /**
+ * @param $email
+ * @param bool $notifyUser
+ * @return string
+ * @throws InvalidArgumentException
+ */
+ public function requestPasswordResetToken($email, $notifyUser = true)
+ {
+ $user = $this->userRepository->findByEmail($email);
+
+ if (! $user) {
+ throw new InvalidArgumentException('phraseanet::erreur: Le compte n\'a pas ete trouve');
+ }
+
+ return $this->requestPasswordResetTokenByUser($user, $notifyUser);
+ }
+
+ /**
+ * @param $login
+ * @param bool $notifyUser
+ * @return string
+ * @throws InvalidArgumentException
+ */
+ public function requestPasswordResetTokenByLogin($login, $notifyUser = true)
+ {
+ $user = $this->userRepository->findByLogin($login);
+
+ if (! $user) {
+ throw new InvalidArgumentException('phraseanet::erreur: Le compte n\'a pas ete trouve');
+ }
+
+ return $this->requestPasswordResetTokenByUser($user, $notifyUser);
+ }
+
+ /**
+ * @param User $user
+ * @param bool $notifyUser
+ * @return string
+ */
+ private function requestPasswordResetTokenByUser(User $user, $notifyUser = true)
+ {
+ $receiver = Receiver::fromUser($user);
+ $token = $this->tokenManipulator->createResetPasswordToken($user);
+
+ if ($notifyUser) {
+ $url = $this->urlGenerator->generate('login_renew_password', [ 'token' => $token->getValue() ], true);
+
+ $mail = MailRequestPasswordUpdate::create($this->application, $receiver);
+ $mail->setLogin($user->getLogin());
+ $mail->setButtonUrl($url);
+ $mail->setExpiration(new \DateTime('+1 day'));
+
+ $this->mailer->deliver($mail);
+ }
+
+ return $token;
+ }
+
+ public function resetPassword($resetToken, $newPassword)
+ {
+ $token = $this->tokenRepository->findValidToken($resetToken);
+
+ if ($token === null || $token->getType() != TokenManipulator::TYPE_PASSWORD) {
+ $this->application->abort(401, 'A token is required');
+ }
+
+ $this->userManipulator->setPassword($token->getUser(), $newPassword);
+ $this->tokenManipulator->delete($token);
+ }
+}
diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php
index 48496cea46..d46b45b2bf 100644
--- a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php
+++ b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php
@@ -9,6 +9,7 @@
*/
namespace Alchemy\Phrasea\Controller\Api;
+use Alchemy\Phrasea\Account\CollectionRequestMapper;
use Alchemy\Phrasea\Application\Helper\DataboxLoggerAware;
use Alchemy\Phrasea\Application\Helper\DispatcherAware;
use Alchemy\Phrasea\Authentication\Context;
@@ -782,6 +783,42 @@ class V1Controller extends Controller
return $grants;
}
+ private function listUserDemands(User $user)
+ {
+ return (new CollectionRequestMapper($this->app, $this->app['registration.manager']))->getUserRequests($user);
+ }
+
+ public function resetPassword(Request $request, $email)
+ {
+ /** @var \Alchemy\Phrasea\Authentication\RecoveryService $service */
+ $service = $this->app['authentication.recovery_service'];
+
+ try {
+ $token = $service->requestPasswordResetToken($email, false);
+ }
+ catch (\Exception $exception) {
+ $token = $service->requestPasswordResetTokenByLogin($email, false);
+ }
+
+ return Result::create($request, [ 'reset_token' => $token ]);
+ }
+
+ public function setNewPassword(Request $request, $token)
+ {
+ $password = $request->request->get('password', null);
+ /** @var \Alchemy\Phrasea\Authentication\RecoveryService $service */
+ $service = $this->app['authentication.recovery_service'];
+
+ try {
+ $service->resetPassword($token, $password);
+ }
+ catch (\Exception $exception) {
+ return Result::create($request, [ 'success' => false ]);
+ }
+
+ return Result::create($request, [ 'success' => true ]);
+ }
+
public function addRecordAction(Request $request)
{
if (count($request->files->get('file')) == 0) {
@@ -1036,7 +1073,6 @@ class V1Controller extends Controller
continue;
}
-
if ($record->isStory()) {
$ret['results']['stories'][] = $this->listStory($request, $record);
} else {
@@ -2299,6 +2335,12 @@ class V1Controller extends Controller
"collections" => $this->listUserCollections($this->getAuthenticatedUser())
];
+ if (! constant('API_SKIP_USER_REGISTRATIONS')) {
+ // I am infinitely sorry... if you feel like it, you can fix the tests database bootstrapping
+ // to use SQLite in all cases and remove this check. Good luck...
+ $ret["demands"] = $this->listUserDemands($this->getAuthenticatedUser());
+ }
+
return Result::create($request, $ret)->createResponse();
}
diff --git a/lib/Alchemy/Phrasea/Controller/Root/LoginController.php b/lib/Alchemy/Phrasea/Controller/Root/LoginController.php
index dc67e1d3f4..3b75de108c 100644
--- a/lib/Alchemy/Phrasea/Controller/Root/LoginController.php
+++ b/lib/Alchemy/Phrasea/Controller/Root/LoginController.php
@@ -16,10 +16,11 @@ use Alchemy\Phrasea\Authentication\AccountCreator;
use Alchemy\Phrasea\Authentication\Exception\NotAuthenticatedException;
use Alchemy\Phrasea\Authentication\Exception\AuthenticationException;
use Alchemy\Phrasea\Authentication\Context;
-use Alchemy\Phrasea\Authentication\Phrasea\NativeAuthentication;
+use Alchemy\Phrasea\Authentication\Phrasea\PasswordAuthenticationInterface;
use Alchemy\Phrasea\Authentication\Phrasea\PasswordEncoder;
use Alchemy\Phrasea\Authentication\Provider\ProviderInterface;
use Alchemy\Phrasea\Authentication\ProvidersCollection;
+use Alchemy\Phrasea\Authentication\RecoveryService;
use Alchemy\Phrasea\Authentication\SuggestionFinder;
use Alchemy\Phrasea\Controller\Controller;
use Alchemy\Phrasea\Core\Configuration\ConfigurationInterface;
@@ -470,23 +471,17 @@ class LoginController extends Controller
public function renewPassword(Request $request)
{
- if (null === $tokenValue = $request->get('token')) {
- $this->app->abort(401, 'A token is required');
- }
-
- if (null === $token = $this->getTokenRepository()->findValidToken($tokenValue)) {
- $this->app->abort(401, 'A token is required');
- }
+ $service = $this->getRecoveryService();
$form = $this->app->form(new PhraseaRecoverPasswordForm($this->getTokenRepository()));
- $form->setData(['token' => $token->getValue()]);
+ $form->setData(['token' => $request->get('token') ]);
if ('POST' === $request->getMethod()) {
$form->handleRequest($request);
if ($form->isValid()) {
$data = $form->getData();
- $this->getUserManipulator()->setPassword($token->getUser(), $data['password']);
- $this->getTokenManipulator()->delete($token);
+
+ $service->resetPassword($data['token'], $data['password']);
$this->app->addFlash('success', $this->app->trans('login::notification: Mise a jour du mot de passe avec succes'));
return $this->app->redirectPath('homepage');
@@ -495,7 +490,7 @@ class LoginController extends Controller
return $this->render('login/renew-password.html.twig', array_merge(
$this->getDefaultTemplateVariables($request),
- ['form' => $form->createView()]
+ ['form' => $form->createView() ]
));
}
@@ -508,6 +503,7 @@ class LoginController extends Controller
public function forgotPassword(Request $request)
{
$form = $this->app->form(new PhraseaForgotPasswordForm());
+ $service = $this->getRecoveryService();
try {
if ('POST' === $request->getMethod()) {
@@ -516,27 +512,13 @@ class LoginController extends Controller
if ($form->isValid()) {
$data = $form->getData();
- if (null === $user = $this->getUserRepository()->findByEmail($data['email'])) {
- throw new FormProcessingException(_('phraseanet::erreur: Le compte n\'a pas ete trouve'));
- }
-
try {
- $receiver = Receiver::fromUser($user);
- } catch (InvalidArgumentException $e) {
- throw new FormProcessingException($this->app->trans('Invalid email address'));
+ $service->requestPasswordResetToken($data['email'], true);
+ }
+ catch (InvalidArgumentException $ex) {
+ throw new FormProcessingException($this->app->trans($ex->getMessage()));
}
- $token = $this->getTokenManipulator()->createResetPasswordToken($user);
-
- $url = $this->app->url('login_renew_password', ['token' => $token->getValue()], true);
-
- $expirationDate = new \DateTime('+1 day');
- $mail = MailRequestPasswordUpdate::create($this->app, $receiver);
- $mail->setLogin($user->getLogin());
- $mail->setButtonUrl($url);
- $mail->setExpiration($expirationDate);
-
- $this->deliver($mail);
$this->app->addFlash('info', $this->app->trans('phraseanet:: Un email vient de vous etre envoye'));
return $this->app->redirectPath('login_forgot_password');
@@ -548,9 +530,8 @@ class LoginController extends Controller
return $this->render('login/forgot-password.html.twig', array_merge(
$this->getDefaultTemplateVariables($request),
- [
- 'form' => $form->createView(),
- ]));
+ [ 'form' => $form->createView() ]
+ ));
}
/**
@@ -698,8 +679,8 @@ class LoginController extends Controller
public function postAuthProcess(Request $request, User $user)
{
$date = new \DateTime('+' . (int) $this->getConf()->get(['registry', 'actions', 'validation-reminder-days']) . ' days');
-
$manager = $this->getEntityManager();
+
foreach ($this->getValidationParticipantRepository()->findNotConfirmedAndNotRemindedParticipantsByExpireDate($date) as $participant) {
$validationSession = $participant->getSession();
$basket = $validationSession->getBasket();
@@ -862,8 +843,11 @@ class LoginController extends Controller
}
try {
- $usr_id = $this->getNativeAuthentication()
- ->getUsrId($request->request->get('login'), $request->request->get('password'), $request);
+ $usr_id = $this->getPasswordAuthentication()->getUsrId(
+ $request->request->get('login'),
+ $request->request->get('password'),
+ $request
+ );
} catch (RequireCaptchaException $e) {
$this->app->requireCaptcha();
$this->app->addFlash('warning', $this->app->trans('Please fill the captcha'));
@@ -883,7 +867,6 @@ class LoginController extends Controller
}
$user = $this->getUserRepository()->find($usr_id);
-
$session = $this->postAuthProcess($request, $user);
$response = $this->generateAuthResponse($this->getBrowser(), $request->request->get('redirect'));
@@ -1064,9 +1047,9 @@ class LoginController extends Controller
}
/**
- * @return NativeAuthentication
+ * @return PasswordAuthenticationInterface
*/
- private function getNativeAuthentication()
+ private function getPasswordAuthentication()
{
return $this->app['auth.native'];
}
@@ -1086,4 +1069,12 @@ class LoginController extends Controller
{
return $this->app['configuration.store'];
}
+
+ /**
+ * @return RecoveryService
+ */
+ private function getRecoveryService()
+ {
+ return $this->app['authentication.recovery_service'];
+ }
}
diff --git a/lib/Alchemy/Phrasea/ControllerProvider/Api/V1.php b/lib/Alchemy/Phrasea/ControllerProvider/Api/V1.php
index 362da070e7..051b723f85 100644
--- a/lib/Alchemy/Phrasea/ControllerProvider/Api/V1.php
+++ b/lib/Alchemy/Phrasea/ControllerProvider/Api/V1.php
@@ -249,6 +249,12 @@ class V1 implements ControllerProviderInterface, ServiceProviderInterface
$controllers->get('/me/', 'controller.api.v1:getCurrentUserAction');
+ $controllers->post('/accounts/reset-password/{login}/', 'controller.api.v1:resetPassword')
+ ->before('controller.api.v1:ensureAdmin');
+
+ $controllers->post('/accounts/update-password/{token}/', 'controller.api.v1:setNewPassword')
+ ->before('controller.api.v1:ensureAdmin');
+
return $controllers;
}
}
diff --git a/lib/Alchemy/Phrasea/Core/Provider/AuthenticationManagerServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/AuthenticationManagerServiceProvider.php
index b911194b53..632e675efd 100644
--- a/lib/Alchemy/Phrasea/Core/Provider/AuthenticationManagerServiceProvider.php
+++ b/lib/Alchemy/Phrasea/Core/Provider/AuthenticationManagerServiceProvider.php
@@ -22,6 +22,7 @@ use Alchemy\Phrasea\Authentication\Phrasea\FailureHandledNativeAuthentication;
use Alchemy\Phrasea\Authentication\Phrasea\NativeAuthentication;
use Alchemy\Phrasea\Authentication\Phrasea\OldPasswordEncoder;
use Alchemy\Phrasea\Authentication\Phrasea\PasswordEncoder;
+use Alchemy\Phrasea\Authentication\RecoveryService;
use Alchemy\Phrasea\Authentication\SuggestionFinder;
use Silex\Application;
use Silex\ServiceProviderInterface;
@@ -87,6 +88,18 @@ class AuthenticationManagerServiceProvider implements ServiceProviderInterface
return new Manager($app['authentication'], $app['authentication.providers']);
});
+ $app['authentication.recovery_service'] = $app->share(function (Application $app) {
+ return new RecoveryService(
+ $app,
+ $app['notification.deliverer'],
+ $app['manipulator.token'],
+ $app['repo.tokens'],
+ $app['manipulator.user'],
+ $app['repo.users'],
+ $app['url_generator']
+ );
+ });
+
$app['auth.password-encoder'] = $app->share(function (Application $app) {
return new PasswordEncoder($app['conf']->get(['main','key']));
});
diff --git a/lib/Alchemy/Phrasea/Model/Entities/Token.php b/lib/Alchemy/Phrasea/Model/Entities/Token.php
index cb0cd40aee..576cf05d36 100644
--- a/lib/Alchemy/Phrasea/Model/Entities/Token.php
+++ b/lib/Alchemy/Phrasea/Model/Entities/Token.php
@@ -177,7 +177,7 @@ class Token
/**
* Set expiration
*
- * @param \DateTime $updated
+ * @param \DateTime $expiration
* @return Token
*/
public function setExpiration(\DateTime $expiration = null)
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 16ddea2dc4..4c13095589 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -14,6 +14,7 @@
+
diff --git a/tests/Alchemy/Tests/Phrasea/Controller/Root/LoginTest.php b/tests/Alchemy/Tests/Phrasea/Controller/Root/LoginTest.php
index 185ccc64ee..a54630c467 100644
--- a/tests/Alchemy/Tests/Phrasea/Controller/Root/LoginTest.php
+++ b/tests/Alchemy/Tests/Phrasea/Controller/Root/LoginTest.php
@@ -332,7 +332,7 @@ class LoginTest extends \PhraseanetAuthenticatedWebTestCase
]);
$response = self::$DI['client']->getResponse();
- $this->assertEquals(401, $response->getStatusCode());
+ $this->assertEquals(200, $response->getStatusCode());
}
public function testRenewPasswordBadTokenWheneverItsAuthenticated()
@@ -359,7 +359,7 @@ class LoginTest extends \PhraseanetAuthenticatedWebTestCase
$response = self::$DI['client']->getResponse();
- $this->assertEquals(401, $response->getStatusCode());
+ $this->assertEquals(200, $response->getStatusCode());
}
public function testRenewPasswordNoTokenWheneverItsAuthenticated()
diff --git a/tests/classes/PhraseanetTestCase.php b/tests/classes/PhraseanetTestCase.php
index d7cb0e818e..53a449e162 100644
--- a/tests/classes/PhraseanetTestCase.php
+++ b/tests/classes/PhraseanetTestCase.php
@@ -30,7 +30,6 @@ abstract class PhraseanetTestCase extends WebTestCase
const USER_AGENT_IE6 = 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; .NET CLR 1.1.4322)';
const USER_AGENT_IPHONE = 'Mozilla/5.0 (iPod; U; CPU iPhone OS 2_1 like Mac OS X; fr-fr) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.1 Mobile/5F137 Safari/525.20';
-
protected static $DI;
private static $recordsInitialized = false;