Quellcode durchsuchen

Merge branch 'master' into fixes

El RIDO vor 1 Jahr
Ursprung
Commit
34028229c8
10 geänderte Dateien mit 268 neuen und 194 gelöschten Zeilen
  1. 1 1
      .github/workflows/test-results.yml
  2. 2 0
      CHANGELOG.md
  3. 1 1
      CREDITS.md
  4. 6 0
      css/common.css
  5. 216 146
      js/privatebin.js
  6. 23 14
      js/test/AttachmentViewer.js
  7. 1 1
      lib/Configuration.php
  8. 6 11
      tpl/bootstrap.php
  9. 7 12
      tpl/bootstrap5.php
  10. 5 8
      tpl/page.php

+ 1 - 1
.github/workflows/test-results.yml

@@ -24,7 +24,7 @@ jobs:
 
     steps:
        - name: Download and Extract Artifacts
-         uses: dawidd6/action-download-artifact@07ab29fd4a977ae4d2b275087cf67563dfdf0295
+         uses: dawidd6/action-download-artifact@4c1e823582f43b179e2cbb49c3eade4e41f992e2
          with:
           run_id: ${{ github.event.workflow_run.id }}
           path: artifacts

+ 2 - 0
CHANGELOG.md

@@ -5,6 +5,8 @@
 * CHANGED: Passing large data structures by reference to reduce memory consumption (#858)
 * CHANGED: Removed use of ctype functions and polyfill library for ctype
 * CHANGED: Upgrading libraries to: DOMpurify 3.2.5, ip-lib 1.20.0
+* CHANGED: Support for multiple file uploads (#1060)
+* FIXED: Hide Reply button in the discussions once clicked to avoid losing the text input (#1508)
 * FIXED: Bump zlib library suffix, ensuring cache refresh for WASM streaming change
 * FIXED: Handle undefined globals in file based persisted values (#1544)
 

+ 1 - 1
CREDITS.md

@@ -33,7 +33,7 @@
 * Mounir Idrassi & J. Mozdzen - secure YOURLS integration
 * Felipe Nakandakari - enabled AWS SDK to use default credential provider chain in the S3 Storage backend
 * Aaron Sherber - cache control headers for API calls & use of `shortenviayourls` in query parameters
-* Mikhail Romanov - copying to clipboard, UI/UX improvements, templates switching
+* Mikhail Romanov - UI improvements, theme switching, clipboard support, multi-file upload, bugfixes, code refactoring
 
 ## Translations
 * Hexalyse - French

+ 6 - 0
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;

+ 216 - 146
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:[<mimeType>][;base64],<data>
 
+            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("<br>"));
+        }
+
         /**
          * 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('<form>').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
-         * @function
-         * @return {jQuery}
-         */
-        me.getAttachmentData = function () {
-            return attachmentData;
-        };
-
-        /**
-         * getter for attachment link
-         *
-         * @name   AttachmentViewer.getAttachmentLink
+         * @name   AttachmentViewer.getAttachmentsData
          * @function
-         * @return {jQuery}
+         * @return {string[]}
          */
-        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') || $('<a>');
             if($attachment.length) {
                 $attachmentPreview = $('#attachmentPreview');
 
@@ -3477,6 +3526,12 @@ jQuery.PrivateBin = (function($, RawDeflate) {
         {
             const $source = $(event.target);
 
+            // show all reply buttons
+            $commentContainer.find('button').removeClass('hidden');
+
+            // hide the current reply button
+            $source.addClass('hidden');
+
             // clear input
             $replyMessage.val('');
             $replyNickname.val('');
@@ -5129,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) {
@@ -5169,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];
-
-                // 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);
+                let attachments = AttachmentViewer.getAttachments();
+                cipherMessage['attachment'] = attachments.map(attachment => attachment[0]);
+                cipherMessage['attachment_name'] = attachments.map(attachment => attachment[1]);
+
+                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
@@ -5319,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;
@@ -5410,6 +5475,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
         me.run = function(paste)
         {
             Alert.hideMessages();
+            Alert.setCustomHandler(null);
             Alert.showLoading('Decrypting paste…', 'cloud-download');
 
             if (typeof paste === 'undefined' || paste.type === 'click') {
@@ -5801,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
@@ -5812,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'
                 );

+ 23 - 14
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(
-                    '<div id="attachment" role="alert" class="hidden alert ' +
-                    'alert-info"><span class="glyphicon glyphicon-download-' +
-                    'alt" aria-hidden="true"></span> <a class="alert-link">' +
-                    'Download attachment</a></div><div id="attachmentPrevie' +
-                    'w" class="hidden"></div>'
+                    '<div id="attachmentPreview" class="col-md-12 text-center hidden"></div>' +
+                    '<div id="attachment" class="hidden"></div>' +
+                    '<div id="templates">' +
+                        '<div id="attachmenttemplate" role="alert" class="attachment hidden alert alert-info">' +
+                            '<span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span>' +
+                            '<a class="alert-link">Download attachment</a>' +
+                        '</div>' +
+                    '</div>'
                 );
                 // 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 = $('<div>');
-                $.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('<a') === -1 && postfix.indexOf('<a') === -1) {
                     result = $('<textarea>').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();

+ 1 - 1
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-yN0ZaVFrXpiYaeFRcofdCrlHVcw4ZJAXU3K1l6H9rsoY//iLKegX0GdWxRCFWzhd1xO4eDJKCT7qyaA1Vt65bg==',
+            '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==',

+ 6 - 11
tpl/bootstrap.php

@@ -386,7 +386,7 @@ if ($FILEUPLOAD) :
 						<ul class="dropdown-menu">
 							<li id="filewrap">
 								<div>
-									<input type="file" id="file" name="file" />
+									<input type="file" id="file" name="file" multiple />
 								</div>
 								<div id="dragAndDropFileName" class="dragAndDropFile"><?php echo I18n::_('alternatively drag & drop a file or paste an image from the clipboard'); ?></div>
 							</li>
@@ -505,10 +505,7 @@ endif;
 <?php
 if ($FILEUPLOAD) :
 ?>
-				<div id="attachment" role="alert" class="hidden alert alert-info">
-					<span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span>
-					<a class="alert-link"><?php echo I18n::_('Download attachment'); ?></a>
-				</div>
+				<div id="attachment" class="hidden"></div>
 <?php
 endif;
 ?>
@@ -656,9 +653,6 @@ endif;
 				</div>
 			</footer>
 		</main>
-<?php
-if ($DISCUSSION) :
-?>
 		<div id="serverdata" class="hidden" aria-hidden="true">
 			<div id="templates">
 				<article id="commenttemplate" class="comment">
@@ -680,12 +674,13 @@ if ($DISCUSSION) :
 					</div>
 					<button id="replybutton" class="btn btn-default btn-sm"><?php echo I18n::_('Post comment'); ?></button>
 				</div>
+				<div id="attachmenttemplate" role="alert" class="attachment hidden alert alert-info">
+					<span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span>
+					<a class="alert-link"><?php echo I18n::_('Download attachment'); ?></a>
+				</div>
 			</div>
 		</div>
 <?php
-endif;
-?>
-<?php
 if ($FILEUPLOAD) :
 ?>
 		<div id="dropzone" class="hidden" tabindex="-1" aria-hidden="true"></div>

+ 7 - 12
tpl/bootstrap5.php

@@ -261,11 +261,11 @@ if ($FILEUPLOAD) :
 							<ul class="dropdown-menu px-2">
 								<li id="filewrap">
 									<div>
-										<input type="file" id="file" name="file" class="form-control" />
+										<input type="file" id="file" name="file" class="form-control" multiple />
 									</div>
 									<div id="dragAndDropFileName" class="dragAndDropFile"><?php echo I18n::_('alternatively drag & drop a file or paste an image from the clipboard'); ?></div>
 								</li>
-								<li id="customattachment" class="hidden"></li>
+								<li id="customattachment" class="hidden d-flex flex-column px-3"></li>
 								<li>
 									<a id="fileremovebutton" href="#" class="dropdown-item">
 										<?php echo I18n::_('Remove attachment'), PHP_EOL; ?>
@@ -370,10 +370,7 @@ endif;
 <?php
 if ($FILEUPLOAD) :
 ?>
-				<div id="attachment" role="alert" class="hidden alert alert-info">
-					<svg width="16" height="16" fill="currentColor" aria-hidden="true"><use href="img/bootstrap-icons.svg#download" /></svg>
-					<a class="alert-link"><?php echo I18n::_('Download attachment'); ?></a>
-				</div>
+				<div id="attachment" class="hidden"></div>
 <?php
 endif;
 ?>
@@ -514,9 +511,6 @@ endif;
 				</p>
 			</div>
 		</footer>
-<?php
-if ($DISCUSSION) :
-?>
 		<div id="serverdata" class="hidden" aria-hidden="true">
 			<div id="templates">
 				<article id="commenttemplate" class="comment px-2 pb-3">
@@ -538,12 +532,13 @@ if ($DISCUSSION) :
 					</div>
 					<button id="replybutton" class="btn btn-secondary btn-sm"><?php echo I18n::_('Post comment'); ?></button>
 				</div>
+				<div id="attachmenttemplate" role="alert" class="hidden alert alert-info">
+					<svg width="16" height="16" fill="currentColor" aria-hidden="true"><use href="img/bootstrap-icons.svg#download" /></svg>
+					<a class="alert-link"><?php echo I18n::_('Download attachment'); ?></a>
+				</div>
 			</div>
 		</div>
 <?php
-endif;
-?>
-<?php
 if ($FILEUPLOAD) :
 ?>
 		<div id="dropzone" class="hidden" tabindex="-1" aria-hidden="true"></div>

+ 5 - 8
tpl/page.php

@@ -265,10 +265,10 @@ endif;
 <?php
 if ($FILEUPLOAD):
 ?>
-				<div id="attachment" class="hidden"><a><?php echo I18n::_('Download attachment'); ?></a></div>
+				<div id="attachment" class="hidden"></div>
 				<div id="attach" class="hidden">
 					<span id="clonedfile" class="hidden"><?php echo I18n::_('Cloned file attached.'); ?></span>
-					<span id="filewrap"><?php echo I18n::_('Attach a file'); ?>: <input type="file" id="file" name="file" /></span>
+					<span id="filewrap"><?php echo I18n::_('Attach a file'); ?>: <input type="file" id="file" name="file" multiple /></span>
 					<span id="dragAndDropFileName" class="dragAndDropFile"><?php echo I18n::_('alternatively drag & drop a file or paste an image from the clipboard'); ?></span>
 					<button id="fileremovebutton"><?php echo I18n::_('Remove attachment'); ?></button>
 				</div>
@@ -297,9 +297,6 @@ endif;
 				<div id="commentcontainer"></div>
 			</div>
 		</section>
-<?php
-if ($DISCUSSION):
-?>
 		<div id="serverdata" class="hidden" aria-hidden="true">
 			<div id="templates">
 				<article id="commenttemplate" class="comment">
@@ -321,12 +318,12 @@ if ($DISCUSSION):
 					</div>
 					<button id="replybutton" class="btn btn-default btn-sm"><?php echo I18n::_('Post comment'); ?></button>
 				</div>
+				<div id="attachmenttemplate" class="attachment">
+					<a><?php echo I18n::_('Download attachment'); ?></a>
+				</div>
 			</div>
 		</div>
 <?php
-endif;
-?>
-<?php
 if ($FILEUPLOAD):
 ?>
 		<div id="dropzone" class="hidden" tabindex="-1" aria-hidden="true"></div>