Compare commits

..
261 Commits
Author SHA1 Message Date
antelle 2c386e67e2 plugin settings 2021-07-04 20:54:59 +02:00
antelle b572876099 global shortcuts 2021-07-04 17:12:28 +02:00
antelle 48c4f28d5d settings 2021-07-04 16:01:34 +02:00
antelle 8df3899920 advanced settings 2021-07-04 14:35:54 +02:00
antelle f9a79bf2c4 general settings 2021-07-04 14:02:43 +02:00
antelle 809dd897f7 settings scrolling 2021-07-04 13:15:19 +02:00
antelle 8d2e5b12aa fixed storage logout 2021-07-04 13:05:42 +02:00
antelle d5a61acd01 settings 2021-07-04 13:02:48 +02:00
antelle aa11e1a65e settings general pages 2021-07-03 21:55:11 +02:00
antelle 839afdda36 storage listing 2021-07-03 20:22:54 +02:00
antelle 40b85c76fb storage file listing 2021-07-03 20:05:28 +02:00
antelle da8ba46d21 icon style fix 2021-06-19 13:03:57 +02:00
antelle b15fd7258d icon selection 2021-06-19 12:49:16 +02:00
antelle 701d593b5b math.div sass migration 2021-06-19 11:50:37 +02:00
antelle c8ad6c8416 grp 2021-06-17 22:36:51 +02:00
antelle b859daf39b up deps 2021-06-17 22:36:10 +02:00
antelle d2d58e675a removed test code 2021-06-14 21:09:38 +02:00
antelle 518b34873b auto-type hint 2021-06-14 21:08:55 +02:00
antelle 763ee2e1f5 group 2021-06-14 07:25:29 +02:00
antelle 9dd8ee05e9 eslint 2021-06-13 12:03:03 +02:00
antelle b4a49e2321 generator presets 2021-06-13 12:02:46 +02:00
antelle 04c2f487fc tag panel 2021-06-13 10:34:44 +02:00
antelle 708ac26837 panel mode 2021-06-13 10:11:45 +02:00
antelle 9431a1bbf5 generator presets link 2021-06-12 22:22:08 +02:00
antelle 006e841a84 ci linting 2021-06-12 21:18:49 +02:00
antelle 7a9a16ce0e eslint 2021-06-12 21:14:05 +02:00
antelle 342242b2c3 about 2021-06-12 20:52:39 +02:00
antelle 06ece835c7 generator fix 2021-06-12 20:40:23 +02:00
antelle 78f322b51a moved some models 2021-06-12 20:39:01 +02:00
antelle 88b25a54cb generator fix 2021-06-12 20:15:55 +02:00
antelle 638b1a9aac menu fix 2021-06-12 20:14:19 +02:00
antelle e526d0e3f0 fixed double context menu 2021-06-12 17:16:05 +02:00
antelle 75da650a80 footer generator shortcut 2021-06-12 16:36:16 +02:00
antelle 8a9243b639 fix 2021-06-12 16:32:21 +02:00
antelle 2baf2b0dcf hide menu on any key 2021-06-12 16:30:25 +02:00
antelle ec126d52e2 fixed list sorting 2021-06-12 16:23:23 +02:00
antelle 3c2cab94d7 list context menu 2021-06-12 16:19:19 +02:00
antelle 12dea11a9a generator 2021-06-12 13:47:00 +02:00
antelle bf0192412a Model.reset 2021-06-12 10:01:19 +02:00
antelle acad35a52b fixed displaying first item 2021-06-06 15:54:37 +02:00
antelle 671212c23d eslint 2021-06-06 10:22:42 +02:00
antelle 146de47e0c list size calculation 2021-06-06 10:21:17 +02:00
antelle 7187cc5e1d fixed list jitter 2021-06-06 10:18:24 +02:00
antelle 0a656830c8 tooltip fixes 2021-06-05 23:42:46 +02:00
antelle 8142025fdd filtering 2021-06-05 22:39:03 +02:00
antelle 98b78b3856 item height 2021-06-05 22:05:23 +02:00
antelle 5140d729e2 scrolling 2021-06-05 21:15:44 +02:00
antelle 3a4b1f56d9 scrolling 2021-06-05 20:14:56 +02:00
antelle bd5d8bded9 list search 2021-06-05 18:39:49 +02:00
antelle ee6c96cdc8 modular lodash 2021-06-02 22:00:45 +02:00
antelle 720118c5df menu filters 2021-06-01 23:58:32 +02:00
antelle 3ec6e2f8e8 removed a prop 2021-06-01 22:03:40 +02:00
antelle af540cf250 settings about and help 2021-06-01 21:30:21 +02:00
antelle d9ed7ced22 settings menu 2021-06-01 20:35:34 +02:00
antelle 61a78107c4 settings page 2021-06-01 20:01:04 +02:00
antelle 3de2f1d25d query 2021-06-01 18:31:32 +02:00
antelle e0fa3e18d1 query 2021-06-01 17:57:29 +02:00
antelle 3e49fc4c75 file backups 2021-06-01 17:10:30 +02:00
antelle 6fe9d56b3c settings mode 2021-05-31 23:48:30 +02:00
antelle 32126939a6 sync 2021-05-31 22:25:03 +02:00
antelle 7962631a03 OpenParams 2021-05-30 22:53:01 +02:00
antelle 10dc9a4528 fixed open 2021-05-30 22:49:42 +02:00
antelle e93cdae808 cleanup 2021-05-30 22:22:23 +02:00
antelle cd9ba11d24 logging modals 2021-05-30 22:21:53 +02:00
antelle f7d6802f58 opening files 2021-05-30 22:18:38 +02:00
antelle b1b1911eba open 2021-05-30 16:26:14 +02:00
antelle d2536d8dcd displaying an import error 2021-05-29 22:32:11 +02:00
antelle 500eaa4569 tests tsconfig 2021-05-29 22:12:28 +02:00
antelle 6f068ebf18 better tests compatibility 2021-05-29 22:04:59 +02:00
antelle f840f7b15d local file alert 2021-05-29 21:40:20 +02:00
antelle c172477004 rename 2021-05-29 21:31:22 +02:00
antelle 9e76878c7c xml 2021-05-29 20:12:31 +02:00
antelle 06fef55e2c drag-drop 2021-05-29 19:29:39 +02:00
antelle 0ce5fc462a eslint 2021-05-29 16:22:45 +02:00
antelle 40dd3637d7 open screen progress 2021-05-29 16:16:16 +02:00
antelle caa17a48a8 open idle 2021-05-29 08:59:58 +02:00
antelle 2fe980f2ab open view split 2021-05-28 23:16:14 +02:00
antelle 0b38897868 password focus 2021-05-28 20:10:07 +02:00
antelle f3a7f75be3 moved a variable 2021-05-28 19:50:13 +02:00
antelle 0bf3c0d43f useKey hook 2021-05-28 19:38:29 +02:00
antelle 6a25732a9f simplified code 2021-05-28 17:37:39 +02:00
antelle 117955bcf9 removed a manifest check 2021-05-28 17:17:48 +02:00
antelle 95d2731d13 list wrap 2021-05-28 17:08:18 +02:00
antelle f7da54dded plugins 2021-05-28 16:42:00 +02:00
antelle 87aebe9216 plugins 2021-05-28 15:51:20 +02:00
antelle 6bc140e123 plugin vars 2021-05-28 14:28:13 +02:00
antelle 4897f9e41e plugin gallery 2021-05-28 13:20:25 +02:00
antelle 4a7797df6c menu selection 2021-05-28 11:56:33 +02:00
antelle f438ed6d54 cleanup 2021-05-28 11:47:08 +02:00
antelle 9230389a91 moved a component 2021-05-28 11:44:06 +02:00
antelle 9857f818ca dragging 2021-05-28 11:43:10 +02:00
antelle c6bc6b059e cleanup 2021-05-28 09:24:31 +02:00
antelle 7556ebf974 scroll 2021-05-28 09:22:17 +02:00
antelle 2591b22f35 readonly 2021-05-27 23:14:01 +02:00
antelle 748c70cd0f optimized id generation a bit 2021-05-27 23:13:33 +02:00
antelle d0a6791f21 id 2021-05-27 21:54:55 +02:00
antelle 8beef89250 fix 2021-05-27 21:50:46 +02:00
antelle b5a19b0186 properly destroying nodes 2021-05-27 21:50:30 +02:00
antelle b530f6d29a shortcuts 2021-05-27 21:40:48 +02:00
antelle d71b7d2f3d menu improvements 2021-05-27 21:33:27 +02:00
antelle 170f50f6fc menu 2021-05-27 20:57:27 +02:00
antelle d01aebe484 alerts fix 2021-05-27 18:50:11 +02:00
antelle 881ffa115e todo 2021-05-27 17:48:17 +02:00
antelle 9cc9958bdf footer 2021-05-27 17:39:19 +02:00
antelle 16d2bd6786 opening demo 2021-05-27 17:13:44 +02:00
antelle 1e607d30e6 open-view 2021-05-27 16:58:27 +02:00
antelle 1923234ffa secure-input 2021-05-27 16:05:15 +02:00
antelle 7a25b0fd39 mode 2021-05-27 14:38:14 +02:00
antelle 1dbf647b1b open view 2021-05-27 14:37:36 +02:00
antelle 0b3b49189f eslint 2021-05-27 14:06:03 +02:00
antelle 73b8ae6431 fix 2021-05-27 14:04:23 +02:00
antelle 4402db826d alerts 2021-05-27 14:03:07 +02:00
antelle 95078ea2f1 renamed localized-with 2021-05-27 12:10:38 +02:00
antelle b270ad52b4 titlebar 2021-05-27 12:09:21 +02:00
antelle 810f785a1f app-view 2021-05-27 09:51:42 +02:00
antelle 8f5e19b64b workspace 2021-05-27 09:42:49 +02:00
antelle 3017b39e5f file-info 2021-05-26 09:33:49 +02:00
antelle c2ad509283 disabled input 2021-05-25 22:45:38 +02:00
antelle 540c607527 secure-input 2021-05-25 22:35:25 +02:00
antelle 17d51ee5f1 yubikey 2021-05-25 21:26:50 +02:00
antelle 522e356fe3 filter 2021-05-25 20:25:53 +02:00
antelle 5c0756855a kdbx-to-html 2021-05-25 19:56:23 +02:00
antelle 38466f934f version 2021-05-25 18:51:27 +02:00
antelle c198e634d4 release notes 2021-05-25 18:51:12 +02:00
antelle 45e1f87d07 file, group, entry 2021-05-25 18:44:53 +02:00
antelle 89f3741f17 cleanup 2021-05-25 08:57:04 +02:00
antelle dba37cc40a locale 2021-05-25 08:56:21 +02:00
antelle a2c3d13bc4 cleanup 2021-05-25 08:32:14 +02:00
antelle c576b52b97 optional 2021-05-25 08:24:05 +02:00
antelle 4234cf5de6 optional field 2021-05-25 08:23:44 +02:00
antelle b7cd6e97e4 added FileStorageExtraOptions 2021-05-25 08:23:28 +02:00
antelle 03cf6c0d50 removed a weird module 2021-05-25 08:20:01 +02:00
antelle 9658535f15 fullscreen 2021-05-24 23:53:53 +02:00
antelle 3c30f1519e extracted window-class 2021-05-24 23:48:44 +02:00
antelle afbf0f369f storage 2021-05-24 23:42:08 +02:00
antelle 4527de78f0 fixed file init 2021-05-24 22:54:59 +02:00
antelle 0dd453e0e2 better model init 2021-05-24 22:51:33 +02:00
antelle bdb7ce22c0 chalresp 2021-05-24 22:46:23 +02:00
antelle 1c7a41ad4f backup 2021-05-24 22:43:20 +02:00
antelle 42126fa5a9 file-info 2021-05-24 22:37:34 +02:00
antelle 9baf47cd6d menu 2021-05-24 22:26:27 +02:00
antelle f25353135d menu 2021-05-24 22:26:04 +02:00
antelle f5d2936594 fix 2021-05-24 18:57:12 +02:00
antelle 8be52aee53 config-loader 2021-05-24 18:49:45 +02:00
antelle 0306f604aa logger.info 2021-05-24 18:03:06 +02:00
antelle 04789ecc22 rename 2021-05-24 18:02:02 +02:00
antelle c55aceb937 logger debug 2021-05-24 18:01:26 +02:00
antelle b8363a53f3 updater 2021-05-24 17:51:24 +02:00
antelle 372acf88b1 simplified code 2021-05-24 16:43:32 +02:00
antelle 4d7336ddec extracted PropertiesOfType 2021-05-24 16:41:26 +02:00
antelle d85cf24f8a improved runtime-data 2021-05-24 16:37:41 +02:00
antelle f64c146d2c moved some components 2021-05-24 16:06:09 +02:00
antelle a83e035ac8 renamed some events 2021-05-24 14:00:35 +02:00
antelle 520438814c cleanup 2021-05-24 13:47:07 +02:00
antelle f716fe50de tip 2021-05-24 13:19:50 +02:00
antelle 6a7df2444d rename 2021-05-24 10:01:25 +02:00
antelle 89c644a1c4 readme 2021-05-23 08:27:33 +02:00
antelle 02a505e2f9 webpack 2021-05-23 08:25:01 +02:00
antelle b46533d243 non-enumerable emitter 2021-05-23 01:02:30 +02:00
antelle 76ed537d61 bootstrap 2021-05-23 00:51:37 +02:00
antelle 6da998f13a views 101 2021-05-23 00:12:02 +02:00
antelle c8a854b60c rename 2021-05-22 23:32:29 +02:00
antelle 582211dde7 cleanup 2021-05-22 22:54:13 +02:00
antelle 9ee4ed6ef9 storage 2021-05-22 22:46:46 +02:00
antelle ee11875459 storage 2021-05-22 17:39:10 +02:00
antelle fb973e3021 storage 2021-05-21 10:08:18 +02:00
antelle bbbef70984 storage oauth listener 2021-05-20 09:53:32 +02:00
antelle ba043e09e9 html-renderer 2021-05-20 09:30:31 +02:00
antelle 84df59498a pkce 2021-05-20 09:13:14 +02:00
antelle 7e3f134024 io-cache 2021-05-20 08:54:28 +02:00
antelle 1eb354a322 kdbxweb-init 2021-05-19 20:48:42 +02:00
antelle 826c761dc6 some restructure 2021-05-19 19:18:31 +02:00
antelle 14b2d10e17 moved desktop-ipc 2021-05-19 17:57:42 +02:00
antelle 404dd1baff qr reader 2021-05-19 17:37:43 +02:00
antelle 19e79187fc app rights checker 2021-05-19 17:08:21 +02:00
antelle bf9f81b80b test 2021-05-19 16:09:57 +02:00
antelle ae1f141b63 deleted a class 2021-05-19 15:49:04 +02:00
antelle 971cee45a0 transport 2021-05-19 15:47:36 +02:00
antelle 0a7e285188 shortcuts 2021-05-19 14:07:29 +02:00
antelle 0047b54f50 some refactoring 2021-05-19 13:06:07 +02:00
antelle 8692aed447 restructured files 2021-05-19 12:26:44 +02:00
antelle 52cef043e1 merge fix 2021-05-19 10:44:18 +02:00
antelle 5c9f4c042e Merge branch 'develop' into ts
# Conflicts:
#	app/scripts/comp/launcher/launcher-electron.js
2021-05-19 10:44:03 +02:00
antelle 47b19d1d74 Merge branch 'master' into develop 2021-05-19 10:42:24 +02:00
antelle 1c5be170c5 more components 2021-05-19 09:57:47 +02:00
antelle a0ec451696 more components 2021-05-19 08:58:34 +02:00
antelle a97897ce6f native modules 2021-05-18 23:22:41 +02:00
antelle f431951ec5 alerts 2021-05-18 23:22:28 +02:00
antelle 5a0389444a logging 2021-05-16 23:04:56 +02:00
antelle 52d10b279f export-api 2021-05-16 23:03:26 +02:00
antelle 10f52c9027 runtime data model 2021-05-16 22:56:50 +02:00
antelle 5a8d0f47a3 stricter checking 2021-05-16 15:22:52 +02:00
antelle c66ce36d70 date-format 2021-05-16 15:06:45 +02:00
antelle dfb4859007 settings-manager 2021-05-16 15:04:02 +02:00
antelle cb79552103 idle-tracker 2021-05-16 14:45:40 +02:00
antelle b2846d0a29 feature tester 2021-05-16 14:41:16 +02:00
antelle 55844451f2 theme watcher 2021-05-16 14:32:06 +02:00
antelle 743cba7538 removed supported flags from launcher 2021-05-16 14:26:41 +02:00
antelle 2fb58ebb27 more code style 2021-05-16 14:25:03 +02:00
antelle a68874bc34 enforcing code style 2021-05-16 14:15:21 +02:00
antelle cc12720856 more checks 2021-05-16 13:57:39 +02:00
antelle 1659103ab8 better types 2021-05-16 13:55:56 +02:00
antelle 22994253c5 changed desktop-ipc location 2021-05-16 13:33:34 +02:00
antelle 52da7649e1 changed a symlink 2021-05-16 13:32:15 +02:00
antelle 8ea3502d8e setting deletion 2021-05-16 13:22:13 +02:00
antelle cabe9e3ae7 storage tests fix 2021-05-16 13:14:02 +02:00
antelle 7af9556d59 app settings 2021-05-16 11:30:44 +02:00
antelle e5ef7ea065 removed a cast 2021-05-15 23:24:09 +02:00
antelle 49f82746a8 cleanup 2021-05-15 23:23:07 +02:00
antelle e086cf392b settings store 2021-05-15 23:21:02 +02:00
antelle 8e45ffbddc launcher 2021-05-15 22:51:06 +02:00
antelle 7bb59fed76 checking batch set without listeners 2021-05-15 20:41:42 +02:00
antelle a8e1665214 derived model events support 2021-05-15 20:37:54 +02:00
antelle 3c4604595a signature verifier test 2021-05-15 17:33:03 +02:00
antelle fbc4cab3a8 -jquery 2021-05-15 17:10:21 +02:00
antelle e8e1f3cc70 moved eslint-disable closer 2021-05-15 17:03:12 +02:00
antelle b990a087a1 removed eslint-disable 2021-05-15 16:59:09 +02:00
antelle 84ac7568ee fix eslint 2021-05-15 16:54:04 +02:00
antelle 093f6e7e25 records 2021-05-15 16:34:56 +02:00
antelle 849ae772e2 moved model 2021-05-15 16:26:31 +02:00
antelle 2fd0bf4339 model 2021-05-15 16:13:05 +02:00
antelle 68ce33c34b events 2021-05-14 23:29:17 +02:00
antelle 882c730738 modules 2021-05-14 23:25:44 +02:00
antelle 55ad94bbb9 todo-ts 2021-05-14 23:22:30 +02:00
antelle 54870ab8fd cleanup 2021-05-14 23:21:38 +02:00
antelle edc32041fd package-lock 2021-05-14 21:20:01 +02:00
antelle 4b6c8876e7 Merge branch 'develop' into ts
# Conflicts:
#	package-lock.json
#	package.json
2021-05-14 21:19:28 +02:00
antelle d7e11e20c1 Merge branch 'master' into develop 2021-05-14 21:18:46 +02:00
antelle 6cdca3af38 ignored an inspection 2021-05-14 19:09:09 +02:00
antelle 27b8633bc2 improved locale 2021-05-14 18:50:45 +02:00
antelle a035796503 signature verifier tests 2021-05-13 23:59:20 +02:00
antelle 7feed36dac more otp tests 2021-05-13 23:41:46 +02:00
antelle 6c7a824608 locale 2021-05-13 20:01:08 +02:00
antelle ed2c5af7e9 locale 2021-05-13 13:56:29 +02:00
antelle 0c143d0491 moved locales out of scripts 2021-05-13 13:05:57 +02:00
antelle 9efce05f11 eslint fix 2021-05-13 12:39:22 +02:00
antelle 4903b1e141 npm run eslint 2021-05-13 12:12:07 +02:00
antelle 5718f31fe6 otp 2021-05-13 12:11:18 +02:00
antelle 860dd5bdcd file-saver 2021-05-13 10:13:17 +02:00
antelle 6326620cd8 kdbxweb tests 2021-05-13 10:09:50 +02:00
antelle c6c9c19be6 utils 2021-05-13 09:33:36 +02:00
antelle ac38b8f60a features 2021-05-13 09:15:59 +02:00
antelle 91e5a4c894 logger 2021-05-13 09:10:42 +02:00
antelle 385224077b more utils 2021-05-13 09:01:55 +02:00
antelle a011bf7ec2 Merge branch 'develop' into ts 2021-05-12 21:43:59 +02:00
antelle 58a6a97130 Merge branch 'master' into develop 2021-05-12 21:43:27 +02:00
antelle 71d9dd84ae password strength 2021-05-10 23:49:40 +02:00
antelle eaf5875d4c more utils 2021-05-10 23:41:54 +02:00
antelle caae1ac801 more utils 2021-05-10 23:21:18 +02:00
antelle 746d0196b3 protected-value 2021-05-10 23:10:36 +02:00
antelle f04f54cc82 fn.ts 2021-05-10 21:23:16 +02:00
antelle 8132325e11 eslintrc formatting 2021-05-10 20:31:19 +02:00
antelle 57684537fa first test 2021-05-10 18:30:12 +02:00
antelle 167be95bd4 const 2021-05-10 18:21:56 +02:00
antelle 6cdf53f1a4 Merge branch 'develop' into ts 2021-05-10 18:12:36 +02:00
antelle 6d06d825e5 fixed donate button border 2021-05-10 18:12:11 +02:00
antelle c923e46321 sorted dependencies 2021-05-10 17:57:54 +02:00
antelle b122231485 tests 2021-05-10 17:56:09 +02:00
antelle 73f94402dd TS setup 2021-05-09 23:55:40 +02:00
antelle 0800d5f33a Merge branch 'master' into develop 2021-05-09 23:25:27 +02:00
antelle 6929931cae Merge branch 'master' into develop 2021-05-09 09:04:12 +02:00
493 changed files with 34504 additions and 27065 deletions
+7 -1
View File
@@ -1,6 +1,12 @@
{
"presets": [],
"presets": [
["@babel/typescript", { "jsxPragma": "h" }]
],
"plugins": [
["@babel/transform-react-jsx", {
"runtime": "automatic",
"importSource": "preact"
}],
["@babel/plugin-proposal-class-properties", { "loose": true }],
["@babel/plugin-proposal-object-rest-spread", { "loose": true,
"useBuiltIns": true
-69
View File
@@ -1,69 +0,0 @@
{
"plugins": ["prettier", "import", "babel"],
"extends": ["standard", "eslint:recommended", "plugin:prettier/recommended"],
"rules": {
"semi": ["error", "always"],
"one-var": "off",
"space-before-function-paren": "off",
"no-throw-literal": "off",
"camelcase": ["error", { "properties": "always" }],
"no-console": "error",
"no-alert": "error",
"no-debugger": "error",
"prefer-arrow-callback": "error",
"object-property-newline": "off",
"no-useless-escape": "off",
"no-var": "error",
"prefer-const": "error",
"no-unused-expressions": "off",
"strict": ["error", "never"],
"no-mixed-operators": "off",
"prefer-promise-reject-errors": "off",
"standard/no-callback-literal": "off",
"object-curly-spacing": "off",
"quote-props": "off",
"no-new-object": "error",
"object-shorthand": "error",
"no-array-constructor": "error",
"array-callback-return": "error",
"no-eval": "error",
"no-new-func": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"no-useless-constructor": "error",
"no-dupe-class-members": "error",
"no-duplicate-imports": "error",
"eqeqeq": "error",
"no-unneeded-ternary": "error",
"curly": "error",
"prettier/prettier": "error",
"no-empty": "off",
"no-restricted-syntax": [
"error",
{
"selector": "ExportDefaultDeclaration",
"message": "Prefer named exports"
}
],
"import/no-webpack-loader-syntax": "off",
"import/no-relative-parent-imports": "error",
"import/first": "error",
"import/no-default-export": "error",
"babel/no-unused-expressions": "error",
"node/no-callback-literal": "off"
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"impliedStrict": true
}
},
"parser": "babel-eslint",
"globals": {
"_": true,
"$": true
},
"env": {
"browser": true
}
}
+243
View File
@@ -0,0 +1,243 @@
{
"plugins": [
"prettier",
"import",
"babel",
"react",
"@typescript-eslint"
],
"extends": [
"standard",
"eslint:recommended",
"plugin:prettier/recommended"
],
"rules": {
"semi": [
"error",
"always"
],
"one-var": "off",
"space-before-function-paren": "off",
"no-throw-literal": "off",
"camelcase": [
"error",
{
"properties": "always"
}
],
"no-console": "error",
"no-alert": "error",
"no-debugger": "error",
"prefer-arrow-callback": "error",
"object-property-newline": "off",
"no-useless-escape": "off",
"no-var": "error",
"prefer-const": "error",
"no-unused-expressions": "off",
"strict": [
"error",
"never"
],
"no-mixed-operators": "off",
"prefer-promise-reject-errors": "off",
"standard/no-callback-literal": "off",
"object-curly-spacing": "off",
"quote-props": "off",
"no-new-object": "error",
"object-shorthand": "error",
"no-array-constructor": "error",
"array-callback-return": "error",
"no-eval": "error",
"no-new-func": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"no-useless-constructor": "error",
"no-dupe-class-members": "error",
"no-duplicate-imports": "error",
"eqeqeq": "error",
"no-unneeded-ternary": "error",
"curly": "error",
"prettier/prettier": "error",
"no-empty": "off",
"no-restricted-syntax": [
"error",
{
"selector": "ExportDefaultDeclaration",
"message": "Prefer named exports"
}
],
"import/no-webpack-loader-syntax": "off",
"import/no-relative-parent-imports": "error",
"import/first": "error",
"import/no-default-export": "error",
"babel/no-unused-expressions": "error",
"node/no-callback-literal": "off"
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"impliedStrict": true
}
},
"parser": "babel-eslint",
"overrides": [
{
"files": [
"*.ts",
"*.tsx"
],
"extends": [
"standard",
"eslint:recommended",
"preact",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:import/typescript"
],
"rules": {
"semi": [
"error",
"always"
],
"one-var": "off",
"space-before-function-paren": "off",
"no-throw-literal": "error",
"camelcase": [
"error",
{
"properties": "always"
}
],
"no-console": "error",
"no-alert": "error",
"no-debugger": "error",
"prefer-arrow-callback": "error",
"object-property-newline": "off",
"no-useless-escape": "off",
"no-var": "error",
"prefer-const": "error",
"no-unused-expressions": "off",
"strict": [
"error",
"never"
],
"no-mixed-operators": "off",
"prefer-promise-reject-errors": "off",
"standard/no-callback-literal": "off",
"object-curly-spacing": "off",
"quote-props": "off",
"no-new-object": "error",
"object-shorthand": "error",
"no-array-constructor": "error",
"array-callback-return": "error",
"no-eval": "error",
"no-new-func": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"no-useless-constructor": "error",
"no-dupe-class-members": "off",
"no-duplicate-imports": "error",
"eqeqeq": "error",
"no-unneeded-ternary": "error",
"curly": "error",
"prettier/prettier": "error",
"no-empty": "off",
"no-restricted-syntax": [
"error",
{
"selector": "ExportDefaultDeclaration",
"message": "Prefer named exports"
}
],
"import/no-webpack-loader-syntax": "off",
"import/no-relative-parent-imports": "error",
"import/first": "error",
"import/no-default-export": "error",
"babel/no-unused-expressions": "error",
"node/no-callback-literal": "off",
"import/no-commonjs": "off",
"prefer-template": "off",
"no-else-return": "off",
"jest/valid-expect": "off",
"jest/expect-expect": "off",
"react-hooks/exhaustive-deps": "off",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-module-boundary-types": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/naming-convention": [
"error",
{
"selector": ["objectLiteralProperty", "classProperty", "typeProperty"],
"modifiers": ["private", "protected"],
"format": ["camelCase"],
"leadingUnderscore": "require",
"trailingUnderscore": "forbid"
},
{
"selector": ["classProperty", "typeProperty"],
"modifiers": ["public"],
"format": ["camelCase", "PascalCase", "UPPER_CASE", "snake_case"],
"leadingUnderscore": "forbid",
"trailingUnderscore": "forbid"
},
{
"selector": ["method"],
"format": ["camelCase"],
"leadingUnderscore": "forbid",
"trailingUnderscore": "forbid"
},
{
"selector": ["class", "enum"],
"format": ["PascalCase"],
"leadingUnderscore": "forbid",
"trailingUnderscore": "forbid"
},
{
"selector": ["enumMember"],
"format": ["PascalCase", "UPPER_CASE"],
"leadingUnderscore": "forbid",
"trailingUnderscore": "forbid"
},
{
"selector": ["variable"],
"format": ["camelCase", "PascalCase", "UPPER_CASE"],
"leadingUnderscore": "forbid",
"trailingUnderscore": "forbid"
}
]
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 11,
"sourceType": "module",
"project": [
"tsconfig.json",
"jsconfig.json"
]
}
}
],
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [
".ts",
".tsx"
]
}
},
"globals": {
"_": true,
"$": true
},
"env": {
"browser": true,
"node": true,
"es2020": true
}
}
+11 -13
View File
@@ -25,10 +25,12 @@ jobs:
registry-url: 'https://registry.npmjs.org'
- name: Install npm modules
run: npm ci
- name: Lint
run: npm lint
- name: Test
run: npm test
- name: Grunt
run: grunt
- name: Build
run: npm start
- name: Upload artifact
uses: actions/upload-artifact@v1
with:
@@ -41,7 +43,7 @@ jobs:
mkdir keys
echo "$VIRUS_TOTAL" > keys/virus-total.json
- name: Check on VirusTotal
run: grunt virustotal
run: npm start virustotal
if: ${{ github.repository == 'keeweb/keeweb' }}
linux:
@@ -129,8 +131,6 @@ jobs:
registry-url: 'https://registry.npmjs.org'
- name: Install npm modules
run: npm ci
- name: Install grunt
run: sudo npm i -g grunt-cli
- name: Write secrets
env:
CODESIGN: ${{ secrets.CODESIGN }}
@@ -147,8 +147,8 @@ jobs:
with:
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: Grunt
run: grunt --max-old-space-size=4096 desktop-darwin ${{ github.repository != 'keeweb/keeweb' && '--skip-sign' || '' }}
- name: Build
run: npm start -- --max-old-space-size=4096 desktop-darwin ${{ github.repository != 'keeweb/keeweb' && '--skip-sign' || '' }}
- name: Upload x64 dmg artifact
uses: actions/upload-artifact@v1
with:
@@ -188,8 +188,6 @@ jobs:
registry-url: 'https://registry.npmjs.org'
- name: Install npm modules
run: npm ci
- name: Install grunt
run: npm i -g grunt-cli
- name: Write secrets
env:
CODESIGN: ${{ secrets.CODESIGN }}
@@ -203,8 +201,8 @@ jobs:
echo $Env:MS_CODESIGN_PRIVATE_KEY_CLIENT > keys/code-signing/private-key-client.pem
echo $Env:MS_CODESIGN_PUBLIC_KEY_CLIENT > keys/code-signing/public-key-client.pem
echo $Env:MS_CODESIGN_PUBLIC_KEY_SERVER > keys/code-signing/public-key-server.pem
- name: Grunt
run: grunt desktop-win32 ${{ github.repository != 'keeweb/keeweb' && '--skip-sign' || '' }}
- name: Build
run: npm start -- desktop-win32 ${{ github.repository != 'keeweb/keeweb' && '--skip-sign' || '' }}
- name: Upload ia32 exe artifact
uses: actions/upload-artifact@v1
with:
@@ -353,9 +351,9 @@ jobs:
mkdir keys
echo "$PRIVATE_KEY" > keys/private-key.pem
echo "$KEEWEB_SIGN" > keys/keeweb-sign.json
- name: Grunt
- name: Build
working-directory: keeweb
run: grunt finish-release
run: npm start finish-release
- name: Copy signatures to assets
run: cp keeweb/dist/desktop/Verify.sign.sha256 assets
- name: Copy checksums to assets
+14 -70
View File
@@ -1,12 +1,10 @@
/* eslint-env node */
const fs = require('fs-extra');
const path = require('path');
const { execSync } = require('child_process');
const debug = require('debug');
const webpackConfig = require('./build/webpack.config');
const webpackConfigTest = require('./test/test.webpack.config');
const pkg = require('./package.json');
debug.enable('electron-notarize');
@@ -93,11 +91,6 @@ module.exports = function (grunt) {
desktop: ['tmp/desktop', 'dist/desktop']
},
copy: {
html: {
src: 'app/index.html',
dest: 'tmp/index.html',
nonull: true
},
'content-dist': {
cwd: 'app/content/',
src: '**',
@@ -105,13 +98,6 @@ module.exports = function (grunt) {
expand: true,
nonull: true
},
icons: {
cwd: 'app/icons/',
src: ['*.png', '*.svg'],
dest: 'tmp/icons/',
expand: true,
nonull: true
},
'dist-icons': {
cwd: 'app/icons/',
src: ['*.png', '*.svg'],
@@ -119,13 +105,6 @@ module.exports = function (grunt) {
expand: true,
nonull: true
},
manifest: {
cwd: 'app/manifest/',
src: ['*.json', '*.xml'],
dest: 'tmp/',
expand: true,
nonull: true
},
'dist-manifest': {
cwd: 'app/manifest/',
src: ['*.json', '*.xml'],
@@ -148,8 +127,7 @@ module.exports = function (grunt) {
'desktop-darwin-installer-helper-x64': {
cwd: 'tmp/desktop/KeeWeb Installer.app',
src: '**',
dest:
'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
expand: true,
nonull: true,
options: { mode: true }
@@ -157,8 +135,7 @@ module.exports = function (grunt) {
'desktop-darwin-installer-helper-arm64': {
cwd: 'tmp/desktop/KeeWeb Installer.app',
src: '**',
dest:
'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
dest: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
expand: true,
nonull: true,
options: { mode: true }
@@ -229,50 +206,43 @@ module.exports = function (grunt) {
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',
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',
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',
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',
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',
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',
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: {
app: ['app/scripts/**/*.js'],
desktop: ['desktop/**/*.js', '!desktop/node_modules/**'],
app: ['app/scripts/**/*.js', 'app/scripts/**/*.ts'],
test: ['test/**/*.ts'],
desktop: ['desktop/**/*.js'],
build: ['Gruntfile.js', 'grunt.*.js', 'build/**/*.js', 'webpack.config.js'],
plugins: ['plugins/**/*.js'],
util: ['util/**/*.js'],
@@ -348,27 +318,7 @@ module.exports = function (grunt) {
}
},
webpack: {
app: webpackConfig.config(webpackOptions),
test: webpackConfigTest
},
'webpack-dev-server': {
options: {
webpack: webpackConfig.config({
...webpackOptions,
mode: 'development',
sha: 'dev'
}),
publicPath: '/',
contentBase: [
path.resolve(__dirname, 'tmp'),
path.resolve(__dirname, 'app/content')
],
progress: false
},
js: {
keepalive: true,
port: 8085
}
app: webpackConfig.config(webpackOptions)
},
electron: {
options: {
@@ -774,12 +724,6 @@ module.exports = function (grunt) {
}
}
},
'run-test': {
options: {
headless: true
},
default: 'test/runner.html'
},
virustotal: {
options: {
prefix: `keeweb.v${pkg.version}-${sha}.`,
+1 -1
View File
@@ -44,7 +44,7 @@ The app can be built with grunt: `grunt` (html files will be in `dist/`).
Desktop apps are built with `grunt desktop`. This requires some magic and currently works only on CI,
you can find more details in [the GitHub Actions workflow](.github/workflows/build.yaml).
To run the desktop (electron) app without building an installer, build the app with `grunt` and start it this way:
To run the desktop (electron) app in development mode, start it this way:
```bash
npm run dev
npm run electron
-16
View File
@@ -1,16 +0,0 @@
<!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>
+7 -8
View File
@@ -73,13 +73,12 @@
<link rel="stylesheet" href="css/app.css?__inline=true" />
<script src="js/app.js?__inline=true"></script>
</head>
<body class="th-d">
<noscript>
<h1>KeeWeb</h1>
<p>
KeeWeb is a password manager written in JavaScript. Please enable JavaScript to run
it.
</p>
</noscript>
<body class="th-dark">
<div class="app">
<noscript>
<h1>KeeWeb</h1>
<p>KeeWeb is a password manager written in JavaScript. Please enable JavaScript to run it.</p>
</noscript>
</div>
</body>
</html>
@@ -43,7 +43,6 @@
"dropbox": "Dropbox",
"gdrive": "Google Drive",
"onedrive": "OneDrive",
"msteams": "Microsoft Teams",
"menuAllItems": "All Items",
"menuColors": "Colors",
"menuTrash": "Trash",
@@ -726,6 +725,7 @@
"exportHtmlDate": "Export date",
"exportGenerator": "Software",
"exportDescription": "This file is generated with {}.",
"exportEntries": "Entries",
"importCsvTitle": "Import from CSV",
"importCsvRun": "Import",
"importIgnoreField": "Ignore",
@@ -14,7 +14,6 @@
"or": "ou",
"history": "historique",
"template": "modèle",
"templates": "templates",
"notImplemented": "Non Implémenté",
"saveChanges": "Sauvegarder les modifications",
"discardChanges": "Annuler les modifications",
@@ -47,7 +46,6 @@
"menuColors": "Couleurs",
"menuTrash": "Corbeille",
"menuSetGeneral": "Général",
"menuSetBrowser": "Navigateur",
"menuSetAbout": "À propos",
"menuSetDevices": "Appareils",
"menuAlertNoTags": "Aucun tag",
@@ -62,7 +60,7 @@
"sysMenuServices": "Services",
"sysMenuHide": "Cacher {}",
"sysMenuHideOthers": "Cacher autres",
"sysMenuUnhide": "Montrer tout",
"sysMenuUnhide": "Montrer tous",
"sysMenuQuit": "Quitter {}",
"sysMenuEdit": "Editer",
"sysMenuUndo": "Annuler",
@@ -82,8 +80,6 @@
"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",
@@ -143,7 +139,6 @@
"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",
@@ -272,7 +267,6 @@
"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 {}",
@@ -346,7 +340,6 @@
"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.",
@@ -621,38 +614,6 @@
"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.",
@@ -687,12 +648,12 @@
"setHelpProblems2": "ou {}",
"setHelpOpenIssue": "faire connaître le problème sur GitHub",
"setHelpContactLink": "contactez le développeur directement",
"setHelpAppInfo": "Informations",
"setHelpAppInfo": "Information",
"setHelpOtherPlatforms": "Autres plateformes",
"setHelpDesktopApps": "Applis Desktop",
"setHelpWebApp": "Appli web",
"setHelpUpdates": "Mises à jour",
"setHelpTwitter": "Twitter",
"setHelpTwitter": "Appli 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)",
@@ -708,8 +669,6 @@
"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",
@@ -738,39 +697,5 @@
"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 \"{}\"",
"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."
"bioOpenAuthPrompt": "ouvrir \"{}\""
}
-5
View File
@@ -1,5 +0,0 @@
{
"rules": {
"import/no-commonjs": "error"
}
}
-212
View File
@@ -1,212 +0,0 @@
import { Events } from 'framework/events';
import { StartProfiler } from 'comp/app/start-profiler';
import { FileInfoCollection } from 'collections/file-info-collection';
import { AppRightsChecker } from 'comp/app/app-rights-checker';
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';
import { ThemeWatcher } from 'comp/browser/theme-watcher';
import { KeyHandler } from 'comp/browser/key-handler';
import { PopupNotifier } from 'comp/browser/popup-notifier';
import { Launcher } from 'comp/launcher';
import { SettingsManager } from 'comp/settings/settings-manager';
import { Alerts } from 'comp/ui/alerts';
import { Timeouts } from 'const/timeouts';
import { AppModel } from 'models/app-model';
import { AppSettingsModel } from 'models/app-settings-model';
import { RuntimeDataModel } from 'models/runtime-data-model';
import { UpdateModel } from 'models/update-model';
import { PluginManager } from 'plugins/plugin-manager';
import { Features } from 'util/features';
import { KdbxwebInit } from 'util/kdbxweb/kdbxweb-init';
import { Locale } from 'util/locale';
import { AppView } from 'views/app-view';
import 'hbs-helpers';
import { AutoType } from './auto-type';
import { Storage } from './storage';
StartProfiler.milestone('loading modules');
const ready = (Launcher && Launcher.ready) || $;
ready(() => {
StartProfiler.milestone('document ready');
const appModel = new AppModel();
StartProfiler.milestone('creating app model');
Promise.resolve()
.then(loadConfigs)
.then(initModules)
.then(loadRemoteConfig)
.then(ensureCanRun)
.then(initStorage)
.then(initUsbListener)
.then(showApp)
.then(postInit)
.catch((e) => {
appModel.appLogger.error('Error starting app', e);
});
function ensureCanRun() {
if (Features.isFrame && !appModel.settings.allowIframes) {
return Promise.reject(
'Running in iframe is not allowed (this can be changed in the app config).'
);
}
return FeatureTester.test()
.catch((e) => {
Alerts.error({
header: Locale.appSettingsError,
body: Locale.appNotSupportedError,
pre: e,
buttons: [],
esc: false,
enter: false,
click: false
});
throw 'Feature testing failed: ' + e;
})
.then(() => {
StartProfiler.milestone('checking features');
});
}
function loadConfigs() {
return Promise.all([
AppSettingsModel.load(),
UpdateModel.load(),
RuntimeDataModel.load(),
FileInfoCollection.load()
]).then(() => {
StartProfiler.milestone('loading configs');
});
}
function initModules() {
KeyHandler.init();
PopupNotifier.init();
KdbxwebInit.init();
FocusDetector.init();
AutoType.init();
ThemeWatcher.init();
SettingsManager.init();
window.kw = ExportApi;
return PluginManager.init().then(() => {
StartProfiler.milestone('initializing modules');
});
}
function showSettingsLoadError() {
Alerts.error({
header: Locale.appSettingsError,
body: Locale.appSettingsErrorBody,
buttons: [],
esc: false,
enter: false,
click: false
});
}
function loadRemoteConfig() {
return Promise.resolve()
.then(() => {
SettingsManager.setBySettings();
const configParam = getConfigParam();
if (configParam) {
return appModel
.loadConfig(configParam)
.then(() => {
SettingsManager.setBySettings();
})
.catch((e) => {
if (!appModel.settings.cacheConfigSettings) {
showSettingsLoadError();
throw e;
}
});
}
})
.then(() => {
StartProfiler.milestone('loading remote config');
});
}
function initStorage() {
for (const prv of Object.values(Storage)) {
prv.init();
}
StartProfiler.milestone('initializing storage');
}
function initUsbListener() {
UsbListener.init();
StartProfiler.milestone('starting usb');
}
function showApp() {
return Promise.resolve().then(() => {
const skipHttpsWarning =
localStorage.skipHttpsWarning || appModel.settings.skipHttpsWarning;
const protocolIsInsecure = ['https:', 'file:', 'app:'].indexOf(location.protocol) < 0;
const hostIsInsecure = location.hostname !== 'localhost';
if (protocolIsInsecure && hostIsInsecure && !skipHttpsWarning) {
return new Promise((resolve) => {
Alerts.error({
header: Locale.appSecWarn,
icon: 'user-secret',
esc: false,
enter: false,
click: false,
body: Locale.appSecWarnBody1 + '\n\n' + Locale.appSecWarnBody2,
buttons: [{ result: '', title: Locale.appSecWarnBtn, error: true }],
complete: () => {
showView();
resolve();
}
});
});
} else {
showView();
return new Promise((resolve) => requestAnimationFrame(resolve));
}
});
}
function postInit() {
setTimeout(() => {
Updater.init();
SingleInstanceChecker.init();
AppRightsChecker.init();
IdleTracker.init();
BrowserExtensionConnector.init(appModel);
PluginManager.runAutoUpdate();
}, Timeouts.AutoUpdatePluginsAfterStart);
}
function showView() {
new AppView(appModel).render();
StartProfiler.milestone('first view rendering');
Events.emit('app-ready');
StartProfiler.milestone('app ready event');
StartProfiler.report();
}
function getConfigParam() {
const metaConfig = document.head.querySelector('meta[name=kw-config]');
if (metaConfig && metaConfig.content && metaConfig.content[0] !== '(') {
return metaConfig.content;
}
const match = location.search.match(/[?&]config=([^&]+)/i);
if (match && match[1]) {
return match[1];
}
}
});
@@ -1,10 +1,6 @@
import { Logger } from 'util/logger';
const logger = new Logger(
'auto-type-obfuscator',
undefined,
localStorage.debugAutoType ? Logger.Level.All : Logger.Level.Warn
);
const logger = new Logger('auto-type-obfuscator');
const MaxFakeOps = 30;
const MaxSteps = 1000;
+1 -5
View File
@@ -3,11 +3,7 @@ import { AutoTypeObfuscator } from 'auto-type/auto-type-obfuscator';
import { StringFormat } from 'util/formatting/string-format';
import { Logger } from 'util/logger';
const emitterLogger = new Logger(
'auto-type-emitter',
undefined,
localStorage.debugAutoType ? Logger.Level.All : Logger.Level.Info
);
const emitterLogger = new Logger('auto-type-emitter');
const AutoTypeRunner = function (ops) {
this.ops = ops;
+18 -17
View File
@@ -33,9 +33,9 @@ const AutoType = {
const entry = (e && e.entry) || null;
const sequence = (e && e.sequence) || null;
const context = (e && e.context) || null;
logger.debug('Auto type event', entry);
logger.info('Auto type event', entry);
if (this.running) {
logger.debug('Already running, skipping event');
logger.info('Already running, skipping event');
return;
}
if (entry) {
@@ -83,19 +83,19 @@ const AutoType = {
this.running = true;
const sequence = result.sequence || result.entry.getEffectiveAutoTypeSeq();
const context = result.context;
logger.debug('Start', sequence);
logger.info('Start', sequence);
const ts = logger.ts();
try {
const parser = new AutoTypeParser(sequence);
const runner = parser.parse();
logger.debug('Parsed', this.printOps(runner.ops));
logger.info('Parsed', this.printOps(runner.ops));
runner.resolve(result.entry, context, (err) => {
if (err) {
this.running = false;
logger.error('Resolve error', err);
return callback && callback(err);
}
logger.debug('Resolved', this.printOps(runner.ops));
logger.info('Resolved', this.printOps(runner.ops));
if (result.entry.autoTypeObfuscation) {
try {
runner.obfuscate();
@@ -104,7 +104,7 @@ const AutoType = {
logger.error('Obfuscate error', e);
return callback && callback(e);
}
logger.debug('Obfuscated');
logger.info('Obfuscated');
}
runner.run((err) => {
this.running = false;
@@ -112,7 +112,7 @@ const AutoType = {
logger.error('Run error', err);
return callback && callback(err);
}
logger.debug('Complete', logger.ts(ts));
logger.info('Complete', logger.ts(ts));
return callback && callback();
}, windowId);
});
@@ -153,7 +153,7 @@ const AutoType = {
},
hideWindow(callback) {
logger.debug('Hide window');
logger.info('Hide window');
if (Launcher.isAppFocused()) {
Launcher.hideApp();
setTimeout(callback, Timeouts.AutoTypeAfterHide);
@@ -163,7 +163,7 @@ const AutoType = {
},
getActiveWindowInfo(callback) {
logger.debug('Getting window info');
logger.info('Getting window info');
const helper = new AutoTypeHelper();
return helper.getActiveWindowInfo((err, windowInfo) => {
if (err) {
@@ -171,11 +171,12 @@ const AutoType = {
} else {
if (!windowInfo.url) {
// try to find a URL in the title
const urlMatcher = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\\+.~#?&\/=]*)/;
const urlMatcher =
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\\+.~#?&\/=]*)/;
const urlMatches = urlMatcher.exec(windowInfo.title);
windowInfo.url = urlMatches && urlMatches.length > 0 ? urlMatches[0] : null;
}
logger.debug('Window info', windowInfo.id, windowInfo.title, windowInfo.url);
logger.info('Window info', windowInfo.id, windowInfo.title, windowInfo.url);
}
return callback(err, windowInfo);
});
@@ -183,12 +184,12 @@ const AutoType = {
activeWindowMatches(windowInfo, callback) {
if (!windowInfo || !windowInfo.id) {
logger.debug('Skipped active window check because window id is unknown');
logger.info('Skipped active window check because window id is unknown');
return callback(true);
}
this.getActiveWindowInfo((err, activeWindowInfo) => {
if (!activeWindowInfo) {
logger.debug('Error during active window check, something is wrong', err);
logger.info('Error during active window check, something is wrong', err);
return callback(false);
}
if (activeWindowInfo.id !== windowInfo.id && !this.supportsEventsWithWindowId) {
@@ -222,19 +223,19 @@ const AutoType = {
);
const evt = { filter, windowInfo };
if (!AppModel.instance.files.hasOpenFiles()) {
logger.debug('auto-type event delayed');
logger.info('auto-type event delayed');
this.focusMainWindow();
try {
await AppModel.instance.unlockAnyFile('autoTypeUnlockMessage');
} catch {
logger.debug('auto-type event canceled');
logger.info('auto-type event canceled');
return;
}
if (this.selectEntryView) {
this.selectEntryView.show();
}
}
logger.debug('processing auto-type event');
logger.info('processing auto-type event');
this.processEventWithFilter(evt);
});
},
@@ -280,7 +281,7 @@ const AutoType = {
topMessage
});
this.selectEntryView.on('result', (result) => {
logger.debug('Entry selected', result);
logger.info('Entry selected', result);
this.selectEntryView.off('result');
this.selectEntryView.remove();
this.selectEntryView = null;
+188
View File
@@ -0,0 +1,188 @@
import 'util/kdbxweb/protected-value';
import { h, render } from 'preact';
import { App } from 'ui/app';
import { Events } from 'util/events';
import { StartProfiler } from 'util/logging/start-profiler';
import { AppRightsChecker } from 'comp/launcher/app-rights-checker';
import { ExportApi } from 'comp/app/export-api';
import { SingleInstanceChecker } from 'comp/browser/single-instance-checker';
import { FocusDetector } from 'comp/browser/focus-detector';
import { IdleTracker } from 'comp/browser/idle-tracker';
import { ThemeWatcher } from 'comp/browser/theme-watcher';
import { KeyHandler } from 'comp/browser/key-handler';
import { PopupNotifier } from 'comp/browser/popup-notifier';
import { SettingsManager } from 'comp/settings/settings-manager';
import { Alerts } from 'comp/ui/alerts';
import { AppSettings } from 'models/app-settings';
import { RuntimeData } from 'models/runtime-data';
import { Features } from 'util/features';
import { KdbxwebInit } from 'comp/kdbxweb/kdbxweb-init';
import { Locale } from 'util/locale';
import { Logger } from 'util/logger';
import { FeatureTester } from 'util/browser/feature-tester';
import { UsbListener } from 'comp/devices/usb-listener';
import { noop } from 'util/fn';
import { ConfigLoader } from 'comp/settings/config-loader';
import { WindowClass } from 'comp/browser/window-class';
import { FileManager } from 'models/file-manager';
import { Updater } from './comp/app/updater';
import { Timeouts } from 'const/timeouts';
import { PluginManager } from 'plugins/plugin-manager';
import { Workspace } from 'models/workspace';
declare global {
interface Window {
kw: typeof ExportApi;
}
}
StartProfiler.milestone('loading modules');
document.addEventListener('DOMContentLoaded', () => {
bootstrap().catch(noop);
});
export async function bootstrap(): Promise<void> {
StartProfiler.milestone('document ready');
const logger = new Logger('bootstrap');
try {
await loadConfigs();
await initModules();
await loadRemoteConfig();
await ensureCanRun();
addWindowClasses();
initUsbListener();
await showApp();
postInit();
} catch (e) {
logger.error('Error starting app', e);
}
async function ensureCanRun() {
if (Features.isFrame && !AppSettings.allowIframes) {
return Promise.reject(
'Running in iframe is not allowed (this can be changed in the app config).'
);
}
try {
await FeatureTester.test();
StartProfiler.milestone('checking features');
} catch (e) {
Alerts.error({
header: Locale.appSettingsError,
body: Locale.appNotSupportedError,
pre: String(e),
buttons: [],
esc: undefined,
enter: undefined,
click: undefined
});
logger.error('Feature test failed', e);
throw new Error(`Feature test failed`);
}
}
function loadConfigs() {
return Promise.all([AppSettings.init(), RuntimeData.init(), FileManager.init()]).then(
() => {
StartProfiler.milestone('loading configs');
}
);
}
async function initModules() {
KeyHandler.init();
PopupNotifier.init();
KdbxwebInit.init();
FocusDetector.init();
// AutoType.init(); // TODO
ThemeWatcher.init();
SettingsManager.init();
await PluginManager.init();
window.kw = ExportApi;
StartProfiler.milestone('initializing modules');
}
async function loadRemoteConfig() {
SettingsManager.setBySettings();
const loaded = await ConfigLoader.loadConfig();
if (loaded) {
SettingsManager.setBySettings();
}
StartProfiler.milestone('loading remote config');
}
function addWindowClasses() {
WindowClass.init();
StartProfiler.milestone('adding window classes');
}
function initUsbListener() {
UsbListener.init();
StartProfiler.milestone('starting usb');
}
function showApp() {
return Promise.resolve().then(() => {
const skipHttpsWarning = AppSettings.skipHttpsWarning;
const protocolIsInsecure = ['https:', 'file:', 'app:'].indexOf(location.protocol) < 0;
const hostIsInsecure = location.hostname !== 'localhost';
if (protocolIsInsecure && hostIsInsecure && !skipHttpsWarning) {
return new Promise<void>((resolve) => {
Alerts.error({
header: Locale.appSecWarn,
icon: 'user-secret',
esc: undefined,
enter: undefined,
click: undefined,
body: Locale.appSecWarnBody1 + '\n\n' + Locale.appSecWarnBody2,
buttons: [{ result: '', title: Locale.appSecWarnBtn, error: true }],
complete: () => {
resolve(showView());
}
});
});
} else {
return showView().then(
() => new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
);
}
});
}
function postInit() {
Updater.init();
SingleInstanceChecker.init();
AppRightsChecker.init().catch(noop);
IdleTracker.init();
// BrowserExtensionConnector.init(appModel); // TODO
setTimeout(() => {
PluginManager.runAutoUpdate().catch(noop);
}, Timeouts.PluginsAutoUpdateAfterStart);
}
function showView() {
Workspace.showOpen();
const root = document.querySelector('.app');
if (root) {
const app = h(App, null);
render(app, document.body, root);
}
StartProfiler.milestone('first view rendering');
Events.emit('app-ready');
StartProfiler.milestone('app ready event');
StartProfiler.report();
return Promise.resolve();
}
}
@@ -1,8 +0,0 @@
import { Collection } from 'framework/collection';
import { EntryModel } from 'models/entry-model';
class EntryCollection extends Collection {
static model = EntryModel;
}
export { EntryCollection };
@@ -1,24 +0,0 @@
import { Collection } from 'framework/collection';
import { Model } from 'framework/model';
class FileCollection extends Collection {
static model = Model;
hasOpenFiles() {
return this.some((file) => file.active);
}
hasUnsavedFiles() {
return this.some((file) => file.modified);
}
hasDirtyFiles() {
return this.some((file) => file.dirty);
}
getByName(name) {
return this.find((file) => file.name.toLowerCase() === name.toLowerCase());
}
}
export { FileCollection };
@@ -1,39 +0,0 @@
import { Collection } from 'framework/collection';
import { SettingsStore } from 'comp/settings/settings-store';
import { FileInfoModel } from 'models/file-info-model';
class FileInfoCollection extends Collection {
static model = FileInfoModel;
load() {
return SettingsStore.load('file-info').then((data) => {
if (data) {
for (const item of data) {
this.push(new FileInfoModel(item));
}
}
});
}
save() {
SettingsStore.save('file-info', this);
}
getMatch(storage, name, path) {
return this.find((fi) => {
return (
(fi.storage || '') === (storage || '') &&
(fi.name || '') === (name || '') &&
(fi.path || '') === (path || '')
);
});
}
getByName(name) {
return this.find((file) => file.name.toLowerCase() === name.toLowerCase());
}
}
const instance = new FileInfoCollection();
export { instance as FileInfoCollection };
@@ -1,8 +0,0 @@
import { Collection } from 'framework/collection';
import { GroupModel } from 'models/group-model';
class GroupCollection extends Collection {
static model = GroupModel;
}
export { GroupCollection };
@@ -1,8 +0,0 @@
import { Collection } from 'framework/collection';
import { MenuItemModel } from 'models/menu/menu-item-model';
class MenuItemCollection extends Collection {
static model = MenuItemModel;
}
export { MenuItemCollection };
@@ -1,8 +0,0 @@
import { Collection } from 'framework/collection';
import { MenuOptionModel } from 'models/menu/menu-option-model';
class MenuOptionCollection extends Collection {
static model = MenuOptionModel;
}
export { MenuOptionCollection };
@@ -1,8 +0,0 @@
import { Collection } from 'framework/collection';
import { MenuSectionModel } from 'models/menu/menu-section-model';
class MenuSectionCollection extends Collection {
static model = MenuSectionModel;
}
export { MenuSectionCollection };
@@ -1,52 +0,0 @@
import { Collection } from 'framework/collection';
import { Model } from 'framework/model';
import { Comparators } from 'util/data/comparators';
class SearchResultCollection extends Collection {
static model = Model;
comparators = {
'none': null,
'title': Comparators.stringComparator('title', true),
'-title': Comparators.stringComparator('title', false),
'website': Comparators.stringComparator('url', true),
'-website': Comparators.stringComparator('url', false),
'user': Comparators.stringComparator('user', true),
'-user': Comparators.stringComparator('user', false),
'created': Comparators.dateComparator('created', true),
'-created': Comparators.dateComparator('created', false),
'updated': Comparators.dateComparator('updated', true),
'-updated': Comparators.dateComparator('updated', false),
'-attachments': (x, y) => {
return this.attachmentSortVal(x).localeCompare(this.attachmentSortVal(y));
},
'-rank': Comparators.rankComparator().bind(this)
};
defaultComparator = 'title';
entryFilter = null;
constructor(models, options) {
super(models);
const comparatorName = (options && options.comparator) || this.defaultComparator;
this.comparator = this.comparators[comparatorName];
}
sortEntries(comparator, filter) {
this.entryFilter = filter;
this.comparator = this.comparators[comparator] || this.comparators[this.defaultComparator];
this.sort();
}
attachmentSortVal(entry) {
const att = entry.attachments;
let str = att.length ? String.fromCharCode(64 + att.length) : 'Z';
if (att[0]) {
str += att[0].title;
}
return str;
}
}
export { SearchResultCollection };
@@ -1,81 +0,0 @@
import { Launcher } from 'comp/launcher';
import { Alerts } from 'comp/ui/alerts';
import { AppSettingsModel } from 'models/app-settings-model';
import { Features } from 'util/features';
import { Locale } from 'util/locale';
const AppRightsChecker = {
AppPath: '/Applications/KeeWeb.app',
init() {
if (!Features.isDesktop || !Features.isMac) {
return;
}
if (AppSettingsModel.skipFolderRightsWarning) {
return;
}
if (!Launcher.getAppPath().startsWith(this.AppPath)) {
return;
}
this.needRunInstaller((needRun) => {
if (needRun) {
this.showAlert();
this.runInstaller();
}
});
},
needRunInstaller(callback) {
Launcher.statFile(this.AppPath, (stat) => {
const folderIsRoot = stat && stat.uid === 0;
callback(!folderIsRoot);
});
},
showAlert() {
const command = 'sudo chown -R root ' + this.AppPath;
this.alert = Alerts.alert({
icon: 'lock',
header: Locale.appRightsAlert,
body:
Locale.appRightsAlertBody1.replace('{}', this.AppPath) +
'\n' +
Locale.appRightsAlertBody2,
pre: command,
buttons: [
{ result: 'skip', title: Locale.alertDoNotAsk, error: true },
Alerts.buttons.ok
],
success: (result) => {
if (result === 'skip') {
this.dontAskAnymore();
}
this.alert = null;
}
});
},
runInstaller() {
Launcher.spawn({
cmd: this.AppPath + '/Contents/Installer/KeeWeb Installer.app/Contents/MacOS/applet',
args: ['--install'],
complete: () => {
this.needRunInstaller((needRun) => {
if (this.alert && !needRun) {
this.alert.closeWithResult('cancel');
}
});
}
});
},
dontAskAnymore() {
this.needRunInstaller((needRun) => {
if (needRun) {
AppSettingsModel.skipFolderRightsWarning = true;
}
});
}
};
export { AppRightsChecker };
@@ -1,5 +1,5 @@
const DragDropInfo = {
dragObject: null
dragObject: null as unknown
};
export { DragDropInfo };
-102
View File
@@ -1,102 +0,0 @@
import { AppSettingsModel } from 'models/app-settings-model';
import { UrlFormat } from 'util/formatting/url-format';
const ChooserAppKey = 'qp7ctun6qt5n9d6';
const DropboxChooser = function (callback) {
this.cb = callback;
this.onMessage = this.onMessage.bind(this);
};
DropboxChooser.prototype.callback = function (err, res) {
if (this.cb) {
this.cb(err, res);
}
this.cb = null;
};
DropboxChooser.prototype.choose = function () {
const windowFeatures = 'width=640,height=552,left=357,top=100,resizable=yes,location=yes';
const url = this.buildUrl();
this.popup = window.open(url, 'dropbox', windowFeatures);
if (!this.popup) {
return this.callback('Failed to open window');
}
window.addEventListener('message', this.onMessage);
this.closeInt = setInterval(this.checkClose.bind(this), 200);
};
DropboxChooser.prototype.buildUrl = function () {
return UrlFormat.makeUrl('https://www.dropbox.com/chooser', {
origin: window.location.protocol + '//' + window.location.host,
'app_key': AppSettingsModel.dropboxAppKey || ChooserAppKey,
'link_type': 'direct',
trigger: 'js',
multiselect: 'false',
extensions: '',
folderselect: 'false',
iframe: 'false',
version: 2
});
};
DropboxChooser.prototype.onMessage = function (e) {
if (e.source !== this.popup || e.origin !== 'https://www.dropbox.com') {
return;
}
const data = JSON.parse(e.data);
switch (data.method) {
case 'origin_request':
e.source.postMessage(JSON.stringify({ method: 'origin' }), 'https://www.dropbox.com');
break;
case 'files_selected':
this.popup.close();
this.success(data.params);
break;
case 'close_dialog':
this.popup.close();
break;
case 'web_session_error':
case 'web_session_unlinked':
this.callback(data.method);
break;
case 'resize':
this.popup.resize(data.params);
break;
case 'error':
this.callback(data.params);
break;
}
};
DropboxChooser.prototype.checkClose = function () {
if (this.popup.closed) {
clearInterval(this.closeInt);
window.removeEventListener('message', this.onMessage);
if (!this.result) {
this.callback('closed');
}
}
};
DropboxChooser.prototype.success = function (params) {
if (!params || !params[0] || !params[0].link || params[0].is_dir) {
return this.callback('bad result');
}
this.result = params[0];
this.readFile(this.result.link);
};
DropboxChooser.prototype.readFile = function (url) {
const xhr = new XMLHttpRequest();
xhr.addEventListener('load', () => {
this.callback(null, { name: this.result.name, data: xhr.response });
});
xhr.addEventListener('error', this.callback.bind(this, 'download error'));
xhr.addEventListener('abort', this.callback.bind(this, 'download abort'));
xhr.open('GET', url);
xhr.responseType = 'arraybuffer';
xhr.send();
};
export { DropboxChooser };
-29
View File
@@ -1,29 +0,0 @@
import { AppSettingsModel } from 'models/app-settings-model';
import { RuntimeDataModel } from 'models/runtime-data-model';
const ExportApi = {
settings: {
get(key) {
return key ? AppSettingsModel[key] : { ...AppSettingsModel };
},
set(key, value) {
AppSettingsModel[key] = value;
},
del(key) {
delete AppSettingsModel[key];
}
},
runtimeData: {
get(key) {
return key ? RuntimeDataModel[key] : { ...RuntimeDataModel };
},
set(key, value) {
RuntimeDataModel[key] = value;
},
del(key) {
delete RuntimeDataModel[key];
}
}
};
export { ExportApi };
+29
View File
@@ -0,0 +1,29 @@
import { AppSettingsFieldName, AppSettings } from 'models/app-settings';
import { RuntimeData } from 'models/runtime-data';
const ExportApi = {
settings: {
get(key?: AppSettingsFieldName): unknown {
return key ? AppSettings[key] : { ...AppSettings };
},
set(key: AppSettingsFieldName, value: unknown): boolean {
return AppSettings.set(key, value);
},
del(key: AppSettingsFieldName): void {
AppSettings.delete(key);
}
},
runtimeData: {
get(key?: string): unknown {
return key ? RuntimeData.get(key) : { ...RuntimeData };
},
set(key: string, value: unknown): boolean {
return RuntimeData.set(key, value);
},
del(key: string): void {
RuntimeData.delete(key);
}
}
};
export { ExportApi };
+689
View File
@@ -0,0 +1,689 @@
import * as kdbxweb from 'kdbxweb';
import { Logger } from 'util/logger';
import { Storage } from 'storage';
import { Locale } from 'util/locale';
import { IdGenerator } from 'util/generators/id-generator';
import { OpenParams } from 'models/ui/open-state';
import { FileManager } from 'models/file-manager';
import { File } from 'models/file';
import { FileInfo } from 'models/file-info';
import {
StorageFileData,
StorageFileNotFoundError,
StorageFileOptions,
StorageFileStat,
StorageRevConflictError,
StorageSaveResult
} from 'storage/types';
import { AppSettings } from 'models/app-settings';
import { errorToString, noop } from 'util/fn';
import { StorageBase } from 'storage/storage-base';
import { UrlFormat } from 'util/formatting/url-format';
import { DateFormat } from 'util/formatting/date-format';
const MaxLoadLoops = 3;
interface FileSyncOptions {
storage?: string;
path?: string;
opts: StorageFileOptions;
}
interface SyncContext {
logger: Logger;
file: File;
fileInfo: FileInfo;
storage: StorageBase;
path: string;
stat?: StorageFileStat;
opts?: StorageFileOptions;
retryCount: number;
}
class FileController {
async open(params: OpenParams): Promise<File> {
const logger = new Logger('open', params.name);
logger.info('File open request');
if (!params.name) {
throw new Error('Empty name');
}
const fileInfo = params.id
? FileManager.getFileInfoById(params.id)
: FileManager.getFileInfo(params.name, params.storage, params.path);
if (!params.opts && fileInfo && fileInfo.opts) {
params.opts = fileInfo.opts;
}
if (fileInfo && fileInfo.modified) {
logger.info('Open file from cache because it is modified');
const file = await this.openFileFromCache(params, fileInfo, logger);
logger.info('Sync just opened modified file');
setTimeout(() => {
this.syncFile(file).catch(noop);
}, 0);
return file;
} else if (params.fileData) {
logger.info('Open file from supplied content');
if (params.storage === 'file' && params.path) {
const stat = await Storage.file.stat(params.path);
params.rev = stat.rev;
return this.openFileWithData(params, fileInfo, params.fileData, false);
} else {
return this.openFileWithData(params, fileInfo, params.fileData, true);
}
} else if (!params.storage && fileInfo) {
logger.info('Open file from cache as main storage');
return this.openFileFromCache(params, fileInfo, logger);
} else if (
fileInfo?.openDate &&
fileInfo.rev === params.rev &&
fileInfo.storage !== 'file' &&
!AppSettings.disableOfflineStorage
) {
logger.info('Open file from cache because it is latest');
try {
return this.openFileFromCache(params, fileInfo, logger);
} catch (err) {
if (err instanceof kdbxweb.KdbxError) {
// TODO: || err.ykError
throw err;
}
logger.info('Error loading file from cache, trying to open from storage', err);
return this.openFileFromStorage(params, fileInfo, logger, true);
}
} else if (
!fileInfo ||
!fileInfo.openDate ||
params.storage === 'file' ||
AppSettings.disableOfflineStorage
) {
return this.openFileFromStorage(params, fileInfo, logger, false);
} else {
logger.info('Open file from cache, will sync after load', params.storage);
let file: File;
try {
file = await this.openFileFromCache(params, fileInfo, logger);
} catch (err) {
if (err instanceof kdbxweb.KdbxError) {
// TODO: || err.ykError
throw err;
}
logger.info('Error loading file from cache, trying to open from storage', err);
return this.openFileFromStorage(params, fileInfo, logger, true);
}
logger.info('Sync just opened file');
setTimeout(() => {
this.syncFile(file).catch(noop);
}, 0);
return file;
}
}
private async openFileFromCache(
params: OpenParams,
fileInfo: FileInfo,
logger: Logger
): Promise<File> {
let data: ArrayBuffer;
try {
const res = await Storage.cache.load(fileInfo.id);
if (!res?.data) {
throw new Error(Locale.openFileNoCacheError);
}
data = res.data;
logger.info('Loaded from cache');
} catch (err) {
logger.info('Loaded error from cache', err);
throw err;
}
return this.openFileWithData(params, fileInfo, data, false);
}
private async openFileFromStorage(
params: OpenParams,
fileInfo: FileInfo | undefined,
logger: Logger,
noCache: boolean
): Promise<File> {
if (!params.storage) {
throw new Error('Storage open error: empty storage');
}
if (!params.path) {
throw new Error('Storage open error: empty path');
}
logger.info('Open file from storage', params.storage);
const storage = Storage.get(params.storage ?? '');
if (!storage) {
logger.error('Storage not found', storage);
throw new Error(`Storage not found: ${storage ?? ''}`);
}
const cacheRev = (fileInfo && fileInfo.rev) || null;
if (cacheRev && storage.stat) {
logger.info('Stat file');
let stat: StorageFileStat;
try {
stat = await storage.stat(params.path, params.opts);
} catch (err) {
if (
!noCache &&
fileInfo &&
storage.name !== 'file' &&
!AppSettings.disableOfflineStorage
) {
logger.info('Open file from cache because of stat error', err);
return this.openFileFromCache(params, fileInfo, logger);
} else {
logger.info('Stat error', err);
throw err;
}
}
if (
!noCache &&
fileInfo &&
storage.name !== 'file' &&
stat.rev === cacheRev &&
!AppSettings.disableOfflineStorage
) {
logger.info('Open file from cache because it is latest');
return this.openFileFromCache(params, fileInfo, logger);
}
logger.info(`Open file from storage (${stat.rev ?? '(empty)'}, local ${cacheRev})`);
}
logger.info('Load from storage');
let res: StorageFileData;
try {
res = await storage.load(params.path, params.opts);
} catch (err) {
if (fileInfo && fileInfo.openDate && !AppSettings.disableOfflineStorage) {
logger.info('Open file from cache because of storage load error', err);
return this.openFileFromCache(params, fileInfo, logger);
} else {
logger.info('Storage load error', err);
throw err;
}
}
logger.info('Open file from content loaded from storage');
params.fileData = res.data;
params.rev = res.rev;
const needSaveToCache = storage.name !== 'file';
return this.openFileWithData(params, fileInfo, res.data, needSaveToCache);
}
private async openFileWithData(
params: OpenParams,
fileInfo: FileInfo | undefined,
data: ArrayBuffer,
updateCacheOnSuccess: boolean
): Promise<File> {
if (!params.name) {
throw new Error('Open file without name');
}
if (!params.password) {
throw new Error('Open file without password');
}
const logger = new Logger('open', params.name);
let needLoadKeyFile = false;
if (!params.keyFileData && fileInfo && fileInfo.keyFileName) {
params.keyFileName = fileInfo.keyFileName;
if (AppSettings.rememberKeyFiles === 'data' && fileInfo.keyFileHash) {
params.keyFileData = File.createKeyFileWithHash(fileInfo.keyFileHash);
} else if (AppSettings.rememberKeyFiles === 'path' && fileInfo.keyFilePath) {
params.keyFilePath = fileInfo.keyFilePath;
if (Storage.file.enabled) {
needLoadKeyFile = true;
}
}
} else if (params.keyFilePath && !params.keyFileData && !fileInfo) {
needLoadKeyFile = true;
}
if (needLoadKeyFile && params.keyFilePath) {
let data: ArrayBuffer;
try {
const res = await Storage.file.load(params.keyFilePath);
data = res.data;
} catch (err) {
logger.info('Storage load error', err);
throw err;
}
params.keyFileData = data;
}
const id = fileInfo ? fileInfo.id : IdGenerator.uuid();
if (FileManager.getFileById(id)) {
throw new Error('Duplicate file id');
}
const file = await File.open(id, params.name, params.password, data, params.keyFileData);
file.batchSet(() => {
file.storage = params.storage;
file.path = params.path;
file.keyFileName = params.keyFileName;
file.keyFilePath = params.keyFilePath;
file.backup = fileInfo?.backup;
file.chalResp = params.chalResp;
file.syncDate = fileInfo?.syncDate;
// if (params.encryptedPassword) { // TODO: touch id
// file.encryptedPassword = fileInfo?.encryptedPassword;
// file.encryptedPasswordDate = fileInfo?.encryptedPasswordDate || new Date();
// }
});
if (fileInfo?.modified) {
if (fileInfo.editState) {
logger.info('Loaded local edit state');
file.setLocalEditState(fileInfo.editState);
}
logger.info('Mark file as modified');
file.modified = true;
}
if (updateCacheOnSuccess && !AppSettings.disableOfflineStorage && params.fileData) {
logger.info('Save loaded file to cache');
await Storage.cache.save(file.id, params.fileData);
}
const rev = params.rev || (fileInfo && fileInfo.rev);
// this.setFileOpts(file, params.opts); // TODO: file opts
logger.info('Add last open file', file.id, file.name, file.storage, file.path, rev);
FileManager.addLastOpenFileInfo(file, rev);
FileManager.addFile(file);
// this.fileOpened(file, data, params); // TODO: fileOpened
return file;
}
async syncFile(file: File, options?: FileSyncOptions): Promise<void> {
if (file.demo) {
return;
}
const logger = new Logger('sync', file.name);
const storageName = options?.storage || file.storage;
const storage = storageName ? Storage.get(storageName) : undefined;
const opts = options?.opts || file.opts;
let path = options?.path || file.path;
if (storage?.getPathForName && (!path || storage.name !== file.storage)) {
path = storage.getPathForName(file.name);
}
const optionsForLogging = { ...options };
if (optionsForLogging.opts && optionsForLogging.opts.password) {
optionsForLogging.opts = { ...optionsForLogging.opts };
optionsForLogging.opts.password = '***';
}
logger.info('Sync started', storage, path, optionsForLogging);
const fileInfo = this.getOrCreateFileInfo(file, logger);
let syncError: unknown;
try {
if (file.syncing) {
throw new Error('Sync in progress');
}
file.setSyncProgress();
if (!storage) {
if (!file.modified && fileInfo.id === file.id) {
logger.info('Local, not modified');
} else {
logger.info('Local, save to cache');
const data = await file.getData();
try {
await Storage.cache.save(fileInfo.id, data);
logger.info('Saved to cache');
} catch (err) {
logger.info('Cache save error', err);
throw err;
}
// this.scheduleBackupFile(file, data); // TODO: backups
}
} else {
if (!path) {
throw new Error('Cannot sync file to storage but without path');
}
const ctx: SyncContext = {
logger,
file,
fileInfo,
storage,
path,
opts,
retryCount: 0
};
logger.info('Stat file');
let stat: StorageFileStat | undefined;
try {
stat = await storage.stat(path, opts);
ctx.stat = stat;
} catch (err) {
if (err instanceof StorageFileNotFoundError) {
logger.info('File does not exist in storage, creating');
await this.saveToCacheAndStorage(ctx);
} else if (file.dirty) {
if (AppSettings.disableOfflineStorage) {
logger.info('Stat error, dirty, cache is disabled', err);
throw err;
}
logger.info('Stat error, dirty, save to cache');
const data = await file.getData();
try {
await Storage.cache.save(fileInfo.id, data);
file.dirty = false;
logger.info('Saved to cache, exit with error', err);
} catch (e) {
logger.error('Error saving to cache', e);
}
throw err;
} else {
logger.info('Stat error, not dirty', err);
throw err;
}
}
if (stat) {
if (stat.rev === fileInfo.rev) {
if (file.modified) {
logger.info('Stat found same version, modified, saving');
await this.saveToCacheAndStorage(ctx);
} else {
logger.info('Stat found same version, not modified');
}
} else {
logger.info('Found new version, loading from storage');
await this.loadFromStorageAndMerge(ctx);
}
}
}
logger.info('Sync finished OK');
} catch (e) {
syncError = e;
logger.error('Sync finished with error', syncError);
}
file.setSyncComplete(path, storageName, syncError ? errorToString(syncError) : undefined);
fileInfo.batchSet(() => {
fileInfo.name = file.name;
fileInfo.storage = storageName;
fileInfo.path = path;
// fileInfo.opts = this.getStoreOpts(file); // TODO: storage opts
fileInfo.modified = file.dirty ? fileInfo.modified : file.modified;
fileInfo.editState = file.dirty ? fileInfo.editState : file.getLocalEditState();
fileInfo.syncDate = file.syncDate;
fileInfo.chalResp = file.chalResp;
if (AppSettings.rememberKeyFiles === 'data') {
fileInfo.keyFileName = file.keyFileName;
fileInfo.keyFileHash = file.getKeyFileHash();
}
});
if (!FileManager.getFileById(fileInfo.id)) {
FileManager.addFileInfo(fileInfo, true);
}
if (syncError) {
throw syncError;
}
}
private getOrCreateFileInfo(file: File, logger: Logger): FileInfo {
let fileInfo = FileManager.getFileInfoById(file.id);
if (!fileInfo) {
logger.info('Create new file info');
const dt = new Date();
fileInfo = new FileInfo({
id: file.id,
name: file.name,
storage: file.storage,
path: file.path,
// opts: this.getStoreOpts(file), // TODO: store opts
modified: file.modified,
syncDate: dt,
openDate: dt,
backup: file.backup
});
}
return fileInfo;
}
private async saveToStorage(ctx: SyncContext, data: ArrayBuffer) {
ctx.logger.info('Save data to storage');
const storageRev = ctx.fileInfo.storage === ctx.storage.name ? ctx.fileInfo.rev : undefined;
let saveResult: StorageSaveResult;
try {
saveResult = await ctx.storage.save(ctx.path, data, ctx.opts, storageRev);
} catch (err) {
if (err instanceof StorageRevConflictError) {
ctx.logger.info('Save rev conflict, reloading from storage');
await this.loadFromStorageAndMerge(ctx);
return;
} else {
ctx.logger.info('Error saving data to storage');
throw err;
}
}
if (saveResult.rev) {
ctx.logger.info('Update rev in file info');
ctx.fileInfo.rev = saveResult.rev;
}
if (saveResult.path) {
ctx.logger.info('Update path in file info', saveResult.path);
ctx.file.path = saveResult.path;
ctx.fileInfo.path = saveResult.path;
ctx.path = saveResult.path;
}
ctx.file.syncDate = new Date();
ctx.logger.info('Save to storage complete, updated sync date');
// this.scheduleBackupFile(file, data); // TODO: backups
}
private async saveToCacheAndStorage(ctx: SyncContext) {
ctx.logger.info('Getting file data for saving');
const data = await ctx.file.getData();
if (ctx.storage.name === 'file') {
ctx.logger.info('Saving to file storage');
} else if (!ctx.file.dirty) {
ctx.logger.info('Saving to storage, skip cache because not dirty');
} else if (AppSettings.disableOfflineStorage) {
ctx.logger.info('Saving to storage, cache is disabled');
} else {
ctx.logger.info('Saving to cache');
await Storage.cache.save(ctx.file.id, data);
ctx.file.dirty = false;
ctx.logger.info('Saved to cache, saving to storage');
}
await this.saveToStorage(ctx, data);
}
private async loadFromStorageAndMerge(ctx: SyncContext) {
if (++ctx.retryCount === MaxLoadLoops) {
throw new Error('Too many load attempts');
}
ctx.logger.info('Load from storage, attempt', ctx.retryCount);
const storageLoadRes = await ctx.storage.load(ctx.path, ctx.opts);
ctx.logger.info('Load from storage', storageLoadRes.rev);
try {
await ctx.file.mergeOrUpdate(storageLoadRes.data);
ctx.logger.info('Merge complete OK');
} catch (err) {
ctx.logger.info('Merge error', err);
// this.refresh(); // TODO: trigger refresh
if (
err instanceof kdbxweb.KdbxError &&
err.code === kdbxweb.Consts.ErrorCodes.InvalidKey
) {
ctx.logger.info('Remote key changed, request to enter new key');
// Events.emit('remote-key-changed', { file }); // TODO: show the key change screen
}
throw err;
}
if (ctx.stat?.rev) {
ctx.logger.info('Update rev in file info');
ctx.fileInfo.rev = ctx.stat.rev;
}
ctx.file.syncDate = new Date();
if (ctx.file.modified) {
ctx.logger.info('Updated sync date, saving modified file');
await this.saveToCacheAndStorage(ctx);
} else if (ctx.file.dirty) {
if (AppSettings.disableOfflineStorage) {
ctx.logger.info('File is dirty and cache is disabled');
throw new Error('File is dirty and cache is disabled');
}
ctx.logger.info('Saving not modified dirty file to cache');
await Storage.cache.save(ctx.fileInfo.id, storageLoadRes.data);
ctx.file.dirty = false;
ctx.logger.info('Complete, remove dirty flag');
} else {
ctx.logger.info('Complete, no changes');
}
}
private async backupFile(file: File, data: ArrayBuffer): Promise<void> {
if (file.backupInProgress) {
return;
}
file.backupInProgress = true;
try {
const opts = file.opts;
const backup = file.backup;
const logger = new Logger('backup', file.name);
if (!backup || !backup.storage || !backup.path) {
throw new Error('Invalid backup settings');
}
const storage = Storage.get(backup.storage);
if (!storage) {
throw new Error(`Invalid backup storage: ${backup.storage}`);
}
let path = backup.path.replace('{date}', DateFormat.dtStrFs(new Date()));
logger.info('Backup file to', backup.storage, path);
let folderPath = UrlFormat.fileToDir(path);
if (storage.getPathForName) {
folderPath = storage.getPathForName(folderPath).replace('.kdbx', '');
}
try {
await storage.stat(folderPath, opts);
logger.info('Backup folder exists, saving');
} catch (err) {
if (err instanceof StorageFileNotFoundError) {
logger.info('Backup folder does not exist');
if (!storage.mkdir) {
throw new Error('Mkdir not supported by ' + backup.storage);
}
try {
await storage.mkdir(folderPath);
logger.info('Backup folder created');
} catch (err) {
logger.error('Error creating backup folder', err);
throw new Error('Error creating backup folder');
}
} else {
logger.error('Stat folder error', err);
throw new Error('Cannot stat backup folder');
}
}
if (storage.getPathForName) {
path = storage.getPathForName(path);
}
try {
await storage.save(path, data, opts);
} catch (err) {
logger.error('Backup error', err);
throw err;
}
logger.info('Backup complete');
backup.lastTime = Date.now();
file.backup = backup;
const fileInfo = FileManager.getFileInfoById(file.id);
if (fileInfo) {
fileInfo.backup = backup;
}
} finally {
file.backupInProgress = false;
}
}
private scheduleBackupFile(file: File, data: ArrayBuffer): void {
const backup = file.backup;
if (!backup || !backup.enabled || file.backupInProgress) {
return;
}
const logger = new Logger('backup', file.name);
let needBackup = false;
if (!backup.lastTime) {
needBackup = true;
logger.info('No last backup time, backup now');
} else {
const dt = new Date(backup.lastTime);
switch (backup.schedule) {
case '0':
break;
case '1d':
dt.setDate(dt.getDate() + 1);
break;
case '1w':
dt.setDate(dt.getDate() + 7);
break;
case '1m':
dt.setMonth(dt.getMonth() + 1);
break;
default:
return;
}
if (dt.getTime() <= Date.now()) {
needBackup = true;
}
const lastTimeStr = DateFormat.dtStrFs(backup.lastTime);
const dtStr = DateFormat.dtStrFs(dt);
logger.info(
`Last backup time: ${lastTimeStr}, schedule: ${backup.schedule}, ` +
`next time: ${dtStr}, ${needBackup ? 'backup now' : 'skip backup'}`
);
}
const fileInfo = FileManager.getFileInfoById(file.id);
if (fileInfo) {
fileInfo.backup = backup;
}
if (needBackup) {
this.backupFile(file, data).catch(noop);
}
}
deleteAllCachedFiles(): void {
for (const fileInfo of FileManager.fileInfos) {
if (fileInfo.storage && !fileInfo.modified) {
Storage.cache.remove(fileInfo.id).catch(noop);
}
}
}
deleteAllStoredTokens(): void {
for (const storage of Object.values(Storage.getAll())) {
storage.deleteStoredToken();
}
}
}
const instance = new FileController();
export { instance as FileController };
-18
View File
@@ -1,18 +0,0 @@
import { Logger } from 'util/logger';
const logger = new Logger(
'focus-manager',
undefined,
localStorage.debugFocusManager ? Logger.Level.Debug : Logger.Level.Info
);
const FocusManager = {
modal: null,
setModal(modal) {
this.modal = modal;
logger.debug('Set modal', modal);
}
};
export { FocusManager };
+26
View File
@@ -0,0 +1,26 @@
import { Logger } from 'util/logger';
const logger = new Logger('focus-manager');
class FocusManager {
private readonly _modalStack: string[] = [];
get modal(): string | undefined {
return this._modalStack[this._modalStack.length - 1];
}
pushModal(value: string): void {
this._modalStack.push(value);
logger.debug('Push modal', value);
}
popModal(): string | undefined {
const lastModal = this._modalStack.pop();
logger.debug('Pop modal', lastModal, '=>', this._modalStack[this._modalStack.length - 1]);
return lastModal;
}
}
const instance = new FocusManager();
export { instance as FocusManager };
@@ -1,11 +1,18 @@
import { AppSettingsModel } from 'models/app-settings-model';
import { AppSettings } from 'models/app-settings';
import { Locale } from 'util/locale';
import {
PasswordGeneratorAppSetting,
PasswordGeneratorOptions,
PasswordGeneratorPreset,
PasswordGeneratorCustomPreset
} from 'util/generators/password-generator';
const GeneratorPresets = {
get defaultPreset() {
export const GeneratorPresets = {
get defaultPreset(): PasswordGeneratorPreset {
return {
name: 'Default',
title: Locale.genPresetDefault,
builtIn: true,
length: 16,
upper: true,
lower: true,
@@ -13,7 +20,7 @@ const GeneratorPresets = {
};
},
get browserExtensionPreset() {
get browserExtensionPreset(): PasswordGeneratorOptions {
return {
name: 'BrowserExtension',
length: 20,
@@ -25,12 +32,13 @@ const GeneratorPresets = {
};
},
get builtIn() {
get builtIn(): PasswordGeneratorPreset[] {
return [
this.defaultPreset,
{
name: 'Pronounceable',
title: Locale.genPresetPronounceable,
builtIn: true,
length: 10,
lower: true,
upper: true
@@ -38,6 +46,7 @@ const GeneratorPresets = {
{
name: 'Med',
title: Locale.genPresetMed,
builtIn: true,
length: 16,
upper: true,
lower: true,
@@ -49,15 +58,17 @@ const GeneratorPresets = {
{
name: 'Long',
title: Locale.genPresetLong,
builtIn: true,
length: 32,
upper: true,
lower: true,
digits: true
},
{ name: 'Pin4', title: Locale.genPresetPin4, length: 4, digits: true },
{ name: 'Pin4', title: Locale.genPresetPin4, builtIn: true, length: 4, digits: true },
{
name: 'Mac',
title: Locale.genPresetMac,
builtIn: true,
length: 17,
include: '0123456789ABCDEF',
pattern: 'XX-'
@@ -65,24 +76,23 @@ const GeneratorPresets = {
{
name: 'Hash128',
title: Locale.genPresetHash128,
builtIn: true,
length: 32,
include: '0123456789abcdef'
},
{
name: 'Hash256',
title: Locale.genPresetHash256,
builtIn: true,
length: 64,
include: '0123456789abcdef'
}
];
},
get all() {
get all(): PasswordGeneratorPreset[] {
let presets = this.builtIn;
presets.forEach((preset) => {
preset.builtIn = true;
});
const setting = AppSettingsModel.generatorPresets;
const setting = AppSettings.generatorPresets;
if (setting) {
if (setting.user) {
presets = presets.concat(setting.user.map((item) => ({ ...item })));
@@ -104,7 +114,7 @@ const GeneratorPresets = {
return presets;
},
get enabled() {
get enabled(): PasswordGeneratorPreset[] {
const allPresets = this.all.filter((preset) => !preset.disabled);
if (!allPresets.length) {
allPresets.push(this.defaultPreset);
@@ -112,15 +122,15 @@ const GeneratorPresets = {
return allPresets;
},
getOrCreateSetting() {
let setting = AppSettingsModel.generatorPresets;
getOrCreateSetting(): PasswordGeneratorAppSetting {
let setting = AppSettings.generatorPresets;
if (!setting) {
setting = { user: [] };
setting = { user: [], disabled: {} };
}
return setting;
},
add(preset) {
add(preset: PasswordGeneratorCustomPreset): void {
const setting = this.getOrCreateSetting();
if (preset.name && !setting.user.filter((p) => p.name === preset.name).length) {
setting.user.push(preset);
@@ -128,13 +138,13 @@ const GeneratorPresets = {
}
},
remove(name) {
remove(name: string): void {
const setting = this.getOrCreateSetting();
setting.user = setting.user.filter((p) => p.name !== name);
this.save(setting);
},
setPreset(name, props) {
setPreset(name: string, props: Partial<PasswordGeneratorCustomPreset>): void {
const setting = this.getOrCreateSetting();
const preset = setting.user.filter((p) => p.name === name)[0];
if (preset) {
@@ -143,22 +153,20 @@ const GeneratorPresets = {
}
},
setDisabled(name, disabled) {
setDisabled(name: string, disabled: boolean): void {
const setting = this.getOrCreateSetting();
if (disabled) {
if (!setting.disabled) {
setting.disabled = {};
}
setting.disabled[name] = true;
} else {
if (setting.disabled) {
delete setting.disabled[name];
}
} else if (setting.disabled) {
delete setting.disabled[name];
}
this.save(setting);
},
setDefault(name) {
setDefault(name: string | null): void {
const setting = this.getOrCreateSetting();
if (name) {
setting.default = name;
@@ -168,10 +176,7 @@ const GeneratorPresets = {
this.save(setting);
},
save(setting) {
AppSettingsModel.set({ generatorPresets: undefined }, { silent: true });
AppSettingsModel.generatorPresets = setting;
save(setting: PasswordGeneratorAppSetting): void {
AppSettings.generatorPresets = { ...setting };
}
};
export { GeneratorPresets };
@@ -0,0 +1,63 @@
import { FileOpener } from 'util/browser/file-opener';
import { IconSelectState } from 'models/ui/icon-select-state';
import { Logger } from 'util/logger';
import { Events } from 'util/events';
import { IconUrlFormat } from 'util/formatting/icon-url-format';
const logger = new Logger('icon-select');
class IconSelectController {
openFile() {
FileOpener.open((file) => {
FileOpener.readBinary(file)
.then((fileData) => {
IconSelectState.selectedIconDataUrl =
IconUrlFormat.toDataUrl(fileData) ?? undefined;
})
.catch((ex) => logger.error('Error reading file', ex));
}, 'image/*');
}
downloadFavicon(websiteUrl: string): void {
if (IconSelectState.downloading) {
return;
}
IconSelectState.setDownloading();
const iconUrl = this.getIconUrl(websiteUrl);
const img = document.createElement('img');
img.crossOrigin = 'Anonymous';
img.src = iconUrl;
img.onload = () => {
IconSelectState.downloading = false;
logger.debug('Favicon downloaded', iconUrl);
const data = this.getIconData(img);
logger.debug(`Favicon data extracted: ${data.length} chars`);
Events.emit('custom-icon-downloaded', data);
};
img.onerror = (e) => {
logger.error('Favicon download error', iconUrl, e);
IconSelectState.setDownloadError();
};
}
private getIconUrl(websiteUrl: string): string {
const domain = websiteUrl.replace(/^.*:\/+/, '').replace(/\/.*/, '');
return 'https://services.keeweb.info/favicon/' + domain;
}
private getIconData(img: HTMLImageElement): string {
const size = Math.min(img.width, 32);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = size;
canvas.height = size;
ctx?.drawImage(img, 0, 0, size, size);
return canvas.toDataURL().replace(/^.*,/, '');
}
}
const instance = new IconSelectController();
export { instance as IconSelectController };
+314
View File
@@ -0,0 +1,314 @@
import * as kdbxweb from 'kdbxweb';
import { FileManager } from 'models/file-manager';
import { Workspace } from 'models/workspace';
import { AppSettings } from 'models/app-settings';
import { FileOpener } from 'util/browser/file-opener';
import { Alerts } from 'comp/ui/alerts';
import { Locale } from 'util/locale';
import { errorToString } from 'util/fn';
import { DropboxChooser } from 'storage/dropbox-chooser';
import { Logger } from 'util/logger';
import { FileController } from 'comp/app/file-controller';
import { OpenState } from 'models/ui/open-state';
import { StorageBase } from 'storage/storage-base';
import { BrowserAuthStartedError, OAuthRejectedError, StorageListItem } from 'storage/types';
import { FunctionComponent, h } from 'preact';
import { OpenStorageFileList } from 'ui/open/open-storage-file-list';
import { UrlFormat } from 'util/formatting/url-format';
const logger = new Logger('open');
class OpenController {
open(): void {
if (OpenState.busy) {
return;
}
if (OpenState.id && FileManager.getFileById(OpenState.id)) {
Workspace.showList();
return;
}
OpenState.beginOpen();
FileController.open(OpenState)
.then(() => {
Workspace.showList();
})
.catch((err) => {
logger.error('Open error', err);
const invalidKey =
err instanceof kdbxweb.KdbxError &&
err.code === kdbxweb.Consts.ErrorCodes.InvalidKey;
OpenState.setOpenError(invalidKey);
if (!invalidKey) {
// } else if (err.userCanceled) { // TODO: yubikey cancellation
// if (err.notFound) { // TODO: handle file not found
// err = Locale.openErrorFileNotFound;
// }
const alertBody = Locale.openErrorDescription;
// if (err.maybeTouchIdChanged) { // TODO: handle touch ID
// alertBody += '\n' + Locale.openErrorDescriptionMaybeTouchIdChanged;
// }
Alerts.error({
header: Locale.openError,
body: alertBody,
pre: errorToString(err)
});
}
});
}
selectKeyFileFromDropbox(): void {
if (OpenState.busy || !AppSettings.canOpen) {
return;
}
const dropboxChooser = new DropboxChooser((err, res) => {
if (!err && res) {
OpenState.setKeyFile(res.name, res.data);
}
});
dropboxChooser.choose();
}
chooseFile(): void {
if (OpenState.busy || !AppSettings.canOpen) {
return;
}
FileOpener.open((file) => {
this.readFile(file).catch((e) => logger.error('Error selecting file', e));
});
}
async readFile(file: File): Promise<void> {
const fileData = await FileOpener.readBinary(file);
const format = OpenController.getOpenFileFormat(file, fileData);
switch (format) {
case 'kdbx': {
const name = file.name.replace(/\.kdbx$/i, '');
OpenState.setFile(
name,
fileData,
file.path || undefined,
file.path ? 'file' : undefined
);
break;
}
case 'xml': {
const name = file.name.replace(/\.\w+$/i, '');
const xml = kdbxweb.ByteUtils.bytesToString(fileData);
try {
await Workspace.importFileFromXml(name, xml);
} catch (e) {
logger.error('Error importing XML', e);
Alerts.error({
header: Locale.openXml,
body: Locale.openError,
pre: errorToString(e)
});
}
break;
}
case 'csv':
// TODO: load CSV
break;
case 'kdb':
Alerts.error({
header: Locale.openWrongFile,
body: Locale.openKdbFileBody
});
break;
default:
Alerts.error({
header: Locale.openWrongFile,
body: Locale.openWrongFileBody
});
break;
}
}
chooseKeyFile(): void {
if (OpenState.busy || !AppSettings.canOpen) {
return;
}
FileOpener.open((file) => {
this.readKeyFile(file).catch((e) => logger.error('Error selecting keyfile', e));
});
}
async readKeyFile(file: File): Promise<void> {
const keyFileData = await FileOpener.readBinary(file);
const path = AppSettings.rememberKeyFiles === 'path' && file.path ? file.path : undefined;
OpenState.setKeyFile(file.name, keyFileData, path);
}
readFileAndKeyFile(file: File, keyFile?: File): void {
this.readFile(file)
.then(() => {
if (keyFile) {
return this.readKeyFile(keyFile);
}
})
.catch((e) => {
logger.error('Error reading file and keyfile', e);
});
}
readDroppedFiles(files: File[]): void {
if (!files.length) {
return;
}
const dataFile = files.find((file) => /\.kdbx$/i.test(file.name));
const keyFile = files.find((file) => /\.keyx?$/i.test(file.name));
if (dataFile) {
this.readFileAndKeyFile(dataFile, keyFile);
return;
}
this.readFile(files[0]).catch((e) => logger.error('Error reading dropped files', e));
}
listStorage(storage: StorageBase, dir?: string, prevDir?: string): void {
if (OpenState.busy || !storage.list) {
return;
}
OpenState.setStorageProgress(storage.name);
storage
.list(dir)
.then((files) => {
OpenState.resetStorageProgress();
if (!files.length) {
Alerts.error({
header: Locale.openNothingFound,
body: Locale.openNothingFoundBody
});
return;
}
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
files.sort((x, y) => {
if (x.dir !== y.dir) {
return (y.dir ? 1 : 0) - (x.dir ? 1 : 0);
}
return collator.compare(x.path, y.path);
});
if (dir) {
files.unshift({
path: prevDir ?? '',
name: '..',
dir: true
});
}
this.showStorageFileListAlert(storage, files, dir);
})
.catch((e) => {
OpenState.resetStorageProgress();
if (e instanceof BrowserAuthStartedError || e instanceof OAuthRejectedError) {
return;
}
if (!Alerts.alertDisplayed) {
Alerts.error({
header: Locale.openError,
body: Locale.openListErrorBody,
pre: errorToString(e)
});
}
});
}
private showStorageFileListAlert(
storage: StorageBase,
files: StorageListItem[],
currentDir?: string
) {
const listView: FunctionComponent = () => {
const fileSelected = (file: StorageListItem) => {
if (file.dir) {
this.listStorage(storage, file.path, currentDir);
} else {
alert.closeWithResult('');
OpenState.setStorageFile(storage.name, {
path: file.path,
name: UrlFormat.getDataFileName(file.name),
rev: file.rev
});
}
};
return h(OpenStorageFileList, {
files,
fileSelected
});
};
const alert = Alerts.alert({
header: Locale.openSelectFile,
body: Locale.openSelectFileBody,
icon: storage.icon || 'file-alt',
buttons: [{ result: '', title: Locale.alertCancel }],
esc: '',
click: '',
view: listView
});
}
private static getOpenFileFormat(
file: File,
fileData: ArrayBuffer
): 'kdbx' | 'kdb' | 'xml' | 'csv' | undefined {
if (fileData.byteLength < 8) {
return undefined;
}
const fileSig = new Uint32Array(fileData, 0, 2);
if (fileSig[0] === kdbxweb.Consts.Signatures.FileMagic) {
if (fileSig[1] === kdbxweb.Consts.Signatures.Sig2Kdb) {
return 'kdb';
} else if (fileSig[1] === kdbxweb.Consts.Signatures.Sig2Kdbx) {
return 'kdbx';
} else {
return undefined;
}
} else if (AppSettings.canImportXml) {
try {
const str = kdbxweb.ByteUtils.bytesToString(fileSig).trim();
if (str.startsWith('<?xml')) {
return 'xml';
}
} catch (e) {}
return undefined;
} else if (AppSettings.canImportCsv && /\.csv$/i.test(file.name)) {
return 'csv';
} else {
return undefined;
}
}
private static async showLocalFileAlert(): Promise<void> {
if (AppSettings.skipOpenLocalWarn) {
return;
}
const res = await Alerts.alert({
header: Locale.openLocalFile,
body: Locale.openLocalFileBody,
icon: 'file-alt',
buttons: [
{ result: 'skip', title: Locale.openLocalFileDontShow, error: true },
{ result: 'ok', title: Locale.alertOk }
],
click: '',
esc: '',
enter: ''
}).wait();
if (res === 'skip') {
AppSettings.skipOpenLocalWarn = true;
}
}
}
const instance = new OpenController();
export { instance as OpenController };
@@ -1,18 +1,18 @@
import QrCode from 'jsqrcode';
import { Events } from 'framework/events';
import { Shortcuts } from 'comp/app/shortcuts';
import { Alerts } from 'comp/ui/alerts';
import { Otp } from 'util/data/otp';
import { Events } from 'util/events';
import { Shortcuts } from 'comp/browser/shortcuts';
import { Alert, Alerts } from 'comp/ui/alerts';
import { Features } from 'util/features';
import { Locale } from 'util/locale';
import { Logger } from 'util/logger';
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const QrCode = require('jsqrcode');
const logger = new Logger('otp-qr-reader');
class OtpQrReader {
alert = null;
fileInput = null;
private _alert?: Alert;
private _fileInput?: HTMLInputElement;
constructor() {
this.pasteEvent = this.pasteEvent.bind(this);
@@ -21,12 +21,12 @@ class OtpQrReader {
read() {
let screenshotKey = Shortcuts.screenshotToClipboardShortcut();
if (screenshotKey) {
screenshotKey = Locale.detSetupOtpAlertBodyWith.replace('{}', screenshotKey);
screenshotKey = Locale.detSetupOtpAlertBodyWith.with(screenshotKey);
}
const pasteKey = Features.isMobile
? ''
: Locale.detSetupOtpAlertBodyWith.replace('{}', Shortcuts.actionShortcutSymbol() + 'V');
this.startListenClipoard();
: Locale.detSetupOtpAlertBodyWith.with(Shortcuts.actionShortcutSymbol() + 'V');
this.startListenClipboard();
const buttons = [
{ result: 'manually', title: Locale.detSetupOtpManualButton, silent: true },
Alerts.buttons.cancel
@@ -36,14 +36,14 @@ class OtpQrReader {
}
const line3 = Features.isMobile
? Locale.detSetupOtpAlertBody3Mobile
: Locale.detSetupOtpAlertBody3.replace('{}', pasteKey || '');
this.alert = Alerts.alert({
: Locale.detSetupOtpAlertBody3.with(pasteKey || '');
this._alert = Alerts.alert({
icon: 'qrcode',
header: Locale.detSetupOtpAlert,
body: [
Locale.detSetupOtpAlertBody,
Locale.detSetupOtpAlertBody1,
Locale.detSetupOtpAlertBody2.replace('{}', screenshotKey || ''),
Locale.detSetupOtpAlertBody2.with(screenshotKey || ''),
line3,
Locale.detSetupOtpAlertBody4
].join('\n'),
@@ -52,7 +52,7 @@ class OtpQrReader {
enter: '',
buttons,
complete: (res) => {
this.alert = null;
this._alert = undefined;
this.stopListenClipboard();
if (res === 'select') {
this.selectFile();
@@ -64,78 +64,93 @@ class OtpQrReader {
}
selectFile() {
if (!this.fileInput) {
if (!this._fileInput) {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('capture', 'camera');
input.setAttribute('accept', 'image/*');
input.setAttribute('class', 'hide-by-pos');
this.fileInput = input;
this.fileInput.onchange = this.fileSelected;
this._fileInput = input;
this._fileInput.onchange = () => this.fileSelected();
}
this.fileInput.click();
this._fileInput.click();
}
fileSelected() {
const file = this.fileInput.files[0];
private fileSelected() {
const file = this._fileInput?.files?.[0];
if (!file || file.type.indexOf('image') < 0) {
return;
}
this.readFile(file);
}
startListenClipoard() {
private startListenClipboard() {
// eslint-disable-next-line @typescript-eslint/unbound-method
document.addEventListener('paste', this.pasteEvent);
}
stopListenClipboard() {
private stopListenClipboard() {
// eslint-disable-next-line @typescript-eslint/unbound-method
document.removeEventListener('paste', this.pasteEvent);
}
pasteEvent(e) {
const item = [...e.clipboardData.items].find(
private pasteEvent(e: ClipboardEvent) {
const items = e.clipboardData?.items;
if (!items) {
logger.info('Empty clipboard data');
return;
}
const item = [...items].find(
(item) => item.kind === 'file' && item.type.indexOf('image') !== -1
);
if (!item) {
logger.debug('Paste without file');
logger.info('Paste without file');
return;
}
logger.info('Reading pasted image', item.type);
if (this.alert) {
this.alert.change({
if (this._alert) {
this._alert.change({
header: Locale.detOtpImageReading
});
}
this.readFile(item.getAsFile());
const file = item.getAsFile();
if (file) {
this.readFile(file);
} else {
logger.info('Empty file');
}
}
readFile(file) {
private readFile(file: File) {
const reader = new FileReader();
reader.onload = () => {
logger.debug('Image data loaded');
if (typeof reader.result !== 'string') {
logger.info('Read not a string');
return;
}
logger.info('Image data loaded');
this.readQr(reader.result);
};
reader.readAsDataURL(file);
}
readQr(imageData) {
private readQr(imageData: string) {
const image = new Image();
image.onload = () => {
logger.debug('Image format loaded');
logger.info('Image format loaded');
try {
const ts = logger.ts();
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
const url = new QrCode(image).decode();
logger.info('QR code read', logger.ts(ts));
this.removeAlert();
try {
const otp = Otp.parseUrl(url);
Events.emit('qr-read', otp);
} catch (err) {
logger.error('Error parsing QR code', err);
if (typeof url === 'string' && url) {
Events.emit('qr-url-read', url);
} else {
logger.error('Error reading QR code');
Alerts.error({
header: Locale.detOtpQrWrong,
body: Locale.detOtpQrWrongBody,
pre: err.toString()
body: Locale.detOtpQrWrongBody
});
}
} catch (e) {
@@ -148,7 +163,7 @@ class OtpQrReader {
}
};
image.onerror = () => {
logger.debug('Image load error');
logger.info('Image load error');
this.removeAlert();
Alerts.error({
header: Locale.detOtpImageError,
@@ -158,13 +173,13 @@ class OtpQrReader {
image.src = imageData;
}
enterManually() {
private enterManually() {
Events.emit('qr-enter-manually');
}
removeAlert() {
if (this.alert) {
this.alert.closeImmediate();
private removeAlert() {
if (this._alert) {
this._alert.closeImmediate();
}
}
}
@@ -1,99 +0,0 @@
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 };
-306
View File
@@ -1,306 +0,0 @@
import * as kdbxweb from 'kdbxweb';
import { Events } from 'framework/events';
import { RuntimeInfo } from 'const/runtime-info';
import { Transport } from 'comp/browser/transport';
import { Launcher } from 'comp/launcher';
import { Links } from 'const/links';
import { AppSettingsModel } from 'models/app-settings-model';
import { UpdateModel } from 'models/update-model';
import { SemVer } from 'util/data/semver';
import { Logger } from 'util/logger';
import { SignatureVerifier } from 'util/data/signature-verifier';
const logger = new Logger('updater');
const Updater = {
UpdateInterval: 1000 * 60 * 60 * 24,
MinUpdateTimeout: 500,
MinUpdateSize: 10000,
nextCheckTimeout: null,
updateCheckDate: new Date(0),
enabled: Launcher?.updaterEnabled(),
getAutoUpdateType() {
if (!this.enabled) {
return false;
}
let autoUpdate = AppSettingsModel.autoUpdate;
if (autoUpdate && autoUpdate === true) {
autoUpdate = 'install';
}
return autoUpdate;
},
updateInProgress() {
return (
UpdateModel.status === 'checking' ||
['downloading', 'extracting', 'updating'].indexOf(UpdateModel.updateStatus) >= 0
);
},
init() {
this.scheduleNextCheck();
if (!Launcher && navigator.serviceWorker && !RuntimeInfo.beta && !RuntimeInfo.devMode) {
navigator.serviceWorker
.register('service-worker.js')
.then((reg) => {
logger.info('Service worker registered');
reg.addEventListener('updatefound', () => {
if (reg.active) {
logger.info('Service worker found an update');
UpdateModel.set({ updateStatus: 'ready' });
}
});
})
.catch((e) => {
logger.error('Failed to register a service worker', e);
});
}
},
scheduleNextCheck() {
if (this.nextCheckTimeout) {
clearTimeout(this.nextCheckTimeout);
this.nextCheckTimeout = null;
}
if (!this.getAutoUpdateType()) {
return;
}
let timeDiff = this.MinUpdateTimeout;
const lastCheckDate = UpdateModel.lastCheckDate;
if (lastCheckDate) {
timeDiff = Math.min(
Math.max(this.UpdateInterval + (lastCheckDate - new Date()), this.MinUpdateTimeout),
this.UpdateInterval
);
}
this.nextCheckTimeout = setTimeout(this.check.bind(this), timeDiff);
logger.info('Next update check will happen in ' + Math.round(timeDiff / 1000) + 's');
},
check(startedByUser) {
if (!this.enabled || this.updateInProgress()) {
return;
}
UpdateModel.set({ status: 'checking' });
if (!startedByUser) {
// additional protection from broken program logic, to ensure that auto-checks are not performed more than once an hour
const diffMs = new Date() - this.updateCheckDate;
if (isNaN(diffMs) || diffMs < 1000 * 60 * 60) {
logger.error(
'Prevented update check; last check was performed at ' + this.updateCheckDate
);
this.scheduleNextCheck();
return;
}
this.updateCheckDate = new Date();
}
logger.info('Checking for update...');
Transport.httpGet({
url: Links.UpdateJson,
json: true,
success: (updateJson) => {
const dt = new Date();
logger.info('Update check: ' + (updateJson.version || 'unknown'));
if (!updateJson.version) {
const errMsg = 'No version info found';
UpdateModel.set({
status: 'error',
lastCheckDate: dt,
lastCheckError: errMsg
});
UpdateModel.save();
this.scheduleNextCheck();
return;
}
const prevLastVersion = UpdateModel.lastVersion;
UpdateModel.set({
status: 'ok',
lastCheckDate: dt,
lastSuccessCheckDate: dt,
lastVersionReleaseDate: new Date(updateJson.date),
lastVersion: updateJson.version,
lastCheckError: null,
lastCheckUpdMin: updateJson.minVersion || null
});
UpdateModel.save();
this.scheduleNextCheck();
if (!this.canAutoUpdate()) {
return;
}
if (
prevLastVersion === UpdateModel.lastVersion &&
UpdateModel.updateStatus === 'ready'
) {
logger.info('Waiting for the user to apply downloaded update');
return;
}
if (!startedByUser && this.getAutoUpdateType() === 'install') {
this.update(startedByUser);
} else if (
SemVer.compareVersions(UpdateModel.lastVersion, RuntimeInfo.version) > 0
) {
UpdateModel.set({ updateStatus: 'found' });
}
},
error: (e) => {
logger.error('Update check error', e);
UpdateModel.set({
status: 'error',
lastCheckDate: new Date(),
lastCheckError: 'Error checking last version'
});
UpdateModel.save();
this.scheduleNextCheck();
}
});
},
canAutoUpdate() {
const minLauncherVersion = UpdateModel.lastCheckUpdMin;
if (minLauncherVersion) {
const cmp = SemVer.compareVersions(RuntimeInfo.version, minLauncherVersion);
if (cmp < 0) {
UpdateModel.set({ updateStatus: 'ready', updateManual: true });
return false;
}
}
return true;
},
update(startedByUser, successCallback) {
const ver = UpdateModel.lastVersion;
if (!this.enabled) {
logger.info('Updater is disabled');
return;
}
if (SemVer.compareVersions(RuntimeInfo.version, ver) >= 0) {
logger.info('You are using the latest version');
return;
}
UpdateModel.set({ updateStatus: 'downloading', updateError: null });
logger.info('Downloading update', ver);
const updateAssetName = this.getUpdateAssetName(ver);
if (!updateAssetName) {
logger.error('Empty updater asset name for', Launcher.platform(), Launcher.arch());
return;
}
const updateUrlBasePath = Links.UpdateBasePath.replace('{ver}', ver);
const updateAssetUrl = updateUrlBasePath + updateAssetName;
Transport.httpGet({
url: updateAssetUrl,
file: updateAssetName,
cleanupOldFiles: true,
cache: true,
success: (assetFilePath) => {
logger.info('Downloading update signatures');
Transport.httpGet({
url: updateUrlBasePath + 'Verify.sign.sha256',
text: true,
file: updateAssetName + '.sign',
cleanupOldFiles: true,
cache: true,
success: (assetFileSignaturePath) => {
this.verifySignature(assetFilePath, updateAssetName, (err, valid) => {
if (err || !valid) {
UpdateModel.set({
updateStatus: 'error',
updateError: err
? 'Error verifying update signature'
: 'Invalid update signature'
});
Launcher.deleteFile(assetFilePath);
Launcher.deleteFile(assetFileSignaturePath);
return;
}
logger.info('Update is ready', assetFilePath);
UpdateModel.set({ updateStatus: 'ready', updateError: null });
if (!startedByUser) {
Events.emit('update-app');
}
if (typeof successCallback === 'function') {
successCallback();
}
});
},
error(e) {
logger.error('Error downloading update signatures', e);
UpdateModel.set({
updateStatus: 'error',
updateError: 'Error downloading update signatures'
});
}
});
},
error(e) {
logger.error('Error downloading update', e);
UpdateModel.set({
updateStatus: 'error',
updateError: 'Error downloading update'
});
}
});
},
verifySignature(assetFilePath, assetName, callback) {
logger.info('Verifying update signature', assetName);
const fs = Launcher.req('fs');
const signaturesTxt = fs.readFileSync(assetFilePath + '.sign', 'utf8');
const assetSignatureLine = signaturesTxt
.split('\n')
.find((line) => line.endsWith(assetName));
if (!assetSignatureLine) {
logger.error('Signature not found for asset', assetName);
callback('Asset signature not found');
return;
}
const signature = kdbxweb.ByteUtils.hexToBytes(assetSignatureLine.split(' ')[0]);
const fileBytes = fs.readFileSync(assetFilePath);
SignatureVerifier.verify(fileBytes, signature)
.catch((e) => {
logger.error('Error verifying signature', e);
callback('Error verifying signature');
})
.then((valid) => {
logger.info(`Update asset signature is ${valid ? 'valid' : 'invalid'}`);
callback(undefined, valid);
});
},
getUpdateAssetName(ver) {
const platform = Launcher.platform();
const arch = Launcher.arch();
switch (platform) {
case 'win32':
switch (arch) {
case 'x64':
return `KeeWeb-${ver}.win.x64.exe`;
case 'ia32':
return `KeeWeb-${ver}.win.ia32.exe`;
case 'arm64':
return `KeeWeb-${ver}.win.arm64.exe`;
}
break;
case 'darwin':
switch (arch) {
case 'x64':
return `KeeWeb-${ver}.mac.x64.dmg`;
case 'arm64':
return `KeeWeb-${ver}.mac.arm64.dmg`;
}
break;
}
return undefined;
},
installAndRestart() {
if (!Launcher) {
return;
}
const updateAssetName = this.getUpdateAssetName(UpdateModel.lastVersion);
const updateFilePath = Transport.cacheFilePath(updateAssetName);
Launcher.requestRestartAndUpdate(updateFilePath);
}
};
export { Updater };
+472
View File
@@ -0,0 +1,472 @@
import * as kdbxweb from 'kdbxweb';
import { RuntimeInfo } from 'const/runtime-info';
import { Launcher } from 'comp/launcher';
import { Links } from 'const/links';
import { AppSettings } from 'models/app-settings';
import { SemVer } from 'util/data/semver';
import { Logger } from 'util/logger';
import { SignatureVerifier } from 'util/data/signature-verifier';
import { Model } from 'util/model';
import { Timeouts } from 'const/timeouts';
import { RuntimeData } from 'models/runtime-data';
import { noop } from 'util/fn';
import { StringFormat } from 'util/formatting/string-format';
import { ClientRequestArgs } from 'http';
import * as fs from 'fs';
import path from 'path';
import https from 'https';
const logger = new Logger('updater');
class Updater extends Model {
readonly enabled = Launcher?.updaterEnabled() ?? false;
status: 'unknown' | 'ready' | 'checking' | 'error' | 'ok' = 'unknown';
updateStatus:
| 'unknown'
| 'found'
| 'ready'
| 'downloading'
| 'extracting'
| 'updating'
| 'error' = 'unknown';
updateError?: string;
private _nextCheckTimeout?: number;
private _updateCheckDate = new Date(0);
init(): void {
this.scheduleNextCheck();
if (!Launcher && navigator.serviceWorker && !RuntimeInfo.beta && !RuntimeInfo.devMode) {
navigator.serviceWorker
.register('service-worker.js')
.then((reg) => {
logger.info('Service worker registered');
reg.addEventListener('updatefound', () => {
if (reg.active) {
logger.info('Service worker found an update');
this.updateStatus = 'ready';
}
});
})
.catch((e) => {
logger.error('Failed to register a service worker', e);
});
}
}
get updateInProgress(): boolean {
return (
this.status === 'checking' ||
['downloading', 'extracting', 'updating'].includes(this.updateStatus)
);
}
scheduleNextCheck(): void {
if (this._nextCheckTimeout) {
clearTimeout(this._nextCheckTimeout);
this._nextCheckTimeout = undefined;
}
if (!this.enabled || !AppSettings.autoUpdate) {
return;
}
let timeDiff = Timeouts.MinUpdateTimeout;
const lastCheckDate = RuntimeData.lastUpdateCheckDate;
if (lastCheckDate) {
timeDiff = Math.min(
Math.max(
Timeouts.UpdateInterval + (lastCheckDate.getTime() - Date.now()),
Timeouts.MinUpdateTimeout
),
Timeouts.UpdateInterval
);
}
this._nextCheckTimeout = window.setTimeout(() => {
this.check(false).catch(noop);
}, timeDiff);
logger.info(`Next update check will happen in ${Math.round(timeDiff / 1000)}s`);
}
async check(startedByUser: boolean) {
if (!this.enabled || this.updateInProgress) {
return;
}
this.status = 'checking';
if (!startedByUser) {
// additional protection from broken program logic, to ensure that auto-checks are not performed more than once an hour
const diffMs = Date.now() - this._updateCheckDate.getTime();
if (isNaN(diffMs) || diffMs < 1000 * 60 * 60) {
logger.error(
'Prevented update check; last check was performed at',
this._updateCheckDate
);
this.scheduleNextCheck();
return;
}
this._updateCheckDate = new Date();
}
logger.info('Checking for update...');
let data: Buffer | undefined;
try {
const httpRes = await this.httpGet({
url: Links.UpdateJson
});
data = httpRes.data;
} catch (e) {
logger.error('Update check error', e);
RuntimeData.batchSet(() => {
RuntimeData.lastUpdateCheckDate = new Date();
RuntimeData.lastUpdateCheckError = 'Error checking last version';
});
this.status = 'error';
this.scheduleNextCheck();
return;
}
let updateJson: Record<string, unknown>;
try {
const parsed = JSON.parse(data?.toString() || '') as unknown;
if (!parsed || typeof parsed !== 'object') {
throw new Error('Empty data');
}
updateJson = parsed as Record<string, unknown>;
} catch (e) {
logger.error('Failed to parse JSON', data);
return;
}
const updateVersion = updateJson.version;
const updateDate = new Date(String(updateJson.date));
const dt = new Date();
logger.info('Update check', updateVersion || 'no version', updateDate);
if (typeof updateVersion !== 'string' || !updateDate.getTime()) {
RuntimeData.batchSet(() => {
RuntimeData.lastUpdateCheckDate = dt;
RuntimeData.lastUpdateCheckError = 'Failed to parse update information';
});
this.status = 'error';
this.scheduleNextCheck();
return;
}
const prevLastVersion = RuntimeData.lastUpdateVersion;
RuntimeData.batchSet(() => {
RuntimeData.lastUpdateCheckDate = dt;
RuntimeData.lastSuccessUpdateCheckDate = dt;
RuntimeData.lastUpdateVersionReleaseDate = updateDate;
RuntimeData.lastUpdateVersion = updateVersion;
RuntimeData.lastUpdateCheckError = undefined;
});
this.status = 'ok';
this.scheduleNextCheck();
if (prevLastVersion === RuntimeData.lastUpdateVersion && this.updateStatus === 'ready') {
logger.info('Waiting for the user to apply downloaded update');
return;
}
if (!startedByUser && AppSettings.autoUpdate === 'install') {
await this.update();
} else if (SemVer.compareVersions(updateVersion, RuntimeInfo.version) > 0) {
this.updateStatus = 'found';
}
}
async updateAndRestart() {
await this.update();
await this.installAndRestart();
}
private async update() {
if (!this.enabled) {
logger.info('Updater is disabled');
return;
}
const ver = RuntimeData.lastUpdateVersion;
if (!ver) {
logger.info('No last update version');
return;
}
if (SemVer.compareVersions(RuntimeInfo.version, ver) >= 0) {
logger.info('You are using the latest version');
return;
}
this.batchSet(() => {
this.updateStatus = 'downloading';
this.updateError = undefined;
});
logger.info('Downloading update', ver);
const updateAssetName = this.getUpdateAssetName(ver);
if (!updateAssetName) {
logger.error('Empty updater asset name for', process.platform, process.arch);
return;
}
const updateUrlBasePath = Links.UpdateBasePath.replace('{ver}', ver);
const updateAssetUrl = updateUrlBasePath + updateAssetName;
let assetFilePath: string;
try {
const httpRes = await this.httpGet({
url: updateAssetUrl,
file: updateAssetName,
cleanupOldFiles: true,
cache: true
});
if (!httpRes.fileName) {
throw new Error('No fileName');
}
assetFilePath = httpRes.fileName;
} catch (e) {
logger.error('Error downloading update', e);
this.batchSet(() => {
this.updateStatus = 'error';
this.updateError = 'Error downloading update';
});
return;
}
logger.info('Downloading update signatures');
let assetFileSignaturePath: string;
try {
const httpRes = await this.httpGet({
url: updateUrlBasePath + 'Verify.sign.sha256',
file: updateAssetName + '.sign',
cleanupOldFiles: true,
cache: true
});
if (!httpRes.fileName) {
throw new Error('No fileName');
}
assetFileSignaturePath = httpRes.fileName;
} catch (e) {
logger.error('Error downloading update signatures', e);
this.batchSet(() => {
this.updateStatus = 'error';
this.updateError = 'Error downloading update signatures';
});
return;
}
let valid: boolean;
try {
valid = await this.verifySignature(assetFilePath, updateAssetName);
} catch (err) {
this.batchSet(() => {
this.updateStatus = 'error';
this.updateError = 'Error verifying update signature';
});
return;
}
if (!valid) {
this.batchSet(() => {
this.updateStatus = 'error';
this.updateError = 'Invalid update signature';
});
fs.unlink(assetFilePath, noop);
fs.unlink(assetFileSignaturePath, noop);
return;
}
logger.info('Update is ready', assetFilePath);
this.batchSet(() => {
this.updateStatus = 'ready';
this.updateError = undefined;
});
}
private async verifySignature(assetFilePath: string, assetName: string): Promise<boolean> {
logger.info('Verifying update signature', assetName);
const signaturesTxt = await fs.promises.readFile(assetFilePath + '.sign', 'utf8');
const assetSignatureLine = signaturesTxt
.split('\n')
.find((line) => line.endsWith(assetName));
if (!assetSignatureLine) {
logger.error('Signature not found for asset', assetName);
throw new Error('Asset signature not found');
}
const signature = kdbxweb.ByteUtils.hexToBytes(assetSignatureLine.split(' ')[0]);
const fileBytes = fs.readFileSync(assetFilePath);
try {
const valid = await SignatureVerifier.verify(fileBytes, signature);
logger.info(`Update asset signature is ${valid ? 'valid' : 'invalid'}`);
return valid;
} catch (e) {
logger.error('Error verifying signature', e);
throw e;
}
}
private getUpdateAssetName(ver: string): string | undefined {
const platform = process.platform;
const arch = process.arch;
switch (platform) {
case 'win32':
switch (arch) {
case 'x64':
return `KeeWeb-${ver}.win.x64.exe`;
case 'ia32':
return `KeeWeb-${ver}.win.ia32.exe`;
case 'arm64':
return `KeeWeb-${ver}.win.arm64.exe`;
}
break;
case 'darwin':
switch (arch) {
case 'x64':
return `KeeWeb-${ver}.mac.x64.dmg`;
case 'arm64':
return `KeeWeb-${ver}.mac.arm64.dmg`;
}
break;
}
return undefined;
}
async installAndRestart(): Promise<void> {
if (!Launcher || !RuntimeData.lastUpdateVersion) {
return;
}
const updateAssetName = this.getUpdateAssetName(RuntimeData.lastUpdateVersion);
const updateFilePath = await this.cacheFilePath(updateAssetName);
Launcher.requestRestartAndUpdate(updateFilePath);
}
async cacheFilePath(fileName?: string): Promise<string> {
const tempPath = await Launcher?.ipcRenderer.invoke('get-temp-path');
if (!tempPath) {
throw new Error('Failed to get temp path');
}
return fileName ? path.join(tempPath, fileName) : tempPath;
}
async httpGet(config: {
url: string;
file?: string;
cache?: boolean;
cleanupOldFiles?: boolean;
noRedirect?: boolean;
}): Promise<{ fileName?: string; data?: Buffer }> {
let tmpFile: string;
if (config.file) {
const baseTempPath = await this.cacheFilePath();
if (config.cleanupOldFiles) {
const allFiles = await fs.promises.readdir(baseTempPath);
for (const file of allFiles) {
if (
file !== config.file &&
StringFormat.replaceVersion(file, '0') ===
StringFormat.replaceVersion(config.file, '0')
) {
await fs.promises.unlink(path.join(baseTempPath, file));
}
}
}
tmpFile = path.join(baseTempPath, config.file);
let tmpFileExists = true;
try {
await fs.promises.access(tmpFile);
} catch {
tmpFileExists = false;
}
if (tmpFileExists) {
try {
if (config.cache && (await fs.promises.stat(tmpFile)).size > 0) {
logger.info('File already downloaded', config.url);
return Promise.resolve({ fileName: tmpFile });
} else {
await fs.promises.unlink(tmpFile);
}
} catch (e) {
fs.promises.unlink(tmpFile).catch(noop);
}
}
}
logger.info('GET ' + config.url);
const proxy = await Launcher?.ipcRenderer.invoke('resolve-proxy', config.url);
const url = new URL(config.url);
const opts: ClientRequestArgs = {
host: url.host,
port: url.port,
path: url.pathname + url.search
};
opts.headers = { 'User-Agent': navigator.userAgent };
logger.info(
'Request to ' +
config.url +
' ' +
(proxy ? `using proxy ${proxy.host}:${proxy.port}` : 'without proxy')
);
if (proxy) {
opts.headers.Host = url.host;
opts.host = proxy.host;
opts.port = proxy.port;
opts.path = config.url;
}
return new Promise((resolve, reject) => {
https
.get(opts, (res) => {
logger.info(`Response from ${config.url}: `, res.statusCode);
if (res.statusCode === 200) {
if (config.file) {
const file = fs.createWriteStream(tmpFile);
res.pipe(file);
file.on('finish', () => {
file.on('close', () => {
resolve({ fileName: tmpFile });
});
file.close();
});
file.on('error', (err) => {
reject(err);
});
} else {
const chunks: Buffer[] = [];
res.on('data', (chunk) => {
chunks.push(chunk);
});
res.on('end', () => {
resolve({ data: Buffer.concat(chunks) });
});
}
} else if (
res.headers.location &&
(res.statusCode === 301 || res.statusCode === 302)
) {
if (config.noRedirect) {
return reject(new Error('Too many redirects'));
}
config.url = res.headers.location;
config.noRedirect = true;
resolve(this.httpGet(config));
} else {
reject(new Error(`HTTP status ${res.statusCode ?? 0}`));
}
})
.on('error', (e) => {
logger.error('Cannot GET ' + config.url, e);
if (tmpFile) {
fs.unlink(tmpFile, noop);
}
reject(e);
});
});
}
}
const instance = new Updater();
export { instance as Updater };
-295
View File
@@ -1,295 +0,0 @@
import { Events } from 'framework/events';
import { Launcher } from 'comp/launcher';
import { NativeModules } from 'comp/launcher/native-modules';
import { Logger } from 'util/logger';
import { UsbListener } from 'comp/app/usb-listener';
import { AppSettingsModel } from 'models/app-settings-model';
import { Timeouts } from 'const/timeouts';
import { YubiKeyProductIds, YubiKeyChallengeSize } from 'const/hardware';
import { Locale } from 'util/locale';
const logger = new Logger('yubikey');
const YubiKey = {
ykmanStatus: undefined,
process: null,
aborted: false,
cmd() {
if (this._cmd) {
return this._cmd;
}
const macYkmanPath = '/usr/local/bin/ykman';
if (Launcher.platform() === 'darwin' && Launcher.fileExistsSync(macYkmanPath)) {
this._cmd = macYkmanPath;
} else {
this._cmd = 'ykman';
}
return this._cmd;
},
checkToolStatus() {
if (this.ykmanStatus === 'ok') {
return Promise.resolve(this.ykmanStatus);
}
return new Promise((resolve) => {
this.ykmanStatus = 'checking';
this._cmd = undefined;
Launcher.spawn({
cmd: this.cmd(),
args: ['-v'],
noStdOutLogging: true,
complete: (err, stdout, code) => {
if (err || code !== 0) {
this.ykmanStatus = 'error';
} else {
this.ykmanStatus = 'ok';
}
resolve(this.ykmanStatus);
}
});
});
},
abort() {
logger.info('Aborting');
if (this.process) {
logger.info('Killing the process');
try {
this.process.kill();
} catch {}
}
this.aborted = true;
this.process = null;
},
list(callback) {
NativeModules.getYubiKeys({})
.then((yubiKeys) => {
yubiKeys = yubiKeys.map(({ serial, vid, pid, version, slots }) => {
return {
vid,
pid,
serial,
slots,
fullName: this.getKeyFullName(pid, version, serial)
};
});
callback(null, yubiKeys);
})
.catch(callback);
},
getKeyFullName(pid, version, serial) {
let name = 'YubiKey';
if (YubiKeyProductIds.Gen1.includes(pid)) {
name += ' Gen 1';
} else if (YubiKeyProductIds.NEO.includes(pid)) {
name += ' NEO';
} else if (YubiKeyProductIds.YK4.includes(pid)) {
if (version >= '5.1.0') {
name += ' 5';
}
}
return `${name} ${serial}`;
},
listWithYkman(callback) {
this._listWithYkman(callback, true);
},
_listWithYkman(callback, canRetry) {
if (this.process) {
return callback('Already in progress');
}
this.aborted = false;
logger.info('Listing YubiKeys');
if (!UsbListener.attachedYubiKeys) {
return callback(null, []);
}
this.process = Launcher.spawn({
cmd: this.cmd(),
args: ['list'],
noStdOutLogging: true,
complete: (err, stdout) => {
this.process = null;
if (this.aborted) {
return callback('Aborted');
}
if (err) {
return callback(err);
}
const yubiKeysIncludingEmpty = stdout
.trim()
.split(/\n/g)
.map((line) => {
const fullName = line;
const serial = (line.match(/\d{5,}$/g) || [])[0];
return { fullName, serial };
});
const yubiKeys = yubiKeysIncludingEmpty.filter((s) => s.serial);
if (
yubiKeysIncludingEmpty.length === 1 &&
yubiKeys.length === 0 &&
stdout.startsWith('YubiKey') &&
stdout.includes('CCID') &&
!stdout.includes('Serial')
) {
logger.info('The YubiKey is probably stuck');
if (!AppSettingsModel.yubiKeyStuckWorkaround) {
return callback(Locale.yubiKeyStuckError);
}
if (canRetry) {
return this._repairStuckYubiKey(callback);
}
}
if (!yubiKeys.length) {
return callback('No YubiKeys returned by "ykman list"');
}
callback(null, yubiKeys);
}
});
},
_repairStuckYubiKey(callback) {
logger.info('Repairing a stuck YubiKey');
let openTimeout;
const countYubiKeys = UsbListener.attachedYubiKeys;
const onDevicesChangedDuringRepair = () => {
if (UsbListener.attachedYubiKeys === countYubiKeys) {
logger.info('YubiKey was reconnected');
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
clearTimeout(openTimeout);
this.aborted = false;
setTimeout(() => {
this._listWithYkman(callback, false);
}, Timeouts.ExternalDeviceAfterReconnect);
}
};
Events.on('usb-devices-changed', onDevicesChangedDuringRepair);
Launcher.spawn({
cmd: this.cmd(),
args: ['config', 'usb', '-e', 'oath', '-f'],
noStdOutLogging: true,
complete: (err) => {
logger.info('Repair complete', err ? 'with error' : 'OK');
if (err) {
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
return callback(`YubiKey repair error: ${err}`);
}
openTimeout = setTimeout(() => {
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
}, Timeouts.ExternalDeviceReconnect);
}
});
},
getOtpCodes(serial, callback) {
if (this.process) {
return callback('Already in progress');
}
this.aborted = false;
this.process = Launcher.spawn({
cmd: this.cmd(),
args: ['-d', serial, 'oath', 'accounts', 'code'],
noStdOutLogging: true,
throwOnStdErr: true,
complete: (err, stdout) => {
this.process = null;
if (window.debugYubiKey) {
logger.info('received codes', err, stdout);
}
if (this.aborted) {
return callback('Aborted');
}
if (err) {
return callback(err);
}
const codes = [];
for (const line of stdout.split('\n')) {
const match = line.match(/^(.*?):(.*?)\s+(.*)$/);
if (!match) {
continue;
}
const [, title, user, code] = match;
const needsTouch = !code.match(/^\d+$/);
codes.push({
title,
user,
needsTouch
});
}
callback(null, codes);
}
});
},
getOtp(serial, entry, callback) {
return Launcher.spawn({
cmd: this.cmd(),
args: ['-d', serial, 'oath', 'accounts', 'code', '--single', entry],
noStdOutLogging: true,
complete: (err, stdout) => {
if (err) {
return callback(err);
}
const otp = stdout.trim();
callback(null, otp);
}
});
},
calculateChalResp(chalResp, challenge, callback) {
const { vid, pid, serial, slot } = chalResp;
const yubiKey = { vid, pid, serial };
challenge = Buffer.from(challenge);
// https://github.com/Yubico/yubikey-personalization-gui/issues/86
// https://github.com/keepassxreboot/keepassxc/blob/develop/src/keys/drivers/YubiKey.cpp#L318
const padLen = YubiKeyChallengeSize - challenge.byteLength;
const paddedChallenge = Buffer.alloc(YubiKeyChallengeSize, padLen);
challenge.copy(paddedChallenge);
NativeModules.yubiKeyChallengeResponse(
yubiKey,
[...paddedChallenge],
slot,
(err, result) => {
if (result) {
result = Buffer.from(result);
}
if (err) {
err.ykError = true;
}
return callback(err, result);
}
);
},
cancelChalResp() {
NativeModules.yubiKeyCancelChallengeResponse();
}
};
export { YubiKey };
@@ -1,17 +1,20 @@
import { Events } from 'framework/events';
import { Events } from 'util/events';
import { Launcher } from 'comp/launcher';
import { AppSettingsModel } from 'models/app-settings-model';
import { AppSettings } from 'models/app-settings';
interface CopyPasteResult {
success: boolean;
seconds?: number;
}
const CopyPaste = {
simpleCopy: !!(Launcher && Launcher.clipboardSupported),
copy(text) {
if (this.simpleCopy) {
copy(text: string): CopyPasteResult {
if (Launcher) {
Launcher.setClipboardText(text);
const clipboardSeconds = AppSettingsModel.clipboardSeconds;
const clipboardSeconds = AppSettings.clipboardSeconds;
if (clipboardSeconds > 0) {
const clearClipboard = () => {
if (Launcher.getClipboardText() === text) {
if (Launcher?.getClipboardText() === text) {
Launcher.clearClipboardText();
}
};
@@ -28,39 +31,41 @@ const CopyPaste = {
return { success: true };
}
} catch (e) {}
return false;
return { success: false };
}
},
createHiddenInput(text) {
const hiddenInput = $('<input/>')
.val(text)
.attr({ type: 'text', 'class': 'hide-by-pos' })
.appendTo(document.body);
hiddenInput[0].selectionStart = 0;
hiddenInput[0].selectionEnd = text.length;
createHiddenInput(text: string): void {
const hiddenInput = document.createElement('input');
hiddenInput.value = text;
hiddenInput.classList.add('hide-by-pos');
document.body.appendChild(hiddenInput);
hiddenInput.selectionStart = 0;
hiddenInput.selectionEnd = text.length;
hiddenInput.focus();
hiddenInput.on({
'copy cut paste'() {
setTimeout(() => hiddenInput.blur(), 0);
},
blur() {
hiddenInput.remove();
}
});
const onChange = () => {
setTimeout(() => hiddenInput.blur(), 0);
};
hiddenInput.addEventListener('cut', onChange);
hiddenInput.addEventListener('copy', onChange);
hiddenInput.addEventListener('paste', onChange);
hiddenInput.addEventListener('blur', () => hiddenInput.remove());
},
copyHtml(html) {
copyHtml(html: string): boolean {
const el = document.createElement('div');
el.style.userSelect = 'auto';
el.style.webkitUserSelect = 'auto';
el.style.mozUserSelect = 'auto';
el.innerHTML = html;
document.body.appendChild(el);
const range = document.createRange();
range.selectNodeContents(el);
const sel = window.getSelection();
if (!sel) {
el.remove();
return false;
}
sel.removeAllRanges();
sel.addRange(range);
@@ -1,9 +1,12 @@
import { Events } from 'framework/events';
import { Events } from 'util/events';
import { Launcher } from 'comp/launcher';
import { Features } from 'util/features';
const FocusDetector = {
init() {
isFocused: false,
detectsFocusWithEvents: false,
init(): void {
this.isFocused = true;
this.detectsFocusWithEvents = !Features.isDesktop && !Features.isMobile;
if (this.detectsFocusWithEvents) {
@@ -22,13 +25,13 @@ const FocusDetector = {
}
},
hasFocus() {
hasFocus(): Promise<boolean> {
if (this.detectsFocusWithEvents) {
return this.isFocused;
return Promise.resolve(this.isFocused);
} else if (Launcher) {
return Launcher.isAppFocused();
return Launcher.ipcRenderer.invoke('is-app-focused');
} else {
return true;
return Promise.resolve(true);
}
}
};
@@ -1,24 +1,24 @@
import { Events } from 'framework/events';
import { AppSettingsModel } from 'models/app-settings-model';
import { Events } from 'util/events';
import { AppSettings } from 'models/app-settings';
const IdleTracker = {
actionTime: Date.now(),
init() {
init(): void {
setInterval(this.checkIdle.bind(this), 1000 * 60);
},
checkIdle() {
checkIdle(): void {
const idleMinutes = (Date.now() - this.actionTime) / 1000 / 60;
const maxIdleMinutes = AppSettingsModel.idleMinutes;
const maxIdleMinutes = AppSettings.idleMinutes;
if (maxIdleMinutes && idleMinutes > maxIdleMinutes) {
Events.emit('before-user-idle');
Events.emit('user-idle');
}
},
regUserAction() {
regUserAction(): void {
this.actionTime = Date.now();
}
};
Events.on('power-monitor-resume', () => IdleTracker.checkIdle);
Events.on('power-monitor-resume', () => IdleTracker.checkIdle());
export { IdleTracker };
@@ -1,62 +1,77 @@
import { Events } from 'framework/events';
import { Events } from 'util/events';
import { IdleTracker } from 'comp/browser/idle-tracker';
import { Keys } from 'const/keys';
import { FocusManager } from 'comp/app/focus-manager';
const shortcutKeyProp = navigator.platform.indexOf('Mac') >= 0 ? 'metaKey' : 'ctrlKey';
interface ShortcutDef {
handler: (e: KeyboardEvent, code: number) => void;
shortcut?: number;
modal?: string;
noPrevent?: boolean;
}
class KeyHandler {
SHORTCUT_ACTION = 1;
SHORTCUT_OPT = 2;
SHORTCUT_SHIFT = 4;
shortcuts = {};
shortcuts = new Map<Keys, ShortcutDef[]>();
init() {
$(document).bind('keypress', this.keypress.bind(this));
$(document).bind('keydown', this.keydown.bind(this));
document.addEventListener('keypress', (e) => this.keypress(e));
document.addEventListener('keydown', (e) => this.keydown(e));
this.shortcuts[Keys.DOM_VK_A] = [
this.shortcuts.set(Keys.DOM_VK_A, [
{
handler: this.handleAKey,
thisArg: this,
handler: (e) => this.handleAKey(e),
shortcut: this.SHORTCUT_ACTION,
modal: true,
modal: '*',
noPrevent: true
}
];
]);
}
onKey(key, handler, thisArg, shortcut, modal, noPrevent) {
let keyShortcuts = this.shortcuts[key];
onKey(
key: Keys,
handler: (e: KeyboardEvent) => void,
shortcut?: number,
modal?: string,
noPrevent?: boolean
): () => void {
let keyShortcuts = this.shortcuts.get(key);
if (!keyShortcuts) {
this.shortcuts[key] = keyShortcuts = [];
keyShortcuts = [];
this.shortcuts.set(key, keyShortcuts);
}
keyShortcuts.push({
const def: ShortcutDef = {
handler,
thisArg,
shortcut,
modal,
noPrevent
});
};
keyShortcuts.push(def);
return () => {
const keyShortcuts = this.shortcuts.get(key);
if (keyShortcuts) {
const ix = keyShortcuts.indexOf(def);
if (ix >= 0) {
keyShortcuts.splice(ix, 1);
}
}
};
}
offKey(key, handler, thisArg) {
if (this.shortcuts[key]) {
this.shortcuts[key] = this.shortcuts[key].filter(
(sh) => sh.handler !== handler || sh.thisArg !== thisArg
);
}
}
isActionKey(e) {
isActionKey(e: KeyboardEvent): boolean {
return e[shortcutKeyProp];
}
keydown(e) {
keydown(e: KeyboardEvent): void {
IdleTracker.regUserAction();
const code = e.keyCode || e.which;
const keyShortcuts = this.shortcuts[code];
const keyShortcuts = this.shortcuts.get(code);
if (keyShortcuts && keyShortcuts.length) {
for (const sh of keyShortcuts) {
if (FocusManager.modal && sh.modal !== FocusManager.modal && sh.modal !== '*') {
@@ -91,18 +106,23 @@ class KeyHandler {
}
break;
}
sh.handler.call(sh.thisArg, e, code);
let immediatePropagationStopped = false;
e.stopImmediatePropagation = () => {
immediatePropagationStopped = true;
KeyboardEvent.prototype.stopImmediatePropagation.call(e);
};
sh.handler(e, code);
if (isActionKey && !sh.noPrevent) {
e.preventDefault();
}
if (e.isImmediatePropagationStopped()) {
if (immediatePropagationStopped) {
break;
}
}
}
}
keypress(e) {
keypress(e: KeyboardEvent): void {
if (
!FocusManager.modal &&
e.which !== Keys.DOM_VK_RETURN &&
@@ -114,7 +134,7 @@ class KeyHandler {
) {
Events.emit('keypress', e);
} else if (FocusManager.modal) {
Events.emit('keypress:' + FocusManager.modal, e);
Events.emit('keypress-modal', e, FocusManager.modal);
}
}
@@ -122,11 +142,8 @@ class KeyHandler {
IdleTracker.regUserAction();
}
handleAKey(e) {
if (
e.target.tagName.toLowerCase() === 'input' &&
['password', 'text'].indexOf(e.target.type) >= 0
) {
handleAKey(e: KeyboardEvent): void {
if (e.target instanceof HTMLInputElement && ['password', 'text'].includes(e.target.type)) {
e.stopImmediatePropagation();
} else {
e.preventDefault();
@@ -1,19 +1,13 @@
import { Events } from 'framework/events';
import { Events } from 'util/events';
import { Launcher } from 'comp/launcher';
import { Alerts } from 'comp/ui/alerts';
import { Timeouts } from 'const/timeouts';
import { Locale } from 'util/locale';
import { Logger } from 'util/logger';
import { noop } from 'util/fn';
const PopupNotifier = {
logger: null,
init() {
this.logger = new Logger('popup-notifier');
init(): void {
if (Launcher) {
window.open = noop;
window.open = () => null;
} else {
const windowOpen = window.open;
window.open = function (...args) {
@@ -21,24 +15,22 @@ const PopupNotifier = {
if (win) {
PopupNotifier.deferCheckClosed(win);
Events.emit('popup-opened', win);
} else {
if (!Alerts.alertDisplayed) {
Alerts.error({
header: Locale.authPopupRequired,
body: Locale.authPopupRequiredBody
});
}
} else if (!Alerts.alertDisplayed) {
Alerts.error({
header: Locale.authPopupRequired,
body: Locale.authPopupRequiredBody
});
}
return win;
};
}
},
deferCheckClosed(win) {
deferCheckClosed(win: Window): void {
setTimeout(PopupNotifier.checkClosed.bind(PopupNotifier, win), Timeouts.CheckWindowClosed);
},
checkClosed(win) {
checkClosed(win: Window): void {
if (win.closed) {
setTimeout(
PopupNotifier.triggerClosed.bind(PopupNotifier, win),
@@ -57,7 +49,7 @@ const PopupNotifier = {
}
},
tryGetLocationSearch(win) {
tryGetLocationSearch(win: Window): string | undefined {
try {
if (win.location.host === location.host) {
return win.location.search;
@@ -65,8 +57,8 @@ const PopupNotifier = {
} catch {}
},
triggerClosed(window, locationSearch) {
Events.emit('popup-closed', { window, locationSearch });
triggerClosed(window: Window, locationSearch?: string): void {
Events.emit('popup-closed', window, locationSearch);
}
};
-101
View File
@@ -1,101 +0,0 @@
import * as kdbxweb from 'kdbxweb';
const SecureInput = function () {
this.el = null;
this.minChar = 0x1400 + Math.round(Math.random() * 100);
this.maxLen = 1024;
this.length = 0;
this.pseudoValue = '';
this.salt = new Uint32Array(0);
};
SecureInput.prototype.setElement = function (el) {
this.el = el;
this.el.val(this.pseudoValue);
this.el.on('input', this._input.bind(this));
};
SecureInput.prototype.reset = function () {
this.el = null;
this.length = 0;
this.pseudoValue = '';
if (this.salt) {
for (let i = 0; i < this.salt.length; i++) {
this.salt[i] = 0;
}
}
this.salt = new Uint32Array(0);
};
SecureInput.prototype._input = function () {
const selStart = this.el[0].selectionStart;
const value = this.el.val();
let newPs = '';
const newSalt = new Uint32Array(this.maxLen);
let valIx = 0,
psIx = 0;
while (valIx < value.length) {
const valCh = value.charCodeAt(valIx);
const psCh = this.pseudoValue.charCodeAt(psIx);
const isSpecial = this._isSpecialChar(valCh);
if (psCh === valCh) {
// not changed
newPs += this._getChar(newPs.length);
newSalt[newPs.length - 1] = psCh ^ this.salt[psIx] ^ newPs.charCodeAt(newPs.length - 1);
psIx++;
valIx++;
} else if (isSpecial) {
// deleted
psIx++;
} else {
// inserted or replaced
newPs += this._getChar(newPs.length);
newSalt[newPs.length - 1] = newPs.charCodeAt(newPs.length - 1) ^ valCh;
valIx++;
}
}
this.length = newPs.length;
this.pseudoValue = newPs;
this.salt = newSalt;
this.el.val(newPs);
this.el[0].selectionStart = selStart;
this.el[0].selectionEnd = selStart;
};
SecureInput.prototype._getChar = function (ix) {
return String.fromCharCode(this.minChar + ix);
};
SecureInput.prototype._isSpecialChar = function (ch) {
return ch >= this.minChar && ch <= this.minChar + this.maxLen;
};
Object.defineProperty(SecureInput.prototype, 'value', {
enumerable: true,
get() {
const pseudoValue = this.pseudoValue;
const salt = this.salt;
const len = pseudoValue.length;
let byteLength = 0;
const valueBytes = new Uint8Array(len * 4);
const saltBytes = kdbxweb.CryptoEngine.random(len * 4);
let ch;
let bytes;
for (let i = 0; i < len; i++) {
const pseudoCharCode = pseudoValue.charCodeAt(i);
ch = String.fromCharCode(salt[i] ^ pseudoCharCode);
bytes = kdbxweb.ByteUtils.stringToBytes(ch);
for (let j = 0; j < bytes.length; j++) {
valueBytes[byteLength] = bytes[j] ^ saltBytes[byteLength];
byteLength++;
}
}
return new kdbxweb.ProtectedValue(
valueBytes.buffer.slice(0, byteLength),
saltBytes.buffer.slice(0, byteLength)
);
}
});
export { SecureInput };
@@ -1,36 +1,49 @@
import { Launcher } from 'comp/launcher';
import { Keys } from 'const/keys';
import { AppSettingsModel } from 'models/app-settings-model';
import { AppSettingsFieldName, AppSettings } from 'models/app-settings';
import { Features } from 'util/features';
import { StringFormat } from 'util/formatting/string-format';
import { Locale } from 'util/locale';
let allowedKeys;
let allowedKeys: Map<number, string>;
function getAllowedKeys() {
function getAllowedKeys(): Map<number, string> {
if (!allowedKeys) {
allowedKeys = {};
allowedKeys = new Map<number, string>();
for (const [name, code] of Object.entries(Keys)) {
if (typeof code === 'string') {
continue;
}
const keyName = name.replace('DOM_VK_', '');
if (/^([0-9A-Z]|F\d{1,2})$/.test(keyName)) {
allowedKeys[code] = keyName;
allowedKeys.set(code, keyName);
}
}
}
return allowedKeys;
}
const globalShortcuts = {
const AllGlobalShortcuts = {
copyPassword: { mac: 'Ctrl+Alt+C', all: 'Shift+Alt+C' },
copyUser: { mac: 'Ctrl+Alt+B', all: 'Shift+Alt+B' },
copyUrl: { mac: 'Ctrl+Alt+U', all: 'Shift+Alt+U' },
copyOtp: {},
copyOtp: undefined,
autoType: { mac: 'Ctrl+Alt+T', all: 'Shift+Alt+T' },
restoreApp: {}
restoreApp: undefined
};
export type GlobalShortcutType = keyof typeof AllGlobalShortcuts;
const GlobalShortcutAppSettingsFields: Record<GlobalShortcutType, AppSettingsFieldName> = {
copyPassword: 'globalShortcutCopyPassword',
copyUser: 'globalShortcutCopyUser',
copyUrl: 'globalShortcutCopyUrl',
copyOtp: 'globalShortcutCopyOtp',
autoType: 'globalShortcutAutoType',
restoreApp: 'globalShortcutRestoreApp'
};
const Shortcuts = {
keyEventToShortcut(event) {
keyEventToShortcut(event: KeyboardEvent): { valid: boolean; value: string } {
const modifiers = [];
if (event.ctrlKey) {
modifiers.push('Ctrl');
@@ -44,13 +57,14 @@ const Shortcuts = {
if (Features.isMac && event.metaKey) {
modifiers.push('Meta');
}
const keyName = getAllowedKeys()[event.which];
const keyName = getAllowedKeys().get(event.which);
return {
value: modifiers.join('+') + '+' + (keyName || '…'),
valid: modifiers.length > 0 && !!keyName
};
},
presentShortcut(shortcutValue, formatting) {
presentShortcut(shortcutValue: string | undefined, formatting?: boolean): string {
if (!shortcutValue) {
return '-';
}
@@ -72,30 +86,38 @@ const Shortcuts = {
})
.join('');
},
actionShortcutSymbol(formatting) {
actionShortcutSymbol(formatting?: boolean): string {
return Features.isMac ? '⌘' : this.formatShortcut(Locale.ctrlKey, formatting);
},
altShortcutSymbol(formatting) {
altShortcutSymbol(formatting?: boolean): string {
return Features.isMac ? '⌥' : this.formatShortcut(Locale.altKey, formatting);
},
shiftShortcutSymbol(formatting) {
shiftShortcutSymbol(formatting?: boolean): string {
return Features.isMac ? '⇧' : this.formatShortcut(Locale.shiftKey, formatting);
},
ctrlShortcutSymbol(formatting) {
ctrlShortcutSymbol(formatting?: boolean): string {
return Features.isMac ? '⌃' : this.formatShortcut(Locale.ctrlKey, formatting);
},
formatShortcut(shortcut, formatting) {
formatShortcut(shortcut: string, formatting?: boolean): string {
return formatting ? `${shortcut} + ` : `${shortcut}+`;
},
globalShortcutText(type, formatting) {
globalShortcutText(type: GlobalShortcutType, formatting?: boolean): string {
return this.presentShortcut(this.globalShortcut(type), formatting);
},
globalShortcut(type) {
const appSettingsShortcut = AppSettingsModel[this.globalShortcutAppSettingsKey(type)];
if (appSettingsShortcut) {
globalShortcut(type: GlobalShortcutType): string | undefined {
const setting = GlobalShortcutAppSettingsFields[type];
const appSettingsShortcut = AppSettings[setting];
if (typeof appSettingsShortcut === 'string') {
return appSettingsShortcut;
}
const globalShortcut = globalShortcuts[type];
const globalShortcut = AllGlobalShortcuts[type];
if (globalShortcut) {
if (Features.isMac && globalShortcut.mac) {
return globalShortcut.mac;
@@ -104,21 +126,21 @@ const Shortcuts = {
}
return undefined;
},
setGlobalShortcut(type, value) {
if (!globalShortcuts[type]) {
setGlobalShortcut(type: GlobalShortcutType, value: string | null): void {
if (!AllGlobalShortcuts[type]) {
throw new Error('Bad shortcut: ' + type);
}
const setting = GlobalShortcutAppSettingsFields[type];
if (value) {
AppSettingsModel[this.globalShortcutAppSettingsKey(type)] = value;
AppSettings.set(setting, value);
} else {
delete AppSettingsModel[this.globalShortcutAppSettingsKey(type)];
AppSettings.delete(setting);
}
Launcher.setGlobalShortcuts(AppSettingsModel);
Launcher?.ipcRenderer.invoke('set-global-shortcuts', { [setting]: value });
},
globalShortcutAppSettingsKey(type) {
return 'globalShortcut' + StringFormat.capFirst(type);
},
screenshotToClipboardShortcut() {
screenshotToClipboardShortcut(): string {
if (Features.isiOS) {
return 'Sleep+Home';
}
@@ -1,4 +1,4 @@
import { Events } from 'framework/events';
import { Events } from 'util/events';
import { Launcher } from 'comp/launcher';
const LocalStorageKeyName = 'instanceCheck';
@@ -7,15 +7,16 @@ const LocalStorageResponseKeyName = 'instanceMaster';
const instanceKey = Date.now().toString();
const SingleInstanceChecker = {
init() {
init(): void {
if (Launcher) {
return;
}
// eslint-disable-next-line @typescript-eslint/unbound-method
window.addEventListener('storage', SingleInstanceChecker.storageChanged);
SingleInstanceChecker.setKey(LocalStorageKeyName, instanceKey);
},
storageChanged(e) {
storageChanged(e: StorageEvent): void {
if (!e.newValue) {
return;
}
@@ -25,12 +26,13 @@ const SingleInstanceChecker = {
instanceKey + Math.random().toString()
);
} else if (e.key === LocalStorageResponseKeyName && e.newValue.indexOf(instanceKey) < 0) {
// eslint-disable-next-line @typescript-eslint/unbound-method
window.removeEventListener('storage', SingleInstanceChecker.storageChanged);
Events.emit('second-instance');
}
},
setKey(key, value) {
setKey(key: string, value: string): void {
try {
localStorage.setItem(key, value);
setTimeout(() => {
@@ -1,18 +1,18 @@
import { Events } from 'framework/events';
import { Events } from 'util/events';
const ThemeWatcher = {
dark: false,
init() {
init(): void {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
if (mediaQuery && mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', (e) => {
const dark = e.matches;
this.dark = dark;
Events.emit('dark-mode-changed', { dark });
Events.emit('dark-mode-changed', dark);
});
}
this.dark = !!mediaQuery.matches;
this.dark = mediaQuery.matches;
}
};
-118
View File
@@ -1,118 +0,0 @@
import { Launcher } from 'comp/launcher';
import { Logger } from 'util/logger';
import { noop } from 'util/fn';
import { StringFormat } from 'util/formatting/string-format';
const logger = new Logger('transport');
const Transport = {
cacheFilePath(fileName) {
return Launcher.getTempPath(fileName);
},
httpGet(config) {
let tmpFile;
const fs = Launcher.req('fs');
if (config.file) {
const baseTempPath = Launcher.getTempPath();
if (config.cleanupOldFiles) {
const allFiles = fs.readdirSync(baseTempPath);
for (const file of allFiles) {
if (
file !== config.file &&
StringFormat.replaceVersion(file, '0') ===
StringFormat.replaceVersion(config.file, '0')
) {
fs.unlinkSync(Launcher.joinPath(baseTempPath, file));
}
}
}
tmpFile = Launcher.joinPath(baseTempPath, config.file);
if (fs.existsSync(tmpFile)) {
try {
if (config.cache && fs.statSync(tmpFile).size > 0) {
logger.info('File already downloaded ' + config.url);
return config.success(tmpFile);
} else {
fs.unlinkSync(tmpFile);
}
} catch (e) {
fs.unlink(tmpFile, noop);
}
}
}
const proto = config.url.split(':')[0];
logger.info('GET ' + config.url);
const opts = Launcher.req('url').parse(config.url);
opts.headers = { 'User-Agent': navigator.userAgent };
Launcher.resolveProxy(config.url, (proxy) => {
logger.info(
'Request to ' +
config.url +
' ' +
(proxy ? 'using proxy ' + proxy.host + ':' + proxy.port : 'without proxy')
);
if (proxy) {
opts.headers.Host = opts.host;
opts.host = proxy.host;
opts.port = proxy.port;
opts.path = config.url;
}
Launcher.req(proto)
.get(opts, (res) => {
logger.info('Response from ' + config.url + ': ' + res.statusCode);
if (res.statusCode === 200) {
if (config.file) {
const file = fs.createWriteStream(tmpFile);
res.pipe(file);
file.on('finish', () => {
file.close(() => {
config.success(tmpFile);
});
});
file.on('error', (err) => {
config.error(err);
});
} else {
let data = [];
res.on('data', (chunk) => {
data.push(chunk);
});
res.on('end', () => {
data = window.Buffer.concat(data);
if (config.text || config.json) {
data = data.toString('utf8');
}
if (config.json) {
try {
data = JSON.parse(data);
} catch (e) {
config.error('Error parsing JSON: ' + e.message);
}
}
config.success(data);
});
}
} else if (res.headers.location && [301, 302].indexOf(res.statusCode) >= 0) {
if (config.noRedirect) {
return config.error('Too many redirects');
}
config.url = res.headers.location;
config.noRedirect = true;
Transport.httpGet(config);
} else {
config.error('HTTP status ' + res.statusCode);
}
})
.on('error', (e) => {
logger.error('Cannot GET ' + config.url, e);
if (tmpFile) {
fs.unlink(tmpFile, noop);
}
config.error(e);
});
});
}
};
export { Transport };
+42
View File
@@ -0,0 +1,42 @@
import { Features } from 'util/features';
import { AppSettings } from 'models/app-settings';
import { Events } from 'util/events';
export const WindowClass = {
init(): void {
const browserCssClass = Features.browserCssClass;
if (browserCssClass) {
document.body.classList.add(browserCssClass);
}
if (AppSettings.titlebarStyle !== 'default') {
document.body.classList.add(`titlebar-${AppSettings.titlebarStyle}`);
if (Features.renderCustomTitleBar) {
document.body.classList.add('titlebar-custom');
}
}
if (Features.isMobile) {
document.body.classList.add('mobile');
}
Events.on('enter-full-screen', () => document.body.classList.add('fullscreen'));
Events.on('leave-full-screen', () => document.body.classList.remove('fullscreen'));
},
setThemeClass(theme: string): void {
for (const cls of document.body.classList) {
if (cls.startsWith('th-')) {
document.body.classList.remove(cls);
}
}
document.body.classList.add(`th-${theme}`);
},
setFontSizeClass(fontSize: number): void {
for (const cls of document.body.classList) {
if (cls.startsWith('font-size-')) {
document.body.classList.remove(cls);
}
}
document.body.classList.add(`font-size-${fontSize}`);
}
};
@@ -0,0 +1,80 @@
import { ContextMenu, ContextMenuItem } from 'models/menu/context-menu';
import { StringFormat } from 'util/formatting/string-format';
import { Locale } from 'util/locale';
import { Features } from 'util/features';
import { Shortcuts } from 'comp/browser/shortcuts';
import { Position } from 'util/types';
import { FileManager } from 'models/file-manager';
import { Entry } from 'models/entry';
import { File } from 'models/file';
export class ContextMenuNew {
static show(pos: Position): void {
const newEntryMenuItem = new ContextMenuItem(
'entry',
'key',
StringFormat.capFirst(Locale.entry),
() => this.newEntryClicked()
);
if (!Features.isMobile) {
const shortcut = Shortcuts.presentShortcut('Alt+N');
newEntryMenuItem.hint = `(${Locale.searchShiftClickOr} ${shortcut})`;
}
const newGroupMenuItem = new ContextMenuItem(
'group',
'folder',
StringFormat.capFirst(Locale.group),
() => this.newGroupClicked()
);
const templateMenuItems: ContextMenuItem[] = [];
const hasMultipleFiles = FileManager.files.length > 1;
for (const file of FileManager.files) {
for (const entry of file.allEntryTemplates()) {
if (!entry.title) {
continue;
}
const id = `tmpl:${entry.id}`;
templateMenuItems.push(
new ContextMenuItem(
id,
entry.icon ?? 'sticky-note-o',
hasMultipleFiles ? `${file.name} / ${entry.title}` : entry.title,
() => this.newFromTemplateClicked(entry, file)
)
);
}
}
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
templateMenuItems.sort((x, y) => {
return collator.compare(x.title, y.title);
});
templateMenuItems.push(
new ContextMenuItem(
'tmpl',
'sticky-note-o',
StringFormat.capFirst(Locale.template),
() => this.newTemplateClicked()
)
);
ContextMenu.toggle('new', pos, [newEntryMenuItem, newGroupMenuItem, ...templateMenuItems]);
}
static newEntryClicked(): void {
// TODO
}
static newGroupClicked(): void {
// TODO
}
static newTemplateClicked(): void {
// TODO
}
static newFromTemplateClicked(entry: Entry, file: File): void {
// TODO
}
}
@@ -0,0 +1,92 @@
import { ContextMenu, ContextMenuItem } from 'models/menu/context-menu';
import { Position } from 'util/types';
import { StringFormat } from 'util/formatting/string-format';
import { Locale } from 'util/locale';
import { QuerySort } from 'models/query';
import { Workspace } from 'models/workspace';
const arrow = '→';
export class ContextMenuSort {
static show(pos: Position): void {
const items: ContextMenuItem[] = [
new ContextMenuItem(
QuerySort.TitleAsc,
'sort-alpha-down',
StringFormat.capFirst(Locale.title) + ' ' + Locale.searchAZ.with(arrow),
() => this.itemClicked(QuerySort.TitleAsc)
),
new ContextMenuItem(
QuerySort.TitleDesc,
'sort-alpha-down-alt',
StringFormat.capFirst(Locale.title) + ' ' + Locale.searchZA.with(arrow),
() => this.itemClicked(QuerySort.TitleDesc)
),
new ContextMenuItem(
QuerySort.WebsiteAsc,
'sort-alpha-down',
StringFormat.capFirst(Locale.website) + ' ' + Locale.searchAZ.with(arrow),
() => this.itemClicked(QuerySort.WebsiteAsc)
),
new ContextMenuItem(
QuerySort.WebsiteDesc,
'sort-alpha-down-alt',
StringFormat.capFirst(Locale.website) + ' ' + Locale.searchZA.with(arrow),
() => this.itemClicked(QuerySort.WebsiteDesc)
),
new ContextMenuItem(
QuerySort.UserAsc,
'sort-alpha-down',
StringFormat.capFirst(Locale.user) + ' ' + Locale.searchAZ.with(arrow),
() => this.itemClicked(QuerySort.UserAsc)
),
new ContextMenuItem(
QuerySort.UserDesc,
'sort-alpha-down-alt',
StringFormat.capFirst(Locale.user) + ' ' + Locale.searchZA.with(arrow),
() => this.itemClicked(QuerySort.UserDesc)
),
new ContextMenuItem(
QuerySort.CreatedAsc,
'sort-numeric-down',
StringFormat.capFirst(Locale.searchCreated) + ' ' + Locale.searchON.with(arrow),
() => this.itemClicked(QuerySort.CreatedAsc)
),
new ContextMenuItem(
QuerySort.CreatedDesc,
'sort-numeric-down-alt',
StringFormat.capFirst(Locale.searchCreated) + ' ' + Locale.searchNO.with(arrow),
() => this.itemClicked(QuerySort.CreatedDesc)
),
new ContextMenuItem(
QuerySort.UpdatedAsc,
'sort-numeric-down',
StringFormat.capFirst(Locale.searchUpdated) + ' ' + Locale.searchON.with(arrow),
() => this.itemClicked(QuerySort.UpdatedAsc)
),
new ContextMenuItem(
QuerySort.UpdatedDesc,
'sort-numeric-down-alt',
StringFormat.capFirst(Locale.searchUpdated) + ' ' + Locale.searchNO.with(arrow),
() => this.itemClicked(QuerySort.UpdatedDesc)
),
new ContextMenuItem(
QuerySort.AttachmentsDesc,
'sort-amount-down',
Locale.searchAttachments,
() => this.itemClicked(QuerySort.AttachmentsDesc)
),
new ContextMenuItem(QuerySort.RankDesc, 'sort-amount-down', Locale.searchRank, () =>
this.itemClicked(QuerySort.RankDesc)
)
];
const selectedItem = items.find((it) => it.id === Workspace.query.sort);
ContextMenu.toggle('sort', pos, items, selectedItem);
}
private static itemClicked(sort: QuerySort) {
Workspace.query.sort = sort;
}
}
@@ -29,12 +29,12 @@ const ChalRespCalculator = {
const cacheKey = this.getCacheKey(params);
const respFromCache = this.cache[cacheKey]?.[hexChallenge];
if (respFromCache) {
logger.debug('Found ChalResp in cache');
logger.info('Found ChalResp in cache');
return resolve(Buffer.from(respFromCache, 'hex'));
}
if (!AppSettingsModel.enableUsb) {
logger.debug('USB is disabled');
logger.info('USB is disabled');
Alerts.error({
header: Locale.yubiKeyDisabledErrorHeader,
body: Locale.yubiKeyDisabledErrorBody,
@@ -49,7 +49,7 @@ const ChalRespCalculator = {
return;
}
logger.debug('Calculating ChalResp using a YubiKey', params);
logger.info('Calculating ChalResp using a YubiKey', params);
this._calc(params, challenge, (err, response) => {
if (err) {
@@ -1,8 +1,9 @@
import { Events } from 'framework/events';
import { Events } from 'util/events';
import { Logger } from 'util/logger';
import { NativeModules } from 'comp/launcher/native-modules';
import { AppSettingsModel } from 'models/app-settings-model';
import { AppSettings } from 'models/app-settings';
import { Features } from 'util/features';
import { noop } from 'util/fn';
const logger = new Logger('usb-listener');
@@ -10,51 +11,47 @@ const UsbListener = {
supported: Features.isDesktop,
attachedYubiKeys: 0,
init() {
init(): void {
if (!this.supported) {
return;
}
Events.on('native-modules-yubikeys', (e) => {
if (e.numYubiKeys !== this.attachedYubiKeys) {
logger.debug(`YubiKeys changed ${this.attachedYubiKeys} => ${e.numYubiKeys}`);
this.attachedYubiKeys = e.numYubiKeys;
Events.on('native-modules-yubikeys', (numYubiKeys) => {
if (numYubiKeys !== this.attachedYubiKeys) {
logger.info(`YubiKeys changed ${this.attachedYubiKeys} => ${numYubiKeys}`);
this.attachedYubiKeys = numYubiKeys;
Events.emit('usb-devices-changed');
}
});
AppSettingsModel.on('change:enableUsb', (model, enabled) => {
AppSettings.onChange('enableUsb', (enabled) => {
if (enabled) {
this.start();
this.start().catch(noop);
} else {
this.stop();
this.stop().catch(noop);
}
});
if (AppSettingsModel.enableUsb) {
this.start();
if (AppSettings.enableUsb) {
this.start().catch(noop);
}
},
start() {
async start(): Promise<void> {
logger.info('Starting USB listener');
if (this.usb) {
this.stop();
}
try {
NativeModules.startUsbListener();
await NativeModules.startUsbListener();
} catch (e) {
logger.error('Error starting USB listener', e);
}
},
stop() {
async stop(): Promise<void> {
logger.info('Stopping USB listener');
try {
NativeModules.stopUsbListener();
await NativeModules.stopUsbListener();
} catch (e) {
logger.error('Error stopping USB listener', e);
}
+323
View File
@@ -0,0 +1,323 @@
import { Events } from 'util/events';
import { Launcher } from 'comp/launcher';
import { NativeModules } from 'comp/launcher/native-modules';
import { Logger } from 'util/logger';
import { UsbListener } from 'comp/devices/usb-listener';
import { AppSettings } from 'models/app-settings';
import { Timeouts } from 'const/timeouts';
import { YubiKeyProductIds, YubiKeyChallengeSize } from 'const/hardware';
import { Locale } from 'util/locale';
import * as fs from 'fs';
import { YubiKeyInfo } from 'comp/launcher/desktop-ipc';
import { FileChalRespConfig } from 'models/file-info';
const logger = new Logger('yubikey');
let cmd: string | undefined;
type ToolStatus = 'unknown' | 'ok' | 'checking' | 'error';
const YkmanProcessName = 'ykman';
export interface YubiKeyInfoWithName extends YubiKeyInfo {
fullName: string;
}
export interface ShortYubiKeyInfo {
fullName: string;
serial: string;
}
export interface YubiKeyOtpCodeInfo {
title: string;
user: string;
needsTouch: boolean;
}
const YubiKey = {
ykmanStatus: 'unknown' as ToolStatus,
processIsRunning: false,
aborted: false,
async cmd(): Promise<string> {
if (cmd) {
return cmd;
}
const macYkmanPath = '/usr/local/bin/ykman';
if (process.platform === 'darwin') {
try {
await fs.promises.access(macYkmanPath);
cmd = macYkmanPath;
return cmd;
} catch (err) {}
}
cmd = 'ykman';
return cmd;
},
async checkToolStatus(): Promise<ToolStatus> {
if (this.ykmanStatus === 'ok') {
return Promise.resolve(this.ykmanStatus);
}
this.ykmanStatus = 'checking';
cmd = undefined;
try {
await Launcher?.spawn({
cmd: await this.cmd(),
args: ['-v'],
noStdOutLogging: true
});
this.ykmanStatus = 'ok';
} catch {
this.ykmanStatus = 'error';
}
return this.ykmanStatus;
},
abort(): void {
logger.info('Aborting');
if (this.processIsRunning) {
logger.info('Killing the process');
try {
Launcher?.killProcess(YkmanProcessName);
} catch {}
}
this.aborted = true;
this.processIsRunning = false;
},
async list(): Promise<YubiKeyInfoWithName[]> {
const yubiKeys = await NativeModules.getYubiKeys();
return yubiKeys.map((yk) => ({
...yk,
fullName: this.getKeyFullName(yk)
}));
},
getKeyFullName(yk: YubiKeyInfo): string {
let name = 'YubiKey';
if (YubiKeyProductIds.Gen1.includes(yk.pid)) {
name += ' Gen 1';
} else if (YubiKeyProductIds.NEO.includes(yk.pid)) {
name += ' NEO';
} else if (YubiKeyProductIds.YK4.includes(yk.pid)) {
if (yk.version >= '5.1.0') {
name += ' 5';
}
}
return `${name} ${yk.serial}`;
},
listWithYkman(): Promise<ShortYubiKeyInfo[]> {
return this.listWithYkmanInternal(true);
},
async listWithYkmanInternal(canRetry: boolean): Promise<ShortYubiKeyInfo[]> {
if (this.processIsRunning) {
throw new Error('Already in progress');
}
this.aborted = false;
logger.info('Listing YubiKeys');
if (!UsbListener.attachedYubiKeys) {
return Promise.resolve([]);
}
this.processIsRunning = true;
let stdout;
try {
const res = await Launcher?.spawn({
cmd: await this.cmd(),
args: ['list'],
noStdOutLogging: true,
name: YkmanProcessName
});
stdout = res?.stdout ?? '';
} catch (err) {
if (this.aborted) {
throw new Error('Aborted');
}
throw err;
} finally {
this.processIsRunning = false;
}
const yubiKeysIncludingEmpty: ShortYubiKeyInfo[] = stdout
.trim()
.split(/\n/g)
.map((line) => {
const fullName = line;
const serial = (line.match(/\d{5,}$/g) || [])[0];
return { fullName, serial };
});
const yubiKeys = yubiKeysIncludingEmpty.filter((s) => s.serial);
if (
yubiKeysIncludingEmpty.length === 1 &&
yubiKeys.length === 0 &&
stdout.startsWith('YubiKey') &&
stdout.includes('CCID') &&
!stdout.includes('Serial')
) {
logger.info('The YubiKey is probably stuck');
if (!AppSettings.yubiKeyStuckWorkaround) {
throw new Error(Locale.yubiKeyStuckError);
}
if (canRetry) {
return this.repairStuckYubiKey();
}
}
if (!yubiKeys.length) {
throw new Error('No YubiKeys returned by "ykman list"');
}
return yubiKeys;
},
async repairStuckYubiKey(): Promise<ShortYubiKeyInfo[]> {
logger.info('Repairing a stuck YubiKey');
let result: Promise<ShortYubiKeyInfo[]> | undefined;
let openTimeout = 0;
const countYubiKeys = UsbListener.attachedYubiKeys;
const onDevicesChangedDuringRepair = () => {
if (UsbListener.attachedYubiKeys === countYubiKeys) {
logger.info('YubiKey was reconnected');
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
clearTimeout(openTimeout);
this.aborted = false;
result = new Promise<ShortYubiKeyInfo[]>((resolve) => {
setTimeout(() => {
resolve(this.listWithYkmanInternal(false));
}, Timeouts.ExternalDeviceAfterReconnect);
});
}
};
Events.on('usb-devices-changed', onDevicesChangedDuringRepair);
try {
await Launcher?.spawn({
cmd: await this.cmd(),
args: ['config', 'usb', '-e', 'oath', '-f'],
noStdOutLogging: true
});
} catch (err) {
logger.info('Repair complete with error', err);
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
throw new Error('YubiKey repair error');
}
logger.info('Repair complete OK');
openTimeout = window.setTimeout(() => {
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
}, Timeouts.ExternalDeviceReconnect);
if (!result) {
throw new Error('YubiKeys not changed during repair');
}
return result;
},
async getOtpCodes(serial: number): Promise<YubiKeyOtpCodeInfo[]> {
if (this.processIsRunning) {
throw new Error('Already in progress');
}
this.aborted = false;
let stdout: string;
this.processIsRunning = true;
try {
const res = await Launcher?.spawn({
cmd: await this.cmd(),
args: ['-d', String(serial), 'oath', 'accounts', 'code'],
noStdOutLogging: true,
throwOnStdErr: true
});
stdout = res?.stdout ?? '';
} catch (err) {
if (this.aborted) {
throw new Error('Aborted');
}
throw err;
} finally {
this.processIsRunning = false;
}
const codes: YubiKeyOtpCodeInfo[] = [];
for (const line of stdout.split('\n')) {
const match = /^(.*?):(.*?)\s+(.*)$/.exec(line);
if (!match) {
continue;
}
const [, title, user, code] = match;
const needsTouch = !/^\d+$/.exec(code);
codes.push({
title,
user,
needsTouch
});
}
return codes;
},
async getOtp(serial: number, entry: string): Promise<string> {
const res = await Launcher?.spawn({
cmd: await this.cmd(),
args: ['-d', String(serial), 'oath', 'accounts', 'code', '--single', entry],
noStdOutLogging: true
});
const code = res?.stdout?.trim();
if (!code) {
throw new Error('Nothing returned');
}
return code;
},
calculateChalResp(chalResp: FileChalRespConfig, challenge: ArrayBuffer): Promise<ArrayBuffer> {
// const { vid, pid, serial, slot } = chalResp;
// const yubiKey = { vid, pid, serial };
const challengeBuffer = Buffer.from(challenge);
// https://github.com/Yubico/yubikey-personalization-gui/issues/86
// https://github.com/keepassxreboot/keepassxc/blob/develop/src/keys/drivers/YubiKey.cpp#L318
const padLen = YubiKeyChallengeSize - challenge.byteLength;
const paddedChallenge = Buffer.alloc(YubiKeyChallengeSize, padLen);
challengeBuffer.copy(paddedChallenge);
throw new Error('Not implemented'); // TODO: chal-resp
// NativeModules.yubiKeyChallengeResponse(
// yubiKey,
// [...paddedChallenge],
// slot,
// (err, result) => {
// if (result) {
// result = Buffer.from(result);
// }
// if (err) {
// err.ykError = true;
// }
// return callback(err, result);
// }
// );
},
async cancelChalResp(): Promise<void> {
await NativeModules.yubiKeyCancelChallengeResponse();
}
};
export { YubiKey };
-119
View File
@@ -1,119 +0,0 @@
/* eslint-disable import/no-commonjs */
import * as kdbxweb from 'kdbxweb';
import { RuntimeInfo } from 'const/runtime-info';
import { Links } from 'const/links';
import { DateFormat } from 'comp/i18n/date-format';
import { StringFormat } from 'util/formatting/string-format';
import { Locale } from 'util/locale';
const Templates = {
db: require('templates/export/db.hbs'),
entry: require('templates/export/entry.hbs')
};
const FieldMapping = [
{ name: 'UserName', locStr: 'user' },
{ name: 'Password', locStr: 'password', protect: true },
{ name: 'URL', locStr: 'website' },
{ name: 'Notes', locStr: 'notes' }
];
const KnownFields = { 'Title': true };
for (const { name } of FieldMapping) {
KnownFields[name] = true;
}
function walkGroup(db, group, parents) {
parents = [...parents, group];
if (
group.uuid.equals(db.meta.recycleBinUuid) ||
group.uuid.equals(db.meta.entryTemplatesGroup)
) {
return '';
}
const self = group.entries.map((entry) => walkEntry(db, entry, parents)).join('\n');
const children = group.groups
.map((childGroup) => walkGroup(db, childGroup, parents))
.join('\n');
return self + children;
}
function walkEntry(db, entry, parents) {
const path = parents.map((group) => group.name).join(' / ');
const fields = [];
for (const field of FieldMapping) {
const value = entryField(entry, field.name);
if (value) {
fields.push({
title: StringFormat.capFirst(Locale[field.locStr]),
value,
protect: field.protect
});
}
}
for (const [fieldName, fieldValue] of entry.fields) {
if (!KnownFields[fieldName]) {
const value = entryField(entry, fieldName);
if (value) {
fields.push({
title: fieldName,
value,
protect: fieldValue.isProtected
});
}
}
}
const title = entryField(entry, 'Title');
let expires;
if (entry.times.expires && entry.times.expiryTime) {
expires = DateFormat.dtStr(entry.times.expiryTime);
}
const attachments = [...entry.binaries]
.map(([name, data]) => {
if (data && data.ref) {
data = data.value;
}
if (data) {
const base64 = kdbxweb.ByteUtils.bytesToBase64(data);
data = 'data:application/octet-stream;base64,' + base64;
}
return { name, data };
})
.filter((att) => att.name && att.data);
return Templates.entry({
path,
title,
fields,
tags: entry.tags.join(', '),
created: DateFormat.dtStr(entry.times.creationTime),
modified: DateFormat.dtStr(entry.times.lastModTime),
expires,
attachments
});
}
function entryField(entry, fieldName) {
const value = entry.fields.get(fieldName);
return (value && value.isProtected && value.getText()) || value || '';
}
const KdbxToHtml = {
convert(db, options) {
const content = db.groups.map((group) => walkGroup(db, group, [])).join('\n');
return Templates.db({
name: options.name,
date: DateFormat.dtStr(Date.now()),
appLink: Links.Homepage,
appVersion: RuntimeInfo.version,
contentHtml: content
});
},
entryToHtml(db, entry) {
return walkEntry(db, entry, []);
}
};
export { KdbxToHtml };
+136
View File
@@ -0,0 +1,136 @@
import { h } from 'preact';
import * as kdbxweb from 'kdbxweb';
import { RuntimeInfo } from 'const/runtime-info';
import { Links } from 'const/links';
import { DateFormat } from 'util/formatting/date-format';
import { StringFormat } from 'util/formatting/string-format';
import { Locale } from 'util/locale';
import { HtmlRenderer } from 'util/browser/html-renderer';
import { ExportFile } from 'views/standalone/export-file';
import { ExportEntry, ExportEntryParameters } from 'views/standalone/export-entry';
const FieldMapping = [
{ name: 'UserName', loc: () => Locale.user },
{ name: 'Password', loc: () => Locale.password, protect: true },
{ name: 'URL', loc: () => Locale.website },
{ name: 'Notes', loc: () => Locale.notes }
];
const KnownFields = new Set('Title');
for (const { name } of FieldMapping) {
KnownFields.add(name);
}
function convertEntry(db: kdbxweb.Kdbx, entry: kdbxweb.KdbxEntry): ExportEntryParameters {
const path = getParents(entry).join(' / ');
const fields = [];
for (const field of FieldMapping) {
const value = entryField(entry, field.name);
if (value) {
fields.push({
title: StringFormat.capFirst(field.loc()),
value,
protect: field.protect
});
}
}
for (const [fieldName, fieldValue] of entry.fields) {
if (!KnownFields.has(fieldName)) {
const value = entryField(entry, fieldName);
if (value) {
fields.push({
title: fieldName,
value,
protect: fieldValue instanceof kdbxweb.ProtectedValue
});
}
}
}
const title = entryField(entry, 'Title');
let expires;
if (entry.times.expires && entry.times.expiryTime) {
expires = DateFormat.dtStr(entry.times.expiryTime);
}
const attachments = [...entry.binaries]
.map(([name, data]) => {
let value;
if (kdbxweb.KdbxBinaries.isKdbxBinaryWithHash(data)) {
value = data.value;
} else {
value = data;
}
if (value instanceof kdbxweb.ProtectedValue) {
value = value.getBinary();
}
let dataHref = '';
if (value) {
const base64 = kdbxweb.ByteUtils.bytesToBase64(value);
dataHref = 'data:application/octet-stream;base64,' + base64;
}
return { name, dataHref };
})
.filter((att) => att.name && att.dataHref);
return {
id: entry.uuid.id,
path,
title,
fields,
tags: entry.tags,
created: entry.times.creationTime ? DateFormat.dtStr(entry.times.creationTime) : '',
modified: entry.times.lastModTime ? DateFormat.dtStr(entry.times.lastModTime) : '',
expires,
attachments
};
}
function entryField(entry: kdbxweb.KdbxEntry, fieldName: string): string {
const value = entry.fields.get(fieldName);
if (value instanceof kdbxweb.ProtectedValue) {
return value.getText();
}
return value || '';
}
function getParents(entry: kdbxweb.KdbxEntry): string[] {
const parents = [];
const group = entry.parentGroup;
while (group?.name) {
parents.push(group.name);
}
return parents;
}
const KdbxToHtml = {
convert(db: kdbxweb.Kdbx, name: string): string {
const entries: ExportEntryParameters[] = [];
for (const group of db.groups) {
if (
group.uuid.equals(db.meta.recycleBinUuid) ||
group.uuid.equals(db.meta.entryTemplatesGroup)
) {
continue;
}
for (const entry of group.allEntries()) {
entries.push(convertEntry(db, entry));
}
}
return HtmlRenderer.renderToHtml(
h(ExportFile, {
name,
date: DateFormat.dtStr(Date.now()),
appVersion: RuntimeInfo.version,
appLink: Links.Homepage,
entries
})
);
},
entryToHtml(db: kdbxweb.Kdbx, entry: kdbxweb.KdbxEntry): string {
const params = convertEntry(db, entry);
return HtmlRenderer.renderToHtml(h(ExportEntry, params));
}
};
export { KdbxToHtml };
+308
View File
@@ -0,0 +1,308 @@
import * as kdbxweb from 'kdbxweb';
import { Logger } from 'util/logger';
import { Features } from 'util/features';
import { NativeModules } from 'comp/launcher/native-modules';
const logger = new Logger('argon2');
interface Argon2WasmModule {
ALLOC_NORMAL: 'ALLOC_NORMAL';
HEAP8: Uint8Array;
// eslint-disable-next-line @typescript-eslint/naming-convention
_free(ptr: number): void;
allocate(buf: ArrayLike<number>, type: 'i8', flags: 'ALLOC_NORMAL'): number;
// eslint-disable-next-line @typescript-eslint/naming-convention,camelcase
_argon2_hash(
iterations: number,
memory: number,
parallelism: number,
password: number,
passwordLen: number,
salt: number,
saltLen: number,
hash: number,
length: number,
encoded: number,
encodedLen: number,
type: number,
version: number
): number;
}
interface Argon2HashArgs {
password: ArrayBuffer;
salt: ArrayBuffer;
memory: number;
iterations: number;
length: number;
parallelism: number;
type: kdbxweb.CryptoEngine.Argon2Type;
version: kdbxweb.CryptoEngine.Argon2Version;
}
interface Argon2Runtime {
hash(args: Argon2HashArgs): Promise<Uint8Array>;
}
let runtimeModule: Argon2Runtime | undefined;
interface MessageFromWorkerWorkerLog {
op: 'log';
args: unknown[];
}
interface MessageFromWorkerWorkerPostRun {
op: 'postRun';
}
interface MessageFromWorkerWorkerHash {
op: 'hash';
hash: Uint8Array;
}
interface MessageFromWorkerWorkerError {
op: 'error';
error: string;
}
type MessageFromWorkerWorker =
| MessageFromWorkerWorkerLog
| MessageFromWorkerWorkerPostRun
| MessageFromWorkerWorkerHash
| MessageFromWorkerWorkerError;
const KdbxwebInit = {
init(): void {
kdbxweb.CryptoEngine.setArgon2Impl((...args) => this.argon2(...args));
},
async argon2(
password: ArrayBuffer,
salt: ArrayBuffer,
memory: number,
iterations: number,
length: number,
parallelism: number,
type: kdbxweb.CryptoEngine.Argon2Type,
version: kdbxweb.CryptoEngine.Argon2Version
): Promise<ArrayBuffer> {
const runtime = await this.loadRuntime(memory);
const ts = logger.ts();
const hash = await runtime.hash({
password,
salt,
memory,
iterations,
length,
parallelism,
type,
version
});
logger.info('Hash computed', logger.ts(ts));
return hash;
},
loadRuntime(requiredMemory: number): Promise<Argon2Runtime> {
if (runtimeModule) {
return Promise.resolve(runtimeModule);
}
if (!global.WebAssembly) {
return Promise.reject(new Error('WebAssembly is not supported'));
}
if (Features.isDesktop) {
logger.info('Using native argon2');
runtimeModule = {
hash(args) {
const ts = logger.ts();
const password = kdbxweb.ProtectedValue.fromBinary(args.password).dataAndSalt();
const salt = kdbxweb.ProtectedValue.fromBinary(args.salt).dataAndSalt();
return NativeModules.argon2(password, salt, {
type: args.type,
version: args.version,
hashLength: args.length,
timeCost: args.iterations,
parallelism: args.parallelism,
memoryCost: args.memory
})
.then((res) => {
password.data.fill(0);
salt.data.fill(0);
logger.info('Argon2 hash calculated', logger.ts(ts));
const pv = new kdbxweb.ProtectedValue(
kdbxweb.ByteUtils.arrayToBuffer(new Uint8Array(res.data)),
kdbxweb.ByteUtils.arrayToBuffer(new Uint8Array(res.salt))
);
return pv.getBinary();
})
.catch((err) => {
password.data.fill(0);
salt.data.fill(0);
logger.error('Argon2 error', err);
throw err;
});
}
};
return Promise.resolve(runtimeModule);
}
return new Promise<Argon2Runtime>((resolve, reject) => {
const loadTimeout = setTimeout(() => reject(new Error('Timeout')), 5000);
try {
const ts = logger.ts();
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const argon2LoaderCode = require('argon2').default as string;
const wasmBinaryBase64 = require('argon2-wasm') as string;
const KB = 1024 * 1024;
const MB = 1024 * KB;
const GB = 1024 * MB;
const WASM_PAGE_SIZE = 64 * 1024;
const totalMemory = (2 * GB - 64 * KB) / 1024 / WASM_PAGE_SIZE;
const initialMemory = Math.min(
Math.max(Math.ceil((requiredMemory * 1024) / WASM_PAGE_SIZE), 256) + 256,
totalMemory
);
const memoryDecl = `var wasmMemory=new WebAssembly.Memory({initial:${initialMemory},maximum:${totalMemory}});`;
const moduleDecl =
'var Module={' +
'wasmJSMethod: "native-wasm",' +
`wasmBinary: Uint8Array.from(atob("${wasmBinaryBase64}"), c => c.charCodeAt(0)),` +
'print(...args) { postMessage({op:"log",args}) },' +
'printErr(...args) { postMessage({op:"log",args}) },' +
`postRun:${this.workerPostRun.toString()},` +
`calcHash:${this.calcHash.toString()},` +
'wasmMemory:wasmMemory,' +
'buffer:wasmMemory.buffer,' +
`TOTAL_MEMORY:${initialMemory * WASM_PAGE_SIZE}` +
'}';
const script = argon2LoaderCode.replace(/^var Module.*?}/, memoryDecl + moduleDecl);
const blob = new Blob([script], { type: 'application/javascript' });
const objectUrl = URL.createObjectURL(blob);
const worker = new Worker(objectUrl);
const onMessage = (e: MessageEvent) => {
const data = e.data as MessageFromWorkerWorker;
switch (data.op) {
case 'log':
logger.info(...data.args);
break;
case 'postRun':
logger.info('WebAssembly runtime loaded (web worker)', logger.ts(ts));
URL.revokeObjectURL(objectUrl);
clearTimeout(loadTimeout);
worker.removeEventListener('message', onMessage);
runtimeModule = {
hash(args: Argon2HashArgs): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
worker.postMessage(args);
const onHashMessage = (e: MessageEvent) => {
worker.removeEventListener('message', onHashMessage);
worker.terminate();
runtimeModule = undefined;
const msg = e.data as MessageFromWorkerWorker;
switch (msg.op) {
case 'hash':
resolve(msg.hash);
break;
case 'error':
logger.error('Worker error', msg.error);
reject(new Error(msg.error));
break;
default:
reject(
new Error('Unexpected message from worker')
);
}
};
worker.addEventListener('message', onHashMessage);
});
}
};
resolve(runtimeModule);
break;
default:
logger.error('Unknown message', e.data);
URL.revokeObjectURL(objectUrl);
reject(new Error('Load error'));
}
};
worker.addEventListener('message', onMessage);
} catch (err) {
reject(err);
}
}).catch((err) => {
logger.warn('WebAssembly error', err);
throw new Error('WebAssembly error');
});
},
// eslint-disable-next-line object-shorthand
workerPostRun: function (): void {
/* eslint-disable */
const postRunMsg: MessageFromWorkerWorkerPostRun = { op: 'postRun' };
(self as unknown as MessagePort).postMessage(postRunMsg);
self.onmessage = (e: MessageEvent) => {
try {
// @ts-ignore
const hash = Module.calcHash(Module, e.data) as Uint8Array;
const hashMsg: MessageFromWorkerWorkerHash = { op: 'hash', hash };
(self as unknown as MessagePort).postMessage(hashMsg);
} catch (e) {
const errMsg: MessageFromWorkerWorkerError = { op: 'error', error: e.toString() };
(self as unknown as MessagePort).postMessage(errMsg);
}
};
/* eslint-enable */
},
// eslint-disable-next-line object-shorthand
calcHash: function (Module: Argon2WasmModule, args: Argon2HashArgs): Uint8Array {
const { memory, iterations, length, parallelism, type, version } = args;
const passwordLen = args.password.byteLength;
const password = Module.allocate(new Uint8Array(args.password), 'i8', Module.ALLOC_NORMAL);
const saltLen = args.salt.byteLength;
const salt = Module.allocate(new Uint8Array(args.salt), 'i8', Module.ALLOC_NORMAL);
const hash = Module.allocate(new Array(length), 'i8', Module.ALLOC_NORMAL);
const encodedLen = 512;
const encoded = Module.allocate(new Array(encodedLen), 'i8', Module.ALLOC_NORMAL);
const res = Module._argon2_hash(
iterations,
memory,
parallelism,
password,
passwordLen,
salt,
saltLen,
hash,
length,
encoded,
encodedLen,
type,
version
);
if (res) {
throw new Error(`Argon2 error ${res}`);
}
const hashArr = new Uint8Array(length);
for (let i = 0; i < length; i++) {
hashArr[i] = Module.HEAP8[hash + i];
}
Module._free(password);
Module._free(salt);
Module._free(hash);
Module._free(encoded);
return hashArr;
}
};
export { KdbxwebInit };
@@ -0,0 +1,76 @@
import { Launcher } from 'comp/launcher';
import { Alert, Alerts } from 'comp/ui/alerts';
import { Features } from 'util/features';
import { Locale } from 'util/locale';
import { RuntimeData } from 'models/runtime-data';
import * as fs from 'fs';
const InstalledAppPath = '/Applications/KeeWeb.app';
let alert: Alert | undefined;
const AppRightsChecker = {
async init(): Promise<void> {
if (!Launcher || !Features.isMac) {
return;
}
if (RuntimeData.skipFolderRightsWarning) {
return;
}
const appPath = await Launcher.ipcRenderer.invoke('get-app-path');
if (!appPath.startsWith(InstalledAppPath)) {
return;
}
const needRun = await this.needRunInstaller();
if (needRun) {
this.showAlert();
await this.runInstaller();
}
},
async needRunInstaller(): Promise<boolean> {
const stat = await fs.promises.stat(InstalledAppPath);
const folderIsRoot = stat && stat.uid === 0;
return !folderIsRoot;
},
showAlert(): void {
const command = `sudo chown -R root ${InstalledAppPath}`;
alert = Alerts.alert({
icon: 'lock',
header: Locale.appRightsAlert,
body: Locale.appRightsAlertBody1 + '\n' + Locale.appRightsAlertBody2,
pre: command,
buttons: [
{ result: 'skip', title: Locale.alertDoNotAsk, error: true },
Alerts.buttons.ok
],
success: async (result) => {
if (result === 'skip') {
await this.dontAskAnymore();
}
alert = undefined;
}
});
},
async runInstaller(): Promise<void> {
await Launcher?.spawn({
cmd: `${InstalledAppPath}/Contents/Installer/KeeWeb Installer.app/Contents/MacOS/applet`,
args: ['--install']
});
const needRun = await this.needRunInstaller();
if (alert && !needRun) {
alert.closeWithResult('cancel');
}
},
async dontAskAnymore(): Promise<void> {
const needRun = await this.needRunInstaller();
if (needRun) {
RuntimeData.skipFolderRightsWarning = true;
}
}
};
export { AppRightsChecker };
+1
View File
@@ -0,0 +1 @@
../../../../desktop/desktop-ipc.d.ts
-7
View File
@@ -1,7 +0,0 @@
let Launcher;
if (window.process && window.process.versions && window.process.versions.electron) {
Launcher = require('./launcher-electron').Launcher;
}
export { Launcher };
+9
View File
@@ -0,0 +1,9 @@
import { LauncherElectron } from './launcher-electron';
let Launcher: LauncherElectron | undefined;
if (global.process?.versions?.electron) {
Launcher = new LauncherElectron();
}
export { Launcher };
@@ -1,330 +0,0 @@
import { Events } from 'framework/events';
import { StartProfiler } from 'comp/app/start-profiler';
import { RuntimeInfo } from 'const/runtime-info';
import { Locale } from 'util/locale';
import { Logger } from 'util/logger';
import { noop } from 'util/fn';
const logger = new Logger('launcher');
const Launcher = {
name: 'electron',
version: window.process.versions.electron,
autoTypeSupported: true,
thirdPartyStoragesSupported: true,
clipboardSupported: true,
req: window.require,
platform() {
return process.platform;
},
arch() {
return process.arch;
},
electron() {
return this.req('electron');
},
remoteApp() {
return this.electron().remote.app;
},
remReq(mod) {
return this.electron().remote.require(mod);
},
openLink(href) {
if (/^(http|https|ftp|sftp|mailto):/i.test(href)) {
this.electron().shell.openExternal(href);
}
},
devTools: true,
openDevTools() {
this.electron().remote.getCurrentWindow().webContents.openDevTools({ mode: 'bottom' });
},
getSaveFileName(defaultPath, callback) {
if (defaultPath) {
const homePath = this.remReq('electron').app.getPath('userDesktop');
defaultPath = this.joinPath(homePath, defaultPath);
}
this.remReq('electron')
.dialog.showSaveDialog({
title: Locale.launcherSave,
defaultPath,
filters: [{ name: Locale.launcherFileFilter, extensions: ['kdbx'] }]
})
.then((res) => callback(res.filePath));
},
getUserDataPath(fileName) {
if (!this.userDataPath) {
this.userDataPath = this.remoteApp().getPath('userData');
}
return this.joinPath(this.userDataPath, fileName || '');
},
getTempPath(fileName) {
let tempPath = this.joinPath(this.remoteApp().getPath('temp'), 'KeeWeb');
const fs = this.req('fs');
if (!fs.existsSync(tempPath)) {
fs.mkdirSync(tempPath);
}
if (fileName) {
tempPath = this.joinPath(tempPath, fileName);
}
return tempPath;
},
getDocumentsPath(fileName) {
return this.joinPath(this.remoteApp().getPath('documents'), fileName || '');
},
getAppPath(fileName) {
const dirname = this.req('path').dirname;
const appPath = __dirname.endsWith('app.asar') ? __dirname : this.remoteApp().getAppPath();
return this.joinPath(dirname(appPath), fileName || '');
},
getWorkDirPath(fileName) {
return this.joinPath(process.cwd(), fileName || '');
},
joinPath(...parts) {
return this.req('path').join(...parts);
},
writeFile(path, data, callback) {
this.req('fs').writeFile(path, window.Buffer.from(data), callback);
},
readFile(path, encoding, callback) {
this.req('fs').readFile(path, encoding, (err, contents) => {
const data = typeof contents === 'string' ? contents : new Uint8Array(contents);
callback(data, err);
});
},
fileExists(path, callback) {
const fs = this.req('fs');
fs.access(path, fs.constants.F_OK, (err) => callback(!err));
},
fileExistsSync(path) {
const fs = this.req('fs');
return !fs.accessSync(path, fs.constants.F_OK);
},
deleteFile(path, callback) {
this.req('fs').unlink(path, callback || noop);
},
statFile(path, callback) {
this.req('fs').stat(path, (err, stats) => callback(stats, err));
},
mkdir(dir, callback) {
const fs = this.req('fs');
const path = this.req('path');
const stack = [];
const collect = function (dir, stack, callback) {
fs.exists(dir, (exists) => {
if (exists) {
return callback();
}
stack.unshift(dir);
const newDir = path.dirname(dir);
if (newDir === dir || !newDir || newDir === '.' || newDir === '/') {
return callback();
}
collect(newDir, stack, callback);
});
};
const create = function (stack, callback) {
if (!stack.length) {
return callback();
}
fs.mkdir(stack.shift(), (err) => (err ? callback(err) : create(stack, callback)));
};
collect(dir, stack, () => create(stack, callback));
},
parsePath(fileName) {
const path = this.req('path');
return {
path: fileName,
dir: path.dirname(fileName),
file: path.basename(fileName)
};
},
createFsWatcher(path) {
return this.req('fs').watch(path, { persistent: false });
},
loadConfig(name) {
return this.remoteApp().loadConfig(name);
},
saveConfig(name, data) {
return this.remoteApp().saveConfig(name, data);
},
preventExit(e) {
e.returnValue = false;
return false;
},
exit() {
this.exitRequested = true;
this.requestExit();
},
requestExit() {
const app = this.remoteApp();
app.setHookBeforeQuitEvent(false);
if (this.pendingUpdateFile) {
app.restartAndUpdate(this.pendingUpdateFile);
} else {
app.quit();
}
},
requestRestartAndUpdate(updateFilePath) {
this.pendingUpdateFile = updateFilePath;
this.requestExit();
},
cancelRestart() {
this.pendingUpdateFile = undefined;
},
setClipboardText(text) {
return this.electron().clipboard.writeText(text);
},
getClipboardText() {
return this.electron().clipboard.readText();
},
clearClipboardText() {
const { clipboard } = this.electron();
clipboard.clear();
if (process.platform === 'linux') {
clipboard.clear('selection');
}
},
quitOnRealQuitEventIfMinimizeOnQuitIsEnabled() {
return !!this.pendingUpdateFile;
},
minimizeApp() {
this.remoteApp().minimizeApp({
restore: Locale.menuRestoreApp.replace('{}', 'KeeWeb'),
quit: Locale.menuQuitApp.replace('{}', 'KeeWeb')
});
},
canDetectOsSleep() {
return process.platform !== 'linux';
},
updaterEnabled() {
return process.platform !== 'linux';
},
getMainWindow() {
return this.remoteApp().getMainWindow();
},
resolveProxy(url, callback) {
const window = this.getMainWindow();
const session = window.webContents.session;
session.resolveProxy(url).then((proxy) => {
const match = /^proxy\s+([\w\.]+):(\d+)+\s*/i.exec(proxy);
proxy = match && match[1] ? { host: match[1], port: +match[2] } : null;
callback(proxy);
});
},
hideApp() {
const app = this.remoteApp();
if (this.platform() === 'darwin') {
app.hide();
} else {
app.minimizeThenHideIfInTray();
}
},
isAppFocused() {
return !!this.electron().remote.BrowserWindow.getFocusedWindow();
},
showMainWindow() {
this.remoteApp().showAndFocusMainWindow();
},
spawn(config) {
const ts = logger.ts();
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;
})
.catch((err) => {
complete?.(err);
});
},
checkOpenFiles() {
this.readyToOpenFiles = true;
if (this.pendingFileToOpen) {
this.openFile(this.pendingFileToOpen);
delete this.pendingFileToOpen;
}
},
openFile(file) {
if (this.readyToOpenFiles) {
Events.emit('launcher-open-file', file);
} else {
this.pendingFileToOpen = file;
}
},
setGlobalShortcuts(appSettings) {
this.remoteApp().setGlobalShortcuts(appSettings);
},
minimizeMainWindow() {
this.getMainWindow().minimize();
},
maximizeMainWindow() {
this.getMainWindow().maximize();
},
restoreMainWindow() {
this.getMainWindow().restore();
},
mainWindowMaximized() {
return this.getMainWindow().isMaximized();
}
};
Events.on('launcher-exit-request', () => {
setTimeout(() => Launcher.exit(), 0);
});
Events.on('launcher-minimize', () => setTimeout(() => Events.emit('app-minimized'), 0));
Events.on('launcher-maximize', () => setTimeout(() => Events.emit('app-maximized'), 0));
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));
window.launcherOpen = (file) => Launcher.openFile(file);
if (window.launcherOpenedFile) {
logger.info('Open file request', window.launcherOpenedFile);
Launcher.openFile(window.launcherOpenedFile);
delete window.launcherOpenedFile;
}
Events.on('app-ready', () =>
setTimeout(() => {
Launcher.checkOpenFiles();
Launcher.remoteApp().setAboutPanelOptions({
applicationVersion: RuntimeInfo.version,
version: RuntimeInfo.commit
});
}, 0)
);
if (process.platform === 'darwin') {
Launcher.remoteApp().setHookBeforeQuitEvent(true);
}
Launcher.remoteApp().on('remote-app-event', (e) => {
if (window.debugRemoteAppEvents) {
logger.debug('remote-app-event', e.name);
}
Events.emit(e.name, e.data);
});
export { Launcher };
@@ -0,0 +1,350 @@
// import { Events } from 'util/events';
// import { StartProfiler } from 'comp/app/start-profiler';
// import { RuntimeInfo } from 'const/runtime-info';
import { Logger } from 'util/logger';
import { noop } from 'util/fn';
import * as electron from 'electron';
import { DesktopIpcRendererEvents, DesktopIpcMainEvents, DesktopIpcMainCalls } from './desktop-ipc';
const logger = new Logger('launcher');
type TypedIpcRenderer = {
on<EventName extends keyof DesktopIpcRendererEvents>(
event: EventName,
listener: (
event: electron.IpcRendererEvent,
...args: Parameters<DesktopIpcRendererEvents[EventName]>
) => void
): void;
send<EventName extends keyof DesktopIpcMainEvents>(
event: EventName,
...args: Parameters<DesktopIpcMainEvents[EventName]>
): void;
invoke<CallName extends keyof DesktopIpcMainCalls>(
method: CallName,
...args: Parameters<DesktopIpcMainCalls[CallName]>
): Promise<ReturnType<DesktopIpcMainCalls[CallName]>>;
};
export class LauncherElectron {
name = 'electron';
version = process.versions.electron;
pendingUpdateFile?: string;
mainWindowMaximized?: boolean;
constructor() {
this.ipcRenderer.on('log', (e, name, level, ...args) => {
const logger = new Logger(name);
logger[level](...args);
});
}
get ipcRenderer(): TypedIpcRenderer {
return electron.ipcRenderer as TypedIpcRenderer;
}
openLink(href: string): void {
if (/^(http|https|ftp|sftp|mailto):/i.test(href)) {
electron.shell.openExternal(href).catch(noop);
}
}
// openDevTools() {
// this.electron().remote.getCurrentWindow().webContents.openDevTools({ mode: 'bottom' });
// }
// getSaveFileName(defaultPath, callback) {
// if (defaultPath) {
// const homePath = this.remReq('electron').app.getPath('userDesktop');
// defaultPath = this.joinPath(homePath, defaultPath);
// }
// this.remReq('electron')
// .dialog.showSaveDialog({
// title: Locale.launcherSave,
// defaultPath,
// filters: [{ name: Locale.launcherFileFilter, extensions: ['kdbx'] }]
// })
// .then((res) => callback(res.filePath));
// },
// getUserDataPath(fileName) {
// if (!this.userDataPath) {
// this.userDataPath = this.remoteApp().getPath('userData');
// }
// return this.joinPath(this.userDataPath, fileName || '');
// },
// getTempPath(fileName) {
// let tempPath = this.joinPath(this.remoteApp().getPath('temp'), 'KeeWeb');
// const fs = this.req('fs');
// if (!fs.existsSync(tempPath)) {
// fs.mkdirSync(tempPath);
// }
// if (fileName) {
// tempPath = this.joinPath(tempPath, fileName);
// }
// return tempPath;
// },
// getDocumentsPath(fileName) {
// return this.joinPath(this.remoteApp().getPath('documents'), fileName || '');
// },
// getAppPath(fileName) {
// const dirname = this.req('path').dirname;
// const appPath = __dirname.endsWith('app.asar') ? __dirname : this.remoteApp().getAppPath();
// return this.joinPath(dirname(appPath), fileName || '');
// },
// getWorkDirPath(fileName) {
// return this.joinPath(process.cwd(), fileName || '');
// },
// joinPath(...parts) {
// return this.req('path').join(...parts);
// },
// writeFile(path, data, callback) {
// this.req('fs').writeFile(path, window.Buffer.from(data), callback);
// },
// readFile(path, encoding, callback) {
// this.req('fs').readFile(path, encoding, (err, contents) => {
// const data = typeof contents === 'string' ? contents : new Uint8Array(contents);
// callback(data, err);
// });
// },
// fileExists(path, callback) {
// const fs = this.req('fs');
// fs.access(path, fs.constants.F_OK, (err) => callback(!err));
// },
// fileExistsSync(path) {
// const fs = this.req('fs');
// return !fs.accessSync(path, fs.constants.F_OK);
// },
// deleteFile(path, callback) {
// this.req('fs').unlink(path, callback || noop);
// },
// statFile(path, callback) {
// this.req('fs').stat(path, (err, stats) => callback(stats, err));
// },
// mkdir(dir, callback) {
// const fs = this.req('fs');
// const path = this.req('path');
// const stack = [];
//
// const collect = function (dir, stack, callback) {
// fs.exists(dir, (exists) => {
// if (exists) {
// return callback();
// }
//
// stack.unshift(dir);
// const newDir = path.dirname(dir);
// if (newDir === dir || !newDir || newDir === '.' || newDir === '/') {
// return callback();
// }
//
// collect(newDir, stack, callback);
// });
// };
//
// const create = function (stack, callback) {
// if (!stack.length) {
// return callback();
// }
//
// fs.mkdir(stack.shift(), (err) => (err ? callback(err) : create(stack, callback)));
// };
//
// collect(dir, stack, () => create(stack, callback));
// },
// parsePath(fileName) {
// const path = this.req('path');
// return {
// path: fileName,
// dir: path.dirname(fileName),
// file: path.basename(fileName)
// };
// },
// createFsWatcher(path) {
// return this.req('fs').watch(path, { persistent: false });
// },
// preventExit(e) {
// e.returnValue = false;
// return false;
// },
// exit() {
// this.exitRequested = true;
// this.requestExit();
// },
requestExit(): void {
throw new Error('TODO');
// const app = this.remoteApp();
// app.setHookBeforeQuitEvent(false);
// if (this.pendingUpdateFile) {
// app.restartAndUpdate(this.pendingUpdateFile);
// } else {
// app.quit();
// }
}
requestRestartAndUpdate(updateFilePath: string): void {
this.pendingUpdateFile = updateFilePath;
this.requestExit();
}
// cancelRestart() {
// this.pendingUpdateFile = undefined;
// },
setClipboardText(text: string): void {
electron.clipboard.writeText(text);
}
getClipboardText(): string {
return electron.clipboard.readText();
}
clearClipboardText(): void {
electron.clipboard.clear();
if (process.platform === 'linux') {
electron.clipboard.clear('selection');
}
}
// quitOnRealQuitEventIfMinimizeOnQuitIsEnabled() {
// return !!this.pendingUpdateFile;
// },
// minimizeApp() {
// this.remoteApp().minimizeApp({
// restore: Locale.menuRestoreApp.replace('{}', 'KeeWeb'),
// quit: Locale.menuQuitApp.replace('{}', 'KeeWeb')
// });
// },
// canDetectOsSleep() {
// return process.platform !== 'linux';
// },
updaterEnabled(): boolean {
return process.platform !== 'linux';
}
// getMainWindow() {
// return this.remoteApp().getMainWindow();
// },
// resolveProxy(url, callback) {
// const window = this.getMainWindow();
// const session = window.webContents.session;
// session.resolveProxy(url).then((proxy) => {
// const match = /^proxy\s+([\w\.]+):(\d+)+\s*/i.exec(proxy);
// proxy = match && match[1] ? { host: match[1], port: +match[2] } : null;
// callback(proxy);
// });
// },
// hideApp() {
// const app = this.remoteApp();
// if (this.platform() === 'darwin') {
// app.hide();
// } else {
// app.minimizeThenHideIfInTray();
// }
// },
// isAppFocused() {
// return !!this.electron().remote.BrowserWindow.getFocusedWindow();
// },
// showMainWindow() {
// this.remoteApp().showAndFocusMainWindow();
// },
async spawn(config: {
cmd: string;
args: string[];
noStdOutLogging?: boolean;
throwOnStdErr?: boolean;
name?: string;
}): Promise<{ code?: number; stdout?: string; stderr?: string }> {
const ts = logger.ts();
const res = await this.ipcRenderer.invoke('spawn-process', {
cmd: config.cmd,
args: config.args
});
if (res.err) {
logger.error('spawn error: ' + config.cmd + ', ' + logger.ts(ts), res.err);
throw new Error(res.err);
} else {
const msg = `spawn ${config.cmd}: ${res.code || 0}, ${logger.ts(ts)}`;
if (res.code) {
logger.error(`${msg}\n${res.stdout ?? ''}\n${res.stderr ?? ''}`);
throw new Error(`Exit code ${res.code}`);
} else {
logger.info(msg + (res.stdout && !config.noStdOutLogging ? '\n' + res.stdout : ''));
if (res.stderr && config.throwOnStdErr) {
throw new Error(`Error returned: ${res.stderr.trim()}`);
}
}
return res;
}
}
async killProcess(name: string): Promise<void> {
await this.ipcRenderer.invoke('kill-process', name);
}
// checkOpenFiles() {
// this.readyToOpenFiles = true;
// if (this.pendingFileToOpen) {
// this.openFile(this.pendingFileToOpen);
// delete this.pendingFileToOpen;
// }
// },
// openFile(file) {
// if (this.readyToOpenFiles) {
// Events.emit('launcher-open-file', file);
// } else {
// this.pendingFileToOpen = file;
// }
// },
// minimizeMainWindow() {
// this.getMainWindow().minimize();
// },
// maximizeMainWindow() {
// this.getMainWindow().maximize();
// },
// restoreMainWindow() {
// this.getMainWindow().restore();
// },
// mainWindowMaximized() {
// return this.getMainWindow().isMaximized();
// }
}
// Events.on('launcher-exit-request', () => {
// setTimeout(() => Launcher.exit(), 0);
// });
// Events.on('launcher-minimize', () => setTimeout(() => Events.emit('app-minimized'), 0));
// Events.on('launcher-maximize', () => setTimeout(() => Events.emit('app-maximized'), 0));
// 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));
//
// window.launcherOpen = (file) => Launcher.openFile(file);
// if (window.launcherOpenedFile) {
// logger.info('Open file request', window.launcherOpenedFile);
// Launcher.openFile(window.launcherOpenedFile);
// delete window.launcherOpenedFile;
// }
// Events.on('app-ready', () =>
// setTimeout(() => {
// Launcher.checkOpenFiles();
// Launcher.remoteApp().setAboutPanelOptions({
// applicationVersion: RuntimeInfo.version,
// version: RuntimeInfo.commit
// });
// }, 0)
// );
//
// if (process.platform === 'darwin') {
// Launcher.remoteApp().setHookBeforeQuitEvent(true);
// }
//
// Launcher.remoteApp().on('remote-app-event', (e) => {
// if (window.debugRemoteAppEvents) {
// logger.info('remote-app-event', e.name);
// }
// Events.emit(e.name, e.data);
// });
-250
View File
@@ -1,250 +0,0 @@
import * as kdbxweb from 'kdbxweb';
import { Events } from 'framework/events';
import { Logger } from 'util/logger';
import { Launcher } from 'comp/launcher';
import { Timeouts } from 'const/timeouts';
let NativeModules;
if (Launcher) {
const logger = new Logger('native-module-connector');
let hostRunning = false;
let hostStartPromise;
let callId = 0;
let promises = {};
let ykChalRespCallbacks = {};
const { ipcRenderer } = Launcher.electron();
ipcRenderer.on('nativeModuleCallback', (e, msg) => NativeModules.hostCallback(msg));
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) {
Events.emit('native-modules-yubikeys', { numYubiKeys });
},
log(...args) {
logger.info('Message from host', ...args);
},
result({ callId, result, error }) {
const promise = promises[callId];
if (promise) {
delete promises[callId];
if (error) {
logger.error('Received an error', promise.cmd, error);
promise.reject(error);
} else {
promise.resolve(result);
}
}
},
yubiKeyChallengeResponseResult({ callbackId, error, result }) {
const callback = ykChalRespCallbacks[callbackId];
if (callback) {
const willBeCalledAgain = error && error.touchRequested;
if (!willBeCalledAgain) {
delete ykChalRespCallbacks[callbackId];
}
callback(error, result);
}
}
};
NativeModules = {
startHost() {
if (hostRunning) {
return Promise.resolve();
}
if (hostStartPromise) {
return hostStartPromise;
}
logger.debug('Starting native module host');
hostStartPromise = this.callNoWait('start').then(() => {
hostStartPromise = undefined;
hostRunning = true;
if (this.usbListenerRunning) {
return this.call('startUsbListener');
}
});
return hostStartPromise;
},
hostError(e) {
logger.error('Host error', e);
},
hostDisconnect() {
logger.error('Host disconnected');
},
hostExit(code, sig) {
logger.error(`Host exited with code ${code} and signal ${sig}`);
hostRunning = false;
const err = new Error('Native module host crashed');
for (const promise of Object.values(promises)) {
promise.reject(err);
}
promises = {};
for (const callback of Object.values(ykChalRespCallbacks)) {
callback(err);
}
ykChalRespCallbacks = {};
if (code !== 0) {
this.autoRestartHost();
}
},
hostCallback(message) {
const { cmd, args } = message;
// logger.debug('Callback', cmd, args);
if (handlers[cmd]) {
handlers[cmd](...args);
} else {
logger.error('No callback', cmd);
}
},
log(name, level, ...args) {
if (!name) {
return;
}
const logger = new Logger(name);
logger[level](...args);
},
autoRestartHost() {
setTimeout(() => {
try {
this.startHost();
} catch (e) {
logger.error('Native module host failed to auto-restart', e);
}
}, Timeouts.NativeModuleHostRestartTime);
},
call(cmd, ...args) {
return this.startHost().then(() => this.callNoWait(cmd, ...args));
},
callNoWait(cmd, ...args) {
return new Promise((resolve, reject) => {
callId++;
if (callId === Number.MAX_SAFE_INTEGER) {
callId = 1;
}
// logger.debug('Call', cmd, args, callId);
promises[callId] = { cmd, resolve, reject };
ipcRenderer.send('nativeModuleCall', { cmd, args, callId });
});
},
startUsbListener() {
this.call('startUsbListener');
this.usbListenerRunning = true;
},
stopUsbListener() {
this.usbListenerRunning = false;
if (hostRunning) {
this.call('stopUsbListener');
}
},
getYubiKeys(config) {
return this.call('getYubiKeys', config);
},
yubiKeyChallengeResponse(yubiKey, challenge, slot, callback) {
ykChalRespCallbacks[callId] = callback;
return this.call('yubiKeyChallengeResponse', yubiKey, challenge, slot, callId);
},
yubiKeyCancelChallengeResponse() {
if (hostRunning) {
this.call('yubiKeyCancelChallengeResponse');
}
},
argon2(password, salt, options) {
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);
},
hardwareDecrypt: async (value, touchIdPrompt) => {
const { data, salt } = await ipcRenderer.invoke(
'hardwareDecrypt',
value.dataAndSalt(),
touchIdPrompt
);
return new kdbxweb.ProtectedValue(data, salt);
},
kbdGetActiveWindow(options) {
return this.call('kbdGetActiveWindow', options);
},
kbdGetActivePid() {
return this.call('kbdGetActivePid');
},
kbdShowWindow(win) {
return this.call('kbdShowWindow', win);
},
kbdText(str) {
return this.call('kbdText', str);
},
kbdTextAsKeys(str, mods) {
return this.call('kbdTextAsKeys', str, mods);
},
kbdKeyPress(code, modifiers) {
return this.call('kbdKeyPress', code, modifiers);
},
kbdShortcut(code, modifiers) {
return this.call('kbdShortcut', code, modifiers);
},
kbdKeyMoveWithModifier(down, modifiers) {
return this.call('kbdKeyMoveWithModifier', down, modifiers);
},
kbdKeyPressWithCharacter(character, code, modifiers) {
return this.call('kbdKeyPressWithCharacter', character, code, modifiers);
},
kbdEnsureModifierNotPressed() {
return this.call('kbdEnsureModifierNotPressed');
}
};
global.NativeModules = NativeModules;
}
export { NativeModules };
+268
View File
@@ -0,0 +1,268 @@
// import * as kdbxweb from 'kdbxweb';
import { Events } from 'util/events';
import { Logger } from 'util/logger';
import { noop, unreachable } from 'util/fn';
import { Launcher } from 'comp/launcher';
import { Timeouts } from 'const/timeouts';
import {
NativeArgon2OptionsArg,
NativeDataWithSalt,
NativeModuleCalls,
NativeModuleHostCallbackMessage,
YubiKeyInfo
} from './desktop-ipc';
const logger = new Logger('native-module-connector');
type YkChalRespCallback = (err?: Error) => void;
interface PendingCall {
cmd: string;
resolve: (result: unknown) => void;
reject: (err: unknown) => void;
}
class NativeModules {
private _hostRunning = false;
private _hostStartPromise: Promise<void> | undefined;
private _usbListenerRunning = false;
private _callId = 0;
private _pendingCalls = new Map<number, PendingCall>();
private _ykChalRespCallbacks = new Map<number, YkChalRespCallback>();
constructor() {
if (!Launcher) {
return;
}
Launcher.ipcRenderer.on('native-module-host-callback', (e, msg) => {
this.hostCallback(msg);
});
Launcher.ipcRenderer.on('native-module-host-error', (e, err) => {
this.hostError(err);
});
Launcher.ipcRenderer.on('native-module-host-exit', (e, code, sig) => {
this.hostExit(code, sig);
});
Launcher.ipcRenderer.on('native-module-host-disconnect', () => {
this.hostDisconnect();
});
}
private startHost(): Promise<void> {
if (this._hostRunning) {
return Promise.resolve();
}
if (this._hostStartPromise) {
return this._hostStartPromise;
}
logger.info('Starting native module host');
this._hostStartPromise = this.call('start').then(() => {
this._hostStartPromise = undefined;
this._hostRunning = true;
if (this._usbListenerRunning) {
return this.call('start-usb-listener');
}
});
return this._hostStartPromise;
}
private hostError(e: unknown): void {
logger.error('Host error', e);
}
private hostDisconnect(): void {
logger.error('Host disconnected');
}
private hostExit(code: number | undefined, sig: string | undefined): void {
logger.error(`Host exited with code ${String(code)} and signal ${String(sig)}`);
this._hostRunning = false;
const err = new Error('Native module host crashed');
for (const call of this._pendingCalls.values()) {
call.reject(err);
}
this._pendingCalls.clear();
for (const callback of this._ykChalRespCallbacks.values()) {
callback(err);
}
this._ykChalRespCallbacks.clear();
if (code !== 0) {
this.autoRestartHost();
}
}
private hostCallback(msg: NativeModuleHostCallbackMessage): void {
switch (msg.cmd) {
case 'yubikeys':
this.numYubiKeysReceived(msg.numYubiKeys);
return;
}
unreachable('Unexpected native host callback', msg.cmd);
}
private autoRestartHost(): void {
setTimeout(() => {
try {
this.startHost().catch(noop);
} catch (e) {
logger.error('Native module host failed to auto-restart', e);
}
}, Timeouts.NativeModuleHostRestartTime);
}
private async call<Cmd extends keyof NativeModuleCalls>(
cmd: Cmd,
...args: Parameters<NativeModuleCalls[Cmd]>
): Promise<ReturnType<NativeModuleCalls[Cmd]>> {
if (cmd !== 'start') {
await this.startHost();
}
return new Promise<unknown>((resolve, reject) => {
this._callId++;
if (this._callId === Number.MAX_SAFE_INTEGER) {
this._callId = 1;
}
this._pendingCalls.set(this._callId, { cmd, resolve, reject });
Launcher?.ipcRenderer.send('native-module-call', cmd, ...args);
}) as Promise<ReturnType<NativeModuleCalls[Cmd]>>;
}
private result(callId: number, result: unknown, error: unknown) {
const call = this._pendingCalls.get(callId);
if (call) {
this._pendingCalls.delete(callId);
if (error) {
logger.error('Received an error', call.cmd, error);
call.reject(error);
} else {
call.resolve(result);
}
}
}
private numYubiKeysReceived(numYubiKeys: number) {
Events.emit('native-modules-yubikeys', numYubiKeys);
}
// private yubiKeyChallengeResponseResult({ callbackId, error, result }) {
// const callback = ykChalRespCallbacks[callbackId];
// if (callback) {
// const willBeCalledAgain = error && error.touchRequested;
// if (!willBeCalledAgain) {
// delete ykChalRespCallbacks[callbackId];
// }
// callback(error, result);
// }
// }
startUsbListener(): Promise<void> {
return this.call('start-usb-listener');
this._usbListenerRunning = true;
}
stopUsbListener(): Promise<void> {
this._usbListenerRunning = false;
if (this._hostRunning) {
return this.call('stop-usb-listener');
}
return Promise.resolve();
}
getYubiKeys(): Promise<YubiKeyInfo[]> {
return this.call('get-yubikeys');
}
// yubiKeyChallengeResponse(yubiKey, challenge, slot, callback) {
// ykChalRespCallbacks[callId] = callback;
// return this.call('yubiKeyChallengeResponse', yubiKey, challenge, slot, callId);
// }
yubiKeyCancelChallengeResponse(): Promise<void> {
if (this._hostRunning) {
return this.call('yubikey-cancel-chal-resp');
}
return Promise.resolve();
}
argon2(
password: NativeDataWithSalt,
salt: NativeDataWithSalt,
options: NativeArgon2OptionsArg
): Promise<NativeDataWithSalt> {
return this.call('argon2', password, salt, options);
}
// hardwareCryptoDeleteKey: () => {
// return Launcher.ipcRenderer.invoke('hardwareCryptoDeleteKey');
// },
//
// hardwareEncrypt: async (value) => {
// const { data, salt } = await ipcRenderer.invoke('hardwareEncrypt', value.dataAndSalt());
// return new kdbxweb.ProtectedValue(data, salt);
// },
//
// hardwareDecrypt: async (value, touchIdPrompt) => {
// const { data, salt } = await ipcRenderer.invoke(
// 'hardwareDecrypt',
// value.dataAndSalt(),
// touchIdPrompt
// );
// return new kdbxweb.ProtectedValue(data, salt);
// },
//
// kbdGetActiveWindow(options) {
// return this.call('kbdGetActiveWindow', options);
// },
//
// kbdGetActivePid() {
// return this.call('kbdGetActivePid');
// },
//
// kbdShowWindow(win) {
// return this.call('kbdShowWindow', win);
// },
//
// kbdText(str) {
// return this.call('kbdText', str);
// },
//
// kbdTextAsKeys(str, mods) {
// return this.call('kbdTextAsKeys', str, mods);
// },
//
// kbdKeyPress(code, modifiers) {
// return this.call('kbdKeyPress', code, modifiers);
// },
//
// kbdShortcut(code, modifiers) {
// return this.call('kbdShortcut', code, modifiers);
// },
//
// kbdKeyMoveWithModifier(down, modifiers) {
// return this.call('kbdKeyMoveWithModifier', down, modifiers);
// },
//
// kbdKeyPressWithCharacter(character, code, modifiers) {
// return this.call('kbdKeyPressWithCharacter', character, code, modifiers);
// },
//
// kbdEnsureModifierNotPressed() {
// return this.call('kbdEnsureModifierNotPressed');
// }
}
const instance = new NativeModules();
export { instance as NativeModules };
+196
View File
@@ -0,0 +1,196 @@
import * as kdbxweb from 'kdbxweb';
import { BuiltInFields } from 'const/entry-fields';
import { AdvancedFilter, Filter } from 'models/filter';
import { Entry } from 'models/entry';
interface SearchContext {
matches: string[];
}
export const EntryMatcher = { matches };
const BuiltInFieldsSet = new Set<string>(BuiltInFields);
function matches(entry: Entry, filter: Filter): boolean {
if (filter.tagLower) {
if (entry.searchTags && entry.searchTags.indexOf(filter.tagLower) < 0) {
return false;
}
}
if (filter.textLower) {
if (filter.advanced) {
if (!matchesAdv(entry, filter)) {
return false;
}
} else if (filter.textLowerParts) {
const parts = filter.textLowerParts;
for (let i = 0; i < parts.length; i++) {
if (!entry.searchText?.includes(parts[i])) {
return false;
}
}
} else if (!entry.searchText?.includes(filter.textLower)) {
return false;
}
}
if (filter.color) {
if (filter.color === '*') {
if (!entry.searchColor) {
return false;
}
} else if (entry.searchColor !== filter.color) {
return false;
}
}
if (filter.autoType) {
if (!entry.autoTypeEnabled) {
return false;
}
}
if (filter.otp) {
if (
!entry.fields?.has('otp') &&
!entry.fields?.has('TOTP Seed') &&
entry.backend !== 'otp-device'
) {
return false;
}
}
return true;
}
function matchesAdv(entry: Entry, filter: Filter): boolean {
const adv = filter.advanced;
if (!adv || !filter.text || !filter.textLower) {
return false;
}
let compare: (val: kdbxweb.KdbxEntryField) => boolean;
const context: SearchContext = { matches: [] };
if (adv.regex) {
try {
const regex = new RegExp(filter.text, adv.cs ? '' : 'i');
compare = (val) => matchRegex(val, regex);
} catch (e) {
return false;
}
} else if (adv.cs) {
if (filter.textParts) {
const textParts = filter.textParts;
compare = (val) => matchStringMulti(val, textParts, context, false);
} else {
const text = filter.text;
compare = (val) => matchString(val, text);
}
} else if (filter.textLowerParts) {
const textLowerParts = filter.textLowerParts;
compare = (val) => matchStringMultiLower(val, textLowerParts, context);
} else {
const textLower = filter.textLower;
compare = (val) => matchStringLower(val, textLower);
}
if (matchFields(entry.getAllFields(), adv, compare)) {
return true;
}
if (adv.history && entry.getAllHistoryEntriesFields) {
for (const historyEntryFields of entry.getAllHistoryEntriesFields()) {
if (matchFields(historyEntryFields, adv, compare)) {
return true;
}
}
}
return false;
}
function matchString(str: kdbxweb.KdbxEntryField, find: string): boolean {
if (str instanceof kdbxweb.ProtectedValue) {
return str.includes(find);
}
return str.includes(find);
}
function matchStringLower(str: kdbxweb.KdbxEntryField, findLower: string): boolean {
if (str instanceof kdbxweb.ProtectedValue) {
return str.includesLower(findLower);
}
return str.toLowerCase().includes(findLower);
}
function matchStringMulti(
str: kdbxweb.KdbxEntryField,
find: string[],
context: SearchContext,
lower: boolean
): boolean {
for (let i = 0; i < find.length; i++) {
const item = find[i];
const strMatches = lower ? matchStringLower(str, item) : matchString(str, item);
if (strMatches) {
if (context.matches) {
if (!context.matches.includes(item)) {
context.matches.push(item);
}
} else {
context.matches = [item];
}
}
}
return context.matches && context.matches.length === find.length;
}
function matchStringMultiLower(
str: kdbxweb.KdbxEntryField,
find: string[],
context: SearchContext
): boolean {
return matchStringMulti(str, find, context, true);
}
function matchRegex(str: kdbxweb.KdbxEntryField, regex: RegExp): boolean {
if (str instanceof kdbxweb.ProtectedValue) {
str = str.getText();
}
return regex.test(str);
}
function matchFields(
fields: Map<string, kdbxweb.KdbxEntryField>,
adv: AdvancedFilter,
compare: (val: kdbxweb.KdbxEntryField) => boolean
): boolean {
if (adv.user && matchField(fields.get('UserName'), compare)) {
return true;
}
if (adv.url && matchField(fields.get('URL'), compare)) {
return true;
}
if (adv.notes && matchField(fields.get('Notes'), compare)) {
return true;
}
if (adv.pass && matchField(fields.get('Password'), compare)) {
return true;
}
if (adv.title && matchField(fields.get('Title'), compare)) {
return true;
}
if (adv.other || adv.protect) {
for (const [field, value] of fields) {
if (BuiltInFieldsSet.has(field)) {
if (typeof value === 'string') {
if (adv.other && matchField(value, compare)) {
return true;
}
} else if (adv.protect && matchField(value, compare)) {
return true;
}
}
}
}
return false;
}
function matchField(
val: kdbxweb.KdbxEntryField | undefined,
compare: (val: kdbxweb.KdbxEntryField) => boolean
): boolean {
return val ? compare(val) : false;
}
+182
View File
@@ -0,0 +1,182 @@
import { Features } from 'util/features';
import { Logger } from 'util/logger';
import { AppSettings, AppSettingsFieldName } from 'models/app-settings';
import { Alerts } from 'comp/ui/alerts';
import { Locale } from 'util/locale';
import { FileManager } from 'models/file-manager';
import { FileInfo, FileStorageExtraOptions } from 'models/file-info';
import { IdGenerator } from 'util/generators/id-generator';
import { PluginManager } from 'plugins/plugin-manager';
import { PluginManifest } from 'plugins/types';
const logger = new Logger('config-loader');
class ConfigLoader {
async loadConfig(): Promise<boolean> {
const configParam = this.getConfigParam();
if (configParam) {
try {
const config = await this.loadFromNetwork(configParam);
if (!config.settings) {
logger.error('Invalid app config, no settings section', config);
throw new Error('Invalid app config, no settings section');
}
await this.applyUserConfig(config);
return true;
} catch (e) {
if (!AppSettings.cacheConfigSettings) {
this.showSettingsLoadError();
throw e;
}
}
}
return false;
}
private getConfigParam(): string | null {
const metaConfig = document.head.querySelector('meta[name=kw-config]') as
| HTMLMetaElement
| undefined;
if (metaConfig && metaConfig.content && metaConfig.content[0] !== '(') {
return metaConfig.content;
}
if (!location.search) {
return null;
}
const sp = new URLSearchParams(location.search);
return sp.get('config');
}
private loadFromNetwork(configLocation: string): Promise<Record<string, string>> {
return new Promise((resolve, reject) => {
this.ensureCanLoadConfig(configLocation);
logger.info('Loading config from', configLocation);
const ts = logger.ts();
const xhr = new XMLHttpRequest();
xhr.open('GET', configLocation);
xhr.responseType = 'json';
xhr.send();
xhr.addEventListener('load', () => {
const response = xhr.response as unknown;
if (!response || typeof response !== 'object') {
const errorDesc = xhr.statusText === 'OK' ? 'Malformed JSON' : xhr.statusText;
logger.error('Error loading app config', errorDesc);
return reject('Error loading app config');
}
logger.info('Loaded app config from', configLocation, logger.ts(ts));
resolve(response as Record<string, string>);
});
xhr.addEventListener('error', () => {
logger.error('Error loading app config', xhr.statusText, xhr.status);
reject(new Error('Error loading app config'));
});
});
}
private ensureCanLoadConfig(url: string) {
if (!Features.isSelfHosted) {
throw new Error('Configs are supported only in self-hosted installations');
}
const link = document.createElement('a');
link.href = url;
const isExternal = link.host && link.host !== location.host;
if (isExternal) {
throw new Error('Loading config from this location is not allowed');
}
}
private async applyUserConfig(config: Record<string, unknown>): Promise<void> {
if (!config.settings) {
logger.error('Invalid app config, no settings section', config);
throw new Error('Invalid app config, no settings section');
}
const settings = config.settings as Record<string, unknown>;
for (const [key, value] of Object.entries(settings)) {
const isSet = AppSettings.set(key as AppSettingsFieldName, value);
if (!isSet) {
logger.warn('Invalid setting ignored', key, value);
}
}
if (Array.isArray(config.files)) {
if (config.showOnlyFilesFromConfig === true) {
FileManager.reset();
}
for (const file of config.files.reverse()) {
if (!file || typeof file !== 'object') {
continue;
}
const fileRec = file as Record<string, unknown>;
if (typeof fileRec.storage !== 'string' || !fileRec.storage) {
continue;
}
if (typeof fileRec.name !== 'string' || !fileRec.name) {
continue;
}
if (typeof fileRec.path !== 'string' || !fileRec.path) {
continue;
}
let opts: FileStorageExtraOptions | undefined;
if (typeof fileRec.opts === 'object' && fileRec.opts) {
const optsRec = fileRec.opts as Record<string, unknown>;
opts = {
user: typeof optsRec.user === 'string' ? optsRec.user : undefined,
encpass: typeof optsRec.encpass === 'string' ? optsRec.encpass : undefined
};
}
if (FileManager.getFileInfo(fileRec.storage, fileRec.name, fileRec.path)) {
continue;
}
const fi = new FileInfo({
id: IdGenerator.uuid(),
name: fileRec.name,
storage: fileRec.storage,
path: fileRec.path,
opts
});
FileManager.addFileInfo(fi, true);
}
}
if (Array.isArray(config.plugins)) {
const pluginPromises: Promise<void>[] = [];
for (const plugin of config.plugins as Record<string, unknown>[]) {
if (typeof plugin.url === 'string') {
const pluginPromise = PluginManager.installIfNew(
plugin.url,
plugin.manifest as PluginManifest,
true
);
pluginPromises.push(pluginPromise);
}
}
await Promise.all(pluginPromises);
}
if (config.advancedSearch) {
// TODO: set advanced search
// this.advancedSearch = config.advancedSearch;
// this.addFilter({ advanced: this.advancedSearch });
}
}
private showSettingsLoadError() {
Alerts.error({
header: Locale.appSettingsError,
body: Locale.appSettingsErrorBody,
buttons: [],
esc: undefined,
enter: undefined,
click: undefined
});
}
}
const instance = new ConfigLoader();
export { instance as ConfigLoader };
@@ -1,193 +0,0 @@
import { Events } from 'framework/events';
import { Features } from 'util/features';
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');
const SettingsManager = {
neutralLocale: null,
activeLocale: 'en-US',
activeTheme: null,
allLocales: {
'en-US': 'English',
'de-DE': 'Deutsch',
'fr-FR': 'Français'
},
allThemes: {
dark: 'setGenThemeDark',
light: 'setGenThemeLight',
sd: 'setGenThemeSd',
sl: 'setGenThemeSl',
fb: 'setGenThemeFb',
bl: 'setGenThemeBl',
db: 'setGenThemeDb',
lb: 'setGenThemeLb',
te: 'setGenThemeTe',
lt: 'setGenThemeLt',
dc: 'setGenThemeDc',
hc: 'setGenThemeHc'
},
// changing something here? don't forget about desktop/app.js
autoSwitchedThemes: [
{
name: 'setGenThemeDefault',
dark: 'dark',
light: 'light'
},
{
name: 'setGenThemeSol',
dark: 'sd',
light: 'sl'
},
{
name: 'setGenThemeBlue',
dark: 'fb',
light: 'bl'
},
{
name: 'setGenThemeBrown',
dark: 'db',
light: 'lb'
},
{
name: 'setGenThemeTerminal',
dark: 'te',
light: 'lt'
},
{
name: 'setGenThemeHighContrast',
dark: 'dc',
light: 'hc'
}
],
customLocales: {},
init() {
Events.on('dark-mode-changed', () => this.darkModeChanged());
},
setBySettings() {
this.setTheme(AppSettingsModel.theme);
this.setFontSize(AppSettingsModel.fontSize);
const locale = AppSettingsModel.locale;
try {
if (locale) {
this.setLocale(AppSettingsModel.locale);
} else {
this.setLocale(this.getBrowserLocale());
}
} catch (ex) {}
},
getDefaultTheme() {
return 'dark';
},
setTheme(theme) {
if (!theme) {
if (this.activeTheme) {
return;
}
theme = this.getDefaultTheme();
}
for (const cls of document.body.classList) {
if (/^th-/.test(cls)) {
document.body.classList.remove(cls);
}
}
if (AppSettingsModel.autoSwitchTheme) {
theme = this.selectDarkOrLightTheme(theme);
}
document.body.classList.add(this.getThemeClass(theme));
const metaThemeColor = document.head.querySelector('meta[name=theme-color]');
if (metaThemeColor) {
metaThemeColor.content = window.getComputedStyle(document.body).backgroundColor;
}
this.activeTheme = theme;
logger.debug('Theme changed', theme);
Events.emit('theme-applied');
},
getThemeClass(theme) {
return 'th-' + theme;
},
selectDarkOrLightTheme(theme) {
for (const config of this.autoSwitchedThemes) {
if (config.light === theme || config.dark === theme) {
return ThemeWatcher.dark ? config.dark : config.light;
}
}
return theme;
},
darkModeChanged() {
if (AppSettingsModel.autoSwitchTheme) {
for (const config of this.autoSwitchedThemes) {
if (config.light === this.activeTheme || config.dark === this.activeTheme) {
const newTheme = ThemeWatcher.dark ? config.dark : config.light;
logger.debug('Setting theme triggered by system settings change', newTheme);
this.setTheme(newTheme);
break;
}
}
}
},
setFontSize(fontSize) {
const defaultFontSize = Features.isMobile ? 14 : 12;
document.documentElement.style.fontSize = defaultFontSize + (fontSize || 0) * 2 + 'px';
},
setLocale(loc) {
if (!loc || loc === this.activeLocale) {
return;
}
let localeValues;
if (loc !== 'en-US') {
if (this.customLocales[loc]) {
localeValues = this.customLocales[loc];
} else {
localeValues = require('../../locales/' + loc + '.json');
}
}
if (!this.neutralLocale) {
this.neutralLocale = { ...Locale };
}
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() {
const language = (navigator.languages && navigator.languages[0]) || navigator.language;
if (language && language.startsWith('en')) {
return 'en-US';
}
return language;
}
};
export { SettingsManager };
@@ -0,0 +1,213 @@
import { Events } from 'util/events';
import { Features } from 'util/features';
import { Locale } from 'util/locale';
import { ThemeWatcher } from 'comp/browser/theme-watcher';
import { AppSettings, AppSettingsFontSize } from 'models/app-settings';
import { Logger } from 'util/logger';
import { Launcher } from 'comp/launcher';
import { noop } from 'util/fn';
import { WindowClass } from 'comp/browser/window-class';
const logger = new Logger('settings-manager');
const SettingsManager = {
activeTheme: null as string | null,
allLocales: {
'en-US': 'English',
'de-DE': 'Deutsch',
'fr-FR': 'Français'
} as Record<string, string>,
get builtInThemes(): Record<string, string> {
return {
dark: Locale.setGenThemeDark,
light: Locale.setGenThemeLight,
sd: Locale.setGenThemeSd,
sl: Locale.setGenThemeSl,
fb: Locale.setGenThemeFb,
bl: Locale.setGenThemeBl,
db: Locale.setGenThemeDb,
lb: Locale.setGenThemeLb,
te: Locale.setGenThemeTe,
lt: Locale.setGenThemeLt,
dc: Locale.setGenThemeDc,
hc: Locale.setGenThemeHc
};
},
get allThemes(): Record<string, string> {
return {
...this.builtInThemes,
...this.customThemes
};
},
// changing something here? don't forget about desktop/app.js
get autoSwitchedThemes(): { name: string; dark: string; light: string }[] {
return [
{
name: Locale.setGenThemeDefault,
dark: 'dark',
light: 'light'
},
{
name: Locale.setGenThemeSol,
dark: 'sd',
light: 'sl'
},
{
name: Locale.setGenThemeBlue,
dark: 'fb',
light: 'bl'
},
{
name: Locale.setGenThemeBrown,
dark: 'db',
light: 'lb'
},
{
name: Locale.setGenThemeTerminal,
dark: 'te',
light: 'lt'
},
{
name: Locale.setGenThemeHighContrast,
dark: 'dc',
light: 'hc'
}
];
},
customLocales: new Map<string, Record<string, string>>(),
customThemes: {} as Record<string, string>,
init(): void {
Events.on('dark-mode-changed', () => this.darkModeChanged());
},
setBySettings(): void {
this.setTheme(AppSettings.theme);
this.setFontSize(AppSettings.fontSize);
const locale = AppSettings.locale;
try {
if (locale) {
this.setLocale(AppSettings.locale);
} else {
this.setLocale(this.getBrowserLocale());
}
} catch (ex) {}
},
getDefaultTheme(): string {
return 'dark';
},
setTheme(theme: string | undefined | null): void {
if (!theme) {
if (this.activeTheme) {
return;
}
theme = this.getDefaultTheme();
}
for (const cls of document.body.classList) {
if (/^th-/.test(cls)) {
document.body.classList.remove(cls);
}
}
if (AppSettings.autoSwitchTheme) {
theme = this.selectDarkOrLightTheme(theme);
}
WindowClass.setThemeClass(theme);
const metaThemeColor = document.head.querySelector('meta[name=theme-color]') as
| HTMLMetaElement
| undefined;
if (metaThemeColor) {
metaThemeColor.content = window.getComputedStyle(document.body).backgroundColor;
}
this.activeTheme = theme;
logger.info('Theme changed', theme);
Events.emit('theme-changed');
},
selectDarkOrLightTheme(theme: string): string {
for (const config of this.autoSwitchedThemes) {
if (config.light === theme || config.dark === theme) {
return ThemeWatcher.dark ? config.dark : config.light;
}
}
return theme;
},
darkModeChanged(): void {
if (AppSettings.autoSwitchTheme) {
for (const config of this.autoSwitchedThemes) {
if (config.light === this.activeTheme || config.dark === this.activeTheme) {
const newTheme = ThemeWatcher.dark ? config.dark : config.light;
logger.info('Setting theme triggered by system settings change', newTheme);
this.setTheme(newTheme);
break;
}
}
}
},
setFontSize(fontSize: AppSettingsFontSize): void {
const defaultFontSize = Features.isMobile ? 14 : 12;
const sizeInPx = defaultFontSize + (fontSize || 0) * 2;
document.documentElement.style.fontSize = `${sizeInPx}px`;
WindowClass.setFontSizeClass(fontSize);
},
setLocale(loc: string | undefined | null): void {
if (!loc || loc === Locale.localeName) {
return;
}
if (loc === 'en-US') {
Locale.set(undefined);
} else {
let localeValues = this.customLocales.get(loc);
if (!localeValues) {
localeValues = require('locales/' + loc + '.json') as Record<string, string>;
}
Locale.set(localeValues, loc);
}
Events.emit('locale-changed', loc);
if (Launcher) {
const localeValuesForDesktopApp = this.getDesktopAppLocaleValues();
Launcher.ipcRenderer.invoke('set-locale', loc, localeValuesForDesktopApp).catch(noop);
}
},
getBrowserLocale(): string {
const language = (navigator.languages && navigator.languages[0]) || navigator.language;
if (language && language.startsWith('en')) {
return 'en-US';
}
return language;
},
getDesktopAppLocaleValues(): Record<string, string> {
return {
sysMenuAboutKeeWeb: Locale.sysMenuAboutKeeWeb.with('KeeWeb'),
sysMenuServices: Locale.sysMenuServices,
sysMenuHide: Locale.sysMenuHide.with('KeeWeb'),
sysMenuHideOthers: Locale.sysMenuHideOthers,
sysMenuUnhide: Locale.sysMenuUnhide,
sysMenuQuit: Locale.sysMenuQuit.with('KeeWeb'),
sysMenuEdit: Locale.sysMenuEdit,
sysMenuUndo: Locale.sysMenuUndo,
sysMenuRedo: Locale.sysMenuRedo,
sysMenuCut: Locale.sysMenuCut,
sysMenuCopy: Locale.sysMenuCopy,
sysMenuPaste: Locale.sysMenuPaste,
sysMenuSelectAll: Locale.sysMenuSelectAll,
sysMenuWindow: Locale.sysMenuWindow,
sysMenuMinimize: Locale.sysMenuMinimize,
sysMenuClose: Locale.sysMenuClose
};
}
};
export { SettingsManager };
@@ -1,38 +0,0 @@
import { Launcher } from 'comp/launcher';
import { StringFormat } from 'util/formatting/string-format';
import { Logger } from 'util/logger';
const logger = new Logger('settings');
const SettingsStore = {
load(key) {
let loadPromise;
if (Launcher) {
loadPromise = Launcher.loadConfig(key);
} else {
loadPromise = Promise.resolve().then(() => {
return localStorage[StringFormat.camelCase(key)];
});
}
return loadPromise
.then((data) => (data ? JSON.parse(data) : null))
.catch((err) => {
logger.error(`Error loading ${key}`, err);
});
},
save(key, data) {
if (Launcher) {
return Launcher.saveConfig(key, JSON.stringify(data)).catch((err) => {
logger.error(`Error saving ${key}`, err);
});
}
return Promise.resolve().then(() => {
if (typeof localStorage !== 'undefined') {
localStorage[StringFormat.camelCase(key)] = JSON.stringify(data);
}
});
}
};
export { SettingsStore };
@@ -0,0 +1,46 @@
import { Launcher } from 'comp/launcher';
import { StringFormat } from 'util/formatting/string-format';
import { Logger } from 'util/logger';
const logger = new Logger('settings');
const SettingsStore = {
async load(key: string): Promise<unknown> {
let data: string | null;
if (Launcher) {
data = await Launcher.ipcRenderer.invoke('load-config', key);
} else {
data = localStorage?.getItem(StringFormat.camelCase(key));
}
if (!data) {
return Promise.resolve();
}
try {
const parsed = JSON.parse(data) as unknown;
if (typeof parsed !== 'object' || !parsed) {
logger.error(`Error loading ${key}: read not an object`, parsed);
return Promise.resolve();
}
return Promise.resolve(parsed);
} catch (err) {
logger.error(`Error loading ${key}`, err);
return Promise.resolve();
}
},
async save(key: string, data: unknown): Promise<void> {
const dataStr = JSON.stringify(data);
if (Launcher) {
try {
await Launcher.ipcRenderer.invoke('save-config', key, dataStr);
} catch (err) {
logger.error(`Error saving ${key}`, err);
}
} else {
localStorage?.setItem(StringFormat.camelCase(key), dataStr);
}
return Promise.resolve();
}
};
export { SettingsStore };
-122
View File
@@ -1,122 +0,0 @@
import { Locale } from 'util/locale';
import { ModalView } from 'views/modal-view';
const Alerts = {
alertDisplayed: false,
buttons: {
ok: {
result: 'yes',
get title() {
return Locale.alertOk;
}
},
yes: {
result: 'yes',
get title() {
return Locale.alertYes;
}
},
allow: {
result: 'yes',
get title() {
return Locale.alertAllow;
}
},
no: {
result: '',
get title() {
return Locale.alertNo;
}
},
cancel: {
result: '',
get title() {
return Locale.alertCancel;
}
},
deny: {
result: '',
get title() {
return Locale.alertDeny;
}
}
},
alert(config) {
if (config.skipIfAlertDisplayed && Alerts.alertDisplayed) {
return null;
}
Alerts.alertDisplayed = true;
const view = new ModalView(config);
view.render();
view.once('result', (res, check) => {
if (res && config.success) {
config.success(res, check);
}
if (!res && config.cancel) {
config.cancel();
}
if (config.complete) {
config.complete(res, check);
}
});
view.on('will-close', () => {
Alerts.alertDisplayed = false;
});
return view;
},
notImplemented() {
this.alert({
header: Locale.notImplemented,
body: '',
icon: 'exclamation-triangle',
buttons: [this.buttons.ok],
esc: '',
click: '',
enter: ''
});
},
info(config) {
this.alert({
header: '',
body: '',
icon: 'info',
buttons: [this.buttons.ok],
esc: '',
click: '',
enter: '',
...config
});
},
error(config) {
this.alert({
header: '',
body: '',
icon: 'exclamation-circle',
buttons: [this.buttons.ok],
esc: '',
click: '',
enter: '',
...config
});
},
yesno(config) {
this.alert({
header: '',
body: '',
icon: 'question',
buttons: [this.buttons.yes, this.buttons.no],
esc: '',
click: '',
enter: 'yes',
...config
});
}
};
export { Alerts };
+305
View File
@@ -0,0 +1,305 @@
import { FunctionComponent, h, render } from 'preact';
import { Locale } from 'util/locale';
import { Modal, ModalEmitterEvents } from 'ui/modal';
import { TypedEmitter } from 'tiny-typed-emitter';
import { Timeouts } from 'const/timeouts';
import { KeyHandler } from 'comp/browser/key-handler';
import { Keys } from 'const/keys';
import { Callback } from 'util/types';
import { FocusManager } from 'comp/app/focus-manager';
export interface AlertButton {
result: string;
title: string;
error?: boolean;
silent?: boolean;
}
export interface AlertConfig {
header: string;
body: string | string[];
icon?: string;
buttons?: AlertButton[];
esc?: string;
click?: string;
enter?: string;
skipIfAlertDisplayed?: boolean;
pre?: string;
hint?: string;
opaque?: boolean;
wide?: boolean;
checkbox?: string;
view?: FunctionComponent;
success?: (result: string, checked?: boolean) => void;
complete?: (result: string, checked?: boolean) => void;
cancel?: () => void;
}
let alertDisplayed = false;
export class Alert {
readonly config: AlertConfig;
private _promise?: Promise<string>;
private _resolve?: (result: string) => void;
private _emitter?: TypedEmitter<ModalEmitterEvents>;
private _result: string | undefined;
private _checked: boolean | undefined;
private _visible = false;
private _el?: HTMLElement;
private _listeners: Callback[] = [];
constructor(config: AlertConfig) {
this.config = config;
}
get result(): string | undefined {
return this._result;
}
get visible(): boolean {
return this._visible;
}
show(): void {
this._emitter = new TypedEmitter<ModalEmitterEvents>();
const vnode = h(Modal, {
emitter: this._emitter,
header: this.config.header,
body: typeof this.config.body === 'string' ? [this.config.body] : this.config.body,
opaque: this.config.opaque,
wide: this.config.wide,
icon: this.config.icon,
pre: this.config.pre,
link: this.config.checkbox,
hint: this.config.hint,
checkbox: this.config.checkbox,
buttons: this.config.buttons ?? [],
view: this.config.view,
modalClicked: () => this.modalClicked(),
buttonClicked: (result) => this.buttonClicked(result),
checkboxChanged: () => this.checkboxChanged()
});
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
this._el = document.createElement('div');
document.body.appendChild(this._el);
render(vnode, document.body, this._el);
this.listenKeys();
FocusManager.pushModal('alert');
this._visible = true;
alertDisplayed = true;
}
wait(): Promise<string | undefined> {
if (!this._visible) {
return Promise.resolve(this._result);
}
if (!this._promise) {
this._promise = new Promise((resolve) => {
this._resolve = resolve;
});
}
return this._promise;
}
closeWithResult(result: string, immediate?: boolean): void {
this._visible = false;
alertDisplayed = false;
for (const off of this._listeners) {
off();
}
FocusManager.popModal();
this._result = result;
if (result) {
this.config.success?.(result, this._checked);
} else {
this.config.cancel?.();
}
this.config.complete?.(result, this._checked);
this._resolve?.(result);
if (immediate) {
this.destroy();
} else {
this._emitter?.emit('hide');
setTimeout(() => {
this.destroy();
}, Timeouts.HideAlert);
}
}
closeImmediate(): void {
if (this._el) {
this.closeWithResult('', true);
}
}
change(config: { header?: string }): void {
this._emitter?.emit('change-config', config);
}
private modalClicked() {
if (typeof this.config.click === 'string') {
this.closeWithResult('');
}
}
private buttonClicked(result: string) {
this.closeWithResult(result);
}
private checkboxChanged() {
this._checked = !this._checked;
}
private escPressed() {
this.closeWithResult(this.config.esc || '');
}
private enterPressed(e: Event) {
e.stopImmediatePropagation();
e.preventDefault();
this.closeWithResult(this.config.enter || '');
}
private listenKeys() {
if (typeof this.config.esc === 'string') {
const offKey = KeyHandler.onKey(
Keys.DOM_VK_ESCAPE,
() => this.escPressed(),
undefined,
'alert'
);
this._listeners.push(offKey);
}
if (typeof this.config.enter === 'string') {
const offKey = KeyHandler.onKey(
Keys.DOM_VK_RETURN,
(e) => this.enterPressed(e),
undefined,
'alert'
);
this._listeners.push(offKey);
}
}
private destroy() {
if (!this._el) {
return;
}
render(null, this._el);
this._el?.remove();
this._el = undefined;
}
}
export const Alerts = {
get alertDisplayed(): boolean {
return alertDisplayed;
},
buttons: {
ok: {
result: 'yes',
get title() {
return Locale.alertOk;
}
},
yes: {
result: 'yes',
get title() {
return Locale.alertYes;
}
},
allow: {
result: 'yes',
get title() {
return Locale.alertAllow;
}
},
no: {
result: '',
get title() {
return Locale.alertNo;
}
},
cancel: {
result: '',
get title() {
return Locale.alertCancel;
}
},
deny: {
result: '',
get title() {
return Locale.alertDeny;
}
}
} as Record<string, AlertButton>,
alert(config: AlertConfig): Alert {
const alert = new Alert(config);
if (config.skipIfAlertDisplayed && Alerts.alertDisplayed) {
return alert;
}
alert.show();
return alert;
},
notImplemented(): Alert {
return this.alert({
header: Locale.notImplemented,
body: '',
icon: 'exclamation-triangle',
buttons: [this.buttons.ok],
esc: '',
click: '',
enter: ''
});
},
info(config: AlertConfig): Alert {
return this.alert({
icon: 'info',
buttons: [this.buttons.ok],
esc: '',
click: '',
enter: '',
...config
});
},
error(config: AlertConfig): Alert {
return this.alert({
icon: 'exclamation-circle',
buttons: [this.buttons.ok],
esc: '',
click: '',
enter: '',
...config
});
},
yesno(config: AlertConfig): Alert {
return this.alert({
icon: 'question',
buttons: [this.buttons.yes, this.buttons.no],
esc: '',
click: '',
enter: 'yes',
...config
});
}
};
@@ -4,12 +4,14 @@
// which you embed in the source code of your application.
// (In this context, the client secret is obviously not treated as a secret.)
const DropboxApps = {
export const DropboxApps = {
AppFolder: { id: 'qp7ctun6qt5n9d6', secret: '07s5r4ck1uvlj6a' },
FullDropbox: { id: 'eor7hvv6u6oslq9', secret: 'ez04o1iwf6yprq3' }
};
} as const;
const GDriveApps = {
export const DropboxChooserAppKey = 'qp7ctun6qt5n9d6';
export const GDriveApps = {
Local: {
id: '783608538594-36tkdh8iscrq8t8dq87gghubnhivhjp5.apps.googleusercontent.com',
secret: 'yAtyfc9TIQ9GyQgQmo3i0HAP'
@@ -22,32 +24,19 @@ const GDriveApps = {
id: '847548101761-h2pcl2p6m1tssnlqm0vrm33crlveccbr.apps.googleusercontent.com',
secret: 'nTSCiqXtUNmURIIdASaC1TJK'
}
};
} as const;
const OneDriveApps = {
export const OneDriveApps = {
Local: {
id: 'b97c53d5-db5b-4124-aab9-d39195293815'
id: 'b97c53d5-db5b-4124-aab9-d39195293815',
secret: null
},
Production: {
id: 'bbc74d1b-3a9c-46e6-9da4-4c645e830923'
id: 'bbc74d1b-3a9c-46e6-9da4-4c645e830923',
secret: null
},
Desktop: {
id: 'bbc74d1b-3a9c-46e6-9da4-4c645e830923',
secret: 'aOMJaktJEAs_Tmh]fx4iQ[Zd3mp3KK7-'
}
};
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 };
} as const;
@@ -1,5 +1,5 @@
const Colors = {
AllColors: ['yellow', 'green', 'red', 'orange', 'blue', 'violet'],
export const Colors = {
AllColors: ['yellow', 'green', 'red', 'orange', 'blue', 'violet'] as const,
ColorsValues: {
yellow: 'ffff00',
@@ -8,7 +8,7 @@ const Colors = {
orange: 'ff8800',
blue: '0000ff',
violet: 'ff00ff'
},
} as Record<string, string>,
BgColors: {
yellow: 'ffff88',
@@ -17,7 +17,5 @@ const Colors = {
orange: 'ffcc88',
blue: '8888ff',
violet: 'ff88ff'
}
};
export { Colors };
} as Record<string, string>
} as const;
-99
View File
@@ -1,99 +0,0 @@
const DefaultAppSettings = {
theme: null, // UI theme
autoSwitchTheme: false, // automatically switch between light and dark theme
locale: null, // user interface language
expandGroups: true, // show entries from all subgroups
listViewWidth: null, // width of the entry list representation
menuViewWidth: null, // width of the left menu
tagsViewHeight: null, // tags menu section height
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, -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
minimizeOnFieldCopy: false, // minimise the app on copy
tableView: false, // view entries as a table instead of list
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
lockOnAutoType: false, // lock the app after performing auto-type
lockOnOsLock: false, // lock the app when the computer is locked
helpTipCopyShown: false, // disable the tooltip about copying fields
templateHelpShown: false, // disable the tooltip about entry templates
skipOpenLocalWarn: false, // disable the warning about opening a local file
hideEmptyFields: false, // hide empty fields in entries
skipHttpsWarning: false, // disable the non-HTTPS warning
demoOpened: false, // hide the demo button inside the More... menu
fontSize: 0, // font size: 0, 1, 2
tableViewColumns: null, // columns displayed in the table view
generatorPresets: null, // presets used in the password generator
generatorHidePassword: false, // hide password in the generator
cacheConfigSettings: false, // cache config settings and use them if the config can't be loaded
allowIframes: false, // allow displaying the app in IFrames
useGroupIconForEntries: false, // automatically use group icon when creating new entries
enableUsb: true, // enable interaction with USB devices
fieldLabelDblClickAutoType: false, // trigger auto-type by doubleclicking field label
auditPasswords: true, // enable password audit
auditPasswordEntropy: true, // show warnings for weak passwords
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
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
yubiKeyMatchEntries: true, // show matching one-time codes in entries
yubiKeyShowChalResp: true, // show YubiKey challenge-response option
yubiKeyRememberChalResp: false, // remember YubiKey challenge-response codes while the app is open
yubiKeyStuckWorkaround: false, // enable the workaround for stuck YubiKeys
canOpen: true, // can select and open new files
canOpenDemo: true, // can open a demo file
canOpenSettings: true, // can go to settings
canCreate: true, // can create new files
canImportXml: true, // can import files from XML
canImportCsv: true, // can import files from CSV
canRemoveLatest: true, // can remove files from the recent file list
canExportXml: true, // can export files as XML
canExportHtml: true, // can export files as HTML
canSaveTo: true, // can save existing files to filesystem
canOpenStorage: true, // can open files from cloud storage providers
canOpenGenerator: true, // can open password generator
canOpenOtpDevice: true, // can open OTP codes from USB tokens
dropbox: true, // enable Dropbox integration
dropboxFolder: null, // default folder path
dropboxAppKey: null, // custom Dropbox app key
dropboxSecret: null, // custom Dropbox app secret
webdav: true, // enable WebDAV integration
webdavSaveMethod: 'move', // how to save files with WebDAV: "move" or "put"
webdavStatReload: false, // WebDAV: reload the file instead of relying on Last-Modified
gdrive: true, // enable Google Drive integration
gdriveClientId: null, // custom Google Drive client id
gdriveClientSecret: null, // custom Google Drive client secret
onedrive: true, // enable OneDrive integration
onedriveClientId: null, // custom OneDrive client id
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 };
@@ -1,4 +1,4 @@
const BuiltInFields = [
export const BuiltInFields = [
'Title',
'Password',
'UserName',
@@ -8,5 +8,3 @@ const BuiltInFields = [
'TOTP Settings',
'_etm_template_uuid'
];
export { BuiltInFields };
@@ -1,14 +1,12 @@
// https://support.yubico.com/support/solutions/articles/15000028104-yubikey-usb-id-values
// https://github.com/Yubico/yubikey-personalization/blob/master/ykcore/ykdef.h#L274
const YubiKeyVendorId = 0x1050;
export const YubiKeyVendorId = 0x1050;
const YubiKeyProductIds = {
export const YubiKeyProductIds = {
Gen1: [0x0010],
NEO: [0x0110, 0x0112, 0x0113, 0x0114, 0x0115, 0x0116],
YK4: [0x0401, 0x0402, 0x0403, 0x0404, 0x0405, 0x0406, 0x0407]
};
const YubiKeyChallengeSize = 64;
export { YubiKeyVendorId, YubiKeyProductIds, YubiKeyChallengeSize };
export const YubiKeyChallengeSize = 64;
@@ -1,4 +1,4 @@
const IconMap = [
export const IconMap = [
'key',
'globe',
'exclamation-triangle',
@@ -69,5 +69,3 @@ const IconMap = [
'signature',
'mobile'
];
export { IconMap };
-119
View File
@@ -1,119 +0,0 @@
const Keys = {
DOM_VK_CANCEL: 3,
DOM_VK_HELP: 6,
DOM_VK_BACK_SPACE: 8,
DOM_VK_TAB: 9,
DOM_VK_CLEAR: 12,
DOM_VK_RETURN: 13,
DOM_VK_ENTER: 14,
DOM_VK_SHIFT: 16,
DOM_VK_CONTROL: 17,
DOM_VK_ALT: 18,
DOM_VK_PAUSE: 19,
DOM_VK_CAPS_LOCK: 20,
DOM_VK_ESCAPE: 27,
DOM_VK_SPACE: 32,
DOM_VK_PAGE_UP: 33,
DOM_VK_PAGE_DOWN: 34,
DOM_VK_END: 35,
DOM_VK_HOME: 36,
DOM_VK_LEFT: 37,
DOM_VK_UP: 38,
DOM_VK_RIGHT: 39,
DOM_VK_DOWN: 40,
DOM_VK_PRINTSCREEN: 44,
DOM_VK_INSERT: 45,
DOM_VK_DELETE: 46,
DOM_VK_0: 48,
DOM_VK_1: 49,
DOM_VK_2: 50,
DOM_VK_3: 51,
DOM_VK_4: 52,
DOM_VK_5: 53,
DOM_VK_6: 54,
DOM_VK_7: 55,
DOM_VK_8: 56,
DOM_VK_9: 57,
DOM_VK_SEMICOLON: 59,
DOM_VK_EQUALS: 61,
DOM_VK_A: 65,
DOM_VK_B: 66,
DOM_VK_C: 67,
DOM_VK_D: 68,
DOM_VK_E: 69,
DOM_VK_F: 70,
DOM_VK_G: 71,
DOM_VK_H: 72,
DOM_VK_I: 73,
DOM_VK_J: 74,
DOM_VK_K: 75,
DOM_VK_L: 76,
DOM_VK_M: 77,
DOM_VK_N: 78,
DOM_VK_O: 79,
DOM_VK_P: 80,
DOM_VK_Q: 81,
DOM_VK_R: 82,
DOM_VK_S: 83,
DOM_VK_T: 84,
DOM_VK_U: 85,
DOM_VK_V: 86,
DOM_VK_W: 87,
DOM_VK_X: 88,
DOM_VK_Y: 89,
DOM_VK_Z: 90,
DOM_VK_CONTEXT_MENU: 93,
DOM_VK_NUMPAD0: 96,
DOM_VK_NUMPAD1: 97,
DOM_VK_NUMPAD2: 98,
DOM_VK_NUMPAD3: 99,
DOM_VK_NUMPAD4: 100,
DOM_VK_NUMPAD5: 101,
DOM_VK_NUMPAD6: 102,
DOM_VK_NUMPAD7: 103,
DOM_VK_NUMPAD8: 104,
DOM_VK_NUMPAD9: 105,
DOM_VK_MULTIPLY: 106,
DOM_VK_ADD: 107,
DOM_VK_SEPARATOR: 108,
DOM_VK_SUBTRACT: 109,
DOM_VK_DECIMAL: 110,
DOM_VK_DIVIDE: 111,
DOM_VK_F1: 112,
DOM_VK_F2: 113,
DOM_VK_F3: 114,
DOM_VK_F4: 115,
DOM_VK_F5: 116,
DOM_VK_F6: 117,
DOM_VK_F7: 118,
DOM_VK_F8: 119,
DOM_VK_F9: 120,
DOM_VK_F10: 121,
DOM_VK_F11: 122,
DOM_VK_F12: 123,
DOM_VK_F13: 124,
DOM_VK_F14: 125,
DOM_VK_F15: 126,
DOM_VK_F16: 127,
DOM_VK_F17: 128,
DOM_VK_F18: 129,
DOM_VK_F19: 130,
DOM_VK_F20: 131,
DOM_VK_F21: 132,
DOM_VK_F22: 133,
DOM_VK_F23: 134,
DOM_VK_F24: 135,
DOM_VK_NUM_LOCK: 144,
DOM_VK_SCROLL_LOCK: 145,
DOM_VK_COMMA: 188,
DOM_VK_PERIOD: 190,
DOM_VK_SLASH: 191,
DOM_VK_BACK_QUOTE: 192,
DOM_VK_OPEN_BRACKET: 219,
DOM_VK_BACK_SLASH: 220,
DOM_VK_CLOSE_BRACKET: 221,
DOM_VK_QUOTE: 222,
DOM_VK_META: 224
};
export { Keys };
+117
View File
@@ -0,0 +1,117 @@
export enum Keys {
DOM_VK_CANCEL = 3,
DOM_VK_HELP = 6,
DOM_VK_BACK_SPACE = 8,
DOM_VK_TAB = 9,
DOM_VK_CLEAR = 12,
DOM_VK_RETURN = 13,
DOM_VK_ENTER = 14,
DOM_VK_SHIFT = 16,
DOM_VK_CONTROL = 17,
DOM_VK_ALT = 18,
DOM_VK_PAUSE = 19,
DOM_VK_CAPS_LOCK = 20,
DOM_VK_ESCAPE = 27,
DOM_VK_SPACE = 32,
DOM_VK_PAGE_UP = 33,
DOM_VK_PAGE_DOWN = 34,
DOM_VK_END = 35,
DOM_VK_HOME = 36,
DOM_VK_LEFT = 37,
DOM_VK_UP = 38,
DOM_VK_RIGHT = 39,
DOM_VK_DOWN = 40,
DOM_VK_PRINTSCREEN = 44,
DOM_VK_INSERT = 45,
DOM_VK_DELETE = 46,
DOM_VK_0 = 48,
DOM_VK_1 = 49,
DOM_VK_2 = 50,
DOM_VK_3 = 51,
DOM_VK_4 = 52,
DOM_VK_5 = 53,
DOM_VK_6 = 54,
DOM_VK_7 = 55,
DOM_VK_8 = 56,
DOM_VK_9 = 57,
DOM_VK_SEMICOLON = 59,
DOM_VK_EQUALS = 61,
DOM_VK_A = 65,
DOM_VK_B = 66,
DOM_VK_C = 67,
DOM_VK_D = 68,
DOM_VK_E = 69,
DOM_VK_F = 70,
DOM_VK_G = 71,
DOM_VK_H = 72,
DOM_VK_I = 73,
DOM_VK_J = 74,
DOM_VK_K = 75,
DOM_VK_L = 76,
DOM_VK_M = 77,
DOM_VK_N = 78,
DOM_VK_O = 79,
DOM_VK_P = 80,
DOM_VK_Q = 81,
DOM_VK_R = 82,
DOM_VK_S = 83,
DOM_VK_T = 84,
DOM_VK_U = 85,
DOM_VK_V = 86,
DOM_VK_W = 87,
DOM_VK_X = 88,
DOM_VK_Y = 89,
DOM_VK_Z = 90,
DOM_VK_CONTEXT_MENU = 93,
DOM_VK_NUMPAD0 = 96,
DOM_VK_NUMPAD1 = 97,
DOM_VK_NUMPAD2 = 98,
DOM_VK_NUMPAD3 = 99,
DOM_VK_NUMPAD4 = 100,
DOM_VK_NUMPAD5 = 101,
DOM_VK_NUMPAD6 = 102,
DOM_VK_NUMPAD7 = 103,
DOM_VK_NUMPAD8 = 104,
DOM_VK_NUMPAD9 = 105,
DOM_VK_MULTIPLY = 106,
DOM_VK_ADD = 107,
DOM_VK_SEPARATOR = 108,
DOM_VK_SUBTRACT = 109,
DOM_VK_DECIMAL = 110,
DOM_VK_DIVIDE = 111,
DOM_VK_F1 = 112,
DOM_VK_F2 = 113,
DOM_VK_F3 = 114,
DOM_VK_F4 = 115,
DOM_VK_F5 = 116,
DOM_VK_F6 = 117,
DOM_VK_F7 = 118,
DOM_VK_F8 = 119,
DOM_VK_F9 = 120,
DOM_VK_F10 = 121,
DOM_VK_F11 = 122,
DOM_VK_F12 = 123,
DOM_VK_F13 = 124,
DOM_VK_F14 = 125,
DOM_VK_F15 = 126,
DOM_VK_F16 = 127,
DOM_VK_F17 = 128,
DOM_VK_F18 = 129,
DOM_VK_F19 = 130,
DOM_VK_F20 = 131,
DOM_VK_F21 = 132,
DOM_VK_F22 = 133,
DOM_VK_F23 = 134,
DOM_VK_F24 = 135,
DOM_VK_NUM_LOCK = 144,
DOM_VK_SCROLL_LOCK = 145,
DOM_VK_COMMA = 188,
DOM_VK_PERIOD = 190,
DOM_VK_SLASH = 191,
DOM_VK_BACK_QUOTE = 192,
DOM_VK_OPEN_BRACKET = 219,
DOM_VK_BACK_SLASH = 220,
DOM_VK_CLOSE_BRACKET = 221,
DOM_VK_QUOTE = 222,
DOM_VK_META = 224
}
-5
View File
@@ -1,5 +0,0 @@
const KnownAppVersions = {
KeePassXC: '2.6.4'
};
export { KnownAppVersions };
+3
View File
@@ -0,0 +1,3 @@
export const KnownAppVersions = {
KeePassXC: '2.6.4'
};
@@ -1,4 +1,4 @@
const Links = {
export const Links = {
Homepage: 'https://keeweb.info',
Repo: 'https://github.com/keeweb/keeweb',
Desktop: 'https://github.com/keeweb/keeweb/releases/latest',
@@ -23,14 +23,15 @@ const Links = {
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'
KWCForSafari: 'https://apps.apple.com/app/keeweb-connect/id1565748094',
KPXCForChrome:
'https://chrome.google.com/webstore/detail/keepassxc-browser/oboonakemofpalcgghocfoadofidjkkk',
KPXCForFirefox: 'https://addons.mozilla.org/firefox/addon/keepassxc-browser/',
KPXCForEdge:
'https://microsoftedge.microsoft.com/addons/detail/keepassxcbrowser/pdffhmdngciaglkoonimfcmckehcpafo'
};
export { Links };
@@ -1,4 +1,4 @@
const RuntimeInfo = {
export const RuntimeInfo = {
version: '@@VERSION',
beta: !!'@@BETA',
buildDate: '@@DATE',
@@ -6,5 +6,3 @@ const RuntimeInfo = {
devMode: '@@DEVMODE',
appleTeamId: '@@APPLE_TEAM_ID'
};
export { RuntimeInfo };
@@ -1,5 +1,8 @@
const Timeouts = {
export const Timeouts = {
ShowTip: 200,
HideTip: 500,
CopyTip: 1500,
HideAlert: 150,
AutoHideHint: 3000,
FileChangeSync: 3000,
BeforeAutoLock: 300,
@@ -19,7 +22,13 @@ const Timeouts = {
NativeModuleHostRestartTime: 3000,
FastAnimation: 100,
AutoTypeCopyPaste: 300,
KeeWebConnectRequest: 60000
KeeWebConnectRequest: 60000,
UpdateInterval: 1000 * 60 * 60 * 24,
MinUpdateTimeout: 500,
InputShake: 1000,
WindowResizeUpdateDebounce: 200,
SaveFileInfoDebounce: 100,
PluginsAutoUpdateAfterStart: 2000,
PluginsUpdate: 1000 * 60 * 60 * 24 * 7,
ContextMenuCleanup: 1000
};
export { Timeouts };
@@ -143,7 +143,7 @@ const BrowserExtensionConnector = {
if (e?.data?.kwConnect !== 'request') {
return;
}
logger.debug('Extension -> KeeWeb', e.data);
logger.info('Extension -> KeeWeb', e.data);
pendingBrowserMessages.push(e.data);
this.processBrowserMessages();
},
@@ -173,7 +173,7 @@ const BrowserExtensionConnector = {
},
sendWebResponse(response) {
logger.debug('KeeWeb -> Extension', response);
logger.info('KeeWeb -> Extension', response);
response.kwConnect = 'response';
postMessage(response, window.location.origin);
},
@@ -102,7 +102,7 @@ function decryptRequest(request) {
const json = new TextDecoder().decode(data);
const payload = JSON.parse(json);
logger.debug('Extension -> KeeWeb -> (decrypted)', payload);
logger.info('Extension -> KeeWeb -> (decrypted)', payload);
if (!payload) {
throw new Error('Empty request payload');
@@ -115,7 +115,7 @@ function decryptRequest(request) {
}
function encryptResponse(request, payload) {
logger.debug('KeeWeb -> Extension (decrypted)', payload);
logger.info('KeeWeb -> Extension (decrypted)', payload);
const nonceBytes = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
incrementNonce(nonceBytes);
@@ -280,7 +280,7 @@ function getHumanReadableExtensionName(client) {
}
function focusKeeWeb() {
logger.debug('Focus KeeWeb');
logger.info('Focus KeeWeb');
if (Launcher) {
if (!Launcher.isAppFocused()) {
Launcher.showMainWindow();
-251
View File
@@ -1,251 +0,0 @@
import EventEmitter from 'events';
const SymbolEvents = Symbol('events');
const SymbolArray = Symbol('array');
function emitSet(target, value, prevValue) {
const emitter = target[SymbolEvents];
if (!emitter.paused) {
const updates = { added: [], removed: [] };
if (prevValue) {
emitter.emit('remove', prevValue, target);
updates.removed.push(prevValue);
}
if (value) {
emitter.emit('add', value, target);
updates.added.push(value);
}
emitter.emit('change', updates, target);
}
}
function emitRemoved(target, removed) {
const emitter = target[SymbolEvents];
if (!emitter.paused) {
for (const item of removed) {
emitter.emit('remove', item, target);
}
emitter.emit('change', { added: [], removed }, target);
}
}
function checkType(target, value) {
const modelClass = target.constructor.model;
if (!modelClass) {
throw new Error(`Model type not defined for ${target.constructor.name}`);
}
if (!(value instanceof modelClass)) {
const valueType = value && value.constructor ? value.constructor.name : typeof value;
throw new Error(`Attempt to write ${valueType} into ${target.constructor.name}`);
}
}
const ProxyDef = {
set(target, property, value) {
const numProp = parseInt(property);
if (isNaN(numProp)) {
target[property] = value;
return true;
}
checkType(target, value);
const array = target[SymbolArray];
const prevValue = array[property];
if (prevValue !== value) {
array[property] = value;
emitSet(target, value, prevValue);
}
return true;
},
get(target, property) {
if (typeof property !== 'string') {
return target[property];
}
const numProp = parseInt(property);
if (isNaN(numProp)) {
return target[property];
}
return target[SymbolArray][property];
}
};
class Collection {
constructor(items) {
const emitter = new EventEmitter();
emitter.setMaxListeners(100);
const properties = {
[SymbolEvents]: { value: emitter },
[SymbolArray]: { value: [] }
};
Object.defineProperties(this, properties);
if (items) {
this.push(...items);
}
return new Proxy(this, ProxyDef);
}
get length() {
return this[SymbolArray].length;
}
set length(value) {
const array = this[SymbolArray];
let removed;
if (value < array.length) {
removed = array.slice(value);
}
array.length = value;
if (removed) {
emitRemoved(this, removed);
}
}
push(...items) {
if (items.length) {
for (const item of items) {
checkType(this, item);
}
this[SymbolEvents].paused = true;
this[SymbolArray].push(...items);
this[SymbolEvents].paused = false;
for (const item of items) {
this[SymbolEvents].emit('add', item, this);
}
this[SymbolEvents].emit('change', { added: items, removed: [] }, this);
}
}
pop() {
this[SymbolEvents].paused = true;
const item = this[SymbolArray].pop();
this[SymbolEvents].paused = false;
if (item) {
this[SymbolEvents].emit('remove', item, this);
this[SymbolEvents].emit('change', { added: [], removed: [item] }, this);
}
return item;
}
shift() {
this[SymbolEvents].paused = true;
const item = this[SymbolArray].shift();
this[SymbolEvents].paused = false;
if (item) {
this[SymbolEvents].emit('remove', item, this);
this[SymbolEvents].emit('change', { added: [], removed: [item] }, this);
}
return item;
}
unshift(...items) {
if (items.length) {
for (const item of items) {
checkType(this, item);
}
this[SymbolEvents].paused = true;
this[SymbolArray].unshift(...items);
this[SymbolEvents].paused = false;
for (const item of items) {
this[SymbolEvents].emit('add', item, this);
}
this[SymbolEvents].emit('change', { added: items, removed: [] }, this);
}
}
splice(start, deleteCount, ...items) {
for (const item of items) {
checkType(this, item);
}
this[SymbolEvents].paused = true;
const removed = this[SymbolArray].splice(start, deleteCount, ...items);
this[SymbolEvents].paused = false;
for (const item of removed) {
this[SymbolEvents].emit('remove', item, this);
}
for (const item of items) {
this[SymbolEvents].emit('add', item, this);
}
if (removed.length || items.length) {
this[SymbolEvents].emit('change', { added: items, removed }, this);
}
}
on(eventName, listener) {
this[SymbolEvents].on(eventName, listener);
}
once(eventName, listener) {
this[SymbolEvents].once(eventName, listener);
}
off(eventName, listener) {
this[SymbolEvents].off(eventName, listener);
}
get(id) {
return this.find((model) => model.id === id);
}
remove(idOrModel) {
for (let i = 0; i < this.length; i++) {
while (i < this.length && (this[i].id === idOrModel || this[i] === idOrModel)) {
this.splice(i, 1);
}
}
}
sort() {
return this[SymbolArray].sort(this.comparator);
}
fill() {
throw new Error('Not implemented');
}
copyWithin() {
throw new Error('Not implemented');
}
toJSON() {
return this[SymbolArray].concat();
}
}
const ProxiedArrayMethods = [
Symbol.iterator,
'concat',
'entries',
'every',
'filter',
'find',
'findIndex',
'flat',
'flatMap',
'forEach',
'includes',
'indexOf',
'join',
'keys',
'lastIndexOf',
'map',
'reduce',
'reduceRight',
'reverse',
'slice',
'some',
'values'
];
for (const method of ProxiedArrayMethods) {
Object.defineProperty(Collection.prototype, method, {
value: function proxyMethod(...args) {
return this[SymbolArray][method](...args);
}
});
}
export { Collection };
-12
View File
@@ -1,12 +0,0 @@
import EventEmitter from 'events';
class Events extends EventEmitter {
constructor() {
super();
this.setMaxListeners(1000);
}
}
const instance = new Events();
export { instance as Events };
-126
View File
@@ -1,126 +0,0 @@
import EventEmitter from 'events';
import { Logger } from 'util/logger';
const SymbolEvents = Symbol('events');
const SymbolDefaults = Symbol('defaults');
const SymbolExtensions = Symbol('extensions');
function emitPropChange(target, property, value, prevValue) {
const emitter = target[SymbolEvents];
if (!emitter.paused) {
emitter.emit('change:' + property, target, value, prevValue);
if (!emitter.noChange) {
emitter.emit('change', target, { [property]: value });
}
}
}
const ProxyDef = {
deleteProperty(target, property) {
if (Object.prototype.hasOwnProperty.call(target, property)) {
const defaults = target[SymbolDefaults];
const value = defaults[property];
const prevValue = target[property];
if (prevValue !== value) {
if (Object.prototype.hasOwnProperty.call(defaults, property)) {
target[property] = value;
} else {
delete target[property];
}
emitPropChange(target, property, value, prevValue);
}
return true;
}
return true;
},
set(target, property, value, receiver) {
if (Object.prototype.hasOwnProperty.call(target, property) || target[SymbolExtensions]) {
if (target[property] !== value) {
const prevValue = target[property];
target[property] = value;
emitPropChange(target, property, value, prevValue);
}
return true;
} else {
new Logger(receiver.constructor.name).warn(
`Unknown property: ${property}`,
new Error().stack
);
}
return false;
}
};
class Model {
constructor(data) {
const emitter = new EventEmitter();
emitter.setMaxListeners(100);
const properties = {
[SymbolEvents]: { value: emitter }
};
for (const [propName, defaultValue] of Object.entries(this[SymbolDefaults])) {
properties[propName] = {
configurable: true,
enumerable: true,
writable: true,
value: defaultValue
};
}
Object.defineProperties(this, properties);
const object = new Proxy(this, ProxyDef);
if (data) {
object.set(data, { silent: true });
}
return object;
}
set(props, { silent } = {}) {
const emitter = this[SymbolEvents];
if (silent) {
emitter.paused = true;
}
emitter.noChange = true;
for (const [prop, value] of Object.entries(props)) {
this[prop] = value;
}
emitter.noChange = false;
if (silent) {
emitter.paused = false;
} else {
emitter.emit('change', this, props);
}
}
on(eventName, listener) {
this[SymbolEvents].on(eventName, listener);
}
once(eventName, listener) {
this[SymbolEvents].once(eventName, listener);
}
off(eventName, listener) {
this[SymbolEvents].off(eventName, listener);
}
emit(eventName, ...args) {
this[SymbolEvents].emit(eventName, ...args);
}
static defineModelProperties(properties, options) {
this.prototype[SymbolDefaults] = { ...this.prototype[SymbolDefaults], ...properties };
if (options && options.extensions) {
this.prototype[SymbolExtensions] = true;
}
}
static set(properties) {
this.prototype[SymbolDefaults] = properties;
}
}
export { Model };
-48
View File
@@ -1,48 +0,0 @@
import { Events } from 'framework/events';
import { AppSettingsModel } from 'models/app-settings-model';
import { Locale } from 'util/locale';
import { Tip } from 'util/ui/tip';
import { Timeouts } from 'const/timeouts';
const Copyable = {
hideFieldCopyTip() {
if (this.fieldCopyTip) {
this.fieldCopyTip.hide();
this.fieldCopyTip = null;
}
},
fieldCopied(e) {
this.hideFieldCopyTip();
const fieldLabel = e.source.labelEl;
const clipboardTime = e.copyRes.seconds;
const msg = clipboardTime
? Locale.detFieldCopiedTime.replace('{}', clipboardTime)
: Locale.detFieldCopied;
let tip;
if (!this.isHidden()) {
tip = Tip.createTip(fieldLabel[0], {
title: msg,
placement: 'right',
fast: true,
force: true,
noInit: true
});
this.fieldCopyTip = tip;
tip.show();
}
setTimeout(() => {
if (tip) {
tip.hide();
}
this.fieldCopyTip = null;
if (e.source.model.name === '$Password' && AppSettingsModel.lockOnCopy) {
setTimeout(() => {
Events.emit('lock-workspace');
}, Timeouts.BeforeAutoLock);
}
}, Timeouts.CopyTip);
}
};
export { Copyable };
-69
View File
@@ -1,69 +0,0 @@
import { Events } from 'framework/events';
const Resizable = {
listenDrag(dragView) {
this.listenTo(dragView, 'dragstart', this.dragStart);
this.listenTo(dragView, 'drag', this.drag);
this.listenTo(dragView, 'autosize', this.autoSize);
},
dragStart(e) {
this._dragInfo = this.getDragInfo(e.coord);
},
drag(e) {
const dragInfo = this._dragInfo;
let size = dragInfo.startSize + e.offset;
size = Math.max(dragInfo.min, Math.min(dragInfo.max, size));
this.$el[dragInfo.prop](size);
this.emit('view-resize', size);
Events.emit('page-geometry', { source: 'resizable' });
},
autoSize(e) {
const dragInfo = this.getDragInfo(e.coord);
if (dragInfo.auto !== undefined) {
this.$el.css(dragInfo.prop, dragInfo.auto);
} else {
this.$el.css(dragInfo.prop, '');
}
this.fixSize(dragInfo);
this.emit('view-resize', null);
Events.emit('page-geometry', { source: 'resizable' });
},
fixSize(cfg) {
const size = this.$el[cfg.prop]();
const newSize = Math.max(cfg.min, Math.min(cfg.max, size));
if (newSize !== size) {
this.$el[cfg.prop](size);
}
},
// TODO: check size on window resize
// checkSize: function() {
// if (this.maxWidth) {
// this.fixSize(this.getDragInfo('x'));
// }
// if (this.maxHeight) {
// this.fixSize(this.getDragInfo('y'));
// }
// },
getDragInfo(coord) {
const prop = coord === 'x' ? 'Width' : 'Height';
const propLower = prop.toLowerCase();
const min = this.getSizeProp('min' + prop);
const max = this.getSizeProp('max' + prop);
const auto = this.getSizeProp('auto' + prop);
const startSize = this.$el[propLower]();
return { startSize, prop: propLower, min, max, auto };
},
getSizeProp(prop) {
const member = this[prop];
return typeof member === 'function' ? member.call(this) : member;
}
};
export { Resizable };
-61
View File
@@ -1,61 +0,0 @@
import baron from 'baron';
import { Events } from 'framework/events';
import { Features } from 'util/features';
const isEnabled = !Features.isMobile;
const SymbolRemoveScrollListenerAdded = Symbol('removeScrollAdded');
const Scrollable = {
createScroll(opts) {
// opts.cssGuru = true;
if (isEnabled) {
if (this.scroll) {
this.removeScroll();
}
this.scroll = baron(opts);
if (!this[SymbolRemoveScrollListenerAdded]) {
this.once('remove', () => this.removeScroll);
this[SymbolRemoveScrollListenerAdded] = true;
}
}
this.scroller = this.$el.find('.scroller');
this.scrollerBar = this.$el.find('.scroller__bar');
this.scrollerBarWrapper = this.$el.find('.scroller__bar-wrapper');
},
removeScroll() {
if (this.scroll) {
try {
this.scroll.dispose();
} catch {}
this.scroll = null;
}
},
pageResized() {
// TODO: check size on window resize
// if (this.checkSize && (!e || e.source === 'window')) {
// this.checkSize();
// }
if (this.scroll) {
this.scroll.update();
requestAnimationFrame(() => {
if (this.scroll) {
this.scroll.update();
const barHeight = Math.round(this.scrollerBar.height());
const wrapperHeight = Math.round(this.scrollerBarWrapper.height());
this.scrollerBarWrapper.toggleClass('invisible', barHeight >= wrapperHeight);
}
});
}
},
initScroll() {
if (isEnabled) {
this.listenTo(Events, 'page-geometry', this.pageResized);
}
}
};
export { Scrollable };
-290
View File
@@ -1,290 +0,0 @@
import morphdom from 'morphdom';
import EventEmitter from 'events';
import { Tip } from 'util/ui/tip';
import { KeyHandler } from 'comp/browser/key-handler';
import { FocusManager } from 'comp/app/focus-manager';
import { Logger } from 'util/logger';
const DoesNotBubble = {
mouseenter: true,
mouseleave: true,
blur: true,
focus: true
};
const DefaultTemplateOptions = {
allowProtoPropertiesByDefault: true,
allowedProtoProperties: { length: true, active: true }
};
class View extends EventEmitter {
parent = undefined;
template = undefined;
events = {};
model = undefined;
options = {};
views = {};
hidden = undefined;
removed = false;
modal = undefined;
eventListeners = {};
elementEventListeners = [];
debugLogger = localStorage.debugView ? new Logger('view', this.constructor.name) : undefined;
constructor(model = undefined, options = {}) {
super();
this.model = model;
this.options = options;
this.setMaxListeners(100);
}
render(templateData) {
if (this.removed) {
return;
}
let ts;
if (this.debugLogger) {
this.debugLogger.debug('Render start');
ts = this.debugLogger.ts();
}
if (this.el) {
Tip.destroyTips(this.el);
}
this.renderElement(templateData);
Tip.createTips(this.el);
this.debugLogger?.debug('Render finished', this.debugLogger.ts(ts));
return this;
}
renderElement(templateData) {
const html = this.template(templateData, DefaultTemplateOptions);
if (this.el) {
const mountRoot = this.options.ownParent ? this.el.firstChild : this.el;
morphdom(mountRoot, html);
this.bindElementEvents();
} else {
let parent = this.options.parent || this.parent;
if (parent) {
if (typeof parent === 'string') {
parent = document.querySelector(parent);
}
if (!parent) {
throw new Error(`Error rendering ${this.constructor.name}: parent not found`);
}
if (this.options.replace) {
Tip.destroyTips(parent);
parent.innerHTML = '';
}
const el = document.createElement('div');
el.innerHTML = html;
const root = el.firstChild;
if (this.options.ownParent) {
if (root) {
parent.appendChild(root);
}
this.el = parent;
} else {
this.el = root;
parent.appendChild(this.el);
}
if (this.modal) {
FocusManager.setModal(this.modal);
}
this.bindEvents();
} else {
throw new Error(
`Error rendering ${this.constructor.name}: I don't know how to insert the view`
);
}
this.$el = $(this.el); // legacy
}
}
bindEvents() {
const eventsMap = {};
for (const [eventDef, method] of Object.entries(this.events)) {
const spaceIx = eventDef.indexOf(' ');
let event, selector;
if (spaceIx > 0) {
event = eventDef.substr(0, spaceIx);
selector = eventDef.substr(spaceIx + 1);
if (DoesNotBubble[event]) {
this.elementEventListeners.push({ event, selector, method, els: [] });
continue;
}
} else {
event = eventDef;
}
if (!eventsMap[event]) {
eventsMap[event] = [];
}
eventsMap[event].push({ selector, method });
}
for (const [event, handlers] of Object.entries(eventsMap)) {
this.debugLogger?.debug('Bind', 'view', event, handlers);
const listener = (e) => this.eventListener(e, handlers);
this.eventListeners[event] = listener;
this.el.addEventListener(event, listener);
}
this.bindElementEvents();
}
unbindEvents() {
for (const [event, listener] of Object.entries(this.eventListeners)) {
this.el.removeEventListener(event, listener);
}
this.unbindElementEvents();
}
bindElementEvents() {
if (!this.elementEventListeners.length) {
return;
}
this.unbindElementEvents();
for (const cfg of this.elementEventListeners) {
const els = this.el.querySelectorAll(cfg.selector);
this.debugLogger?.debug('Bind', 'element', cfg.event, cfg.selector, els.length);
cfg.listener = (e) => this.eventListener(e, [cfg]);
for (const el of els) {
el.addEventListener(cfg.event, cfg.listener);
cfg.els.push(el);
}
}
}
unbindElementEvents() {
if (!this.elementEventListeners.length) {
return;
}
for (const cfg of this.elementEventListeners) {
for (const el of cfg.els) {
el.removeEventListener(cfg.event, cfg.listener);
}
cfg.els = [];
}
}
eventListener(e, handlers) {
this.debugLogger?.debug('Listener fired', e.type);
for (const { selector, method } of handlers) {
if (selector) {
const closest = e.target.closest(selector);
if (!closest || !this.el.contains(closest)) {
continue;
}
}
if (!this[method]) {
this.debugLogger?.debug('Method not defined', method);
continue;
}
this.debugLogger?.debug('Handling event', e.type, method);
this[method](e);
}
}
remove() {
if (this.modal && FocusManager.modal === this.modal) {
FocusManager.setModal(null);
}
this.emit('remove');
this.removeInnerViews();
Tip.hideTips(this.el);
this.el.remove();
this.removed = true;
this.debugLogger?.debug('Remove');
}
removeInnerViews() {
if (this.views) {
for (const view of Object.values(this.views)) {
if (view) {
if (view instanceof Array) {
view.forEach((v) => v.remove());
} else {
view.remove();
}
}
}
this.views = {};
}
}
listenTo(model, event, callback) {
const boundCallback = callback.bind(this);
model.on(event, boundCallback);
this.once('remove', () => model.off(event, boundCallback));
}
hide() {
Tip.hideTips(this.el);
return this.toggle(false);
}
show() {
return this.toggle(true);
}
toggle(visible) {
this.debugLogger?.debug(visible ? 'Show' : 'Hide');
if (visible === undefined) {
visible = this.hidden;
}
if (this.hidden === !visible) {
this.debugLogger?.debug('Toggle: noop', visible);
return;
}
this.hidden = !visible;
if (this.modal) {
if (visible) {
FocusManager.setModal(this.modal);
} else if (FocusManager.modal === this.modal) {
FocusManager.setModal(null);
}
}
if (this.el) {
this.el.classList.toggle('show', !!visible);
this.el.classList.toggle('hide', !visible);
if (!visible) {
Tip.hideTips(this.el);
}
}
this.emit(visible ? 'show' : 'hide');
}
isHidden() {
return !!this.hidden;
}
isVisible() {
return !this.hidden;
}
afterPaint(callback) {
requestAnimationFrame(() => requestAnimationFrame(callback));
}
onKey(key, handler, shortcut, modal, noPrevent) {
KeyHandler.onKey(key, handler, this, shortcut, modal, noPrevent);
this.once('remove', () => KeyHandler.offKey(key, handler, this));
}
off(event, listener) {
if (listener === undefined) {
return super.removeAllListeners(event);
} else {
return super.off(event, listener);
}
}
}
export { View, DefaultTemplateOptions };
-4
View File
@@ -1,4 +0,0 @@
import Handlebars from 'hbs';
// inspired by https://stackoverflow.com/questions/22103989/adding-offset-to-index-when-looping-through-items-in-handlebars/39588001#39588001
Handlebars.registerHelper('add', (lvalue, rvalue) => parseInt(lvalue) + parseInt(rvalue));

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