Преглед изворни кода

fix: replace e-mail button click handlers instead of stacking them

Since the jQuery removal in 5b008ac5, the removeEventListener calls in
TopNav never matched the anonymous handlers added next to them, unlike
the namespaced .off() calls of 2.0.x. Every time the e-mail button was
shown or its confirmation dialog opened, another click handler was
added, so a click could send several e-mails, possibly with the
expiration of an earlier document, and hiding the button didn't unbind
it.

- the e-mail button and the confirmation dialog each keep an
AbortController for their listeners, which showEmailButton,
hideEmailButton and each opening of the dialog abort before binding
new ones
- choosing a timezone removes the dialog's listeners, so each opening
sends at most one e-mail; the focus handler on shown.bs.modal no
longer stacks either
- uses the window's AbortController, as jsdom rejects signals of the
Node.js one in the unit tests
- updated SRI hash and CHANGELOG

The tests added in ee9eb82c now pass.
El RIDO пре 1 недеља
родитељ
комит
6236d26637
3 измењених фајлова са 48 додато и 11 уклоњено
  1. 1 0
      CHANGELOG.md
  2. 46 10
      js/privatebin.js
  3. 1 1
      lib/Configuration.php

+ 1 - 0
CHANGELOG.md

@@ -9,6 +9,7 @@
 * FIXED: Slovenian plural forms were stored in the wrong order, i.e. "1 uri" instead of "1 ura" for one hour (#1951)
 * FIXED: Slovenian plural forms were stored in the wrong order, i.e. "1 uri" instead of "1 ura" for one hour (#1951)
 * FIXED: Lithuanian plural formula selected the "few" form for numbers ending in 0 above 10, i.e. "20 sekundės" instead of "20 sekundžių" (#1956)
 * FIXED: Lithuanian plural formula selected the "few" form for numbers ending in 0 above 10, i.e. "20 sekundės" instead of "20 sekundžių" (#1956)
 * FIXED: new paste button resets it to default formatter/style instead of hardcoding 'plaintext' (#2026)
 * FIXED: new paste button resets it to default formatter/style instead of hardcoding 'plaintext' (#2026)
+* FIXED: E-mail button sent several e-mails after being shown again or after its confirmation dialog was dismissed, possibly with the expiration of an earlier document
 
 
 ## 2.1.0 (2026-10-18)
 ## 2.1.0 (2026-10-18)
 * ADDED: Added `shortenviachhoto` endpoint with an `chhoto` configuration section
 * ADDED: Added `shortenviachhoto` endpoint with an `chhoto` configuration section

+ 46 - 10
js/privatebin.js

@@ -3807,7 +3807,9 @@ window.PrivateBin = (function () {
             sendButton,
             sendButton,
             retryButton,
             retryButton,
             pasteExpiration = null,
             pasteExpiration = null,
-            retryButtonCallback;
+            retryButtonCallback,
+            emailLinkListeners = null,
+            emailConfirmListeners = null;
 
 
         /**
         /**
          * set the expiration on bootstrap templates in dropdown
          * set the expiration on bootstrap templates in dropdown
@@ -4189,6 +4191,35 @@ window.PrivateBin = (function () {
             );
             );
         }
         }
 
 
+        /**
+         * removes the event listeners of the e-mail confirmation dialog
+         *
+         * @name   TopNav.removeEmailConfirmListeners
+         * @private
+         * @function
+         */
+        function removeEmailConfirmListeners() {
+            if (emailConfirmListeners) {
+                emailConfirmListeners.abort();
+                emailConfirmListeners = null;
+            }
+        }
+
+        /**
+         * removes the event listeners of the e-mail button and its dialog
+         *
+         * @name   TopNav.removeEmailListeners
+         * @private
+         * @function
+         */
+        function removeEmailListeners() {
+            removeEmailConfirmListeners();
+            if (emailLinkListeners) {
+                emailLinkListeners.abort();
+                emailLinkListeners = null;
+            }
+        }
+
         /**
         /**
          * Send Email with current document (URL).
          * Send Email with current document (URL).
          *
          *
@@ -4225,23 +4256,26 @@ window.PrivateBin = (function () {
                             [], localeConfiguration
                             [], localeConfiguration
                         ), isBurnafterreading
                         ), isBurnafterreading
                     );
                     );
+                    // only send once per opening of the dialog
+                    removeEmailConfirmListeners();
                     if (bootstrap5EmailConfirmModal) {
                     if (bootstrap5EmailConfirmModal) {
                         bootstrap5EmailConfirmModal.hide();
                         bootstrap5EmailConfirmModal.hide();
                     }
                     }
                     triggerEmailSend(emailSubject, emailBody);
                     triggerEmailSend(emailSubject, emailBody);
                 }
                 }
 
 
+                // replace the listeners of a previous opening of the dialog
+                removeEmailConfirmListeners();
+                emailConfirmListeners = new window.AbortController();
+                const signal = emailConfirmListeners.signal;
                 emailconfirmmodal.addEventListener('shown.bs.modal', () => {
                 emailconfirmmodal.addEventListener('shown.bs.modal', () => {
                     emailconfirmTimezoneUtc.focus();
                     emailconfirmTimezoneUtc.focus();
-                });
-
-                emailconfirmTimezoneCurrent.removeEventListener('click', sendEmailAndHideModal);
-                emailconfirmTimezoneCurrent.addEventListener('click', sendEmailAndHideModal);
-                emailconfirmTimezoneUtc.removeEventListener('click', sendEmailAndHideModal);
+                }, {signal});
+                emailconfirmTimezoneCurrent.addEventListener('click', sendEmailAndHideModal, {signal});
                 emailconfirmTimezoneUtc.addEventListener('click', () => {
                 emailconfirmTimezoneUtc.addEventListener('click', () => {
                     localeConfiguration.timeZone = 'UTC';
                     localeConfiguration.timeZone = 'UTC';
                     sendEmailAndHideModal();
                     sendEmailAndHideModal();
-                });
+                }, {signal});
                 if (bootstrap5EmailConfirmModal) {
                 if (bootstrap5EmailConfirmModal) {
                     bootstrap5EmailConfirmModal.show();
                     bootstrap5EmailConfirmModal.show();
                 }
                 }
@@ -4425,10 +4459,12 @@ window.PrivateBin = (function () {
                     optionalBurnAfterReading : TopNav.getBurnAfterReading();
                     optionalBurnAfterReading : TopNav.getBurnAfterReading();
 
 
                 emailLink.classList.remove('hidden');
                 emailLink.classList.remove('hidden');
-                emailLink.removeEventListener('click', sendEmail);
+                // replace the listeners of a previous document
+                removeEmailListeners();
+                emailLinkListeners = new window.AbortController();
                 emailLink.addEventListener('click', () => {
                 emailLink.addEventListener('click', () => {
                     sendEmail(expirationDate, isBurnafterreading);
                     sendEmail(expirationDate, isBurnafterreading);
-                });
+                }, {signal: emailLinkListeners.signal});
             } catch (error) {
             } catch (error) {
                 console.error(error);
                 console.error(error);
                 Alert.showError('Cannot calculate expiration date.');
                 Alert.showError('Cannot calculate expiration date.');
@@ -4447,7 +4483,7 @@ window.PrivateBin = (function () {
             }
             }
 
 
             emailLink.classList.add('hidden');
             emailLink.classList.add('hidden');
-            emailLink.removeEventListener('click', sendEmail);
+            removeEmailListeners();
         };
         };
 
 
         /**
         /**

+ 1 - 1
lib/Configuration.php

@@ -118,7 +118,7 @@ class Configuration
             'js/kjua-0.10.0.js'      => 'sha512-BYj4xggowR7QD150VLSTRlzH62YPfhpIM+b/1EUEr7RQpdWAGKulxWnOvjFx1FUlba4m6ihpNYuQab51H6XlYg==',
             'js/kjua-0.10.0.js'      => 'sha512-BYj4xggowR7QD150VLSTRlzH62YPfhpIM+b/1EUEr7RQpdWAGKulxWnOvjFx1FUlba4m6ihpNYuQab51H6XlYg==',
             'js/legacy.js'           => 'sha512-pRofxsrf5UItjiP22Dcjh3FAcBjF/n7h8U9/W5xqJk17U0N2U1oajhXypq/omo9jhwS1iVGOhWrRepoPeFns+w==',
             'js/legacy.js'           => 'sha512-pRofxsrf5UItjiP22Dcjh3FAcBjF/n7h8U9/W5xqJk17U0N2U1oajhXypq/omo9jhwS1iVGOhWrRepoPeFns+w==',
             'js/prettify.js'         => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
             'js/prettify.js'         => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
-            'js/privatebin.js'       => 'sha512-jXV3mKXtezHb3zaJ/Ijhj4EVT9IoB/RQGhrVxFE6ID8zAZIE3otvIL3Nf+L7AR78xNNCTm0jLJSvHeNPVxoJIw==',
+            'js/privatebin.js'       => 'sha512-wvS9Ti+SlpUUqWz39b95043hI1yvEPhJAm5EXnJw13Oy+52KGmB66aD8ICNwVIngbmKnq1WnQW8hjJMYj3Gs2A==',
             'js/purify-3.4.17.js'    => 'sha512-udF2zPdEyVCAxIGeBbeSGFK1Fj5c4eHGi6NqOSsr8M75lGs39POzgUNxO3SFxOg4cHfAoZJgR2EaXdmDi6v6AQ==',
             'js/purify-3.4.17.js'    => 'sha512-udF2zPdEyVCAxIGeBbeSGFK1Fj5c4eHGi6NqOSsr8M75lGs39POzgUNxO3SFxOg4cHfAoZJgR2EaXdmDi6v6AQ==',
             'js/showdown-2.1.0.js'   => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
             'js/showdown-2.1.0.js'   => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
             'js/zlib-1.3.2.js'       => 'sha512-RAhJgxg9siMIA8ky4c10Rc2zUgnK80olHB8Tt1IOYWY4Eh1WmrviQkDn+sgBlb38ZHq3tzufGC41kP360gmosQ==',
             'js/zlib-1.3.2.js'       => 'sha512-RAhJgxg9siMIA8ky4c10Rc2zUgnK80olHB8Tt1IOYWY4Eh1WmrviQkDn+sgBlb38ZHq3tzufGC41kP360gmosQ==',