diff --git a/app/scripts/comp/extension/browser-extension-connector.js b/app/scripts/comp/extension/browser-extension-connector.js index b869e0b0..fc592b24 100644 --- a/app/scripts/comp/extension/browser-extension-connector.js +++ b/app/scripts/comp/extension/browser-extension-connector.js @@ -7,7 +7,7 @@ import { Features } from 'util/features'; const WebConnectionInfo = { connectionId: 1, - extensionName: 'keeweb-connect', + extensionName: 'KeeWeb Connect', supportsNotifications: true }; diff --git a/app/scripts/comp/extension/protocol-impl.js b/app/scripts/comp/extension/protocol-impl.js index 9a7a7175..ad21b861 100644 --- a/app/scripts/comp/extension/protocol-impl.js +++ b/app/scripts/comp/extension/protocol-impl.js @@ -8,6 +8,8 @@ import { Alerts } from 'comp/ui/alerts'; import { Locale } from 'util/locale'; import { RuntimeInfo } from 'const/runtime-info'; import { KnownAppVersions } from 'const/known-app-versions'; +import { ExtensionConnectView } from 'views/extension/extension-connect-view'; +import { RuntimeDataModel } from 'models/runtime-data-model'; const KeeWebAssociationId = 'KeeWeb'; const KeeWebHash = '398d9c782ec76ae9e9877c2321cbda2b31fc6d18ccf0fed5ca4bd746bab4d64a'; // sha256('KeeWeb') @@ -136,7 +138,7 @@ function checkContentRequestPermissions(request) { ensureAtLeastOneFileIsOpen(); const client = getClient(request); - if (client.authorized) { + if (client.permissions) { return; } @@ -147,31 +149,41 @@ function checkContentRequestPermissions(request) { focusKeeWeb(); - // TODO: make a proper dialog here instead of a simple question - - if (Launcher) { - Alerts.yesno({ - header: 'Extension connection', - body: 'Allow this extension to connect?', - success: () => { - resolve(); - }, - cancel: () => reject(makeError(Errors.userRejected)) - }); - } else { - // it's 'confirm' here because other browser extensions can't interact with browser alerts - // while they can easily press a button on our alert - // eslint-disable-next-line no-alert - const allowed = confirm('Allow this extension to connect?'); - if (allowed) { - resolve(); - } else { - reject(makeError(Errors.userRejected)); + const config = RuntimeDataModel.extensionConnectConfig; + const files = appModel.files.map((f) => ({ + id: f.id, + name: f.name, + checked: !config || config.allFiles || config.files.includes(f.id) + })); + if (!files.some((f) => f.checked)) { + for (const f of files) { + f.checked = true; } } + + const extensionConnectView = new ExtensionConnectView({ + extensionName: `${client.connection.extensionName} (${client.connection.appName})`, + identityVerified: !Launcher, + files, + allFiles: config?.allFiles ?? true, + askGet: config?.askGet || 'multiple' + }); + Alerts.alert({ + header: Locale.extensionConnectHeader, + icon: 'exchange-alt', + view: extensionConnectView, + wide: true, + opaque: true, + buttons: [Alerts.buttons.allow, Alerts.buttons.deny], + success: () => { + RuntimeDataModel.extensionConnectConfig = extensionConnectView.config; + client.permissions = extensionConnectView.config; + resolve(); + }, + cancel: () => reject(makeError(Errors.userRejected)) + }); }) .then(() => { - client.authorized = true; Launcher.hideApp(); }) .catch((e) => { @@ -186,7 +198,7 @@ function getVersion(request) { } function isKeeWebConnect(request) { - return getClient(request).connection.extensionName === 'keeweb-connect'; + return getClient(request).connection.extensionName === 'KeeWeb Connect'; } function focusKeeWeb() { diff --git a/app/scripts/comp/ui/alerts.js b/app/scripts/comp/ui/alerts.js index 2f8c8b69..2443ad27 100644 --- a/app/scripts/comp/ui/alerts.js +++ b/app/scripts/comp/ui/alerts.js @@ -17,6 +17,12 @@ const Alerts = { return Locale.alertYes; } }, + allow: { + result: 'yes', + get title() { + return Locale.alertAllow; + } + }, no: { result: '', get title() { @@ -28,6 +34,12 @@ const Alerts = { get title() { return Locale.alertCancel; } + }, + deny: { + result: '', + get title() { + return Locale.alertDeny; + } } }, diff --git a/app/scripts/locales/base.json b/app/scripts/locales/base.json index 6811b480..7cb327a7 100644 --- a/app/scripts/locales/base.json +++ b/app/scripts/locales/base.json @@ -86,6 +86,8 @@ "alertCopy": "Copy", "alertClose": "Close", "alertDoNotAsk": "Don't ask me anymore", + "alertAllow": "Allow", + "alertDeny": "Deny", "appBeta": "WARNING: beta version, only for preview", @@ -755,5 +757,14 @@ "extensionErrorNoOpenFiles": "No open files", "extensionErrorUserRejected": "The request was denied", - "extensionErrorAlertDisplayed": "Cannot ask user a question now, please try again" + "extensionErrorAlertDisplayed": "Cannot ask user a question now, please try again", + "extensionConnectHeader": "Extension data exchange", + "extensionConnectIntro": "A browser extension that identifies itself as {} tries to exchange data with KeeWeb.", + "extensionConnectUnknownActivity": "KeeWeb doesn't verify that the connected application is what it pretends to be. Approve the request only if you recognize this activity.", + "extensionConnectFiles": "In this session, allow access to files:", + "extensionConnectAllFiles": "All other files", + "extensionConnectAskGet": "Ask before returning passwords to the extension:", + "extensionConnectAskGetMultiple": "if there's more than one match", + "extensionConnectAskGetAlways": "always", + "extensionConnectSettingsAreForSession": "Settings you select here are valid only for the active session. You can view and manage sessions in KeeWeb settings." } diff --git a/app/scripts/views/extension/extension-connect-view.js b/app/scripts/views/extension/extension-connect-view.js new file mode 100644 index 00000000..3a9bf39b --- /dev/null +++ b/app/scripts/views/extension/extension-connect-view.js @@ -0,0 +1,62 @@ +import { View } from 'framework/views/view'; +import template from 'templates/extension/extension-connect.hbs'; + +class ExtensionConnectView extends View { + template = template; + + events = { + 'change #extension-connect__ask-get': 'askGetChanged', + 'change .extension-connect__file-check': 'fileChecked' + }; + + constructor(model) { + super(model); + this.config = { + askGet: this.model.askGet, + allFiles: this.model.allFiles, + files: this.model.files.filter((f) => f.checked).map((f) => f.id) + }; + } + + render() { + super.render({ + ...this.model, + ...this.config, + files: this.model.files.map((f) => ({ + id: f.id, + name: f.name, + checked: this.config.files.includes(f.id) + })) + }); + } + + fileChecked(e) { + const fileId = e.target.dataset.file; + const checked = e.target.checked; + + if (fileId === 'all') { + this.config.allFiles = checked; + this.config.files = this.model.files.map((f) => f.id); + } else { + if (checked) { + this.config.files.push(fileId); + } else { + this.config.files = this.config.files.filter((f) => f !== fileId); + this.config.allFiles = false; + } + } + + this.render(); + + const atLeastOneFileSelected = this.config.files.length > 0 || this.config.allFiles; + + const allowButton = document.querySelector('.modal button[data-result=yes]'); + allowButton.classList.toggle('hide', !atLeastOneFileSelected); + } + + askGetChanged(e) { + this.config.askGet = e.target.value; + } +} + +export { ExtensionConnectView }; diff --git a/app/styles/areas/_extension.scss b/app/styles/areas/_extension.scss new file mode 100644 index 00000000..596724cf --- /dev/null +++ b/app/styles/areas/_extension.scss @@ -0,0 +1,10 @@ +.extension-connect { + &__files { + display: flex; + flex-wrap: wrap; + } + + &__file { + margin: $base-padding; + } +} diff --git a/app/styles/base/_forms.scss b/app/styles/base/_forms.scss index b31c30df..e3d88349 100644 --- a/app/styles/base/_forms.scss +++ b/app/styles/base/_forms.scss @@ -284,3 +284,7 @@ input[type='range'] { .input-base { @extend .input-size-base; } + +select.input-base { + height: 2em; +} diff --git a/app/styles/base/_icon-font.scss b/app/styles/base/_icon-font.scss index cf5a65a1..081b1c74 100644 --- a/app/styles/base/_icon-font.scss +++ b/app/styles/base/_icon-font.scss @@ -202,3 +202,4 @@ $fa-var-titlebar-minimize: next-fa-glyph(); $fa-var-titlebar-restore: next-fa-glyph(); $fa-var-window-maximize: next-fa-glyph(); $fa-var-download: next-fa-glyph(); +$fa-var-exchange-alt: next-fa-glyph(); diff --git a/app/styles/common/_modal.scss b/app/styles/common/_modal.scss index 20942dee..f9888d4e 100644 --- a/app/styles/common/_modal.scss +++ b/app/styles/common/_modal.scss @@ -51,6 +51,9 @@ &__buttons { align-self: center; width: 40%; + .modal--wide & { + width: 80%; + } @include tablet { width: 90%; } diff --git a/app/styles/main.scss b/app/styles/main.scss index a94e06a6..e65e9f53 100644 --- a/app/styles/main.scss +++ b/app/styles/main.scss @@ -29,6 +29,7 @@ $fa-font-path: '~font-awesome/fonts'; @import 'areas/app'; @import 'areas/auto-type'; @import 'areas/details'; +@import 'areas/extension'; @import 'areas/footer'; @import 'areas/grp'; @import 'areas/tag'; diff --git a/app/templates/extension/extension-connect.hbs b/app/templates/extension/extension-connect.hbs new file mode 100644 index 00000000..a0b1c4cd --- /dev/null +++ b/app/templates/extension/extension-connect.hbs @@ -0,0 +1,39 @@ +
+

{{#res 'extensionConnectIntro'}}{{extensionName}}{{/res}}

+ {{#unless identityVerified}} +

{{res 'extensionConnectUnknownActivity'}}

+ {{/unless}} +
+
+ {{#each files as |file|}} +
+ + +
+ {{/each}} +
+ + +
+
+
+
+ +
+

{{res 'extensionConnectSettingsAreForSession'}}

+
diff --git a/app/templates/modal.hbs b/app/templates/modal.hbs index 64c36016..7d22715f 100644 --- a/app/templates/modal.hbs +++ b/app/templates/modal.hbs @@ -1,4 +1,4 @@ -