mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 09:18:24 +00:00 
			
		
		
		
	Compare commits
	
		
			6 Commits
		
	
	
		
			425-rename
			...
			295-cancel
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fad7bdf235 | |||
| 8521cea46c | |||
| 4ead7ba761 | |||
| 9721b166eb | |||
| 1b21cd6c33 | |||
| 97860a9487 | 
| @@ -122,7 +122,7 @@ unit_tests: | ||||
|         - php tests/console chill:db:sync-views --env=test | ||||
|         - php -d memory_limit=2G tests/console cache:clear --env=test | ||||
|         - php -d memory_limit=3G tests/console doctrine:fixtures:load -n --env=test | ||||
|         - php -d memory_limit=4G bin/phpunit --colors=never --exclude-group dbIntensive,openstack-integration | ||||
|         - php -d memory_limit=4G bin/phpunit --colors=never --exclude-group dbIntensive,openstack-integration,collabora-integration | ||||
|     artifacts: | ||||
|         expire_in: 1 day | ||||
|         paths: | ||||
|   | ||||
| @@ -0,0 +1,67 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Entity; | ||||
|  | ||||
| use Chill\MainBundle\Doctrine\Model\TrackCreationInterface; | ||||
| use Chill\MainBundle\Doctrine\Model\TrackCreationTrait; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Doctrine\ORM\Mapping as ORM; | ||||
|  | ||||
| /** | ||||
|  * Represents a snapshot of a stored object at a specific point in time. | ||||
|  * | ||||
|  * This entity tracks versions of stored objects, reasons for the snapshot, | ||||
|  * and the user who initiated the action. | ||||
|  */ | ||||
| #[ORM\Entity] | ||||
| #[ORM\Table(name: 'stored_object_point_in_time', schema: 'chill_doc')] | ||||
| class StoredObjectPointInTime implements TrackCreationInterface | ||||
| { | ||||
|     use TrackCreationTrait; | ||||
|  | ||||
|     #[ORM\Id] | ||||
|     #[ORM\GeneratedValue] | ||||
|     #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)] | ||||
|     private ?int $id = null; | ||||
|  | ||||
|     public function __construct( | ||||
|         #[ORM\ManyToOne(targetEntity: StoredObjectVersion::class, inversedBy: 'pointInTimes')] | ||||
|         #[ORM\JoinColumn(name: 'stored_object_version_id', nullable: false)] | ||||
|         private StoredObjectVersion $objectVersion, | ||||
|         #[ORM\Column(name: 'reason', type: 'text', nullable: false, enumType: StoredObjectPointInTimeReasonEnum::class)] | ||||
|         private StoredObjectPointInTimeReasonEnum $reason, | ||||
|         #[ORM\ManyToOne(targetEntity: User::class)] | ||||
|         private ?User $byUser = null, | ||||
|     ) { | ||||
|         $this->objectVersion->addPointInTime($this); | ||||
|     } | ||||
|  | ||||
|     public function getId(): ?int | ||||
|     { | ||||
|         return $this->id; | ||||
|     } | ||||
|  | ||||
|     public function getByUser(): ?User | ||||
|     { | ||||
|         return $this->byUser; | ||||
|     } | ||||
|  | ||||
|     public function getObjectVersion(): StoredObjectVersion | ||||
|     { | ||||
|         return $this->objectVersion; | ||||
|     } | ||||
|  | ||||
|     public function getReason(): StoredObjectPointInTimeReasonEnum | ||||
|     { | ||||
|         return $this->reason; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,18 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Entity; | ||||
|  | ||||
| enum StoredObjectPointInTimeReasonEnum: string | ||||
| { | ||||
|     case KEEP_BEFORE_CONVERSION = 'keep-before-conversion'; | ||||
|     case KEEP_BY_USER = 'keep-by-user'; | ||||
| } | ||||
| @@ -13,6 +13,9 @@ namespace Chill\DocStoreBundle\Entity; | ||||
|  | ||||
| use Chill\MainBundle\Doctrine\Model\TrackCreationInterface; | ||||
| use Chill\MainBundle\Doctrine\Model\TrackCreationTrait; | ||||
| use Doctrine\Common\Collections\ArrayCollection; | ||||
| use Doctrine\Common\Collections\Collection; | ||||
| use Doctrine\Common\Collections\Selectable; | ||||
| use Doctrine\ORM\Mapping as ORM; | ||||
| use Random\RandomException; | ||||
|  | ||||
| @@ -39,6 +42,12 @@ class StoredObjectVersion implements TrackCreationInterface | ||||
|     #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])] | ||||
|     private string $filename = ''; | ||||
|  | ||||
|     /** | ||||
|      * @var Collection<int, StoredObjectPointInTime>&Selectable<int, StoredObjectPointInTime> | ||||
|      */ | ||||
|     #[ORM\OneToMany(mappedBy: 'objectVersion', targetEntity: StoredObjectPointInTime::class, cascade: ['persist', 'remove'], orphanRemoval: true)] | ||||
|     private Collection&Selectable $pointInTimes; | ||||
|  | ||||
|     public function __construct( | ||||
|         /** | ||||
|          * The stored object associated with this version. | ||||
| @@ -77,6 +86,7 @@ class StoredObjectVersion implements TrackCreationInterface | ||||
|         ?string $filename = null, | ||||
|     ) { | ||||
|         $this->filename = $filename ?? self::generateFilename($this); | ||||
|         $this->pointInTimes = new ArrayCollection(); | ||||
|     } | ||||
|  | ||||
|     public static function generateFilename(StoredObjectVersion $storedObjectVersion): string | ||||
| @@ -124,4 +134,40 @@ class StoredObjectVersion implements TrackCreationInterface | ||||
|     { | ||||
|         return $this->version; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return Collection<int, StoredObjectPointInTime>&Selectable<int, StoredObjectPointInTime> | ||||
|      */ | ||||
|     public function getPointInTimes(): Selectable&Collection | ||||
|     { | ||||
|         return $this->pointInTimes; | ||||
|     } | ||||
|  | ||||
|     public function hasPointInTimes(): bool | ||||
|     { | ||||
|         return $this->pointInTimes->count() > 0; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return $this | ||||
|      * | ||||
|      * @internal use @see{StoredObjectPointInTime} constructor instead | ||||
|      */ | ||||
|     public function addPointInTime(StoredObjectPointInTime $storedObjectPointInTime): self | ||||
|     { | ||||
|         if (!$this->pointInTimes->contains($storedObjectPointInTime)) { | ||||
|             $this->pointInTimes->add($storedObjectPointInTime); | ||||
|         } | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function removePointInTime(StoredObjectPointInTime $storedObjectPointInTime): self | ||||
|     { | ||||
|         if ($this->pointInTimes->contains($storedObjectPointInTime)) { | ||||
|             $this->pointInTimes->removeElement($storedObjectPointInTime); | ||||
|         } | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,27 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Repository; | ||||
|  | ||||
| use Chill\DocStoreBundle\Entity\StoredObjectPointInTime; | ||||
| use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | ||||
| use Doctrine\Persistence\ManagerRegistry; | ||||
|  | ||||
| /** | ||||
|  * @template-extends ServiceEntityRepository<StoredObjectPointInTime> | ||||
|  */ | ||||
| class StoredObjectPointInTimeRepository extends ServiceEntityRepository | ||||
| { | ||||
|     public function __construct(ManagerRegistry $registry) | ||||
|     { | ||||
|         parent::__construct($registry, StoredObjectPointInTime::class); | ||||
|     } | ||||
| } | ||||
| @@ -62,7 +62,7 @@ class StoredObjectVersionRepository implements ObjectRepository | ||||
|      * | ||||
|      * @return iterable returns an iterable with the IDs of the versions | ||||
|      */ | ||||
|     public function findIdsByVersionsOlderThanDateAndNotLastVersion(\DateTimeImmutable $beforeDate): iterable | ||||
|     public function findIdsByVersionsOlderThanDateAndNotLastVersionAndNotPointInTime(\DateTimeImmutable $beforeDate): iterable | ||||
|     { | ||||
|         $results = $this->connection->executeQuery( | ||||
|             self::QUERY_FIND_IDS_BY_VERSIONS_OLDER_THAN_DATE_AND_NOT_LAST_VERSION, | ||||
| @@ -83,6 +83,8 @@ class StoredObjectVersionRepository implements ObjectRepository | ||||
|             sov.createdat < ?::timestamp | ||||
|             AND | ||||
|             sov.version < (SELECT MAX(sub_sov.version) FROM chill_doc.stored_object_version sub_sov WHERE sub_sov.stored_object_id = sov.stored_object_id) | ||||
|             AND | ||||
|             NOT EXISTS (SELECT 1 FROM chill_doc.stored_object_point_in_time sub_poi WHERE sub_poi.stored_object_version_id = sov.id) | ||||
|         SQL; | ||||
|  | ||||
|     public function getClassName(): string | ||||
|   | ||||
| @@ -50,7 +50,7 @@ final readonly class RemoveOldVersionCronJob implements CronJobInterface | ||||
|         $deleteBeforeDate = $this->clock->now()->sub(new \DateInterval(self::KEEP_INTERVAL)); | ||||
|         $maxDeleted = $lastExecutionData[self::LAST_DELETED_KEY] ?? 0; | ||||
|  | ||||
|         foreach ($this->storedObjectVersionRepository->findIdsByVersionsOlderThanDateAndNotLastVersion($deleteBeforeDate) as $id) { | ||||
|         foreach ($this->storedObjectVersionRepository->findIdsByVersionsOlderThanDateAndNotLastVersionAndNotPointInTime($deleteBeforeDate) as $id) { | ||||
|             $this->messageBus->dispatch(new RemoveOldVersionMessage($id)); | ||||
|             $maxDeleted = max($maxDeleted, $id); | ||||
|         } | ||||
|   | ||||
| @@ -18,6 +18,7 @@ use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Psr\Log\LoggerInterface; | ||||
| use Symfony\Component\Clock\ClockInterface; | ||||
| use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; | ||||
| use Symfony\Component\Messenger\Handler\MessageHandlerInterface; | ||||
|  | ||||
| /** | ||||
| @@ -49,13 +50,18 @@ final readonly class RemoveOldVersionMessageHandler implements MessageHandlerInt | ||||
|         $this->logger->info(self::LOG_PREFIX.'Received one message', ['storedObjectVersionId' => $message->storedObjectVersionId]); | ||||
|  | ||||
|         $storedObjectVersion = $this->storedObjectVersionRepository->find($message->storedObjectVersionId); | ||||
|         $storedObject = $storedObjectVersion->getStoredObject(); | ||||
|  | ||||
|         if (null === $storedObjectVersion) { | ||||
|             $this->logger->error(self::LOG_PREFIX.'StoredObjectVersion not found in database', ['storedObjectVersionId' => $message->storedObjectVersionId]); | ||||
|             throw new \RuntimeException('StoredObjectVersion not found with id '.$message->storedObjectVersionId); | ||||
|         } | ||||
|  | ||||
|         if ($storedObjectVersion->hasPointInTimes()) { | ||||
|             throw new UnrecoverableMessageHandlingException('the stored object version is now associated with a point in time'); | ||||
|         } | ||||
|  | ||||
|         $storedObject = $storedObjectVersion->getStoredObject(); | ||||
|  | ||||
|         $this->storedObjectManager->delete($storedObjectVersion); | ||||
|         // to ensure an immediate deletion | ||||
|         $this->entityManager->remove($storedObjectVersion); | ||||
|   | ||||
| @@ -0,0 +1,75 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Service; | ||||
|  | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Entity\StoredObjectPointInTime; | ||||
| use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum; | ||||
| use Chill\DocStoreBundle\Entity\StoredObjectVersion; | ||||
| use Chill\DocStoreBundle\Exception\StoredObjectManagerException; | ||||
| use Chill\WopiBundle\Service\WopiConverter; | ||||
| use Symfony\Component\Mime\MimeTypesInterface; | ||||
|  | ||||
| /** | ||||
|  * Class StoredObjectToPdfConverter. | ||||
|  * | ||||
|  * Converts stored objects to PDF or other specified formats using WopiConverter. | ||||
|  */ | ||||
| class StoredObjectToPdfConverter | ||||
| { | ||||
|     public function __construct( | ||||
|         private readonly StoredObjectManagerInterface $storedObjectManager, | ||||
|         private readonly WopiConverter $wopiConverter, | ||||
|         private readonly MimeTypesInterface $mimeTypes, | ||||
|     ) {} | ||||
|  | ||||
|     /** | ||||
|      * Converts the given stored object to a specified format and stores the new version. | ||||
|      * | ||||
|      * @param StoredObject $storedObject the stored object to be converted | ||||
|      * @param string       $lang         the language for the conversion context | ||||
|      * @param string       $convertTo    The target format for the conversion. Default is 'pdf'. | ||||
|      * | ||||
|      * @return array{0: StoredObjectPointInTime, 1: StoredObjectVersion} contains the point in time before conversion and the new version of the stored object | ||||
|      * | ||||
|      * @throws \UnexpectedValueException    if the preferred mime type for the conversion is not found | ||||
|      * @throws \RuntimeException            if the conversion or storage of the new version fails | ||||
|      * @throws StoredObjectManagerException | ||||
|      */ | ||||
|     public function addConvertedVersion(StoredObject $storedObject, string $lang, $convertTo = 'pdf'): array | ||||
|     { | ||||
|         $newMimeType = $this->mimeTypes->getMimeTypes($convertTo)[0] ?? null; | ||||
|  | ||||
|         if (null === $newMimeType) { | ||||
|             throw new \UnexpectedValueException(sprintf('could not find a preferred mime type for conversion to %s', $convertTo)); | ||||
|         } | ||||
|  | ||||
|         $currentVersion = $storedObject->getCurrentVersion(); | ||||
|  | ||||
|         if ($currentVersion->getType() === $newMimeType) { | ||||
|             throw new \UnexpectedValueException('Already at the same mime type'); | ||||
|         } | ||||
|  | ||||
|         $content = $this->storedObjectManager->read($currentVersion); | ||||
|  | ||||
|         try { | ||||
|             $converted = $this->wopiConverter->convert($lang, $content, $newMimeType, $convertTo); | ||||
|         } catch (\RuntimeException $e) { | ||||
|             throw new \RuntimeException('could not store a new version for document', previous: $e); | ||||
|         } | ||||
|  | ||||
|         $pointInTime = new StoredObjectPointInTime($currentVersion, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION); | ||||
|         $version = $this->storedObjectManager->write($storedObject, $converted, $newMimeType); | ||||
|  | ||||
|         return [$pointInTime, $version]; | ||||
|     } | ||||
| } | ||||
| @@ -35,7 +35,7 @@ class StoredObjectVersionRepositoryTest extends KernelTestCase | ||||
|         $repository = new StoredObjectVersionRepository($this->entityManager); | ||||
|  | ||||
|         // get old version, to get a chance to get one | ||||
|         $actual = $repository->findIdsByVersionsOlderThanDateAndNotLastVersion(new \DateTimeImmutable('1970-01-01')); | ||||
|         $actual = $repository->findIdsByVersionsOlderThanDateAndNotLastVersionAndNotPointInTime(new \DateTimeImmutable('1970-01-01')); | ||||
|  | ||||
|         self::assertIsIterable($actual); | ||||
|         self::assertContainsOnly('int', $actual); | ||||
|   | ||||
| @@ -46,7 +46,7 @@ class RemoveOldVersionCronJobTest extends KernelTestCase | ||||
|         $clock = new MockClock(new \DateTimeImmutable('2024-01-01 00:00:00', new \DateTimeZone('+00:00'))); | ||||
|         $repository = $this->createMock(StoredObjectVersionRepository::class); | ||||
|         $repository->expects($this->once()) | ||||
|             ->method('findIdsByVersionsOlderThanDateAndNotLastVersion') | ||||
|             ->method('findIdsByVersionsOlderThanDateAndNotLastVersionAndNotPointInTime') | ||||
|             ->with(new \DateTime('2023-10-03 00:00:00', new \DateTimeZone('+00:00'))) | ||||
|             ->willReturnCallback(function ($arg) { | ||||
|                 yield 1; | ||||
|   | ||||
| @@ -0,0 +1,61 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Tests\Service; | ||||
|  | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Entity\StoredObjectPointInTime; | ||||
| use Chill\DocStoreBundle\Entity\StoredObjectVersion; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter; | ||||
| use Chill\WopiBundle\Service\WopiConverter; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Symfony\Component\Mime\MimeTypes; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class StoredObjectToPdfConverterTest extends TestCase | ||||
| { | ||||
|     use ProphecyTrait; | ||||
|  | ||||
|     public function testAddConvertedVersion(): void | ||||
|     { | ||||
|         $storedObject = new StoredObject(); | ||||
|         $currentVersion = $storedObject->registerVersion(type: 'text/html'); | ||||
|  | ||||
|         $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); | ||||
|         $storedObjectManager->read($currentVersion)->willReturn('1234'); | ||||
|         $storedObjectManager->write($storedObject, '5678', 'application/pdf')->shouldBeCalled() | ||||
|             ->will(function ($args) { | ||||
|                 /** @var StoredObject $storedObject */ | ||||
|                 $storedObject = $args[0]; | ||||
|  | ||||
|                 return $storedObject->registerVersion(type: $args[2]); | ||||
|             }); | ||||
|  | ||||
|         $converter = $this->prophesize(WopiConverter::class); | ||||
|         $converter->convert('fr', '1234', 'application/pdf', 'pdf')->shouldBeCalled() | ||||
|             ->willReturn('5678'); | ||||
|  | ||||
|         $converter = new StoredObjectToPdfConverter($storedObjectManager->reveal(), $converter->reveal(), MimeTypes::getDefault()); | ||||
|  | ||||
|         $actual = $converter->addConvertedVersion($storedObject, 'fr'); | ||||
|  | ||||
|         self::assertIsArray($actual); | ||||
|         self::assertInstanceOf(StoredObjectPointInTime::class, $actual[0]); | ||||
|         self::assertSame($currentVersion, $actual[0]->getObjectVersion()); | ||||
|         self::assertInstanceOf(StoredObjectVersion::class, $actual[1]); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,208 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Tests\Workflow; | ||||
|  | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Entity\StoredObjectPointInTime; | ||||
| use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter; | ||||
| use Chill\DocStoreBundle\Workflow\ConvertToPdfBeforeSignatureStepEventSubscriber; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Chill\MainBundle\Workflow\EntityWorkflowManager; | ||||
| use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore; | ||||
| use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; | ||||
| use Prophecy\Argument; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Symfony\Component\EventDispatcher\EventDispatcher; | ||||
| use Symfony\Component\EventDispatcher\EventSubscriberInterface; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Symfony\Component\HttpFoundation\RequestStack; | ||||
| use Symfony\Component\Workflow\DefinitionBuilder; | ||||
| use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore; | ||||
| use Symfony\Component\Workflow\Registry; | ||||
| use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface; | ||||
| use Symfony\Component\Workflow\Transition; | ||||
| use Symfony\Component\Workflow\Workflow; | ||||
| use Symfony\Component\Workflow\WorkflowInterface; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class ConvertToPdfBeforeSignatureStepEventSubscriberTest extends \PHPUnit\Framework\TestCase | ||||
| { | ||||
|     use ProphecyTrait; | ||||
|  | ||||
|     public function testConvertToPdfBeforeSignatureStepEventSubscriberToSignature(): void | ||||
|     { | ||||
|         $entityWorkflow = new EntityWorkflow(); | ||||
|         $storedObject = new StoredObject(); | ||||
|         $previousVersion = $storedObject->registerVersion(); | ||||
|  | ||||
|         $converter = $this->prophesize(StoredObjectToPdfConverter::class); | ||||
|         $converter->addConvertedVersion($storedObject, 'fr', 'pdf') | ||||
|             ->shouldBeCalledOnce() | ||||
|             ->will(function ($args) { | ||||
|                 /** @var StoredObject $storedObject */ | ||||
|                 $storedObject = $args[0]; | ||||
|  | ||||
|                 $pointInTime = new StoredObjectPointInTime($storedObject->getCurrentVersion(), StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION); | ||||
|                 $newVersion = $storedObject->registerVersion(filename: 'next'); | ||||
|  | ||||
|                 return [$pointInTime, $newVersion]; | ||||
|             }); | ||||
|  | ||||
|         $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class); | ||||
|         $entityWorkflowManager->getAssociatedStoredObject($entityWorkflow)->willReturn($storedObject); | ||||
|  | ||||
|         $request = new Request(); | ||||
|         $request->setLocale('fr'); | ||||
|         $stack = new RequestStack(); | ||||
|         $stack->push($request); | ||||
|  | ||||
|         $eventSubscriber = new ConvertToPdfBeforeSignatureStepEventSubscriber($entityWorkflowManager->reveal(), $converter->reveal(), $stack); | ||||
|  | ||||
|         $registry = $this->buildRegistry($eventSubscriber); | ||||
|         $workflow = $registry->get($entityWorkflow, 'dummy'); | ||||
|  | ||||
|         $dto = new WorkflowTransitionContextDTO($entityWorkflow); | ||||
|         $workflow->apply($entityWorkflow, 'to_signature', ['context' => $dto, 'transition' => 'to_signature', 'transitionAt' => new \DateTimeImmutable('now')]); | ||||
|  | ||||
|         self::assertEquals('signature', $entityWorkflow->getStep()); | ||||
|         self::assertNotSame($previousVersion, $storedObject->getCurrentVersion()); | ||||
|         self::assertTrue($previousVersion->hasPointInTimes()); | ||||
|         self::assertCount(2, $storedObject->getVersions()); | ||||
|         self::assertEquals('next', $storedObject->getCurrentVersion()->getFilename()); | ||||
|     } | ||||
|  | ||||
|     public function testConvertToPdfBeforeSignatureStepEventSubscriberToNotASignatureStep(): void | ||||
|     { | ||||
|         $entityWorkflow = new EntityWorkflow(); | ||||
|         $storedObject = new StoredObject(); | ||||
|         $previousVersion = $storedObject->registerVersion(); | ||||
|  | ||||
|         $converter = $this->prophesize(StoredObjectToPdfConverter::class); | ||||
|         $converter->addConvertedVersion($storedObject, 'fr', 'pdf') | ||||
|             ->shouldNotBeCalled(); | ||||
|  | ||||
|         $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class); | ||||
|         $entityWorkflowManager->getAssociatedStoredObject($entityWorkflow)->willReturn($storedObject); | ||||
|  | ||||
|         $request = new Request(); | ||||
|         $request->setLocale('fr'); | ||||
|         $stack = new RequestStack(); | ||||
|         $stack->push($request); | ||||
|  | ||||
|         $eventSubscriber = new ConvertToPdfBeforeSignatureStepEventSubscriber($entityWorkflowManager->reveal(), $converter->reveal(), $stack); | ||||
|  | ||||
|         $registry = $this->buildRegistry($eventSubscriber); | ||||
|         $workflow = $registry->get($entityWorkflow, 'dummy'); | ||||
|  | ||||
|         $dto = new WorkflowTransitionContextDTO($entityWorkflow); | ||||
|         $workflow->apply($entityWorkflow, 'to_something', ['context' => $dto, 'transition' => 'to_something', 'transitionAt' => new \DateTimeImmutable('now')]); | ||||
|  | ||||
|         self::assertEquals('something', $entityWorkflow->getStep()); | ||||
|         self::assertSame($previousVersion, $storedObject->getCurrentVersion()); | ||||
|         self::assertFalse($previousVersion->hasPointInTimes()); | ||||
|         self::assertCount(1, $storedObject->getVersions()); | ||||
|     } | ||||
|  | ||||
|     public function testConvertToPdfBeforeSignatureStepEventSubscriberToSignatureAlreadyAPdf(): void | ||||
|     { | ||||
|         $entityWorkflow = new EntityWorkflow(); | ||||
|         $storedObject = new StoredObject(); | ||||
|         $previousVersion = $storedObject->registerVersion(type: 'application/pdf'); | ||||
|  | ||||
|         $converter = $this->prophesize(StoredObjectToPdfConverter::class); | ||||
|         $converter->addConvertedVersion($storedObject, 'fr', 'pdf') | ||||
|             ->shouldNotBeCalled(); | ||||
|  | ||||
|         $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class); | ||||
|         $entityWorkflowManager->getAssociatedStoredObject($entityWorkflow)->willReturn($storedObject); | ||||
|  | ||||
|         $request = new Request(); | ||||
|         $request->setLocale('fr'); | ||||
|         $stack = new RequestStack(); | ||||
|         $stack->push($request); | ||||
|  | ||||
|         $eventSubscriber = new ConvertToPdfBeforeSignatureStepEventSubscriber($entityWorkflowManager->reveal(), $converter->reveal(), $stack); | ||||
|  | ||||
|         $registry = $this->buildRegistry($eventSubscriber); | ||||
|         $workflow = $registry->get($entityWorkflow, 'dummy'); | ||||
|  | ||||
|         $dto = new WorkflowTransitionContextDTO($entityWorkflow); | ||||
|         $workflow->apply($entityWorkflow, 'to_signature', ['context' => $dto, 'transition' => 'to_signature', 'transitionAt' => new \DateTimeImmutable('now')]); | ||||
|  | ||||
|         self::assertEquals('signature', $entityWorkflow->getStep()); | ||||
|         self::assertSame($previousVersion, $storedObject->getCurrentVersion()); | ||||
|         self::assertFalse($previousVersion->hasPointInTimes()); | ||||
|         self::assertCount(1, $storedObject->getVersions()); | ||||
|     } | ||||
|  | ||||
|     public function testConvertToPdfBeforeSignatureStepEventSubscriberToSignatureWithNoStoredObject(): void | ||||
|     { | ||||
|         $entityWorkflow = new EntityWorkflow(); | ||||
|  | ||||
|         $converter = $this->prophesize(StoredObjectToPdfConverter::class); | ||||
|         $converter->addConvertedVersion(Argument::type(StoredObject::class), 'fr', 'pdf') | ||||
|             ->shouldNotBeCalled(); | ||||
|  | ||||
|         $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class); | ||||
|         $entityWorkflowManager->getAssociatedStoredObject($entityWorkflow)->willReturn(null); | ||||
|  | ||||
|         $request = new Request(); | ||||
|         $request->setLocale('fr'); | ||||
|         $stack = new RequestStack(); | ||||
|         $stack->push($request); | ||||
|  | ||||
|         $eventSubscriber = new ConvertToPdfBeforeSignatureStepEventSubscriber($entityWorkflowManager->reveal(), $converter->reveal(), $stack); | ||||
|  | ||||
|         $registry = $this->buildRegistry($eventSubscriber); | ||||
|         $workflow = $registry->get($entityWorkflow, 'dummy'); | ||||
|  | ||||
|         $dto = new WorkflowTransitionContextDTO($entityWorkflow); | ||||
|         $workflow->apply($entityWorkflow, 'to_signature', ['context' => $dto, 'transition' => 'to_signature', 'transitionAt' => new \DateTimeImmutable('now')]); | ||||
|  | ||||
|         self::assertEquals('signature', $entityWorkflow->getStep()); | ||||
|     } | ||||
|  | ||||
|     private function buildRegistry(EventSubscriberInterface $eventSubscriber): Registry | ||||
|     { | ||||
|         $builder = new DefinitionBuilder(); | ||||
|         $builder | ||||
|             ->setInitialPlaces('initial') | ||||
|             ->addPlaces(['initial', 'signature', 'something']) | ||||
|             ->addTransition(new Transition('to_something', 'initial', 'something')) | ||||
|             ->addTransition(new Transition('to_signature', 'initial', 'signature')); | ||||
|  | ||||
|         $metadataStore = new InMemoryMetadataStore([], ['signature' => ['isSignature' => ['user']]]); | ||||
|         $builder->setMetadataStore($metadataStore); | ||||
|  | ||||
|         $eventDispatcher = new EventDispatcher(); | ||||
|         $eventDispatcher->addSubscriber($eventSubscriber); | ||||
|  | ||||
|         $workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), $eventDispatcher, 'dummy'); | ||||
|  | ||||
|         $supports = new class () implements WorkflowSupportStrategyInterface { | ||||
|             public function supports(WorkflowInterface $workflow, object $subject): bool | ||||
|             { | ||||
|                 return true; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         $registry = new Registry(); | ||||
|         $registry->addWorkflow($workflow, $supports); | ||||
|  | ||||
|         return $registry; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,75 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Workflow; | ||||
|  | ||||
| use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Chill\MainBundle\Workflow\EntityWorkflowManager; | ||||
| use Symfony\Component\EventDispatcher\EventSubscriberInterface; | ||||
| use Symfony\Component\HttpFoundation\RequestStack; | ||||
| use Symfony\Component\Workflow\Event\CompletedEvent; | ||||
| use Symfony\Component\Workflow\WorkflowEvents; | ||||
|  | ||||
| /** | ||||
|  * Event subscriber to convert objects to PDF when the document reach a signature step. | ||||
|  */ | ||||
| class ConvertToPdfBeforeSignatureStepEventSubscriber implements EventSubscriberInterface | ||||
| { | ||||
|     public function __construct( | ||||
|         private readonly EntityWorkflowManager $entityWorkflowManager, | ||||
|         private readonly StoredObjectToPdfConverter $storedObjectToPdfConverter, | ||||
|         private readonly RequestStack $requestStack, | ||||
|     ) {} | ||||
|  | ||||
|     public static function getSubscribedEvents(): array | ||||
|     { | ||||
|         return [ | ||||
|             WorkflowEvents::COMPLETED => 'convertToPdfBeforeSignatureStepEvent', | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public function convertToPdfBeforeSignatureStepEvent(CompletedEvent $event): void | ||||
|     { | ||||
|         $entityWorkflow = $event->getSubject(); | ||||
|         if (!$entityWorkflow instanceof EntityWorkflow) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $tos = $event->getTransition()->getTos(); | ||||
|         $workflow = $event->getWorkflow(); | ||||
|         $metadataStore = $workflow->getMetadataStore(); | ||||
|  | ||||
|         foreach ($tos as $to) { | ||||
|             $metadata = $metadataStore->getPlaceMetadata($to); | ||||
|             if (array_key_exists('isSignature', $metadata) && 0 < count($metadata['isSignature'])) { | ||||
|                 $this->convertToPdf($entityWorkflow); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private function convertToPdf(EntityWorkflow $entityWorkflow): void | ||||
|     { | ||||
|         $storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow); | ||||
|  | ||||
|         if (null === $storedObject) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if ('application/pdf' === $storedObject->getCurrentVersion()->getType()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $this->storedObjectToPdfConverter->addConvertedVersion($storedObject, $this->requestStack->getCurrentRequest()->getLocale(), 'pdf'); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,49 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\Migrations\DocStore; | ||||
|  | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
|  | ||||
| final class Version20240910093735 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|         return 'Add point in time for stored object version'; | ||||
|     } | ||||
|  | ||||
|     public function up(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('CREATE SEQUENCE chill_doc.stored_object_point_in_time_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); | ||||
|         $this->addSql('CREATE TABLE chill_doc.stored_object_point_in_time (id INT NOT NULL, stored_object_version_id INT NOT NULL, reason TEXT NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, byUser_id INT DEFAULT NULL, createdBy_id INT DEFAULT NULL, PRIMARY KEY(id))'); | ||||
|         $this->addSql('CREATE INDEX IDX_CC83C7B81D0AB8B9 ON chill_doc.stored_object_point_in_time (stored_object_version_id)'); | ||||
|         $this->addSql('CREATE INDEX IDX_CC83C7B8D23C0240 ON chill_doc.stored_object_point_in_time (byUser_id)'); | ||||
|         $this->addSql('CREATE INDEX IDX_CC83C7B83174800F ON chill_doc.stored_object_point_in_time (createdBy_id)'); | ||||
|         $this->addSql('COMMENT ON COLUMN chill_doc.stored_object_point_in_time.createdAt IS \'(DC2Type:datetime_immutable)\''); | ||||
|         $this->addSql('ALTER TABLE chill_doc.stored_object_point_in_time ADD CONSTRAINT FK_CC83C7B81D0AB8B9 FOREIGN KEY (stored_object_version_id) REFERENCES chill_doc.stored_object_version (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|         $this->addSql('ALTER TABLE chill_doc.stored_object_point_in_time ADD CONSTRAINT FK_CC83C7B8D23C0240 FOREIGN KEY (byUser_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|         $this->addSql('ALTER TABLE chill_doc.stored_object_point_in_time ADD CONSTRAINT FK_CC83C7B83174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|         $this->addSql('ALTER TABLE chill_doc.stored_object ALTER prefix SET DEFAULT \'\''); | ||||
|         $this->addSql('ALTER TABLE chill_doc.stored_object_version ALTER filename SET DEFAULT \'\''); | ||||
|     } | ||||
|  | ||||
|     public function down(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('DROP SEQUENCE chill_doc.stored_object_point_in_time_id_seq CASCADE'); | ||||
|         $this->addSql('ALTER TABLE chill_doc.stored_object_point_in_time DROP CONSTRAINT FK_CC83C7B81D0AB8B9'); | ||||
|         $this->addSql('ALTER TABLE chill_doc.stored_object_point_in_time DROP CONSTRAINT FK_CC83C7B8D23C0240'); | ||||
|         $this->addSql('ALTER TABLE chill_doc.stored_object_point_in_time DROP CONSTRAINT FK_CC83C7B83174800F'); | ||||
|         $this->addSql('DROP TABLE chill_doc.stored_object_point_in_time'); | ||||
|         $this->addSql('ALTER TABLE chill_doc.stored_object ALTER prefix DROP DEFAULT'); | ||||
|         $this->addSql('ALTER TABLE chill_doc.stored_object_version ALTER filename DROP DEFAULT'); | ||||
|     } | ||||
| } | ||||
| @@ -140,4 +140,16 @@ class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdate | ||||
|     { | ||||
|         return EntityWorkflowSignatureStateEnum::SIGNED == $this->getState(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return 'person'|'user' | ||||
|      */ | ||||
|     public function getSignerKind(): string | ||||
|     { | ||||
|         if ($this->personSigner instanceof Person) { | ||||
|             return 'person'; | ||||
|         } | ||||
|  | ||||
|         return 'user'; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,12 +4,20 @@ | ||||
|     {% for s in signatures %} | ||||
|         <div class="row row-hover align-items-center"> | ||||
|                 <div class="col-sm-12 col-md-8"> | ||||
|                     {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { | ||||
|                         action: 'show', displayBadge: true, | ||||
|                         targetEntity: { name: 'person', id: s.signer.id }, | ||||
|                         buttonText: s.signer|chill_entity_render_string, | ||||
|                         isDead: s.signer.deathDate is not null | ||||
|                     } %} | ||||
|                     {% if s.signerKind == 'person' %} | ||||
|                         {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { | ||||
|                             action: 'show', displayBadge: true, | ||||
|                             targetEntity: { name: 'person', id: s.signer.id }, | ||||
|                             buttonText: s.signer|chill_entity_render_string, | ||||
|                             isDead: s.signer.deathDate is not null | ||||
|                         } %} | ||||
|                     {% else %} | ||||
|                         {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { | ||||
|                             action: 'show', displayBadge: true, | ||||
|                             targetEntity: { name: 'user', id: s.signer.id }, | ||||
|                             buttonText: s.signer|chill_entity_render_string, | ||||
|                         } %} | ||||
|                     {% endif %} | ||||
|                 </div> | ||||
|                 <div class="col-sm-12 col-md-4"> | ||||
|                         {% if s.isSigned %} | ||||
|   | ||||
| @@ -14,84 +14,44 @@ namespace Chill\WopiBundle\Controller; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManager; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\WopiBundle\Service\WopiConverter; | ||||
| use Psr\Log\LoggerInterface; | ||||
| use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; | ||||
| use Symfony\Component\HttpFoundation\JsonResponse; | ||||
| use Symfony\Component\HttpFoundation\RequestStack; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; | ||||
| use Symfony\Component\Mime\Part\DataPart; | ||||
| use Symfony\Component\Mime\Part\Multipart\FormDataPart; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
| use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; | ||||
| use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; | ||||
| use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; | ||||
| use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; | ||||
| use Symfony\Contracts\HttpClient\HttpClientInterface; | ||||
| use Symfony\Contracts\HttpClient\ResponseInterface; | ||||
|  | ||||
| class ConvertController | ||||
| { | ||||
|     private const LOG_PREFIX = '[convert] '; | ||||
|  | ||||
|     private readonly string $collaboraDomain; | ||||
|  | ||||
|     /** | ||||
|      * @param StoredObjectManager $storedObjectManager | ||||
|      */ | ||||
|     public function __construct( | ||||
|         private readonly HttpClientInterface $httpClient, | ||||
|         private readonly RequestStack $requestStack, | ||||
|         private readonly Security $security, | ||||
|         private readonly StoredObjectManagerInterface $storedObjectManager, | ||||
|         private readonly WopiConverter $wopiConverter, | ||||
|         private readonly LoggerInterface $logger, | ||||
|         ParameterBagInterface $parameters, | ||||
|     ) { | ||||
|         $this->collaboraDomain = $parameters->get('wopi')['server']; | ||||
|     } | ||||
|     ) {} | ||||
|  | ||||
|     public function __invoke(StoredObject $storedObject): Response | ||||
|     public function __invoke(StoredObject $storedObject, Request $request): Response | ||||
|     { | ||||
|         if (!$this->security->getUser() instanceof User) { | ||||
|         if (!($this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN'))) { | ||||
|             throw new AccessDeniedHttpException('User must be authenticated'); | ||||
|         } | ||||
|  | ||||
|         $content = $this->storedObjectManager->read($storedObject); | ||||
|         $query = []; | ||||
|         if (null !== $request = $this->requestStack->getCurrentRequest()) { | ||||
|             $query['lang'] = $request->getLocale(); | ||||
|         } | ||||
|         $lang = $request->getLocale(); | ||||
|  | ||||
|         try { | ||||
|             $url = sprintf('%s/cool/convert-to/pdf', $this->collaboraDomain); | ||||
|             $form = new FormDataPart([ | ||||
|                 'data' => new DataPart($content, $storedObject->getUuid()->toString(), $storedObject->getType()), | ||||
|             ]); | ||||
|             $response = $this->httpClient->request('POST', $url, [ | ||||
|                 'headers' => $form->getPreparedHeaders()->toArray(), | ||||
|                 'query' => $query, | ||||
|                 'body' => $form->bodyToString(), | ||||
|                 'timeout' => 10, | ||||
|             ]); | ||||
|  | ||||
|             return new Response($response->getContent(), Response::HTTP_OK, [ | ||||
|             return new Response($this->wopiConverter->convert($lang, $content, $storedObject->getType()), Response::HTTP_OK, [ | ||||
|                 'Content-Type' => 'application/pdf', | ||||
|             ]); | ||||
|         } catch (ClientExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface|TransportExceptionInterface $exception) { | ||||
|             return $this->onConversionFailed($url, $exception->getResponse()); | ||||
|         } catch (\RuntimeException $exception) { | ||||
|             $this->logger->alert(self::LOG_PREFIX.'Could not convert document', ['message' => $exception->getMessage(), 'exception', $exception->getTraceAsString()]); | ||||
|  | ||||
|             return new Response('convert server not available', Response::HTTP_SERVICE_UNAVAILABLE); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private function onConversionFailed(string $url, ResponseInterface $response): JsonResponse | ||||
|     { | ||||
|         $this->logger->error(self::LOG_PREFIX.' could not convert document', [ | ||||
|             'response_status' => $response->getStatusCode(), | ||||
|             'message' => $response->getContent(false), | ||||
|             'server' => $this->collaboraDomain, | ||||
|             'url' => $url, | ||||
|         ]); | ||||
|  | ||||
|         return new JsonResponse(['message' => 'conversion failed : '.$response->getContent(false)], Response::HTTP_SERVICE_UNAVAILABLE); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										69
									
								
								src/Bundle/ChillWopiBundle/src/Service/WopiConverter.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/Bundle/ChillWopiBundle/src/Service/WopiConverter.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\WopiBundle\Service; | ||||
|  | ||||
| use Psr\Log\LoggerInterface; | ||||
| use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; | ||||
| use Symfony\Component\Mime\Part\DataPart; | ||||
| use Symfony\Component\Mime\Part\Multipart\FormDataPart; | ||||
| use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; | ||||
| use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; | ||||
| use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; | ||||
| use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; | ||||
| use Symfony\Contracts\HttpClient\HttpClientInterface; | ||||
|  | ||||
| /** | ||||
|  * Handles the conversion of documents to PDF using the Collabora Online server. | ||||
|  */ | ||||
| class WopiConverter | ||||
| { | ||||
|     private readonly string $collaboraDomain; | ||||
|  | ||||
|     private const LOG_PREFIX = '[WopiConverterPDF] '; | ||||
|  | ||||
|     public function __construct( | ||||
|         private readonly HttpClientInterface $httpClient, | ||||
|         private readonly LoggerInterface $logger, | ||||
|         ParameterBagInterface $parameters, | ||||
|     ) { | ||||
|         $this->collaboraDomain = $parameters->get('wopi')['server']; | ||||
|     } | ||||
|  | ||||
|     public function convert(string $lang, string $content, string $contentType, $convertTo = 'pdf'): string | ||||
|     { | ||||
|         try { | ||||
|             $url = sprintf('%s/cool/convert-to/%s', $this->collaboraDomain, $convertTo); | ||||
|  | ||||
|             $form = new FormDataPart([ | ||||
|                 'data' => new DataPart($content, uniqid('temp-file-'), contentType: $contentType), | ||||
|             ]); | ||||
|             $response = $this->httpClient->request('POST', $url, [ | ||||
|                 'headers' => $form->getPreparedHeaders()->toArray(), | ||||
|                 'query' => ['lang' => $lang], | ||||
|                 'body' => $form->bodyToString(), | ||||
|                 'timeout' => 10, | ||||
|             ]); | ||||
|  | ||||
|             if (200 === $response->getStatusCode()) { | ||||
|                 $this->logger->info(self::LOG_PREFIX.'document converted successfully', ['size' => strlen($content)]); | ||||
|             } | ||||
|  | ||||
|             return $response->getContent(); | ||||
|         } catch (ClientExceptionInterface $e) { | ||||
|             throw new \LogicException('no correct request to collabora online', previous: $e); | ||||
|         } catch (RedirectionExceptionInterface $e) { | ||||
|             throw new \RuntimeException('no redirection expected', previous: $e); | ||||
|         } catch (ServerExceptionInterface|TransportExceptionInterface $e) { | ||||
|             throw new \RuntimeException('error while converting document', previous: $e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -13,16 +13,12 @@ namespace Chill\WopiBundle\Tests\Controller; | ||||
|  | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\WopiBundle\Controller\ConvertController; | ||||
| use Chill\WopiBundle\Service\WopiConverter; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Psr\Log\NullLogger; | ||||
| use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; | ||||
| use Symfony\Component\HttpClient\MockHttpClient; | ||||
| use Symfony\Component\HttpClient\Response\MockResponse; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Symfony\Component\HttpFoundation\RequestStack; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
|  | ||||
| /** | ||||
| @@ -39,28 +35,27 @@ final class ConvertControllerTest extends TestCase | ||||
|         $storedObject = new StoredObject(); | ||||
|         $storedObject->registerVersion(type: 'application/vnd.oasis.opendocument.text'); | ||||
|  | ||||
|         $httpClient = new MockHttpClient([ | ||||
|             new MockResponse('not authorized', ['http_code' => 401]), | ||||
|         ], 'http://collabora:9980'); | ||||
|  | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->getUser()->willReturn(new User()); | ||||
|         $security->isGranted('ROLE_USER')->willReturn(true); | ||||
|  | ||||
|         $storeManager = $this->prophesize(StoredObjectManagerInterface::class); | ||||
|         $storeManager->read($storedObject)->willReturn('content'); | ||||
|  | ||||
|         $parameterBag = new ParameterBag(['wopi' => ['server' => 'http://collabora:9980']]); | ||||
|         $wopiConverter = $this->prophesize(WopiConverter::class); | ||||
|         $wopiConverter->convert('fr', 'content', 'application/vnd.oasis.opendocument.text') | ||||
|             ->willThrow(new \RuntimeException()); | ||||
|  | ||||
|         $convert = new ConvertController( | ||||
|             $httpClient, | ||||
|             $this->makeRequestStack(), | ||||
|         $controller = new ConvertController( | ||||
|             $security->reveal(), | ||||
|             $storeManager->reveal(), | ||||
|             $wopiConverter->reveal(), | ||||
|             new NullLogger(), | ||||
|             $parameterBag | ||||
|         ); | ||||
|  | ||||
|         $response = $convert($storedObject); | ||||
|         $request = new Request(); | ||||
|         $request->setLocale('fr'); | ||||
|  | ||||
|         $response = $controller($storedObject, $request); | ||||
|  | ||||
|         $this->assertNotEquals(200, $response->getStatusCode()); | ||||
|     } | ||||
| @@ -70,38 +65,29 @@ final class ConvertControllerTest extends TestCase | ||||
|         $storedObject = new StoredObject(); | ||||
|         $storedObject->registerVersion(type: 'application/vnd.oasis.opendocument.text'); | ||||
|  | ||||
|         $httpClient = new MockHttpClient([ | ||||
|             new MockResponse('1234', ['http_code' => 200]), | ||||
|         ], 'http://collabora:9980'); | ||||
|  | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->getUser()->willReturn(new User()); | ||||
|         $security->isGranted('ROLE_USER')->willReturn(true); | ||||
|  | ||||
|         $storeManager = $this->prophesize(StoredObjectManagerInterface::class); | ||||
|         $storeManager->read($storedObject)->willReturn('content'); | ||||
|  | ||||
|         $parameterBag = new ParameterBag(['wopi' => ['server' => 'http://collabora:9980']]); | ||||
|         $wopiConverter = $this->prophesize(WopiConverter::class); | ||||
|         $wopiConverter->convert('fr', 'content', 'application/vnd.oasis.opendocument.text') | ||||
|             ->willReturn('1234'); | ||||
|  | ||||
|         $convert = new ConvertController( | ||||
|             $httpClient, | ||||
|             $this->makeRequestStack(), | ||||
|         $controller = new ConvertController( | ||||
|             $security->reveal(), | ||||
|             $storeManager->reveal(), | ||||
|             $wopiConverter->reveal(), | ||||
|             new NullLogger(), | ||||
|             $parameterBag | ||||
|         ); | ||||
|  | ||||
|         $response = $convert($storedObject); | ||||
|         $request = new Request(); | ||||
|         $request->setLocale('fr'); | ||||
|  | ||||
|         $response = $controller($storedObject, $request); | ||||
|  | ||||
|         $this->assertEquals(200, $response->getStatusCode()); | ||||
|         $this->assertEquals('1234', $response->getContent()); | ||||
|     } | ||||
|  | ||||
|     private function makeRequestStack(): RequestStack | ||||
|     { | ||||
|         $requestStack = new RequestStack(); | ||||
|         $requestStack->push(new Request()); | ||||
|  | ||||
|         return $requestStack; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,63 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\WopiBundle\Tests\Service; | ||||
|  | ||||
| use Chill\WopiBundle\Service\WopiConverter; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Psr\Log\NullLogger; | ||||
| use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; | ||||
| use Symfony\Component\HttpClient\HttpClient; | ||||
| use Symfony\Component\HttpClient\MockHttpClient; | ||||
| use Symfony\Component\HttpClient\Response\MockResponse; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class WopiConvertToPdfTest extends TestCase | ||||
| { | ||||
|     /** | ||||
|      * @group collabora-integration | ||||
|      */ | ||||
|     public function testConvertToPdfWithRealServer(): void | ||||
|     { | ||||
|         $content = file_get_contents(__DIR__.'/fixtures/test-document.odt'); | ||||
|  | ||||
|         $client = HttpClient::create(); | ||||
|         $parameters = new ParameterBag([ | ||||
|             'wopi' => ['server' => $_ENV['EDITOR_SERVER']], | ||||
|         ]); | ||||
|  | ||||
|         $converter = new WopiConverter($client, new NullLogger(), $parameters); | ||||
|  | ||||
|         $actual = $converter->convert('fr', $content, 'application/vnd.oasis.opendocument.text'); | ||||
|  | ||||
|         self::assertIsString($actual); | ||||
|     } | ||||
|  | ||||
|     public function testConvertToPdfWithMock(): void | ||||
|     { | ||||
|         $httpClient = new MockHttpClient([ | ||||
|             new MockResponse('1234', ['http_code' => 200]), | ||||
|         ], 'http://collabora:9980'); | ||||
|         $parameters = new ParameterBag([ | ||||
|             'wopi' => ['server' => 'http://collabora:9980'], | ||||
|         ]); | ||||
|  | ||||
|         $converter = new WopiConverter($httpClient, new NullLogger(), $parameters); | ||||
|  | ||||
|         $actual = $converter->convert('fr', 'content-string', 'application/vnd.oasis.opendocument.text'); | ||||
|  | ||||
|         self::assertEquals('1234', $actual); | ||||
|     } | ||||
| } | ||||
										
											Binary file not shown.
										
									
								
							
		Reference in New Issue
	
	Block a user