diff --git a/CHANGELOG.md b/CHANGELOG.md index 009097ad4..80e5dcd5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,4 +9,7 @@ Branch add_dropzone =================== - fix some missing translations on update / create document and "any document" in list +- use dropzone to upload a document with a better UI + +You must add `"dropzone": "^5.5.1"` to your dependencies in `packages.json`. diff --git a/Resources/public/module/async_upload/downloader.js b/Resources/public/module/async_upload/downloader.js index 10f03b6ad..cfeee5c37 100644 --- a/Resources/public/module/async_upload/downloader.js +++ b/Resources/public/module/async_upload/downloader.js @@ -29,6 +29,8 @@ var download = (button) => { labelReady = button.dataset.labelReady, mimeType = button.dataset.mimeType, extension = mime.extension(mimeType), + decryptError = "Error while decrypting file", + fetchError = "Error while fetching file", key, url ; @@ -44,14 +46,25 @@ var download = (button) => { }) .then(data => { url = data.url; - + return window.crypto.subtle.importKey('jwk', keyData, { name: algo, iv: iv}, false, ['decrypt']); }) + .catch(e => { + console.error("error while importing key"); + console.error(e); + button.appendChild(document.createTextNode(decryptError)); + }) .then(nKey => { key = nKey; return window.fetch(url); }) + .catch(e => { + console.error("error while fetching data"); + console.error(e); + button.textContent = ""; + button.appendChild(document.createTextNode(fetchError)); + }) .then(r => { if (r.ok) { return r.arrayBuffer(); @@ -62,6 +75,12 @@ var download = (button) => { .then(buffer => { return window.crypto.subtle.decrypt({ name: algo, iv: iv }, key, buffer); }) + .catch(e => { + console.error("error while importing key"); + console.error(e); + button.textContent = ""; + button.appendChild(document.createTextNode(decryptError)); + }) .then(decrypted => { var blob = new Blob([decrypted], { type: mimeType }), @@ -82,6 +101,8 @@ var download = (button) => { }) .catch(error => { console.log(error); + button.textContent = ""; + button.appendChild(document.createTextNode("error while handling decrypted file")); }) ; }; @@ -89,3 +110,5 @@ var download = (button) => { window.addEventListener('load', function(e) { initializeButtons(e.target); }); + +module.exports = initializeButtons; diff --git a/Resources/public/module/async_upload/index.scss b/Resources/public/module/async_upload/index.scss new file mode 100644 index 000000000..9d42d88d9 --- /dev/null +++ b/Resources/public/module/async_upload/index.scss @@ -0,0 +1,25 @@ +.dropzone { + margin-bottom: 0.5rem; + + .dz-preview { + display: initial; + margin-left: auto; + margin-right: auto; + + .dz-image { + margin-left: auto; + margin-right: auto; + } + + .dz-details, .dz-progress, .dz-success-mark, .dz-error-mark { + position: initial; + margin-left: auto; + margin-right: auto; + } + } +} + +.sc-button.dz-bt-below-dropzone { + width: 100%; +} + diff --git a/Resources/public/module/async_upload/uploader.js b/Resources/public/module/async_upload/uploader.js index c871152d3..7362a26ec 100644 --- a/Resources/public/module/async_upload/uploader.js +++ b/Resources/public/module/async_upload/uploader.js @@ -1,4 +1,15 @@ var algo = 'AES-CBC'; +var Dropzone = require('dropzone'); +var initializeDownload = require('./downloader.js'); + +// load css +//require('dropzone/dist/basic.css'); +require('dropzone/dist/dropzone.css'); +require('./index.scss'); +// + +// disable dropzone autodiscover +Dropzone.autoDiscover = false; var keyDefinition = { name: algo, @@ -13,111 +24,135 @@ var searchForZones = function(root) { } }; +var getUploadUrl = function(zoneData, files) { + var + generateTempUrlPost = zoneData.zone.querySelector('input[data-async-file-upload]').dataset.generateTempUrlPost, + oReq = new XMLHttpRequest() + ; + + // arg, dropzone, you cannot handle async upload... + oReq.open("GET", generateTempUrlPost, false); + oReq.send(); + + if (oReq.readyState !== XMLHttpRequest.DONE) { + throw new Error("Error while fetching url to upload"); + } + + zoneData.params = JSON.parse(oReq.responseText); + + return zoneData.params.url; +}; + +var encryptFile = function(originalFile, zoneData, done) { + var + iv = crypto.getRandomValues(new Uint8Array(16)), + reader = new FileReader(), + jsKey, rawKey + ; + + zoneData.originalType = originalFile.type; + + reader.onload = e => { + window.crypto.subtle.generateKey(keyDefinition, true, [ "encrypt", "decrypt" ]) + .then(key => { + jsKey = key; + + // we register the key somwhere + return window.crypto.subtle.exportKey('jwk', key); + }).then(exportedKey => { + rawKey = exportedKey; + + // we start encryption + return window.crypto.subtle.encrypt({ name: algo, iv: iv}, jsKey, e.target.result); + }) + .then(encrypted => { + zoneData.crypto = { + jsKey: jsKey, + rawKey: rawKey, + iv: iv + }; + + done(new File( [ encrypted ], zoneData.suffix)); + }); + }; + + reader.readAsArrayBuffer(originalFile); +}; + var initialize = function(zone) { var - dropZone = document.createElement('div'), - input = document.createElement('input') - ; + created = document.createElement('div'), + initMessage = document.createElement('div'), + initContent = zone.dataset.labelInitMessage, + zoneData = { zone: zone, suffix: createFilename() }, + dropzoneI; - input.type = 'file'; - input.addEventListener('change', function(e) { - handleInputFile(zone); + created.classList.add('dropzone'); + initMessage.classList.add('dz-message'); + initMessage.appendChild(document.createTextNode(initContent)); + + dropzoneI = new Dropzone(created, { + url: function(files) { + return getUploadUrl(zoneData, files); + }, + dictDefaultMessage: zone.dataset.dictDefaultMessage, + dictFileTooBig: zone.dataset.dictFileTooBig, + dictRemoveFile: zone.dataset.dictRemoveFile, + dictMaxFilesExceeded: zone.dataset.dictMaxFilesExceeded, + dictCancelUpload: zone.dataset.dictCancelUpload, + dictCancelUploadConfirm: zone.dataset.dictCancelUploadConfirm, + dictUploadCanceled: zone.dataset.dictUploadCanceled, + maxFiles: 1, + addRemoveLinks: true, + transformFile: function(file, done) { + encryptFile(file, zoneData, done); + }, + renameFile: function(file) { + return zoneData.suffix; + } + }); + + dropzoneI.on("sending", function(file, xhr, formData) { + formData.append("redirect", zoneData.params.redirect); + formData.append("max_file_size", zoneData.params.max_file_size); + formData.append("max_file_count", zoneData.params.max_file_count); + formData.append("expires", zoneData.params.expires); + formData.append("signature", zoneData.params.signature); }); - dropZone.classList.add('chill-doc__dropzone__drop'); + dropzoneI.on("success", function(file, response) { + zoneData.currentFile = file; + storeDataInForm(zone, zoneData); + }); - zone.insertBefore(input, zone.firstChild); - zone.insertBefore(dropZone, zone.firstChild); -}; + dropzoneI.on("addedfile", function(file) { + if (zoneData.hasOwnProperty('currentFile')) { + dropzoneI.removeFile(zoneData.currentFile); + } + }); + + dropzoneI.on("removedfile", function(file) { + removeDataInForm(zone, zoneData); + }); -var handleInputFile = function (zone) { - var - file = zone.querySelector('input[type="file"]').files[0], - type = file.type, - reader = new FileReader() - ; + zone.insertBefore(created, zone.firstChild); - reader.onload = e => { - transmitArrayBuffer(zone, e.target.result, type); - }; - - reader.readAsArrayBuffer(file); + insertDownloadButton(zone, zoneData); }; var createFilename = () => { var text = ""; var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for (let i = 0; i < 7; i++) - text += possible.charAt(Math.floor(Math.random() * possible.length)); + for (let i = 0; i < 7; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } return text; }; -var transmitArrayBuffer = (zone, data, type) => { - var - iv = crypto.getRandomValues(new Uint8Array(16)), - generateTempUrlPost = zone.querySelector('input[data-async-file-upload]').dataset.generateTempUrlPost, - suffix = createFilename(), - jsKey, rawKey, encryptedData, uploadData - ; - - window.crypto.subtle.generateKey(keyDefinition, true, [ "encrypt", "decrypt" ]) - .then(key => { - jsKey = key; - - // we register the key somwhere - return window.crypto.subtle.exportKey('jwk', key); - }).then(exportedKey => { - rawKey = exportedKey; - - // we start encryption - return window.crypto.subtle.encrypt({ name: algo, iv: iv}, jsKey, data); - }) - .then(encrypted => { - - encryptedData = encrypted; - - // we get the url and parameters to upload document - return window.fetch(generateTempUrlPost); - }) - .then(response => response.json()) - .then(data => { - var - formData = new FormData(); - - uploadData = data; - - formData.append("redirect", data.redirect); - formData.append("max_file_size", data.max_file_size); - formData.append("max_file_count", data.max_file_count); - formData.append("expires", data.expires); - formData.append("signature", data.signature); - formData.append("file", new Blob([ encryptedData ]), suffix); - - return window.fetch(data.url, { - method: 'POST', - mode: 'cors', - body: formData - }); - }) - .then(response => { - if (response.ok) { - storeDataInForm(zone, suffix, rawKey, iv, uploadData, type); - - } else { - throw new Error("error while sending data"); - } - }) - .catch(error => { - window.alert("Error while sending document."); - console.log(error); - }) - ; -}; - -var storeDataInForm = (zone, suffix, jskey, iv, uploaddata, type) => { +var storeDataInForm = (zone, zoneData) => { var inputKey = zone.querySelector('input[data-stored-object-key]'), inputIv = zone.querySelector('input[data-stored-object-iv]'), @@ -125,10 +160,68 @@ var storeDataInForm = (zone, suffix, jskey, iv, uploaddata, type) => { inputType = zone.querySelector('input[data-async-file-type]') ; - inputKey.value = JSON.stringify(jskey); - inputIv.value = JSON.stringify(iv); - inputType.value = type; - inputObject.value = uploaddata.prefix + suffix; + inputKey.value = JSON.stringify(zoneData.crypto.rawKey); + inputIv.value = JSON.stringify(Array.from(zoneData.crypto.iv)); + inputType.value = zoneData.originalType; + inputObject.value = zoneData.params.prefix + zoneData.suffix; + + insertDownloadButton(zone); +}; + +var removeDataInForm = (zone, zoneData) => { + var + inputKey = zone.querySelector('input[data-stored-object-key]'), + inputIv = zone.querySelector('input[data-stored-object-iv]'), + inputObject = zone.querySelector('input[data-async-file-upload]'), + inputType = zone.querySelector('input[data-async-file-type]') + ; + + inputKey.value = ""; + inputIv.value = ""; + inputType.value = ""; + inputObject.value = ""; + + insertDownloadButton(zone); +}; + +var insertDownloadButton = (zone) => { + var + existingButtons = zone.querySelectorAll('a[data-download-button]'), + newButton = document.createElement('a'), + inputKey = zone.querySelector('input[data-stored-object-key]'), + inputIv = zone.querySelector('input[data-stored-object-iv]'), + inputObject = zone.querySelector('input[data-async-file-upload]'), + inputType = zone.querySelector('input[data-async-file-type]'), + labelPreparing = zone.dataset.labelPreparing, + labelQuietButton = zone.dataset.labelQuietButton, + labelReady = zone.dataset.labelReady, + tempUrlGenerator = zone.dataset.tempUrlGenerator, + tempUrlGeneratorParams = new URLSearchParams() + ; + + // remove existing + existingButtons.forEach(function(b) { + b.remove(); + }); + + if (inputObject.value === '') { + return; + } + + tempUrlGeneratorParams.append('object_name', inputObject.value); + + newButton.dataset.downloadButton = true; + newButton.dataset.key = inputKey.value; + newButton.dataset.iv = inputIv.value; + newButton.dataset.mimeType = inputType.value; + newButton.dataset.labelPreparing = labelPreparing; + newButton.dataset.labelReady = labelReady; + newButton.dataset.tempUrlGetGenerator = tempUrlGenerator + '?' + tempUrlGeneratorParams.toString(); + newButton.classList.add('sc-button', 'bt-download', 'dz-bt-below-dropzone'); + newButton.textContent = labelQuietButton; + + zone.appendChild(newButton); + initializeDownload(zone); }; window.addEventListener('load', function(e) { diff --git a/Resources/translations/messages.fr.yml b/Resources/translations/messages.fr.yml index e35862872..d6bdf92f7 100644 --- a/Resources/translations/messages.fr.yml +++ b/Resources/translations/messages.fr.yml @@ -13,4 +13,13 @@ No document to download: Aucun document à télécharger 'Choose a document category': Choisissez une catégorie de document Any document found: Aucun document trouvé The document is successfully registered: Le document est enregistré -The document is successfully updated: Le document est mis à jour \ No newline at end of file +The document is successfully updated: Le document est mis à jour + +# dropzone upload +File too big: Fichier trop volumineux +Drop your file or click here: Cliquez ici ou faites glissez votre nouveau fichier dans cette zone +Remove file in order to upload a new one: Supprimer ce fichier pour en insérer un autre +Max files exceeded. Remove previous files: Nombre maximum de fichier atteint. Supprimez les précédents +Cancel upload: Annuler le téléversement +Are you sure you want to cancel this upload ?: Êtes-vous sûrs de vouloir annuler ce téléversement ? +Upload canceled: Téléversement annulé diff --git a/Resources/views/Form/fields.html.twig b/Resources/views/Form/fields.html.twig index ee21c0cb1..2be382f5d 100644 --- a/Resources/views/Form/fields.html.twig +++ b/Resources/views/Form/fields.html.twig @@ -1,5 +1,17 @@ {% block stored_object_widget %} -