diff --git a/migrations/Version20220114111659.php b/migrations/Version20220114111659.php new file mode 100644 index 0000000000000000000000000000000000000000..f55bde648f089f8a8467e556cb140c02b950a309 --- /dev/null +++ b/migrations/Version20220114111659.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +namespace DoctrineMigrations; + +use Doctrine\DBAL\Schema\Schema; +use Doctrine\Migrations\AbstractMigration; + +/** + * Auto-generated Migration: Please modify to your needs! + */ +final class Version20220114111659 extends AbstractMigration +{ + public function getDescription(): string + { + return ''; + } + + public function up(Schema $schema): void + { + // this up() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE capsule ADD password VARCHAR(50) DEFAULT \'\' NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE capsule DROP password'); + } +} diff --git a/src/Builder/CapsuleBuilder.php b/src/Builder/CapsuleBuilder.php index ca49a830f33c313706ef5e35c10ecf98e2b427e9..ea2dca1beabf4beff755e34745592edb1aa0faba 100644 --- a/src/Builder/CapsuleBuilder.php +++ b/src/Builder/CapsuleBuilder.php @@ -15,6 +15,7 @@ class CapsuleBuilder private bool $hasRequiredPreviewLink = false; private bool $hasRequiredEditionLink = false; private bool $hasRequiredUpdateDate = false; + private bool $hasPasswordSet = false; public function __construct() { @@ -50,11 +51,21 @@ class CapsuleBuilder return $this; } - public function withEditionLink(string $edition_link): CapsuleBuilder + private function createEditionLink(): void { - $this->capsule->setEditionLink($edition_link); + ContractHelper::requires( + $this->hasRequiredPreviewLink, + "The call of CapsuleBuilder::withPreviewLink should be " . + "called before CapsuleBuilder::createEditionLink" + ); + ContractHelper::requires( + $this->hasPasswordSet, + "The call of CapsuleBuilder::withPassword should be " . + "called before CapsuleBuilder::createEditionLink" + ); + $this->capsule->setEditionLink($this->capsule->getPreviewLink() . + "/?p=" . $this->capsule->getPassword()); $this->hasRequiredEditionLink = true; - return $this; } public function withUpdateAuthor(User $update_author): CapsuleBuilder @@ -70,8 +81,17 @@ class CapsuleBuilder return $this; } + public function withPassword(string $password): CapsuleBuilder + { + $this->capsule->setPassword($password); + $this->hasPasswordSet = true; + + return $this; + } + public function createCapsule(): Capsule { + $this->createEditionLink(); ContractHelper::requires( $this->hasRequiredName, "The call of CapsuleBuilder::withName should be called before CapsuleBuilder::create" @@ -96,6 +116,10 @@ class CapsuleBuilder $this->hasRequiredUpdateDate, "The call of CapsuleBuilder::withUpdateDate should be called before CapsuleBuilder::create" ); + ContractHelper::requires( + $this->hasPasswordSet, + "The capsule should have its password defined" + ); return $this->capsule; } diff --git a/src/Builder/UserBuilder.php b/src/Builder/UserBuilder.php index 639effabb6c42cc710a71aa33c7baf270c5fc7f4..12b29b7559821d3ce4d6c79aa3c701c368523d9c 100644 --- a/src/Builder/UserBuilder.php +++ b/src/Builder/UserBuilder.php @@ -54,6 +54,10 @@ class UserBuilder public function withPassword(string $password): UserBuilder { + ContractHelper::requires( + $this->hasRequiredSalt, + "The call of UserBuilder::withSalt should be called before UserBuilder::withPassword" + ); $this->user->setPassword($this->password_hasher->hashPassword($this->user, $password)); $this->hasRequiredPassword = true; return $this; diff --git a/src/Controller/CapsuleController.php b/src/Controller/CapsuleController.php index fc19bcc0f147acd5e04fce69f7f5dc93ecf86cab..6591914ff9bba923e51b64ae4eb89baef4237393 100644 --- a/src/Controller/CapsuleController.php +++ b/src/Controller/CapsuleController.php @@ -3,6 +3,7 @@ namespace App\Controller; use App\Entity\User; +use App\Helper\StringHelper; use App\Repository\CapsuleRepository; use App\Builder\CapsuleBuilder; use App\Form\CreateCapsuleFormType; @@ -51,8 +52,8 @@ class CapsuleController extends AbstractController $new_date_time = new \DateTime(); $capsule_name = $form->get('name')->getData(); $video_url = htmlspecialchars($form->get('video_url')->getData()); + $password = StringHelper::generateRandomHashedString(); $preview_link = Uuid::v4(); - $edition_link = $preview_link . '/?p=edit'; $entityManager = $this->getDoctrine()->getManager(); $capsule_builder = new CapsuleBuilder(); @@ -61,8 +62,8 @@ class CapsuleController extends AbstractController ->withCreationAuthor($current_user) ->withCreationDate($new_date_time) ->withPreviewLink($preview_link) - ->withEditionLink($edition_link) ->withUpdateDate($new_date_time) + ->withPassword($password) ->createCapsule(); $entityManager->persist($capsule); diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 051935a01aa2f22fe6a20f1135dd82cbde11f696..3e089828a1f124c1c4e7e8fb1dc89ca6ed188744 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -7,6 +7,7 @@ use App\Exception\ZipArchiveNotOpeningException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Config\Util\Exception\XmlParsingException; use Symfony\Component\Filesystem\Exception\FileNotFoundException; +use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; @@ -46,6 +47,8 @@ class ProjectController extends AbstractController $this->addProjectVideoUrlInXMLProjectFile($capsule_directory, $video_url); + $this->createOrUpdatePasswordFile($capsule_directory, $capsule->getPassword()); + $this->addFlash( 'capsule_created_success', $translator->trans( @@ -92,4 +95,10 @@ class ProjectController extends AbstractController throw new XmlParsingException('Video URL could not be written in XML project file'); } } + + private function createOrUpdatePasswordFile(string $capsule_directory, string $password): void + { + $project_password_file = $capsule_directory . "/file/projectPassword.txt"; + file_put_contents($project_password_file, $password); + } } diff --git a/src/DataFixtures/CapsuleFixtures.php b/src/DataFixtures/CapsuleFixtures.php index 56b6ec4891a0062c27d6354b9f1de1b2a174a43f..0f2cb8ddbdd7365f38723aa15bf672d67087c422 100644 --- a/src/DataFixtures/CapsuleFixtures.php +++ b/src/DataFixtures/CapsuleFixtures.php @@ -38,13 +38,14 @@ class CapsuleFixtures extends Fixture implements DependentFixtureInterface $new_date_time ) { $uuid = Uuid::v4(); + $password = sha1(random_bytes(100)); $builder ->withName("Pomme") ->withCreationAuthor($verified_user1) ->withCreationDate($new_date_time) ->withUpdateDate($new_date_time) ->withPreviewLink($uuid) - ->withEditionLink($uuid . '/?p=edit'); + ->withPassword($password); } ); @@ -54,13 +55,14 @@ class CapsuleFixtures extends Fixture implements DependentFixtureInterface $new_date_time ) { $uuid = Uuid::v4(); + $password = sha1(random_bytes(100)); $builder ->withName("Adele") ->withCreationAuthor($verified_user2) ->withCreationDate($new_date_time) ->withUpdateDate($new_date_time) ->withPreviewLink($uuid) - ->withEditionLink($uuid . '/?p=edit'); + ->withPassword($password); } ); diff --git a/src/Entity/Capsule.php b/src/Entity/Capsule.php index ef40356cb52556d41817803303b43d49a046f3cd..3b4a39d34504ce6550bee9852cb9c451d13c6d4b 100644 --- a/src/Entity/Capsule.php +++ b/src/Entity/Capsule.php @@ -70,6 +70,13 @@ class Capsule */ private string $edition_link; + /** + * @var string the capsule edit password access + * + * @ORM\Column (name="password", type="string", length=50, nullable=false, options={"default":""}) + */ + private string $password; + public function getId(): int { return $this->id; @@ -155,4 +162,14 @@ class Capsule { $this->updated_date = $update_date; } + + public function getPassword(): string + { + return $this->password; + } + + public function setPassword(string $password): void + { + $this->password = $password; + } } diff --git a/src/Helper/StringHelper.php b/src/Helper/StringHelper.php new file mode 100644 index 0000000000000000000000000000000000000000..741c4b84bb14d94b471d081622462c3e1389d207 --- /dev/null +++ b/src/Helper/StringHelper.php @@ -0,0 +1,11 @@ +<?php + +namespace App\Helper; + +class StringHelper +{ + public static function generateRandomHashedString(): string + { + return strtoupper(sha1(random_bytes(100))); + } +} diff --git a/tests/functional/ProjectControllerTest.php b/tests/functional/ProjectControllerTest.php index 0935ff7d8ce2025635d1ffe7f51f3a9164801c5f..8687740c2ca4321febcc4f93b38d65e5ac6b2753 100644 --- a/tests/functional/ProjectControllerTest.php +++ b/tests/functional/ProjectControllerTest.php @@ -87,6 +87,11 @@ class ProjectControllerTest extends WebTestCase return $this->getCapsuleDirPath($capsule_directory) . '/file/project.xml'; } + private function getPasswordFilePath(string $capsule_directory): string + { + return $this->getCapsuleDirPath($capsule_directory) . '/file/projectPassword.txt'; + } + public function testProjectDirectoryWithCorrespondingXMLFileIsCreatedWhenCapsuleCreationIsSuccessful(): void { $video_url = "https://TestUrl"; @@ -129,4 +134,45 @@ class ProjectControllerTest extends WebTestCase $this->assertEquals($video_url, $video_url_in_xml_file); $this->assertSame(self::CAPSULE_NAME, $capsule_name_in_db); } + + public function testProjectDirectoryWithCorrespondingPasswordFileIsCreatedWhenCapsuleCreationIsSuccessful(): void + { + $video_url = "https://TestUrl"; + $this->form['create_capsule_form[name]'] = self::CAPSULE_NAME; + $this->form['create_capsule_form[video_url]'] = $video_url; + + $this->client->submit($this->form); + + $this->assertResponseRedirects( + '/my_capsules', + 302, + 'Once the capsule is created, the user should be redirected to its capsules lists' + ); + + $this->client->followRedirect(); + $this->assertResponseIsSuccessful('/my_capsules'); + + $capsule_repository = $this->object_manager->getRepository(Capsule::class); + $capsule_in_db = $capsule_repository->findOneBy(['name' => self::CAPSULE_NAME]); + + if (! $capsule_in_db instanceof Capsule) { + throw new \Exception("Capsule does not exist."); + } + + $capsule_name_in_db = $capsule_in_db->getName(); + $capsule_directory = $capsule_in_db->getPreviewLink(); + + $this->assertDirectoryExists($capsule_directory); + $this->assertDirectoryIsReadable($capsule_directory); + + $password_file_path = $this->getPasswordFilePath($capsule_directory); + $password = file_get_contents($password_file_path, true); + + $this->assertTrue(false !== $password, "The projectPassword.txt should be readable"); + $this->assertSame( + $capsule_in_db->getPassword(), + $password, + "The password should be saved in projectPassword.txt file" + ); + } }