Compare commits

..
324 Commits
Author SHA1 Message Date
marc 43d5279acf package.json 2021-12-23 13:30:37 +01:00
Dimitri Witkowski e674607fce Merge pull request #1945 from dolfinus/develop
Install NativeMessagingHost for Chromium and Chrome dev/canary
2021-12-20 22:01:39 +01:00
dolfinus 2e6239f637 Install NativeMessagingHost for Chromium and Chrome dev/canary 2021-12-06 00:53:23 +03:00
antelle bcb8ddc17b fixed msteams html file name 2021-07-19 23:21:43 +02:00
antelle cd619582ec fixed variable usage 2021-07-13 19:05:25 +02:00
antelle 3b49562f64 added MS Teams apps 2021-07-13 19:04:27 +02:00
antelle 674ed2cc72 renames some ms teams constants 2021-07-11 16:22:38 +02:00
Dimitri Witkowski 5621fd8a06 Merge pull request #1856 from saulobejo/develop
added teams storage; added tenant id configuration for microsot
2021-07-11 16:18:53 +02:00
Saulo Alves af27e0b111 renamed teams to microsoft teams; fixed grunt prettier error in onedrive 2021-07-11 10:52:40 +02:00
Saulo Alves 5a78e12f7d added teams storage; added tenant id configuration for onedrive and teams 2021-07-11 00:56:51 +02:00
antelle 47b19d1d74 Merge branch 'master' into develop 2021-05-19 10:42:24 +02:00
antelle 070573e68a Merge branch 'release-1.18'
Build / web (push) Canceled after 0s
Build / linux (push) Canceled after 0s
Build / darwin (push) Canceled after 0s
Build / win32 (push) Canceled after 0s
Build / publish (push) Canceled after 0s
2021-05-19 10:35:35 +02:00
antelle 946d7769f1 bump version 2021-05-19 10:33:41 +02:00
antelle f0426a3ec2 fix #1820: minimize on close on macOS 2021-05-19 10:33:17 +02:00
antelle b2d67818a8 fix #1818: extension connection error if browser cannot be identified 2021-05-19 10:16:40 +02:00
antelle c0b159d824 fix #1824: saving KDBX3 files with compression disabled 2021-05-19 10:07:59 +02:00
antelle d7e11e20c1 Merge branch 'master' into develop 2021-05-14 21:18:46 +02:00
antelle d590318153 Merge branch 'release-1.18'
Build / web (push) Canceled after 0s
Build / linux (push) Canceled after 0s
Build / darwin (push) Canceled after 0s
Build / win32 (push) Canceled after 0s
Build / publish (push) Canceled after 0s
2021-05-14 21:02:12 +02:00
antelle 5816ca9c4d bump version 2021-05-14 21:01:43 +02:00
antelle 6627e8c0ec fix #1817: crash on files with large attachments as KDBX3 2021-05-14 21:00:43 +02:00
antelle 379e28e8a4 about version in the the bug report template 2021-05-14 20:56:48 +02:00
antelle 4fb25451a2 fix #1816: old Chromium support, such as Android Edge 2021-05-13 10:45:48 +02:00
antelle 58a6a97130 Merge branch 'master' into develop 2021-05-12 21:43:27 +02:00
antelle f59b846643 Merge branch 'release-1.18'
Build / linux (push) Canceled after 0s
Build / web (push) Canceled after 0s
Build / darwin (push) Canceled after 0s
Build / win32 (push) Canceled after 0s
Build / publish (push) Canceled after 0s
2021-05-12 21:40:25 +02:00
antelle 28d7dfeb67 bump version 2021-05-12 21:39:47 +02:00
antelle 6cc39d2107 fix #1808: restore KeeWeb from system tray on extension request 2021-05-12 21:38:54 +02:00
antelle a92a01095b fix #1814: option to disable auto-type title filter by default 2021-05-12 21:00:51 +02:00
antelle 3f4cddd2a2 fix #1811: app doesn't quit during update 2021-05-12 20:50:24 +02:00
antelle aa734ea976 fix #1813: fixed custom icon selection 2021-05-11 18:50:37 +02:00
antelle 30628004fd https link in the readme 2021-05-10 20:40:38 +02:00
antelle a6e87b52fd fix #1810: fixed the extension on old macOS starting 10.11 2021-05-10 20:40:30 +02:00
antelle 6d06d825e5 fixed donate button border 2021-05-10 18:12:11 +02:00
antelle 0800d5f33a Merge branch 'master' into develop 2021-05-09 23:25:27 +02:00
antelle 337ef44b56 Merge branch 'release-1.18'
Build / web (push) Canceled after 0s
Build / linux (push) Canceled after 0s
Build / darwin (push) Canceled after 0s
Build / win32 (push) Canceled after 0s
Build / publish (push) Canceled after 0s
2021-05-09 14:32:23 +02:00
antelle 70b4effde8 fix #1806: fixed a possible config loading error during startup 2021-05-09 14:31:58 +02:00
antelle 9e4c0f5c47 Merge branch 'release-1.18' 2021-05-09 14:12:50 +02:00
antelle 535cdc72f7 fix #1805: auto-unchecking auto-type filters if nothing found 2021-05-09 14:07:22 +02:00
antelle 8d258ecab2 fix #1804: entry table sizing in Firefox 2021-05-09 10:17:55 +02:00
antelle 3079e28f6f bump version 2021-05-09 10:09:26 +02:00
antelle 4970290614 fix #1804: filling OTP in browser extensions 2021-05-09 10:08:59 +02:00
antelle 6929931cae Merge branch 'master' into develop 2021-05-09 09:04:12 +02:00
antelle e348310602 Merge branch 'release-1.18'
Build / web (push) Canceled after 0s
Build / linux (push) Canceled after 0s
Build / darwin (push) Canceled after 0s
Build / win32 (push) Canceled after 0s
Build / publish (push) Canceled after 0s
2021-05-08 22:33:52 +02:00
antelle 7abbd2f253 bump version 2021-05-08 22:32:53 +02:00
antelle c56698492e fix #1802: opening files with saved keyfiles 2021-05-08 22:32:35 +02:00
antelle bc4c33fcbb Merge branch 'release-1.18'
Build / web (push) Canceled after 0s
Build / linux (push) Canceled after 0s
Build / darwin (push) Canceled after 0s
Build / win32 (push) Canceled after 0s
Build / publish (push) Canceled after 0s
2021-05-08 21:02:32 +02:00
antelle 05d5ac960c bump version 2021-05-08 21:01:00 +02:00
antelle 80331401b5 Merge branch 'master' into release-1.18 2021-05-08 21:00:47 +02:00
antelle b4459e1247 fixed entry attachments display 2021-05-08 21:00:38 +02:00
antelle 706c82d3ca fixed Windows code signing
Build / web (push) Canceled after 0s
Build / linux (push) Canceled after 0s
Build / darwin (push) Canceled after 0s
Build / win32 (push) Canceled after 0s
Build / publish (push) Canceled after 0s
2021-05-08 19:57:02 +02:00
antelle 0e1b086096 Merge branch 'develop' 2021-05-08 19:37:01 +02:00
antelle 80359c98f5 bump version 2021-05-08 19:35:51 +02:00
antelle 034e0ec2e3 always write version with 2 spaces 2021-05-08 19:35:36 +02:00
antelle cc6839d13e fixed native messaging host mode 2021-05-08 17:48:30 +02:00
antelle 0ce436c2e9 fixed phonetic password generation 2021-05-08 17:13:10 +02:00
antelle 966d814281 fixed removing an extra url 2021-05-08 16:59:36 +02:00
antelle c983bc10ab fixed git tag name 2021-05-08 16:11:26 +02:00
antelle d603398da0 upgrading node.js on ci 2021-05-08 14:21:56 +02:00
antelle 1404d2688f ci bugfixes 2021-05-08 14:14:00 +02:00
antelle c72158cf0d release name 2021-05-08 14:12:05 +02:00
antelle 1e6ed1308f fix eslint 2021-05-08 13:50:25 +02:00
antelle fae241373a fixed tests 2021-05-08 13:46:08 +02:00
antelle 2bf4cd83df upgrading node.js 2021-05-08 13:45:14 +02:00
antelle efd3290ee8 upgraded github release publishing 2021-05-08 13:35:03 +02:00
antelle aa729d9af8 don't log extension events when not connected 2021-05-08 11:46:53 +02:00
antelle 2d6d3658a1 fixed an error during exit without launcher 2021-05-08 11:42:08 +02:00
antelle 0cbdd6281a kdbxweb v2 2021-05-08 11:38:23 +02:00
antelle a845a42d0d up deps 2021-05-08 10:56:49 +02:00
antelle 60329e086a translated application menu on macOS 2021-05-08 10:37:24 +02:00
antelle a3174b96d7 up languages 2021-05-08 09:57:33 +02:00
antelle 29c00066bc Safari extension link 2021-05-08 09:53:57 +02:00
antelle b90ffc4d3f fix #1794: update installation on Windows when minimize instead of close is enabled 2021-05-06 20:44:09 +02:00
antelle 7e030320f5 option to auto-save on file change, fix #1364 2021-05-06 20:13:58 +02:00
antelle c96bcad32d release notes formatting 2021-05-05 19:56:29 +02:00
antelle a655cce195 removed file version fix 2021-05-05 18:49:24 +02:00
antelle aafb0e7964 editorconfig 2021-05-04 17:18:26 +02:00
antelle 288f7f117f removed an http link 2021-05-02 20:51:32 +02:00
antelle 125ac2eda8 wording 2021-05-02 20:43:44 +02:00
antelle ea80bfae07 fix #keeweb/keeweb-connect#3: extension manifest file name 2021-05-01 19:20:14 +02:00
antelle 5e727a3547 fixed native messaging host permissions 2021-04-30 10:16:30 +02:00
antelle 6672b60d0f fix #1779: don't copy passwords from the inline genertor in fields 2021-04-29 14:20:36 +02:00
antelle 09fe1a4649 fix #1780: custom icon selection issue 2021-04-29 14:16:39 +02:00
antelle e4fa4cc413 fixed selecting password as custom field 2021-04-29 14:02:42 +02:00
antelle 0be60475cf selecting other fields for browser extensions 2021-04-29 12:55:00 +02:00
antelle b7e53185b0 returning otp codes 2021-04-29 12:16:53 +02:00
antelle cd8c76b3eb up deps 2021-04-29 11:30:01 +02:00
antelle a1d7a1fe48 ignoring errors during usb init 2021-04-29 10:15:41 +02:00
antelle c48153819f checking all urls in the auto-type search 2021-04-29 09:18:32 +02:00
antelle d2d5590887 replaced a constant 2021-04-29 08:31:37 +02:00
antelle 65b319e127 extension ids constants 2021-04-29 08:29:33 +02:00
antelle b34c0dd34e ignoring custom websites in the selectiong pop-up 2021-04-28 23:47:56 +02:00
antelle ddc1a371bf increased the timeout 2021-04-28 23:40:37 +02:00
antelle a0c0b7b195 cancel button in the entry selection view 2021-04-28 23:37:15 +02:00
antelle b07cea9049 option to show an empty entry selection screen 2021-04-28 23:29:13 +02:00
antelle 991388d4cd removed debugging code 2021-04-28 23:09:24 +02:00
antelle 7adc77c823 closing the selection view on inactivity 2021-04-28 23:08:01 +02:00
antelle 550f93d4ae release notes
fix #874
2021-04-28 22:12:07 +02:00
antelle b43187e5b8 update deps again 2021-04-28 21:08:42 +02:00
antelle 6230710bb0 fixing build 2021-04-28 21:07:22 +02:00
antelle ff119cbc0a Revert "audit fix"
This reverts commit 2a2ec713
2021-04-28 21:05:22 +02:00
antelle 2a2ec713af audit fix 2021-04-28 21:04:37 +02:00
antelle 41eb560d1a fixed deps 2021-04-28 21:03:49 +02:00
antelle f7b9b91144 temporary safari link 2021-04-28 20:25:46 +02:00
antelle 97ab5d066e Added extension links 2021-04-28 20:22:21 +02:00
antelle 448e19d28c fixed entry searching with subdomains 2021-04-28 18:57:41 +02:00
antelle 391f2c9c00 fixed entry creation 2021-04-28 18:29:45 +02:00
antelle 18524ad9eb returning one matching entry automatically 2021-04-28 17:59:47 +02:00
antelle 732c35625b entry selection filters 2021-04-28 17:34:48 +02:00
antelle 95857450c2 focusing KeeWeb before entry selection 2021-04-28 12:04:54 +02:00
antelle 98ea7a542d extracted select-entry-filter 2021-04-28 11:29:48 +02:00
antelle 338f6d47d1 extracted entry selection view 2021-04-28 09:44:02 +02:00
antelle fdb492c572 don't prevent closing main window during auto-type 2021-04-28 08:25:18 +02:00
antelle ce3ff8c3d8 fixed extra file opened during auto-type 2021-04-28 08:23:15 +02:00
antelle 1789d9bb67 up deps 2021-04-28 07:47:13 +02:00
antelle 1883b527a4 fixed window blur during auto-type 2021-04-27 21:38:31 +02:00
antelle 2f4a9d450a displaying extension name in views 2021-04-27 20:01:53 +02:00
antelle 12fc33a15e fixed web extension focus 2021-04-27 19:38:27 +02:00
antelle e4693ecf61 updating entries 2021-04-27 19:15:03 +02:00
antelle c15d709db2 fixed table view column width 2021-04-27 16:23:49 +02:00
antelle e09040b0fb fixed auto-type from minimized state 2021-04-27 14:49:13 +02:00
antelle 4477c3fc2c get-logins messages 2021-04-27 14:29:00 +02:00
antelle f52a00a156 implemented set-login for extensions 2021-04-27 13:58:43 +02:00
antelle 946a063993 fixed data decryption error 2021-04-26 23:55:44 +02:00
antelle d3fcbcb6a5 fixed read passwords count display 2021-04-26 23:44:41 +02:00
antelle f91cfc0f79 processing triggerUnlock sent by the extension 2021-04-26 22:35:17 +02:00
antelle f0bbc54fd5 fixed extension version 2021-04-26 22:29:02 +02:00
antelle 3ca5f94b9b creating new groups form browser extensions 2021-04-26 20:47:43 +02:00
antelle 7a35ebec73 fixed a memory leak
see #1580
2021-04-26 19:20:29 +02:00
antelle 66d3623f90 implemented new group creation 2021-04-26 16:40:36 +02:00
antelle 8c9badd440 checking available files per connection 2021-04-26 15:36:21 +02:00
antelle e0d47a474d switching to the open view on request 2021-04-26 13:40:37 +02:00
antelle 1a8aff0594 fix eslint 2021-04-26 13:34:33 +02:00
antelle cf5e01c213 wording 2021-04-26 13:31:57 +02:00
antelle ffae956e21 removing empty websites 2021-04-26 13:30:12 +02:00
antelle 5c4cb7ff6d displaying extra websites as urls 2021-04-26 13:26:27 +02:00
antelle 7794c8b89b release notes 2021-04-26 12:49:59 +02:00
antelle bb78e8e8a7 adding multiple websites to entries 2021-04-26 12:49:25 +02:00
antelle 00a2a95adc fixed read passwords display 2021-04-26 12:22:35 +02:00
antelle e55dd47f6e don't hide keeweb after successful result 2021-04-26 12:16:22 +02:00
antelle 7b10bda0a4 eslint fix 2021-04-26 12:08:30 +02:00
antelle d5b9aefa01 unlocking on request from browser extension 2021-04-26 12:06:03 +02:00
antelle cdc3f5e1df displaying a message on the open screen 2021-04-26 11:51:24 +02:00
antelle 8fc6f49fb4 reworked unlocking files for auto-type and browser extension connection
fix #1768
2021-04-26 11:20:20 +02:00
antelle 29e5c14009 setting for saving credentials 2021-04-25 23:17:14 +02:00
antelle fe9ff205b9 wording 2021-04-25 23:02:02 +02:00
antelle 87b59eccee fixed extension connection on web 2021-04-25 20:38:49 +02:00
antelle 42cb5a6c36 editing file permissions in settings 2021-04-25 19:59:52 +02:00
antelle 9826965803 session management in settings 2021-04-25 18:08:16 +02:00
antelle 47153e41d3 extension connection timeout 2021-04-25 14:09:54 +02:00
antelle 677b428e63 all other files and all files 2021-04-25 13:50:03 +02:00
antelle 15936fcef0 extension connection dialog 2021-04-25 13:46:59 +02:00
antelle b2e828e7ef up native messaging host 2021-04-25 00:17:35 +02:00
antelle ac6e5a3ae8 fixed processing first message 2021-04-24 23:47:35 +02:00
antelle 3722477eea added a warning about extension association 2021-04-24 19:31:39 +02:00
antelle 72d0f76acc getting parent info for cmd 2021-04-24 10:48:28 +02:00
antelle b7333574aa replaced .exe in app name on windows 2021-04-24 10:38:43 +02:00
antelle 5caccbd1f5 getting appName 2021-04-24 10:26:07 +02:00
antelle 698b559935 up native-messaging-host 2021-04-24 10:06:55 +02:00
antelle 36d5e8f730 windows process information 2021-04-23 23:49:14 +02:00
antelle 233721bf51 extension installation on Windows 2021-04-23 23:17:35 +02:00
antelle 822602a38f added a note about windows-registry 2021-04-23 21:25:49 +02:00
antelle e69dbf697c up keeweb-native-modules 2021-04-23 21:17:11 +02:00
antelle 184d71a1b9 fixed firefox manifest 2021-04-21 23:30:32 +02:00
antelle ccb9b03287 creating native manifests 2021-04-21 22:23:17 +02:00
antelle dd4ad6d72b starting the extension host based on settings 2021-04-21 20:27:55 +02:00
antelle 195e2cc8f0 fixed exports 2021-04-21 20:09:26 +02:00
antelle ef573336d8 moved isDev to utils 2021-04-21 19:31:37 +02:00
antelle 9377a849f6 added native messaging host package 2021-04-21 19:28:44 +02:00
antelle 00ccbf4be2 deleted native-messaging-host 2021-04-20 17:07:50 +02:00
antelle 00159d26dc extracted protocolimpl 2021-04-19 23:40:57 +02:00
antelle b4f93adf05 reorganized imports a bit 2021-04-19 23:33:16 +02:00
antelle 5bbc9003f4 enable extension settings 2021-04-19 22:40:59 +02:00
antelle c4be926b7c better extension messages 2021-04-19 21:56:08 +02:00
antelle 147e3911f1 fixed no extensions message 2021-04-19 21:31:55 +02:00
antelle c7b814fc01 browser extension settings 2021-04-19 21:29:52 +02:00
antelle 8525571afc disabled the extension setting for mobile 2021-04-19 18:18:37 +02:00
antelle 5c5de91672 fixed links in desktop apps 2021-04-19 18:15:41 +02:00
antelle 7dbb0a2e8f added a missing whitespace 2021-04-18 21:42:22 +02:00
antelle 11b54ffdf9 wording 2021-04-18 21:39:24 +02:00
antelle 5d76ac33dd wording 2021-04-18 21:38:58 +02:00
antelle 8e92bf3766 kw.runtimeData api 2021-04-18 21:37:17 +02:00
antelle 56517eec62 typo 2021-04-18 21:35:45 +02:00
antelle ed56f0733e added an option to focus KeeWeb when the file is locked 2021-04-18 21:02:43 +02:00
antelle 3fd799ee74 browser extension settings page 2021-04-18 20:55:27 +02:00
antelle 4ed8a67eda simplified a condition 2021-04-18 18:57:38 +02:00
antelle cbb9a8dbd0 sending origin information in the first message 2021-04-18 18:50:03 +02:00
antelle 4280b84459 moved browser socket connection management to the main process 2021-04-18 14:26:45 +02:00
antelle e06e9ac1f8 moved ipc handlers setup to corresponding files 2021-04-18 11:50:11 +02:00
antelle 7bea4ad282 added team id to avoid the requirement for codesigning config 2021-04-17 20:33:37 +02:00
antelle 316c14d845 shorter socket name 2021-04-17 17:25:14 +02:00
antelle d17be63312 keeweb-browse => keeweb-connect 2021-04-17 17:00:14 +02:00
antelle 2c2253db31 renamed the extension 2021-04-17 16:57:06 +02:00
antelle a3128e56a3 client.supportsNotifications 2021-04-17 09:51:18 +02:00
antelle de0cf0ce67 fixed an empty notification about closing files 2021-04-17 09:28:25 +02:00
antelle e3f33b9d4c don't require client id for ping 2021-04-16 21:06:20 +02:00
antelle 27143e2663 fixed macos socket 2021-04-15 23:46:34 +02:00
antelle 566e2fb35d creating browser socket in a group container on macOS 2021-04-15 20:38:13 +02:00
antelle dc5ada1c04 etter error handling 2021-04-14 21:31:29 +02:00
antelle 603e698f5a fixed socket cleanup on windows 2021-04-14 21:26:58 +02:00
antelle f893dcb19a native messaging host tests 2021-04-14 21:11:24 +02:00
antelle 27b116798e tests fix 2021-04-14 20:41:45 +02:00
antelle ae932e5710 windows extension connection fixes 2021-04-14 20:34:03 +02:00
antelle bab5a979be windows compilation options 2021-04-13 23:37:43 +02:00
antelle 1b8c9a3e0c fixed linux helper paths 2021-04-13 23:03:38 +02:00
antelle 99d0fa30ed removed keeweb launching 2021-04-13 22:59:39 +02:00
antelle 5cc146481b fixed grunt-cmake 2021-04-13 22:55:38 +02:00
antelle 102da5cba1 signing the native messaging host 2021-04-13 22:40:35 +02:00
antelle d374eecb18 running cmake on build 2021-04-13 22:39:07 +02:00
antelle 08d31c0ffe don't launch keeweb if it's not running 2021-04-13 21:37:00 +02:00
antelle c62e40465a eslint 2021-04-13 20:43:34 +02:00
antelle 5367f32d5c reorganized app startup 2021-04-13 20:27:20 +02:00
antelle ad20698a42 deleted the browser extension setting 2021-04-13 20:27:07 +02:00
antelle e865d80a63 moved more code to protocol-impl 2021-04-12 23:42:42 +02:00
antelle 81a311e7fa refactored browser extension connector module 2021-04-12 23:37:08 +02:00
antelle 5397fa8fd3 create-group 2021-04-12 23:23:22 +02:00
antelle b9e08cd9e3 implemented get-database-groups 2021-04-12 23:05:57 +02:00
antelle fe86cd6320 native messaging on Firefox 2021-04-11 23:28:24 +02:00
antelle bba07bb83a fixed constant name 2021-04-11 19:52:49 +02:00
antelle 83a53ee1f0 error code property 2021-04-11 19:14:34 +02:00
antelle 23ba363a0d asking questions about accessing data in the extension 2021-04-11 18:59:33 +02:00
antelle 101aee81c0 added more browser extensions to the native host 2021-04-11 18:01:36 +02:00
antelle fa60d02129 option to debug browser extension transport 2021-04-11 17:59:05 +02:00
antelle 6a9e5fd801 always returning KeeWeb hash 2021-04-11 17:22:58 +02:00
antelle 12ae7845ec renamed the file 2021-04-11 15:41:39 +02:00
antelle 045e8ee96f fixed error codes 2021-04-11 15:04:04 +02:00
antelle 993f75b14e fixed some contract types 2021-04-11 14:53:56 +02:00
antelle b48f3746bc closing browser extension connections 2021-04-11 12:48:58 +02:00
antelle dcc477acbc tightened content security policy to prevent browser extension from adding scripts to KeeWeb 2021-04-11 11:44:51 +02:00
antelle 5f4f007b31 more extension logging 2021-04-11 10:17:02 +02:00
antelle 9d4788cb7c returning appName only for own extension 2021-04-10 23:37:24 +02:00
antelle 5a1380f9ef removed extra check 2021-04-10 23:36:21 +02:00
antelle 0d82aa26f3 checking own extension 2021-04-10 23:35:42 +02:00
antelle 05f2ec19f2 reorganized the extension connector code 2021-04-10 21:32:29 +02:00
antelle 25c3c9c565 connecting to the native app from the extension 2021-04-10 21:16:48 +02:00
antelle d6e7e3d4a0 reverted the timeout 2021-04-10 19:33:31 +02:00
antelle 6148112904 max 1 minute to connect to KeeWeb 2021-04-10 19:31:34 +02:00
antelle ba338e9282 launching keeweb 2021-04-10 19:29:24 +02:00
antelle 2e132690d1 fixed deletion 2021-04-10 18:59:58 +02:00
antelle 8f1b6ca3b7 removed some manual memory management 2021-04-10 18:54:15 +02:00
antelle 85d0252972 removed double stderr 2021-04-10 18:47:00 +02:00
antelle 077f72a0c5 printing stderr 2021-04-10 18:42:12 +02:00
antelle 25bec513f3 extracted some constants 2021-04-10 18:39:23 +02:00
antelle 0a6378d84c renamed the tests 2021-04-10 18:39:12 +02:00
antelle 1e6034ce37 native host tests 2021-04-10 18:38:27 +02:00
antelle ef6e5ebcbe added native messaging host 2021-04-10 17:00:10 +02:00
antelle 19a42b7463 fix #1783: crash after disabling USB devices on Linux 2021-04-10 00:19:58 +02:00
antelle 9d0e608454 Merge branch 'master' into develop
# Conflicts:
#	app/scripts/locales/base.json
#	app/scripts/storage/impl/storage-gdrive.js
#	package-lock.json
#	release-notes.md
2021-04-09 20:10:52 +02:00
antelle bd9a98dbfd fixed docker image tagging 2021-04-09 20:09:44 +02:00
antelle 0ffca3dfe4 npm audit fix 2021-04-09 19:03:02 +02:00
antelle 01958559ee fix #1767: updater issues on macOS 2021-04-09 19:01:55 +02:00
antelle bb4aef206a fixed wait-pid 2021-04-09 19:01:54 +02:00
antelle e74227b0db added --verbose option to the installer 2021-04-09 19:01:53 +02:00
antelle a47aec1cc4 bump version 2021-04-09 19:01:52 +02:00
antelle cd9d8cde3e release notes 2021-04-09 19:01:49 +02:00
antelle d4f68d5ec7 fix #1786: refresh_token in Google Drive 2021-04-09 19:01:48 +02:00
antelle a910cfcb35 fix #618: team drive support in Google Drive 2021-04-09 19:01:47 +02:00
antelle 962de503fd up kdbxweb 2021-04-09 19:01:27 +02:00
antelle f40e72c882 shared drives variable 2021-04-09 18:20:15 +02:00
antelle b7993e1d0e renamed team drives to shared drives 2021-04-09 18:19:26 +02:00
antelle d6e5e73e82 fixed refresh_token in Google Drive
see #1786
2021-04-08 23:07:19 +02:00
antelle 1794f87f99 fix #618: team drive support in Google Drive 2021-04-08 21:47:57 +02:00
antelle 81c7d57f30 localized templates folder name 2021-04-08 20:09:25 +02:00
antelle b4f0149bd8 saving extension name 2021-04-08 19:01:08 +02:00
antelle 87ec06eef0 browser extension: generate-password method 2021-04-08 18:10:48 +02:00
antelle c764548214 browser extension: generate-password method 2021-04-08 17:59:39 +02:00
antelle c78af7c3ad setting nonce in response payload 2021-04-08 17:36:42 +02:00
antelle f9cd0d4e29 browser extension: asking for user attention 2021-04-08 17:28:41 +02:00
antelle ee03435ca7 implemented lock-database 2021-04-07 22:49:23 +02:00
antelle a9b83256c6 returning only active db hashes 2021-04-07 22:07:16 +02:00
antelle 7086ddcf58 returning the list of hashes 2021-04-07 22:04:21 +02:00
antelle 26d226bb17 browser-extension: returning appName 2021-04-07 21:16:48 +02:00
antelle dc4742bf5c browser extension connector: implemented get-databasehash 2021-04-07 21:08:22 +02:00
antelle 933df5fae4 browser extension file notifications 2021-04-06 21:27:58 +02:00
antelle 4b17ac19dc browser extension backend 101 2021-04-06 20:10:42 +02:00
antelle 43f6a4f499 clone keeweb-browser in dev-env 2021-04-05 09:53:49 +02:00
antelle 076b99676d fix #1775: special error message for cases when Touch ID is changed after setup
also added Touch ID key deletion when it's disabled
2021-04-02 18:47:04 +02:00
antelle b3dfa05a4a added menu translations 2021-04-02 13:34:27 +02:00
antelle c5946b1a0b fixed default character count calculation in the generator 2021-04-01 21:57:42 +02:00
antelle 58a85eabdf fix #1774: password generator now includes all selected character ranges 2021-04-01 21:54:42 +02:00
Dimitri Witkowski 8a1d9b96dd Merge pull request #1770 from aslafy-z/patch-1
fix(docker): publish all docker image tags
2021-03-29 20:31:41 +02:00
Zadkiel 36a3d2f766 fix(docker): publish all docker image tags 2021-03-29 20:25:37 +02:00
antelle 68010076f9 renamed external model to otp-device 2021-03-28 19:05:20 +02:00
antelle 92da783e82 moved spawn to the main process 2021-03-28 18:41:31 +02:00
antelle 7d1e343d25 fixed wait-pid 2021-03-27 14:24:54 +01:00
antelle 84d17b7255 added --verbose option to the installer 2021-03-27 14:11:28 +01:00
antelle 64e06a32ec Merge branch 'master' into develop
# Conflicts:
#	package-lock.json
#	release-notes.md
2021-03-27 10:25:29 +01:00
antelle 4ee87e197d using new accounts ykman command
Build / publish (push) Canceled after 0s
Build / web (push) Canceled after 0s
Build / linux (push) Canceled after 0s
Build / darwin (push) Canceled after 0s
Build / win32 (push) Canceled after 0s
2021-03-27 09:19:20 +01:00
antelle cace667b26 release notes 2021-03-27 09:19:19 +01:00
antelle 67d3e28af7 bump version 2021-03-27 09:19:18 +01:00
antelle 3c5f4e7b84 release notes 2021-03-27 09:19:17 +01:00
antelle 8f913c737c removed debugging code from build 2021-03-27 09:19:16 +01:00
antelle 8ba147f52f fix #1762: line breaks in Markdown notes 2021-03-27 09:19:15 +01:00
antelle 9cfbd24b76 fix #1758: export format HTML issues 2021-03-27 09:19:14 +01:00
antelle 259abf6d25 added missing overflow to revealed passwords 2021-03-27 09:19:13 +01:00
antelle 8c59838a6f fix #1734: overlapping generator icon on password inputs 2021-03-27 09:19:12 +01:00
antelle f7d24c6379 fix #1725: setting expire date in the past 2021-03-27 09:19:11 +01:00
antelle 0f5466e075 fix #1755: calendar tooltips in different time zones 2021-03-27 09:19:10 +01:00
antelle 7ab0e83f55 ykman v4 support 2021-03-27 09:19:09 +01:00
antelle dc02ede8d7 up kdbxweb 2021-03-27 09:19:08 +01:00
antelle 42e484827f restored the missing disk icon 2021-03-27 09:19:07 +01:00
antelle 576c8f97bd removed dist from dev-env 2021-03-27 09:19:06 +01:00
antelle 808b4a687d fix #1762: line breaks in Markdown notes 2021-03-24 17:02:05 +01:00
antelle 954f2c235b fix #1758: export format HTML issues 2021-03-22 17:13:09 +01:00
antelle 51a7bbbfb8 added missing overflow to revealed passwords 2021-03-21 17:38:36 +01:00
antelle 9b5f5857bb fix #1734: overlapping generator icon on password inputs 2021-03-21 17:35:06 +01:00
antelle e66f52a8ce fix #1753: option to use short-lived tokens in cloud storages 2021-03-21 17:22:38 +01:00
antelle 8023bcea96 fix #1725: setting expire date in the past 2021-03-21 16:38:09 +01:00
antelle dfb7491503 fix #1755: calendar tooltips in different time zones 2021-03-21 16:36:18 +01:00
antelle b43d9c963a ykman v4 support 2021-03-20 22:45:42 +01:00
antelle 51c0637134 fixed list item size in firefox 2021-03-20 22:40:13 +01:00
antelle b16c23190b fixed scrolled entry modification 2021-03-20 20:26:29 +01:00
antelle fddf2892cd fix #1580: high memory usage on large files, virtual scrolling of entries 2021-03-20 19:17:57 +01:00
antelle 53b5715da0 up kdbxweb 2021-03-19 23:07:10 +01:00
antelle 3a79fcb366 optimized memory consumption for large files
see #1580
2021-03-19 22:52:41 +01:00
antelle 4df034ac55 restored the missing disk icon 2021-03-19 22:11:55 +01:00
antelle a0bb04fe3a removed dist from dev-env 2021-03-19 22:08:33 +01:00
antelle a923f28a8d Merge branch 'master' into develop
# Conflicts:
#	release-notes.md
2021-03-19 18:57:01 +01:00
antelle 78642f1c9b opening XML and CSV files using the Open button 2021-03-19 18:56:36 +01:00
antelle 879ba17760 Merge branch 'master' into develop
# Conflicts:
#	release-notes.md
2021-03-14 21:17:26 +01:00
antelle 40b0643eaa Merge branch 'master' into develop 2021-03-13 19:31:54 +01:00
antelle 0a776c6a72 Merge branch 'release-1.17' into develop
# Conflicts:
#	release-notes.md
2021-03-13 17:31:05 +01:00
antelle e7a8d5232f Merge branch 'master' into develop
# Conflicts:
#	release-notes.md
2021-03-10 21:37:23 +01:00
antelle f8ec47bfdb release notes 2021-03-08 22:34:29 +01:00
antelle 4e84121553 cleaned up legacy auto-type 2021-03-08 22:29:59 +01:00
antelle 6ba3d44fa1 Merge branch 'master' into develop 2021-03-07 15:16:48 +01:00
243 changed files with 8181 additions and 56976 deletions
+2
View File
@@ -7,6 +7,8 @@
}],
"@babel/plugin-proposal-optional-catch-binding",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator",
"@babel/plugin-proposal-logical-assignment-operators",
"@babel/plugin-external-helpers"
]
}
+1 -4
View File
@@ -15,8 +15,5 @@ trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.nsh]
indent_size = 2
[{*.yml,*.yaml}]
[{*.nsh,*.yml,*.yaml,*.json}]
indent_size = 2
-1
View File
@@ -48,7 +48,6 @@
"import/no-webpack-loader-syntax": "off",
"import/no-relative-parent-imports": "error",
"import/first": "error",
"import/no-namespace": "error",
"import/no-default-export": "error",
"babel/no-unused-expressions": "error",
"node/no-callback-literal": "off"
+1 -1
View File
@@ -24,7 +24,7 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**Environment**
Your user-agent (from Settings/Help section)
Please copy all version information from Settings/Help
**Kdbx File**
Does it happen on Demo or New database?
@@ -1,12 +0,0 @@
name: 'Publish release'
description: 'Publish a release and update the release notes'
inputs:
version:
description: 'Release version'
required: true
id:
description: 'Release ID'
required: true
runs:
using: 'node12'
main: 'dist/index.js'
File diff suppressed because it is too large Load Diff
-419
View File
@@ -1,419 +0,0 @@
{
"name": "publish-release",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@actions/core": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.4.tgz",
"integrity": "sha512-YJCEq8BE3CdN8+7HPZ/4DxJjk/OkZV2FFIf+DlZTC/4iBlzYCD5yjRR6eiOS5llO11zbRltIRuKAjMKaWTE6cg=="
},
"@actions/exec": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.4.tgz",
"integrity": "sha512-4DPChWow9yc9W3WqEbUj8Nr86xkpyE29ZzWjXucHItclLbEW6jr80Zx4nqv18QL6KK65+cifiQZXvnqgTV6oHw==",
"requires": {
"@actions/io": "^1.0.1"
}
},
"@actions/github": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-2.1.1.tgz",
"integrity": "sha512-kAgTGUx7yf5KQCndVeHSwCNZuDBvPyxm5xKTswW2lofugeuC1AZX73nUUVDNaysnM9aKFMHv9YCdVJbg7syEyA==",
"requires": {
"@actions/http-client": "^1.0.3",
"@octokit/graphql": "^4.3.1",
"@octokit/rest": "^16.43.1"
}
},
"@actions/http-client": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.7.tgz",
"integrity": "sha512-PY3ys/XH5WMekkHyZhYSa/scYvlE5T/TV/T++vABHuY5ZRgtiBZkn2L2tV5Pv/xDCl59lSZb9WwRuWExDyAsSg==",
"requires": {
"tunnel": "0.0.6"
}
},
"@actions/io": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz",
"integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg=="
},
"@octokit/auth-token": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.0.tgz",
"integrity": "sha512-eoOVMjILna7FVQf96iWc3+ZtE/ZT6y8ob8ZzcqKY1ibSQCnu4O/B7pJvzMx5cyZ/RjAff6DAdEb0O0Cjcxidkg==",
"requires": {
"@octokit/types": "^2.0.0"
}
},
"@octokit/endpoint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.0.tgz",
"integrity": "sha512-3nx+MEYoZeD0uJ+7F/gvELLvQJzLXhep2Az0bBSXagbApDvDW0LWwpnAIY/hb0Jwe17A0fJdz0O12dPh05cj7A==",
"requires": {
"@octokit/types": "^2.0.0",
"is-plain-object": "^3.0.0",
"universal-user-agent": "^5.0.0"
},
"dependencies": {
"universal-user-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-5.0.0.tgz",
"integrity": "sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q==",
"requires": {
"os-name": "^3.1.0"
}
}
}
},
"@octokit/graphql": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.3.1.tgz",
"integrity": "sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA==",
"requires": {
"@octokit/request": "^5.3.0",
"@octokit/types": "^2.0.0",
"universal-user-agent": "^4.0.0"
}
},
"@octokit/plugin-paginate-rest": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz",
"integrity": "sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q==",
"requires": {
"@octokit/types": "^2.0.1"
}
},
"@octokit/plugin-request-log": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz",
"integrity": "sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw=="
},
"@octokit/plugin-rest-endpoint-methods": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz",
"integrity": "sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ==",
"requires": {
"@octokit/types": "^2.0.1",
"deprecation": "^2.3.1"
}
},
"@octokit/request": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.3.4.tgz",
"integrity": "sha512-qyj8G8BxQyXjt9Xu6NvfvOr1E0l35lsXtwm3SopsYg/JWXjlsnwqLc8rsD2OLguEL/JjLfBvrXr4az7z8Lch2A==",
"requires": {
"@octokit/endpoint": "^6.0.0",
"@octokit/request-error": "^2.0.0",
"@octokit/types": "^2.0.0",
"deprecation": "^2.0.0",
"is-plain-object": "^3.0.0",
"node-fetch": "^2.3.0",
"once": "^1.4.0",
"universal-user-agent": "^5.0.0"
},
"dependencies": {
"universal-user-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-5.0.0.tgz",
"integrity": "sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q==",
"requires": {
"os-name": "^3.1.0"
}
}
}
},
"@octokit/request-error": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.0.tgz",
"integrity": "sha512-rtYicB4Absc60rUv74Rjpzek84UbVHGHJRu4fNVlZ1mCcyUPPuzFfG9Rn6sjHrd95DEsmjSt1Axlc699ZlbDkw==",
"requires": {
"@octokit/types": "^2.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
}
},
"@octokit/rest": {
"version": "16.43.1",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.43.1.tgz",
"integrity": "sha512-gfFKwRT/wFxq5qlNjnW2dh+qh74XgTQ2B179UX5K1HYCluioWj8Ndbgqw2PVqa1NnVJkGHp2ovMpVn/DImlmkw==",
"requires": {
"@octokit/auth-token": "^2.4.0",
"@octokit/plugin-paginate-rest": "^1.1.1",
"@octokit/plugin-request-log": "^1.0.0",
"@octokit/plugin-rest-endpoint-methods": "2.4.0",
"@octokit/request": "^5.2.0",
"@octokit/request-error": "^1.0.2",
"atob-lite": "^2.0.0",
"before-after-hook": "^2.0.0",
"btoa-lite": "^1.0.0",
"deprecation": "^2.0.0",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"lodash.uniq": "^4.5.0",
"octokit-pagination-methods": "^1.1.0",
"once": "^1.4.0",
"universal-user-agent": "^4.0.0"
},
"dependencies": {
"@octokit/request-error": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz",
"integrity": "sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==",
"requires": {
"@octokit/types": "^2.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
}
}
}
},
"@octokit/types": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.5.1.tgz",
"integrity": "sha512-q4Wr7RexkPRrkQpXzUYF5Fj/14Mr65RyOHj6B9d/sQACpqGcStkHZj4qMEtlMY5SnD/69jlL9ItGPbDM0dR/dA==",
"requires": {
"@types/node": ">= 8"
}
},
"@types/node": {
"version": "13.11.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.11.0.tgz",
"integrity": "sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ=="
},
"@zeit/ncc": {
"version": "0.22.1",
"resolved": "https://registry.npmjs.org/@zeit/ncc/-/ncc-0.22.1.tgz",
"integrity": "sha512-Qq3bMuonkcnV/96jhy9SQYdh39NXHxNMJ1O31ZFzWG9n52fR2DLtgrNzhj/ahlEjnBziMLGVWDbaS9sf03/fEw==",
"dev": true
},
"atob-lite": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz",
"integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY="
},
"before-after-hook": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz",
"integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A=="
},
"btoa-lite": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz",
"integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc="
},
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
"requires": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
"semver": "^5.5.0",
"shebang-command": "^1.2.0",
"which": "^1.2.9"
}
},
"deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
},
"end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"requires": {
"once": "^1.4.0"
}
},
"execa": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
"integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
"requires": {
"cross-spawn": "^6.0.0",
"get-stream": "^4.0.0",
"is-stream": "^1.1.0",
"npm-run-path": "^2.0.0",
"p-finally": "^1.0.0",
"signal-exit": "^3.0.0",
"strip-eof": "^1.0.0"
}
},
"get-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
"integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
"requires": {
"pump": "^3.0.0"
}
},
"is-plain-object": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz",
"integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==",
"requires": {
"isobject": "^4.0.0"
}
},
"is-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"isobject": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz",
"integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA=="
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
"lodash.set": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
"integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM="
},
"lodash.uniq": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
"integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M="
},
"macos-release": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz",
"integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA=="
},
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
},
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
},
"npm-run-path": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
"integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
"requires": {
"path-key": "^2.0.0"
}
},
"octokit-pagination-methods": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz",
"integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"os-name": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz",
"integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==",
"requires": {
"macos-release": "^2.2.0",
"windows-release": "^3.1.0"
}
},
"p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
},
"path-key": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
},
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
},
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
"requires": {
"shebang-regex": "^1.0.0"
}
},
"shebang-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
},
"signal-exit": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
},
"strip-eof": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
},
"tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
},
"universal-user-agent": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.1.tgz",
"integrity": "sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg==",
"requires": {
"os-name": "^3.1.0"
}
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"requires": {
"isexe": "^2.0.0"
}
},
"windows-release": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.0.tgz",
"integrity": "sha512-2HetyTg1Y+R+rUgrKeUEhAG/ZuOmTrI1NBb3ZyAGQMYmOJjBBPe4MTodghRkmLJZHwkuPi02anbeGP+Zf401LQ==",
"requires": {
"execa": "^1.0.0"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}
}
}
@@ -1,19 +0,0 @@
{
"name": "publish-release",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "ncc build src/main.js"
},
"author": "",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.2.4",
"@actions/exec": "^1.0.4",
"@actions/github": "^2.1.1"
},
"devDependencies": {
"@zeit/ncc": "^0.22.1"
}
}
@@ -1,45 +0,0 @@
const fs = require('fs');
const path = require('path');
const core = require('@actions/core');
const { GitHub, context } = require('@actions/github');
async function run() {
try {
const github = new GitHub(process.env.GITHUB_TOKEN);
const { owner, repo } = context.repo;
const version = core.getInput('version', { required: false });
const draft = false;
const release_id = core.getInput('release_id', { required: false });
const releaseNotesPath = path.join(__dirname, '../../../../release-notes.md');
const releaseNotes = fs.readFileSync(releaseNotesPath, 'utf8');
const regex = new RegExp(
`#####\\s+v${version.replace(/\./g, '\\.')}.*?\n([\\s\\S]*?)\n#####`
);
const bodyReleaseNotes = releaseNotes.match(regex)[1].trim().replace(/\s*\n/g, '\n');
const bodyTemplate =
'{release_notes}\n\n' +
'Want to keep releases happening? ' +
'Donate to KeeWeb on [OpenCollective](https://opencollective.com/keeweb) or [GitHub](https://github.com/sponsors/keeweb). ' +
'Thank you!';
const body = bodyTemplate.replace('{release_notes}', bodyReleaseNotes);
console.log(`Updating release with notes:\n${body}`);
await github.repos.updateRelease({
owner,
repo,
release_id,
body,
draft
});
} catch (error) {
core.setFailed(error.message);
}
}
run();
+32 -163
View File
@@ -18,6 +18,11 @@ jobs:
with:
repository: keeweb/keeweb
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
- name: Upgrade Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
registry-url: 'https://registry.npmjs.org'
- name: Install npm modules
run: npm ci
- name: Test
@@ -117,6 +122,11 @@ jobs:
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
path: dist
- name: Upgrade Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
registry-url: 'https://registry.npmjs.org'
- name: Install npm modules
run: npm ci
- name: Install grunt
@@ -171,6 +181,11 @@ jobs:
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
path: dist
- name: Upgrade Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
registry-url: 'https://registry.npmjs.org'
- name: Install npm modules
run: npm ci
- name: Install grunt
@@ -246,6 +261,11 @@ jobs:
repository: keeweb/keeweb
path: keeweb
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
- name: Upgrade Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
registry-url: 'https://registry.npmjs.org'
- name: Install npm modules
working-directory: keeweb
run: npm ci
@@ -349,173 +369,22 @@ jobs:
run: cp -r html keeweb/package/docker/dist
- name: Build the Docker image
working-directory: keeweb
run: docker build -t antelle/keeweb:latest package/docker
- name: Tag the Docker image
run: docker tag antelle/keeweb:latest antelle/keeweb:${{ steps.get_tag.outputs.tag }}
- name: Push the Docker image to the registry
run: docker build -t antelle/keeweb:latest -t antelle/keeweb:${{ steps.get_tag.outputs.tag }} package/docker
- name: Push the Docker image to dockerhub
if: ${{ github.repository == 'keeweb/keeweb' }}
run: docker push antelle/keeweb
- name: Create a GitHub release
id: create_release
uses: actions/create-release@latest
run: docker push --all-tags antelle/keeweb
- name: Extract release notes
run: node util/extract-release-notes.js --version=${{ steps.get_tag.outputs.tag }} --output=tmp/release-body.md
working-directory: keeweb
- name: Publish GitHub release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Desktop apps v${{ steps.get_tag.outputs.tag }}
draft: true
prerelease: false
- name: Upload html asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.html.zip
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html.zip
asset_content_type: application/octet-stream
- name: Upload linux.AppImage asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.AppImage
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.AppImage
asset_content_type: application/octet-stream
- name: Upload linux.snap asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.snap
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.snap
asset_content_type: application/octet-stream
- name: Upload linux.deb asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.deb
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.deb
asset_content_type: application/octet-stream
- name: Upload linux.zip asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.zip
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.zip
asset_content_type: application/octet-stream
- name: Upload linux.rpm asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
asset_content_type: application/octet-stream
- name: Upload darwin.x64.dmg asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.x64.dmg
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.x64.dmg
asset_content_type: application/octet-stream
- name: Upload darwin.arm64.dmg asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.arm64.dmg
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.arm64.dmg
asset_content_type: application/octet-stream
- name: Upload win32.ia32.exe asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.exe
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.exe
asset_content_type: application/octet-stream
- name: Upload win32.ia32.zip asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.zip
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.zip
asset_content_type: application/octet-stream
- name: Upload win32.x64.exe asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.exe
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.exe
asset_content_type: application/octet-stream
- name: Upload win32.x64.zip asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.zip
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.zip
asset_content_type: application/octet-stream
- name: Upload win32.arm64.exe asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.exe
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.exe
asset_content_type: application/octet-stream
- name: Upload win32.arm64.zip asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
asset_content_type: application/octet-stream
- name: Upload verify.sign asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: assets/Verify.sign.sha256
asset_name: Verify.sign.sha256
asset_content_type: application/octet-stream
- name: Upload verify.sha asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: assets/Verify.sha256
asset_name: Verify.sha256
asset_content_type: application/octet-stream
- name: Publish the GitHub release
uses: ./keeweb/.github/actions/publish-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
version: ${{ steps.get_tag.outputs.tag }}
release_id: ${{ steps.create_release.outputs.id }}
tag_name: v${{ steps.get_tag.outputs.tag }}
name: Desktop apps v${{ steps.get_tag.outputs.tag }}
files: assets/*
body_path: keeweb/tmp/release-body.md
- name: Checkout gh-pages
uses: actions/checkout@v2
with:
+1 -1
View File
@@ -14,6 +14,6 @@ obj/
xcuserdata/
*.suo
.idea/
.vscode
.vscode/
*.iml
test/dist/
+49 -28
View File
@@ -47,7 +47,8 @@ module.exports = function (grunt) {
const webpackOptions = {
date,
beta: !!grunt.option('beta'),
sha
sha,
appleTeamId: '3LE7JZ657W'
};
const windowsAppVersionString = {
@@ -144,18 +145,6 @@ module.exports = function (grunt) {
expand: true,
nonull: true
},
'desktop-darwin-helper-x64': {
src: 'helper/darwin/KeeWebHelper',
dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Resources/',
nonull: true,
options: { mode: '0755' }
},
'desktop-darwin-helper-arm64': {
src: 'helper/darwin/KeeWebHelper',
dest: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/Resources/',
nonull: true,
options: { mode: '0755' }
},
'desktop-darwin-installer-helper-x64': {
cwd: 'tmp/desktop/KeeWeb Installer.app',
src: '**',
@@ -174,21 +163,6 @@ module.exports = function (grunt) {
nonull: true,
options: { mode: true }
},
'desktop-windows-helper-x64': {
src: 'helper/win32/KeeWebHelper.exe',
dest: 'tmp/desktop/KeeWeb-win32-x64/Resources/',
nonull: true
},
'desktop-windows-helper-ia32': {
src: 'helper/win32/KeeWebHelper.exe',
dest: 'tmp/desktop/KeeWeb-win32-ia32/Resources/',
nonull: true
},
'desktop-windows-helper-arm64': {
src: 'helper/win32/KeeWebHelper.exe',
dest: 'tmp/desktop/KeeWeb-win32-arm64/Resources/',
nonull: true
},
'desktop-win32-dist-x64': {
src: 'tmp/desktop/KeeWeb.win.x64.exe',
dest: `dist/desktop/KeeWeb-${pkg.version}.win.x64.exe`,
@@ -253,6 +227,47 @@ module.exports = function (grunt) {
src: 'graphics/icon.icns',
dest: 'tmp/desktop/KeeWeb Installer.app/Contents/Resources/applet.icns',
nonull: true
},
'native-messaging-host-darwin-x64': {
src:
'node_modules/@keeweb/keeweb-native-messaging-host/darwin-x64/keeweb-native-messaging-host',
dest:
'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/MacOS/util/keeweb-native-messaging-host',
nonull: true,
options: { mode: '0755' }
},
'native-messaging-host-darwin-arm64': {
src:
'node_modules/@keeweb/keeweb-native-messaging-host/darwin-arm64/keeweb-native-messaging-host',
dest:
'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/MacOS/util/keeweb-native-messaging-host',
nonull: true,
options: { mode: '0755' }
},
'native-messaging-host-linux-x64': {
src:
'node_modules/@keeweb/keeweb-native-messaging-host/linux-x64/keeweb-native-messaging-host',
dest: 'tmp/desktop/keeweb-linux-x64/keeweb-native-messaging-host',
nonull: true,
options: { mode: '0755' }
},
'native-messaging-host-win32-x64': {
src:
'node_modules/@keeweb/keeweb-native-messaging-host/win32-x64/keeweb-native-messaging-host.exe',
dest: 'tmp/desktop/KeeWeb-win32-x64/keeweb-native-messaging-host.exe',
nonull: true
},
'native-messaging-host-win32-ia32': {
src:
'node_modules/@keeweb/keeweb-native-messaging-host/win32-ia32/keeweb-native-messaging-host.exe',
dest: 'tmp/desktop/KeeWeb-win32-ia32/keeweb-native-messaging-host.exe',
nonull: true
},
'native-messaging-host-win32-arm64': {
src:
'node_modules/@keeweb/keeweb-native-messaging-host/win32-arm64/keeweb-native-messaging-host.exe',
dest: 'tmp/desktop/KeeWeb-win32-arm64/keeweb-native-messaging-host.exe',
nonull: true
}
},
eslint: {
@@ -668,6 +683,8 @@ module.exports = function (grunt) {
options: {
files: {
'tmp/desktop/KeeWeb-win32-x64/KeeWeb.exe': 'KeeWeb',
'tmp/desktop/KeeWeb-win32-x64/keeweb-native-messaging-host.exe':
'KeeWeb Native Messaging Host',
'tmp/desktop/KeeWeb-win32-x64/ffmpeg.dll': '',
'tmp/desktop/KeeWeb-win32-x64/libEGL.dll':
'ANGLE libEGL Dynamic Link Library',
@@ -680,6 +697,8 @@ module.exports = function (grunt) {
options: {
files: {
'tmp/desktop/KeeWeb-win32-ia32/KeeWeb.exe': 'KeeWeb',
'tmp/desktop/KeeWeb-win32-ia32/keeweb-native-messaging-host.exe':
'KeeWeb Native Messaging Host',
'tmp/desktop/KeeWeb-win32-ia32/ffmpeg.dll': '',
'tmp/desktop/KeeWeb-win32-ia32/libEGL.dll':
'ANGLE libEGL Dynamic Link Library',
@@ -692,6 +711,8 @@ module.exports = function (grunt) {
options: {
files: {
'tmp/desktop/KeeWeb-win32-arm64/KeeWeb.exe': 'KeeWeb',
'tmp/desktop/KeeWeb-win32-arm64/keeweb-native-messaging-host.exe':
'KeeWeb Native Messaging Host',
'tmp/desktop/KeeWeb-win32-arm64/ffmpeg.dll': '',
'tmp/desktop/KeeWeb-win32-arm64/libEGL.dll':
'ANGLE libEGL Dynamic Link Library',
+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>KeeWeb</title>
</head>
<body>
<script>
window.opener.postMessage(
{ storage: 'teams', search: location.search },
window.location.origin
);
window.close();
</script>
</body>
</html>
+1 -1
View File
@@ -8,7 +8,7 @@
content="
default-src 'self';
font-src data:;
script-src 'self' blob: 'unsafe-eval';
script-src 'self' 'unsafe-eval';
style-src 'self' blob:;
connect-src 'self' ws: https:;
child-src 'self' blob:;
+12 -5
View File
@@ -6,6 +6,7 @@ import { ExportApi } from 'comp/app/export-api';
import { SingleInstanceChecker } from 'comp/app/single-instance-checker';
import { Updater } from 'comp/app/updater';
import { UsbListener } from 'comp/app/usb-listener';
import { BrowserExtensionConnector } from 'comp/extension/browser-extension-connector';
import { FeatureTester } from 'comp/browser/feature-tester';
import { FocusDetector } from 'comp/browser/focus-detector';
import { IdleTracker } from 'comp/browser/idle-tracker';
@@ -45,6 +46,7 @@ ready(() => {
.then(loadRemoteConfig)
.then(ensureCanRun)
.then(initStorage)
.then(initUsbListener)
.then(showApp)
.then(postInit)
.catch((e) => {
@@ -142,6 +144,11 @@ ready(() => {
StartProfiler.milestone('initializing storage');
}
function initUsbListener() {
UsbListener.init();
StartProfiler.milestone('starting usb');
}
function showApp() {
return Promise.resolve().then(() => {
const skipHttpsWarning =
@@ -172,12 +179,12 @@ ready(() => {
}
function postInit() {
Updater.init();
SingleInstanceChecker.init();
AppRightsChecker.init();
IdleTracker.init();
UsbListener.init();
setTimeout(() => {
Updater.init();
SingleInstanceChecker.init();
AppRightsChecker.init();
IdleTracker.init();
BrowserExtensionConnector.init(appModel);
PluginManager.runAutoUpdate();
}, Timeouts.AutoUpdatePluginsAfterStart);
}
@@ -1,19 +0,0 @@
import { Launcher } from 'comp/launcher';
import { AppSettingsModel } from 'models/app-settings-model';
import { AutoTypeEmitter } from 'auto-type/auto-type-emitter';
const AutoTypeEmitterFactory = {
create(callback, windowId) {
if (Launcher && Launcher.autoTypeSupported) {
if (AppSettingsModel.useLegacyAutoType) {
const { AutoTypeEmitter } = require('./emitter/auto-type-emitter-' +
Launcher.platform());
return new AutoTypeEmitter(callback, windowId);
}
return new AutoTypeEmitter(callback, windowId);
}
return null;
}
};
export { AutoTypeEmitterFactory };
-90
View File
@@ -1,90 +0,0 @@
import { SearchResultCollection } from 'collections/search-result-collection';
import { Ranking } from 'util/data/ranking';
const urlPartsRegex = /^(\w+:\/\/)?(?:(?:www|wwws|secure)\.)?([^\/]+)\/?(.*)/;
const AutoTypeFilter = function (windowInfo, appModel) {
this.title = windowInfo.title;
this.url = windowInfo.url;
this.text = '';
this.ignoreWindowInfo = false;
this.appModel = appModel;
};
AutoTypeFilter.prototype.getEntries = function () {
const filter = {
text: this.text,
autoType: true
};
this.prepareFilter();
let entries = this.appModel.getEntriesByFilter(filter).map((e) => [e, this.getEntryRank(e)]);
if (!this.ignoreWindowInfo) {
entries = entries.filter((e) => e[1]);
}
entries = entries.sort((x, y) =>
x[1] === y[1] ? x[0].title.localeCompare(y[0].title) : y[1] - x[1]
);
entries = entries.map((p) => p[0]);
return new SearchResultCollection(entries, { comparator: 'none' });
};
AutoTypeFilter.prototype.hasWindowInfo = function () {
return this.title || this.url;
};
AutoTypeFilter.prototype.prepareFilter = function () {
this.titleLower = this.title ? this.title.toLowerCase() : null;
this.urlLower = this.url ? this.url.toLowerCase() : null;
this.urlParts = this.url ? urlPartsRegex.exec(this.urlLower) : null;
};
AutoTypeFilter.prototype.getEntryRank = function (entry) {
let rank = 0;
if (this.titleLower && entry.title) {
rank += Ranking.getStringRank(entry.title.toLowerCase(), this.titleLower);
}
if (this.urlParts) {
if (entry.url) {
const entryUrlParts = urlPartsRegex.exec(entry.url.toLowerCase());
if (entryUrlParts) {
const [, scheme, domain, path] = entryUrlParts;
const [, thisScheme, thisDomain, thisPath] = this.urlParts;
if (domain === thisDomain || thisDomain.indexOf('.' + domain) > 0) {
if (domain === thisDomain) {
rank += 20;
} else {
rank += 10;
}
if (path === thisPath) {
rank += 10;
} else if (path && thisPath) {
if (path.lastIndexOf(thisPath, 0) === 0) {
rank += 5;
} else if (thisPath.lastIndexOf(path, 0) === 0) {
rank += 3;
}
}
if (scheme === thisScheme) {
rank += 1;
}
} else {
if (entry.searchText.indexOf(this.urlLower) >= 0) {
// the url is in some field; include it
rank += 5;
} else {
// another domain; don't show this record at all, ignore title match
return 0;
}
}
}
} else {
if (entry.searchText.indexOf(this.urlLower) >= 0) {
// the url is in some field; include it
rank += 5;
}
}
}
return rank;
};
export { AutoTypeFilter };
@@ -1,19 +0,0 @@
import { Launcher } from 'comp/launcher';
import { AppSettingsModel } from 'models/app-settings-model';
import { AutoTypeHelper } from 'auto-type/auto-type-helper';
const AutoTypeHelperFactory = {
create() {
if (Launcher && Launcher.autoTypeSupported) {
if (AppSettingsModel.useLegacyAutoType) {
const { AutoTypeHelper } = require('./helper/auto-type-helper-' +
Launcher.platform());
return new AutoTypeHelper();
}
return new AutoTypeHelper();
}
return null;
}
};
export { AutoTypeHelperFactory };
+2 -5
View File
@@ -1,8 +1,7 @@
import { AutoTypeEmitterFactory } from 'auto-type/auto-type-emitter-factory';
import { AutoTypeEmitter } from 'auto-type/auto-type-emitter';
import { AutoTypeObfuscator } from 'auto-type/auto-type-obfuscator';
import { StringFormat } from 'util/formatting/string-format';
import { Logger } from 'util/logger';
import { AppSettingsModel } from 'models/app-settings-model';
const emitterLogger = new Logger(
'auto-type-emitter',
@@ -433,9 +432,7 @@ AutoTypeRunner.prototype.obfuscateOp = function (op) {
};
AutoTypeRunner.prototype.run = function (callback, windowId) {
const emitterType = AppSettingsModel.useLegacyAutoType ? 'legacy' : 'native';
emitterLogger.info(`Using ${emitterType} auto-type emitter`);
this.emitter = AutoTypeEmitterFactory.create(this.emitNext.bind(this), windowId);
this.emitter = new AutoTypeEmitter(this.emitNext.bind(this), windowId);
this.emitterState = {
callback,
stack: [],
@@ -1,133 +0,0 @@
import { AutoTypeNativeHelper } from 'auto-type/helper/auto-type-native-helper';
import { Launcher } from 'comp/launcher';
// http://eastmanreference.com/complete-list-of-applescript-key-codes/
const KeyMap = {
tab: 48,
enter: 36,
space: 49,
up: 126,
down: 125,
left: 123,
right: 124,
home: 115,
end: 119,
pgup: 116,
pgdn: 121,
ins: 114,
del: 117,
bs: 51,
esc: 53,
win: 55,
rwin: 55,
f1: 122,
f2: 120,
f3: 99,
f4: 118,
f5: 96,
f6: 97,
f7: 98,
f8: 100,
f9: 101,
f10: 109,
f11: 103,
f12: 111,
f13: 105,
f14: 107,
f15: 113,
f16: 106,
add: 69,
subtract: 78,
multiply: 67,
divide: 75,
n0: 82,
n1: 83,
n2: 84,
n3: 85,
n4: 86,
n5: 87,
n6: 88,
n7: 89,
n8: 91,
n9: 92
};
const ModMap = {
'^': '@',
'+': '+',
'%': '%',
'^^': '^'
};
const AutoTypeEmitter = function (callback) {
this.callback = callback;
this.mod = {};
this.pendingScript = [];
};
AutoTypeEmitter.prototype.begin = function () {
this.callback();
};
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
if (enabled) {
this.mod[ModMap[mod]] = true;
} else {
delete this.mod[ModMap[mod]];
}
};
AutoTypeEmitter.prototype.text = function (text) {
this.pendingScript.push('text ' + this.modString() + ' ' + text);
this.callback();
};
AutoTypeEmitter.prototype.key = function (key) {
if (typeof key !== 'number') {
if (!KeyMap[key]) {
return this.callback('Bad key: ' + key);
}
key = KeyMap[key];
}
this.pendingScript.push('key ' + this.modString() + key);
this.callback();
};
AutoTypeEmitter.prototype.copyPaste = function (text) {
this.pendingScript.push('copypaste ' + text);
this.callback();
};
AutoTypeEmitter.prototype.wait = function (time) {
this.pendingScript.push('wait ' + time);
this.callback();
};
AutoTypeEmitter.prototype.waitComplete = function () {
if (this.pendingScript.length) {
const script = this.pendingScript.join('\n');
this.pendingScript.length = 0;
this.runScript(script);
} else {
this.callback();
}
};
AutoTypeEmitter.prototype.setDelay = function (delay) {
this.delay = delay || 0;
this.callback('Not implemented');
};
AutoTypeEmitter.prototype.modString = function () {
return Object.keys(this.mod).join('');
};
AutoTypeEmitter.prototype.runScript = function (script) {
Launcher.spawn({
cmd: AutoTypeNativeHelper.getHelperPath(),
data: script,
complete: this.callback
});
};
export { AutoTypeEmitter };
@@ -1,162 +0,0 @@
import { Launcher } from 'comp/launcher';
import { Locale } from 'util/locale';
// https://cgit.freedesktop.org/xorg/proto/x11proto/plain/keysymdef.h
const KeyMap = {
tab: 'Tab',
enter: 'KP_Enter',
space: 'KP_Space',
up: 'Up',
down: 'Down',
left: 'Left',
right: 'Right',
home: 'Home',
end: 'End',
pgup: 'Page_Up',
pgdn: 'Page_Down',
ins: 'Insert',
del: 'Delete',
bs: 'BackSpace',
esc: 'Escape',
win: 'Meta_L',
rwin: 'Meta_R',
f1: 'F1',
f2: 'F2',
f3: 'F3',
f4: 'F4',
f5: 'F5',
f6: 'F6',
f7: 'F7',
f8: 'F8',
f9: 'F9',
f10: 'F10',
f11: 'F11',
f12: 'F12',
f13: 'F13',
f14: 'F14',
f15: 'F15',
f16: 'F16',
add: 'KP_Add',
subtract: 'KP_Subtract',
multiply: 'KP_Multiply',
divide: 'KP_Divide',
n0: 'KP_0',
n1: 'KP_1',
n2: 'KP_2',
n3: 'KP_3',
n4: 'KP_4',
n5: 'KP_5',
n6: 'KP_6',
n7: 'KP_7',
n8: 'KP_8',
n9: 'KP_9'
};
const ModMap = {
'^': 'ctrl',
'+': 'shift',
'%': 'alt',
'^^': 'ctrl'
};
const AutoTypeEmitter = function (callback, windowId) {
this.callback = callback;
this.mod = {};
this.pendingScript = [];
if (typeof windowId !== 'undefined' && windowId) {
this.pendingScript.push('windowactivate --sync ' + windowId);
}
};
AutoTypeEmitter.prototype.begin = function () {
this.callback();
};
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
if (enabled) {
this.mod[ModMap[mod]] = true;
} else {
delete this.mod[ModMap[mod]];
}
};
AutoTypeEmitter.prototype.text = function (text) {
this.pendingScript.push('keyup ctrl alt shift t');
Object.keys(this.mod).forEach((mod) => {
this.pendingScript.push('keydown ' + ModMap[mod]);
});
text.split('').forEach((char) => {
this.pendingScript.push('key ' + 'U' + char.charCodeAt(0).toString(16));
});
Object.keys(this.mod).forEach((mod) => {
this.pendingScript.push('keyup ' + ModMap[mod]);
});
this.waitComplete();
};
AutoTypeEmitter.prototype.key = function (key) {
const isSpecialKey = typeof key !== 'number';
if (isSpecialKey) {
if (!KeyMap[key]) {
return this.callback('Bad key: ' + key);
}
key = KeyMap[key].toString();
}
this.pendingScript.push('key --clearmodifiers ' + this.modString() + key);
if (isSpecialKey) {
this.waitComplete();
} else {
this.callback();
}
};
AutoTypeEmitter.prototype.copyPaste = function (text) {
this.pendingScript.push('sleep 0.5');
Launcher.setClipboardText(text);
this.pendingScript.push('key --clearmodifiers ' + 'shift+Insert');
this.pendingScript.push('sleep 0.5');
this.waitComplete();
};
AutoTypeEmitter.prototype.wait = function (time) {
this.pendingScript.push('sleep ' + time / 1000);
this.callback();
};
AutoTypeEmitter.prototype.waitComplete = function (callback) {
if (this.pendingScript.length) {
const script = this.pendingScript.join(' ');
this.pendingScript.length = 0;
this.runScript(script, callback);
} else {
this.callback();
}
};
AutoTypeEmitter.prototype.modString = function () {
let mod = '';
Object.keys(this.mod).forEach((key) => {
mod += key + '+';
});
return mod;
};
AutoTypeEmitter.prototype.runScript = function (script, callback) {
// xdotool doesn't like it when stdin doesn't end with a linebreak
// see https://github.com/keeweb/keeweb/issues/1409
const data = script + '\n';
Launcher.spawn({
cmd: 'xdotool',
args: ['-'],
data,
complete: (err, stdout, code) => {
if (err && err.code === 'ENOENT') {
err = Locale.autoTypeErrorNotInstalled.replace('{}', 'xdotool');
}
const cb = callback || this.callback;
cb(err, stdout, code);
}
});
};
export { AutoTypeEmitter };
@@ -1,135 +0,0 @@
import { AutoTypeNativeHelper } from 'auto-type/helper/auto-type-native-helper';
import { Launcher } from 'comp/launcher';
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
const KeyMap = {
tab: 0x09,
enter: 0x0d,
space: 0x20,
up: 0x26,
down: 0x28,
left: 0x25,
right: 0x27,
home: 0x24,
end: 0x23,
pgup: 0x21,
pgdn: 0x22,
ins: 0x2d,
del: 0x2e,
bs: 0x08,
esc: 0x1b,
win: 0x5b,
rwin: 0x5c,
f1: 0x70,
f2: 0x71,
f3: 0x72,
f4: 0x73,
f5: 0x74,
f6: 0x75,
f7: 0x76,
f8: 0x77,
f9: 0x78,
f10: 0x79,
f11: 0x7a,
f12: 0x7b,
f13: 0x7c,
f14: 0x7d,
f15: 0x7e,
f16: 0x7f,
add: 0x6b,
subtract: 0x6d,
multiply: 0x6a,
divide: 0x6f,
n0: 0x30,
n1: 0x31,
n2: 0x32,
n3: 0x33,
n4: 0x34,
n5: 0x35,
n6: 0x36,
n7: 0x37,
n8: 0x38,
n9: 0x39
};
const ModMap = {
'^': '^',
'+': '+',
'%': '%',
'^^': '^'
};
const AutoTypeEmitter = function (callback) {
this.callback = callback;
this.mod = {};
this.pendingScript = [];
};
AutoTypeEmitter.prototype.begin = function () {
this.callback();
};
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
if (enabled) {
this.mod[ModMap[mod]] = true;
} else {
delete this.mod[ModMap[mod]];
}
};
AutoTypeEmitter.prototype.text = function (text) {
if (text) {
this.pendingScript.push('text ' + this.modStr() + ' ' + text);
}
this.callback();
};
AutoTypeEmitter.prototype.key = function (key) {
if (typeof key !== 'number') {
if (!KeyMap[key]) {
return this.callback('Bad key: ' + key);
}
key = KeyMap[key];
}
this.pendingScript.push('key ' + this.modStr() + key);
this.callback();
};
AutoTypeEmitter.prototype.copyPaste = function (text) {
this.pendingScript.push('copypaste ' + text);
this.callback();
};
AutoTypeEmitter.prototype.wait = function (time) {
this.pendingScript.push('wait ' + time);
this.callback();
};
AutoTypeEmitter.prototype.waitComplete = function () {
if (this.pendingScript.length) {
const script = this.pendingScript.join('\n');
this.pendingScript.length = 0;
this.runScript(script);
} else {
this.callback();
}
};
AutoTypeEmitter.prototype.setDelay = function (delay) {
this.delay = delay || 0;
this.callback('Not implemented');
};
AutoTypeEmitter.prototype.modStr = function () {
return Object.keys(this.mod).join('');
};
AutoTypeEmitter.prototype.runScript = function (script) {
Launcher.spawn({
cmd: AutoTypeNativeHelper.getHelperPath(),
data: script,
complete: this.callback
});
};
export { AutoTypeEmitter };
@@ -1,92 +0,0 @@
import { Launcher } from 'comp/launcher';
const ForeMostAppScript =
'tell application "System Events"\n' +
' set frontAppName to name of first process whose frontmost is true\n' +
' set frontAppId to id of first process whose frontmost is true\n' +
'end tell\n' +
'"" & frontAppId & " " & frontAppName';
const ChromeScript =
'tell application "{}" to set appUrl to URL of active tab of front window\n' +
'tell application "{}" to set appTitle to title of active tab of front window\n' +
'return appUrl & "\n" & appTitle';
const SafariScript =
'tell application "{}" to set appUrl to URL of front document\n' +
'tell application "{}" to set appTitle to name of front document\n' +
'return appUrl & "\n" & appTitle';
const OtherAppsScript =
'tell application "System Events"\n' +
' tell process "{}"\n' +
' tell (1st window whose value of attribute "AXMain" is true)\n' +
' set windowTitle to value of attribute "AXTitle"\n' +
' end tell\n' +
' end tell\n' +
'end tell';
const AutoTypeHelper = function () {};
AutoTypeHelper.prototype.getActiveWindowInfo = function (callback) {
AutoTypeHelper.exec(ForeMostAppScript, (err, out) => {
if (err) {
return callback(err);
}
const output = out.trim();
const spaceIx = output.indexOf(' ');
let id = '',
appName = '';
if (spaceIx >= 0) {
id = output.substr(0, spaceIx);
appName = output.substr(spaceIx + 1).trim();
}
// getting urls and titles from Chrome or Safari:
// - will suit in 90% cases
// - does not require assistive access
// - allows to get url
if (['Google Chrome', 'Chromium', 'Google Chrome Canary'].indexOf(appName) >= 0) {
AutoTypeHelper.exec(ChromeScript.replace(/\{}/g, appName), (err, out) => {
if (err) {
return callback(err, { id });
}
const parts = out.split('\n');
return callback(null, {
id,
url: parts[0].trim(),
title: (parts[1] || '').trim()
});
});
} else if (['Safari', 'Webkit'].indexOf(appName) >= 0) {
AutoTypeHelper.exec(SafariScript.replace(/\{}/g, appName), (err, out) => {
if (err) {
return callback(err, { id });
}
const parts = out.split('\n');
return callback(null, {
id,
url: parts[0].trim(),
title: (parts[1] || '').trim()
});
});
} else {
// special cases are not available. this method may ask the user about assistive access
AutoTypeHelper.exec(OtherAppsScript.replace(/\{}/g, appName), (err, out) => {
if (err) {
return callback(err, { id });
}
return callback(null, {
id,
title: out.trim()
});
});
}
});
};
AutoTypeHelper.exec = function (script, callback) {
Launcher.spawn({
cmd: 'osascript',
args: ['-e', script],
complete: callback
});
};
export { AutoTypeHelper };
@@ -1,23 +0,0 @@
import { Launcher } from 'comp/launcher';
const AutoTypeHelper = function () {};
AutoTypeHelper.prototype.getActiveWindowInfo = function (callback) {
Launcher.spawn({
cmd: 'xdotool',
args: ['getactivewindow', 'getwindowname', 'getactivewindow'],
complete(err, out) {
let windowInfo;
if (out) {
const [title, id] = out.trim().split('\n');
windowInfo = {
id,
title
};
}
return callback(err, windowInfo);
}
});
};
export { AutoTypeHelper };
@@ -1,25 +0,0 @@
import { AutoTypeNativeHelper } from 'auto-type/helper/auto-type-native-helper';
import { Launcher } from 'comp/launcher';
const AutoTypeHelper = function () {};
AutoTypeHelper.prototype.getActiveWindowInfo = function (callback) {
Launcher.spawn({
cmd: AutoTypeNativeHelper.getHelperPath(),
args: ['--window-info'],
complete(err, out) {
if (err) {
return callback(err);
}
const [id, title, url] = out.trim().split('\n');
const windowInfo = {
id,
title,
url
};
return callback(null, windowInfo);
}
});
};
export { AutoTypeHelper };
@@ -1,21 +0,0 @@
import { Launcher } from 'comp/launcher';
import { Logger } from 'util/logger';
const logger = new Logger('auto-type');
const AutoTypeNativeHelper = {
getHelperPath() {
if (this._helperPath) {
return this._helperPath;
}
const ext = process.platform === 'win32' ? '.exe' : '';
const helperRelPath = `helper/${process.platform}/KeeWebHelper${ext}`;
const helperPath = Launcher.getAppPath(helperRelPath);
Launcher.ensureRunnable(helperPath);
logger.debug('Using auto-type helper', helperPath);
this._helperPath = helperPath;
return helperPath;
}
};
export { AutoTypeNativeHelper };
+61 -65
View File
@@ -1,7 +1,7 @@
import { Events } from 'framework/events';
import { AutoTypeFilter } from 'auto-type/auto-type-filter';
import { AutoTypeHelperFactory } from 'auto-type/auto-type-helper-factory';
import { AutoTypeHelper } from 'auto-type/auto-type-helper';
import { AutoTypeParser } from 'auto-type/auto-type-parser';
import { SelectEntryFilter } from 'comp/app/select-entry-filter';
import { Launcher } from 'comp/launcher';
import { Features } from 'util/features';
import { Alerts } from 'comp/ui/alerts';
@@ -11,7 +11,7 @@ import { AppModel } from 'models/app-model';
import { Locale } from 'util/locale';
import { Logger } from 'util/logger';
import { Links } from 'const/links';
import { AutoTypeSelectView } from 'views/auto-type/auto-type-select-view';
import { SelectEntryView } from 'views/select/select-entry-view';
const logger = new Logger('auto-type');
const clearTextAutoTypeLog = !!localStorage.debugAutoType;
@@ -20,7 +20,6 @@ const AutoType = {
enabled: !!(Launcher && Launcher.autoTypeSupported),
supportsEventsWithWindowId: !!(Launcher && Launcher.platform() === 'linux'),
selectEntryView: false,
pendingEvent: null,
running: false,
init() {
@@ -28,10 +27,6 @@ const AutoType = {
return;
}
Events.on('auto-type', (e) => this.handleEvent(e));
Events.on('main-window-blur', (e) => this.mainWindowBlur(e));
Events.on('main-window-focus', (e) => this.mainWindowFocus(e));
Events.on('main-window-will-close', (e) => this.mainWindowWillClose(e));
Events.on('closed-open-view', (e) => this.processPendingEvent(e));
},
handleEvent(e) {
@@ -168,9 +163,8 @@ const AutoType = {
},
getActiveWindowInfo(callback) {
const helperType = AppSettingsModel.useLegacyAutoType ? 'legacy' : 'native';
logger.debug(`Getting window info using ${helperType} helper`);
const helper = AutoTypeHelperFactory.create();
logger.debug('Getting window info');
const helper = new AutoTypeHelper();
return helper.getActiveWindowInfo((err, windowInfo) => {
if (err) {
logger.error('Error getting window info', err);
@@ -217,25 +211,42 @@ const AutoType = {
},
selectEntryAndRun() {
this.getActiveWindowInfo((e, windowInfo) => {
const filter = new AutoTypeFilter(windowInfo, AppModel.instance);
this.getActiveWindowInfo(async (e, windowInfo) => {
const filter = new SelectEntryFilter(
windowInfo,
AppModel.instance,
AppModel.instance.files,
{
autoType: true
}
);
const evt = { filter, windowInfo };
if (!AppModel.instance.files.hasOpenFiles()) {
this.pendingEvent = evt;
logger.debug('auto-type event delayed');
this.focusMainWindow();
} else {
this.processEventWithFilter(evt);
try {
await AppModel.instance.unlockAnyFile('autoTypeUnlockMessage');
} catch {
logger.debug('auto-type event canceled');
return;
}
if (this.selectEntryView) {
this.selectEntryView.show();
}
}
logger.debug('processing auto-type event');
this.processEventWithFilter(evt);
});
},
focusMainWindow() {
setTimeout(() => Launcher.showMainWindow(), Timeouts.RedrawInactiveWindow);
if (!Launcher.isAppFocused()) {
setTimeout(() => Launcher.showMainWindow(), Timeouts.RedrawInactiveWindow);
}
},
processEventWithFilter(evt) {
const entries = evt.filter.getEntries();
let entries = evt.filter.getEntries();
if (entries.length === 1 && AppSettingsModel.directAutotype) {
this.hideWindow(() => {
this.runAndHandleResult({ entry: entries[0] }, evt.windowInfo.id);
@@ -243,8 +254,31 @@ const AutoType = {
return;
}
this.focusMainWindow();
evt.filter.ignoreWindowInfo = true;
this.selectEntryView = new AutoTypeSelectView({ filter: evt.filter });
if (entries.length === 0) {
if (evt.filter.useUrl) {
evt.filter.useUrl = false;
if (evt.filter.title && AppSettingsModel.autoTypeTitleFilterEnabled) {
evt.filter.useTitle = true;
}
}
entries = evt.filter.getEntries();
if (entries.length === 0 && evt.filter.useTitle) {
evt.filter.useTitle = false;
}
}
const humanReadableTarget = evt.filter.title || evt.filter.url;
const topMessage = humanReadableTarget
? Locale.autoTypeMsgMatchedByWindow.replace('{}', humanReadableTarget)
: Locale.autoTypeMsgNoWindow;
this.selectEntryView = new SelectEntryView({
isAutoType: true,
itemOptions: true,
filter: evt.filter,
topMessage
});
this.selectEntryView.on('result', (result) => {
logger.debug('Entry selected', result);
this.selectEntryView.off('result');
@@ -261,55 +295,17 @@ const AutoType = {
});
});
this.selectEntryView.render();
this.selectEntryView.on('show-open-files', () => {
this.selectEntryView.on('show-open-files', async () => {
this.selectEntryView.hide();
Events.emit('open-file');
});
},
mainWindowBlur() {
this.mainWindowBlurTimer = setTimeout(() => {
// macOS emits focus-blur-focus event in a row when triggering auto-type from minimized state
delete this.mainWindowBlurTimer;
this.resetPendingEvent();
if (this.selectEntryView) {
try {
await AppModel.instance.unlockAnyFile('autoTypeUnlockMessage');
} catch {
this.selectEntryView.emit('result', undefined);
return;
}
}, Timeouts.AutoTypeWindowFocusAfterBlur);
},
mainWindowFocus() {
if (this.mainWindowBlurTimer) {
clearTimeout(this.mainWindowBlurTimer);
this.mainWindowBlurTimer = null;
}
},
mainWindowWillClose() {
this.resetPendingEvent();
if (this.selectEntryView) {
this.selectEntryView.emit('result', undefined);
}
},
resetPendingEvent() {
if (this.pendingEvent) {
this.pendingEvent = null;
logger.debug('auto-type event canceled');
}
},
processPendingEvent() {
if (this.selectEntryView) {
this.selectEntryView.show();
}
if (!this.pendingEvent) {
return;
}
logger.debug('processing pending auto-type event');
const evt = this.pendingEvent;
this.pendingEvent = null;
this.processEventWithFilter(evt);
this.selectEntryView.render();
});
}
};
@@ -1,8 +0,0 @@
import { Collection } from 'framework/collection';
import { ExternalEntryModel } from 'models/external/external-entry-model';
class ExternalEntryCollection extends Collection {
static model = ExternalEntryModel;
}
export { ExternalEntryCollection };
+12
View File
@@ -1,4 +1,5 @@
import { AppSettingsModel } from 'models/app-settings-model';
import { RuntimeDataModel } from 'models/runtime-data-model';
const ExportApi = {
settings: {
@@ -11,6 +12,17 @@ const ExportApi = {
del(key) {
delete AppSettingsModel[key];
}
},
runtimeData: {
get(key) {
return key ? RuntimeDataModel[key] : { ...RuntimeDataModel };
},
set(key, value) {
RuntimeDataModel[key] = value;
},
del(key) {
delete RuntimeDataModel[key];
}
}
};
+12
View File
@@ -13,6 +13,18 @@ const GeneratorPresets = {
};
},
get browserExtensionPreset() {
return {
name: 'BrowserExtension',
length: 20,
upper: true,
lower: true,
special: true,
brackets: true,
ambiguous: true
};
},
get builtIn() {
return [
this.defaultPreset,
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Logger } from 'util/logger';
const logger = new Logger('online-password-checker');
@@ -0,0 +1,99 @@
import { SearchResultCollection } from 'collections/search-result-collection';
import { Ranking } from 'util/data/ranking';
const urlPartsRegex = /^(\w+:\/\/)?(?:(?:www|wwws|secure)\.)?([^\/]+)\/?(.*)/;
class SelectEntryFilter {
constructor(windowInfo, appModel, files, filterOptions) {
this.title = windowInfo.title;
this.useTitle = !!windowInfo.title && !windowInfo.url;
this.url = windowInfo.url;
this.useUrl = !!windowInfo.url;
this.subdomains = true;
this.text = '';
this.appModel = appModel;
this.files = files;
this.filterOptions = filterOptions;
}
getEntries() {
const filter = {
text: this.text,
...this.filterOptions
};
let entries = this.appModel
.getEntriesByFilter(filter, this.files)
.map((e) => [e, this._getEntryRank(e)]);
if (this.useUrl || this.useTitle) {
entries = entries.filter((e) => e[1]);
}
entries = entries.sort((x, y) =>
x[1] === y[1] ? x[0].title.localeCompare(y[0].title) : y[1] - x[1]
);
entries = entries.map((p) => p[0]);
return new SearchResultCollection(entries, { comparator: 'none' });
}
_getEntryRank(entry) {
let titleRank = 0;
let urlRank = 0;
if (this.useTitle && this.title && entry.title) {
titleRank = Ranking.getStringRank(entry.title.toLowerCase(), this.title.toLowerCase());
if (!titleRank) {
return 0;
}
}
if (this.useUrl && this.url) {
const searchUrlLower = this.url.toLowerCase();
const searchUrlParts = urlPartsRegex.exec(searchUrlLower);
for (const url of entry.getAllUrls()) {
const entryUrlParts = urlPartsRegex.exec(url.toLowerCase());
if (entryUrlParts) {
const [, scheme, domain, path] = entryUrlParts;
const [, searchScheme, searchDomain, searchPath] = searchUrlParts;
if (
domain === searchDomain ||
(this.subdomains && searchDomain.indexOf('.' + domain) > 0)
) {
if (domain === searchDomain) {
urlRank += 20;
} else {
urlRank += 10;
}
if (path === searchPath) {
urlRank += 10;
} else if (path && searchPath) {
if (path.lastIndexOf(searchPath, 0) === 0) {
urlRank += 5;
} else if (searchPath.lastIndexOf(path, 0) === 0) {
urlRank += 3;
}
}
if (scheme === searchScheme) {
urlRank += 1;
}
}
}
}
if (entry.searchText.includes(searchUrlLower)) {
// the url is in some field; include it
urlRank += 5;
}
}
if (this.useTitle && !titleRank) {
return 0;
}
if (this.useUrl && !urlRank) {
return 0;
}
return titleRank + urlRank;
}
}
export { SelectEntryFilter };
+1 -1
View File
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Events } from 'framework/events';
import { RuntimeInfo } from 'const/runtime-info';
import { Transport } from 'comp/browser/transport';
+2 -2
View File
@@ -203,7 +203,7 @@ const YubiKey = {
this.process = Launcher.spawn({
cmd: this.cmd(),
args: ['-d', serial, 'oath', 'code'],
args: ['-d', serial, 'oath', 'accounts', 'code'],
noStdOutLogging: true,
throwOnStdErr: true,
complete: (err, stdout) => {
@@ -241,7 +241,7 @@ const YubiKey = {
getOtp(serial, entry, callback) {
return Launcher.spawn({
cmd: this.cmd(),
args: ['-d', serial, 'oath', 'code', '--single', entry],
args: ['-d', serial, 'oath', 'accounts', 'code', '--single', entry],
noStdOutLogging: true,
complete: (err, stdout) => {
if (err) {
+2 -2
View File
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
const SecureInput = function () {
this.el = null;
@@ -79,7 +79,7 @@ Object.defineProperty(SecureInput.prototype, 'value', {
const len = pseudoValue.length;
let byteLength = 0;
const valueBytes = new Uint8Array(len * 4);
const saltBytes = kdbxweb.Random.getBytes(len * 4);
const saltBytes = kdbxweb.CryptoEngine.random(len * 4);
let ch;
let bytes;
for (let i = 0; i < len; i++) {
@@ -0,0 +1,248 @@
import { Launcher } from 'comp/launcher';
import { Logger } from 'util/logger';
import { ProtocolImpl } from './protocol-impl';
import { RuntimeInfo } from 'const/runtime-info';
import { AppSettingsModel } from 'models/app-settings-model';
import { Features } from 'util/features';
const WebConnectionInfo = {
connectionId: 1,
extensionName: 'KeeWeb Connect',
supportsNotifications: true
};
const SupportedExtensions = [
{ alias: 'KWC', name: 'KeeWeb Connect' },
{ alias: 'KPXC', name: 'KeePassXC-Browser' }
];
const SupportedBrowsers = ['Chrome', 'Firefox', 'Edge', 'Other'];
if (Features.isMac) {
SupportedBrowsers.unshift('Safari');
}
const logger = new Logger('browser-extension-connector');
if (!localStorage.debugBrowserExtension) {
logger.level = Logger.Level.Info;
}
const connections = new Map();
const pendingBrowserMessages = [];
let processingBrowserMessage = false;
const BrowserExtensionConnector = {
started: false,
logger,
init(appModel) {
const sendEvent = this.sendEvent.bind(this);
ProtocolImpl.init({ appModel, logger, sendEvent });
this.browserWindowMessage = this.browserWindowMessage.bind(this);
if (Launcher) {
const { ipcRenderer } = Launcher.electron();
ipcRenderer.on('browserExtensionSocketConnected', (e, socketId, connectionInfo) =>
this.socketConnected(socketId, connectionInfo)
);
ipcRenderer.on('browserExtensionSocketClosed', (e, socketId) =>
this.socketClosed(socketId)
);
ipcRenderer.on('browserExtensionSocketRequest', (e, socketId, request) =>
this.socketRequest(socketId, request)
);
AppSettingsModel.on('change', () => this.appSettingsChanged());
}
if (this.isEnabled()) {
this.start();
}
},
start() {
if (Launcher) {
this.startDesktopAppListener();
} else {
this.startWebMessageListener();
}
this.started = true;
},
stop() {
if (Launcher) {
this.stopDesktopAppListener();
} else {
this.stopWebMessageListener();
}
ProtocolImpl.cleanup();
connections.clear();
this.started = false;
},
appSettingsChanged() {
if (this.isEnabled()) {
if (!this.started) {
this.start();
}
} else if (this.started) {
this.stop();
}
},
isEnabled() {
if (!Launcher) {
return true;
}
for (const ext of SupportedExtensions) {
for (const browser of SupportedBrowsers) {
if (AppSettingsModel[`extensionEnabled${ext.alias}${browser}`]) {
return true;
}
}
}
return false;
},
startWebMessageListener() {
window.addEventListener('message', this.browserWindowMessage);
logger.info('Started');
},
stopWebMessageListener() {
window.removeEventListener('message', this.browserWindowMessage);
},
enable(browser, extension, enabled) {
const { ipcRenderer } = Launcher.electron();
ipcRenderer.invoke('browserExtensionConnectorEnable', browser, extension, enabled);
},
async startDesktopAppListener() {
const { ipcRenderer } = Launcher.electron();
ipcRenderer.invoke('browserExtensionConnectorStart', {
appleTeamId: RuntimeInfo.appleTeamId
});
},
stopDesktopAppListener() {
const { ipcRenderer } = Launcher.electron();
ipcRenderer.invoke('browserExtensionConnectorStop');
},
browserWindowMessage(e) {
if (e.origin !== location.origin) {
return;
}
if (e.source !== window) {
return;
}
if (e?.data?.kwConnect !== 'request') {
return;
}
logger.debug('Extension -> KeeWeb', e.data);
pendingBrowserMessages.push(e.data);
this.processBrowserMessages();
},
async processBrowserMessages() {
if (!pendingBrowserMessages.length || processingBrowserMessage) {
return;
}
if (!connections.has(WebConnectionInfo.connectionId)) {
connections.set(WebConnectionInfo.connectionId, WebConnectionInfo);
}
processingBrowserMessage = true;
const request = pendingBrowserMessages.shift();
const response = await ProtocolImpl.handleRequest(request, WebConnectionInfo);
processingBrowserMessage = false;
if (response) {
this.sendWebResponse(response);
}
this.processBrowserMessages();
},
sendWebResponse(response) {
logger.debug('KeeWeb -> Extension', response);
response.kwConnect = 'response';
postMessage(response, window.location.origin);
},
sendSocketEvent(data) {
const { ipcRenderer } = Launcher.electron();
ipcRenderer.invoke('browserExtensionConnectorSocketEvent', data);
},
sendSocketResult(socketId, data) {
const { ipcRenderer } = Launcher.electron();
ipcRenderer.invoke('browserExtensionConnectorSocketResult', socketId, data);
},
sendEvent(data) {
if (!this.isEnabled() || !connections.size) {
return;
}
if (Launcher) {
this.sendSocketEvent(data);
} else {
this.sendWebResponse(data);
}
},
socketConnected(socketId, connectionInfo) {
connections.set(socketId, connectionInfo);
},
socketClosed(socketId) {
connections.delete(socketId);
ProtocolImpl.deleteConnection(socketId);
},
async socketRequest(socketId, request) {
let result;
const connectionInfo = connections.get(socketId);
if (connectionInfo) {
result = await ProtocolImpl.handleRequest(request, connectionInfo);
} else {
const message = `Connection not found: ${socketId}`;
result = ProtocolImpl.errorToResponse({ message }, request);
}
this.sendSocketResult(socketId, result);
},
get sessions() {
return ProtocolImpl.sessions;
},
terminateConnection(connectionId) {
connectionId = +connectionId;
if (Launcher) {
const { ipcRenderer } = Launcher.electron();
ipcRenderer.invoke('browserExtensionConnectorCloseSocket', connectionId);
} else {
ProtocolImpl.deleteConnection(connectionId);
}
},
getClientPermissions(clientId) {
return ProtocolImpl.getClientPermissions(clientId);
},
setClientPermissions(clientId, permissions) {
ProtocolImpl.setClientPermissions(clientId, permissions);
}
};
export { BrowserExtensionConnector, SupportedExtensions, SupportedBrowsers };
+940
View File
@@ -0,0 +1,940 @@
import * as kdbxweb from 'kdbxweb';
import { Events } from 'framework/events';
import { Launcher } from 'comp/launcher';
import { box as tweetnaclBox } from 'tweetnacl';
import { PasswordGenerator } from 'util/generators/password-generator';
import { GeneratorPresets } from 'comp/app/generator-presets';
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 { ExtensionCreateGroupView } from 'views/extension/extension-create-group-view';
import { ExtensionSaveEntryView } from 'views/extension/extension-save-entry-view';
import { RuntimeDataModel } from 'models/runtime-data-model';
import { AppSettingsModel } from 'models/app-settings-model';
import { Timeouts } from 'const/timeouts';
import { SelectEntryView } from 'views/select/select-entry-view';
import { SelectEntryFieldView } from 'views/select/select-entry-field-view';
import { SelectEntryFilter } from 'comp/app/select-entry-filter';
const KeeWebAssociationId = 'KeeWeb';
const KeeWebHash = '398d9c782ec76ae9e9877c2321cbda2b31fc6d18ccf0fed5ca4bd746bab4d64a'; // sha256('KeeWeb')
const ExtensionGroupIconId = 1;
const DefaultExtensionGroupName = 'Browser';
const ExtensionGroupNames = new Set(['KeePassXC-Browser Passwords', DefaultExtensionGroupName]);
const Errors = {
noOpenFiles: {
message: Locale.extensionErrorNoOpenFiles,
code: '1'
},
userRejected: {
message: Locale.extensionErrorUserRejected,
code: '6'
},
noMatches: {
message: Locale.extensionErrorNoMatches,
code: '15'
}
};
const connectedClients = new Map();
let logger;
let appModel;
let sendEvent;
function setupListeners() {
Events.on('file-opened', () => {
sendEvent({ action: 'database-unlocked' });
});
Events.on('one-file-closed', () => {
if (!appModel.files.hasOpenFiles()) {
sendEvent({ action: 'database-locked' });
}
});
Events.on('all-files-closed', () => {
sendEvent({ action: 'database-locked' });
});
}
function incrementNonce(nonce) {
// from libsodium/utils.c, like it is in KeePassXC
let i = 0;
let c = 1;
for (; i < nonce.length; ++i) {
c += nonce[i];
nonce[i] = c;
c >>= 8;
}
}
function getClient(request) {
if (!request.clientID) {
throw new Error('Empty clientID');
}
const client = connectedClients.get(request.clientID);
if (!client) {
throw new Error(`Client not connected: ${request.clientID}`);
}
return client;
}
function decryptRequest(request) {
const client = getClient(request);
if (!request.nonce) {
throw new Error('Empty nonce');
}
if (!request.message) {
throw new Error('Empty message');
}
const nonce = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
const message = kdbxweb.ByteUtils.base64ToBytes(request.message);
const data = tweetnaclBox.open(message, nonce, client.publicKey, client.keys.secretKey);
if (!data) {
throw new Error('Failed to decrypt data');
}
const json = new TextDecoder().decode(data);
const payload = JSON.parse(json);
logger.debug('Extension -> KeeWeb -> (decrypted)', payload);
if (!payload) {
throw new Error('Empty request payload');
}
if (payload.action !== request.action) {
throw new Error(`Bad action in decrypted payload`);
}
return payload;
}
function encryptResponse(request, payload) {
logger.debug('KeeWeb -> Extension (decrypted)', payload);
const nonceBytes = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
incrementNonce(nonceBytes);
const nonce = kdbxweb.ByteUtils.bytesToBase64(nonceBytes);
const client = getClient(request);
payload.nonce = nonce;
const json = JSON.stringify(payload);
const data = new TextEncoder().encode(json);
const encrypted = tweetnaclBox(data, nonceBytes, client.publicKey, client.keys.secretKey);
const message = kdbxweb.ByteUtils.bytesToBase64(encrypted);
return {
action: request.action,
message,
nonce
};
}
function makeError(def) {
const e = new Error(def.message);
e.code = def.code;
return e;
}
function ensureAtLeastOneFileIsOpen() {
if (!appModel.files.hasOpenFiles()) {
throw makeError(Errors.noOpenFiles);
}
}
async function checkContentRequestPermissions(request) {
if (!appModel.files.hasOpenFiles()) {
if (AppSettingsModel.extensionFocusIfLocked) {
try {
focusKeeWeb();
await appModel.unlockAnyFile(
'extensionUnlockMessage',
Timeouts.KeeWebConnectRequest
);
} catch {
throw makeError(Errors.noOpenFiles);
}
} else {
throw makeError(Errors.noOpenFiles);
}
}
const client = getClient(request);
if (client.permissions) {
return;
}
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;
}
}
const extensionConnectView = new ExtensionConnectView({
extensionName: getHumanReadableExtensionName(client),
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({
...config,
enter: 'yes',
esc: '',
success: (res) => {
clearTimeout(inactivityTimer);
resolve(res);
},
cancel: () => {
clearTimeout(inactivityTimer);
reject(makeError(Errors.userRejected));
}
});
inactivityTimer = setTimeout(() => {
alert.closeWithResult('');
}, Timeouts.KeeWebConnectRequest);
});
}
function getAvailableFiles(request) {
const client = getClient(request);
if (!client.permissions) {
return;
}
const files = appModel.files.filter(
(file) =>
file.active &&
(client.permissions.allFiles || client.permissions.files.includes(file.id))
);
if (!files.length) {
throw makeError(Errors.noOpenFiles);
}
return files;
}
function getVersion(request) {
return isKeePassXcBrowser(request) ? KnownAppVersions.KeePassXC : RuntimeInfo.version;
}
function isKeeWebConnect(request) {
return getClient(request).connection.extensionName === 'KeeWeb Connect';
}
function isKeePassXcBrowser(request) {
return getClient(request).connection.extensionName === 'KeePassXC-Browser';
}
function getHumanReadableExtensionName(client) {
return client.connection.appName
? `${client.connection.extensionName} (${client.connection.appName})`
: client.connection.extensionName;
}
function focusKeeWeb() {
logger.debug('Focus KeeWeb');
if (Launcher) {
if (!Launcher.isAppFocused()) {
Launcher.showMainWindow();
}
} else {
sendEvent({ action: 'attention-required' });
}
}
async function findEntry(request, returnIfOneMatch, filterOptions) {
const payload = decryptRequest(request);
await checkContentRequestPermissions(request);
if (!payload.url) {
throw new Error('Empty url');
}
const files = getAvailableFiles(request);
const client = getClient(request);
const filter = new SelectEntryFilter(
{ url: payload.url, title: payload.title },
appModel,
files,
filterOptions
);
filter.subdomains = false;
let entries = filter.getEntries();
filter.subdomains = true;
let entry;
if (entries.length) {
if (entries.length === 1 && returnIfOneMatch && client.permissions.askGet === 'multiple') {
entry = entries[0];
}
} else {
entries = filter.getEntries();
if (!entries.length) {
if (AppSettingsModel.extensionFocusIfEmpty) {
filter.useUrl = false;
if (filter.title && AppSettingsModel.autoTypeTitleFilterEnabled) {
filter.useTitle = true;
entries = filter.getEntries();
if (!entries.length) {
filter.useTitle = false;
}
}
} else {
throw makeError(Errors.noMatches);
}
}
}
if (!entry) {
const extName = getHumanReadableExtensionName(client);
const topMessage = Locale.extensionSelectPasswordFor.replace('{}', extName);
const selectEntryView = new SelectEntryView({ filter, topMessage });
focusKeeWeb();
const inactivityTimer = setTimeout(() => {
selectEntryView.emit('result', undefined);
}, Timeouts.KeeWebConnectRequest);
const result = await selectEntryView.showAndGetResult();
clearTimeout(inactivityTimer);
entry = result?.entry;
if (!entry) {
throw makeError(Errors.userRejected);
}
}
client.stats.passwordsRead++;
return entry;
}
const ProtocolHandlers = {
'ping'({ data }) {
return { data };
},
'change-public-keys'(request, connection) {
let { publicKey, version, clientID: clientId } = request;
if (connectedClients.has(clientId)) {
throw new Error('Changing keys is not allowed');
}
if (!Launcher) {
// on web there can be only one connected client
connectedClients.clear();
}
const keys = tweetnaclBox.keyPair();
publicKey = kdbxweb.ByteUtils.base64ToBytes(publicKey);
const stats = {
connectedDate: new Date(),
passwordsRead: 0,
passwordsWritten: 0
};
connectedClients.set(clientId, { connection, publicKey, version, keys, stats });
Events.emit('browser-extension-sessions-changed');
logger.info('New client key created', clientId, version);
const nonceBytes = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
incrementNonce(nonceBytes);
const nonce = kdbxweb.ByteUtils.bytesToBase64(nonceBytes);
return {
action: 'change-public-keys',
version: getVersion(request),
publicKey: kdbxweb.ByteUtils.bytesToBase64(keys.publicKey),
nonce,
success: 'true',
...(isKeeWebConnect(request) ? { appName: 'KeeWeb' } : undefined)
};
},
async 'get-databasehash'(request) {
decryptRequest(request);
if (request.triggerUnlock) {
await checkContentRequestPermissions(request);
} else {
ensureAtLeastOneFileIsOpen();
}
return encryptResponse(request, {
hash: KeeWebHash,
success: 'true',
version: getVersion(request)
});
},
'generate-password'(request) {
const password = PasswordGenerator.generate(GeneratorPresets.browserExtensionPreset);
return encryptResponse(request, {
version: getVersion(request),
success: 'true',
entries: [{ password }]
});
},
'lock-database'(request) {
decryptRequest(request);
ensureAtLeastOneFileIsOpen();
Events.emit('lock-workspace');
if (Alerts.alertDisplayed) {
focusKeeWeb();
}
return encryptResponse(request, {
success: 'true',
version: getVersion(request)
});
},
'associate'(request) {
decryptRequest(request);
ensureAtLeastOneFileIsOpen();
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
hash: KeeWebHash,
id: KeeWebAssociationId
});
},
'test-associate'(request) {
const payload = decryptRequest(request);
// ensureAtLeastOneFileIsOpen();
if (payload.id !== KeeWebAssociationId) {
throw makeError(Errors.noOpenFiles);
}
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
hash: KeeWebHash,
id: payload.id
});
},
async 'get-logins'(request) {
const entry = await findEntry(request, true);
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
hash: KeeWebHash,
count: 1,
entries: [
{
group: entry.group.title,
login: entry.user || '',
name: entry.title || '',
password: entry.password?.getText() || '',
skipAutoSubmit: 'false',
stringFields: [],
uuid: kdbxweb.ByteUtils.bytesToHex(entry.entry.uuid.bytes)
}
],
id: ''
});
},
async 'get-totp-by-url'(request) {
const entry = await findEntry(request, true, { otp: true });
entry.initOtpGenerator();
if (!entry.otpGenerator) {
throw makeError(Errors.noMatches);
}
let selectEntryFieldView;
if (entry.needsTouch) {
selectEntryFieldView = new SelectEntryFieldView({
needsTouch: true,
deviceShortName: entry.device.shortName
});
selectEntryFieldView.render();
}
const otpPromise = new Promise((resolve, reject) => {
selectEntryFieldView?.on('result', () => reject(makeError(Errors.userRejected)));
entry.otpGenerator.next((err, otp) => {
if (otp) {
resolve(otp);
} else {
reject(err || makeError(Errors.userRejected));
}
});
});
let totp;
try {
totp = await otpPromise;
} finally {
selectEntryFieldView?.remove();
}
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
totp
});
},
async 'get-any-field'(request) {
const entry = await findEntry(request, false);
const selectEntryFieldView = new SelectEntryFieldView({
entry
});
const inactivityTimer = setTimeout(() => {
selectEntryFieldView.emit('result', undefined);
}, Timeouts.KeeWebConnectRequest);
const field = await selectEntryFieldView.showAndGetResult();
clearTimeout(inactivityTimer);
if (!field) {
throw makeError(Errors.userRejected);
}
let value = entry.getAllFields()[field];
if (value.isProtected) {
value = value.getText();
}
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
field,
value
});
},
async 'get-totp'(request) {
decryptRequest(request);
await checkContentRequestPermissions(request);
throw new Error('Not implemented');
},
async 'set-login'(request) {
const payload = decryptRequest(request);
await checkContentRequestPermissions(request);
focusKeeWeb();
if (!payload.url) {
throw new Error('Empty url');
}
const url = new URL(payload.url);
const files = getAvailableFiles(request);
const client = getClient(request);
let selectedGroup;
let entryToUpdate;
if (payload.uuid) {
for (const file of files) {
const entryId = kdbxweb.ByteUtils.bytesToBase64(
kdbxweb.ByteUtils.hexToBytes(payload.uuid)
);
const foundEntry = file.getEntry(file.subId(entryId));
if (foundEntry) {
if (entryToUpdate) {
throw new Error('Two entries with the same ID found');
} else {
entryToUpdate = foundEntry;
selectedGroup = foundEntry.group;
}
}
}
if (!entryToUpdate) {
throw new Error('Updated entry not found');
}
}
if (client.permissions.askSave === 'auto' && client.permissions.saveTo && !selectedGroup) {
const file = files.find((f) => f.id === client.permissions.saveTo.fileId);
selectedGroup = file?.getGroup(client.permissions.saveTo.groupId);
}
if (client.permissions.askSave !== 'auto' || !selectedGroup) {
if (!selectedGroup && RuntimeDataModel.extensionSaveConfig) {
const file = files.find(
(f) => f.id === RuntimeDataModel.extensionSaveConfig.fileId
);
selectedGroup = file?.getGroup(RuntimeDataModel.extensionSaveConfig.groupId);
}
const allGroups = [];
for (const file of files) {
file.forEachGroup((group) => {
const spaces = [];
for (let parent = group; parent.parentGroup; parent = parent.parentGroup) {
spaces.push(' ', ' ');
}
if (
!selectedGroup &&
group.iconId === ExtensionGroupIconId &&
ExtensionGroupNames.has(group.title)
) {
selectedGroup = group;
}
allGroups.push({
id: group.id,
fileId: file.id,
spaces,
title: group.title,
selected: group.id === selectedGroup?.id
});
});
}
if (!selectedGroup) {
allGroups.splice(1, 0, {
id: '',
fileId: files[0].id,
spaces: [' ', ' '],
title: `${DefaultExtensionGroupName} (${Locale.extensionSaveEntryNewGroup})`,
selected: true
});
}
const saveEntryView = new ExtensionSaveEntryView({
extensionName: getHumanReadableExtensionName(client),
url: payload.url,
user: payload.login,
askSave: RuntimeDataModel.extensionSaveConfig?.askSave || 'always',
update: !!entryToUpdate,
allGroups
});
await alertWithTimeout({
header: Locale.extensionSaveEntryHeader,
icon: 'plus',
buttons: [Alerts.buttons.allow, Alerts.buttons.deny],
view: saveEntryView
});
const config = { ...saveEntryView.config };
if (!entryToUpdate) {
if (config.groupId) {
const file = files.find((f) => f.id === config.fileId);
selectedGroup = file.getGroup(config.groupId);
} else {
selectedGroup = appModel.createNewGroupWithName(
files[0].groups[0],
files[0],
DefaultExtensionGroupName
);
selectedGroup.setIcon(ExtensionGroupIconId);
config.groupId = selectedGroup.id;
}
RuntimeDataModel.extensionSaveConfig = config;
client.permissions.saveTo = { fileId: config.fileId, groupId: config.groupId };
}
client.permissions.askSave = config.askSave;
}
const entryFields = {
Title: url.hostname,
UserName: payload.login,
Password: kdbxweb.ProtectedValue.fromString(payload.password || ''),
URL: payload.url
};
if (entryToUpdate) {
for (const [field, value] of Object.entries(entryFields)) {
if (value) {
entryToUpdate.setField(field, value);
}
}
} else {
appModel.createNewEntryWithFields(selectedGroup, entryFields);
}
client.stats.passwordsWritten++;
Events.emit('browser-extension-sessions-changed');
Events.emit('refresh');
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
count: null,
entries: null,
hash: KeeWebHash
});
},
async 'get-database-groups'(request) {
decryptRequest(request);
await checkContentRequestPermissions(request);
const makeGroups = (group) => {
const res = {
name: group.title,
uuid: kdbxweb.ByteUtils.bytesToHex(group.group.uuid.bytes),
children: []
};
for (const subGroup of group.items) {
if (subGroup.matches()) {
res.children.push(makeGroups(subGroup));
}
}
return res;
};
const groups = [];
for (const file of getAvailableFiles(request)) {
for (const group of file.groups) {
groups.push(makeGroups(group));
}
}
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
groups: { groups }
});
},
async 'create-new-group'(request) {
const payload = decryptRequest(request);
await checkContentRequestPermissions(request);
if (!payload.groupName) {
throw new Error('No groupName');
}
const groupNames = payload.groupName
.split('/')
.map((g) => g.trim())
.filter((g) => g);
if (!groupNames.length) {
throw new Error('Empty group path');
}
const files = getAvailableFiles(request);
for (const file of files) {
for (const rootGroup of file.groups) {
let foundGroup = rootGroup;
const pendingGroups = [...groupNames];
while (pendingGroups.length && foundGroup) {
const title = pendingGroups.shift();
foundGroup = foundGroup.items.find((g) => g.title === title);
}
if (foundGroup) {
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
name: foundGroup.title,
uuid: kdbxweb.ByteUtils.bytesToHex(foundGroup.group.uuid.bytes)
});
}
}
}
const client = getClient(request);
const createGroupView = new ExtensionCreateGroupView({
extensionName: getHumanReadableExtensionName(client),
groupPath: groupNames.join(' / '),
files: files.map((f, ix) => ({ id: f.id, name: f.name, selected: ix === 0 }))
});
await alertWithTimeout({
header: Locale.extensionNewGroupHeader,
icon: 'folder-plus',
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)
});
}
};
const ProtocolImpl = {
init(vars) {
appModel = vars.appModel;
logger = vars.logger;
sendEvent = vars.sendEvent;
setupListeners();
},
cleanup() {
const wasNotEmpty = connectedClients.size;
connectedClients.clear();
if (wasNotEmpty) {
Events.emit('browser-extension-sessions-changed');
}
},
deleteConnection(connectionId) {
for (const [clientId, client] of connectedClients.entries()) {
if (client.connection.connectionId === connectionId) {
connectedClients.delete(clientId);
}
}
Events.emit('browser-extension-sessions-changed');
},
getClientPermissions(clientId) {
return connectedClients.get(clientId)?.permissions;
},
setClientPermissions(clientId, permissions) {
const client = connectedClients.get(clientId);
if (client?.permissions) {
client.permissions = { ...client.permissions, ...permissions };
}
},
errorToResponse(e, request) {
return {
action: request?.action,
error: e.message || 'Unknown error',
errorCode: e.code || 0
};
},
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}`);
}
result = await handler(request, connectionInfo);
if (!result) {
throw new Error(`Handler returned an empty result: ${request.action}`);
}
} catch (e) {
if (!e.code) {
logger.error(`Error in handler ${request.action}`, e);
}
result = this.errorToResponse(e, request);
}
if (!appWindowWasFocused && Launcher?.isAppFocused()) {
Launcher.hideApp();
}
return result;
},
get sessions() {
return [...connectedClients.entries()]
.map(([clientId, client]) => ({
clientId,
connectionId: client.connection.connectionId,
appName: client.connection.appName,
extensionName: client.connection.extensionName,
connectedDate: client.stats.connectedDate,
passwordsRead: client.stats.passwordsRead,
passwordsWritten: client.stats.passwordsWritten,
permissions: client.permissions,
permissionsDenied: client.permissionsDenied
}))
.sort((x, y) => y.connectedDate - x.connectedDate);
}
};
export { ProtocolImpl };
+5 -5
View File
@@ -1,5 +1,5 @@
/* eslint-disable import/no-commonjs */
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { RuntimeInfo } from 'const/runtime-info';
import { Links } from 'const/links';
import { DateFormat } from 'comp/i18n/date-format';
@@ -51,14 +51,14 @@ function walkEntry(db, entry, parents) {
});
}
}
for (const fieldName of Object.keys(entry.fields)) {
for (const [fieldName, fieldValue] of entry.fields) {
if (!KnownFields[fieldName]) {
const value = entryField(entry, fieldName);
if (value) {
fields.push({
title: fieldName,
value,
protect: entry.fields[fieldName].isProtected
protect: fieldValue.isProtected
});
}
}
@@ -69,7 +69,7 @@ function walkEntry(db, entry, parents) {
expires = DateFormat.dtStr(entry.times.expiryTime);
}
const attachments = Object.entries(entry.binaries)
const attachments = [...entry.binaries]
.map(([name, data]) => {
if (data && data.ref) {
data = data.value;
@@ -95,7 +95,7 @@ function walkEntry(db, entry, parents) {
}
function entryField(entry, fieldName) {
const value = entry.fields[fieldName];
const value = entry.fields.get(fieldName);
return (value && value.isProtected && value.getText()) || value || '';
}
+1 -1
View File
@@ -24,7 +24,7 @@ const DateFormat = {
const format = new Intl.DateTimeFormat(SettingsManager.activeLocale, { weekday: 'short' });
const weekdays = [];
for (let day = 1; day < 8; day++) {
weekdays.push(format.format(new Date(Date.UTC(2007, 9, 6 + day))));
weekdays.push(format.format(new Date(2007, 9, 6 + day)));
}
return weekdays;
},
+26 -63
View File
@@ -153,17 +153,6 @@ const Launcher = {
saveConfig(name, data) {
return this.remoteApp().saveConfig(name, data);
},
ensureRunnable(path) {
if (process.platform !== 'win32') {
const fs = this.req('fs');
const stat = fs.statSync(path);
if ((stat.mode & 0o0111) === 0) {
const mode = stat.mode | 0o0100;
logger.info(`chmod 0${mode.toString(8)} ${path}`);
fs.chmodSync(path, mode);
}
}
},
preventExit(e) {
e.returnValue = false;
return false;
@@ -202,7 +191,7 @@ const Launcher = {
}
},
quitOnRealQuitEventIfMinimizeOnQuitIsEnabled() {
return !!(this.platform() === 'darwin' || this.pendingUpdateFile);
return !!this.pendingUpdateFile;
},
minimizeApp() {
this.remoteApp().minimizeApp({
@@ -244,57 +233,32 @@ const Launcher = {
},
spawn(config) {
const ts = logger.ts();
let complete = config.complete;
const ps = this.req('child_process').spawn(config.cmd, config.args);
[ps.stdin, ps.stdout, ps.stderr].forEach((s) => s.setEncoding('utf-8'));
let stderr = '';
let stdout = '';
ps.stderr.on('data', (d) => {
stderr += d.toString('utf-8');
if (config.throwOnStdErr) {
try {
ps.kill();
} catch {}
}
});
ps.stdout.on('data', (d) => {
stdout += d.toString('utf-8');
});
ps.on('close', (code) => {
stdout = stdout.trim();
stderr = stderr.trim();
const msg = 'spawn ' + config.cmd + ': ' + code + ', ' + logger.ts(ts);
if (code !== 0) {
logger.error(msg + '\n' + stdout + '\n' + stderr);
} else {
logger.info(msg + (stdout && !config.noStdOutLogging ? '\n' + stdout : ''));
}
if (complete) {
complete(code !== 0 ? 'Exit code ' + code : null, stdout, code);
const { ipcRenderer } = this.electron();
let { complete } = config;
delete config.complete;
ipcRenderer
.invoke('spawnProcess', config)
.then((res) => {
if (res.err) {
logger.error('spawn error: ' + config.cmd + ', ' + logger.ts(ts), res.err);
complete?.(res.err);
} else {
const code = res.code;
const stdout = res.stdout || '';
const stderr = res.stderr || '';
const msg = 'spawn ' + config.cmd + ': ' + code + ', ' + logger.ts(ts);
if (code !== 0) {
logger.error(msg + '\n' + stdout + '\n' + stderr);
} else {
logger.info(msg + (stdout && !config.noStdOutLogging ? '\n' + stdout : ''));
}
complete?.(code !== 0 ? 'Exit code ' + code : null, stdout, code);
}
complete = null;
}
});
ps.on('error', (err) => {
logger.error('spawn error: ' + config.cmd + ', ' + logger.ts(ts), err);
if (complete) {
complete(err);
complete = null;
}
});
if (config.data) {
try {
ps.stdin.end(config.data);
} catch (e) {
logger.error('spawn write error', e);
}
}
process.nextTick(() => {
// it should work without destroy, but a process doesn't get launched
// xubuntu-desktop 19.04 / xfce
// see https://github.com/keeweb/keeweb/issues/1234
ps.stdin.destroy();
});
return ps;
})
.catch((err) => {
complete?.(err);
});
},
checkOpenFiles() {
this.readyToOpenFiles = true;
@@ -335,7 +299,6 @@ Events.on('launcher-maximize', () => setTimeout(() => Events.emit('app-maximized
Events.on('launcher-unmaximize', () => setTimeout(() => Events.emit('app-unmaximized'), 0));
Events.on('launcher-started-minimized', () => setTimeout(() => Launcher.minimizeApp(), 0));
Events.on('start-profile', (data) => StartProfiler.reportAppProfile(data));
Events.on('log', (e) => new Logger(e.category || 'remote-app')[e.method || 'info'](e.message));
window.launcherOpen = (file) => Launcher.openFile(file);
if (window.launcherOpenedFile) {
+14 -1
View File
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Events } from 'framework/events';
import { Logger } from 'util/logger';
import { Launcher } from 'comp/launcher';
@@ -20,6 +20,7 @@ if (Launcher) {
ipcRenderer.on('nativeModuleHostError', (e, err) => NativeModules.hostError(err));
ipcRenderer.on('nativeModuleHostExit', (e, { code, sig }) => NativeModules.hostExit(code, sig));
ipcRenderer.on('nativeModuleHostDisconnect', () => NativeModules.hostDisconnect());
ipcRenderer.on('log', (e, ...args) => NativeModules.log(...args));
const handlers = {
yubikeys(numYubiKeys) {
@@ -118,6 +119,14 @@ if (Launcher) {
}
},
log(name, level, ...args) {
if (!name) {
return;
}
const logger = new Logger(name);
logger[level](...args);
},
autoRestartHost() {
setTimeout(() => {
try {
@@ -176,6 +185,10 @@ if (Launcher) {
return this.call('argon2', password, salt, options);
},
hardwareCryptoDeleteKey: async () => {
await ipcRenderer.invoke('hardwareCryptoDeleteKey');
},
hardwareEncrypt: async (value) => {
const { data, salt } = await ipcRenderer.invoke('hardwareEncrypt', value.dataAndSalt());
return new kdbxweb.ProtectedValue(data, salt);
@@ -4,6 +4,7 @@ import { Locale } from 'util/locale';
import { ThemeWatcher } from 'comp/browser/theme-watcher';
import { AppSettingsModel } from 'models/app-settings-model';
import { Logger } from 'util/logger';
import { Launcher } from 'comp/launcher';
const logger = new Logger('settings-manager');
@@ -164,6 +165,20 @@ const SettingsManager = {
Object.assign(Locale, this.neutralLocale, localeValues);
this.activeLocale = loc;
Events.emit('set-locale', loc);
if (Launcher) {
const { ipcRenderer } = Launcher.electron();
const localeValuesForDesktopApp = {};
for (const [key, value] of Object.entries(Locale)) {
if (key.startsWith('sysMenu')) {
localeValuesForDesktopApp[key] = value;
}
}
ipcRenderer.invoke('setLocale', {
locale: loc,
...localeValuesForDesktopApp
});
}
},
getBrowserLocale() {
+12
View File
@@ -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;
}
}
},
+14 -1
View File
@@ -37,4 +37,17 @@ const OneDriveApps = {
}
};
export { DropboxApps, GDriveApps, OneDriveApps };
const MsTeamsApps = {
Local: {
id: '8fbe2245-13d5-446f-bedc-74c3b2e1f635'
},
Production: {
id: '8fbe2245-13d5-446f-bedc-74c3b2e1f635'
},
Desktop: {
id: '8fbe2245-13d5-446f-bedc-74c3b2e1f635',
secret: 'F02~HYaWs-~7MndJcVRtv9~h-50Brk_9ho'
}
};
export { DropboxApps, GDriveApps, OneDriveApps, MsTeamsApps };
+12 -3
View File
@@ -9,7 +9,7 @@ const DefaultAppSettings = {
autoUpdate: 'install', // auto-update options: "install", "check", ""
clipboardSeconds: 0, // number of seconds after which the clipboard will be cleared
autoSave: true, // auto-save open files
autoSaveInterval: 0, // interval between performing automatic sync, minutes
autoSaveInterval: 0, // interval between performing automatic sync, minutes, -1: on every change
rememberKeyFiles: 'path', // remember keyfiles selected on the Open screen
idleMinutes: 15, // app lock timeout after inactivity, minutes
minimizeOnClose: false, // minimise the app instead of closing
@@ -18,6 +18,7 @@ const DefaultAppSettings = {
colorfulIcons: false, // use colorful custom icons instead of grayscale
useMarkdown: true, // use Markdown in Notes field
directAutotype: true, // if only one matching entry is found, select that one automatically
autoTypeTitleFilterEnabled: true, // enable the title filtering in auto-type by default
titlebarStyle: 'default', // window titlebar style
lockOnMinimize: true, // lock the app when it's minimized
lockOnCopy: false, // lock the app after a password was copied
@@ -43,10 +44,12 @@ const DefaultAppSettings = {
excludePinsFromAudit: true, // exclude PIN codes from audit
checkPasswordsOnHIBP: false, // check passwords on Have I Been Pwned
auditPasswordAge: 0, // show warnings about old passwords, number of years, 0 = disabled
useLegacyAutoType: false, // use legacy auto-type engine (will be removed in future versions)
deviceOwnerAuth: null, // Touch ID: null / 'memory' / 'file'
deviceOwnerAuthTimeoutMinutes: 0, // how often master password is required with Touch ID
disableOfflineStorage: false, // don't cache loaded files in offline storage
shortLivedStorageToken: false, // short-lived sessions in cloud storage providers
extensionFocusIfLocked: true, // focus KeeWeb if a browser extension tries to connect while KeeWeb is locked
extensionFocusIfEmpty: true, // show the entry selection screen if there's no match found by URL
yubiKeyShowIcon: true, // show an icon to open OTP codes from YubiKey
yubiKeyAutoOpen: false, // auto-load one-time codes when there are open files
@@ -84,7 +87,13 @@ const DefaultAppSettings = {
onedrive: true, // enable OneDrive integration
onedriveClientId: null, // custom OneDrive client id
onedriveClientSecret: null // custom OneDrive client secret
onedriveClientSecret: null, // custom OneDrive client secret
onedriveTenantId: null, // custom OneDrive tenant id
msteams: false, // enable Microsoft Teams integration
msteamsClientId: null, // custom Microsoft Teams client id
msteamsClientSecret: null, // custom Microsoft Teams client secret
msteamsTenantId: null // custom Microsoft Teams tenant id
};
export { DefaultAppSettings };
+5
View File
@@ -0,0 +1,5 @@
const KnownAppVersions = {
KeePassXC: '2.6.4'
};
export { KnownAppVersions };
+14 -1
View File
@@ -20,7 +20,20 @@ const Links = {
YubiKeyManual: 'https://github.com/keeweb/keeweb/wiki/YubiKey',
YubiKeyManagerInstall: 'https://github.com/Yubico/yubikey-manager#installation',
HaveIBeenPwned: 'https://haveibeenpwned.com',
HaveIBeenPwnedPrivacy: 'https://haveibeenpwned.com/Passwords'
HaveIBeenPwnedPrivacy: 'https://haveibeenpwned.com/Passwords',
ExtensionHelpForOtherBrowsers:
'https://github.com/keeweb/keeweb/wiki/Browser-AutoFill#other-browsers',
KWCForChrome:
'https://chrome.google.com/webstore/detail/keeweb-connect/pikpfmjfkekaeinceagbebpfkmkdlcjk',
KWCForFirefox: 'https://addons.mozilla.org/firefox/addon/keeweb-connect/',
KWCForEdge:
'https://microsoftedge.microsoft.com/addons/detail/keewebconnect/nmggpehkjmeaeocmaijenpejbepckinm',
KWCForSafari: 'https://apps.apple.com/app/keeweb-connect/id1565748094',
KPXCForChrome:
'https://chrome.google.com/webstore/detail/keepassxc-browser/oboonakemofpalcgghocfoadofidjkkk',
KPXCForFirefox: 'https://addons.mozilla.org/firefox/addon/keepassxc-browser/',
KPXCForEdge:
'https://microsoftedge.microsoft.com/addons/detail/keepassxcbrowser/pdffhmdngciaglkoonimfcmckehcpafo'
};
export { Links };
+2 -1
View File
@@ -3,7 +3,8 @@ const RuntimeInfo = {
beta: !!'@@BETA',
buildDate: '@@DATE',
commit: '@@COMMIT',
devMode: '@@DEVMODE'
devMode: '@@DEVMODE',
appleTeamId: '@@APPLE_TEAM_ID'
};
export { RuntimeInfo };
+2 -1
View File
@@ -18,7 +18,8 @@ const Timeouts = {
FieldLabelDoubleClick: 300,
NativeModuleHostRestartTime: 3000,
FastAnimation: 100,
AutoTypeCopyPaste: 300
AutoTypeCopyPaste: 300,
KeeWebConnectRequest: 60000
};
export { Timeouts };
+1 -1
View File
@@ -251,7 +251,6 @@ class View extends EventEmitter {
FocusManager.setModal(null);
}
}
this.emit(visible ? 'show' : 'hide');
if (this.el) {
this.el.classList.toggle('show', !!visible);
this.el.classList.toggle('hide', !visible);
@@ -259,6 +258,7 @@ class View extends EventEmitter {
Tip.hideTips(this.el);
}
}
this.emit(visible ? 'show' : 'hide');
}
isHidden() {
File diff suppressed because it is too large Load Diff
+58 -2
View File
@@ -14,6 +14,7 @@
"or": "oder",
"history": "Verlauf",
"template": "Vorlage",
"templates": "Vorlagen",
"notImplemented": "Nicht implementiert",
"saveChanges": "Änderungen speichern",
"discardChanges": "Änderungen verwerfen",
@@ -56,6 +57,22 @@
"menuItemCollapsed": "Doppelklick zum Aufklappen",
"menuRestoreApp": "{} öffnen",
"menuQuitApp": "{} beenden",
"sysMenuAboutKeeWeb": "Über {}",
"sysMenuServices": "Dienste",
"sysMenuHide": "{} ausblenden",
"sysMenuHideOthers": "Andere ausblenden",
"sysMenuUnhide": "Alle anzeigen",
"sysMenuQuit": "{} beenden",
"sysMenuEdit": "Bearbeiten",
"sysMenuUndo": "Rückgängig",
"sysMenuRedo": "Wiederherstellen",
"sysMenuCut": "Ausschneiden",
"sysMenuCopy": "Kopieren",
"sysMenuPaste": "Einfügen",
"sysMenuSelectAll": "Alles auswählen",
"sysMenuWindow": "Fenster",
"sysMenuMinimize": "Minimieren",
"sysMenuClose": "Fenster schließen",
"alertYes": "Ja",
"alertNo": "Nein",
"alertOk": "OK",
@@ -228,7 +245,7 @@
"detHistoryDiscardChangesAlertBody": "Ungespeicherte Änderungen gehen dabei unwiderruflich verloren.",
"detBackToList": "zurück zur Liste",
"detSetIconColor": "Farbe ändern",
"detSetIcon": "Icon ändern",
"detSetIcon": "Symbol ändern",
"detDropAttachments": "Anhänge hier ablegen",
"detDelEntry": "Löschen",
"detDelEntryPerm": "Dauerhaft löschen",
@@ -295,6 +312,16 @@
"detRevealField": "Anzeigen",
"detHideField": "Verbergen",
"detAutoTypeField": "Auto-Type",
"detIssuesHideTooltip": "Diese Warnung verstecken",
"detIssueWeakPassword": "Das Passwort ist schwach, es wird empfohlen es zu ändern.",
"detIssuePoorPassword": "Das Passwort ist schwach, es wird dringend empfohlen es zu ändern.",
"detIssuePwnedPassword": "Dieses Passwort wurde laut {} in einem Datenleck gefunden, es wird empfohlen das Passwort zu ändern.",
"detIssuePasswordCheckError": "Es gab einen Fehler bei der online-überprüfung des Passwort.",
"detIssueOldPassword": "Das Passwort ist alt.",
"detIssueCloseAlertHeader": "Verstecke Passwortmeldungen",
"detIssueCloseAlertBody": "Es gibt verschiedene Möglichkeiten diese Warnung zu verstecken:",
"detIssueCloseAlertEntry": "Nicht für diesen Eintrag anzeigen.",
"detIssueCloseAlertSettings": "Globale Einstellungen anpassen",
"autoTypeEntryFields": "Feld-Platzhalter",
"autoTypeModifiers": "Hilfstasten",
"autoTypeKeys": "Tasten",
@@ -325,6 +352,8 @@
"appSaveError": "Fehler beim Speichern",
"appSaveErrorBody": "Automatisches Speichern fehlgeschlagen",
"appSaveErrorBodyMul": "Automatisches Speichern fehlgeschlagen:",
"appSaveErrorExitLoseChanges": "Schließen und alle Änderungen verlieren",
"appSaveErrorExitLoseChangesBody": "Du kannst entweder die App schließen und alle Änderungen verlieren, oder gehe in die Dateieinstellungen und exportiere die Daten",
"appSettingsError": "Fehler beim Laden der App",
"appSettingsErrorBody": "Es gab einen Fehler beim Laden der App-Einstellungen. Bitte überprüfen Sie die App-URL oder kontaktieren Sie Ihren Systemadministrator.",
"appNotSupportedError": "Ihr Browser unterstützt einige wichtige Funktionen nicht, die für diese App benötigt werden.",
@@ -360,12 +389,26 @@
"setGenDownloadAndRestart": "Update herunterladen und App neu starten",
"setGenAppearance": "Oberfläche",
"setGenTheme": "Theme",
"setGenThemeDefault": "Standard",
"setGenThemeDark": "Dunkel",
"setGenThemeLight": "Hell",
"setGenThemeBlue": "Flach blau",
"setGenThemeFb": "Mattblau",
"setGenThemeBl": "Hell blau",
"setGenThemeBrown": "Brownie",
"setGenThemeDb": "Dunkelbraun",
"setGenThemeLb": "Hell braun",
"setGenThemeTerminal": "Terminal",
"setGenThemeTe": "Terminal",
"setGenThemeLt": "Hell Terminal",
"setGenThemeHighContrast": "Hoher Kontrast",
"setGenThemeHc": "Hoher Kontrast",
"setGenThemeDc": "Dunkel Kontrast",
"setGenThemeSol": "Solarized",
"setGenThemeSd": "Solarisiert dunkel",
"setGenThemeSl": "Solarisiert hell",
"setGenMoreThemes": "Mehr Themes",
"setGenAutoSwitchTheme": "Automatisch zwischen hellem und dunklem Thema umschalten, sofern möglich",
"setGenLocale": "Sprache",
"setGenLocOther": "Andere Sprachen sind als Plugins verfügbar.",
"setGenFontSize": "Schriftgröße",
@@ -408,6 +451,7 @@
"setGenLockAutoType": "Bei Auto-Type",
"setGenLockOrSleep": "Bei Aktivierung von Bildschirmsperre oder Ruhezustand",
"setGenStorage": "Synchronisierung",
"setGenDisableOfflineStorage": "Geladene Dateien nicht in Offline-Speicher zwischenspeichern",
"setGenStorageLogout": "Abmelden",
"setGenShowAdvanced": "Erweiterte Einstellungen anzeigen",
"setGenDevTools": "Entwicklerwerkzeuge anzeigen",
@@ -422,6 +466,16 @@
"setGenTouchIdMemory": "Nur mit Fingerabdruck entsperren, wenn KeeWeb aktiv ist",
"setGenTouchIdFile": "Benutze immer den Fingerabdruck anstatt das Master-Passwort",
"setGenTouchIdPass": "Benötige Master-Passwort nach",
"setGenAudit": "Audit",
"setGenAuditPasswords": "Warnungen für schwache Passwörter anzeigen",
"setGenAuditPasswordEntropy": "Passwortlänge und Zufälligkeit prüfen",
"setGenExcludePinsFromAudit": "Kurze Pin-Codes niemals prüfen (z. B. 12345)",
"setGenHelpHIBP": "KeeWeb kann überprüfen ob deine Passwörter bei einem früheren Datenleck veröffentlicht wurden. Deine Passwörter können nicht beim übertragen ausgelesen werden, aber die Anzahl der überprüften Passwörter kann möglicherweise entdeckt werden. {} kannst du mehr über den Datenschutz beim Nutzen dieses Dienstes erfahren. Wenn diese Option aktiviert ist überprüft KeeWeb deine Passwörter automatisch.",
"setGenHelpHIBPLink": "hier",
"setGenAuditPasswordAge": "Alte Passwörter",
"setGenAuditPasswordAgeOff": "Zeige keine Warnungen über alte Passwörter",
"setGenAuditPasswordAgeOneYear": "Zeige Warnungen für Passwörter die über ein Jahr alt sind",
"setGenAuditPasswordAgeYears": "Zeige Warnungen für Passwörter die über {} Jahre alt sind",
"setFilePath": "Dateipfad",
"setFileStorage": "Diese Datei wird von {} geladen.",
"setFileIntl": "Diese Datei ist im internen App-Speicher abgelegt",
@@ -635,5 +689,7 @@
"yubiKeyTouchRequestedHeader": "Bitte den YubiKey berühren",
"yubiKeyTouchRequestedBody": "Bitte den YubiKey mit der Seriennummer {} berühren",
"yubiKeyDisabledErrorHeader": "USB ist deaktiviert",
"yubiKeyDisabledErrorBody": "YubiKey ist zum Öffnen dieser Datei erforderlich. Bitte USB-Geräte in den Einstellungen aktivieren."
"yubiKeyDisabledErrorBody": "YubiKey ist zum Öffnen dieser Datei erforderlich. Bitte USB-Geräte in den Einstellungen aktivieren.",
"yubiKeyErrorWithCode": "YubiKey-Fehler, Code {}.",
"bioOpenAuthPrompt": "„{}“ öffnen"
}
+31 -6
View File
@@ -51,11 +51,27 @@
"menuAlertNoTags": "Aucun tag",
"menuAlertNoTagsBody": "Vous pouvez ajouter de nouveaux tags en éditant les champs, dans la section Tags.",
"menuEmptyTrash": "Vider la Corbeille",
"menuEmptyTrashAlert": "Vider la Corbeille?",
"menuEmptyTrashAlert": "Vider la Corbeille ?",
"menuEmptyTrashAlertBody": "Vous ne pourrez plus récupérer ces éléments",
"menuItemCollapsed": "Double-cliquez pour déplier",
"menuRestoreApp": "Ouvrir {}",
"menuQuitApp": "Quitter {}",
"sysMenuAboutKeeWeb": "A propos {}",
"sysMenuServices": "Services",
"sysMenuHide": "Cacher {}",
"sysMenuHideOthers": "Cacher autres",
"sysMenuUnhide": "Montrer tous",
"sysMenuQuit": "Quitter {}",
"sysMenuEdit": "Editer",
"sysMenuUndo": "Annuler",
"sysMenuRedo": "Répéter",
"sysMenuCut": "Couper",
"sysMenuCopy": "Copier",
"sysMenuPaste": "Coller",
"sysMenuSelectAll": "Tout sélectionner",
"sysMenuWindow": "Fenêtre",
"sysMenuMinimize": "Réduire",
"sysMenuClose": "Fermer fenêtre",
"alertYes": "Oui",
"alertNo": "Non",
"alertOk": "OK",
@@ -72,7 +88,7 @@
"genLen": "Longueur",
"genNewPass": "Nouveau mot de passe",
"genPresetDefault": "réglages par défaut",
"genPresetDerived": "identique à l'ancien mot de passe",
"genPresetDerived": "similaire à l'ancien mot de passe",
"genPresetPronounceable": "prononçable",
"genPresetMed": "longueur moyenne",
"genPresetLong": "long",
@@ -96,7 +112,7 @@
"tagExists": "Ce tag existe déjà",
"tagExistsBody": "Un tag existe déjà avec ce nom. Merci de choisir un autre nom.",
"tagBadName": "Nom invalide",
"tagBadNameBody": "Tag name cannot contain characters {}. Please remove them.",
"tagBadNameBody": "Les noms de Tag ne peuvent contenir les caractères {}. Veuillez les supprimer.",
"genPsTitle": "Préréglages du Générateur",
"genPsCreate": "Nouveau préréglage",
"genPsDelete": "Supprimer préréglage",
@@ -193,6 +209,7 @@
"openConfigErrorNotFound": "Aucun fichier trouvé",
"openError": "Erreur",
"openErrorDescription": "Une erreur est survenue à l'ouverture du fichier",
"openErrorDescriptionMaybeTouchIdChanged": "L'erreur ressemble à ce qu'y arrive habituellement quand la configuration de Touch ID a été changée. Par exemple, vous avez ajouté ou supprimé un doigt supplémentaire. Si c'est le cas, allez dans Paramètres, désactiver Touch ID et activer le de nouveau.",
"openErrorFileNotFound": "Fichier non trouvé",
"openListErrorBody": "Erreur au chargement de la liste des fichiers",
"openShowAllFiles": "Tous les fichiers",
@@ -246,7 +263,7 @@
"detDelToTrashBody": "L'entrée sera déplacée dans la corbeille.",
"detFieldCopied": "Copié",
"detFieldCopiedTime": "Copié pendant {} secondes",
"detCopyHint": "You can copy field value by clicking its title",
"detCopyHint": "Vous pouvez copier la valeur du champ en cliquant sur ton titre",
"detMore": "plus",
"detClickToAddField": "cliquez pour ajouter un nouveau champ",
"detMenuAddNewField": "Ajouter nouveau champ",
@@ -301,13 +318,18 @@
"detIssuePwnedPassword": "Ce mot de passe a été exposé à une faille selon {}, il est recommandé de le changer",
"detIssuePasswordCheckError": "Une erreur est survenue en vérifiant la force du mot de passe en ligne",
"detIssueOldPassword": "Ce mot de passe est vieux",
"detIssueCloseAlertHeader": "Cacher les problèmes de mots de passe",
"detIssueCloseAlertBody": "Il y a plusieurs façon de cacher ce message :",
"detIssueCloseAlertEntry": "Ne plus montrer pour cette entrée",
"detIssueCloseAlertSettings": "Modifier les paramètres globaux",
"autoTypeEntryFields": "Champs",
"autoTypeModifiers": "Touches modificatrices",
"autoTypeKeys": "Clés",
"autoTypeLink": "plus...",
"autoTypeError": "Erreur saisie auto",
"autoTypeErrorGeneric": "Il y a eu une erreur lors de la saisie automatique : {}",
"autoTypeErrorGlobal": "Pour utiliser un raccourci, sélectionner l'application là où vous voulez saisir le mot de passe",
"autoTypeErrorAccessibilityMacOS": "Nous avons essayé d'envoyer les frappes de caractères à cette application mais cela ne semble pas fonctionner parce qu'il manque des permissions. Cliquer ici pour en lire plus.",
"autoTypeErrorGlobal": "Pour utiliser un raccourci global, sélectionner l'application là où vous voulez saisir le mot de passe",
"autoTypeErrorNotInstalled": "{} n'est pas installé",
"autoTypeHeader": "Saisie auto : Selectionner",
"autoTypeMsgNoWindow": "Nous n'avons pas pu obtenir le titre de la fenêtre active, commencez à taper pour rechercher",
@@ -424,12 +446,15 @@
"setGenClearSeconds": "Dans {} secondes",
"setGenClearMinute": "Dans une minute",
"setGenMinInstead": "Réduire l'application au lieu de fermer",
"setGenMinOnFieldCopy": "Réduire sur copie d'un champ",
"setGenLock": "Verrou Auto",
"setGenLockMinimize": "quand l'appli est en arrière-plan",
"setGenLockCopy": "sur copie de m.d.p.",
"setGenLockAutoType": "sur saisie auto",
"setGenLockOrSleep": "Quand l'ordinateur est verrouillé ou mis en sommeil",
"setGenStorage": "Stockage",
"setGenDisableOfflineStorage": "Ne pas mettre en cache des fichiers d'un stockage offline",
"setGenShortLivedStorageToken": "Utiliser des sessions de courte-durée avec les fournisseurs de stockage dans le cloud",
"setGenStorageLogout": "Déconnexion",
"setGenShowAdvanced": "Afficher les paramètres avancés",
"setGenDevTools": "Afficher les outils de développement",
@@ -441,7 +466,7 @@
"setGenFieldLabelDblClickAutoType": "Remplissage auto par double clic sur les noms de champ",
"setGenTouchId": "Touch ID",
"setGenTouchIdDisabled": "Ne pas utiliser Touch ID",
"setGenTouchIdMemory": "Dévérouiller avec Touch ID uniquement quand Keeweb est lancé",
"setGenTouchIdMemory": "Dévérouiller avec Touch ID uniquement quand KeeWeb est lancé",
"setGenTouchIdFile": "Toujours utiliser Touch ID à la place du mot de passe",
"setGenTouchIdPass": "Exige le mot de passe après",
"setGenAudit": "Audit",
+134 -16
View File
@@ -12,11 +12,12 @@ import { EntryModel } from 'models/entry-model';
import { FileInfoModel } from 'models/file-info-model';
import { FileModel } from 'models/file-model';
import { GroupModel } from 'models/group-model';
import { YubiKeyOtpModel } from 'models/external/yubikey-otp-model';
import { YubiKeyOtpModel } from 'models/otp-device/yubikey-otp-model';
import { MenuModel } from 'models/menu/menu-model';
import { PluginManager } from 'plugins/plugin-manager';
import { Features } from 'util/features';
import { DateFormat } from 'comp/i18n/date-format';
import { Launcher } from 'comp/launcher';
import { UrlFormat } from 'util/formatting/url-format';
import { IdGenerator } from 'util/generators/id-generator';
import { Locale } from 'util/locale';
@@ -38,6 +39,9 @@ class AppModel {
advancedSearch = null;
attachedYubiKeysCount = 0;
memoryPasswordStorage = {};
fileUnlockPromise = null;
hardwareDecryptInProgress = false;
mainWindowBlurTimer = null;
constructor() {
Events.on('refresh', this.refresh.bind(this));
@@ -48,6 +52,11 @@ class AppModel {
Events.on('select-entry', this.selectEntry.bind(this));
Events.on('unset-keyfile', this.unsetKeyFile.bind(this));
Events.on('usb-devices-changed', this.usbDevicesChanged.bind(this));
Events.on('main-window-blur', this.mainWindowBlur.bind(this));
Events.on('main-window-focus', this.mainWindowFocus.bind(this));
Events.on('main-window-will-close', this.mainWindowWillClose.bind(this));
Events.on('hardware-decrypt-started', this.hardwareDecryptStarted.bind(this));
Events.on('hardware-decrypt-finished', this.hardwareDecryptFinished.bind(this));
this.appLogger = new Logger('app');
AppModel.instance = this;
@@ -167,10 +176,29 @@ class AppModel {
page: 'file',
file
});
this.refresh();
file.on('reload', this.reloadFile.bind(this));
file.on('change', () => Events.emit('file-changed', file));
file.on('change', () => {
Events.emit('file-changed', file);
});
file.on('ejected', () => this.closeFile(file));
file.on('change:dirty', (file, dirty) => {
if (dirty && this.settings.autoSaveInterval === -1) {
this.syncFile(file);
}
});
Events.emit('file-opened');
if (this.fileUnlockPromise) {
this.appLogger.info('Running pending file unlock operation');
this.fileUnlockPromise.resolve(file);
this.fileUnlockPromise = null;
Events.emit('unlock-message-changed', null);
}
return true;
}
@@ -232,6 +260,9 @@ class AppModel {
}
closeAllFiles() {
if (!this.files.hasOpenFiles()) {
return;
}
for (const file of this.files) {
file.close();
this.fileClosed(file);
@@ -244,6 +275,7 @@ class AppModel {
this.tags.splice(0, this.tags.length);
this.filter = {};
this.menu.select({ item: this.menu.allItemsItem });
Events.emit('all-files-closed');
}
closeFile(file) {
@@ -254,6 +286,7 @@ class AppModel {
this.menu.groupsSection.removeByFile(file);
this.menu.filesSection.removeByFile(file);
this.menu.select({ item: this.menu.allItemsSection.items[0] });
Events.emit('one-file-closed');
}
emptyTrash() {
@@ -295,7 +328,7 @@ class AppModel {
}
getEntries() {
const entries = this.getEntriesByFilter(this.filter);
const entries = this.getEntriesByFilter(this.filter, this.files);
entries.sortEntries(this.sort, this.filter);
if (this.filter.trash) {
this.addTrashGroups(entries);
@@ -303,16 +336,16 @@ class AppModel {
return entries;
}
getEntriesByFilter(filter) {
getEntriesByFilter(filter, files) {
const preparedFilter = this.prepareFilter(filter);
const entries = new SearchResultCollection();
const devicesToMatchOtpEntries = this.files.filter((file) => file.external);
const devicesToMatchOtpEntries = files.filter((file) => file.backend === 'otp-device');
const matchedOtpEntrySet = this.settings.yubiKeyMatchEntries ? new Set() : undefined;
this.files
.filter((file) => !file.external)
files
.filter((file) => file.backend !== 'otp-device')
.forEach((file) => {
file.forEachEntry(preparedFilter, (entry) => {
if (matchedOtpEntrySet) {
@@ -427,7 +460,7 @@ class AppModel {
createNewEntry(args) {
const sel = this.getFirstSelectedGroupForCreation();
if (args && args.template) {
if (args?.template) {
if (sel.file !== args.template.file) {
sel.file = args.template.file;
sel.group = args.template.file.groups[0];
@@ -443,11 +476,21 @@ class AppModel {
}
}
createNewEntryWithFields(group, fields) {
return EntryModel.newEntryWithFields(group, fields);
}
createNewGroup() {
const sel = this.getFirstSelectedGroupForCreation();
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();
@@ -466,7 +509,7 @@ class AppModel {
}
}
createNewFile(name) {
createNewFile(name, callback) {
if (!name) {
for (let i = 0; ; i++) {
name = Locale.openNewFile + (i || '');
@@ -476,9 +519,10 @@ class AppModel {
}
}
const newFile = new FileModel({ id: IdGenerator.uuid() });
newFile.create(name);
this.addFile(newFile);
return newFile;
newFile.create(name, () => {
this.addFile(newFile);
callback?.(newFile);
});
}
openFile(params, callback) {
@@ -653,7 +697,7 @@ class AppModel {
let needLoadKeyFile = false;
if (!params.keyFileData && fileInfo && fileInfo.keyFileName) {
params.keyFileName = fileInfo.keyFileName;
if (this.settings.rememberKeyFiles === 'data') {
if (this.settings.rememberKeyFiles === 'data' && fileInfo.keyFileHash) {
params.keyFileData = FileModel.createKeyFileWithHash(fileInfo.keyFileHash);
} else if (this.settings.rememberKeyFiles === 'path' && fileInfo.keyFilePath) {
params.keyFilePath = fileInfo.keyFilePath;
@@ -827,7 +871,10 @@ class AppModel {
this.scheduleBackupFile(file, data);
}
if (this.settings.yubiKeyAutoOpen) {
if (this.attachedYubiKeysCount > 0 && !this.files.some((f) => f.external)) {
if (
this.attachedYubiKeysCount > 0 &&
!this.files.some((f) => f.backend === 'otp-device')
) {
this.tryOpenOtpDeviceInBackground();
}
}
@@ -1271,7 +1318,9 @@ class AppModel {
}
const isNewYubiKey = UsbListener.attachedYubiKeys > attachedYubiKeysCount;
const hasOpenFiles = this.files.some((file) => file.active && !file.external);
const hasOpenFiles = this.files.some(
(file) => file.active && file.backend !== 'otp-device'
);
if (isNewYubiKey && hasOpenFiles && !this.openingOtpDevice) {
this.tryOpenOtpDeviceInBackground();
@@ -1303,7 +1352,7 @@ class AppModel {
return null;
}
for (const file of this.files) {
if (file.external) {
if (file.backend === 'otp-device') {
const matchingEntry = file.getMatchingEntry(entry);
if (matchingEntry) {
return matchingEntry;
@@ -1402,6 +1451,75 @@ class AppModel {
this.memoryPasswordStorage = {};
}
}
unlockAnyFile(unlockRes, timeout) {
this.rejectPendingFileUnlockPromise('Replaced with a new operation');
Events.emit('show-open-view');
return new Promise((resolve, reject) => {
this.fileUnlockPromise = { resolve, reject, unlockRes };
if (timeout) {
const timer = setTimeout(
() => this.rejectPendingFileUnlockPromise('Timeout'),
timeout
);
this.fileUnlockPromise.resolve = (res) => {
clearTimeout(timer);
resolve(res);
};
this.fileUnlockPromise.reject = (err) => {
clearTimeout(timer);
reject(err);
};
}
this.appLogger.info('Pending file unlock operation is set');
Events.emit('unlock-message-changed', unlockRes);
});
}
get unlockMessageRes() {
return this.fileUnlockPromise?.unlockRes;
}
rejectPendingFileUnlockPromise(reason) {
if (this.fileUnlockPromise) {
this.appLogger.info('Cancel pending file unlock operation', reason);
this.fileUnlockPromise.reject(new Error(reason));
this.fileUnlockPromise = null;
Events.emit('unlock-message-changed', null);
}
}
mainWindowBlur() {
if (!this.hardwareDecryptInProgress) {
this.mainWindowBlurTimer = setTimeout(() => {
// macOS emits focus-blur-focus event in a row when triggering auto-type from minimized state
delete this.mainWindowBlurTimer;
this.rejectPendingFileUnlockPromise('Main window blur');
}, Timeouts.AutoTypeWindowFocusAfterBlur);
}
}
mainWindowFocus() {
if (this.mainWindowBlurTimer) {
clearTimeout(this.mainWindowBlurTimer);
this.mainWindowBlurTimer = null;
}
}
mainWindowWillClose() {
this.rejectPendingFileUnlockPromise('Main window will close');
}
hardwareDecryptStarted() {
this.hardwareDecryptInProgress = true;
}
hardwareDecryptFinished() {
this.hardwareDecryptInProgress = false;
if (!Launcher.isAppFocused()) {
this.rejectPendingFileUnlockPromise('App is not focused after hardware decrypt');
}
}
}
export { AppModel };
+2 -2
View File
@@ -3,10 +3,10 @@ import { Model } from 'framework/model';
class AttachmentModel extends Model {
getBinary() {
let data = this.data;
if (data && data.ref) {
if (data?.value) {
data = data.value;
}
if (data && data.getBinary) {
if (data?.getBinary) {
data = data.getBinary();
}
if (data instanceof ArrayBuffer && data.byteLength) {
+71 -36
View File
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Model } from 'framework/model';
import { AppSettingsModel } from 'models/app-settings-model';
import { KdbxToHtml } from 'comp/format/kdbx-to-html';
@@ -16,6 +16,7 @@ const UrlRegex = /^https?:\/\//i;
const FieldRefRegex = /^\{REF:([TNPAU])@I:(\w{32})}$/;
const FieldRefFields = ['title', 'password', 'user', 'url', 'notes'];
const FieldRefIds = { T: 'Title', U: 'UserName', P: 'Password', A: 'URL', N: 'Notes' };
const ExtraUrlFieldName = 'KP2A_URL';
class EntryModel extends Model {
constructor(props) {
@@ -51,7 +52,7 @@ class EntryModel extends Model {
this.icon = this._iconFromId(entry.icon);
this.tags = entry.tags;
this.color = this._colorToModel(entry.bgColor) || this._colorToModel(entry.fgColor);
this.fields = this._fieldsToModel(entry.fields);
this.fields = this._fieldsToModel();
this.attachments = this._attachmentsToModel(entry.binaries);
this.created = entry.times.creationTime;
this.updated = entry.times.lastModTime;
@@ -70,7 +71,7 @@ class EntryModel extends Model {
}
_getPassword() {
const password = this.entry.fields.Password || kdbxweb.ProtectedValue.fromString('');
const password = this.entry.fields.get('Password') || kdbxweb.ProtectedValue.fromString('');
if (!password.isProtected) {
return kdbxweb.ProtectedValue.fromString(password);
}
@@ -78,7 +79,7 @@ class EntryModel extends Model {
}
_getFieldString(field) {
const val = this.entry.fields[field];
const val = this.entry.fields.get(field);
if (!val) {
return '';
}
@@ -102,7 +103,7 @@ class EntryModel extends Model {
_buildSearchText() {
let text = '';
for (const value of Object.values(this.entry.fields)) {
for (const value of this.entry.fields.values()) {
if (typeof value === 'string') {
text += value.toLowerCase() + '\n';
}
@@ -121,7 +122,7 @@ class EntryModel extends Model {
this.customIconId = null;
if (this.entry.customIcon) {
this.customIcon = IconUrlFormat.toDataUrl(
this.file.db.meta.customIcons[this.entry.customIcon]
this.file.db.meta.customIcons.get(this.entry.customIcon.id)?.data
);
this.customIconId = this.entry.customIcon.toString();
}
@@ -163,13 +164,13 @@ class EntryModel extends Model {
return color ? Color.getNearest(color) : null;
}
_fieldsToModel(fields) {
return omit(fields, BuiltInFields);
_fieldsToModel() {
return omit(this.getAllFields(), BuiltInFields);
}
_attachmentsToModel(binaries) {
const att = [];
for (let [title, data] of Object.entries(binaries)) {
for (let [title, data] of binaries) {
if (data && data.ref) {
data = data.value;
}
@@ -190,6 +191,7 @@ class EntryModel extends Model {
}
if (this.isJustCreated) {
this.isJustCreated = false;
this.file.reload();
}
this.entry.times.update();
}
@@ -208,7 +210,11 @@ class EntryModel extends Model {
}
getAllFields() {
return this.entry.fields;
const fields = {};
for (const [key, value] of this.entry.fields) {
fields[key] = value;
}
return fields;
}
getHistoryEntriesForSearch() {
@@ -230,7 +236,7 @@ class EntryModel extends Model {
getFieldValue(field) {
field = field.toLowerCase();
let resolvedField;
Object.keys(this.entry.fields).some((entryField) => {
[...this.entry.fields.keys()].some((entryField) => {
if (entryField.toLowerCase() === field) {
resolvedField = entryField;
return true;
@@ -238,7 +244,7 @@ class EntryModel extends Model {
return false;
});
if (resolvedField) {
let fieldValue = this.entry.fields[resolvedField];
let fieldValue = this.entry.fields.get(resolvedField);
const refValue = this._resolveFieldReference(fieldValue);
if (refValue !== undefined) {
fieldValue = refValue;
@@ -274,7 +280,7 @@ class EntryModel extends Model {
if (!entry) {
return;
}
return entry.entry.fields[FieldRefIds[fieldRefId]];
return entry.entry.fields.get(FieldRefIds[fieldRefId]);
}
setColor(color) {
@@ -327,10 +333,10 @@ class EntryModel extends Model {
if (hasValue || allowEmpty || BuiltInFields.indexOf(field) >= 0) {
this._entryModified();
val = this.sanitizeFieldValue(val);
this.entry.fields[field] = val;
} else if (Object.prototype.hasOwnProperty.call(this.entry.fields, field)) {
this.entry.fields.set(field, val);
} else if (this.entry.fields.has(field)) {
this._entryModified();
delete this.entry.fields[field];
this.entry.fields.delete(field);
}
this._fillByEntry();
}
@@ -345,20 +351,20 @@ class EntryModel extends Model {
}
hasField(field) {
return Object.prototype.hasOwnProperty.call(this.entry.fields, field);
return this.entry.fields.has(field);
}
addAttachment(name, data) {
this._entryModified();
return this.file.db.createBinary(data).then((binaryRef) => {
this.entry.binaries[name] = binaryRef;
this.entry.binaries.set(name, binaryRef);
this._fillByEntry();
});
}
removeAttachment(name) {
this._entryModified();
delete this.entry.binaries[name];
this.entry.binaries.delete(name);
this._fillByEntry();
}
@@ -388,8 +394,8 @@ class EntryModel extends Model {
this.entry.pushHistory();
this.unsaved = true;
this.file.setModified();
this.entry.fields = {};
this.entry.binaries = {};
this.entry.fields = new Map();
this.entry.binaries = new Map();
this.entry.copyFrom(historyEntry);
this._entryModified();
this._fillByEntry();
@@ -400,8 +406,8 @@ class EntryModel extends Model {
this.unsaved = false;
const historyEntry = this.entry.history[this.entry.history.length - 1];
this.entry.removeHistory(this.entry.history.length - 1);
this.entry.fields = {};
this.entry.binaries = {};
this.entry.fields = new Map();
this.entry.binaries = new Map();
this.entry.copyFrom(historyEntry);
this._fillByEntry();
}
@@ -474,14 +480,14 @@ class EntryModel extends Model {
otpUrl = Otp.makeUrl(args.key, args.step, args.size);
}
}
} else if (this.entry.fields['TOTP Seed']) {
} else if (this.entry.fields.get('TOTP Seed')) {
// TrayTOTP plugin format
let secret = this.entry.fields['TOTP Seed'];
let secret = this.entry.fields.get('TOTP Seed');
if (secret.isProtected) {
secret = secret.getText();
}
if (secret) {
let settings = this.entry.fields['TOTP Settings'];
let settings = this.entry.fields.get('TOTP Settings');
if (settings && settings.isProtected) {
settings = settings.getText();
}
@@ -505,7 +511,7 @@ class EntryModel extends Model {
}
try {
this.otpGenerator = Otp.parseUrl(otpUrl);
} catch (e) {
} catch {
this.otpGenerator = null;
}
} else {
@@ -520,8 +526,8 @@ class EntryModel extends Model {
setOtpUrl(url) {
this.setField('otp', url ? kdbxweb.ProtectedValue.fromString(url) : undefined);
delete this.entry.fields['TOTP Seed'];
delete this.entry.fields['TOTP Settings'];
this.entry.fields.delete('TOTP Seed');
this.entry.fields.delete('TOTP Settings');
}
getEffectiveEnableAutoType() {
@@ -572,7 +578,7 @@ class EntryModel extends Model {
newEntry.entry.uuid = uuid;
newEntry.entry.times.update();
newEntry.entry.times.creationTime = newEntry.entry.times.lastModTime;
newEntry.entry.fields.Title = this.title + nameSuffix;
newEntry.entry.fields.set('Title', this.title + nameSuffix);
newEntry._fillByEntry();
this.file.reload();
return newEntry;
@@ -584,7 +590,7 @@ class EntryModel extends Model {
this.entry.uuid = uuid;
this.entry.times.update();
this.entry.times.creationTime = this.entry.times.lastModTime;
this.entry.fields.Title = '';
this.entry.fields.set('Title', '');
this._fillByEntry();
}
@@ -610,7 +616,7 @@ class EntryModel extends Model {
const allFields = Object.keys(fieldWeights).concat(Object.keys(this.fields));
return allFields.reduce((rank, fieldName) => {
const val = this.entry.fields[fieldName];
const val = this.entry.fields.get(fieldName);
if (!val) {
return rank;
}
@@ -628,17 +634,38 @@ class EntryModel extends Model {
}
canCheckPasswordIssues() {
return !this.entry.customData?.IgnorePwIssues;
return !this.entry.customData?.has('IgnorePwIssues');
}
setIgnorePasswordIssues() {
if (!this.entry.customData) {
this.entry.customData = {};
this.entry.customData = new Map();
}
this.entry.customData.IgnorePwIssues = '1';
this.entry.customData.set('IgnorePwIssues', '1');
this._entryModified();
}
getNextUrlFieldName() {
const takenFields = new Set(
[...this.entry.fields.keys()].filter((f) => f.startsWith(ExtraUrlFieldName))
);
for (let i = 0; ; i++) {
const fieldName = i ? `${ExtraUrlFieldName}_${i}` : ExtraUrlFieldName;
if (!takenFields.has(fieldName)) {
return fieldName;
}
}
}
getAllUrls() {
const urls = this.url ? [this.url] : [];
const extraUrls = Object.entries(this.fields)
.filter(([field]) => field.startsWith(ExtraUrlFieldName))
.map(([, value]) => (value.isProtected ? value.getText() : value))
.filter((value) => value);
return urls.concat(extraUrls);
}
static fromEntry(entry, group, file) {
const model = new EntryModel();
model.setEntry(entry, group, file);
@@ -663,8 +690,16 @@ class EntryModel extends Model {
file.setModified();
return model;
}
static newEntryWithFields(group, fields) {
const entry = EntryModel.newEntry(group, group.file);
for (const [field, value] of Object.entries(fields)) {
entry.setField(field, value);
}
return entry;
}
}
EntryModel.defineModelProperties({}, { extensions: true });
export { EntryModel };
export { EntryModel, ExtraUrlFieldName };
@@ -1,21 +0,0 @@
import { ExternalDeviceModel } from 'models/external/external-device-model';
class ExternalOtpDeviceModel extends ExternalDeviceModel {
open(callback) {
throw 'Not implemented';
}
cancelOpen() {
throw 'Not implemented';
}
close(callback) {
throw 'Not implemented';
}
getOtp(callback) {
throw 'Not implemented';
}
}
export { ExternalOtpDeviceModel };
+36 -32
View File
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import demoFileData from 'demo.kdbx';
import { Model } from 'framework/model';
import { Events } from 'framework/events';
@@ -8,7 +8,8 @@ import { GroupModel } from 'models/group-model';
import { AppSettingsModel } from 'models/app-settings-model';
import { IconUrlFormat } from 'util/formatting/icon-url-format';
import { Logger } from 'util/logger';
import { mapObject } from 'util/fn';
import { Locale } from 'util/locale';
import { StringFormat } from 'util/formatting/string-format';
import { ChalRespCalculator } from 'comp/app/chal-resp-calculator';
const logger = new Logger('file');
@@ -31,19 +32,20 @@ class FileModel extends Model {
kdbxweb.Kdbx.load(fileData, credentials)
.then((db) => {
this.db = db;
})
.then(() => {
this.readModel();
this.setOpenFile({ passwordLength: password ? password.textLength : 0 });
if (keyFileData) {
kdbxweb.ByteUtils.zeroBuffer(keyFileData);
}
this.fixVersion();
logger.info(
'Opened file ' +
this.name +
': ' +
logger.ts(ts) +
', ' +
this.kdfArgsToString(db.header) +
this.kdfArgsToString(this.db.header) +
', ' +
Math.round(fileData.byteLength / 1024) +
' kB'
@@ -90,13 +92,14 @@ class FileModel extends Model {
}
}
create(name) {
create(name, callback) {
const password = kdbxweb.ProtectedValue.fromString('');
const credentials = new kdbxweb.Credentials(password);
this.db = kdbxweb.Kdbx.create(credentials, name);
this.name = name;
this.readModel();
this.set({ active: true, created: true, name });
callback();
}
importWithXml(fileXml, callback) {
@@ -107,6 +110,8 @@ class FileModel extends Model {
kdbxweb.Kdbx.loadXml(fileXml, credentials)
.then((db) => {
this.db = db;
})
.then(() => {
this.readModel();
this.set({ active: true, created: true });
logger.info('Imported file ' + this.name + ': ' + logger.ts(ts));
@@ -128,13 +133,16 @@ class FileModel extends Model {
const demoFile = kdbxweb.ByteUtils.arrayToBuffer(
kdbxweb.ByteUtils.base64ToBytes(demoFileData)
);
kdbxweb.Kdbx.load(demoFile, credentials).then((db) => {
this.db = db;
this.name = 'Demo';
this.readModel();
this.setOpenFile({ passwordLength: 4, demo: true });
callback();
});
kdbxweb.Kdbx.load(demoFile, credentials)
.then((db) => {
this.db = db;
})
.then(() => {
this.name = 'Demo';
this.readModel();
this.setOpenFile({ passwordLength: 4, demo: true });
callback();
});
}
setOpenFile(props) {
@@ -257,17 +265,6 @@ class FileModel extends Model {
});
}
fixVersion() {
if (
this.db.meta.generator === 'KdbxWeb' &&
this.db.header.versionMajor === 4 &&
this.db.header.versionMinor === 1
) {
this.db.header.versionMinor = 0;
logger.info('Fixed file version: 4.1 => 4.0');
}
}
reload() {
this.buildObjectMap();
this.readModel();
@@ -407,7 +404,7 @@ class FileModel extends Model {
createEntryTemplatesGroup() {
const rootGroup = this.groups[0];
const templatesGroup = GroupModel.newGroup(rootGroup, this);
templatesGroup.setName('Templates');
templatesGroup.setName(StringFormat.capFirst(Locale.templates));
this.db.meta.entryTemplatesGroup = templatesGroup.group.uuid;
this.reload();
return templatesGroup;
@@ -416,7 +413,6 @@ class FileModel extends Model {
setModified() {
if (!this.demo) {
this.set({ modified: true, dirty: true });
Events.emit('file-modified');
}
}
@@ -672,16 +668,19 @@ class FileModel extends Model {
}
getCustomIcons() {
return mapObject(this.db.meta.customIcons, (customIcon) =>
IconUrlFormat.toDataUrl(customIcon)
);
const customIcons = {};
for (const [id, icon] of this.db.meta.customIcons) {
customIcons[id] = IconUrlFormat.toDataUrl(icon.data);
}
return customIcons;
}
addCustomIcon(iconData) {
const uuid = kdbxweb.KdbxUuid.random();
this.db.meta.customIcons[uuid] = kdbxweb.ByteUtils.arrayToBuffer(
kdbxweb.ByteUtils.base64ToBytes(iconData)
);
this.db.meta.customIcons.set(uuid.id, {
data: kdbxweb.ByteUtils.arrayToBuffer(kdbxweb.ByteUtils.base64ToBytes(iconData)),
lastModified: new Date()
});
return uuid.toString();
}
@@ -766,7 +765,12 @@ FileModel.defineModelProperties({
oldKeyFileHash: null,
oldKeyChangeDate: null,
encryptedPassword: null,
encryptedPasswordDate: null
encryptedPasswordDate: null,
supportsTags: true,
supportsColors: true,
supportsIcons: true,
supportsExpiration: true,
defaultGroupHash: ''
});
export { FileModel };
+4 -2
View File
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { IconMap } from 'const/icon-map';
import { EntryModel } from 'models/entry-model';
import { MenuItemModel } from 'models/menu/menu-item-model';
@@ -86,7 +86,9 @@ class GroupModel extends MenuItemModel {
_buildCustomIcon() {
this.customIcon = null;
if (this.group.customIcon) {
return IconUrlFormat.toDataUrl(this.file.db.meta.customIcons[this.group.customIcon]);
return IconUrlFormat.toDataUrl(
this.file.db.meta.customIcons.get(this.group.customIcon.id)?.data
);
}
return null;
}
+7
View File
@@ -8,6 +8,7 @@ import { MenuSectionModel } from 'models/menu/menu-section-model';
import { StringFormat } from 'util/formatting/string-format';
import { Locale } from 'util/locale';
import { Launcher } from 'comp/launcher';
import { Features } from 'util/features';
class MenuModel extends Model {
constructor() {
@@ -113,6 +114,11 @@ class MenuModel extends Model {
this.shortcutsSection = new MenuSectionModel([
{ locTitle: 'shortcuts', icon: 'keyboard', page: 'shortcuts' }
]);
if (Features.supportsBrowserExtensions) {
this.browserSection = new MenuSectionModel([
{ locTitle: 'menuSetBrowser', icon: Features.browserIcon, page: 'browser' }
]);
}
this.pluginsSection = new MenuSectionModel([
{ locTitle: 'plugins', icon: 'puzzle-piece', page: 'plugins' }
]);
@@ -133,6 +139,7 @@ class MenuModel extends Model {
[
this.generalSection,
this.shortcutsSection,
this.browserSection,
this.pluginsSection,
this.devicesSection,
this.aboutSection,
@@ -0,0 +1,8 @@
import { Collection } from 'framework/collection';
import { OtpDeviceEntryModel } from './otp-device-entry-model';
class OtpDeviceEntryCollection extends Collection {
static model = OtpDeviceEntryModel;
}
export { OtpDeviceEntryCollection };
@@ -1,7 +1,7 @@
import { Model } from 'framework/model';
import { EntrySearch } from 'util/entry-search';
class ExternalEntryModel extends Model {
class OtpDeviceEntryModel extends Model {
tags = [];
fields = {};
@@ -35,6 +35,10 @@ class ExternalEntryModel extends Model {
return this.fields;
}
getAllUrls() {
return [];
}
getFieldValue(field) {
return this.fields[field];
}
@@ -44,9 +48,10 @@ class ExternalEntryModel extends Model {
}
}
ExternalEntryModel.defineModelProperties({
OtpDeviceEntryModel.defineModelProperties({
id: '',
external: true,
file: null,
entry: true,
readOnly: true,
device: undefined,
deviceSubId: null,
@@ -59,4 +64,4 @@ ExternalEntryModel.defineModelProperties({
_search: undefined
});
export { ExternalEntryModel };
export { OtpDeviceEntryModel };
@@ -1,13 +1,11 @@
import { Model } from 'framework/model';
import { ExternalEntryCollection } from 'collections/external-entry-collection';
import { OtpDeviceEntryCollection } from './otp-device-entry-collection';
class ExternalDeviceModel extends Model {
entries = new ExternalEntryCollection();
class OtpDeviceModel extends Model {
entries = new OtpDeviceEntryCollection();
groups = [];
entryMap = {};
close() {}
forEachEntry(filter, callback) {
if (filter.trash || filter.group || filter.tag) {
return;
@@ -32,11 +30,28 @@ class ExternalDeviceModel extends Model {
this.entryMap[entry.id.toLowerCase()] = entry;
}
}
open(callback) {
throw 'Not implemented';
}
cancelOpen() {
throw 'Not implemented';
}
close(callback) {
throw 'Not implemented';
}
getOtp(callback) {
throw 'Not implemented';
}
}
ExternalDeviceModel.defineModelProperties({
OtpDeviceModel.defineModelProperties({
id: '',
external: true,
backend: 'otp-device',
skipOpenList: true,
readOnly: true,
active: false,
entries: undefined,
@@ -47,4 +62,4 @@ ExternalDeviceModel.defineModelProperties({
entryMap: undefined
});
export { ExternalDeviceModel };
export { OtpDeviceModel };
@@ -1,6 +1,6 @@
import { ExternalEntryModel } from 'models/external/external-entry-model';
import { OtpDeviceEntryModel } from './otp-device-entry-model';
class ExternalOtpEntryModel extends ExternalEntryModel {
class OtpEntryModel extends OtpDeviceEntryModel {
constructor(props) {
super({
...props,
@@ -59,10 +59,11 @@ class ExternalOtpEntryModel extends ExternalEntryModel {
}
}
ExternalOtpEntryModel.defineModelProperties({
OtpEntryModel.defineModelProperties({
user: undefined,
backend: 'otp-device',
otpGenerator: undefined,
needsTouch: false
});
export { ExternalOtpEntryModel };
export { OtpEntryModel };
@@ -1,13 +1,13 @@
import { Events } from 'framework/events';
import { ExternalOtpDeviceModel } from 'models/external/external-otp-device-model';
import { ExternalOtpEntryModel } from 'models/external/external-otp-entry-model';
import { OtpDeviceModel } from './otp-device-model';
import { OtpEntryModel } from './otp-entry-model';
import { Logger } from 'util/logger';
import { UsbListener } from 'comp/app/usb-listener';
import { YubiKey } from 'comp/app/yubikey';
const logger = new Logger('yubikey');
class YubiKeyOtpModel extends ExternalOtpDeviceModel {
class YubiKeyOtpModel extends OtpDeviceModel {
constructor(props) {
super({
id: 'yubikey',
@@ -67,8 +67,9 @@ class YubiKeyOtpModel extends ExternalOtpDeviceModel {
for (const code of codes) {
this.entries.push(
new ExternalOtpEntryModel({
new OtpEntryModel({
id: this.entryId(code.title, code.user),
file: this,
device: this,
deviceSubId: serial,
icon: 'clock',
+1 -1
View File
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Events } from 'framework/events';
import { SettingsStore } from 'comp/settings/settings-store';
import { Links } from 'const/links';
+22 -20
View File
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import BaseLocale from 'locales/base.json';
import { Model } from 'framework/model';
import { RuntimeInfo } from 'const/runtime-info';
@@ -373,25 +373,27 @@ class Plugin extends Model {
};
text = `(function(require, module){${text}})(window["${jsVar}"].require,window["${jsVar}"].module);`;
const ts = this.logger.ts();
const blob = new Blob([text], { type: 'text/javascript' });
const objectUrl = URL.createObjectURL(blob);
const elId = 'plugin-js-' + name;
const el = this.createElementInHead('script', elId, {
src: objectUrl
});
el.addEventListener('load', () => {
URL.revokeObjectURL(objectUrl);
setTimeout(() => {
delete global[jsVar];
if (this.module.exports.uninstall) {
this.logger.debug('Plugin script installed', this.logger.ts(ts));
this.loadPluginSettings();
resolve();
} else {
reject('Plugin script installation failed');
}
}, 0);
});
// Note that here we're calling eval to run the plugin code,
// previously it was loaded as 'blob:' scheme (see the code below), however:
// 1. we need to have eval enabled in our CSP anyway for WASM,
// see https://github.com/WebAssembly/content-security-policy/issues/7
// 2. we would like to prevent Chrome extensions from injecting scripts to our page,
// which is possible to do if we have 'blob:', but they can't call eval
// Previous implementation with 'blob:' can be found in git, if we ever need to restore it.
// eslint-disable-next-line no-eval
eval(text);
setTimeout(() => {
delete global[jsVar];
if (this.module.exports.uninstall) {
this.logger.debug('Plugin script installed', this.logger.ts(ts));
this.loadPluginSettings();
resolve();
} else {
reject('Plugin script installation failed');
}
}, 0);
} catch (e) {
this.logger.error('Error installing plugin script', e);
reject(e);
+5 -4
View File
@@ -14,12 +14,13 @@ EntryPresenter.prototype = {
this.entry = item;
} else if (item.group) {
this.group = item;
} else if (item.external) {
this.entry = item;
this.external = true;
}
return this;
},
reset() {
this.entry = null;
this.group = null;
},
get id() {
return this.entry ? this.entry.id : this.group.id;
},
@@ -71,7 +72,7 @@ EntryPresenter.prototype = {
if (!this.entry) {
return '[' + Locale.listGroup + ']';
}
if (this.external) {
if (this.entry.backend === 'otp-device') {
return this.entry.description;
}
switch (this.descField) {
+3 -1
View File
@@ -83,7 +83,9 @@ class StorageDropbox extends StorageBase {
pkce: true,
width: 600,
height: 400,
urlParams: { 'token_access_type': 'offline' }
urlParams: this.appSettings.shortLivedStorageToken
? {}
: { 'token_access_type': 'offline' }
};
}
+1 -1
View File
@@ -5,7 +5,7 @@ const fileWatchers = {};
class StorageFile extends StorageBase {
name = 'file';
icon = 'hdd-o';
icon = 'hdd';
enabled = !!Launcher;
system = true;
backup = true;
+109 -58
View File
@@ -1,6 +1,7 @@
import { StorageBase } from 'storage/storage-base';
import { Locale } from 'util/locale';
import { Features } from 'util/features';
import { UrlFormat } from 'util/formatting/url-format';
import { GDriveApps } from 'const/cloud-storage-apps';
const NewFileIdPrefix = 'NewFile:';
@@ -27,11 +28,9 @@ class StorageGDrive extends StorageBase {
}
this.logger.debug('Load', path);
const ts = this.logger.ts();
const url =
this._baseUrl +
'/files/{id}/revisions/{rev}?alt=media'
.replace('{id}', path)
.replace('{rev}', stat.rev);
const url = UrlFormat.makeUrl(`${this._baseUrl}/files/${path}/revisions/${stat.rev}`, {
'alt': 'media'
});
this._xhr({
url,
responseType: 'arraybuffer',
@@ -57,7 +56,11 @@ class StorageGDrive extends StorageBase {
}
this.logger.debug('Stat', path);
const ts = this.logger.ts();
const url = this._baseUrl + '/files/{id}?fields=headRevisionId'.replace('{id}', path);
const url = UrlFormat.makeUrl(`${this._baseUrl}/files/${path}`, {
fields: 'headRevisionId',
includeItemsFromAllDrives: true,
supportsAllDrives: true
});
this._xhr({
url,
responseType: 'json',
@@ -95,9 +98,12 @@ class StorageGDrive extends StorageBase {
let dataType;
let dataIsMultipart = false;
if (isNew) {
url =
this._baseUrlUpload +
'/files?uploadType=multipart&fields=id,headRevisionId';
url = UrlFormat.makeUrl(`${this._baseUrlUpload}/files`, {
uploadType: 'multipart',
fields: 'id,headRevisionId',
includeItemsFromAllDrives: true,
supportsAllDrives: true
});
const fileName = path.replace(NewFileIdPrefix, '') + '.kdbx';
const boundary = 'b' + Date.now() + 'x' + Math.round(Math.random() * 1000000);
data = [
@@ -123,9 +129,12 @@ class StorageGDrive extends StorageBase {
dataType = 'multipart/related; boundary="' + boundary + '"';
dataIsMultipart = true;
} else {
url =
this._baseUrlUpload +
'/files/{id}?uploadType=media&fields=headRevisionId'.replace('{id}', path);
url = UrlFormat.makeUrl(`${this._baseUrlUpload}/files/${path}`, {
uploadType: 'media',
fields: 'headRevisionId',
includeItemsFromAllDrives: true,
supportsAllDrives: true
});
}
this._xhr({
url,
@@ -160,59 +169,103 @@ class StorageGDrive extends StorageBase {
return callback && callback(err);
}
this.logger.debug('List');
let query =
dir === 'shared'
? 'sharedWithMe=true'
: dir
? `"${dir}" in parents`
: '"root" in parents';
query += ' and trashed=false';
const url =
this._baseUrl +
'/files?fields={fields}&q={q}&pageSize=1000'
.replace(
'{fields}',
encodeURIComponent('files(id,name,mimeType,headRevisionId)')
)
.replace('{q}', encodeURIComponent(query));
const ts = this.logger.ts();
this._xhr({
url,
responseType: 'json',
success: (response) => {
if (!response) {
this.logger.error('List error', this.logger.ts(ts));
return callback && callback('list error');
}
this.logger.debug('Listed', this.logger.ts(ts));
const fileList = response.files.map((f) => ({
name: f.name,
path: f.id,
rev: f.headRevisionId,
dir: f.mimeType === 'application/vnd.google-apps.folder'
}));
if (!dir) {
fileList.unshift({
name: Locale.gdriveSharedWithMe,
path: 'shared',
rev: undefined,
if (dir === 'drives') {
const urlParams = {
pageSize: 100
};
const url = UrlFormat.makeUrl(`${this._baseUrl}/drives`, urlParams);
this._xhr({
url,
responseType: 'json',
success: (response) => {
if (!response) {
this.logger.error('Drive list error', this.logger.ts(ts));
return callback?.('drive list error');
}
this.logger.debug('Listed drives', this.logger.ts(ts));
const fileList = response.drives.map((d) => ({
name: d.name,
path: d.id,
dir: true
});
}));
return callback?.(null, fileList);
},
error: (err) => {
this.logger.error('Drive dist error', this.logger.ts(ts), err);
return callback?.(err);
}
return callback && callback(null, fileList);
},
error: (err) => {
this.logger.error('List error', this.logger.ts(ts), err);
return callback && callback(err);
});
} else {
let query = 'trashed=false and ';
if (dir === 'shared') {
query += 'sharedWithMe=true';
} else if (dir) {
query += `"${dir}" in parents`;
} else {
query += '"root" in parents';
}
});
const urlParams = {
fields: 'files(id,name,mimeType,headRevisionId)',
q: query,
pageSize: 1000,
includeItemsFromAllDrives: true,
supportsAllDrives: true
};
const url = UrlFormat.makeUrl(`${this._baseUrl}/files`, urlParams);
this._xhr({
url,
responseType: 'json',
success: (response) => {
if (!response) {
this.logger.error('List error', this.logger.ts(ts));
return callback?.('list error');
}
this.logger.debug('Listed', this.logger.ts(ts));
const fileList = response.files.map((f) => ({
name: f.name,
path: f.id,
rev: f.headRevisionId,
dir: f.mimeType === 'application/vnd.google-apps.folder'
}));
if (!dir) {
fileList.unshift({
name: Locale.gdriveSharedWithMe,
path: 'shared',
rev: undefined,
dir: true
});
fileList.unshift({
name: Locale.gdriveSharedDrives,
path: 'drives',
rev: undefined,
dir: true
});
}
return callback?.(null, fileList);
},
error: (err) => {
this.logger.error('List error', this.logger.ts(ts), err);
return callback?.(err);
}
});
}
});
}
remove(path, callback) {
this.logger.debug('Remove', path);
const ts = this.logger.ts();
const url = this._baseUrl + '/files/{id}'.replace('{id}', path);
const url = `${this._baseUrl}/files/${path}`;
this._xhr({
url,
method: 'DELETE',
@@ -254,9 +307,7 @@ class StorageGDrive extends StorageBase {
width: 600,
height: 400,
pkce: true,
redirectUrlParams: {
'access_type': 'offline'
}
urlParams: this.appSettings.shortLivedStorageToken ? {} : { 'access_type': 'offline' }
};
}
}
+18 -6
View File
@@ -221,19 +221,31 @@ class StorageOneDrive extends StorageBase {
_getOAuthConfig() {
let clientId = this.appSettings.onedriveClientId;
let clientSecret = this.appSettings.onedriveClientSecret;
let tenant = this.appSettings.onedriveTenantId;
if (!clientId) {
if (Features.isDesktop) {
({ id: clientId, secret: clientSecret } = OneDriveApps.Desktop);
({ id: clientId, secret: clientSecret, tenantId: tenant } = OneDriveApps.Desktop);
} else if (Features.isLocal) {
({ id: clientId, secret: clientSecret } = OneDriveApps.Local);
({ id: clientId, secret: clientSecret, tenantId: tenant } = OneDriveApps.Local);
} else {
({ id: clientId, secret: clientSecret } = OneDriveApps.Production);
({
id: clientId,
secret: clientSecret,
tenantId: tenant
} = OneDriveApps.Production);
}
}
tenant = tenant || 'common';
let scope = 'files.readwrite';
if (!this.appSettings.shortLivedStorageToken) {
scope += ' offline_access';
}
return {
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
scope: 'files.readwrite offline_access',
url: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
tokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
scope,
clientId,
clientSecret,
pkce: true,
+365
View File
@@ -0,0 +1,365 @@
import { StorageBase } from 'storage/storage-base';
import { MsTeamsApps } from 'const/cloud-storage-apps';
import { Features } from 'util/features';
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
// https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true
// https://graph.microsoft.com/v1.0/groups?$filter=groupTypes/any(c:c+eq+'Unified')
// https://graph.microsoft.com/v1.0/groups?$filter=groupTypes/any(c:c+eq+'Unified')&$orderby=displayName
// https://graph.microsoft.com/v1.0/groups?$orderby=displayName
// /me/joinedTeams
// https://graph.microsoft.com/v1.0/groups/{group id}/drive/root/children
class StorageTeams extends StorageBase {
name = 'msteams';
enabled = true;
uipos = 50;
icon = 'user-friends';
_graphUrl = 'https://graph.microsoft.com/v1.0';
_groupsUrl = `${this._graphUrl}/me/joinedTeams`;
_baseUrl = `${this._graphUrl}/groups`;
getPathForName(fileName) {
return '/drive/root:/' + fileName + '.kdbx';
}
genUrlAddress(groupId, path) {
if (groupId) {
return this._baseUrl + '/' + groupId + (path ? '/' + path.replace(/^\/+/, '') : '');
} else {
return this._groupsUrl;
}
}
genUrl(path) {
if (!path) {
const groupId = null;
const dir = null;
const url = this.genUrlAddress(groupId, dir);
return [groupId, dir, url];
}
const parts = path.replace(/^\/+/, '').split('/');
if (parts.length === 0) {
const groupId = null;
const dir = null;
const url = this.genUrlAddress(groupId, dir);
return [groupId, dir, url];
} else if (parts.length === 1) {
const groupId = parts[0];
const dir = null;
const url = this.genUrlAddress(groupId, dir);
return [groupId, dir, url];
} else {
path = path.replace(/\/drive\/root\:/, '');
const groupId = parts[0];
const dir = ('/' + parts.slice(1).join('/')).replace(/^\/+/, '');
const url = this.genUrlAddress(groupId, dir);
return [groupId, dir, url];
}
}
load(path, opts, callback) {
this._oauthAuthorize((err) => {
if (err) {
return callback && callback(err);
}
this.logger.debug('Load', path);
const ts = this.logger.ts();
const urlParts = this.genUrl(path);
const groupId = urlParts[0];
path = urlParts[1];
const url = urlParts[2];
if (!groupId) {
const err = 'no group id defined';
return callback && callback(err);
}
this._xhr({
url,
responseType: 'json',
success: (response) => {
const downloadUrl = response['@microsoft.graph.downloadUrl'];
let rev = response.eTag;
if (!downloadUrl || !response.eTag) {
this.logger.debug(
'Load error',
path,
'no download url',
response,
this.logger.ts(ts)
);
return callback && callback('no download url');
}
this._xhr({
url: downloadUrl,
responseType: 'arraybuffer',
skipAuth: true,
success: (response, xhr) => {
rev = xhr.getResponseHeader('ETag') || rev;
this.logger.debug('Loaded', path, rev, this.logger.ts(ts));
return callback && callback(null, response, { rev });
},
error: (err) => {
this.logger.error('Load error', path, err, this.logger.ts(ts));
return callback && callback(err);
}
});
},
error: (err) => {
this.logger.error('Load error', path, err, this.logger.ts(ts));
return callback && callback(err);
}
});
});
}
stat(path, opts, callback) {
this._oauthAuthorize((err) => {
if (err) {
return callback && callback(err);
}
this.logger.debug('Stat', path);
const ts = this.logger.ts();
const urlParts = this.genUrl(path);
const groupId = urlParts[0];
path = urlParts[1];
const url = urlParts[2];
if (!groupId) {
const err = 'no group id defined';
return callback && callback(err);
}
this._xhr({
url,
responseType: 'json',
success: (response) => {
const rev = response.eTag;
if (!rev) {
this.logger.error('Stat error', path, 'no eTag', this.logger.ts(ts));
return callback && callback('no eTag');
}
this.logger.debug('Stated', path, rev, this.logger.ts(ts));
return callback && callback(null, { rev });
},
error: (err, xhr) => {
if (xhr.status === 404) {
this.logger.debug('Stated not found', path, this.logger.ts(ts));
return callback && callback({ notFound: true });
}
this.logger.error('Stat error', path, err, this.logger.ts(ts));
return callback && callback(err);
}
});
});
}
save(path, opts, data, callback, rev) {
this._oauthAuthorize((err) => {
if (err) {
return callback && callback(err);
}
this.logger.debug('Save', path, rev);
const ts = this.logger.ts();
const urlParts = this.genUrl(path);
const groupId = urlParts[0];
path = urlParts[1];
const url = urlParts[2] + ':/content';
if (!groupId) {
const err = 'no group id defined';
return callback && callback(err);
}
this._xhr({
url,
method: 'PUT',
responseType: 'json',
headers: rev ? { 'If-Match': rev } : null,
data,
statuses: [200, 201, 412],
success: (response, xhr) => {
rev = response.eTag;
if (!rev) {
this.logger.error('Save error', path, 'no eTag', this.logger.ts(ts));
return callback && callback('no eTag');
}
if (xhr.status === 412) {
this.logger.debug('Save conflict', path, rev, this.logger.ts(ts));
return callback && callback({ revConflict: true }, { rev });
}
this.logger.debug('Saved', path, rev, this.logger.ts(ts));
return callback && callback(null, { rev });
},
error: (err) => {
this.logger.error('Save error', path, err, this.logger.ts(ts));
return callback && callback(err);
}
});
});
}
list(dir, callback) {
this._oauthAuthorize((err) => {
if (err) {
return callback && callback(err);
}
this.logger.debug('List', dir);
const ts = this.logger.ts();
const urlParts = this.genUrl(dir);
const groupId = urlParts[0];
dir = urlParts[1];
const urlPath = groupId ? (dir ? ':/children' : '/drive/root/children') : '';
const url = urlParts[2] + urlPath;
const self = this;
self._groupId = groupId;
this._xhr({
url,
responseType: 'json',
success: (response) => {
if (!response || !response.value) {
this.logger.error('List error', this.logger.ts(ts), response);
return callback && callback('list error');
}
this.logger.debug('Listed', this.logger.ts(ts));
let fileList;
if (!self._groupId) {
fileList = response.value
.filter((f) => f.displayName)
.map((f) => ({
name: f.displayName,
path: '/' + f.id,
rev: f.id,
dir: true
}));
} else {
fileList = response.value
.filter((f) => f.name)
.map((f) => ({
name: f.name,
path: `/${self._groupId}${f.parentReference.path}/${f.name}`,
rev: f.eTag,
dir: !!f.folder
}));
}
return callback && callback(null, fileList);
},
error: (err) => {
this.logger.error('List error', this.logger.ts(ts), err);
return callback && callback(err);
}
});
});
}
remove(path, callback) {
this.logger.debug('Remove', path);
const ts = this.logger.ts();
const urlParts = this.genUrl(path);
const groupId = urlParts[0];
path = urlParts[1];
const url = urlParts[2];
if (!groupId) {
const err = 'no group id defined';
return callback && callback(err);
}
this._xhr({
url,
method: 'DELETE',
responseType: 'json',
statuses: [200, 204],
success: () => {
this.logger.debug('Removed', path, this.logger.ts(ts));
return callback && callback();
},
error: (err) => {
this.logger.error('Remove error', path, err, this.logger.ts(ts));
return callback && callback(err);
}
});
}
mkdir(path, callback) {
this._oauthAuthorize((err) => {
if (err) {
return callback && callback(err);
}
this.logger.debug('Make dir', path);
const ts = this.logger.ts();
const urlParts = this.genUrl(path);
const groupId = urlParts[0];
path = urlParts[1];
const url = urlParts[2] + '/drive/root/children';
if (!groupId) {
const err = 'no group id defined';
return callback && callback(err);
}
const data = JSON.stringify({ name: path.replace('/drive/root:/', ''), folder: {} });
this._xhr({
url,
method: 'POST',
responseType: 'json',
statuses: [200, 204],
data,
dataType: 'application/json',
success: () => {
this.logger.debug('Made dir', path, this.logger.ts(ts));
return callback && callback();
},
error: (err) => {
this.logger.error('Make dir error', path, err, this.logger.ts(ts));
return callback && callback(err);
}
});
});
}
logout(enabled) {
this._oauthRevokeToken();
}
_getOAuthConfig() {
let clientId = this.appSettings.msteamsClientId;
let clientSecret = this.appSettings.msteamsClientSecret;
let tenant = this.appSettings.msteamsTenantId;
if (!clientId) {
if (Features.isDesktop) {
({ id: clientId, secret: clientSecret, tenantId: tenant } = MsTeamsApps.Desktop);
} else if (Features.isLocal) {
({ id: clientId, secret: clientSecret, tenantId: tenant } = MsTeamsApps.Local);
} else {
({ id: clientId, secret: clientSecret, tenantId: tenant } = MsTeamsApps.Production);
}
}
tenant = tenant || 'common';
let scope = 'Sites.ReadWrite.All Team.ReadBasic.All';
if (!this.appSettings.shortLivedStorageToken) {
scope += ' offline_access';
}
return {
url: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
tokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
scope,
clientId,
clientSecret,
pkce: true,
width: 600,
height: 500
};
}
}
export { StorageTeams };
+1 -1
View File
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { StorageBase } from 'storage/storage-base';
import { Locale } from 'util/locale';
+2
View File
@@ -5,6 +5,7 @@ import { StorageFile } from 'storage/impl/storage-file';
import { StorageFileCache } from 'storage/impl/storage-file-cache';
import { StorageGDrive } from 'storage/impl/storage-gdrive';
import { StorageOneDrive } from 'storage/impl/storage-onedrive';
import { StorageTeams } from 'storage/impl/storage-teams';
import { StorageWebDav } from 'storage/impl/storage-webdav';
import { createOAuthSession } from 'storage/pkce';
@@ -17,6 +18,7 @@ const ThirdPartyStorage = {
dropbox: new StorageDropbox(),
gdrive: new StorageGDrive(),
onedrive: new StorageOneDrive(),
msteams: new StorageTeams(),
webdav: new StorageWebDav()
};
+3 -3
View File
@@ -1,12 +1,12 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
let newOAuthSession;
function createOAuthSession() {
const session = newOAuthSession;
const state = kdbxweb.ByteUtils.bytesToHex(kdbxweb.Random.getBytes(64));
const codeVerifier = kdbxweb.ByteUtils.bytesToHex(kdbxweb.Random.getBytes(50));
const state = kdbxweb.ByteUtils.bytesToHex(kdbxweb.CryptoEngine.random(64));
const codeVerifier = kdbxweb.ByteUtils.bytesToHex(kdbxweb.CryptoEngine.random(50));
const codeVerifierBytes = kdbxweb.ByteUtils.arrayToBuffer(
kdbxweb.ByteUtils.stringToBytes(codeVerifier)
+10 -2
View File
@@ -50,6 +50,10 @@ class StorageBase {
logout() {}
deleteStoredToken() {
delete this.runtimeData[this.name + 'OAuthToken'];
}
_xhr(config) {
this.logger.info('HTTP request', config.method || 'GET', config.url);
if (config.data) {
@@ -315,7 +319,9 @@ class StorageBase {
const token = this._oauthMsgToToken(message);
if (token && !token.error) {
this._oauthToken = token;
this.runtimeData[this.name + 'OAuthToken'] = token;
if (!this.appSettings.shortLivedStorageToken) {
this.runtimeData[this.name + 'OAuthToken'] = token;
}
this.logger.debug('OAuth token received');
}
return token;
@@ -343,7 +349,9 @@ class StorageBase {
_oauthGetNewToken(callback) {
this._oauthToken.expired = true;
this.runtimeData[this.name + 'OAuthToken'] = this._oauthToken;
if (!this.appSettings.shortLivedStorageToken) {
this.runtimeData[this.name + 'OAuthToken'] = this._oauthToken;
}
if (this._oauthToken.refreshToken) {
this._oauthExchangeRefreshToken(callback);
} else {
+1 -1
View File
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Logger } from 'util/logger';
import publicKeyData from 'public-key.pem';
import publicKeyDataNew from 'public-key-new.pem';
+9
View File
@@ -48,6 +48,15 @@ class EntrySearch {
return false;
}
}
if (filter.otp) {
if (
!this.model.fields.otp &&
!this.model.fields['TOTP Seed'] &&
this.model.backend !== 'otp-device'
) {
return false;
}
}
return true;
}
+44 -5
View File
@@ -17,19 +17,19 @@ const Features = {
!/^http(s?):\/\/((localhost:8085)|((app|beta)\.keeweb\.info))/.test(location.href),
isLocal: location.origin.indexOf('localhost') >= 0,
supportsTitleBarStyles() {
get supportsTitleBarStyles() {
return isDesktop && (this.isMac || this.isWindows);
},
supportsCustomTitleBarAndDraggableWindow() {
get supportsCustomTitleBarAndDraggableWindow() {
return isDesktop && this.isMac;
},
renderCustomTitleBar() {
get renderCustomTitleBar() {
return isDesktop && this.isWindows;
},
hasUnicodeFlags() {
get hasUnicodeFlags() {
return this.isMac;
},
getBrowserCssClass() {
get browserCssClass() {
if (window.chrome && window.navigator.userAgent.indexOf('Chrome/') > -1) {
return 'chrome';
}
@@ -40,6 +40,45 @@ const Features = {
return 'standalone';
}
return '';
},
get browserIcon() {
if (this._browserIcon) {
return this._browserIcon;
}
if (this.isDesktop) {
this._browserIcon = this.isMac ? 'safari' : this.isWindows ? 'edge' : 'chrome';
} else if (/Gecko\//.test(navigator.userAgent)) {
this._browserIcon = 'firefox-browser';
} else if (/Edg\//.test(navigator.userAgent)) {
this._browserIcon = 'edge';
} else if (/Chrome\//.test(navigator.userAgent)) {
this._browserIcon = 'chrome';
} else if (this.isMac && /Safari\//.test(navigator.userAgent)) {
this._browserIcon = 'safari';
} else {
this._browserIcon = 'window-maximize';
}
return this._browserIcon;
},
get supportsBrowserExtensions() {
return !this.isMobile && (this.isDesktop || this.browserIcon !== 'safari');
},
get extensionBrowserFamily() {
if (Features.isDesktop) {
return undefined;
} else if (/Gecko\//.test(navigator.userAgent)) {
return 'Firefox';
} else if (/Edg\//.test(navigator.userAgent)) {
return 'Edge';
} else if (/Chrome\//.test(navigator.userAgent)) {
return 'Chrome';
} else if (this.isMac && /Safari\//.test(navigator.userAgent)) {
return 'Safari';
} else {
return 'Chrome';
}
}
};
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
const IconUrlFormat = {
toDataUrl(iconData) {
+1 -1
View File
@@ -17,7 +17,7 @@ const MdToHtml = {
return '';
}
const renderer = new MdRenderer();
const html = marked(md, { renderer });
const html = marked(md, { renderer, breaks: true });
const htmlWithoutLineBreaks = html.replace(whiteSpaceRegex, '');
const mdWithoutLineBreaks = md.replace(whiteSpaceRegex, '');
if (htmlWithoutLineBreaks === mdWithoutLineBreaks) {
@@ -49,6 +49,8 @@ const PasswordPresenter = {
const topEl = document.createElement('div');
topEl.style.display = 'flex';
topEl.style.overflow = 'hidden';
topEl.style.textOverflow = 'ellipsis';
for (const item of items) {
const el = document.createElement('div');
+31
View File
@@ -2,6 +2,7 @@ const UrlFormat = {
multiSlashRegex: /\/{2,}/g,
lastPartRegex: /[\/\\]?[^\/\\]+$/,
kdbxEndRegex: /\.kdbx$/i,
maxShortPresentableUrlLength: 60,
getDataFileName(url) {
const ix = url.lastIndexOf('/');
@@ -35,6 +36,36 @@ const UrlFormat = {
return Object.entries(params)
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join('&');
},
presentAsShortUrl(url) {
if (url.length <= this.maxShortPresentableUrlLength) {
return url;
}
const [beforeHash] = url.split('#', 1);
if (beforeHash.length <= this.maxShortPresentableUrlLength) {
return beforeHash + '#…';
}
const [beforeQuestionMark] = url.split('?', 1);
if (beforeQuestionMark.length <= this.maxShortPresentableUrlLength) {
return beforeQuestionMark + '?…';
}
const parsed = new URL(beforeQuestionMark);
const pathParts = parsed.pathname.split('/');
while (pathParts.length > 1) {
pathParts.pop();
parsed.pathname = pathParts.join('/');
const res = parsed.toString();
if (res.length < this.maxShortPresentableUrlLength) {
return res + '/…';
}
}
return parsed + '…';
}
};
@@ -1,5 +1,6 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { phonetic } from 'util/generators/phonetic';
import { shuffle } from 'util/fn';
const CharRanges = {
upper: 'ABCDEFGHJKLMNPQRSTUVWXYZ',
@@ -41,18 +42,41 @@ const PasswordGenerator = {
}
const rangesByPatternChar = {
...DefaultCharRangesByPattern,
'X': ranges.join(''),
'I': opts.include || ''
};
const pattern = opts.pattern || 'X';
const randomBytes = kdbxweb.Random.getBytes(opts.length);
let countDefaultChars = 0;
for (let i = 0; i < opts.length; i++) {
const patternChar = pattern[i % pattern.length];
if (patternChar === 'X') {
countDefaultChars++;
}
}
const rangeIxRandomBytes = kdbxweb.CryptoEngine.random(countDefaultChars);
const rangeCharRandomBytes = kdbxweb.CryptoEngine.random(countDefaultChars);
const defaultRangeGeneratedChars = [];
for (let i = 0; i < countDefaultChars; i++) {
const rangeIx = i < ranges.length ? i : rangeIxRandomBytes[i] % ranges.length;
const range = ranges[rangeIx];
const char = range[rangeCharRandomBytes[i] % range.length];
defaultRangeGeneratedChars.push(char);
}
shuffle(defaultRangeGeneratedChars);
const randomBytes = kdbxweb.CryptoEngine.random(opts.length);
const chars = [];
for (let i = 0; i < opts.length; i++) {
const rand = Math.round(Math.random() * 1000) + randomBytes[i];
const patternChar = pattern[i % pattern.length];
const range = rangesByPatternChar[patternChar];
const char = range ? range[rand % range.length] : patternChar;
chars.push(char);
if (patternChar === 'X') {
chars.push(defaultRangeGeneratedChars.pop());
} else {
const range = rangesByPatternChar[patternChar];
const char = range ? range[rand % range.length] : patternChar;
chars.push(char);
}
}
return chars.join('');
},
+2 -1
View File
@@ -312,7 +312,8 @@ function generate(options) {
word: '',
opts: options
};
while (wordObj.word.length < length) {
const safeMaxLength = length + 5;
while (wordObj.word.length < safeMaxLength) {
addSyllable(wordObj);
}
return postProcess(wordObj).substr(0, length);
+2 -2
View File
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Logger } from 'util/logger';
import { Features } from 'util/features';
import { NativeModules } from 'comp/launcher/native-modules';
@@ -7,7 +7,7 @@ const logger = new Logger('argon2');
const KdbxwebInit = {
init() {
kdbxweb.CryptoEngine.argon2 = (...args) => this.argon2(...args);
kdbxweb.CryptoEngine.setArgon2Impl((...args) => this.argon2(...args));
},
argon2(password, salt, memory, iterations, length, parallelism, type, version) {
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
const ExpectedFieldRefChars = '{REF:0@I:00000000000000000000000000000000}'.split('');
const ExpectedFieldRefByteLength = ExpectedFieldRefChars.length;
@@ -6,8 +6,8 @@ const ExpectedFieldRefByteLength = ExpectedFieldRefChars.length;
kdbxweb.ProtectedValue.prototype.isProtected = true;
kdbxweb.ProtectedValue.prototype.forEachChar = function (fn) {
const value = this._value;
const salt = this._salt;
const value = this.value;
const salt = this.salt;
let b, b1, b2, b3;
for (let i = 0, len = value.length; i < len; i++) {
b = value[i] ^ salt[i];
@@ -154,7 +154,7 @@ kdbxweb.ProtectedValue.prototype.equals = function (other) {
return false;
}
for (let i = 0; i < len; i++) {
if ((this._value[i] ^ this._salt[i]) !== (other._value[i] ^ other._salt[i])) {
if ((this.value[i] ^ this.salt[i]) !== (other.value[i] ^ other.salt[i])) {
return false;
}
}
@@ -175,14 +175,14 @@ kdbxweb.ProtectedValue.prototype.isFieldReference = function () {
return true;
};
const RandomSalt = kdbxweb.Random.getBytes(128);
const RandomSalt = kdbxweb.CryptoEngine.random(128);
kdbxweb.ProtectedValue.prototype.saltedValue = function () {
if (!this.byteLength) {
return 0;
}
const value = this._value;
const salt = this._salt;
const value = this.value;
const salt = this.salt;
let salted = '';
for (let i = 0, len = value.length; i < len; i++) {
const byte = value[i] ^ salt[i];
@@ -193,8 +193,8 @@ kdbxweb.ProtectedValue.prototype.saltedValue = function () {
kdbxweb.ProtectedValue.prototype.dataAndSalt = function () {
return {
data: [...this._value],
salt: [...this._salt]
data: [...this.value],
salt: [...this.salt]
};
};
+36 -28
View File
@@ -58,7 +58,7 @@ class AppView extends View {
this.views.list.dragView = this.views.listDrag;
this.views.details = new DetailsView(undefined, { ownParent: true });
this.views.details.appModel = this.model;
if (this.titlebarStyle !== 'default' && Features.renderCustomTitleBar()) {
if (this.titlebarStyle !== 'default' && Features.renderCustomTitleBar) {
this.views.titlebar = new TitlebarView(this.model);
}
@@ -82,6 +82,7 @@ class AppView extends View {
this.listenTo(Events, 'toggle-settings', this.toggleSettings);
this.listenTo(Events, 'toggle-menu', this.toggleMenu);
this.listenTo(Events, 'toggle-details', this.toggleDetails);
this.listenTo(Events, 'show-open-view', this.showOpenIfNotThere);
this.listenTo(Events, 'edit-group', this.editGroup);
this.listenTo(Events, 'edit-tag', this.editTag);
this.listenTo(Events, 'edit-generator-presets', this.editGeneratorPresets);
@@ -119,13 +120,13 @@ class AppView extends View {
}
setWindowClass() {
const getBrowserCssClass = Features.getBrowserCssClass();
if (getBrowserCssClass) {
document.body.classList.add(getBrowserCssClass);
const browserCssClass = Features.browserCssClass;
if (browserCssClass) {
document.body.classList.add(browserCssClass);
}
if (this.titlebarStyle !== 'default') {
document.body.classList.add('titlebar-' + this.titlebarStyle);
if (Features.renderCustomTitleBar()) {
if (Features.renderCustomTitleBar) {
document.body.classList.add('titlebar-custom');
}
}
@@ -138,7 +139,7 @@ class AppView extends View {
super.render({
beta: this.model.isBeta,
titlebarStyle: this.titlebarStyle,
customTitlebar: Features.renderCustomTitleBar()
customTitlebar: Features.renderCustomTitleBar
});
this.panelEl = this.$el.find('.app__panel:first');
this.views.listWrap.render();
@@ -171,9 +172,6 @@ class AppView extends View {
this.views.open.on('close', () => {
this.showEntries();
});
this.views.open.on('remove', () => {
Events.emit('closed-open-view');
});
}
showLastOpenFile() {
@@ -202,7 +200,6 @@ class AppView extends View {
this.views.menu.show();
this.views.menuDrag.$el.parent().show();
this.views.listWrap.show();
this.views.list.show();
this.views.listDrag.show();
this.views.details.show();
this.views.footer.show();
@@ -211,6 +208,8 @@ class AppView extends View {
this.hideSettings();
this.hideKeyChange();
this.hideImportCsv();
this.views.list.show();
}
hideOpenFile() {
@@ -351,7 +350,6 @@ class AppView extends View {
launcherBeforeQuit() {
// this is currently called only on macos
const event = {
fromBeforeQuit: true,
preventDefault() {}
};
const result = this.beforeUnload(event);
@@ -372,24 +370,22 @@ class AppView extends View {
}
let minimizeInsteadOfClose = this.model.settings.minimizeOnClose;
if (e.fromBeforeQuit) {
if (Launcher.quitOnRealQuitEventIfMinimizeOnQuitIsEnabled()) {
minimizeInsteadOfClose = false;
}
if (Launcher?.quitOnRealQuitEventIfMinimizeOnQuitIsEnabled()) {
minimizeInsteadOfClose = false;
}
if (this.model.files.hasDirtyFiles()) {
const exit = () => {
if (minimizeInsteadOfClose) {
Launcher.minimizeApp();
} else {
Launcher.exit();
}
};
if (Launcher && Launcher.exitRequested) {
return;
}
if (Launcher) {
const exit = () => {
if (minimizeInsteadOfClose) {
Launcher.minimizeApp();
} else {
Launcher.exit();
}
};
if (Launcher.exitRequested) {
return;
}
if (!this.exitAlertShown) {
if (this.model.settings.autoSave) {
this.saveAndLock(
@@ -614,8 +610,11 @@ class AppView extends View {
}
closeAllFilesAndShowFirst() {
if (!this.model.files.hasOpenFiles()) {
return;
}
let fileToShow = this.model.files.find(
(file) => !file.demo && !file.created && !file.external
(file) => !file.demo && !file.created && !file.skipOpenList
);
this.model.closeAllFiles();
if (!fileToShow) {
@@ -650,7 +649,7 @@ class AppView extends View {
if (this.autoSaveTimer) {
clearInterval(this.autoSaveTimer);
}
if (this.model.settings.autoSaveInterval) {
if (this.model.settings.autoSaveInterval > 0) {
this.autoSaveTimer = setInterval(
this.saveAll.bind(this),
this.model.settings.autoSaveInterval * 1000 * 60
@@ -723,6 +722,12 @@ class AppView extends View {
this.views.menu.switchVisibility(false);
}
showOpenIfNotThere() {
if (!this.views.open) {
this.showLastOpenFile();
}
}
editGroup(group) {
if (group && !(this.views.panel instanceof GrpView)) {
this.showEditGroup(group);
@@ -832,7 +837,10 @@ class AppView extends View {
extLinkClick(e) {
if (Launcher) {
e.preventDefault();
Launcher.openLink(e.target.href);
const link = e.target.closest('a');
if (link?.href) {
Launcher.openLink(link.href);
}
}
}
+136 -70
View File
@@ -13,6 +13,7 @@ import { FieldViewDate } from 'views/fields/field-view-date';
import { FieldViewHistory } from 'views/fields/field-view-history';
import { FieldViewCustom } from 'views/fields/field-view-custom';
import { FieldViewReadOnlyWithOptions } from 'views/fields/field-view-read-only-with-options';
import { ExtraUrlFieldName } from 'models/entry-model';
function createDetailsFields(detailsView) {
const model = detailsView.model;
@@ -21,7 +22,7 @@ function createDetailsFields(detailsView) {
const fieldViews = [];
const fieldViewsAside = [];
if (model.external) {
if (model.backend === 'otp-device') {
fieldViewsAside.push(
new FieldViewReadOnly({
name: 'Device',
@@ -73,15 +74,27 @@ function createDetailsFields(detailsView) {
})
);
} else {
fieldViewsAside.push(
new FieldViewReadOnly({
name: 'File',
title: StringFormat.capFirst(Locale.file),
value() {
return model.fileName;
}
})
);
if (model.backend) {
fieldViewsAside.push(
new FieldViewReadOnly({
name: 'Storage',
title: StringFormat.capFirst(Locale.storage),
value() {
return model.fileName;
}
})
);
} else {
fieldViewsAside.push(
new FieldViewReadOnly({
name: 'File',
title: StringFormat.capFirst(Locale.file),
value() {
return model.fileName;
}
})
);
}
}
fieldViews.push(
new FieldViewAutocomplete({
@@ -127,26 +140,30 @@ function createDetailsFields(detailsView) {
sequence: '{NOTES}'
})
);
fieldViews.push(
new FieldViewTags({
name: 'Tags',
title: StringFormat.capFirst(Locale.tags),
tags: AppModel.instance.tags,
value() {
return model.tags;
}
})
);
fieldViews.push(
new FieldViewDate({
name: 'Expires',
title: Locale.detExpires,
lessThanNow: '(' + Locale.detExpired + ')',
value() {
return model.expires;
}
})
);
if (model.file.supportsTags) {
fieldViews.push(
new FieldViewTags({
name: 'Tags',
title: StringFormat.capFirst(Locale.tags),
tags: AppModel.instance.tags,
value() {
return model.tags;
}
})
);
}
if (model.file.supportsExpiration) {
fieldViews.push(
new FieldViewDate({
name: 'Expires',
title: Locale.detExpires,
lessThanNow: '(' + Locale.detExpired + ')',
value() {
return model.expires;
}
})
);
}
fieldViewsAside.push(
new FieldViewReadOnly({
name: 'Group',
@@ -159,33 +176,39 @@ function createDetailsFields(detailsView) {
}
})
);
fieldViewsAside.push(
new FieldViewReadOnly({
name: 'Created',
title: Locale.detCreated,
value() {
return DateFormat.dtStr(model.created);
}
})
);
fieldViewsAside.push(
new FieldViewReadOnly({
name: 'Updated',
title: Locale.detUpdated,
value() {
return DateFormat.dtStr(model.updated);
}
})
);
fieldViewsAside.push(
new FieldViewHistory({
name: 'History',
title: StringFormat.capFirst(Locale.history),
value() {
return { length: model.historyLength, unsaved: model.unsaved };
}
})
);
if (model.created) {
fieldViewsAside.push(
new FieldViewReadOnly({
name: 'Created',
title: Locale.detCreated,
value() {
return DateFormat.dtStr(model.created);
}
})
);
}
if (model.updated) {
fieldViewsAside.push(
new FieldViewReadOnly({
name: 'Updated',
title: Locale.detUpdated,
value() {
return DateFormat.dtStr(model.updated);
}
})
);
}
if (!model.backend) {
fieldViewsAside.push(
new FieldViewHistory({
name: 'History',
title: StringFormat.capFirst(Locale.history),
value() {
return { length: model.historyLength, unsaved: model.unsaved };
}
})
);
}
if (otpEntry) {
fieldViews.push(
new FieldViewOtp({
@@ -207,7 +230,7 @@ function createDetailsFields(detailsView) {
fieldViews.push(
new FieldViewOtp({
name: '$' + field,
title: field,
title: Locale.detOtpField,
value() {
return model.otpGenerator;
},
@@ -216,17 +239,32 @@ function createDetailsFields(detailsView) {
);
}
} else {
fieldViews.push(
new FieldViewCustom({
name: '$' + field,
title: field,
multiline: true,
value() {
return model.fields[field];
},
sequence: `{S:${field}}`
})
);
const isUrl = field.startsWith(ExtraUrlFieldName);
if (isUrl) {
fieldViews.push(
new FieldViewUrl({
name: '$' + field,
title: StringFormat.capFirst(Locale.website),
isExtraUrl: true,
value() {
return model.fields[field];
},
sequence: `{S:${field}}`
})
);
} else {
fieldViews.push(
new FieldViewCustom({
name: '$' + field,
title: field,
multiline: true,
value() {
return model.fields[field];
},
sequence: `{S:${field}}`
})
);
}
}
}
}
@@ -234,4 +272,32 @@ function createDetailsFields(detailsView) {
return { fieldViews, fieldViewsAside };
}
export { createDetailsFields };
function createNewCustomField(newFieldTitle, newFieldOptions, model) {
const isUrl = newFieldTitle.startsWith(ExtraUrlFieldName);
if (isUrl) {
return new FieldViewUrl(
{
name: '$' + newFieldTitle,
title: StringFormat.capFirst(Locale.website),
newField: newFieldTitle,
isExtraUrl: true,
value: () => model.fields[newFieldTitle]
},
newFieldOptions
);
} else {
return new FieldViewCustom(
{
name: '$' + newFieldTitle,
title: newFieldTitle,
newField: newFieldTitle,
multiline: true,
value: () => ''
},
newFieldOptions
);
}
}
export { createDetailsFields, createNewCustomField };
+47 -27
View File
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { View } from 'framework/views/view';
import { Events } from 'framework/events';
import { AutoType } from 'auto-type';
@@ -22,7 +22,7 @@ import { DetailsAutoTypeView } from 'views/details/details-auto-type-view';
import { DetailsHistoryView } from 'views/details/details-history-view';
import { DetailsIssuesView } from 'views/details/details-issues-view';
import { DropdownView } from 'views/dropdown-view';
import { createDetailsFields } from 'views/details/details-fields';
import { createDetailsFields, createNewCustomField } from 'views/details/details-fields';
import { FieldViewCustom } from 'views/fields/field-view-custom';
import { IconSelectView } from 'views/icon-select-view';
import { isEqual } from 'util/fn';
@@ -121,6 +121,9 @@ class DetailsView extends View {
}
const model = {
deleted: this.appModel.filter.trash,
canEditColor: this.model.file.supportsColors && !this.model.readOnly,
canEditIcon: this.model.file.supportsIcons && !this.model.readOnly,
showButtons: !this.model.backend && !this.model.readOnly,
...this.model
};
this.template = template;
@@ -185,7 +188,7 @@ class DetailsView extends View {
this.fieldViews = fieldViews.concat(fieldViewsAside);
if (!this.model.external) {
if (!this.model.backend) {
this.moreView = new DetailsAddFieldView();
this.moreView.render();
this.moreView.on('add-field', this.addNewField.bind(this));
@@ -193,10 +196,10 @@ class DetailsView extends View {
}
}
addNewField() {
addNewField(title) {
this.moreView.remove();
this.moreView = null;
let newFieldTitle = Locale.detNetField;
let newFieldTitle = title || Locale.detNetField;
if (this.model.fields[newFieldTitle]) {
for (let i = 1; ; i++) {
const newFieldTitleVariant = newFieldTitle + i;
@@ -206,20 +209,15 @@ class DetailsView extends View {
}
}
}
const fieldView = new FieldViewCustom(
{
name: '$' + newFieldTitle,
title: newFieldTitle,
newField: newFieldTitle,
multiline: true,
value() {
return '';
}
},
const fieldView = createNewCustomField(
newFieldTitle,
{
parent: this.$el.find('.details__body-fields')[0]
}
},
this.model
);
fieldView.on('change', this.fieldChanged.bind(this));
fieldView.render();
fieldView.edit();
@@ -252,6 +250,13 @@ class DetailsView extends View {
icon: 'plus',
text: Locale.detMenuAddNewField
});
if (this.model.url) {
moreOptions.push({
value: 'add-website',
icon: 'plus',
text: Locale.detMenuAddNewWebsite
});
}
moreOptions.push({
value: 'toggle-empty',
icon: 'eye',
@@ -263,6 +268,13 @@ class DetailsView extends View {
icon: 'plus',
text: Locale.detMenuAddNewField
});
if (this.model.url) {
moreOptions.push({
value: 'add-website',
icon: 'plus',
text: Locale.detMenuAddNewWebsite
});
}
moreOptions.push({
value: 'toggle-empty',
icon: 'eye-slash',
@@ -300,6 +312,9 @@ class DetailsView extends View {
case 'add-new':
this.addNewField();
break;
case 'add-website':
this.addNewField(this.model.getNextUrlFieldName());
break;
case 'toggle-empty': {
const hideEmptyFields = AppSettingsModel.hideEmptyFields;
AppSettingsModel.hideEmptyFields = !hideEmptyFields;
@@ -363,7 +378,7 @@ class DetailsView extends View {
}
toggleIcons() {
if (this.model.external) {
if (this.model.backend) {
return;
}
if (this.views.sub && this.views.sub instanceof IconSelectView) {
@@ -464,7 +479,7 @@ class DetailsView extends View {
}
this.model.initOtpGenerator?.();
if (this.model.external) {
if (this.model.backend === 'otp-device') {
return;
}
@@ -496,7 +511,7 @@ class DetailsView extends View {
if (!this.model) {
return;
}
if (this.model.external) {
if (this.model.backend === 'otp-device') {
this.copyOtp();
e.preventDefault();
}
@@ -520,7 +535,7 @@ class DetailsView extends View {
copyOtp() {
const otpField = this.getFieldView('$otp');
if (this.model.external) {
if (this.model.backend === 'otp-device') {
if (!otpField) {
return false;
}
@@ -600,10 +615,14 @@ class DetailsView extends View {
}
this.entryUpdated(true);
this.fieldViews.forEach(function (fieldView, ix) {
// TODO: render the view instead
if (
fieldView instanceof FieldViewCustom &&
!fieldView.model.newField &&
!this.model.hasField(fieldView.model.title)
(fieldView instanceof FieldViewCustom &&
!fieldView.model.newField &&
!this.model.hasField(fieldView.model.title)) ||
(fieldView.model.isExtraUrl &&
!fieldView.model.newField &&
!this.model.hasField(fieldView.model.name.replace('$', '')))
) {
fieldView.remove();
this.fieldViews.splice(ix, 1);
@@ -715,7 +734,7 @@ class DetailsView extends View {
}
editTitle() {
if (this.model.external) {
if (this.model.backend === 'otp-device') {
return;
}
const input = $('<input/>')
@@ -875,7 +894,7 @@ class DetailsView extends View {
const canCopy = document.queryCommandSupported('copy');
const options = [];
if (canCopy) {
if (this.model.external) {
if (this.model.backend === 'otp-device') {
options.push({
value: 'det-copy-otp',
icon: 'copy',
@@ -894,7 +913,7 @@ class DetailsView extends View {
text: Locale.detMenuCopyUser
});
}
if (!this.model.external) {
if (!this.model.backend) {
options.push({ value: 'det-add-new', icon: 'plus', text: Locale.detMenuAddNewField });
options.push({ value: 'det-clone', icon: 'clone', text: Locale.detClone });
if (canCopy) {
@@ -985,7 +1004,8 @@ class DetailsView extends View {
autoType(sequence) {
const entry = this.model;
const hasOtp = sequence?.includes('{TOTP}') || (entry.external && !sequence);
const hasOtp =
sequence?.includes('{TOTP}') || (entry.backend === 'otp-device' && !sequence);
if (hasOtp) {
const otpField = this.getFieldView('$otp');
otpField.refreshOtp((err) => {
@@ -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 };
@@ -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 };
@@ -0,0 +1,38 @@
import { View } from 'framework/views/view';
import template from 'templates/extension/extension-save-entry.hbs';
class ExtensionSaveEntryView extends View {
template = template;
events = {
'change #extension-save-entry__auto': 'autoChanged',
'change #extension-save-entry__group': 'groupChanged'
};
constructor(model) {
super(model);
const selectedGroup = model.allGroups.find((g) => g.selected);
this.config = {
askSave: model.askSave || 'always',
groupId: selectedGroup.id,
fileId: selectedGroup.fileId
};
}
render() {
super.render(this.model);
}
autoChanged(e) {
this.config.askSave = e.target.checked ? 'auto' : 'always';
}
groupChanged(e) {
const option = e.target.options[e.target.selectedIndex];
this.config.groupId = option.value;
this.config.fileId = option.dataset.file;
}
}
export { ExtensionSaveEntryView };
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Keys } from 'const/keys';
import { Locale } from 'util/locale';
import { Tip } from 'util/ui/tip';
@@ -38,7 +38,7 @@ class FieldViewCustom extends FieldViewText {
endEdit(newVal, extra) {
this.$el.removeClass('details__field--can-edit-title');
extra = { ...extra };
if (this.model.titleChanged || this.model.newField) {
if (this.model.titleChanged) {
extra.newField = this.model.title;
}
if (!this.editing) {
@@ -96,6 +96,7 @@ class FieldViewCustom extends FieldViewText {
fieldLabelClick(e) {
e.stopImmediatePropagation();
if (this.model.newField) {
this.startEditTitle(true);
} else if (this.editing) {
@@ -24,7 +24,6 @@ class FieldViewDate extends FieldViewText {
onSelect: this.pickerSelect.bind(this),
onClose: this.pickerClose.bind(this),
defaultDate: this.value,
minDate: new Date(),
firstDay: 1,
i18n: {
previousMonth: '',
@@ -5,7 +5,7 @@ class FieldViewReadOnly extends FieldView {
readonly = true;
renderValue(value) {
value = value.isProtected ? new Array(value.textLength + 1).join('•') : escape(value);
value = value?.isProtected ? new Array(value.textLength + 1).join('•') : escape(value);
value = value.replace(/\n/g, '<br/>');
return value;
}
+1 -1
View File
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Events } from 'framework/events';
import { KeyHandler } from 'comp/browser/key-handler';
import { Keys } from 'const/keys';
+6 -1
View File
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { View } from 'framework/views/view';
import { Events } from 'framework/events';
import { CopyPaste } from 'comp/browser/copy-paste';
@@ -45,6 +45,7 @@ class FieldView extends View {
multiline: this.model.multiline,
title: this.model.title,
canEditTitle: this.model.newField,
canGen: this.model.canGen,
protect: this.value && this.value.isProtected,
hasOptions: !Features.isMobile && renderedValue && this.hasOptions
});
@@ -187,6 +188,10 @@ class FieldView extends View {
}
const protectedEqual =
(newVal && newVal.isProtected) === (this.value && this.value.isProtected);
if (!extra?.newField && this.model.newField) {
extra ??= {};
extra.newField = this.model.newField;
}
const nameChanged = extra && extra.newField;
let arg;
if (newVal !== undefined && (!textEqual || !protectedEqual || nameChanged)) {
+5 -3
View File
@@ -175,10 +175,12 @@ class GeneratorView extends View {
}
btnOkClick() {
if (!CopyPaste.simpleCopy) {
CopyPaste.createHiddenInput(this.password);
if (this.model.copy) {
if (!CopyPaste.simpleCopy) {
CopyPaste.createHiddenInput(this.password);
}
CopyPaste.copy(this.password);
}
CopyPaste.copy(this.password);
this.emit('result', this.password);
this.remove();
}

Some files were not shown because too many files have changed in this diff Show More