diff --git a/containers/libreportal/frontend/core/setup/js/setup-wizard.js b/containers/libreportal/frontend/core/setup/js/setup-wizard.js index 027ef32..a5533d4 100755 --- a/containers/libreportal/frontend/core/setup/js/setup-wizard.js +++ b/containers/libreportal/frontend/core/setup/js/setup-wizard.js @@ -452,8 +452,8 @@ class SetupWizard {
@@ -1232,8 +1232,10 @@ class SetupWizard { const box = this.container.querySelector('#sw-rs-verify-result'); if (!box) return; if (d && d.repo) { - box.innerHTML = this._backupCard(d, 'chosen') + - `

Enter its password below to see what is inside.

`; + // No "now enter the password" line: the step will not advance without + // one, and saying it as well is telling someone what a locked door is + // for while they are standing in front of it. + box.innerHTML = this._backupCard(d, 'chosen'); return; } // A suggestion means they pointed at the folder holding the repositories @@ -1367,6 +1369,22 @@ class SetupWizard { if (sel) sel.addEventListener('change', sync); const verify = this.container.querySelector('#sw-rs-verify'); if (verify) verify.addEventListener('click', () => this.verifyBackupPath()); + + // Changing where the backup is, or its password, invalidates whatever was + // read from the last one. Without this the wizard would happily carry the + // old repository's contents forward under the new path. + const stale = () => { + if (!this.restoreInfo) return; + this.restoreInfo = null; + const st = this.container.querySelector('#sw-rs-status'); + if (st) st.innerHTML = ''; + }; + box.querySelectorAll('input, select').forEach(el => { + el.addEventListener('input', stale); + el.addEventListener('change', stale); + }); + const pw = this.container.querySelector('#sw-rs-pass'); + if (pw) pw.addEventListener('input', stale); sync(); } @@ -1413,7 +1431,7 @@ class SetupWizard { return 'Give the repository URL.'; } if (!pass || !pass.value) { - return 'The repository password is needed to open the backup.'; + return 'Enter the backup password.'; } return ''; } @@ -2350,6 +2368,19 @@ class SetupWizard { } // Only an edited exact path can be invalid; the scanned options all came // from the backend. + // A restore cannot go past the Backup step on a promise: the next step + // renders what is IN the backup, which means it has to have been opened. + // Gated here rather than printed as an instruction — the step says what is + // wrong at the moment the user tries to move on, instead of in advance and + // forever. + if (name === 'Backup') { + const problem = this._restoreSourceProblem(); + if (problem) return problem; + if (!this.restoreInfo) { + return 'Select "Read this backup" first \u2014 we have to open it before we can show you what is inside.'; + } + } + if (name === 'Storage') { const problem = this._syncStorageNav(); if (problem) return problem; diff --git a/scripts/dev/lp-restore-wizard-test b/scripts/dev/lp-restore-wizard-test index 21644e4..c6847b8 100755 --- a/scripts/dev/lp-restore-wizard-test +++ b/scripts/dev/lp-restore-wizard-test @@ -192,6 +192,30 @@ read -r -d '' DRIVE <<'JS' ($('#sw-rs-verify-result') || {}).textContent || ''); out.offersTheRealPath = !!$('#sw-rs-usesuggest'); + // The step will not advance on a promise: the next one renders what is IN + // the backup, so it has to have been opened. Gated rather than instructed — + // the old copy told the user to enter a password beneath a field that had + // one, and kept telling them after they had. + const BACKUP = w.stepNames.indexOf('Backup'); + out.passwordLabel = ($('label[for="sw-rs-pass"]') || {}).textContent?.trim().split('\n')[0].trim(); + out.verdictHasNoInstruction = + !/enter its password/i.test(($('#sw-rs-verify-result') || {}).textContent || ''); + + w.restoreInfo = null; + $('#sw-rs-path').value = '/libreportal-backups/1'; + $('#sw-rs-pass').value = ''; + out.blockedWithoutPassword = !!w.validateStep(BACKUP); + $('#sw-rs-pass').value = 'x'; + out.blockedUntilRead = !!w.validateStep(BACKUP); + w.restoreInfo = { host: 'h', hosts: ['h'], system: { present: true, date: '', domains: [] }, apps: [] }; + out.allowedOnceRead = !w.validateStep(BACKUP); + + // Editing the source after a read makes that read stale — otherwise the + // wizard carries the old repository's contents forward under a new path. + $('#sw-rs-path').value = '/somewhere/else'; + $('#sw-rs-path').dispatchEvent(new Event('input', { bubbles: true })); + out.readInvalidatedByEdit = !w.restoreInfo; + // A password must leave as a reference and not linger in the DOM. Stubbed: // the real channel is covered by lp-secret-channel-test, and what matters // here is that readBackup routes through it at all rather than putting the @@ -334,6 +358,14 @@ chk "placeholder is a real path" "$(g .placeholderIsReal)" true chk "the parent-folder mistake is explained" "$(g .parentFolderExplained)" true chk "and the real path is offered" "$(g .offersTheRealPath)" true +echo "the step gates rather than instructs" +chk "the field is called Backup Password" "$(g .passwordLabel)" "Backup Password" +chk "no 'enter its password' copy" "$(g .verdictHasNoInstruction)" true +chk "blocked without a password" "$(g .blockedWithoutPassword)" true +chk "blocked until the backup is read" "$(g .blockedUntilRead)" true +chk "allowed once it has been" "$(g .allowedOnceRead)" true +chk "editing the source invalidates it" "$(g .readInvalidatedByEdit)" true + echo "the password" chk "goes through the secret channel" "$(g .passwordWasStashed)" true chk "leaves the payload as a ref" "$(g .payloadCarriesRef)" true