Compare commits

...
311 Commits
Author SHA1 Message Date
antelle dfc6201285 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-07-18 16:21:23 +02:00
antelle 1930c265c8 Merge branch 'master' into release-1.18 2021-07-18 16:21:06 +02:00
antelle 1c434d7c79 release notes 2021-07-18 16:20:52 +02:00
antelle c0c75ac8a8 Merge branch 'master' into release-1.18 2021-07-18 16:19:59 +02:00
antelle 4b7124aa57 disabled automatic installation of KeePassXC-Browser extension 2021-07-18 16:19:28 +02:00
antelle 93cee76b4f up translations 2021-07-18 16:09:59 +02:00
antelle 1673580dfa bump version 2021-07-18 16:06:15 +02:00
antelle bf0e94582d release notes 2021-07-18 16:06:14 +02:00
antelle 29ae7e8ed2 fix #1845: fixed an error during writing to closed socket 2021-07-18 16:06:13 +02:00
antelle 9b2c40a078 added yubikeys code logging 2021-07-18 16:06:12 +02:00
antelle f0bb382449 fixed variable usage 2021-07-18 16:06:11 +02:00
antelle 062ddb3f72 added MS Teams apps 2021-07-18 16:06:10 +02:00
antelle 208a1b551e renames some ms teams constants 2021-07-18 16:06:09 +02:00
Saulo Alves f8af64e45b renamed teams to microsoft teams; fixed grunt prettier error in onedrive 2021-07-18 16:06:08 +02:00
Saulo Alves 98268f21c0 added teams storage; added tenant id configuration for onedrive and teams 2021-07-18 16:06:07 +02:00
antelle dcbe8d70da fixed donate button border 2021-07-18 16:06:06 +02:00
Dimitri Witkowski 1d61c6ca03 Merge pull request #1838 from magnetikonline/fix-keeweb-connect-extension-link
Fix broken links to download the KeeWeb Connect browser extension
2021-06-19 20:12:38 +02:00
Peter Mescalchin 433f0acf3c fix broken links to download KeeWeb Connect browser extension due to incorrect const/Links property 2021-06-05 15:11:53 +10: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 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 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 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 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 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
234 changed files with 9291 additions and 37050 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';
+4
View File
@@ -209,6 +209,10 @@ const YubiKey = {
complete: (err, stdout) => {
this.process = null;
if (window.debugYubiKey) {
logger.info('received codes', err, stdout);
}
if (this.aborted) {
return callback('Aborted');
}
+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 || '';
}
+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 };
+11 -1
View File
@@ -20,7 +20,17 @@ 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',
ExtensionHelpForKPXC:
'https://github.com/keeweb/keeweb/wiki/Browser-AutoFill#keepassxc-browser',
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'
};
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"
}
+109 -9
View File
@@ -14,6 +14,7 @@
"or": "ou",
"history": "historique",
"template": "modèle",
"templates": "templates",
"notImplemented": "Non Implémenté",
"saveChanges": "Sauvegarder les modifications",
"discardChanges": "Annuler les modifications",
@@ -46,16 +47,33 @@
"menuColors": "Couleurs",
"menuTrash": "Corbeille",
"menuSetGeneral": "Général",
"menuSetBrowser": "Navigateur",
"menuSetAbout": "À propos",
"menuSetDevices": "Appareils",
"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 tout",
"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",
@@ -64,6 +82,8 @@
"alertCopy": "Copier",
"alertClose": "Fermer",
"alertDoNotAsk": "Ne plus me le redemander",
"alertAllow": "Autoriser",
"alertDeny": "Refuser",
"appBeta": "ATTENTION: version beta, aperçu seulement",
"footerOpen": "Ouvrir/Nouveau",
"footerSyncError": "Erreur de synchronisation",
@@ -72,7 +92,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 +116,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",
@@ -123,6 +143,7 @@
"keyChangeMessageExpired": "La clé maître pour cette base de donnée est expirée. Merci de saisir une nouvelle clé.",
"keyChangeRepeatPassword": "Mot de passe, encore une fois",
"keyEnter": "Entrée",
"keyEsc": "Esc",
"iconFavTitle": "Télécharger et utiliser le favicon du site web",
"iconSelCustom": "Sélectionner une icône personnalisée",
"listEmptyTitle": "Vide",
@@ -193,6 +214,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,10 +268,11 @@
"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",
"detMenuAddNewWebsite": "Ajouter un autre site",
"detMenuShowEmpty": "Montrer les champs vides",
"detMenuHideEmpty": "Cacher les champs vides",
"detMenuAddField": "Ajouter {}",
@@ -301,13 +324,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",
@@ -318,6 +346,7 @@
"autoTypeSelectionHintOpt": "Entrez seulement le compte",
"autoTypeSelectionHintShift": "Autres champs",
"autoTypeSelectionOtp": "code à usage unique",
"autoTypeUnlockMessage": "Déverrouiller pour saisir automatiquement",
"appSecWarn": "Non sécurisé !",
"appSecWarnBody1": "Vous avez chargé cette appli avec une connexion non sécurisée. Quelqu'un peut vous observer et voler vos mots de passe. Nous vous recommandons fortement d'arrêter cette connexion, à moins que vous ne compreniez exactement ce que vous faites.",
"appSecWarnBody2": "Oui, votre base est chiffrée mais personne ne peut garantir que l'application n'a pas été modifiée avant d'arriver à vous.",
@@ -424,12 +453,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 +473,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",
@@ -589,6 +621,38 @@
"setPlTranslateLink": "traduire l'appli dans votre langue",
"setPlAutoUpdate": "Mise à jour automatique",
"setPlLoadGallery": "Charger le catalogue",
"setBrowserTitle": "Navigateur",
"setBrowserIntroDesktop": "KeeWeb peut entrer des mots de passe en utilisant la saisie automatique, cependant, une extension peut être un moyen plus pratique de le faire. KeeWeb prend en charge deux extensions de navigateur:",
"setBrowserIntroKeeWebConnect": "l'extension officielle KeeWeb est construite avec les fonctionnalités de KeeWeb, mais vous risquez de manquer certaines fonctionnalités avancées que l'on peut trouver dans l'autre extension.",
"setBrowserIntroKeePassXcBrowser": "cette extension a été développée pour KeePassXC, elle existe depuis un moment et est assez fiable. L'extension ne fait pas partie de KeeWeb et il peut y avoir des problèmes de compatibilité.",
"setBrowserIntroWeb": "Installez notre extension de navigateur pour remplir automatiquement les mots de passe de KeeWeb sur différentes pages. L'extension de navigateur se connectera à un onglet KeeWeb de votre navigateur pour récupérer les mots de passe. Cliquez ici pour télécharger l'extension:",
"setBrowserNotEnabled": "L'intégration du navigateur n'est pas activée, les extensions ne pourront pas se connecter à KeeWeb. Utilisez les cases à cocher ci-dessous pour l'activer:",
"setBrowserEnablePerBrowser": "Activer l'intégration dans le navigateur en cochant ci dessous:",
"setBrowserFocusIfLocked": "Ouvrir KeeWeb si le navigateur essaye de se connecter alors que aucun fichier n'est ouvert",
"setBrowserFocusIfEmpty": "Afficher la liste si aucune correspondance n'est trouvée avec l'URL",
"setBrowserOtherBrowsers": "Autres navigateurs",
"setBrowserExtensionFor": "pour {}",
"setBrowserExtensionNotSupported": "Non supporté",
"setBrowserExtensionHelp": "Comment installer ?",
"setBrowserExtensionInstall": "Installer l'application",
"setBrowserExtensionKPXCWarnHeader": "{} cessera de fonctionner",
"setBrowserExtensionKPXCWarnBody1": "Malheureusement, il n'est pas possible de connecter une extension à plusieurs applications. Si vous connectez l'extension à KeeWeb, nous remplacerons son association d'application, ce qui signifie que l'intégration avec {} cessera de fonctionner. Même si vous décochez cette case, l'association avec {} ne sera pas restaurée. Pour le faire fonctionner à nouveau, configurez l'intégration du navigateur dans les paramètres de {}.",
"setBrowserExtensionKPXCWarnBody2": "Configurer l'extension pour utiliser KeeWeb?",
"setBrowserSessions": "Sessions",
"setBrowserSessionsEmpty": "Aucune session connectée",
"setBrowserSessionsIntro": "Ces extensions sont connectées à KeeWeb",
"setBrowserSessionsActiveTooltip": "Session active",
"setBrowserSessionsActiveText": "Cette session est active. Elle peut échanger des données avec KeeWeb selon les permissions:",
"setBrowserSessionsInactiveTooltip": "session inactive",
"setBrowserSessionsInactiveText": "Cette session est inactive. L'extension est connectée à KeeWeb, cependant, elle n'a pas essayé d'échanger des données. Lorsque l'extension demande quelque chose, vous pourrez choisir ce que vous souhaitez partager.",
"setBrowserSessionsDeniedTooltip": "Accès interdit",
"setBrowserSessionsDeniedText": "Cette session est inactive. L'extension est connectée à KeeWeb mais vous avez refusé l'accès aux données.",
"setBrowserSessionsConnectedDate": "Connecté",
"setBrowserSessionsTerminate": "Clôturer la session",
"setBrowserSessionsAccessToFiles": "Autoriser l'accès aux fichiers",
"setBrowserSessionsNoFileAccess": "L'extension n'a accès à aucun fichier, elle ne pourra pas récupérer les mots de passe de KeeWeb.",
"setBrowserSessionsPasswordsRead": "Accès aux mot de passe",
"setBrowserSessionsPasswordsWritten": "Mots de passe enregistrés",
"setDevicesTitle": "Appareils",
"setDevicesEnableUsb": "Autoriser les interactions avec les appareils USB",
"setDevicesYubiKeyIntro": "Il est recommandé de lire {} avant d'utiliser une YubiKey.",
@@ -623,12 +687,12 @@
"setHelpProblems2": "ou {}",
"setHelpOpenIssue": "faire connaître le problème sur GitHub",
"setHelpContactLink": "contactez le développeur directement",
"setHelpAppInfo": "Information",
"setHelpAppInfo": "Informations",
"setHelpOtherPlatforms": "Autres plateformes",
"setHelpDesktopApps": "Applis Desktop",
"setHelpWebApp": "Appli web",
"setHelpUpdates": "Mises à jour",
"setHelpTwitter": "Appli twitter",
"setHelpTwitter": "Twitter",
"dropboxSetupDesc": "Une configuration particulière est nécessaire pour utiliser Dropbox dans une application auto-hébergée. Merci de créer votre propre application Dropbox et d'inscrire sa clé ci-dessous.",
"dropboxAppKey": "Clé Dropbox",
"dropboxAppKeyDesc": "Copier la clé de votre appli Dropbox (Réglages développeur)",
@@ -644,6 +708,8 @@
"dropboxLinkFull": "Tout Dropbox ou n'importe quel dossier",
"dropboxLinkCustom": "Votre appli Dropbox",
"gdriveSharedWithMe": "Partagé avec moi",
"gdriveSharedDrives": "Lecteurs partagés",
"gdriveTeamDrives": "Team drives",
"webdavSaveMethod": "Méthode de sauvegarde",
"webdavSaveMove": "Envoyer un fichier temporaire et le déplacer",
"webdavSavePut": "Écraser le fichier kdbx avec PUT",
@@ -672,5 +738,39 @@
"yubiKeyDisabledErrorHeader": "L'USB est désactivé",
"yubiKeyDisabledErrorBody": "Yubikey est nécessaire pour ouvrir ce fichier, merci d'activer les appareils USB dans les paramètres",
"yubiKeyErrorWithCode": "Erreur Yubikey code {}.",
"bioOpenAuthPrompt": "ouvrir \"{}\""
"bioOpenAuthPrompt": "ouvrir \"{}\"",
"extensionErrorNoOpenFiles": "Aucun fichier ouvert",
"extensionErrorUserRejected": "La requête a été refusée",
"extensionErrorNoMatches": "Aucune correspondance",
"extensionErrorAlertDisplayed": "Impossible de poser une question maintenant car une autre boîte de dialogue est affichée, veuillez réessayer",
"extensionConnectHeader": "Échange de données avec l'extension",
"extensionConnectIntro": "Une extension de navigateur qui a pour nom {} essaie d'échanger des données avec KeeWeb.",
"extensionConnectUnknownActivity": "KeeWeb ne vérifie pas que l'application connectée est ce qu'elle prétend être. N'approuvez la demande que si vous en connaissez l'origine.",
"extensionConnectFiles": "Lors de cette session, autoriser l'accès à:",
"extensionConnectAllOtherFiles": "Tous les autres fichiers",
"extensionConnectAllFiles": "Tous les fichiers",
"extensionConnectAskGet": "Demandez avant d'envoyer les mots de passe à l'extension:",
"extensionConnectAskGetMultiple": "s'il y a plus d'une correspondance",
"extensionConnectAskGetAlways": "Toujours",
"extensionConnectAskSave": "Demandez avant d'enregistrer de nouveaux mots de passe dans KeeWeb:",
"extensionConnectAskSaveAlways": "Toujours",
"extensionConnectAskSaveAuto": "quand ce n'est pas possible de sauvegarder automatiquement",
"extensionConnectSettingsAreForSession": "Les paramètres que vous sélectionnez ici ne sont valides que pour la session en cours. Vous pouvez afficher et gérer les sessions dans les paramètres de KeeWeb.",
"extensionUnlockMessage": "Déverrouiller pour connecter l'extension du navigateur",
"extensionNewGroupHeader": "Nouveau groupe",
"extensionNewGroupBody": "{} essaye de créer un nouveau groupe. Autoriser cela?",
"extensionNewGroupPath": "Dossier du groupe",
"extensionNewGroupFile": "Ce groupe sera créé dans:",
"extensionSaveEntryHeader": "Sauvegarder mot de passe",
"extensionSaveEntryBody": "{} essaye de sauvegarder un mot de passe. Autoriser cela?",
"extensionSaveEntryAuto": "Sauvegarder les autres mots de passe automatiquement lors de cette session",
"extensionSaveEntryNewGroup": "nouveau groupe",
"extensionSelectPasswordFor": "Sélectionner un mot de passe pour {}",
"selectEntryHeader": "Sélectionner une entrée",
"selectEntryEnterHint": "Utiliser la ligne sélectionnée",
"selectEntryTypingHint": "Ecrire pour filtrer",
"selectEntryContains": "Contient le texte",
"selectEntrySubdomains": "Sous-domaines",
"selectEntryFieldHeader": "Sélectionnez un champ",
"selectEntryFieldTouch": "Appuyez sur le bouton de votre appareil pour générer un code à usage unique."
}
+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' }
};
}
+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) {
+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) {
@@ -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';
+5 -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';
@@ -188,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();
}
+6 -1
View File
@@ -108,7 +108,12 @@ class IconSelectView extends View {
return url;
}
selectIcon() {
selectIcon(e) {
const btn = e.target.closest('.icon-select__icon-select');
if (btn.classList.contains('icon-select__icon--custom-selected')) {
return;
}
this.$el.find('.icon-select__file-input').click();
}
+33 -23
View File
@@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { View } from 'framework/views/view';
import { Scrollable } from 'framework/views/scrollable';
import template from 'templates/import-csv.hbs';
@@ -145,31 +145,41 @@ class ImportCsvView extends View {
runImport() {
let group = this.targetGroup;
let file = group ? group.file : undefined;
if (!group) {
let filePromise;
if (group) {
filePromise = Promise.resolve(group.file);
} else {
const fileName = this.fileName.replace(/\.csv$/i, '');
file = this.appModel.createNewFile(fileName);
group = file.groups[0];
filePromise = new Promise((resolve) => this.appModel.createNewFile(fileName, resolve));
}
for (const row of this.model.rows) {
const newEntry = EntryModel.newEntry(group, file);
for (let ix = 0; ix < row.length; ix++) {
let value = row[ix];
if (!value) {
continue;
}
const mapping = this.fieldMapping[ix];
if (mapping.type === 'ignore' || !mapping.field) {
continue;
}
if (mapping.field === 'Password') {
value = kdbxweb.ProtectedValue.fromString(value);
}
newEntry.setField(mapping.field, value);
filePromise.then((file) => {
if (!group) {
group = file.groups[0];
}
}
file.reload();
this.emit('done');
for (const row of this.model.rows) {
const newEntry = EntryModel.newEntry(group, file);
for (let ix = 0; ix < row.length; ix++) {
let value = row[ix];
if (!value) {
continue;
}
const mapping = this.fieldMapping[ix];
if (mapping.type === 'ignore' || !mapping.field) {
continue;
}
if (mapping.field === 'Password') {
value = kdbxweb.ProtectedValue.fromString(value);
}
newEntry.setField(mapping.field, value);
}
}
file.reload();
this.emit('done');
});
}
}
+124 -12
View File
@@ -68,12 +68,20 @@ class ListView extends View {
this.readTableColumnsEnabled();
this.items = new SearchResultCollection();
this.renderedItems = new Map();
}
render() {
if (!this.isVisible()) {
this.pendingRender = true;
return;
}
this.pendingRender = false;
if (!this.itemsEl) {
super.render();
this.itemsEl = this.$el.find('.list__items>.scroller');
this.itemsEl.on('scroll', () => this.renderVisibleItems());
this.views.search.render();
this.setTableView();
@@ -84,9 +92,9 @@ class ListView extends View {
});
}
if (this.items.length) {
const itemTemplate = this.getItemTemplate();
const itemsTemplate = this.getItemsTemplate();
const noColor = AppSettingsModel.colorfulIcons ? '' : 'grayscale';
const presenter = new EntryPresenter(
this.getDescField(),
noColor,
@@ -99,16 +107,29 @@ class ListView extends View {
}
});
presenter.columns = columns;
let itemsHtml = '';
this.items.forEach((item) => {
presenter.present(item);
itemsHtml += itemTemplate(presenter, DefaultTemplateOptions);
}, this);
this.presenter = presenter;
presenter.present(this.items[0]);
const itemTemplate = this.getItemTemplate();
const itemsHtml = itemTemplate(presenter, DefaultTemplateOptions);
presenter.reset();
const html = itemsTemplate(
{ itemsHtml, columns: this.tableColumns },
DefaultTemplateOptions
);
this.itemsEl.html(html);
this.itemsContainerEl = this.itemsEl.find('.list__items-container:first')[0];
const firstListItem = this.itemsContainerEl.firstElementChild;
this.itemHeight = firstListItem.getBoundingClientRect().height;
this.renderedItems = new Map([[0, firstListItem]]);
const totalHeight = this.itemHeight * this.items.length;
this.itemsContainerEl.style.minHeight = totalHeight + 'px';
this.renderVisibleItems();
} else {
this.itemsEl.html(this.emptyTemplate({}, DefaultTemplateOptions));
}
@@ -117,16 +138,12 @@ class ListView extends View {
getItemsTemplate() {
if (this.model.settings.tableView) {
return require('templates/list-table.hbs');
return require('templates/list-mode-table.hbs');
} else {
return this.renderPlainItems;
return require('templates/list-mode-list.hbs');
}
}
renderPlainItems(data) {
return data.itemsHtml;
}
getItemTemplate() {
if (this.model.settings.tableView) {
return require('templates/list-item-table.hbs');
@@ -135,6 +152,90 @@ class ListView extends View {
}
}
renderVisibleItems() {
if (!this.isVisible()) {
return;
}
const scrollEl = this.itemsEl[0];
const rect = scrollEl.getBoundingClientRect();
const pxTop = scrollEl.scrollTop;
const pxHeight = rect.height;
const itemHeight = this.itemHeight;
const renderedItems = this.renderedItems;
let firstIx = Math.max(0, Math.floor(pxTop / itemHeight));
let lastIx = Math.min(this.items.length - 1, Math.ceil((pxTop + pxHeight) / itemHeight));
const visibleCount = lastIx - firstIx;
firstIx = Math.max(0, firstIx - visibleCount);
lastIx = Math.min(this.items.length - 1, lastIx + visibleCount);
const itemTemplate = this.getItemTemplate();
const presenter = this.presenter;
let itemsHtml = '';
const renderedIndices = [];
for (let ix = firstIx; ix <= lastIx; ix++) {
const item = this.items[ix];
if (renderedItems.has(ix)) {
continue;
}
presenter.present(item);
itemsHtml += itemTemplate(presenter, DefaultTemplateOptions);
renderedIndices.push(ix);
}
presenter.reset();
const tempEl = document.createElement('div');
tempEl.innerHTML = itemsHtml;
const renderedElements = [...tempEl.children];
for (let i = 0; i < renderedElements.length; i++) {
const el = renderedElements[i];
const ix = renderedIndices[i];
this.itemsContainerEl.append(el);
el.style.top = ix * itemHeight + 'px';
renderedItems.set(ix, el);
}
const maxRenderedItems = visibleCount * 5;
if (renderedItems.size > maxRenderedItems) {
for (const [ix, el] of this.renderedItems) {
if (ix < firstIx || ix > lastIx) {
el.remove();
renderedItems.delete(ix);
}
}
}
}
ensureItemRendered(ix) {
if (this.renderedItems.has(ix)) {
return;
}
const item = this.items[ix];
const itemTemplate = this.getItemTemplate();
this.presenter.present(item);
const itemHtml = itemTemplate(this.presenter, DefaultTemplateOptions);
this.presenter.reset();
const tempEl = document.createElement('div');
tempEl.innerHTML = itemHtml;
const [el] = tempEl.children;
this.itemsContainerEl.append(el);
el.style.top = ix * this.itemHeight + 'px';
this.renderedItems.set(ix, el);
}
getDescField() {
return this.model.sort.replace('-', '');
}
@@ -203,7 +304,12 @@ class ListView extends View {
}
selectItem(item) {
this.presenter.activeEntryId = item.id;
this.model.activeEntryId = item.id;
const ix = this.items.indexOf(item);
this.ensureItemRendered(ix);
Events.emit('entry-selected', item);
this.itemsEl.find('.list__item--active').removeClass('list__item--active');
const itemEl = document.getElementById(item.id);
@@ -220,6 +326,9 @@ class ListView extends View {
viewShown() {
this.views.search.show();
if (this.pendingRender) {
this.render();
}
}
viewHidden() {
@@ -248,6 +357,7 @@ class ListView extends View {
viewResized(size) {
this.setSize(size);
this.throttleSetViewSizeSetting(size);
this.renderVisibleItems();
}
throttleSetViewSizeSetting = throttle((size) => {
@@ -256,6 +366,7 @@ class ListView extends View {
filterChanged(filter) {
this.items = filter.entries;
this.renderedItems = new Map();
this.render();
}
@@ -263,6 +374,7 @@ class ListView extends View {
const scrollTop = this.itemsEl[0].scrollTop;
this.render();
this.itemsEl[0].scrollTop = scrollTop;
this.renderVisibleItems();
}
itemDragStart(e) {
+1 -1
View File
@@ -41,7 +41,7 @@ class ModalView extends View {
document.activeElement.blur();
}, 20);
if (this.model.view) {
this.model.view.parent = '.modal__body';
this.model.view.parent = this.el.querySelector('.modal__body');
this.model.view.render();
}
}
+42 -7
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 { Storage } from 'storage';
@@ -52,6 +52,7 @@ class OpenView extends View {
'click .open__settings-yubikey': 'selectYubiKeyChalResp',
'click .open__last-item': 'openLast',
'click .open__icon-generate': 'toggleGenerator',
'click .open__message-cancel-btn': 'openMessageCancelClick',
dragover: 'dragover',
dragleave: 'dragleave',
drop: 'drop'
@@ -76,6 +77,7 @@ class OpenView extends View {
this.onKey(Keys.DOM_VK_UP, this.moveOpenFileSelectionUp, null, 'open');
this.listenTo(Events, 'main-window-focus', this.windowFocused.bind(this));
this.listenTo(Events, 'usb-devices-changed', this.usbDevicesChanged.bind(this));
this.listenTo(Events, 'unlock-message-changed', this.unlockMessageChanged.bind(this));
this.once('remove', () => {
this.passwordInput.reset();
});
@@ -118,6 +120,7 @@ class OpenView extends View {
canOpenKeyFromDropbox: !Launcher && Storage.dropbox.enabled,
demoOpened: this.model.settings.demoOpened,
storageProviders,
unlockMessageRes: this.model.unlockMessageRes,
canOpen: this.model.settings.canOpen,
canOpenDemo: this.model.settings.canOpenDemo,
canOpenSettings: this.model.settings.canOpenSettings,
@@ -211,11 +214,17 @@ class OpenView extends View {
fileSelected(e) {
const file = e.target.files[0];
if (file) {
this.processFile(file, (success) => {
if (success && !file.path && this.reading === 'fileData') {
this.showLocalFileAlert();
}
});
if (this.model.settings.canImportCsv && /\.csv$/.test(file.name)) {
Events.emit('import-csv-requested', file);
} else if (this.model.settings.canImportXml && /\.xml$/.test(file.name)) {
this.setFile(file, null, this.showLocalFileAlert.bind(this));
} else {
this.processFile(file, (success) => {
if (success && !file.path && this.reading === 'fileData') {
this.showLocalFileAlert();
}
});
}
}
}
@@ -674,16 +683,24 @@ class OpenView extends View {
const encryptedPassword = kdbxweb.ProtectedValue.fromBase64(
this.encryptedPassword.value
);
Events.emit('hardware-decrypt-started');
NativeModules.hardwareDecrypt(encryptedPassword, touchIdPrompt)
.then((password) => {
Events.emit('hardware-decrypt-finished');
this.params.password = password;
this.params.encryptedPassword = this.encryptedPassword;
this.model.openFile(this.params, (err) => this.openDbComplete(err));
})
.catch((err) => {
Events.emit('hardware-decrypt-finished');
if (err.message.includes('User refused')) {
err.userCanceled = true;
} else if (err.message.includes('SecKeyCreateDecryptedData')) {
err.maybeTouchIdChanged = true;
}
logger.error('Error in hardware decryption', err);
this.openDbComplete(err);
});
} else {
@@ -712,9 +729,13 @@ class OpenView extends View {
if (err.notFound) {
err = Locale.openErrorFileNotFound;
}
let alertBody = Locale.openErrorDescription;
if (err.maybeTouchIdChanged) {
alertBody += '\n' + Locale.openErrorDescriptionMaybeTouchIdChanged;
}
Alerts.error({
header: Locale.openError,
body: Locale.openErrorDescription,
body: alertBody,
pre: this.errorToString(err)
});
}
@@ -1141,6 +1162,20 @@ class OpenView extends View {
}
}
}
unlockMessageChanged(unlockMessageRes) {
const messageEl = this.el.querySelector('.open__message');
messageEl.classList.toggle('hide', !unlockMessageRes);
if (unlockMessageRes) {
const contentEl = this.el.querySelector('.open__message-content');
contentEl.innerText = Locale[unlockMessageRes];
}
}
openMessageCancelClick() {
this.model.rejectPendingFileUnlockPromise('User canceled');
}
}
export { OpenView };

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