Selaa lähdekoodia

Merge branch 'master' into uint8array-encoding

El RIDO 6 päivää sitten
vanhempi
sitoutus
6e22bb33e0
4 muutettua tiedostoa jossa 143 lisäystä ja 6 poistoa
  1. 1 0
      CHANGELOG.md
  2. 3 5
      js/privatebin.js
  3. 138 0
      js/test/PasswordPeek.js
  4. 1 1
      lib/Configuration.php

+ 1 - 0
CHANGELOG.md

@@ -10,6 +10,7 @@
 * 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: Comments must now be unique in cipher text within a document, not reuse existing cipher texts (#2100)
+* FIXED: Password peek button next to the password input when creating a document had no effect
 * FIXED: SVG attachment previews could turn non-ASCII characters into markup after sanitization, enabling script execution when opening the preview in a new tab
 * FIXED: SVG attachment previews use a data URL, so they no longer run in the instance's origin when opened in a new tab
 * FIXED: Password characters above U+00FF got truncated before key derivation, now passwords get UTF-8 encoded and existing documents remain readable

+ 3 - 5
js/privatebin.js

@@ -5910,11 +5910,9 @@ window.PrivateBin = (function () {
          * @function
          */
         me.init = function () {
-            const revealButton = document.querySelector('.toggle-password');
-            if (!revealButton) {
-                return;
-            }
-            revealButton.addEventListener('click', handleRevealButtonClick);
+            document.querySelectorAll('.toggle-password').forEach(function (revealButton) {
+                revealButton.addEventListener('click', handleRevealButtonClick);
+            });
         };
 
         return me;

+ 138 - 0
js/test/PasswordPeek.js

@@ -0,0 +1,138 @@
+'use strict';
+require('../common');
+const fc = require('fast-check');
+
+describe('PasswordPeek', function () {
+    const iconHref = 'img/bootstrap-icons.svg#eye';
+
+    /**
+     * input group with a password field and its reveal button, as in the
+     * bootstrap5 template
+     *
+     * @param {string} id
+     * @returns {string}
+     */
+    function passwordGroup(id) {
+        return (
+            '<div class="input-group">' +
+            '<input id="' + id + '" type="password" class="form-control input-password" />' +
+            '<button class="btn btn-outline-secondary toggle-password" type="button" ' +
+            'title="Show password" aria-label="Show password">' +
+            '<svg width="16" height="16" fill="currentColor" aria-hidden="true">' +
+            '<use href="' + iconHref + '" /></svg>' +
+            '</button></div>'
+        );
+    }
+
+    /**
+     * asserts that the input and its reveal button are in the expected state
+     *
+     * @param {string} id
+     * @param {boolean} isRevealed
+     */
+    function assertState(id, isRevealed) {
+        const input = document.getElementById(id);
+        const button = input.closest('.input-group').querySelector('.toggle-password');
+        const label = isRevealed ? 'Hide password' : 'Show password';
+        assert.strictEqual(input.getAttribute('type'), isRevealed ? 'text' : 'password', id + ' type');
+        assert.strictEqual(button.getAttribute('title'), label, id + ' title');
+        assert.strictEqual(button.getAttribute('aria-label'), label, id + ' aria-label');
+        assert.strictEqual(
+            button.querySelector('use').getAttribute('href'),
+            isRevealed ? iconHref + '-slash' : iconHref,
+            id + ' icon'
+        );
+    }
+
+    /**
+     * clicks the reveal button belonging to the given input
+     *
+     * @param {string} id
+     */
+    function clickButton(id) {
+        document.getElementById(id).closest('.input-group')
+            .querySelector('.toggle-password').click();
+    }
+
+    beforeEach(function () {
+        globalThis.cleanup();
+        // same order as in the template: the decryption prompt comes before
+        // the password input of the create form
+        document.body.innerHTML = (
+            '<div id="passwordmodal"><form id="passwordform">' +
+            passwordGroup('passworddecrypt') +
+            '</form></div>' +
+            '<nav><div id="password">' +
+            passwordGroup('passwordinput') +
+            '</div></nav>'
+        );
+        PrivateBin.PasswordPeek.init();
+    });
+
+    after(function () {
+        globalThis.cleanup();
+    });
+
+    it('toggles the password prompt when opening a document', function () {
+        clickButton('passworddecrypt');
+        assertState('passworddecrypt', true);
+        assertState('passwordinput', false);
+        clickButton('passworddecrypt');
+        assertState('passworddecrypt', false);
+        assertState('passwordinput', false);
+    });
+
+    it('toggles the password input when creating a document', function () {
+        clickButton('passwordinput');
+        assertState('passwordinput', true);
+        assertState('passworddecrypt', false);
+        clickButton('passwordinput');
+        assertState('passwordinput', false);
+        assertState('passworddecrypt', false);
+    });
+
+    it('toggles each input independently, in any click order', function () {
+        fc.assert(fc.property(
+            fc.integer({min: 1, max: 5}).chain(function (count) {
+                return fc.tuple(
+                    fc.constant(count),
+                    fc.array(fc.nat({max: count - 1}), {maxLength: 20})
+                );
+            }),
+            function ([count, clicks]) {
+                globalThis.cleanup();
+                const ids = [];
+                for (let i = 0; i < count; ++i) {
+                    ids.push('password' + i);
+                }
+                document.body.innerHTML = ids.map(passwordGroup).join('');
+                PrivateBin.PasswordPeek.init();
+
+                const revealed = ids.map(() => false);
+                for (const index of clicks) {
+                    clickButton(ids[index]);
+                    revealed[index] = !revealed[index];
+                }
+                ids.forEach(function (id, index) {
+                    assertState(id, revealed[index]);
+                });
+            }
+        ), {numRuns: 50});
+    });
+
+    it('ignores a reveal button without a password input', function () {
+        globalThis.cleanup();
+        document.body.innerHTML = (
+            '<div class="input-group">' +
+            '<button class="toggle-password" type="button" title="Show password">' +
+            '<svg><use href="' + iconHref + '" /></svg></button></div>' +
+            '<button class="toggle-password" id="orphan" type="button" title="Show password"></button>'
+        );
+        PrivateBin.PasswordPeek.init();
+        document.querySelectorAll('.toggle-password').forEach(button => button.click());
+        document.querySelectorAll('.toggle-password').forEach(function (button) {
+            assert.strictEqual(button.getAttribute('title'), 'Show password');
+        });
+        assert.strictEqual(document.querySelector('use').getAttribute('href'), iconHref);
+    });
+});

+ 1 - 1
lib/Configuration.php

@@ -119,7 +119,7 @@ class Configuration
             'js/kjua-0.10.0.js'      => 'sha512-BYj4xggowR7QD150VLSTRlzH62YPfhpIM+b/1EUEr7RQpdWAGKulxWnOvjFx1FUlba4m6ihpNYuQab51H6XlYg==',
             'js/legacy.js'           => 'sha512-pRofxsrf5UItjiP22Dcjh3FAcBjF/n7h8U9/W5xqJk17U0N2U1oajhXypq/omo9jhwS1iVGOhWrRepoPeFns+w==',
             'js/prettify.js'         => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
-            'js/privatebin.js'       => 'sha512-n1Os9VuZo6xnlK/4cWYtXrQEJL6DQTxVhL/US2k6xCf+qBIruERWAjlDYshYyHF5Lnfrrnz2PE4iuwiT483hZg==',
+            'js/privatebin.js'       => 'sha512-sRSfrejpIlEcne9lEvuqMRegrKCk17Kom6+KvYl124KHrTkpkEUDROLcRjQluRyJteluwlho0NJP6n/FNnLS9Q==',
             'js/purify-3.4.17.js'    => 'sha512-udF2zPdEyVCAxIGeBbeSGFK1Fj5c4eHGi6NqOSsr8M75lGs39POzgUNxO3SFxOg4cHfAoZJgR2EaXdmDi6v6AQ==',
             'js/showdown-2.1.0.js'   => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
             'js/zlib-1.3.2.js'       => 'sha512-RAhJgxg9siMIA8ky4c10Rc2zUgnK80olHB8Tt1IOYWY4Eh1WmrviQkDn+sgBlb38ZHq3tzufGC41kP360gmosQ==',