diff --git a/app/scripts/comp/extension/protocol-impl.js b/app/scripts/comp/extension/protocol-impl.js index 8a38e6f1..3c651d40 100644 --- a/app/scripts/comp/extension/protocol-impl.js +++ b/app/scripts/comp/extension/protocol-impl.js @@ -9,6 +9,7 @@ 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 { ExtensionCreateGroupView } from 'views/extension/extension-create-group-view'; import { RuntimeDataModel } from 'models/runtime-data-model'; import { AppSettingsModel } from 'models/app-settings-model'; import { Timeouts } from 'const/timeouts'; @@ -146,7 +147,6 @@ async function checkContentRequestPermissions(request) { Timeouts.KeeWebConnectRequest ); } catch { - Launcher?.hideApp(); throw makeError(Errors.noOpenFiles); } } else { @@ -159,57 +159,68 @@ async function checkContentRequestPermissions(request) { return; } - return new Promise((resolve, reject) => { - if (Alerts.alertDisplayed) { - return reject(new Error(Locale.extensionErrorAlertDisplayed)); + if (Alerts.alertDisplayed) { + throw new Error(Locale.extensionErrorAlertDisplayed); + } + + focusKeeWeb(); + + 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; } + } - focusKeeWeb(); + const extensionName = client.connection.appName + ? `${client.connection.extensionName} (${client.connection.appName})` + : client.connection.extensionName; - 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, + identityVerified: !Launcher, + files, + allFiles: config?.allFiles ?? true, + askGet: config?.askGet || 'multiple' + }); - const extensionName = client.connection.appName - ? `${client.connection.extensionName} (${client.connection.appName})` - : client.connection.extensionName; - - const extensionConnectView = new ExtensionConnectView({ - extensionName, - identityVerified: !Launcher, - files, - allFiles: config?.allFiles ?? true, - askGet: config?.askGet || 'multiple' + try { + await alertWithTimeout({ + header: Locale.extensionConnectHeader, + icon: 'exchange-alt', + buttons: [Alerts.buttons.allow, Alerts.buttons.deny], + view: extensionConnectView, + wide: true, + opaque: true }); + } catch (e) { + client.permissionsDenied = true; + Events.emit('browser-extension-sessions-changed'); + throw e; + } + RuntimeDataModel.extensionConnectConfig = extensionConnectView.config; + client.permissions = extensionConnectView.config; + Events.emit('browser-extension-sessions-changed'); +} + +function alertWithTimeout(config) { + return new Promise((resolve, reject) => { let inactivityTimer = 0; const alert = Alerts.alert({ - header: Locale.extensionConnectHeader, - icon: 'exchange-alt', - view: extensionConnectView, - wide: true, - opaque: true, - buttons: [Alerts.buttons.allow, Alerts.buttons.deny], - success: () => { + ...config, + success: (res) => { clearTimeout(inactivityTimer); - RuntimeDataModel.extensionConnectConfig = extensionConnectView.config; - client.permissions = extensionConnectView.config; - Events.emit('browser-extension-sessions-changed'); - resolve(); + resolve(res); }, cancel: () => { - client.permissionsDenied = true; clearTimeout(inactivityTimer); - Events.emit('browser-extension-sessions-changed'); reject(makeError(Errors.userRejected)); } }); @@ -217,9 +228,6 @@ async function checkContentRequestPermissions(request) { inactivityTimer = setTimeout(() => { alert.closeWithResult(''); }, Timeouts.KeeWebConnectRequest); - }).catch((e) => { - Launcher?.hideApp(); - throw e; }); } @@ -461,15 +469,39 @@ const ProtocolHandlers = { } } - // TODO: create a new group - throw new Error('Not implemented'); + const createGroupView = new ExtensionCreateGroupView({ + groupPath: groupNames.join(' / '), + files: files.map((f, ix) => ({ id: f.id, name: f.name, selected: ix === 0 })) + }); - // return encryptResponse(request, { - // success: 'true', - // version: getVersion(request), - // name: newGroup.title, - // uuid: kdbxweb.ByteUtils.bytesToHex(newGroup.group.uuid.bytes) - // }); + await alertWithTimeout({ + header: Locale.extensionNewGroupHeader, + icon: 'folder', + buttons: [Alerts.buttons.allow, Alerts.buttons.deny], + view: createGroupView + }); + + const selectedFile = files.find((f) => f.id === createGroupView.selectedFile); + + let newGroup = selectedFile.groups[0]; + const pendingGroups = [...groupNames]; + + while (pendingGroups.length) { + const title = pendingGroups.shift(); + const item = newGroup.items.find((g) => g.title === title); + if (item) { + newGroup = item; + } else { + newGroup = appModel.createNewGroupWithName(newGroup, selectedFile, title); + } + } + + return encryptResponse(request, { + success: 'true', + version: getVersion(request), + name: newGroup.title, + uuid: kdbxweb.ByteUtils.bytesToHex(newGroup.group.uuid.bytes) + }); } }; @@ -521,18 +553,27 @@ const ProtocolImpl = { }, async handleRequest(request, connectionInfo) { + const appWindowWasFocused = Launcher?.isAppFocused(); + + let result; try { const handler = ProtocolHandlers[request.action]; if (!handler) { throw new Error(`Handler not found: ${request.action}`); } - return await handler(request, connectionInfo); + result = await handler(request, connectionInfo); } catch (e) { if (!e.code) { logger.error(`Error in handler ${request.action}`, e); } - return this.errorToResponse(e, request); + result = this.errorToResponse(e, request); } + + if (!appWindowWasFocused && Launcher?.isAppFocused()) { + Launcher.hideApp(); + } + + return result; }, get sessions() { diff --git a/app/scripts/locales/base.json b/app/scripts/locales/base.json index 017e39d9..90a73966 100644 --- a/app/scripts/locales/base.json +++ b/app/scripts/locales/base.json @@ -774,7 +774,7 @@ "extensionErrorNoOpenFiles": "No open files", "extensionErrorUserRejected": "The request was denied", - "extensionErrorAlertDisplayed": "Cannot ask user a question now, please try again", + "extensionErrorAlertDisplayed": "Cannot ask a question now because there's another alert displayed, 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.", @@ -789,5 +789,8 @@ "extensionConnectAskSaveAuto": "when it's not possible to save automatically", "extensionConnectSettingsAreForSession": "Settings you select here are valid only for the active session. You can view and manage sessions in KeeWeb settings.", "extensionUnlockMessage": "Unlock to connect a browser extension", - "extensionNewGroup": "" + "extensionNewGroupHeader": "Create a new group?", + "extensionNewGroupBody": "A browser extension is trying to create a new group. Allow this?", + "extensionNewGroupPath": "Group path", + "extensionNewGroupFile": "This group will be created in:" } diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index 94be14c4..f387a7dc 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -471,6 +471,12 @@ class AppModel { return GroupModel.newGroup(sel.group, sel.file); } + createNewGroupWithName(group, file, name) { + const newGroup = GroupModel.newGroup(group, file); + newGroup.setName(name); + return newGroup; + } + createNewTemplateEntry() { const file = this.getFirstSelectedGroupForCreation().file; const group = file.getEntryTemplatesGroup() || file.createEntryTemplatesGroup(); diff --git a/app/scripts/views/extension/extension-create-group-view.js b/app/scripts/views/extension/extension-create-group-view.js new file mode 100644 index 00000000..e84e3112 --- /dev/null +++ b/app/scripts/views/extension/extension-create-group-view.js @@ -0,0 +1,26 @@ +import { View } from 'framework/views/view'; +import template from 'templates/extension/extension-create-group.hbs'; + +class ExtensionCreateGroupView extends View { + template = template; + + events = { + 'change #extension-create-group__file': 'fileChanged' + }; + + constructor(model) { + super(model); + + this.selectedFile = model.files.find((f) => f.selected).id; + } + + render() { + super.render(this.model); + } + + fileChanged(e) { + this.selectedFile = e.target.value; + } +} + +export { ExtensionCreateGroupView }; diff --git a/app/scripts/views/modal-view.js b/app/scripts/views/modal-view.js index 05b9c204..ea9d443e 100644 --- a/app/scripts/views/modal-view.js +++ b/app/scripts/views/modal-view.js @@ -41,7 +41,7 @@ class ModalView extends View { document.activeElement.blur(); }, 20); if (this.model.view) { - this.model.view.parent = '.modal__body'; + this.model.view.parent = this.el.querySelector('.modal__body'); this.model.view.render(); } } diff --git a/app/templates/extension/extension-create-group.hbs b/app/templates/extension/extension-create-group.hbs new file mode 100644 index 00000000..45a97c2f --- /dev/null +++ b/app/templates/extension/extension-create-group.hbs @@ -0,0 +1,14 @@ +
+

{{res 'extensionNewGroupBody'}}

+

{{res 'extensionNewGroupPath'}}: {{groupPath}}

+
+ + +
+