mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Update DropFile to handle object versioning
This commit is contained in:
parent
b6edbb3eed
commit
3d49c959e0
@ -122,7 +122,7 @@ unit_tests:
|
||||
- php tests/console chill:db:sync-views --env=test
|
||||
- php -d memory_limit=2G tests/console cache:clear --env=test
|
||||
- php -d memory_limit=3G tests/console doctrine:fixtures:load -n --env=test
|
||||
- php -d memory_limit=4G bin/phpunit --colors=never --exclude-group dbIntensive
|
||||
- php -d memory_limit=4G bin/phpunit --colors=never --exclude-group dbIntensive --exclude-group openstack-integration
|
||||
artifacts:
|
||||
expire_in: 1 day
|
||||
paths:
|
||||
|
@ -182,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',
|
||||
|
@ -15,6 +15,7 @@ use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
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;
|
||||
@ -99,8 +100,15 @@ final readonly class AsyncUploadController
|
||||
$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(),
|
||||
'doc_uuid' => $storedObject->getUuid()->toString(),
|
||||
'user_id' => $userId,
|
||||
]);
|
||||
|
||||
return new JsonResponse(
|
||||
|
@ -23,6 +23,7 @@ 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.
|
||||
@ -37,7 +38,6 @@ use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
*/
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table('stored_object', schema: 'chill_doc')]
|
||||
#[AsyncFileExists(message: 'The file is not stored properly')]
|
||||
class StoredObject implements Document, TrackCreationInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
@ -91,7 +91,7 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
/**
|
||||
* @var Collection<int, StoredObjectVersion>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: StoredObjectVersion::class, cascade: ['persist'], mappedBy: 'storedObject', orphanRemoval: true)]
|
||||
#[ORM\OneToMany(mappedBy: 'storedObject', targetEntity: StoredObjectVersion::class, cascade: ['persist'], orphanRemoval: true)]
|
||||
private Collection $versions;
|
||||
|
||||
/**
|
||||
@ -127,6 +127,8 @@ 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;
|
||||
@ -350,20 +352,7 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function saveHistory(): void
|
||||
{
|
||||
if ('' === $this->getFilename()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->datas['history'][] = [
|
||||
'filename' => $this->getFilename(),
|
||||
'iv' => $this->getIv(),
|
||||
'key_infos' => $this->getKeyInfos(),
|
||||
'type' => $this->getType(),
|
||||
'before' => (new \DateTimeImmutable('now'))->getTimestamp(),
|
||||
];
|
||||
}
|
||||
public function saveHistory(): void {}
|
||||
|
||||
public static function generatePrefix(): string
|
||||
{
|
||||
|
@ -86,7 +86,7 @@ class StoredObjectVersion implements TrackCreationInterface
|
||||
{
|
||||
try {
|
||||
$suffix = base_convert(bin2hex(random_bytes(8)), 16, 36);
|
||||
} catch (RandomException $e) {
|
||||
} catch (RandomException) {
|
||||
$suffix = uniqid(more_entropy: true);
|
||||
}
|
||||
|
||||
|
@ -55,15 +55,8 @@ class StoredObjectDataMapper implements DataMapperInterface
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var StoredObject $viewData */
|
||||
if ($viewData->getFilename() !== $forms['stored_object']->getData()['filename']) {
|
||||
$viewData->registerVersion(
|
||||
$forms['stored_object']->getData()['iv'],
|
||||
$forms['stored_object']->getData()['keyInfos'],
|
||||
$forms['stored_object']->getData()['type'],
|
||||
$forms['stored_object']->getData()['filename'],
|
||||
);
|
||||
}
|
||||
/* @var StoredObject $viewData */
|
||||
$viewData = $forms['stored_object']->getData();
|
||||
|
||||
if (array_key_exists('title', $forms)) {
|
||||
$viewData->setTitle($forms['title']->getData());
|
||||
|
@ -19,7 +19,7 @@ use Symfony\Component\Serializer\SerializerInterface;
|
||||
class StoredObjectDataTransformer implements DataTransformerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SerializerInterface $serializer
|
||||
private readonly SerializerInterface $serializer,
|
||||
) {}
|
||||
|
||||
public function transform(mixed $value): mixed
|
||||
@ -41,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');
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,11 @@ final readonly class StoredObjectRepository implements StoredObjectRepositoryInt
|
||||
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;
|
||||
|
@ -23,4 +23,6 @@ interface StoredObjectRepositoryInterface extends ObjectRepository
|
||||
* @return iterable<StoredObject>
|
||||
*/
|
||||
public function findByExpired(\DateTimeImmutable $expiredAtDate): iterable;
|
||||
|
||||
public function findOneByUUID(string $uuid): ?StoredObject;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {makeFetch} from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||
import {PostStoreObjectSignature} from "../../types";
|
||||
import {PostStoreObjectSignature, StoredObject} from "../../types";
|
||||
|
||||
const algo = 'AES-CBC';
|
||||
|
||||
@ -21,11 +21,22 @@ const createFilename = (): string => {
|
||||
return text;
|
||||
};
|
||||
|
||||
export const uploadFile = async (uploadFile: ArrayBuffer): Promise<string> => {
|
||||
/**
|
||||
* 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", URL_POST + "?" + params.toString());
|
||||
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();
|
||||
@ -50,7 +61,6 @@ export const uploadFile = async (uploadFile: ArrayBuffer): Promise<string> => {
|
||||
}
|
||||
|
||||
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);
|
@ -1,7 +1,7 @@
|
||||
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 {StoredObject, StoredObjectVersion} from "../../types";
|
||||
import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
|
||||
const i18n = _createI18n({});
|
||||
|
||||
@ -30,15 +30,17 @@ const startApp = (divElement: HTMLDivElement, collectionEntry: null|HTMLLIElemen
|
||||
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);
|
||||
input_stored_object.value = "";
|
||||
this.$data.existingDoc = null;
|
||||
this.$data.existingDoc = undefined;
|
||||
console.log('collectionEntry', collectionEntry);
|
||||
|
||||
if (null !== collectionEntry) {
|
||||
|
@ -4,11 +4,11 @@ export type StoredObjectStatus = "empty"|"ready"|"failure"|"pending";
|
||||
|
||||
export interface StoredObject {
|
||||
id: number,
|
||||
title: string,
|
||||
title: string|null,
|
||||
uuid: string,
|
||||
prefix: string,
|
||||
status: StoredObjectStatus,
|
||||
currentVersion: null|StoredObjectVersion,
|
||||
currentVersion: null|StoredObjectVersionCreated|StoredObjectVersionPersisted,
|
||||
totalVersions: number,
|
||||
datas: object,
|
||||
/** @deprecated */
|
||||
@ -32,21 +32,20 @@ export interface StoredObjectVersion {
|
||||
* filename of the object in the object storage
|
||||
*/
|
||||
filename: string,
|
||||
version: number,
|
||||
id: number,
|
||||
iv: number[],
|
||||
keyInfos: object,
|
||||
keyInfos: JsonWebKey,
|
||||
type: string,
|
||||
createdAt: DateTime|null,
|
||||
createdBy: User|null,
|
||||
}
|
||||
|
||||
export interface StoredObjectCreated {
|
||||
status: "stored_object_created",
|
||||
filename: string,
|
||||
iv: Uint8Array,
|
||||
keyInfos: object,
|
||||
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 {
|
||||
|
@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<div v-if="'ready' === props.storedObject.status || 'stored_object_created' === props.storedObject.status" class="btn-group">
|
||||
<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="props.canEdit && is_extension_editable(props.storedObject.type) && props.storedObject.status !== 'stored_object_created'">
|
||||
<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="props.canEdit && is_extension_editable(props.storedObject.type) && props.davLink !== undefined && props.davLinkExpiration !== undefined">
|
||||
<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="props.storedObject.type != 'application/pdf' && is_extension_viewable(props.storedObject.type) && props.canConvertPdf && props.storedObject.status !== 'stored_object_created'">
|
||||
<li v-if="isConvertibleToPdf">
|
||||
<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 v-if="isDownloadable">
|
||||
<download-button :stored-object="props.storedObject" :at-version="props.storedObject.currentVersion" :filename="filename" :classes="{'dropdown-item': true}"></download-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -29,20 +29,20 @@
|
||||
|
||||
<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 {
|
||||
StoredObject, StoredObjectCreated,
|
||||
StoredObjectStatusChange,
|
||||
StoredObject,
|
||||
StoredObjectStatusChange, StoredObjectVersion,
|
||||
WopiEditButtonExecutableBeforeLeaveFunction
|
||||
} from "../types";
|
||||
import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue";
|
||||
|
||||
interface DocumentActionButtonsGroupConfig {
|
||||
storedObject: StoredObject|StoredObjectCreated,
|
||||
storedObject: StoredObject,
|
||||
small?: boolean,
|
||||
canEdit?: boolean,
|
||||
canDownload?: boolean,
|
||||
@ -95,11 +95,44 @@ let tryiesForReady = 0;
|
||||
*/
|
||||
const maxTryiesForReady = 120;
|
||||
|
||||
const isButtonGroupDisplayable = computed<boolean>(() => {
|
||||
return isDownloadable.value || isEditableOnline.value || isEditableOnDesktop.value || isConvertibleToPdf.value;
|
||||
});
|
||||
|
||||
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')
|
||||
});
|
||||
|
||||
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 checkForReady = function(): void {
|
||||
if (
|
||||
'ready' === props.storedObject.status
|
||||
|| 'empty' === 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
|
||||
) {
|
||||
|
@ -132,7 +132,6 @@ console.log(PdfWorker); // incredible but this is needed
|
||||
|
||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||
import {
|
||||
build_download_info_link,
|
||||
download_and_decrypt_doc,
|
||||
} from "../StoredObjectButton/helpers";
|
||||
|
||||
@ -157,7 +156,6 @@ declare global {
|
||||
const $toast = useToast();
|
||||
|
||||
const signature = window.signature;
|
||||
const urlInfo = build_download_info_link(signature.storedObject.filename);
|
||||
|
||||
const mountPdf = async (url: string) => {
|
||||
const loadingTask = pdfjsLib.getDocument(url);
|
||||
@ -189,11 +187,7 @@ const setPage = async (page: number) => {
|
||||
async function downloadAndOpen(): Promise<Blob> {
|
||||
let raw;
|
||||
try {
|
||||
raw = await download_and_decrypt_doc(
|
||||
urlInfo,
|
||||
signature.storedObject.keyInfos,
|
||||
new Uint8Array(signature.storedObject.iv)
|
||||
);
|
||||
raw = await download_and_decrypt_doc(signature.storedObject, signature.storedObject.currentVersion);
|
||||
} catch (e) {
|
||||
console.error("error while downloading and decrypting document", e);
|
||||
throw e;
|
||||
|
@ -1,17 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {StoredObject, StoredObjectCreated} from "../../types";
|
||||
import {encryptFile, uploadFile} from "../_components/helper";
|
||||
import {StoredObject, StoredObjectVersionCreated} from "../../types";
|
||||
import {encryptFile, fetchNewStoredObject, uploadVersion} from "../../js/async-upload/uploader";
|
||||
import {computed, ref, Ref} from "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,
|
||||
(e: 'addDocument', {stored_object_version: StoredObjectVersionCreated, stored_object: StoredObject}): void,
|
||||
}>();
|
||||
|
||||
const is_dragging: Ref<boolean> = ref(false);
|
||||
@ -34,7 +34,6 @@ const onDragLeave = (e: Event) => {
|
||||
}
|
||||
|
||||
const onDrop = (e: DragEvent) => {
|
||||
console.log('on drop', e);
|
||||
e.preventDefault();
|
||||
|
||||
const files = e.dataTransfer?.files;
|
||||
@ -64,7 +63,6 @@ const onZoneClick = (e: Event) => {
|
||||
|
||||
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]);
|
||||
@ -80,21 +78,28 @@ const onFileChange = async (event: Event): Promise<void> => {
|
||||
const handleFile = async (file: File): Promise<void> => {
|
||||
uploading.value = true;
|
||||
const type = file.type;
|
||||
const buffer = await file.arrayBuffer();
|
||||
const [encrypted, iv, jsonWebKey] = await encryptFile(buffer);
|
||||
const filename = await uploadFile(encrypted);
|
||||
|
||||
console.log(iv, jsonWebKey);
|
||||
|
||||
const storedObject: StoredObjectCreated = {
|
||||
filename: filename,
|
||||
iv,
|
||||
keyInfos: jsonWebKey,
|
||||
type: type,
|
||||
status: "stored_object_created",
|
||||
// create a stored_object if not exists
|
||||
let stored_object;
|
||||
if (null === props.existingDoc) {
|
||||
stored_object = await fetchNewStoredObject();
|
||||
} else {
|
||||
stored_object = props.existingDoc;
|
||||
}
|
||||
|
||||
emit('addDocument', storedObject);
|
||||
const buffer = await file.arrayBuffer();
|
||||
const [encrypted, iv, jsonWebKey] = await encryptFile(buffer);
|
||||
const filename = await uploadVersion(encrypted, stored_object);
|
||||
|
||||
const stored_object_version: StoredObjectVersionCreated = {
|
||||
filename: filename,
|
||||
iv: Array.from(iv),
|
||||
keyInfos: jsonWebKey,
|
||||
type: type,
|
||||
persisted: false,
|
||||
}
|
||||
|
||||
emit('addDocument', {stored_object, stored_object_version});
|
||||
uploading.value = false;
|
||||
}
|
||||
|
||||
@ -138,6 +143,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 {
|
||||
@ -148,8 +158,4 @@ const handleFile = async (file: File): Promise<void> => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.chill-collection ul.list-entry li.entry:nth-child(2n) {
|
||||
|
||||
}
|
||||
</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,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {StoredObject, StoredObjectCreated} from "../../types";
|
||||
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,
|
||||
existingDoc?: StoredObject,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<DropFileConfig>(), {
|
||||
@ -15,8 +15,8 @@ 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>(() => {
|
||||
@ -45,14 +45,14 @@ const dav_link_href = computed<string|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>
|
||||
|
@ -10,7 +10,7 @@
|
||||
import {build_convert_link, download_and_decrypt_doc, download_doc} from "./helpers";
|
||||
import mime from "mime";
|
||||
import {reactive} from "vue";
|
||||
import {StoredObject, StoredObjectCreated} from "../../types";
|
||||
import {StoredObject} from "../../types";
|
||||
|
||||
interface ConvertButtonConfig {
|
||||
storedObject: StoredObject,
|
||||
@ -45,7 +45,7 @@ async function download_and_open(event: Event): Promise<void> {
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="sass">
|
||||
<style scoped lang="scss">
|
||||
i.fa::before {
|
||||
color: var(--bs-dropdown-link-hover-color);
|
||||
}
|
||||
|
@ -63,4 +63,7 @@ const editionUntilFormatted = computed<string>(() => {
|
||||
.desktop-edit {
|
||||
text-align: center;
|
||||
}
|
||||
i.fa::before {
|
||||
color: var(--bs-dropdown-link-hover-color);
|
||||
}
|
||||
</style>
|
||||
|
@ -11,12 +11,13 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {reactive, ref, nextTick, onMounted} from "vue";
|
||||
import {build_download_info_link, download_and_decrypt_doc} from "./helpers";
|
||||
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,
|
||||
storedObject: StoredObject,
|
||||
atVersion: StoredObjectVersion,
|
||||
classes: { [k: string]: boolean },
|
||||
filename?: string,
|
||||
}
|
||||
@ -33,8 +34,9 @@ const state: DownloadButtonState = reactive({is_ready: false, is_running: false,
|
||||
const open_button = ref<HTMLAnchorElement | null>(null);
|
||||
|
||||
function buildDocumentName(): string {
|
||||
const document_name = props.filename || 'document';
|
||||
const ext = mime.getExtension(props.storedObject.type);
|
||||
const document_name = props.filename ?? props.storedObject.title ?? 'document';
|
||||
|
||||
const ext = mime.getExtension(props.atVersion.type);
|
||||
|
||||
if (null !== ext) {
|
||||
return document_name + '.' + ext;
|
||||
@ -58,38 +60,26 @@ async function download_and_open(event: Event): Promise<void> {
|
||||
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);
|
||||
|
||||
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');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="sass">
|
||||
<style scoped lang="scss">
|
||||
i.fa::before {
|
||||
color: var(--bs-dropdown-link-hover-color);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
<script lang="ts" setup>
|
||||
import WopiEditButton from "./WopiEditButton.vue";
|
||||
import {build_wopi_editor_link} from "./helpers";
|
||||
import {StoredObject, StoredObjectCreated, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types";
|
||||
import {StoredObject, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types";
|
||||
|
||||
interface WopiEditButtonConfig {
|
||||
storedObject: StoredObject,
|
||||
@ -22,7 +22,6 @@ 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);
|
||||
}
|
||||
@ -39,7 +38,7 @@ async function beforeLeave(event: Event): Promise<true> {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="sass">
|
||||
<style scoped lang="scss">
|
||||
i.fa::before {
|
||||
color: var(--bs-dropdown-link-hover-color);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
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',
|
||||
@ -97,6 +98,13 @@ const MIMES_VIEW = new Set([
|
||||
]
|
||||
])
|
||||
|
||||
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);
|
||||
}
|
||||
@ -109,8 +117,20 @@ function build_convert_link(uuid: string) {
|
||||
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) {
|
||||
@ -131,43 +151,39 @@ function download_doc(url: string): Promise<Blob> {
|
||||
});
|
||||
}
|
||||
|
||||
async function download_and_decrypt_doc(urlGenerator: string, keyData: JsonWebKey, iv: Uint8Array): Promise<Blob>
|
||||
async function download_and_decrypt_doc(storedObject: StoredObject, atVersion: null|StoredObjectVersion): Promise<Blob>
|
||||
{
|
||||
const algo = 'AES-CBC';
|
||||
// get an url to download the object
|
||||
const downloadInfoResponse = await window.fetch(urlGenerator);
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
const downloadInfo = await downloadInfoResponse.json() as {url: string};
|
||||
const downloadInfo= await download_info_link(storedObject, atVersionToDownload);
|
||||
|
||||
const rawResponse = await window.fetch(downloadInfo.url);
|
||||
|
||||
if (!rawResponse.ok) {
|
||||
throw new Error("error while downloading raw file " + rawResponse.status + " " + rawResponse.statusText);
|
||||
}
|
||||
|
||||
if (iv.length === 0) {
|
||||
console.log('returning document immediatly');
|
||||
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', keyData, { name: algo }, false, ['decrypt']);
|
||||
console.log('key created');
|
||||
.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);
|
||||
console.log('doc decrypted');
|
||||
|
||||
return Promise.resolve(new Blob([decrypted]));
|
||||
} catch (e) {
|
||||
console.error('get error while keys and decrypt operations');
|
||||
console.error('encounter error while keys and decrypt operations');
|
||||
console.error(e);
|
||||
|
||||
throw e;
|
||||
@ -188,7 +204,6 @@ async function is_object_ready(storedObject: StoredObject): Promise<StoredObject
|
||||
|
||||
export {
|
||||
build_convert_link,
|
||||
build_download_info_link,
|
||||
build_wopi_editor_link,
|
||||
download_and_decrypt_doc,
|
||||
download_doc,
|
||||
|
@ -1,174 +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"
|
||||
:modalDialogClass="modal.modalDialogClass"
|
||||
@close="modal.showModal = false">
|
||||
|
||||
<template v-slot:header>
|
||||
{{ $t('upload_a_document') }}
|
||||
</template>
|
||||
|
||||
<template v-slot: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 v-slot: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,45 +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">
|
||||
</a>
|
||||
</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>
|
@ -12,37 +12,75 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
||||
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
|
||||
use Symfony\Component\Form\Exception\TransformationFailedException;
|
||||
use Symfony\Component\Serializer\Exception\LogicException;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;
|
||||
|
||||
/**
|
||||
* Implements the DenormalizerInterface and is responsible for denormalizing data into StoredObject objects.
|
||||
*
|
||||
* If a new StoredObjectVersion has been added to the StoredObject, the version is created here and registered
|
||||
* to the StoredObject.
|
||||
*/
|
||||
class StoredObjectDenormalizer implements DenormalizerInterface
|
||||
{
|
||||
use ObjectToPopulateTrait;
|
||||
|
||||
public function __construct(private readonly StoredObjectRepository $storedObjectRepository) {}
|
||||
public function __construct(private readonly StoredObjectRepositoryInterface $storedObjectRepository) {}
|
||||
|
||||
public function denormalize($data, $type, $format = null, array $context = [])
|
||||
public function denormalize($data, $type, $format = null, array $context = []): ?StoredObject
|
||||
{
|
||||
$object = $this->extractObjectToPopulate(StoredObject::class, $context);
|
||||
$storedObject = $this->extractObjectToPopulate(StoredObject::class, $context);
|
||||
|
||||
if (null !== $object) {
|
||||
return $object;
|
||||
if (null === $storedObject) {
|
||||
if (array_key_exists('uuid', $data)) {
|
||||
$storedObject = $this->storedObjectRepository->findOneByUUID($data['uuid']);
|
||||
} else {
|
||||
$storedObject = $this->storedObjectRepository->find($data['id']);
|
||||
}
|
||||
|
||||
if (null === $storedObject) {
|
||||
throw new LogicException('Object not found');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->storedObjectRepository->find($data['id']);
|
||||
$storedObject->setTitle($data['title'] ?? $storedObject->getTitle());
|
||||
|
||||
if (true === ($data['currentVersion']['persisted'] ?? true)) {
|
||||
// nothing has change, stop here
|
||||
return $storedObject;
|
||||
}
|
||||
|
||||
if ([] !== $diff = array_diff(['filename', 'iv', 'keyInfos', 'type'], array_keys($data['currentVersion']))) {
|
||||
throw new TransformationFailedException(sprintf('missing some keys in currentVersion: %s', implode(', ', $diff)));
|
||||
}
|
||||
|
||||
$storedObject->registerVersion(
|
||||
$data['currentVersion']['iv'],
|
||||
$data['currentVersion']['keyInfos'],
|
||||
$data['currentVersion']['type'],
|
||||
$data['currentVersion']['filename']
|
||||
);
|
||||
|
||||
return $storedObject;
|
||||
}
|
||||
|
||||
public function supportsDenormalization($data, $type, $format = null)
|
||||
public function supportsDenormalization($data, $type, $format = null): bool
|
||||
{
|
||||
if (false === \is_array($data)) {
|
||||
if (StoredObject::class !== $type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (false === \array_key_exists('id', $data)) {
|
||||
if (false === is_array($data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return StoredObject::class === $type;
|
||||
if (array_key_exists('id', $data) || array_key_exists('uuid', $data)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
|
||||
],
|
||||
UrlGeneratorInterface::ABSOLUTE_URL,
|
||||
),
|
||||
'expiration' => $this->JWTDavTokenProvider->getTokenExpiration($accessToken)->format('U'),
|
||||
'expiration' => $this->JWTDavTokenProvider->getTokenExpiration($accessToken)->getTimestamp(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
@ -0,0 +1,45 @@
|
||||
<?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\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
class StoredObjectVersionNormalizer implements NormalizerInterface, NormalizerAwareInterface
|
||||
{
|
||||
use NormalizerAwareTrait;
|
||||
|
||||
public function normalize($object, ?string $format = null, array $context = [])
|
||||
{
|
||||
if (!$object instanceof StoredObjectVersion) {
|
||||
throw new \InvalidArgumentException('The object must be an instance of '.StoredObjectVersion::class);
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $object->getId(),
|
||||
'filename' => $object->getFilename(),
|
||||
'version' => $object->getVersion(),
|
||||
'iv' => array_values($object->getIv()),
|
||||
'keyInfos' => $object->getKeyInfos(),
|
||||
'type' => $object->getType(),
|
||||
'createdAt' => $this->normalizer->normalize($object->getCreatedAt(), $format, $context),
|
||||
'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, $context),
|
||||
];
|
||||
}
|
||||
|
||||
public function supportsNormalization($data, ?string $format = null, array $context = [])
|
||||
{
|
||||
return $data instanceof StoredObjectVersion;
|
||||
}
|
||||
}
|
@ -101,6 +101,37 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
||||
return strlen($this->read($document));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws StoredObjectManagerException
|
||||
*/
|
||||
public function exists(StoredObject|StoredObjectVersion $document): bool
|
||||
{
|
||||
$version = $document instanceof StoredObject ? $document->getCurrentVersion() : $document;
|
||||
|
||||
if ($this->hasCache($version)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
$response = $this
|
||||
->client
|
||||
->request(
|
||||
Request::METHOD_HEAD,
|
||||
$this
|
||||
->tempUrlGenerator
|
||||
->generate(
|
||||
Request::METHOD_HEAD,
|
||||
$version->getFilename()
|
||||
)
|
||||
->url
|
||||
);
|
||||
|
||||
return 200 === $response->getStatusCode();
|
||||
} catch (TransportExceptionInterface $exception) {
|
||||
throw StoredObjectManagerException::errorDuringHttpRequest($exception);
|
||||
}
|
||||
}
|
||||
|
||||
public function etag(StoredObject|StoredObjectVersion $document): string
|
||||
{
|
||||
$version = $document instanceof StoredObject ? $document->getCurrentVersion() : $document;
|
||||
@ -117,7 +148,7 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
||||
->tempUrlGenerator
|
||||
->generate(
|
||||
Request::METHOD_HEAD,
|
||||
$document->getFilename()
|
||||
$version->getFilename()
|
||||
)
|
||||
->url
|
||||
);
|
||||
@ -214,6 +245,8 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
||||
throw StoredObjectManagerException::invalidStatusCode($response->getStatusCode());
|
||||
}
|
||||
|
||||
$this->clearCache();
|
||||
|
||||
return $version;
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ namespace Chill\DocStoreBundle\Service;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
|
||||
interface StoredObjectManagerInterface
|
||||
{
|
||||
@ -27,6 +28,11 @@ interface StoredObjectManagerInterface
|
||||
*/
|
||||
public function getContentLength(StoredObject|StoredObjectVersion $document): int;
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
public function exists(StoredObject|StoredObjectVersion $document): bool;
|
||||
|
||||
/**
|
||||
* Get the content of a StoredObject.
|
||||
*
|
||||
|
@ -14,19 +14,37 @@ namespace AsyncUpload\Driver\OpenstackObjectStore;
|
||||
use Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore\TempUrlOpenstackGenerator;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\Mime\Part\DataPart;
|
||||
use Symfony\Component\Mime\Part\Multipart\FormDataPart;
|
||||
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class TempUrlOpenstackGeneratorTest extends TestCase
|
||||
class TempUrlOpenstackGeneratorTest extends KernelTestCase
|
||||
{
|
||||
private ParameterBagInterface $parameterBag;
|
||||
private HttpClientInterface $client;
|
||||
|
||||
private const TESTING_OBJECT_NAME_PREFIX = 'test-prefix-o0o008wk404gcos40k8s4s4c44cgwwos4k4o8k/';
|
||||
private const TESTING_OBJECT_NAME = 'object-name-4fI0iAtq';
|
||||
|
||||
private function setUpIntegration(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->parameterBag = self::getContainer()->get(ParameterBagInterface::class);
|
||||
$this->client = self::getContainer()->get(HttpClientInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderGenerate
|
||||
*/
|
||||
@ -175,4 +193,62 @@ class TempUrlOpenstackGeneratorTest extends TestCase
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @group openstack-integration
|
||||
*/
|
||||
public function testGeneratePostIntegration(): void
|
||||
{
|
||||
$this->setUpIntegration();
|
||||
$generator = new TempUrlOpenstackGenerator(new NullLogger(), new EventDispatcher(), new MockClock(), $this->parameterBag);
|
||||
|
||||
$signedUrl = $generator->generatePost(object_name: self::TESTING_OBJECT_NAME_PREFIX);
|
||||
$formData = new FormDataPart([
|
||||
'redirect', $signedUrl->redirect,
|
||||
'max_file_size' => (string) $signedUrl->max_file_size,
|
||||
'max_file_count' => (string) $signedUrl->max_file_count,
|
||||
'expires' => (string) $signedUrl->expires->getTimestamp(),
|
||||
'signature' => $signedUrl->signature,
|
||||
self::TESTING_OBJECT_NAME => DataPart::fromPath(
|
||||
__DIR__.'/file-to-upload.txt',
|
||||
self::TESTING_OBJECT_NAME
|
||||
),
|
||||
]);
|
||||
|
||||
$response = $this->client
|
||||
->request(
|
||||
'POST',
|
||||
$signedUrl->url,
|
||||
[
|
||||
'body' => $formData->bodyToString(),
|
||||
'headers' => $formData->getPreparedHeaders()->toArray(),
|
||||
]
|
||||
);
|
||||
|
||||
self::assertEquals(201, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group openstack-integration
|
||||
*
|
||||
* @depends testGeneratePostIntegration
|
||||
*/
|
||||
public function testGenerateGetIntegration(): void
|
||||
{
|
||||
$this->setUpIntegration();
|
||||
$generator = new TempUrlOpenstackGenerator(new NullLogger(), new EventDispatcher(), new MockClock(), $this->parameterBag);
|
||||
|
||||
$signedUrl = $generator->generate('GET', self::TESTING_OBJECT_NAME_PREFIX.self::TESTING_OBJECT_NAME);
|
||||
|
||||
$response = $this->client->request('GET', $signedUrl->url);
|
||||
|
||||
self::assertEquals(200, $response->getStatusCode());
|
||||
|
||||
try {
|
||||
$content = $response->getContent();
|
||||
self::assertEquals(file_get_contents(__DIR__.'/file-to-upload.txt'), $content);
|
||||
} catch (HttpExceptionInterface $exception) {
|
||||
$this->fail('could not retrieve file content: '.$exception->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1 @@
|
||||
test file
|
@ -13,6 +13,7 @@ namespace Chill\DocStoreBundle\Tests\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Controller\WebdavController;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Prophecy\Argument;
|
||||
@ -417,30 +418,32 @@ class WebdavControllerTest extends KernelTestCase
|
||||
|
||||
class MockedStoredObjectManager implements StoredObjectManagerInterface
|
||||
{
|
||||
public function getLastModified(StoredObject|\Chill\DocStoreBundle\Entity\StoredObjectVersion $document): \DateTimeInterface
|
||||
public function getLastModified(StoredObject|StoredObjectVersion $document): \DateTimeInterface
|
||||
{
|
||||
return new \DateTimeImmutable('2023-09-13T14:15', new \DateTimeZone('+02:00'));
|
||||
}
|
||||
|
||||
public function getContentLength(StoredObject|\Chill\DocStoreBundle\Entity\StoredObjectVersion $document): int
|
||||
public function getContentLength(StoredObject|StoredObjectVersion $document): int
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
|
||||
public function read(StoredObject|\Chill\DocStoreBundle\Entity\StoredObjectVersion $document): string
|
||||
public function read(StoredObject|StoredObjectVersion $document): string
|
||||
{
|
||||
return 'abcde';
|
||||
}
|
||||
|
||||
public function write(StoredObject $document, string $clearContent, ?string $contentType = null): \Chill\DocStoreBundle\Entity\StoredObjectVersion
|
||||
public function write(StoredObject $document, string $clearContent, ?string $contentType = null): StoredObjectVersion
|
||||
{
|
||||
return $document->registerVersion();
|
||||
}
|
||||
|
||||
public function etag(StoredObject|\Chill\DocStoreBundle\Entity\StoredObjectVersion $document): string
|
||||
public function etag(StoredObject|StoredObjectVersion $document): string
|
||||
{
|
||||
return 'ab56b4d92b40713acc5af89985d4b786';
|
||||
}
|
||||
|
||||
public function clearCache(): void {}
|
||||
|
||||
public function delete(StoredObjectVersion $storedObjectVersion): void {}
|
||||
}
|
||||
|
@ -21,40 +21,6 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
*/
|
||||
class StoredObjectTest extends KernelTestCase
|
||||
{
|
||||
public function testSaveHistory(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
$storedObject
|
||||
->registerVersion(
|
||||
[2, 4, 6, 8],
|
||||
['key' => ['data0' => 'data0']],
|
||||
'text/html',
|
||||
'test_0',
|
||||
);
|
||||
|
||||
$storedObject->saveHistory();
|
||||
|
||||
$storedObject
|
||||
->registerVersion(
|
||||
[8, 10, 12],
|
||||
['key' => ['data1' => 'data1']],
|
||||
'text/text',
|
||||
'test_1',
|
||||
);
|
||||
|
||||
$storedObject->saveHistory();
|
||||
|
||||
self::assertEquals('test_0', $storedObject->getDatas()['history'][0]['filename']);
|
||||
self::assertEquals([2, 4, 6, 8], $storedObject->getDatas()['history'][0]['iv']);
|
||||
self::assertEquals(['key' => ['data0' => 'data0']], $storedObject->getDatas()['history'][0]['key_infos']);
|
||||
self::assertEquals('text/html', $storedObject->getDatas()['history'][0]['type']);
|
||||
|
||||
self::assertEquals('test_1', $storedObject->getDatas()['history'][1]['filename']);
|
||||
self::assertEquals([8, 10, 12], $storedObject->getDatas()['history'][1]['iv']);
|
||||
self::assertEquals(['key' => ['data1' => 'data1']], $storedObject->getDatas()['history'][1]['key_infos']);
|
||||
self::assertEquals('text/text', $storedObject->getDatas()['history'][1]['type']);
|
||||
}
|
||||
|
||||
public function testRegisterVersion(): void
|
||||
{
|
||||
$object = new StoredObject();
|
||||
@ -63,6 +29,9 @@ class StoredObjectTest extends KernelTestCase
|
||||
['key' => ['some key']],
|
||||
'text/html',
|
||||
);
|
||||
|
||||
self::assertSame($firstVersion, $object->getCurrentVersion());
|
||||
|
||||
$version = $object->registerVersion(
|
||||
[1, 2, 3, 4],
|
||||
$k = ['key' => ['data0' => 'data0']],
|
||||
@ -70,6 +39,8 @@ class StoredObjectTest extends KernelTestCase
|
||||
'abcde',
|
||||
);
|
||||
|
||||
self::assertSame($version, $object->getCurrentVersion());
|
||||
|
||||
self::assertCount(2, $object->getVersions());
|
||||
self::assertEquals('abcde', $object->getFilename());
|
||||
self::assertEquals([1, 2, 3, 4], $object->getIv());
|
||||
|
@ -15,11 +15,17 @@ use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Form\DataMapper\StoredObjectDataMapper;
|
||||
use Chill\DocStoreBundle\Form\DataTransformer\StoredObjectDataTransformer;
|
||||
use Chill\DocStoreBundle\Form\StoredObjectType;
|
||||
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectDenormalizer;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectNormalizer;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectVersionNormalizer;
|
||||
use Chill\MainBundle\Serializer\Normalizer\UserNormalizer;
|
||||
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Component\Form\PreloadedExtension;
|
||||
use Symfony\Component\Form\Test\TypeTestCase;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
@ -36,33 +42,41 @@ class StoredObjectTypeTest extends TypeTestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private StoredObject $model;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->model = new StoredObject();
|
||||
$this->model->registerVersion();
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testChangeTitleValue(): void
|
||||
{
|
||||
$formData = ['title' => $newTitle = 'new title', 'stored_object' => <<<'JSON'
|
||||
{"datas":[],"filename":"","id":null,"iv":[],"keyInfos":[],"title":"","type":"","uuid":"3c6a28fe-f913-40b9-a201-5eccc4f2d312","status":"ready","createdAt":null,"createdBy":null,"creationDate":null,"_links":{"dav_link":{"href":"http:\/\/url\/fake","expiration":"1716889578"}}}
|
||||
{"uuid":"9855d676-690b-11ef-88d3-9f5a4129a7b7"}
|
||||
JSON];
|
||||
$model = new StoredObject();
|
||||
$form = $this->factory->create(StoredObjectType::class, $model, ['has_title' => true]);
|
||||
$form = $this->factory->create(StoredObjectType::class, $this->model, ['has_title' => true]);
|
||||
|
||||
$form->submit($formData);
|
||||
|
||||
$this->assertTrue($form->isSynchronized());
|
||||
|
||||
$this->assertEquals($newTitle, $model->getTitle());
|
||||
$this->assertEquals($newTitle, $this->model->getTitle());
|
||||
}
|
||||
|
||||
public function testReplaceByAnotherObject(): void
|
||||
{
|
||||
$formData = ['title' => $newTitle = 'new title', 'stored_object' => <<<'JSON'
|
||||
{"filename":"abcdef","iv":[10, 15, 20, 30],"keyInfos":[],"type":"text/html","status":"object_store_created"}
|
||||
{"uuid":"9855d676-690b-11ef-88d3-9f5a4129a7b7","currentVersion":{"filename":"abcdef","iv":[10, 15, 20, 30],"keyInfos":[],"type":"text/html","persisted": false}}
|
||||
JSON];
|
||||
$model = new StoredObject();
|
||||
$originalObjectId = spl_object_hash($model);
|
||||
$form = $this->factory->create(StoredObjectType::class, $model, ['has_title' => true]);
|
||||
$originalObjectId = spl_object_hash($this->model);
|
||||
$form = $this->factory->create(StoredObjectType::class, $this->model, ['has_title' => true]);
|
||||
|
||||
$form->submit($formData);
|
||||
|
||||
$this->assertTrue($form->isSynchronized());
|
||||
|
||||
$model = $form->getData();
|
||||
$this->assertEquals($originalObjectId, spl_object_hash($model));
|
||||
$this->assertEquals('abcdef', $model->getFilename());
|
||||
@ -71,6 +85,29 @@ class StoredObjectTypeTest extends TypeTestCase
|
||||
$this->assertEquals($newTitle, $model->getTitle());
|
||||
}
|
||||
|
||||
public function testNothingIsChanged(): void
|
||||
{
|
||||
$formData = ['title' => $newTitle = 'new title', 'stored_object' => <<<'JSON'
|
||||
{"uuid":"9855d676-690b-11ef-88d3-9f5a4129a7b7","currentVersion":{"filename":"abcdef","iv":[10, 15, 20, 30],"keyInfos":[],"type":"text/html"}}
|
||||
JSON];
|
||||
$originalObjectId = spl_object_hash($this->model);
|
||||
$originalVersion = $this->model->getCurrentVersion();
|
||||
$originalFilename = $originalVersion->getFilename();
|
||||
$originalKeyInfos = $originalVersion->getKeyInfos();
|
||||
|
||||
$form = $this->factory->create(StoredObjectType::class, $this->model, ['has_title' => true]);
|
||||
|
||||
$form->submit($formData);
|
||||
|
||||
$this->assertTrue($form->isSynchronized());
|
||||
|
||||
$model = $form->getData();
|
||||
$this->assertEquals($originalObjectId, spl_object_hash($model));
|
||||
$this->assertSame($originalVersion, $model->getCurrentVersion());
|
||||
$this->assertEquals($originalFilename, $model->getCurrentVersion()->getFilename());
|
||||
$this->assertEquals($originalKeyInfos, $model->getCurrentVersion()->getKeyInfos());
|
||||
}
|
||||
|
||||
protected function getExtensions()
|
||||
{
|
||||
$jwtTokenProvider = $this->prophesize(JWTDavTokenProviderInterface::class);
|
||||
@ -84,6 +121,12 @@ class StoredObjectTypeTest extends TypeTestCase
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->isGranted(Argument::cetera())->willReturn(true);
|
||||
|
||||
$storedObjectRepository = $this->prophesize(StoredObjectRepositoryInterface::class);
|
||||
$storedObjectRepository->findOneByUUID(Argument::type('string'))
|
||||
->willReturn($this->model);
|
||||
|
||||
$userRender = $this->prophesize(UserRender::class);
|
||||
|
||||
$serializer = new Serializer(
|
||||
[
|
||||
new StoredObjectNormalizer(
|
||||
@ -91,6 +134,9 @@ class StoredObjectTypeTest extends TypeTestCase
|
||||
$urlGenerator->reveal(),
|
||||
$security->reveal()
|
||||
),
|
||||
new StoredObjectDenormalizer($storedObjectRepository->reveal()),
|
||||
new StoredObjectVersionNormalizer(),
|
||||
new UserNormalizer($userRender->reveal(), new MockClock()),
|
||||
],
|
||||
[
|
||||
new JsonEncoder(),
|
||||
|
@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectDenormalizer;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class StoredObjectDenormalizerTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testDenormalizeWithoutObjectToPopulateWithUUID(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
|
||||
$storedObjectRepository = $this->prophesize(StoredObjectRepositoryInterface::class);
|
||||
$storedObjectRepository->findOneByUUID($uuid = $storedObject->getUUID()->toString())
|
||||
->shouldBeCalledOnce()
|
||||
->willReturn($storedObject);
|
||||
|
||||
$denormalizer = new StoredObjectDenormalizer($storedObjectRepository->reveal());
|
||||
|
||||
$actual = $denormalizer->denormalize(['uuid' => $uuid], 'json');
|
||||
|
||||
self::assertSame($storedObject, $actual);
|
||||
}
|
||||
|
||||
public function testDenormalizeWithoutObjectToPopulateWithId(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
|
||||
$storedObjectRepository = $this->prophesize(StoredObjectRepositoryInterface::class);
|
||||
$storedObjectRepository->find($id = 1)
|
||||
->shouldBeCalledOnce()
|
||||
->willReturn($storedObject);
|
||||
|
||||
$denormalizer = new StoredObjectDenormalizer($storedObjectRepository->reveal());
|
||||
|
||||
$actual = $denormalizer->denormalize(['id' => $id], 'json');
|
||||
|
||||
self::assertSame($storedObject, $actual);
|
||||
}
|
||||
|
||||
public function testDenormalizeTitle(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
$storedObject->setTitle('foo');
|
||||
|
||||
$storedObjectRepository = $this->prophesize(StoredObjectRepositoryInterface::class);
|
||||
|
||||
$denormalizer = new StoredObjectDenormalizer($storedObjectRepository->reveal());
|
||||
|
||||
$actual = $denormalizer->denormalize([], StoredObject::class, 'json', [AbstractNormalizer::OBJECT_TO_POPULATE => $storedObject]);
|
||||
|
||||
self::assertEquals('foo', $actual->getTitle(), 'the title should remains the same');
|
||||
|
||||
$actual = $denormalizer->denormalize(['title' => 'bar'], StoredObject::class, 'json', [AbstractNormalizer::OBJECT_TO_POPULATE => $storedObject]);
|
||||
|
||||
self::assertEquals('bar', $actual->getTitle(), 'the title should have been updated');
|
||||
}
|
||||
|
||||
public function testDenormalizeNoNewVersion(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
$version = $storedObject->registerVersion();
|
||||
$iv = $version->getIv();
|
||||
$keyInfos = $version->getKeyInfos();
|
||||
$type = $version->getType();
|
||||
$filename = $version->getFilename();
|
||||
|
||||
$storedObjectRepository = $this->prophesize(StoredObjectRepositoryInterface::class);
|
||||
|
||||
$denormalizer = new StoredObjectDenormalizer($storedObjectRepository->reveal());
|
||||
|
||||
$actual = $denormalizer->denormalize([
|
||||
'currentVersion' => [
|
||||
'iv' => $iv,
|
||||
'keyInfos' => $keyInfos,
|
||||
'type' => $type,
|
||||
'filename' => $filename,
|
||||
],
|
||||
], StoredObject::class, 'json', [AbstractNormalizer::OBJECT_TO_POPULATE => $storedObject]);
|
||||
|
||||
self::assertSame($storedObject, $actual);
|
||||
self::assertSame($version, $storedObject->getCurrentVersion());
|
||||
self::assertEquals($iv, $version->getIv());
|
||||
self::assertEquals($keyInfos, $version->getKeyInfos());
|
||||
self::assertEquals($type, $version->getType());
|
||||
self::assertEquals($filename, $version->getFilename());
|
||||
}
|
||||
|
||||
public function testDenormalizeNewVersion(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
$version = $storedObject->registerVersion();
|
||||
$iv = ['1, 2, 3'];
|
||||
$keyInfos = ['some-key' => 'some'];
|
||||
$type = 'text/html';
|
||||
$filename = 'Foo-Bar';
|
||||
|
||||
$storedObjectRepository = $this->prophesize(StoredObjectRepositoryInterface::class);
|
||||
|
||||
$denormalizer = new StoredObjectDenormalizer($storedObjectRepository->reveal());
|
||||
|
||||
$actual = $denormalizer->denormalize([
|
||||
'currentVersion' => [
|
||||
'iv' => $iv,
|
||||
'keyInfos' => $keyInfos,
|
||||
'type' => $type,
|
||||
'filename' => $filename,
|
||||
// this is the required key for new versions
|
||||
'persisted' => false,
|
||||
],
|
||||
], StoredObject::class, 'json', [AbstractNormalizer::OBJECT_TO_POPULATE => $storedObject]);
|
||||
|
||||
self::assertSame($storedObject, $actual);
|
||||
self::assertNotSame($version, $storedObject->getCurrentVersion());
|
||||
|
||||
$version = $storedObject->getCurrentVersion();
|
||||
|
||||
self::assertEquals($iv, $version->getIv());
|
||||
self::assertEquals($keyInfos, $version->getKeyInfos());
|
||||
self::assertEquals($type, $version->getType());
|
||||
self::assertEquals($filename, $version->getFilename());
|
||||
}
|
||||
}
|
@ -9,25 +9,32 @@ declare(strict_types=1);
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Serializer\Normalizer;
|
||||
namespace ChillDocStoreBundle\Tests\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectVersionNormalizer;
|
||||
use Chill\MainBundle\Serializer\Normalizer\UserNormalizer;
|
||||
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class StoredObjectVersionNormalizerTest extends KernelTestCase
|
||||
class StoredObjectVersionNormalizerTest extends TestCase
|
||||
{
|
||||
private NormalizerInterface $normalizer;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->normalizer = self::getContainer()->get(NormalizerInterface::class);
|
||||
$userRender = $this->createMock(UserRender::class);
|
||||
$userRender->method('renderString')->willReturn('user');
|
||||
$this->normalizer = new StoredObjectVersionNormalizer();
|
||||
$this->normalizer->setNormalizer(new Serializer([new UserNormalizer($userRender, new MockClock())]));
|
||||
}
|
||||
|
||||
public function testNormalize(): void
|
||||
@ -58,4 +65,14 @@ class StoredObjectVersionNormalizerTest extends KernelTestCase
|
||||
$actual
|
||||
);
|
||||
}
|
||||
|
||||
public function testNormalizeUnsupportedObject(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('The object must be an instance of Chill\DocStoreBundle\Entity\StoredObjectVersion');
|
||||
|
||||
$unsupportedObject = new \stdClass();
|
||||
|
||||
$this->normalizer->normalize($unsupportedObject, 'json', ['groups' => ['read']]);
|
||||
}
|
||||
}
|
||||
|
@ -11,66 +11,43 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Validator\Constraints;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Exception\BadCallToRemoteServer;
|
||||
use Chill\DocStoreBundle\AsyncUpload\Exception\TempUrlRemoteServerException;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||||
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
final class AsyncFileExistsValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
|
||||
private readonly HttpClientInterface $client
|
||||
private readonly StoredObjectManagerInterface $storedObjectManager,
|
||||
) {}
|
||||
|
||||
public function validate($value, Constraint $constraint): void
|
||||
{
|
||||
if ($value instanceof StoredObject) {
|
||||
$this->validateObject($value->getFilename(), $constraint);
|
||||
} elseif (is_string($value)) {
|
||||
$this->validateObject($value, $constraint);
|
||||
} else {
|
||||
throw new UnexpectedValueException($value, StoredObject::class.' or string');
|
||||
}
|
||||
}
|
||||
|
||||
protected function validateObject(string $file, Constraint $constraint): void
|
||||
{
|
||||
if (!$constraint instanceof AsyncFileExists) {
|
||||
throw new UnexpectedTypeException($constraint, AsyncFileExists::class);
|
||||
}
|
||||
|
||||
$urlHead = $this->tempUrlGenerator->generate(
|
||||
'HEAD',
|
||||
$file,
|
||||
30
|
||||
);
|
||||
if (null === $value) {
|
||||
return;
|
||||
}
|
||||
if ($value instanceof StoredObjectVersion) {
|
||||
$this->validateObject($value, $constraint);
|
||||
} elseif ($value instanceof StoredObject) {
|
||||
$this->validateObject($value->getCurrentVersion(), $constraint);
|
||||
} else {
|
||||
throw new \Symfony\Component\Form\Exception\UnexpectedTypeException($value, StoredObjectVersion::class);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->client->request('HEAD', $urlHead->url);
|
||||
|
||||
if (404 === $status = $response->getStatusCode()) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->setParameter('{{ filename }}', $file)
|
||||
->addViolation();
|
||||
} elseif (500 <= $status) {
|
||||
throw new TempUrlRemoteServerException($response->getStatusCode());
|
||||
} elseif (400 <= $status) {
|
||||
throw new BadCallToRemoteServer($response->getContent(false), $response->getStatusCode());
|
||||
}
|
||||
} catch (HttpExceptionInterface $exception) {
|
||||
if (404 !== $exception->getResponse()->getStatusCode()) {
|
||||
throw $exception;
|
||||
}
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
throw new TempUrlRemoteServerException(0, previous: $e);
|
||||
protected function validateObject(StoredObjectVersion $file, AsyncFileExists $constraint): void
|
||||
{
|
||||
if (!$this->storedObjectManager->exists($file)) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->setParameter('{{ filename }}', $file->getFilename())
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,64 @@
|
||||
<?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\MainBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
final class WorkflowAddSignatureController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
private readonly StoredObjectManagerInterface $storedObjectManager,
|
||||
private readonly PDFSignatureZoneParser $PDFSignatureZoneParser,
|
||||
private readonly NormalizerInterface $normalizer,
|
||||
private readonly Environment $twig
|
||||
) {}
|
||||
|
||||
#[Route(path: '/{_locale}/main/workflow/signature/{signature_id}/sign', name: 'chill_main_workflow_signature', methods: 'GET')]
|
||||
public function __invoke(int $signature_id, Request $request, WorkflowController $workflowController): Response
|
||||
{
|
||||
$signature = $this->entityManager->getRepository(EntityWorkflowStepSignature::class)->find($signature_id);
|
||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
||||
|
||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
||||
if (null === $storedObject) {
|
||||
throw new NotFoundHttpException('No stored object found');
|
||||
}
|
||||
|
||||
$zones = [];
|
||||
$content = $this->storedObjectManager->read($storedObject);
|
||||
if (null != $content) {
|
||||
$zones = $this->PDFSignatureZoneParser->findSignatureZones($content);
|
||||
}
|
||||
|
||||
$signatureClient = [];
|
||||
$signatureClient['id'] = $signature->getId();
|
||||
$signatureClient['storedObject'] = $this->normalizer->normalize($storedObject, 'json');
|
||||
$signatureClient['zones'] = $zones;
|
||||
|
||||
return new Response($this->twig->render(
|
||||
'@ChillMain/Workflow/_signature_sign.html.twig',
|
||||
['signature' => $signatureClient]
|
||||
));
|
||||
}
|
||||
}
|
@ -11,8 +11,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
@ -34,7 +32,6 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
@ -47,7 +44,6 @@ class WorkflowController extends AbstractController
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
private readonly EntityWorkflowRepository $entityWorkflowRepository,
|
||||
private readonly ValidatorInterface $validator,
|
||||
private readonly StoredObjectManagerInterface $storedObjectManagerInterface,
|
||||
private readonly PaginatorFactory $paginatorFactory,
|
||||
private readonly Registry $registry,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
@ -55,7 +51,6 @@ class WorkflowController extends AbstractController
|
||||
private readonly ChillSecurity $security,
|
||||
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
|
||||
private readonly ClockInterface $clock,
|
||||
private readonly PDFSignatureZoneParser $PDFSignatureZoneParser,
|
||||
) {}
|
||||
|
||||
#[Route(path: '/{_locale}/main/workflow/create', name: 'chill_main_workflow_create')]
|
||||
@ -417,36 +412,4 @@ class WorkflowController extends AbstractController
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[Route(path: '/{_locale}/main/workflow/signature/{signature_id}/sign', name: 'chill_main_workflow_signature', methods: 'GET')]
|
||||
public function addSignature(int $signature_id, Request $request): Response
|
||||
{
|
||||
$signature = $this->entityManager->getRepository(EntityWorkflowStepSignature::class)->find($signature_id);
|
||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
||||
|
||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
||||
if (null === $storedObject) {
|
||||
throw new NotFoundHttpException('No stored object found');
|
||||
}
|
||||
|
||||
$zones = [];
|
||||
$content = $this->storedObjectManagerInterface->read($storedObject);
|
||||
if (null != $content) {
|
||||
$zones = $this->PDFSignatureZoneParser->findSignatureZones($content);
|
||||
}
|
||||
|
||||
$signatureClient = [];
|
||||
$signatureClient['id'] = $signature->getId();
|
||||
$signatureClient['storedObject'] = [
|
||||
'filename' => $storedObject->getFilename(),
|
||||
'iv' => $storedObject->getIv(),
|
||||
'keyInfos' => $storedObject->getKeyInfos(),
|
||||
];
|
||||
$signatureClient['zones'] = $zones;
|
||||
|
||||
return $this->render(
|
||||
'@ChillMain/Workflow/_signature_sign.html.twig',
|
||||
['signature' => $signatureClient]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -140,14 +140,8 @@
|
||||
@on-stored-object-status-change="onStatusDocumentChanged"
|
||||
></document-action-buttons-group>
|
||||
</li>
|
||||
<li>
|
||||
<add-async-upload
|
||||
:buttonTitle="$t('replace')"
|
||||
:options="asyncUploadOptions"
|
||||
:btnClasses="{'btn': true, 'btn-edit': true}"
|
||||
@addDocument="(arg) => replaceDocument(d, arg)"
|
||||
>
|
||||
</add-async-upload>
|
||||
<li v-if="d.storedObject._permissions.canEdit">
|
||||
<drop-file-modal :existing-doc="d.storedObject" :allow-remove="false" @add-document="(arg) => replaceDocument(d, arg.stored_object, arg.stored_object_version)"></drop-file-modal>
|
||||
</li>
|
||||
<li v-if="d.workflows.length === 0">
|
||||
<a class="btn btn-delete" @click="removeDocument(d)">
|
||||
@ -177,12 +171,7 @@
|
||||
<label class="col-form-label">{{ $t('document_upload') }}</label>
|
||||
<ul class="record_actions document-upload">
|
||||
<li>
|
||||
<add-async-upload
|
||||
:buttonTitle="$t('browse')"
|
||||
:options="asyncUploadOptions"
|
||||
@addDocument="addDocument"
|
||||
>
|
||||
</add-async-upload>
|
||||
<drop-file-modal :allow-remove="false" @add-document="addDocument"></drop-file-modal>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -199,12 +188,11 @@ import ClassicEditor from 'ChillMainAssets/module/ckeditor5/editor_config';
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue';
|
||||
import {buildLink} from 'ChillDocGeneratorAssets/lib/document-generator';
|
||||
import AddAsyncUpload from 'ChillDocStoreAssets/vuejs/_components/AddAsyncUpload.vue';
|
||||
import AddAsyncUploadDownloader from 'ChillDocStoreAssets/vuejs/_components/AddAsyncUploadDownloader.vue';
|
||||
import ListWorkflowModal from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue';
|
||||
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
|
||||
import {buildLinkCreate as buildLinkCreateNotification} from 'ChillMainAssets/lib/entity-notification/api';
|
||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||
import DropFileModal from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileModal.vue";
|
||||
|
||||
const i18n = {
|
||||
messages: {
|
||||
@ -243,10 +231,9 @@ export default {
|
||||
name: "FormEvaluation",
|
||||
props: ['evaluation', 'docAnchorId'],
|
||||
components: {
|
||||
DropFileModal,
|
||||
ckeditor: CKEditor.component,
|
||||
PickTemplate,
|
||||
AddAsyncUpload,
|
||||
AddAsyncUploadDownloader,
|
||||
ListWorkflowModal,
|
||||
DocumentActionButtonsGroup,
|
||||
},
|
||||
@ -380,21 +367,29 @@ export default {
|
||||
const title = event.target.value;
|
||||
this.$store.commit('updateDocumentTitle', {id: id, key: key, evaluationKey: this.evaluation.key, title: title});
|
||||
},
|
||||
addDocument(storedObject) {
|
||||
addDocument({stored_object, stored_object_version}) {
|
||||
let document = {
|
||||
type: 'accompanying_period_work_evaluation_document',
|
||||
storedObject: storedObject,
|
||||
storedObject: stored_object,
|
||||
title: 'Nouveau document',
|
||||
};
|
||||
this.$store.commit('addDocument', {key: this.evaluation.key, document: document});
|
||||
this.$store.commit('addDocument', {key: this.evaluation.key, document, stored_object_version});
|
||||
},
|
||||
replaceDocument(oldDocument, storedObject) {
|
||||
/**
|
||||
* Replaces a document in the store with a new document.
|
||||
*
|
||||
* @param {Object} oldDocument - The document to be replaced.
|
||||
* @param {StoredObject} storedObject - The stored object of the new document.
|
||||
* @param {StoredObjectVersion} storedObjectVersion - The new version of the document
|
||||
* @return {void}
|
||||
*/
|
||||
replaceDocument(oldDocument, storedObject, storedObjectVersion) {
|
||||
let document = {
|
||||
type: 'accompanying_period_work_evaluation_document',
|
||||
storedObject: storedObject,
|
||||
title: oldDocument.title
|
||||
};
|
||||
this.$store.commit('replaceDocument', {key: this.evaluation.key, document: document, oldDocument: oldDocument});
|
||||
this.$store.commit('replaceDocument', {key: this.evaluation.key, document, oldDocument: oldDocument, stored_object_version: storedObjectVersion});
|
||||
},
|
||||
removeDocument(document) {
|
||||
if (window.confirm("Êtes-vous sûr·e de vouloir supprimer le document qui a pour titre \"" + document.title +"\" ?")) {
|
||||
|
@ -219,7 +219,11 @@ const store = createStore({
|
||||
found.results = found.results.filter(r => r.id !== result.id);
|
||||
},
|
||||
addDocument(state, payload) {
|
||||
// associate version to stored object
|
||||
payload.document.storedObject.currentVersion = payload.stored_object_version;
|
||||
|
||||
let evaluation = state.evaluationsPicked.find(e => e.key === payload.key);
|
||||
|
||||
evaluation.documents.push(Object.assign(
|
||||
payload.document, {
|
||||
key: evaluation.documents.length + 1,
|
||||
@ -234,6 +238,13 @@ const store = createStore({
|
||||
}
|
||||
evaluation.documents = evaluation.documents.filter(d => d.key !== document.key);
|
||||
},
|
||||
/**
|
||||
* Replaces a document in the state with a new document.
|
||||
*
|
||||
* @param {object} state - The current state of the application.
|
||||
* @param {{key: number, oldDocument: {key: number}, stored_object_version: StoredObjectVersion}} payload - The object containing the information about the document to be replaced.
|
||||
* @return {void} - returns nothing.
|
||||
*/
|
||||
replaceDocument(state, payload) {
|
||||
let evaluation = state.evaluationsPicked.find(e => e.key === payload.key);
|
||||
if (evaluation === undefined) {
|
||||
@ -244,9 +255,10 @@ const store = createStore({
|
||||
|
||||
if (typeof doc === 'undefined') {
|
||||
console.error('doc not found');
|
||||
return;
|
||||
}
|
||||
|
||||
doc.storedObject = payload.document.storedObject;
|
||||
doc.storedObject.currentVersion = payload.stored_object_version;
|
||||
return;
|
||||
let newDocument = Object.assign(
|
||||
payload.document, {
|
||||
|
31
tsconfig.json
Normal file
31
tsconfig.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"extends": "@tsconfig/node14/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"ChillMainAssets": ["./src/Bundle/ChillMainBundle/Resources/public"],
|
||||
"ChillDocStoreAssets": ["./src/Bundle/ChillDocStoreBundle/Resources/public"]
|
||||
},
|
||||
"lib": [
|
||||
"es2020",
|
||||
"dom"
|
||||
],
|
||||
"module": "es6",
|
||||
"moduleResolution": "node",
|
||||
"isolatedModules": true,
|
||||
"allowJs": false,
|
||||
"checkJs": false,
|
||||
"importHelpers": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"sourceMap": true
|
||||
},
|
||||
"includes": [
|
||||
"./src/**/*.ts",
|
||||
"./src/**/*.vue"
|
||||
],
|
||||
"exclude": [
|
||||
"./docs/*",
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user