CVE-2021-3976: Cross-Site Request Forgery (CSRF) (CWE-352)
value: kimai2 is vulnerable to Cross-Site Request Forgery (CSRF)
CWE description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
CVSS v3 base score: 6.5
Published: 2021-11-19T11:15Z
Repository: https://github.com/kevinpapst/kimai2
Language: PHP
Files changed: src/Constants.php, src/Controller/ProjectController.php, src/Controller/TeamController.php, src/Controller/TimesheetAbstractController.php, src/Controller/TimesheetController.php, src/Controller/TimesheetTeamController.php, src/EventSubscriber/Actions/AbstractTimesheetSubscriber.php, src/EventSubscriber/Actions/ProjectSubscriber.php, src/EventSubscriber/Actions/TeamSubscriber.php, src/Migrations/Version20180701120000.php, src/Migrations/Version20180715160326.php, src/Migrations/Version20180730044139.php, src/Migrations/Version20180924111853.php, src/Migrations/Version20181031220003.php, src/Migrations/Version20190305152308.php, src/Migrations/Version20210802152814.php, src/Migrations/Version20211008092010.php, templates/form/kimai-theme.html.twig, templates/project/actions.html.twig, templates/team/actions.html.twig, templates/timesheet-team/actions.html.twig, templates/timesheet/actions.html.twig, tests/Controller/ControllerBaseTest.php, tests/Controller/DoctorControllerTest.php, tests/Controller/ProjectControllerTest.php, tests/Controller/TeamControllerTest.php, tests/Controller/TimesheetControllerTest.php, tests/Controller/TimesheetTeamControllerTest.php, translations/about.ja.xlf, translations/flashmessages.el.xlf, translations/flashmessages.fr.xlf, translations/flashmessages.he.xlf, translations/flashmessages.pt.xlf, translations/flashmessages.pt_BR.xlf, translations/flashmessages.tr.xlf, translations/messages.de.xlf, translations/messages.en.xlf
Commit date: 2021-11-18 12:33:13 +0100
Commit message: version 1.16.2 (#2942)

* bump version
* include calendar week in week chooser
* table names in SQL
* show save flash message
* prevent migration warning
* drop default value to prevent error when server version is not set
* csrf token for duplicate actions
* updated translations
Diff: diff --git a/src/Constants.php b/src/Constants.php
index 1deece0dc..379a31d4d 100644
--- a/src/Constants.php
+++ b/src/Constants.php
@@ -15,15 +15,15 @@ namespace App;
 class Constants
 {
     /**
      * The current release version
      */
-    public const VERSION = '1.16.0';
+    public const VERSION = '1.16.2';
     /**
      * The current release: major * 10000 + minor * 100 + patch
      */
-    public const VERSION_ID = 11600;
+    public const VERSION_ID = 11602;
     /**
      * The current release status, either "stable" or "dev"
      */
     public const STATUS = 'prod';
     /**
diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php
index c2800c6ae..50f35d872 100644
--- a/src/Controller/ProjectController.php
+++ b/src/Controller/ProjectController.php
@@ -419,17 +419,27 @@ final class ProjectController extends AbstractController
             'form' => $editForm->createView()
         ]);
     }
 
     /**
-     * @Route(path="/{id}/duplicate", name="admin_project_duplicate", methods={"GET", "POST"})
+     * @Route(path="/{id}/duplicate/{token}", name="admin_project_duplicate", methods={"GET", "POST"})
      * @Security("is_granted('edit', project)")
      */
-    public function duplicateAction(Project $project, Request $request, ProjectDuplicationService $projectDuplicationService)
+    public function duplicateAction(Project $project, string $token, ProjectDuplicationService $projectDuplicationService, CsrfTokenManagerInterface $csrfTokenManager)
     {
+        if (!$csrfTokenManager->isTokenValid(new CsrfToken('project.duplicate', $token))) {
+            $this->flashError('action.csrf.error');
+
+            return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
+        }
+
+        $csrfTokenManager->refreshToken($token);
+
         $newProject = $projectDuplicationService->duplicate($project, $project->getName() . ' [COPY]');
 
+        $this->flashSuccess('action.update.success');
+
         return $this->redirectToRoute('project_details', ['id' => $newProject->getId()]);
     }
 
     /**
      * @Route(path="/{id}/delete", name="admin_project_delete", methods={"GET", "POST"})
diff --git a/src/Controller/TeamController.php b/src/Controller/TeamController.php
index 98681c654..3181f7539 100644
--- a/src/Controller/TeamController.php
+++ b/src/Controller/TeamController.php
@@ -20,10 +20,12 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
 use Symfony\Component\Form\FormInterface;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
+use Symfony\Component\Security\Csrf\CsrfToken;
+use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
 
 /**
  * @Route(path="/admin/teams")
  * @Security("is_granted('view_team')")
  */
@@ -79,15 +81,23 @@ final class TeamController extends AbstractController
     {
         return $this->renderEditScreen(new Team(), $request);
     }
 
     /**
-     * @Route(path="/{id}/duplicate", name="team_duplicate", methods={"GET", "POST"})
+     * @Route(path="/{id}/duplicate/{token}", name="team_duplicate", methods={"GET", "POST"})
      * @Security("is_granted('edit', team) and is_granted('create_team')")
      */
-    public function duplicateTeam(Team $team, Request $request)
+    public function duplicateTeam(Team $team, string $token, CsrfTokenManagerInterface $csrfTokenManager)
     {
+        if (!$csrfTokenManager->isTokenValid(new CsrfToken('team.duplicate', $token))) {
+            $this->flashError('action.csrf.error');
+
+            return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
+        }
+
+        $csrfTokenManager->refreshToken($token);
+
         $newTeam = clone $team;
         $newTeam->setName($team->getName() . ' [COPY]');
 
         try {
             $this->repository->saveTeam($newTeam);
diff --git a/src/Controller/TimesheetAbstractController.php b/src/Controller/TimesheetAbstractController.php
index a536a1f27..2677a586c 100644
--- a/src/Controller/TimesheetAbstractController.php
+++ b/src/Controller/TimesheetAbstractController.php
@@ -209,18 +209,18 @@ abstract class TimesheetAbstractController extends AbstractController
             'timesheet' => $entry,
             'form' => $createForm->createView(),
         ]);
     }
 
-    protected function duplicate(Timesheet $timesheet, Request $request, string $renderTemplate): Response
+    protected function duplicate(Timesheet $timesheet, Request $request, string $renderTemplate, string $token): Response
     {
         $copyTimesheet = clone $timesheet;
 
         $event = new TimesheetMetaDefinitionEvent($copyTimesheet);
         $this->dispatcher->dispatch($event);
 
-        $form = $this->getDuplicateForm($copyTimesheet, $timesheet);
+        $form = $this->getDuplicateForm($copyTimesheet, $timesheet, $token);
         $form->handleRequest($request);
 
         if ($form->isSubmitted() && $form->isValid()) {
             try {
                 $this->dispatcher->dispatch(new TimesheetDuplicatePreEvent($copyTimesheet, $timesheet));
@@ -610,9 +610,9 @@ abstract class TimesheetAbstractController extends AbstractController
         $query->setName($this->getQueryNamePrefix() . $suffix);
 
         return $query;
     }
 
-    abstract protected function getDuplicateForm(Timesheet $entry, Timesheet $original): FormInterface;
+    abstract protected function getDuplicateForm(Timesheet $entry, Timesheet $original, string $token): FormInterface;
 
     abstract protected function getCreateForm(Timesheet $entry): FormInterface;
 }
diff --git a/src/Controller/TimesheetController.php b/src/Controller/TimesheetController.php
index df14b2ccd..4f5bbad48 100644
--- a/src/Controller/TimesheetController.php
+++ b/src/Controller/TimesheetController.php
@@ -19,10 +19,12 @@ use App\Repository\TagRepository;
 use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
 use Symfony\Component\Form\FormInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
+use Symfony\Component\Security\Csrf\CsrfToken;
+use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
 
 /**
  * @Route(path="/timesheet")
  * @Security("is_granted('view_own_timesheet')")
  */
@@ -58,16 +60,24 @@ class TimesheetController extends TimesheetAbstractController
     {
         return $this->edit($entry, $request, 'timesheet/edit.html.twig');
     }
 
     /**
-     * @Route(path="/{id}/duplicate", name="timesheet_duplicate", methods={"GET", "POST"})
+     * @Route(path="/{id}/duplicate/{token}", name="timesheet_duplicate", methods={"GET", "POST"})
      * @Security("is_granted('duplicate', entry)")
      */
-    public function duplicateAction(Timesheet $entry, Request $request): Response
+    public function duplicateAction(Timesheet $entry, Request $request, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
     {
-        return $this->duplicate($entry, $request, 'timesheet/edit.html.twig');
+        if (!$csrfTokenManager->isTokenValid(new CsrfToken('timesheet.duplicate', $token))) {
+            $this->flashError('action.csrf.error');
+
+            return $this->redirectToRoute('timesheet');
+        }
+
+        $csrfTokenManager->refreshToken($token);
+
+        return $this->duplicate($entry, $request, 'timesheet/edit.html.twig', $token);
     }
 
     /**
      * @Route(path="/multi-update", name="timesheet_multi_update", methods={"POST"})
      * @Security("is_granted('edit_own_timesheet')")
@@ -98,10 +108,10 @@ class TimesheetController extends TimesheetAbstractController
     protected function getCreateForm(Timesheet $entry): FormInterface
     {
         return $this->generateCreateForm($entry, TimesheetEditForm::class, $this->generateUrl('timesheet_create'));
     }
 
-    protected function getDuplicateForm(Timesheet $entry, Timesheet $original): FormInterface
+    protected function getDuplicateForm(Timesheet $entry, Timesheet $original, string $token): FormInterface
     {
-        return $this->generateCreateForm($entry, TimesheetEditForm::class, $this->generateUrl('timesheet_duplicate', ['id' => $original->getId()]));
+        return $this->generateCreateForm($entry, TimesheetEditForm::class, $this->generateUrl('timesheet_duplicate', ['id' => $original->getId(), 'token' => $token]));
     }
 }
diff --git a/src/Controller/TimesheetTeamController.php b/src/Controller/TimesheetTeamController.php
index 9cd147266..3a073315a 100644
--- a/src/Controller/TimesheetTeamController.php
+++ b/src/Controller/TimesheetTeamController.php
@@ -26,10 +26,12 @@ use Doctrine\Common\Collections\ArrayCollection;
 use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
 use Symfony\Component\Form\FormInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
+use Symfony\Component\Security\Csrf\CsrfToken;
+use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
 
 /**
  * @Route(path="/team/timesheet")
  * @Security("is_granted('view_other_timesheet')")
  */
@@ -69,16 +71,24 @@ class TimesheetTeamController extends TimesheetAbstractController
     {
         return $this->edit($entry, $request, 'timesheet-team/edit.html.twig');
     }
 
     /**
-     * @Route(path="/{id}/duplicate", name="admin_timesheet_duplicate", methods={"GET", "POST"})
+     * @Route(path="/{id}/duplicate/{token}", name="admin_timesheet_duplicate", methods={"GET", "POST"})
      * @Security("is_granted('duplicate', entry)")
      */
-    public function duplicateAction(Timesheet $entry, Request $request): Response
+    public function duplicateAction(Timesheet $entry, Request $request, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
     {
-        return $this->duplicate($entry, $request, 'timesheet-team/edit.html.twig');
+        if (!$csrfTokenManager->isTokenValid(new CsrfToken('admin_timesheet.duplicate', $token))) {
+            $this->flashError('action.csrf.error');
+
+            return $this->redirectToRoute('admin_timesheet');
+        }
+
+        $csrfTokenManager->refreshToken($token);
+
+        return $this->duplicate($entry, $request, 'timesheet-team/edit.html.twig', $token);
     }
 
     /**
      * @Route(path="/create", name="admin_timesheet_create", methods={"GET", "POST"})
      * @Security("is_granted('create_other_timesheet')")
@@ -193,13 +203,13 @@ class TimesheetTeamController extends TimesheetAbstractController
     protected function getCreateForm(Timesheet $entry): FormInterface
     {
         return $this->generateCreateForm($entry, TimesheetAdminEditForm::class, $this->generateUrl('admin_timesheet_create'));
     }
 
-    protected function getDuplicateForm(Timesheet $entry, Timesheet $original): FormInterface
+    protected function getDuplicateForm(Timesheet $entry, Timesheet $original, string $token): FormInterface
     {
-        return $this->generateCreateForm($entry, TimesheetAdminEditForm::class, $this->generateUrl('admin_timesheet_duplicate', ['id' => $original->getId()]));
+        return $this->generateCreateForm($entry, TimesheetAdminEditForm::class, $this->generateUrl('admin_timesheet_duplicate', ['id' => $original->getId(), 'token' => $token]));
     }
 
     protected function getPermissionEditExport(): string
     {
         return 'edit_export_other_timesheet';
diff --git a/src/EventSubscriber/Actions/AbstractTimesheetSubscriber.php b/src/EventSubscriber/Actions/AbstractTimesheetSubscriber.php
index 24b96ed80..47eb82025 100644
--- a/src/EventSubscriber/Actions/AbstractTimesheetSubscriber.php
+++ b/src/EventSubscriber/Actions/AbstractTimesheetSubscriber.php
@@ -37,11 +37,11 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
                 $event->addAction('edit', ['url' => $this->path($routeEdit, ['id' => $timesheet->getId()]), 'class' => $class]);
             }
 
             if ($this->isGranted('duplicate', $timesheet)) {
                 $class = $event->isView('edit') ? '' : 'modal-ajax-form';
-                $event->addAction('copy', ['url' => $this->path($routeDuplicate, ['id' => $timesheet->getId()]), 'class' => $class]);
+                $event->addAction('copy', ['url' => $this->path($routeDuplicate, ['id' => $timesheet->getId(), 'token' => $payload['token']]), 'class' => $class]);
             }
 
             if ($event->countActions() > 0) {
                 $event->addDivider();
             }
diff --git a/src/EventSubscriber/Actions/ProjectSubscriber.php b/src/EventSubscriber/Actions/ProjectSubscriber.php
index ccabf0efc..da6771f1a 100644
--- a/src/EventSubscriber/Actions/ProjectSubscriber.php
+++ b/src/EventSubscriber/Actions/ProjectSubscriber.php
@@ -71,11 +71,11 @@ class ProjectSubscriber extends AbstractActionsSubscriber
         }
 
         if ($this->isGranted('edit', $project) && $this->isGranted('create_project')) {
             $event->addAction(
                 'copy',
-                ['url' => $this->path('admin_project_duplicate', ['id' => $project->getId()])]
+                ['url' => $this->path('admin_project_duplicate', ['id' => $project->getId(), 'token' => $payload['token']])]
             );
         }
 
         if (($event->isIndexView() || $event->isView('customer_details')) && $this->isGranted('delete', $project)) {
             $event->addDelete($this->path('admin_project_delete', ['id' => $project->getId()]));
diff --git a/src/EventSubscriber/Actions/TeamSubscriber.php b/src/EventSubscriber/Actions/TeamSubscriber.php
index 1a0a7ac74..9c2e468b6 100644
--- a/src/EventSubscriber/Actions/TeamSubscriber.php
+++ b/src/EventSubscriber/Actions/TeamSubscriber.php
@@ -32,11 +32,11 @@ class TeamSubscriber extends AbstractActionsSubscriber
 
         if ($this->isGranted('edit', $team)) {
             $event->addAction('edit', ['url' => $this->path('admin_team_edit', ['id' => $team->getId()])]);
 
             if ($this->isGranted('create_team')) {
-                $event->addAction('copy', ['url' => $this->path('team_duplicate', ['id' => $team->getId()])]);
+                $event->addAction('copy', ['url' => $this->path('team_duplicate', ['id' => $team->getId(), 'token' => $payload['token']])]);
             }
         }
 
         if ($event->isIndexView() && $this->isGranted('delete', $team)) {
             $event->addAction('trash', [
diff --git a/src/Migrations/Version20180701120000.php b/src/Migrations/Version20180701120000.php
index 6ed541281..033649b39 100644
--- a/src/Migrations/Version20180701120000.php
+++ b/src/Migrations/Version20180701120000.php
@@ -20,30 +20,22 @@ use Doctrine\DBAL\Schema\Schema;
  */
 final class Version20180701120000 extends AbstractMigration
 {
     public function up(Schema $schema): void
     {
-        $users = 'kimai2_users';
-        $userPreferences = 'kimai2_user_preferences';
-        $customers = 'kimai2_customers';
-        $projects = 'kimai2_projects';
-        $activities = 'kimai2_activities';
-        $timesheets = 'kimai2_timesheet';
-        $invoiceTemplates = 'kimai2_invoice_templates';
-
-        $this->addSql('CREATE TABLE ' . $users . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(60) NOT NULL, mail VARCHAR(160) NOT NULL, password VARCHAR(254) DEFAULT NULL, alias VARCHAR(60) DEFAULT NULL, active TINYINT(1) NOT NULL, registration_date DATETIME DEFAULT NULL, title VARCHAR(50) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', UNIQUE INDEX UNIQ_B9AC5BCE5E237E06 (name), UNIQUE INDEX UNIQ_B9AC5BCE5126AC48 (mail), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
-        $this->addSql('CREATE TABLE ' . $userPreferences . ' (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, name VARCHAR(50) NOT NULL, value VARCHAR(255) DEFAULT NULL, INDEX IDX_8D08F631A76ED395 (user_id), UNIQUE INDEX UNIQ_8D08F631A76ED3955E237E06 (user_id, name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
-        $this->addSql('CREATE TABLE ' . $customers . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(150) NOT NULL, number VARCHAR(50) DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, company VARCHAR(255) DEFAULT NULL, contact VARCHAR(255) DEFAULT NULL, address TEXT DEFAULT NULL, country VARCHAR(2) NOT NULL, currency VARCHAR(3) NOT NULL, phone VARCHAR(255) DEFAULT NULL, fax VARCHAR(255) DEFAULT NULL, mobile VARCHAR(255) DEFAULT NULL, mail VARCHAR(255) DEFAULT NULL, homepage VARCHAR(255) DEFAULT NULL, timezone VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
-        $this->addSql('CREATE TABLE ' . $projects . ' (id INT AUTO_INCREMENT NOT NULL, customer_id INT DEFAULT NULL, name VARCHAR(150) NOT NULL, order_number TINYTEXT DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, budget NUMERIC(10, 2) NOT NULL, INDEX IDX_407F12069395C3F3 (customer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
-        $this->addSql('CREATE TABLE ' . $activities . ' (id INT AUTO_INCREMENT NOT NULL, project_id INT DEFAULT NULL, name VARCHAR(150) NOT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, INDEX IDX_8811FE1C166D1F9C (project_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
-        $this->addSql('CREATE TABLE ' . $timesheets . ' (id INT AUTO_INCREMENT NOT NULL, user INT DEFAULT NULL, activity_id INT DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INT DEFAULT NULL, description TEXT DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, INDEX IDX_4F60C6B18D93D649 (user), INDEX IDX_4F60C6B181C06096 (activity_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
-        $this->addSql('CREATE TABLE ' . $invoiceTemplates . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(60) NOT NULL, title VARCHAR(255) NOT NULL, company VARCHAR(255) NOT NULL, address TEXT DEFAULT NULL, due_days INT NOT NULL, vat INT DEFAULT NULL, calculator VARCHAR(20) NOT NULL, number_generator VARCHAR(20) NOT NULL, renderer VARCHAR(20) NOT NULL, payment_terms TEXT DEFAULT NULL, UNIQUE INDEX UNIQ_1626CFE95E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
-        $this->addSql('ALTER TABLE ' . $userPreferences . ' ADD CONSTRAINT FK_8D08F631A76ED395 FOREIGN KEY (user_id) REFERENCES ' . $users . ' (id) ON DELETE CASCADE');
-        $this->addSql('ALTER TABLE ' . $projects . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE');
-        $this->addSql('ALTER TABLE ' . $activities . ' ADD CONSTRAINT FK_8811FE1C166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE');
-        $this->addSql('ALTER TABLE ' . $timesheets . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id)');
-        $this->addSql('ALTER TABLE ' . $timesheets . ' ADD CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE');
+        $this->addSql('CREATE TABLE kimai2_users (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(60) NOT NULL, mail VARCHAR(160) NOT NULL, password VARCHAR(254) DEFAULT NULL, alias VARCHAR(60) DEFAULT NULL, active TINYINT(1) NOT NULL, registration_date DATETIME DEFAULT NULL, title VARCHAR(50) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', UNIQUE INDEX UNIQ_B9AC5BCE5E237E06 (name), UNIQUE INDEX UNIQ_B9AC5BCE5126AC48 (mail), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
+        $this->addSql('CREATE TABLE kimai2_user_preferences (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, name VARCHAR(50) NOT NULL, value VARCHAR(255) DEFAULT NULL, INDEX IDX_8D08F631A76ED395 (user_id), UNIQUE INDEX UNIQ_8D08F631A76ED3955E237E06 (user_id, name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
+        $this->addSql('CREATE TABLE kimai2_customers (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(150) NOT NULL, number VARCHAR(50) DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, company VARCHAR(255) DEFAULT NULL, contact VARCHAR(255) DEFAULT NULL, address TEXT DEFAULT NULL, country VARCHAR(2) NOT NULL, currency VARCHAR(3) NOT NULL, phone VARCHAR(255) DEFAULT NULL, fax VARCHAR(255) DEFAULT NULL, mobile VARCHAR(255) DEFAULT NULL, mail VARCHAR(255) DEFAULT NULL, homepage VARCHAR(255) DEFAULT NULL, timezone VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
+        $this->addSql('CREATE TABLE kimai2_projects (id INT AUTO_INCREMENT NOT NULL, customer_id INT DEFAULT NULL, name VARCHAR(150) NOT NULL, order_number TINYTEXT DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, budget NUMERIC(10, 2) NOT NULL, INDEX IDX_407F12069395C3F3 (customer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
+        $this->addSql('CREATE TABLE kimai2_activities (id INT AUTO_INCREMENT NOT NULL, project_id INT DEFAULT NULL, name VARCHAR(150) NOT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, INDEX IDX_8811FE1C166D1F9C (project_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
+        $this->addSql('CREATE TABLE kimai2_timesheet (id INT AUTO_INCREMENT NOT NULL, user INT DEFAULT NULL, activity_id INT DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INT DEFAULT NULL, description TEXT DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, INDEX IDX_4F60C6B18D93D649 (user), INDEX IDX_4F60C6B181C06096 (activity_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
+        $this->addSql('CREATE TABLE kimai2_invoice_templates (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(60) NOT NULL, title VARCHAR(255) NOT NULL, company VARCHAR(255) NOT NULL, address TEXT DEFAULT NULL, due_days INT NOT NULL, vat INT DEFAULT NULL, calculator VARCHAR(20) NOT NULL, number_generator VARCHAR(20) NOT NULL, renderer VARCHAR(20) NOT NULL, payment_terms TEXT DEFAULT NULL, UNIQUE INDEX UNIQ_1626CFE95E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
+        $this->addSql('ALTER TABLE kimai2_user_preferences ADD CONSTRAINT FK_8D08F631A76ED395 FOREIGN KEY (user_id) REFERENCES kimai2_users (id) ON DELETE CASCADE');
+        $this->addSql('ALTER TABLE kimai2_projects ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES kimai2_customers (id) ON DELETE CASCADE');
+        $this->addSql('ALTER TABLE kimai2_activities ADD CONSTRAINT FK_8811FE1C166D1F9C FOREIGN KEY (project_id) REFERENCES kimai2_projects (id) ON DELETE CASCADE');
+        $this->addSql('ALTER TABLE kimai2_timesheet ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES kimai2_users (id)');
+        $this->addSql('ALTER TABLE kimai2_timesheet ADD CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES kimai2_activities (id) ON DELETE CASCADE');
     }
 
     public function down(Schema $schema): void
     {
         $schema->dropTable('kimai2_invoice_templates');
diff --git a/src/Migrations/Version20180715160326.php b/src/Migrations/Version20180715160326.php
index 08012ca2e..dd485009f 100644
--- a/src/Migrations/Version20180715160326.php
+++ b/src/Migrations/Version20180715160326.php
@@ -35,64 +35,60 @@ final class Version20180715160326 extends AbstractMigration
      * @throws \Doctrine\DBAL\DBALException
      * @throws \Doctrine\DBAL\Schema\SchemaException
      */
     public function up(Schema $schema): void
     {
-        $users = 'kimai2_users';
-
         // delete all existing indexes
-        $indexesOld = $schema->getTable($users)->getIndexes();
+        $indexesOld = $schema->getTable('kimai2_users')->getIndexes();
         foreach ($indexesOld as $index) {
             if (\in_array('name', $index->getColumns()) || \in_array('mail', $index->getColumns())) {
                 $this->indexesOld[] = $index;
-                $this->addSql('DROP INDEX ' . $index->getName() . ' ON ' . $users);
+                $this->addSql('DROP INDEX ' . $index->getName() . ' ON kimai2_users');
             }
         }
 
-        $this->addSql('ALTER TABLE ' . $users . ' CHANGE name username VARCHAR(180) NOT NULL, ADD username_canonical VARCHAR(180) NOT NULL, CHANGE mail email VARCHAR(180) NOT NULL, ADD email_canonical VARCHAR(180) NOT NULL, ADD salt VARCHAR(255) DEFAULT NULL, ADD last_login DATETIME DEFAULT NULL, ADD confirmation_token VARCHAR(180) DEFAULT NULL, ADD password_requested_at DATETIME DEFAULT NULL, CHANGE password password VARCHAR(255) NOT NULL, CHANGE alias alias VARCHAR(60) DEFAULT NULL, CHANGE registration_date registration_date DATETIME DEFAULT NULL, CHANGE title title VARCHAR(50) DEFAULT NULL, CHANGE avatar avatar VARCHAR(255) DEFAULT NULL, CHANGE roles roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', CHANGE active enabled TINYINT(1) NOT NULL');
-        $this->addSql('UPDATE ' . $users . ' set username_canonical = username');
-        $this->addSql('UPDATE ' . $users . ' set email_canonical = email');
-
-        $this->addSql('UPDATE ' . $users . ' SET roles = \'a:1:{i:0;s:16:"ROLE_SUPER_ADMIN";}\' WHERE roles LIKE \'%ROLE_SUPER_ADMIN%\'');
-        $this->addSql('UPDATE ' . $users . ' SET roles = \'a:1:{i:0;s:10:"ROLE_ADMIN";}\' WHERE roles LIKE \'%ROLE_ADMIN%\'');
-        $this->addSql('UPDATE ' . $users . ' SET roles = \'a:1:{i:0;s:13:"ROLE_TEAMLEAD";}\' WHERE roles LIKE \'%ROLE_TEAMLEAD%\'');
-        $this->addSql('UPDATE ' . $users . ' SET roles = \'a:0:{}\' WHERE roles LIKE \'%ROLE_USER%\'');
-        $this->addSql('UPDATE ' . $users . ' SET roles = \'a:1:{i:0;s:13:"ROLE_CUSTOMER";}\' WHERE roles LIKE \'%ROLE_CUSTOMER%\'');
-
-        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE92FC23A8 ON ' . $users . ' (username_canonical)');
-        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEA0D96FBF ON ' . $users . ' (email_canonical)');
-        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEC05FB297 ON ' . $users . ' (confirmation_token)');
-        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEF85E0677 ON ' . $users . ' (username)');
-        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEE7927C74 ON ' . $users . ' (email)');
+        $this->addSql('ALTER TABLE kimai2_users CHANGE name username VARCHAR(180) NOT NULL, ADD username_canonical VARCHAR(180) NOT NULL, CHANGE mail email VARCHAR(180) NOT NULL, ADD email_canonical VARCHAR(180) NOT NULL, ADD salt VARCHAR(255) DEFAULT NULL, ADD last_login DATETIME DEFAULT NULL, ADD confirmation_token VARCHAR(180) DEFAULT NULL, ADD password_requested_at DATETIME DEFAULT NULL, CHANGE password password VARCHAR(255) NOT NULL, CHANGE alias alias VARCHAR(60) DEFAULT NULL, CHANGE registration_date registration_date DATETIME DEFAULT NULL, CHANGE title title VARCHAR(50) DEFAULT NULL, CHANGE avatar avatar VARCHAR(255) DEFAULT NULL, CHANGE roles roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', CHANGE active enabled TINYINT(1) NOT NULL');
+        $this->addSql('UPDATE kimai2_users set username_canonical = username');
+        $this->addSql('UPDATE kimai2_users set email_canonical = email');
+
+        $this->addSql('UPDATE kimai2_users SET roles = \'a:1:{i:0;s:16:"ROLE_SUPER_ADMIN";}\' WHERE roles LIKE \'%ROLE_SUPER_ADMIN%\'');
+        $this->addSql('UPDATE kimai2_users SET roles = \'a:1:{i:0;s:10:"ROLE_ADMIN";}\' WHERE roles LIKE \'%ROLE_ADMIN%\'');
+        $this->addSql('UPDATE kimai2_users SET roles = \'a:1:{i:0;s:13:"ROLE_TEAMLEAD";}\' WHERE roles LIKE \'%ROLE_TEAMLEAD%\'');
+        $this->addSql('UPDATE kimai2_users SET roles = \'a:0:{}\' WHERE roles LIKE \'%ROLE_USER%\'');
+        $this->addSql('UPDATE kimai2_users SET roles = \'a:1:{i:0;s:13:"ROLE_CUSTOMER";}\' WHERE roles LIKE \'%ROLE_CUSTOMER%\'');
+
+        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE92FC23A8 ON kimai2_users (username_canonical)');
+        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEA0D96FBF ON kimai2_users (email_canonical)');
+        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEC05FB297 ON kimai2_users (confirmation_token)');
+        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEF85E0677 ON kimai2_users (username)');
+        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEE7927C74 ON kimai2_users (email)');
     }
 
     /**
      * @param Schema $schema
      * @throws \Doctrine\DBAL\DBALException
      * @throws \Doctrine\DBAL\Schema\SchemaException
      */
     public function down(Schema $schema): void
     {
-        $users = 'kimai2_users';
-
         $indexToDelete = ['UNIQ_B9AC5BCE92FC23A8', 'UNIQ_B9AC5BCEA0D96FBF', 'UNIQ_B9AC5BCEC05FB297', 'UNIQ_B9AC5BCEF85E0677', 'UNIQ_B9AC5BCEE7927C74'];
         foreach ($indexToDelete as $indexName) {
-            $this->addSql('DROP INDEX ' . $indexName . ' ON ' . $users);
+            $this->addSql('DROP INDEX ' . $indexName . ' ON kimai2_users');
         }
 
-        $this->addSql('ALTER TABLE ' . $users . ' CHANGE username name VARCHAR(60) NOT NULL COLLATE utf8mb4_unicode_ci, CHANGE email mail VARCHAR(160) NOT NULL COLLATE utf8mb4_unicode_ci, DROP username_canonical, DROP email_canonical, DROP salt, DROP last_login, DROP confirmation_token, DROP password_requested_at, CHANGE password password VARCHAR(254) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE roles roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', CHANGE alias alias VARCHAR(60) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE registration_date registration_date DATETIME DEFAULT NULL, CHANGE title title VARCHAR(50) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE avatar avatar VARCHAR(255) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE enabled active TINYINT(1) NOT NULL');
+        $this->addSql('ALTER TABLE kimai2_users CHANGE username name VARCHAR(60) NOT NULL COLLATE utf8mb4_unicode_ci, CHANGE email mail VARCHAR(160) NOT NULL COLLATE utf8mb4_unicode_ci, DROP username_canonical, DROP email_canonical, DROP salt, DROP last_login, DROP confirmation_token, DROP password_requested_at, CHANGE password password VARCHAR(254) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE roles roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', CHANGE alias alias VARCHAR(60) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE registration_date registration_date DATETIME DEFAULT NULL, CHANGE title title VARCHAR(50) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE avatar avatar VARCHAR(255) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE enabled active TINYINT(1) NOT NULL');
 
-        $this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_SUPER_ADMIN"]\' WHERE roles LIKE \'%ROLE_SUPER_ADMIN%\'');
-        $this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_ADMIN"]\' WHERE roles LIKE \'%ROLE_ADMIN%\'');
-        $this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_TEAMLEAD"]\' WHERE roles LIKE \'%ROLE_TEAMLEAD%\'');
-        $this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_USER"]\' WHERE roles LIKE \'%ROLE_USER%\'');
-        $this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_CUSTOMER"]\' WHERE roles LIKE \'%ROLE_CUSTOMER%\'');
+        $this->addSql('UPDATE kimai2_users SET roles = \'["ROLE_SUPER_ADMIN"]\' WHERE roles LIKE \'%ROLE_SUPER_ADMIN%\'');
+        $this->addSql('UPDATE kimai2_users SET roles = \'["ROLE_ADMIN"]\' WHERE roles LIKE \'%ROLE_ADMIN%\'');
+        $this->addSql('UPDATE kimai2_users SET roles = \'["ROLE_TEAMLEAD"]\' WHERE roles LIKE \'%ROLE_TEAMLEAD%\'');
+        $this->addSql('UPDATE kimai2_users SET roles = \'["ROLE_USER"]\' WHERE roles LIKE \'%ROLE_USER%\'');
+        $this->addSql('UPDATE kimai2_users SET roles = \'["ROLE_CUSTOMER"]\' WHERE roles LIKE \'%ROLE_CUSTOMER%\'');
 
-        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE5E237E06 ON ' . $users . ' (name)');
-        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE5126AC48 ON ' . $users . ' (mail)');
+        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE5E237E06 ON kimai2_users (name)');
+        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE5126AC48 ON kimai2_users (mail)');
 
-        $usersTable = $schema->getTable($users);
+        $usersTable = $schema->getTable('kimai2_users');
         foreach ($this->indexesOld as $index) {
             $usersTable->addIndex($index->getColumns(), $index->getName(), $index->getFlags(), $index->getOptions());
         }
     }
 }
diff --git a/src/Migrations/Version20180730044139.php b/src/Migrations/Version20180730044139.php
index 737ded9b8..d9708a528 100644
--- a/src/Migrations/Version20180730044139.php
+++ b/src/Migrations/Version20180730044139.php
@@ -31,26 +31,19 @@ final class Version20180730044139 extends AbstractMigration
      * @param Schema $schema
      * @throws \Doctrine\DBAL\DBALException
      */
     public function up(Schema $schema): void
     {
-        $timesheet = 'kimai2_timesheet';
-        $user = 'kimai2_users';
-        $activity = 'kimai2_activities';
-
-        $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649');
-        $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id) ON DELETE CASCADE');
+        $this->addSql('ALTER TABLE kimai2_timesheet DROP FOREIGN KEY FK_4F60C6B18D93D649');
+        $this->addSql('ALTER TABLE kimai2_timesheet ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES kimai2_users (id) ON DELETE CASCADE');
     }
 
     /**
      * @param Schema $schema
      * @throws \Doctrine\DBAL\DBALException
      */
     public function down(Schema $schema): void
     {
-        $timesheet = 'kimai2_timesheet';
-        $user = 'kimai2_users';
-
-        $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649');
-        $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id)');
+        $this->addSql('ALTER TABLE kimai2_timesheet DROP FOREIGN KEY FK_4F60C6B18D93D649');
+        $this->addSql('ALTER TABLE kimai2_timesheet ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES kimai2_users (id)');
     }
 }
diff --git a/src/Migrations/Version20180924111853.php b/src/Migrations/Version20180924111853.php
index 8844411df..12879912a 100644
--- a/src/Migrations/Version20180924111853.php
+++ b/src/Migrations/Version20180924111853.php
@@ -21,18 +21,14 @@ use Doctrine\DBAL\Schema\Schema;
  */
 final class Version20180924111853 extends AbstractMigration
 {
     public function up(Schema $schema): void
     {
-        $invoiceTemplates = 'kimai2_invoice_templates';
-
-        $this->addSql('UPDATE ' . $invoiceTemplates . ' SET name=SUBSTRING(name, 1, 60)');
-        $this->addSql('ALTER TABLE ' . $invoiceTemplates . ' CHANGE name name VARCHAR(60) NOT NULL, CHANGE vat vat DOUBLE PRECISION DEFAULT 0');
+        $this->addSql('UPDATE kimai2_invoice_templates SET name=SUBSTRING(name, 1, 60)');
+        $this->addSql('ALTER TABLE kimai2_invoice_templates CHANGE name name VARCHAR(60) NOT NULL, CHANGE vat vat DOUBLE PRECISION DEFAULT 0');
     }
 
     public function down(Schema $schema): void
     {
-        $invoiceTemplates = 'kimai2_invoice_templates';
-
-        $this->addSql('ALTER TABLE ' . $invoiceTemplates . ' CHANGE name name VARCHAR(255) NOT NULL COLLATE utf8mb4_unicode_ci, CHANGE vat vat INT DEFAULT NULL');
+        $this->addSql('ALTER TABLE kimai2_invoice_templates CHANGE name name VARCHAR(255) NOT NULL COLLATE utf8mb4_unicode_ci, CHANGE vat vat INT DEFAULT NULL');
     }
 }
diff --git a/src/Migrations/Version20181031220003.php b/src/Migrations/Version20181031220003.php
index 307aee78a..39ca98e33 100644
--- a/src/Migrations/Version20181031220003.php
+++ b/src/Migrations/Version20181031220003.php
@@ -20,47 +20,37 @@ use Doctrine\DBAL\Schema\Schema;
  */
 final class Version20181031220003 extends AbstractMigration
 {
     public function up(Schema $schema): void
     {
-        $timesheet = 'kimai2_timesheet';
-        $projects = 'kimai2_projects';
-        $activities = 'kimai2_activities';
-        $users = 'kimai2_users';
-        $customers = 'kimai2_customers';
-
         // project table
-        $this->addSql('ALTER TABLE ' . $projects . ' DROP FOREIGN KEY FK_407F12069395C3F3');
-        $this->addSql('ALTER TABLE ' . $projects . ' CHANGE customer_id customer_id INT NOT NULL');
-        $this->addSql('ALTER TABLE ' . $projects . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE');
+        $this->addSql('ALTER TABLE kimai2_projects DROP FOREIGN KEY FK_407F12069395C3F3');
+        $this->addSql('ALTER TABLE kimai2_projects CHANGE customer_id customer_id INT NOT NULL');
+        $this->addSql('ALTER TABLE kimai2_projects ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES kimai2_customers (id) ON DELETE CASCADE');
         // timesheet table
-        $this->addSql('ALTER TABLE ' . $timesheet . ' ADD project_id INT DEFAULT NULL AFTER activity_id');
-        $this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)');
+        $this->addSql('ALTER TABLE kimai2_timesheet ADD project_id INT DEFAULT NULL AFTER activity_id');
+        $this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON kimai2_timesheet (project_id)');
 
         // update timesheet table and insert project_id from activity table
-        $this->addSql('UPDATE ' . $timesheet . ' SET project_id = (SELECT project_id FROM ' . $activities . ' WHERE id = activity_id)');
+        $this->addSql('UPDATE kimai2_timesheet SET project_id = (SELECT project_id FROM kimai2_activities WHERE id = activity_id)');
 
         // now update the timesheet table and disallow null values for all required columns (that was a bug before)
-        $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649');
-        $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B181C06096');
-        $this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE project_id project_id INT NOT NULL, CHANGE user user INT NOT NULL, CHANGE activity_id activity_id INT NOT NULL');
-        $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE');
-        $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id) ON DELETE CASCADE');
-        $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE');
+        $this->addSql('ALTER TABLE kimai2_timesheet DROP FOREIGN KEY FK_4F60C6B18D93D649');
+        $this->addSql('ALTER TABLE kimai2_timesheet DROP FOREIGN KEY FK_4F60C6B181C06096');
+        $this->addSql('ALTER TABLE kimai2_timesheet CHANGE project_id project_id INT NOT NULL, CHANGE user user INT NOT NULL, CHANGE activity_id activity_id INT NOT NULL');
+        $this->addSql('ALTER TABLE kimai2_timesheet ADD CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES kimai2_projects (id) ON DELETE CASCADE');
+        $this->addSql('ALTER TABLE kimai2_timesheet ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES kimai2_users (id) ON DELETE CASCADE');
+        $this->addSql('ALTER TABLE kimai2_timesheet ADD CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES kimai2_activities (id) ON DELETE CASCADE');
     }
 
     public function down(Schema $schema): void
     {
-        $timesheet = 'kimai2_timesheet';
-        $projects = 'kimai2_projects';
-        $customers = 'kimai2_customers';
-
         // project table
-        $this->addSql('ALTER TABLE ' . $projects . ' DROP FOREIGN KEY FK_407F12069395C3F3');
-        $this->addSql('ALTER TABLE ' . $projects . ' CHANGE customer_id customer_id INT DEFAULT NULL');
-        $this->addSql('ALTER TABLE ' . $projects . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE');
+        $this->addSql('ALTER TABLE kimai2_projects DROP FOREIGN KEY FK_407F12069395C3F3');
+        $this->addSql('ALTER TABLE kimai2_projects CHANGE customer_id customer_id INT DEFAULT NULL');
+        $this->addSql('ALTER TABLE kimai2_projects ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES kimai2_customers (id) ON DELETE CASCADE');
         // timesheet table
-        $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B1166D1F9C');
-        $this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet);
-        $this->addSql('ALTER TABLE ' . $timesheet . ' DROP project_id, CHANGE user user INT DEFAULT NULL, CHANGE activity_id activity_id INT DEFAULT NULL');
+        $this->addSql('ALTER TABLE kimai2_timesheet DROP FOREIGN KEY FK_4F60C6B1166D1F9C');
+        $this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C ON kimai2_timesheet');
+        $this->addSql('ALTER TABLE kimai2_timesheet DROP project_id, CHANGE user user INT DEFAULT NULL, CHANGE activity_id activity_id INT DEFAULT NULL');
     }
 }
diff --git a/src/Migrations/Version20190305152308.php b/src/Migrations/Version20190305152308.php
index d5481a373..4c01d8c67 100644
--- a/src/Migrations/Version20190305152308.php
+++ b/src/Migrations/Version20190305152308.php
@@ -23,30 +23,19 @@ use Doctrine\DBAL\Schema\Schema;
  */
 final class Version20190305152308 extends AbstractMigration
 {
     public function up(Schema $schema): void
     {
-        $customers = 'kimai2_customers';
-        $projects = 'kimai2_projects';
-        $activities = 'kimai2_activities';
-        $timesheet = 'kimai2_timesheet';
-        $users = 'kimai2_users';
-
-        $this->addSql('ALTER TABLE ' . $activities . ' CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
-        $this->addSql('ALTER TABLE ' . $customers . ' CHANGE mail email VARCHAR(255) DEFAULT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
-        $this->addSql('ALTER TABLE ' . $projects . ' CHANGE budget budget DOUBLE PRECISION NOT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
-        $this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE rate rate DOUBLE PRECISION NOT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
+        $this->addSql('ALTER TABLE kimai2_activities CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
+        $this->addSql('ALTER TABLE kimai2_customers CHANGE mail email VARCHAR(255) DEFAULT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
+        $this->addSql('ALTER TABLE kimai2_projects CHANGE budget budget DOUBLE PRECISION NOT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
+        $this->addSql('ALTER TABLE kimai2_timesheet CHANGE rate rate DOUBLE PRECISION NOT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
     }
 
     public function down(Schema $schema): void
     {
-        $customers = 'kimai2_customers';
-        $projects = 'kimai2_projects';
-        $activities = 'kimai2_activities';
-        $timesheet = 'kimai2_timesheet';
-
-        $this->addSql('ALTER TABLE ' . $activities . ' CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
-        $this->addSql('ALTER TABLE ' . $customers . ' CHANGE email mail VARCHAR(255) DEFAULT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
-        $this->addSql('ALTER TABLE ' . $projects . ' CHANGE budget budget NUMERIC(10, 2) NOT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
-        $this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE rate rate NUMERIC(10, 2) NOT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
+        $this->addSql('ALTER TABLE kimai2_activities CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
+        $this->addSql('ALTER TABLE kimai2_customers CHANGE email mail VARCHAR(255) DEFAULT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
+        $this->addSql('ALTER TABLE kimai2_projects CHANGE budget budget NUMERIC(10, 2) NOT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
+        $this->addSql('ALTER TABLE kimai2_timesheet CHANGE rate rate NUMERIC(10, 2) NOT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
     }
 }
diff --git a/src/Migrations/Version20210802152814.php b/src/Migrations/Version20210802152814.php
index e65ac384e..20a71a9be 100644
--- a/src/Migrations/Version20210802152814.php
+++ b/src/Migrations/Version20210802152814.php
@@ -42,11 +42,11 @@ final class Version20210802152814 extends AbstractMigration
             $this->addSql('UPDATE kimai2_timesheet SET date_tz = ? WHERE id = ?', [$date->format('Y-m-d'), $row['id']]);
         }
 
         $fetch->free();
 
-        $this->preventEmptyMigrationWarning();
+        $this->addSql('ALTER TABLE kimai2_timesheet ALTER date_tz DROP DEFAULT');
     }
 
     public function down(Schema $schema): void
     {
         $timesheet = $schema->getTable('kimai2_timesheet');
diff --git a/src/Migrations/Version20211008092010.php b/src/Migrations/Version20211008092010.php
index eb7bbc52e..218fa86be 100644
--- a/src/Migrations/Version20211008092010.php
+++ b/src/Migrations/Version20211008092010.php
@@ -27,10 +27,12 @@ final class Version20211008092010 extends AbstractMigration
     public function up(Schema $schema): void
     {
         $projects = $schema->getTable('kimai2_projects');
         $column = $projects->getColumn('order_number');
         $column->setOptions(['length' => 50]);
+
+        $this->preventEmptyMigrationWarning();
     }
 
     public function down(Schema $schema): void
     {
         $projects = $schema->getTable('kimai2_projects');
diff --git a/templates/form/kimai-theme.html.twig b/templates/form/kimai-theme.html.twig
index 0a4546270..0d8f8430b 100644
--- a/templates/form/kimai-theme.html.twig
+++ b/templates/form/kimai-theme.html.twig
@@ -156,14 +156,12 @@
             <i class="{{ 'left'|icon }}"></i>
         </a>
         <a class="btn btn-default" href="#" onclick="return false;">
             <span id="{{ form.vars.id }}_week_number" data-toggle="tooltip" data-placement="top" title="{{ 'stats.workingTimeWeek'|trans({'%week%': week|date_format('W')}) }}">
                 {{ week|month_name(true) }}
-                {% if week|date_format('m') != nextWeek|date_format('m') %}
-                    &ndash;
-                    {{ nextWeek|month_name(true) }}
-                {% endif %}
+                &ndash;
+                {{ 'stats.workingTimeWeekShort'|trans({'%week%': week|date_format('W')}) }}
             </span>
         </a>
         <a class="btn btn-default btn-right" href="#" onclick="jQuery('#{{ form.vars.id }}').val('{{ nextWeek|report_date }}').change()" data-toggle="tooltip" data-placement="top" title="{{ 'stats.workingTimeWeek'|trans({'%week%': nextWeek|date_format('W')}) }}">
             <i class="{{ 'right'|icon }}"></i>
         </a>
diff --git a/templates/project/actions.html.twig b/templates/project/actions.html.twig
index d61b8d8f6..f235ae2e3 100644
--- a/templates/project/actions.html.twig
+++ b/templates/project/actions.html.twig
@@ -4,11 +4,11 @@
     {{ widgets.page_actions(event.actions) }}
 {% endmacro %}
 
 {% macro project(project, view, isTable) %}
     {% import "macros/widgets.html.twig" as widgets %}
-    {% set event = actions(app.user, 'project', view, {'project': project}) %}
+    {% set event = actions(app.user, 'project', view, {'project': project, 'token': csrf_token('project.duplicate')}) %}
     {% if view == 'index' or view == 'custom' or isTable is not null %}
         {{ widgets.table_actions(event.actions) }}
     {% else %}
         {{ widgets.page_actions(event.actions) }}
     {% endif %}
diff --git a/templates/team/actions.html.twig b/templates/team/actions.html.twig
index e7f58c91c..8d72ef897 100644
--- a/templates/team/actions.html.twig
+++ b/templates/team/actions.html.twig
@@ -4,11 +4,11 @@
     {{ widgets.page_actions(event.actions) }}
 {% endmacro %}
 
 {% macro team(team, view) %}
     {% import "macros/widgets.html.twig" as widgets %}
-    {% set event = actions(app.user, 'team', view, {'team': team}) %}
+    {% set event = actions(app.user, 'team', view, {'team': team, 'token': csrf_token('team.duplicate')}) %}
     {% if view == 'index' %}
         {{ widgets.table_actions(event.actions) }}
     {% else %}
         {{ widgets.page_actions(event.actions) }}
     {% endif %}
diff --git a/templates/timesheet-team/actions.html.twig b/templates/timesheet-team/actions.html.twig
index 406a9c870..a4b0aef32 100644
--- a/templates/timesheet-team/actions.html.twig
+++ b/templates/timesheet-team/actions.html.twig
@@ -4,11 +4,11 @@
     {{ widgets.page_actions(event.actions) }}
 {% endmacro %}
 
 {% macro timesheet_team(timesheet, view) %}
     {% import "macros/widgets.html.twig" as widgets %}
-    {% set event = actions(app.user, 'timesheet_team', view, {'timesheet': timesheet}) %}
+    {% set event = actions(app.user, 'timesheet_team', view, {'timesheet': timesheet, 'token': csrf_token('admin_timesheet.duplicate')}) %}
     {% if view == 'index' or view == 'custom' %}
         {{ widgets.table_actions(event.actions) }}
     {% else %}
         {{ widgets.page_actions(event.actions) }}
     {% endif %}
diff --git a/templates/timesheet/actions.html.twig b/templates/timesheet/actions.html.twig
index 521bda168..525ec5e87 100644
--- a/templates/timesheet/actions.html.twig
+++ b/templates/timesheet/actions.html.twig
@@ -4,11 +4,11 @@
     {{ widgets.page_actions(event.actions) }}
 {% endmacro %}
 
 {% macro timesheet(timesheet, view, options) %}
     {% import "macros/widgets.html.twig" as widgets %}
-    {% set event = actions(app.user, 'timesheet', view, {'timesheet': timesheet}) %}
+    {% set event = actions(app.user, 'timesheet', view, {'timesheet': timesheet, 'token': csrf_token('timesheet.duplicate')}) %}
     {% if view == 'index' or view == 'custom' %}
         {{ widgets.table_actions(event.actions) }}
     {% else %}
         {{ widgets.page_actions(event.actions) }}
     {% endif %}
diff --git a/tests/Controller/ControllerBaseTest.php b/tests/Controller/ControllerBaseTest.php
index ffeff3920..50c5d98f0 100644
--- a/tests/Controller/ControllerBaseTest.php
+++ b/tests/Controller/ControllerBaseTest.php
@@ -423,6 +423,16 @@ abstract class ControllerBaseTest extends WebTestCase
 
         self::assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('Content-Type'));
         self::assertStringContainsString('attachment; filename=' . $prefix, $response->headers->get('Content-Disposition'));
         self::assertStringContainsString('.xlsx', $response->headers->get('Content-Disposition'));
     }
+
+    protected function assertInvalidCsrfToken(HttpKernelBrowser $client, string $url, string $expectedRedirect)
+    {
+        $this->request($client, $url);
+
+        $this->assertIsRedirect($client);
+        $this->assertRedirectUrl($client, $expectedRedirect);
+        $client->followRedirect();
+        $this->assertHasFlashError($client, 'The action could not be performed: invalid security token.');
+    }
 }
diff --git a/tests/Controller/DoctorControllerTest.php b/tests/Controller/DoctorControllerTest.php
index 32987333a..ab8faacdb 100644
--- a/tests/Controller/DoctorControllerTest.php
+++ b/tests/Controller/DoctorControllerTest.php
@@ -32,6 +32,13 @@ class DoctorControllerTest extends ControllerBaseTest
         $this->assertAccessIsGranted($client, '/doctor');
 
         $result = $client->getCrawler()->filter('.content .box-header');
         self::assertCount(6, $result);
     }
+
+    public function testFlushLogWithInvalidCsrf()
+    {
+        $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
+
+        $this->assertInvalidCsrfToken($client, '/doctor/flush-log/rsetdzfukgli78t6r5uedtjfzkugl', $this->createUrl('/doctor'));
+    }
 }
diff --git a/tests/Controller/ProjectControllerTest.php b/tests/Controller/ProjectControllerTest.php
index a14a34ba2..6a0eb488b 100644
--- a/tests/Controller/ProjectControllerTest.php
+++ b/tests/Controller/ProjectControllerTest.php
@@ -214,20 +214,41 @@ class ProjectControllerTest extends ControllerBaseTest
         $rate->setActivity($activity);
         $rate->setRate(123.45);
         $em->persist($rate);
         $em->flush();
 
-        $this->request($client, '/admin/project/1/duplicate');
+        $token = self::$container->get('security.csrf.token_manager')->getToken('project.duplicate');
+
+        $this->request($client, '/admin/project/1/duplicate/' . $token);
         $this->assertIsRedirect($client, '/details');
         $client->followRedirect();
         $node = $client->getCrawler()->filter('div.box#project_rates_box');
         self::assertEquals(1, $node->count());
         $node = $client->getCrawler()->filter('div.box#project_rates_box table.dataTable tbody tr:not(.summary)');
         self::assertEquals(1, $node->count());
         self::assertStringContainsString('123.45', $node->text(null, true));
     }
 
+    public function testDuplicateActionWithInvalidCsrf()
+    {
+        $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
+        /** @var EntityManager $em */
+        $em = $this->getEntityManager();
+        $project = $em->find(Project::class, 1);
+        $project->setMetaField((new ProjectMeta())->setName('foo')->setValue('bar'));
+        $project->setEnd(new \DateTime());
+        $em->persist($project);
+        $activity = new Activity();
+        $activity->setName('blub');
+        $activity->setProject($project);
+        $activity->setMetaField((new ActivityMeta())->setName('blub')->setValue('blab'));
+        $em->persist($activity);
+        $em->flush();
+
+        $this->assertInvalidCsrfToken($client, '/admin/project/1/duplicate/rsetdzfukgli78t6r5uedtjfzkugl', $this->createUrl('/admin/project/1/details'));
+    }
+
     public function testAddCommentAction()
     {
         $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
         $this->assertAccessIsGranted($client, '/admin/project/1/details');
         $form = $client->getCrawler()->filter('form[name=project_comment_form]')->form();
diff --git a/tests/Controller/TeamControllerTest.php b/tests/Controller/TeamControllerTest.php
index 7632d79ff..7aeadaf43 100644
--- a/tests/Controller/TeamControllerTest.php
+++ b/tests/Controller/TeamControllerTest.php
@@ -204,13 +204,22 @@ class TeamControllerTest extends ControllerBaseTest
     }
 
     public function testDuplicateAction()
     {
         $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
-        $this->request($client, '/admin/teams/1/duplicate');
+
+        $token = self::$container->get('security.csrf.token_manager')->getToken('team.duplicate');
+
+        $this->request($client, '/admin/teams/1/duplicate/' . $token);
         $this->assertIsRedirect($client, '/edit');
         $client->followRedirect();
         $node = $client->getCrawler()->filter('#team_edit_form_name');
         self::assertEquals(1, $node->count());
         self::assertEquals('Test team [COPY]', $node->attr('value'));
     }
+
+    public function testDuplicateActionWithInvalidCsrf()
+    {
+        $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
+        $this->assertInvalidCsrfToken($client, '/admin/teams/1/duplicate/rsetdzfukgli78t6r5uedtjfzkugl', $this->createUrl('/admin/teams/1/edit'));
+    }
 }
diff --git a/tests/Controller/TimesheetControllerTest.php b/tests/Controller/TimesheetControllerTest.php
index 2dede1e87..02d65efc3 100644
--- a/tests/Controller/TimesheetControllerTest.php
+++ b/tests/Controller/TimesheetControllerTest.php
@@ -706,11 +706,13 @@ class TimesheetControllerTest extends ControllerBaseTest
 
         /** @var Timesheet[] $ids */
         $ids = $this->importFixture($fixture);
         $newId = $ids[0]->getId();
 
-        $this->request($client, '/timesheet/' . $newId . '/duplicate');
+        $token = self::$container->get('security.csrf.token_manager')->getToken('timesheet.duplicate');
+
+        $this->request($client, '/timesheet/' . $newId . '/duplicate/' . $token);
         $this->assertTrue($client->getResponse()->isSuccessful());
 
         $form = $client->getCrawler()->filter('form[name=timesheet_edit_form]')->form();
         $client->submit($form, $form->getPhpValues());
 
@@ -728,6 +730,35 @@ class TimesheetControllerTest extends ControllerBaseTest
         $this->assertEquals(2016, $timesheet->getRate());
         $this->assertEquals(127, $timesheet->getHourlyRate());
         $this->assertEquals(2016, $timesheet->getFixedRate());
         $this->assertEquals(2016, $timesheet->getRate());
     }
+
+    public function testDuplicateActionWithInvalidCsrf()
+    {
+        $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
+        $dateTime = new DateTimeFactory(new \DateTimeZone('Europe/London'));
+
+        $fixture = new TimesheetFixtures();
+        $fixture->setAmount(1);
+        $fixture->setAmountRunning(0);
+        $fixture->setUser($this->getUserByRole(User::ROLE_USER));
+        $fixture->setStartDate($dateTime->createDateTime());
+        $fixture->setCallback(function (Timesheet $timesheet) {
+            $timesheet->setDescription('Testing is fun!');
+            $begin = clone $timesheet->getBegin();
+            $begin->setTime(0, 0, 0);
+            $timesheet->setBegin($begin);
+            $end = clone $timesheet->getBegin();
+            $end->modify('+ 8 hours');
+            $timesheet->setEnd($end);
+            $timesheet->setFixedRate(2016);
+            $timesheet->setHourlyRate(127);
+        });
+
+        /** @var Timesheet[] $ids */
+        $ids = $this->importFixture($fixture);
+        $newId = $ids[0]->getId();
+
+        $this->assertInvalidCsrfToken($client, '/timesheet/' . $newId . '/duplicate/dfghdfghdfghdfghdfgh', $this->createUrl('/timesheet/'));
+    }
 }
diff --git a/tests/Controller/TimesheetTeamControllerTest.php b/tests/Controller/TimesheetTeamControllerTest.php
index 442326381..c4b217433 100644
--- a/tests/Controller/TimesheetTeamControllerTest.php
+++ b/tests/Controller/TimesheetTeamControllerTest.php
@@ -426,11 +426,13 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
 
         /** @var Timesheet[] $ids */
         $ids = $this->importFixture($fixture);
         $newId = $ids[0]->getId();
 
-        $this->request($client, '/team/timesheet/' . $newId . '/duplicate');
+        $token = self::$container->get('security.csrf.token_manager')->getToken('admin_timesheet.duplicate');
+
+        $this->request($client, '/team/timesheet/' . $newId . '/duplicate/' . $token);
         $this->assertTrue($client->getResponse()->isSuccessful());
 
         $form = $client->getCrawler()->filter('form[name=timesheet_admin_edit_form]')->form();
         $client->submit($form, $form->getPhpValues());
 
@@ -448,6 +450,35 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
         $this->assertEquals(2016, $timesheet->getRate());
         $this->assertEquals(127, $timesheet->getHourlyRate());
         $this->assertEquals(2016, $timesheet->getFixedRate());
         $this->assertEquals(2016, $timesheet->getRate());
     }
+
+    public function testDuplicateActionWithInvalidCsrf()
+    {
+        $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
+        $dateTime = new DateTimeFactory(new \DateTimeZone('Europe/London'));
+
+        $fixture = new TimesheetFixtures();
+        $fixture->setAmount(1);
+        $fixture->setAmountRunning(0);
+        $fixture->setUser($this->getUserByRole(User::ROLE_USER));
+        $fixture->setStartDate($dateTime->createDateTime());
+        $fixture->setCallback(function (Timesheet $timesheet) {
+            $timesheet->setDescription('Testing is fun!');
+            $begin = clone $timesheet->getBegin();
+            $begin->setTime(0, 0, 0);
+            $timesheet->setBegin($begin);
+            $end = clone $timesheet->getBegin();
+            $end->modify('+ 8 hours');
+            $timesheet->setEnd($end);
+            $timesheet->setFixedRate(2016);
+            $timesheet->setHourlyRate(127);
+        });
+
+        /** @var Timesheet[] $ids */
+        $ids = $this->importFixture($fixture);
+        $newId = $ids[0]->getId();
+
+        $this->assertInvalidCsrfToken($client, '/team/timesheet/' . $newId . '/duplicate/dfghdfghdfghdfghdfgh', $this->createUrl('/team/timesheet/'));
+    }
 }
diff --git a/translations/about.ja.xlf b/translations/about.ja.xlf
index d6eddc882..9e60ff4b6 100644
--- a/translations/about.ja.xlf
+++ b/translations/about.ja.xlf
@@ -1,6 +1,6 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
   <file source-language="en" target-language="ja" datatype="plaintext" original="about.en.xlf">
     <body>
       <trans-unit id="3Clo55j" resname="about.title">
         <source>about.title</source>
@@ -26,11 +26,11 @@
         <source>published_under</source>
         <target>%kimai% は次のもとに公開されています</target>
       </trans-unit>
       <trans-unit id="A0nWoXZ" resname="special_thanks">
         <source>special_thanks</source>
-        <target>Special thanks go to …</target>
+        <target>これらの方々に特に感謝いたします。</target>
       </trans-unit>
       <trans-unit id="L7cff3Q" resname="library_authors">
         <source>library_authors</source>
         <target>… これらのソフトウェア・ライブラリなくして Kimai は実現できませんでした。作成者の方々に感謝します 👍</target>
       </trans-unit>
diff --git a/translations/flashmessages.el.xlf b/translations/flashmessages.el.xlf
index 759dd05b5..acc002339 100644
--- a/translations/flashmessages.el.xlf
+++ b/translations/flashmessages.el.xlf
@@ -1,6 +1,6 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
   <file source-language="en" target-language="el" datatype="plaintext" original="flashmessages.en.xlf">
     <body>
       <trans-unit id="S9k1S7Z" resname="warning">
         <source>warning</source>
@@ -52,8 +52,12 @@
       </trans-unit>
       <trans-unit id="wejAEcR" resname="action.upload.error">
         <source>action.upload.error</source>
         <target>Δεν ήταν δυνατή η μεταφόρτωση ή η αποθήκευση του αρχείου: %reason%</target>
       </trans-unit>
+      <trans-unit resname="action.csrf.error" id="bOE_q5R">
+        <source>action.csrf.error</source>
+        <target>Αυτή η ενέργεια δεν μπορεί να πραγματοποιηθεί: Μη έγκυρο τεκμήριο ασφάλειας.</target>
+      </trans-unit>
     </body>
   </file>
 </xliff>
diff --git a/translations/flashmessages.fr.xlf b/translations/flashmessages.fr.xlf
index 44f95eaff..65b682ae2 100644
--- a/translations/flashmessages.fr.xlf
+++ b/translations/flashmessages.fr.xlf
@@ -52,8 +52,12 @@
       </trans-unit>
       <trans-unit id="wejAEcR" resname="action.upload.error">
         <source>action.upload.error</source>
         <target>Le fichier n'a pas pu être mis en ligne ou enregistré : %reason%</target>
       </trans-unit>
+      <trans-unit resname="action.csrf.error" id="bOE_q5R">
+        <source>action.csrf.error</source>
+        <target>L'action n'a pas pu être effectuée : jeton de sécurité invalide.</target>
+      </trans-unit>
     </body>
   </file>
 </xliff>
diff --git a/translations/flashmessages.he.xlf b/translations/flashmessages.he.xlf
index 2f01bee6d..6d82b773a 100644
--- a/translations/flashmessages.he.xlf
+++ b/translations/flashmessages.he.xlf
@@ -52,8 +52,12 @@
       </trans-unit>
       <trans-unit id="wejAEcR" resname="action.upload.error">
         <source>action.upload.error</source>
         <target>לא ניתן להעלות או לשמור את הקובץ: %reason%</target>
       </trans-unit>
+      <trans-unit resname="action.csrf.error" id="bOE_q5R">
+        <source>action.csrf.error</source>
+        <target>אי אפשר לבצע את הפעולה: אסימון אבטחה שגוי.</target>
+      </trans-unit>
     </body>
   </file>
 </xliff>
diff --git a/translations/flashmessages.pt.xlf b/translations/flashmessages.pt.xlf
index 7d78e74ac..2517380d9 100644
--- a/translations/flashmessages.pt.xlf
+++ b/translations/flashmessages.pt.xlf
@@ -52,8 +52,12 @@
       </trans-unit>
       <trans-unit id="wejAEcR" resname="action.upload.error">
         <source>action.upload.error</source>
         <target>Não foi possível enviar ou guardar o ficheiro: %reason%</target>
       </trans-unit>
+      <trans-unit resname="action.csrf.error" id="bOE_q5R">
+        <source>action.csrf.error</source>
+        <target>Não foi possível realizar a ação: token de segurança inválido.</target>
+      </trans-unit>
     </body>
   </file>
 </xliff>
diff --git a/translations/flashmessages.pt_BR.xlf b/translations/flashmessages.pt_BR.xlf
index 74d128d34..763883aa7 100644
--- a/translations/flashmessages.pt_BR.xlf
+++ b/translations/flashmessages.pt_BR.xlf
@@ -52,8 +52,12 @@
       </trans-unit>
       <trans-unit id="wejAEcR" resname="action.upload.error">
         <source>action.upload.error</source>
         <target>O arquivo não pôde ser carregado ou salvo: %reason%</target>
       </trans-unit>
+      <trans-unit resname="action.csrf.error" id="bOE_q5R">
+        <source>action.csrf.error</source>
+        <target>A ação não pôde ser realizada: token de segurança inválido.</target>
+      </trans-unit>
     </body>
   </file>
 </xliff>
diff --git a/translations/flashmessages.tr.xlf b/translations/flashmessages.tr.xlf
index a2269c2b4..d9b15f0b1 100644
--- a/translations/flashmessages.tr.xlf
+++ b/translations/flashmessages.tr.xlf
@@ -52,8 +52,12 @@
       </trans-unit>
       <trans-unit id="wejAEcR" resname="action.upload.error">
         <source>action.upload.error</source>
         <target>Dosya karşıya yüklenemedi veya kaydedilemedi: %reason%</target>
       </trans-unit>
+      <trans-unit resname="action.csrf.error" id="bOE_q5R">
+        <source>action.csrf.error</source>
+        <target>Eylem gerçekleştirilemedi: geçersiz güvenlik belirteci.</target>
+      </trans-unit>
     </body>
   </file>
 </xliff>
diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf
index e19495322..eebc70fe8 100644
--- a/translations/messages.de.xlf
+++ b/translations/messages.de.xlf
@@ -790,10 +790,14 @@
       </trans-unit>
       <trans-unit id="XdYs3I8" resname="stats.workingTimeWeek">
         <source>stats.workingTimeWeek</source>
         <target>Kalenderwoche %week%</target>
       </trans-unit>
+      <trans-unit id="HPhRbtc" resname="stats.workingTimeWeekShort">
+        <source>stats.workingTimeWeekShort</source>
+        <target>KW %week%</target>
+      </trans-unit>
       <trans-unit id="JIKNAYP" approved="yes" resname="stats.workingTimeMonth">
         <source>stats.workingTimeMonth</source>
         <target>%month% %year%</target>
       </trans-unit>
       <trans-unit id="JnUFsNi" resname="stats.workingTimeYear">
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf
index 2b5c2a0bc..bd902d68b 100644
--- a/translations/messages.en.xlf
+++ b/translations/messages.en.xlf
@@ -790,10 +790,14 @@
       </trans-unit>
       <trans-unit id="XdYs3I8" resname="stats.workingTimeWeek">
         <source>stats.workingTimeWeek</source>
         <target>Calendar week %week%</target>
       </trans-unit>
+      <trans-unit id="HPhRbtc" resname="stats.workingTimeWeekShort">
+        <source>stats.workingTimeWeekShort</source>
+        <target>Week %week%</target>
+      </trans-unit>
       <trans-unit id="JIKNAYP" resname="stats.workingTimeMonth">
         <source>stats.workingTimeMonth</source>
         <target>%month% %year%</target>
       </trans-unit>
       <trans-unit id="JnUFsNi" resname="stats.workingTimeYear">

Vulnerable code:     public const VERSION = '1.16.0';
    public const VERSION_ID = 11600;
     * @Route(path="/{id}/duplicate", name="admin_project_duplicate", methods={"GET", "POST"})
    public function duplicateAction(Project $project, Request $request, ProjectDuplicationService $projectDuplicationService)
     * @Route(path="/{id}/duplicate", name="team_duplicate", methods={"GET", "POST"})
    public function duplicateTeam(Team $team, Request $request)
    protected function duplicate(Timesheet $timesheet, Request $request, string $renderTemplate): Response
        $form = $this->getDuplicateForm($copyTimesheet, $timesheet);
    abstract protected function getDuplicateForm(Timesheet $entry, Timesheet $original): FormInterface;
     * @Route(path="/{id}/duplicate", name="timesheet_duplicate", methods={"GET", "POST"})
    public function duplicateAction(Timesheet $entry, Request $request): Response
        return $this->duplicate($entry, $request, 'timesheet/edit.html.twig');
    protected function getDuplicateForm(Timesheet $entry, Timesheet $original): FormInterface
        return $this->generateCreateForm($entry, TimesheetEditForm::class, $this->generateUrl('timesheet_duplicate', ['id' => $original->getId()]));
     * @Route(path="/{id}/duplicate", name="admin_timesheet_duplicate", methods={"GET", "POST"})
    public function duplicateAction(Timesheet $entry, Request $request): Response
        return $this->duplicate($entry, $request, 'timesheet-team/edit.html.twig');
    protected function getDuplicateForm(Timesheet $entry, Timesheet $original): FormInterface
        return $this->generateCreateForm($entry, TimesheetAdminEditForm::class, $this->generateUrl('admin_timesheet_duplicate', ['id' => $original->getId()]));
                $event->addAction('copy', ['url' => $this->path($routeDuplicate, ['id' => $timesheet->getId()]), 'class' => $class]);
                ['url' => $this->path('admin_project_duplicate', ['id' => $project->getId()])]
                $event->addAction('copy', ['url' => $this->path('team_duplicate', ['id' => $team->getId()])]);
        $users = 'kimai2_users';
        $userPreferences = 'kimai2_user_preferences';
        $customers = 'kimai2_customers';
        $projects = 'kimai2_projects';
        $activities = 'kimai2_activities';
        $timesheets = 'kimai2_timesheet';
        $invoiceTemplates = 'kimai2_invoice_templates';

        $this->addSql('CREATE TABLE ' . $users . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(60) NOT NULL, mail VARCHAR(160) NOT NULL, password VARCHAR(254) DEFAULT NULL, alias VARCHAR(60) DEFAULT NULL, active TINYINT(1) NOT NULL, registration_date DATETIME DEFAULT NULL, title VARCHAR(50) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', UNIQUE INDEX UNIQ_B9AC5BCE5E237E06 (name), UNIQUE INDEX UNIQ_B9AC5BCE5126AC48 (mail), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
        $this->addSql('CREATE TABLE ' . $userPreferences . ' (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, name VARCHAR(50) NOT NULL, value VARCHAR(255) DEFAULT NULL, INDEX IDX_8D08F631A76ED395 (user_id), UNIQUE INDEX UNIQ_8D08F631A76ED3955E237E06 (user_id, name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
        $this->addSql('CREATE TABLE ' . $customers . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(150) NOT NULL, number VARCHAR(50) DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, company VARCHAR(255) DEFAULT NULL, contact VARCHAR(255) DEFAULT NULL, address TEXT DEFAULT NULL, country VARCHAR(2) NOT NULL, currency VARCHAR(3) NOT NULL, phone VARCHAR(255) DEFAULT NULL, fax VARCHAR(255) DEFAULT NULL, mobile VARCHAR(255) DEFAULT NULL, mail VARCHAR(255) DEFAULT NULL, homepage VARCHAR(255) DEFAULT NULL, timezone VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
        $this->addSql('CREATE TABLE ' . $projects . ' (id INT AUTO_INCREMENT NOT NULL, customer_id INT DEFAULT NULL, name VARCHAR(150) NOT NULL, order_number TINYTEXT DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, budget NUMERIC(10, 2) NOT NULL, INDEX IDX_407F12069395C3F3 (customer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
        $this->addSql('CREATE TABLE ' . $activities . ' (id INT AUTO_INCREMENT NOT NULL, project_id INT DEFAULT NULL, name VARCHAR(150) NOT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, INDEX IDX_8811FE1C166D1F9C (project_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
        $this->addSql('CREATE TABLE ' . $timesheets . ' (id INT AUTO_INCREMENT NOT NULL, user INT DEFAULT NULL, activity_id INT DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INT DEFAULT NULL, description TEXT DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, INDEX IDX_4F60C6B18D93D649 (user), INDEX IDX_4F60C6B181C06096 (activity_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
        $this->addSql('CREATE TABLE ' . $invoiceTemplates . ' (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(60) NOT NULL, title VARCHAR(255) NOT NULL, company VARCHAR(255) NOT NULL, address TEXT DEFAULT NULL, due_days INT NOT NULL, vat INT DEFAULT NULL, calculator VARCHAR(20) NOT NULL, number_generator VARCHAR(20) NOT NULL, renderer VARCHAR(20) NOT NULL, payment_terms TEXT DEFAULT NULL, UNIQUE INDEX UNIQ_1626CFE95E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
        $this->addSql('ALTER TABLE ' . $userPreferences . ' ADD CONSTRAINT FK_8D08F631A76ED395 FOREIGN KEY (user_id) REFERENCES ' . $users . ' (id) ON DELETE CASCADE');
        $this->addSql('ALTER TABLE ' . $projects . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE');
        $this->addSql('ALTER TABLE ' . $activities . ' ADD CONSTRAINT FK_8811FE1C166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE');
        $this->addSql('ALTER TABLE ' . $timesheets . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id)');
        $this->addSql('ALTER TABLE ' . $timesheets . ' ADD CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE');
        $users = 'kimai2_users';

        $indexesOld = $schema->getTable($users)->getIndexes();
                $this->addSql('DROP INDEX ' . $index->getName() . ' ON ' . $users);
        $this->addSql('ALTER TABLE ' . $users . ' CHANGE name username VARCHAR(180) NOT NULL, ADD username_canonical VARCHAR(180) NOT NULL, CHANGE mail email VARCHAR(180) NOT NULL, ADD email_canonical VARCHAR(180) NOT NULL, ADD salt VARCHAR(255) DEFAULT NULL, ADD last_login DATETIME DEFAULT NULL, ADD confirmation_token VARCHAR(180) DEFAULT NULL, ADD password_requested_at DATETIME DEFAULT NULL, CHANGE password password VARCHAR(255) NOT NULL, CHANGE alias alias VARCHAR(60) DEFAULT NULL, CHANGE registration_date registration_date DATETIME DEFAULT NULL, CHANGE title title VARCHAR(50) DEFAULT NULL, CHANGE avatar avatar VARCHAR(255) DEFAULT NULL, CHANGE roles roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', CHANGE active enabled TINYINT(1) NOT NULL');
        $this->addSql('UPDATE ' . $users . ' set username_canonical = username');
        $this->addSql('UPDATE ' . $users . ' set email_canonical = email');

        $this->addSql('UPDATE ' . $users . ' SET roles = \'a:1:{i:0;s:16:"ROLE_SUPER_ADMIN";}\' WHERE roles LIKE \'%ROLE_SUPER_ADMIN%\'');
        $this->addSql('UPDATE ' . $users . ' SET roles = \'a:1:{i:0;s:10:"ROLE_ADMIN";}\' WHERE roles LIKE \'%ROLE_ADMIN%\'');
        $this->addSql('UPDATE ' . $users . ' SET roles = \'a:1:{i:0;s:13:"ROLE_TEAMLEAD";}\' WHERE roles LIKE \'%ROLE_TEAMLEAD%\'');
        $this->addSql('UPDATE ' . $users . ' SET roles = \'a:0:{}\' WHERE roles LIKE \'%ROLE_USER%\'');
        $this->addSql('UPDATE ' . $users . ' SET roles = \'a:1:{i:0;s:13:"ROLE_CUSTOMER";}\' WHERE roles LIKE \'%ROLE_CUSTOMER%\'');

        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE92FC23A8 ON ' . $users . ' (username_canonical)');
        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEA0D96FBF ON ' . $users . ' (email_canonical)');
        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEC05FB297 ON ' . $users . ' (confirmation_token)');
        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEF85E0677 ON ' . $users . ' (username)');
        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEE7927C74 ON ' . $users . ' (email)');
        $users = 'kimai2_users';

            $this->addSql('DROP INDEX ' . $indexName . ' ON ' . $users);
        $this->addSql('ALTER TABLE ' . $users . ' CHANGE username name VARCHAR(60) NOT NULL COLLATE utf8mb4_unicode_ci, CHANGE email mail VARCHAR(160) NOT NULL COLLATE utf8mb4_unicode_ci, DROP username_canonical, DROP email_canonical, DROP salt, DROP last_login, DROP confirmation_token, DROP password_requested_at, CHANGE password password VARCHAR(254) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE roles roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', CHANGE alias alias VARCHAR(60) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE registration_date registration_date DATETIME DEFAULT NULL, CHANGE title title VARCHAR(50) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE avatar avatar VARCHAR(255) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE enabled active TINYINT(1) NOT NULL');
        $this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_SUPER_ADMIN"]\' WHERE roles LIKE \'%ROLE_SUPER_ADMIN%\'');
        $this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_ADMIN"]\' WHERE roles LIKE \'%ROLE_ADMIN%\'');
        $this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_TEAMLEAD"]\' WHERE roles LIKE \'%ROLE_TEAMLEAD%\'');
        $this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_USER"]\' WHERE roles LIKE \'%ROLE_USER%\'');
        $this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_CUSTOMER"]\' WHERE roles LIKE \'%ROLE_CUSTOMER%\'');
        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE5E237E06 ON ' . $users . ' (name)');
        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE5126AC48 ON ' . $users . ' (mail)');
        $usersTable = $schema->getTable($users);
        $timesheet = 'kimai2_timesheet';
        $user = 'kimai2_users';
        $activity = 'kimai2_activities';

        $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649');
        $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id) ON DELETE CASCADE');
        $timesheet = 'kimai2_timesheet';
        $user = 'kimai2_users';

        $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649');
        $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id)');
        $invoiceTemplates = 'kimai2_invoice_templates';

        $this->addSql('UPDATE ' . $invoiceTemplates . ' SET name=SUBSTRING(name, 1, 60)');
        $this->addSql('ALTER TABLE ' . $invoiceTemplates . ' CHANGE name name VARCHAR(60) NOT NULL, CHANGE vat vat DOUBLE PRECISION DEFAULT 0');
        $invoiceTemplates = 'kimai2_invoice_templates';

        $this->addSql('ALTER TABLE ' . $invoiceTemplates . ' CHANGE name name VARCHAR(255) NOT NULL COLLATE utf8mb4_unicode_ci, CHANGE vat vat INT DEFAULT NULL');
        $timesheet = 'kimai2_timesheet';
        $projects = 'kimai2_projects';
        $activities = 'kimai2_activities';
        $users = 'kimai2_users';
        $customers = 'kimai2_customers';

        $this->addSql('ALTER TABLE ' . $projects . ' DROP FOREIGN KEY FK_407F12069395C3F3');
        $this->addSql('ALTER TABLE ' . $projects . ' CHANGE customer_id customer_id INT NOT NULL');
        $this->addSql('ALTER TABLE ' . $projects . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE');
        $this->addSql('ALTER TABLE ' . $timesheet . ' ADD project_id INT DEFAULT NULL AFTER activity_id');
        $this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)');
        $this->addSql('UPDATE ' . $timesheet . ' SET project_id = (SELECT project_id FROM ' . $activities . ' WHERE id = activity_id)');
        $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649');
        $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B181C06096');
        $this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE project_id project_id INT NOT NULL, CHANGE user user INT NOT NULL, CHANGE activity_id activity_id INT NOT NULL');
        $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE');
        $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id) ON DELETE CASCADE');
        $this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE');
        $timesheet = 'kimai2_timesheet';
        $projects = 'kimai2_projects';
        $customers = 'kimai2_customers';

        $this->addSql('ALTER TABLE ' . $projects . ' DROP FOREIGN KEY FK_407F12069395C3F3');
        $this->addSql('ALTER TABLE ' . $projects . ' CHANGE customer_id customer_id INT DEFAULT NULL');
        $this->addSql('ALTER TABLE ' . $projects . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE');
        $this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B1166D1F9C');
        $this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet);
        $this->addSql('ALTER TABLE ' . $timesheet . ' DROP project_id, CHANGE user user INT DEFAULT NULL, CHANGE activity_id activity_id INT DEFAULT NULL');
        $customers = 'kimai2_customers';
        $projects = 'kimai2_projects';
        $activities = 'kimai2_activities';
        $timesheet = 'kimai2_timesheet';
        $users = 'kimai2_users';

        $this->addSql('ALTER TABLE ' . $activities . ' CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
        $this->addSql('ALTER TABLE ' . $customers . ' CHANGE mail email VARCHAR(255) DEFAULT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
        $this->addSql('ALTER TABLE ' . $projects . ' CHANGE budget budget DOUBLE PRECISION NOT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
        $this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE rate rate DOUBLE PRECISION NOT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
        $customers = 'kimai2_customers';
        $projects = 'kimai2_projects';
        $activities = 'kimai2_activities';
        $timesheet = 'kimai2_timesheet';

        $this->addSql('ALTER TABLE ' . $activities . ' CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
        $this->addSql('ALTER TABLE ' . $customers . ' CHANGE email mail VARCHAR(255) DEFAULT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
        $this->addSql('ALTER TABLE ' . $projects . ' CHANGE budget budget NUMERIC(10, 2) NOT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
        $this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE rate rate NUMERIC(10, 2) NOT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
        $this->preventEmptyMigrationWarning();
                {% if week|date_format('m') != nextWeek|date_format('m') %}
                    &ndash;
                    {{ nextWeek|month_name(true) }}
                {% endif %}
    {% set event = actions(app.user, 'project', view, {'project': project}) %}
    {% set event = actions(app.user, 'team', view, {'team': team}) %}
    {% set event = actions(app.user, 'timesheet_team', view, {'timesheet': timesheet}) %}
    {% set event = actions(app.user, 'timesheet', view, {'timesheet': timesheet}) %}
        $this->request($client, '/admin/project/1/duplicate');
        $this->request($client, '/admin/teams/1/duplicate');
        $this->request($client, '/timesheet/' . $newId . '/duplicate');
        $this->request($client, '/team/timesheet/' . $newId . '/duplicate');
<?xml version="1.0" encoding="utf-8"?>
        <target>Special thanks go to …</target>
<?xml version="1.0" encoding="utf-8"?>
Fixed code:     public const VERSION = '1.16.2';
    public const VERSION_ID = 11602;
     * @Route(path="/{id}/duplicate/{token}", name="admin_project_duplicate", methods={"GET", "POST"})
    public function duplicateAction(Project $project, string $token, ProjectDuplicationService $projectDuplicationService, CsrfTokenManagerInterface $csrfTokenManager)
        if (!$csrfTokenManager->isTokenValid(new CsrfToken('project.duplicate', $token))) {
            $this->flashError('action.csrf.error');

            return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
        }

        $csrfTokenManager->refreshToken($token);

        $this->flashSuccess('action.update.success');

use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
     * @Route(path="/{id}/duplicate/{token}", name="team_duplicate", methods={"GET", "POST"})
    public function duplicateTeam(Team $team, string $token, CsrfTokenManagerInterface $csrfTokenManager)
        if (!$csrfTokenManager->isTokenValid(new CsrfToken('team.duplicate', $token))) {
            $this->flashError('action.csrf.error');

            return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
        }

        $csrfTokenManager->refreshToken($token);

    protected function duplicate(Timesheet $timesheet, Request $request, string $renderTemplate, string $token): Response
        $form = $this->getDuplicateForm($copyTimesheet, $timesheet, $token);
    abstract protected function getDuplicateForm(Timesheet $entry, Timesheet $original, string $token): FormInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
     * @Route(path="/{id}/duplicate/{token}", name="timesheet_duplicate", methods={"GET", "POST"})
    public function duplicateAction(Timesheet $entry, Request $request, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
        if (!$csrfTokenManager->isTokenValid(new CsrfToken('timesheet.duplicate', $token))) {
            $this->flashError('action.csrf.error');

            return $this->redirectToRoute('timesheet');
        }

        $csrfTokenManager->refreshToken($token);

        return $this->duplicate($entry, $request, 'timesheet/edit.html.twig', $token);
    protected function getDuplicateForm(Timesheet $entry, Timesheet $original, string $token): FormInterface
        return $this->generateCreateForm($entry, TimesheetEditForm::class, $this->generateUrl('timesheet_duplicate', ['id' => $original->getId(), 'token' => $token]));
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
     * @Route(path="/{id}/duplicate/{token}", name="admin_timesheet_duplicate", methods={"GET", "POST"})
    public function duplicateAction(Timesheet $entry, Request $request, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
        if (!$csrfTokenManager->isTokenValid(new CsrfToken('admin_timesheet.duplicate', $token))) {
            $this->flashError('action.csrf.error');

            return $this->redirectToRoute('admin_timesheet');
        }

        $csrfTokenManager->refreshToken($token);

        return $this->duplicate($entry, $request, 'timesheet-team/edit.html.twig', $token);
    protected function getDuplicateForm(Timesheet $entry, Timesheet $original, string $token): FormInterface
        return $this->generateCreateForm($entry, TimesheetAdminEditForm::class, $this->generateUrl('admin_timesheet_duplicate', ['id' => $original->getId(), 'token' => $token]));
                $event->addAction('copy', ['url' => $this->path($routeDuplicate, ['id' => $timesheet->getId(), 'token' => $payload['token']]), 'class' => $class]);
                ['url' => $this->path('admin_project_duplicate', ['id' => $project->getId(), 'token' => $payload['token']])]
                $event->addAction('copy', ['url' => $this->path('team_duplicate', ['id' => $team->getId(), 'token' => $payload['token']])]);
        $this->addSql('CREATE TABLE kimai2_users (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(60) NOT NULL, mail VARCHAR(160) NOT NULL, password VARCHAR(254) DEFAULT NULL, alias VARCHAR(60) DEFAULT NULL, active TINYINT(1) NOT NULL, registration_date DATETIME DEFAULT NULL, title VARCHAR(50) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', UNIQUE INDEX UNIQ_B9AC5BCE5E237E06 (name), UNIQUE INDEX UNIQ_B9AC5BCE5126AC48 (mail), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
        $this->addSql('CREATE TABLE kimai2_user_preferences (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, name VARCHAR(50) NOT NULL, value VARCHAR(255) DEFAULT NULL, INDEX IDX_8D08F631A76ED395 (user_id), UNIQUE INDEX UNIQ_8D08F631A76ED3955E237E06 (user_id, name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
        $this->addSql('CREATE TABLE kimai2_customers (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(150) NOT NULL, number VARCHAR(50) DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, company VARCHAR(255) DEFAULT NULL, contact VARCHAR(255) DEFAULT NULL, address TEXT DEFAULT NULL, country VARCHAR(2) NOT NULL, currency VARCHAR(3) NOT NULL, phone VARCHAR(255) DEFAULT NULL, fax VARCHAR(255) DEFAULT NULL, mobile VARCHAR(255) DEFAULT NULL, mail VARCHAR(255) DEFAULT NULL, homepage VARCHAR(255) DEFAULT NULL, timezone VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
        $this->addSql('CREATE TABLE kimai2_projects (id INT AUTO_INCREMENT NOT NULL, customer_id INT DEFAULT NULL, name VARCHAR(150) NOT NULL, order_number TINYTEXT DEFAULT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, budget NUMERIC(10, 2) NOT NULL, INDEX IDX_407F12069395C3F3 (customer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
        $this->addSql('CREATE TABLE kimai2_activities (id INT AUTO_INCREMENT NOT NULL, project_id INT DEFAULT NULL, name VARCHAR(150) NOT NULL, comment TEXT DEFAULT NULL, visible TINYINT(1) NOT NULL, INDEX IDX_8811FE1C166D1F9C (project_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
        $this->addSql('CREATE TABLE kimai2_timesheet (id INT AUTO_INCREMENT NOT NULL, user INT DEFAULT NULL, activity_id INT DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INT DEFAULT NULL, description TEXT DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, INDEX IDX_4F60C6B18D93D649 (user), INDEX IDX_4F60C6B181C06096 (activity_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
        $this->addSql('CREATE TABLE kimai2_invoice_templates (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(60) NOT NULL, title VARCHAR(255) NOT NULL, company VARCHAR(255) NOT NULL, address TEXT DEFAULT NULL, due_days INT NOT NULL, vat INT DEFAULT NULL, calculator VARCHAR(20) NOT NULL, number_generator VARCHAR(20) NOT NULL, renderer VARCHAR(20) NOT NULL, payment_terms TEXT DEFAULT NULL, UNIQUE INDEX UNIQ_1626CFE95E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
        $this->addSql('ALTER TABLE kimai2_user_preferences ADD CONSTRAINT FK_8D08F631A76ED395 FOREIGN KEY (user_id) REFERENCES kimai2_users (id) ON DELETE CASCADE');
        $this->addSql('ALTER TABLE kimai2_projects ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES kimai2_customers (id) ON DELETE CASCADE');
        $this->addSql('ALTER TABLE kimai2_activities ADD CONSTRAINT FK_8811FE1C166D1F9C FOREIGN KEY (project_id) REFERENCES kimai2_projects (id) ON DELETE CASCADE');
        $this->addSql('ALTER TABLE kimai2_timesheet ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES kimai2_users (id)');
        $this->addSql('ALTER TABLE kimai2_timesheet ADD CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES kimai2_activities (id) ON DELETE CASCADE');
        $indexesOld = $schema->getTable('kimai2_users')->getIndexes();
                $this->addSql('DROP INDEX ' . $index->getName() . ' ON kimai2_users');
        $this->addSql('ALTER TABLE kimai2_users CHANGE name username VARCHAR(180) NOT NULL, ADD username_canonical VARCHAR(180) NOT NULL, CHANGE mail email VARCHAR(180) NOT NULL, ADD email_canonical VARCHAR(180) NOT NULL, ADD salt VARCHAR(255) DEFAULT NULL, ADD last_login DATETIME DEFAULT NULL, ADD confirmation_token VARCHAR(180) DEFAULT NULL, ADD password_requested_at DATETIME DEFAULT NULL, CHANGE password password VARCHAR(255) NOT NULL, CHANGE alias alias VARCHAR(60) DEFAULT NULL, CHANGE registration_date registration_date DATETIME DEFAULT NULL, CHANGE title title VARCHAR(50) DEFAULT NULL, CHANGE avatar avatar VARCHAR(255) DEFAULT NULL, CHANGE roles roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', CHANGE active enabled TINYINT(1) NOT NULL');
        $this->addSql('UPDATE kimai2_users set username_canonical = username');
        $this->addSql('UPDATE kimai2_users set email_canonical = email');

        $this->addSql('UPDATE kimai2_users SET roles = \'a:1:{i:0;s:16:"ROLE_SUPER_ADMIN";}\' WHERE roles LIKE \'%ROLE_SUPER_ADMIN%\'');
        $this->addSql('UPDATE kimai2_users SET roles = \'a:1:{i:0;s:10:"ROLE_ADMIN";}\' WHERE roles LIKE \'%ROLE_ADMIN%\'');
        $this->addSql('UPDATE kimai2_users SET roles = \'a:1:{i:0;s:13:"ROLE_TEAMLEAD";}\' WHERE roles LIKE \'%ROLE_TEAMLEAD%\'');
        $this->addSql('UPDATE kimai2_users SET roles = \'a:0:{}\' WHERE roles LIKE \'%ROLE_USER%\'');
        $this->addSql('UPDATE kimai2_users SET roles = \'a:1:{i:0;s:13:"ROLE_CUSTOMER";}\' WHERE roles LIKE \'%ROLE_CUSTOMER%\'');

        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE92FC23A8 ON kimai2_users (username_canonical)');
        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEA0D96FBF ON kimai2_users (email_canonical)');
        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEC05FB297 ON kimai2_users (confirmation_token)');
        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEF85E0677 ON kimai2_users (username)');
        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEE7927C74 ON kimai2_users (email)');
            $this->addSql('DROP INDEX ' . $indexName . ' ON kimai2_users');
        $this->addSql('ALTER TABLE kimai2_users CHANGE username name VARCHAR(60) NOT NULL COLLATE utf8mb4_unicode_ci, CHANGE email mail VARCHAR(160) NOT NULL COLLATE utf8mb4_unicode_ci, DROP username_canonical, DROP email_canonical, DROP salt, DROP last_login, DROP confirmation_token, DROP password_requested_at, CHANGE password password VARCHAR(254) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE roles roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', CHANGE alias alias VARCHAR(60) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE registration_date registration_date DATETIME DEFAULT NULL, CHANGE title title VARCHAR(50) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE avatar avatar VARCHAR(255) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE enabled active TINYINT(1) NOT NULL');
        $this->addSql('UPDATE kimai2_users SET roles = \'["ROLE_SUPER_ADMIN"]\' WHERE roles LIKE \'%ROLE_SUPER_ADMIN%\'');
        $this->addSql('UPDATE kimai2_users SET roles = \'["ROLE_ADMIN"]\' WHERE roles LIKE \'%ROLE_ADMIN%\'');
        $this->addSql('UPDATE kimai2_users SET roles = \'["ROLE_TEAMLEAD"]\' WHERE roles LIKE \'%ROLE_TEAMLEAD%\'');
        $this->addSql('UPDATE kimai2_users SET roles = \'["ROLE_USER"]\' WHERE roles LIKE \'%ROLE_USER%\'');
        $this->addSql('UPDATE kimai2_users SET roles = \'["ROLE_CUSTOMER"]\' WHERE roles LIKE \'%ROLE_CUSTOMER%\'');
        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE5E237E06 ON kimai2_users (name)');
        $this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE5126AC48 ON kimai2_users (mail)');
        $usersTable = $schema->getTable('kimai2_users');
        $this->addSql('ALTER TABLE kimai2_timesheet DROP FOREIGN KEY FK_4F60C6B18D93D649');
        $this->addSql('ALTER TABLE kimai2_timesheet ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES kimai2_users (id) ON DELETE CASCADE');
        $this->addSql('ALTER TABLE kimai2_timesheet DROP FOREIGN KEY FK_4F60C6B18D93D649');
        $this->addSql('ALTER TABLE kimai2_timesheet ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES kimai2_users (id)');
        $this->addSql('UPDATE kimai2_invoice_templates SET name=SUBSTRING(name, 1, 60)');
        $this->addSql('ALTER TABLE kimai2_invoice_templates CHANGE name name VARCHAR(60) NOT NULL, CHANGE vat vat DOUBLE PRECISION DEFAULT 0');
        $this->addSql('ALTER TABLE kimai2_invoice_templates CHANGE name name VARCHAR(255) NOT NULL COLLATE utf8mb4_unicode_ci, CHANGE vat vat INT DEFAULT NULL');
        $this->addSql('ALTER TABLE kimai2_projects DROP FOREIGN KEY FK_407F12069395C3F3');
        $this->addSql('ALTER TABLE kimai2_projects CHANGE customer_id customer_id INT NOT NULL');
        $this->addSql('ALTER TABLE kimai2_projects ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES kimai2_customers (id) ON DELETE CASCADE');
        $this->addSql('ALTER TABLE kimai2_timesheet ADD project_id INT DEFAULT NULL AFTER activity_id');
        $this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON kimai2_timesheet (project_id)');
        $this->addSql('UPDATE kimai2_timesheet SET project_id = (SELECT project_id FROM kimai2_activities WHERE id = activity_id)');
        $this->addSql('ALTER TABLE kimai2_timesheet DROP FOREIGN KEY FK_4F60C6B18D93D649');
        $this->addSql('ALTER TABLE kimai2_timesheet DROP FOREIGN KEY FK_4F60C6B181C06096');
        $this->addSql('ALTER TABLE kimai2_timesheet CHANGE project_id project_id INT NOT NULL, CHANGE user user INT NOT NULL, CHANGE activity_id activity_id INT NOT NULL');
        $this->addSql('ALTER TABLE kimai2_timesheet ADD CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES kimai2_projects (id) ON DELETE CASCADE');
        $this->addSql('ALTER TABLE kimai2_timesheet ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES kimai2_users (id) ON DELETE CASCADE');
        $this->addSql('ALTER TABLE kimai2_timesheet ADD CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES kimai2_activities (id) ON DELETE CASCADE');
        $this->addSql('ALTER TABLE kimai2_projects DROP FOREIGN KEY FK_407F12069395C3F3');
        $this->addSql('ALTER TABLE kimai2_projects CHANGE customer_id customer_id INT DEFAULT NULL');
        $this->addSql('ALTER TABLE kimai2_projects ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES kimai2_customers (id) ON DELETE CASCADE');
        $this->addSql('ALTER TABLE kimai2_timesheet DROP FOREIGN KEY FK_4F60C6B1166D1F9C');
        $this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C ON kimai2_timesheet');
        $this->addSql('ALTER TABLE kimai2_timesheet DROP project_id, CHANGE user user INT DEFAULT NULL, CHANGE activity_id activity_id INT DEFAULT NULL');
        $this->addSql('ALTER TABLE kimai2_activities CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
        $this->addSql('ALTER TABLE kimai2_customers CHANGE mail email VARCHAR(255) DEFAULT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
        $this->addSql('ALTER TABLE kimai2_projects CHANGE budget budget DOUBLE PRECISION NOT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
        $this->addSql('ALTER TABLE kimai2_timesheet CHANGE rate rate DOUBLE PRECISION NOT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
        $this->addSql('ALTER TABLE kimai2_activities CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
        $this->addSql('ALTER TABLE kimai2_customers CHANGE email mail VARCHAR(255) DEFAULT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
        $this->addSql('ALTER TABLE kimai2_projects CHANGE budget budget NUMERIC(10, 2) NOT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
        $this->addSql('ALTER TABLE kimai2_timesheet CHANGE rate rate NUMERIC(10, 2) NOT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
        $this->addSql('ALTER TABLE kimai2_timesheet ALTER date_tz DROP DEFAULT');

        $this->preventEmptyMigrationWarning();
                &ndash;
                {{ 'stats.workingTimeWeekShort'|trans({'%week%': week|date_format('W')}) }}
    {% set event = actions(app.user, 'project', view, {'project': project, 'token': csrf_token('project.duplicate')}) %}
    {% set event = actions(app.user, 'team', view, {'team': team, 'token': csrf_token('team.duplicate')}) %}
    {% set event = actions(app.user, 'timesheet_team', view, {'timesheet': timesheet, 'token': csrf_token('admin_timesheet.duplicate')}) %}
    {% set event = actions(app.user, 'timesheet', view, {'timesheet': timesheet, 'token': csrf_token('timesheet.duplicate')}) %}

    protected function assertInvalidCsrfToken(HttpKernelBrowser $client, string $url, string $expectedRedirect)
    {
        $this->request($client, $url);

        $this->assertIsRedirect($client);
        $this->assertRedirectUrl($client, $expectedRedirect);
        $client->followRedirect();
        $this->assertHasFlashError($client, 'The action could not be performed: invalid security token.');
    }

    public function testFlushLogWithInvalidCsrf()
    {
        $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);

        $this->assertInvalidCsrfToken($client, '/doctor/flush-log/rsetdzfukgli78t6r5uedtjfzkugl', $this->createUrl('/doctor'));
    }
        $token = self::$container->get('security.csrf.token_manager')->getToken('project.duplicate');

        $this->request($client, '/admin/project/1/duplicate/' . $token);
    public function testDuplicateActionWithInvalidCsrf()
    {
        $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
        /** @var EntityManager $em */
        $em = $this->getEntityManager();
        $project = $em->find(Project::class, 1);
        $project->setMetaField((new ProjectMeta())->setName('foo')->setValue('bar'));
        $project->setEnd(new \DateTime());
        $em->persist($project);
        $activity = new Activity();
        $activity->setName('blub');
        $activity->setProject($project);
        $activity->setMetaField((new ActivityMeta())->setName('blub')->setValue('blab'));
        $em->persist($activity);
        $em->flush();

        $this->assertInvalidCsrfToken($client, '/admin/project/1/duplicate/rsetdzfukgli78t6r5uedtjfzkugl', $this->createUrl('/admin/project/1/details'));
    }


        $token = self::$container->get('security.csrf.token_manager')->getToken('team.duplicate');

        $this->request($client, '/admin/teams/1/duplicate/' . $token);

    public function testDuplicateActionWithInvalidCsrf()
    {
        $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
        $this->assertInvalidCsrfToken($client, '/admin/teams/1/duplicate/rsetdzfukgli78t6r5uedtjfzkugl', $this->createUrl('/admin/teams/1/edit'));
    }
        $token = self::$container->get('security.csrf.token_manager')->getToken('timesheet.duplicate');

        $this->request($client, '/timesheet/' . $newId . '/duplicate/' . $token);

    public function testDuplicateActionWithInvalidCsrf()
    {
        $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
        $dateTime = new DateTimeFactory(new \DateTimeZone('Europe/London'));

        $fixture = new TimesheetFixtures();
        $fixture->setAmount(1);
        $fixture->setAmountRunning(0);
        $fixture->setUser($this->getUserByRole(User::ROLE_USER));
        $fixture->setStartDate($dateTime->createDateTime());
        $fixture->setCallback(function (Timesheet $timesheet) {
            $timesheet->setDescription('Testing is fun!');
            $begin = clone $timesheet->getBegin();
            $begin->setTime(0, 0, 0);
            $timesheet->setBegin($begin);
            $end = clone $timesheet->getBegin();
            $end->modify('+ 8 hours');
            $timesheet->setEnd($end);
            $timesheet->setFixedRate(2016);
            $timesheet->setHourlyRate(127);
        });

        /** @var Timesheet[] $ids */
        $ids = $this->importFixture($fixture);
        $newId = $ids[0]->getId();

        $this->assertInvalidCsrfToken($client, '/timesheet/' . $newId . '/duplicate/dfghdfghdfghdfghdfgh', $this->createUrl('/timesheet/'));
    }
        $token = self::$container->get('security.csrf.token_manager')->getToken('admin_timesheet.duplicate');

        $this->request($client, '/team/timesheet/' . $newId . '/duplicate/' . $token);

    public function testDuplicateActionWithInvalidCsrf()
    {
        $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
        $dateTime = new DateTimeFactory(new \DateTimeZone('Europe/London'));

        $fixture = new TimesheetFixtures();
        $fixture->setAmount(1);
        $fixture->setAmountRunning(0);
        $fixture->setUser($this->getUserByRole(User::ROLE_USER));
        $fixture->setStartDate($dateTime->createDateTime());
        $fixture->setCallback(function (Timesheet $timesheet) {
            $timesheet->setDescription('Testing is fun!');
            $begin = clone $timesheet->getBegin();
            $begin->setTime(0, 0, 0);
            $timesheet->setBegin($begin);
            $end = clone $timesheet->getBegin();
            $end->modify('+ 8 hours');
            $timesheet->setEnd($end);
            $timesheet->setFixedRate(2016);
            $timesheet->setHourlyRate(127);
        });

        /** @var Timesheet[] $ids */
        $ids = $this->importFixture($fixture);
        $newId = $ids[0]->getId();

        $this->assertInvalidCsrfToken($client, '/team/timesheet/' . $newId . '/duplicate/dfghdfghdfghdfghdfgh', $this->createUrl('/team/timesheet/'));
    }
<?xml version="1.0" encoding="UTF-8"?>
        <target>これらの方々に特に感謝いたします。</target>
<?xml version="1.0" encoding="UTF-8"?>
      <trans-unit resname="action.csrf.error" id="bOE_q5R">
        <source>action.csrf.error</source>
        <target>Αυτή η ενέργεια δεν μπορεί να πραγματοποιηθεί: Μη έγκυρο τεκμήριο ασφάλειας.</target>
      </trans-unit>
      <trans-unit resname="action.csrf.error" id="bOE_q5R">
        <source>action.csrf.error</source>
        <target>L'action n'a pas pu être effectuée : jeton de sécurité invalide.</target>
      </trans-unit>
      <trans-unit resname="action.csrf.error" id="bOE_q5R">
        <source>action.csrf.error</source>
        <target>אי אפשר לבצע את הפעולה: אסימון אבטחה שגוי.</target>
      </trans-unit>
      <trans-unit resname="action.csrf.error" id="bOE_q5R">
        <source>action.csrf.error</source>
        <target>Não foi possível realizar a ação: token de segurança inválido.</target>
      </trans-unit>
      <trans-unit resname="action.csrf.error" id="bOE_q5R">
        <source>action.csrf.error</source>
        <target>A ação não pôde ser realizada: token de segurança inválido.</target>
      </trans-unit>
      <trans-unit resname="action.csrf.error" id="bOE_q5R">
        <source>action.csrf.error</source>
        <target>Eylem gerçekleştirilemedi: geçersiz güvenlik belirteci.</target>
      </trans-unit>
      <trans-unit id="HPhRbtc" resname="stats.workingTimeWeekShort">
        <source>stats.workingTimeWeekShort</source>
        <target>KW %week%</target>
      </trans-unit>
      <trans-unit id="HPhRbtc" resname="stats.workingTimeWeekShort">
        <source>stats.workingTimeWeekShort</source>
        <target>Week %week%</target>
      </trans-unit>