diff --git a/lib/Alchemy/Phrasea/Application.php b/lib/Alchemy/Phrasea/Application.php index 8c7b2f58c2..5c3dfdffe0 100644 --- a/lib/Alchemy/Phrasea/Application.php +++ b/lib/Alchemy/Phrasea/Application.php @@ -72,6 +72,7 @@ use Alchemy\Phrasea\Controller\Utils\ConnectionTest; use Alchemy\Phrasea\Controller\Utils\PathFileTest; use Alchemy\Phrasea\Controller\User\Notifications; use Alchemy\Phrasea\Controller\User\Preferences; +use Alchemy\Phrasea\Core\Middleware\TokenMiddlewareProvider; use Alchemy\Phrasea\Core\PhraseaExceptionHandler; use Alchemy\Phrasea\Core\Event\Subscriber\LogoutSubscriber; use Alchemy\Phrasea\Core\Event\Subscriber\PhraseaLocaleSubscriber; @@ -211,6 +212,7 @@ class Application extends SilexApplication } $this->register(new BasketMiddlewareProvider()); + $this->register(new TokenMiddlewareProvider()); $this->register(new ACLServiceProvider()); $this->register(new AuthenticationManagerServiceProvider()); diff --git a/lib/Alchemy/Phrasea/Authentication/Token/TokenValidator.php b/lib/Alchemy/Phrasea/Authentication/Token/TokenValidator.php deleted file mode 100644 index 6a19d0f66f..0000000000 --- a/lib/Alchemy/Phrasea/Authentication/Token/TokenValidator.php +++ /dev/null @@ -1,43 +0,0 @@ -random = $random; - } - - /** - * Returns true if the token is valid - * - * @param type $token - * @return boolean - */ - public function isValid($token) - { - try { - $datas = $this->random->helloToken($token); - - return $datas['usr_id']; - } catch (NotFoundHttpException $e) { - - } - - return false; - } -} diff --git a/lib/Alchemy/Phrasea/Command/Developer/RegenerateSqliteDb.php b/lib/Alchemy/Phrasea/Command/Developer/RegenerateSqliteDb.php index a2318932c3..6da4cb6540 100644 --- a/lib/Alchemy/Phrasea/Command/Developer/RegenerateSqliteDb.php +++ b/lib/Alchemy/Phrasea/Command/Developer/RegenerateSqliteDb.php @@ -26,6 +26,7 @@ use Alchemy\Phrasea\Model\Entities\LazaretSession; use Alchemy\Phrasea\Model\Entities\Registration; use Alchemy\Phrasea\Model\Entities\Session; use Alchemy\Phrasea\Model\Entities\Task; +use Alchemy\Phrasea\Model\Entities\Token; use Alchemy\Phrasea\Model\Entities\User; use Alchemy\Phrasea\Model\Entities\ValidationData; use Alchemy\Phrasea\Model\Entities\ValidationSession; @@ -35,6 +36,7 @@ use Alchemy\Phrasea\Model\Entities\UsrList; use Alchemy\Phrasea\Model\Entities\UsrListEntry; use Alchemy\Phrasea\Model\Entities\StoryWZ; use Alchemy\Phrasea\Core\Provider\ORMServiceProvider; +use Alchemy\Phrasea\Model\Manipulator\TokenManipulator; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Tools\SchemaTool; use Gedmo\Timestampable\TimestampableListener; @@ -88,6 +90,8 @@ class RegenerateSqliteDb extends Command $this->insertOauthApps($DI); $this->generateCollection($DI); $this->generateRecord($DI); + $this->insertTwoTasks($this->container['EM']); + $this->insertTwoBasket($this->container['EM'], $DI); $this->insertOneStoryInWz($this->container['EM'], $DI); $this->insertUsrLists($this->container['EM'], $DI); $this->insertOnePrivateFeed($this->container['EM'], $DI); @@ -99,7 +103,21 @@ class RegenerateSqliteDb extends Command $this->insertOneRegistration($DI, $this->container['EM'], $DI['user_alt1'], $DI['coll'], 'now', 'registration_1'); $this->insertOneRegistration($DI, $this->container['EM'], $DI['user_alt2'], $DI['coll'], '-3 months', 'registration_2'); $this->insertOneRegistration($DI, $this->container['EM'], $DI['user_notAdmin'], $DI['coll'], 'now', 'registration_3'); + $this->insertTwoTokens($this->container['EM'], $DI); + $this->insertOneInvalidToken($this->container['EM'], $DI); + $this->insertOneValidationToken($this->container['EM'], $DI); + $this->container['EM']->flush(); + + $fixtures['basket']['basket_1'] = $DI['basket_1']->getId(); + $fixtures['basket']['basket_2'] = $DI['basket_2']->getId(); + $fixtures['basket']['basket_3'] = $DI['basket_3']->getId(); + $fixtures['basket']['basket_4'] = $DI['basket_4']->getId(); + + $fixtures['token']['token_1'] = $DI['token_1']->getValue(); + $fixtures['token']['token_2'] = $DI['token_2']->getValue(); + $fixtures['token']['token_invalid'] = $DI['token_invalid']->getValue(); + $fixtures['token']['token_validation'] = $DI['token_validation']->getValue(); $fixtures['user']['test_phpunit'] = $DI['user']->getId(); $fixtures['user']['test_phpunit_not_admin'] = $DI['user_notAdmin']->getId(); $fixtures['user']['test_phpunit_alt1'] = $DI['user_alt1']->getId(); @@ -140,10 +158,13 @@ class RegenerateSqliteDb extends Command $fixtures['user']['user_3_deleted'] = $DI['user_3_deleted']->getId(); $fixtures['user']['user_template'] = $DI['user_template']->getId(); - $this->insertTwoTasks($this->container['EM']); - $this->insertTwoBasket($this->container['EM'], $DI); + $fixtures['feed']['public']['feed'] = $DI['feed_public']->getId(); + $fixtures['feed']['public']['entry'] = $DI['feed_public_entry']->getId(); + $fixtures['feed']['public']['token'] = $DI['feed_public_token']->getId(); - $this->container['EM']->flush(); + $fixtures['feed']['private']['feed'] = $DI['feed_private']->getId(); + $fixtures['feed']['private']['entry'] = $DI['feed_private_entry']->getId(); + $fixtures['feed']['private']['token'] = $DI['feed_private_token']->getId(); } catch (\Exception $e) { $output->writeln("".$e->getMessage().""); if ($renamed) { @@ -200,7 +221,6 @@ class RegenerateSqliteDb extends Command $session = new LazaretSession(); $session->setUser($DI['user']); $em->persist($session); - $em->flush(); $file = File::buildFromPathfile($this->container['root.path'] . '/tests/files/cestlafete.jpg', $DI['coll'], $this->container); @@ -248,8 +268,6 @@ class RegenerateSqliteDb extends Command $em->persist($user2Deleted); $em->persist($user3Deleted); $em->persist($template); - - $em->flush(); } protected function insertOneUser($login, $email = null, $admin = false) @@ -385,6 +403,8 @@ class RegenerateSqliteDb extends Command $basket1->setName('test'); $basket1->setDescription('description test'); + $DI['basket_1'] = $basket1; + $element = new BasketElement(); $element->setRecord($DI['record_1']); $basket1->addElement($element); @@ -395,11 +415,15 @@ class RegenerateSqliteDb extends Command $basket2->setName('test'); $basket2->setDescription('description test'); + $DI['basket_2'] = $basket2; + $basket3 = new Basket(); $basket3->setUser($this->getUserAlt1()); $basket3->setName('test'); $basket3->setDescription('description test'); + $DI['basket_3'] = $basket3; + $em->persist($basket1); $em->persist($element); $em->persist($basket2); @@ -442,6 +466,8 @@ class RegenerateSqliteDb extends Command $em->persist($validationParticipant); } + $DI['basket_4'] = $basket4; + $em->persist($basket4); } @@ -524,8 +550,12 @@ class RegenerateSqliteDb extends Command $em->persist($feed); $em->persist($publisher); - $this->insertOneFeedEntry($em, $DI, $feed, true); - $this->insertOneFeedToken($em, $DI, $feed); + $entry = $this->insertOneFeedEntry($em, $DI, $feed, true); + $token = $this->insertOneFeedToken($em, $DI, $feed); + + $DI['feed_public'] = $feed; + $DI['feed_public_entry'] = $entry; + $DI['feed_public_token'] = $token; } private function insertOnePrivateFeed(EntityManager $em, \Pimple $DI) @@ -547,8 +577,12 @@ class RegenerateSqliteDb extends Command $em->persist($feed); $em->persist($publisher); - $this->insertOneFeedEntry($em, $DI, $feed, false); - $this->insertOneFeedToken($em, $DI, $feed); + $entry = $this->insertOneFeedEntry($em, $DI, $feed, false); + $token = $this->insertOneFeedToken($em, $DI, $feed); + + $DI['feed_private'] = $feed; + $DI['feed_private_entry'] = $entry; + $DI['feed_private_token'] = $token; } private function insertOneExtraFeed(EntityManager $em, \Pimple $DI) @@ -595,12 +629,14 @@ class RegenerateSqliteDb extends Command $em->persist($feed); $this->insertOneFeedItem($em, $DI, $entry, $public); + + return $entry; } private function insertOneFeedToken(EntityManager $em, \Pimple $DI, Feed $feed) { $token = new FeedToken(); - $token->setValue($this->container['random.low']->generateString(64, \random::LETTERS_AND_NUMBERS)); + $token->setValue($this->container['random.low']->generateString(64, TokenManipulator::LETTERS_AND_NUMBERS)); $token->setFeed($feed); $token->setUser($DI['user']); @@ -608,6 +644,8 @@ class RegenerateSqliteDb extends Command $em->persist($token); $em->persist($feed); + + return $token; } private function insertOneAggregateToken(EntityManager $em, \Pimple $DI) @@ -615,12 +653,61 @@ class RegenerateSqliteDb extends Command $user = $DI['user']; $token = new AggregateToken(); - $token->setValue($this->container['random.low']->generateString(64, \random::LETTERS_AND_NUMBERS)); + $token->setValue($this->container['random.low']->generateString(64, TokenManipulator::LETTERS_AND_NUMBERS)); $token->setUser($user); $em->persist($token); } + private function insertTwoTokens(EntityManager $em, \Pimple $DI) + { + $user = $DI['user']; + + $token = new Token(); + $token->setValue($this->container['random.low']->generateString(12, TokenManipulator::LETTERS_AND_NUMBERS)); + $token->setUser($user); + $token->setType(TokenManipulator::TYPE_RSS); + $token->setData('some data'); + $DI['token_1'] = $token; + $em->persist($token); + + $token = new Token(); + $token->setValue($this->container['random.low']->generateString(12, TokenManipulator::LETTERS_AND_NUMBERS)); + $token->setUser($user); + $token->setType(TokenManipulator::TYPE_RSS); + $token->setData('some data'); + $token->setExpiration(new \DateTime('+1 year')); + $DI['token_2'] = $token; + $em->persist($token); + } + + private function insertOneInvalidToken(EntityManager $em, \Pimple $DI) + { + $user = $DI['user']; + + $token = new Token(); + $token->setValue($this->container['random.low']->generateString(12, TokenManipulator::LETTERS_AND_NUMBERS)); + $token->setUser($user); + $token->setType(TokenManipulator::TYPE_RSS); + $token->setData('some data'); + $token->setExpiration(new \DateTime('-1 day')); + $DI['token_invalid'] = $token; + $em->persist($token); + } + + private function insertOneValidationToken(EntityManager $em, \Pimple $DI) + { + $user = $DI['user']; + + $token = new Token(); + $token->setValue($this->container['random.low']->generateString(12, TokenManipulator::LETTERS_AND_NUMBERS)); + $token->setUser($user); + $token->setType(TokenManipulator::TYPE_VALIDATE); + $token->setData($DI['basket_1']->getId()); + $DI['token_validation'] = $token; + $em->persist($token); + } + private function insertOneFeedItem(EntityManager $em, \Pimple $DI, FeedEntry $entry, $public) { if ($public) { @@ -656,7 +743,6 @@ class RegenerateSqliteDb extends Command $registration->setUpdated(new \DateTime($when)); $registration->setCreated(new \DateTime($when)); $em->persist($registration); - $em->flush(); $em->getEventManager()->addEventSubscriber(new TimestampableListener()); $DI[$name] = $registration; diff --git a/lib/Alchemy/Phrasea/Controller/Lightbox.php b/lib/Alchemy/Phrasea/Controller/Lightbox.php index ea3dda739a..f91d948e22 100644 --- a/lib/Alchemy/Phrasea/Controller/Lightbox.php +++ b/lib/Alchemy/Phrasea/Controller/Lightbox.php @@ -15,6 +15,7 @@ use Alchemy\Phrasea\Model\Entities\Basket; use Alchemy\Phrasea\Model\Entities\BasketElement; use Alchemy\Phrasea\Exception\SessionNotFound; use Alchemy\Phrasea\Controller\Exception as ControllerException; +use Alchemy\Phrasea\Model\Manipulator\TokenManipulator; use Silex\ControllerProviderInterface; use Silex\Application as SilexApplication; use Symfony\Component\HttpFoundation\Request; @@ -38,26 +39,21 @@ class Lightbox implements ControllerProviderInterface $app['authentication']->closeAccount(); } - if (false === $usr_id = $app['authentication.token-validator']->isValid($request->query->get('LOG'))) { + if (null === $token = $app['repo.tokens']->findValidToken($request->query->get('LOG'))) { $app->addFlash('error', $app->trans('The URL you used is out of date, please login')); return $app->redirectPath('homepage'); } - $app['authentication']->openAccount($app['repo.users']->find($usr_id)); + $app['authentication']->openAccount($token->getUser()); - try { - $datas = $app['tokens']->helloToken($request->query->get('LOG')); - } catch (NotFoundHttpException $e) { - return; - } - switch ($datas['type']) { - case \random::TYPE_FEED_ENTRY: - return $app->redirectPath('lightbox_feed_entry', ['entry_id' => $datas['datas']]); + switch ($token->getType()) { + case TokenManipulator::TYPE_FEED_ENTRY: + return $app->redirectPath('lightbox_feed_entry', ['entry_id' => $token->getData()]); break; - case \random::TYPE_VALIDATE: - case \random::TYPE_VIEW: - return $app->redirectPath('lightbox_validation', ['basket' => $datas['datas']]); + case TokenManipulator::TYPE_VALIDATE: + case TokenManipulator::TYPE_VIEW: + return $app->redirectPath('lightbox_validation', ['basket' => $token->getData()]); break; } }); @@ -464,13 +460,8 @@ class Lightbox implements ControllerProviderInterface /* @var $basket Basket */ $participant = $basket->getValidation()->getParticipant($app['authentication']->getUser()); - $expires = new \DateTime('+10 days'); - $url = $app->url('lightbox', ['LOG' => $app['tokens']->getUrlToken( - \random::TYPE_VALIDATE - , $basket->getValidation()->getInitiator($app)->getId() - , $expires - , $basket->getId() - )]); + $token = $app['manipulator.token']->createBasketValidationToken($basket); + $url = $app->url('lightbox', ['LOG' => $token->getValue()]); $to = $basket->getValidation()->getInitiator($app)->getId(); $params = [ diff --git a/lib/Alchemy/Phrasea/Controller/Prod/DoDownload.php b/lib/Alchemy/Phrasea/Controller/Prod/DoDownload.php index 5391c09e5d..b513c9183e 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/DoDownload.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/DoDownload.php @@ -12,6 +12,7 @@ namespace Alchemy\Phrasea\Controller\Prod; use Alchemy\Phrasea\Http\DeliverDataInterface; +use Alchemy\Phrasea\Model\Entities\Token; use Silex\Application; use Silex\ControllerProviderInterface; use Symfony\Component\HttpFoundation\Request; @@ -32,16 +33,19 @@ class DoDownload implements ControllerProviderInterface $controllers = $app['controllers_factory']; $controllers->get('/{token}/prepare/', 'controller.prod.do-download:prepareDownload') + ->before($app['middleware.token.converter']) ->bind('prepare_download') - ->assert('token', '[a-zA-Z0-9\.\/]{8,16}'); + ->assert('token', '[a-zA-Z0-9]{8,32}'); $controllers->match('/{token}/get/', 'controller.prod.do-download:downloadDocuments') + ->before($app['middleware.token.converter']) ->bind('document_download') - ->assert('token', '[a-zA-Z0-9\.\/]{8,16}'); + ->assert('token', '[a-zA-Z0-9]{8,32}'); $controllers->post('/{token}/execute/', 'controller.prod.do-download:downloadExecute') + ->before($app['middleware.token.converter']) ->bind('execute_download') - ->assert('token', '[a-zA-Z0-9\.\/]{8,16}'); + ->assert('token', '[a-zA-Z0-9]{8,32}'); return $controllers; } @@ -51,15 +55,13 @@ class DoDownload implements ControllerProviderInterface * * @param Application $app * @param Request $request - * @param String $token + * @param Token $token * * @return Response */ - public function prepareDownload(Application $app, Request $request, $token) + public function prepareDownload(Application $app, Request $request, Token $token) { - $datas = $app['tokens']->helloToken($token); - - if (false === $list = @unserialize((string) $datas['datas'])) { + if (false === $list = @unserialize($token->getData())) { $app->abort(500, 'Invalid datas'); } @@ -96,15 +98,13 @@ class DoDownload implements ControllerProviderInterface * * @param Application $app * @param Request $request - * @param String $token + * @param Token $token * * @return Response */ - public function downloadDocuments(Application $app, Request $request, $token) + public function downloadDocuments(Application $app, Request $request, Token $token) { - $datas = $app['tokens']->helloToken($token); - - if (false === $list = @unserialize((string) $datas['datas'])) { + if (false === $list = @unserialize($token->getData())) { $app->abort(500, 'Invalid datas'); } @@ -118,7 +118,7 @@ class DoDownload implements ControllerProviderInterface $mime = $subdef['mime']; $list['complete'] = true; } else { - $exportFile = $app['root.path'] . '/tmp/download/' . $datas['value'] . '.zip'; + $exportFile = $app['root.path'] . '/tmp/download/' . $token->getValue() . '.zip'; $mime = 'application/zip'; } @@ -144,22 +144,13 @@ class DoDownload implements ControllerProviderInterface * * @param Application $app * @param Request $request - * @param String $token + * @param Token $token * * @return Response */ - public function downloadExecute(Application $app, Request $request, $token) + public function downloadExecute(Application $app, Request $request, Token $token) { - try { - $datas = $app['tokens']->helloToken($token); - } catch (NotFoundHttpException $e) { - return $app->json([ - 'success' => false, - 'message' => 'Invalid token' - ]); - } - - if (false === $list = @unserialize((string) $datas['datas'])) { + if (false === $list = @unserialize($token->getData())) { return $app->json([ 'success' => false, 'message' => 'Invalid datas' @@ -175,7 +166,7 @@ class DoDownload implements ControllerProviderInterface $app, $token, $list, - sprintf($app['root.path'] . '/tmp/download/%s.zip', $datas['value']) // Dest file + sprintf($app['root.path'] . '/tmp/download/%s.zip', $token->getValue()) // Dest file ); return $app->json([ diff --git a/lib/Alchemy/Phrasea/Controller/Prod/Download.php b/lib/Alchemy/Phrasea/Controller/Prod/Download.php index 778491c79d..0a6ce8e57e 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/Download.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/Download.php @@ -64,16 +64,7 @@ class Download implements ControllerProviderInterface $list['export_name'] = sprintf('%s.zip', $download->getExportName()); - $token = $app['tokens']->getUrlToken( - \random::TYPE_DOWNLOAD, - $app['authentication']->getUser()->getId(), - new \DateTime('+3 hours'), // Token lifetime - serialize($list) - ); - - if (!$token) { - throw new \RuntimeException('Download token could not be generated'); - } + $token = $app['manipulator.token']->createDownloadToken($app['authentication']->getUser(), serialize($list)); $app['events-manager']->trigger('__DOWNLOAD__', [ 'lst' => $lst, @@ -83,6 +74,6 @@ class Download implements ControllerProviderInterface 'export_file' => $download->getExportName() ]); - return $app->redirectPath('prepare_download', ['token' => $token]); + return $app->redirectPath('prepare_download', ['token' => $token->getValue()]); } } diff --git a/lib/Alchemy/Phrasea/Controller/Prod/Export.php b/lib/Alchemy/Phrasea/Controller/Prod/Export.php index 51d617b63c..d97f8f47b0 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/Export.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/Export.php @@ -215,22 +215,20 @@ class Export implements ControllerProviderInterface } } - //generate validation token - $endDateObject = new \DateTime('+1 day'); - $token = $app['tokens']->getUrlToken(\random::TYPE_EMAIL, false, $endDateObject, serialize($list)); + $token = $app['manipulator.token']->createEmailExportToken(serialize($list)); - if (count($destMails) > 0 && $token) { + if (count($destMails) > 0) { //zip documents \set_export::build_zip( $app, $token, $list, - $app['root.path'] . '/tmp/download/' . $token . '.zip' + $app['root.path'] . '/tmp/download/' . $token->getValue() . '.zip' ); $remaingEmails = $destMails; - $url = $app->url('prepare_download', ['token' => $token, 'anonymous']); + $url = $app->url('prepare_download', ['token' => $token->getValue(), 'anonymous']); $emitter = new Emitter($app['authentication']->getUser()->getDisplayName(), $app['authentication']->getUser()->getEmail()); @@ -243,7 +241,7 @@ class Export implements ControllerProviderInterface $mail = MailRecordsExport::create($app, $receiver, $emitter, $request->request->get('textmail')); $mail->setButtonUrl($url); - $mail->setExpiration($endDateObject); + $mail->setExpiration($token->getExpiration()); $app['notification.deliverer']->deliver($mail); unset($remaingEmails[$key]); @@ -261,16 +259,6 @@ class Export implements ControllerProviderInterface ]); } } - } elseif (!$token && count($destMails) > 0) { //couldn't generate token - foreach ($destMails as $mail) { - $app['events-manager']->trigger('__EXPORT_MAIL_FAIL__', [ - 'usr_id' => $app['authentication']->getUser()->getId(), - 'lst' => $lst, - 'ssttid' => $ssttid, - 'dest' => $mail, - 'reason' => 0 - ]); - } } return $app->json([ diff --git a/lib/Alchemy/Phrasea/Controller/Prod/Push.php b/lib/Alchemy/Phrasea/Controller/Prod/Push.php index 6e0e821c7f..a92379c7a4 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/Push.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/Push.php @@ -219,12 +219,7 @@ class Push implements ControllerProviderInterface $url = $app->url('lightbox_compare', [ 'basket' => $Basket->getId(), - 'LOG' => $app['tokens']->getUrlToken( - \random::TYPE_VIEW, - $user_receiver->getId(), - null, - $Basket->getId() - ) + 'LOG' => $app['manipulator.token']->createBasketAccessToken($Basket, $user_receiver), ]); $receipt = $request->get('recept') ? $app['authentication']->getUser()->getEmail() : ''; @@ -414,12 +409,7 @@ class Push implements ControllerProviderInterface $url = $app->url('lightbox_validation', [ 'basket' => $Basket->getId(), - 'LOG' => $app['tokens']->getUrlToken( - \random::TYPE_VALIDATE, - $participant_user->getId(), - null, - $Basket->getId() - ) + 'LOG' => $app['manipulator.token']->createBasketValidationToken($Basket, $participant_user), ]); $receipt = $request->get('recept') ? $app['authentication']->getUser()->getEmail() : ''; diff --git a/lib/Alchemy/Phrasea/Controller/Root/Account.php b/lib/Alchemy/Phrasea/Controller/Root/Account.php index a72b992caf..7a259eb660 100644 --- a/lib/Alchemy/Phrasea/Controller/Root/Account.php +++ b/lib/Alchemy/Phrasea/Controller/Root/Account.php @@ -138,9 +138,8 @@ class Account implements ControllerProviderInterface return $app->redirectPath('account_reset_email'); } - $date = new \DateTime('1 day'); - $token = $app['tokens']->getUrlToken(\random::TYPE_EMAIL, $app['authentication']->getUser()->getId(), $date, $app['authentication']->getUser()->getEmail()); - $url = $app->url('account_reset_email', ['token' => $token]); + $token = $app['manipulator.token']->createResetEmailToken($app['authentication']->getUser(), $email); + $url = $app->url('account_reset_email', ['token' => $token->getValue()]); try { $receiver = Receiver::fromUser($app['authentication']->getUser()); @@ -152,7 +151,7 @@ class Account implements ControllerProviderInterface $mail = MailRequestEmailUpdate::create($app, $receiver, null); $mail->setButtonUrl($url); - $mail->setExpiration($date); + $mail->setExpiration($token->getExpiration()); $app['notification.deliverer']->deliver($mail); @@ -170,21 +169,20 @@ class Account implements ControllerProviderInterface */ public function displayResetEmailForm(Application $app, Request $request) { - if (null !== $token = $request->query->get('token')) { - try { - $datas = $app['tokens']->helloToken($token); - $user = $app['repo.users']->find((int) $datas['usr_id']); - $user->setEmail($datas['datas']); - $app['tokens']->removeToken($token); - - $app->addFlash('success', $app->trans('admin::compte-utilisateur: L\'email a correctement ete mis a jour')); - - return $app->redirectPath('account'); - } catch (\Exception $e) { + if (null !== $tokenValue = $request->query->get('token')) { + if (null === $token = $app['repo.tokens']->findValidToken($tokenValue)) { $app->addFlash('error', $app->trans('admin::compte-utilisateur: erreur lors de la mise a jour')); return $app->redirectPath('account'); } + + $user = $token->getUser(); + $user->setEmail($token->getData()); + $app['manipulator.token']->delete($token); + + $app->addFlash('success', $app->trans('admin::compte-utilisateur: L\'email a correctement ete mis a jour')); + + return $app->redirectPath('account'); } return $app['twig']->render('account/reset-email.html.twig', Login::getDefaultTemplateVariables($app)); diff --git a/lib/Alchemy/Phrasea/Controller/Root/Login.php b/lib/Alchemy/Phrasea/Controller/Root/Login.php index 5f093a5290..546403921b 100644 --- a/lib/Alchemy/Phrasea/Controller/Root/Login.php +++ b/lib/Alchemy/Phrasea/Controller/Root/Login.php @@ -505,12 +505,11 @@ class Login implements ControllerProviderInterface { $receiver = Receiver::fromUser($user); - $expire = new \DateTime('+3 days'); - $token = $app['tokens']->getUrlToken(\random::TYPE_PASSWORD, $user->getId(), $expire, $user->getEmail()); + $token = $app['manipulator.token']->createAccountUnlockToken($user); $mail = MailRequestEmailConfirmation::create($app, $receiver); - $mail->setButtonUrl($app->url('login_register_confirm', ['code' => $token])); - $mail->setExpiration($expire); + $mail->setButtonUrl($app->url('login_register_confirm', ['code' => $token->getValue()])); + $mail->setExpiration($token->getExpiration()); $app['notification.deliverer']->deliver($mail); } @@ -530,19 +529,13 @@ class Login implements ControllerProviderInterface return $app->redirectPath('homepage'); } - try { - $datas = $app['tokens']->helloToken($code); - } catch (NotFoundHttpException $e) { + if (null === $token = $app['repo.tokens']->findValidToken($code)) { $app->addFlash('error', $app->trans('Invalid unlock link.')); return $app->redirectPath('homepage'); } - if (null === $user = $app['repo.users']->find((int) $datas['usr_id'])) { - $app->addFlash('error', _('Invalid unlock link.')); - - return $app->redirectPath('homepage'); - } + $user = $token->getUser(); if (!$user->isMailLocked()) { $app->addFlash('info', $app->trans('Account is already unlocked, you can login.')); @@ -550,7 +543,7 @@ class Login implements ControllerProviderInterface return $app->redirectPath('homepage'); } - $app['tokens']->removeToken($code); + $app['manipulator.token']->delete($token); $user->setMailLocked(false); try { @@ -561,7 +554,7 @@ class Login implements ControllerProviderInterface return $app->redirectPath('homepage'); } - $app['tokens']->removeToken($code); + $app['manipulator.token']->delete($token); if (count($app['acl']->get($user)->get_granted_base()) > 0) { $mail = MailSuccessEmailConfirmationRegistered::create($app, $receiver); @@ -580,38 +573,26 @@ class Login implements ControllerProviderInterface public function renewPassword(PhraseaApplication $app, Request $request) { - if (null === $token = $request->get('token')) { + if (null === $tokenValue = $request->get('token')) { $app->abort(401, 'A token is required'); } - try { - $app['tokens']->helloToken($token); - } catch (\Exception $e) { + if (null === $token = $app['repo.tokens']->findValidToken($tokenValue)) { $app->abort(401, 'A token is required'); } - $form = $app->form(new PhraseaRecoverPasswordForm($app['tokens'])); - $form->setData(['token' => $token]); + $form = $app->form(new PhraseaRecoverPasswordForm($app['repo.tokens'])); + $form->setData(['token' => $token->getValue()]); if ('POST' === $request->getMethod()) { $form->bind($request); - try { - if ($form->isValid()) { - $data = $form->getData(); + if ($form->isValid()) { + $data = $form->getData(); + $app['manipulator.user']->setPassword($token->getUser(), $data['password']); + $app['manipulator.token']->delete($token); + $app->addFlash('success', $app->trans('login::notification: Mise a jour du mot de passe avec succes')); - $datas = $app['tokens']->helloToken($token); - - $user = $app['repo.users']->find($datas['usr_id']); - $app['manipulator.user']->setPassword($user, $data['password']); - - $app['tokens']->removeToken($token); - - $app->addFlash('success', $app->trans('login::notification: Mise a jour du mot de passe avec succes')); - - return $app->redirectPath('homepage'); - } - } catch (FormProcessingException $e) { - $app->addFlash('error', $e->getMessage()); + return $app->redirectPath('homepage'); } } @@ -649,13 +630,9 @@ class Login implements ControllerProviderInterface throw new FormProcessingException($app->trans('Invalid email address')); } - $token = $app['tokens']->getUrlToken(\random::TYPE_PASSWORD, $user->getId(), new \DateTime('+1 day')); + $token = $app['manipulator.token']->createResetPasswordToken($user); - if (!$token) { - return $app->abort(500, 'Unable to generate a token'); - } - - $url = $app->url('login_renew_password', ['token' => $token], true); + $url = $app->url('login_renew_password', ['token' => $token->getValue()], true); $mail = MailRequestPasswordUpdate::create($app, $receiver); $mail->setLogin($user->getLogin()); @@ -837,20 +814,18 @@ class Login implements ControllerProviderInterface $validationSession = $participant->getSession(); $participantId = $participant->getUser()->getId(); - $basketId = $validationSession->getBasket()->getId(); + $basket = $validationSession->getBasket(); - try { - $token = $app['tokens']->getValidationToken($participantId, $basketId); - } catch (NotFoundHttpException $e) { + if (null === $token = $app['repo.tokens']->findValidationToken($basket, $participant->getUser())) { continue; } $app['events-manager']->trigger('__VALIDATION_REMINDER__', [ 'to' => $participantId, - 'ssel_id' => $basketId, + 'ssel_id' => $basket->getId(), 'from' => $validationSession->getInitiator()->getId(), 'validate_id' => $validationSession->getId(), - 'url' => $app->url('lightbox_validation', ['basket' => $basketId, 'LOG' => $token]), + 'url' => $app->url('lightbox_validation', ['basket' => $basket->getId(), 'LOG' => $token->getValue()]), ]); $participant->setReminded(new \DateTime('now')); diff --git a/lib/Alchemy/Phrasea/Core/Event/Subscriber/JsonRequestSubscriber.php b/lib/Alchemy/Phrasea/Core/Event/Subscriber/JsonRequestSubscriber.php index ef133cfca4..540c53e957 100644 --- a/lib/Alchemy/Phrasea/Core/Event/Subscriber/JsonRequestSubscriber.php +++ b/lib/Alchemy/Phrasea/Core/Event/Subscriber/JsonRequestSubscriber.php @@ -25,6 +25,7 @@ class JsonRequestSubscriber implements EventSubscriberInterface if ((0 !== strpos($request->getPathInfo(), '/admin/') || 0 === strpos($request->getPathInfo(), '/admin/collection/') + || preg_match('/^\/download\/[a-zA-Z0-9]+\/execute\/$/', $request->getPathInfo()) || 0 === strpos($request->getPathInfo(), '/admin/databox/')) && $request->getRequestFormat() == 'json') { $datas = [ diff --git a/lib/Alchemy/Phrasea/Core/Middleware/TokenMiddlewareProvider.php b/lib/Alchemy/Phrasea/Core/Middleware/TokenMiddlewareProvider.php new file mode 100644 index 0000000000..7502c9a086 --- /dev/null +++ b/lib/Alchemy/Phrasea/Core/Middleware/TokenMiddlewareProvider.php @@ -0,0 +1,32 @@ +protect(function (Request $request, Application $app) { + if ($request->attributes->has('token')) { + $request->attributes->set('token', $app['converter.token']->convert($request->attributes->get('token'))); + } + }); + } + + public function boot(Application $app) + { + } +} diff --git a/lib/Alchemy/Phrasea/Core/Provider/AuthenticationManagerServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/AuthenticationManagerServiceProvider.php index 925f2eb82b..a64948fa26 100644 --- a/lib/Alchemy/Phrasea/Core/Provider/AuthenticationManagerServiceProvider.php +++ b/lib/Alchemy/Phrasea/Core/Provider/AuthenticationManagerServiceProvider.php @@ -36,10 +36,6 @@ class AuthenticationManagerServiceProvider implements ServiceProviderInterface return new Authenticator($app, $app['browser'], $app['session'], $app['EM']); }); - $app['authentication.token-validator'] = $app->share(function (Application $app) { - return new TokenValidator($app['tokens']); - }); - $app['authentication.persistent-manager'] = $app->share(function (Application $app) { return new CookieManager($app['auth.password-encoder'], $app['repo.sessions'], $app['browser']); }); diff --git a/lib/Alchemy/Phrasea/Core/Provider/ConfigurationTesterServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/ConfigurationTesterServiceProvider.php index 7e80258cdb..9be12c08f9 100644 --- a/lib/Alchemy/Phrasea/Core/Provider/ConfigurationTesterServiceProvider.php +++ b/lib/Alchemy/Phrasea/Core/Provider/ConfigurationTesterServiceProvider.php @@ -15,6 +15,7 @@ use Alchemy\Phrasea\Setup\ConfigurationTester; use Alchemy\Phrasea\Application; use Alchemy\Phrasea\Setup\Version\PreSchemaUpgrade\PreSchemaUpgradeCollection; use Alchemy\Phrasea\Setup\Version\PreSchemaUpgrade\Upgrade39Feeds; +use Alchemy\Phrasea\Setup\Version\PreSchemaUpgrade\Upgrade39Tokens; use Alchemy\Phrasea\Setup\Version\PreSchemaUpgrade\Upgrade39Users; use Silex\Application as SilexApplication; use Silex\ServiceProviderInterface; @@ -29,7 +30,7 @@ class ConfigurationTesterServiceProvider implements ServiceProviderInterface }); $app['phraseanet.pre-schema-upgrader.upgrades'] = $app->share(function () { - return [new Upgrade39Feeds(), new Upgrade39Users()]; + return [new Upgrade39Feeds(), new Upgrade39Users(), new Upgrade39Tokens()]; }); $app['phraseanet.pre-schema-upgrader'] = $app->share(function (Application $app) { diff --git a/lib/Alchemy/Phrasea/Core/Provider/ConvertersServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/ConvertersServiceProvider.php index 82f42ac061..be04e45f92 100644 --- a/lib/Alchemy/Phrasea/Core/Provider/ConvertersServiceProvider.php +++ b/lib/Alchemy/Phrasea/Core/Provider/ConvertersServiceProvider.php @@ -13,6 +13,7 @@ namespace Alchemy\Phrasea\Core\Provider; use Alchemy\Phrasea\Model\Converter\BasketConverter; use Alchemy\Phrasea\Model\Converter\TaskConverter; +use Alchemy\Phrasea\Model\Converter\TokenConverter; use Silex\Application; use Silex\ServiceProviderInterface; @@ -27,6 +28,10 @@ class ConvertersServiceProvider implements ServiceProviderInterface $app['converter.basket'] = $app->share(function ($app) { return new BasketConverter($app['EM']); }); + + $app['converter.token'] = $app->share(function ($app) { + return new TokenConverter($app['repo.tokens']); + }); } public function boot(Application $app) diff --git a/lib/Alchemy/Phrasea/Core/Provider/ManipulatorServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/ManipulatorServiceProvider.php index 766d8a00b3..2ded0578b5 100644 --- a/lib/Alchemy/Phrasea/Core/Provider/ManipulatorServiceProvider.php +++ b/lib/Alchemy/Phrasea/Core/Provider/ManipulatorServiceProvider.php @@ -14,6 +14,7 @@ namespace Alchemy\Phrasea\Core\Provider; use Alchemy\Phrasea\Model\Manipulator\ACLManipulator; use Alchemy\Phrasea\Model\Manipulator\RegistrationManipulator; use Alchemy\Phrasea\Model\Manipulator\TaskManipulator; +use Alchemy\Phrasea\Model\Manipulator\TokenManipulator; use Alchemy\Phrasea\Model\Manipulator\UserManipulator; use Alchemy\Phrasea\Model\Manager\UserManager; use Silex\Application as SilexApplication; @@ -31,6 +32,10 @@ class ManipulatorServiceProvider implements ServiceProviderInterface return new UserManipulator($app['model.user-manager'], $app['auth.password-encoder'], $app['geonames.connector'], $app['repo.users'], $app['random.low']); }); + $app['manipulator.token'] = $app->share(function ($app) { + return new TokenManipulator($app['EM'], $app['random.medium'], $app['repo.tokens']); + }); + $app['manipulator.acl'] = $app->share(function ($app) { return new ACLManipulator($app['acl'], $app['phraseanet.appbox']); }); diff --git a/lib/Alchemy/Phrasea/Core/Provider/RepositoriesServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/RepositoriesServiceProvider.php index 9220e53f11..02768a6400 100644 --- a/lib/Alchemy/Phrasea/Core/Provider/RepositoriesServiceProvider.php +++ b/lib/Alchemy/Phrasea/Core/Provider/RepositoriesServiceProvider.php @@ -91,6 +91,9 @@ class RepositoriesServiceProvider implements ServiceProviderInterface $app['repo.user-queries'] = $app->share(function (PhraseaApplication $app) { return $app['EM']->getRepository('Phraseanet:UserQuery'); }); + $app['repo.tokens'] = $app->share(function ($app) { + return $app['EM']->getRepository('Phraseanet:Token'); + }); } public function boot(Application $app) diff --git a/lib/Alchemy/Phrasea/Feed/Link/AggregateLinkGenerator.php b/lib/Alchemy/Phrasea/Feed/Link/AggregateLinkGenerator.php index 6b09295bec..06d416f5c7 100644 --- a/lib/Alchemy/Phrasea/Feed/Link/AggregateLinkGenerator.php +++ b/lib/Alchemy/Phrasea/Feed/Link/AggregateLinkGenerator.php @@ -16,6 +16,7 @@ use Alchemy\Phrasea\Feed\Aggregate; use Alchemy\Phrasea\Feed\FeedInterface; use Alchemy\Phrasea\Model\Entities\AggregateToken; use Alchemy\Phrasea\Model\Entities\User; +use Alchemy\Phrasea\Model\Manipulator\TokenManipulator; use Doctrine\ORM\EntityManager; use RandomLib\Generator; use Symfony\Component\Routing\Generator\UrlGenerator; @@ -141,7 +142,7 @@ class AggregateLinkGenerator implements LinkGeneratorInterface $token->setUser($user); } - $token->setValue($this->random->generateString(64, \random::LETTERS_AND_NUMBERS)); + $token->setValue($this->random->generateString(64, TokenManipulator::LETTERS_AND_NUMBERS)); $this->em->persist($token); $this->em->flush(); } diff --git a/lib/Alchemy/Phrasea/Feed/Link/FeedLinkGenerator.php b/lib/Alchemy/Phrasea/Feed/Link/FeedLinkGenerator.php index 4856112513..aeb7f631c2 100644 --- a/lib/Alchemy/Phrasea/Feed/Link/FeedLinkGenerator.php +++ b/lib/Alchemy/Phrasea/Feed/Link/FeedLinkGenerator.php @@ -14,6 +14,7 @@ namespace Alchemy\Phrasea\Feed\Link; use Alchemy\Phrasea\Exception\InvalidArgumentException; use Alchemy\Phrasea\Feed\FeedInterface; use Alchemy\Phrasea\Model\Entities\User; +use Alchemy\Phrasea\Model\Manipulator\TokenManipulator; use Doctrine\ORM\EntityManager; use Alchemy\Phrasea\Model\Entities\Feed; use Alchemy\Phrasea\Model\Entities\FeedToken; @@ -153,7 +154,7 @@ class FeedLinkGenerator implements LinkGeneratorInterface $this->em->persist($feed); } - $token->setValue($this->random->generateString(64, \random::LETTERS_AND_NUMBERS)); + $token->setValue($this->random->generateString(64, TokenManipulator::LETTERS_AND_NUMBERS)); $this->em->persist($token); $this->em->flush(); } diff --git a/lib/Alchemy/Phrasea/Form/Constraint/PasswordToken.php b/lib/Alchemy/Phrasea/Form/Constraint/PasswordToken.php index 7edbdd8a5d..d117138aea 100644 --- a/lib/Alchemy/Phrasea/Form/Constraint/PasswordToken.php +++ b/lib/Alchemy/Phrasea/Form/Constraint/PasswordToken.php @@ -12,33 +12,33 @@ namespace Alchemy\Phrasea\Form\Constraint; use Alchemy\Phrasea\Application; +use Alchemy\Phrasea\Model\Manipulator\TokenManipulator; +use Doctrine\ORM\EntityRepository; use Symfony\Component\Validator\Constraint; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class PasswordToken extends Constraint { public $message = 'The token provided is not valid anymore'; - private $random; + private $repository; - public function __construct(\random $random) + public function __construct(EntityRepository $repository) { - $this->random = $random; + $this->repository = $repository; parent::__construct(); } - public function isValid($token) + public function isValid($tokenValue) { - try { - $data = $this->random->helloToken($token); - } catch (NotFoundHttpException $e) { + if (null === $token = $this->repository->findValidToken($tokenValue)) { return false; } - return \random::TYPE_PASSWORD === $data['type']; + return TokenManipulator::TYPE_PASSWORD === $token->getType(); } public static function create(Application $app) { - return new static($app['tokens']); + return new static($app['repo.tokens']); } } diff --git a/lib/Alchemy/Phrasea/Form/Login/PhraseaRecoverPasswordForm.php b/lib/Alchemy/Phrasea/Form/Login/PhraseaRecoverPasswordForm.php index e7f21225d1..7669a42ed7 100644 --- a/lib/Alchemy/Phrasea/Form/Login/PhraseaRecoverPasswordForm.php +++ b/lib/Alchemy/Phrasea/Form/Login/PhraseaRecoverPasswordForm.php @@ -12,6 +12,7 @@ namespace Alchemy\Phrasea\Form\Login; use Alchemy\Phrasea\Form\Constraint\PasswordToken; +use Doctrine\ORM\EntityRepository; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Validator\Constraints as Assert; @@ -21,11 +22,11 @@ use Symfony\Component\Validator\Constraints as Assert; */ class PhraseaRecoverPasswordForm extends AbstractType { - private $tokens; + private $repository; - public function __construct(\random $tokens) + public function __construct(EntityRepository $repository) { - $this->tokens = $tokens; + $this->repository = $repository; } public function buildForm(FormBuilderInterface $builder, array $options) @@ -33,7 +34,7 @@ class PhraseaRecoverPasswordForm extends AbstractType $builder->add('token', 'hidden', [ 'required' => true, 'constraints' => [ - new PasswordToken($this->tokens) + new PasswordToken($this->repository) ] ]); diff --git a/lib/Alchemy/Phrasea/Helper/User/Manage.php b/lib/Alchemy/Phrasea/Helper/User/Manage.php index 03a24aa639..45916432b4 100644 --- a/lib/Alchemy/Phrasea/Helper/User/Manage.php +++ b/lib/Alchemy/Phrasea/Helper/User/Manage.php @@ -159,28 +159,22 @@ class Manage extends Helper } - if ($sendCredentials) { - $urlToken = $this->app['tokens']->getUrlToken(\random::TYPE_PASSWORD, $createdUser->getId()); - - if ($receiver && false !== $urlToken) { - $url = $this->app->url('login_renew_password', ['token' => $urlToken]); - $mail = MailRequestPasswordSetup::create($this->app, $receiver, null, '', $url); - $mail->setLogin($createdUser->getLogin()); - $this->app['notification.deliverer']->deliver($mail); - } + if ($sendCredentials && $receiver) { + $urlToken = $this->app['manipulator.token']->createResetPasswordToken($createdUser); + $url = $this->app->url('login_renew_password', ['token' => $urlToken->getValue()]); + $mail = MailRequestPasswordSetup::create($this->app, $receiver, null, '', $url); + $mail->setLogin($createdUser->getLogin()); + $this->app['notification.deliverer']->deliver($mail); } - if ($validateMail) { + if ($validateMail && $receiver) { $createdUser->setMailLocked(true); - if ($receiver) { - $expire = new \DateTime('+3 days'); - $token = $this->app['tokens']->getUrlToken(\random::TYPE_PASSWORD, $createdUser->getId(), $expire, $createdUser->getEmail()); - $url = $this->app->url('login_register_confirm', ['code' => $token]); + $token = $this->app['manipulator.token']->createAccountUnlockToken($createdUser); + $url = $this->app->url('login_register_confirm', ['code' => $token]); - $mail = MailRequestEmailConfirmation::create($this->app, $receiver, null, '', $url, $expire); - $this->app['notification.deliverer']->deliver($mail); - } + $mail = MailRequestEmailConfirmation::create($this->app, $receiver, null, '', $url, $token->getExpiration()); + $this->app['notification.deliverer']->deliver($mail); } } diff --git a/lib/Alchemy/Phrasea/Model/Converter/TokenConverter.php b/lib/Alchemy/Phrasea/Model/Converter/TokenConverter.php new file mode 100644 index 0000000000..c5e25313d1 --- /dev/null +++ b/lib/Alchemy/Phrasea/Model/Converter/TokenConverter.php @@ -0,0 +1,40 @@ +repository = $repository; + } + + /** + * {@inheritdoc} + * + * @return Token + */ + public function convert($value) + { + if (null === $token = $this->repository->findValidToken($value)) { + throw new NotFoundHttpException('Token is not valid.'); + } + + return $token; + } +} diff --git a/lib/Alchemy/Phrasea/Model/Entities/Token.php b/lib/Alchemy/Phrasea/Model/Entities/Token.php new file mode 100644 index 0000000000..2bdcd673f1 --- /dev/null +++ b/lib/Alchemy/Phrasea/Model/Entities/Token.php @@ -0,0 +1,222 @@ +value = $value; + + return $this; + } + + /** + * Get value + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * Set type + * + * @param string $type + * @return Token + */ + public function setType($type) + { + $this->type = $type; + + return $this; + } + + /** + * Get type + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Set data + * + * @param string $data + * @return Token + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } + + /** + * Get data + * + * @return string + */ + public function getData() + { + return $this->data; + } + + /** + * Set created + * + * @param \DateTime $created + * @return Token + */ + public function setCreated($created) + { + $this->created = $created; + + return $this; + } + + /** + * Get created + * + * @return \DateTime + */ + public function getCreated() + { + return $this->created; + } + + /** + * Set updated + * + * @param \DateTime $updated + * @return Token + */ + public function setUpdated($updated) + { + $this->updated = $updated; + + return $this; + } + + /** + * Get updated + * + * @return \DateTime + */ + public function getUpdated() + { + return $this->updated; + } + + /** + * Set expiration + * + * @param \DateTime $updated + * @return Token + */ + public function setExpiration(\DateTime $expiration = null) + { + $this->expiration = $expiration; + + return $this; + } + + /** + * Get expiration + * + * @return \DateTime + */ + public function getExpiration() + { + return $this->expiration; + } + + /** + * Set user + * + * @param User $user + * @return Token + */ + public function setUser(User $user = null) + { + $this->user = $user; + + return $this; + } + + /** + * Get user + * + * @return User + */ + public function getUser() + { + return $this->user; + } +} diff --git a/lib/Alchemy/Phrasea/Model/Manager/UserManager.php b/lib/Alchemy/Phrasea/Model/Manager/UserManager.php index ea83c62d67..731ac865a3 100644 --- a/lib/Alchemy/Phrasea/Model/Manager/UserManager.php +++ b/lib/Alchemy/Phrasea/Model/Manager/UserManager.php @@ -101,6 +101,16 @@ class UserManager $user->getSettings()->clear(); } + private function cleanTokens(User $user) + { + $elements = $this->objectManager->getRepository('Phraseanet:Token') + ->findBy(['user' => $user]); + + foreach ($elements as $element) { + $this->objectManager->remove($element); + } + } + /** * Removes user queries. * @@ -194,16 +204,13 @@ class UserManager */ private function cleanProperties(User $user) { - foreach ([ - 'DELETE FROM `edit_presets` WHERE usr_id = :usr_id', - 'DELETE FROM `tokens` WHERE usr_id = :usr_id', - ] as $sql) { - $stmt = $this->appboxConnection->prepare($sql); - $stmt->execute([':usr_id' => $user->getId()]); - $stmt->closeCursor(); - } + $sql = 'DELETE FROM `edit_presets` WHERE usr_id = :usr_id'; + $stmt = $this->appboxConnection->prepare($sql); + $stmt->execute([':usr_id' => $user->getId()]); + $stmt->closeCursor(); $this->cleanSettings($user); + $this->cleanTokens($user); $this->cleanQueries($user); $this->cleanFtpCredentials($user); $this->cleanOrders($user); diff --git a/lib/Alchemy/Phrasea/Model/Manipulator/TokenManipulator.php b/lib/Alchemy/Phrasea/Model/Manipulator/TokenManipulator.php new file mode 100644 index 0000000000..3d840ab3c7 --- /dev/null +++ b/lib/Alchemy/Phrasea/Model/Manipulator/TokenManipulator.php @@ -0,0 +1,219 @@ +om = $om; + $this->random = $random; + $this->repository = $repository; + } + + /** + * @param User|null $user + * @param string $type + * @param \DateTime|null $expiration + * @param mixed|null $data + * + * @return Token + */ + public function create(User $user = null, $type, \DateTime $expiration = null, $data = null) + { + $this->removeExpiredTokens(); + + $n = 0; + do { + if ($n++ > 1024) { + throw new \RuntimeException('Unable to create a token.'); + } + $value = $this->random->generateString(32, self::LETTERS_AND_NUMBERS); + $found = null !== $this->om->getRepository('Phraseanet:Token')->find($value); + } while ($found); + + $token = new Token(); + + $token->setUser($user) + ->setType($type) + ->setValue($value) + ->setExpiration($expiration) + ->setData($data); + + $this->om->persist($token); + $this->om->flush(); + + return $token; + } + + /** + * @param Basket $basket + * @param User $user + * + * @return Token + */ + public function createBasketValidationToken(Basket $basket, User $user = null) + { + if (null === $basket->getValidation()) { + throw new \InvalidArgumentException('A validation token requires a validation basket.'); + } + + return $this->create($user ?: $basket->getValidation()->getInitiator(), self::TYPE_VALIDATE, new \DateTime('+10 days'), $basket->getId()); + } + + /** + * @param Basket $basket + * @param User $user + * + * @return Token + */ + public function createBasketAccessToken(Basket $basket, User $user) + { + return $this->create($user, self::TYPE_VIEW, null, $basket->getId()); + } + + /** + * @param User $user + * @param FeedEntry $entry + * + * @return Token + */ + public function createFeedEntryToken(User $user, FeedEntry $entry) + { + return $this->create($user, self::TYPE_FEED_ENTRY, null, $entry->getId()); + } + + /** + * @param User $user + * @param $data + * + * @return Token + */ + public function createDownloadToken(User $user, $data) + { + return $this->create($user, self::TYPE_DOWNLOAD, new \DateTime('+3 hours'), $data); + } + + /** + * @param $data + * + * @return Token + */ + public function createEmailExportToken($data) + { + return $this->create(null, self::TYPE_EMAIL, new \DateTime('+1 day'), $data); + } + + /** + * @param User $user + * @param $email + * + * @return Token + */ + public function createResetEmailToken(User $user, $email) + { + return $this->create($user, self::TYPE_EMAIL_RESET, new \DateTime('+1 day'), $email); + } + + /** + * @param User $user + * + * @return Token + */ + public function createAccountUnlockToken(User $user) + { + return $this->create($user, self::TYPE_ACCOUNT_UNLOCK, new \DateTime('+3 days')); + } + + /** + * @param User $user + * + * @return Token + */ + public function createResetPasswordToken(User $user) + { + return $this->create($user, self::TYPE_PASSWORD, new \DateTime('+1 day')); + } + + /** + * Updates a token. + * + * @param Token $token + * + * @return Token + */ + public function update(Token $token) + { + $this->om->persist($token); + $this->om->flush(); + + return $token; + } + + /** + * Removes a token. + * + * @param Token $token + */ + public function delete(Token $token) + { + $this->om->remove($token); + $this->om->flush(); + } + + /** + * Removes expired tokens + */ + public function removeExpiredTokens() + { + foreach ($this->repository->findExpiredTokens() as $token) { + switch ($token->getType()) { + case 'download': + case 'email': + $file = $this->app['root.path'] . '/tmp/download/' . $token->getValue() . '.zip'; + if (is_file($file)) { + unlink($file); + } + break; + } + $this->om->remove($token); + } + $this->om->flush(); + } +} diff --git a/lib/Alchemy/Phrasea/Model/Repositories/TokenRepository.php b/lib/Alchemy/Phrasea/Model/Repositories/TokenRepository.php new file mode 100644 index 0000000000..020d9ae678 --- /dev/null +++ b/lib/Alchemy/Phrasea/Model/Repositories/TokenRepository.php @@ -0,0 +1,59 @@ + CURRENT_TIMESTAMP() OR t.expiration IS NULL)'; + + $query = $this->_em->createQuery($dql); + $query->setParameters([ + ':type' => TokenManipulator::TYPE_VALIDATE, + ':user' => $user, + ':basket_id' => $basket->getId(), + ]); + + return $query->getOneOrNullResult(); + } + + public function findValidToken($value) + { + $dql = 'SELECT t FROM Phraseanet:Token t + WHERE t.value = :value + AND (t.expiration IS NULL OR t.expiration >= CURRENT_TIMESTAMP())'; + + $query = $this->_em->createQuery($dql); + $query->setParameters([':value' => $value]); + + return $query->getOneOrNullResult(); + } + + public function findExpiredTokens() + { + $dql = 'SELECT t FROM Phraseanet:Token t + WHERE t.expiration < :date'; + + $query = $this->_em->createQuery($dql); + $query->setParameters([':date' => new \DateTime()]); + + return $query->getResult(); + } +} diff --git a/lib/Alchemy/Phrasea/Setup/DoctrineMigrations/TokenMigration.php b/lib/Alchemy/Phrasea/Setup/DoctrineMigrations/TokenMigration.php new file mode 100644 index 0000000000..d7444e81dc --- /dev/null +++ b/lib/Alchemy/Phrasea/Setup/DoctrineMigrations/TokenMigration.php @@ -0,0 +1,34 @@ +abortIf($this->connection->getDatabasePlatform()->getName() != "mysql", "Migration can only be executed safely on 'mysql'."); + + $this->addSql("CREATE TABLE Tokens (value VARCHAR(16) NOT NULL, user_id INT DEFAULT NULL, type VARCHAR(32) NOT NULL, data LONGTEXT DEFAULT NULL, created DATETIME NOT NULL, updated DATETIME NOT NULL, expiration DATETIME DEFAULT NULL, INDEX IDX_ADF614B8A76ED395 (user_id), PRIMARY KEY(value)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB"); + $this->addSql("ALTER TABLE Tokens ADD CONSTRAINT FK_ADF614B8A76ED395 FOREIGN KEY (user_id) REFERENCES Users (id)"); + } + + public function doDownSql(Schema $schema) + { + // this down() migration is auto-generated, please modify it to your needs + $this->abortIf($this->connection->getDatabasePlatform()->getName() != "mysql", "Migration can only be executed safely on 'mysql'."); + + $this->addSql("DROP TABLE Tokens"); + } +} diff --git a/lib/Alchemy/Phrasea/Setup/Version/PreSchemaUpgrade/Upgrade39Feeds.php b/lib/Alchemy/Phrasea/Setup/Version/PreSchemaUpgrade/Upgrade39Feeds.php index 5981201974..ab9e448d5e 100644 --- a/lib/Alchemy/Phrasea/Setup/Version/PreSchemaUpgrade/Upgrade39Feeds.php +++ b/lib/Alchemy/Phrasea/Setup/Version/PreSchemaUpgrade/Upgrade39Feeds.php @@ -41,7 +41,7 @@ class Upgrade39Feeds implements PreSchemaUpgradeInterface public function rollback(EntityManager $em, \appbox $appbox, Configuration $conf) { if ($this->tableExists($em, 'feeds_backup')) { - $em->getConnection()->executeUpdate('RENAME TABLE `feeds_backup` TO `feeds`'); + $em->getConnection()->getSchemaManager()->renameTable('feeds_backup', 'feeds'); } } @@ -67,6 +67,6 @@ class Upgrade39Feeds implements PreSchemaUpgradeInterface */ private function doBackupFeedsTable(EntityManager $em) { - $em->getConnection()->executeUpdate('RENAME TABLE `feeds` TO `feeds_backup`'); + $em->getConnection()->getSchemaManager()->renameTable('feeds', 'feeds_backup'); } } diff --git a/lib/Alchemy/Phrasea/Setup/Version/PreSchemaUpgrade/Upgrade39Tokens.php b/lib/Alchemy/Phrasea/Setup/Version/PreSchemaUpgrade/Upgrade39Tokens.php new file mode 100644 index 0000000000..4055285616 --- /dev/null +++ b/lib/Alchemy/Phrasea/Setup/Version/PreSchemaUpgrade/Upgrade39Tokens.php @@ -0,0 +1,75 @@ +doBackupFeedsTable($em); + } + + /** + * {@inheritdoc} + */ + public function isApplyable(Application $app) + { + return $this->tableExists($app['EM'], 'tokens'); + } + + /** + * {@inheritdoc} + */ + public function rollback(EntityManager $em, \appbox $appbox, Configuration $conf) + { + if ($this->tableExists($em, 'tokens_backup')) { + $em->getConnection()->executeUpdate('RENAME TABLE `tokens_backup` TO `tokens`'); + } + } + + /** + * Checks whether the table exists or not. + * + * @param $tableName + * + * @return boolean + */ + + private function tableExists(EntityManager $em, $table) + { + return (Boolean) $em->createNativeQuery( + 'SHOW TABLE STATUS WHERE Name="'.$table.'" COLLATE utf8_bin ', (new ResultSetMapping())->addScalarResult('Name', 'Name') + )->getOneOrNullResult(); + } + + /** + * Renames feed table. + * + * @param EntityManager $em + */ + private function doBackupFeedsTable(EntityManager $em) + { + $em->getConnection()->executeUpdate('RENAME TABLE `tokens` TO `tokens_backup`'); + } +} diff --git a/lib/classes/API/OAuth2/Application.php b/lib/classes/API/OAuth2/Application.php index 7334221334..bb8dbc7cab 100644 --- a/lib/classes/API/OAuth2/Application.php +++ b/lib/classes/API/OAuth2/Application.php @@ -10,8 +10,9 @@ */ use Alchemy\Phrasea\Application; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Alchemy\Phrasea\Model\Entities\User; +use Alchemy\Phrasea\Model\Manipulator\TokenManipulator; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class API_OAuth2_Application { @@ -606,8 +607,8 @@ class API_OAuth2_Application )'; $nonce = $app['random.medium']->generateString(64); - $client_secret = $app['random.medium']->generateString(32, \random::LETTERS_AND_NUMBERS); - $client_token = $app['random.medium']->generateString(32, \random::LETTERS_AND_NUMBERS); + $client_secret = $app['random.medium']->generateString(32, TokenManipulator::LETTERS_AND_NUMBERS); + $client_token = $app['random.medium']->generateString(32, TokenManipulator::LETTERS_AND_NUMBERS); $params = [ ':usr_id' => $user ? $user->getId() : null, diff --git a/lib/classes/API/OAuth2/Token.php b/lib/classes/API/OAuth2/Token.php index 3bd21fe2fa..d85b5db509 100644 --- a/lib/classes/API/OAuth2/Token.php +++ b/lib/classes/API/OAuth2/Token.php @@ -10,6 +10,7 @@ */ use Alchemy\Phrasea\Application; +use Alchemy\Phrasea\Model\Manipulator\TokenManipulator; use RandomLib\Generator; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -230,7 +231,7 @@ class API_OAuth2_Token $sql = 'UPDATE api_oauth_tokens SET oauth_token = :new_token WHERE oauth_token = :old_token'; - $new_token = $this->generator->generateString(32, \random::LETTERS_AND_NUMBERS); + $new_token = $this->generator->generateString(32, TokenManipulator::LETTERS_AND_NUMBERS); $params = [ ':new_token' => $new_token @@ -303,7 +304,7 @@ class API_OAuth2_Token $expires = new \DateTime('+1 hour'); $params = [ - ':token' => $generator->generateString(32, \random::LETTERS_AND_NUMBERS) + ':token' => $generator->generateString(32, TokenManipulator::LETTERS_AND_NUMBERS) , ':account_id' => $account->get_id() , ':expire' => $expires->format(DATE_ISO8601) , ':scope' => $scope diff --git a/lib/classes/eventsmanager/notify/feed.php b/lib/classes/eventsmanager/notify/feed.php index 88fc883560..c159b93a4d 100644 --- a/lib/classes/eventsmanager/notify/feed.php +++ b/lib/classes/eventsmanager/notify/feed.php @@ -88,14 +88,8 @@ class eventsmanager_notify_feed extends eventsmanager_notifyAbstract if ($params['notify_email'] && $this->shouldSendNotificationFor($user_to_notif->getId())) { $readyToSend = false; try { - $token = $this->app['tokens']->getUrlToken( - \random::TYPE_FEED_ENTRY - , $user_to_notif->getId() - , null - , $entry->getId() - ); - - $url = $this->app->url('lightbox', ['LOG' => $token]); + $token = $this->app['manipulator.token']->createFeedEntryToken($user_to_notif, $entry); + $url = $this->app->url('lightbox', ['LOG' => $token->getValue()]); $receiver = Receiver::fromUser($user_to_notif); $readyToSend = true; diff --git a/lib/classes/eventsmanager/notify/orderdeliver.php b/lib/classes/eventsmanager/notify/orderdeliver.php index be539d6004..f06861c581 100644 --- a/lib/classes/eventsmanager/notify/orderdeliver.php +++ b/lib/classes/eventsmanager/notify/orderdeliver.php @@ -109,12 +109,7 @@ class eventsmanager_notify_orderdeliver extends eventsmanager_notifyAbstract if ($readyToSend) { $url = $this->app->url('lightbox_compare', [ 'basket' => $basket->getId(), - 'LOG' => $this->app['tokens']->getUrlToken( - \random::TYPE_VIEW, - $user_to->getId(), - null, - $basket->getId() - ) + 'LOG' => $this->app['manipulator.token']->createBasketAccessToken($basket, $user_to)->getValue(), ]); $mail = MailInfoOrderDelivered::create($this->app, $receiver, $emitter, null); diff --git a/lib/classes/media/Permalink/Adapter.php b/lib/classes/media/Permalink/Adapter.php index f6d4c30168..ba50f1adb9 100644 --- a/lib/classes/media/Permalink/Adapter.php +++ b/lib/classes/media/Permalink/Adapter.php @@ -11,8 +11,9 @@ use Alchemy\Phrasea\Application; use Alchemy\Phrasea\Exception\RuntimeException; -use Guzzle\Http\Url; +use Alchemy\Phrasea\Model\Manipulator\TokenManipulator; use Doctrine\DBAL\DBALException; +use Guzzle\Http\Url; class media_Permalink_Adapter implements media_Permalink_Interface, cache_cacheableInterface { @@ -327,7 +328,7 @@ class media_Permalink_Adapter implements media_Permalink_Interface, cache_cachea $params = [ ':subdef_id' => $media_subdef->get_subdef_id() - , ':token' => $app['random.medium']->generateString(64, \random::LETTERS_AND_NUMBERS) + , ':token' => $app['random.medium']->generateString(64, TokenManipulator::LETTERS_AND_NUMBERS) , ':activated' => '1' ]; diff --git a/lib/classes/patch/390alpha14a.php b/lib/classes/patch/390alpha14a.php index 958fe50228..84f0e9e824 100644 --- a/lib/classes/patch/390alpha14a.php +++ b/lib/classes/patch/390alpha14a.php @@ -58,7 +58,7 @@ class patch_390alpha14a extends patchAbstract { $app['conf']->remove(['main', 'api-timers']); - if ($this->tableHasField($appbox, 'api_logs', 'api_log_ressource')) { + if ($this->tableHasField($app['EM'], 'api_logs', 'api_log_ressource')) { $sql = 'UPDATE api_logs SET api_log_resource = api_log_ressource'; $app['phraseanet.appbox']->get_connection()->executeUpdate($sql); } diff --git a/lib/classes/patch/390alpha15a.php b/lib/classes/patch/390alpha15a.php new file mode 100644 index 0000000000..3e58e25e18 --- /dev/null +++ b/lib/classes/patch/390alpha15a.php @@ -0,0 +1,70 @@ +release; + } + + /** + * {@inheritdoc} + */ + public function require_all_upgrades() + { + return false; + } + + /** + * {@inheritdoc} + */ + public function concern() + { + return $this->concern; + } + + /** + * {@inheritdoc} + */ + public function getDoctrineMigrations() + { + return ['token']; + } + + /** + * {@inheritdoc} + */ + public function apply(base $appbox, Application $app) + { + if (!$this->tableExists($app['EM'], 'tokens_backup')) { + return true; + } + + $sql = 'INSERT INTO Tokens + (value, user_id, type, data, created, updated, expiration) + (SELECT value, usr_id, type, datas, created_on, created_on, expire_on FROM tokens_backup)'; + $appbox->get_connection()->exec($sql); + + return true; + } +} diff --git a/lib/classes/patchAbstract.php b/lib/classes/patchAbstract.php index 70400ea56f..6fc7a3fdaf 100644 --- a/lib/classes/patchAbstract.php +++ b/lib/classes/patchAbstract.php @@ -11,6 +11,7 @@ use Doctrine\ORM\NoResultException; use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Query\ResultSetMapping; abstract class patchAbstract implements patchInterface { @@ -26,22 +27,21 @@ abstract class patchAbstract implements patchInterface } } - protected function tableExists(base $base, $tableName) + protected function tableExists(EntityManager $em, $tableName) { - return $base - ->get_connection() - ->getSchemaManager() - ->tablesExist([$tableName]); + return (Boolean) $em->createNativeQuery( + 'SHOW TABLE STATUS WHERE Name="'.$tableName.'" COLLATE utf8_bin ', (new ResultSetMapping())->addScalarResult('Name', 'Name') + )->getOneOrNullResult(); } - protected function tableHasField(base $base, $tableName, $fieldName) + protected function tableHasField(EntityManager $em, $tableName, $fieldName) { - if (!$this->tableExists($base, $tableName)) { + if (!$this->tableExists($em, $tableName)) { return false; } - return $base - ->get_connection() + return $em + ->getConnection() ->getSchemaManager() ->listTableDetails($tableName) ->hasColumn($fieldName); diff --git a/lib/classes/random.php b/lib/classes/random.php index 02d1a997ab..07a0980a88 100644 --- a/lib/classes/random.php +++ b/lib/classes/random.php @@ -10,31 +10,9 @@ */ use Alchemy\Phrasea\Application; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class random { - /** - * - */ - const NUMBERS = "0123456789"; - /** - * - */ - const LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - /** - * - */ - const LETTERS_AND_NUMBERS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - const TYPE_FEED_ENTRY = 'FEED_ENTRY'; - const TYPE_PASSWORD = 'password'; - const TYPE_DOWNLOAD = 'download'; - const TYPE_MAIL_DOWNLOAD = 'mail-download'; - const TYPE_EMAIL = 'email'; - const TYPE_VIEW = 'view'; - const TYPE_VALIDATE = 'validate'; - const TYPE_RSS = 'rss'; - private $app; public function __construct(Application $app) @@ -82,162 +60,4 @@ class random return false; } - - /** - * - * @param string $type - * @param int $usr - * @param DateTime $end_date - * @param mixed content $datas - * - * @return boolean - */ - public function getUrlToken($type, $usr, DateTime $end_date = null, $datas = '') - { - $this->cleanTokens(); - $conn = $this->app['phraseanet.appbox']->get_connection(); - $token = $test = false; - - switch ($type) { - case self::TYPE_DOWNLOAD: - case self::TYPE_MAIL_DOWNLOAD: - case self::TYPE_EMAIL: - case self::TYPE_PASSWORD: - case self::TYPE_VALIDATE: - case self::TYPE_VIEW: - case self::TYPE_RSS: - case self::TYPE_FEED_ENTRY: - break; - default: - throw new Exception_InvalidArgument(); - break; - } - - $n = 1; - - $sql = 'SELECT id FROM tokens WHERE value = :test '; - $stmt = $conn->prepare($sql); - while ($n < 100) { - $test = $this->app['random.medium']->generateString(16, self::LETTERS_AND_NUMBERS); - $stmt->execute([':test' => $test]); - if ($stmt->rowCount() === 0) { - $token = $test; - break; - } - $n ++; - } - $stmt->closeCursor(); - - if ($token) { - $sql = 'INSERT INTO tokens (id, value, type, usr_id, created_on, expire_on, datas) - VALUES (null, :token, :type, :usr, NOW(), :end_date, :datas)'; - $stmt = $conn->prepare($sql); - - $params = [ - ':token' => $token - , ':type' => $type - , ':usr' => ($usr ? $usr : '-1') - , ':end_date' => ($end_date instanceof DateTime ? $end_date->format(DATE_ISO8601) : null) - , ':datas' => ((trim($datas) != '') ? $datas : null) - ]; - $stmt->execute($params); - $stmt->closeCursor(); - } - - return $token; - } - - public function removeToken($token) - { - $this->cleanTokens(); - - try { - $conn = $this->app['phraseanet.appbox']->get_connection(); - $sql = 'DELETE FROM tokens WHERE value = :token'; - $stmt = $conn->prepare($sql); - $stmt->execute([':token' => $token]); - $stmt->closeCursor(); - - return true; - } catch (\Exception $e) { - - } - - return false; - } - - public function updateToken($token, $datas) - { - try { - $conn = $this->app['phraseanet.appbox']->get_connection(); - - $sql = 'UPDATE tokens SET datas = :datas - WHERE value = :token'; - - $stmt = $conn->prepare($sql); - $stmt->execute([':datas' => $datas, ':token' => $token]); - $stmt->closeCursor(); - - return true; - } catch (\Exception $e) { - - } - - return false; - } - - public function helloToken($token) - { - $this->cleanTokens(); - - $conn = $this->app['phraseanet.appbox']->get_connection(); - $sql = 'SELECT * FROM tokens - WHERE value = :token - AND (expire_on > NOW() OR expire_on IS NULL)'; - $stmt = $conn->prepare($sql); - $stmt->execute([':token' => $token]); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - $stmt->closeCursor(); - - if ( ! $row) - throw new NotFoundHttpException('Token not found'); - - return $row; - } - - /** - * Get the validation token for one user and one validation basket - * - * @param integer $userId - * @param integer $basketId - * - * @return string The token - * - * @throws NotFoundHttpException - */ - public function getValidationToken($userId, $basketId) - { - $conn = $this->app['phraseanet.appbox']->get_connection(); - $sql = ' - SELECT value FROM tokens - WHERE type = :type - AND usr_id = :usr_id - AND datas = :basket_id - AND (expire_on > NOW() OR expire_on IS NULL)'; - - $stmt = $conn->prepare($sql); - $stmt->execute([ - ':type' => self::TYPE_VALIDATE, - ':usr_id' => (int) $userId, - ':basket_id' => (int) $basketId, - ]); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - $stmt->closeCursor(); - - if (! $row) { - throw new NotFoundHttpException('Token not found'); - } - - return $row['value']; - } } diff --git a/lib/classes/set/export.php b/lib/classes/set/export.php index a448caf3a6..4fd651d1fb 100644 --- a/lib/classes/set/export.php +++ b/lib/classes/set/export.php @@ -11,6 +11,7 @@ use Alchemy\Phrasea\Application; use Alchemy\Phrasea\Model\Serializer\CaptionSerializer; +use Alchemy\Phrasea\Model\Entities\Token; use Alchemy\Phrasea\Model\Entities\User; use Symfony\Component\Filesystem\Filesystem; @@ -693,7 +694,7 @@ class set_export extends set_abstract * * @return string */ - public static function build_zip(Application $app, $token, Array $list, $zipFile) + public static function build_zip(Application $app, Token $token, Array $list, $zipFile) { if (isset($list['complete']) && $list['complete'] === true) { return; @@ -703,7 +704,8 @@ class set_export extends set_abstract $list['complete'] = false; - $app['tokens']->updateToken($token, serialize($list)); + $token->setData(serialize($list)); + $app['manipulator.token']->update($token); $toRemove = []; $archiveFiles = []; @@ -734,7 +736,8 @@ class set_export extends set_abstract $list['complete'] = true; - $app['tokens']->updateToken($token, serialize($list)); + $token->setData(serialize($list)); + $app['manipulator.token']->update($token); $app['filesystem']->remove($toRemove); $app['filesystem']->chmod($zipFile, 0760); diff --git a/lib/conf.d/bases_structure.xml b/lib/conf.d/bases_structure.xml index c8986bd03a..ecbe7f6013 100644 --- a/lib/conf.d/bases_structure.xml +++ b/lib/conf.d/bases_structure.xml @@ -2124,85 +2124,6 @@ InnoDB - - - - id - int(11) unsigned - - auto_increment - - - value - char(16) - - - ascii_bin - - - type - enum('FEED_ENTRY', 'view','validate','password','rss','email','download') - - - ascii_bin - - - usr_id - int(11) unsigned - - - - - datas - longtext - YES - - - - created_on - datetime - - - - - expire_on - datetime - YES - - - - - - PRIMARY - PRIMARY - - id - - - - value - UNIQUE - - value - - - - expire - INDEX - - expire_on - - - - type - INDEX - - type - - - - InnoDB -
diff --git a/lib/conf.d/migrations.yml b/lib/conf.d/migrations.yml index 9c5a00ea9e..c63fcd51a0 100644 --- a/lib/conf.d/migrations.yml +++ b/lib/conf.d/migrations.yml @@ -57,3 +57,6 @@ migrations: migration18: version: registration class: Alchemy\Phrasea\Setup\DoctrineMigrations\RegistrationMigration + migration19: + version: token + class: Alchemy\Phrasea\Setup\DoctrineMigrations\TokenMigration diff --git a/templates/web/prod/actions/Download/prepare.html.twig b/templates/web/prod/actions/Download/prepare.html.twig index 5b8625857d..d435f48236 100644 --- a/templates/web/prod/actions/Download/prepare.html.twig +++ b/templates/web/prod/actions/Download/prepare.html.twig @@ -19,7 +19,7 @@ {% elseif list['complete'] is defined and list['complete'] %}
- {% set url = path('document_download', {'token': token}) %} + {% set url = path('document_download', {'token': token.getValue()}) %} {% set before_link = '' %} {% set after_link = '' %} {% trans with {'%before_link%' : before_link, '%after_link%' : after_link} %}Your documents are ready. If the download does not start, %before_link%click here%after_link%{% endtrans %} @@ -91,7 +91,7 @@
-
+ {% if anonymous %} {% endif%} @@ -105,7 +105,7 @@ {% set time = time < 1 ? 2 : (time > 10 ? 10 : time) %} {% if list['complete'] is not defined %} {# Zip not done #} - $.post("{{ path('execute_download', {'token': token}) }}", function(data){ + $.post("{{ path('execute_download', {'token': token.getValue()}) }}", function(data){ var data = $.parseJSON(data); if(data.success) { $('form[name=download]').submit(); diff --git a/tests/Alchemy/Tests/Phrasea/Application/LightboxTest.php b/tests/Alchemy/Tests/Phrasea/Application/LightboxTest.php index 11792ebe5b..7c101aa338 100644 --- a/tests/Alchemy/Tests/Phrasea/Application/LightboxTest.php +++ b/tests/Alchemy/Tests/Phrasea/Application/LightboxTest.php @@ -49,9 +49,9 @@ class LightboxTest extends \PhraseanetAuthenticatedWebTestCase $this->logout(self::$DI['app']); $Basket = self::$DI['app']['EM']->find('Phraseanet:Basket', 1); - $token = self::$DI['app']['tokens']->getUrlToken(\random::TYPE_VIEW, self::$DI['user_alt2']->getId(), null, $Basket->getId()); + $token = self::$DI['app']['manipulator.token']->createBasketAccessToken($Basket, self::$DI['user_alt2']); - self::$DI['client']->request('GET', '/lightbox/?LOG='.$token); + self::$DI['client']->request('GET', '/lightbox/?LOG='.$token->getValue()); $this->assertTrue(self::$DI['client']->getResponse()->isRedirect()); $this->assertRegExp('/\/lightbox\/validate\/\d+\//', self::$DI['client']->getResponse()->headers->get('location')); diff --git a/tests/Alchemy/Tests/Phrasea/Authentication/Token/TokenValidatorTest.php b/tests/Alchemy/Tests/Phrasea/Authentication/Token/TokenValidatorTest.php deleted file mode 100644 index 7ad1a915c3..0000000000 --- a/tests/Alchemy/Tests/Phrasea/Authentication/Token/TokenValidatorTest.php +++ /dev/null @@ -1,31 +0,0 @@ -getUrlToken(\random::TYPE_VALIDATE, $usr_id); - - $validator = new TokenValidator(self::$DI['app']['tokens']); - $this->assertEquals($usr_id, $validator->isValid($token)); - } - /** - * @covers Alchemy\Phrasea\Authentication\TokenValidator::isValid - */ - public function testInvalidTokenIsNotValid() - { - $usr_id = 42; - $token = self::$DI['app']['tokens']->getUrlToken(\random::TYPE_VALIDATE, $usr_id, new \DateTime('-2 hours')); - - $validator = new TokenValidator(self::$DI['app']['tokens']); - $this->assertFalse($validator->isValid($token)); - } -} diff --git a/tests/Alchemy/Tests/Phrasea/Controller/Prod/DoDownloadTest.php b/tests/Alchemy/Tests/Phrasea/Controller/Prod/DoDownloadTest.php index 2d9a75f4a9..7cbefe58bf 100644 --- a/tests/Alchemy/Tests/Phrasea/Controller/Prod/DoDownloadTest.php +++ b/tests/Alchemy/Tests/Phrasea/Controller/Prod/DoDownloadTest.php @@ -37,7 +37,7 @@ class DoDownloadTest extends \PhraseanetAuthenticatedWebTestCase ] ] ]); - $url = sprintf('/download/%s/prepare/', $token); + $url = sprintf('/download/%s/prepare/', $token->getValue()); self::$DI['client']->request('GET', $url); $response = self::$DI['client']->getResponse(); $this->assertTrue($response->isOk()); @@ -61,7 +61,7 @@ class DoDownloadTest extends \PhraseanetAuthenticatedWebTestCase public function testPrepareDownloadInvalidData() { $token = $this->getToken(['bad_string' => base64_decode(serialize(['fail']))]); - self::$DI['client']->request('GET', sprintf('/download/%s/prepare/', $token)); + self::$DI['client']->request('GET', sprintf('/download/%s/prepare/', $token->getValue())); $response = self::$DI['client']->getResponse(); $this->assertEquals(500, $response->getStatusCode()); @@ -101,7 +101,7 @@ class DoDownloadTest extends \PhraseanetAuthenticatedWebTestCase ] ]); - $url = sprintf('/download/%s/get/', $token); + $url = sprintf('/download/%s/get/', $token->getValue()); self::$DI['client']->request('POST', $url); $response = self::$DI['client']->getResponse(); $this->assertTrue($response->isOk()); @@ -165,18 +165,16 @@ class DoDownloadTest extends \PhraseanetAuthenticatedWebTestCase ]; $token = $this->getToken($list); - // Get token - $datas = self::$DI['app']['tokens']->helloToken($token); // Build zip \set_export::build_zip( self::$DI['app'], $token, $list, - sprintf('%s/../../../../../../tmp/download/%s.zip', __DIR__, $datas['value']) // Dest file + sprintf('%s/../../../../../../tmp/download/%s.zip', __DIR__, $token->getValue()) // Dest file ); // Check response - $url = sprintf('/download/%s/get/', $token); + $url = sprintf('/download/%s/get/', $token->getValue()); self::$DI['client']->request('POST', $url); $response = self::$DI['client']->getResponse(); $this->assertTrue($response->isOk()); @@ -216,7 +214,7 @@ class DoDownloadTest extends \PhraseanetAuthenticatedWebTestCase ] ] ]); - $url = sprintf('/download/%s/get/', $token); + $url = sprintf('/download/%s/get/', $token->getValue()); self::$DI['client']->request('POST', $url); $this->assertNotFoundResponse(self::$DI['client']->getResponse()); @@ -239,7 +237,7 @@ class DoDownloadTest extends \PhraseanetAuthenticatedWebTestCase public function testDocumentsDownloadInvalidData() { $token = $this->getToken(['bad_string' => base64_decode(serialize(['fail']))]); - self::$DI['client']->request('POST', sprintf('/download/%s/get/', $token)); + self::$DI['client']->request('POST', sprintf('/download/%s/get/', $token->getValue())); $response = self::$DI['client']->getResponse(); $this->assertEquals(500, $response->getStatusCode()); @@ -251,8 +249,11 @@ class DoDownloadTest extends \PhraseanetAuthenticatedWebTestCase */ public function testExecuteDownloadInvalidData() { - $token = $this->getToken(['bad_string' => base64_decode(serialize(['fail']))]); - $url = sprintf('/download/%s/execute/', $token); + $token = self::$DI['app']['manipulator.token']->createDownloadToken( + self::$DI['user'], + base64_decode(serialize(['fail'])) + ); + $url = sprintf('/download/%s/execute/', $token->getValue()); self::$DI['client']->request('POST', $url); $response = self::$DI['client']->getResponse(); $datas = (array) json_decode($response->getContent()); @@ -268,7 +269,7 @@ class DoDownloadTest extends \PhraseanetAuthenticatedWebTestCase { $token = 'ABCDEFGHJaajKISU'; $url = sprintf('/download/%s/execute/', $token); - self::$DI['client']->request('POST', $url); + self::$DI['client']->request('POST', $url, [], [], ["HTTP_ACCEPT" => "application/json"]); $response = self::$DI['client']->getResponse(); $datas = (array) json_decode($response->getContent()); $this->assertArrayHasKey('success', $datas); @@ -329,7 +330,7 @@ class DoDownloadTest extends \PhraseanetAuthenticatedWebTestCase $token = $this->getToken($list); - $url = sprintf('/download/%s/execute/', $token); + $url = sprintf('/download/%s/execute/', $token->getValue()); self::$DI['client']->request('POST', $url); $response = self::$DI['client']->getResponse(); $datas = (array) json_decode($response->getContent()); @@ -340,10 +341,8 @@ class DoDownloadTest extends \PhraseanetAuthenticatedWebTestCase private function getToken($datas = []) { - return self::$DI['app']['tokens']->getUrlToken( - \random::TYPE_DOWNLOAD, - self::$DI['user']->getId(), - new \DateTime('+10 seconds'), // Token lifetime + return self::$DI['app']['manipulator.token']->createDownloadToken( + self::$DI['user'], serialize($datas) ); } diff --git a/tests/Alchemy/Tests/Phrasea/Controller/Prod/DownloadTest.php b/tests/Alchemy/Tests/Phrasea/Controller/Prod/DownloadTest.php index 61144ed776..faf8e47102 100644 --- a/tests/Alchemy/Tests/Phrasea/Controller/Prod/DownloadTest.php +++ b/tests/Alchemy/Tests/Phrasea/Controller/Prod/DownloadTest.php @@ -32,7 +32,7 @@ class DownloadTest extends \PhraseanetAuthenticatedWebTestCase $response = self::$DI['client']->getResponse(); $this->assertTrue($response->isRedirect()); - $this->assertRegExp('#/download/[a-zA-Z0-9\.\/]{8,16}/#', $response->headers->get('location')); + $this->assertRegExp('#/download/[a-zA-Z0-9]{8,32}/#', $response->headers->get('location')); unset($response, $eventManagerStub); } @@ -52,7 +52,7 @@ class DownloadTest extends \PhraseanetAuthenticatedWebTestCase self::$DI['app']['events-manager'] = $eventManagerStub; - self::$DI['app']['authentication']->setUser($this->createUserMock()); + self::$DI['app']['authentication']->setUser(self::$DI['user']); $stubbedACL = $this->getMockBuilder('\ACL') ->disableOriginalConstructor() @@ -92,7 +92,7 @@ class DownloadTest extends \PhraseanetAuthenticatedWebTestCase $response = self::$DI['client']->getResponse(); $this->assertTrue($response->isRedirect()); - $this->assertRegExp('#/download/[a-zA-Z0-9\.\/]{8,16}/#', $response->headers->get('location')); + $this->assertRegExp('#/download/[a-zA-Z0-9]{8,32}/#', $response->headers->get('location')); unset($response, $eventManagerStub); } @@ -124,7 +124,7 @@ class DownloadTest extends \PhraseanetAuthenticatedWebTestCase $response = self::$DI['client']->getResponse(); $this->assertTrue($response->isRedirect()); - $this->assertRegExp('#/download/[a-zA-Z0-9\.\/]{8,16}/#', $response->headers->get('location')); + $this->assertRegExp('#/download/[a-zA-Z0-9]{8,32}/#', $response->headers->get('location')); unset($response, $eventManagerStub); } @@ -156,7 +156,7 @@ class DownloadTest extends \PhraseanetAuthenticatedWebTestCase $response = self::$DI['client']->getResponse(); $this->assertTrue($response->isRedirect()); - $this->assertRegExp('#/download/[a-zA-Z0-9\.\/]{8,16}/#', $response->headers->get('location')); + $this->assertRegExp('#/download/[a-zA-Z0-9]{8,32}/#', $response->headers->get('location')); unset($response, $eventManagerStub); } diff --git a/tests/Alchemy/Tests/Phrasea/Controller/Root/AccountTest.php b/tests/Alchemy/Tests/Phrasea/Controller/Root/AccountTest.php index a22c38518e..7c4c67393c 100644 --- a/tests/Alchemy/Tests/Phrasea/Controller/Root/AccountTest.php +++ b/tests/Alchemy/Tests/Phrasea/Controller/Root/AccountTest.php @@ -104,19 +104,16 @@ class AccountTest extends \PhraseanetAuthenticatedWebTestCase */ public function testGetResetMailWithToken() { - $token = self::$DI['app']['tokens']->getUrlToken(\random::TYPE_EMAIL, self::$DI['user']->getId(), null, 'new_email@email.com'); - $crawler = self::$DI['client']->request('GET', '/account/reset-email/', ['token' => $token]); + $tokenValue = self::$DI['app']['manipulator.token']->createResetEmailToken(self::$DI['user'], 'new_email@email.com')->getValue(); + $crawler = self::$DI['client']->request('GET', '/account/reset-email/', ['token' => $tokenValue]); $response = self::$DI['client']->getResponse(); $this->assertTrue($response->isRedirect()); $this->assertEquals('/account/', $response->headers->get('location')); $this->assertEquals('new_email@email.com', self::$DI['user']->getEmail()); self::$DI['user']->setEmail('noone@example.com'); - try { - self::$DI['app']['tokens']->helloToken($token); + if (null !== self::$DI['app']['repo.tokens']->find($tokenValue)) { $this->fail('Token has not been removed'); - } catch (NotFoundHttpException $e) { - } $this->assertFlashMessagePopulated(self::$DI['app'], 'success', 1); diff --git a/tests/Alchemy/Tests/Phrasea/Controller/Root/LoginTest.php b/tests/Alchemy/Tests/Phrasea/Controller/Root/LoginTest.php index bd7c8bf6e3..9ccafb9b4b 100644 --- a/tests/Alchemy/Tests/Phrasea/Controller/Root/LoginTest.php +++ b/tests/Alchemy/Tests/Phrasea/Controller/Root/LoginTest.php @@ -11,6 +11,7 @@ use Alchemy\Phrasea\Exception\InvalidArgumentException; use Alchemy\Phrasea\Authentication\ProvidersCollection; use Alchemy\Phrasea\Model\Entities\Registration; use Alchemy\Phrasea\Model\Entities\User; +use Alchemy\Phrasea\Model\Manipulator\TokenManipulator; use RandomLib\Factory; use Symfony\Component\HttpKernel\Client; use Symfony\Component\HttpFoundation\ResponseHeaderBag; @@ -170,9 +171,12 @@ class LoginTest extends \PhraseanetAuthenticatedWebTestCase { $this->logout(self::$DI['app']); $email = $this->generateEmail(); - $token = self::$DI['app']['tokens']->getUrlToken(\random::TYPE_EMAIL, 0, null, $email); + $token = self::$DI['app']['manipulator.token']->createResetEmailToken(self::$DI['user'], $email); + $tokenValue = $token->getValue(); + self::$DI['app']['EM']->remove($token); + self::$DI['app']['EM']->flush(); self::$DI['client']->request('GET', '/login/register-confirm/', [ - 'code' => $token + 'code' => $tokenValue ]); $response = self::$DI['client']->getResponse(); @@ -188,11 +192,11 @@ class LoginTest extends \PhraseanetAuthenticatedWebTestCase { $this->logout(self::$DI['app']); $email = $this->generateEmail(); - $token = self::$DI['app']['tokens']->getUrlToken(\random::TYPE_EMAIL, self::$DI['user']->getId(), null, $email); + $token = self::$DI['app']['manipulator.token']->createResetEmailToken(self::$DI['user'], $email); self::$DI['user']->setMailLocked(false); - self::$DI['client']->request('GET', '/login/register-confirm/', ['code' => $token]); + self::$DI['client']->request('GET', '/login/register-confirm/', ['code' => $token->getValue()]); $response = self::$DI['client']->getResponse(); $this->assertTrue($response->isRedirect()); @@ -207,7 +211,7 @@ class LoginTest extends \PhraseanetAuthenticatedWebTestCase $this->logout(self::$DI['app']); $email = $this->generateEmail(); - $token = self::$DI['app']['tokens']->getUrlToken(\random::TYPE_EMAIL, self::$DI['user']->getId(), null, $email); + $token = self::$DI['app']['manipulator.token']->createResetEmailToken(self::$DI['user'], $email); self::$DI['user']->setMailLocked(true); $this->deleteRequest(); @@ -218,7 +222,7 @@ class LoginTest extends \PhraseanetAuthenticatedWebTestCase self::$DI['app']['EM']->persist($registration); self::$DI['app']['EM']->flush(); - self::$DI['client']->request('GET', '/login/register-confirm/', ['code' => $token]); + self::$DI['client']->request('GET', '/login/register-confirm/', ['code' => $token->getValue()]); $response = self::$DI['client']->getResponse(); $this->assertTrue($response->isRedirect()); @@ -235,13 +239,13 @@ class LoginTest extends \PhraseanetAuthenticatedWebTestCase $this->logout(self::$DI['app']); $email = $this->generateEmail(); $user = self::$DI['app']['manipulator.user']->createUser('test', 'test', $email); - $token = self::$DI['app']['tokens']->getUrlToken(\random::TYPE_EMAIL, $user->getId(), null, $email); + $token = self::$DI['app']['manipulator.token']->createResetEmailToken($user, $email); $user->setMailLocked(true); $this->deleteRequest(); - self::$DI['client']->request('GET', '/login/register-confirm/', ['code' => $token]); + self::$DI['client']->request('GET', '/login/register-confirm/', ['code' => $token->getValue()]); $response = self::$DI['client']->getResponse(); $this->assertTrue($response->isRedirect()); $this->assertFlashMessagePopulated(self::$DI['app'], 'info', 1); @@ -304,9 +308,9 @@ class LoginTest extends \PhraseanetAuthenticatedWebTestCase public function testRenewPasswordBadArguments() { $this->logout(self::$DI['app']); - $token = self::$DI['app']['tokens']->getUrlToken(\random::TYPE_PASSWORD, self::$DI['user']->getId()); + $token = self::$DI['app']['manipulator.token']->createResetPasswordToken(self::$DI['user']); $crawler = self::$DI['client']->request('POST', '/login/renew-password/', [ - 'token' => $token, + 'token' => $token->getValue(), '_token' => 'token', 'password' => ['password' => 'password', 'confirm' => 'not_identical'] ]); @@ -376,10 +380,10 @@ class LoginTest extends \PhraseanetAuthenticatedWebTestCase public function testRenewPassword() { $this->logout(self::$DI['app']); - $token = self::$DI['app']['tokens']->getUrlToken(\random::TYPE_PASSWORD, self::$DI['user']->getId()); + $token = self::$DI['app']['manipulator.token']->createResetPasswordToken(self::$DI['user']); self::$DI['client']->request('POST', '/login/renew-password/', [ - 'token' => $token, + 'token' => $token->getValue(), '_token' => 'token', 'password' => ['password' => 'password', 'confirm' => 'password'] ]); @@ -400,10 +404,10 @@ class LoginTest extends \PhraseanetAuthenticatedWebTestCase $this->logout(self::$DI['app']); self::$DI['app']->addFlash($type, $message); - $token = self::$DI['app']['tokens']->getUrlToken(\random::TYPE_PASSWORD, self::$DI['user']->getId()); + $token = self::$DI['app']['manipulator.token']->createResetPasswordToken(self::$DI['user']); $crawler = self::$DI['client']->request('GET', '/login/renew-password/', [ - 'token' => $token + 'token' => $token->getValue() ]); $this->assertTrue(self::$DI['client']->getResponse()->isOk()); @@ -1794,7 +1798,7 @@ class LoginTest extends \PhraseanetAuthenticatedWebTestCase $factory = new Factory(); $generator = $factory->getLowStrengthGenerator(); - return $generator->generateString(8, \random::LETTERS_AND_NUMBERS) . '_email@email.com'; + return $generator->generateString(8, TokenManipulator::LETTERS_AND_NUMBERS) . '_email@email.com'; } private function disableTOU() diff --git a/tests/Alchemy/Tests/Phrasea/Core/Middleware/TokenMiddlewareProviderTest.php b/tests/Alchemy/Tests/Phrasea/Core/Middleware/TokenMiddlewareProviderTest.php new file mode 100644 index 0000000000..b6ec82bd25 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Core/Middleware/TokenMiddlewareProviderTest.php @@ -0,0 +1,39 @@ +authenticate(self::$DI['app']); + self::$DI['app']->register(new TokenMiddlewareProvider()); + $request = new Request(); + call_user_func(self::$DI['app']['middleware.token.converter'], $request, self::$DI['app']); + $this->assertNull($request->attributes->get('token')); + } + + public function testConverterWithBasketParameter() + { + $this->authenticate(self::$DI['app']); + self::$DI['app']->register(new TokenMiddlewareProvider()); + $request = new Request(); + $token = self::$DI['token_1']; + $request->attributes->set('token', $token->getValue()); + call_user_func(self::$DI['app']['middleware.token.converter'], $request, self::$DI['app']); + $this->assertSame($token, $request->attributes->get('token')); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Core/Provider/AuthenticationManagerServiceProviderTest.php b/tests/Alchemy/Tests/Phrasea/Core/Provider/AuthenticationManagerServiceProviderTest.php index 36065ee9b6..5a3d453558 100644 --- a/tests/Alchemy/Tests/Phrasea/Core/Provider/AuthenticationManagerServiceProviderTest.php +++ b/tests/Alchemy/Tests/Phrasea/Core/Provider/AuthenticationManagerServiceProviderTest.php @@ -20,11 +20,6 @@ class AuthenticationManagerServiceProviderTest extends ServiceProviderTestCase 'authentication', 'Alchemy\\Phrasea\\Authentication\\Authenticator', ], - [ - 'Alchemy\Phrasea\Core\Provider\AuthenticationManagerServiceProvider', - 'authentication.token-validator', - 'Alchemy\Phrasea\Authentication\Token\TokenValidator' - ], [ 'Alchemy\Phrasea\Core\Provider\AuthenticationManagerServiceProvider', 'authentication.persistent-manager', diff --git a/tests/Alchemy/Tests/Phrasea/Core/Provider/ConvertersServiceProvider.php b/tests/Alchemy/Tests/Phrasea/Core/Provider/ConvertersServiceProvider.php index f0e7c8cc30..2bd05318bf 100644 --- a/tests/Alchemy/Tests/Phrasea/Core/Provider/ConvertersServiceProvider.php +++ b/tests/Alchemy/Tests/Phrasea/Core/Provider/ConvertersServiceProvider.php @@ -17,6 +17,11 @@ class ConvertersServiceProvider extends ServiceProviderTestCase 'converter.basket', 'Alchemy\Phrasea\Controller\Converter\BasketConverter' ], + [ + 'Alchemy\Phrasea\Core\Provider\ConvertersServiceProvider', + 'converter.token', + 'Alchemy\Phrasea\Controller\Converter\TokenConverter' + ], ]; } } diff --git a/tests/Alchemy/Tests/Phrasea/Core/Provider/ManipulatorServiceProviderTest.php b/tests/Alchemy/Tests/Phrasea/Core/Provider/ManipulatorServiceProviderTest.php index a313f90c32..6ee7d04b2c 100644 --- a/tests/Alchemy/Tests/Phrasea/Core/Provider/ManipulatorServiceProviderTest.php +++ b/tests/Alchemy/Tests/Phrasea/Core/Provider/ManipulatorServiceProviderTest.php @@ -22,6 +22,11 @@ class ManipulatorServiceProviderTest extends ServiceProviderTestCase 'manipulator.registration', 'Alchemy\Phrasea\Model\Manipulator\RegistrationManipulator' ], + [ + 'Alchemy\Phrasea\Core\Provider\ManipulatorServiceProvider', + 'manipulator.token', + 'Alchemy\Phrasea\Model\Manipulator\TokenManipulator' + ], ]; } } diff --git a/tests/Alchemy/Tests/Phrasea/Core/Provider/RepositoriesServiceProviderTest.php b/tests/Alchemy/Tests/Phrasea/Core/Provider/RepositoriesServiceProviderTest.php index 7ab63d3c3c..5b8cd3de36 100644 --- a/tests/Alchemy/Tests/Phrasea/Core/Provider/RepositoriesServiceProviderTest.php +++ b/tests/Alchemy/Tests/Phrasea/Core/Provider/RepositoriesServiceProviderTest.php @@ -31,6 +31,7 @@ class RepositoriesServiceProviderTest extends ServiceProviderTestCase ['Alchemy\Phrasea\Core\Provider\RepositoriesServiceProvider', 'repo.usr-auth-providers', 'Alchemy\Phrasea\Model\Repositories\UsrAuthProviderRepository'], ['Alchemy\Phrasea\Core\Provider\RepositoriesServiceProvider', 'repo.ftp-exports', 'Alchemy\Phrasea\Model\Repositories\FtpExportRepository'], ['Alchemy\Phrasea\Core\Provider\RepositoriesServiceProvider', 'repo.user-queries', 'Alchemy\Phrasea\Model\Repositories\UserQueryRepository'], + ['Alchemy\Phrasea\Core\Provider\RepositoriesServiceProvider', 'repo.tokens', 'Alchemy\Phrasea\Model\Repositories\TokenRepository'], ]; } } diff --git a/tests/Alchemy/Tests/Phrasea/Form/Constraint/PasswordTokenTest.php b/tests/Alchemy/Tests/Phrasea/Form/Constraint/PasswordTokenTest.php index 8ef5d35762..4c96f702d4 100644 --- a/tests/Alchemy/Tests/Phrasea/Form/Constraint/PasswordTokenTest.php +++ b/tests/Alchemy/Tests/Phrasea/Form/Constraint/PasswordTokenTest.php @@ -3,47 +3,51 @@ namespace Alchemy\Tests\Phrasea\Form\Constraint; use Alchemy\Phrasea\Form\Constraint\PasswordToken; +use Alchemy\Phrasea\Model\Entities\Token; +use Alchemy\Phrasea\Model\Manipulator\TokenManipulator; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class PasswordTokenTest extends \PhraseanetTestCase { public function testInvalidTokenIsNotValid() { - $random = $this - ->getMockBuilder('random') + $repo = $this + ->getMockBuilder('Doctrine\ORM\EntityRepository') ->disableOriginalConstructor() - ->setMethods(['helloToken']) + ->setMethods(['findValidToken']) ->getMock(); - $token = self::$DI['app']['random.low']->generateString(8); + $tokenValue = self::$DI['app']['random.low']->generateString(8); - $random + $repo ->expects($this->once()) - ->method('helloToken') - ->with($token) - ->will($this->throwException(new NotFoundHttpException('Token not found'))); + ->method('findValidToken') + ->with($tokenValue) + ->will($this->returnValue(null)); - $constraint = new PasswordToken($random); - $this->assertFalse($constraint->isValid($token)); + $constraint = new PasswordToken($repo); + $this->assertFalse($constraint->isValid($tokenValue)); } public function testValidTokenIsValid() { - $random = $this - ->getMockBuilder('random') + $repo = $this + ->getMockBuilder('Doctrine\ORM\EntityRepository') ->disableOriginalConstructor() - ->setMethods(['helloToken']) + ->setMethods(['findValidToken']) ->getMock(); - $token = self::$DI['app']['random.low']->generateString(8); + $tokenValue = self::$DI['app']['random.low']->generateString(8); + $token = new Token(); + $token->setType(TokenManipulator::TYPE_PASSWORD); - $random + $repo ->expects($this->once()) - ->method('helloToken') - ->with($token) - ->will($this->returnValue(['usr_id' => mt_rand(), 'type' => \random::TYPE_PASSWORD])); + ->method('findValidToken') + ->with($tokenValue) + ->will($this->returnValue($token)); - $constraint = new PasswordToken($random); - $this->assertTrue($constraint->isValid($token)); + $constraint = new PasswordToken($repo); + $this->assertTrue($constraint->isValid($tokenValue)); } } diff --git a/tests/Alchemy/Tests/Phrasea/Form/Login/PhraseaRecoverPasswordFormTest.php b/tests/Alchemy/Tests/Phrasea/Form/Login/PhraseaRecoverPasswordFormTest.php index b74cbfb8d8..fad851cb40 100644 --- a/tests/Alchemy/Tests/Phrasea/Form/Login/PhraseaRecoverPasswordFormTest.php +++ b/tests/Alchemy/Tests/Phrasea/Form/Login/PhraseaRecoverPasswordFormTest.php @@ -9,6 +9,6 @@ class PhraseaRecoverPasswordFormTest extends FormTestCase { protected function getForm() { - return new PhraseaRecoverPasswordForm(self::$DI['app']['tokens']); + return new PhraseaRecoverPasswordForm(self::$DI['app']['repo.tokens']); } } diff --git a/tests/Alchemy/Tests/Phrasea/Model/Converter/TokenConverterTest.php b/tests/Alchemy/Tests/Phrasea/Model/Converter/TokenConverterTest.php new file mode 100644 index 0000000000..e26d541021 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Model/Converter/TokenConverterTest.php @@ -0,0 +1,26 @@ +assertSame($token, $converter->convert($token->getValue())); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + * @expectedExceptionMessage Token is not valid. + */ + public function testConvertFailure() + { + $converter = new TokenConverter(self::$DI['app']['repo.tokens']); + $converter->convert('prout'); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Model/Manipulator/TokenManipulatorTest.php b/tests/Alchemy/Tests/Phrasea/Model/Manipulator/TokenManipulatorTest.php new file mode 100644 index 0000000000..1e4dd76e44 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Model/Manipulator/TokenManipulatorTest.php @@ -0,0 +1,185 @@ +create($user, $type, $expiration, $data); + + $this->assertSame($user, $token->getUser()); + $this->assertSame($type, $token->getType()); + $this->assertSame($expiration, $token->getExpiration()); + $this->assertSame($data, $token->getData()); + + $this->assertInternalType('string', $token->getValue()); + $this->assertEquals(32, strlen($token->getValue())); + } + + public function provideConstructorArguments() + { + return [ + [true, TokenManipulator::TYPE_RSS, null, null], + [false, TokenManipulator::TYPE_RSS, null, null], + [false, TokenManipulator::TYPE_RSS, new \DateTime('-1 day'), 'data'], + ]; + } + + public function testCreateBasketValidationToken() + { + $manipulator = new TokenManipulator(self::$DI['app']['EM'], self::$DI['app']['random.low'], self::$DI['app']['repo.tokens']); + $token = $manipulator->createBasketValidationToken(self::$DI['basket_4'], self::$DI['user_1']); + + $this->assertSame(self::$DI['basket_4']->getId(), $token->getData()); + $this->assertSame(self::$DI['user_1'], $token->getUser()); + $this->assertSame(TokenManipulator::TYPE_VALIDATE, $token->getType()); + $this->assertDateNear('+10 days', $token->getExpiration()); + } + + public function testCreateBasketValidationTokenWithoutUser() + { + $manipulator = new TokenManipulator(self::$DI['app']['EM'], self::$DI['app']['random.low'], self::$DI['app']['repo.tokens']); + $token = $manipulator->createBasketValidationToken(self::$DI['basket_4']); + + $this->assertSame(self::$DI['basket_4']->getId(), $token->getData()); + $this->assertSame(self::$DI['basket_4']->getValidation()->getInitiator(), $token->getUser()); + $this->assertSame(TokenManipulator::TYPE_VALIDATE, $token->getType()); + $this->assertDateNear('+10 days', $token->getExpiration()); + } + + public function testCreateBasketValidationTokenWithInvalidBasket() + { + $manipulator = new TokenManipulator(self::$DI['app']['EM'], self::$DI['app']['random.low'], self::$DI['app']['repo.tokens']); + $this->setExpectedException('InvalidArgumentException', 'A validation token requires a validation basket.'); + $manipulator->createBasketValidationToken(self::$DI['basket_1']); + } + + public function testCreateBasketAccessToken() + { + $manipulator = new TokenManipulator(self::$DI['app']['EM'], self::$DI['app']['random.low'], self::$DI['app']['repo.tokens']); + $token = $manipulator->createBasketAccessToken(self::$DI['basket_4'], self::$DI['user']); + + $this->assertSame(self::$DI['basket_4']->getId(), $token->getData()); + $this->assertSame(self::$DI['user'], $token->getUser()); + $this->assertSame(TokenManipulator::TYPE_VIEW, $token->getType()); + $this->assertNull($token->getExpiration()); + } + + public function testCreateFeedEntryToken() + { + $manipulator = new TokenManipulator(self::$DI['app']['EM'], self::$DI['app']['random.low'], self::$DI['app']['repo.tokens']); + $token = $manipulator->createFeedEntryToken(self::$DI['user'], self::$DI['feed_public_entry']); + + $this->assertSame(self::$DI['feed_public_entry']->getId(), $token->getData()); + $this->assertSame(self::$DI['user'], $token->getUser()); + $this->assertSame(TokenManipulator::TYPE_FEED_ENTRY, $token->getType()); + $this->assertNull($token->getExpiration()); + } + + public function testCreateDownloadToken() + { + $data = serialize(array('some' => 'data')); + $manipulator = new TokenManipulator(self::$DI['app']['EM'], self::$DI['app']['random.low'], self::$DI['app']['repo.tokens']); + $token = $manipulator->createDownloadToken(self::$DI['user'], $data); + + $this->assertSame($data, $token->getData()); + $this->assertSame(self::$DI['user'], $token->getUser()); + $this->assertSame(TokenManipulator::TYPE_DOWNLOAD, $token->getType()); + $this->assertDateNear('+3 hours', $token->getExpiration()); + } + + public function testCreateEmailExportToken() + { + $data = serialize(array('some' => 'data')); + $manipulator = new TokenManipulator(self::$DI['app']['EM'], self::$DI['app']['random.low'], self::$DI['app']['repo.tokens']); + $token = $manipulator->createEmailExportToken($data); + + $this->assertSame($data, $token->getData()); + $this->assertNull($token->getUser()); + $this->assertSame(TokenManipulator::TYPE_EMAIL, $token->getType()); + $this->assertDateNear('+1 day', $token->getExpiration()); + } + + public function testCreateResetEmailToken() + { + $manipulator = new TokenManipulator(self::$DI['app']['EM'], self::$DI['app']['random.low'], self::$DI['app']['repo.tokens']); + $token = $manipulator->createResetEmailToken(self::$DI['user'], 'newemail@phraseanet.com'); + + $this->assertSame('newemail@phraseanet.com', $token->getData()); + $this->assertSame(self::$DI['user'], $token->getUser()); + $this->assertSame(TokenManipulator::TYPE_EMAIL_RESET, $token->getType()); + $this->assertDateNear('+1 day', $token->getExpiration()); + } + + public function testCreateAccountUnlockToken() + { + $manipulator = new TokenManipulator(self::$DI['app']['EM'], self::$DI['app']['random.low'], self::$DI['app']['repo.tokens']); + $token = $manipulator->createAccountUnlockToken(self::$DI['user']); + + $this->assertNull($token->getData()); + $this->assertSame(self::$DI['user'], $token->getUser()); + $this->assertSame(TokenManipulator::TYPE_ACCOUNT_UNLOCK, $token->getType()); + $this->assertDateNear('+3 days', $token->getExpiration()); + } + + public function testCreateResetPasswordToken() + { + $manipulator = new TokenManipulator(self::$DI['app']['EM'], self::$DI['app']['random.low'], self::$DI['app']['repo.tokens']); + $token = $manipulator->createResetPasswordToken(self::$DI['user']); + + $this->assertNull($token->getData()); + $this->assertSame(self::$DI['user'], $token->getUser()); + $this->assertSame(TokenManipulator::TYPE_PASSWORD, $token->getType()); + $this->assertDateNear('+1 day', $token->getExpiration()); + } + + public function testUpdate() + { + $em = $this->createEntityManagerMock(); + $token = new Token(); + + $em->expects($this->once()) + ->method('persist') + ->with($token); + $em->expects($this->once()) + ->method('flush'); + + $manipulator = new TokenManipulator($em, self::$DI['app']['random.low'], self::$DI['app']['repo.tokens']); + $manipulator->update($token); + } + + public function testDelete() + { + $em = $this->createEntityManagerMock(); + $token = new Token(); + + $em->expects($this->once()) + ->method('remove') + ->with($token); + $em->expects($this->once()) + ->method('flush'); + + $manipulator = new TokenManipulator($em, self::$DI['app']['random.low'], self::$DI['app']['repo.tokens']); + $manipulator->delete($token); + } + + public function testRemoveExpiredToken() + { + $this->assertCount(4, self::$DI['app']['repo.tokens']->findAll()); + + $manipulator = new TokenManipulator(self::$DI['app']['EM'], self::$DI['app']['random.low'], self::$DI['app']['repo.tokens']); + $manipulator->removeExpiredTokens(); + + $this->assertCount(3, self::$DI['app']['repo.tokens']->findAll()); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Model/Repositories/TokenRepositoryTest.php b/tests/Alchemy/Tests/Phrasea/Model/Repositories/TokenRepositoryTest.php new file mode 100644 index 0000000000..59391d6451 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Model/Repositories/TokenRepositoryTest.php @@ -0,0 +1,28 @@ +assertSame(self::$DI['token_1'], $repo->findValidToken(self::$DI['token_1']->getValue())); + $this->assertSame(self::$DI['token_2'], $repo->findValidToken(self::$DI['token_2']->getValue())); + $this->assertNull($repo->findValidToken(self::$DI['token_invalid']->getValue())); + } + + public function testFindValidationToken() + { + $repo = self::$DI['app']['repo.tokens']; + $this->assertSame(self::$DI['token_validation'], $repo->findValidationToken(self::$DI['basket_1'], self::$DI['user'])); + } + + public function testExpiredTokens() + { + $repo = self::$DI['app']['repo.tokens']; + $tokens = $repo->findExpiredTokens(); + $this->assertCount(1, $tokens); + $this->assertSame(self::$DI['token_invalid'], array_pop($tokens)); + } +} diff --git a/tests/classes/PhraseanetTestCase.php b/tests/classes/PhraseanetTestCase.php index 1727f9f6a5..b6d75e07b2 100644 --- a/tests/classes/PhraseanetTestCase.php +++ b/tests/classes/PhraseanetTestCase.php @@ -100,6 +100,58 @@ abstract class PhraseanetTestCase extends WebTestCase return new Client($DI['app'], []); }); + self::$DI['feed_public'] = self::$DI->share(function ($DI) { + return $DI['app']['repo.feeds']->find(self::$fixtureIds['feed']['public']['feed']); + }); + self::$DI['feed_public_entry'] = self::$DI->share(function ($DI) { + return $DI['app']['repo.feed-entries']->find(self::$fixtureIds['feed']['public']['entry']); + }); + self::$DI['feed_public_token'] = self::$DI->share(function ($DI) { + return $DI['app']['repo.feed-tokens']->find(self::$fixtureIds['feed']['public']['token']); + }); + + self::$DI['feed_private'] = self::$DI->share(function ($DI) { + return $DI['app']['repo.feeds']->find(self::$fixtureIds['feed']['private']['feed']); + }); + self::$DI['feed_private_entry'] = self::$DI->share(function ($DI) { + return $DI['app']['repo.feed-entries']->find(self::$fixtureIds['feed']['private']['entry']); + }); + self::$DI['feed_private_token'] = self::$DI->share(function ($DI) { + return $DI['app']['repo.feed-tokens']->find(self::$fixtureIds['feed']['private']['token']); + }); + + self::$DI['basket_1'] = self::$DI->share(function ($DI) { + return $DI['app']['repo.baskets']->find(self::$fixtureIds['basket']['basket_1']); + }); + + self::$DI['basket_2'] = self::$DI->share(function ($DI) { + return $DI['app']['repo.baskets']->find(self::$fixtureIds['basket']['basket_2']); + }); + + self::$DI['basket_3'] = self::$DI->share(function ($DI) { + return $DI['app']['repo.baskets']->find(self::$fixtureIds['basket']['basket_3']); + }); + + self::$DI['basket_4'] = self::$DI->share(function ($DI) { + return $DI['app']['repo.baskets']->find(self::$fixtureIds['basket']['basket_4']); + }); + + self::$DI['token_1'] = self::$DI->share(function ($DI) { + return $DI['app']['repo.tokens']->find(self::$fixtureIds['token']['token_1']); + }); + + self::$DI['token_2'] = self::$DI->share(function ($DI) { + return $DI['app']['repo.tokens']->find(self::$fixtureIds['token']['token_2']); + }); + + self::$DI['token_invalid'] = self::$DI->share(function ($DI) { + return $DI['app']['repo.tokens']->find(self::$fixtureIds['token']['token_invalid']); + }); + + self::$DI['token_validation'] = self::$DI->share(function ($DI) { + return $DI['app']['repo.tokens']->find(self::$fixtureIds['token']['token_validation']); + }); + self::$DI['user'] = self::$DI->share(function ($DI) { return $DI['app']['repo.users']->find(self::$fixtureIds['user']['test_phpunit']); }); @@ -672,6 +724,14 @@ abstract class PhraseanetTestCase extends WebTestCase ->getMock(); } + protected function assertDateNear($expected, $tested, $precision = 1) + { + $tested = $tested instanceof \DateTime ? $tested : new \DateTime($tested); + $expected = $expected instanceof \DateTime ? $expected : new \DateTime($expected); + + $this->assertLessThanOrEqual($precision, abs($expected->format('U') - $tested->format('U'))); + } + protected function createSearchEngineMock() { $mock = $this->getMock('Alchemy\Phrasea\SearchEngine\SearchEngineInterface'); diff --git a/tests/classes/randomTest.php b/tests/classes/randomTest.php deleted file mode 100644 index 8351e99740..0000000000 --- a/tests/classes/randomTest.php +++ /dev/null @@ -1,107 +0,0 @@ -random = new \random(self::$DI['app']); - } - - public function testCleanTokens() - { - $expires_on = new DateTime('-5 minutes'); - $usr_id = self::$DI['user']->getId(); - $token = $this->random->getUrlToken(\random::TYPE_PASSWORD, $usr_id, $expires_on, 'some nice datas'); - $this->random->cleanTokens(self::$DI['app']); - - try { - $this->random->helloToken($token); - $this->fail(); - } catch (NotFoundHttpException $e) { - - } - } - - public function testGetUrlToken() - { - $usr_id = self::$DI['user']->getId(); - $token = $this->random->getUrlToken(\random::TYPE_PASSWORD, $usr_id, null, 'some nice datas'); - $datas = $this->random->helloToken($token); - $this->assertEquals('some nice datas', $datas['datas']); - $this->random->updateToken($token, 'some very nice datas'); - $datas = $this->random->helloToken($token); - $this->assertEquals('some very nice datas', $datas['datas']); - $this->random->removeToken($token); - } - - public function testRemoveToken() - { - $this->testGetUrlToken(); - } - - public function testUpdateToken() - { - $this->testGetUrlToken(); - } - - public function testHelloToken() - { - $usr_id = self::$DI['user']->getId(); - $token = $this->random->getUrlToken(\random::TYPE_PASSWORD, $usr_id, null, 'some nice datas'); - $datas = $this->random->helloToken($token); - $this->assertEquals('some nice datas', $datas['datas']); - $this->assertNull($datas['expire_on']); - $created_on = new DateTime($datas['created_on']); - $date = new DateTime('-3 seconds'); - $this->assertTrue($date < $created_on, "asserting that " . $date->format(DATE_ATOM) . " is before " . $created_on->format(DATE_ATOM)); - $date = new DateTime(); - $this->assertTrue($date >= $created_on); - $this->assertEquals('password', $datas['type']); - - $this->random->removeToken($token); - try { - $this->random->helloToken($token); - $this->fail(); - } catch (NotFoundHttpException $e) { - - } - - $expires_on = new DateTime('+5 minutes'); - $usr_id = self::$DI['user']->getId(); - $token = $this->random->getUrlToken(\random::TYPE_PASSWORD, $usr_id, $expires_on, 'some nice datas'); - $datas = $this->random->helloToken($token); - $this->assertEquals('some nice datas', $datas['datas']); - $sql_expires = new DateTime($datas['expire_on']); - $this->assertTrue($sql_expires == $expires_on); - $created_on = new DateTime($datas['created_on']); - $date = new DateTime('-3 seconds'); - $this->assertTrue($date < $created_on); - $date = new DateTime(); - $this->assertTrue($date >= $created_on); - $this->assertEquals('password', $datas['type']); - - $this->random->removeToken($token); - try { - $this->random->helloToken($token); - $this->fail(); - } catch (NotFoundHttpException $e) { - - } - - $expires_on = new DateTime('-5 minutes'); - $usr_id = self::$DI['user']->getId(); - $token = $this->random->getUrlToken(\random::TYPE_PASSWORD, $usr_id, $expires_on, 'some nice datas'); - - try { - $this->random->helloToken($token); - $this->fail(); - } catch (NotFoundHttpException $e) { - - } - } -}