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

Merge branch 'master' into email-handlers

El RIDO пре 6 дана
родитељ
комит
7df3b434ad
6 измењених фајлова са 151 додато и 14 уклоњено
  1. 1 1
      .github/workflows/test-results.yml
  2. 2 1
      CHANGELOG.md
  3. 6 6
      js/package-lock.json
  4. 1 1
      js/package.json
  5. 3 5
      js/privatebin.js
  6. 138 0
      js/test/PasswordPeek.js

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

@@ -25,7 +25,7 @@ jobs:
 
     steps:
        - name: Download and Extract Artifacts
-         uses: dawidd6/action-download-artifact@eab87c9830c39eff17e5a6eadb20bfb4bc880477
+         uses: dawidd6/action-download-artifact@a92c7ae74cf1df8810783758581e485ac3aaa309
          with:
           run_id: ${{ github.event.workflow_run.id }}
           path: artifacts

+ 2 - 1
CHANGELOG.md

@@ -10,7 +10,8 @@
 * 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: 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
+* FIXED: Password peek button next to the password input when creating a document had no effect
+* FIXED: E-mail button sent several e-mails after being shown again or after its confirmation dialog was dismissed
 
 ## 2.1.0 (2026-10-18)
 * ADDED: Added `shortenviachhoto` endpoint with an `chhoto` configuration section

+ 6 - 6
js/package-lock.json

@@ -11,7 +11,7 @@
       "devDependencies": {
         "@eslint/js": "^10.0.1",
         "@peculiar/webcrypto": "^2.0.0",
-        "eslint": "^10.13.0",
+        "eslint": "^10.14.0",
         "fast-check": "^4.10.2",
         "globals": "^17.13.0",
         "jsdom": "^30.1.3",
@@ -1585,9 +1585,9 @@
       }
     },
     "node_modules/eslint": {
-      "version": "10.13.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.13.0.tgz",
-      "integrity": "sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==",
+      "version": "10.14.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.14.0.tgz",
+      "integrity": "sha512-T0pNsxkb9IKmtQjhpK3d7nWOpFv+YHuMbTJnSVMoQblF0onvXfErrtp4V1jHtsQPan7dKaGVD0q1WCXnzMF31A==",
       "dev": true,
       "license": "MIT",
       "workspaces": [
@@ -1600,7 +1600,7 @@
         "@eslint/config-helpers": "^0.7.0",
         "@eslint/core": "^1.2.1",
         "@eslint/plugin-kit": "^0.7.3",
-        "@humanfs/node": "^0.16.6",
+        "@humanfs/node": "^0.16.8",
         "@humanwhocodes/module-importer": "^1.0.1",
         "@humanwhocodes/retry": "^0.4.2",
         "@types/estree": "^1.0.6",
@@ -1621,7 +1621,7 @@
         "imurmurhash": "^0.1.4",
         "is-glob": "^4.0.0",
         "json-stable-stringify-without-jsonify": "^1.0.1",
-        "minimatch": "^10.2.5",
+        "minimatch": "^10.2.6",
         "natural-compare": "^1.4.0",
         "optionator": "^0.9.3"
       },

+ 1 - 1
js/package.json

@@ -9,7 +9,7 @@
   "devDependencies": {
     "@eslint/js": "^10.0.1",
     "@peculiar/webcrypto": "^2.0.0",
-    "eslint": "^10.13.0",
+    "eslint": "^10.14.0",
     "fast-check": "^4.10.2",
     "globals": "^17.13.0",
     "jsdom": "^30.1.3",

+ 3 - 5
js/privatebin.js

@@ -5925,11 +5925,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);
+    });
+});