diff --git a/css/common.css b/css/common.css index 5f032aed..b5019c47 100644 --- a/css/common.css +++ b/css/common.css @@ -8,6 +8,12 @@ * @license https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License */ +#attachmentPreview { + display: flex; + flex-direction: column; + align-items: center; +} + #attachmentPreview img { max-width: 100%; height: auto; diff --git a/js/privatebin.js b/js/privatebin.js index 439afefd..cfe1e4c6 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -2536,11 +2536,18 @@ jQuery.PrivateBin = (function($, RawDeflate) { // show preview PasteViewer.setText($message.val()); if (AttachmentViewer.hasAttachmentData()) { - const attachment = AttachmentViewer.getAttachment(); - AttachmentViewer.handleBlobAttachmentPreview( - AttachmentViewer.getAttachmentPreview(), - attachment[0], attachment[1] - ); + const attachmentsData = AttachmentViewer.getAttachmentsData(); + + attachmentsData.forEach(attachmentData => { + const mimeType = AttachmentViewer.getAttachmentMimeType(attachmentData); + + AttachmentViewer.handleBlobAttachmentPreview( + AttachmentViewer.getAttachmentPreview(), + attachmentData, mimeType + ); + }); + + AttachmentViewer.showAttachment(); } PasteViewer.run(); @@ -2925,14 +2932,12 @@ jQuery.PrivateBin = (function($, RawDeflate) { const AttachmentViewer = (function () { const me = {}; - let $attachmentLink, - $attachmentPreview, + let $attachmentPreview, $attachment, - attachmentData, - file, + attachmentsData = [], + files, $fileInput, - $dragAndDropFileName, - attachmentHasPreview = false, + $dragAndDropFileNames, $dropzone; /** @@ -2974,26 +2979,28 @@ jQuery.PrivateBin = (function($, RawDeflate) { me.setAttachment = function(attachmentData, fileName) { // skip, if attachments got disabled - if (!$attachmentLink || !$attachmentPreview) return; + if (!$attachment || !$attachmentPreview) return; // data URI format: data:[][;base64], + const template = Model.getTemplate('attachment'); + const attachmentLink = template.find('a'); + // position in data URI string of where data begins const base64Start = attachmentData.indexOf(',') + 1; - // position in data URI string of where mimeType ends - const mimeTypeEnd = attachmentData.indexOf(';'); - // extract mimeType - const mimeType = attachmentData.substring(5, mimeTypeEnd); + const mimeType = me.getAttachmentMimeType(attachmentData); + // extract data and convert to binary const rawData = attachmentData.substring(base64Start); const decodedData = rawData.length > 0 ? atob(rawData) : ''; let blobUrl = getBlobUrl(decodedData, mimeType); - $attachmentLink.attr('href', blobUrl); + attachmentLink.attr('href', blobUrl); if (typeof fileName !== 'undefined') { - $attachmentLink.attr('download', fileName); + attachmentLink.attr('download', fileName); + template.append(fileName); } // sanitize SVG preview @@ -3008,6 +3015,9 @@ jQuery.PrivateBin = (function($, RawDeflate) { blobUrl = getBlobUrl(sanitizedData, mimeType); } + template.removeClass('hidden'); + $attachment.append(template); + me.handleBlobAttachmentPreview($attachmentPreview, blobUrl, mimeType); }; @@ -3024,7 +3034,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { $attachment.removeClass('hidden'); - if (attachmentHasPreview) { + if (me.hasAttachmentPreview()) { $attachmentPreview.removeClass('hidden'); } }; @@ -3045,11 +3055,9 @@ jQuery.PrivateBin = (function($, RawDeflate) { } me.hideAttachment(); me.hideAttachmentPreview(); - $attachmentLink.removeAttr('href'); - $attachmentLink.removeAttr('download'); - $attachmentLink.off('click'); + $attachment.html(''); $attachmentPreview.html(''); - $dragAndDropFileName.text(''); + $dragAndDropFileNames.html(''); AttachmentViewer.removeAttachmentData(); }; @@ -3064,8 +3072,8 @@ jQuery.PrivateBin = (function($, RawDeflate) { */ me.removeAttachmentData = function() { - file = undefined; - attachmentData = undefined; + files = undefined; + attachmentsData = []; }; /** @@ -3076,9 +3084,21 @@ jQuery.PrivateBin = (function($, RawDeflate) { */ me.clearDragAndDrop = function() { - $dragAndDropFileName.text(''); + $dragAndDropFileNames.html(''); }; + /** + * Print file names added via drag & drop + * + * @name AttachmentViewer.printDragAndDropFileNames + * @private + * @function + * @param {array} fileNames + */ + function printDragAndDropFileNames(fileNames) { + $dragAndDropFileNames.html(fileNames.join("
")); + } + /** * hides the attachment * @@ -3107,6 +3127,18 @@ jQuery.PrivateBin = (function($, RawDeflate) { } }; + /** + * checks if has any attachment preview + * + * @name AttachmentViewer.hasAttachmentPreview + * @function + * @return {JQuery} + */ + me.hasAttachmentPreview = function() + { + return $attachmentPreview.children().length > 0; + } + /** * checks if there is an attachment displayed * @@ -3118,8 +3150,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { if (!$attachment.length) { return false; } - const link = $attachmentLink.prop('href'); - return (typeof link !== 'undefined' && link !== ''); + return [...$attachment.children()].length > 0; }; /** @@ -3139,20 +3170,38 @@ jQuery.PrivateBin = (function($, RawDeflate) { }; /** - * return the attachment + * return the attachments * - * @name AttachmentViewer.getAttachment + * @name AttachmentViewer.getAttachments * @function * @returns {array} */ - me.getAttachment = function() + me.getAttachments = function() { - return [ - $attachmentLink.prop('href'), - $attachmentLink.prop('download') - ]; + return [...$attachment.find('a')].map(link => ( + [ + $(link).prop('href'), + $(link).prop('download') + ] + )); }; + /** + * Get attachment mime type + * + * @name AttachmentViewer.getAttachmentMimeType + * @function + * @param {string} attachmentData - Base64 string + */ + me.getAttachmentMimeType = function(attachmentData) + { + // position in data URI string of where mimeType ends + const mimeTypeEnd = attachmentData.indexOf(';'); + + // extract mimeType + return attachmentData.substring(5, mimeTypeEnd); + } + /** * moves the attachment link to another element * @@ -3161,27 +3210,33 @@ jQuery.PrivateBin = (function($, RawDeflate) { * @name AttachmentViewer.moveAttachmentTo * @function * @param {jQuery} $element - the wrapper/container element where this should be moved to + * @param {array} attachment - attachment data * @param {string} label - the text to show (%s will be replaced with the file name), will automatically be translated */ - me.moveAttachmentTo = function($element, label) + me.moveAttachmentTo = function($element, attachment, label) { + const attachmentLink = $(document.createElement('a')) + .addClass('alert-link') + .prop('href', attachment[0]) + .prop('download', attachment[1]); + // move elemement to new place - $attachmentLink.appendTo($element); + attachmentLink.appendTo($element); // update text - ensuring no HTML is inserted into the text node - I18n._($attachmentLink, label, $attachmentLink.attr('download')); + I18n._(attachmentLink, label, attachment[1]); }; /** - * read file data as data URL using the FileReader API + * read files data as data URL using the FileReader API * * @name AttachmentViewer.readFileData * @private * @function - * @param {object} loadedFile (optional) loaded file object + * @param {FileList[]} loadedFiles (optional) loaded files array * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/FileReader#readAsDataURL()} */ - function readFileData(loadedFile) { + function readFileData(loadedFiles) { if (typeof FileReader === 'undefined') { // revert loading status… me.hideAttachment(); @@ -3190,28 +3245,35 @@ jQuery.PrivateBin = (function($, RawDeflate) { return; } - const fileReader = new FileReader(); - if (loadedFile === undefined) { - loadedFile = $fileInput[0].files[0]; - $dragAndDropFileName.text(''); + if (loadedFiles === undefined) { + loadedFiles = [...$fileInput[0].files]; + me.clearDragAndDrop(); } else { - $dragAndDropFileName.text(loadedFile.name); + const fileNames = loadedFiles.map((loadedFile => loadedFile.name)); + printDragAndDropFileNames(fileNames); } - if (typeof loadedFile !== 'undefined') { - file = loadedFile; - fileReader.onload = function (event) { - const dataURL = event.target.result; - attachmentData = dataURL; + if (typeof loadedFiles !== 'undefined') { + files = loadedFiles; + loadedFiles.forEach(loadedFile => { + const fileReader = new FileReader(); - if (Editor.isPreview()) { - me.handleAttachmentPreview($attachmentPreview, dataURL); - $attachmentPreview.removeClass('hidden'); - } + fileReader.onload = function (event) { + const dataURL = event.target.result; + if (dataURL) { + attachmentsData.push(dataURL); + } - TopNav.highlightFileupload(); - }; - fileReader.readAsDataURL(loadedFile); + if (Editor.isPreview()) { + me.handleAttachmentPreview($attachmentPreview, dataURL); + $attachmentPreview.removeClass('hidden'); + } + + TopNav.highlightFileupload(); + }; + + fileReader.readAsDataURL(loadedFile); + }); } else { me.removeAttachmentData(); } @@ -3227,16 +3289,17 @@ jQuery.PrivateBin = (function($, RawDeflate) { * @argument {string} mime type */ me.handleBlobAttachmentPreview = function ($targetElement, blobUrl, mimeType) { - if (blobUrl) { - attachmentHasPreview = true; + const alreadyIncludesCurrentAttachment = $targetElement.find(`[src='${blobUrl}']`).length > 0; + + if (blobUrl && !alreadyIncludesCurrentAttachment) { if (mimeType.match(/^image\//i)) { - $targetElement.html( + $targetElement.append( $(document.createElement('img')) .attr('src', blobUrl) .attr('class', 'img-thumbnail') ); } else if (mimeType.match(/^video\//i)) { - $targetElement.html( + $targetElement.append( $(document.createElement('video')) .attr('controls', 'true') .attr('autoplay', 'true') @@ -3247,7 +3310,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { .attr('src', blobUrl)) ); } else if (mimeType.match(/^audio\//i)) { - $targetElement.html( + $targetElement.append( $(document.createElement('audio')) .attr('controls', 'true') .attr('autoplay', 'true') @@ -3260,15 +3323,13 @@ jQuery.PrivateBin = (function($, RawDeflate) { // Fallback for browsers, that don't support the vh unit const clientHeight = $(window).height(); - $targetElement.html( + $targetElement.append( $(document.createElement('embed')) .attr('src', blobUrl) .attr('type', 'application/pdf') .attr('class', 'pdfPreview') .css('height', clientHeight) ); - } else { - attachmentHasPreview = false; } } }; @@ -3301,14 +3362,14 @@ jQuery.PrivateBin = (function($, RawDeflate) { } if ($fileInput) { - const file = evt.dataTransfer.files[0]; + const files = [...evt.dataTransfer.files]; //Clear the file input: $fileInput.wrap('
').closest('form').get(0).reset(); $fileInput.unwrap(); //Only works in Chrome: //fileInput[0].files = e.dataTransfer.files; - readFileData(file); + readFileData(files); } }; @@ -3362,23 +3423,12 @@ jQuery.PrivateBin = (function($, RawDeflate) { /** * getter for attachment data * - * @name AttachmentViewer.getAttachmentData + * @name AttachmentViewer.getAttachmentsData * @function - * @return {jQuery} + * @return {string[]} */ - me.getAttachmentData = function () { - return attachmentData; - }; - - /** - * getter for attachment link - * - * @name AttachmentViewer.getAttachmentLink - * @function - * @return {jQuery} - */ - me.getAttachmentLink = function () { - return $attachmentLink; + me.getAttachmentsData = function () { + return attachmentsData; }; /** @@ -3393,14 +3443,14 @@ jQuery.PrivateBin = (function($, RawDeflate) { }; /** - * getter for file data, returns the file contents + * getter for files data, returns the file list * - * @name AttachmentViewer.getFile + * @name AttachmentViewer.getFiles * @function - * @return {string} + * @return {FileList[]} */ - me.getFile = function () { - return file; + me.getFiles = function () { + return files; }; /** @@ -3414,9 +3464,8 @@ jQuery.PrivateBin = (function($, RawDeflate) { me.init = function() { $attachment = $('#attachment'); - $dragAndDropFileName = $('#dragAndDropFileName'); + $dragAndDropFileNames = $('#dragAndDropFileName'); $dropzone = $('#dropzone'); - $attachmentLink = $('#attachment a') || $(''); if($attachment.length) { $attachmentPreview = $('#attachmentPreview'); @@ -5135,7 +5184,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { const plainText = Editor.getText(), format = PasteViewer.getFormat(), // the methods may return different values if no files are attached (null, undefined or false) - files = TopNav.getFileList() || AttachmentViewer.getFile() || AttachmentViewer.hasAttachment(); + files = TopNav.getFileList() || AttachmentViewer.getFiles() || AttachmentViewer.hasAttachment(); // do not send if there is no data if (plainText.length === 0 && !files) { @@ -5175,62 +5224,64 @@ jQuery.PrivateBin = (function($, RawDeflate) { PasteViewer.setFormat(format); // prepare cypher message - let file = AttachmentViewer.getAttachmentData(), + let attachmentsData = AttachmentViewer.getAttachmentsData(), cipherMessage = { 'paste': plainText }; - if (typeof file !== 'undefined' && file !== null) { - cipherMessage['attachment'] = file; - cipherMessage['attachment_name'] = AttachmentViewer.getFile().name; + if (attachmentsData.length) { + cipherMessage['attachment'] = attachmentsData; + cipherMessage['attachment_name'] = AttachmentViewer.getFiles().map((fileInfo => fileInfo.name)); } else if (AttachmentViewer.hasAttachment()) { // fall back to cloned part - let attachment = AttachmentViewer.getAttachment(); - cipherMessage['attachment'] = attachment[0]; - cipherMessage['attachment_name'] = attachment[1]; + let attachments = AttachmentViewer.getAttachments(); + cipherMessage['attachment'] = attachments.map(attachment => attachment[0]); + cipherMessage['attachment_name'] = attachments.map(attachment => attachment[1]); - // we need to retrieve data from blob if browser already parsed it in memory - if (typeof attachment[0] === 'string' && attachment[0].startsWith('blob:')) { - Alert.showStatus( - [ - 'Retrieving cloned file \'%s\' from memory...', - attachment[1] - ], - 'copy' - ); - try { - const blobData = await $.ajax({ - type: 'GET', - url: `${attachment[0]}`, - processData: false, - timeout: 10000, - xhrFields: { - withCredentials: false, - responseType: 'blob' - } - }); - if (blobData instanceof window.Blob) { - const fileReading = new Promise(function(resolve, reject) { - const fileReader = new FileReader(); - fileReader.onload = function (event) { - resolve(event.target.result); - }; - fileReader.onerror = function (error) { - reject(error); + cipherMessage['attachment'] = await Promise.all(cipherMessage['attachment'].map(async (attachment) => { + // we need to retrieve data from blob if browser already parsed it in memory + if (typeof attachment === 'string' && attachment.startsWith('blob:')) { + Alert.showStatus( + [ + 'Retrieving cloned file \'%s\' from memory...', + attachment[1] + ], + 'copy' + ); + try { + const blobData = await $.ajax({ + type: 'GET', + url: `${attachment}`, + processData: false, + timeout: 10000, + xhrFields: { + withCredentials: false, + responseType: 'blob' } - fileReader.readAsDataURL(blobData); }); - cipherMessage['attachment'] = await fileReading; - } else { - const error = 'Cannot process attachment data.'; - Alert.showError(error); - throw new TypeError(error); + if (blobData instanceof window.Blob) { + const fileReading = new Promise(function(resolve, reject) { + const fileReader = new FileReader(); + fileReader.onload = function (event) { + resolve(event.target.result); + }; + fileReader.onerror = function (error) { + reject(error); + } + fileReader.readAsDataURL(blobData); + }); + + return await fileReading; + } else { + const error = 'Cannot process attachment data.'; + Alert.showError(error); + throw new TypeError(error); + } + } catch (error) { + Alert.showError('Cannot retrieve attachment.'); + throw error; } - } catch (error) { - console.error(error); - Alert.showError('Cannot retrieve attachment.'); - throw error; } - } + })); } // encrypt message @@ -5325,7 +5376,15 @@ jQuery.PrivateBin = (function($, RawDeflate) { // version 2 paste const pasteMessage = JSON.parse(pastePlain); if (pasteMessage.hasOwnProperty('attachment') && pasteMessage.hasOwnProperty('attachment_name')) { - AttachmentViewer.setAttachment(pasteMessage.attachment, pasteMessage.attachment_name); + if (Array.isArray(pasteMessage.attachment) && Array.isArray(pasteMessage.attachment_name)) { + pasteMessage.attachment.forEach((attachment, key) => { + const attachment_name = pasteMessage.attachment_name[key]; + AttachmentViewer.setAttachment(attachment, attachment_name); + }); + } else { + // Continue to process attachment parameters as strings to ensure backward compatibility + AttachmentViewer.setAttachment(pasteMessage.attachment, pasteMessage.attachment_name); + } AttachmentViewer.showAttachment(); } pastePlain = pasteMessage.paste; @@ -5808,10 +5867,14 @@ jQuery.PrivateBin = (function($, RawDeflate) { history.pushState({type: 'clone'}, document.title, Helper.baseUri()); if (AttachmentViewer.hasAttachment()) { - AttachmentViewer.moveAttachmentTo( - TopNav.getCustomAttachment(), - 'Cloned: \'%s\'' - ); + const attachments = AttachmentViewer.getAttachments(); + attachments.forEach(attachment => { + AttachmentViewer.moveAttachmentTo( + TopNav.getCustomAttachment(), + attachment, + 'Cloned: \'%s\'' + ); + }); TopNav.hideFileSelector(); AttachmentViewer.hideAttachment(); // NOTE: it also looks nice without removing the attachment @@ -5819,12 +5882,12 @@ jQuery.PrivateBin = (function($, RawDeflate) { AttachmentViewer.hideAttachmentPreview(); TopNav.showCustomAttachment(); - // show another status message to make the user aware that the - // file was cloned too! + // show another status messages to make the user aware that the + // files were cloned too! Alert.showStatus( [ 'The cloned file \'%s\' was attached to this paste.', - AttachmentViewer.getAttachment()[1] + attachments.map(attachment => attachment[1]).join(', '), ], 'copy' ); diff --git a/js/test/AttachmentViewer.js b/js/test/AttachmentViewer.js index c8b09012..029b0d84 100644 --- a/js/test/AttachmentViewer.js +++ b/js/test/AttachmentViewer.js @@ -27,11 +27,14 @@ describe('AttachmentViewer', function () { prefix = prefix.replace(/%(s|d)/g, '%%'); postfix = postfix.replace(/%(s|d)/g, '%%'); $('body').html( - '' + '' + + '' + + '
' + + '' + + '
' ); // mock createObjectURL for jsDOM if (typeof window.URL.createObjectURL === 'undefined') { @@ -44,9 +47,12 @@ describe('AttachmentViewer', function () { ) } $.PrivateBin.AttachmentViewer.init(); + $.PrivateBin.Model.init(); results.push( !$.PrivateBin.AttachmentViewer.hasAttachment() && $('#attachment').hasClass('hidden') && + $('#attachment').children().length === 0 && + $('#attachmenttemplate').hasClass('hidden') && $('#attachmentPreview').hasClass('hidden') ); global.atob = common.atob; @@ -55,19 +61,21 @@ describe('AttachmentViewer', function () { } else { $.PrivateBin.AttachmentViewer.setAttachment(data); } - // beyond this point we will get the blob URL instead of the data + // // beyond this point we will get the blob URL instead of the data data = window.URL.createObjectURL(data); - const attachment = $.PrivateBin.AttachmentViewer.getAttachment(); + const attachment = $.PrivateBin.AttachmentViewer.getAttachments(); results.push( $.PrivateBin.AttachmentViewer.hasAttachment() && $('#attachment').hasClass('hidden') && + $('#attachment').children().length > 0 && $('#attachmentPreview').hasClass('hidden') && - attachment[0] === data && - attachment[1] === filename + attachment[0][0] === data && + attachment[0][1] === filename ); $.PrivateBin.AttachmentViewer.showAttachment(); results.push( !$('#attachment').hasClass('hidden') && + $('#attachment').children().length > 0 && (previewSupported ? !$('#attachmentPreview').hasClass('hidden') : $('#attachmentPreview').hasClass('hidden')) ); $.PrivateBin.AttachmentViewer.hideAttachment(); @@ -85,7 +93,7 @@ describe('AttachmentViewer', function () { (previewSupported ? !$('#attachmentPreview').hasClass('hidden') : $('#attachmentPreview').hasClass('hidden')) ); let element = $('
'); - $.PrivateBin.AttachmentViewer.moveAttachmentTo(element, prefix + '%s' + postfix); + $.PrivateBin.AttachmentViewer.moveAttachmentTo(element, attachment[0], prefix + '%s' + postfix); // messageIDs with links get a relaxed treatment if (prefix.indexOf('').text((prefix + filename + postfix)).text(); @@ -99,16 +107,17 @@ describe('AttachmentViewer', function () { } if (filename.length) { results.push( - element.children()[0].href === data && - element.children()[0].getAttribute('download') === filename && - element.children()[0].text === result + element.find('a')[0].href === data && + element.find('a')[0].getAttribute('download') === filename && + element.find('a')[0].text === result ); } else { - results.push(element.children()[0].href === data); + results.push(element.find('a')[0].href === data); } $.PrivateBin.AttachmentViewer.removeAttachment(); results.push( $('#attachment').hasClass('hidden') && + $('#attachment').children().length === 0 && $('#attachmentPreview').hasClass('hidden') ); clean(); diff --git a/lib/Configuration.php b/lib/Configuration.php index f78f3ae5..b562a047 100644 --- a/lib/Configuration.php +++ b/lib/Configuration.php @@ -119,7 +119,7 @@ class Configuration 'js/kjua-0.9.0.js' => 'sha512-CVn7af+vTMBd9RjoS4QM5fpLFEOtBCoB0zPtaqIDC7sF4F8qgUSRFQQpIyEDGsr6yrjbuOLzdf20tkHHmpaqwQ==', 'js/legacy.js' => 'sha512-UxW/TOZKon83n6dk/09GsYKIyeO5LeBHokxyIq+r7KFS5KMBeIB/EM7NrkVYIezwZBaovnyNtY2d9tKFicRlXg==', 'js/prettify.js' => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==', - 'js/privatebin.js' => 'sha512-QkOUM8rg4MI60YRwHqWmayBzCdf/e3XnbHtrX17h2nn0EcyOQNhtSq8a0dXR1hoQFHFfF+9PiT73nZ6qoogjQA==', + 'js/privatebin.js' => 'm6RrsOsz4RgIWXDzgRghQDx6aegFCpkpqURwhfXwE/rNWhe/1rPJaLR+FXII82iTWo0n9JCzSbqrDqkYVPI50w==', 'js/purify-3.2.5.js' => 'sha512-eLlLLL/zYuf5JuG0x4WQm687MToqOGP9cDQHIdmOy1ZpjiY4J48BBcOM7DtZheKk1UogW920+9RslWYB4KGuuA==', 'js/rawinflate-0.3.js' => 'sha512-g8uelGgJW9A/Z1tB6Izxab++oj5kdD7B4qC7DHwZkB6DGMXKyzx7v5mvap2HXueI2IIn08YlRYM56jwWdm2ucQ==', 'js/showdown-2.1.0.js' => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==', diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 5d5ea677..f8e7cfca 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -386,7 +386,7 @@ if ($FILEUPLOAD) :
- + - diff --git a/tpl/bootstrap5.php b/tpl/bootstrap5.php index 916669ca..8aa3ffc8 100644 --- a/tpl/bootstrap5.php +++ b/tpl/bootstrap5.php @@ -261,11 +261,11 @@ if ($FILEUPLOAD) :