mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-08-23 08:03:49 +00:00
Resolve merge with master
This commit is contained in:
@@ -12,8 +12,12 @@ declare(strict_types=1);
|
||||
namespace Chill\ActivityBundle\DataFixtures\ORM;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Entity\ActivityReason;
|
||||
use Chill\ActivityBundle\Entity\ActivityType;
|
||||
use Chill\MainBundle\DataFixtures\ORM\LoadScopes;
|
||||
use Chill\MainBundle\DataFixtures\ORM\LoadUsers;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
@@ -32,12 +36,12 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
|
||||
$this->faker = FakerFactory::create('fr_FR');
|
||||
}
|
||||
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 16400;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$persons = $this->em
|
||||
->getRepository(Person::class)
|
||||
@@ -84,49 +88,41 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
|
||||
|
||||
/**
|
||||
* Return a random activityReason.
|
||||
*
|
||||
* @return \Chill\ActivityBundle\Entity\ActivityReason
|
||||
*/
|
||||
private function getRandomActivityReason()
|
||||
private function getRandomActivityReason(): ActivityReason
|
||||
{
|
||||
$reasonRef = LoadActivityReason::$references[array_rand(LoadActivityReason::$references)];
|
||||
|
||||
return $this->getReference($reasonRef);
|
||||
return $this->getReference($reasonRef, ActivityReason::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a random activityType.
|
||||
*
|
||||
* @return \Chill\ActivityBundle\Entity\ActivityType
|
||||
*/
|
||||
private function getRandomActivityType()
|
||||
private function getRandomActivityType(): ActivityType
|
||||
{
|
||||
$typeRef = LoadActivityType::$references[array_rand(LoadActivityType::$references)];
|
||||
|
||||
return $this->getReference($typeRef);
|
||||
return $this->getReference($typeRef, ActivityType::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a random scope.
|
||||
*
|
||||
* @return \Chill\MainBundle\Entity\Scope
|
||||
*/
|
||||
private function getRandomScope()
|
||||
private function getRandomScope(): Scope
|
||||
{
|
||||
$scopeRef = LoadScopes::$references[array_rand(LoadScopes::$references)];
|
||||
|
||||
return $this->getReference($scopeRef);
|
||||
return $this->getReference($scopeRef, Scope::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a random user.
|
||||
*
|
||||
* @return \Chill\MainBundle\Entity\User
|
||||
*/
|
||||
private function getRandomUser()
|
||||
private function getRandomUser(): User
|
||||
{
|
||||
$userRef = array_rand(LoadUsers::$refs);
|
||||
|
||||
return $this->getReference($userRef);
|
||||
return $this->getReference($userRef, User::class);
|
||||
}
|
||||
}
|
||||
|
@@ -38,7 +38,7 @@ class LoadActivityNotifications extends AbstractFixture implements DependentFixt
|
||||
],
|
||||
];
|
||||
|
||||
public function getDependencies()
|
||||
public function getDependencies(): array
|
||||
{
|
||||
return [
|
||||
LoadActivity::class,
|
||||
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\ActivityBundle\DataFixtures\ORM;
|
||||
|
||||
use Chill\ActivityBundle\Entity\ActivityReason;
|
||||
use Chill\ActivityBundle\Entity\ActivityReasonCategory;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
@@ -23,12 +24,12 @@ class LoadActivityReason extends AbstractFixture implements OrderedFixtureInterf
|
||||
{
|
||||
public static $references = [];
|
||||
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 16300;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$reasons = [
|
||||
[
|
||||
@@ -56,7 +57,7 @@ class LoadActivityReason extends AbstractFixture implements OrderedFixtureInterf
|
||||
$activityReason = (new ActivityReason())
|
||||
->setName($r['name'])
|
||||
->setActive(true)
|
||||
->setCategory($this->getReference($r['category']));
|
||||
->setCategory($this->getReference($r['category'], ActivityReasonCategory::class));
|
||||
$manager->persist($activityReason);
|
||||
$reference = 'activity_reason_'.$r['name']['en'];
|
||||
$this->addReference($reference, $activityReason);
|
||||
|
@@ -21,12 +21,12 @@ use Doctrine\Persistence\ObjectManager;
|
||||
*/
|
||||
class LoadActivityReasonCategory extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 16200;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$categs = [
|
||||
['name' => ['fr' => 'Logement', 'en' => 'Housing', 'nl' => 'Woning']],
|
||||
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\ActivityBundle\DataFixtures\ORM;
|
||||
|
||||
use Chill\ActivityBundle\Entity\ActivityType;
|
||||
use Chill\ActivityBundle\Entity\ActivityTypeCategory;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
@@ -23,12 +24,12 @@ class LoadActivityType extends Fixture implements OrderedFixtureInterface
|
||||
{
|
||||
public static $references = [];
|
||||
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 16100;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$types = [
|
||||
// Exange
|
||||
@@ -57,7 +58,7 @@ class LoadActivityType extends Fixture implements OrderedFixtureInterface
|
||||
echo 'Creating activity type : '.$t['name']['fr'].' (cat:'.$t['category']." \n";
|
||||
$activityType = (new ActivityType())
|
||||
->setName($t['name'])
|
||||
->setCategory($this->getReference('activity_type_cat_'.$t['category']))
|
||||
->setCategory($this->getReference('activity_type_cat_'.$t['category'], ActivityTypeCategory::class))
|
||||
->setSocialIssuesVisible(1)
|
||||
->setSocialActionsVisible(1);
|
||||
$manager->persist($activityType);
|
||||
|
@@ -23,12 +23,12 @@ class LoadActivityTypeCategory extends Fixture implements OrderedFixtureInterfac
|
||||
{
|
||||
public static $references = [];
|
||||
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 16050;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$categories = [
|
||||
[
|
||||
|
@@ -15,7 +15,9 @@ use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter;
|
||||
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
|
||||
use Chill\MainBundle\DataFixtures\ORM\LoadPermissionsGroup;
|
||||
use Chill\MainBundle\DataFixtures\ORM\LoadScopes;
|
||||
use Chill\MainBundle\Entity\PermissionsGroup;
|
||||
use Chill\MainBundle\Entity\RoleScope;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
@@ -26,18 +28,18 @@ use Doctrine\Persistence\ObjectManager;
|
||||
*/
|
||||
class LoadActivitytACL extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 16000;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
foreach (LoadPermissionsGroup::$refs as $permissionsGroupRef) {
|
||||
$permissionsGroup = $this->getReference($permissionsGroupRef);
|
||||
$permissionsGroup = $this->getReference($permissionsGroupRef, PermissionsGroup::class);
|
||||
|
||||
foreach (LoadScopes::$references as $scopeRef) {
|
||||
$scope = $this->getReference($scopeRef);
|
||||
$scope = $this->getReference($scopeRef, Scope::class);
|
||||
// create permission group
|
||||
switch ($permissionsGroup->getName()) {
|
||||
case 'social':
|
||||
|
@@ -107,7 +107,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
private ?Person $person = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, \Chill\PersonBundle\Entity\Person>
|
||||
* @var Collection<int, Person>
|
||||
*/
|
||||
#[Groups(['read', 'docgen:read'])]
|
||||
#[ORM\ManyToMany(targetEntity: Person::class)]
|
||||
@@ -132,7 +132,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
private string $sentReceived = '';
|
||||
|
||||
/**
|
||||
* @var Collection<int, \Chill\PersonBundle\Entity\SocialWork\SocialAction>
|
||||
* @var Collection<int, SocialAction>
|
||||
*/
|
||||
#[Groups(['read', 'docgen:read'])]
|
||||
#[ORM\ManyToMany(targetEntity: SocialAction::class)]
|
||||
|
@@ -32,8 +32,8 @@ class ActivityReason
|
||||
#[ORM\GeneratedValue(strategy: 'AUTO')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
|
||||
private array $name;
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '{}', 'jsonb' => true])]
|
||||
private array $name = [];
|
||||
|
||||
/**
|
||||
* Get active.
|
||||
|
@@ -31,11 +31,9 @@ class ActivityReasonCategory implements \Stringable
|
||||
#[ORM\GeneratedValue(strategy: 'AUTO')]
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
|
||||
private $name;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '{}', 'jsonb' => true])]
|
||||
private array $name = [];
|
||||
|
||||
/**
|
||||
* Array of ActivityReason.
|
||||
@@ -127,11 +125,9 @@ class ActivityReasonCategory implements \Stringable
|
||||
/**
|
||||
* Set name.
|
||||
*
|
||||
* @param array $name
|
||||
*
|
||||
* @return ActivityReasonCategory
|
||||
*/
|
||||
public function setName($name)
|
||||
public function setName(array $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
|
@@ -12,6 +12,8 @@ declare(strict_types=1);
|
||||
namespace Chill\ActivityBundle\Repository;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
@@ -23,7 +25,7 @@ use Doctrine\Persistence\ManagerRegistry;
|
||||
* @method Activity[] findAll()
|
||||
* @method Activity[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class ActivityRepository extends ServiceEntityRepository
|
||||
class ActivityRepository extends ServiceEntityRepository implements AssociatedEntityToStoredObjectInterface
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
@@ -97,4 +99,16 @@ class ActivityRepository extends ServiceEntityRepository
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?Activity
|
||||
{
|
||||
$qb = $this->createQueryBuilder('a');
|
||||
$query = $qb
|
||||
->leftJoin('a.documents', 'ad')
|
||||
->where('ad.id = :storedObjectId')
|
||||
->setParameter('storedObjectId', $storedObject->getId())
|
||||
->getQuery();
|
||||
|
||||
return $query->getOneOrNullResult();
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,54 @@
|
||||
<?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\ActivityBundle\Security\Authorization;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Repository\ActivityRepository;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ActivityRepository $repository,
|
||||
Security $security,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
) {
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
{
|
||||
return $this->repository;
|
||||
}
|
||||
|
||||
protected function getClass(): string
|
||||
{
|
||||
return Activity::class;
|
||||
}
|
||||
|
||||
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
|
||||
{
|
||||
return match ($attribute) {
|
||||
StoredObjectRoleEnum::EDIT => ActivityVoter::UPDATE,
|
||||
StoredObjectRoleEnum::SEE => ActivityVoter::SEE_DETAILS,
|
||||
};
|
||||
}
|
||||
|
||||
protected function canBeAssociatedWithWorkflow(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
<?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\Activity;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20240918142723 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Fix not null and default values for activityreason, activityreasoncategory, comments on activity';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE activity ALTER privatecomment_comments SET NOT NULL');
|
||||
$this->addSql('ALTER TABLE activityreason ALTER name SET DEFAULT \'{}\'');
|
||||
$this->addSql('ALTER TABLE activityreason ALTER name SET NOT NULL');
|
||||
$this->addSql('ALTER INDEX idx_654a2fcd12469de2 RENAME TO IDX_AF82522312469DE2');
|
||||
$this->addSql('ALTER TABLE activityreasoncategory ALTER name SET DEFAULT \'{}\'');
|
||||
$this->addSql('ALTER TABLE activityreasoncategory ALTER name SET NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE activityreason ALTER name DROP DEFAULT');
|
||||
$this->addSql('ALTER TABLE activityreason ALTER name DROP NOT NULL');
|
||||
$this->addSql('ALTER TABLE activityreasoncategory ALTER name DROP DEFAULT');
|
||||
$this->addSql('ALTER TABLE activityreasoncategory ALTER name DROP NOT NULL');
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
<?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\Activity;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20241202173942 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add a unique constraint on the storedobject linked to an activity';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql(
|
||||
'WITH duplicate_activities AS (
|
||||
SELECT storedobject_id, array_agg(activity_id ORDER BY activity_id DESC) AS activities
|
||||
FROM activity_storedobject
|
||||
GROUP BY storedobject_id
|
||||
HAVING count(*) > 1
|
||||
)
|
||||
DELETE FROM activity_storedobject
|
||||
WHERE activity_id IN (
|
||||
SELECT unnest(activities[2:]) -- Keep the highest ID, delete the rest
|
||||
FROM duplicate_activities
|
||||
);'
|
||||
);
|
||||
$this->addSql('CREATE UNIQUE INDEX unique_storedobject_id ON activity_storedobject (storedobject_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX IF EXISTS unique_storedobject_id');
|
||||
}
|
||||
}
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\AsideActivityBundle\DataFixtures\ORM;
|
||||
|
||||
use Chill\AsideActivityBundle\Entity\AsideActivity;
|
||||
use Chill\AsideActivityBundle\Entity\AsideActivityCategory;
|
||||
use Chill\MainBundle\DataFixtures\ORM\LoadUsers;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
@@ -30,7 +31,7 @@ class LoadAsideActivity extends Fixture implements DependentFixtureInterface
|
||||
];
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$user = $this->userRepository->findOneBy(['username' => 'center a_social']);
|
||||
|
||||
@@ -43,7 +44,7 @@ class LoadAsideActivity extends Fixture implements DependentFixtureInterface
|
||||
->setUpdatedAt(new \DateTimeImmutable('now'))
|
||||
->setUpdatedBy($user)
|
||||
->setType(
|
||||
$this->getReference('aside_activity_category_0')
|
||||
$this->getReference('aside_activity_category_0', AsideActivityCategory::class)
|
||||
)
|
||||
->setDate((new \DateTimeImmutable('today'))
|
||||
->sub(new \DateInterval('P'.\random_int(1, 100).'D')));
|
||||
|
@@ -16,7 +16,7 @@ use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
class LoadAsideActivityCategory extends \Doctrine\Bundle\FixturesBundle\Fixture
|
||||
{
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
foreach (
|
||||
[
|
||||
|
@@ -37,13 +37,13 @@ class ChargeKind
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255, options: ['default' => ''], nullable: false)]
|
||||
private string $kind = '';
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, length: 255, options: ['default' => '[]'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, length: 255, options: ['default' => '{}', 'jsonb' => true])]
|
||||
private array $name = [];
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::FLOAT, options: ['default' => '0.00'])]
|
||||
private float $ordering = 0.00;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, length: 255, options: ['default' => '[]'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, length: 255, options: ['default' => '{}', 'jsonb' => true])]
|
||||
private array $tags = [];
|
||||
|
||||
public function getId(): ?int
|
||||
|
@@ -39,13 +39,13 @@ class ResourceKind
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255, nullable: false, options: ['default' => ''])]
|
||||
private string $kind = '';
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, length: 255, options: ['default' => '[]'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, length: 255, options: ['default' => '{}', 'jsonb' => true])]
|
||||
private array $name = [];
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::FLOAT, options: ['default' => '0.00'])]
|
||||
private float $ordering = 0.00;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, length: 255, options: ['default' => '[]'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, length: 255, options: ['default' => '{}', 'jsonb' => true])]
|
||||
private array $tags = [];
|
||||
|
||||
public function getId(): ?int
|
||||
|
@@ -37,7 +37,7 @@ class LoadCalendarACL extends Fixture implements OrderedFixtureInterface
|
||||
|
||||
foreach (LoadPermissionsGroup::$refs as $permissionGroupRef) {
|
||||
/** @var PermissionsGroup $group */
|
||||
$group = $this->getReference($permissionGroupRef);
|
||||
$group = $this->getReference($permissionGroupRef, PermissionsGroup::class);
|
||||
|
||||
foreach ($roleScopes as $scope) {
|
||||
$group->addRoleScope($scope);
|
||||
|
@@ -63,6 +63,6 @@ class LoadInvite extends Fixture implements FixtureGroupInterface
|
||||
{
|
||||
$userRef = array_rand(LoadUsers::$refs);
|
||||
|
||||
return $this->getReference($userRef);
|
||||
return $this->getReference($userRef, User::class);
|
||||
}
|
||||
}
|
||||
|
@@ -103,7 +103,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
|
||||
private int $dateTimeVersion = 0;
|
||||
|
||||
/**
|
||||
* @var Collection<int, \Chill\CalendarBundle\Entity\CalendarDoc>
|
||||
* @var Collection<int, CalendarDoc>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'calendar', targetEntity: CalendarDoc::class, orphanRemoval: true)]
|
||||
private Collection $documents;
|
||||
@@ -120,7 +120,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* @var \Doctrine\Common\Collections\Collection<int, \Chill\CalendarBundle\Entity\Invite>&Selectable
|
||||
* @var Collection<int, Invite>&Selectable
|
||||
*/
|
||||
#[Serializer\Groups(['read', 'docgen:read'])]
|
||||
#[ORM\OneToMany(mappedBy: 'calendar', targetEntity: Invite::class, cascade: ['persist', 'remove', 'merge', 'detach'], orphanRemoval: true)]
|
||||
|
@@ -18,8 +18,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Service\ShortMessageNotification;
|
||||
|
||||
use Monolog\DateTimeImmutable;
|
||||
|
||||
/**
|
||||
* * Lundi => Envoi des rdv du mardi et mercredi.
|
||||
* * Mardi => Envoi des rdv du jeudi.
|
||||
@@ -31,7 +29,7 @@ class DefaultRangeGenerator implements RangeGeneratorInterface
|
||||
{
|
||||
public function generateRange(\DateTimeImmutable $date): ?array
|
||||
{
|
||||
$onMidnight = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date->format('Y-m-d').' 00:00:00');
|
||||
$onMidnight = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date->format('Y-m-d').' 00:00:00');
|
||||
|
||||
switch ($dow = (int) $onMidnight->format('w')) {
|
||||
case 6: // Saturday
|
||||
|
@@ -46,12 +46,12 @@ class LoadOption extends AbstractFixture implements OrderedFixtureInterface
|
||||
$this->fakerNl = \Faker\Factory::create('nl_NL');
|
||||
}
|
||||
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 1000;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
echo "Loading Options \n";
|
||||
|
||||
|
@@ -48,7 +48,7 @@ class CustomField
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::FLOAT)]
|
||||
private ?float $ordering = null;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN)]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])]
|
||||
private false $required = false;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
|
||||
|
@@ -33,7 +33,7 @@ class Option
|
||||
#[ORM\GeneratedValue(strategy: 'AUTO')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 50, name: 'internal_key')]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 50, name: 'internal_key', options: ['default' => ''])]
|
||||
private string $internalKey = '';
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 15)]
|
||||
|
@@ -49,7 +49,7 @@ class CustomFieldsGroup
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
|
||||
private array|string $name;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: true, options: ['default' => '{}', 'jsonb' => true])]
|
||||
private array $options = [];
|
||||
|
||||
/**
|
||||
|
@@ -17,7 +17,7 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
final readonly class ContextManager implements ContextManagerInterface
|
||||
{
|
||||
/**
|
||||
* @param \Chill\DocGeneratorBundle\Context\DocGeneratorContextInterface[] $contexts
|
||||
* @param DocGeneratorContextInterface[] $contexts
|
||||
*/
|
||||
public function __construct(private iterable $contexts) {}
|
||||
|
||||
|
@@ -23,7 +23,7 @@ use Doctrine\Persistence\ObjectManager;
|
||||
*/
|
||||
class LoadDocGeneratorTemplate extends AbstractFixture
|
||||
{
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$templates = [
|
||||
[
|
||||
@@ -54,12 +54,15 @@ class LoadDocGeneratorTemplate extends AbstractFixture
|
||||
];
|
||||
|
||||
foreach ($templates as $template) {
|
||||
$newStoredObj = (new StoredObject())
|
||||
->setFilename($template['file']['filename'])
|
||||
->setKeyInfos(json_decode($template['file']['key'], true))
|
||||
->setIv(json_decode($template['file']['iv'], true))
|
||||
$newStoredObj = (new StoredObject());
|
||||
|
||||
$newStoredObj
|
||||
->setCreatedAt(new \DateTime('today'))
|
||||
->setType($template['file']['type']);
|
||||
->registerVersion(
|
||||
json_decode($template['file']['key'], true),
|
||||
json_decode($template['file']['iv'], true),
|
||||
$template['file']['type'],
|
||||
);
|
||||
|
||||
$manager->persist($newStoredObj);
|
||||
|
||||
|
@@ -134,13 +134,11 @@ class Generator implements GeneratorInterface
|
||||
$content = Yaml::dump($data, 6);
|
||||
/* @var StoredObject $destinationStoredObject */
|
||||
$destinationStoredObject
|
||||
->setType('application/yaml')
|
||||
->setFilename(sprintf('%s_yaml', uniqid('doc_', true)))
|
||||
->setStatus(StoredObject::STATUS_READY)
|
||||
;
|
||||
|
||||
try {
|
||||
$this->storedObjectManager->write($destinationStoredObject, $content);
|
||||
$this->storedObjectManager->write($destinationStoredObject, $content, 'application/yaml');
|
||||
} catch (StoredObjectManagerException $e) {
|
||||
$destinationStoredObject->addGenerationErrors($e->getMessage());
|
||||
|
||||
@@ -174,13 +172,11 @@ class Generator implements GeneratorInterface
|
||||
|
||||
/* @var StoredObject $destinationStoredObject */
|
||||
$destinationStoredObject
|
||||
->setType($template->getFile()->getType())
|
||||
->setFilename(sprintf('%s_odt', uniqid('doc_', true)))
|
||||
->setStatus(StoredObject::STATUS_READY)
|
||||
;
|
||||
|
||||
try {
|
||||
$this->storedObjectManager->write($destinationStoredObject, $generatedResource);
|
||||
$this->storedObjectManager->write($destinationStoredObject, $generatedResource, $template->getFile()->getType());
|
||||
} catch (StoredObjectManagerException $e) {
|
||||
$destinationStoredObject->addGenerationErrors($e->getMessage());
|
||||
|
||||
|
@@ -19,6 +19,7 @@ use Chill\DocGeneratorBundle\Service\Generator\Generator;
|
||||
use Chill\DocGeneratorBundle\Service\Generator\ObjectReadyException;
|
||||
use Chill\DocGeneratorBundle\Service\Generator\RelatedEntityNotFoundException;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@@ -39,11 +40,11 @@ class GeneratorTest extends TestCase
|
||||
|
||||
public function testSuccessfulGeneration(): void
|
||||
{
|
||||
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject())
|
||||
->setType('application/test'));
|
||||
$templateStoredObject = new StoredObject();
|
||||
$templateStoredObject->registerVersion(type: 'application/test');
|
||||
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject);
|
||||
$destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_PENDING);
|
||||
$reflection = new \ReflectionClass($destinationStoredObject);
|
||||
$reflection->getProperty('id')->setAccessible(true);
|
||||
$reflection->getProperty('id')->setValue($destinationStoredObject, 1);
|
||||
$entity = new class () {};
|
||||
$data = [];
|
||||
@@ -76,7 +77,14 @@ class GeneratorTest extends TestCase
|
||||
|
||||
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||
$storedObjectManager->read($templateStoredObject)->willReturn('template');
|
||||
$storedObjectManager->write($destinationStoredObject, 'generated')->shouldBeCalled();
|
||||
$storedObjectManager->write($destinationStoredObject, 'generated', 'application/test')
|
||||
->will(function ($args): StoredObjectVersion {
|
||||
/** @var StoredObject $storedObject */
|
||||
$storedObject = $args[0];
|
||||
|
||||
return $storedObject->registerVersion(type: $args[2]);
|
||||
})
|
||||
->shouldBeCalled();
|
||||
|
||||
$generator = new Generator(
|
||||
$contextManagerInterface->reveal(),
|
||||
@@ -107,8 +115,9 @@ class GeneratorTest extends TestCase
|
||||
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
||||
);
|
||||
|
||||
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject())
|
||||
->setType('application/test'));
|
||||
$templateStoredObject = new StoredObject();
|
||||
$templateStoredObject->registerVersion(type: 'application/test');
|
||||
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject);
|
||||
$destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_READY);
|
||||
|
||||
$generator->generateDocFromTemplate(
|
||||
@@ -124,11 +133,11 @@ class GeneratorTest extends TestCase
|
||||
{
|
||||
$this->expectException(RelatedEntityNotFoundException::class);
|
||||
|
||||
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject())
|
||||
->setType('application/test'));
|
||||
$templateStoredObject = new StoredObject();
|
||||
$templateStoredObject->registerVersion(type: 'application/test');
|
||||
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject);
|
||||
$destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_PENDING);
|
||||
$reflection = new \ReflectionClass($destinationStoredObject);
|
||||
$reflection->getProperty('id')->setAccessible(true);
|
||||
$reflection->getProperty('id')->setValue($destinationStoredObject, 1);
|
||||
|
||||
$context = $this->prophesize(DocGeneratorContextInterface::class);
|
||||
|
@@ -58,6 +58,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
|
||||
?int $expire_delay = null,
|
||||
?int $submit_delay = null,
|
||||
int $max_file_count = 1,
|
||||
?string $object_name = null,
|
||||
): SignedUrlPost {
|
||||
$delay = $expire_delay ?? $this->max_expire_delay;
|
||||
$submit_delay ??= $this->max_submit_delay;
|
||||
@@ -84,11 +85,14 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
|
||||
|
||||
$expires = $this->clock->now()->add(new \DateInterval('PT'.(string) $delay.'S'));
|
||||
|
||||
$object_name = $this->generateObjectName();
|
||||
if (null === $object_name) {
|
||||
$object_name = $this->generateObjectName();
|
||||
}
|
||||
|
||||
$g = new SignedUrlPost(
|
||||
$url = $this->generateUrl($object_name),
|
||||
$expires,
|
||||
$object_name,
|
||||
$this->max_post_file_size,
|
||||
$max_file_count,
|
||||
$submit_delay,
|
||||
@@ -127,7 +131,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
|
||||
];
|
||||
$url = $url.'?'.\http_build_query($args);
|
||||
|
||||
$signature = new SignedUrl(strtoupper($method), $url, $expires);
|
||||
$signature = new SignedUrl(strtoupper($method), $url, $expires, $object_name);
|
||||
|
||||
$this->event_dispatcher->dispatch(
|
||||
new TempUrlGenerateEvent($signature)
|
||||
@@ -178,21 +182,19 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
|
||||
return \hash_hmac('sha512', $body, $this->key, false);
|
||||
}
|
||||
|
||||
private function generateSignature($method, $url, \DateTimeImmutable $expires)
|
||||
private function generateSignature(string $method, $url, \DateTimeImmutable $expires)
|
||||
{
|
||||
if ('POST' === $method) {
|
||||
return $this->generateSignaturePost($url, $expires);
|
||||
}
|
||||
|
||||
$path = \parse_url((string) $url, PHP_URL_PATH);
|
||||
|
||||
$body = sprintf(
|
||||
"%s\n%s\n%s",
|
||||
$method,
|
||||
strtoupper($method),
|
||||
$expires->format('U'),
|
||||
$path
|
||||
)
|
||||
;
|
||||
);
|
||||
|
||||
$this->logger->debug(
|
||||
'generate signature GET',
|
||||
|
@@ -21,6 +21,8 @@ readonly class SignedUrl
|
||||
#[Serializer\Groups(['read'])]
|
||||
public string $url,
|
||||
public \DateTimeImmutable $expires,
|
||||
#[Serializer\Groups(['read'])]
|
||||
public string $object_name,
|
||||
) {}
|
||||
|
||||
#[Serializer\Groups(['read'])]
|
||||
|
@@ -18,6 +18,7 @@ readonly class SignedUrlPost extends SignedUrl
|
||||
public function __construct(
|
||||
string $url,
|
||||
\DateTimeImmutable $expires,
|
||||
string $object_name,
|
||||
#[Serializer\Groups(['read'])]
|
||||
public int $max_file_size,
|
||||
#[Serializer\Groups(['read'])]
|
||||
@@ -31,6 +32,6 @@ readonly class SignedUrlPost extends SignedUrl
|
||||
#[Serializer\Groups(['read'])]
|
||||
public string $signature,
|
||||
) {
|
||||
parent::__construct('POST', $url, $expires);
|
||||
parent::__construct('POST', $url, $expires, $object_name);
|
||||
}
|
||||
}
|
||||
|
@@ -17,6 +17,7 @@ interface TempUrlGeneratorInterface
|
||||
?int $expire_delay = null,
|
||||
?int $submit_delay = null,
|
||||
int $max_file_count = 1,
|
||||
?string $object_name = null,
|
||||
): SignedUrlPost;
|
||||
|
||||
public function generate(string $method, string $object_name, ?int $expire_delay = null): SignedUrl;
|
||||
|
@@ -11,9 +11,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Exception\TempUrlGeneratorException;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AsyncUploadVoter;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -30,62 +32,84 @@ final readonly class AsyncUploadController
|
||||
private TempUrlGeneratorInterface $tempUrlGenerator,
|
||||
private SerializerInterface $serializer,
|
||||
private Security $security,
|
||||
private LoggerInterface $logger,
|
||||
private LoggerInterface $chillLogger,
|
||||
) {}
|
||||
|
||||
#[Route(path: '/asyncupload/temp_url/generate/{method}', name: 'async_upload.generate_url')]
|
||||
public function getSignedUrl(string $method, Request $request): JsonResponse
|
||||
#[Route(path: '/api/1.0/doc-store/async-upload/temp_url/{uuid}/generate/post', name: 'chill_docstore_asyncupload_getsignedurlpost')]
|
||||
public function getSignedUrlPost(Request $request, StoredObject $storedObject): JsonResponse
|
||||
{
|
||||
try {
|
||||
switch (strtolower($method)) {
|
||||
case 'post':
|
||||
$p = $this->tempUrlGenerator
|
||||
->generatePost(
|
||||
$request->query->has('expires_delay') ? $request->query->getInt('expires_delay') : null,
|
||||
$request->query->has('submit_delay') ? $request->query->getInt('submit_delay') : null
|
||||
)
|
||||
;
|
||||
break;
|
||||
case 'get':
|
||||
case 'head':
|
||||
$object_name = $request->query->get('object_name', null);
|
||||
if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) {
|
||||
throw new AccessDeniedHttpException('not able to edit the given stored object');
|
||||
}
|
||||
|
||||
if (null === $object_name) {
|
||||
return (new JsonResponse((object) [
|
||||
'message' => 'the object_name is null',
|
||||
]))
|
||||
->setStatusCode(JsonResponse::HTTP_BAD_REQUEST);
|
||||
}
|
||||
$p = $this->tempUrlGenerator->generate(
|
||||
$method,
|
||||
$object_name,
|
||||
$request->query->has('expires_delay') ? $request->query->getInt('expires_delay') : null
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return (new JsonResponse((object) ['message' => 'the method '
|
||||
."{$method} is not valid"]))
|
||||
->setStatusCode(JsonResponse::HTTP_BAD_REQUEST);
|
||||
// we create a dummy version, to generate a filename
|
||||
$version = $storedObject->registerVersion();
|
||||
|
||||
$p = $this->tempUrlGenerator
|
||||
->generatePost(
|
||||
$request->query->has('expires_delay') ? $request->query->getInt('expires_delay') : null,
|
||||
$request->query->has('submit_delay') ? $request->query->getInt('submit_delay') : null,
|
||||
object_name: $version->getFilename()
|
||||
);
|
||||
|
||||
$this->chillLogger->notice('[Privacy Event] a request to upload a document has been generated', [
|
||||
'doc_uuid' => $storedObject->getUuid(),
|
||||
]);
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($p, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
||||
Response::HTTP_OK,
|
||||
[],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[Route(path: '/api/1.0/doc-store/async-upload/temp_url/{uuid}/generate/{method}', name: 'chill_docstore_asyncupload_getsignedurlget', requirements: ['method' => 'get|head'])]
|
||||
public function getSignedUrlGet(Request $request, StoredObject $storedObject, string $method): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||
throw new AccessDeniedHttpException('not able to read the given stored object');
|
||||
}
|
||||
|
||||
// we really want to be sure that there are no other method than get or head:
|
||||
if (!in_array($method, ['get', 'head'], true)) {
|
||||
throw new AccessDeniedHttpException('Only methods get and head are allowed');
|
||||
}
|
||||
|
||||
if ($request->query->has('version')) {
|
||||
$filename = $request->query->get('version');
|
||||
|
||||
$storedObjectVersion = $storedObject->getVersions()->findFirst(fn (int $index, StoredObjectVersion $version): bool => $version->getFilename() === $filename);
|
||||
|
||||
if (null === $storedObjectVersion) {
|
||||
// we are here in the case where the version is not stored into the database
|
||||
// as the version is prefixed by the stored object prefix, we just have to check that this prefix
|
||||
// is the same. It means that the user had previously the permission to "SEE_AND_EDIT" this stored
|
||||
// object with same prefix that we checked before
|
||||
if (!str_starts_with($filename, $storedObject->getPrefix())) {
|
||||
throw new AccessDeniedHttpException('not able to match the version with the same filename');
|
||||
}
|
||||
}
|
||||
} catch (TempUrlGeneratorException $e) {
|
||||
$this->logger->warning('The client requested a temp url'
|
||||
.' which sparkle an error.', [
|
||||
'message' => $e->getMessage(),
|
||||
'expire_delay' => $request->query->getInt('expire_delay', 0),
|
||||
'file_count' => $request->query->getInt('file_count', 1),
|
||||
'method' => $method,
|
||||
]);
|
||||
|
||||
$p = new \stdClass();
|
||||
$p->message = $e->getMessage();
|
||||
$p->status = JsonResponse::HTTP_BAD_REQUEST;
|
||||
|
||||
return new JsonResponse($p, JsonResponse::HTTP_BAD_REQUEST);
|
||||
} else {
|
||||
$filename = $storedObject->getCurrentVersion()->getFilename();
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(AsyncUploadVoter::GENERATE_SIGNATURE, $p)) {
|
||||
throw new AccessDeniedHttpException('not allowed to generate this signature');
|
||||
}
|
||||
$p = $this->tempUrlGenerator->generate(
|
||||
$method,
|
||||
$filename,
|
||||
$request->query->has('expires_delay') ? $request->query->getInt('expires_delay') : null
|
||||
);
|
||||
|
||||
$user = $this->security->getUser();
|
||||
$userId = match ($user instanceof User) {
|
||||
true => $user->getId(),
|
||||
false => $user->getUserIdentifier(),
|
||||
};
|
||||
|
||||
$this->chillLogger->notice('[Privacy Event] a request to see a document has been granted', [
|
||||
'doc_uuid' => $storedObject->getUuid()->toString(),
|
||||
'user_id' => $userId,
|
||||
]);
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($p, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
||||
|
@@ -0,0 +1,55 @@
|
||||
<?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\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
||||
use Chill\DocStoreBundle\Workflow\AccompanyingCourseDocumentDuplicator;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
final readonly class DocumentAccompanyingCourseDuplicateController
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private AccompanyingCourseDocumentDuplicator $documentWorkflowDuplicator,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private UrlGeneratorInterface $urlGenerator,
|
||||
) {}
|
||||
|
||||
#[Route('/{_locale}/doc-store/accompanying-course-document/{id}/duplicate', name: 'chill_doc_store_accompanying_course_document_duplicate')]
|
||||
public function __invoke(AccompanyingCourseDocument $document, Request $request, Session $session): Response
|
||||
{
|
||||
if (!$this->security->isGranted(AccompanyingCourseDocumentVoter::SEE, $document)) {
|
||||
throw new AccessDeniedHttpException('not allowed to see this document');
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(AccompanyingCourseDocumentVoter::CREATE, $document->getCourse())) {
|
||||
throw new AccessDeniedHttpException('not allowed to create this document');
|
||||
}
|
||||
|
||||
$duplicated = $this->documentWorkflowDuplicator->duplicate($document);
|
||||
$this->entityManager->persist($duplicated);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->generate('accompanying_course_document_edit', ['id' => $duplicated->getId(), 'course' => $duplicated->getCourse()->getId()])
|
||||
);
|
||||
}
|
||||
}
|
@@ -26,6 +26,8 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
|
||||
/**
|
||||
* Class DocumentPersonController.
|
||||
@@ -40,6 +42,8 @@ class DocumentPersonController extends AbstractController
|
||||
protected TranslatorInterface $translator,
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
protected AuthorizationHelper $authorizationHelper,
|
||||
protected PDFSignatureZoneParser $PDFSignatureZoneParser,
|
||||
protected StoredObjectManagerInterface $storedObjectManagerInterface,
|
||||
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
|
||||
) {}
|
||||
|
||||
|
@@ -0,0 +1,114 @@
|
||||
<?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\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
|
||||
use Chill\DocStoreBundle\Service\Signature\PDFPage;
|
||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface;
|
||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
|
||||
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
class SignatureRequestController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MessageBusInterface $messageBus,
|
||||
private readonly StoredObjectManagerInterface $storedObjectManager,
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
private readonly ChillEntityRenderManagerInterface $entityRender,
|
||||
private readonly NormalizerInterface $normalizer,
|
||||
private readonly Security $security,
|
||||
private readonly StoredObjectToPdfConverter $converter,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
#[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
|
||||
public function processSignature(EntityWorkflowStepSignature $signature, Request $request): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted(EntityWorkflowStepSignatureVoter::SIGN, $signature)) {
|
||||
throw new AccessDeniedHttpException('not authorized to sign this step');
|
||||
}
|
||||
|
||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
||||
|
||||
if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) {
|
||||
return new JsonResponse([], status: Response::HTTP_CONFLICT);
|
||||
}
|
||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
||||
|
||||
if ('application/pdf' !== $storedObject->getType()) {
|
||||
[$storedObject, $storedObjectVersion, $content] = $this->converter->addConvertedVersion($storedObject, $request->getLocale(), includeConvertedContent: true);
|
||||
$this->entityManager->persist($storedObjectVersion);
|
||||
$this->entityManager->flush();
|
||||
} else {
|
||||
$content = $this->storedObjectManager->read($storedObject);
|
||||
}
|
||||
|
||||
$data = \json_decode((string) $request->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
$zone = new PDFSignatureZone(
|
||||
$data['zone']['index'],
|
||||
$data['zone']['x'],
|
||||
$data['zone']['y'],
|
||||
$data['zone']['height'],
|
||||
$data['zone']['width'],
|
||||
new PDFPage($data['zone']['PDFPage']['index'], $data['zone']['PDFPage']['width'], $data['zone']['PDFPage']['height'])
|
||||
);
|
||||
|
||||
$this->messageBus->dispatch(new RequestPdfSignMessage(
|
||||
$signature->getId(),
|
||||
$zone,
|
||||
$data['zone']['index'],
|
||||
'Signed by IP: '.(string) $request->getClientIp().', authenticated user: '.$this->entityRender->renderString($this->security->getUser(), []),
|
||||
$this->entityRender->renderString($signature->getSigner(), [
|
||||
// options for user render
|
||||
'absence' => false,
|
||||
'main_scope' => false,
|
||||
UserRender::SPLIT_LINE_BEFORE_CHARACTER => 30,
|
||||
// options for person render
|
||||
'addAge' => false,
|
||||
]),
|
||||
$content
|
||||
));
|
||||
|
||||
return new JsonResponse(null, JsonResponse::HTTP_OK, []);
|
||||
}
|
||||
|
||||
#[Route('/api/1.0/document/workflow/{id}/check-signature', name: 'chill_docstore_check_signature')]
|
||||
public function checkSignature(EntityWorkflowStepSignature $signature): JsonResponse
|
||||
{
|
||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
||||
|
||||
return new JsonResponse(
|
||||
[
|
||||
'state' => $signature->getState(),
|
||||
'storedObject' => $this->normalizer->normalize($storedObject, 'json'),
|
||||
],
|
||||
JsonResponse::HTTP_OK,
|
||||
[]
|
||||
);
|
||||
}
|
||||
}
|
@@ -11,6 +11,46 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\CRUD\Controller\ApiController;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
class StoredObjectApiController extends ApiController {}
|
||||
class StoredObjectApiController extends ApiController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new stored object.
|
||||
*
|
||||
* @return JsonResponse the response containing the serialized object in JSON format
|
||||
*
|
||||
* @throws AccessDeniedHttpException if the user does not have the necessary role to create a stored object
|
||||
*/
|
||||
#[Route('/api/1.0/doc-store/stored-object/create', methods: ['POST'])]
|
||||
public function createStoredObject(): JsonResponse
|
||||
{
|
||||
if (!($this->security->isGranted('ROLE_ADMIN') || $this->security->isGranted('ROLE_USER'))) {
|
||||
throw new AccessDeniedHttpException('Must be user or admin to create a stored object');
|
||||
}
|
||||
|
||||
$object = new StoredObject();
|
||||
|
||||
$this->entityManager->persist($object);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($object, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
||||
json: true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,47 @@
|
||||
<?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\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectVersionNormalizer;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectRestoreInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
final readonly class StoredObjectRestoreVersionApiController
|
||||
{
|
||||
public function __construct(private Security $security, private StoredObjectRestoreInterface $storedObjectRestore, private EntityManagerInterface $entityManager, private SerializerInterface $serializer) {}
|
||||
|
||||
#[Route('/api/1.0/doc-store/stored-object/restore-from-version/{id}', methods: ['POST'])]
|
||||
public function restoreStoredObjectVersion(StoredObjectVersion $storedObjectVersion): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObjectVersion->getStoredObject())) {
|
||||
throw new AccessDeniedHttpException('not allowed to edit the stored object');
|
||||
}
|
||||
|
||||
$newVersion = $this->storedObjectRestore->restore($storedObjectVersion);
|
||||
|
||||
$this->entityManager->persist($newVersion);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($newVersion, 'json', [AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]),
|
||||
json: true
|
||||
);
|
||||
}
|
||||
}
|
@@ -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\DocStoreBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectVersionNormalizer;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
|
||||
use Chill\MainBundle\Serializer\Model\Collection;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
final readonly class StoredObjectVersionApiController
|
||||
{
|
||||
public function __construct(
|
||||
private PaginatorFactoryInterface $paginatorFactory,
|
||||
private SerializerInterface $serializer,
|
||||
private Security $security,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Lists the versions of the specified stored object.
|
||||
*
|
||||
* @param StoredObject $storedObject the stored object whose versions are to be listed
|
||||
*
|
||||
* @return JsonResponse a JSON response containing the serialized versions of the stored object, encapsulated in a collection
|
||||
*
|
||||
* @throws AccessDeniedHttpException if the user is not allowed to see the stored object
|
||||
*/
|
||||
#[Route('/api/1.0/doc-store/stored-object/{uuid}/versions', name: 'chill_doc_store_stored_object_versions_list')]
|
||||
public function listVersions(StoredObject $storedObject): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||
throw new AccessDeniedHttpException('not allowed to see this stored object');
|
||||
}
|
||||
|
||||
$total = $storedObject->getVersions()->count();
|
||||
$paginator = $this->paginatorFactory->create($total);
|
||||
|
||||
$criteria = Criteria::create();
|
||||
$criteria->orderBy(['id' => Order::Ascending]);
|
||||
$criteria->setMaxResults($paginator->getItemsPerPage())->setFirstResult($paginator->getCurrentPageFirstItemNumber());
|
||||
$items = $storedObject->getVersions()->matching($criteria);
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize(
|
||||
new Collection($items, $paginator),
|
||||
'json',
|
||||
[AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]
|
||||
),
|
||||
json: true
|
||||
);
|
||||
}
|
||||
}
|
@@ -16,6 +16,7 @@ use Chill\DocStoreBundle\Dav\Response\DavResponse;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
@@ -42,6 +43,7 @@ final readonly class WebdavController
|
||||
private \Twig\Environment $engine,
|
||||
private StoredObjectManagerInterface $storedObjectManager,
|
||||
private Security $security,
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {
|
||||
$this->requestAnalyzer = new PropfindRequestAnalyzer();
|
||||
}
|
||||
@@ -201,6 +203,8 @@ final readonly class WebdavController
|
||||
|
||||
$this->storedObjectManager->write($storedObject, $request->getContent());
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new DavResponse('', Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
|
@@ -14,7 +14,9 @@ namespace Chill\DocStoreBundle\DataFixtures\ORM;
|
||||
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
|
||||
use Chill\MainBundle\DataFixtures\ORM\LoadPermissionsGroup;
|
||||
use Chill\MainBundle\DataFixtures\ORM\LoadScopes;
|
||||
use Chill\MainBundle\Entity\PermissionsGroup;
|
||||
use Chill\MainBundle\Entity\RoleScope;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
@@ -24,19 +26,19 @@ use Doctrine\Persistence\ObjectManager;
|
||||
*/
|
||||
class LoadDocumentACL extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 35000;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
foreach (LoadPermissionsGroup::$refs as $permissionsGroupRef) {
|
||||
$permissionsGroup = $this->getReference($permissionsGroupRef);
|
||||
$permissionsGroup = $this->getReference($permissionsGroupRef, PermissionsGroup::class);
|
||||
printf("processing permission group %s \n", $permissionsGroup->getName());
|
||||
|
||||
foreach (LoadScopes::$references as $scopeRef) {
|
||||
$scope = $this->getReference($scopeRef);
|
||||
$scope = $this->getReference($scopeRef, Scope::class);
|
||||
printf("processing scope %s \n", $scope->getName()['en']);
|
||||
// create permission group
|
||||
switch ($permissionsGroup->getName()) {
|
||||
|
@@ -18,12 +18,12 @@ use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
class LoadDocumentCategory extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 35010;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$category = (new DocumentCategory('chill-doc-store', 10))
|
||||
->setDocumentClass(\Chill\DocStoreBundle\Entity\PersonDocument::class)
|
||||
|
@@ -11,14 +11,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\DependencyInjection;
|
||||
|
||||
use Chill\DocStoreBundle\Controller\StoredObjectApiController;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
||||
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
|
||||
use Symfony\Component\DependencyInjection\Loader;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
||||
|
||||
/**
|
||||
@@ -35,6 +34,8 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
||||
|
||||
$container->setParameter('chill_doc_store', $config);
|
||||
|
||||
$container->registerForAutoconfiguration(StoredObjectVoterInterface::class)->addTag('stored_object_voter');
|
||||
|
||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
||||
$loader->load('services.yaml');
|
||||
$loader->load('services/controller.yaml');
|
||||
@@ -42,6 +43,7 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
||||
$loader->load('services/fixtures.yaml');
|
||||
$loader->load('services/form.yaml');
|
||||
$loader->load('services/templating.yaml');
|
||||
$loader->load('services/security.yaml');
|
||||
}
|
||||
|
||||
public function prepend(ContainerBuilder $container)
|
||||
@@ -49,29 +51,6 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
||||
$this->prependRoute($container);
|
||||
$this->prependAuthorization($container);
|
||||
$this->prependTwig($container);
|
||||
$this->prependApis($container);
|
||||
}
|
||||
|
||||
protected function prependApis(ContainerBuilder $container)
|
||||
{
|
||||
$container->prependExtensionConfig('chill_main', [
|
||||
'apis' => [
|
||||
[
|
||||
'class' => \Chill\DocStoreBundle\Entity\StoredObject::class,
|
||||
'controller' => StoredObjectApiController::class,
|
||||
'name' => 'stored_object',
|
||||
'base_path' => '/api/1.0/docstore/stored-object',
|
||||
'base_role' => 'ROLE_USER',
|
||||
'actions' => [
|
||||
'_entity' => [
|
||||
'methods' => [
|
||||
Request::METHOD_POST => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function prependAuthorization(ContainerBuilder $container)
|
||||
|
@@ -18,6 +18,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table('chill_doc.accompanyingcourse_document')]
|
||||
#[ORM\UniqueConstraint(name: 'acc_course_document_unique_stored_object', columns: ['object_id'])]
|
||||
class AccompanyingCourseDocument extends Document implements HasScopesInterface, HasCentersInterface
|
||||
{
|
||||
#[ORM\ManyToOne(targetEntity: AccompanyingPeriod::class)]
|
||||
|
@@ -40,6 +40,7 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
|
||||
#[Assert\Valid]
|
||||
#[Assert\NotNull(message: 'Upload a document')]
|
||||
#[ORM\ManyToOne(targetEntity: StoredObject::class, cascade: ['persist'])]
|
||||
#[ORM\JoinColumn(name: 'object_id', referencedColumnName: 'id')]
|
||||
private ?StoredObject $object = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: DocGeneratorTemplate::class)]
|
||||
|
@@ -19,6 +19,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table('chill_doc.person_document')]
|
||||
#[ORM\UniqueConstraint(name: 'person_document_unique_stored_object', columns: ['object_id'])]
|
||||
class PersonDocument extends Document implements HasCenterInterface, HasScopeInterface
|
||||
{
|
||||
#[ORM\Id]
|
||||
|
@@ -16,10 +16,17 @@ use ChampsLibres\WopiLib\Contract\Entity\Document;
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
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\Order;
|
||||
use Doctrine\Common\Collections\ReadableCollection;
|
||||
use Doctrine\Common\Collections\Selectable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
use Random\RandomException;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Represent a document stored in an object store.
|
||||
@@ -28,13 +35,16 @@ use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
*
|
||||
* The property `$deleteAt` allow a deletion of the document after the given date. But this property should
|
||||
* be set before the document is actually written by the StoredObjectManager.
|
||||
*
|
||||
* Each version is stored within a @see{StoredObjectVersion}, associated with this current's object. The creation
|
||||
* of each new version should be done using the method @see{self::registerVersion}.
|
||||
*/
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table('chill_doc.stored_object')]
|
||||
#[AsyncFileExists(message: 'The file is not stored properly')]
|
||||
#[ORM\Table('stored_object', schema: 'chill_doc')]
|
||||
class StoredObject implements Document, TrackCreationInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
final public const STATUS_EMPTY = 'empty';
|
||||
final public const STATUS_READY = 'ready';
|
||||
final public const STATUS_PENDING = 'pending';
|
||||
final public const STATUS_FAILURE = 'failure';
|
||||
@@ -43,9 +53,11 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'datas')]
|
||||
private array $datas = [];
|
||||
|
||||
#[Serializer\Groups(['write'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
|
||||
private string $filename = '';
|
||||
/**
|
||||
* the prefix of each version.
|
||||
*/
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
|
||||
private string $prefix = '';
|
||||
|
||||
#[Serializer\Groups(['write'])]
|
||||
#[ORM\Id]
|
||||
@@ -53,25 +65,10 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
#[Serializer\Groups(['write'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'iv')]
|
||||
private array $iv = [];
|
||||
|
||||
#[Serializer\Groups(['write'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'key')]
|
||||
private array $keyInfos = [];
|
||||
|
||||
#[Serializer\Groups(['write'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, name: 'title')]
|
||||
#[ORM\Column(name: 'title', type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => ''])]
|
||||
private string $title = '';
|
||||
|
||||
#[Serializer\Groups(['write'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, name: 'type', options: ['default' => ''])]
|
||||
private string $type = '';
|
||||
|
||||
#[Serializer\Groups(['write'])]
|
||||
#[ORM\Column(type: 'uuid', unique: true)]
|
||||
private UuidInterface $uuid;
|
||||
@@ -94,14 +91,22 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
|
||||
private string $generationErrors = '';
|
||||
|
||||
/**
|
||||
* @var Collection<int, StoredObjectVersion>&Selectable<int, StoredObjectVersion>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'storedObject', targetEntity: StoredObjectVersion::class, cascade: ['persist'], orphanRemoval: true)]
|
||||
private Collection&Selectable $versions;
|
||||
|
||||
/**
|
||||
* @param StoredObject::STATUS_* $status
|
||||
*/
|
||||
public function __construct(
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => 'ready'])]
|
||||
private string $status = 'ready',
|
||||
private string $status = 'empty',
|
||||
) {
|
||||
$this->uuid = Uuid::uuid4();
|
||||
$this->versions = new ArrayCollection();
|
||||
$this->prefix = self::generatePrefix();
|
||||
}
|
||||
|
||||
public function addGenerationTrial(): self
|
||||
@@ -125,14 +130,34 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
return \DateTime::createFromImmutable($this->createdAt);
|
||||
}
|
||||
|
||||
#[AsyncFileExists(message: 'The file is not stored properly')]
|
||||
#[Assert\NotNull(message: 'The store object version must be present')]
|
||||
public function getCurrentVersion(): ?StoredObjectVersion
|
||||
{
|
||||
$maxVersion = null;
|
||||
|
||||
foreach ($this->versions as $v) {
|
||||
if ($v->getVersion() > ($maxVersion?->getVersion() ?? -1)) {
|
||||
$maxVersion = $v;
|
||||
}
|
||||
}
|
||||
|
||||
return $maxVersion;
|
||||
}
|
||||
|
||||
public function getDatas(): array
|
||||
{
|
||||
return $this->datas;
|
||||
}
|
||||
|
||||
public function getPrefix(): string
|
||||
{
|
||||
return $this->prefix;
|
||||
}
|
||||
|
||||
public function getFilename(): string
|
||||
{
|
||||
return $this->filename;
|
||||
return $this->getCurrentVersion()?->getFilename() ?? '';
|
||||
}
|
||||
|
||||
public function getGenerationTrialsCounter(): int
|
||||
@@ -145,14 +170,17 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<int>
|
||||
*/
|
||||
public function getIv(): array
|
||||
{
|
||||
return $this->iv;
|
||||
return $this->getCurrentVersion()?->getIv() ?? [];
|
||||
}
|
||||
|
||||
public function getKeyInfos(): array
|
||||
{
|
||||
return $this->keyInfos;
|
||||
return $this->getCurrentVersion()?->getKeyInfos() ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,14 +199,14 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getType()
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
return $this->getCurrentVersion()?->getType() ?? '';
|
||||
}
|
||||
|
||||
public function getUuid(): UuidInterface
|
||||
@@ -209,27 +237,6 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setFilename(?string $filename): self
|
||||
{
|
||||
$this->filename = (string) $filename;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setIv(?array $iv): self
|
||||
{
|
||||
$this->iv = (array) $iv;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setKeyInfos(?array $keyInfos): self
|
||||
{
|
||||
$this->keyInfos = (array) $keyInfos;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StoredObject::STATUS_* $status
|
||||
*/
|
||||
@@ -247,23 +254,89 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setType(?string $type): self
|
||||
{
|
||||
$this->type = (string) $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTemplate(): ?DocGeneratorTemplate
|
||||
{
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Selectable<int, StoredObjectVersion>&Collection<int, StoredObjectVersion>
|
||||
*/
|
||||
public function getVersions(): Collection&Selectable
|
||||
{
|
||||
return $this->versions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves versions sorted by a given order.
|
||||
*
|
||||
* @param 'ASC'|'DESC' $order the sorting order, default is Order::Ascending
|
||||
*
|
||||
* @return ReadableCollection&Selectable The ordered collection of versions
|
||||
*/
|
||||
public function getVersionsOrdered(string $order = 'ASC'): ReadableCollection&Selectable
|
||||
{
|
||||
$versions = $this->getVersions()->toArray();
|
||||
|
||||
match ($order) {
|
||||
'ASC' => usort($versions, static fn (StoredObjectVersion $a, StoredObjectVersion $b) => $a->getVersion() <=> $b->getVersion()),
|
||||
'DESC' => usort($versions, static fn (StoredObjectVersion $a, StoredObjectVersion $b) => $b->getVersion() <=> $a->getVersion()),
|
||||
};
|
||||
|
||||
return new ArrayCollection($versions);
|
||||
}
|
||||
|
||||
public function hasCurrentVersion(): bool
|
||||
{
|
||||
return null !== $this->getCurrentVersion();
|
||||
}
|
||||
|
||||
public function hasTemplate(): bool
|
||||
{
|
||||
return null !== $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is a version kept before conversion.
|
||||
*
|
||||
* @return bool true if a version is kept before conversion, false otherwise
|
||||
*/
|
||||
public function hasKeptBeforeConversionVersion(): bool
|
||||
{
|
||||
foreach ($this->getVersions() as $version) {
|
||||
foreach ($version->getPointInTimes() as $pointInTime) {
|
||||
if (StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION === $pointInTime->getReason()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the last version of the stored object that was kept before conversion.
|
||||
*
|
||||
* This method iterates through the ordered versions and their respective points
|
||||
* in time to find the most recent version that has a point in time with the reason
|
||||
* 'KEEP_BEFORE_CONVERSION'.
|
||||
*
|
||||
* @return StoredObjectVersion|null the version that was kept before conversion,
|
||||
* or null if not found
|
||||
*/
|
||||
public function getLastKeptBeforeConversionVersion(): ?StoredObjectVersion
|
||||
{
|
||||
foreach ($this->getVersionsOrdered('DESC') as $version) {
|
||||
foreach ($version->getPointInTimes() as $pointInTime) {
|
||||
if (StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION === $pointInTime->getReason()) {
|
||||
return $version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function setTemplate(?DocGeneratorTemplate $template): StoredObject
|
||||
{
|
||||
$this->template = $template;
|
||||
@@ -314,18 +387,65 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function saveHistory(): void
|
||||
{
|
||||
if ('' === $this->getFilename()) {
|
||||
return;
|
||||
public function registerVersion(
|
||||
array $iv = [],
|
||||
array $keyInfos = [],
|
||||
string $type = '',
|
||||
?string $filename = null,
|
||||
): StoredObjectVersion {
|
||||
$version = new StoredObjectVersion(
|
||||
$this,
|
||||
null === $this->getCurrentVersion() ? 0 : $this->getCurrentVersion()->getVersion() + 1,
|
||||
$iv,
|
||||
$keyInfos,
|
||||
$type,
|
||||
$filename
|
||||
);
|
||||
|
||||
$this->versions->add($version);
|
||||
|
||||
if ('empty' === $this->status) {
|
||||
$this->status = self::STATUS_READY;
|
||||
}
|
||||
|
||||
$this->datas['history'][] = [
|
||||
'filename' => $this->getFilename(),
|
||||
'iv' => $this->getIv(),
|
||||
'key_infos' => $this->getKeyInfos(),
|
||||
'type' => $this->getType(),
|
||||
'before' => (new \DateTimeImmutable('now'))->getTimestamp(),
|
||||
];
|
||||
return $version;
|
||||
}
|
||||
|
||||
public function removeVersion(StoredObjectVersion $storedObjectVersion): void
|
||||
{
|
||||
if (!$this->versions->contains($storedObjectVersion)) {
|
||||
throw new \UnexpectedValueException('This stored object does not contains this version');
|
||||
}
|
||||
$this->versions->removeElement($storedObjectVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function saveHistory(): void {}
|
||||
|
||||
public static function generatePrefix(): string
|
||||
{
|
||||
try {
|
||||
return base_convert(bin2hex(random_bytes(32)), 16, 36);
|
||||
} catch (RandomException) {
|
||||
return uniqid(more_entropy: true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a stored object can be deleted.
|
||||
*
|
||||
* Currently, return true if the deletedAt date is below the current date, and the object
|
||||
* does not contains any version (which must be removed first).
|
||||
*
|
||||
* @param \DateTimeImmutable $now the current date and time
|
||||
* @param StoredObject $storedObject the stored object to check
|
||||
*
|
||||
* @return bool returns true if the stored object can be deleted, false otherwise
|
||||
*/
|
||||
public static function canBeDeleted(\DateTimeImmutable $now, StoredObject $storedObject): bool
|
||||
{
|
||||
return $storedObject->getDeleteAt() < $now && $storedObject->getVersions()->isEmpty();
|
||||
}
|
||||
}
|
||||
|
@@ -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: \Doctrine\DBAL\Types\Types::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';
|
||||
}
|
229
src/Bundle/ChillDocStoreBundle/Entity/StoredObjectVersion.php
Normal file
229
src/Bundle/ChillDocStoreBundle/Entity/StoredObjectVersion.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?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 Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\Selectable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Random\RandomException;
|
||||
|
||||
/**
|
||||
* Store each version of StoredObject's.
|
||||
*
|
||||
* A version should not be created manually: use the method @see{StoredObject::registerVersion} instead.
|
||||
*/
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table('chill_doc.stored_object_version')]
|
||||
#[ORM\UniqueConstraint(name: 'chill_doc_stored_object_version_unique_by_object', columns: ['stored_object_id', 'version'])]
|
||||
class StoredObjectVersion implements TrackCreationInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* filename of the version in the stored object.
|
||||
*/
|
||||
#[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;
|
||||
|
||||
/**
|
||||
* Previous storedObjectVersion, from which the current stored object version is created.
|
||||
*
|
||||
* If null, the current stored object version is generated by other means.
|
||||
*
|
||||
* Those version may be associated with the same storedObject, or not. In this last case, that means that
|
||||
* the stored object's current version is created from another stored object version.
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: StoredObjectVersion::class)]
|
||||
private ?StoredObjectVersion $createdFrom = null;
|
||||
|
||||
/**
|
||||
* List of stored object versions created from the current version.
|
||||
*
|
||||
* @var Collection<int, StoredObjectVersion>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'createdFrom', targetEntity: StoredObjectVersion::class)]
|
||||
private Collection $children;
|
||||
|
||||
public function __construct(
|
||||
/**
|
||||
* The stored object associated with this version.
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: StoredObject::class, inversedBy: 'versions')]
|
||||
#[ORM\JoinColumn(name: 'stored_object_id', nullable: false)]
|
||||
private StoredObject $storedObject,
|
||||
|
||||
/**
|
||||
* The incremental version.
|
||||
*/
|
||||
#[ORM\Column(name: 'version', type: \Doctrine\DBAL\Types\Types::INTEGER, options: ['default' => 0])]
|
||||
private int $version = 0,
|
||||
|
||||
/**
|
||||
* vector for encryption.
|
||||
*
|
||||
* @var int[]
|
||||
*/
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'iv')]
|
||||
private array $iv = [],
|
||||
|
||||
/**
|
||||
* Key infos for document encryption.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'key')]
|
||||
private array $keyInfos = [],
|
||||
|
||||
/**
|
||||
* type of the document.
|
||||
*/
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, name: 'type', options: ['default' => ''])]
|
||||
private string $type = '',
|
||||
?string $filename = null,
|
||||
) {
|
||||
$this->filename = $filename ?? self::generateFilename($this);
|
||||
$this->pointInTimes = new ArrayCollection();
|
||||
$this->children = new ArrayCollection();
|
||||
}
|
||||
|
||||
public static function generateFilename(StoredObjectVersion $storedObjectVersion): string
|
||||
{
|
||||
try {
|
||||
$suffix = base_convert(bin2hex(random_bytes(8)), 16, 36);
|
||||
} catch (RandomException) {
|
||||
$suffix = uniqid(more_entropy: true);
|
||||
}
|
||||
|
||||
return $storedObjectVersion->getStoredObject()->getPrefix().'/'.$suffix;
|
||||
}
|
||||
|
||||
public function getFilename(): string
|
||||
{
|
||||
return $this->filename;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getIv(): array
|
||||
{
|
||||
return $this->iv;
|
||||
}
|
||||
|
||||
public function getKeyInfos(): array
|
||||
{
|
||||
return $this->keyInfos;
|
||||
}
|
||||
|
||||
public function getStoredObject(): StoredObject
|
||||
{
|
||||
return $this->storedObject;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getVersion(): int
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
public function getCreatedFrom(): ?StoredObjectVersion
|
||||
{
|
||||
return $this->createdFrom;
|
||||
}
|
||||
|
||||
public function setCreatedFrom(?StoredObjectVersion $createdFrom): StoredObjectVersion
|
||||
{
|
||||
if (null === $createdFrom && null !== $this->createdFrom) {
|
||||
$this->createdFrom->removeChild($this);
|
||||
}
|
||||
|
||||
$createdFrom?->addChild($this);
|
||||
|
||||
$this->createdFrom = $createdFrom;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addChild(StoredObjectVersion $child): self
|
||||
{
|
||||
if (!$this->children->contains($child)) {
|
||||
$this->children->add($child);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeChild(StoredObjectVersion $child): self
|
||||
{
|
||||
$result = $this->children->removeElement($child);
|
||||
|
||||
if (false === $result) {
|
||||
throw new \UnexpectedValueException('the child is not associated with the current stored object version');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@@ -55,16 +55,8 @@ class StoredObjectDataMapper implements DataMapperInterface
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var StoredObject $viewData */
|
||||
if ($viewData->getFilename() !== $forms['stored_object']->getData()['filename']) {
|
||||
// we want to keep the previous history
|
||||
$viewData->saveHistory();
|
||||
}
|
||||
|
||||
$viewData->setFilename($forms['stored_object']->getData()['filename']);
|
||||
$viewData->setIv($forms['stored_object']->getData()['iv']);
|
||||
$viewData->setKeyInfos($forms['stored_object']->getData()['keyInfos']);
|
||||
$viewData->setType($forms['stored_object']->getData()['type']);
|
||||
/* @var StoredObject $viewData */
|
||||
$viewData = $forms['stored_object']->getData();
|
||||
|
||||
if (array_key_exists('title', $forms)) {
|
||||
$viewData->setTitle($forms['title']->getData());
|
||||
|
@@ -12,7 +12,6 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Form\DataTransformer;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectNormalizer;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
use Symfony\Component\Form\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
@@ -30,11 +29,7 @@ class StoredObjectDataTransformer implements DataTransformerInterface
|
||||
}
|
||||
|
||||
if ($value instanceof StoredObject) {
|
||||
return $this->serializer->serialize($value, 'json', [
|
||||
'groups' => [
|
||||
StoredObjectNormalizer::ADD_DAV_EDIT_LINK_CONTEXT,
|
||||
],
|
||||
]);
|
||||
return $this->serializer->serialize($value, 'json');
|
||||
}
|
||||
|
||||
throw new UnexpectedTypeException($value, StoredObject::class);
|
||||
@@ -46,6 +41,6 @@ class StoredObjectDataTransformer implements DataTransformerInterface
|
||||
return null;
|
||||
}
|
||||
|
||||
return json_decode((string) $value, true, 10, JSON_THROW_ON_ERROR);
|
||||
return $this->serializer->deserialize($value, StoredObject::class, 'json');
|
||||
}
|
||||
}
|
||||
|
@@ -17,15 +17,23 @@ use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class DocumentCategoryType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly TranslatorInterface $translator) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$bundles = [
|
||||
'chill-doc-store' => 'chill-doc-store',
|
||||
];
|
||||
|
||||
$documentClasses = [
|
||||
$this->translator->trans('Accompanying period document') => \Chill\DocStoreBundle\Entity\AccompanyingCourseDocument::class,
|
||||
$this->translator->trans('Person document') => \Chill\DocStoreBundle\Entity\PersonDocument::class,
|
||||
];
|
||||
|
||||
$builder
|
||||
->add('bundleId', ChoiceType::class, [
|
||||
'choices' => $bundles,
|
||||
@@ -34,7 +42,10 @@ class DocumentCategoryType extends AbstractType
|
||||
->add('idInsideBundle', null, [
|
||||
'disabled' => true,
|
||||
])
|
||||
->add('documentClass', null, [
|
||||
->add('documentClass', ChoiceType::class, [
|
||||
'choices' => $documentClasses,
|
||||
'expanded' => false,
|
||||
'required' => true,
|
||||
'disabled' => false,
|
||||
])
|
||||
->add('name', TranslatableStringFormType::class);
|
||||
|
@@ -12,17 +12,18 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Repository;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
class AccompanyingCourseDocumentRepository implements ObjectRepository
|
||||
class AccompanyingCourseDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
|
||||
public function __construct(private readonly EntityManagerInterface $em)
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->repository = $em->getRepository(AccompanyingCourseDocument::class);
|
||||
}
|
||||
@@ -45,6 +46,16 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
|
||||
return $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('d');
|
||||
$query = $qb->where('d.object = :storedObject')
|
||||
->setParameter('storedObject', $storedObject)
|
||||
->getQuery();
|
||||
|
||||
return $query->getOneOrNullResult();
|
||||
}
|
||||
|
||||
public function find($id): ?AccompanyingCourseDocument
|
||||
{
|
||||
return $this->repository->find($id);
|
||||
@@ -55,7 +66,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null)
|
||||
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
@@ -65,7 +76,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
|
||||
return $this->findOneBy($criteria);
|
||||
}
|
||||
|
||||
public function getClassName()
|
||||
public function getClassName(): string
|
||||
{
|
||||
return AccompanyingCourseDocument::class;
|
||||
}
|
||||
|
@@ -0,0 +1,19 @@
|
||||
<?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\StoredObject;
|
||||
|
||||
interface AssociatedEntityToStoredObjectInterface
|
||||
{
|
||||
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object;
|
||||
}
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Repository;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\PersonDocument;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
@@ -19,7 +20,7 @@ use Doctrine\Persistence\ObjectRepository;
|
||||
/**
|
||||
* @template ObjectRepository<PersonDocument::class>
|
||||
*/
|
||||
readonly class PersonDocumentRepository implements ObjectRepository
|
||||
readonly class PersonDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
@@ -53,4 +54,14 @@ readonly class PersonDocumentRepository implements ObjectRepository
|
||||
{
|
||||
return PersonDocument::class;
|
||||
}
|
||||
|
||||
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('d');
|
||||
$query = $qb->where('d.object = :storedObject')
|
||||
->setParameter('storedObject', $storedObject)
|
||||
->getQuery();
|
||||
|
||||
return $query->getOneOrNullResult();
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -14,6 +14,7 @@ namespace Chill\DocStoreBundle\Repository;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
|
||||
final readonly class StoredObjectRepository implements StoredObjectRepositoryInterface
|
||||
{
|
||||
@@ -53,6 +54,21 @@ final readonly class StoredObjectRepository implements StoredObjectRepositoryInt
|
||||
return $this->repository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
public function findByExpired(\DateTimeImmutable $expiredAtDate): iterable
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('stored_object');
|
||||
$qb
|
||||
->where('stored_object.deleteAt <= :expiredAt')
|
||||
->setParameter('expiredAt', $expiredAtDate);
|
||||
|
||||
return $qb->getQuery()->toIterable(hydrationMode: Query::HYDRATE_OBJECT);
|
||||
}
|
||||
|
||||
public function findOneByUUID(string $uuid): ?StoredObject
|
||||
{
|
||||
return $this->repository->findOneBy(['uuid' => $uuid]);
|
||||
}
|
||||
|
||||
public function getClassName(): string
|
||||
{
|
||||
return StoredObject::class;
|
||||
|
@@ -17,4 +17,12 @@ use Doctrine\Persistence\ObjectRepository;
|
||||
/**
|
||||
* @extends ObjectRepository<StoredObject>
|
||||
*/
|
||||
interface StoredObjectRepositoryInterface extends ObjectRepository {}
|
||||
interface StoredObjectRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
/**
|
||||
* @return iterable<StoredObject>
|
||||
*/
|
||||
public function findByExpired(\DateTimeImmutable $expiredAtDate): iterable;
|
||||
|
||||
public function findOneByUUID(string $uuid): ?StoredObject;
|
||||
}
|
||||
|
@@ -0,0 +1,94 @@
|
||||
<?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\StoredObjectVersion;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
/**
|
||||
* @implements ObjectRepository<StoredObjectVersion>
|
||||
*/
|
||||
class StoredObjectVersionRepository implements ObjectRepository
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
|
||||
private readonly Connection $connection;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->repository = $entityManager->getRepository(StoredObjectVersion::class);
|
||||
$this->connection = $entityManager->getConnection();
|
||||
}
|
||||
|
||||
public function find($id): ?StoredObjectVersion
|
||||
{
|
||||
return $this->repository->find($id);
|
||||
}
|
||||
|
||||
public function findAll(): array
|
||||
{
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria): ?StoredObjectVersion
|
||||
{
|
||||
return $this->repository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the IDs of versions older than a given date and that are not the last version.
|
||||
*
|
||||
* Those version are good candidates for a deletion.
|
||||
*
|
||||
* @param \DateTimeImmutable $beforeDate the date to compare versions against
|
||||
*
|
||||
* @return iterable returns an iterable with the IDs of the versions
|
||||
*/
|
||||
public function findIdsByVersionsOlderThanDateAndNotLastVersionAndNotPointInTime(\DateTimeImmutable $beforeDate): iterable
|
||||
{
|
||||
$results = $this->connection->executeQuery(
|
||||
self::QUERY_FIND_IDS_BY_VERSIONS_OLDER_THAN_DATE_AND_NOT_LAST_VERSION,
|
||||
[$beforeDate],
|
||||
[Types::DATETIME_IMMUTABLE]
|
||||
);
|
||||
|
||||
foreach ($results->iterateAssociative() as $row) {
|
||||
yield $row['sov_id'];
|
||||
}
|
||||
}
|
||||
|
||||
private const QUERY_FIND_IDS_BY_VERSIONS_OLDER_THAN_DATE_AND_NOT_LAST_VERSION = <<<'SQL'
|
||||
SELECT
|
||||
sov.id AS sov_id
|
||||
FROM chill_doc.stored_object_version sov
|
||||
WHERE
|
||||
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
|
||||
{
|
||||
return StoredObjectVersion::class;
|
||||
}
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
import {makeFetch} from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||
import {PostStoreObjectSignature, StoredObject} from "../../types";
|
||||
|
||||
const algo = 'AES-CBC';
|
||||
|
||||
const URL_POST = '/asyncupload/temp_url/generate/post';
|
||||
|
||||
const keyDefinition = {
|
||||
name: algo,
|
||||
length: 256
|
||||
};
|
||||
|
||||
const createFilename = (): string => {
|
||||
var text = "";
|
||||
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches a new stored object from the server.
|
||||
*
|
||||
* @async
|
||||
* @function fetchNewStoredObject
|
||||
* @returns {Promise<StoredObject>} A Promise that resolves to the newly created StoredObject.
|
||||
*/
|
||||
export const fetchNewStoredObject = async (): Promise<StoredObject> => {
|
||||
return makeFetch("POST", '/api/1.0/doc-store/stored-object/create', null);
|
||||
}
|
||||
|
||||
export const uploadVersion = async (uploadFile: ArrayBuffer, storedObject: StoredObject): Promise<string> => {
|
||||
const params = new URLSearchParams();
|
||||
params.append('expires_delay', "180");
|
||||
params.append('submit_delay', "180");
|
||||
const asyncData: PostStoreObjectSignature = await makeFetch("GET", `/api/1.0/doc-store/async-upload/temp_url/${storedObject.uuid}/generate/post` + "?" + params.toString());
|
||||
const suffix = createFilename();
|
||||
const filename = asyncData.prefix + suffix;
|
||||
const formData = new FormData();
|
||||
formData.append("redirect", asyncData.redirect);
|
||||
formData.append("max_file_size", asyncData.max_file_size.toString());
|
||||
formData.append("max_file_count", asyncData.max_file_count.toString());
|
||||
formData.append("expires", asyncData.expires.toString());
|
||||
formData.append("signature", asyncData.signature);
|
||||
formData.append(filename, new Blob([uploadFile]), suffix);
|
||||
|
||||
const response = await window.fetch(asyncData.url, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Error while sending file to store", response);
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
return Promise.resolve(filename);
|
||||
}
|
||||
|
||||
export const encryptFile = async (originalFile: ArrayBuffer): Promise<[ArrayBuffer, Uint8Array, JsonWebKey]> => {
|
||||
const iv = crypto.getRandomValues(new Uint8Array(16));
|
||||
const key = await window.crypto.subtle.generateKey(keyDefinition, true, [ "encrypt", "decrypt" ]);
|
||||
const exportedKey = await window.crypto.subtle.exportKey('jwk', key);
|
||||
const encrypted = await window.crypto.subtle.encrypt({ name: algo, iv: iv}, key, originalFile);
|
||||
|
||||
return Promise.resolve([encrypted, iv, exportedKey]);
|
||||
};
|
@@ -1,22 +1,18 @@
|
||||
import { CollectionEventPayload } from "../../../../../ChillMainBundle/Resources/public/module/collection";
|
||||
import { createApp } from "vue";
|
||||
import DropFileWidget from "../../vuejs/DropFileWidget/DropFileWidget.vue";
|
||||
import { StoredObject, StoredObjectCreated } from "../../types";
|
||||
import { _createI18n } from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
|
||||
import {CollectionEventPayload} from "../../../../../ChillMainBundle/Resources/public/module/collection";
|
||||
import {createApp} from "vue";
|
||||
import DropFileWidget from "../../vuejs/DropFileWidget/DropFileWidget.vue"
|
||||
import {StoredObject, StoredObjectVersion} from "../../types";
|
||||
import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
|
||||
const i18n = _createI18n({});
|
||||
|
||||
const startApp = (
|
||||
divElement: HTMLDivElement,
|
||||
collectionEntry: null | HTMLLIElement,
|
||||
): void => {
|
||||
console.log("app started", divElement);
|
||||
const input_stored_object: HTMLInputElement | null =
|
||||
divElement.querySelector("input[data-stored-object]");
|
||||
const startApp = (divElement: HTMLDivElement, collectionEntry: null|HTMLLIElement): void => {
|
||||
console.log('app started', divElement);
|
||||
const input_stored_object: HTMLInputElement|null = divElement.querySelector("input[data-stored-object]");
|
||||
if (null === input_stored_object) {
|
||||
throw new Error("input to stored object not found");
|
||||
throw new Error('input to stored object not found');
|
||||
}
|
||||
|
||||
let existingDoc: StoredObject | null = null;
|
||||
let existingDoc: StoredObject|null = null;
|
||||
if (input_stored_object.value !== "") {
|
||||
existingDoc = JSON.parse(input_stored_object.value);
|
||||
}
|
||||
@@ -24,77 +20,69 @@ const startApp = (
|
||||
divElement.appendChild(app_container);
|
||||
|
||||
const app = createApp({
|
||||
template:
|
||||
'<drop-file-widget :existingDoc="this.$data.existingDoc" :allowRemove="true" @addDocument="this.addDocument" @removeDocument="removeDocument"></drop-file-widget>',
|
||||
template: '<drop-file-widget :existingDoc="this.$data.existingDoc" :allowRemove="true" @addDocument="this.addDocument" @removeDocument="removeDocument"></drop-file-widget>',
|
||||
data(vm) {
|
||||
return {
|
||||
existingDoc: existingDoc,
|
||||
};
|
||||
}
|
||||
},
|
||||
components: {
|
||||
DropFileWidget,
|
||||
},
|
||||
methods: {
|
||||
addDocument: function (object: StoredObjectCreated): void {
|
||||
console.log("object added", object);
|
||||
this.$data.existingDoc = object;
|
||||
input_stored_object.value = JSON.stringify(object);
|
||||
addDocument: function({stored_object, stored_object_version}: {stored_object: StoredObject, stored_object_version: StoredObjectVersion}): void {
|
||||
console.log('object added', stored_object);
|
||||
console.log('version added', stored_object_version);
|
||||
this.$data.existingDoc = stored_object;
|
||||
this.$data.existingDoc.currentVersion = stored_object_version;
|
||||
input_stored_object.value = JSON.stringify(this.$data.existingDoc);
|
||||
},
|
||||
removeDocument: function (object: StoredObject): void {
|
||||
console.log("catch remove document", object);
|
||||
removeDocument: function(object: StoredObject): void {
|
||||
console.log('catch remove document', object);
|
||||
input_stored_object.value = "";
|
||||
this.$data.existingDoc = null;
|
||||
console.log("collectionEntry", collectionEntry);
|
||||
this.$data.existingDoc = undefined;
|
||||
console.log('collectionEntry', collectionEntry);
|
||||
|
||||
if (null !== collectionEntry) {
|
||||
console.log("will remove collection");
|
||||
console.log('will remove collection');
|
||||
collectionEntry.remove();
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.use(i18n).mount(app_container);
|
||||
};
|
||||
window.addEventListener("collection-add-entry", ((
|
||||
e: CustomEvent<CollectionEventPayload>,
|
||||
) => {
|
||||
}
|
||||
window.addEventListener('collection-add-entry', ((e: CustomEvent<CollectionEventPayload>) => {
|
||||
const detail = e.detail;
|
||||
const divElement: null | HTMLDivElement = detail.entry.querySelector(
|
||||
"div[data-stored-object]",
|
||||
);
|
||||
const divElement: null|HTMLDivElement = detail.entry.querySelector('div[data-stored-object]');
|
||||
|
||||
if (null === divElement) {
|
||||
throw new Error("div[data-stored-object] not found");
|
||||
throw new Error('div[data-stored-object] not found');
|
||||
}
|
||||
|
||||
startApp(divElement, detail.entry);
|
||||
}) as EventListener);
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const upload_inputs: NodeListOf<HTMLDivElement> = document.querySelectorAll(
|
||||
"div[data-stored-object]",
|
||||
);
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const upload_inputs: NodeListOf<HTMLDivElement> = document.querySelectorAll('div[data-stored-object]');
|
||||
|
||||
upload_inputs.forEach((input: HTMLDivElement): void => {
|
||||
// test for a parent to check if this is a collection entry
|
||||
let collectionEntry: null | HTMLLIElement = null;
|
||||
const parent = input.parentElement;
|
||||
console.log("parent", parent);
|
||||
let collectionEntry: null|HTMLLIElement = null;
|
||||
let parent = input.parentElement;
|
||||
console.log('parent', parent);
|
||||
if (null !== parent) {
|
||||
const grandParent = parent.parentElement;
|
||||
console.log("grandParent", grandParent);
|
||||
let grandParent = parent.parentElement;
|
||||
console.log('grandParent', grandParent);
|
||||
if (null !== grandParent) {
|
||||
if (
|
||||
grandParent.tagName.toLowerCase() === "li" &&
|
||||
grandParent.classList.contains("entry")
|
||||
) {
|
||||
if (grandParent.tagName.toLowerCase() === 'li' && grandParent.classList.contains('entry')) {
|
||||
collectionEntry = grandParent as HTMLLIElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
startApp(input, collectionEntry);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
export {};
|
||||
export {}
|
||||
|
@@ -0,0 +1,27 @@
|
||||
import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
|
||||
import {createApp} from "vue";
|
||||
import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue";
|
||||
import {StoredObject, StoredObjectStatusChange} from "../../types";
|
||||
import {defineComponent} from "vue";
|
||||
import DownloadButton from "../../vuejs/StoredObjectButton/DownloadButton.vue";
|
||||
import ToastPlugin from "vue-toast-notification";
|
||||
|
||||
|
||||
|
||||
const i18n = _createI18n({});
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function (e) {
|
||||
document.querySelectorAll<HTMLDivElement>('div[data-download-button-single]').forEach((el) => {
|
||||
const storedObject = JSON.parse(el.dataset.storedObject as string) as StoredObject;
|
||||
const title = el.dataset.title as string;
|
||||
const app = createApp({
|
||||
components: {DownloadButton},
|
||||
data() {
|
||||
return {storedObject, title, classes: {btn: true, "btn-outline-primary": true}};
|
||||
},
|
||||
template: '<download-button :stored-object="storedObject" :at-version="storedObject.currentVersion" :classes="classes" :filename="title" :direct-download="true"></download-button>',
|
||||
});
|
||||
|
||||
app.use(i18n).use(ToastPlugin).mount(el);
|
||||
});
|
||||
});
|
@@ -1,72 +1,54 @@
|
||||
import { _createI18n } from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
|
||||
import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
|
||||
import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue";
|
||||
import { createApp } from "vue";
|
||||
import { StoredObject, StoredObjectStatusChange } from "../../types";
|
||||
import { is_object_ready } from "../../vuejs/StoredObjectButton/helpers";
|
||||
import {createApp} from "vue";
|
||||
import {StoredObject, StoredObjectStatusChange} from "../../types";
|
||||
import {is_object_ready} from "../../vuejs/StoredObjectButton/helpers";
|
||||
import ToastPlugin from "vue-toast-notification";
|
||||
|
||||
const i18n = _createI18n({});
|
||||
|
||||
window.addEventListener("DOMContentLoaded", function (e) {
|
||||
document
|
||||
.querySelectorAll<HTMLDivElement>("div[data-download-buttons]")
|
||||
.forEach((el) => {
|
||||
const app = createApp({
|
||||
components: { DocumentActionButtonsGroup },
|
||||
data() {
|
||||
const datasets = el.dataset as {
|
||||
filename: string;
|
||||
canEdit: string;
|
||||
storedObject: string;
|
||||
buttonSmall: string;
|
||||
davLink: string;
|
||||
davLinkExpiration: string;
|
||||
};
|
||||
window.addEventListener('DOMContentLoaded', function (e) {
|
||||
document.querySelectorAll<HTMLDivElement>('div[data-download-buttons]').forEach((el) => {
|
||||
const app = createApp({
|
||||
components: {DocumentActionButtonsGroup},
|
||||
data() {
|
||||
|
||||
const storedObject = JSON.parse(
|
||||
datasets.storedObject,
|
||||
) as StoredObject,
|
||||
filename = datasets.filename,
|
||||
canEdit = datasets.canEdit === "1",
|
||||
small = datasets.buttonSmall === "1",
|
||||
davLink =
|
||||
"davLink" in datasets && datasets.davLink !== ""
|
||||
? datasets.davLink
|
||||
: null,
|
||||
davLinkExpiration =
|
||||
"davLinkExpiration" in datasets
|
||||
? Number.parseInt(datasets.davLinkExpiration)
|
||||
: null;
|
||||
return {
|
||||
storedObject,
|
||||
filename,
|
||||
canEdit,
|
||||
small,
|
||||
davLink,
|
||||
davLinkExpiration,
|
||||
};
|
||||
},
|
||||
template:
|
||||
'<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small" :dav-link="davLink" :dav-link-expiration="davLinkExpiration" @on-stored-object-status-change="onStoredObjectStatusChange"></document-action-buttons-group>',
|
||||
methods: {
|
||||
onStoredObjectStatusChange: function (
|
||||
newStatus: StoredObjectStatusChange,
|
||||
): void {
|
||||
this.$data.storedObject.status = newStatus.status;
|
||||
this.$data.storedObject.filename = newStatus.filename;
|
||||
this.$data.storedObject.type = newStatus.type;
|
||||
const datasets = el.dataset as {
|
||||
filename: string,
|
||||
canEdit: string,
|
||||
storedObject: string,
|
||||
buttonSmall: string,
|
||||
davLink: string,
|
||||
davLinkExpiration: string,
|
||||
};
|
||||
|
||||
// remove eventual div which inform pending status
|
||||
document
|
||||
.querySelectorAll(
|
||||
`[data-docgen-is-pending="${this.$data.storedObject.id}"]`,
|
||||
)
|
||||
.forEach(function (el) {
|
||||
el.remove();
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
const
|
||||
storedObject = JSON.parse(datasets.storedObject) as StoredObject,
|
||||
filename = datasets.filename,
|
||||
canEdit = datasets.canEdit === '1',
|
||||
small = datasets.buttonSmall === '1',
|
||||
davLink = 'davLink' in datasets && datasets.davLink !== '' ? datasets.davLink : null,
|
||||
davLinkExpiration = 'davLinkExpiration' in datasets ? Number.parseInt(datasets.davLinkExpiration) : null
|
||||
;
|
||||
|
||||
app.use(i18n).mount(el);
|
||||
});
|
||||
return { storedObject, filename, canEdit, small, davLink, davLinkExpiration };
|
||||
},
|
||||
template: '<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small" :dav-link="davLink" :dav-link-expiration="davLinkExpiration" @on-stored-object-status-change="onStoredObjectStatusChange"></document-action-buttons-group>',
|
||||
methods: {
|
||||
onStoredObjectStatusChange: function(newStatus: StoredObjectStatusChange): void {
|
||||
this.$data.storedObject.status = newStatus.status;
|
||||
this.$data.storedObject.filename = newStatus.filename;
|
||||
this.$data.storedObject.type = newStatus.type;
|
||||
|
||||
// remove eventual div which inform pending status
|
||||
document.querySelectorAll(`[data-docgen-is-pending="${this.$data.storedObject.id}"]`)
|
||||
.forEach(function(el) {
|
||||
el.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.use(i18n).use(ToastPlugin).mount(el);
|
||||
})
|
||||
});
|
||||
|
@@ -1,38 +1,62 @@
|
||||
import { DateTime } from "../../../ChillMainBundle/Resources/public/types";
|
||||
import {
|
||||
DateTime,
|
||||
User,
|
||||
} from "../../../ChillMainBundle/Resources/public/types";
|
||||
import {SignedUrlGet} from "./vuejs/StoredObjectButton/helpers";
|
||||
|
||||
export type StoredObjectStatus = "ready" | "failure" | "pending";
|
||||
export type StoredObjectStatus = "empty" | "ready" | "failure" | "pending";
|
||||
|
||||
export interface StoredObject {
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* filename of the object in the object storage
|
||||
*/
|
||||
filename: string;
|
||||
creationDate: DateTime;
|
||||
datas: object;
|
||||
iv: number[];
|
||||
keyInfos: object;
|
||||
title: string;
|
||||
type: string;
|
||||
title: string | null;
|
||||
uuid: string;
|
||||
prefix: string;
|
||||
status: StoredObjectStatus;
|
||||
currentVersion:
|
||||
| null
|
||||
| StoredObjectVersionCreated
|
||||
| StoredObjectVersionPersisted;
|
||||
totalVersions: number;
|
||||
datas: object;
|
||||
/** @deprecated */
|
||||
creationDate: DateTime;
|
||||
createdAt: DateTime | null;
|
||||
createdBy: User | null;
|
||||
_permissions: {
|
||||
canEdit: boolean;
|
||||
canSee: boolean;
|
||||
};
|
||||
_links?: {
|
||||
dav_link?: {
|
||||
href: string;
|
||||
expiration: number;
|
||||
};
|
||||
downloadLink?: SignedUrlGet;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StoredObjectCreated {
|
||||
status: "stored_object_created";
|
||||
export interface StoredObjectVersion {
|
||||
/**
|
||||
* filename of the object in the object storage
|
||||
*/
|
||||
filename: string;
|
||||
iv: Uint8Array;
|
||||
keyInfos: object;
|
||||
iv: number[];
|
||||
keyInfos: JsonWebKey;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface StoredObjectVersionCreated extends StoredObjectVersion {
|
||||
persisted: false;
|
||||
}
|
||||
|
||||
export interface StoredObjectVersionPersisted
|
||||
extends StoredObjectVersionCreated {
|
||||
version: number;
|
||||
id: number;
|
||||
createdAt: DateTime | null;
|
||||
createdBy: User | null;
|
||||
}
|
||||
|
||||
export interface StoredObjectStatusChange {
|
||||
id: number;
|
||||
filename: string;
|
||||
@@ -40,10 +64,23 @@ export interface StoredObjectStatusChange {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface StoredObjectVersionWithPointInTime extends StoredObjectVersionPersisted {
|
||||
"point-in-times": StoredObjectPointInTime[];
|
||||
"from-restored": StoredObjectVersionPersisted|null;
|
||||
}
|
||||
|
||||
export interface StoredObjectPointInTime {
|
||||
id: number;
|
||||
byUser: User | null;
|
||||
reason: 'keep-before-conversion'|'keep-by-user';
|
||||
}
|
||||
|
||||
/**
|
||||
* Function executed by the WopiEditButton component.
|
||||
*/
|
||||
export type WopiEditButtonExecutableBeforeLeaveFunction = () => Promise<void>;
|
||||
export type WopiEditButtonExecutableBeforeLeaveFunction = {
|
||||
(): Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Object containing information for performering a POST request to a swift object store
|
||||
@@ -59,3 +96,46 @@ export interface PostStoreObjectSignature {
|
||||
url: string;
|
||||
signature: string;
|
||||
}
|
||||
|
||||
export interface PDFPage {
|
||||
index: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
export interface SignatureZone {
|
||||
index: number | null;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
PDFPage: PDFPage;
|
||||
}
|
||||
|
||||
export interface Signature {
|
||||
id: number;
|
||||
storedObject: StoredObject;
|
||||
zones: SignatureZone[];
|
||||
}
|
||||
|
||||
export type SignedState =
|
||||
| "pending"
|
||||
| "signed"
|
||||
| "rejected"
|
||||
| "canceled"
|
||||
| "error";
|
||||
|
||||
export interface CheckSignature {
|
||||
state: SignedState;
|
||||
storedObject: StoredObject;
|
||||
}
|
||||
|
||||
export type CanvasEvent = "select" | "add";
|
||||
|
||||
export interface ZoomLevel {
|
||||
id: number;
|
||||
zoom: number;
|
||||
label: {
|
||||
fr?: string,
|
||||
nl?: string
|
||||
};
|
||||
}
|
@@ -1,152 +1,92 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="
|
||||
'ready' === props.storedObject.status ||
|
||||
'stored_object_created' === props.storedObject.status
|
||||
"
|
||||
class="btn-group"
|
||||
>
|
||||
<button
|
||||
:class="
|
||||
Object.assign({
|
||||
btn: true,
|
||||
'btn-outline-primary': true,
|
||||
'dropdown-toggle': true,
|
||||
'btn-sm': props.small,
|
||||
})
|
||||
"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
Actions
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li
|
||||
v-if="
|
||||
props.canEdit &&
|
||||
is_extension_editable(props.storedObject.type) &&
|
||||
props.storedObject.status !== 'stored_object_created'
|
||||
"
|
||||
>
|
||||
<wopi-edit-button
|
||||
:stored-object="props.storedObject"
|
||||
:classes="{ 'dropdown-item': true }"
|
||||
:execute-before-leave="props.executeBeforeLeave"
|
||||
></wopi-edit-button>
|
||||
</li>
|
||||
<li
|
||||
v-if="
|
||||
props.canEdit &&
|
||||
is_extension_editable(props.storedObject.type) &&
|
||||
props.davLink !== undefined &&
|
||||
props.davLinkExpiration !== undefined
|
||||
"
|
||||
>
|
||||
<desktop-edit-button
|
||||
:classes="{ 'dropdown-item': true }"
|
||||
:edit-link="props.davLink"
|
||||
:expiration-link="props.davLinkExpiration"
|
||||
></desktop-edit-button>
|
||||
</li>
|
||||
<li
|
||||
v-if="
|
||||
props.storedObject.type != 'application/pdf' &&
|
||||
is_extension_viewable(props.storedObject.type) &&
|
||||
props.canConvertPdf &&
|
||||
props.storedObject.status !== 'stored_object_created'
|
||||
"
|
||||
>
|
||||
<convert-button
|
||||
:stored-object="props.storedObject"
|
||||
:filename="filename"
|
||||
:classes="{ 'dropdown-item': true }"
|
||||
></convert-button>
|
||||
</li>
|
||||
<li v-if="props.canDownload">
|
||||
<download-button
|
||||
:stored-object="props.storedObject"
|
||||
:filename="filename"
|
||||
:classes="{ 'dropdown-item': true }"
|
||||
></download-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else-if="'pending' === props.storedObject.status">
|
||||
<div class="btn btn-outline-info">Génération en cours</div>
|
||||
</div>
|
||||
<div v-else-if="'failure' === props.storedObject.status">
|
||||
<div class="btn btn-outline-danger">La génération a échoué</div>
|
||||
</div>
|
||||
<div v-if="isButtonGroupDisplayable" class="btn-group">
|
||||
<button :class="Object.assign({'btn': true, 'btn-outline-primary': true, 'dropdown-toggle': true, 'btn-sm': props.small})" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Actions
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li v-if="isEditableOnline">
|
||||
<wopi-edit-button :stored-object="props.storedObject" :classes="{'dropdown-item': true}" :execute-before-leave="props.executeBeforeLeave"></wopi-edit-button>
|
||||
</li>
|
||||
<li v-if="isEditableOnDesktop">
|
||||
<desktop-edit-button :classes="{'dropdown-item': true}" :edit-link="props.davLink" :expiration-link="props.davLinkExpiration"></desktop-edit-button>
|
||||
</li>
|
||||
<li v-if="isConvertibleToPdf">
|
||||
<convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button>
|
||||
</li>
|
||||
<li v-if="isDownloadable">
|
||||
<download-button :stored-object="props.storedObject" :at-version="props.storedObject.currentVersion" :filename="filename" :classes="{'dropdown-item': true}" :display-action-string-in-button="true"></download-button>
|
||||
</li>
|
||||
<li v-if="isHistoryViewable">
|
||||
<history-button :stored-object="props.storedObject" :can-edit="canEdit && props.storedObject._permissions.canEdit"></history-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else-if="'pending' === props.storedObject.status">
|
||||
<div class="btn btn-outline-info">Génération en cours</div>
|
||||
</div>
|
||||
<div v-else-if="'failure' === props.storedObject.status">
|
||||
<div class="btn btn-outline-danger">La génération a échoué</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from "vue";
|
||||
|
||||
import {computed, onMounted} from "vue";
|
||||
import ConvertButton from "./StoredObjectButton/ConvertButton.vue";
|
||||
import DownloadButton from "./StoredObjectButton/DownloadButton.vue";
|
||||
import WopiEditButton from "./StoredObjectButton/WopiEditButton.vue";
|
||||
import {
|
||||
is_extension_editable,
|
||||
is_extension_viewable,
|
||||
is_object_ready,
|
||||
} from "./StoredObjectButton/helpers";
|
||||
import {is_extension_editable, is_extension_viewable, is_object_ready} from "./StoredObjectButton/helpers";
|
||||
import {
|
||||
StoredObject,
|
||||
StoredObjectCreated,
|
||||
StoredObjectStatusChange,
|
||||
WopiEditButtonExecutableBeforeLeaveFunction,
|
||||
StoredObjectStatusChange, StoredObjectVersion,
|
||||
WopiEditButtonExecutableBeforeLeaveFunction
|
||||
} from "../types";
|
||||
import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue";
|
||||
import HistoryButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton.vue";
|
||||
|
||||
interface DocumentActionButtonsGroupConfig {
|
||||
storedObject: StoredObject | StoredObjectCreated;
|
||||
small?: boolean;
|
||||
canEdit?: boolean;
|
||||
canDownload?: boolean;
|
||||
canConvertPdf?: boolean;
|
||||
returnPath?: string;
|
||||
storedObject: StoredObject,
|
||||
small?: boolean,
|
||||
canEdit?: boolean,
|
||||
canDownload?: boolean,
|
||||
canConvertPdf?: boolean,
|
||||
returnPath?: string,
|
||||
|
||||
/**
|
||||
* Will be the filename displayed to the user when he·she download the document
|
||||
* (the document will be saved on his disk with this name)
|
||||
*
|
||||
* If not set, 'document' will be used.
|
||||
*/
|
||||
filename?: string;
|
||||
/**
|
||||
* Will be the filename displayed to the user when he·she download the document
|
||||
* (the document will be saved on his disk with this name)
|
||||
*
|
||||
* If not set, 'document' will be used.
|
||||
*/
|
||||
filename?: string,
|
||||
|
||||
/**
|
||||
* If set, will execute this function before leaving to the editor
|
||||
*/
|
||||
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction;
|
||||
/**
|
||||
* If set, will execute this function before leaving to the editor
|
||||
*/
|
||||
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
|
||||
|
||||
/**
|
||||
* a link to download and edit file using webdav
|
||||
*/
|
||||
davLink?: string;
|
||||
/**
|
||||
* a link to download and edit file using webdav
|
||||
*/
|
||||
davLink?: string,
|
||||
|
||||
/**
|
||||
* the expiration date of the download, as a unix timestamp
|
||||
*/
|
||||
davLinkExpiration?: number;
|
||||
/**
|
||||
* the expiration date of the download, as a unix timestamp
|
||||
*/
|
||||
davLinkExpiration?: number,
|
||||
}
|
||||
|
||||
const emit =
|
||||
defineEmits<
|
||||
(
|
||||
e: "onStoredObjectStatusChange",
|
||||
newStatus: StoredObjectStatusChange,
|
||||
) => void
|
||||
>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'onStoredObjectStatusChange', newStatus: StoredObjectStatusChange): void
|
||||
}>();
|
||||
|
||||
const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), {
|
||||
small: false,
|
||||
canEdit: true,
|
||||
canDownload: true,
|
||||
canConvertPdf: true,
|
||||
returnPath:
|
||||
window.location.pathname +
|
||||
window.location.search +
|
||||
window.location.hash,
|
||||
small: false,
|
||||
canEdit: true,
|
||||
canDownload: true,
|
||||
canConvertPdf: true,
|
||||
returnPath: window.location.pathname + window.location.search + window.location.hash
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -159,46 +99,86 @@ let tryiesForReady = 0;
|
||||
*/
|
||||
const maxTryiesForReady = 120;
|
||||
|
||||
const checkForReady = function (): void {
|
||||
if (
|
||||
"ready" === props.storedObject.status ||
|
||||
"failure" === props.storedObject.status ||
|
||||
"stored_object_created" === props.storedObject.status ||
|
||||
// stop reloading if the page stays opened for a long time
|
||||
tryiesForReady > maxTryiesForReady
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const isButtonGroupDisplayable = computed<boolean>(() => {
|
||||
return isDownloadable.value || isEditableOnline.value || isEditableOnDesktop.value || isConvertibleToPdf.value;
|
||||
});
|
||||
|
||||
tryiesForReady = tryiesForReady + 1;
|
||||
const isDownloadable = computed<boolean>(() => {
|
||||
return props.storedObject.status === 'ready'
|
||||
// happens when the stored object version is just added, but not persisted
|
||||
|| (props.storedObject.currentVersion !== null && props.storedObject.status === 'empty')
|
||||
});
|
||||
|
||||
setTimeout(onObjectNewStatusCallback, 5000);
|
||||
const isEditableOnline = computed<boolean>(() => {
|
||||
return props.storedObject.status === 'ready'
|
||||
&& props.storedObject._permissions.canEdit
|
||||
&& props.canEdit
|
||||
&& props.storedObject.currentVersion !== null
|
||||
&& is_extension_editable(props.storedObject.currentVersion.type)
|
||||
&& props.storedObject.currentVersion.persisted !== false;
|
||||
});
|
||||
|
||||
const isEditableOnDesktop = computed<boolean>(() => {
|
||||
return isEditableOnline.value;
|
||||
});
|
||||
|
||||
const isConvertibleToPdf = computed<boolean>(() => {
|
||||
return props.storedObject.status === 'ready'
|
||||
&& props.storedObject._permissions.canSee
|
||||
&& props.canConvertPdf
|
||||
&& props.storedObject.currentVersion !== null
|
||||
&& is_extension_viewable(props.storedObject.currentVersion.type)
|
||||
&& props.storedObject.currentVersion.type !== 'application/pdf'
|
||||
&& props.storedObject.currentVersion.persisted !== false;
|
||||
});
|
||||
|
||||
const isHistoryViewable = computed<boolean>(() => {
|
||||
return props.storedObject.status === 'ready';
|
||||
});
|
||||
|
||||
const checkForReady = function(): void {
|
||||
if (
|
||||
'ready' === props.storedObject.status
|
||||
|| 'empty' === props.storedObject.status
|
||||
|| 'failure' === props.storedObject.status
|
||||
// stop reloading if the page stays opened for a long time
|
||||
|| tryiesForReady > maxTryiesForReady
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
tryiesForReady = tryiesForReady + 1;
|
||||
|
||||
setTimeout(onObjectNewStatusCallback, 5000);
|
||||
};
|
||||
|
||||
const onObjectNewStatusCallback = async function (): Promise<void> {
|
||||
if (props.storedObject.status === "stored_object_created") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const new_status = await is_object_ready(props.storedObject);
|
||||
if (props.storedObject.status !== new_status.status) {
|
||||
emit("onStoredObjectStatusChange", new_status);
|
||||
return Promise.resolve();
|
||||
} else if ("failure" === new_status.status) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if ("ready" !== new_status.status) {
|
||||
// we check for new status, unless it is ready
|
||||
checkForReady();
|
||||
}
|
||||
const onObjectNewStatusCallback = async function(): Promise<void> {
|
||||
|
||||
if (props.storedObject.status === 'stored_object_created') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const new_status = await is_object_ready(props.storedObject);
|
||||
if (props.storedObject.status !== new_status.status) {
|
||||
emit('onStoredObjectStatusChange', new_status);
|
||||
return Promise.resolve();
|
||||
} else if ('failure' === new_status.status) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if ('ready' !== new_status.status) {
|
||||
// we check for new status, unless it is ready
|
||||
checkForReady();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkForReady();
|
||||
});
|
||||
checkForReady();
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
@@ -0,0 +1,845 @@
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<modal v-if="modalOpen" @close="modalOpen = false">
|
||||
<template v-slot:header>
|
||||
<h2>{{ $t("signature_confirmation") }}</h2>
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<div class="signature-modal-body text-center" v-if="loading">
|
||||
<p>{{ $t("electronic_signature_in_progress") }}</p>
|
||||
<div class="loading">
|
||||
<i
|
||||
class="fa fa-circle-o-notch fa-spin fa-3x"
|
||||
:title="$t('loading')"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="signature-modal-body text-center" v-else>
|
||||
<p>{{ $t("you_are_going_to_sign") }}</p>
|
||||
<p>{{ $t("are_you_sure") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<button class="btn btn-action" @click.prevent="confirmSign">
|
||||
{{ $t("yes") }}
|
||||
</button>
|
||||
</template>
|
||||
</modal>
|
||||
</teleport>
|
||||
<div class="col-12 m-auto sticky-top">
|
||||
<div class="row justify-content-center border-bottom pdf-tools d-md-none">
|
||||
<div class="col-5 text-center turn-page">
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
id="zoomSelect"
|
||||
v-model="zoomLevel"
|
||||
@change="setZoomLevel(zoomLevel)"
|
||||
>
|
||||
<option value="" selected disabled>Zoom</option>
|
||||
<option v-for="z in zoomLevels" :value="z.zoom" :key="z.id">
|
||||
{{ z.label.fr }}
|
||||
</option>
|
||||
</select>
|
||||
<template v-if="pageCount > 1">
|
||||
<button
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page <= 1"
|
||||
@click="turnPage(-1)"
|
||||
>
|
||||
❮
|
||||
</button>
|
||||
<span>{{ page }}/{{ pageCount }}</span>
|
||||
<button
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page >= pageCount"
|
||||
@click="turnPage(1)"
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
</template>
|
||||
<template v-if="pageCount > 1">
|
||||
<button
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page <= 1"
|
||||
@click="turnPage(-1)"
|
||||
>
|
||||
❮
|
||||
</button>
|
||||
<span>{{ page }}/{{ pageCount }}</span>
|
||||
<button
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page >= pageCount"
|
||||
@click="turnPage(1)"
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="checkboxMulti"
|
||||
v-model="multiPage"
|
||||
@change="toggleMultiPage"
|
||||
/>
|
||||
<label class="form-check-label" for="checkboxMulti">
|
||||
{{ $t("all_pages") }}
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="signature.zones.length > 0"
|
||||
class="col-5 p-0 text-center turnSignature"
|
||||
>
|
||||
<button
|
||||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
||||
class="btn btn-light btn-sm"
|
||||
@click="turnSignature(-1)"
|
||||
>
|
||||
{{ $t("last_zone") }}
|
||||
</button>
|
||||
<span>|</span>
|
||||
<button
|
||||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
||||
class="btn btn-light btn-sm"
|
||||
@click="turnSignature(1)"
|
||||
>
|
||||
{{ $t("next_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col text-end" v-if="signedState !== 'signed'">
|
||||
<button
|
||||
class="btn btn-misc btn-sm"
|
||||
:hidden="!userSignatureZone"
|
||||
@click="undoSign"
|
||||
v-if="signature.zones.length > 1"
|
||||
:title="$t('choose_another_signature')"
|
||||
>
|
||||
{{ $t("another_zone") }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-misc btn-sm"
|
||||
:hidden="!userSignatureZone"
|
||||
@click="undoSign"
|
||||
v-else
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
</button>
|
||||
<button
|
||||
v-if="userSignatureZone === null"
|
||||
:class="{
|
||||
btn: true,
|
||||
'btn-sm': true,
|
||||
'btn-create': canvasEvent !== 'add',
|
||||
'btn-chill-green': canvasEvent === 'add',
|
||||
active: canvasEvent === 'add',
|
||||
}"
|
||||
@click="toggleAddZone()"
|
||||
:title="$t('add_sign_zone')"
|
||||
>
|
||||
<template v-if="canvasEvent === 'add'">
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="row justify-content-center border-bottom pdf-tools d-none d-md-flex"
|
||||
>
|
||||
<div class="col-5 text-center turn-page ps-3">
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
id="zoomSelect"
|
||||
v-model="zoomLevel"
|
||||
@change="setZoomLevel(zoomLevel)"
|
||||
>
|
||||
<option value="" selected disabled>Zoom</option>
|
||||
<option v-for="z in zoomLevels" :value="z.zoom" :key="z.id">
|
||||
{{ z.label.fr }}
|
||||
</option>
|
||||
</select>
|
||||
<template v-if="pageCount > 1">
|
||||
<button
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page <= 1"
|
||||
@click="turnPage(-1)"
|
||||
>
|
||||
❮
|
||||
</button>
|
||||
<span>{{ page }} / {{ pageCount }}</span>
|
||||
<button
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page >= pageCount"
|
||||
@click="turnPage(1)"
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="checkboxMulti"
|
||||
v-model="multiPage"
|
||||
@change="toggleMultiPage"
|
||||
/>
|
||||
<label class="form-check-label" for="checkboxMulti">
|
||||
{{ $t("see_all_pages") }}
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="signature.zones.length > 0 && signedState !== 'signed'"
|
||||
class="col-4 d-xl-none text-center turnSignature p-0"
|
||||
>
|
||||
<button
|
||||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
||||
class="btn btn-light btn-sm"
|
||||
@click="turnSignature(-1)"
|
||||
>
|
||||
{{ $t("last_zone") }}
|
||||
</button>
|
||||
<span>|</span>
|
||||
<button
|
||||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
||||
class="btn btn-light btn-sm"
|
||||
@click="turnSignature(1)"
|
||||
>
|
||||
{{ $t("next_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="signature.zones.length > 0 && signedState !== 'signed'"
|
||||
class="col-4 d-none d-xl-flex p-0 text-center turnSignature"
|
||||
>
|
||||
<button
|
||||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
||||
class="btn btn-light btn-sm"
|
||||
@click="turnSignature(-1)"
|
||||
>
|
||||
{{ $t("last_sign_zone") }}
|
||||
</button>
|
||||
<span>|</span>
|
||||
<button
|
||||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
||||
class="btn btn-light btn-sm"
|
||||
@click="turnSignature(1)"
|
||||
>
|
||||
{{ $t("next_sign_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col text-end" v-if="signedState !== 'signed'">
|
||||
<button
|
||||
class="btn btn-misc btn-sm"
|
||||
:hidden="!userSignatureZone"
|
||||
@click="undoSign"
|
||||
v-if="signature.zones.length > 1"
|
||||
>
|
||||
{{ $t("choose_another_signature") }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-misc btn-sm"
|
||||
:hidden="!userSignatureZone"
|
||||
@click="undoSign"
|
||||
v-else
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
</button>
|
||||
<button
|
||||
v-if="userSignatureZone === null"
|
||||
:class="{
|
||||
btn: true,
|
||||
'btn-sm': true,
|
||||
'btn-create': canvasEvent !== 'add',
|
||||
'btn-chill-green': canvasEvent === 'add',
|
||||
active: canvasEvent === 'add',
|
||||
}"
|
||||
@click="toggleAddZone()"
|
||||
:title="$t('add_sign_zone')"
|
||||
>
|
||||
<template v-if="canvasEvent !== 'add'">
|
||||
{{ $t("add_zone") }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t("click_on_document") }}
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="multiPage"
|
||||
class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center d-flex flex-column"
|
||||
:class="{ onAddZone: canvasEvent === 'add' }"
|
||||
>
|
||||
<canvas v-for="p in pageCount" :key="p" :id="`canvas-${p}`"></canvas>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center"
|
||||
:class="{ onAddZone: canvasEvent === 'add' }"
|
||||
>
|
||||
<canvas class="m-auto" id="canvas"></canvas>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-12 col-lg-9 m-auto p-4" id="action-buttons">
|
||||
<div class="row">
|
||||
<div class="col d-flex">
|
||||
<a
|
||||
class="btn btn-cancel"
|
||||
v-if="signedState !== 'signed'"
|
||||
:href="getReturnPath()"
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
</a>
|
||||
<a class="btn btn-misc" v-else :href="getReturnPath()">
|
||||
{{ $t("return") }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col text-end" v-if="signedState !== 'signed'">
|
||||
<button
|
||||
class="btn btn-action me-2"
|
||||
:disabled="!userSignatureZone"
|
||||
@click="sign"
|
||||
>
|
||||
{{ $t("sign") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-4" v-else></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, Ref, reactive } from "vue";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import "vue-toast-notification/dist/theme-sugar.css";
|
||||
import {
|
||||
CanvasEvent,
|
||||
CheckSignature,
|
||||
Signature,
|
||||
SignatureZone,
|
||||
SignedState,
|
||||
ZoomLevel,
|
||||
} from "../../types";
|
||||
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||
import * as pdfjsLib from "pdfjs-dist";
|
||||
import {
|
||||
PDFDocumentProxy,
|
||||
PDFPageProxy,
|
||||
} from "pdfjs-dist/types/src/display/api";
|
||||
|
||||
// @ts-ignore
|
||||
import * as PdfWorker from "pdfjs-dist/build/pdf.worker.mjs";
|
||||
console.log(PdfWorker); // incredible but this is needed
|
||||
|
||||
// import { PdfWorker } from 'pdfjs-dist/build/pdf.worker.mjs'
|
||||
// pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker;
|
||||
|
||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||
import {download_and_decrypt_doc, download_doc_as_pdf} from "../StoredObjectButton/helpers";
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
|
||||
|
||||
const multiPage: Ref<boolean> = ref(true);
|
||||
const modalOpen: Ref<boolean> = ref(false);
|
||||
const loading: Ref<boolean> = ref(false);
|
||||
const adding: Ref<boolean> = ref(false);
|
||||
const canvasEvent: Ref<CanvasEvent> = ref("select");
|
||||
const signedState: Ref<SignedState> = ref("pending");
|
||||
const page: Ref<number> = ref(1);
|
||||
const pageCount: Ref<number> = ref(0);
|
||||
const zoom: Ref<number> = ref(1);
|
||||
let zoomLevel = "";
|
||||
const zoomLevels: Ref<ZoomLevel[]> = ref([
|
||||
{
|
||||
id: 0,
|
||||
zoom: 0.75,
|
||||
label: {
|
||||
fr: "75%",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
zoom: zoom.value,
|
||||
label: {
|
||||
fr: "100%",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
zoom: 1.25,
|
||||
label: {
|
||||
fr: "125%",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
zoom: 1.5,
|
||||
label: {
|
||||
fr: "150%",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
zoom: 2,
|
||||
label: {
|
||||
fr: "200%",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
zoom: 3,
|
||||
label: {
|
||||
fr: "300%",
|
||||
},
|
||||
},
|
||||
]);
|
||||
let userSignatureZone: Ref<null | SignatureZone> = ref(null);
|
||||
let pdf = {} as PDFDocumentProxy;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
signature: Signature;
|
||||
}
|
||||
}
|
||||
|
||||
const $toast = useToast();
|
||||
|
||||
const signature = window.signature;
|
||||
|
||||
const setZoomLevel = async (zoomLevel: string) => {
|
||||
zoom.value = Number.parseFloat(zoomLevel);
|
||||
await resetPages();
|
||||
setTimeout(drawAllZones, 200);
|
||||
};
|
||||
|
||||
const mountPdf = async (doc: ArrayBuffer) => {
|
||||
const loadingTask = pdfjsLib.getDocument(doc);
|
||||
pdf = await loadingTask.promise;
|
||||
pageCount.value = pdf.numPages;
|
||||
if (multiPage.value) {
|
||||
await setAllPages();
|
||||
} else {
|
||||
await setPage(1);
|
||||
}
|
||||
};
|
||||
|
||||
const getCanvas = (page: number) =>
|
||||
multiPage.value
|
||||
? (document.getElementById(`canvas-${page}`) as HTMLCanvasElement)
|
||||
: (document.querySelectorAll("canvas")[0] as HTMLCanvasElement);
|
||||
|
||||
const getCanvasId = (canvas: HTMLCanvasElement) => {
|
||||
const number = canvas.id.split("-").pop();
|
||||
return number ? parseInt(number) : 0;
|
||||
};
|
||||
|
||||
const getRenderContext = (pdfPage: PDFPageProxy) => {
|
||||
const scale = 1 * zoom.value;
|
||||
const viewport = pdfPage.getViewport({ scale });
|
||||
const canvas = getCanvas(pdfPage.pageNumber);
|
||||
const context = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
return {
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
};
|
||||
};
|
||||
|
||||
const setAllPages = async () =>
|
||||
Array.from(Array(pageCount.value).keys()).map((p) => setPage(p + 1));
|
||||
|
||||
const setPage = async (page: number) => {
|
||||
const pdfPage = await pdf.getPage(page);
|
||||
const renderContext = getRenderContext(pdfPage);
|
||||
await pdfPage.render(renderContext);
|
||||
};
|
||||
|
||||
const init = () => downloadAndOpen().then(initPdf);
|
||||
|
||||
async function downloadAndOpen(): Promise<Blob> {
|
||||
let raw;
|
||||
try {
|
||||
raw = await download_doc_as_pdf(signature.storedObject);
|
||||
} catch (e) {
|
||||
console.error("error while downloading and decrypting document", e);
|
||||
throw e;
|
||||
}
|
||||
const doc = await raw.arrayBuffer();
|
||||
await mountPdf(doc);
|
||||
return raw;
|
||||
}
|
||||
|
||||
const addCanvasEvents = () => {
|
||||
if (multiPage.value) {
|
||||
Array.from(Array(pageCount.value).keys()).map((p) => {
|
||||
const canvas = getCanvas(p + 1);
|
||||
canvas.addEventListener(
|
||||
"pointerup",
|
||||
(e) => canvasClick(e, canvas),
|
||||
false
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||||
canvas.addEventListener("pointerup", (e) => canvasClick(e, canvas), false);
|
||||
}
|
||||
};
|
||||
|
||||
const initPdf = () => {
|
||||
addCanvasEvents();
|
||||
setTimeout(drawAllZones, 800);
|
||||
};
|
||||
|
||||
const resetPages = () =>
|
||||
multiPage.value ? setAllPages() : setPage(page.value);
|
||||
|
||||
const toggleMultiPage = async () => {
|
||||
await resetPages();
|
||||
setTimeout(drawAllZones, 200);
|
||||
addCanvasEvents();
|
||||
};
|
||||
|
||||
const scaleXToCanvas = (x: number, canvasWidth: number, PDFWidth: number) =>
|
||||
Math.round((x * canvasWidth) / PDFWidth);
|
||||
|
||||
const scaleYToCanvas = (h: number, canvasHeight: number, PDFHeight: number) =>
|
||||
Math.round((h * canvasHeight) / PDFHeight);
|
||||
|
||||
const hitSignature = (
|
||||
zone: SignatureZone,
|
||||
xy: number[],
|
||||
canvas: HTMLCanvasElement
|
||||
) =>
|
||||
scaleXToCanvas(zone.x, canvas.width, zone.PDFPage.width) < xy[0] &&
|
||||
xy[0] <
|
||||
scaleXToCanvas(zone.x + zone.width, canvas.width, zone.PDFPage.width) &&
|
||||
zone.PDFPage.height * zoom.value -
|
||||
scaleYToCanvas(zone.y, canvas.height, zone.PDFPage.height) <
|
||||
xy[1] &&
|
||||
xy[1] <
|
||||
scaleYToCanvas(zone.height - zone.y, canvas.height, zone.PDFPage.height) +
|
||||
zone.PDFPage.height * zoom.value;
|
||||
|
||||
const selectZone = async (z: SignatureZone, canvas: HTMLCanvasElement) => {
|
||||
userSignatureZone.value = z;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
await resetPages();
|
||||
setTimeout(drawAllZones, 200);
|
||||
}
|
||||
};
|
||||
|
||||
const selectZoneEvent = (e: PointerEvent, canvas: HTMLCanvasElement) =>
|
||||
signature.zones
|
||||
.filter(
|
||||
(z) =>
|
||||
(z.PDFPage.index + 1 === getCanvasId(canvas) && multiPage.value) ||
|
||||
(z.PDFPage.index + 1 === page.value && !multiPage.value)
|
||||
)
|
||||
.map((z) => {
|
||||
if (hitSignature(z, [e.offsetX, e.offsetY], canvas)) {
|
||||
if (userSignatureZone.value === null) {
|
||||
selectZone(z, canvas);
|
||||
} else {
|
||||
if (userSignatureZone.value.index === z.index) {
|
||||
sign();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const canvasClick = (e: PointerEvent, canvas: HTMLCanvasElement) =>
|
||||
canvasEvent.value === "select"
|
||||
? selectZoneEvent(e, canvas)
|
||||
: addZoneEvent(e, canvas);
|
||||
|
||||
const turnPage = async (upOrDown: number) => {
|
||||
page.value = page.value + upOrDown;
|
||||
if (multiPage.value) {
|
||||
const canvas = getCanvas(page.value);
|
||||
canvas.scrollIntoView();
|
||||
} else {
|
||||
await setPage(page.value);
|
||||
setTimeout(drawAllZones, 200);
|
||||
}
|
||||
};
|
||||
|
||||
const turnSignature = async (upOrDown: number) => {
|
||||
let zoneIndex = userSignatureZone.value?.index ?? -1;
|
||||
if (zoneIndex < -1) {
|
||||
zoneIndex = -1;
|
||||
}
|
||||
if (zoneIndex < signature.zones.length) {
|
||||
zoneIndex = zoneIndex + upOrDown;
|
||||
} else {
|
||||
zoneIndex = 0;
|
||||
}
|
||||
let currentZone = signature.zones[zoneIndex];
|
||||
if (currentZone) {
|
||||
page.value = currentZone.PDFPage.index + 1;
|
||||
const canvas = getCanvas(currentZone.PDFPage.index + 1);
|
||||
selectZone(currentZone, canvas);
|
||||
canvas.scrollIntoView();
|
||||
}
|
||||
};
|
||||
|
||||
const drawZone = (
|
||||
zone: SignatureZone,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
canvasWidth: number,
|
||||
canvasHeight: number
|
||||
) => {
|
||||
const unselectedBlue = "#007bff";
|
||||
const selectedBlue = "#034286";
|
||||
ctx.strokeStyle =
|
||||
userSignatureZone.value?.index === zone.index
|
||||
? selectedBlue
|
||||
: unselectedBlue;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineJoin = "bevel";
|
||||
ctx.strokeRect(
|
||||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width),
|
||||
zone.PDFPage.height * zoom.value -
|
||||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height),
|
||||
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width),
|
||||
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height)
|
||||
);
|
||||
ctx.font = `bold ${16 * zoom.value}px serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillStyle = "black";
|
||||
const xText =
|
||||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) +
|
||||
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width) / 2;
|
||||
const yText =
|
||||
zone.PDFPage.height * zoom.value -
|
||||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) +
|
||||
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height) / 2;
|
||||
if (userSignatureZone.value?.index === zone.index) {
|
||||
ctx.fillStyle = selectedBlue;
|
||||
ctx.fillText("Signer ici", xText, yText);
|
||||
} else {
|
||||
ctx.fillStyle = unselectedBlue;
|
||||
ctx.fillText("Choisir cette", xText, yText - 12 * zoom.value);
|
||||
ctx.fillText("zone de signature", xText, yText + 12 * zoom.value);
|
||||
}
|
||||
};
|
||||
|
||||
const drawAllZones = () => {
|
||||
if (signedState.value !== "signed") {
|
||||
signature.zones
|
||||
.filter(
|
||||
(z) =>
|
||||
multiPage.value ||
|
||||
(z.PDFPage.index + 1 === page.value && !multiPage.value)
|
||||
)
|
||||
.map((z) => {
|
||||
const canvas = getCanvas(z.PDFPage.index + 1);
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
if (userSignatureZone.value) {
|
||||
if (userSignatureZone.value?.index === z.index) {
|
||||
drawZone(z, ctx, canvas.width, canvas.height);
|
||||
}
|
||||
} else {
|
||||
drawZone(z, ctx, canvas.width, canvas.height);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const checkSignature = () => {
|
||||
const url = `/api/1.0/document/workflow/${signature.id}/check-signature`;
|
||||
return makeFetch<null, CheckSignature>("GET", url)
|
||||
.then((r) => {
|
||||
signedState.value = r.state;
|
||||
signature.storedObject = r.storedObject;
|
||||
checkForReady();
|
||||
})
|
||||
.catch((error) => {
|
||||
signedState.value = "error";
|
||||
console.log("Error while checking the signature", error);
|
||||
$toast.error(
|
||||
`Erreur lors de la vérification de la signature: ${error.txt}`
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const maxTryForReady = 60; //2 minutes for trying to sign
|
||||
let tryForReady = 0;
|
||||
|
||||
const stopTrySigning = () => {
|
||||
loading.value = false;
|
||||
modalOpen.value = false;
|
||||
};
|
||||
|
||||
const checkForReady = () => {
|
||||
if (tryForReady > maxTryForReady) {
|
||||
stopTrySigning();
|
||||
tryForReady = 0;
|
||||
console.log("Reached the maximum number of tentative to try signing");
|
||||
$toast.error(
|
||||
"Le nombre maximum de tentatives pour essayer de signer est atteint"
|
||||
);
|
||||
}
|
||||
if (signedState.value === "rejected") {
|
||||
stopTrySigning();
|
||||
console.log("Signature rejected by the server");
|
||||
$toast.error("Signature rejetée par le serveur");
|
||||
}
|
||||
if (signedState.value === "canceled") {
|
||||
stopTrySigning();
|
||||
console.log("Signature canceled");
|
||||
$toast.error("Signature annulée");
|
||||
}
|
||||
if (signedState.value === "pending") {
|
||||
tryForReady = tryForReady + 1;
|
||||
setTimeout(() => checkSignature(), 2000);
|
||||
} else {
|
||||
stopTrySigning();
|
||||
if (signedState.value === "signed") {
|
||||
userSignatureZone.value = null;
|
||||
downloadAndOpen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sign = () => (modalOpen.value = true);
|
||||
|
||||
const confirmSign = () => {
|
||||
loading.value = true;
|
||||
const url = `/api/1.0/document/workflow/${signature.id}/signature-request`;
|
||||
const body = {
|
||||
storedObject: signature.storedObject,
|
||||
zone: userSignatureZone.value,
|
||||
};
|
||||
makeFetch("POST", url, body)
|
||||
.then((r) => {
|
||||
checkForReady();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error while posting the signature", error);
|
||||
stopTrySigning();
|
||||
$toast.error(
|
||||
`Erreur lors de la soumission de la signature: ${error.txt}`
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const undoSign = async () => {
|
||||
signature.zones = signature.zones.filter((z) => z.index !== null);
|
||||
await resetPages();
|
||||
setTimeout(drawAllZones, 200);
|
||||
userSignatureZone.value = null;
|
||||
adding.value = false;
|
||||
canvasEvent.value = "select";
|
||||
};
|
||||
|
||||
const toggleAddZone = () => {
|
||||
canvasEvent.value === "select"
|
||||
? (canvasEvent.value = "add")
|
||||
: (canvasEvent.value = "select");
|
||||
};
|
||||
|
||||
const addZoneEvent = async (e: PointerEvent, canvas: HTMLCanvasElement) => {
|
||||
const BOX_WIDTH = 180;
|
||||
const BOX_HEIGHT = 90;
|
||||
const PDFPageHeight = canvas.height;
|
||||
const PDFPageWidth = canvas.width;
|
||||
|
||||
const x = e.offsetX;
|
||||
const y = e.offsetY;
|
||||
const newZone: SignatureZone = {
|
||||
index: null,
|
||||
x:
|
||||
scaleXToCanvas(x, canvas.width, PDFPageWidth) -
|
||||
scaleXToCanvas(BOX_WIDTH / 2, canvas.width, PDFPageWidth),
|
||||
y:
|
||||
PDFPageHeight * zoom.value -
|
||||
scaleYToCanvas(y, canvas.height, PDFPageHeight) +
|
||||
scaleYToCanvas(BOX_HEIGHT / 2, canvas.height, PDFPageHeight),
|
||||
width: BOX_WIDTH * zoom.value,
|
||||
height: BOX_HEIGHT * zoom.value,
|
||||
PDFPage: {
|
||||
index: multiPage.value ? getCanvasId(canvas) - 1 : page.value - 1,
|
||||
width: PDFPageWidth,
|
||||
height: PDFPageHeight,
|
||||
},
|
||||
};
|
||||
signature.zones.push(newZone);
|
||||
userSignatureZone.value = newZone;
|
||||
|
||||
await resetPages();
|
||||
setTimeout(drawAllZones, 200);
|
||||
canvasEvent.value = "select";
|
||||
adding.value = true;
|
||||
};
|
||||
|
||||
const getReturnPath = () =>
|
||||
window.location.search
|
||||
? window.location.search.split("?returnPath=")[1] ??
|
||||
window.location.pathname
|
||||
: window.location.pathname;
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
canvas {
|
||||
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.2);
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.onAddZone {
|
||||
cursor: not-allowed;
|
||||
|
||||
canvas {
|
||||
cursor: copy;
|
||||
}
|
||||
}
|
||||
|
||||
div#action-buttons {
|
||||
position: sticky;
|
||||
bottom: 0px;
|
||||
background-color: white;
|
||||
z-index: 100;
|
||||
}
|
||||
div.pdf-tools {
|
||||
background-color: #f3f3f3;
|
||||
font-size: 0.6rem;
|
||||
label {
|
||||
font-size: 0.75rem !important;
|
||||
margin: auto 0 auto 0.3rem;
|
||||
}
|
||||
button {
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
div.turnSignature {
|
||||
span {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1400px) {
|
||||
// background: none;
|
||||
// border: none !important;
|
||||
}
|
||||
}
|
||||
div.turn-page {
|
||||
display: flex;
|
||||
span {
|
||||
font-size: 0.75rem;
|
||||
margin: auto 0.4rem;
|
||||
}
|
||||
select {
|
||||
width: 5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
div.signature-modal-body {
|
||||
height: 8rem;
|
||||
}
|
||||
</style>
|
||||
|
@@ -0,0 +1,39 @@
|
||||
import { createApp } from "vue";
|
||||
// @ts-ignore
|
||||
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
|
||||
import App from "./App.vue";
|
||||
|
||||
const appMessages = {
|
||||
fr: {
|
||||
yes: 'Oui',
|
||||
are_you_sure: 'Êtes-vous sûr·e?',
|
||||
you_are_going_to_sign: 'Vous allez signer le document',
|
||||
signature_confirmation: 'Confirmation de la signature',
|
||||
sign: 'Signer',
|
||||
choose_another_signature: 'Choisir une autre zone',
|
||||
cancel: 'Annuler',
|
||||
last_sign_zone: 'Zone de signature précédente',
|
||||
next_sign_zone: 'Zone de signature suivante',
|
||||
add_sign_zone: 'Ajouter une zone de signature',
|
||||
click_on_document: 'Cliquer sur le document',
|
||||
last_zone: 'Zone précédente',
|
||||
next_zone: 'Zone suivante',
|
||||
add_zone: 'Ajouter une zone',
|
||||
another_zone: 'Autre zone',
|
||||
electronic_signature_in_progress: 'Signature électronique en cours...',
|
||||
loading: 'Chargement...',
|
||||
remove_sign_zone: 'Enlever la zone',
|
||||
return: 'Retour',
|
||||
see_all_pages: 'Voir toutes les pages',
|
||||
all_pages: 'Toutes les pages',
|
||||
}
|
||||
}
|
||||
|
||||
const i18n = _createI18n(appMessages);
|
||||
|
||||
const app = createApp({
|
||||
template: `<app></app>`,
|
||||
})
|
||||
.use(i18n)
|
||||
.component("app", App)
|
||||
.mount("#document-signature");
|
@@ -1,22 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { StoredObject, StoredObjectCreated } from "../../types";
|
||||
import { encryptFile, uploadFile } from "../_components/helper";
|
||||
import { computed, ref, Ref } from "vue";
|
||||
|
||||
import {StoredObject, StoredObjectVersionCreated} from "../../types";
|
||||
import {encryptFile, fetchNewStoredObject, uploadVersion} from "../../js/async-upload/uploader";
|
||||
import {computed, ref, Ref} from "vue";
|
||||
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
|
||||
|
||||
interface DropFileConfig {
|
||||
existingDoc?: StoredObjectCreated | StoredObject;
|
||||
existingDoc?: StoredObject,
|
||||
}
|
||||
|
||||
const props = defineProps<DropFileConfig>();
|
||||
const props = withDefaults(defineProps<DropFileConfig>(), {existingDoc: null});
|
||||
|
||||
const emit =
|
||||
defineEmits<
|
||||
(e: "addDocument", stored_object: StoredObjectCreated) => void
|
||||
>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'addDocument', {stored_object_version: StoredObjectVersionCreated, stored_object: StoredObject}): void,
|
||||
}>();
|
||||
|
||||
const is_dragging: Ref<boolean> = ref(false);
|
||||
const uploading: Ref<boolean> = ref(false);
|
||||
const display_filename: Ref<string | null> = ref(null);
|
||||
const display_filename: Ref<string|null> = ref(null);
|
||||
|
||||
const has_existing_doc = computed<boolean>(() => {
|
||||
return props.existingDoc !== undefined && props.existingDoc !== null;
|
||||
@@ -26,16 +27,15 @@ const onDragOver = (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
is_dragging.value = true;
|
||||
};
|
||||
}
|
||||
|
||||
const onDragLeave = (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
is_dragging.value = false;
|
||||
};
|
||||
}
|
||||
|
||||
const onDrop = (e: DragEvent) => {
|
||||
console.log("on drop", e);
|
||||
e.preventDefault();
|
||||
|
||||
const files = e.dataTransfer?.files;
|
||||
@@ -49,8 +49,8 @@ const onDrop = (e: DragEvent) => {
|
||||
return;
|
||||
}
|
||||
|
||||
handleFile(files[0]);
|
||||
};
|
||||
handleFile(files[0])
|
||||
}
|
||||
|
||||
const onZoneClick = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
@@ -61,122 +61,64 @@ const onZoneClick = (e: Event) => {
|
||||
input.addEventListener("change", onFileChange);
|
||||
|
||||
input.click();
|
||||
};
|
||||
}
|
||||
|
||||
const onFileChange = async (event: Event): Promise<void> => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
console.log("event triggered", input);
|
||||
|
||||
if (input.files && input.files[0]) {
|
||||
console.log("file added", input.files[0]);
|
||||
console.log('file added', input.files[0]);
|
||||
const file = input.files[0];
|
||||
await handleFile(file);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
throw "No file given";
|
||||
};
|
||||
throw 'No file given';
|
||||
}
|
||||
|
||||
const handleFile = async (file: File): Promise<void> => {
|
||||
uploading.value = true;
|
||||
display_filename.value = file.name;
|
||||
const type = file.type;
|
||||
|
||||
// create a stored_object if not exists
|
||||
let stored_object;
|
||||
if (null === props.existingDoc) {
|
||||
stored_object = await fetchNewStoredObject();
|
||||
} else {
|
||||
stored_object = props.existingDoc;
|
||||
}
|
||||
|
||||
const buffer = await file.arrayBuffer();
|
||||
const [encrypted, iv, jsonWebKey] = await encryptFile(buffer);
|
||||
const filename = await uploadFile(encrypted);
|
||||
const filename = await uploadVersion(encrypted, stored_object);
|
||||
|
||||
console.log(iv, jsonWebKey);
|
||||
|
||||
const storedObject: StoredObjectCreated = {
|
||||
const stored_object_version: StoredObjectVersionCreated = {
|
||||
filename: filename,
|
||||
iv,
|
||||
iv: Array.from(iv),
|
||||
keyInfos: jsonWebKey,
|
||||
type: type,
|
||||
status: "stored_object_created",
|
||||
};
|
||||
persisted: false,
|
||||
}
|
||||
|
||||
emit("addDocument", storedObject);
|
||||
emit('addDocument', {stored_object, stored_object_version});
|
||||
uploading.value = false;
|
||||
};
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="drop-file">
|
||||
<div
|
||||
v-if="!uploading"
|
||||
:class="{ area: true, dragging: is_dragging }"
|
||||
@click="onZoneClick"
|
||||
@dragover="onDragOver"
|
||||
@dragleave="onDragLeave"
|
||||
@drop="onDrop"
|
||||
>
|
||||
<div v-if="!uploading" :class="{ area: true, dragging: is_dragging}" @click="onZoneClick" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
|
||||
<p v-if="has_existing_doc" class="file-icon">
|
||||
<i
|
||||
class="fa fa-file-pdf-o"
|
||||
v-if="props.existingDoc?.type === 'application/pdf'"
|
||||
></i>
|
||||
<i
|
||||
class="fa fa-file-word-o"
|
||||
v-else-if="
|
||||
props.existingDoc?.type ===
|
||||
'application/vnd.oasis.opendocument.text'
|
||||
"
|
||||
></i>
|
||||
<i
|
||||
class="fa fa-file-word-o"
|
||||
v-else-if="
|
||||
props.existingDoc?.type ===
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
"
|
||||
></i>
|
||||
<i
|
||||
class="fa fa-file-word-o"
|
||||
v-else-if="props.existingDoc?.type === 'application/msword'"
|
||||
></i>
|
||||
<i
|
||||
class="fa fa-file-excel-o"
|
||||
v-else-if="
|
||||
props.existingDoc?.type ===
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
"
|
||||
></i>
|
||||
<i
|
||||
class="fa fa-file-excel-o"
|
||||
v-else-if="
|
||||
props.existingDoc?.type === 'application/vnd.ms-excel'
|
||||
"
|
||||
></i>
|
||||
<i
|
||||
class="fa fa-file-image-o"
|
||||
v-else-if="props.existingDoc?.type === 'image/jpeg'"
|
||||
></i>
|
||||
<i
|
||||
class="fa fa-file-image-o"
|
||||
v-else-if="props.existingDoc?.type === 'image/png'"
|
||||
></i>
|
||||
<i
|
||||
class="fa fa-file-archive-o"
|
||||
v-else-if="
|
||||
props.existingDoc?.type ===
|
||||
'application/x-zip-compressed'
|
||||
"
|
||||
></i>
|
||||
<i class="fa fa-file-code-o" v-else></i>
|
||||
<file-icon :type="props.existingDoc?.type"></file-icon>
|
||||
</p>
|
||||
|
||||
<p v-if="display_filename !== null" class="display-filename">
|
||||
{{ display_filename }}
|
||||
</p>
|
||||
<p v-if="display_filename !== null" class="display-filename">{{ display_filename }}</p>
|
||||
<!-- todo i18n -->
|
||||
<p v-if="has_existing_doc">
|
||||
Déposez un document ou cliquez ici pour remplacer le document
|
||||
existant
|
||||
</p>
|
||||
<p v-else>
|
||||
Déposez un document ou cliquez ici pour ouvrir le navigateur de
|
||||
fichier
|
||||
</p>
|
||||
<p v-if="has_existing_doc">Déposez un document ou cliquez ici pour remplacer le document existant</p>
|
||||
<p v-else>Déposez un document ou cliquez ici pour ouvrir le navigateur de fichier</p>
|
||||
</div>
|
||||
<div v-else class="waiting">
|
||||
<i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
|
||||
@@ -198,8 +140,7 @@ const handleFile = async (file: File): Promise<void> => {
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
& > .area,
|
||||
& > .waiting {
|
||||
& > .area, & > .waiting {
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
|
||||
@@ -207,6 +148,11 @@ const handleFile = async (file: File): Promise<void> => {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
// require for display in DropFileModal
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
& > .area {
|
||||
@@ -217,4 +163,5 @@ const handleFile = async (file: File): Promise<void> => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||
import {StoredObject, StoredObjectVersion} from "../../types";
|
||||
import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue";
|
||||
import {computed, reactive} from "vue";
|
||||
import {useToast} from 'vue-toast-notification';
|
||||
|
||||
interface DropFileConfig {
|
||||
allowRemove: boolean,
|
||||
existingDoc?: StoredObject,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<DropFileConfig>(), {
|
||||
allowRemove: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'addDocument', {stored_object: StoredObject, stored_object_version: StoredObjectVersion}): void,
|
||||
(e: 'removeDocument'): void
|
||||
}>();
|
||||
|
||||
const $toast = useToast();
|
||||
|
||||
const state = reactive({showModal: false});
|
||||
|
||||
const modalClasses = {"modal-dialog-centered": true, "modal-md": true};
|
||||
|
||||
const buttonState = computed<'add'|'replace'>(() => {
|
||||
if (props.existingDoc === undefined || props.existingDoc === null) {
|
||||
return 'add';
|
||||
}
|
||||
|
||||
return 'replace';
|
||||
})
|
||||
|
||||
function onAddDocument({stored_object, stored_object_version}: {stored_object: StoredObject, stored_object_version: StoredObjectVersion}): void {
|
||||
const message = buttonState.value === 'add' ? "Document ajouté" : "Document remplacé";
|
||||
$toast.success(message);
|
||||
emit('addDocument', {stored_object_version, stored_object});
|
||||
state.showModal = false;
|
||||
}
|
||||
|
||||
function onRemoveDocument(): void {
|
||||
emit('removeDocument');
|
||||
}
|
||||
|
||||
function openModal(): void {
|
||||
state.showModal = true;
|
||||
}
|
||||
|
||||
function closeModal(): void {
|
||||
state.showModal = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button v-if="buttonState === 'add'" @click="openModal" class="btn btn-create">Ajouter un document</button>
|
||||
<button v-else @click="openModal" class="btn btn-edit">Remplacer le document</button>
|
||||
<modal v-if="state.showModal" :modal-dialog-class="modalClasses" @close="closeModal">
|
||||
<template v-slot:body>
|
||||
<drop-file-widget :existing-doc="existingDoc" :allow-remove="allowRemove" @add-document="onAddDocument" @remove-document="onRemoveDocument" ></drop-file-widget>
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { StoredObject, StoredObjectCreated } from "../../types";
|
||||
import { computed, ref, Ref } from "vue";
|
||||
|
||||
import {StoredObject, StoredObjectVersion} from "../../types";
|
||||
import {computed, ref, Ref} from "vue";
|
||||
import DropFile from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFile.vue";
|
||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||
|
||||
interface DropFileConfig {
|
||||
allowRemove: boolean;
|
||||
existingDoc?: StoredObjectCreated | StoredObject;
|
||||
allowRemove: boolean,
|
||||
existingDoc?: StoredObject,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<DropFileConfig>(), {
|
||||
@@ -14,53 +15,51 @@ const props = withDefaults(defineProps<DropFileConfig>(), {
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "addDocument", stored_object: StoredObjectCreated): void;
|
||||
(e: "removeDocument", stored_object: null): void;
|
||||
(e: 'addDocument', {stored_object: StoredObject, stored_object_version: StoredObjectVersion}): void,
|
||||
(e: 'removeDocument'): void
|
||||
}>();
|
||||
|
||||
const has_existing_doc = computed<boolean>(() => {
|
||||
return props.existingDoc !== undefined && props.existingDoc !== null;
|
||||
});
|
||||
|
||||
const dav_link_expiration = computed<number | undefined>(() => {
|
||||
const dav_link_expiration = computed<number|undefined>(() => {
|
||||
if (props.existingDoc === undefined || props.existingDoc === null) {
|
||||
return undefined;
|
||||
}
|
||||
if (props.existingDoc.status !== "ready") {
|
||||
if (props.existingDoc.status !== 'ready') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return props.existingDoc._links?.dav_link?.expiration;
|
||||
});
|
||||
|
||||
const dav_link_href = computed<string | undefined>(() => {
|
||||
const dav_link_href = computed<string|undefined>(() => {
|
||||
if (props.existingDoc === undefined || props.existingDoc === null) {
|
||||
return undefined;
|
||||
}
|
||||
if (props.existingDoc.status !== "ready") {
|
||||
if (props.existingDoc.status !== 'ready') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return props.existingDoc._links?.dav_link?.href;
|
||||
});
|
||||
})
|
||||
|
||||
const onAddDocument = (s: StoredObjectCreated): void => {
|
||||
emit("addDocument", s);
|
||||
};
|
||||
const onAddDocument = ({stored_object, stored_object_version}: {stored_object: StoredObject, stored_object_version: StoredObjectVersion}): void => {
|
||||
emit('addDocument', {stored_object, stored_object_version});
|
||||
}
|
||||
|
||||
const onRemoveDocument = (e: Event): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
emit("removeDocument", null);
|
||||
};
|
||||
emit('removeDocument');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<drop-file
|
||||
:existingDoc="props.existingDoc"
|
||||
@addDocument="onAddDocument"
|
||||
></drop-file>
|
||||
<drop-file :existingDoc="props.existingDoc" @addDocument="onAddDocument"></drop-file>
|
||||
|
||||
<ul class="record_actions">
|
||||
<li v-if="has_existing_doc">
|
||||
@@ -73,14 +72,12 @@ const onRemoveDocument = (e: Event): void => {
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-if="allowRemove"
|
||||
class="btn btn-delete"
|
||||
@click="onRemoveDocument($event)"
|
||||
></button>
|
||||
<button v-if="allowRemove" class="btn btn-delete" @click="onRemoveDocument($event)" ></button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
|
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
interface FileIconConfig {
|
||||
type: string;
|
||||
}
|
||||
|
||||
const props = defineProps<FileIconConfig>();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<i class="fa fa-file-pdf-o" v-if="props.type === 'application/pdf'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.type === 'application/vnd.oasis.opendocument.text'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.type === 'application/msword'"></i>
|
||||
<i class="fa fa-file-excel-o" v-else-if="props.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'"></i>
|
||||
<i class="fa fa-file-excel-o" v-else-if="props.type === 'application/vnd.ms-excel'"></i>
|
||||
<i class="fa fa-file-image-o" v-else-if="props.type === 'image/jpeg'"></i>
|
||||
<i class="fa fa-file-image-o" v-else-if="props.type === 'image/png'"></i>
|
||||
<i class="fa fa-file-archive-o" v-else-if="props.type === 'application/x-zip-compressed'"></i>
|
||||
<i class="fa fa-file-code-o" v-else ></i>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@@ -1,64 +1,60 @@
|
||||
<template>
|
||||
<a :class="props.classes" @click="download_and_open($event)" ref="btn">
|
||||
<i class="fa fa-file-pdf-o"></i>
|
||||
Télécharger en pdf
|
||||
</a>
|
||||
<a :class="props.classes" @click="download_and_open($event)" ref="btn">
|
||||
<i class="fa fa-file-pdf-o"></i>
|
||||
Télécharger en pdf
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
build_convert_link,
|
||||
download_and_decrypt_doc,
|
||||
download_doc,
|
||||
} from "./helpers";
|
||||
|
||||
import {build_convert_link, download_and_decrypt_doc, download_doc} from "./helpers";
|
||||
import mime from "mime";
|
||||
import { reactive, ref } from "vue";
|
||||
import { StoredObject, StoredObjectCreated } from "../../types";
|
||||
import {reactive, ref} from "vue";
|
||||
import {StoredObject} from "../../types";
|
||||
|
||||
interface ConvertButtonConfig {
|
||||
storedObject: StoredObject;
|
||||
classes: Record<string, boolean>;
|
||||
filename?: string;
|
||||
}
|
||||
storedObject: StoredObject,
|
||||
classes: { [key: string]: boolean},
|
||||
filename?: string,
|
||||
};
|
||||
|
||||
interface DownloadButtonState {
|
||||
content: null | string;
|
||||
content: null|string
|
||||
}
|
||||
|
||||
const props = defineProps<ConvertButtonConfig>();
|
||||
const state: DownloadButtonState = reactive({ content: null });
|
||||
const state: DownloadButtonState = reactive({content: null});
|
||||
const btn = ref<HTMLAnchorElement | null>(null);
|
||||
|
||||
async function download_and_open(event: Event): Promise<void> {
|
||||
const button = event.target as HTMLAnchorElement;
|
||||
const button = event.target as HTMLAnchorElement;
|
||||
|
||||
if (null === state.content) {
|
||||
event.preventDefault();
|
||||
if (null === state.content) {
|
||||
event.preventDefault();
|
||||
|
||||
const raw = await download_doc(
|
||||
build_convert_link(props.storedObject.uuid),
|
||||
);
|
||||
state.content = window.URL.createObjectURL(raw);
|
||||
const raw = await download_doc(build_convert_link(props.storedObject.uuid));
|
||||
state.content = window.URL.createObjectURL(raw);
|
||||
|
||||
button.href = window.URL.createObjectURL(raw);
|
||||
button.type = "application/pdf";
|
||||
button.href = window.URL.createObjectURL(raw);
|
||||
button.type = 'application/pdf';
|
||||
|
||||
button.download = props.filename + ".pdf" || "document.pdf";
|
||||
}
|
||||
button.download = (props.filename + '.pdf') || 'document.pdf';
|
||||
}
|
||||
|
||||
button.click();
|
||||
const reset_pending = setTimeout(reset_state, 45000);
|
||||
button.click();
|
||||
const reset_pending = setTimeout(reset_state, 45000);
|
||||
}
|
||||
|
||||
function reset_state(): void {
|
||||
state.content = null;
|
||||
btn.value?.removeAttribute("download");
|
||||
btn.value?.removeAttribute("href");
|
||||
btn.value?.removeAttribute("type");
|
||||
btn.value?.removeAttribute('download');
|
||||
btn.value?.removeAttribute('href');
|
||||
btn.value?.removeAttribute('type');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="sass">
|
||||
<style scoped lang="scss">
|
||||
i.fa::before {
|
||||
color: var(--bs-dropdown-link-hover-color);
|
||||
}
|
||||
|
@@ -90,4 +90,7 @@ const editionUntilFormatted = computed<string>(() => {
|
||||
.desktop-edit {
|
||||
text-align: center;
|
||||
}
|
||||
i.fa::before {
|
||||
color: var(--bs-dropdown-link-hover-color);
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,124 +1,117 @@
|
||||
<template>
|
||||
<a
|
||||
v-if="!state.is_ready"
|
||||
:class="props.classes"
|
||||
@click="download_and_open($event)"
|
||||
>
|
||||
<a v-if="!state.is_ready" :class="props.classes" @click="download_and_open()" title="Télécharger">
|
||||
<i class="fa fa-download"></i>
|
||||
Télécharger
|
||||
<template v-if="displayActionStringInButton">Télécharger</template>
|
||||
</a>
|
||||
<a
|
||||
v-else
|
||||
:class="props.classes"
|
||||
target="_blank"
|
||||
:type="props.storedObject.type"
|
||||
:download="buildDocumentName()"
|
||||
:href="state.href_url"
|
||||
ref="open_button"
|
||||
>
|
||||
<a v-else :class="props.classes" target="_blank" :type="props.atVersion.type" :download="buildDocumentName()" :href="state.href_url" ref="open_button" title="Ouvrir">
|
||||
<i class="fa fa-external-link"></i>
|
||||
Ouvrir
|
||||
<template v-if="displayActionStringInButton">Ouvrir</template>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, nextTick, onMounted } from "vue";
|
||||
import { build_download_info_link, download_and_decrypt_doc } from "./helpers";
|
||||
import {reactive, ref, nextTick, onMounted} from "vue";
|
||||
import {download_and_decrypt_doc} from "./helpers";
|
||||
import mime from "mime";
|
||||
import { StoredObject, StoredObjectCreated } from "../../types";
|
||||
import {StoredObject, StoredObjectVersion} from "../../types";
|
||||
|
||||
interface DownloadButtonConfig {
|
||||
storedObject: StoredObject | StoredObjectCreated;
|
||||
classes: Record<string, boolean>;
|
||||
filename?: string;
|
||||
storedObject: StoredObject,
|
||||
atVersion: StoredObjectVersion,
|
||||
classes: { [k: string]: boolean },
|
||||
filename?: string,
|
||||
/**
|
||||
* if true, display the action string into the button. If false, displays only
|
||||
* the icon
|
||||
*/
|
||||
displayActionStringInButton?: boolean,
|
||||
/**
|
||||
* if true, will download directly the file on load
|
||||
*/
|
||||
directDownload?: boolean,
|
||||
}
|
||||
|
||||
interface DownloadButtonState {
|
||||
is_ready: boolean;
|
||||
is_running: boolean;
|
||||
href_url: string;
|
||||
is_ready: boolean,
|
||||
is_running: boolean,
|
||||
href_url: string,
|
||||
}
|
||||
|
||||
const props = defineProps<DownloadButtonConfig>();
|
||||
const state: DownloadButtonState = reactive({
|
||||
is_ready: false,
|
||||
is_running: false,
|
||||
href_url: "#",
|
||||
});
|
||||
const props = withDefaults(defineProps<DownloadButtonConfig>(), {displayActionStringInButton: true, directDownload: false});
|
||||
const state: DownloadButtonState = reactive({is_ready: false, is_running: false, href_url: "#"});
|
||||
|
||||
const open_button = ref<HTMLAnchorElement | null>(null);
|
||||
|
||||
function buildDocumentName(): string {
|
||||
const document_name = props.filename || "document";
|
||||
const ext = mime.getExtension(props.storedObject.type);
|
||||
let document_name = props.filename ?? props.storedObject.title;
|
||||
|
||||
if ('' === document_name) {
|
||||
document_name = 'document';
|
||||
}
|
||||
|
||||
const ext = mime.getExtension(props.atVersion.type);
|
||||
|
||||
if (null !== ext) {
|
||||
return document_name + "." + ext;
|
||||
return document_name + '.' + ext;
|
||||
}
|
||||
|
||||
return document_name;
|
||||
}
|
||||
|
||||
async function download_and_open(event: Event): Promise<void> {
|
||||
const button = event.target as HTMLAnchorElement;
|
||||
|
||||
async function download_and_open(): Promise<void> {
|
||||
if (state.is_running) {
|
||||
console.log("state is running, aborting");
|
||||
console.log('state is running, aborting');
|
||||
return;
|
||||
}
|
||||
|
||||
state.is_running = true;
|
||||
|
||||
if (state.is_ready) {
|
||||
console.log("state is ready. This should not happens");
|
||||
console.log('state is ready. This should not happens');
|
||||
return;
|
||||
}
|
||||
|
||||
const urlInfo = build_download_info_link(props.storedObject.filename);
|
||||
let raw;
|
||||
|
||||
try {
|
||||
raw = await download_and_decrypt_doc(
|
||||
urlInfo,
|
||||
props.storedObject.keyInfos,
|
||||
new Uint8Array(props.storedObject.iv),
|
||||
);
|
||||
raw = await download_and_decrypt_doc(props.storedObject, props.atVersion);
|
||||
} catch (e) {
|
||||
console.error("error while downloading and decrypting document");
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
console.log("document downloading (and decrypting) successfully");
|
||||
|
||||
console.log("creating the url");
|
||||
state.href_url = window.URL.createObjectURL(raw);
|
||||
console.log("url created", state.href_url);
|
||||
state.is_running = false;
|
||||
state.is_ready = true;
|
||||
console.log("new button marked as ready");
|
||||
console.log("will click on button");
|
||||
|
||||
console.log("openbutton is now", open_button.value);
|
||||
if (!props.directDownload) {
|
||||
await nextTick();
|
||||
open_button.value?.click();
|
||||
|
||||
await nextTick();
|
||||
console.log("next tick actions");
|
||||
console.log("openbutton after next tick", open_button.value);
|
||||
open_button.value?.click();
|
||||
console.log("open button should have been clicked");
|
||||
|
||||
const timer = setTimeout(reset_state, 45000);
|
||||
console.log('open button should have been clicked');
|
||||
setTimeout(reset_state, 45000);
|
||||
}
|
||||
}
|
||||
|
||||
function reset_state(): void {
|
||||
state.href_url = "#";
|
||||
state.href_url = '#';
|
||||
state.is_ready = false;
|
||||
state.is_running = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.directDownload) {
|
||||
download_and_open();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="sass">
|
||||
<style scoped lang="scss">
|
||||
i.fa::before {
|
||||
color: var(--bs-dropdown-link-hover-color);
|
||||
}
|
||||
i.fa {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import HistoryButtonModal from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonModal.vue";
|
||||
import {StoredObject, StoredObjectVersionWithPointInTime} from "./../../types";
|
||||
import {computed, reactive, ref, useTemplateRef} from "vue";
|
||||
import {get_versions} from "./HistoryButton/api";
|
||||
|
||||
interface HistoryButtonConfig {
|
||||
storedObject: StoredObject;
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
interface HistoryButtonState {
|
||||
versions: StoredObjectVersionWithPointInTime[];
|
||||
loaded: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<HistoryButtonConfig>();
|
||||
const state = reactive<HistoryButtonState>({versions: [], loaded: false});
|
||||
const modal = useTemplateRef<typeof HistoryButtonModal>('modal');
|
||||
|
||||
const download_version_and_open_modal = async function (): Promise<void> {
|
||||
if (null !== modal.value) {
|
||||
modal.value.open();
|
||||
} else {
|
||||
console.log("modal is null");
|
||||
}
|
||||
|
||||
if (!state.loaded) {
|
||||
const versions = await get_versions(props.storedObject);
|
||||
|
||||
for (const version of versions) {
|
||||
state.versions.push(version);
|
||||
}
|
||||
state.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
const onRestoreVersion = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTime}) => {
|
||||
state.versions.unshift(newVersion);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a @click="download_version_and_open_modal" class="dropdown-item">
|
||||
<history-button-modal ref="modal" :versions="state.versions" :stored-object="storedObject" :can-edit="canEdit" @restore-version="onRestoreVersion"></history-button-modal>
|
||||
<i class="fa fa-history"></i>
|
||||
Historique
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
i.fa::before {
|
||||
color: var(--bs-dropdown-link-hover-color);
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import {StoredObject, StoredObjectVersionWithPointInTime} from "./../../../types";
|
||||
import HistoryButtonListItem from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonListItem.vue";
|
||||
import {computed, reactive} from "vue";
|
||||
|
||||
interface HistoryButtonListConfig {
|
||||
versions: StoredObjectVersionWithPointInTime[];
|
||||
storedObject: StoredObject;
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
|
||||
}>()
|
||||
|
||||
interface HistoryButtonListState {
|
||||
/**
|
||||
* Contains the number of the newly created version when a version is restored.
|
||||
*/
|
||||
restored: number;
|
||||
}
|
||||
|
||||
const props = defineProps<HistoryButtonListConfig>();
|
||||
|
||||
const state = reactive<HistoryButtonListState>({restored: -1})
|
||||
|
||||
const higher_version = computed<number>(() => props.versions.reduce(
|
||||
(accumulator: number, version: StoredObjectVersionWithPointInTime) => Math.max(accumulator, version.version),
|
||||
-1
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Executed when a version in child component is restored.
|
||||
*
|
||||
* internally, keep track of the newly restored version
|
||||
*/
|
||||
const onRestored = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTime}) => {
|
||||
state.restored = newVersion.version;
|
||||
emit('restoreVersion', {newVersion});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="props.versions.length > 0">
|
||||
<div class="container">
|
||||
<template v-for="v in props.versions">
|
||||
<history-button-list-item
|
||||
:version="v"
|
||||
:can-edit="canEdit"
|
||||
:is-current="higher_version === v.version"
|
||||
:stored-object="storedObject"
|
||||
@restore-version="onRestored"
|
||||
></history-button-list-item>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>Chargement des versions</p>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import {StoredObject, StoredObjectPointInTime, StoredObjectVersionWithPointInTime} from "./../../../types";
|
||||
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
||||
import {ISOToDatetime} from "./../../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
||||
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
|
||||
import RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue";
|
||||
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
|
||||
import {computed} from "vue";
|
||||
|
||||
interface HistoryButtonListItemConfig {
|
||||
version: StoredObjectVersionWithPointInTime;
|
||||
storedObject: StoredObject;
|
||||
canEdit: boolean;
|
||||
isCurrent: boolean;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
|
||||
}>()
|
||||
|
||||
const props = defineProps<HistoryButtonListItemConfig>();
|
||||
|
||||
const onRestore = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTime}) => {
|
||||
emit('restoreVersion', {newVersion});
|
||||
}
|
||||
|
||||
const isKeptBeforeConversion = computed<boolean>(() => props.version["point-in-times"].reduce(
|
||||
(accumulator: boolean, pit: StoredObjectPointInTime) => accumulator || "keep-before-conversion" === pit.reason,
|
||||
false
|
||||
),
|
||||
);
|
||||
|
||||
const isRestored = computed<boolean>(() => props.version.version > 0 && null !== props.version["from-restored"]);
|
||||
|
||||
const isDuplicated = computed<boolean>(() => props.version.version === 0 && null !== props.version["from-restored"]);
|
||||
|
||||
const classes = computed<{row: true, 'row-hover': true, 'blinking-1': boolean, 'blinking-2': boolean}>(() => ({row: true, 'row-hover': true, 'blinking-1': props.isRestored && 0 === props.version.version % 2, 'blinking-2': props.isRestored && 1 === props.version.version % 2}));
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<div class="col-12 tags" v-if="isCurrent || isKeptBeforeConversion || isRestored || isDuplicated">
|
||||
<span class="badge bg-success" v-if="isCurrent">Version actuelle</span>
|
||||
<span class="badge bg-info" v-if="isKeptBeforeConversion">Conservée avant conversion dans un autre format</span>
|
||||
<span class="badge bg-info" v-if="isRestored">Restaurée depuis la version {{ version["from-restored"]?.version + 1 }}</span>
|
||||
<span class="badge bg-info" v-if="isDuplicated">Dupliqué depuis un autre document</span>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<file-icon :type="version.type"></file-icon> <span><strong>#{{ version.version + 1 }}</strong></span> <template v-if="version.createdBy !== null && version.createdAt !== null"><strong v-if="version.version == 0">Créé par</strong><strong v-else>modifié par</strong> <span class="badge-user"><UserRenderBoxBadge :user="version.createdBy"></UserRenderBoxBadge></span> <strong>à</strong> {{ $d(ISOToDatetime(version.createdAt.datetime8601), 'long') }}</template><template v-if="version.createdBy === null && version.createdAt !== null"><strong v-if="version.version == 0">Créé le</strong><strong v-else>modifié le</strong> {{ $d(ISOToDatetime(version.createdAt.datetime8601), 'long') }}</template>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<ul class="record_actions small slim on-version-actions">
|
||||
<li v-if="canEdit && !isCurrent">
|
||||
<restore-version-button :stored-object-version="props.version" @restore-version="onRestore"></restore-version-button>
|
||||
</li>
|
||||
<li>
|
||||
<download-button :stored-object="storedObject" :at-version="version" :classes="{btn: true, 'btn-outline-primary': true, 'btn-sm': true}" :display-action-string-in-button="false"></download-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
div.tags {
|
||||
span.badge:not(:last-child) {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
// to make the animation restart, we have the same animation twice,
|
||||
// and alternate between both
|
||||
.blinking-1 {
|
||||
animation-name: backgroundColorPalette-1;
|
||||
animation-duration: 8s;
|
||||
animation-iteration-count: 1;
|
||||
animation-direction: normal;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
@keyframes backgroundColorPalette-1 {
|
||||
0% {
|
||||
background: var(--bs-chill-green-dark);
|
||||
}
|
||||
25% {
|
||||
background: var(--bs-chill-green);
|
||||
}
|
||||
65% {
|
||||
background: var(--bs-chill-beige);
|
||||
}
|
||||
100% {
|
||||
background: unset;
|
||||
}
|
||||
}
|
||||
.blinking-2 {
|
||||
animation-name: backgroundColorPalette-2;
|
||||
animation-duration: 8s;
|
||||
animation-iteration-count: 1;
|
||||
animation-direction: normal;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
@keyframes backgroundColorPalette-2 {
|
||||
0% {
|
||||
background: var(--bs-chill-green-dark);
|
||||
}
|
||||
25% {
|
||||
background: var(--bs-chill-green);
|
||||
}
|
||||
65% {
|
||||
background: var(--bs-chill-beige);
|
||||
}
|
||||
100% {
|
||||
background: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||
import {reactive} from "vue";
|
||||
import HistoryButtonList from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonList.vue";
|
||||
import {StoredObject, StoredObjectVersionWithPointInTime} from "./../../../types";
|
||||
|
||||
interface HistoryButtonListConfig {
|
||||
versions: StoredObjectVersionWithPointInTime[];
|
||||
storedObject: StoredObject;
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
|
||||
}>()
|
||||
|
||||
interface HistoryButtonModalState {
|
||||
opened: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<HistoryButtonListConfig>();
|
||||
const state = reactive<HistoryButtonModalState>({opened: false});
|
||||
|
||||
const open = () => {
|
||||
state.opened = true;
|
||||
}
|
||||
|
||||
defineExpose({open});
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<modal v-if="state.opened" @close="state.opened = false">
|
||||
<template v-slot:header>
|
||||
<h3>Historique des versions du document</h3>
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<p>Les versions sont conservées pendant 90 jours.</p>
|
||||
<history-button-list :versions="props.versions" :can-edit="canEdit" :stored-object="storedObject" @restore-version="(payload) => emit('restoreVersion', payload)"></history-button-list>
|
||||
</template>
|
||||
</modal>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import {StoredObjectVersionPersisted, StoredObjectVersionWithPointInTime} from "../../../types";
|
||||
import {useToast} from "vue-toast-notification";
|
||||
import {restore_version} from "./api";
|
||||
|
||||
interface RestoreVersionButtonProps {
|
||||
storedObjectVersion: StoredObjectVersionPersisted,
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
|
||||
}>()
|
||||
|
||||
const props = defineProps<RestoreVersionButtonProps>()
|
||||
|
||||
const $toast = useToast();
|
||||
|
||||
const restore_version_fn = async () => {
|
||||
const newVersion = await restore_version(props.storedObjectVersion);
|
||||
|
||||
$toast.success("Version restaurée");
|
||||
emit('restoreVersion', {newVersion});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="btn btn-outline-action" @click="restore_version_fn" title="Restaurer"><i class="fa fa-rotate-left"></i> Restaurer</button>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@@ -0,0 +1,12 @@
|
||||
import {StoredObject, StoredObjectVersionPersisted, StoredObjectVersionWithPointInTime} from "../../../types";
|
||||
import {fetchResults, makeFetch} from "../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||
|
||||
export const get_versions = async (storedObject: StoredObject): Promise<StoredObjectVersionWithPointInTime[]> => {
|
||||
const versions = await fetchResults<StoredObjectVersionWithPointInTime>(`/api/1.0/doc-store/stored-object/${storedObject.uuid}/versions`);
|
||||
|
||||
return versions.sort((a: StoredObjectVersionWithPointInTime, b: StoredObjectVersionWithPointInTime) => b.version - a.version);
|
||||
}
|
||||
|
||||
export const restore_version = async (version: StoredObjectVersionPersisted): Promise<StoredObjectVersionWithPointInTime> => {
|
||||
return await makeFetch<null, StoredObjectVersionWithPointInTime>("POST", `/api/1.0/doc-store/stored-object/restore-from-version/${version.id}`);
|
||||
}
|
@@ -1,30 +1,20 @@
|
||||
<template>
|
||||
<a
|
||||
:class="Object.assign(props.classes, { btn: true })"
|
||||
@click="beforeLeave($event)"
|
||||
:href="
|
||||
build_wopi_editor_link(props.storedObject.uuid, props.returnPath)
|
||||
"
|
||||
>
|
||||
<i class="fa fa-paragraph"></i>
|
||||
Editer en ligne
|
||||
</a>
|
||||
<a :class="Object.assign(props.classes, {'btn': true})" @click="beforeLeave($event)" :href="build_wopi_editor_link(props.storedObject.uuid, props.returnPath)">
|
||||
<i class="fa fa-paragraph"></i>
|
||||
Editer en ligne
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import WopiEditButton from "./WopiEditButton.vue";
|
||||
import { build_wopi_editor_link } from "./helpers";
|
||||
import {
|
||||
StoredObject,
|
||||
StoredObjectCreated,
|
||||
WopiEditButtonExecutableBeforeLeaveFunction,
|
||||
} from "../../types";
|
||||
import {build_wopi_editor_link} from "./helpers";
|
||||
import {StoredObject, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types";
|
||||
|
||||
interface WopiEditButtonConfig {
|
||||
storedObject: StoredObject;
|
||||
returnPath?: string;
|
||||
classes: Record<string, boolean>;
|
||||
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction;
|
||||
storedObject: StoredObject,
|
||||
returnPath?: string,
|
||||
classes: {[k: string] : boolean},
|
||||
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
|
||||
}
|
||||
|
||||
const props = defineProps<WopiEditButtonConfig>();
|
||||
@@ -32,25 +22,25 @@ const props = defineProps<WopiEditButtonConfig>();
|
||||
let executed = false;
|
||||
|
||||
async function beforeLeave(event: Event): Promise<true> {
|
||||
console.log(executed);
|
||||
if (props.executeBeforeLeave === undefined || executed === true) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
await props.executeBeforeLeave();
|
||||
executed = true;
|
||||
|
||||
const link = event.target as HTMLAnchorElement;
|
||||
link.click();
|
||||
|
||||
if (props.executeBeforeLeave === undefined || executed === true) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
await props.executeBeforeLeave();
|
||||
executed = true;
|
||||
|
||||
const link = event.target as HTMLAnchorElement;
|
||||
link.click();
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="sass">
|
||||
<style scoped lang="scss">
|
||||
i.fa::before {
|
||||
color: var(--bs-dropdown-link-hover-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@@ -1,228 +1,247 @@
|
||||
import {
|
||||
StoredObject,
|
||||
StoredObjectStatus,
|
||||
StoredObjectStatusChange,
|
||||
} from "../../types";
|
||||
import {StoredObject, StoredObjectStatus, StoredObjectStatusChange, StoredObjectVersion} from "../../types";
|
||||
import {makeFetch} from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||
|
||||
const MIMES_EDIT = new Set([
|
||||
"application/vnd.ms-powerpoint",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.oasis.opendocument.text",
|
||||
"application/vnd.oasis.opendocument.text-flat-xml",
|
||||
"application/vnd.oasis.opendocument.spreadsheet",
|
||||
"application/vnd.oasis.opendocument.spreadsheet-flat-xml",
|
||||
"application/vnd.oasis.opendocument.presentation",
|
||||
"application/vnd.oasis.opendocument.presentation-flat-xml",
|
||||
"application/vnd.oasis.opendocument.graphics",
|
||||
"application/vnd.oasis.opendocument.graphics-flat-xml",
|
||||
"application/vnd.oasis.opendocument.chart",
|
||||
"application/msword",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.ms-powerpoint",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.ms-word.document.macroEnabled.12",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.ms-excel.sheet.binary.macroEnabled.12",
|
||||
"application/vnd.ms-excel.sheet.macroEnabled.12",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"application/vnd.ms-powerpoint.presentation.macroEnabled.12",
|
||||
"application/x-dif-document",
|
||||
"text/spreadsheet",
|
||||
"text/csv",
|
||||
"application/x-dbase",
|
||||
"text/rtf",
|
||||
"text/plain",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.oasis.opendocument.text',
|
||||
'application/vnd.oasis.opendocument.text-flat-xml',
|
||||
'application/vnd.oasis.opendocument.spreadsheet',
|
||||
'application/vnd.oasis.opendocument.spreadsheet-flat-xml',
|
||||
'application/vnd.oasis.opendocument.presentation',
|
||||
'application/vnd.oasis.opendocument.presentation-flat-xml',
|
||||
'application/vnd.oasis.opendocument.graphics',
|
||||
'application/vnd.oasis.opendocument.graphics-flat-xml',
|
||||
'application/vnd.oasis.opendocument.chart',
|
||||
'application/msword',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-word.document.macroEnabled.12',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
|
||||
'application/vnd.ms-excel.sheet.macroEnabled.12',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
|
||||
'application/x-dif-document',
|
||||
'text/spreadsheet',
|
||||
'text/csv',
|
||||
'application/x-dbase',
|
||||
'text/rtf',
|
||||
'text/plain',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
|
||||
]);
|
||||
|
||||
|
||||
|
||||
const MIMES_VIEW = new Set([
|
||||
...MIMES_EDIT,
|
||||
[
|
||||
"image/svg+xml",
|
||||
"application/vnd.sun.xml.writer",
|
||||
"application/vnd.sun.xml.calc",
|
||||
"application/vnd.sun.xml.impress",
|
||||
"application/vnd.sun.xml.draw",
|
||||
"application/vnd.sun.xml.writer.global",
|
||||
"application/vnd.sun.xml.writer.template",
|
||||
"application/vnd.sun.xml.calc.template",
|
||||
"application/vnd.sun.xml.impress.template",
|
||||
"application/vnd.sun.xml.draw.template",
|
||||
"application/vnd.oasis.opendocument.text-master",
|
||||
"application/vnd.oasis.opendocument.text-template",
|
||||
"application/vnd.oasis.opendocument.text-master-template",
|
||||
"application/vnd.oasis.opendocument.spreadsheet-template",
|
||||
"application/vnd.oasis.opendocument.presentation-template",
|
||||
"application/vnd.oasis.opendocument.graphics-template",
|
||||
"application/vnd.ms-word.template.macroEnabled.12",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
||||
"application/vnd.ms-excel.template.macroEnabled.12",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.template",
|
||||
"application/vnd.ms-powerpoint.template.macroEnabled.12",
|
||||
"application/vnd.wordperfect",
|
||||
"application/x-aportisdoc",
|
||||
"application/x-hwp",
|
||||
"application/vnd.ms-works",
|
||||
"application/x-mswrite",
|
||||
"application/vnd.lotus-1-2-3",
|
||||
"image/cgm",
|
||||
"image/vnd.dxf",
|
||||
"image/x-emf",
|
||||
"image/x-wmf",
|
||||
"application/coreldraw",
|
||||
"application/vnd.visio2013",
|
||||
"application/vnd.visio",
|
||||
"application/vnd.ms-visio.drawing",
|
||||
"application/x-mspublisher",
|
||||
"application/x-sony-bbeb",
|
||||
"application/x-gnumeric",
|
||||
"application/macwriteii",
|
||||
"application/x-iwork-numbers-sffnumbers",
|
||||
"application/vnd.oasis.opendocument.text-web",
|
||||
"application/x-pagemaker",
|
||||
"application/x-fictionbook+xml",
|
||||
"application/clarisworks",
|
||||
"image/x-wpg",
|
||||
"application/x-iwork-pages-sffpages",
|
||||
"application/x-iwork-keynote-sffkey",
|
||||
"application/x-abiword",
|
||||
"image/x-freehand",
|
||||
"application/vnd.sun.xml.chart",
|
||||
"application/x-t602",
|
||||
"image/bmp",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/tiff",
|
||||
"image/jpg",
|
||||
"image/jpeg",
|
||||
"application/pdf",
|
||||
],
|
||||
]);
|
||||
...MIMES_EDIT,
|
||||
[
|
||||
'image/svg+xml',
|
||||
'application/vnd.sun.xml.writer',
|
||||
'application/vnd.sun.xml.calc',
|
||||
'application/vnd.sun.xml.impress',
|
||||
'application/vnd.sun.xml.draw',
|
||||
'application/vnd.sun.xml.writer.global',
|
||||
'application/vnd.sun.xml.writer.template',
|
||||
'application/vnd.sun.xml.calc.template',
|
||||
'application/vnd.sun.xml.impress.template',
|
||||
'application/vnd.sun.xml.draw.template',
|
||||
'application/vnd.oasis.opendocument.text-master',
|
||||
'application/vnd.oasis.opendocument.text-template',
|
||||
'application/vnd.oasis.opendocument.text-master-template',
|
||||
'application/vnd.oasis.opendocument.spreadsheet-template',
|
||||
'application/vnd.oasis.opendocument.presentation-template',
|
||||
'application/vnd.oasis.opendocument.graphics-template',
|
||||
'application/vnd.ms-word.template.macroEnabled.12',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
|
||||
'application/vnd.ms-excel.template.macroEnabled.12',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.template',
|
||||
'application/vnd.ms-powerpoint.template.macroEnabled.12',
|
||||
'application/vnd.wordperfect',
|
||||
'application/x-aportisdoc',
|
||||
'application/x-hwp',
|
||||
'application/vnd.ms-works',
|
||||
'application/x-mswrite',
|
||||
'application/vnd.lotus-1-2-3',
|
||||
'image/cgm',
|
||||
'image/vnd.dxf',
|
||||
'image/x-emf',
|
||||
'image/x-wmf',
|
||||
'application/coreldraw',
|
||||
'application/vnd.visio2013',
|
||||
'application/vnd.visio',
|
||||
'application/vnd.ms-visio.drawing',
|
||||
'application/x-mspublisher',
|
||||
'application/x-sony-bbeb',
|
||||
'application/x-gnumeric',
|
||||
'application/macwriteii',
|
||||
'application/x-iwork-numbers-sffnumbers',
|
||||
'application/vnd.oasis.opendocument.text-web',
|
||||
'application/x-pagemaker',
|
||||
'application/x-fictionbook+xml',
|
||||
'application/clarisworks',
|
||||
'image/x-wpg',
|
||||
'application/x-iwork-pages-sffpages',
|
||||
'application/x-iwork-keynote-sffkey',
|
||||
'application/x-abiword',
|
||||
'image/x-freehand',
|
||||
'application/vnd.sun.xml.chart',
|
||||
'application/x-t602',
|
||||
'image/bmp',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/tiff',
|
||||
'image/jpg',
|
||||
'image/jpeg',
|
||||
'application/pdf',
|
||||
]
|
||||
])
|
||||
|
||||
export interface SignedUrlGet {
|
||||
method: 'GET'|'HEAD',
|
||||
url: string,
|
||||
expires: number,
|
||||
object_name: string,
|
||||
}
|
||||
|
||||
function is_extension_editable(mimeType: string): boolean {
|
||||
return MIMES_EDIT.has(mimeType);
|
||||
return MIMES_EDIT.has(mimeType);
|
||||
}
|
||||
|
||||
function is_extension_viewable(mimeType: string): boolean {
|
||||
return MIMES_VIEW.has(mimeType);
|
||||
return MIMES_VIEW.has(mimeType);
|
||||
}
|
||||
|
||||
function build_convert_link(uuid: string) {
|
||||
return `/chill/wopi/convert/${uuid}`;
|
||||
return `/chill/wopi/convert/${uuid}`;
|
||||
}
|
||||
|
||||
function build_download_info_link(object_name: string) {
|
||||
return `/asyncupload/temp_url/generate/GET?object_name=${object_name}`;
|
||||
function build_download_info_link(storedObject: StoredObject, atVersion: null|StoredObjectVersion): string {
|
||||
const url = `/api/1.0/doc-store/async-upload/temp_url/${storedObject.uuid}/generate/get`;
|
||||
|
||||
if (null !== atVersion) {
|
||||
const params = new URLSearchParams({version: atVersion.filename});
|
||||
|
||||
return url + '?' + params.toString();
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
async function download_info_link(storedObject: StoredObject, atVersion: null|StoredObjectVersion): Promise<SignedUrlGet> {
|
||||
return makeFetch('GET', build_download_info_link(storedObject, atVersion));
|
||||
}
|
||||
|
||||
function build_wopi_editor_link(uuid: string, returnPath?: string) {
|
||||
if (returnPath === undefined) {
|
||||
returnPath =
|
||||
window.location.pathname +
|
||||
window.location.search +
|
||||
window.location.hash;
|
||||
}
|
||||
if (returnPath === undefined) {
|
||||
returnPath = window.location.pathname + window.location.search + window.location.hash;
|
||||
}
|
||||
|
||||
return (
|
||||
`/chill/wopi/edit/${uuid}?returnPath=` + encodeURIComponent(returnPath)
|
||||
);
|
||||
return `/chill/wopi/edit/${uuid}?returnPath=` + encodeURIComponent(returnPath);
|
||||
}
|
||||
|
||||
function download_doc(url: string): Promise<Blob> {
|
||||
return window.fetch(url).then((r) => {
|
||||
if (r.ok) {
|
||||
return r.blob();
|
||||
}
|
||||
return window.fetch(url).then(r => {
|
||||
if (r.ok) {
|
||||
return r.blob()
|
||||
}
|
||||
|
||||
throw new Error("Could not download document");
|
||||
});
|
||||
throw new Error('Could not download document');
|
||||
});
|
||||
}
|
||||
|
||||
async function download_and_decrypt_doc(
|
||||
urlGenerator: string,
|
||||
keyData: JsonWebKey,
|
||||
iv: Uint8Array,
|
||||
): Promise<Blob> {
|
||||
const algo = "AES-CBC";
|
||||
// get an url to download the object
|
||||
const downloadInfoResponse = await window.fetch(urlGenerator);
|
||||
async function download_and_decrypt_doc(storedObject: StoredObject, atVersion: null|StoredObjectVersion): Promise<Blob>
|
||||
{
|
||||
const algo = 'AES-CBC';
|
||||
|
||||
if (!downloadInfoResponse.ok) {
|
||||
throw new Error(
|
||||
"error while downloading url " +
|
||||
downloadInfoResponse.status +
|
||||
" " +
|
||||
downloadInfoResponse.statusText,
|
||||
);
|
||||
const atVersionToDownload = atVersion ?? storedObject.currentVersion;
|
||||
|
||||
if (null === atVersionToDownload) {
|
||||
throw new Error("no version associated to stored object");
|
||||
}
|
||||
|
||||
// sometimes, the downloadInfo may be embedded into the storedObject
|
||||
console.log('storedObject', storedObject);
|
||||
let downloadInfo;
|
||||
if (typeof storedObject._links !== 'undefined' && typeof storedObject._links.downloadLink !== 'undefined') {
|
||||
downloadInfo = storedObject._links.downloadLink;
|
||||
} else {
|
||||
downloadInfo = await download_info_link(storedObject, atVersionToDownload);
|
||||
}
|
||||
|
||||
const downloadInfo = (await downloadInfoResponse.json()) as { url: string };
|
||||
const rawResponse = await window.fetch(downloadInfo.url);
|
||||
const rawResponse = await window.fetch(downloadInfo.url);
|
||||
|
||||
if (!rawResponse.ok) {
|
||||
throw new Error(
|
||||
"error while downloading raw file " +
|
||||
rawResponse.status +
|
||||
" " +
|
||||
rawResponse.statusText,
|
||||
);
|
||||
}
|
||||
if (!rawResponse.ok) {
|
||||
throw new Error("error while downloading raw file " + rawResponse.status + " " + rawResponse.statusText);
|
||||
}
|
||||
|
||||
if (iv.length === 0) {
|
||||
console.log("returning document immediatly");
|
||||
return rawResponse.blob();
|
||||
}
|
||||
if (atVersionToDownload.iv.length === 0) {
|
||||
return rawResponse.blob();
|
||||
}
|
||||
|
||||
console.log("start decrypting doc");
|
||||
const rawBuffer = await rawResponse.arrayBuffer();
|
||||
try {
|
||||
const key = await window.crypto.subtle
|
||||
.importKey('jwk', atVersionToDownload.keyInfos, { name: algo }, false, ['decrypt']);
|
||||
const iv = Uint8Array.from(atVersionToDownload.iv);
|
||||
const decrypted = await window.crypto.subtle
|
||||
.decrypt({ name: algo, iv: iv }, key, rawBuffer);
|
||||
|
||||
const rawBuffer = await rawResponse.arrayBuffer();
|
||||
return Promise.resolve(new Blob([decrypted]));
|
||||
} catch (e) {
|
||||
console.error('encounter error while keys and decrypt operations');
|
||||
console.error(e);
|
||||
|
||||
try {
|
||||
const key = await window.crypto.subtle.importKey(
|
||||
"jwk",
|
||||
keyData,
|
||||
{ name: algo },
|
||||
false,
|
||||
["decrypt"],
|
||||
);
|
||||
console.log("key created");
|
||||
const decrypted = await window.crypto.subtle.decrypt(
|
||||
{ name: algo, iv: iv },
|
||||
key,
|
||||
rawBuffer,
|
||||
);
|
||||
console.log("doc decrypted");
|
||||
|
||||
return Promise.resolve(new Blob([decrypted]));
|
||||
} catch (e) {
|
||||
console.error("get error while keys and decrypt operations");
|
||||
console.error(e);
|
||||
|
||||
throw e;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function is_object_ready(
|
||||
storedObject: StoredObject,
|
||||
): Promise<StoredObjectStatusChange> {
|
||||
const new_status_response = await window.fetch(
|
||||
`/api/1.0/doc-store/stored-object/${storedObject.uuid}/is-ready`,
|
||||
);
|
||||
/**
|
||||
* Fetch the stored object as a pdf.
|
||||
*
|
||||
* If the document is already in a pdf on the server side, the document is retrieved "as is" from the usual
|
||||
* storage.
|
||||
*/
|
||||
async function download_doc_as_pdf(storedObject: StoredObject): Promise<Blob>
|
||||
{
|
||||
if (null === storedObject.currentVersion) {
|
||||
throw new Error("the stored object does not count any version");
|
||||
}
|
||||
|
||||
if (storedObject.currentVersion?.type === 'application/pdf') {
|
||||
return download_and_decrypt_doc(storedObject, storedObject.currentVersion);
|
||||
}
|
||||
|
||||
const convertLink = build_convert_link(storedObject.uuid);
|
||||
const response = await fetch(convertLink);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Could not convert the document: " + response.status);
|
||||
}
|
||||
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
async function is_object_ready(storedObject: StoredObject): Promise<StoredObjectStatusChange>
|
||||
{
|
||||
const new_status_response = await window
|
||||
.fetch( `/api/1.0/doc-store/stored-object/${storedObject.uuid}/is-ready`);
|
||||
|
||||
if (!new_status_response.ok) {
|
||||
throw new Error("could not fetch the new status");
|
||||
throw new Error("could not fetch the new status");
|
||||
}
|
||||
|
||||
return await new_status_response.json();
|
||||
}
|
||||
|
||||
export {
|
||||
build_convert_link,
|
||||
build_download_info_link,
|
||||
build_wopi_editor_link,
|
||||
download_and_decrypt_doc,
|
||||
download_doc,
|
||||
is_extension_editable,
|
||||
is_extension_viewable,
|
||||
is_object_ready,
|
||||
build_convert_link,
|
||||
build_wopi_editor_link,
|
||||
download_and_decrypt_doc,
|
||||
download_doc,
|
||||
download_doc_as_pdf,
|
||||
is_extension_editable,
|
||||
is_extension_viewable,
|
||||
is_object_ready,
|
||||
};
|
||||
|
@@ -1,195 +0,0 @@
|
||||
<template>
|
||||
<a :class="btnClasses" :title="$t(buttonTitle)" @click="openModal">
|
||||
<span>{{ $t(buttonTitle) }}</span>
|
||||
</a>
|
||||
<teleport to="body">
|
||||
<div>
|
||||
<modal
|
||||
v-if="modal.showModal"
|
||||
:modal-dialog-class="modal.modalDialogClass"
|
||||
@close="modal.showModal = false"
|
||||
>
|
||||
<template #header>
|
||||
{{ $t("upload_a_document") }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div id="dropZoneWrapper" ref="dropZoneWrapper">
|
||||
<div
|
||||
data-stored-object="data-stored-object"
|
||||
:data-label-preparing="$t('data_label_preparing')"
|
||||
:data-label-quiet-button="
|
||||
$t('data_label_quiet_button')
|
||||
"
|
||||
:data-label-ready="$t('data_label_ready')"
|
||||
:data-dict-file-too-big="
|
||||
$t('data_dict_file_too_big')
|
||||
"
|
||||
:data-dict-default-message="
|
||||
$t('data_dict_default_message')
|
||||
"
|
||||
:data-dict-remove-file="$t('data_dict_remove_file')"
|
||||
:data-dict-max-files-exceeded="
|
||||
$t('data_dict_max_files_exceeded')
|
||||
"
|
||||
:data-dict-cancel-upload="
|
||||
$t('data_dict_cancel_upload')
|
||||
"
|
||||
:data-dict-cancel-upload-confirm="
|
||||
$t('data_dict_cancel_upload_confirm')
|
||||
"
|
||||
:data-dict-upload-canceled="
|
||||
$t('data_dict_upload_canceled')
|
||||
"
|
||||
:data-dict-remove="$t('data_dict_remove')"
|
||||
:data-allow-remove="!options.required"
|
||||
data-temp-url-generator="/asyncupload/temp_url/generate/GET"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
data-async-file-upload="data-async-file-upload"
|
||||
data-generate-temp-url-post="/asyncupload/temp_url/generate/post?expires_delay=180&submit_delay=3600"
|
||||
data-temp-url-get="/asyncupload/temp_url/generate/GET"
|
||||
:data-max-files="options.maxFiles"
|
||||
:data-max-post-size="options.maxPostSize"
|
||||
:v-model="dataAsyncFileUpload"
|
||||
/>
|
||||
<input type="hidden" data-stored-object-key="1" />
|
||||
<input type="hidden" data-stored-object-iv="1" />
|
||||
<input type="hidden" data-async-file-type="1" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<button
|
||||
class="btn btn-create"
|
||||
@click.prevent="saveDocument"
|
||||
>
|
||||
{{ $t("action.add") }}
|
||||
</button>
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from "ChillMainAssets/vuejs/_components/Modal";
|
||||
import { searchForZones } from "../../module/async_upload/uploader";
|
||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
||||
|
||||
const i18n = {
|
||||
messages: {
|
||||
fr: {
|
||||
upload_a_document: "Téléversez un document",
|
||||
data_label_preparing: "Chargement...",
|
||||
data_label_quiet_button: "Téléchargez le fichier existant",
|
||||
data_label_ready: "Prêt à montrer",
|
||||
data_dict_file_too_big: "Fichier trop volumineux",
|
||||
data_dict_default_message: "Glissez votre fichier ou cliquez ici",
|
||||
data_dict_remove_file:
|
||||
"Enlevez votre fichier pour en téléversez un autre",
|
||||
data_dict_max_files_exceeded:
|
||||
"Nombre maximum de fichiers atteint. Enlevez les fichiers précédents",
|
||||
data_dict_cancel_upload: "Annulez le téléversement",
|
||||
data_dict_cancel_upload_confirm:
|
||||
"Êtes-vous sûr·e de vouloir annuler ce téléversement?",
|
||||
data_dict_upload_canceled: "Téléversement annulé",
|
||||
data_dict_remove: "Enlevez le fichier existant",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "AddAsyncUpload",
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
i18n,
|
||||
props: {
|
||||
buttonTitle: {
|
||||
type: String,
|
||||
default: "Ajouter un document",
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: {
|
||||
maxFiles: 1,
|
||||
maxPostSize: 262144000, // 250MB
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
btnClasses: {
|
||||
type: Object,
|
||||
default: {
|
||||
btn: true,
|
||||
"btn-create": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: ["addDocument"],
|
||||
data() {
|
||||
return {
|
||||
modal: {
|
||||
showModal: false,
|
||||
modalDialogClass: "modal-dialog-centered modal-md",
|
||||
},
|
||||
};
|
||||
},
|
||||
updated() {
|
||||
if (this.modal.showModal) {
|
||||
searchForZones(this.$refs.dropZoneWrapper);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
this.modal.showModal = true;
|
||||
},
|
||||
saveDocument() {
|
||||
const dropzone = this.$refs.dropZoneWrapper;
|
||||
if (dropzone) {
|
||||
const inputKey = dropzone.querySelector(
|
||||
"input[data-stored-object-key]",
|
||||
);
|
||||
const inputIv = dropzone.querySelector(
|
||||
"input[data-stored-object-iv]",
|
||||
);
|
||||
const inputObject = dropzone.querySelector(
|
||||
"input[data-async-file-upload]",
|
||||
);
|
||||
const inputType = dropzone.querySelector(
|
||||
"input[data-async-file-type]",
|
||||
);
|
||||
|
||||
const url = "/api/1.0/docstore/stored-object.json";
|
||||
const body = {
|
||||
filename: inputObject.value,
|
||||
keyInfos: JSON.parse(inputKey.value),
|
||||
iv: JSON.parse(inputIv.value),
|
||||
type: inputType.value,
|
||||
};
|
||||
makeFetch("POST", url, body)
|
||||
.then((r) => {
|
||||
this.$emit("addDocument", r);
|
||||
this.modal.showModal = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.name === "ValidationException") {
|
||||
for (let v of error.violations) {
|
||||
this.$toast.open({ message: v });
|
||||
}
|
||||
} else {
|
||||
console.error(error);
|
||||
this.$toast.open({ message: "An error occurred" });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.$toast.open({
|
||||
message: "An error occurred - drop zone not found",
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<a
|
||||
class="btn btn-download"
|
||||
:title="$t(buttonTitle)"
|
||||
:data-key="JSON.stringify(storedObject.keyInfos)"
|
||||
:data-iv="JSON.stringify(storedObject.iv)"
|
||||
:data-mime-type="storedObject.type"
|
||||
:data-label-preparing="$t('dataLabelPreparing')"
|
||||
:data-label-ready="$t('dataLabelReady')"
|
||||
:data-temp-url-get-generator="url"
|
||||
@click.once="downloadDocument"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { download } from "../../module/async_upload/downloader";
|
||||
|
||||
const i18n = {
|
||||
messages: {
|
||||
fr: {
|
||||
dataLabelPreparing: "Chargement...",
|
||||
dataLabelReady: "",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "AddAsyncUploadDownloader",
|
||||
i18n,
|
||||
props: ["buttonTitle", "storedObject"],
|
||||
computed: {
|
||||
url() {
|
||||
return `/asyncupload/temp_url/generate/GET?object_name=${this.storedObject.filename}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
downloadDocument(e) {
|
||||
download(e.target);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@@ -1,73 +0,0 @@
|
||||
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||
import { PostStoreObjectSignature } from "../../types";
|
||||
|
||||
const algo = "AES-CBC";
|
||||
|
||||
const URL_POST = "/asyncupload/temp_url/generate/post";
|
||||
|
||||
const keyDefinition = {
|
||||
name: algo,
|
||||
length: 256,
|
||||
};
|
||||
|
||||
const createFilename = (): string => {
|
||||
let text = "";
|
||||
const possible =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
export const uploadFile = async (uploadFile: ArrayBuffer): Promise<string> => {
|
||||
const params = new URLSearchParams();
|
||||
params.append("expires_delay", "180");
|
||||
params.append("submit_delay", "180");
|
||||
const asyncData: PostStoreObjectSignature = await makeFetch(
|
||||
"GET",
|
||||
URL_POST + "?" + params.toString(),
|
||||
);
|
||||
const suffix = createFilename();
|
||||
const filename = asyncData.prefix + suffix;
|
||||
const formData = new FormData();
|
||||
formData.append("redirect", asyncData.redirect);
|
||||
formData.append("max_file_size", asyncData.max_file_size.toString());
|
||||
formData.append("max_file_count", asyncData.max_file_count.toString());
|
||||
formData.append("expires", asyncData.expires.toString());
|
||||
formData.append("signature", asyncData.signature);
|
||||
formData.append(filename, new Blob([uploadFile]), suffix);
|
||||
|
||||
const response = await window.fetch(asyncData.url, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Error while sending file to store", response);
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
return Promise.resolve(filename);
|
||||
};
|
||||
|
||||
export const encryptFile = async (
|
||||
originalFile: ArrayBuffer,
|
||||
): Promise<[ArrayBuffer, Uint8Array, JsonWebKey]> => {
|
||||
console.log("encrypt", originalFile);
|
||||
const iv = crypto.getRandomValues(new Uint8Array(16));
|
||||
const key = await window.crypto.subtle.generateKey(keyDefinition, true, [
|
||||
"encrypt",
|
||||
"decrypt",
|
||||
]);
|
||||
const exportedKey = await window.crypto.subtle.exportKey("jwk", key);
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
{ name: algo, iv: iv },
|
||||
key,
|
||||
originalFile,
|
||||
);
|
||||
|
||||
return Promise.resolve([encrypted, iv, exportedKey]);
|
||||
};
|
@@ -38,6 +38,11 @@
|
||||
|
||||
{% if display_action is defined and display_action == true %}
|
||||
<ul class="record_actions">
|
||||
{% for dam in display_action_more|default([]) %}
|
||||
<li>
|
||||
{{ dam|raw }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if document.course != null and is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_SEE', document.course) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_course_index', {'accompanying_period_id': document.course.id}) }}" class="btn btn-show change-icon">
|
||||
|
@@ -0,0 +1 @@
|
||||
<div data-download-button-single="data-download-button-single" data-stored-object="{{ document_json|json_encode|escape('html_attr') }}" data-title="{{ title|escape('html_attr') }}"></div>
|
@@ -8,7 +8,7 @@
|
||||
<table class="table table-bordered border-dark align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'Creator bundle id' | trans }}</th>
|
||||
{# <th>{{ 'Creator bundle id' | trans }}</th>#}
|
||||
<th>{{ 'Internal id inside creator bundle' | trans }}</th>
|
||||
<th>{{ 'Document class' | trans }}</th>
|
||||
<th>{{ 'Name' | trans }}</th>
|
||||
@@ -18,7 +18,7 @@
|
||||
<tbody>
|
||||
{% for document_category in document_categories %}
|
||||
<tr>
|
||||
<td>{{ document_category.bundleId }}</td>
|
||||
{# <td>{{ document_category.bundleId }}</td>#}
|
||||
<td>{{ document_category.idInsideBundle }}</td>
|
||||
<td>{{ document_category.documentClass }}</td>
|
||||
<td>{{ document_category.name | localize_translatable_string}}</td>
|
||||
|
@@ -71,15 +71,7 @@
|
||||
</li>
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
|
||||
<li>
|
||||
{{ document.object|chill_document_button_group(document.title, is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document)) }}
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %}
|
||||
<li>
|
||||
<a href="{{ path('accompanying_course_document_edit', {'course': accompanyingCourse.id, 'id': document.id }) }}" class="btn btn-update"></a>
|
||||
{{ document.object|chill_document_button_group(document.title) }}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE', document) %}
|
||||
@@ -87,10 +79,25 @@
|
||||
<a href="{{ chill_return_path_or('chill_docstore_accompanying_course_document_delete', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-delete"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE', document.course) %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_doc_store_accompanying_course_document_duplicate', {'id': document.id}) }}" class="btn btn-duplicate" title="{{ 'Duplicate'|trans|e('html_attr') }}"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %}
|
||||
<li>
|
||||
<a href="{{ path('accompanying_course_document_edit', {'course': accompanyingCourse.id, 'id': document.id }) }}" class="btn btn-update"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
|
||||
<li>
|
||||
{{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }}
|
||||
{{ document.object|chill_document_button_group(document.title) }}
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ path('person_document_show', {'person': person.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
||||
|
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon">
|
||||
<title>Signature</title>
|
||||
|
||||
{{ encore_entry_link_tags('mod_bootstrap') }}
|
||||
{{ encore_entry_link_tags('mod_forkawesome') }}
|
||||
{{ encore_entry_link_tags('chill') }}
|
||||
{{ encore_entry_link_tags('vue_document_signature') }}
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
{% block js %}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
<script type="text/javascript">
|
||||
window.signature = {{ signature|json_encode|raw }};
|
||||
</script>
|
||||
{{ encore_entry_script_tags('vue_document_signature') }}
|
||||
{% endblock %}
|
||||
|
||||
<div class="content" id="content">
|
||||
<div class="container-xxl">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-12 col-lg-9 my-5 m-auto">
|
||||
<h4>{{ 'Document %title%' | trans({ '%title%': document.title }) }}</h4>
|
||||
<div class="row" id="document-signature"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@@ -0,0 +1,43 @@
|
||||
{% extends '@ChillMain/Workflow/workflow_view_send_public_layout.html.twig' %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_document_download_button') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_document_download_button') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{{ 'workflow.public_link.title'|trans }} - {{ title }}{% endblock %}
|
||||
|
||||
{% block public_content %}
|
||||
<h1>{{ 'workflow.public_link.shared_doc'|trans }}</h1>
|
||||
|
||||
{% set previous = send.entityWorkflowStepChained.previous %}
|
||||
{% if previous is not null %}
|
||||
{% if previous.transitionBy is not null %}
|
||||
<p>{{ 'workflow.public_link.doc_shared_by_at_explanation'|trans({'byUser': previous.transitionBy|chill_entity_render_string( { 'at_date': previous.transitionAt } ), 'at': previous.transitionAt }) }}</p>
|
||||
{% else %}
|
||||
<p>{{ 'workflow.public_link.doc_shared_automatically_at_explanation'|trans({'at': previous.transitionAt}) }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-6 col-md-4">
|
||||
<div class="card"">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{{ title }}</h2>
|
||||
<h3>{{ 'workflow.public_link.main_document'|trans }}</h3>
|
||||
|
||||
<ul class="record_actions slim small">
|
||||
<li>
|
||||
{{ storedObject|chill_document_download_only_button(storedObject.title(), false) }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@@ -70,12 +70,12 @@ class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements Prov
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function supports($attribute, $subject): bool
|
||||
public function supports($attribute, $subject): bool
|
||||
{
|
||||
return $this->voterHelper->supports($attribute, $subject);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||
public function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||
{
|
||||
if (!$token->getUser() instanceof User) {
|
||||
return false;
|
||||
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Security\Authorization;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
@@ -22,6 +23,7 @@ final class AsyncUploadVoter extends Voter
|
||||
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly StoredObjectRepository $storedObjectRepository,
|
||||
) {}
|
||||
|
||||
protected function supports($attribute, $subject): bool
|
||||
@@ -32,10 +34,16 @@ final class AsyncUploadVoter extends Voter
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||
{
|
||||
/** @var SignedUrl $subject */
|
||||
if (!in_array($subject->method, ['POST', 'GET', 'HEAD'], true)) {
|
||||
if (!in_array($subject->method, ['POST', 'GET', 'HEAD', 'PUT'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN');
|
||||
$storedObject = $this->storedObjectRepository->findOneBy(['filename' => $subject->object_name]);
|
||||
|
||||
return match ($subject->method) {
|
||||
'GET', 'HEAD' => $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject),
|
||||
'PUT' => $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject),
|
||||
'POST' => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -12,9 +12,10 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Security\Authorization;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
/**
|
||||
* Voter for the content of a stored object.
|
||||
@@ -23,6 +24,10 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
*/
|
||||
class StoredObjectVoter extends Voter
|
||||
{
|
||||
public const LOG_PREFIX = '[stored object voter] ';
|
||||
|
||||
public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters, private readonly LoggerInterface $logger) {}
|
||||
|
||||
protected function supports($attribute, $subject): bool
|
||||
{
|
||||
return StoredObjectRoleEnum::tryFrom($attribute) instanceof StoredObjectRoleEnum
|
||||
@@ -32,24 +37,28 @@ class StoredObjectVoter extends Voter
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||
{
|
||||
/** @var StoredObject $subject */
|
||||
if (
|
||||
!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
|
||||
|| $subject->getUuid()->toString() !== $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
|
||||
) {
|
||||
return false;
|
||||
$attributeAsEnum = StoredObjectRoleEnum::from($attribute);
|
||||
|
||||
// Loop through context-specific voters
|
||||
foreach ($this->storedObjectVoters as $storedObjectVoter) {
|
||||
if ($storedObjectVoter->supports($attributeAsEnum, $subject)) {
|
||||
$grant = $storedObjectVoter->voteOnAttribute($attributeAsEnum, $subject, $token);
|
||||
|
||||
if (false === $grant) {
|
||||
$this->logger->debug(self::LOG_PREFIX.'deny access by storedObjectVoter', ['stored_object_voter' => $storedObjectVoter::class]);
|
||||
}
|
||||
|
||||
return $grant;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)) {
|
||||
return false;
|
||||
// User role-based fallback
|
||||
if ($this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')) {
|
||||
// TODO: this maybe considered as a security issue, as all authenticated users can reach a stored object which
|
||||
// is potentially detached from an existing entity.
|
||||
return true;
|
||||
}
|
||||
|
||||
$askedRole = StoredObjectRoleEnum::from($attribute);
|
||||
$tokenRoleAuthorization =
|
||||
$token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS);
|
||||
|
||||
return match ($askedRole) {
|
||||
StoredObjectRoleEnum::SEE => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization || StoredObjectRoleEnum::SEE === $tokenRoleAuthorization,
|
||||
StoredObjectRoleEnum::EDIT => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization,
|
||||
};
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,72 @@
|
||||
<?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\Security\Authorization\StoredObjectVoter;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
||||
{
|
||||
abstract protected function getRepository(): AssociatedEntityToStoredObjectInterface;
|
||||
|
||||
/**
|
||||
* @return class-string
|
||||
*/
|
||||
abstract protected function getClass(): string;
|
||||
|
||||
abstract protected function attributeToRole(StoredObjectRoleEnum $attribute): string;
|
||||
|
||||
abstract protected function canBeAssociatedWithWorkflow(): bool;
|
||||
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
|
||||
) {}
|
||||
|
||||
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool
|
||||
{
|
||||
$class = $this->getClass();
|
||||
|
||||
return $this->getRepository()->findAssociatedEntityToStoredObject($subject) instanceof $class;
|
||||
}
|
||||
|
||||
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
|
||||
{
|
||||
// Retrieve the related entity
|
||||
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
|
||||
|
||||
// Determine the attribute to pass to the voter for argument
|
||||
$voterAttribute = $this->attributeToRole($attribute);
|
||||
|
||||
$regularPermission = $this->security->isGranted($voterAttribute, $entity);
|
||||
|
||||
if (!$this->canBeAssociatedWithWorkflow()) {
|
||||
return $regularPermission;
|
||||
}
|
||||
|
||||
$workflowPermission = match ($attribute) {
|
||||
StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($entity),
|
||||
StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($entity),
|
||||
};
|
||||
|
||||
return match ($workflowPermission) {
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN => $regularPermission,
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
<?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\Security\Authorization\StoredObjectVoter;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AccompanyingCourseDocumentRepository $repository,
|
||||
Security $security,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
) {
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
{
|
||||
return $this->repository;
|
||||
}
|
||||
|
||||
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
|
||||
{
|
||||
return match ($attribute) {
|
||||
StoredObjectRoleEnum::EDIT => AccompanyingCourseDocumentVoter::UPDATE,
|
||||
StoredObjectRoleEnum::SEE => AccompanyingCourseDocumentVoter::SEE_DETAILS,
|
||||
};
|
||||
}
|
||||
|
||||
protected function getClass(): string
|
||||
{
|
||||
return AccompanyingCourseDocument::class;
|
||||
}
|
||||
|
||||
protected function canBeAssociatedWithWorkflow(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
<?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\Security\Authorization\StoredObjectVoter;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\PersonDocument;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
|
||||
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PersonDocumentRepository $repository,
|
||||
Security $security,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
) {
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
{
|
||||
return $this->repository;
|
||||
}
|
||||
|
||||
protected function getClass(): string
|
||||
{
|
||||
return PersonDocument::class;
|
||||
}
|
||||
|
||||
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
|
||||
{
|
||||
return match ($attribute) {
|
||||
StoredObjectRoleEnum::EDIT => PersonDocumentVoter::UPDATE,
|
||||
StoredObjectRoleEnum::SEE => PersonDocumentVoter::SEE_DETAILS,
|
||||
};
|
||||
}
|
||||
|
||||
protected function canBeAssociatedWithWorkflow(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user