diff --git a/UPGRADING.md b/UPGRADING.md index 3aed41c4..2dcb6dec 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -11,6 +11,7 @@ Perform EACH version specific task between your version and the new one, otherwi ## [1.10](https://github.com/kevinpapst/kimai2/releases/tag/1.10) - Invoice renderer `CSV` was removed +- Sessions are now stored in the database (all users have to re-login after upgrade) ### Developer diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index 29c33e2b..ab8be95d 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -6,9 +6,12 @@ framework: # Enables session support. Note that the session will ONLY be started if you read or write from it. # Remove or comment this section to explicitly disable session support. + #session: + # handler_id: session.handler.native_file + # save_path: "%kernel.project_dir%/var/sessions/%kernel.environment%" + session: - handler_id: session.handler.native_file - save_path: "%kernel.project_dir%/var/sessions/%kernel.environment%" + handler_id: App\Security\SessionHandler #esi: ~ #fragments: ~ diff --git a/config/services.yaml b/config/services.yaml index 2b26359b..6a65951a 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -104,6 +104,11 @@ services: tags: - { name: doctrine.event_listener, event: postConnect } + # store and retrieve sessions in and from database + App\Security\SessionHandler: + arguments: + - !service { class: PDO, factory: ['@database_connection', 'getWrappedConnection'] } + # ================================================================================ # FORMS # ================================================================================ diff --git a/src/EventSubscriber/RedirectToLocaleSubscriber.php b/src/EventSubscriber/RedirectToLocaleSubscriber.php index 9c81f7cd..b8bf1289 100644 --- a/src/EventSubscriber/RedirectToLocaleSubscriber.php +++ b/src/EventSubscriber/RedirectToLocaleSubscriber.php @@ -54,9 +54,6 @@ class RedirectToLocaleSubscriber implements EventSubscriberInterface $this->urlGenerator = $urlGenerator; $this->locales = explode('|', trim($locales)); - if (empty($this->locales)) { - throw new \UnexpectedValueException('The list of supported locales must not be empty.'); - } $this->defaultLocale = $defaultLocale ?: $this->locales[0]; if (!\in_array($this->defaultLocale, $this->locales)) { diff --git a/src/Migrations/Version20200524142042.php b/src/Migrations/Version20200524142042.php new file mode 100644 index 00000000..8e49cc00 --- /dev/null +++ b/src/Migrations/Version20200524142042.php @@ -0,0 +1,41 @@ +createTable('kimai2_sessions'); + $sessions->addColumn('id', 'string', ['length' => 128, 'notnull' => true]); + $sessions->addColumn('data', 'blob', ['length' => 65535, 'notnull' => true]); + $sessions->addColumn('time', 'integer', ['unsigned' => true, 'notnull' => true]); + $sessions->addColumn('lifetime', 'integer', ['unsigned' => true, 'notnull' => true]); + $sessions->setPrimaryKey(['id']); + } + + public function down(Schema $schema): void + { + $schema->dropTable('kimai2_sessions'); + } +} diff --git a/src/Plugin/PluginManager.php b/src/Plugin/PluginManager.php index db3ce184..5df38062 100644 --- a/src/Plugin/PluginManager.php +++ b/src/Plugin/PluginManager.php @@ -20,7 +20,6 @@ class PluginManager /** * @param PluginInterface[] $plugins - * @throws \Exception */ public function __construct(iterable $plugins) { @@ -31,7 +30,6 @@ class PluginManager /** * @param PluginInterface $plugin - * @throws \Exception */ public function addPlugin(PluginInterface $plugin) { diff --git a/src/Security/AclDecisionManager.php b/src/Security/AclDecisionManager.php index 355ca33f..7ef01a4b 100644 --- a/src/Security/AclDecisionManager.php +++ b/src/Security/AclDecisionManager.php @@ -17,11 +17,8 @@ class AclDecisionManager /** * @var AccessDecisionManagerInterface */ - protected $decisionManager; + private $decisionManager; - /** - * @param AccessDecisionManagerInterface $decisionManager - */ public function __construct(AccessDecisionManagerInterface $decisionManager) { $this->decisionManager = $decisionManager; diff --git a/src/Security/SessionHandler.php b/src/Security/SessionHandler.php new file mode 100644 index 00000000..35928c22 --- /dev/null +++ b/src/Security/SessionHandler.php @@ -0,0 +1,27 @@ + 'kimai2_sessions', + 'db_id_col' => 'id', + 'db_data_col' => 'data', + 'db_lifetime_col' => 'lifetime', + 'db_time_col' => 'time', + 'lock_mode' => PdoSessionHandler::LOCK_ADVISORY, + ]); + } +} diff --git a/tests/Command/CreateReleaseCommandTest.php b/tests/Command/CreateReleaseCommandTest.php new file mode 100644 index 00000000..8c14af8e --- /dev/null +++ b/tests/Command/CreateReleaseCommandTest.php @@ -0,0 +1,39 @@ +application = new Application($kernel); + $this->application->add(new CreateReleaseCommand(realpath(__DIR__ . '/../../'))); + } + + public function testCommandName() + { + $command = $this->application->find('kimai:create-release'); + self::assertInstanceOf(CreateReleaseCommand::class, $command); + } +} diff --git a/tests/Command/ImportCustomerCommandTest.php b/tests/Command/ImportCustomerCommandTest.php new file mode 100644 index 00000000..1fca83e3 --- /dev/null +++ b/tests/Command/ImportCustomerCommandTest.php @@ -0,0 +1,51 @@ +application = new Application($kernel); + + $customers = $this->createMock(CustomerRepository::class); + $projects = $this->createMock(ProjectRepository::class); + $teams = $this->createMock(TeamRepository::class); + $users = $this->createMock(UserRepository::class); + $configuration = $this->createMock(FormConfiguration::class); + + $this->application->add(new ImportCustomerCommand($customers, $projects, $teams, $users, $configuration)); + } + + public function testCommandName() + { + $command = $this->application->find('kimai:import:customer'); + self::assertInstanceOf(ImportCustomerCommand::class, $command); + } +} diff --git a/tests/Command/ImportTimesheetCommandTest.php b/tests/Command/ImportTimesheetCommandTest.php new file mode 100644 index 00000000..dc79f26a --- /dev/null +++ b/tests/Command/ImportTimesheetCommandTest.php @@ -0,0 +1,53 @@ +application = new Application($kernel); + + $customers = $this->createMock(CustomerRepository::class); + $projects = $this->createMock(ProjectRepository::class); + $activities = $this->createMock(ActivityRepository::class); + $users = $this->createMock(UserRepository::class); + $timesheets = $this->createMock(TimesheetRepository::class); + $configuration = $this->createMock(FormConfiguration::class); + + $this->application->add(new ImportTimesheetCommand($customers, $projects, $activities, $users, $timesheets, $configuration)); + } + + public function testCommandName() + { + $command = $this->application->find('kimai:import:timesheet'); + self::assertInstanceOf(ImportTimesheetCommand::class, $command); + } +} diff --git a/tests/Command/KimaiImporterCommandTest.php b/tests/Command/KimaiImporterCommandTest.php new file mode 100644 index 00000000..ebe51083 --- /dev/null +++ b/tests/Command/KimaiImporterCommandTest.php @@ -0,0 +1,47 @@ +application = new Application($kernel); + + $encoder = $this->createMock(UserPasswordEncoderInterface::class); + $registry = $this->createMock(ManagerRegistry::class); + $validator = $this->createMock(ValidatorInterface::class); + + $this->application->add(new KimaiImporterCommand($encoder, $registry, $validator)); + } + + public function testCommandName() + { + $command = $this->application->find('kimai:import-v1'); + self::assertInstanceOf(KimaiImporterCommand::class, $command); + } +} diff --git a/tests/Command/ReloadCommandTest.php b/tests/Command/ReloadCommandTest.php new file mode 100644 index 00000000..2e4f42f8 --- /dev/null +++ b/tests/Command/ReloadCommandTest.php @@ -0,0 +1,39 @@ +application = new Application($kernel); + $this->application->add(new ReloadCommand()); + } + + public function testCommandName() + { + $command = $this->application->find('kimai:reload'); + self::assertInstanceOf(ReloadCommand::class, $command); + } +} diff --git a/tests/Command/ResetCommandTest.php b/tests/Command/ResetCommandTest.php new file mode 100644 index 00000000..6e6655b8 --- /dev/null +++ b/tests/Command/ResetCommandTest.php @@ -0,0 +1,39 @@ +application = new Application($kernel); + $this->application->add(new ResetCommand()); + } + + public function testCommandName() + { + $command = $this->application->find('kimai:reset-dev'); + self::assertInstanceOf(ResetCommand::class, $command); + } +} diff --git a/tests/Controller/PluginControllerTest.php b/tests/Controller/PluginControllerTest.php index cd95e5d3..4644d315 100644 --- a/tests/Controller/PluginControllerTest.php +++ b/tests/Controller/PluginControllerTest.php @@ -11,7 +11,7 @@ namespace App\Tests\Controller; use App\Entity\User; use App\Plugin\PluginManager; -use App\Tests\Plugin\Fixtures\TestPlugin; +use App\Tests\Plugin\Fixtures\TestPlugin\TestPlugin; /** * @group integration diff --git a/tests/EventSubscriber/RedirectToLocaleSubscriberTest.php b/tests/EventSubscriber/RedirectToLocaleSubscriberTest.php new file mode 100644 index 00000000..da6e8a6a --- /dev/null +++ b/tests/EventSubscriber/RedirectToLocaleSubscriberTest.php @@ -0,0 +1,49 @@ +createMock(UrlGeneratorInterface::class); + $sut = new RedirectToLocaleSubscriber($urlGenerator, 'de|en', 'en'); + + self::assertEquals([KernelEvents::REQUEST => ['onKernelRequest']], RedirectToLocaleSubscriber::getSubscribedEvents()); + + $request = $this->createMock(Request::class); + $request->expects($this->once())->method('getPathInfo')->willReturn('/de'); + + $event = $this->createMock(RequestEvent::class); + $event->expects($this->once())->method('getRequest')->willReturn($request); + $event->expects($this->never())->method('setResponse'); + + $sut->onKernelRequest($event); + } + + public function testConstructWithUnknownDefaultLocale() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('The default locale ("en") must be one of "de|it".'); + + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $sut = new RedirectToLocaleSubscriber($urlGenerator, 'de|it', 'en'); + } +} diff --git a/tests/Plugin/Fixtures/TestPlugin.php b/tests/Plugin/Fixtures/TestPlugin/TestPlugin.php similarity index 91% rename from tests/Plugin/Fixtures/TestPlugin.php rename to tests/Plugin/Fixtures/TestPlugin/TestPlugin.php index bd13b279..ede822ac 100644 --- a/tests/Plugin/Fixtures/TestPlugin.php +++ b/tests/Plugin/Fixtures/TestPlugin/TestPlugin.php @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -namespace App\Tests\Plugin\Fixtures; +namespace App\Tests\Plugin\Fixtures\TestPlugin; use App\Plugin\PluginInterface; diff --git a/tests/Plugin/Fixtures/composer.json b/tests/Plugin/Fixtures/TestPlugin/composer.json similarity index 100% rename from tests/Plugin/Fixtures/composer.json rename to tests/Plugin/Fixtures/TestPlugin/composer.json diff --git a/tests/Plugin/Fixtures/TestPlugin2/TestPlugin2.php b/tests/Plugin/Fixtures/TestPlugin2/TestPlugin2.php new file mode 100644 index 00000000..babfbc14 --- /dev/null +++ b/tests/Plugin/Fixtures/TestPlugin2/TestPlugin2.php @@ -0,0 +1,31 @@ +getMockBuilder(PluginInterface::class) - ->onlyMethods(['getName', 'getPath']) - ->getMock(); - - $plugin->method('getName')->willReturn('foo'); - $plugin->method('getPath')->willReturn('bar'); + $plugin = $this->createMock(PluginInterface::class); + $plugin->expects($this->any())->method('getName')->willReturn('foo'); + $plugin->expects($this->any())->method('getPath')->willReturn('bar'); $sut->addPlugin(new TestPlugin()); $sut->addPlugin($plugin); + $sut->addPlugin(new TestPlugin2()); $sut->addPlugin(new TestPlugin()); // make sure a plugin with the same name is not added twice, the first one wins! diff --git a/tests/Security/AclDecisionManagerTest.php b/tests/Security/AclDecisionManagerTest.php new file mode 100644 index 00000000..012bb016 --- /dev/null +++ b/tests/Security/AclDecisionManagerTest.php @@ -0,0 +1,45 @@ +createMock(AccessDecisionManagerInterface::class); + $manager->expects($this->once())->method('decide')->willReturn(true); + + $token = $this->createMock(TokenInterface::class); + + $sut = new AclDecisionManager($manager); + $result = $sut->isFullyAuthenticated($token); + self::assertTrue($result); + } + + public function testIsNotFullyAuthenticated() + { + $manager = $this->createMock(AccessDecisionManagerInterface::class); + $manager->expects($this->once())->method('decide')->willReturn(false); + + $token = $this->createMock(TokenInterface::class); + + $sut = new AclDecisionManager($manager); + $result = $sut->isFullyAuthenticated($token); + self::assertFalse($result); + } +} diff --git a/tests/Security/SessionHandlerTest.php b/tests/Security/SessionHandlerTest.php new file mode 100644 index 00000000..7055f665 --- /dev/null +++ b/tests/Security/SessionHandlerTest.php @@ -0,0 +1,26 @@ +isSessionExpired()); + } +} diff --git a/var/data/kimai_test.sqlite b/var/data/kimai_test.sqlite index f1c8fb91..3babc73b 100644 Binary files a/var/data/kimai_test.sqlite and b/var/data/kimai_test.sqlite differ