Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfc6201285 | ||
|
|
1930c265c8 | ||
|
|
1c434d7c79 | ||
|
|
c0c75ac8a8 | ||
|
|
4b7124aa57 | ||
|
|
93cee76b4f | ||
|
|
1673580dfa | ||
|
|
bf0e94582d | ||
|
|
29ae7e8ed2 | ||
|
|
9b2c40a078 | ||
|
|
f0bb382449 | ||
|
|
062ddb3f72 | ||
|
|
208a1b551e | ||
|
|
f8af64e45b | ||
|
|
98268f21c0 | ||
|
|
dcbe8d70da | ||
|
|
1d61c6ca03 | ||
|
|
433f0acf3c | ||
|
|
070573e68a | ||
|
|
946d7769f1 | ||
|
|
f0426a3ec2 | ||
|
|
b2d67818a8 | ||
|
|
c0b159d824 | ||
|
|
d590318153 | ||
|
|
5816ca9c4d | ||
|
|
6627e8c0ec | ||
|
|
379e28e8a4 | ||
|
|
4fb25451a2 | ||
|
|
f59b846643 | ||
|
|
28d7dfeb67 | ||
|
|
6cc39d2107 | ||
|
|
a92a01095b | ||
|
|
3f4cddd2a2 | ||
|
|
aa734ea976 | ||
|
|
30628004fd | ||
|
|
a6e87b52fd | ||
|
|
337ef44b56 | ||
|
|
70b4effde8 | ||
|
|
9e4c0f5c47 | ||
|
|
535cdc72f7 | ||
|
|
8d258ecab2 | ||
|
|
3079e28f6f | ||
|
|
4970290614 | ||
|
|
e348310602 | ||
|
|
7abbd2f253 | ||
|
|
c56698492e | ||
|
|
bc4c33fcbb | ||
|
|
05d5ac960c | ||
|
|
80331401b5 | ||
|
|
b4459e1247 | ||
|
|
706c82d3ca | ||
|
|
0e1b086096 | ||
|
|
80359c98f5 | ||
|
|
034e0ec2e3 | ||
|
|
cc6839d13e | ||
|
|
0ce436c2e9 | ||
|
|
966d814281 | ||
|
|
c983bc10ab | ||
|
|
d603398da0 | ||
|
|
1404d2688f | ||
|
|
c72158cf0d | ||
|
|
1e6ed1308f | ||
|
|
fae241373a | ||
|
|
2bf4cd83df | ||
|
|
efd3290ee8 | ||
|
|
aa729d9af8 | ||
|
|
2d6d3658a1 | ||
|
|
0cbdd6281a | ||
|
|
a845a42d0d | ||
|
|
60329e086a | ||
|
|
a3174b96d7 | ||
|
|
29c00066bc | ||
|
|
b90ffc4d3f | ||
|
|
7e030320f5 | ||
|
|
c96bcad32d | ||
|
|
a655cce195 | ||
|
|
aafb0e7964 | ||
|
|
288f7f117f | ||
|
|
125ac2eda8 | ||
|
|
ea80bfae07 | ||
|
|
5e727a3547 | ||
|
|
6672b60d0f | ||
|
|
09fe1a4649 | ||
|
|
e4fa4cc413 | ||
|
|
0be60475cf | ||
|
|
b7e53185b0 | ||
|
|
cd8c76b3eb | ||
|
|
a1d7a1fe48 | ||
|
|
c48153819f | ||
|
|
d2d5590887 | ||
|
|
65b319e127 | ||
|
|
b34c0dd34e | ||
|
|
ddc1a371bf | ||
|
|
a0c0b7b195 | ||
|
|
b07cea9049 | ||
|
|
991388d4cd | ||
|
|
7adc77c823 | ||
|
|
550f93d4ae | ||
|
|
b43187e5b8 | ||
|
|
6230710bb0 | ||
|
|
ff119cbc0a | ||
|
|
2a2ec713af | ||
|
|
41eb560d1a | ||
|
|
f7b9b91144 | ||
|
|
97ab5d066e | ||
|
|
448e19d28c | ||
|
|
391f2c9c00 | ||
|
|
18524ad9eb | ||
|
|
732c35625b | ||
|
|
95857450c2 | ||
|
|
98ea7a542d | ||
|
|
338f6d47d1 | ||
|
|
fdb492c572 | ||
|
|
ce3ff8c3d8 | ||
|
|
1789d9bb67 | ||
|
|
1883b527a4 | ||
|
|
2f4a9d450a | ||
|
|
12fc33a15e | ||
|
|
e4693ecf61 | ||
|
|
c15d709db2 | ||
|
|
e09040b0fb | ||
|
|
4477c3fc2c | ||
|
|
f52a00a156 | ||
|
|
946a063993 | ||
|
|
d3fcbcb6a5 | ||
|
|
f91cfc0f79 | ||
|
|
f0bbc54fd5 | ||
|
|
3ca5f94b9b | ||
|
|
7a35ebec73 | ||
|
|
66d3623f90 | ||
|
|
8c9badd440 | ||
|
|
e0d47a474d | ||
|
|
1a8aff0594 | ||
|
|
cf5e01c213 | ||
|
|
ffae956e21 | ||
|
|
5c4cb7ff6d | ||
|
|
7794c8b89b | ||
|
|
bb78e8e8a7 | ||
|
|
00a2a95adc | ||
|
|
e55dd47f6e | ||
|
|
7b10bda0a4 | ||
|
|
d5b9aefa01 | ||
|
|
cdc3f5e1df | ||
|
|
8fc6f49fb4 | ||
|
|
29e5c14009 | ||
|
|
fe9ff205b9 | ||
|
|
87b59eccee | ||
|
|
42cb5a6c36 | ||
|
|
9826965803 | ||
|
|
47153e41d3 | ||
|
|
677b428e63 | ||
|
|
15936fcef0 | ||
|
|
b2e828e7ef | ||
|
|
ac6e5a3ae8 | ||
|
|
3722477eea | ||
|
|
72d0f76acc | ||
|
|
b7333574aa | ||
|
|
5caccbd1f5 | ||
|
|
698b559935 | ||
|
|
36d5e8f730 | ||
|
|
233721bf51 | ||
|
|
822602a38f | ||
|
|
e69dbf697c | ||
|
|
184d71a1b9 | ||
|
|
ccb9b03287 | ||
|
|
dd4ad6d72b | ||
|
|
195e2cc8f0 | ||
|
|
ef573336d8 | ||
|
|
9377a849f6 | ||
|
|
00ccbf4be2 | ||
|
|
00159d26dc | ||
|
|
b4f93adf05 | ||
|
|
5bbc9003f4 | ||
|
|
c4be926b7c | ||
|
|
147e3911f1 | ||
|
|
c7b814fc01 | ||
|
|
8525571afc | ||
|
|
5c5de91672 | ||
|
|
7dbb0a2e8f | ||
|
|
11b54ffdf9 | ||
|
|
5d76ac33dd | ||
|
|
8e92bf3766 | ||
|
|
56517eec62 | ||
|
|
ed56f0733e | ||
|
|
3fd799ee74 | ||
|
|
4ed8a67eda | ||
|
|
cbb9a8dbd0 | ||
|
|
4280b84459 | ||
|
|
e06e9ac1f8 | ||
|
|
7bea4ad282 | ||
|
|
316c14d845 | ||
|
|
d17be63312 | ||
|
|
2c2253db31 | ||
|
|
a3128e56a3 | ||
|
|
de0cf0ce67 | ||
|
|
e3f33b9d4c | ||
|
|
27143e2663 | ||
|
|
566e2fb35d | ||
|
|
dc5ada1c04 | ||
|
|
603e698f5a | ||
|
|
f893dcb19a | ||
|
|
27b116798e | ||
|
|
ae932e5710 | ||
|
|
bab5a979be | ||
|
|
1b8c9a3e0c | ||
|
|
99d0fa30ed | ||
|
|
5cc146481b | ||
|
|
102da5cba1 | ||
|
|
d374eecb18 | ||
|
|
08d31c0ffe | ||
|
|
c62e40465a | ||
|
|
5367f32d5c | ||
|
|
ad20698a42 | ||
|
|
e865d80a63 | ||
|
|
81a311e7fa | ||
|
|
5397fa8fd3 | ||
|
|
b9e08cd9e3 | ||
|
|
fe86cd6320 | ||
|
|
bba07bb83a | ||
|
|
83a53ee1f0 | ||
|
|
23ba363a0d | ||
|
|
101aee81c0 | ||
|
|
fa60d02129 | ||
|
|
6a9e5fd801 | ||
|
|
12ae7845ec | ||
|
|
045e8ee96f | ||
|
|
993f75b14e | ||
|
|
b48f3746bc | ||
|
|
dcc477acbc | ||
|
|
5f4f007b31 | ||
|
|
9d4788cb7c | ||
|
|
5a1380f9ef | ||
|
|
0d82aa26f3 | ||
|
|
05f2ec19f2 | ||
|
|
25c3c9c565 | ||
|
|
d6e7e3d4a0 | ||
|
|
6148112904 | ||
|
|
ba338e9282 | ||
|
|
2e132690d1 | ||
|
|
8f1b6ca3b7 | ||
|
|
85d0252972 | ||
|
|
077f72a0c5 | ||
|
|
25bec513f3 | ||
|
|
0a6378d84c | ||
|
|
1e6034ce37 | ||
|
|
ef6e5ebcbe | ||
|
|
19a42b7463 | ||
|
|
9d0e608454 | ||
|
|
bd9a98dbfd | ||
|
|
0ffca3dfe4 | ||
|
|
01958559ee | ||
|
|
bb4aef206a | ||
|
|
e74227b0db | ||
|
|
a47aec1cc4 | ||
|
|
cd9d8cde3e | ||
|
|
d4f68d5ec7 | ||
|
|
a910cfcb35 | ||
|
|
962de503fd | ||
|
|
f40e72c882 | ||
|
|
b7993e1d0e | ||
|
|
d6e5e73e82 | ||
|
|
1794f87f99 | ||
|
|
81c7d57f30 | ||
|
|
b4f0149bd8 | ||
|
|
87ec06eef0 | ||
|
|
c764548214 | ||
|
|
c78af7c3ad | ||
|
|
f9cd0d4e29 | ||
|
|
ee03435ca7 | ||
|
|
a9b83256c6 | ||
|
|
7086ddcf58 | ||
|
|
26d226bb17 | ||
|
|
dc4742bf5c | ||
|
|
933df5fae4 | ||
|
|
4b17ac19dc | ||
|
|
43f6a4f499 | ||
|
|
076b99676d | ||
|
|
b3dfa05a4a | ||
|
|
c5946b1a0b | ||
|
|
58a85eabdf | ||
|
|
8a1d9b96dd | ||
|
|
36a3d2f766 | ||
|
|
68010076f9 | ||
|
|
92da783e82 | ||
|
|
7d1e343d25 | ||
|
|
84d17b7255 | ||
|
|
64e06a32ec | ||
|
|
4ee87e197d | ||
|
|
cace667b26 | ||
|
|
67d3e28af7 | ||
|
|
3c5f4e7b84 | ||
|
|
8f913c737c | ||
|
|
8ba147f52f | ||
|
|
9cfbd24b76 | ||
|
|
259abf6d25 | ||
|
|
8c59838a6f | ||
|
|
f7d24c6379 | ||
|
|
0f5466e075 | ||
|
|
7ab0e83f55 | ||
|
|
dc02ede8d7 | ||
|
|
42e484827f | ||
|
|
576c8f97bd | ||
|
|
808b4a687d | ||
|
|
954f2c235b | ||
|
|
51a7bbbfb8 | ||
|
|
9b5f5857bb | ||
|
|
e66f52a8ce | ||
|
|
8023bcea96 | ||
|
|
dfb7491503 | ||
|
|
b43d9c963a | ||
|
|
51c0637134 | ||
|
|
b16c23190b | ||
|
|
fddf2892cd | ||
|
|
53b5715da0 | ||
|
|
3a79fcb366 | ||
|
|
4df034ac55 | ||
|
|
a0bb04fe3a | ||
|
|
a923f28a8d | ||
|
|
78642f1c9b | ||
|
|
f8a85af859 | ||
|
|
bdb5bd0428 | ||
|
|
9e07f55c12 | ||
|
|
6a3e129ed5 | ||
|
|
879ba17760 | ||
|
|
0d2107e72a | ||
|
|
a23a47fded | ||
|
|
7737a29cca | ||
|
|
34278b3bff | ||
|
|
40b0643eaa | ||
|
|
ae7f5fbff2 | ||
|
|
0a776c6a72 | ||
|
|
5caecb1c8f | ||
|
|
0def3f8b2d | ||
|
|
1593acd8e9 | ||
|
|
da5553364a | ||
|
|
b7c03490d8 | ||
|
|
b8dd7b0f71 | ||
|
|
e7a8d5232f | ||
|
|
c638fa38e8 | ||
|
|
f74b054f01 | ||
|
|
b7af33edae | ||
|
|
166211b6f8 | ||
|
|
f8ec47bfdb | ||
|
|
4e84121553 | ||
|
|
ccf4ce8e58 | ||
|
|
6ba3d44fa1 | ||
|
|
c03ed76fec | ||
|
|
31214b10e7 | ||
|
|
5bbc324b2e | ||
|
|
7dcb22950e | ||
|
|
3c06f1f1a3 | ||
|
|
6bcf5161b1 | ||
|
|
ccc6765e2e | ||
|
|
14d40d4e2d | ||
|
|
75d563b320 | ||
|
|
2fdd53b55e | ||
|
|
4ac16a6701 | ||
|
|
e998788d25 | ||
|
|
e23b80b11b | ||
|
|
7eb884dcb9 | ||
|
|
92397adbb3 | ||
|
|
828485789f | ||
|
|
2f84474216 | ||
|
|
762aab397d | ||
|
|
b03cb4590f | ||
|
|
5d40831dd7 | ||
|
|
0d291afb05 | ||
|
|
e107c5c108 | ||
|
|
44905e674d | ||
|
|
5b790e711a | ||
|
|
1a9c54b237 | ||
|
|
0f4434a361 | ||
|
|
7e2823903b | ||
|
|
92937dbb86 | ||
|
|
0868efe095 | ||
|
|
97374f63f9 | ||
|
|
69f7b02ce6 | ||
|
|
b5ee682a41 | ||
|
|
70f82bdfd8 | ||
|
|
e70185560b | ||
|
|
be4be0e7e8 | ||
|
|
b9a5c64113 | ||
|
|
215f398547 | ||
|
|
ace18f9d95 | ||
|
|
c4ec91f61a | ||
|
|
a4881419df | ||
|
|
839359a794 | ||
|
|
d4d9b7c7a7 | ||
|
|
d4526c0c8a | ||
|
|
78f0bc6f02 | ||
|
|
ad3f5afeba | ||
|
|
7c4326939a | ||
|
|
e5428b67b8 | ||
|
|
59616bd8af | ||
|
|
fdc9ac95d2 | ||
|
|
a8f0d1af62 | ||
|
|
f6e377f222 | ||
|
|
c43d7ab92a | ||
|
|
0464e1de5c | ||
|
|
7a7381eac0 | ||
|
|
1379db1e92 | ||
|
|
ee10894f5f | ||
|
|
87d760708d | ||
|
|
ec7155809b | ||
|
|
5af7837468 | ||
|
|
d3f79af2ab | ||
|
|
a6c0fc3984 | ||
|
|
56604de7f1 | ||
|
|
d5e6199775 | ||
|
|
15920abcf7 | ||
|
|
88205d716a | ||
|
|
fa1a16f140 | ||
|
|
4f8075ca6c | ||
|
|
2615d0644d | ||
|
|
d2686937fd | ||
|
|
b60307e28b | ||
|
|
08d49ddc54 | ||
|
|
4cdc80e530 | ||
|
|
acd360efb9 | ||
|
|
87ad89beeb | ||
|
|
c2663ca7d8 | ||
|
|
4e58285cc2 | ||
|
|
ef354fb8af | ||
|
|
79ce535d4a | ||
|
|
50c43c4408 | ||
|
|
8fdec5aadc | ||
|
|
73eccd5308 | ||
|
|
4449677e36 | ||
|
|
8dc3a4b51f | ||
|
|
cd2de3a1db | ||
|
|
1fa0eebc3b | ||
|
|
e13002bab3 | ||
|
|
a750e80ea1 | ||
|
|
66e1b0a089 | ||
|
|
00c0b3f88c | ||
|
|
674311cb4b | ||
|
|
f54b8e00e1 | ||
|
|
c1beac5d0d | ||
|
|
7123345341 | ||
|
|
0e2475e667 | ||
|
|
acf1f1146d | ||
|
|
d136fa4fdf | ||
|
|
61067391b6 | ||
|
|
affebb1ba4 | ||
|
|
cbffa3d4ef | ||
|
|
6ef5ae2517 | ||
|
|
9701496897 | ||
|
|
22d6a3011f | ||
|
|
d22ddd99f6 | ||
|
|
4e8648fca0 | ||
|
|
9f49c357e9 | ||
|
|
d318467039 | ||
|
|
b05c25ac1a | ||
|
|
5dd0d51514 | ||
|
|
c7574a6b83 | ||
|
|
ab64e9343b | ||
|
|
b82cb0e849 | ||
|
|
c44a8fab42 | ||
|
|
9a75eb0a22 | ||
|
|
f994278a8b | ||
|
|
bd882d0e17 | ||
|
|
fddf62409a | ||
|
|
a8967ac764 | ||
|
|
20bc254a42 | ||
|
|
e298d5fbd8 | ||
|
|
ff54c2070d | ||
|
|
e092caa2ef | ||
|
|
feb44be9d0 | ||
|
|
a43a2e8ce2 | ||
|
|
6fcf77829d | ||
|
|
733833e8a4 | ||
|
|
890f998fe0 | ||
|
|
28c708f599 | ||
|
|
f1e536cbcf | ||
|
|
8dc7ad4f01 | ||
|
|
43c8976282 | ||
|
|
8ee63ccc24 | ||
|
|
2900fe07bf | ||
|
|
1d572d1fa9 | ||
|
|
21b8e9ae8f | ||
|
|
cb77b7fc1e | ||
|
|
763141592f | ||
|
|
09fcddfad2 | ||
|
|
b8ce1b5cc1 | ||
|
|
6a34fffcc4 | ||
|
|
75fa636366 | ||
|
|
bee96ebf15 | ||
|
|
9ec23fac5d | ||
|
|
2a7cd3574d | ||
|
|
4f96629e22 | ||
|
|
74bd270423 | ||
|
|
0469f28a54 |
@@ -7,6 +7,8 @@
|
||||
}],
|
||||
"@babel/plugin-proposal-optional-catch-binding",
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
"@babel/plugin-proposal-logical-assignment-operators",
|
||||
"@babel/plugin-external-helpers"
|
||||
]
|
||||
}
|
||||
|
||||
+1
-4
@@ -15,8 +15,5 @@ trim_trailing_whitespace = true
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.nsh]
|
||||
indent_size = 2
|
||||
|
||||
[{*.yml,*.yaml}]
|
||||
[{*.nsh,*.yml,*.yaml,*.json}]
|
||||
indent_size = 2
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
"import/no-webpack-loader-syntax": "off",
|
||||
"import/no-relative-parent-imports": "error",
|
||||
"import/first": "error",
|
||||
"import/no-namespace": "error",
|
||||
"import/no-default-export": "error",
|
||||
"babel/no-unused-expressions": "error",
|
||||
"node/no-callback-literal": "off"
|
||||
|
||||
@@ -24,7 +24,7 @@ A clear and concise description of what you expected to happen.
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment**
|
||||
Your user-agent (from Settings/Help section)
|
||||
Please copy all version information from Settings/Help
|
||||
|
||||
**Kdbx File**
|
||||
Does it happen on Demo or New database?
|
||||
|
||||
@@ -1,8 +1 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Discussions
|
||||
url: https://github.com/keeweb/keeweb/discussions
|
||||
about: A place to ask and answer questions or discuss different topics
|
||||
- name: Twitter
|
||||
url: https://twitter.com/kee_web
|
||||
about: Ask a quick question there if you like Twitter more
|
||||
blank_issues_enabled: true
|
||||
|
||||
@@ -2,7 +2,4 @@
|
||||
|
||||
cd /github/workspace
|
||||
npm ci
|
||||
cd desktop
|
||||
npm ci
|
||||
cd /github/workspace
|
||||
grunt desktop-linux
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
name: 'Publish release'
|
||||
description: 'Publish a release and update the release notes'
|
||||
inputs:
|
||||
version:
|
||||
description: 'Release version'
|
||||
required: true
|
||||
id:
|
||||
description: 'Release ID'
|
||||
required: true
|
||||
runs:
|
||||
using: 'node12'
|
||||
main: 'dist/index.js'
|
||||
File diff suppressed because it is too large
Load Diff
-419
@@ -1,419 +0,0 @@
|
||||
{
|
||||
"name": "publish-release",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.4.tgz",
|
||||
"integrity": "sha512-YJCEq8BE3CdN8+7HPZ/4DxJjk/OkZV2FFIf+DlZTC/4iBlzYCD5yjRR6eiOS5llO11zbRltIRuKAjMKaWTE6cg=="
|
||||
},
|
||||
"@actions/exec": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.4.tgz",
|
||||
"integrity": "sha512-4DPChWow9yc9W3WqEbUj8Nr86xkpyE29ZzWjXucHItclLbEW6jr80Zx4nqv18QL6KK65+cifiQZXvnqgTV6oHw==",
|
||||
"requires": {
|
||||
"@actions/io": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@actions/github": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/github/-/github-2.1.1.tgz",
|
||||
"integrity": "sha512-kAgTGUx7yf5KQCndVeHSwCNZuDBvPyxm5xKTswW2lofugeuC1AZX73nUUVDNaysnM9aKFMHv9YCdVJbg7syEyA==",
|
||||
"requires": {
|
||||
"@actions/http-client": "^1.0.3",
|
||||
"@octokit/graphql": "^4.3.1",
|
||||
"@octokit/rest": "^16.43.1"
|
||||
}
|
||||
},
|
||||
"@actions/http-client": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.7.tgz",
|
||||
"integrity": "sha512-PY3ys/XH5WMekkHyZhYSa/scYvlE5T/TV/T++vABHuY5ZRgtiBZkn2L2tV5Pv/xDCl59lSZb9WwRuWExDyAsSg==",
|
||||
"requires": {
|
||||
"tunnel": "0.0.6"
|
||||
}
|
||||
},
|
||||
"@actions/io": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz",
|
||||
"integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg=="
|
||||
},
|
||||
"@octokit/auth-token": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.0.tgz",
|
||||
"integrity": "sha512-eoOVMjILna7FVQf96iWc3+ZtE/ZT6y8ob8ZzcqKY1ibSQCnu4O/B7pJvzMx5cyZ/RjAff6DAdEb0O0Cjcxidkg==",
|
||||
"requires": {
|
||||
"@octokit/types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@octokit/endpoint": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.0.tgz",
|
||||
"integrity": "sha512-3nx+MEYoZeD0uJ+7F/gvELLvQJzLXhep2Az0bBSXagbApDvDW0LWwpnAIY/hb0Jwe17A0fJdz0O12dPh05cj7A==",
|
||||
"requires": {
|
||||
"@octokit/types": "^2.0.0",
|
||||
"is-plain-object": "^3.0.0",
|
||||
"universal-user-agent": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"universal-user-agent": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-5.0.0.tgz",
|
||||
"integrity": "sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q==",
|
||||
"requires": {
|
||||
"os-name": "^3.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@octokit/graphql": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.3.1.tgz",
|
||||
"integrity": "sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA==",
|
||||
"requires": {
|
||||
"@octokit/request": "^5.3.0",
|
||||
"@octokit/types": "^2.0.0",
|
||||
"universal-user-agent": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@octokit/plugin-paginate-rest": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz",
|
||||
"integrity": "sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q==",
|
||||
"requires": {
|
||||
"@octokit/types": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"@octokit/plugin-request-log": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz",
|
||||
"integrity": "sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw=="
|
||||
},
|
||||
"@octokit/plugin-rest-endpoint-methods": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz",
|
||||
"integrity": "sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ==",
|
||||
"requires": {
|
||||
"@octokit/types": "^2.0.1",
|
||||
"deprecation": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"@octokit/request": {
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.3.4.tgz",
|
||||
"integrity": "sha512-qyj8G8BxQyXjt9Xu6NvfvOr1E0l35lsXtwm3SopsYg/JWXjlsnwqLc8rsD2OLguEL/JjLfBvrXr4az7z8Lch2A==",
|
||||
"requires": {
|
||||
"@octokit/endpoint": "^6.0.0",
|
||||
"@octokit/request-error": "^2.0.0",
|
||||
"@octokit/types": "^2.0.0",
|
||||
"deprecation": "^2.0.0",
|
||||
"is-plain-object": "^3.0.0",
|
||||
"node-fetch": "^2.3.0",
|
||||
"once": "^1.4.0",
|
||||
"universal-user-agent": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"universal-user-agent": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-5.0.0.tgz",
|
||||
"integrity": "sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q==",
|
||||
"requires": {
|
||||
"os-name": "^3.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@octokit/request-error": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.0.tgz",
|
||||
"integrity": "sha512-rtYicB4Absc60rUv74Rjpzek84UbVHGHJRu4fNVlZ1mCcyUPPuzFfG9Rn6sjHrd95DEsmjSt1Axlc699ZlbDkw==",
|
||||
"requires": {
|
||||
"@octokit/types": "^2.0.0",
|
||||
"deprecation": "^2.0.0",
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"@octokit/rest": {
|
||||
"version": "16.43.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.43.1.tgz",
|
||||
"integrity": "sha512-gfFKwRT/wFxq5qlNjnW2dh+qh74XgTQ2B179UX5K1HYCluioWj8Ndbgqw2PVqa1NnVJkGHp2ovMpVn/DImlmkw==",
|
||||
"requires": {
|
||||
"@octokit/auth-token": "^2.4.0",
|
||||
"@octokit/plugin-paginate-rest": "^1.1.1",
|
||||
"@octokit/plugin-request-log": "^1.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "2.4.0",
|
||||
"@octokit/request": "^5.2.0",
|
||||
"@octokit/request-error": "^1.0.2",
|
||||
"atob-lite": "^2.0.0",
|
||||
"before-after-hook": "^2.0.0",
|
||||
"btoa-lite": "^1.0.0",
|
||||
"deprecation": "^2.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"lodash.uniq": "^4.5.0",
|
||||
"octokit-pagination-methods": "^1.1.0",
|
||||
"once": "^1.4.0",
|
||||
"universal-user-agent": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/request-error": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz",
|
||||
"integrity": "sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==",
|
||||
"requires": {
|
||||
"@octokit/types": "^2.0.0",
|
||||
"deprecation": "^2.0.0",
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@octokit/types": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.5.1.tgz",
|
||||
"integrity": "sha512-q4Wr7RexkPRrkQpXzUYF5Fj/14Mr65RyOHj6B9d/sQACpqGcStkHZj4qMEtlMY5SnD/69jlL9ItGPbDM0dR/dA==",
|
||||
"requires": {
|
||||
"@types/node": ">= 8"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "13.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.11.0.tgz",
|
||||
"integrity": "sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ=="
|
||||
},
|
||||
"@zeit/ncc": {
|
||||
"version": "0.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@zeit/ncc/-/ncc-0.22.1.tgz",
|
||||
"integrity": "sha512-Qq3bMuonkcnV/96jhy9SQYdh39NXHxNMJ1O31ZFzWG9n52fR2DLtgrNzhj/ahlEjnBziMLGVWDbaS9sf03/fEw==",
|
||||
"dev": true
|
||||
},
|
||||
"atob-lite": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz",
|
||||
"integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY="
|
||||
},
|
||||
"before-after-hook": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz",
|
||||
"integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A=="
|
||||
},
|
||||
"btoa-lite": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz",
|
||||
"integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc="
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
|
||||
"requires": {
|
||||
"nice-try": "^1.0.4",
|
||||
"path-key": "^2.0.1",
|
||||
"semver": "^5.5.0",
|
||||
"shebang-command": "^1.2.0",
|
||||
"which": "^1.2.9"
|
||||
}
|
||||
},
|
||||
"deprecation": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
|
||||
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
|
||||
},
|
||||
"end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"requires": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"execa": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
|
||||
"integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
|
||||
"requires": {
|
||||
"cross-spawn": "^6.0.0",
|
||||
"get-stream": "^4.0.0",
|
||||
"is-stream": "^1.1.0",
|
||||
"npm-run-path": "^2.0.0",
|
||||
"p-finally": "^1.0.0",
|
||||
"signal-exit": "^3.0.0",
|
||||
"strip-eof": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
|
||||
"integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
|
||||
"requires": {
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"is-plain-object": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz",
|
||||
"integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==",
|
||||
"requires": {
|
||||
"isobject": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"is-stream": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
||||
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
|
||||
},
|
||||
"isobject": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz",
|
||||
"integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA=="
|
||||
},
|
||||
"lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
|
||||
},
|
||||
"lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
"integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM="
|
||||
},
|
||||
"lodash.uniq": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
|
||||
"integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M="
|
||||
},
|
||||
"macos-release": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz",
|
||||
"integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA=="
|
||||
},
|
||||
"nice-try": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
|
||||
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
|
||||
},
|
||||
"npm-run-path": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
|
||||
"integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
|
||||
"requires": {
|
||||
"path-key": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"octokit-pagination-methods": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz",
|
||||
"integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ=="
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"os-name": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz",
|
||||
"integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==",
|
||||
"requires": {
|
||||
"macos-release": "^2.2.0",
|
||||
"windows-release": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
|
||||
},
|
||||
"path-key": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
|
||||
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
|
||||
"requires": {
|
||||
"shebang-regex": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"shebang-regex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
|
||||
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
|
||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
|
||||
},
|
||||
"strip-eof": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
|
||||
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
|
||||
},
|
||||
"tunnel": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
|
||||
},
|
||||
"universal-user-agent": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.1.tgz",
|
||||
"integrity": "sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg==",
|
||||
"requires": {
|
||||
"os-name": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"windows-release": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.0.tgz",
|
||||
"integrity": "sha512-2HetyTg1Y+R+rUgrKeUEhAG/ZuOmTrI1NBb3ZyAGQMYmOJjBBPe4MTodghRkmLJZHwkuPi02anbeGP+Zf401LQ==",
|
||||
"requires": {
|
||||
"execa": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "publish-release",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "ncc build src/main.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.4",
|
||||
"@actions/exec": "^1.0.4",
|
||||
"@actions/github": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@zeit/ncc": "^0.22.1"
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const core = require('@actions/core');
|
||||
const { GitHub, context } = require('@actions/github');
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const github = new GitHub(process.env.GITHUB_TOKEN);
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
const version = core.getInput('version', { required: false });
|
||||
const draft = false;
|
||||
const release_id = core.getInput('release_id', { required: false });
|
||||
|
||||
const releaseNotesPath = path.join(__dirname, '../../../../release-notes.md');
|
||||
const releaseNotes = fs.readFileSync(releaseNotesPath, 'utf8');
|
||||
const regex = new RegExp(
|
||||
`#####\\s+v${version.replace(/\./g, '\\.')}.*?\n([\\s\\S]*?)\n#####`
|
||||
);
|
||||
|
||||
const bodyReleaseNotes = releaseNotes.match(regex)[1].trim().replace(/\s*\n/g, '\n');
|
||||
|
||||
const bodyTemplate =
|
||||
'{release_notes}\n\n' +
|
||||
'Want to keep releases happening? ' +
|
||||
'Donate to KeeWeb on [OpenCollective](https://opencollective.com/keeweb) or [GitHub](https://github.com/sponsors/keeweb). ' +
|
||||
'Thank you!';
|
||||
const body = bodyTemplate.replace('{release_notes}', bodyReleaseNotes);
|
||||
|
||||
console.log(`Updating release with notes:\n${body}`);
|
||||
|
||||
await github.repos.updateRelease({
|
||||
owner,
|
||||
repo,
|
||||
release_id,
|
||||
body,
|
||||
draft
|
||||
});
|
||||
} catch (error) {
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
+35
-189
@@ -18,6 +18,11 @@ jobs:
|
||||
with:
|
||||
repository: keeweb/keeweb
|
||||
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
|
||||
- name: Upgrade Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install npm modules
|
||||
run: npm ci
|
||||
- name: Test
|
||||
@@ -95,11 +100,6 @@ jobs:
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
|
||||
- name: Upload update artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: UpdateDesktop.zip
|
||||
path: dist/desktop/UpdateDesktop.zip
|
||||
|
||||
darwin:
|
||||
runs-on: macos-latest
|
||||
@@ -122,11 +122,13 @@ jobs:
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: dist
|
||||
- name: Upgrade Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install npm modules
|
||||
run: npm ci
|
||||
- name: Install desktop npm modules
|
||||
working-directory: desktop
|
||||
run: npm ci
|
||||
- name: Install grunt
|
||||
run: sudo npm i -g grunt-cli
|
||||
- name: Write secrets
|
||||
@@ -134,17 +136,19 @@ jobs:
|
||||
CODESIGN: ${{ secrets.CODESIGN }}
|
||||
APPLE_DEPLOY_PASSWORD: ${{ secrets.APPLE_DEPLOY_PASSWORD }}
|
||||
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
||||
APPLE_PROVISIONING_PROFILE: ${{ secrets.APPLE_PROVISIONING_PROFILE }}
|
||||
run: |
|
||||
mkdir keys
|
||||
echo "$CODESIGN" > keys/codesign.json
|
||||
xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u "$APPLE_ID_USERNAME" -p "$APPLE_DEPLOY_PASSWORD"
|
||||
echo "$APPLE_PROVISIONING_PROFILE" | base64 -d > keys/keeweb.provisionprofile
|
||||
- name: Import certificates
|
||||
uses: keeweb/import-codesign-certs@v1
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
- name: Grunt
|
||||
run: grunt desktop-darwin ${{ github.repository != 'keeweb/keeweb' && '--skip-sign' || '' }}
|
||||
run: grunt --max-old-space-size=4096 desktop-darwin ${{ github.repository != 'keeweb/keeweb' && '--skip-sign' || '' }}
|
||||
- name: Upload x64 dmg artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
@@ -177,11 +181,13 @@ jobs:
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: dist
|
||||
- name: Upgrade Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install npm modules
|
||||
run: npm ci
|
||||
- name: Install desktop npm modules
|
||||
working-directory: desktop
|
||||
run: npm ci
|
||||
- name: Install grunt
|
||||
run: npm i -g grunt-cli
|
||||
- name: Write secrets
|
||||
@@ -255,6 +261,11 @@ jobs:
|
||||
repository: keeweb/keeweb
|
||||
path: keeweb
|
||||
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
|
||||
- name: Upgrade Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install npm modules
|
||||
working-directory: keeweb
|
||||
run: npm ci
|
||||
@@ -328,11 +339,6 @@ jobs:
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
||||
path: assets
|
||||
- name: Download update artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: UpdateDesktop.zip
|
||||
path: assets
|
||||
- name: Zip html
|
||||
working-directory: html
|
||||
run: zip -vr ../assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.html.zip .
|
||||
@@ -363,182 +369,22 @@ jobs:
|
||||
run: cp -r html keeweb/package/docker/dist
|
||||
- name: Build the Docker image
|
||||
working-directory: keeweb
|
||||
run: docker build -t antelle/keeweb:latest package/docker
|
||||
- name: Tag the Docker image
|
||||
run: docker tag antelle/keeweb:latest antelle/keeweb:${{ steps.get_tag.outputs.tag }}
|
||||
- name: Push the Docker image to the registry
|
||||
run: docker build -t antelle/keeweb:latest -t antelle/keeweb:${{ steps.get_tag.outputs.tag }} package/docker
|
||||
- name: Push the Docker image to dockerhub
|
||||
if: ${{ github.repository == 'keeweb/keeweb' }}
|
||||
run: docker push antelle/keeweb
|
||||
- name: Create a GitHub release
|
||||
id: create_release
|
||||
uses: actions/create-release@latest
|
||||
run: docker push --all-tags antelle/keeweb
|
||||
- name: Extract release notes
|
||||
run: node util/extract-release-notes.js --version=${{ steps.get_tag.outputs.tag }} --output=tmp/release-body.md
|
||||
working-directory: keeweb
|
||||
- name: Publish GitHub release
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Desktop apps v${{ steps.get_tag.outputs.tag }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
- name: Upload html asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.html.zip
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html.zip
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload linux.AppImage asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.AppImage
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.AppImage
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload linux.snap asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.snap
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.snap
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload linux.deb asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.deb
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.deb
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload linux.zip asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.zip
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.zip
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload linux.rpm asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload darwin.x64.dmg asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.x64.dmg
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.x64.dmg
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload darwin.arm64.dmg asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.arm64.dmg
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.arm64.dmg
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.ia32.exe asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.exe
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.exe
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.ia32.zip asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.zip
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.zip
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.x64.exe asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.exe
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.exe
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.x64.zip asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.zip
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.zip
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.arm64.exe asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.exe
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.exe
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.arm64.zip asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload update asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/UpdateDesktop.zip
|
||||
asset_name: UpdateDesktop.zip
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload verify.sign asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/Verify.sign.sha256
|
||||
asset_name: Verify.sign.sha256
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload verify.sha asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/Verify.sha256
|
||||
asset_name: Verify.sha256
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Publish the GitHub release
|
||||
uses: ./keeweb/.github/actions/publish-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
version: ${{ steps.get_tag.outputs.tag }}
|
||||
release_id: ${{ steps.create_release.outputs.id }}
|
||||
tag_name: v${{ steps.get_tag.outputs.tag }}
|
||||
name: Desktop apps v${{ steps.get_tag.outputs.tag }}
|
||||
files: assets/*
|
||||
body_path: keeweb/tmp/release-body.md
|
||||
- name: Checkout gh-pages
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
|
||||
+1
-1
@@ -14,6 +14,6 @@ obj/
|
||||
xcuserdata/
|
||||
*.suo
|
||||
.idea/
|
||||
.vscode
|
||||
.vscode/
|
||||
*.iml
|
||||
test/dist/
|
||||
|
||||
+106
-98
@@ -25,10 +25,6 @@ module.exports = function (grunt) {
|
||||
|
||||
const dt = date.toISOString().replace(/T.*/, '');
|
||||
const year = date.getFullYear();
|
||||
const minElectronVersionForUpdate = '11.0.3';
|
||||
const zipCommentPlaceholderPart = 'zip_comment_placeholder_that_will_be_replaced_with_hash';
|
||||
const zipCommentPlaceholder =
|
||||
zipCommentPlaceholderPart + '.'.repeat(512 - zipCommentPlaceholderPart.length);
|
||||
const electronVersion = pkg.dependencies.electron.replace(/^\D/, '');
|
||||
const skipSign = grunt.option('skip-sign');
|
||||
const getCodeSignConfig = () =>
|
||||
@@ -51,7 +47,8 @@ module.exports = function (grunt) {
|
||||
const webpackOptions = {
|
||||
date,
|
||||
beta: !!grunt.option('beta'),
|
||||
sha
|
||||
sha,
|
||||
appleTeamId: '3LE7JZ657W'
|
||||
};
|
||||
|
||||
const windowsAppVersionString = {
|
||||
@@ -80,6 +77,15 @@ module.exports = function (grunt) {
|
||||
]
|
||||
});
|
||||
|
||||
const linuxDependencies = [
|
||||
'libappindicator1',
|
||||
'libgconf-2-4',
|
||||
'gnome-keyring',
|
||||
'libxtst6',
|
||||
'libx11-6',
|
||||
'libatspi2.0-0'
|
||||
];
|
||||
|
||||
grunt.initConfig({
|
||||
noop: { noop: {} },
|
||||
clean: {
|
||||
@@ -139,32 +145,8 @@ module.exports = function (grunt) {
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
'desktop-update': {
|
||||
cwd: 'tmp/desktop/keeweb-linux-x64/resources/',
|
||||
src: 'app.asar',
|
||||
dest: 'tmp/desktop/update/',
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
'desktop-update-helper': {
|
||||
src: ['helper/darwin/KeeWebHelper', 'helper/win32/KeeWebHelper.exe'],
|
||||
dest: 'tmp/desktop/update/',
|
||||
nonull: true
|
||||
},
|
||||
'desktop-darwin-helper-x64': {
|
||||
src: 'helper/darwin/KeeWebHelper',
|
||||
dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Resources/',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'desktop-darwin-helper-arm64': {
|
||||
src: 'helper/darwin/KeeWebHelper',
|
||||
dest: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/Resources/',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'desktop-darwin-installer-helper-x64': {
|
||||
cwd: 'package/osx/KeeWeb Installer.app',
|
||||
cwd: 'tmp/desktop/KeeWeb Installer.app',
|
||||
src: '**',
|
||||
dest:
|
||||
'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
|
||||
@@ -173,7 +155,7 @@ module.exports = function (grunt) {
|
||||
options: { mode: true }
|
||||
},
|
||||
'desktop-darwin-installer-helper-arm64': {
|
||||
cwd: 'package/osx/KeeWeb Installer.app',
|
||||
cwd: 'tmp/desktop/KeeWeb Installer.app',
|
||||
src: '**',
|
||||
dest:
|
||||
'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
|
||||
@@ -181,21 +163,6 @@ module.exports = function (grunt) {
|
||||
nonull: true,
|
||||
options: { mode: true }
|
||||
},
|
||||
'desktop-windows-helper-x64': {
|
||||
src: 'helper/win32/KeeWebHelper.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-x64/Resources/',
|
||||
nonull: true
|
||||
},
|
||||
'desktop-windows-helper-ia32': {
|
||||
src: 'helper/win32/KeeWebHelper.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-ia32/Resources/',
|
||||
nonull: true
|
||||
},
|
||||
'desktop-windows-helper-arm64': {
|
||||
src: 'helper/win32/KeeWebHelper.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-arm64/Resources/',
|
||||
nonull: true
|
||||
},
|
||||
'desktop-win32-dist-x64': {
|
||||
src: 'tmp/desktop/KeeWeb.win.x64.exe',
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.win.x64.exe`,
|
||||
@@ -255,6 +222,52 @@ module.exports = function (grunt) {
|
||||
src: `tmp/desktop/electron-builder/keeweb-${pkg.version}.AppImage`,
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.linux.AppImage`,
|
||||
nonull: true
|
||||
},
|
||||
'darwin-installer-icon': {
|
||||
src: 'graphics/icon.icns',
|
||||
dest: 'tmp/desktop/KeeWeb Installer.app/Contents/Resources/applet.icns',
|
||||
nonull: true
|
||||
},
|
||||
'native-messaging-host-darwin-x64': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/darwin-x64/keeweb-native-messaging-host',
|
||||
dest:
|
||||
'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/MacOS/util/keeweb-native-messaging-host',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'native-messaging-host-darwin-arm64': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/darwin-arm64/keeweb-native-messaging-host',
|
||||
dest:
|
||||
'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/MacOS/util/keeweb-native-messaging-host',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'native-messaging-host-linux-x64': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/linux-x64/keeweb-native-messaging-host',
|
||||
dest: 'tmp/desktop/keeweb-linux-x64/keeweb-native-messaging-host',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'native-messaging-host-win32-x64': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/win32-x64/keeweb-native-messaging-host.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-x64/keeweb-native-messaging-host.exe',
|
||||
nonull: true
|
||||
},
|
||||
'native-messaging-host-win32-ia32': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/win32-ia32/keeweb-native-messaging-host.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-ia32/keeweb-native-messaging-host.exe',
|
||||
nonull: true
|
||||
},
|
||||
'native-messaging-host-win32-arm64': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/win32-arm64/keeweb-native-messaging-host.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-arm64/keeweb-native-messaging-host.exe',
|
||||
nonull: true
|
||||
}
|
||||
},
|
||||
eslint: {
|
||||
@@ -262,7 +275,8 @@ module.exports = function (grunt) {
|
||||
desktop: ['desktop/**/*.js', '!desktop/node_modules/**'],
|
||||
build: ['Gruntfile.js', 'grunt.*.js', 'build/**/*.js', 'webpack.config.js'],
|
||||
plugins: ['plugins/**/*.js'],
|
||||
util: ['util/**/*.js']
|
||||
util: ['util/**/*.js'],
|
||||
installer: ['package/osx/installer.js']
|
||||
},
|
||||
inline: {
|
||||
app: {
|
||||
@@ -295,20 +309,20 @@ module.exports = function (grunt) {
|
||||
}
|
||||
},
|
||||
'string-replace': {
|
||||
manifest: {
|
||||
'update-manifest': {
|
||||
options: {
|
||||
replacements: [
|
||||
{
|
||||
pattern: '# YYYY-MM-DD:v0.0.0',
|
||||
replacement: '# ' + dt + ':v' + pkg.version
|
||||
pattern: /"version":\s*".*?"/,
|
||||
replacement: `"version": "${pkg.version}"`
|
||||
},
|
||||
{
|
||||
pattern: '# updmin:v0.0.0',
|
||||
replacement: '# updmin:v' + minElectronVersionForUpdate
|
||||
pattern: /"date":\s*".*?"/,
|
||||
replacement: `"date": "${dt}"`
|
||||
}
|
||||
]
|
||||
},
|
||||
files: { 'dist/manifest.appcache': 'app/manifest.appcache' }
|
||||
files: { 'dist/update.json': 'app/update.json' }
|
||||
},
|
||||
'service-worker': {
|
||||
options: { replacements: [{ pattern: '0.0.0', replacement: pkg.version }] },
|
||||
@@ -449,26 +463,37 @@ module.exports = function (grunt) {
|
||||
category: 'Utility'
|
||||
},
|
||||
rpm: {
|
||||
// depends: ['libappindicator1', 'libgconf-2-4', 'gnome-keyring']
|
||||
// depends: linuxDependencies
|
||||
},
|
||||
snap: {
|
||||
stagePackages: ['libappindicator1', 'libgconf-2-4', 'gnome-keyring']
|
||||
stagePackages: linuxDependencies
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'electron-patch': {
|
||||
'win32-x64': 'tmp/desktop/KeeWeb-win32-x64/KeeWeb.exe',
|
||||
'win32-ia32': 'tmp/desktop/KeeWeb-win32-ia32/KeeWeb.exe',
|
||||
'win32-arm64': 'tmp/desktop/KeeWeb-win32-arm64/KeeWeb.exe',
|
||||
'darwin-x64': 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app',
|
||||
'darwin-arm64': 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app',
|
||||
'linux': 'tmp/desktop/KeeWeb-linux-x64/keeweb'
|
||||
},
|
||||
osacompile: {
|
||||
options: {
|
||||
language: 'JavaScript'
|
||||
},
|
||||
installer: {
|
||||
files: {
|
||||
'tmp/desktop/KeeWeb Installer.app': 'package/osx/installer.js'
|
||||
}
|
||||
}
|
||||
},
|
||||
compress: {
|
||||
options: {
|
||||
level: 6
|
||||
},
|
||||
'desktop-update': {
|
||||
options: {
|
||||
archive: 'dist/desktop/UpdateDesktop.zip',
|
||||
comment: zipCommentPlaceholder
|
||||
},
|
||||
files: [{ cwd: 'tmp/desktop/update', src: '**', expand: true, nonull: true }]
|
||||
},
|
||||
'win32-x64': {
|
||||
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.x64.zip` },
|
||||
files: [{ cwd: 'tmp/desktop/KeeWeb-win32-x64', src: '**', expand: true }]
|
||||
@@ -577,7 +602,7 @@ module.exports = function (grunt) {
|
||||
pkgName: `KeeWeb-${pkg.version}.linux.x64.deb`,
|
||||
targetDir: 'dist/desktop',
|
||||
appName: 'KeeWeb',
|
||||
depends: 'libappindicator1, libgconf-2-4, gnome-keyring',
|
||||
depends: linuxDependencies.join(', '),
|
||||
scripts: {
|
||||
postinst: 'package/deb/scripts/postinst'
|
||||
}
|
||||
@@ -600,50 +625,30 @@ module.exports = function (grunt) {
|
||||
]
|
||||
}
|
||||
},
|
||||
'sign-archive': {
|
||||
'desktop-update': {
|
||||
options: {
|
||||
file: 'dist/desktop/UpdateDesktop.zip',
|
||||
signature: zipCommentPlaceholder
|
||||
}
|
||||
}
|
||||
},
|
||||
'sign-desktop-files': {
|
||||
'desktop-update': {
|
||||
options: {
|
||||
path: 'tmp/desktop/update'
|
||||
}
|
||||
}
|
||||
},
|
||||
'validate-desktop-update': {
|
||||
desktop: {
|
||||
options: {
|
||||
file: 'dist/desktop/UpdateDesktop.zip',
|
||||
expected: [
|
||||
'app.asar',
|
||||
'helper/darwin/KeeWebHelper',
|
||||
'helper/win32/KeeWebHelper.exe'
|
||||
],
|
||||
expectedCount: 7,
|
||||
publicKey: 'app/resources/public-key.pem'
|
||||
}
|
||||
}
|
||||
},
|
||||
'osx-sign': {
|
||||
options: {
|
||||
get identity() {
|
||||
return getCodeSignConfig().identities.app;
|
||||
},
|
||||
hardenedRuntime: true,
|
||||
entitlements: 'package/osx/entitlements.mac.plist',
|
||||
'entitlements-inherit': 'package/osx/entitlements.mac.plist',
|
||||
entitlements: 'package/osx/entitlements.plist',
|
||||
'entitlements-inherit': 'package/osx/entitlements-inherit.plist',
|
||||
'gatekeeper-assess': false
|
||||
},
|
||||
'desktop-x64': {
|
||||
options: {
|
||||
'provisioning-profile': './keys/keeweb.provisionprofile'
|
||||
},
|
||||
src: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app'
|
||||
},
|
||||
'desktop-arm64': {
|
||||
options: {
|
||||
'provisioning-profile': './keys/keeweb.provisionprofile'
|
||||
},
|
||||
src: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app'
|
||||
},
|
||||
'installer': {
|
||||
src: 'tmp/desktop/KeeWeb Installer.app'
|
||||
}
|
||||
},
|
||||
notarize: {
|
||||
@@ -678,6 +683,8 @@ module.exports = function (grunt) {
|
||||
options: {
|
||||
files: {
|
||||
'tmp/desktop/KeeWeb-win32-x64/KeeWeb.exe': 'KeeWeb',
|
||||
'tmp/desktop/KeeWeb-win32-x64/keeweb-native-messaging-host.exe':
|
||||
'KeeWeb Native Messaging Host',
|
||||
'tmp/desktop/KeeWeb-win32-x64/ffmpeg.dll': '',
|
||||
'tmp/desktop/KeeWeb-win32-x64/libEGL.dll':
|
||||
'ANGLE libEGL Dynamic Link Library',
|
||||
@@ -690,6 +697,8 @@ module.exports = function (grunt) {
|
||||
options: {
|
||||
files: {
|
||||
'tmp/desktop/KeeWeb-win32-ia32/KeeWeb.exe': 'KeeWeb',
|
||||
'tmp/desktop/KeeWeb-win32-ia32/keeweb-native-messaging-host.exe':
|
||||
'KeeWeb Native Messaging Host',
|
||||
'tmp/desktop/KeeWeb-win32-ia32/ffmpeg.dll': '',
|
||||
'tmp/desktop/KeeWeb-win32-ia32/libEGL.dll':
|
||||
'ANGLE libEGL Dynamic Link Library',
|
||||
@@ -702,6 +711,8 @@ module.exports = function (grunt) {
|
||||
options: {
|
||||
files: {
|
||||
'tmp/desktop/KeeWeb-win32-arm64/KeeWeb.exe': 'KeeWeb',
|
||||
'tmp/desktop/KeeWeb-win32-arm64/keeweb-native-messaging-host.exe':
|
||||
'KeeWeb Native Messaging Host',
|
||||
'tmp/desktop/KeeWeb-win32-arm64/ffmpeg.dll': '',
|
||||
'tmp/desktop/KeeWeb-win32-arm64/libEGL.dll':
|
||||
'ANGLE libEGL Dynamic Link Library',
|
||||
@@ -759,10 +770,7 @@ module.exports = function (grunt) {
|
||||
sign: 'dist/desktop/Verify.sign.sha256'
|
||||
},
|
||||
files: {
|
||||
'dist/desktop/Verify.sha256': [
|
||||
'dist/desktop/KeeWeb-*',
|
||||
'dist/desktop/UpdateDesktop.zip'
|
||||
]
|
||||
'dist/desktop/Verify.sha256': ['dist/desktop/KeeWeb-*']
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Antelle https://antelle.net
|
||||
Copyright (c) 2021 Antelle https://antelle.net
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -90,7 +90,7 @@ Please note: donation does not imply any type of service contract.
|
||||
|
||||
Notable contributions to KeeWeb:
|
||||
|
||||
- Florian Reuschel ([@Loilo](https://github.com/Loilo)): [German translation](http://keeweb.oneskyapp.com/collaboration/translate/project/project/173183/language/550)
|
||||
- Florian Reuschel ([@Loilo](https://github.com/Loilo)): [German translation](https://keeweb.oneskyapp.com/collaboration/translate/project/project/173183/language/550)
|
||||
- Dennis Ploeger ([@dploeger](https://github.com/dploeger)): [auto-type improvements](https://github.com/keeweb/keeweb/pulls?q=is%3Apr+is%3Aclosed+author%3Adploeger)
|
||||
- Hackmanit ([hackmanit.de](https://www.hackmanit.de)): [penetration test](https://www.hackmanit.de/en/blog-en/104-pro-bono-penetration-test-keeweb)
|
||||
- Peter Bittner ([@bittner](https://github.com/bittner)): [Wikipedia article](https://en.wikipedia.org/wiki/KeeWeb)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>KeeWeb</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.opener.postMessage(
|
||||
{ storage: 'teams', search: location.search },
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
content="
|
||||
default-src 'self';
|
||||
font-src data:;
|
||||
script-src 'self' blob: 'unsafe-eval';
|
||||
script-src 'self' 'unsafe-eval';
|
||||
style-src 'self' blob:;
|
||||
connect-src 'self' ws: https:;
|
||||
child-src 'self' blob:;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
CACHE MANIFEST
|
||||
|
||||
# YYYY-MM-DD:v0.0.0
|
||||
# updmin:v0.0.0
|
||||
|
||||
NETWORK:
|
||||
*
|
||||
+12
-5
@@ -6,6 +6,7 @@ import { ExportApi } from 'comp/app/export-api';
|
||||
import { SingleInstanceChecker } from 'comp/app/single-instance-checker';
|
||||
import { Updater } from 'comp/app/updater';
|
||||
import { UsbListener } from 'comp/app/usb-listener';
|
||||
import { BrowserExtensionConnector } from 'comp/extension/browser-extension-connector';
|
||||
import { FeatureTester } from 'comp/browser/feature-tester';
|
||||
import { FocusDetector } from 'comp/browser/focus-detector';
|
||||
import { IdleTracker } from 'comp/browser/idle-tracker';
|
||||
@@ -45,6 +46,7 @@ ready(() => {
|
||||
.then(loadRemoteConfig)
|
||||
.then(ensureCanRun)
|
||||
.then(initStorage)
|
||||
.then(initUsbListener)
|
||||
.then(showApp)
|
||||
.then(postInit)
|
||||
.catch((e) => {
|
||||
@@ -142,6 +144,11 @@ ready(() => {
|
||||
StartProfiler.milestone('initializing storage');
|
||||
}
|
||||
|
||||
function initUsbListener() {
|
||||
UsbListener.init();
|
||||
StartProfiler.milestone('starting usb');
|
||||
}
|
||||
|
||||
function showApp() {
|
||||
return Promise.resolve().then(() => {
|
||||
const skipHttpsWarning =
|
||||
@@ -172,12 +179,12 @@ ready(() => {
|
||||
}
|
||||
|
||||
function postInit() {
|
||||
Updater.init();
|
||||
SingleInstanceChecker.init();
|
||||
AppRightsChecker.init();
|
||||
IdleTracker.init();
|
||||
UsbListener.init();
|
||||
setTimeout(() => {
|
||||
Updater.init();
|
||||
SingleInstanceChecker.init();
|
||||
AppRightsChecker.init();
|
||||
IdleTracker.init();
|
||||
BrowserExtensionConnector.init(appModel);
|
||||
PluginManager.runAutoUpdate();
|
||||
}, Timeouts.AutoUpdatePluginsAfterStart);
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const AutoTypeEmitterFactory = {
|
||||
create(callback, windowId) {
|
||||
if (Launcher && Launcher.autoTypeSupported) {
|
||||
const { AutoTypeEmitter } = require('./emitter/auto-type-emitter-' +
|
||||
Launcher.platform());
|
||||
return new AutoTypeEmitter(callback, windowId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export { AutoTypeEmitterFactory };
|
||||
@@ -0,0 +1,160 @@
|
||||
import { Features } from 'util/features';
|
||||
import { NativeModules } from 'comp/launcher/native-modules';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
|
||||
const logger = new Logger('auto-type-emitter');
|
||||
|
||||
const KeyMap = {
|
||||
tab: 'Tab',
|
||||
enter: 'Return',
|
||||
space: 'Space',
|
||||
up: 'UpArrow',
|
||||
down: 'DownArrow',
|
||||
left: 'LeftArrow',
|
||||
right: 'RightArrow',
|
||||
home: 'Home',
|
||||
end: 'End',
|
||||
pgup: 'PageUp',
|
||||
pgdn: 'PageDown',
|
||||
ins: 'Insert',
|
||||
del: 'ForwardDelete',
|
||||
bs: 'BackwardDelete',
|
||||
esc: 'Escape',
|
||||
win: 'Meta',
|
||||
rwin: 'RightMeta',
|
||||
f1: 'F1',
|
||||
f2: 'F2',
|
||||
f3: 'F3',
|
||||
f4: 'F4',
|
||||
f5: 'F5',
|
||||
f6: 'F6',
|
||||
f7: 'F7',
|
||||
f8: 'F8',
|
||||
f9: 'F9',
|
||||
f10: 'F10',
|
||||
f11: 'F11',
|
||||
f12: 'F12',
|
||||
f13: 'F13',
|
||||
f14: 'F14',
|
||||
f15: 'F15',
|
||||
f16: 'F16',
|
||||
add: 'KeypadPlus',
|
||||
subtract: 'KeypadMinus',
|
||||
multiply: 'KeypadMultiply',
|
||||
divide: 'KeypadDivide',
|
||||
n0: 'D0',
|
||||
n1: 'D1',
|
||||
n2: 'D2',
|
||||
n3: 'D3',
|
||||
n4: 'D4',
|
||||
n5: 'D5',
|
||||
n6: 'D6',
|
||||
n7: 'D7',
|
||||
n8: 'D8',
|
||||
n9: 'D9'
|
||||
};
|
||||
|
||||
const ModMap = {
|
||||
'^': Features.isMac ? 'Command' : 'Ctrl',
|
||||
'+': 'Shift',
|
||||
'%': 'Alt',
|
||||
'^^': 'Ctrl'
|
||||
};
|
||||
|
||||
class AutoTypeEmitter {
|
||||
constructor(callback) {
|
||||
this.callback = callback;
|
||||
this.mod = {};
|
||||
}
|
||||
|
||||
begin() {
|
||||
this.withCallback(NativeModules.kbdEnsureModifierNotPressed());
|
||||
}
|
||||
|
||||
setMod(mod, enabled) {
|
||||
const nativeMod = ModMap[mod];
|
||||
if (!nativeMod) {
|
||||
return this.callback(`Bad modifier: ${mod}`);
|
||||
}
|
||||
NativeModules.kbdKeyMoveWithModifier(!!enabled, [nativeMod]).catch((e) => {
|
||||
logger.error('Error moving modifier', mod, enabled ? 'down' : 'up', e);
|
||||
});
|
||||
if (enabled) {
|
||||
this.mod[nativeMod] = true;
|
||||
} else {
|
||||
delete this.mod[nativeMod];
|
||||
}
|
||||
}
|
||||
|
||||
text(str) {
|
||||
if (!str) {
|
||||
return this.withCallback(Promise.resolve());
|
||||
}
|
||||
const mods = Object.keys(this.mod);
|
||||
if (mods.length) {
|
||||
this.withCallback(NativeModules.kbdTextAsKeys(str, mods));
|
||||
} else {
|
||||
this.withCallback(NativeModules.kbdText(str));
|
||||
}
|
||||
}
|
||||
|
||||
key(key) {
|
||||
const mods = Object.keys(this.mod);
|
||||
if (typeof key === 'number') {
|
||||
this.withCallback(NativeModules.kbdKeyPressWithCharacter(0, key, mods));
|
||||
} else {
|
||||
if (!KeyMap[key]) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
const code = KeyMap[key];
|
||||
this.withCallback(NativeModules.kbdKeyPress(code, mods));
|
||||
}
|
||||
}
|
||||
|
||||
copyPaste(text) {
|
||||
setTimeout(() => {
|
||||
Launcher.setClipboardText(text);
|
||||
setTimeout(() => {
|
||||
this.withCallback(NativeModules.kbdShortcut('V'));
|
||||
}, Timeouts.AutoTypeCopyPaste);
|
||||
}, Timeouts.AutoTypeCopyPaste);
|
||||
}
|
||||
|
||||
wait(time) {
|
||||
setTimeout(() => this.withCallback(Promise.resolve()), time);
|
||||
}
|
||||
|
||||
waitComplete() {
|
||||
this.withCallback(Promise.resolve());
|
||||
}
|
||||
|
||||
setDelay() {
|
||||
this.callback('Not implemented');
|
||||
}
|
||||
|
||||
withCallback(promise) {
|
||||
promise
|
||||
.then(() => {
|
||||
try {
|
||||
this.callback();
|
||||
} catch (err) {
|
||||
logger.error('Callback error', err);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
const keyPressFailed = err.message === 'Key press failed';
|
||||
if (keyPressFailed) {
|
||||
err.keyPressFailed = true;
|
||||
}
|
||||
try {
|
||||
this.callback(err);
|
||||
} catch (err) {
|
||||
logger.error('Callback error', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { AutoTypeEmitter };
|
||||
@@ -1,90 +0,0 @@
|
||||
import { SearchResultCollection } from 'collections/search-result-collection';
|
||||
import { Ranking } from 'util/data/ranking';
|
||||
|
||||
const urlPartsRegex = /^(\w+:\/\/)?(?:(?:www|wwws|secure)\.)?([^\/]+)\/?(.*)/;
|
||||
|
||||
const AutoTypeFilter = function (windowInfo, appModel) {
|
||||
this.title = windowInfo.title;
|
||||
this.url = windowInfo.url;
|
||||
this.text = '';
|
||||
this.ignoreWindowInfo = false;
|
||||
this.appModel = appModel;
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.getEntries = function () {
|
||||
const filter = {
|
||||
text: this.text,
|
||||
autoType: true
|
||||
};
|
||||
this.prepareFilter();
|
||||
let entries = this.appModel.getEntriesByFilter(filter).map((e) => [e, this.getEntryRank(e)]);
|
||||
if (!this.ignoreWindowInfo) {
|
||||
entries = entries.filter((e) => e[1]);
|
||||
}
|
||||
entries = entries.sort((x, y) =>
|
||||
x[1] === y[1] ? x[0].title.localeCompare(y[0].title) : y[1] - x[1]
|
||||
);
|
||||
entries = entries.map((p) => p[0]);
|
||||
return new SearchResultCollection(entries, { comparator: 'none' });
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.hasWindowInfo = function () {
|
||||
return this.title || this.url;
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.prepareFilter = function () {
|
||||
this.titleLower = this.title ? this.title.toLowerCase() : null;
|
||||
this.urlLower = this.url ? this.url.toLowerCase() : null;
|
||||
this.urlParts = this.url ? urlPartsRegex.exec(this.urlLower) : null;
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.getEntryRank = function (entry) {
|
||||
let rank = 0;
|
||||
if (this.titleLower && entry.title) {
|
||||
rank += Ranking.getStringRank(entry.title.toLowerCase(), this.titleLower);
|
||||
}
|
||||
if (this.urlParts) {
|
||||
if (entry.url) {
|
||||
const entryUrlParts = urlPartsRegex.exec(entry.url.toLowerCase());
|
||||
if (entryUrlParts) {
|
||||
const [, scheme, domain, path] = entryUrlParts;
|
||||
const [, thisScheme, thisDomain, thisPath] = this.urlParts;
|
||||
if (domain === thisDomain || thisDomain.indexOf('.' + domain) > 0) {
|
||||
if (domain === thisDomain) {
|
||||
rank += 20;
|
||||
} else {
|
||||
rank += 10;
|
||||
}
|
||||
if (path === thisPath) {
|
||||
rank += 10;
|
||||
} else if (path && thisPath) {
|
||||
if (path.lastIndexOf(thisPath, 0) === 0) {
|
||||
rank += 5;
|
||||
} else if (thisPath.lastIndexOf(path, 0) === 0) {
|
||||
rank += 3;
|
||||
}
|
||||
}
|
||||
if (scheme === thisScheme) {
|
||||
rank += 1;
|
||||
}
|
||||
} else {
|
||||
if (entry.searchText.indexOf(this.urlLower) >= 0) {
|
||||
// the url is in some field; include it
|
||||
rank += 5;
|
||||
} else {
|
||||
// another domain; don't show this record at all, ignore title match
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (entry.searchText.indexOf(this.urlLower) >= 0) {
|
||||
// the url is in some field; include it
|
||||
rank += 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
return rank;
|
||||
};
|
||||
|
||||
export { AutoTypeFilter };
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const AutoTypeHelperFactory = {
|
||||
create() {
|
||||
if (Launcher && Launcher.autoTypeSupported) {
|
||||
const { AutoTypeHelper } = require('./helper/auto-type-helper-' + Launcher.platform());
|
||||
return new AutoTypeHelper();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export { AutoTypeHelperFactory };
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NativeModules } from 'comp/launcher/native-modules';
|
||||
|
||||
class AutoTypeHelper {
|
||||
getActiveWindowInfo(callback) {
|
||||
NativeModules.kbdGetActiveWindow({
|
||||
getWindowTitle: true,
|
||||
getBrowserUrl: true
|
||||
})
|
||||
.then((win) => {
|
||||
callback(undefined, win);
|
||||
})
|
||||
.catch((err) => callback(err));
|
||||
}
|
||||
}
|
||||
|
||||
export { AutoTypeHelper };
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AutoTypeEmitterFactory } from 'auto-type/auto-type-emitter-factory';
|
||||
import { AutoTypeEmitter } from 'auto-type/auto-type-emitter';
|
||||
import { AutoTypeObfuscator } from 'auto-type/auto-type-obfuscator';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
import { Logger } from 'util/logger';
|
||||
@@ -6,7 +6,7 @@ import { Logger } from 'util/logger';
|
||||
const emitterLogger = new Logger(
|
||||
'auto-type-emitter',
|
||||
undefined,
|
||||
localStorage.debugAutoType ? Logger.Level.All : Logger.Level.Warn
|
||||
localStorage.debugAutoType ? Logger.Level.All : Logger.Level.Info
|
||||
);
|
||||
|
||||
const AutoTypeRunner = function (ops) {
|
||||
@@ -239,7 +239,7 @@ AutoTypeRunner.prototype.tryParseCommand = function (op) {
|
||||
// {VKEY 10} {VKEY 0x1F}
|
||||
op.type = 'key';
|
||||
op.value = parseInt(op.arg);
|
||||
if (isNaN(op.value) || op.value <= 0) {
|
||||
if (isNaN(op.value) || op.value < 0) {
|
||||
throw 'Bad vkey: ' + op.arg;
|
||||
}
|
||||
return true;
|
||||
@@ -432,7 +432,7 @@ AutoTypeRunner.prototype.obfuscateOp = function (op) {
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.run = function (callback, windowId) {
|
||||
this.emitter = AutoTypeEmitterFactory.create(this.emitNext.bind(this), windowId);
|
||||
this.emitter = new AutoTypeEmitter(this.emitNext.bind(this), windowId);
|
||||
this.emitterState = {
|
||||
callback,
|
||||
stack: [],
|
||||
@@ -442,7 +442,7 @@ AutoTypeRunner.prototype.run = function (callback, windowId) {
|
||||
activeMod: {},
|
||||
finished: null
|
||||
};
|
||||
this.emitNext();
|
||||
this.emitter.begin();
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.emitNext = function (err) {
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import { AutoTypeNativeHelper } from 'auto-type/helper/auto-type-native-helper';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
// http://eastmanreference.com/complete-list-of-applescript-key-codes/
|
||||
const KeyMap = {
|
||||
tab: 48,
|
||||
enter: 36,
|
||||
space: 49,
|
||||
up: 126,
|
||||
down: 125,
|
||||
left: 123,
|
||||
right: 124,
|
||||
home: 115,
|
||||
end: 119,
|
||||
pgup: 116,
|
||||
pgdn: 121,
|
||||
ins: 114,
|
||||
del: 117,
|
||||
bs: 51,
|
||||
esc: 53,
|
||||
win: 55,
|
||||
rwin: 55,
|
||||
f1: 122,
|
||||
f2: 120,
|
||||
f3: 99,
|
||||
f4: 118,
|
||||
f5: 96,
|
||||
f6: 97,
|
||||
f7: 98,
|
||||
f8: 100,
|
||||
f9: 101,
|
||||
f10: 109,
|
||||
f11: 103,
|
||||
f12: 111,
|
||||
f13: 105,
|
||||
f14: 107,
|
||||
f15: 113,
|
||||
f16: 106,
|
||||
add: 69,
|
||||
subtract: 78,
|
||||
multiply: 67,
|
||||
divide: 75,
|
||||
n0: 82,
|
||||
n1: 83,
|
||||
n2: 84,
|
||||
n3: 85,
|
||||
n4: 86,
|
||||
n5: 87,
|
||||
n6: 88,
|
||||
n7: 89,
|
||||
n8: 91,
|
||||
n9: 92
|
||||
};
|
||||
|
||||
const ModMap = {
|
||||
'^': '@',
|
||||
'+': '+',
|
||||
'%': '%',
|
||||
'^^': '^'
|
||||
};
|
||||
|
||||
const AutoTypeEmitter = function (callback) {
|
||||
this.callback = callback;
|
||||
this.mod = {};
|
||||
this.pendingScript = [];
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
|
||||
if (enabled) {
|
||||
this.mod[ModMap[mod]] = true;
|
||||
} else {
|
||||
delete this.mod[ModMap[mod]];
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.text = function (text) {
|
||||
this.pendingScript.push('text ' + this.modString() + ' ' + text);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.key = function (key) {
|
||||
if (typeof key !== 'number') {
|
||||
if (!KeyMap[key]) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
key = KeyMap[key];
|
||||
}
|
||||
this.pendingScript.push('key ' + this.modString() + key);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.copyPaste = function (text) {
|
||||
this.pendingScript.push('copypaste ' + text);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.wait = function (time) {
|
||||
this.pendingScript.push('wait ' + time);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.waitComplete = function () {
|
||||
if (this.pendingScript.length) {
|
||||
const script = this.pendingScript.join('\n');
|
||||
this.pendingScript.length = 0;
|
||||
this.runScript(script);
|
||||
} else {
|
||||
this.callback();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setDelay = function (delay) {
|
||||
this.delay = delay || 0;
|
||||
this.callback('Not implemented');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.modString = function () {
|
||||
return Object.keys(this.mod).join('');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.runScript = function (script) {
|
||||
Launcher.spawn({
|
||||
cmd: AutoTypeNativeHelper.getHelperPath(),
|
||||
data: script,
|
||||
complete: this.callback
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeEmitter };
|
||||
@@ -1,158 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Locale } from 'util/locale';
|
||||
|
||||
// https://cgit.freedesktop.org/xorg/proto/x11proto/plain/keysymdef.h
|
||||
const KeyMap = {
|
||||
tab: 'Tab',
|
||||
enter: 'KP_Enter',
|
||||
space: 'KP_Space',
|
||||
up: 'Up',
|
||||
down: 'Down',
|
||||
left: 'Left',
|
||||
right: 'Right',
|
||||
home: 'Home',
|
||||
end: 'End',
|
||||
pgup: 'Page_Up',
|
||||
pgdn: 'Page_Down',
|
||||
ins: 'Insert',
|
||||
del: 'Delete',
|
||||
bs: 'BackSpace',
|
||||
esc: 'Escape',
|
||||
win: 'Meta_L',
|
||||
rwin: 'Meta_R',
|
||||
f1: 'F1',
|
||||
f2: 'F2',
|
||||
f3: 'F3',
|
||||
f4: 'F4',
|
||||
f5: 'F5',
|
||||
f6: 'F6',
|
||||
f7: 'F7',
|
||||
f8: 'F8',
|
||||
f9: 'F9',
|
||||
f10: 'F10',
|
||||
f11: 'F11',
|
||||
f12: 'F12',
|
||||
f13: 'F13',
|
||||
f14: 'F14',
|
||||
f15: 'F15',
|
||||
f16: 'F16',
|
||||
add: 'KP_Add',
|
||||
subtract: 'KP_Subtract',
|
||||
multiply: 'KP_Multiply',
|
||||
divide: 'KP_Divide',
|
||||
n0: 'KP_0',
|
||||
n1: 'KP_1',
|
||||
n2: 'KP_2',
|
||||
n3: 'KP_3',
|
||||
n4: 'KP_4',
|
||||
n5: 'KP_5',
|
||||
n6: 'KP_6',
|
||||
n7: 'KP_7',
|
||||
n8: 'KP_8',
|
||||
n9: 'KP_9'
|
||||
};
|
||||
|
||||
const ModMap = {
|
||||
'^': 'ctrl',
|
||||
'+': 'shift',
|
||||
'%': 'alt',
|
||||
'^^': 'ctrl'
|
||||
};
|
||||
|
||||
const AutoTypeEmitter = function (callback, windowId) {
|
||||
this.callback = callback;
|
||||
this.mod = {};
|
||||
this.pendingScript = [];
|
||||
if (typeof windowId !== 'undefined' && windowId) {
|
||||
this.pendingScript.push('windowactivate --sync ' + windowId);
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
|
||||
if (enabled) {
|
||||
this.mod[ModMap[mod]] = true;
|
||||
} else {
|
||||
delete this.mod[ModMap[mod]];
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.text = function (text) {
|
||||
this.pendingScript.push('keyup ctrl alt shift t');
|
||||
Object.keys(this.mod).forEach((mod) => {
|
||||
this.pendingScript.push('keydown ' + ModMap[mod]);
|
||||
});
|
||||
text.split('').forEach((char) => {
|
||||
this.pendingScript.push('key ' + 'U' + char.charCodeAt(0).toString(16));
|
||||
});
|
||||
Object.keys(this.mod).forEach((mod) => {
|
||||
this.pendingScript.push('keyup ' + ModMap[mod]);
|
||||
});
|
||||
this.waitComplete();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.key = function (key) {
|
||||
const isSpecialKey = typeof key !== 'number';
|
||||
if (isSpecialKey) {
|
||||
if (!KeyMap[key]) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
key = KeyMap[key].toString();
|
||||
}
|
||||
this.pendingScript.push('key --clearmodifiers ' + this.modString() + key);
|
||||
if (isSpecialKey) {
|
||||
this.waitComplete();
|
||||
} else {
|
||||
this.callback();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.copyPaste = function (text) {
|
||||
this.pendingScript.push('sleep 0.5');
|
||||
Launcher.setClipboardText(text);
|
||||
this.pendingScript.push('key --clearmodifiers ' + 'shift+Insert');
|
||||
this.pendingScript.push('sleep 0.5');
|
||||
this.waitComplete();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.wait = function (time) {
|
||||
this.pendingScript.push('sleep ' + time / 1000);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.waitComplete = function (callback) {
|
||||
if (this.pendingScript.length) {
|
||||
const script = this.pendingScript.join(' ');
|
||||
this.pendingScript.length = 0;
|
||||
this.runScript(script, callback);
|
||||
} else {
|
||||
this.callback();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.modString = function () {
|
||||
let mod = '';
|
||||
Object.keys(this.mod).forEach((key) => {
|
||||
mod += key + '+';
|
||||
});
|
||||
return mod;
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.runScript = function (script, callback) {
|
||||
// xdotool doesn't like it when stdin doesn't end with a linebreak
|
||||
// see https://github.com/keeweb/keeweb/issues/1409
|
||||
const data = script + '\n';
|
||||
Launcher.spawn({
|
||||
cmd: 'xdotool',
|
||||
args: ['-'],
|
||||
data,
|
||||
complete: (err, stdout, code) => {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
err = Locale.autoTypeErrorNotInstalled.replace('{}', 'xdotool');
|
||||
}
|
||||
const cb = callback || this.callback;
|
||||
cb(err, stdout, code);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeEmitter };
|
||||
@@ -1,131 +0,0 @@
|
||||
import { AutoTypeNativeHelper } from 'auto-type/helper/auto-type-native-helper';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||
const KeyMap = {
|
||||
tab: 0x09,
|
||||
enter: 0x0d,
|
||||
space: 0x20,
|
||||
up: 0x26,
|
||||
down: 0x28,
|
||||
left: 0x25,
|
||||
right: 0x27,
|
||||
home: 0x24,
|
||||
end: 0x23,
|
||||
pgup: 0x21,
|
||||
pgdn: 0x22,
|
||||
ins: 0x2d,
|
||||
del: 0x2e,
|
||||
bs: 0x08,
|
||||
esc: 0x1b,
|
||||
win: 0x5b,
|
||||
rwin: 0x5c,
|
||||
f1: 0x70,
|
||||
f2: 0x71,
|
||||
f3: 0x72,
|
||||
f4: 0x73,
|
||||
f5: 0x74,
|
||||
f6: 0x75,
|
||||
f7: 0x76,
|
||||
f8: 0x77,
|
||||
f9: 0x78,
|
||||
f10: 0x79,
|
||||
f11: 0x7a,
|
||||
f12: 0x7b,
|
||||
f13: 0x7c,
|
||||
f14: 0x7d,
|
||||
f15: 0x7e,
|
||||
f16: 0x7f,
|
||||
add: 0x6b,
|
||||
subtract: 0x6d,
|
||||
multiply: 0x6a,
|
||||
divide: 0x6f,
|
||||
n0: 0x30,
|
||||
n1: 0x31,
|
||||
n2: 0x32,
|
||||
n3: 0x33,
|
||||
n4: 0x34,
|
||||
n5: 0x35,
|
||||
n6: 0x36,
|
||||
n7: 0x37,
|
||||
n8: 0x38,
|
||||
n9: 0x39
|
||||
};
|
||||
|
||||
const ModMap = {
|
||||
'^': '^',
|
||||
'+': '+',
|
||||
'%': '%',
|
||||
'^^': '^'
|
||||
};
|
||||
|
||||
const AutoTypeEmitter = function (callback) {
|
||||
this.callback = callback;
|
||||
this.mod = {};
|
||||
this.pendingScript = [];
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
|
||||
if (enabled) {
|
||||
this.mod[ModMap[mod]] = true;
|
||||
} else {
|
||||
delete this.mod[ModMap[mod]];
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.text = function (text) {
|
||||
if (text) {
|
||||
this.pendingScript.push('text ' + this.modStr() + ' ' + text);
|
||||
}
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.key = function (key) {
|
||||
if (typeof key !== 'number') {
|
||||
if (!KeyMap[key]) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
key = KeyMap[key];
|
||||
}
|
||||
this.pendingScript.push('key ' + this.modStr() + key);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.copyPaste = function (text) {
|
||||
this.pendingScript.push('copypaste ' + text);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.wait = function (time) {
|
||||
this.pendingScript.push('wait ' + time);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.waitComplete = function () {
|
||||
if (this.pendingScript.length) {
|
||||
const script = this.pendingScript.join('\n');
|
||||
this.pendingScript.length = 0;
|
||||
this.runScript(script);
|
||||
} else {
|
||||
this.callback();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setDelay = function (delay) {
|
||||
this.delay = delay || 0;
|
||||
this.callback('Not implemented');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.modStr = function () {
|
||||
return Object.keys(this.mod).join('');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.runScript = function (script) {
|
||||
Launcher.spawn({
|
||||
cmd: AutoTypeNativeHelper.getHelperPath(),
|
||||
data: script,
|
||||
complete: this.callback
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeEmitter };
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const ForeMostAppScript =
|
||||
'tell application "System Events"\n' +
|
||||
' set frontAppName to name of first process whose frontmost is true\n' +
|
||||
' set frontAppId to id of first process whose frontmost is true\n' +
|
||||
'end tell\n' +
|
||||
'"" & frontAppId & " " & frontAppName';
|
||||
const ChromeScript =
|
||||
'tell application "{}" to set appUrl to URL of active tab of front window\n' +
|
||||
'tell application "{}" to set appTitle to title of active tab of front window\n' +
|
||||
'return appUrl & "\n" & appTitle';
|
||||
const SafariScript =
|
||||
'tell application "{}" to set appUrl to URL of front document\n' +
|
||||
'tell application "{}" to set appTitle to name of front document\n' +
|
||||
'return appUrl & "\n" & appTitle';
|
||||
const OtherAppsScript =
|
||||
'tell application "System Events"\n' +
|
||||
' tell process "{}"\n' +
|
||||
' tell (1st window whose value of attribute "AXMain" is true)\n' +
|
||||
' set windowTitle to value of attribute "AXTitle"\n' +
|
||||
' end tell\n' +
|
||||
' end tell\n' +
|
||||
'end tell';
|
||||
|
||||
const AutoTypeHelper = function () {};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowInfo = function (callback) {
|
||||
AutoTypeHelper.exec(ForeMostAppScript, (err, out) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
const output = out.trim();
|
||||
const spaceIx = output.indexOf(' ');
|
||||
let id = '',
|
||||
appName = '';
|
||||
if (spaceIx >= 0) {
|
||||
id = output.substr(0, spaceIx);
|
||||
appName = output.substr(spaceIx + 1).trim();
|
||||
}
|
||||
// getting urls and titles from Chrome or Safari:
|
||||
// - will suit in 90% cases
|
||||
// - does not require assistive access
|
||||
// - allows to get url
|
||||
if (['Google Chrome', 'Chromium', 'Google Chrome Canary'].indexOf(appName) >= 0) {
|
||||
AutoTypeHelper.exec(ChromeScript.replace(/\{}/g, appName), (err, out) => {
|
||||
if (err) {
|
||||
return callback(err, { id });
|
||||
}
|
||||
const parts = out.split('\n');
|
||||
return callback(null, {
|
||||
id,
|
||||
url: parts[0].trim(),
|
||||
title: (parts[1] || '').trim()
|
||||
});
|
||||
});
|
||||
} else if (['Safari', 'Webkit'].indexOf(appName) >= 0) {
|
||||
AutoTypeHelper.exec(SafariScript.replace(/\{}/g, appName), (err, out) => {
|
||||
if (err) {
|
||||
return callback(err, { id });
|
||||
}
|
||||
const parts = out.split('\n');
|
||||
return callback(null, {
|
||||
id,
|
||||
url: parts[0].trim(),
|
||||
title: (parts[1] || '').trim()
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// special cases are not available. this method may ask the user about assistive access
|
||||
AutoTypeHelper.exec(OtherAppsScript.replace(/\{}/g, appName), (err, out) => {
|
||||
if (err) {
|
||||
return callback(err, { id });
|
||||
}
|
||||
return callback(null, {
|
||||
id,
|
||||
title: out.trim()
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
AutoTypeHelper.exec = function (script, callback) {
|
||||
Launcher.spawn({
|
||||
cmd: 'osascript',
|
||||
args: ['-e', script],
|
||||
complete: callback
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeHelper };
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const AutoTypeHelper = function () {};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowInfo = function (callback) {
|
||||
Launcher.spawn({
|
||||
cmd: 'xdotool',
|
||||
args: ['getactivewindow', 'getwindowname', 'getactivewindow'],
|
||||
complete(err, out) {
|
||||
let windowInfo;
|
||||
if (out) {
|
||||
const [title, id] = out.trim().split('\n');
|
||||
windowInfo = {
|
||||
id,
|
||||
title
|
||||
};
|
||||
}
|
||||
return callback(err, windowInfo);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeHelper };
|
||||
@@ -1,25 +0,0 @@
|
||||
import { AutoTypeNativeHelper } from 'auto-type/helper/auto-type-native-helper';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const AutoTypeHelper = function () {};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowInfo = function (callback) {
|
||||
Launcher.spawn({
|
||||
cmd: AutoTypeNativeHelper.getHelperPath(),
|
||||
args: ['--window-info'],
|
||||
complete(err, out) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
const [id, title, url] = out.trim().split('\n');
|
||||
const windowInfo = {
|
||||
id,
|
||||
title,
|
||||
url
|
||||
};
|
||||
return callback(null, windowInfo);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeHelper };
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Logger } from 'util/logger';
|
||||
|
||||
const logger = new Logger('auto-type');
|
||||
|
||||
const AutoTypeNativeHelper = {
|
||||
getHelperPath() {
|
||||
if (this._helperPath) {
|
||||
return this._helperPath;
|
||||
}
|
||||
const ext = process.platform === 'win32' ? '.exe' : '';
|
||||
const helperRelPath = `helper/${process.platform}/KeeWebHelper${ext}`;
|
||||
const helperPath = Launcher.getAppPath(helperRelPath);
|
||||
Launcher.ensureRunnable(helperPath);
|
||||
logger.debug('Using auto-type helper', helperPath);
|
||||
this._helperPath = helperPath;
|
||||
return helperPath;
|
||||
}
|
||||
};
|
||||
|
||||
export { AutoTypeNativeHelper };
|
||||
@@ -1,25 +1,25 @@
|
||||
import { Events } from 'framework/events';
|
||||
import { AutoTypeFilter } from 'auto-type/auto-type-filter';
|
||||
import { AutoTypeHelperFactory } from 'auto-type/auto-type-helper-factory';
|
||||
import { AutoTypeHelper } from 'auto-type/auto-type-helper';
|
||||
import { AutoTypeParser } from 'auto-type/auto-type-parser';
|
||||
import { SelectEntryFilter } from 'comp/app/select-entry-filter';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Features } from 'util/features';
|
||||
import { Alerts } from 'comp/ui/alerts';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { AppModel } from 'models/app-model';
|
||||
import { Locale } from 'util/locale';
|
||||
import { Logger } from 'util/logger';
|
||||
import { AutoTypeSelectView } from 'views/auto-type/auto-type-select-view';
|
||||
import { Links } from 'const/links';
|
||||
import { SelectEntryView } from 'views/select/select-entry-view';
|
||||
|
||||
const logger = new Logger('auto-type');
|
||||
const clearTextAutoTypeLog = !!localStorage.debugAutoType;
|
||||
|
||||
const AutoType = {
|
||||
helper: AutoTypeHelperFactory.create(),
|
||||
enabled: !!(Launcher && Launcher.autoTypeSupported),
|
||||
supportsEventsWithWindowId: !!(Launcher && Launcher.platform() === 'linux'),
|
||||
selectEntryView: false,
|
||||
pendingEvent: null,
|
||||
running: false,
|
||||
|
||||
init() {
|
||||
@@ -27,10 +27,6 @@ const AutoType = {
|
||||
return;
|
||||
}
|
||||
Events.on('auto-type', (e) => this.handleEvent(e));
|
||||
Events.on('main-window-blur', (e) => this.mainWindowBlur(e));
|
||||
Events.on('main-window-focus', (e) => this.mainWindowFocus(e));
|
||||
Events.on('main-window-will-close', (e) => this.mainWindowWillClose(e));
|
||||
Events.on('closed-open-view', (e) => this.processPendingEvent(e));
|
||||
},
|
||||
|
||||
handleEvent(e) {
|
||||
@@ -64,9 +60,16 @@ const AutoType = {
|
||||
runAndHandleResult(result, windowId) {
|
||||
this.run(result, windowId, (err) => {
|
||||
if (err) {
|
||||
let body = Locale.autoTypeErrorGeneric.replace('{}', err.message || err.toString());
|
||||
let link;
|
||||
if (err.keyPressFailed && Features.isMac) {
|
||||
body = Locale.autoTypeErrorAccessibilityMacOS;
|
||||
link = Links.AutoTypeMacOS;
|
||||
}
|
||||
Alerts.error({
|
||||
header: Locale.autoTypeError,
|
||||
body: Locale.autoTypeErrorGeneric.replace('{}', err.toString())
|
||||
body,
|
||||
link
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -161,7 +164,8 @@ const AutoType = {
|
||||
|
||||
getActiveWindowInfo(callback) {
|
||||
logger.debug('Getting window info');
|
||||
return this.helper.getActiveWindowInfo((err, windowInfo) => {
|
||||
const helper = new AutoTypeHelper();
|
||||
return helper.getActiveWindowInfo((err, windowInfo) => {
|
||||
if (err) {
|
||||
logger.error('Error getting window info', err);
|
||||
} else {
|
||||
@@ -207,25 +211,42 @@ const AutoType = {
|
||||
},
|
||||
|
||||
selectEntryAndRun() {
|
||||
this.getActiveWindowInfo((e, windowInfo) => {
|
||||
const filter = new AutoTypeFilter(windowInfo, AppModel.instance);
|
||||
this.getActiveWindowInfo(async (e, windowInfo) => {
|
||||
const filter = new SelectEntryFilter(
|
||||
windowInfo,
|
||||
AppModel.instance,
|
||||
AppModel.instance.files,
|
||||
{
|
||||
autoType: true
|
||||
}
|
||||
);
|
||||
const evt = { filter, windowInfo };
|
||||
if (!AppModel.instance.files.hasOpenFiles()) {
|
||||
this.pendingEvent = evt;
|
||||
logger.debug('auto-type event delayed');
|
||||
this.focusMainWindow();
|
||||
} else {
|
||||
this.processEventWithFilter(evt);
|
||||
try {
|
||||
await AppModel.instance.unlockAnyFile('autoTypeUnlockMessage');
|
||||
} catch {
|
||||
logger.debug('auto-type event canceled');
|
||||
return;
|
||||
}
|
||||
if (this.selectEntryView) {
|
||||
this.selectEntryView.show();
|
||||
}
|
||||
}
|
||||
logger.debug('processing auto-type event');
|
||||
this.processEventWithFilter(evt);
|
||||
});
|
||||
},
|
||||
|
||||
focusMainWindow() {
|
||||
setTimeout(() => Launcher.showMainWindow(), Timeouts.RedrawInactiveWindow);
|
||||
if (!Launcher.isAppFocused()) {
|
||||
setTimeout(() => Launcher.showMainWindow(), Timeouts.RedrawInactiveWindow);
|
||||
}
|
||||
},
|
||||
|
||||
processEventWithFilter(evt) {
|
||||
const entries = evt.filter.getEntries();
|
||||
let entries = evt.filter.getEntries();
|
||||
if (entries.length === 1 && AppSettingsModel.directAutotype) {
|
||||
this.hideWindow(() => {
|
||||
this.runAndHandleResult({ entry: entries[0] }, evt.windowInfo.id);
|
||||
@@ -233,8 +254,31 @@ const AutoType = {
|
||||
return;
|
||||
}
|
||||
this.focusMainWindow();
|
||||
evt.filter.ignoreWindowInfo = true;
|
||||
this.selectEntryView = new AutoTypeSelectView({ filter: evt.filter });
|
||||
|
||||
if (entries.length === 0) {
|
||||
if (evt.filter.useUrl) {
|
||||
evt.filter.useUrl = false;
|
||||
if (evt.filter.title && AppSettingsModel.autoTypeTitleFilterEnabled) {
|
||||
evt.filter.useTitle = true;
|
||||
}
|
||||
}
|
||||
entries = evt.filter.getEntries();
|
||||
if (entries.length === 0 && evt.filter.useTitle) {
|
||||
evt.filter.useTitle = false;
|
||||
}
|
||||
}
|
||||
|
||||
const humanReadableTarget = evt.filter.title || evt.filter.url;
|
||||
const topMessage = humanReadableTarget
|
||||
? Locale.autoTypeMsgMatchedByWindow.replace('{}', humanReadableTarget)
|
||||
: Locale.autoTypeMsgNoWindow;
|
||||
|
||||
this.selectEntryView = new SelectEntryView({
|
||||
isAutoType: true,
|
||||
itemOptions: true,
|
||||
filter: evt.filter,
|
||||
topMessage
|
||||
});
|
||||
this.selectEntryView.on('result', (result) => {
|
||||
logger.debug('Entry selected', result);
|
||||
this.selectEntryView.off('result');
|
||||
@@ -251,55 +295,17 @@ const AutoType = {
|
||||
});
|
||||
});
|
||||
this.selectEntryView.render();
|
||||
this.selectEntryView.on('show-open-files', () => {
|
||||
this.selectEntryView.on('show-open-files', async () => {
|
||||
this.selectEntryView.hide();
|
||||
Events.emit('open-file');
|
||||
});
|
||||
},
|
||||
|
||||
mainWindowBlur() {
|
||||
this.mainWindowBlurTimer = setTimeout(() => {
|
||||
// macOS emits focus-blur-focus event in a row when triggering auto-type from minimized state
|
||||
delete this.mainWindowBlurTimer;
|
||||
this.resetPendingEvent();
|
||||
if (this.selectEntryView) {
|
||||
try {
|
||||
await AppModel.instance.unlockAnyFile('autoTypeUnlockMessage');
|
||||
} catch {
|
||||
this.selectEntryView.emit('result', undefined);
|
||||
return;
|
||||
}
|
||||
}, Timeouts.AutoTypeWindowFocusAfterBlur);
|
||||
},
|
||||
|
||||
mainWindowFocus() {
|
||||
if (this.mainWindowBlurTimer) {
|
||||
clearTimeout(this.mainWindowBlurTimer);
|
||||
this.mainWindowBlurTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
mainWindowWillClose() {
|
||||
this.resetPendingEvent();
|
||||
if (this.selectEntryView) {
|
||||
this.selectEntryView.emit('result', undefined);
|
||||
}
|
||||
},
|
||||
|
||||
resetPendingEvent() {
|
||||
if (this.pendingEvent) {
|
||||
this.pendingEvent = null;
|
||||
logger.debug('auto-type event canceled');
|
||||
}
|
||||
},
|
||||
|
||||
processPendingEvent() {
|
||||
if (this.selectEntryView) {
|
||||
this.selectEntryView.show();
|
||||
}
|
||||
if (!this.pendingEvent) {
|
||||
return;
|
||||
}
|
||||
logger.debug('processing pending auto-type event');
|
||||
const evt = this.pendingEvent;
|
||||
this.pendingEvent = null;
|
||||
this.processEventWithFilter(evt);
|
||||
this.selectEntryView.render();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Collection } from 'framework/collection';
|
||||
import { ExternalEntryModel } from 'models/external/external-entry-model';
|
||||
|
||||
class ExternalEntryCollection extends Collection {
|
||||
static model = ExternalEntryModel;
|
||||
}
|
||||
|
||||
export { ExternalEntryCollection };
|
||||
@@ -58,6 +58,7 @@ const AppRightsChecker = {
|
||||
runInstaller() {
|
||||
Launcher.spawn({
|
||||
cmd: this.AppPath + '/Contents/Installer/KeeWeb Installer.app/Contents/MacOS/applet',
|
||||
args: ['--install'],
|
||||
complete: () => {
|
||||
this.needRunInstaller((needRun) => {
|
||||
if (this.alert && !needRun) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { RuntimeDataModel } from 'models/runtime-data-model';
|
||||
|
||||
const ExportApi = {
|
||||
settings: {
|
||||
@@ -11,6 +12,17 @@ const ExportApi = {
|
||||
del(key) {
|
||||
delete AppSettingsModel[key];
|
||||
}
|
||||
},
|
||||
runtimeData: {
|
||||
get(key) {
|
||||
return key ? RuntimeDataModel[key] : { ...RuntimeDataModel };
|
||||
},
|
||||
set(key, value) {
|
||||
RuntimeDataModel[key] = value;
|
||||
},
|
||||
del(key) {
|
||||
delete RuntimeDataModel[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,18 @@ const GeneratorPresets = {
|
||||
};
|
||||
},
|
||||
|
||||
get browserExtensionPreset() {
|
||||
return {
|
||||
name: 'BrowserExtension',
|
||||
length: 20,
|
||||
upper: true,
|
||||
lower: true,
|
||||
special: true,
|
||||
brackets: true,
|
||||
ambiguous: true
|
||||
};
|
||||
},
|
||||
|
||||
get builtIn() {
|
||||
return [
|
||||
this.defaultPreset,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Logger } from 'util/logger';
|
||||
|
||||
const logger = new Logger('online-password-checker');
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { SearchResultCollection } from 'collections/search-result-collection';
|
||||
import { Ranking } from 'util/data/ranking';
|
||||
|
||||
const urlPartsRegex = /^(\w+:\/\/)?(?:(?:www|wwws|secure)\.)?([^\/]+)\/?(.*)/;
|
||||
|
||||
class SelectEntryFilter {
|
||||
constructor(windowInfo, appModel, files, filterOptions) {
|
||||
this.title = windowInfo.title;
|
||||
this.useTitle = !!windowInfo.title && !windowInfo.url;
|
||||
this.url = windowInfo.url;
|
||||
this.useUrl = !!windowInfo.url;
|
||||
this.subdomains = true;
|
||||
this.text = '';
|
||||
this.appModel = appModel;
|
||||
this.files = files;
|
||||
this.filterOptions = filterOptions;
|
||||
}
|
||||
|
||||
getEntries() {
|
||||
const filter = {
|
||||
text: this.text,
|
||||
...this.filterOptions
|
||||
};
|
||||
let entries = this.appModel
|
||||
.getEntriesByFilter(filter, this.files)
|
||||
.map((e) => [e, this._getEntryRank(e)]);
|
||||
if (this.useUrl || this.useTitle) {
|
||||
entries = entries.filter((e) => e[1]);
|
||||
}
|
||||
entries = entries.sort((x, y) =>
|
||||
x[1] === y[1] ? x[0].title.localeCompare(y[0].title) : y[1] - x[1]
|
||||
);
|
||||
entries = entries.map((p) => p[0]);
|
||||
return new SearchResultCollection(entries, { comparator: 'none' });
|
||||
}
|
||||
|
||||
_getEntryRank(entry) {
|
||||
let titleRank = 0;
|
||||
let urlRank = 0;
|
||||
|
||||
if (this.useTitle && this.title && entry.title) {
|
||||
titleRank = Ranking.getStringRank(entry.title.toLowerCase(), this.title.toLowerCase());
|
||||
if (!titleRank) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.useUrl && this.url) {
|
||||
const searchUrlLower = this.url.toLowerCase();
|
||||
const searchUrlParts = urlPartsRegex.exec(searchUrlLower);
|
||||
|
||||
for (const url of entry.getAllUrls()) {
|
||||
const entryUrlParts = urlPartsRegex.exec(url.toLowerCase());
|
||||
if (entryUrlParts) {
|
||||
const [, scheme, domain, path] = entryUrlParts;
|
||||
const [, searchScheme, searchDomain, searchPath] = searchUrlParts;
|
||||
if (
|
||||
domain === searchDomain ||
|
||||
(this.subdomains && searchDomain.indexOf('.' + domain) > 0)
|
||||
) {
|
||||
if (domain === searchDomain) {
|
||||
urlRank += 20;
|
||||
} else {
|
||||
urlRank += 10;
|
||||
}
|
||||
if (path === searchPath) {
|
||||
urlRank += 10;
|
||||
} else if (path && searchPath) {
|
||||
if (path.lastIndexOf(searchPath, 0) === 0) {
|
||||
urlRank += 5;
|
||||
} else if (searchPath.lastIndexOf(path, 0) === 0) {
|
||||
urlRank += 3;
|
||||
}
|
||||
}
|
||||
if (scheme === searchScheme) {
|
||||
urlRank += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.searchText.includes(searchUrlLower)) {
|
||||
// the url is in some field; include it
|
||||
urlRank += 5;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.useTitle && !titleRank) {
|
||||
return 0;
|
||||
}
|
||||
if (this.useUrl && !urlRank) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return titleRank + urlRank;
|
||||
}
|
||||
}
|
||||
|
||||
export { SelectEntryFilter };
|
||||
+110
-82
@@ -1,3 +1,4 @@
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Events } from 'framework/events';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { Transport } from 'comp/browser/transport';
|
||||
@@ -15,10 +16,9 @@ const Updater = {
|
||||
UpdateInterval: 1000 * 60 * 60 * 24,
|
||||
MinUpdateTimeout: 500,
|
||||
MinUpdateSize: 10000,
|
||||
UpdateCheckFiles: ['app.asar'],
|
||||
nextCheckTimeout: null,
|
||||
updateCheckDate: new Date(0),
|
||||
enabled: Launcher && Launcher.updaterEnabled(),
|
||||
enabled: Launcher?.updaterEnabled(),
|
||||
|
||||
getAutoUpdateType() {
|
||||
if (!this.enabled) {
|
||||
@@ -34,7 +34,7 @@ const Updater = {
|
||||
updateInProgress() {
|
||||
return (
|
||||
UpdateModel.status === 'checking' ||
|
||||
['downloading', 'extracting'].indexOf(UpdateModel.updateStatus) >= 0
|
||||
['downloading', 'extracting', 'updating'].indexOf(UpdateModel.updateStatus) >= 0
|
||||
);
|
||||
},
|
||||
|
||||
@@ -97,13 +97,12 @@ const Updater = {
|
||||
}
|
||||
logger.info('Checking for update...');
|
||||
Transport.httpGet({
|
||||
url: Links.Manifest,
|
||||
utf8: true,
|
||||
success: (data) => {
|
||||
url: Links.UpdateJson,
|
||||
json: true,
|
||||
success: (updateJson) => {
|
||||
const dt = new Date();
|
||||
const match = data.match(/#\s*(\d+\-\d+\-\d+):v([\d+\.\w]+)/);
|
||||
logger.info('Update check: ' + (match ? match[0] : 'unknown'));
|
||||
if (!match) {
|
||||
logger.info('Update check: ' + (updateJson.version || 'unknown'));
|
||||
if (!updateJson.version) {
|
||||
const errMsg = 'No version info found';
|
||||
UpdateModel.set({
|
||||
status: 'error',
|
||||
@@ -114,16 +113,15 @@ const Updater = {
|
||||
this.scheduleNextCheck();
|
||||
return;
|
||||
}
|
||||
const updateMinVersionMatch = data.match(/#\s*updmin:v([\d+\.\w]+)/);
|
||||
const prevLastVersion = UpdateModel.lastVersion;
|
||||
UpdateModel.set({
|
||||
status: 'ok',
|
||||
lastCheckDate: dt,
|
||||
lastSuccessCheckDate: dt,
|
||||
lastVersionReleaseDate: new Date(match[1]),
|
||||
lastVersion: match[2],
|
||||
lastVersionReleaseDate: new Date(updateJson.date),
|
||||
lastVersion: updateJson.version,
|
||||
lastCheckError: null,
|
||||
lastCheckUpdMin: updateMinVersionMatch ? updateMinVersionMatch[1] : null
|
||||
lastCheckUpdMin: updateJson.minVersion || null
|
||||
});
|
||||
UpdateModel.save();
|
||||
this.scheduleNextCheck();
|
||||
@@ -161,7 +159,7 @@ const Updater = {
|
||||
canAutoUpdate() {
|
||||
const minLauncherVersion = UpdateModel.lastCheckUpdMin;
|
||||
if (minLauncherVersion) {
|
||||
const cmp = SemVer.compareVersions(Launcher.version, minLauncherVersion);
|
||||
const cmp = SemVer.compareVersions(RuntimeInfo.version, minLauncherVersion);
|
||||
if (cmp < 0) {
|
||||
UpdateModel.set({ updateStatus: 'ready', updateManual: true });
|
||||
return false;
|
||||
@@ -182,28 +180,55 @@ const Updater = {
|
||||
}
|
||||
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: Links.UpdateDesktop.replace('{ver}', ver),
|
||||
file: 'KeeWeb-' + ver + '.zip',
|
||||
cache: !startedByUser,
|
||||
success: (filePath) => {
|
||||
UpdateModel.set({ updateStatus: 'extracting' });
|
||||
logger.info('Extracting update file', this.UpdateCheckFiles, filePath);
|
||||
this.extractAppUpdate(filePath, (err) => {
|
||||
if (err) {
|
||||
logger.error('Error extracting update', err);
|
||||
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 extracting update'
|
||||
updateError: 'Error downloading update signatures'
|
||||
});
|
||||
} else {
|
||||
UpdateModel.set({ updateStatus: 'ready', updateError: null });
|
||||
if (!startedByUser) {
|
||||
Events.emit('update-app');
|
||||
}
|
||||
if (typeof successCallback === 'function') {
|
||||
successCallback();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -217,61 +242,64 @@ const Updater = {
|
||||
});
|
||||
},
|
||||
|
||||
extractAppUpdate(updateFile, cb) {
|
||||
const expectedFiles = this.UpdateCheckFiles;
|
||||
const appPath = Launcher.getUserDataPath();
|
||||
const StreamZip = Launcher.req('node-stream-zip');
|
||||
StreamZip.setFs(Launcher.req('original-fs'));
|
||||
const zip = new StreamZip({ file: updateFile, storeEntries: true });
|
||||
zip.on('error', cb);
|
||||
zip.on('ready', () => {
|
||||
const containsAll = expectedFiles.every((expFile) => {
|
||||
const entry = zip.entry(expFile);
|
||||
return entry && entry.isFile;
|
||||
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);
|
||||
});
|
||||
if (!containsAll) {
|
||||
return cb('Bad archive');
|
||||
}
|
||||
this.validateArchiveSignature(updateFile, zip)
|
||||
.then(() => {
|
||||
zip.extract(null, appPath, (err) => {
|
||||
zip.close();
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
Launcher.deleteFile(updateFile);
|
||||
cb();
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
return cb('Invalid archive: ' + e);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
validateArchiveSignature(archivePath, zip) {
|
||||
if (!zip.comment) {
|
||||
return Promise.reject('No comment in ZIP');
|
||||
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;
|
||||
}
|
||||
if (zip.comment.length !== 512) {
|
||||
return Promise.reject('Bad comment length in ZIP: ' + zip.comment.length);
|
||||
}
|
||||
try {
|
||||
const zipFileData = Launcher.req('fs').readFileSync(archivePath);
|
||||
const dataToVerify = zipFileData.slice(0, zip.centralDirectory.headerOffset + 22);
|
||||
const signature = window.Buffer.from(zip.comment, 'hex');
|
||||
return SignatureVerifier.verify(dataToVerify, signature)
|
||||
.catch(() => {
|
||||
throw new Error('Error verifying signature');
|
||||
})
|
||||
.then((isValid) => {
|
||||
if (!isValid) {
|
||||
throw new Error('Invalid signature');
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
return Promise.reject(err.toString());
|
||||
return undefined;
|
||||
},
|
||||
|
||||
installAndRestart() {
|
||||
if (!Launcher) {
|
||||
return;
|
||||
}
|
||||
const updateAssetName = this.getUpdateAssetName(UpdateModel.lastVersion);
|
||||
const updateFilePath = Transport.cacheFilePath(updateAssetName);
|
||||
Launcher.requestRestartAndUpdate(updateFilePath);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -203,12 +203,16 @@ const YubiKey = {
|
||||
|
||||
this.process = Launcher.spawn({
|
||||
cmd: this.cmd(),
|
||||
args: ['-d', serial, 'oath', 'code'],
|
||||
args: ['-d', serial, 'oath', 'accounts', 'code'],
|
||||
noStdOutLogging: true,
|
||||
throwOnStdErr: true,
|
||||
complete: (err, stdout) => {
|
||||
this.process = null;
|
||||
|
||||
if (window.debugYubiKey) {
|
||||
logger.info('received codes', err, stdout);
|
||||
}
|
||||
|
||||
if (this.aborted) {
|
||||
return callback('Aborted');
|
||||
}
|
||||
@@ -241,7 +245,7 @@ const YubiKey = {
|
||||
getOtp(serial, entry, callback) {
|
||||
return Launcher.spawn({
|
||||
cmd: this.cmd(),
|
||||
args: ['-d', serial, 'oath', 'code', '--single', entry],
|
||||
args: ['-d', serial, 'oath', 'accounts', 'code', '--single', entry],
|
||||
noStdOutLogging: true,
|
||||
complete: (err, stdout) => {
|
||||
if (err) {
|
||||
|
||||
@@ -45,12 +45,28 @@ const PopupNotifier = {
|
||||
Timeouts.CheckWindowClosed
|
||||
);
|
||||
} else {
|
||||
const loc = PopupNotifier.tryGetLocationSearch(win);
|
||||
if (loc) {
|
||||
try {
|
||||
win.close();
|
||||
} catch {}
|
||||
PopupNotifier.triggerClosed(win, loc);
|
||||
return;
|
||||
}
|
||||
PopupNotifier.deferCheckClosed(win);
|
||||
}
|
||||
},
|
||||
|
||||
triggerClosed(win) {
|
||||
Events.emit('popup-closed', win);
|
||||
tryGetLocationSearch(win) {
|
||||
try {
|
||||
if (win.location.host === location.host) {
|
||||
return win.location.search;
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
|
||||
triggerClosed(window, locationSearch) {
|
||||
Events.emit('popup-closed', { window, locationSearch });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
|
||||
const SecureInput = function () {
|
||||
this.el = null;
|
||||
@@ -79,7 +79,7 @@ Object.defineProperty(SecureInput.prototype, 'value', {
|
||||
const len = pseudoValue.length;
|
||||
let byteLength = 0;
|
||||
const valueBytes = new Uint8Array(len * 4);
|
||||
const saltBytes = kdbxweb.Random.getBytes(len * 4);
|
||||
const saltBytes = kdbxweb.CryptoEngine.random(len * 4);
|
||||
let ch;
|
||||
let bytes;
|
||||
for (let i = 0; i < len; i++) {
|
||||
|
||||
@@ -5,12 +5,14 @@ const ThemeWatcher = {
|
||||
|
||||
init() {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
mediaQuery.addEventListener('change', (e) => {
|
||||
const dark = e.matches;
|
||||
this.dark = dark;
|
||||
Events.emit('dark-mode-changed', { dark });
|
||||
});
|
||||
this.dark = mediaQuery.matches;
|
||||
if (mediaQuery && mediaQuery.addEventListener) {
|
||||
mediaQuery.addEventListener('change', (e) => {
|
||||
const dark = e.matches;
|
||||
this.dark = dark;
|
||||
Events.emit('dark-mode-changed', { dark });
|
||||
});
|
||||
}
|
||||
this.dark = !!mediaQuery.matches;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,15 +1,33 @@
|
||||
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) {
|
||||
tmpFile = Launcher.getTempPath(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) {
|
||||
@@ -62,9 +80,16 @@ const Transport = {
|
||||
});
|
||||
res.on('end', () => {
|
||||
data = window.Buffer.concat(data);
|
||||
if (config.utf8) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Logger } from 'util/logger';
|
||||
import { ProtocolImpl } from './protocol-impl';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { Features } from 'util/features';
|
||||
|
||||
const WebConnectionInfo = {
|
||||
connectionId: 1,
|
||||
extensionName: 'KeeWeb Connect',
|
||||
supportsNotifications: true
|
||||
};
|
||||
|
||||
const SupportedExtensions = [
|
||||
{ alias: 'KWC', name: 'KeeWeb Connect' },
|
||||
{ alias: 'KPXC', name: 'KeePassXC-Browser' }
|
||||
];
|
||||
const SupportedBrowsers = ['Chrome', 'Firefox', 'Edge', 'Other'];
|
||||
if (Features.isMac) {
|
||||
SupportedBrowsers.unshift('Safari');
|
||||
}
|
||||
|
||||
const logger = new Logger('browser-extension-connector');
|
||||
if (!localStorage.debugBrowserExtension) {
|
||||
logger.level = Logger.Level.Info;
|
||||
}
|
||||
|
||||
const connections = new Map();
|
||||
const pendingBrowserMessages = [];
|
||||
let processingBrowserMessage = false;
|
||||
|
||||
const BrowserExtensionConnector = {
|
||||
started: false,
|
||||
logger,
|
||||
|
||||
init(appModel) {
|
||||
const sendEvent = this.sendEvent.bind(this);
|
||||
ProtocolImpl.init({ appModel, logger, sendEvent });
|
||||
|
||||
this.browserWindowMessage = this.browserWindowMessage.bind(this);
|
||||
|
||||
if (Launcher) {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
|
||||
ipcRenderer.on('browserExtensionSocketConnected', (e, socketId, connectionInfo) =>
|
||||
this.socketConnected(socketId, connectionInfo)
|
||||
);
|
||||
ipcRenderer.on('browserExtensionSocketClosed', (e, socketId) =>
|
||||
this.socketClosed(socketId)
|
||||
);
|
||||
ipcRenderer.on('browserExtensionSocketRequest', (e, socketId, request) =>
|
||||
this.socketRequest(socketId, request)
|
||||
);
|
||||
|
||||
AppSettingsModel.on('change', () => this.appSettingsChanged());
|
||||
}
|
||||
|
||||
if (this.isEnabled()) {
|
||||
this.start();
|
||||
}
|
||||
},
|
||||
|
||||
start() {
|
||||
if (Launcher) {
|
||||
this.startDesktopAppListener();
|
||||
} else {
|
||||
this.startWebMessageListener();
|
||||
}
|
||||
|
||||
this.started = true;
|
||||
},
|
||||
|
||||
stop() {
|
||||
if (Launcher) {
|
||||
this.stopDesktopAppListener();
|
||||
} else {
|
||||
this.stopWebMessageListener();
|
||||
}
|
||||
|
||||
ProtocolImpl.cleanup();
|
||||
connections.clear();
|
||||
|
||||
this.started = false;
|
||||
},
|
||||
|
||||
appSettingsChanged() {
|
||||
if (this.isEnabled()) {
|
||||
if (!this.started) {
|
||||
this.start();
|
||||
}
|
||||
} else if (this.started) {
|
||||
this.stop();
|
||||
}
|
||||
},
|
||||
|
||||
isEnabled() {
|
||||
if (!Launcher) {
|
||||
return true;
|
||||
}
|
||||
for (const ext of SupportedExtensions) {
|
||||
for (const browser of SupportedBrowsers) {
|
||||
if (AppSettingsModel[`extensionEnabled${ext.alias}${browser}`]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
startWebMessageListener() {
|
||||
window.addEventListener('message', this.browserWindowMessage);
|
||||
logger.info('Started');
|
||||
},
|
||||
|
||||
stopWebMessageListener() {
|
||||
window.removeEventListener('message', this.browserWindowMessage);
|
||||
},
|
||||
|
||||
enable(browser, extension, enabled) {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorEnable', browser, extension, enabled);
|
||||
},
|
||||
|
||||
async startDesktopAppListener() {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorStart', {
|
||||
appleTeamId: RuntimeInfo.appleTeamId
|
||||
});
|
||||
},
|
||||
|
||||
stopDesktopAppListener() {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorStop');
|
||||
},
|
||||
|
||||
browserWindowMessage(e) {
|
||||
if (e.origin !== location.origin) {
|
||||
return;
|
||||
}
|
||||
if (e.source !== window) {
|
||||
return;
|
||||
}
|
||||
if (e?.data?.kwConnect !== 'request') {
|
||||
return;
|
||||
}
|
||||
logger.debug('Extension -> KeeWeb', e.data);
|
||||
pendingBrowserMessages.push(e.data);
|
||||
this.processBrowserMessages();
|
||||
},
|
||||
|
||||
async processBrowserMessages() {
|
||||
if (!pendingBrowserMessages.length || processingBrowserMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connections.has(WebConnectionInfo.connectionId)) {
|
||||
connections.set(WebConnectionInfo.connectionId, WebConnectionInfo);
|
||||
}
|
||||
|
||||
processingBrowserMessage = true;
|
||||
|
||||
const request = pendingBrowserMessages.shift();
|
||||
|
||||
const response = await ProtocolImpl.handleRequest(request, WebConnectionInfo);
|
||||
|
||||
processingBrowserMessage = false;
|
||||
|
||||
if (response) {
|
||||
this.sendWebResponse(response);
|
||||
}
|
||||
|
||||
this.processBrowserMessages();
|
||||
},
|
||||
|
||||
sendWebResponse(response) {
|
||||
logger.debug('KeeWeb -> Extension', response);
|
||||
response.kwConnect = 'response';
|
||||
postMessage(response, window.location.origin);
|
||||
},
|
||||
|
||||
sendSocketEvent(data) {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorSocketEvent', data);
|
||||
},
|
||||
|
||||
sendSocketResult(socketId, data) {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorSocketResult', socketId, data);
|
||||
},
|
||||
|
||||
sendEvent(data) {
|
||||
if (!this.isEnabled() || !connections.size) {
|
||||
return;
|
||||
}
|
||||
if (Launcher) {
|
||||
this.sendSocketEvent(data);
|
||||
} else {
|
||||
this.sendWebResponse(data);
|
||||
}
|
||||
},
|
||||
|
||||
socketConnected(socketId, connectionInfo) {
|
||||
connections.set(socketId, connectionInfo);
|
||||
},
|
||||
|
||||
socketClosed(socketId) {
|
||||
connections.delete(socketId);
|
||||
ProtocolImpl.deleteConnection(socketId);
|
||||
},
|
||||
|
||||
async socketRequest(socketId, request) {
|
||||
let result;
|
||||
|
||||
const connectionInfo = connections.get(socketId);
|
||||
if (connectionInfo) {
|
||||
result = await ProtocolImpl.handleRequest(request, connectionInfo);
|
||||
} else {
|
||||
const message = `Connection not found: ${socketId}`;
|
||||
result = ProtocolImpl.errorToResponse({ message }, request);
|
||||
}
|
||||
|
||||
this.sendSocketResult(socketId, result);
|
||||
},
|
||||
|
||||
get sessions() {
|
||||
return ProtocolImpl.sessions;
|
||||
},
|
||||
|
||||
terminateConnection(connectionId) {
|
||||
connectionId = +connectionId;
|
||||
if (Launcher) {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorCloseSocket', connectionId);
|
||||
} else {
|
||||
ProtocolImpl.deleteConnection(connectionId);
|
||||
}
|
||||
},
|
||||
|
||||
getClientPermissions(clientId) {
|
||||
return ProtocolImpl.getClientPermissions(clientId);
|
||||
},
|
||||
|
||||
setClientPermissions(clientId, permissions) {
|
||||
ProtocolImpl.setClientPermissions(clientId, permissions);
|
||||
}
|
||||
};
|
||||
|
||||
export { BrowserExtensionConnector, SupportedExtensions, SupportedBrowsers };
|
||||
@@ -0,0 +1,940 @@
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Events } from 'framework/events';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { box as tweetnaclBox } from 'tweetnacl';
|
||||
import { PasswordGenerator } from 'util/generators/password-generator';
|
||||
import { GeneratorPresets } from 'comp/app/generator-presets';
|
||||
import { Alerts } from 'comp/ui/alerts';
|
||||
import { Locale } from 'util/locale';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { KnownAppVersions } from 'const/known-app-versions';
|
||||
import { ExtensionConnectView } from 'views/extension/extension-connect-view';
|
||||
import { ExtensionCreateGroupView } from 'views/extension/extension-create-group-view';
|
||||
import { ExtensionSaveEntryView } from 'views/extension/extension-save-entry-view';
|
||||
import { RuntimeDataModel } from 'models/runtime-data-model';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
import { SelectEntryView } from 'views/select/select-entry-view';
|
||||
import { SelectEntryFieldView } from 'views/select/select-entry-field-view';
|
||||
import { SelectEntryFilter } from 'comp/app/select-entry-filter';
|
||||
|
||||
const KeeWebAssociationId = 'KeeWeb';
|
||||
const KeeWebHash = '398d9c782ec76ae9e9877c2321cbda2b31fc6d18ccf0fed5ca4bd746bab4d64a'; // sha256('KeeWeb')
|
||||
const ExtensionGroupIconId = 1;
|
||||
const DefaultExtensionGroupName = 'Browser';
|
||||
const ExtensionGroupNames = new Set(['KeePassXC-Browser Passwords', DefaultExtensionGroupName]);
|
||||
|
||||
const Errors = {
|
||||
noOpenFiles: {
|
||||
message: Locale.extensionErrorNoOpenFiles,
|
||||
code: '1'
|
||||
},
|
||||
userRejected: {
|
||||
message: Locale.extensionErrorUserRejected,
|
||||
code: '6'
|
||||
},
|
||||
noMatches: {
|
||||
message: Locale.extensionErrorNoMatches,
|
||||
code: '15'
|
||||
}
|
||||
};
|
||||
|
||||
const connectedClients = new Map();
|
||||
|
||||
let logger;
|
||||
let appModel;
|
||||
let sendEvent;
|
||||
|
||||
function setupListeners() {
|
||||
Events.on('file-opened', () => {
|
||||
sendEvent({ action: 'database-unlocked' });
|
||||
});
|
||||
Events.on('one-file-closed', () => {
|
||||
if (!appModel.files.hasOpenFiles()) {
|
||||
sendEvent({ action: 'database-locked' });
|
||||
}
|
||||
});
|
||||
Events.on('all-files-closed', () => {
|
||||
sendEvent({ action: 'database-locked' });
|
||||
});
|
||||
}
|
||||
|
||||
function incrementNonce(nonce) {
|
||||
// from libsodium/utils.c, like it is in KeePassXC
|
||||
let i = 0;
|
||||
let c = 1;
|
||||
for (; i < nonce.length; ++i) {
|
||||
c += nonce[i];
|
||||
nonce[i] = c;
|
||||
c >>= 8;
|
||||
}
|
||||
}
|
||||
|
||||
function getClient(request) {
|
||||
if (!request.clientID) {
|
||||
throw new Error('Empty clientID');
|
||||
}
|
||||
const client = connectedClients.get(request.clientID);
|
||||
if (!client) {
|
||||
throw new Error(`Client not connected: ${request.clientID}`);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
function decryptRequest(request) {
|
||||
const client = getClient(request);
|
||||
|
||||
if (!request.nonce) {
|
||||
throw new Error('Empty nonce');
|
||||
}
|
||||
if (!request.message) {
|
||||
throw new Error('Empty message');
|
||||
}
|
||||
|
||||
const nonce = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
|
||||
const message = kdbxweb.ByteUtils.base64ToBytes(request.message);
|
||||
|
||||
const data = tweetnaclBox.open(message, nonce, client.publicKey, client.keys.secretKey);
|
||||
if (!data) {
|
||||
throw new Error('Failed to decrypt data');
|
||||
}
|
||||
|
||||
const json = new TextDecoder().decode(data);
|
||||
const payload = JSON.parse(json);
|
||||
|
||||
logger.debug('Extension -> KeeWeb -> (decrypted)', payload);
|
||||
|
||||
if (!payload) {
|
||||
throw new Error('Empty request payload');
|
||||
}
|
||||
if (payload.action !== request.action) {
|
||||
throw new Error(`Bad action in decrypted payload`);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function encryptResponse(request, payload) {
|
||||
logger.debug('KeeWeb -> Extension (decrypted)', payload);
|
||||
|
||||
const nonceBytes = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
|
||||
incrementNonce(nonceBytes);
|
||||
const nonce = kdbxweb.ByteUtils.bytesToBase64(nonceBytes);
|
||||
|
||||
const client = getClient(request);
|
||||
|
||||
payload.nonce = nonce;
|
||||
|
||||
const json = JSON.stringify(payload);
|
||||
const data = new TextEncoder().encode(json);
|
||||
|
||||
const encrypted = tweetnaclBox(data, nonceBytes, client.publicKey, client.keys.secretKey);
|
||||
|
||||
const message = kdbxweb.ByteUtils.bytesToBase64(encrypted);
|
||||
|
||||
return {
|
||||
action: request.action,
|
||||
message,
|
||||
nonce
|
||||
};
|
||||
}
|
||||
|
||||
function makeError(def) {
|
||||
const e = new Error(def.message);
|
||||
e.code = def.code;
|
||||
return e;
|
||||
}
|
||||
|
||||
function ensureAtLeastOneFileIsOpen() {
|
||||
if (!appModel.files.hasOpenFiles()) {
|
||||
throw makeError(Errors.noOpenFiles);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkContentRequestPermissions(request) {
|
||||
if (!appModel.files.hasOpenFiles()) {
|
||||
if (AppSettingsModel.extensionFocusIfLocked) {
|
||||
try {
|
||||
focusKeeWeb();
|
||||
await appModel.unlockAnyFile(
|
||||
'extensionUnlockMessage',
|
||||
Timeouts.KeeWebConnectRequest
|
||||
);
|
||||
} catch {
|
||||
throw makeError(Errors.noOpenFiles);
|
||||
}
|
||||
} else {
|
||||
throw makeError(Errors.noOpenFiles);
|
||||
}
|
||||
}
|
||||
|
||||
const client = getClient(request);
|
||||
if (client.permissions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Alerts.alertDisplayed) {
|
||||
throw new Error(Locale.extensionErrorAlertDisplayed);
|
||||
}
|
||||
|
||||
focusKeeWeb();
|
||||
|
||||
const config = RuntimeDataModel.extensionConnectConfig;
|
||||
const files = appModel.files.map((f) => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
checked: !config || config.allFiles || config.files.includes(f.id)
|
||||
}));
|
||||
if (!files.some((f) => f.checked)) {
|
||||
for (const f of files) {
|
||||
f.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
const extensionConnectView = new ExtensionConnectView({
|
||||
extensionName: getHumanReadableExtensionName(client),
|
||||
identityVerified: !Launcher,
|
||||
files,
|
||||
allFiles: config?.allFiles ?? true,
|
||||
askGet: config?.askGet || 'multiple'
|
||||
});
|
||||
|
||||
try {
|
||||
await alertWithTimeout({
|
||||
header: Locale.extensionConnectHeader,
|
||||
icon: 'exchange-alt',
|
||||
buttons: [Alerts.buttons.allow, Alerts.buttons.deny],
|
||||
view: extensionConnectView,
|
||||
wide: true,
|
||||
opaque: true
|
||||
});
|
||||
} catch (e) {
|
||||
client.permissionsDenied = true;
|
||||
Events.emit('browser-extension-sessions-changed');
|
||||
throw e;
|
||||
}
|
||||
|
||||
RuntimeDataModel.extensionConnectConfig = extensionConnectView.config;
|
||||
client.permissions = extensionConnectView.config;
|
||||
Events.emit('browser-extension-sessions-changed');
|
||||
}
|
||||
|
||||
function alertWithTimeout(config) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let inactivityTimer = 0;
|
||||
|
||||
const alert = Alerts.alert({
|
||||
...config,
|
||||
enter: 'yes',
|
||||
esc: '',
|
||||
success: (res) => {
|
||||
clearTimeout(inactivityTimer);
|
||||
resolve(res);
|
||||
},
|
||||
cancel: () => {
|
||||
clearTimeout(inactivityTimer);
|
||||
reject(makeError(Errors.userRejected));
|
||||
}
|
||||
});
|
||||
|
||||
inactivityTimer = setTimeout(() => {
|
||||
alert.closeWithResult('');
|
||||
}, Timeouts.KeeWebConnectRequest);
|
||||
});
|
||||
}
|
||||
|
||||
function getAvailableFiles(request) {
|
||||
const client = getClient(request);
|
||||
if (!client.permissions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = appModel.files.filter(
|
||||
(file) =>
|
||||
file.active &&
|
||||
(client.permissions.allFiles || client.permissions.files.includes(file.id))
|
||||
);
|
||||
if (!files.length) {
|
||||
throw makeError(Errors.noOpenFiles);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function getVersion(request) {
|
||||
return isKeePassXcBrowser(request) ? KnownAppVersions.KeePassXC : RuntimeInfo.version;
|
||||
}
|
||||
|
||||
function isKeeWebConnect(request) {
|
||||
return getClient(request).connection.extensionName === 'KeeWeb Connect';
|
||||
}
|
||||
|
||||
function isKeePassXcBrowser(request) {
|
||||
return getClient(request).connection.extensionName === 'KeePassXC-Browser';
|
||||
}
|
||||
|
||||
function getHumanReadableExtensionName(client) {
|
||||
return client.connection.appName
|
||||
? `${client.connection.extensionName} (${client.connection.appName})`
|
||||
: client.connection.extensionName;
|
||||
}
|
||||
|
||||
function focusKeeWeb() {
|
||||
logger.debug('Focus KeeWeb');
|
||||
if (Launcher) {
|
||||
if (!Launcher.isAppFocused()) {
|
||||
Launcher.showMainWindow();
|
||||
}
|
||||
} else {
|
||||
sendEvent({ action: 'attention-required' });
|
||||
}
|
||||
}
|
||||
|
||||
async function findEntry(request, returnIfOneMatch, filterOptions) {
|
||||
const payload = decryptRequest(request);
|
||||
await checkContentRequestPermissions(request);
|
||||
|
||||
if (!payload.url) {
|
||||
throw new Error('Empty url');
|
||||
}
|
||||
|
||||
const files = getAvailableFiles(request);
|
||||
const client = getClient(request);
|
||||
|
||||
const filter = new SelectEntryFilter(
|
||||
{ url: payload.url, title: payload.title },
|
||||
appModel,
|
||||
files,
|
||||
filterOptions
|
||||
);
|
||||
filter.subdomains = false;
|
||||
|
||||
let entries = filter.getEntries();
|
||||
|
||||
filter.subdomains = true;
|
||||
|
||||
let entry;
|
||||
|
||||
if (entries.length) {
|
||||
if (entries.length === 1 && returnIfOneMatch && client.permissions.askGet === 'multiple') {
|
||||
entry = entries[0];
|
||||
}
|
||||
} else {
|
||||
entries = filter.getEntries();
|
||||
|
||||
if (!entries.length) {
|
||||
if (AppSettingsModel.extensionFocusIfEmpty) {
|
||||
filter.useUrl = false;
|
||||
if (filter.title && AppSettingsModel.autoTypeTitleFilterEnabled) {
|
||||
filter.useTitle = true;
|
||||
entries = filter.getEntries();
|
||||
if (!entries.length) {
|
||||
filter.useTitle = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw makeError(Errors.noMatches);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!entry) {
|
||||
const extName = getHumanReadableExtensionName(client);
|
||||
const topMessage = Locale.extensionSelectPasswordFor.replace('{}', extName);
|
||||
const selectEntryView = new SelectEntryView({ filter, topMessage });
|
||||
|
||||
focusKeeWeb();
|
||||
|
||||
const inactivityTimer = setTimeout(() => {
|
||||
selectEntryView.emit('result', undefined);
|
||||
}, Timeouts.KeeWebConnectRequest);
|
||||
|
||||
const result = await selectEntryView.showAndGetResult();
|
||||
|
||||
clearTimeout(inactivityTimer);
|
||||
|
||||
entry = result?.entry;
|
||||
if (!entry) {
|
||||
throw makeError(Errors.userRejected);
|
||||
}
|
||||
}
|
||||
|
||||
client.stats.passwordsRead++;
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
const ProtocolHandlers = {
|
||||
'ping'({ data }) {
|
||||
return { data };
|
||||
},
|
||||
|
||||
'change-public-keys'(request, connection) {
|
||||
let { publicKey, version, clientID: clientId } = request;
|
||||
|
||||
if (connectedClients.has(clientId)) {
|
||||
throw new Error('Changing keys is not allowed');
|
||||
}
|
||||
|
||||
if (!Launcher) {
|
||||
// on web there can be only one connected client
|
||||
connectedClients.clear();
|
||||
}
|
||||
|
||||
const keys = tweetnaclBox.keyPair();
|
||||
publicKey = kdbxweb.ByteUtils.base64ToBytes(publicKey);
|
||||
|
||||
const stats = {
|
||||
connectedDate: new Date(),
|
||||
passwordsRead: 0,
|
||||
passwordsWritten: 0
|
||||
};
|
||||
|
||||
connectedClients.set(clientId, { connection, publicKey, version, keys, stats });
|
||||
|
||||
Events.emit('browser-extension-sessions-changed');
|
||||
|
||||
logger.info('New client key created', clientId, version);
|
||||
|
||||
const nonceBytes = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
|
||||
incrementNonce(nonceBytes);
|
||||
const nonce = kdbxweb.ByteUtils.bytesToBase64(nonceBytes);
|
||||
|
||||
return {
|
||||
action: 'change-public-keys',
|
||||
version: getVersion(request),
|
||||
publicKey: kdbxweb.ByteUtils.bytesToBase64(keys.publicKey),
|
||||
nonce,
|
||||
success: 'true',
|
||||
...(isKeeWebConnect(request) ? { appName: 'KeeWeb' } : undefined)
|
||||
};
|
||||
},
|
||||
|
||||
async 'get-databasehash'(request) {
|
||||
decryptRequest(request);
|
||||
|
||||
if (request.triggerUnlock) {
|
||||
await checkContentRequestPermissions(request);
|
||||
} else {
|
||||
ensureAtLeastOneFileIsOpen();
|
||||
}
|
||||
|
||||
return encryptResponse(request, {
|
||||
hash: KeeWebHash,
|
||||
success: 'true',
|
||||
version: getVersion(request)
|
||||
});
|
||||
},
|
||||
|
||||
'generate-password'(request) {
|
||||
const password = PasswordGenerator.generate(GeneratorPresets.browserExtensionPreset);
|
||||
|
||||
return encryptResponse(request, {
|
||||
version: getVersion(request),
|
||||
success: 'true',
|
||||
entries: [{ password }]
|
||||
});
|
||||
},
|
||||
|
||||
'lock-database'(request) {
|
||||
decryptRequest(request);
|
||||
ensureAtLeastOneFileIsOpen();
|
||||
|
||||
Events.emit('lock-workspace');
|
||||
|
||||
if (Alerts.alertDisplayed) {
|
||||
focusKeeWeb();
|
||||
}
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request)
|
||||
});
|
||||
},
|
||||
|
||||
'associate'(request) {
|
||||
decryptRequest(request);
|
||||
ensureAtLeastOneFileIsOpen();
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
hash: KeeWebHash,
|
||||
id: KeeWebAssociationId
|
||||
});
|
||||
},
|
||||
|
||||
'test-associate'(request) {
|
||||
const payload = decryptRequest(request);
|
||||
// ensureAtLeastOneFileIsOpen();
|
||||
|
||||
if (payload.id !== KeeWebAssociationId) {
|
||||
throw makeError(Errors.noOpenFiles);
|
||||
}
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
hash: KeeWebHash,
|
||||
id: payload.id
|
||||
});
|
||||
},
|
||||
|
||||
async 'get-logins'(request) {
|
||||
const entry = await findEntry(request, true);
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
hash: KeeWebHash,
|
||||
count: 1,
|
||||
entries: [
|
||||
{
|
||||
group: entry.group.title,
|
||||
login: entry.user || '',
|
||||
name: entry.title || '',
|
||||
password: entry.password?.getText() || '',
|
||||
skipAutoSubmit: 'false',
|
||||
stringFields: [],
|
||||
uuid: kdbxweb.ByteUtils.bytesToHex(entry.entry.uuid.bytes)
|
||||
}
|
||||
],
|
||||
id: ''
|
||||
});
|
||||
},
|
||||
|
||||
async 'get-totp-by-url'(request) {
|
||||
const entry = await findEntry(request, true, { otp: true });
|
||||
|
||||
entry.initOtpGenerator();
|
||||
|
||||
if (!entry.otpGenerator) {
|
||||
throw makeError(Errors.noMatches);
|
||||
}
|
||||
|
||||
let selectEntryFieldView;
|
||||
if (entry.needsTouch) {
|
||||
selectEntryFieldView = new SelectEntryFieldView({
|
||||
needsTouch: true,
|
||||
deviceShortName: entry.device.shortName
|
||||
});
|
||||
selectEntryFieldView.render();
|
||||
}
|
||||
|
||||
const otpPromise = new Promise((resolve, reject) => {
|
||||
selectEntryFieldView?.on('result', () => reject(makeError(Errors.userRejected)));
|
||||
entry.otpGenerator.next((err, otp) => {
|
||||
if (otp) {
|
||||
resolve(otp);
|
||||
} else {
|
||||
reject(err || makeError(Errors.userRejected));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let totp;
|
||||
try {
|
||||
totp = await otpPromise;
|
||||
} finally {
|
||||
selectEntryFieldView?.remove();
|
||||
}
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
totp
|
||||
});
|
||||
},
|
||||
|
||||
async 'get-any-field'(request) {
|
||||
const entry = await findEntry(request, false);
|
||||
|
||||
const selectEntryFieldView = new SelectEntryFieldView({
|
||||
entry
|
||||
});
|
||||
const inactivityTimer = setTimeout(() => {
|
||||
selectEntryFieldView.emit('result', undefined);
|
||||
}, Timeouts.KeeWebConnectRequest);
|
||||
|
||||
const field = await selectEntryFieldView.showAndGetResult();
|
||||
|
||||
clearTimeout(inactivityTimer);
|
||||
|
||||
if (!field) {
|
||||
throw makeError(Errors.userRejected);
|
||||
}
|
||||
|
||||
let value = entry.getAllFields()[field];
|
||||
if (value.isProtected) {
|
||||
value = value.getText();
|
||||
}
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
field,
|
||||
value
|
||||
});
|
||||
},
|
||||
|
||||
async 'get-totp'(request) {
|
||||
decryptRequest(request);
|
||||
await checkContentRequestPermissions(request);
|
||||
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
|
||||
async 'set-login'(request) {
|
||||
const payload = decryptRequest(request);
|
||||
await checkContentRequestPermissions(request);
|
||||
|
||||
focusKeeWeb();
|
||||
|
||||
if (!payload.url) {
|
||||
throw new Error('Empty url');
|
||||
}
|
||||
const url = new URL(payload.url);
|
||||
|
||||
const files = getAvailableFiles(request);
|
||||
const client = getClient(request);
|
||||
|
||||
let selectedGroup;
|
||||
|
||||
let entryToUpdate;
|
||||
if (payload.uuid) {
|
||||
for (const file of files) {
|
||||
const entryId = kdbxweb.ByteUtils.bytesToBase64(
|
||||
kdbxweb.ByteUtils.hexToBytes(payload.uuid)
|
||||
);
|
||||
const foundEntry = file.getEntry(file.subId(entryId));
|
||||
if (foundEntry) {
|
||||
if (entryToUpdate) {
|
||||
throw new Error('Two entries with the same ID found');
|
||||
} else {
|
||||
entryToUpdate = foundEntry;
|
||||
selectedGroup = foundEntry.group;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!entryToUpdate) {
|
||||
throw new Error('Updated entry not found');
|
||||
}
|
||||
}
|
||||
|
||||
if (client.permissions.askSave === 'auto' && client.permissions.saveTo && !selectedGroup) {
|
||||
const file = files.find((f) => f.id === client.permissions.saveTo.fileId);
|
||||
selectedGroup = file?.getGroup(client.permissions.saveTo.groupId);
|
||||
}
|
||||
|
||||
if (client.permissions.askSave !== 'auto' || !selectedGroup) {
|
||||
if (!selectedGroup && RuntimeDataModel.extensionSaveConfig) {
|
||||
const file = files.find(
|
||||
(f) => f.id === RuntimeDataModel.extensionSaveConfig.fileId
|
||||
);
|
||||
selectedGroup = file?.getGroup(RuntimeDataModel.extensionSaveConfig.groupId);
|
||||
}
|
||||
|
||||
const allGroups = [];
|
||||
for (const file of files) {
|
||||
file.forEachGroup((group) => {
|
||||
const spaces = [];
|
||||
for (let parent = group; parent.parentGroup; parent = parent.parentGroup) {
|
||||
spaces.push(' ', ' ');
|
||||
}
|
||||
|
||||
if (
|
||||
!selectedGroup &&
|
||||
group.iconId === ExtensionGroupIconId &&
|
||||
ExtensionGroupNames.has(group.title)
|
||||
) {
|
||||
selectedGroup = group;
|
||||
}
|
||||
|
||||
allGroups.push({
|
||||
id: group.id,
|
||||
fileId: file.id,
|
||||
spaces,
|
||||
title: group.title,
|
||||
selected: group.id === selectedGroup?.id
|
||||
});
|
||||
});
|
||||
}
|
||||
if (!selectedGroup) {
|
||||
allGroups.splice(1, 0, {
|
||||
id: '',
|
||||
fileId: files[0].id,
|
||||
spaces: [' ', ' '],
|
||||
title: `${DefaultExtensionGroupName} (${Locale.extensionSaveEntryNewGroup})`,
|
||||
selected: true
|
||||
});
|
||||
}
|
||||
|
||||
const saveEntryView = new ExtensionSaveEntryView({
|
||||
extensionName: getHumanReadableExtensionName(client),
|
||||
url: payload.url,
|
||||
user: payload.login,
|
||||
askSave: RuntimeDataModel.extensionSaveConfig?.askSave || 'always',
|
||||
update: !!entryToUpdate,
|
||||
allGroups
|
||||
});
|
||||
|
||||
await alertWithTimeout({
|
||||
header: Locale.extensionSaveEntryHeader,
|
||||
icon: 'plus',
|
||||
buttons: [Alerts.buttons.allow, Alerts.buttons.deny],
|
||||
view: saveEntryView
|
||||
});
|
||||
|
||||
const config = { ...saveEntryView.config };
|
||||
if (!entryToUpdate) {
|
||||
if (config.groupId) {
|
||||
const file = files.find((f) => f.id === config.fileId);
|
||||
selectedGroup = file.getGroup(config.groupId);
|
||||
} else {
|
||||
selectedGroup = appModel.createNewGroupWithName(
|
||||
files[0].groups[0],
|
||||
files[0],
|
||||
DefaultExtensionGroupName
|
||||
);
|
||||
selectedGroup.setIcon(ExtensionGroupIconId);
|
||||
config.groupId = selectedGroup.id;
|
||||
}
|
||||
|
||||
RuntimeDataModel.extensionSaveConfig = config;
|
||||
client.permissions.saveTo = { fileId: config.fileId, groupId: config.groupId };
|
||||
}
|
||||
|
||||
client.permissions.askSave = config.askSave;
|
||||
}
|
||||
|
||||
const entryFields = {
|
||||
Title: url.hostname,
|
||||
UserName: payload.login,
|
||||
Password: kdbxweb.ProtectedValue.fromString(payload.password || ''),
|
||||
URL: payload.url
|
||||
};
|
||||
|
||||
if (entryToUpdate) {
|
||||
for (const [field, value] of Object.entries(entryFields)) {
|
||||
if (value) {
|
||||
entryToUpdate.setField(field, value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
appModel.createNewEntryWithFields(selectedGroup, entryFields);
|
||||
}
|
||||
|
||||
client.stats.passwordsWritten++;
|
||||
|
||||
Events.emit('browser-extension-sessions-changed');
|
||||
Events.emit('refresh');
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
count: null,
|
||||
entries: null,
|
||||
hash: KeeWebHash
|
||||
});
|
||||
},
|
||||
|
||||
async 'get-database-groups'(request) {
|
||||
decryptRequest(request);
|
||||
await checkContentRequestPermissions(request);
|
||||
|
||||
const makeGroups = (group) => {
|
||||
const res = {
|
||||
name: group.title,
|
||||
uuid: kdbxweb.ByteUtils.bytesToHex(group.group.uuid.bytes),
|
||||
children: []
|
||||
};
|
||||
for (const subGroup of group.items) {
|
||||
if (subGroup.matches()) {
|
||||
res.children.push(makeGroups(subGroup));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
const groups = [];
|
||||
for (const file of getAvailableFiles(request)) {
|
||||
for (const group of file.groups) {
|
||||
groups.push(makeGroups(group));
|
||||
}
|
||||
}
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
groups: { groups }
|
||||
});
|
||||
},
|
||||
|
||||
async 'create-new-group'(request) {
|
||||
const payload = decryptRequest(request);
|
||||
await checkContentRequestPermissions(request);
|
||||
|
||||
if (!payload.groupName) {
|
||||
throw new Error('No groupName');
|
||||
}
|
||||
|
||||
const groupNames = payload.groupName
|
||||
.split('/')
|
||||
.map((g) => g.trim())
|
||||
.filter((g) => g);
|
||||
|
||||
if (!groupNames.length) {
|
||||
throw new Error('Empty group path');
|
||||
}
|
||||
|
||||
const files = getAvailableFiles(request);
|
||||
|
||||
for (const file of files) {
|
||||
for (const rootGroup of file.groups) {
|
||||
let foundGroup = rootGroup;
|
||||
const pendingGroups = [...groupNames];
|
||||
while (pendingGroups.length && foundGroup) {
|
||||
const title = pendingGroups.shift();
|
||||
foundGroup = foundGroup.items.find((g) => g.title === title);
|
||||
}
|
||||
if (foundGroup) {
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
name: foundGroup.title,
|
||||
uuid: kdbxweb.ByteUtils.bytesToHex(foundGroup.group.uuid.bytes)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const client = getClient(request);
|
||||
const createGroupView = new ExtensionCreateGroupView({
|
||||
extensionName: getHumanReadableExtensionName(client),
|
||||
groupPath: groupNames.join(' / '),
|
||||
files: files.map((f, ix) => ({ id: f.id, name: f.name, selected: ix === 0 }))
|
||||
});
|
||||
|
||||
await alertWithTimeout({
|
||||
header: Locale.extensionNewGroupHeader,
|
||||
icon: 'folder-plus',
|
||||
buttons: [Alerts.buttons.allow, Alerts.buttons.deny],
|
||||
view: createGroupView
|
||||
});
|
||||
|
||||
const selectedFile = files.find((f) => f.id === createGroupView.selectedFile);
|
||||
|
||||
let newGroup = selectedFile.groups[0];
|
||||
const pendingGroups = [...groupNames];
|
||||
|
||||
while (pendingGroups.length) {
|
||||
const title = pendingGroups.shift();
|
||||
const item = newGroup.items.find((g) => g.title === title);
|
||||
if (item) {
|
||||
newGroup = item;
|
||||
} else {
|
||||
newGroup = appModel.createNewGroupWithName(newGroup, selectedFile, title);
|
||||
}
|
||||
}
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
name: newGroup.title,
|
||||
uuid: kdbxweb.ByteUtils.bytesToHex(newGroup.group.uuid.bytes)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const ProtocolImpl = {
|
||||
init(vars) {
|
||||
appModel = vars.appModel;
|
||||
logger = vars.logger;
|
||||
sendEvent = vars.sendEvent;
|
||||
|
||||
setupListeners();
|
||||
},
|
||||
|
||||
cleanup() {
|
||||
const wasNotEmpty = connectedClients.size;
|
||||
|
||||
connectedClients.clear();
|
||||
|
||||
if (wasNotEmpty) {
|
||||
Events.emit('browser-extension-sessions-changed');
|
||||
}
|
||||
},
|
||||
|
||||
deleteConnection(connectionId) {
|
||||
for (const [clientId, client] of connectedClients.entries()) {
|
||||
if (client.connection.connectionId === connectionId) {
|
||||
connectedClients.delete(clientId);
|
||||
}
|
||||
}
|
||||
Events.emit('browser-extension-sessions-changed');
|
||||
},
|
||||
|
||||
getClientPermissions(clientId) {
|
||||
return connectedClients.get(clientId)?.permissions;
|
||||
},
|
||||
|
||||
setClientPermissions(clientId, permissions) {
|
||||
const client = connectedClients.get(clientId);
|
||||
if (client?.permissions) {
|
||||
client.permissions = { ...client.permissions, ...permissions };
|
||||
}
|
||||
},
|
||||
|
||||
errorToResponse(e, request) {
|
||||
return {
|
||||
action: request?.action,
|
||||
error: e.message || 'Unknown error',
|
||||
errorCode: e.code || 0
|
||||
};
|
||||
},
|
||||
|
||||
async handleRequest(request, connectionInfo) {
|
||||
const appWindowWasFocused = Launcher?.isAppFocused();
|
||||
|
||||
let result;
|
||||
try {
|
||||
const handler = ProtocolHandlers[request.action];
|
||||
if (!handler) {
|
||||
throw new Error(`Handler not found: ${request.action}`);
|
||||
}
|
||||
result = await handler(request, connectionInfo);
|
||||
if (!result) {
|
||||
throw new Error(`Handler returned an empty result: ${request.action}`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!e.code) {
|
||||
logger.error(`Error in handler ${request.action}`, e);
|
||||
}
|
||||
result = this.errorToResponse(e, request);
|
||||
}
|
||||
|
||||
if (!appWindowWasFocused && Launcher?.isAppFocused()) {
|
||||
Launcher.hideApp();
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
get sessions() {
|
||||
return [...connectedClients.entries()]
|
||||
.map(([clientId, client]) => ({
|
||||
clientId,
|
||||
connectionId: client.connection.connectionId,
|
||||
appName: client.connection.appName,
|
||||
extensionName: client.connection.extensionName,
|
||||
connectedDate: client.stats.connectedDate,
|
||||
passwordsRead: client.stats.passwordsRead,
|
||||
passwordsWritten: client.stats.passwordsWritten,
|
||||
permissions: client.permissions,
|
||||
permissionsDenied: client.permissionsDenied
|
||||
}))
|
||||
.sort((x, y) => y.connectedDate - x.connectedDate);
|
||||
}
|
||||
};
|
||||
|
||||
export { ProtocolImpl };
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable import/no-commonjs */
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { Links } from 'const/links';
|
||||
import { DateFormat } from 'comp/i18n/date-format';
|
||||
@@ -51,14 +51,14 @@ function walkEntry(db, entry, parents) {
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const fieldName of Object.keys(entry.fields)) {
|
||||
for (const [fieldName, fieldValue] of entry.fields) {
|
||||
if (!KnownFields[fieldName]) {
|
||||
const value = entryField(entry, fieldName);
|
||||
if (value) {
|
||||
fields.push({
|
||||
title: fieldName,
|
||||
value,
|
||||
protect: entry.fields[fieldName].isProtected
|
||||
protect: fieldValue.isProtected
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ function walkEntry(db, entry, parents) {
|
||||
expires = DateFormat.dtStr(entry.times.expiryTime);
|
||||
}
|
||||
|
||||
const attachments = Object.entries(entry.binaries)
|
||||
const attachments = [...entry.binaries]
|
||||
.map(([name, data]) => {
|
||||
if (data && data.ref) {
|
||||
data = data.value;
|
||||
@@ -95,7 +95,7 @@ function walkEntry(db, entry, parents) {
|
||||
}
|
||||
|
||||
function entryField(entry, fieldName) {
|
||||
const value = entry.fields[fieldName];
|
||||
const value = entry.fields.get(fieldName);
|
||||
return (value && value.isProtected && value.getText()) || value || '';
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const DateFormat = {
|
||||
const format = new Intl.DateTimeFormat(SettingsManager.activeLocale, { weekday: 'short' });
|
||||
const weekdays = [];
|
||||
for (let day = 1; day < 8; day++) {
|
||||
weekdays.push(format.format(new Date(Date.UTC(2007, 9, 6 + day))));
|
||||
weekdays.push(format.format(new Date(2007, 9, 6 + day)));
|
||||
}
|
||||
return weekdays;
|
||||
},
|
||||
|
||||
@@ -17,6 +17,9 @@ const Launcher = {
|
||||
platform() {
|
||||
return process.platform;
|
||||
},
|
||||
arch() {
|
||||
return process.arch;
|
||||
},
|
||||
electron() {
|
||||
return this.req('electron');
|
||||
},
|
||||
@@ -55,7 +58,15 @@ const Launcher = {
|
||||
return this.joinPath(this.userDataPath, fileName || '');
|
||||
},
|
||||
getTempPath(fileName) {
|
||||
return this.joinPath(this.remoteApp().getPath('temp'), 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 || '');
|
||||
@@ -142,17 +153,6 @@ const Launcher = {
|
||||
saveConfig(name, data) {
|
||||
return this.remoteApp().saveConfig(name, data);
|
||||
},
|
||||
ensureRunnable(path) {
|
||||
if (process.platform !== 'win32') {
|
||||
const fs = this.req('fs');
|
||||
const stat = fs.statSync(path);
|
||||
if ((stat.mode & 0o0111) === 0) {
|
||||
const mode = stat.mode | 0o0100;
|
||||
logger.info(`chmod 0${mode.toString(8)} ${path}`);
|
||||
fs.chmodSync(path, mode);
|
||||
}
|
||||
}
|
||||
},
|
||||
preventExit(e) {
|
||||
e.returnValue = false;
|
||||
return false;
|
||||
@@ -164,18 +164,18 @@ const Launcher = {
|
||||
requestExit() {
|
||||
const app = this.remoteApp();
|
||||
app.setHookBeforeQuitEvent(false);
|
||||
if (this.restartPending) {
|
||||
app.restartApp();
|
||||
if (this.pendingUpdateFile) {
|
||||
app.restartAndUpdate(this.pendingUpdateFile);
|
||||
} else {
|
||||
app.quit();
|
||||
}
|
||||
},
|
||||
requestRestart() {
|
||||
this.restartPending = true;
|
||||
requestRestartAndUpdate(updateFilePath) {
|
||||
this.pendingUpdateFile = updateFilePath;
|
||||
this.requestExit();
|
||||
},
|
||||
cancelRestart() {
|
||||
this.restartPending = false;
|
||||
this.pendingUpdateFile = undefined;
|
||||
},
|
||||
setClipboardText(text) {
|
||||
return this.electron().clipboard.writeText(text);
|
||||
@@ -191,7 +191,7 @@ const Launcher = {
|
||||
}
|
||||
},
|
||||
quitOnRealQuitEventIfMinimizeOnQuitIsEnabled() {
|
||||
return this.platform() === 'darwin';
|
||||
return !!this.pendingUpdateFile;
|
||||
},
|
||||
minimizeApp() {
|
||||
this.remoteApp().minimizeApp({
|
||||
@@ -203,7 +203,7 @@ const Launcher = {
|
||||
return process.platform !== 'linux';
|
||||
},
|
||||
updaterEnabled() {
|
||||
return this.electron().remote.process.argv.indexOf('--disable-updater') === -1;
|
||||
return process.platform !== 'linux';
|
||||
},
|
||||
getMainWindow() {
|
||||
return this.remoteApp().getMainWindow();
|
||||
@@ -233,57 +233,32 @@ const Launcher = {
|
||||
},
|
||||
spawn(config) {
|
||||
const ts = logger.ts();
|
||||
let complete = config.complete;
|
||||
const ps = this.req('child_process').spawn(config.cmd, config.args);
|
||||
[ps.stdin, ps.stdout, ps.stderr].forEach((s) => s.setEncoding('utf-8'));
|
||||
let stderr = '';
|
||||
let stdout = '';
|
||||
ps.stderr.on('data', (d) => {
|
||||
stderr += d.toString('utf-8');
|
||||
if (config.throwOnStdErr) {
|
||||
try {
|
||||
ps.kill();
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
ps.stdout.on('data', (d) => {
|
||||
stdout += d.toString('utf-8');
|
||||
});
|
||||
ps.on('close', (code) => {
|
||||
stdout = stdout.trim();
|
||||
stderr = stderr.trim();
|
||||
const msg = 'spawn ' + config.cmd + ': ' + code + ', ' + logger.ts(ts);
|
||||
if (code !== 0) {
|
||||
logger.error(msg + '\n' + stdout + '\n' + stderr);
|
||||
} else {
|
||||
logger.info(msg + (stdout && !config.noStdOutLogging ? '\n' + stdout : ''));
|
||||
}
|
||||
if (complete) {
|
||||
complete(code !== 0 ? 'Exit code ' + code : null, stdout, code);
|
||||
const { ipcRenderer } = this.electron();
|
||||
let { complete } = config;
|
||||
delete config.complete;
|
||||
ipcRenderer
|
||||
.invoke('spawnProcess', config)
|
||||
.then((res) => {
|
||||
if (res.err) {
|
||||
logger.error('spawn error: ' + config.cmd + ', ' + logger.ts(ts), res.err);
|
||||
complete?.(res.err);
|
||||
} else {
|
||||
const code = res.code;
|
||||
const stdout = res.stdout || '';
|
||||
const stderr = res.stderr || '';
|
||||
const msg = 'spawn ' + config.cmd + ': ' + code + ', ' + logger.ts(ts);
|
||||
if (code !== 0) {
|
||||
logger.error(msg + '\n' + stdout + '\n' + stderr);
|
||||
} else {
|
||||
logger.info(msg + (stdout && !config.noStdOutLogging ? '\n' + stdout : ''));
|
||||
}
|
||||
complete?.(code !== 0 ? 'Exit code ' + code : null, stdout, code);
|
||||
}
|
||||
complete = null;
|
||||
}
|
||||
});
|
||||
ps.on('error', (err) => {
|
||||
logger.error('spawn error: ' + config.cmd + ', ' + logger.ts(ts), err);
|
||||
if (complete) {
|
||||
complete(err);
|
||||
complete = null;
|
||||
}
|
||||
});
|
||||
if (config.data) {
|
||||
try {
|
||||
ps.stdin.end(config.data);
|
||||
} catch (e) {
|
||||
logger.error('spawn write error', e);
|
||||
}
|
||||
}
|
||||
process.nextTick(() => {
|
||||
// it should work without destroy, but a process doesn't get launched
|
||||
// xubuntu-desktop 19.04 / xfce
|
||||
// see https://github.com/keeweb/keeweb/issues/1234
|
||||
ps.stdin.destroy();
|
||||
});
|
||||
return ps;
|
||||
})
|
||||
.catch((err) => {
|
||||
complete?.(err);
|
||||
});
|
||||
},
|
||||
checkOpenFiles() {
|
||||
this.readyToOpenFiles = true;
|
||||
@@ -301,6 +276,18 @@ const Launcher = {
|
||||
},
|
||||
setGlobalShortcuts(appSettings) {
|
||||
this.remoteApp().setGlobalShortcuts(appSettings);
|
||||
},
|
||||
minimizeMainWindow() {
|
||||
this.getMainWindow().minimize();
|
||||
},
|
||||
maximizeMainWindow() {
|
||||
this.getMainWindow().maximize();
|
||||
},
|
||||
restoreMainWindow() {
|
||||
this.getMainWindow().restore();
|
||||
},
|
||||
mainWindowMaximized() {
|
||||
return this.getMainWindow().isMaximized();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -308,9 +295,10 @@ 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));
|
||||
Events.on('log', (e) => new Logger(e.category || 'remote-app')[e.method || 'info'](e.message));
|
||||
|
||||
window.launcherOpen = (file) => Launcher.openFile(file);
|
||||
if (window.launcherOpenedFile) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Events } from 'framework/events';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
@@ -8,11 +9,19 @@ let NativeModules;
|
||||
if (Launcher) {
|
||||
const logger = new Logger('native-module-connector');
|
||||
|
||||
let host;
|
||||
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 });
|
||||
@@ -35,7 +44,7 @@ if (Launcher) {
|
||||
}
|
||||
},
|
||||
|
||||
'yk-chal-resp-result'({ callbackId, error, result }) {
|
||||
yubiKeyChallengeResponseResult({ callbackId, error, result }) {
|
||||
const callback = ykChalRespCallbacks[callbackId];
|
||||
if (callback) {
|
||||
const willBeCalledAgain = error && error.touchRequested;
|
||||
@@ -49,39 +58,39 @@ if (Launcher) {
|
||||
|
||||
NativeModules = {
|
||||
startHost() {
|
||||
if (host) {
|
||||
return;
|
||||
if (hostRunning) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (hostStartPromise) {
|
||||
return hostStartPromise;
|
||||
}
|
||||
|
||||
logger.debug('Starting native module host');
|
||||
|
||||
const path = Launcher.req('path');
|
||||
const appContentRoot = Launcher.remoteApp().getAppContentRoot();
|
||||
const mainModulePath = path.join(appContentRoot, 'native-module-host.js');
|
||||
hostStartPromise = this.callNoWait('start').then(() => {
|
||||
hostStartPromise = undefined;
|
||||
hostRunning = true;
|
||||
|
||||
const { fork } = Launcher.req('child_process');
|
||||
if (this.usbListenerRunning) {
|
||||
return this.call('startUsbListener');
|
||||
}
|
||||
});
|
||||
|
||||
host = fork(mainModulePath);
|
||||
|
||||
host.on('message', (message) => this.hostCallback(message));
|
||||
|
||||
host.on('error', (e) => this.hostError(e));
|
||||
host.on('exit', (code, sig) => this.hostExit(code, sig));
|
||||
|
||||
this.call('init', Launcher.remoteApp().getAppMainRoot());
|
||||
|
||||
if (this.usbListenerRunning) {
|
||||
this.call('start-usb');
|
||||
}
|
||||
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}`);
|
||||
host = null;
|
||||
|
||||
hostRunning = false;
|
||||
|
||||
const err = new Error('Native module host crashed');
|
||||
|
||||
@@ -110,6 +119,14 @@ if (Launcher) {
|
||||
}
|
||||
},
|
||||
|
||||
log(name, level, ...args) {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
const logger = new Logger(name);
|
||||
logger[level](...args);
|
||||
},
|
||||
|
||||
autoRestartHost() {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
@@ -121,56 +138,113 @@ if (Launcher) {
|
||||
},
|
||||
|
||||
call(cmd, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!host) {
|
||||
try {
|
||||
this.startHost();
|
||||
} catch (e) {
|
||||
return reject(e);
|
||||
}
|
||||
}
|
||||
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 };
|
||||
host.send({ cmd, args, callId });
|
||||
|
||||
ipcRenderer.send('nativeModuleCall', { cmd, args, callId });
|
||||
});
|
||||
},
|
||||
|
||||
startUsbListener() {
|
||||
this.call('start-usb');
|
||||
this.call('startUsbListener');
|
||||
this.usbListenerRunning = true;
|
||||
},
|
||||
|
||||
stopUsbListener() {
|
||||
this.usbListenerRunning = false;
|
||||
if (host) {
|
||||
this.call('stop-usb');
|
||||
if (hostRunning) {
|
||||
this.call('stopUsbListener');
|
||||
}
|
||||
},
|
||||
|
||||
getYubiKeys(config) {
|
||||
return this.call('get-yubikeys', config);
|
||||
return this.call('getYubiKeys', config);
|
||||
},
|
||||
|
||||
yubiKeyChallengeResponse(yubiKey, challenge, slot, callback) {
|
||||
ykChalRespCallbacks[callId] = callback;
|
||||
return this.call('yk-chal-resp', yubiKey, challenge, slot, callId);
|
||||
return this.call('yubiKeyChallengeResponse', yubiKey, challenge, slot, callId);
|
||||
},
|
||||
|
||||
yubiKeyCancelChallengeResponse() {
|
||||
if (host) {
|
||||
this.call('yk-cancel-chal-resp');
|
||||
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 };
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Locale } from 'util/locale';
|
||||
import { ThemeWatcher } from 'comp/browser/theme-watcher';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const logger = new Logger('settings-manager');
|
||||
|
||||
@@ -164,6 +165,20 @@ const SettingsManager = {
|
||||
Object.assign(Locale, this.neutralLocale, localeValues);
|
||||
this.activeLocale = loc;
|
||||
Events.emit('set-locale', loc);
|
||||
|
||||
if (Launcher) {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
const localeValuesForDesktopApp = {};
|
||||
for (const [key, value] of Object.entries(Locale)) {
|
||||
if (key.startsWith('sysMenu')) {
|
||||
localeValuesForDesktopApp[key] = value;
|
||||
}
|
||||
}
|
||||
ipcRenderer.invoke('setLocale', {
|
||||
locale: loc,
|
||||
...localeValuesForDesktopApp
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getBrowserLocale() {
|
||||
|
||||
@@ -17,6 +17,12 @@ const Alerts = {
|
||||
return Locale.alertYes;
|
||||
}
|
||||
},
|
||||
allow: {
|
||||
result: 'yes',
|
||||
get title() {
|
||||
return Locale.alertAllow;
|
||||
}
|
||||
},
|
||||
no: {
|
||||
result: '',
|
||||
get title() {
|
||||
@@ -28,6 +34,12 @@ const Alerts = {
|
||||
get title() {
|
||||
return Locale.alertCancel;
|
||||
}
|
||||
},
|
||||
deny: {
|
||||
result: '',
|
||||
get title() {
|
||||
return Locale.alertDeny;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -26,13 +26,28 @@ const GDriveApps = {
|
||||
|
||||
const OneDriveApps = {
|
||||
Local: {
|
||||
id: 'b97c53d5-db5b-4124-aab9-d39195293815',
|
||||
secret: 'V9b6:iJU]N7cImE1f_OLNjqZJDBnumR?'
|
||||
id: 'b97c53d5-db5b-4124-aab9-d39195293815'
|
||||
},
|
||||
Production: {
|
||||
id: 'bbc74d1b-3a9c-46e6-9da4-4c645e830923'
|
||||
},
|
||||
Desktop: {
|
||||
id: 'bbc74d1b-3a9c-46e6-9da4-4c645e830923',
|
||||
secret: 'aOMJaktJEAs_Tmh]fx4iQ[Zd3mp3KK7-'
|
||||
}
|
||||
};
|
||||
|
||||
export { DropboxApps, GDriveApps, OneDriveApps };
|
||||
const MsTeamsApps = {
|
||||
Local: {
|
||||
id: '8fbe2245-13d5-446f-bedc-74c3b2e1f635'
|
||||
},
|
||||
Production: {
|
||||
id: '8fbe2245-13d5-446f-bedc-74c3b2e1f635'
|
||||
},
|
||||
Desktop: {
|
||||
id: '8fbe2245-13d5-446f-bedc-74c3b2e1f635',
|
||||
secret: 'F02~HYaWs-~7MndJcVRtv9~h-50Brk_9ho'
|
||||
}
|
||||
};
|
||||
|
||||
export { DropboxApps, GDriveApps, OneDriveApps, MsTeamsApps };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const DefaultAppSettings = {
|
||||
theme: null, // UI theme
|
||||
autoSwitchTheme: true, // automatically switch between light and dark 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
|
||||
@@ -9,14 +9,16 @@ const DefaultAppSettings = {
|
||||
autoUpdate: 'install', // auto-update options: "install", "check", ""
|
||||
clipboardSeconds: 0, // number of seconds after which the clipboard will be cleared
|
||||
autoSave: true, // auto-save open files
|
||||
autoSaveInterval: 0, // interval between performing automatic sync, minutes
|
||||
autoSaveInterval: 0, // interval between performing automatic sync, minutes, -1: on every change
|
||||
rememberKeyFiles: 'path', // remember keyfiles selected on the Open screen
|
||||
idleMinutes: 15, // app lock timeout after inactivity, minutes
|
||||
minimizeOnClose: false, // minimise the app instead of closing
|
||||
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
|
||||
@@ -41,7 +43,13 @@ const DefaultAppSettings = {
|
||||
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,
|
||||
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
|
||||
@@ -79,7 +87,13 @@ const DefaultAppSettings = {
|
||||
|
||||
onedrive: true, // enable OneDrive integration
|
||||
onedriveClientId: null, // custom OneDrive client id
|
||||
onedriveClientSecret: null // custom OneDrive client secret
|
||||
onedriveClientSecret: null, // custom OneDrive client secret
|
||||
onedriveTenantId: null, // custom OneDrive tenant id
|
||||
|
||||
msteams: false, // enable Microsoft Teams integration
|
||||
msteamsClientId: null, // custom Microsoft Teams client id
|
||||
msteamsClientSecret: null, // custom Microsoft Teams client secret
|
||||
msteamsTenantId: null // custom Microsoft Teams tenant id
|
||||
};
|
||||
|
||||
export { DefaultAppSettings };
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const KeeWebLogo =
|
||||
'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxkZWZzPjxyYWRpYWxHcmFkaWVudCBpZD0iYSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGN4PSI2NjMuMTMzIiBjeT0iMTM4LjYwMSIgcj0iODY0LjU2OCIgZng9IjY2My4xMzMiIGZ5PSIxMzguNjAxIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM5MmJmZjUiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMDQxYzgiLz48L3JhZGlhbEdyYWRpZW50PjwvZGVmcz48cGF0aCBkPSJNOTIzIDM1Ny42MjhjMC05Ljc4MiAwLTE5LjU2NC0uMDU2LTI5LjM0OC0uMDUtOC4yNC0uMTQ0LTE2LjQ4LS4zNjgtMjQuNzE2YTM1OC43MjQgMzU4LjcyNCAwIDAwLTQuNzM2LTUzLjgxMSAxODEuNDk3IDE4MS40OTcgMCAwMC0xNi44NTgtNTEuMTMzIDE3Mi4wNjYgMTcyLjA2NiAwIDAwLTc1LjIzNi03NS4yIDE4MS43MDIgMTgxLjcwMiAwIDAwLTUxLjE4OC0xNi44NiAzNTkuNjI2IDM1OS42MjYgMCAwMC01My44Mi00LjczYy04LjI0Mi0uMjMtMTYuNDg2LS4zMTgtMjQuNzM4LS4zNjgtOS43OC0uMDYyLTE5LjU2OC0uMDYyLTI5LjM1Ni0uMDYyTDU1MyAxMDFoLTg1bC0xMTEuNjM4LjRjLTkuOCAwLTE5LjYxNCAwLTI5LjQyLjA1NC04LjI2Mi4wNS0xNi41Mi4xNDQtMjQuNzc4LjM2OGEzNjAuODA4IDM2MC44MDggMCAwMC01My45NDggNC43NDIgMTgyLjI3MiAxODIuMjcyIDAgMDAtNTEuMjU4IDE2Ljg0OCAxNzIuMjMgMTcyLjIzIDAgMDAtNzUuMzg2IDc1LjE4OCAxODEuMjM4IDE4MS4yMzggMCAwMC0xNi45IDUxLjE2IDM1OC4zMTIgMzU4LjMxMiAwIDAwLTQuNzQyIDUzLjhjLS4yMjIgOC4yNC0uMzE4IDE2LjQ4LS4zNjggMjQuNzItLjA2IDkuNzg0LS41NjIgMjEuOTM2LS41NjIgMzEuNzJ2MTk2bC41MDggMTEyLjQyOGMwIDkuOCAwIDE5LjYuMDU0IDI5LjM4OC4wNSA4LjI1Mi4xNDYgMTYuNTAyLjM2OCAyNC43NWEzNTkuMDQ2IDM1OS4wNDYgMCAwMDQuNzQ2IDUzLjg4NSAxODEuNjY4IDE4MS42NjggMCAwMDE2Ljg5IDUxLjIwMSAxNzIuMzU2IDE3Mi4zNTYgMCAwMDc1LjM4MiA3NS4zMDIgMTgyLjEwNSAxODIuMTA1IDAgMDA1MS4yODggMTYuODggMzYwLjIwNyAzNjAuMjA3IDAgMDA1My45MjQgNC43MzZjOC4yNTguMjI0IDE2LjUxOC4zMTggMjQuNzguMzY4IDkuOC4wNiAxOS42MTIuMDU2IDI5LjQyLjA1NmgzMTAuMjg0YzkuNzg4IDAgMTkuNTc2IDAgMjkuMzY0LS4wNTQgOC4yNDQtLjA1IDE2LjQ4OC0uMTQ0IDI0LjczLS4zNjhhMzU5LjEgMzU5LjEgMCAwMDUzLjg0LTQuNzQgMTgxLjUxOCAxODEuNTE4IDAgMDA1MS4xNi0xNi44NzIgMTcyLjE5NSAxNzIuMTk1IDAgMDA3NS4yNC03NS4yOTggMTgxLjk1IDE4MS45NSAwIDAwMTYuODY2LTUxLjIzIDM1OS44MTUgMzU5LjgxNSAwIDAwNC43MzItNTMuODY0Yy4yMjQtOC4yNS4zMTgtMTYuNS4zNjgtMjQuNzUuMDYtOS44LjA1Ni0xOS42LjA1Ni0yOS4zODh2LTMxMC44IiBmaWxsPSJ1cmwoI2EpIi8+PHBhdGggZD0iTTk5IDEwMWg4MjR2ODI0SDk5eiIgZmlsbD0ibm9uZSIvPjxnIGZpbGw9IiNmZmYiPjxwYXRoIGQ9Ik0zNzkuMTk2IDgxMy4yNzNjLTUuOTM5LS4wMS0xMi41My0yLjUwMi0xOS4zMjYtNi4wNjctMTQuNDk3LTcuNjA1LTI0LjQzOS0xNy4yMi0xNi4xMzktMzMuMDQyTDU0NS4zOCAzODkuNzYxYTE1MTguMzEgMTUxOC4zMSAwIDAxLTUuNzU4LTIuOTk4Yy03Mi43MDktMzguMTQyLTEwNC45NjctOTQuNTM1LTc4Ljg1Mi0xNDQuMzE4IDMwLjAxOC01Ny4yMjMgMTIyLjk4NC02Ny42MDggMjA3LjY0Ny0yMy4xOTYgODQuNjYzIDQ0LjQxMyAxMjguOTYxIDEyNi44MDUgOTguOTQzIDE4NC4wMjgtMjUuODE1IDQ5LjIxMS05My4yMTcgNTMuNTk5LTE2MC45OSAxOC4wNDctMS44NjctLjk4LTQuODk2LTIuNTUzLTguMzI0LTQuMzNMMzk2LjIzMSA4MDEuNzA1Yy00LjQwOSA4LjQwNS0xMC4zMDQgMTEuNTc5LTE3LjAzNSAxMS41Njh6bS0zNy4xMjctODIuMzUybC0zLjgxNi0yLjAwMi0uODUtLjQ0NS01MS42NTItMjcuMDk2di0uMDAybC0xMi40NTEtNi41MzFhNC4zMSA0LjMxIDAgMDEtMS44MTUtNS44MmwxNi4zNDYtMzEuMTZhNC4zMTIgNC4zMTIgMCAwMTUuNzg5LTEuODMybC4wMzMuMDE1IDI0LjkwMyAxMy4wNjVhNC4zMTEgNC4zMTEgMCAwMDUuODItMS44MTdsMTYuMzQ4LTMxLjE2YTQuMzEgNC4zMSAwIDAwLTEuODE3LTUuODJsLTEyLjQ1MS02LjUzMy0xMi40NTEtNi41MzJhNC4zMSA0LjMxIDAgMDEtMS44MTUtNS44MmwxNi4zNDYtMzEuMTYyYTQuMzEgNC4zMSAwIDAxNS44Mi0xLjgxNGwxMi40NTMgNi41MzEgMzIuNDExIDE3LjAwMiAyMC4wODkgMTAuNTM5LjAyNi4wMTQgMy43OTMgMS45OS0xNy4xNjggMzIuNzI2LTEuMTgyIDIuMjUyYS4wMDguMDA4IDAgMDEtLjAwMi4wMDRsLTI0LjM1NyA0Ni40My0yLjQ3NyA0LjcyMS0xMy44NjkgMjYuNDM5LS4wMS4wMTgtMS45OTQgMy44ek00MjMuNjkgNTc1LjMzbC0zLjgxOC0yLjAwNC0xOC4xMzUtOS41MTQtMTQuMzE4LTcuNTEyYTQuMzEgNC4zMSAwIDAxLTEuODE3LTUuODJsMTYuMzQ4LTMxLjE2YTQuMzExIDQuMzExIDAgMDE1LjgyLTEuODE3bDE0LjMxOSA3LjUxMiAxOC4xMzYgOS41MTQuMDI2LjAxMyAzLjc5MyAxLjk5MS0yLjAwNCAzLjgxOC04LjE3MiAxNS41OC04LjE3NCAxNS41OC0uMDEuMDE2LTEuOTk0IDMuODAzem0yNjYuODEzLTE4NC40MjZjMTAuNzUzLjAyIDE5LjE2My00LjMxMiAyNC43NS0xNC45NjMgMTcuMDI4LTMyLjQ2LTE0LjQ3Mi04Mi41MzgtNzAuMzU2LTExMS44NTQtNTUuODg0LTI5LjMxNi0xMTQuOTkxLTI2Ljc2OC0xMzIuMDE5IDUuNjkyLTE3LjAyOCAzMi40NTkgMzAuMzM0IDUzLjc3MiA4Ni4yMTkgODMuMDg4IDM3LjU0NyAxOS42OTYgNjkuMzg4IDM3Ljk5NCA5MS40MDYgMzguMDM3eiIvPjxwYXRoIGQ9Ik00MjMuNjkxIDU3NS4zMjlsMTkuMDkxIDEwLjAxNSAyMC4zNTMtMzguNzk3LTE5LjA5MS0xMC4wMTV6TTM0Mi4wNyA3MzAuOTIybDE5LjA5MSAxMC4wMTUgNjEuMDU3LTExNi4zOTItMTkuMDkxLTEwLjAxNXoiIGZpbGwtb3BhY2l0eT0iLjIiLz48L2c+PC9zdmc+';
|
||||
@@ -0,0 +1,5 @@
|
||||
const KnownAppVersions = {
|
||||
KeePassXC: '2.6.4'
|
||||
};
|
||||
|
||||
export { KnownAppVersions };
|
||||
@@ -7,11 +7,12 @@ const Links = {
|
||||
License: 'https://github.com/keeweb/keeweb/blob/master/LICENSE',
|
||||
LicenseApache: 'https://opensource.org/licenses/Apache-2.0',
|
||||
LicenseLinkCCBY40: 'https://creativecommons.org/licenses/by/4.0/',
|
||||
UpdateDesktop: 'https://github.com/keeweb/keeweb/releases/download/v{ver}/UpdateDesktop.zip',
|
||||
UpdateBasePath: 'https://github.com/keeweb/keeweb/releases/download/v{ver}/',
|
||||
ReleaseNotes: 'https://github.com/keeweb/keeweb/blob/master/release-notes.md#release-notes',
|
||||
SelfHostedDropbox: 'https://github.com/keeweb/keeweb#self-hosting',
|
||||
Manifest: 'https://app.keeweb.info/manifest.appcache',
|
||||
UpdateJson: 'https://app.keeweb.info/update.json',
|
||||
AutoType: 'https://github.com/keeweb/keeweb/wiki/Auto-Type',
|
||||
AutoTypeMacOS: 'https://github.com/keeweb/keeweb/wiki/Auto-Type#macos',
|
||||
Translation: 'https://keeweb.oneskyapp.com/',
|
||||
Donation: 'https://opencollective.com/keeweb#support',
|
||||
Plugins: 'https://plugins.keeweb.info',
|
||||
@@ -19,7 +20,17 @@ const Links = {
|
||||
YubiKeyManual: 'https://github.com/keeweb/keeweb/wiki/YubiKey',
|
||||
YubiKeyManagerInstall: 'https://github.com/Yubico/yubikey-manager#installation',
|
||||
HaveIBeenPwned: 'https://haveibeenpwned.com',
|
||||
HaveIBeenPwnedPrivacy: 'https://haveibeenpwned.com/Passwords'
|
||||
HaveIBeenPwnedPrivacy: 'https://haveibeenpwned.com/Passwords',
|
||||
ExtensionHelpForOtherBrowsers:
|
||||
'https://github.com/keeweb/keeweb/wiki/Browser-AutoFill#other-browsers',
|
||||
ExtensionHelpForKPXC:
|
||||
'https://github.com/keeweb/keeweb/wiki/Browser-AutoFill#keepassxc-browser',
|
||||
KWCForChrome:
|
||||
'https://chrome.google.com/webstore/detail/keeweb-connect/pikpfmjfkekaeinceagbebpfkmkdlcjk',
|
||||
KWCForFirefox: 'https://addons.mozilla.org/firefox/addon/keeweb-connect/',
|
||||
KWCForEdge:
|
||||
'https://microsoftedge.microsoft.com/addons/detail/keewebconnect/nmggpehkjmeaeocmaijenpejbepckinm',
|
||||
KWCForSafari: 'https://apps.apple.com/app/keeweb-connect/id1565748094'
|
||||
};
|
||||
|
||||
export { Links };
|
||||
|
||||
@@ -3,7 +3,8 @@ const RuntimeInfo = {
|
||||
beta: !!'@@BETA',
|
||||
buildDate: '@@DATE',
|
||||
commit: '@@COMMIT',
|
||||
devMode: '@@DEVMODE'
|
||||
devMode: '@@DEVMODE',
|
||||
appleTeamId: '@@APPLE_TEAM_ID'
|
||||
};
|
||||
|
||||
export { RuntimeInfo };
|
||||
|
||||
@@ -17,7 +17,9 @@ const Timeouts = {
|
||||
ExternalDeviceAfterReconnect: 1000,
|
||||
FieldLabelDoubleClick: 300,
|
||||
NativeModuleHostRestartTime: 3000,
|
||||
FastAnimation: 100
|
||||
FastAnimation: 100,
|
||||
AutoTypeCopyPaste: 300,
|
||||
KeeWebConnectRequest: 60000
|
||||
};
|
||||
|
||||
export { Timeouts };
|
||||
|
||||
@@ -251,7 +251,6 @@ class View extends EventEmitter {
|
||||
FocusManager.setModal(null);
|
||||
}
|
||||
}
|
||||
this.emit(visible ? 'show' : 'hide');
|
||||
if (this.el) {
|
||||
this.el.classList.toggle('show', !!visible);
|
||||
this.el.classList.toggle('hide', !visible);
|
||||
@@ -259,6 +258,7 @@ class View extends EventEmitter {
|
||||
Tip.hideTips(this.el);
|
||||
}
|
||||
}
|
||||
this.emit(visible ? 'show' : 'hide');
|
||||
}
|
||||
|
||||
isHidden() {
|
||||
|
||||
@@ -12,10 +12,14 @@ Handlebars.registerHelper('res', function (key, options) {
|
||||
return value;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('Res', (key) => {
|
||||
Handlebars.registerHelper('Res', (key, options) => {
|
||||
let value = Locale[key];
|
||||
if (value) {
|
||||
value = value[0].toUpperCase() + value.substr(1);
|
||||
const ix = value.indexOf('{}');
|
||||
if (ix >= 0) {
|
||||
value = value.replace('{}', options.fn(this));
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
+776
-691
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@
|
||||
"or": "oder",
|
||||
"history": "Verlauf",
|
||||
"template": "Vorlage",
|
||||
"templates": "Vorlagen",
|
||||
"notImplemented": "Nicht implementiert",
|
||||
"saveChanges": "Änderungen speichern",
|
||||
"discardChanges": "Änderungen verwerfen",
|
||||
@@ -26,6 +27,15 @@
|
||||
"shiftKey": "Umschalt",
|
||||
"altKey": "Alt",
|
||||
"error": "Fehler",
|
||||
"oneMinute": "Eine Minute",
|
||||
"minutes": "{} Minuten",
|
||||
"oneHour": "Eine Stunde",
|
||||
"hours": "{} Stunden",
|
||||
"oneDay": "Ein Tag",
|
||||
"days": "{} Tage",
|
||||
"oneWeek": "Eine Woche",
|
||||
"oneMonth": "Ein Monat",
|
||||
"oneYear": "Ein Jahr",
|
||||
"cache": "Cache",
|
||||
"file": "Datei",
|
||||
"device": "Gerät",
|
||||
@@ -47,6 +57,22 @@
|
||||
"menuItemCollapsed": "Doppelklick zum Aufklappen",
|
||||
"menuRestoreApp": "{} öffnen",
|
||||
"menuQuitApp": "{} beenden",
|
||||
"sysMenuAboutKeeWeb": "Über {}",
|
||||
"sysMenuServices": "Dienste",
|
||||
"sysMenuHide": "{} ausblenden",
|
||||
"sysMenuHideOthers": "Andere ausblenden",
|
||||
"sysMenuUnhide": "Alle anzeigen",
|
||||
"sysMenuQuit": "{} beenden",
|
||||
"sysMenuEdit": "Bearbeiten",
|
||||
"sysMenuUndo": "Rückgängig",
|
||||
"sysMenuRedo": "Wiederherstellen",
|
||||
"sysMenuCut": "Ausschneiden",
|
||||
"sysMenuCopy": "Kopieren",
|
||||
"sysMenuPaste": "Einfügen",
|
||||
"sysMenuSelectAll": "Alles auswählen",
|
||||
"sysMenuWindow": "Fenster",
|
||||
"sysMenuMinimize": "Minimieren",
|
||||
"sysMenuClose": "Fenster schließen",
|
||||
"alertYes": "Ja",
|
||||
"alertNo": "Nein",
|
||||
"alertOk": "OK",
|
||||
@@ -219,7 +245,7 @@
|
||||
"detHistoryDiscardChangesAlertBody": "Ungespeicherte Änderungen gehen dabei unwiderruflich verloren.",
|
||||
"detBackToList": "zurück zur Liste",
|
||||
"detSetIconColor": "Farbe ändern",
|
||||
"detSetIcon": "Icon ändern",
|
||||
"detSetIcon": "Symbol ändern",
|
||||
"detDropAttachments": "Anhänge hier ablegen",
|
||||
"detDelEntry": "Löschen",
|
||||
"detDelEntryPerm": "Dauerhaft löschen",
|
||||
@@ -286,6 +312,16 @@
|
||||
"detRevealField": "Anzeigen",
|
||||
"detHideField": "Verbergen",
|
||||
"detAutoTypeField": "Auto-Type",
|
||||
"detIssuesHideTooltip": "Diese Warnung verstecken",
|
||||
"detIssueWeakPassword": "Das Passwort ist schwach, es wird empfohlen es zu ändern.",
|
||||
"detIssuePoorPassword": "Das Passwort ist schwach, es wird dringend empfohlen es zu ändern.",
|
||||
"detIssuePwnedPassword": "Dieses Passwort wurde laut {} in einem Datenleck gefunden, es wird empfohlen das Passwort zu ändern.",
|
||||
"detIssuePasswordCheckError": "Es gab einen Fehler bei der online-überprüfung des Passwort.",
|
||||
"detIssueOldPassword": "Das Passwort ist alt.",
|
||||
"detIssueCloseAlertHeader": "Verstecke Passwortmeldungen",
|
||||
"detIssueCloseAlertBody": "Es gibt verschiedene Möglichkeiten diese Warnung zu verstecken:",
|
||||
"detIssueCloseAlertEntry": "Nicht für diesen Eintrag anzeigen.",
|
||||
"detIssueCloseAlertSettings": "Globale Einstellungen anpassen",
|
||||
"autoTypeEntryFields": "Feld-Platzhalter",
|
||||
"autoTypeModifiers": "Hilfstasten",
|
||||
"autoTypeKeys": "Tasten",
|
||||
@@ -316,6 +352,8 @@
|
||||
"appSaveError": "Fehler beim Speichern",
|
||||
"appSaveErrorBody": "Automatisches Speichern fehlgeschlagen",
|
||||
"appSaveErrorBodyMul": "Automatisches Speichern fehlgeschlagen:",
|
||||
"appSaveErrorExitLoseChanges": "Schließen und alle Änderungen verlieren",
|
||||
"appSaveErrorExitLoseChangesBody": "Du kannst entweder die App schließen und alle Änderungen verlieren, oder gehe in die Dateieinstellungen und exportiere die Daten",
|
||||
"appSettingsError": "Fehler beim Laden der App",
|
||||
"appSettingsErrorBody": "Es gab einen Fehler beim Laden der App-Einstellungen. Bitte überprüfen Sie die App-URL oder kontaktieren Sie Ihren Systemadministrator.",
|
||||
"appNotSupportedError": "Ihr Browser unterstützt einige wichtige Funktionen nicht, die für diese App benötigt werden.",
|
||||
@@ -351,12 +389,26 @@
|
||||
"setGenDownloadAndRestart": "Update herunterladen und App neu starten",
|
||||
"setGenAppearance": "Oberfläche",
|
||||
"setGenTheme": "Theme",
|
||||
"setGenThemeDefault": "Standard",
|
||||
"setGenThemeDark": "Dunkel",
|
||||
"setGenThemeLight": "Hell",
|
||||
"setGenThemeBlue": "Flach blau",
|
||||
"setGenThemeFb": "Mattblau",
|
||||
"setGenThemeBl": "Hell blau",
|
||||
"setGenThemeBrown": "Brownie",
|
||||
"setGenThemeDb": "Dunkelbraun",
|
||||
"setGenThemeLb": "Hell braun",
|
||||
"setGenThemeTerminal": "Terminal",
|
||||
"setGenThemeTe": "Terminal",
|
||||
"setGenThemeLt": "Hell Terminal",
|
||||
"setGenThemeHighContrast": "Hoher Kontrast",
|
||||
"setGenThemeHc": "Hoher Kontrast",
|
||||
"setGenThemeDc": "Dunkel Kontrast",
|
||||
"setGenThemeSol": "Solarized",
|
||||
"setGenThemeSd": "Solarisiert dunkel",
|
||||
"setGenThemeSl": "Solarisiert hell",
|
||||
"setGenMoreThemes": "Mehr Themes",
|
||||
"setGenAutoSwitchTheme": "Automatisch zwischen hellem und dunklem Thema umschalten, sofern möglich",
|
||||
"setGenLocale": "Sprache",
|
||||
"setGenLocOther": "Andere Sprachen sind als Plugins verfügbar.",
|
||||
"setGenFontSize": "Schriftgröße",
|
||||
@@ -399,6 +451,7 @@
|
||||
"setGenLockAutoType": "Bei Auto-Type",
|
||||
"setGenLockOrSleep": "Bei Aktivierung von Bildschirmsperre oder Ruhezustand",
|
||||
"setGenStorage": "Synchronisierung",
|
||||
"setGenDisableOfflineStorage": "Geladene Dateien nicht in Offline-Speicher zwischenspeichern",
|
||||
"setGenStorageLogout": "Abmelden",
|
||||
"setGenShowAdvanced": "Erweiterte Einstellungen anzeigen",
|
||||
"setGenDevTools": "Entwicklerwerkzeuge anzeigen",
|
||||
@@ -408,6 +461,21 @@
|
||||
"setGenShowAppLogs": "App-Logs anzeigen",
|
||||
"setGenReloadApp": "App neu laden",
|
||||
"setGenFieldLabelDblClickAutoType": "Auto-Type durch Anklicken von Beschriftungen aktivieren",
|
||||
"setGenTouchId": "Fingerabdruck",
|
||||
"setGenTouchIdDisabled": "Fingerabdruck nicht benutzen",
|
||||
"setGenTouchIdMemory": "Nur mit Fingerabdruck entsperren, wenn KeeWeb aktiv ist",
|
||||
"setGenTouchIdFile": "Benutze immer den Fingerabdruck anstatt das Master-Passwort",
|
||||
"setGenTouchIdPass": "Benötige Master-Passwort nach",
|
||||
"setGenAudit": "Audit",
|
||||
"setGenAuditPasswords": "Warnungen für schwache Passwörter anzeigen",
|
||||
"setGenAuditPasswordEntropy": "Passwortlänge und Zufälligkeit prüfen",
|
||||
"setGenExcludePinsFromAudit": "Kurze Pin-Codes niemals prüfen (z. B. 12345)",
|
||||
"setGenHelpHIBP": "KeeWeb kann überprüfen ob deine Passwörter bei einem früheren Datenleck veröffentlicht wurden. Deine Passwörter können nicht beim übertragen ausgelesen werden, aber die Anzahl der überprüften Passwörter kann möglicherweise entdeckt werden. {} kannst du mehr über den Datenschutz beim Nutzen dieses Dienstes erfahren. Wenn diese Option aktiviert ist überprüft KeeWeb deine Passwörter automatisch.",
|
||||
"setGenHelpHIBPLink": "hier",
|
||||
"setGenAuditPasswordAge": "Alte Passwörter",
|
||||
"setGenAuditPasswordAgeOff": "Zeige keine Warnungen über alte Passwörter",
|
||||
"setGenAuditPasswordAgeOneYear": "Zeige Warnungen für Passwörter die über ein Jahr alt sind",
|
||||
"setGenAuditPasswordAgeYears": "Zeige Warnungen für Passwörter die über {} Jahre alt sind",
|
||||
"setFilePath": "Dateipfad",
|
||||
"setFileStorage": "Diese Datei wird von {} geladen.",
|
||||
"setFileIntl": "Diese Datei ist im internen App-Speicher abgelegt",
|
||||
@@ -621,5 +689,7 @@
|
||||
"yubiKeyTouchRequestedHeader": "Bitte den YubiKey berühren",
|
||||
"yubiKeyTouchRequestedBody": "Bitte den YubiKey mit der Seriennummer {} berühren",
|
||||
"yubiKeyDisabledErrorHeader": "USB ist deaktiviert",
|
||||
"yubiKeyDisabledErrorBody": "YubiKey ist zum Öffnen dieser Datei erforderlich. Bitte USB-Geräte in den Einstellungen aktivieren."
|
||||
"yubiKeyDisabledErrorBody": "YubiKey ist zum Öffnen dieser Datei erforderlich. Bitte USB-Geräte in den Einstellungen aktivieren.",
|
||||
"yubiKeyErrorWithCode": "YubiKey-Fehler, Code {}.",
|
||||
"bioOpenAuthPrompt": "„{}“ öffnen"
|
||||
}
|
||||
+143
-11
@@ -14,6 +14,7 @@
|
||||
"or": "ou",
|
||||
"history": "historique",
|
||||
"template": "modèle",
|
||||
"templates": "templates",
|
||||
"notImplemented": "Non Implémenté",
|
||||
"saveChanges": "Sauvegarder les modifications",
|
||||
"discardChanges": "Annuler les modifications",
|
||||
@@ -26,6 +27,15 @@
|
||||
"shiftKey": "shift",
|
||||
"altKey": "alt",
|
||||
"error": "erreur",
|
||||
"oneMinute": "une minute",
|
||||
"minutes": "{} minutes",
|
||||
"oneHour": "une heure",
|
||||
"hours": "{} heures",
|
||||
"oneDay": "un jour",
|
||||
"days": "{} jours",
|
||||
"oneWeek": "une semaine",
|
||||
"oneMonth": "un mois",
|
||||
"oneYear": "une année",
|
||||
"cache": "cache",
|
||||
"file": "fichier",
|
||||
"device": "appareil",
|
||||
@@ -37,16 +47,33 @@
|
||||
"menuColors": "Couleurs",
|
||||
"menuTrash": "Corbeille",
|
||||
"menuSetGeneral": "Général",
|
||||
"menuSetBrowser": "Navigateur",
|
||||
"menuSetAbout": "À propos",
|
||||
"menuSetDevices": "Appareils",
|
||||
"menuAlertNoTags": "Aucun tag",
|
||||
"menuAlertNoTagsBody": "Vous pouvez ajouter de nouveaux tags en éditant les champs, dans la section Tags.",
|
||||
"menuEmptyTrash": "Vider la Corbeille",
|
||||
"menuEmptyTrashAlert": "Vider la Corbeille?",
|
||||
"menuEmptyTrashAlert": "Vider la Corbeille ?",
|
||||
"menuEmptyTrashAlertBody": "Vous ne pourrez plus récupérer ces éléments",
|
||||
"menuItemCollapsed": "Double-cliquez pour déplier",
|
||||
"menuRestoreApp": "Ouvrir {}",
|
||||
"menuQuitApp": "Quitter {}",
|
||||
"sysMenuAboutKeeWeb": "A propos {}",
|
||||
"sysMenuServices": "Services",
|
||||
"sysMenuHide": "Cacher {}",
|
||||
"sysMenuHideOthers": "Cacher autres",
|
||||
"sysMenuUnhide": "Montrer tout",
|
||||
"sysMenuQuit": "Quitter {}",
|
||||
"sysMenuEdit": "Editer",
|
||||
"sysMenuUndo": "Annuler",
|
||||
"sysMenuRedo": "Répéter",
|
||||
"sysMenuCut": "Couper",
|
||||
"sysMenuCopy": "Copier",
|
||||
"sysMenuPaste": "Coller",
|
||||
"sysMenuSelectAll": "Tout sélectionner",
|
||||
"sysMenuWindow": "Fenêtre",
|
||||
"sysMenuMinimize": "Réduire",
|
||||
"sysMenuClose": "Fermer fenêtre",
|
||||
"alertYes": "Oui",
|
||||
"alertNo": "Non",
|
||||
"alertOk": "OK",
|
||||
@@ -55,6 +82,8 @@
|
||||
"alertCopy": "Copier",
|
||||
"alertClose": "Fermer",
|
||||
"alertDoNotAsk": "Ne plus me le redemander",
|
||||
"alertAllow": "Autoriser",
|
||||
"alertDeny": "Refuser",
|
||||
"appBeta": "ATTENTION: version beta, aperçu seulement",
|
||||
"footerOpen": "Ouvrir/Nouveau",
|
||||
"footerSyncError": "Erreur de synchronisation",
|
||||
@@ -63,7 +92,7 @@
|
||||
"genLen": "Longueur",
|
||||
"genNewPass": "Nouveau mot de passe",
|
||||
"genPresetDefault": "réglages par défaut",
|
||||
"genPresetDerived": "identique à l'ancien mot de passe",
|
||||
"genPresetDerived": "similaire à l'ancien mot de passe",
|
||||
"genPresetPronounceable": "prononçable",
|
||||
"genPresetMed": "longueur moyenne",
|
||||
"genPresetLong": "long",
|
||||
@@ -87,7 +116,7 @@
|
||||
"tagExists": "Ce tag existe déjà",
|
||||
"tagExistsBody": "Un tag existe déjà avec ce nom. Merci de choisir un autre nom.",
|
||||
"tagBadName": "Nom invalide",
|
||||
"tagBadNameBody": "Un nom de tag ne peut pas contenir les caractères {}. Merci de les supprimer.",
|
||||
"tagBadNameBody": "Les noms de Tag ne peuvent contenir les caractères {}. Veuillez les supprimer.",
|
||||
"genPsTitle": "Préréglages du Générateur",
|
||||
"genPsCreate": "Nouveau préréglage",
|
||||
"genPsDelete": "Supprimer préréglage",
|
||||
@@ -114,6 +143,7 @@
|
||||
"keyChangeMessageExpired": "La clé maître pour cette base de donnée est expirée. Merci de saisir une nouvelle clé.",
|
||||
"keyChangeRepeatPassword": "Mot de passe, encore une fois",
|
||||
"keyEnter": "Entrée",
|
||||
"keyEsc": "Esc",
|
||||
"iconFavTitle": "Télécharger et utiliser le favicon du site web",
|
||||
"iconSelCustom": "Sélectionner une icône personnalisée",
|
||||
"listEmptyTitle": "Vide",
|
||||
@@ -122,7 +152,7 @@
|
||||
"listNoWebsite": "aucun site web",
|
||||
"listNoUser": "aucun utilisateur",
|
||||
"listNoAttachments": "aucune pièce-jointe",
|
||||
"listAddTemplateHeader": "Templates",
|
||||
"listAddTemplateHeader": "Modèles",
|
||||
"listAddTemplateBody1": "Les modèles (templates) vous permettent de créer des nouvelles entrées en un clic. Ajouter quelque chose à l'entrée du modèle et ensuite cliquer de nouveau sur {} pour utiliser ce modèle.",
|
||||
"listAddTemplateBody2": "Vous pouvez toujours retrouver vos modèles dans le groupe {}.",
|
||||
"searchAddNew": "Ajouter Nouveau",
|
||||
@@ -184,6 +214,7 @@
|
||||
"openConfigErrorNotFound": "Aucun fichier trouvé",
|
||||
"openError": "Erreur",
|
||||
"openErrorDescription": "Une erreur est survenue à l'ouverture du fichier",
|
||||
"openErrorDescriptionMaybeTouchIdChanged": "L'erreur ressemble à ce qu'y arrive habituellement quand la configuration de Touch ID a été changée. Par exemple, vous avez ajouté ou supprimé un doigt supplémentaire. Si c'est le cas, allez dans Paramètres, désactiver Touch ID et activer le de nouveau.",
|
||||
"openErrorFileNotFound": "Fichier non trouvé",
|
||||
"openListErrorBody": "Erreur au chargement de la liste des fichiers",
|
||||
"openShowAllFiles": "Tous les fichiers",
|
||||
@@ -237,10 +268,11 @@
|
||||
"detDelToTrashBody": "L'entrée sera déplacée dans la corbeille.",
|
||||
"detFieldCopied": "Copié",
|
||||
"detFieldCopiedTime": "Copié pendant {} secondes",
|
||||
"detCopyHint": "Vous pouvez copier la valeur du champ en cliquant sur son titre",
|
||||
"detCopyHint": "Vous pouvez copier la valeur du champ en cliquant sur ton titre",
|
||||
"detMore": "plus",
|
||||
"detClickToAddField": "cliquez pour ajouter un nouveau champ",
|
||||
"detMenuAddNewField": "Ajouter nouveau champ",
|
||||
"detMenuAddNewWebsite": "Ajouter un autre site",
|
||||
"detMenuShowEmpty": "Montrer les champs vides",
|
||||
"detMenuHideEmpty": "Cacher les champs vides",
|
||||
"detMenuAddField": "Ajouter {}",
|
||||
@@ -286,13 +318,24 @@
|
||||
"detRevealField": "Révéler",
|
||||
"detHideField": "Cacher",
|
||||
"detAutoTypeField": "Saisie auto",
|
||||
"detIssuesHideTooltip": "Cacher cet avertissement",
|
||||
"detIssueWeakPassword": "Ce mot de passe est faible, nous vous recommandons de le changer",
|
||||
"detIssuePoorPassword": "Ce mot de passe est très faible, nous vous recommandons très fort de le changer",
|
||||
"detIssuePwnedPassword": "Ce mot de passe a été exposé à une faille selon {}, il est recommandé de le changer",
|
||||
"detIssuePasswordCheckError": "Une erreur est survenue en vérifiant la force du mot de passe en ligne",
|
||||
"detIssueOldPassword": "Ce mot de passe est vieux",
|
||||
"detIssueCloseAlertHeader": "Cacher les problèmes de mots de passe",
|
||||
"detIssueCloseAlertBody": "Il y a plusieurs façon de cacher ce message :",
|
||||
"detIssueCloseAlertEntry": "Ne plus montrer pour cette entrée",
|
||||
"detIssueCloseAlertSettings": "Modifier les paramètres globaux",
|
||||
"autoTypeEntryFields": "Champs",
|
||||
"autoTypeModifiers": "Touches modificatrices",
|
||||
"autoTypeKeys": "Clés",
|
||||
"autoTypeLink": "plus...",
|
||||
"autoTypeError": "Erreur saisie auto",
|
||||
"autoTypeErrorGeneric": "Il y a eu une erreur lors de la saisie automatique : {}",
|
||||
"autoTypeErrorGlobal": "Pour utiliser un raccourci, sélectionner l'application là où vous voulez saisir le mot de passe",
|
||||
"autoTypeErrorAccessibilityMacOS": "Nous avons essayé d'envoyer les frappes de caractères à cette application mais cela ne semble pas fonctionner parce qu'il manque des permissions. Cliquer ici pour en lire plus.",
|
||||
"autoTypeErrorGlobal": "Pour utiliser un raccourci global, sélectionner l'application là où vous voulez saisir le mot de passe",
|
||||
"autoTypeErrorNotInstalled": "{} n'est pas installé",
|
||||
"autoTypeHeader": "Saisie auto : Selectionner",
|
||||
"autoTypeMsgNoWindow": "Nous n'avons pas pu obtenir le titre de la fenêtre active, commencez à taper pour rechercher",
|
||||
@@ -303,6 +346,7 @@
|
||||
"autoTypeSelectionHintOpt": "Entrez seulement le compte",
|
||||
"autoTypeSelectionHintShift": "Autres champs",
|
||||
"autoTypeSelectionOtp": "code à usage unique",
|
||||
"autoTypeUnlockMessage": "Déverrouiller pour saisir automatiquement",
|
||||
"appSecWarn": "Non sécurisé !",
|
||||
"appSecWarnBody1": "Vous avez chargé cette appli avec une connexion non sécurisée. Quelqu'un peut vous observer et voler vos mots de passe. Nous vous recommandons fortement d'arrêter cette connexion, à moins que vous ne compreniez exactement ce que vous faites.",
|
||||
"appSecWarnBody2": "Oui, votre base est chiffrée mais personne ne peut garantir que l'application n'a pas été modifiée avant d'arriver à vous.",
|
||||
@@ -349,7 +393,7 @@
|
||||
"setGenExtractingUpdate": "Décompression de la mise à jour...",
|
||||
"setGenCheckErr": "Une erreur est intervenue durant le téléchargement de la mise à jour",
|
||||
"setGenNeverChecked": "Ne jamais vérifier les mises à jour",
|
||||
"setGenRestartToUpdate": "Redémarrer pour mettre à jour",
|
||||
"setGenRestartToUpdate": "Redémarrer KeeWeb pour mettre à jour",
|
||||
"setGenDownloadAndRestart": "Télécharger la mise à jour et redémarrer",
|
||||
"setGenAppearance": "Apparence",
|
||||
"setGenTheme": "Thème",
|
||||
@@ -409,12 +453,15 @@
|
||||
"setGenClearSeconds": "Dans {} secondes",
|
||||
"setGenClearMinute": "Dans une minute",
|
||||
"setGenMinInstead": "Réduire l'application au lieu de fermer",
|
||||
"setGenMinOnFieldCopy": "Réduire sur copie d'un champ",
|
||||
"setGenLock": "Verrou Auto",
|
||||
"setGenLockMinimize": "quand l'appli est en arrière-plan",
|
||||
"setGenLockCopy": "sur copie de m.d.p.",
|
||||
"setGenLockAutoType": "sur saisie auto",
|
||||
"setGenLockOrSleep": "Quand l'ordinateur est verrouillé ou mis en sommeil",
|
||||
"setGenStorage": "Stockage",
|
||||
"setGenDisableOfflineStorage": "Ne pas mettre en cache des fichiers d'un stockage offline",
|
||||
"setGenShortLivedStorageToken": "Utiliser des sessions de courte-durée avec les fournisseurs de stockage dans le cloud",
|
||||
"setGenStorageLogout": "Déconnexion",
|
||||
"setGenShowAdvanced": "Afficher les paramètres avancés",
|
||||
"setGenDevTools": "Afficher les outils de développement",
|
||||
@@ -424,6 +471,22 @@
|
||||
"setGenShowAppLogs": "Voir les logs",
|
||||
"setGenReloadApp": "Recharger l'application",
|
||||
"setGenFieldLabelDblClickAutoType": "Remplissage auto par double clic sur les noms de champ",
|
||||
"setGenTouchId": "Touch ID",
|
||||
"setGenTouchIdDisabled": "Ne pas utiliser Touch ID",
|
||||
"setGenTouchIdMemory": "Dévérouiller avec Touch ID uniquement quand KeeWeb est lancé",
|
||||
"setGenTouchIdFile": "Toujours utiliser Touch ID à la place du mot de passe",
|
||||
"setGenTouchIdPass": "Exige le mot de passe après",
|
||||
"setGenAudit": "Audit",
|
||||
"setGenAuditPasswords": "Voir des avertissements sur la force du mot de passe",
|
||||
"setGenAuditPasswordEntropy": "Vérifier la longueur et l'aléas du mot de passe",
|
||||
"setGenExcludePinsFromAudit": "Ne jamais vérifier les codes PIN courts, comme 123456",
|
||||
"setGenCheckPasswordsOnHIBP": "Vérifier les mots de passe en utilisant un service en ligne {}",
|
||||
"setGenHelpHIBP": "KeeWeb peut vérifier si vos mots de passes ont été précédemment exposés à une faille de sécurité en utilisant un service en ligne. Votre mot de passe ne peut pas être récupéré à partir de données envoyées en ligne, cependant le nombre de mots de passes vérifiés peut être exposé. Plus d'informations sur votre sécurité en utilisant ce service peuvent être trouvées {}. Si cette option est activée, KeeWeb vérifiera automatiquement vos mots de passes là.",
|
||||
"setGenHelpHIBPLink": "ici",
|
||||
"setGenAuditPasswordAge": "Vieux mots de passe",
|
||||
"setGenAuditPasswordAgeOff": "Ne pas afficher d'avertissements à propos des vieux mots de passe",
|
||||
"setGenAuditPasswordAgeOneYear": "Afficher des avertissements pour les mots de passe plus vieux qu'un an",
|
||||
"setGenAuditPasswordAgeYears": "Afficher des avertissement pour les mots de passe plus vieux que {} ans",
|
||||
"setFilePath": "Chemin",
|
||||
"setFileStorage": "Le fichier est ouvert de {}.",
|
||||
"setFileIntl": "Le fichier est conservé dans le stockage interne de l'application",
|
||||
@@ -558,6 +621,38 @@
|
||||
"setPlTranslateLink": "traduire l'appli dans votre langue",
|
||||
"setPlAutoUpdate": "Mise à jour automatique",
|
||||
"setPlLoadGallery": "Charger le catalogue",
|
||||
"setBrowserTitle": "Navigateur",
|
||||
"setBrowserIntroDesktop": "KeeWeb peut entrer des mots de passe en utilisant la saisie automatique, cependant, une extension peut être un moyen plus pratique de le faire. KeeWeb prend en charge deux extensions de navigateur:",
|
||||
"setBrowserIntroKeeWebConnect": "l'extension officielle KeeWeb est construite avec les fonctionnalités de KeeWeb, mais vous risquez de manquer certaines fonctionnalités avancées que l'on peut trouver dans l'autre extension.",
|
||||
"setBrowserIntroKeePassXcBrowser": "cette extension a été développée pour KeePassXC, elle existe depuis un moment et est assez fiable. L'extension ne fait pas partie de KeeWeb et il peut y avoir des problèmes de compatibilité.",
|
||||
"setBrowserIntroWeb": "Installez notre extension de navigateur pour remplir automatiquement les mots de passe de KeeWeb sur différentes pages. L'extension de navigateur se connectera à un onglet KeeWeb de votre navigateur pour récupérer les mots de passe. Cliquez ici pour télécharger l'extension:",
|
||||
"setBrowserNotEnabled": "L'intégration du navigateur n'est pas activée, les extensions ne pourront pas se connecter à KeeWeb. Utilisez les cases à cocher ci-dessous pour l'activer:",
|
||||
"setBrowserEnablePerBrowser": "Activer l'intégration dans le navigateur en cochant ci dessous:",
|
||||
"setBrowserFocusIfLocked": "Ouvrir KeeWeb si le navigateur essaye de se connecter alors que aucun fichier n'est ouvert",
|
||||
"setBrowserFocusIfEmpty": "Afficher la liste si aucune correspondance n'est trouvée avec l'URL",
|
||||
"setBrowserOtherBrowsers": "Autres navigateurs",
|
||||
"setBrowserExtensionFor": "pour {}",
|
||||
"setBrowserExtensionNotSupported": "Non supporté",
|
||||
"setBrowserExtensionHelp": "Comment installer ?",
|
||||
"setBrowserExtensionInstall": "Installer l'application",
|
||||
"setBrowserExtensionKPXCWarnHeader": "{} cessera de fonctionner",
|
||||
"setBrowserExtensionKPXCWarnBody1": "Malheureusement, il n'est pas possible de connecter une extension à plusieurs applications. Si vous connectez l'extension à KeeWeb, nous remplacerons son association d'application, ce qui signifie que l'intégration avec {} cessera de fonctionner. Même si vous décochez cette case, l'association avec {} ne sera pas restaurée. Pour le faire fonctionner à nouveau, configurez l'intégration du navigateur dans les paramètres de {}.",
|
||||
"setBrowserExtensionKPXCWarnBody2": "Configurer l'extension pour utiliser KeeWeb?",
|
||||
"setBrowserSessions": "Sessions",
|
||||
"setBrowserSessionsEmpty": "Aucune session connectée",
|
||||
"setBrowserSessionsIntro": "Ces extensions sont connectées à KeeWeb",
|
||||
"setBrowserSessionsActiveTooltip": "Session active",
|
||||
"setBrowserSessionsActiveText": "Cette session est active. Elle peut échanger des données avec KeeWeb selon les permissions:",
|
||||
"setBrowserSessionsInactiveTooltip": "session inactive",
|
||||
"setBrowserSessionsInactiveText": "Cette session est inactive. L'extension est connectée à KeeWeb, cependant, elle n'a pas essayé d'échanger des données. Lorsque l'extension demande quelque chose, vous pourrez choisir ce que vous souhaitez partager.",
|
||||
"setBrowserSessionsDeniedTooltip": "Accès interdit",
|
||||
"setBrowserSessionsDeniedText": "Cette session est inactive. L'extension est connectée à KeeWeb mais vous avez refusé l'accès aux données.",
|
||||
"setBrowserSessionsConnectedDate": "Connecté",
|
||||
"setBrowserSessionsTerminate": "Clôturer la session",
|
||||
"setBrowserSessionsAccessToFiles": "Autoriser l'accès aux fichiers",
|
||||
"setBrowserSessionsNoFileAccess": "L'extension n'a accès à aucun fichier, elle ne pourra pas récupérer les mots de passe de KeeWeb.",
|
||||
"setBrowserSessionsPasswordsRead": "Accès aux mot de passe",
|
||||
"setBrowserSessionsPasswordsWritten": "Mots de passe enregistrés",
|
||||
"setDevicesTitle": "Appareils",
|
||||
"setDevicesEnableUsb": "Autoriser les interactions avec les appareils USB",
|
||||
"setDevicesYubiKeyIntro": "Il est recommandé de lire {} avant d'utiliser une YubiKey.",
|
||||
@@ -592,12 +687,12 @@
|
||||
"setHelpProblems2": "ou {}",
|
||||
"setHelpOpenIssue": "faire connaître le problème sur GitHub",
|
||||
"setHelpContactLink": "contactez le développeur directement",
|
||||
"setHelpAppInfo": "Information",
|
||||
"setHelpAppInfo": "Informations",
|
||||
"setHelpOtherPlatforms": "Autres plateformes",
|
||||
"setHelpDesktopApps": "Applis Desktop",
|
||||
"setHelpWebApp": "Appli web",
|
||||
"setHelpUpdates": "Mises à jour",
|
||||
"setHelpTwitter": "Appli twitter",
|
||||
"setHelpTwitter": "Twitter",
|
||||
"dropboxSetupDesc": "Une configuration particulière est nécessaire pour utiliser Dropbox dans une application auto-hébergée. Merci de créer votre propre application Dropbox et d'inscrire sa clé ci-dessous.",
|
||||
"dropboxAppKey": "Clé Dropbox",
|
||||
"dropboxAppKeyDesc": "Copier la clé de votre appli Dropbox (Réglages développeur)",
|
||||
@@ -613,9 +708,11 @@
|
||||
"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": "Ecraser le fichier kdbx avec PUT",
|
||||
"webdavSavePut": "Écraser le fichier kdbx avec PUT",
|
||||
"webdavNoLastModified": "L'entête HTTP \"Last-Modified\" est absent",
|
||||
"webdavStatReload": "Toujours recharger le fichier au lieu de se fier à l'entête HTTP \"Last-Modified\"",
|
||||
"launcherSave": "Sauvegarder base des mots de passe",
|
||||
@@ -640,5 +737,40 @@
|
||||
"yubiKeyTouchRequestedBody": "Merci de toucher votre YubiKey avec le numéro de série {}",
|
||||
"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 {}."
|
||||
"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."
|
||||
}
|
||||
+275
-41
@@ -4,19 +4,20 @@ import { SearchResultCollection } from 'collections/search-result-collection';
|
||||
import { FileCollection } from 'collections/file-collection';
|
||||
import { FileInfoCollection } from 'collections/file-info-collection';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { UsbListener } from 'comp/app/usb-listener';
|
||||
import { NativeModules } from 'comp/launcher/native-modules';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { EntryModel } from 'models/entry-model';
|
||||
import { FileInfoModel } from 'models/file-info-model';
|
||||
import { FileModel } from 'models/file-model';
|
||||
import { GroupModel } from 'models/group-model';
|
||||
import { YubiKeyOtpModel } from 'models/external/yubikey-otp-model';
|
||||
import { YubiKeyOtpModel } from 'models/otp-device/yubikey-otp-model';
|
||||
import { MenuModel } from 'models/menu/menu-model';
|
||||
import { PluginManager } from 'plugins/plugin-manager';
|
||||
import { Features } from 'util/features';
|
||||
import { DateFormat } from 'comp/i18n/date-format';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { UrlFormat } from 'util/formatting/url-format';
|
||||
import { IdGenerator } from 'util/generators/id-generator';
|
||||
import { Locale } from 'util/locale';
|
||||
@@ -37,6 +38,10 @@ class AppModel {
|
||||
isBeta = RuntimeInfo.beta;
|
||||
advancedSearch = null;
|
||||
attachedYubiKeysCount = 0;
|
||||
memoryPasswordStorage = {};
|
||||
fileUnlockPromise = null;
|
||||
hardwareDecryptInProgress = false;
|
||||
mainWindowBlurTimer = null;
|
||||
|
||||
constructor() {
|
||||
Events.on('refresh', this.refresh.bind(this));
|
||||
@@ -47,6 +52,11 @@ class AppModel {
|
||||
Events.on('select-entry', this.selectEntry.bind(this));
|
||||
Events.on('unset-keyfile', this.unsetKeyFile.bind(this));
|
||||
Events.on('usb-devices-changed', this.usbDevicesChanged.bind(this));
|
||||
Events.on('main-window-blur', this.mainWindowBlur.bind(this));
|
||||
Events.on('main-window-focus', this.mainWindowFocus.bind(this));
|
||||
Events.on('main-window-will-close', this.mainWindowWillClose.bind(this));
|
||||
Events.on('hardware-decrypt-started', this.hardwareDecryptStarted.bind(this));
|
||||
Events.on('hardware-decrypt-finished', this.hardwareDecryptFinished.bind(this));
|
||||
|
||||
this.appLogger = new Logger('app');
|
||||
AppModel.instance = this;
|
||||
@@ -166,10 +176,29 @@ class AppModel {
|
||||
page: 'file',
|
||||
file
|
||||
});
|
||||
|
||||
this.refresh();
|
||||
|
||||
file.on('reload', this.reloadFile.bind(this));
|
||||
file.on('change', () => Events.emit('file-changed', file));
|
||||
file.on('change', () => {
|
||||
Events.emit('file-changed', file);
|
||||
});
|
||||
file.on('ejected', () => this.closeFile(file));
|
||||
file.on('change:dirty', (file, dirty) => {
|
||||
if (dirty && this.settings.autoSaveInterval === -1) {
|
||||
this.syncFile(file);
|
||||
}
|
||||
});
|
||||
|
||||
Events.emit('file-opened');
|
||||
|
||||
if (this.fileUnlockPromise) {
|
||||
this.appLogger.info('Running pending file unlock operation');
|
||||
this.fileUnlockPromise.resolve(file);
|
||||
this.fileUnlockPromise = null;
|
||||
Events.emit('unlock-message-changed', null);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -231,6 +260,9 @@ class AppModel {
|
||||
}
|
||||
|
||||
closeAllFiles() {
|
||||
if (!this.files.hasOpenFiles()) {
|
||||
return;
|
||||
}
|
||||
for (const file of this.files) {
|
||||
file.close();
|
||||
this.fileClosed(file);
|
||||
@@ -243,6 +275,7 @@ class AppModel {
|
||||
this.tags.splice(0, this.tags.length);
|
||||
this.filter = {};
|
||||
this.menu.select({ item: this.menu.allItemsItem });
|
||||
Events.emit('all-files-closed');
|
||||
}
|
||||
|
||||
closeFile(file) {
|
||||
@@ -253,6 +286,7 @@ class AppModel {
|
||||
this.menu.groupsSection.removeByFile(file);
|
||||
this.menu.filesSection.removeByFile(file);
|
||||
this.menu.select({ item: this.menu.allItemsSection.items[0] });
|
||||
Events.emit('one-file-closed');
|
||||
}
|
||||
|
||||
emptyTrash() {
|
||||
@@ -294,7 +328,7 @@ class AppModel {
|
||||
}
|
||||
|
||||
getEntries() {
|
||||
const entries = this.getEntriesByFilter(this.filter);
|
||||
const entries = this.getEntriesByFilter(this.filter, this.files);
|
||||
entries.sortEntries(this.sort, this.filter);
|
||||
if (this.filter.trash) {
|
||||
this.addTrashGroups(entries);
|
||||
@@ -302,16 +336,16 @@ class AppModel {
|
||||
return entries;
|
||||
}
|
||||
|
||||
getEntriesByFilter(filter) {
|
||||
getEntriesByFilter(filter, files) {
|
||||
const preparedFilter = this.prepareFilter(filter);
|
||||
const entries = new SearchResultCollection();
|
||||
|
||||
const devicesToMatchOtpEntries = this.files.filter((file) => file.external);
|
||||
const devicesToMatchOtpEntries = files.filter((file) => file.backend === 'otp-device');
|
||||
|
||||
const matchedOtpEntrySet = this.settings.yubiKeyMatchEntries ? new Set() : undefined;
|
||||
|
||||
this.files
|
||||
.filter((file) => !file.external)
|
||||
files
|
||||
.filter((file) => file.backend !== 'otp-device')
|
||||
.forEach((file) => {
|
||||
file.forEachEntry(preparedFilter, (entry) => {
|
||||
if (matchedOtpEntrySet) {
|
||||
@@ -426,7 +460,7 @@ class AppModel {
|
||||
|
||||
createNewEntry(args) {
|
||||
const sel = this.getFirstSelectedGroupForCreation();
|
||||
if (args && args.template) {
|
||||
if (args?.template) {
|
||||
if (sel.file !== args.template.file) {
|
||||
sel.file = args.template.file;
|
||||
sel.group = args.template.file.groups[0];
|
||||
@@ -442,11 +476,21 @@ class AppModel {
|
||||
}
|
||||
}
|
||||
|
||||
createNewEntryWithFields(group, fields) {
|
||||
return EntryModel.newEntryWithFields(group, fields);
|
||||
}
|
||||
|
||||
createNewGroup() {
|
||||
const sel = this.getFirstSelectedGroupForCreation();
|
||||
return GroupModel.newGroup(sel.group, sel.file);
|
||||
}
|
||||
|
||||
createNewGroupWithName(group, file, name) {
|
||||
const newGroup = GroupModel.newGroup(group, file);
|
||||
newGroup.setName(name);
|
||||
return newGroup;
|
||||
}
|
||||
|
||||
createNewTemplateEntry() {
|
||||
const file = this.getFirstSelectedGroupForCreation().file;
|
||||
const group = file.getEntryTemplatesGroup() || file.createEntryTemplatesGroup();
|
||||
@@ -465,7 +509,7 @@ class AppModel {
|
||||
}
|
||||
}
|
||||
|
||||
createNewFile(name) {
|
||||
createNewFile(name, callback) {
|
||||
if (!name) {
|
||||
for (let i = 0; ; i++) {
|
||||
name = Locale.openNewFile + (i || '');
|
||||
@@ -475,9 +519,10 @@ class AppModel {
|
||||
}
|
||||
}
|
||||
const newFile = new FileModel({ id: IdGenerator.uuid() });
|
||||
newFile.create(name);
|
||||
this.addFile(newFile);
|
||||
return newFile;
|
||||
newFile.create(name, () => {
|
||||
this.addFile(newFile);
|
||||
callback?.(newFile);
|
||||
});
|
||||
}
|
||||
|
||||
openFile(params, callback) {
|
||||
@@ -525,7 +570,8 @@ class AppModel {
|
||||
fileInfo &&
|
||||
fileInfo.openDate &&
|
||||
fileInfo.rev === params.rev &&
|
||||
fileInfo.storage !== 'file'
|
||||
fileInfo.storage !== 'file' &&
|
||||
!this.settings.disableOfflineStorage
|
||||
) {
|
||||
logger.info('Open file from cache because it is latest');
|
||||
this.openFileFromCache(
|
||||
@@ -546,7 +592,12 @@ class AppModel {
|
||||
},
|
||||
fileInfo
|
||||
);
|
||||
} else if (!fileInfo || !fileInfo.openDate || params.storage === 'file') {
|
||||
} else if (
|
||||
!fileInfo ||
|
||||
!fileInfo.openDate ||
|
||||
params.storage === 'file' ||
|
||||
this.settings.disableOfflineStorage
|
||||
) {
|
||||
this.openFileFromStorage(params, callback, fileInfo, logger);
|
||||
} else {
|
||||
logger.info('Open file from cache, will sync after load', params.storage);
|
||||
@@ -594,7 +645,7 @@ class AppModel {
|
||||
logger.info('Load from storage');
|
||||
storage.load(params.path, params.opts, (err, data, stat) => {
|
||||
if (err) {
|
||||
if (fileInfo && fileInfo.openDate) {
|
||||
if (fileInfo && fileInfo.openDate && !this.settings.disableOfflineStorage) {
|
||||
logger.info('Open file from cache because of storage load error', err);
|
||||
this.openFileFromCache(params, callback, fileInfo);
|
||||
} else {
|
||||
@@ -618,7 +669,8 @@ class AppModel {
|
||||
!noCache &&
|
||||
fileInfo &&
|
||||
storage.name !== 'file' &&
|
||||
(err || (stat && stat.rev === cacheRev))
|
||||
(err || (stat && stat.rev === cacheRev)) &&
|
||||
!this.settings.disableOfflineStorage
|
||||
) {
|
||||
logger.info(
|
||||
'Open file from cache because ' + (err ? 'stat error' : 'it is latest'),
|
||||
@@ -645,7 +697,7 @@ class AppModel {
|
||||
let needLoadKeyFile = false;
|
||||
if (!params.keyFileData && fileInfo && fileInfo.keyFileName) {
|
||||
params.keyFileName = fileInfo.keyFileName;
|
||||
if (this.settings.rememberKeyFiles === 'data') {
|
||||
if (this.settings.rememberKeyFiles === 'data' && fileInfo.keyFileHash) {
|
||||
params.keyFileData = FileModel.createKeyFileWithHash(fileInfo.keyFileHash);
|
||||
} else if (this.settings.rememberKeyFiles === 'path' && fileInfo.keyFilePath) {
|
||||
params.keyFilePath = fileInfo.keyFilePath;
|
||||
@@ -663,10 +715,13 @@ class AppModel {
|
||||
path: params.path,
|
||||
keyFileName: params.keyFileName,
|
||||
keyFilePath: params.keyFilePath,
|
||||
backup: (fileInfo && fileInfo.backup) || null,
|
||||
fingerprint: (fileInfo && fileInfo.fingerprint) || null,
|
||||
backup: fileInfo?.backup || null,
|
||||
chalResp: params.chalResp
|
||||
});
|
||||
if (params.encryptedPassword) {
|
||||
file.encryptedPassword = fileInfo.encryptedPassword;
|
||||
file.encryptedPasswordDate = fileInfo?.encryptedPasswordDate || new Date();
|
||||
}
|
||||
const openComplete = (err) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
@@ -685,7 +740,7 @@ class AppModel {
|
||||
if (fileInfo) {
|
||||
file.syncDate = fileInfo.syncDate;
|
||||
}
|
||||
if (updateCacheOnSuccess) {
|
||||
if (updateCacheOnSuccess && !this.settings.disableOfflineStorage) {
|
||||
logger.info('Save loaded file to cache');
|
||||
Storage.cache.save(file.id, null, params.fileData);
|
||||
}
|
||||
@@ -755,7 +810,6 @@ class AppModel {
|
||||
syncDate: file.syncDate || dt,
|
||||
openDate: dt,
|
||||
backup: file.backup,
|
||||
fingerprint: file.fingerprint,
|
||||
chalResp: file.chalResp
|
||||
});
|
||||
switch (this.settings.rememberKeyFiles) {
|
||||
@@ -771,6 +825,14 @@ class AppModel {
|
||||
keyFilePath: file.keyFilePath || null
|
||||
});
|
||||
}
|
||||
if (this.settings.deviceOwnerAuth === 'file' && file.encryptedPassword) {
|
||||
const maxDate = new Date(file.encryptedPasswordDate);
|
||||
maxDate.setMinutes(maxDate.getMinutes() + this.settings.deviceOwnerAuthTimeoutMinutes);
|
||||
if (maxDate > new Date()) {
|
||||
fileInfo.encryptedPassword = file.encryptedPassword;
|
||||
fileInfo.encryptedPasswordDate = file.encryptedPasswordDate;
|
||||
}
|
||||
}
|
||||
this.fileInfos.remove(file.id);
|
||||
this.fileInfos.unshift(fileInfo);
|
||||
this.fileInfos.save();
|
||||
@@ -808,14 +870,17 @@ class AppModel {
|
||||
if (data && backup && backup.enabled && backup.pending) {
|
||||
this.scheduleBackupFile(file, data);
|
||||
}
|
||||
if (params) {
|
||||
this.saveFileFingerprint(file, params.password);
|
||||
}
|
||||
if (this.settings.yubiKeyAutoOpen) {
|
||||
if (this.attachedYubiKeysCount > 0 && !this.files.some((f) => f.external)) {
|
||||
if (
|
||||
this.attachedYubiKeysCount > 0 &&
|
||||
!this.files.some((f) => f.backend === 'otp-device')
|
||||
) {
|
||||
this.tryOpenOtpDeviceInBackground();
|
||||
}
|
||||
}
|
||||
if (this.settings.deviceOwnerAuth) {
|
||||
this.saveEncryptedPassword(file, params);
|
||||
}
|
||||
}
|
||||
|
||||
fileClosed(file) {
|
||||
@@ -858,7 +923,7 @@ class AppModel {
|
||||
path = Storage[storage].getPathForName(file.name);
|
||||
}
|
||||
const optionsForLogging = { ...options };
|
||||
if (optionsForLogging && optionsForLogging.opts && optionsForLogging.opts.password) {
|
||||
if (optionsForLogging.opts && optionsForLogging.opts.password) {
|
||||
optionsForLogging.opts = { ...optionsForLogging.opts };
|
||||
optionsForLogging.opts.password = '***';
|
||||
}
|
||||
@@ -965,6 +1030,10 @@ class AppModel {
|
||||
logger.info('Updated sync date, saving modified file');
|
||||
saveToCacheAndStorage();
|
||||
} else if (file.dirty) {
|
||||
if (this.settings.disableOfflineStorage) {
|
||||
logger.info('File is dirty and cache is disabled');
|
||||
return complete(err);
|
||||
}
|
||||
logger.info('Saving not modified dirty file to cache');
|
||||
Storage.cache.save(fileInfo.id, null, data, (err) => {
|
||||
if (err) {
|
||||
@@ -1027,6 +1096,9 @@ class AppModel {
|
||||
} else if (!file.dirty) {
|
||||
logger.info('Saving to storage, skip cache because not dirty');
|
||||
saveToStorage(data);
|
||||
} else if (this.settings.disableOfflineStorage) {
|
||||
logger.info('Saving to storage because cache is disabled');
|
||||
saveToStorage(data);
|
||||
} else {
|
||||
logger.info('Saving to cache');
|
||||
Storage.cache.save(fileInfo.id, null, data, (err) => {
|
||||
@@ -1050,6 +1122,10 @@ class AppModel {
|
||||
logger.info('File does not exist in storage, creating');
|
||||
saveToCacheAndStorage();
|
||||
} else if (file.dirty) {
|
||||
if (this.settings.disableOfflineStorage) {
|
||||
logger.info('Stat error, dirty, cache is disabled', err || 'no error');
|
||||
return complete(err);
|
||||
}
|
||||
logger.info('Stat error, dirty, save to cache', err || 'no error');
|
||||
file.getData((data, e) => {
|
||||
if (e) {
|
||||
@@ -1087,6 +1163,14 @@ class AppModel {
|
||||
}
|
||||
}
|
||||
|
||||
deleteAllCachedFiles() {
|
||||
for (const fileInfo of this.fileInfos) {
|
||||
if (fileInfo.storage && !fileInfo.modified) {
|
||||
Storage.cache.remove(fileInfo.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearStoredKeyFiles() {
|
||||
for (const fileInfo of this.fileInfos) {
|
||||
fileInfo.set({
|
||||
@@ -1224,18 +1308,6 @@ class AppModel {
|
||||
}
|
||||
}
|
||||
|
||||
saveFileFingerprint(file, password) {
|
||||
if (Launcher && Launcher.fingerprints && !file.fingerprint) {
|
||||
const fileInfo = this.fileInfos.get(file.id);
|
||||
Launcher.fingerprints.register(file.id, password, (token) => {
|
||||
if (token) {
|
||||
fileInfo.fingerprint = token;
|
||||
this.fileInfos.save();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
usbDevicesChanged() {
|
||||
const attachedYubiKeysCount = this.attachedYubiKeysCount;
|
||||
|
||||
@@ -1246,7 +1318,9 @@ class AppModel {
|
||||
}
|
||||
|
||||
const isNewYubiKey = UsbListener.attachedYubiKeys > attachedYubiKeysCount;
|
||||
const hasOpenFiles = this.files.some((file) => file.active && !file.external);
|
||||
const hasOpenFiles = this.files.some(
|
||||
(file) => file.active && file.backend !== 'otp-device'
|
||||
);
|
||||
|
||||
if (isNewYubiKey && hasOpenFiles && !this.openingOtpDevice) {
|
||||
this.tryOpenOtpDeviceInBackground();
|
||||
@@ -1278,7 +1352,7 @@ class AppModel {
|
||||
return null;
|
||||
}
|
||||
for (const file of this.files) {
|
||||
if (file.external) {
|
||||
if (file.backend === 'otp-device') {
|
||||
const matchingEntry = file.getMatchingEntry(entry);
|
||||
if (matchingEntry) {
|
||||
return matchingEntry;
|
||||
@@ -1286,6 +1360,166 @@ class AppModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveEncryptedPassword(file, params) {
|
||||
if (!this.settings.deviceOwnerAuth || params.encryptedPassword) {
|
||||
return;
|
||||
}
|
||||
NativeModules.hardwareEncrypt(params.password)
|
||||
.then((encryptedPassword) => {
|
||||
encryptedPassword = encryptedPassword.toBase64();
|
||||
const fileInfo = this.fileInfos.get(file.id);
|
||||
const encryptedPasswordDate = new Date();
|
||||
file.encryptedPassword = encryptedPassword;
|
||||
file.encryptedPasswordDate = encryptedPasswordDate;
|
||||
if (this.settings.deviceOwnerAuth === 'file') {
|
||||
fileInfo.encryptedPassword = encryptedPassword;
|
||||
fileInfo.encryptedPasswordDate = encryptedPasswordDate;
|
||||
this.fileInfos.save();
|
||||
} else if (this.settings.deviceOwnerAuth === 'memory') {
|
||||
this.memoryPasswordStorage[file.id] = {
|
||||
value: encryptedPassword,
|
||||
date: encryptedPasswordDate
|
||||
};
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
file.encryptedPassword = null;
|
||||
file.encryptedPasswordDate = null;
|
||||
delete this.memoryPasswordStorage[file.id];
|
||||
this.appLogger.error('Error encrypting password', e);
|
||||
});
|
||||
}
|
||||
|
||||
getMemoryPassword(fileId) {
|
||||
return this.memoryPasswordStorage[fileId];
|
||||
}
|
||||
|
||||
checkEncryptedPasswordsStorage() {
|
||||
if (this.settings.deviceOwnerAuth === 'file') {
|
||||
let changed = false;
|
||||
for (const fileInfo of this.fileInfos) {
|
||||
if (this.memoryPasswordStorage[fileInfo.id]) {
|
||||
fileInfo.encryptedPassword = this.memoryPasswordStorage[fileInfo.id].value;
|
||||
fileInfo.encryptedPasswordDate = this.memoryPasswordStorage[fileInfo.id].date;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
this.fileInfos.save();
|
||||
}
|
||||
for (const file of this.files) {
|
||||
if (this.memoryPasswordStorage[file.id]) {
|
||||
file.encryptedPassword = this.memoryPasswordStorage[file.id].value;
|
||||
file.encryptedPasswordDate = this.memoryPasswordStorage[file.id].date;
|
||||
}
|
||||
}
|
||||
} else if (this.settings.deviceOwnerAuth === 'memory') {
|
||||
let changed = false;
|
||||
for (const fileInfo of this.fileInfos) {
|
||||
if (fileInfo.encryptedPassword) {
|
||||
this.memoryPasswordStorage[fileInfo.id] = {
|
||||
value: fileInfo.encryptedPassword,
|
||||
date: fileInfo.encryptedPasswordDate
|
||||
};
|
||||
fileInfo.encryptedPassword = null;
|
||||
fileInfo.encryptedPasswordDate = null;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
this.fileInfos.save();
|
||||
}
|
||||
} else {
|
||||
let changed = false;
|
||||
for (const fileInfo of this.fileInfos) {
|
||||
if (fileInfo.encryptedPassword) {
|
||||
fileInfo.encryptedPassword = null;
|
||||
fileInfo.encryptedPasswordDate = null;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
this.fileInfos.save();
|
||||
}
|
||||
for (const file of this.files) {
|
||||
if (file.encryptedPassword) {
|
||||
file.encryptedPassword = null;
|
||||
file.encryptedPasswordDate = null;
|
||||
}
|
||||
}
|
||||
this.memoryPasswordStorage = {};
|
||||
}
|
||||
}
|
||||
|
||||
unlockAnyFile(unlockRes, timeout) {
|
||||
this.rejectPendingFileUnlockPromise('Replaced with a new operation');
|
||||
Events.emit('show-open-view');
|
||||
return new Promise((resolve, reject) => {
|
||||
this.fileUnlockPromise = { resolve, reject, unlockRes };
|
||||
if (timeout) {
|
||||
const timer = setTimeout(
|
||||
() => this.rejectPendingFileUnlockPromise('Timeout'),
|
||||
timeout
|
||||
);
|
||||
this.fileUnlockPromise.resolve = (res) => {
|
||||
clearTimeout(timer);
|
||||
resolve(res);
|
||||
};
|
||||
this.fileUnlockPromise.reject = (err) => {
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
};
|
||||
}
|
||||
this.appLogger.info('Pending file unlock operation is set');
|
||||
Events.emit('unlock-message-changed', unlockRes);
|
||||
});
|
||||
}
|
||||
|
||||
get unlockMessageRes() {
|
||||
return this.fileUnlockPromise?.unlockRes;
|
||||
}
|
||||
|
||||
rejectPendingFileUnlockPromise(reason) {
|
||||
if (this.fileUnlockPromise) {
|
||||
this.appLogger.info('Cancel pending file unlock operation', reason);
|
||||
this.fileUnlockPromise.reject(new Error(reason));
|
||||
this.fileUnlockPromise = null;
|
||||
Events.emit('unlock-message-changed', null);
|
||||
}
|
||||
}
|
||||
|
||||
mainWindowBlur() {
|
||||
if (!this.hardwareDecryptInProgress) {
|
||||
this.mainWindowBlurTimer = setTimeout(() => {
|
||||
// macOS emits focus-blur-focus event in a row when triggering auto-type from minimized state
|
||||
delete this.mainWindowBlurTimer;
|
||||
this.rejectPendingFileUnlockPromise('Main window blur');
|
||||
}, Timeouts.AutoTypeWindowFocusAfterBlur);
|
||||
}
|
||||
}
|
||||
|
||||
mainWindowFocus() {
|
||||
if (this.mainWindowBlurTimer) {
|
||||
clearTimeout(this.mainWindowBlurTimer);
|
||||
this.mainWindowBlurTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
mainWindowWillClose() {
|
||||
this.rejectPendingFileUnlockPromise('Main window will close');
|
||||
}
|
||||
|
||||
hardwareDecryptStarted() {
|
||||
this.hardwareDecryptInProgress = true;
|
||||
}
|
||||
|
||||
hardwareDecryptFinished() {
|
||||
this.hardwareDecryptInProgress = false;
|
||||
if (!Launcher.isAppFocused()) {
|
||||
this.rejectPendingFileUnlockPromise('App is not focused after hardware decrypt');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { AppModel };
|
||||
|
||||
@@ -3,10 +3,10 @@ import { Model } from 'framework/model';
|
||||
class AttachmentModel extends Model {
|
||||
getBinary() {
|
||||
let data = this.data;
|
||||
if (data && data.ref) {
|
||||
if (data?.value) {
|
||||
data = data.value;
|
||||
}
|
||||
if (data && data.getBinary) {
|
||||
if (data?.getBinary) {
|
||||
data = data.getBinary();
|
||||
}
|
||||
if (data instanceof ArrayBuffer && data.byteLength) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Model } from 'framework/model';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { KdbxToHtml } from 'comp/format/kdbx-to-html';
|
||||
@@ -16,6 +16,7 @@ const UrlRegex = /^https?:\/\//i;
|
||||
const FieldRefRegex = /^\{REF:([TNPAU])@I:(\w{32})}$/;
|
||||
const FieldRefFields = ['title', 'password', 'user', 'url', 'notes'];
|
||||
const FieldRefIds = { T: 'Title', U: 'UserName', P: 'Password', A: 'URL', N: 'Notes' };
|
||||
const ExtraUrlFieldName = 'KP2A_URL';
|
||||
|
||||
class EntryModel extends Model {
|
||||
constructor(props) {
|
||||
@@ -51,7 +52,7 @@ class EntryModel extends Model {
|
||||
this.icon = this._iconFromId(entry.icon);
|
||||
this.tags = entry.tags;
|
||||
this.color = this._colorToModel(entry.bgColor) || this._colorToModel(entry.fgColor);
|
||||
this.fields = this._fieldsToModel(entry.fields);
|
||||
this.fields = this._fieldsToModel();
|
||||
this.attachments = this._attachmentsToModel(entry.binaries);
|
||||
this.created = entry.times.creationTime;
|
||||
this.updated = entry.times.lastModTime;
|
||||
@@ -70,7 +71,7 @@ class EntryModel extends Model {
|
||||
}
|
||||
|
||||
_getPassword() {
|
||||
const password = this.entry.fields.Password || kdbxweb.ProtectedValue.fromString('');
|
||||
const password = this.entry.fields.get('Password') || kdbxweb.ProtectedValue.fromString('');
|
||||
if (!password.isProtected) {
|
||||
return kdbxweb.ProtectedValue.fromString(password);
|
||||
}
|
||||
@@ -78,7 +79,7 @@ class EntryModel extends Model {
|
||||
}
|
||||
|
||||
_getFieldString(field) {
|
||||
const val = this.entry.fields[field];
|
||||
const val = this.entry.fields.get(field);
|
||||
if (!val) {
|
||||
return '';
|
||||
}
|
||||
@@ -102,7 +103,7 @@ class EntryModel extends Model {
|
||||
|
||||
_buildSearchText() {
|
||||
let text = '';
|
||||
for (const value of Object.values(this.entry.fields)) {
|
||||
for (const value of this.entry.fields.values()) {
|
||||
if (typeof value === 'string') {
|
||||
text += value.toLowerCase() + '\n';
|
||||
}
|
||||
@@ -121,7 +122,7 @@ class EntryModel extends Model {
|
||||
this.customIconId = null;
|
||||
if (this.entry.customIcon) {
|
||||
this.customIcon = IconUrlFormat.toDataUrl(
|
||||
this.file.db.meta.customIcons[this.entry.customIcon]
|
||||
this.file.db.meta.customIcons.get(this.entry.customIcon.id)?.data
|
||||
);
|
||||
this.customIconId = this.entry.customIcon.toString();
|
||||
}
|
||||
@@ -163,13 +164,13 @@ class EntryModel extends Model {
|
||||
return color ? Color.getNearest(color) : null;
|
||||
}
|
||||
|
||||
_fieldsToModel(fields) {
|
||||
return omit(fields, BuiltInFields);
|
||||
_fieldsToModel() {
|
||||
return omit(this.getAllFields(), BuiltInFields);
|
||||
}
|
||||
|
||||
_attachmentsToModel(binaries) {
|
||||
const att = [];
|
||||
for (let [title, data] of Object.entries(binaries)) {
|
||||
for (let [title, data] of binaries) {
|
||||
if (data && data.ref) {
|
||||
data = data.value;
|
||||
}
|
||||
@@ -190,6 +191,7 @@ class EntryModel extends Model {
|
||||
}
|
||||
if (this.isJustCreated) {
|
||||
this.isJustCreated = false;
|
||||
this.file.reload();
|
||||
}
|
||||
this.entry.times.update();
|
||||
}
|
||||
@@ -208,7 +210,11 @@ class EntryModel extends Model {
|
||||
}
|
||||
|
||||
getAllFields() {
|
||||
return this.entry.fields;
|
||||
const fields = {};
|
||||
for (const [key, value] of this.entry.fields) {
|
||||
fields[key] = value;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
getHistoryEntriesForSearch() {
|
||||
@@ -230,7 +236,7 @@ class EntryModel extends Model {
|
||||
getFieldValue(field) {
|
||||
field = field.toLowerCase();
|
||||
let resolvedField;
|
||||
Object.keys(this.entry.fields).some((entryField) => {
|
||||
[...this.entry.fields.keys()].some((entryField) => {
|
||||
if (entryField.toLowerCase() === field) {
|
||||
resolvedField = entryField;
|
||||
return true;
|
||||
@@ -238,7 +244,7 @@ class EntryModel extends Model {
|
||||
return false;
|
||||
});
|
||||
if (resolvedField) {
|
||||
let fieldValue = this.entry.fields[resolvedField];
|
||||
let fieldValue = this.entry.fields.get(resolvedField);
|
||||
const refValue = this._resolveFieldReference(fieldValue);
|
||||
if (refValue !== undefined) {
|
||||
fieldValue = refValue;
|
||||
@@ -274,7 +280,7 @@ class EntryModel extends Model {
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
return entry.entry.fields[FieldRefIds[fieldRefId]];
|
||||
return entry.entry.fields.get(FieldRefIds[fieldRefId]);
|
||||
}
|
||||
|
||||
setColor(color) {
|
||||
@@ -327,10 +333,10 @@ class EntryModel extends Model {
|
||||
if (hasValue || allowEmpty || BuiltInFields.indexOf(field) >= 0) {
|
||||
this._entryModified();
|
||||
val = this.sanitizeFieldValue(val);
|
||||
this.entry.fields[field] = val;
|
||||
} else if (Object.prototype.hasOwnProperty.call(this.entry.fields, field)) {
|
||||
this.entry.fields.set(field, val);
|
||||
} else if (this.entry.fields.has(field)) {
|
||||
this._entryModified();
|
||||
delete this.entry.fields[field];
|
||||
this.entry.fields.delete(field);
|
||||
}
|
||||
this._fillByEntry();
|
||||
}
|
||||
@@ -339,26 +345,26 @@ class EntryModel extends Model {
|
||||
if (val && !val.isProtected) {
|
||||
// https://github.com/keeweb/keeweb/issues/910
|
||||
// eslint-disable-next-line no-control-regex
|
||||
val = val.replace(/[\x00-\x09\x0B-\x0C\x0E-\x1F\uFFF0-\uFFFF]/g, '');
|
||||
val = val.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\uFFF0-\uFFFF]/g, '');
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
hasField(field) {
|
||||
return Object.prototype.hasOwnProperty.call(this.entry.fields, field);
|
||||
return this.entry.fields.has(field);
|
||||
}
|
||||
|
||||
addAttachment(name, data) {
|
||||
this._entryModified();
|
||||
return this.file.db.createBinary(data).then((binaryRef) => {
|
||||
this.entry.binaries[name] = binaryRef;
|
||||
this.entry.binaries.set(name, binaryRef);
|
||||
this._fillByEntry();
|
||||
});
|
||||
}
|
||||
|
||||
removeAttachment(name) {
|
||||
this._entryModified();
|
||||
delete this.entry.binaries[name];
|
||||
this.entry.binaries.delete(name);
|
||||
this._fillByEntry();
|
||||
}
|
||||
|
||||
@@ -388,8 +394,8 @@ class EntryModel extends Model {
|
||||
this.entry.pushHistory();
|
||||
this.unsaved = true;
|
||||
this.file.setModified();
|
||||
this.entry.fields = {};
|
||||
this.entry.binaries = {};
|
||||
this.entry.fields = new Map();
|
||||
this.entry.binaries = new Map();
|
||||
this.entry.copyFrom(historyEntry);
|
||||
this._entryModified();
|
||||
this._fillByEntry();
|
||||
@@ -400,8 +406,8 @@ class EntryModel extends Model {
|
||||
this.unsaved = false;
|
||||
const historyEntry = this.entry.history[this.entry.history.length - 1];
|
||||
this.entry.removeHistory(this.entry.history.length - 1);
|
||||
this.entry.fields = {};
|
||||
this.entry.binaries = {};
|
||||
this.entry.fields = new Map();
|
||||
this.entry.binaries = new Map();
|
||||
this.entry.copyFrom(historyEntry);
|
||||
this._fillByEntry();
|
||||
}
|
||||
@@ -474,14 +480,14 @@ class EntryModel extends Model {
|
||||
otpUrl = Otp.makeUrl(args.key, args.step, args.size);
|
||||
}
|
||||
}
|
||||
} else if (this.entry.fields['TOTP Seed']) {
|
||||
} else if (this.entry.fields.get('TOTP Seed')) {
|
||||
// TrayTOTP plugin format
|
||||
let secret = this.entry.fields['TOTP Seed'];
|
||||
let secret = this.entry.fields.get('TOTP Seed');
|
||||
if (secret.isProtected) {
|
||||
secret = secret.getText();
|
||||
}
|
||||
if (secret) {
|
||||
let settings = this.entry.fields['TOTP Settings'];
|
||||
let settings = this.entry.fields.get('TOTP Settings');
|
||||
if (settings && settings.isProtected) {
|
||||
settings = settings.getText();
|
||||
}
|
||||
@@ -505,7 +511,7 @@ class EntryModel extends Model {
|
||||
}
|
||||
try {
|
||||
this.otpGenerator = Otp.parseUrl(otpUrl);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
this.otpGenerator = null;
|
||||
}
|
||||
} else {
|
||||
@@ -520,8 +526,8 @@ class EntryModel extends Model {
|
||||
|
||||
setOtpUrl(url) {
|
||||
this.setField('otp', url ? kdbxweb.ProtectedValue.fromString(url) : undefined);
|
||||
delete this.entry.fields['TOTP Seed'];
|
||||
delete this.entry.fields['TOTP Settings'];
|
||||
this.entry.fields.delete('TOTP Seed');
|
||||
this.entry.fields.delete('TOTP Settings');
|
||||
}
|
||||
|
||||
getEffectiveEnableAutoType() {
|
||||
@@ -572,7 +578,7 @@ class EntryModel extends Model {
|
||||
newEntry.entry.uuid = uuid;
|
||||
newEntry.entry.times.update();
|
||||
newEntry.entry.times.creationTime = newEntry.entry.times.lastModTime;
|
||||
newEntry.entry.fields.Title = this.title + nameSuffix;
|
||||
newEntry.entry.fields.set('Title', this.title + nameSuffix);
|
||||
newEntry._fillByEntry();
|
||||
this.file.reload();
|
||||
return newEntry;
|
||||
@@ -584,7 +590,7 @@ class EntryModel extends Model {
|
||||
this.entry.uuid = uuid;
|
||||
this.entry.times.update();
|
||||
this.entry.times.creationTime = this.entry.times.lastModTime;
|
||||
this.entry.fields.Title = '';
|
||||
this.entry.fields.set('Title', '');
|
||||
this._fillByEntry();
|
||||
}
|
||||
|
||||
@@ -610,7 +616,7 @@ class EntryModel extends Model {
|
||||
const allFields = Object.keys(fieldWeights).concat(Object.keys(this.fields));
|
||||
|
||||
return allFields.reduce((rank, fieldName) => {
|
||||
const val = this.entry.fields[fieldName];
|
||||
const val = this.entry.fields.get(fieldName);
|
||||
if (!val) {
|
||||
return rank;
|
||||
}
|
||||
@@ -627,6 +633,39 @@ class EntryModel extends Model {
|
||||
return KdbxToHtml.entryToHtml(this.file.db, this.entry);
|
||||
}
|
||||
|
||||
canCheckPasswordIssues() {
|
||||
return !this.entry.customData?.has('IgnorePwIssues');
|
||||
}
|
||||
|
||||
setIgnorePasswordIssues() {
|
||||
if (!this.entry.customData) {
|
||||
this.entry.customData = new Map();
|
||||
}
|
||||
this.entry.customData.set('IgnorePwIssues', '1');
|
||||
this._entryModified();
|
||||
}
|
||||
|
||||
getNextUrlFieldName() {
|
||||
const takenFields = new Set(
|
||||
[...this.entry.fields.keys()].filter((f) => f.startsWith(ExtraUrlFieldName))
|
||||
);
|
||||
for (let i = 0; ; i++) {
|
||||
const fieldName = i ? `${ExtraUrlFieldName}_${i}` : ExtraUrlFieldName;
|
||||
if (!takenFields.has(fieldName)) {
|
||||
return fieldName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAllUrls() {
|
||||
const urls = this.url ? [this.url] : [];
|
||||
const extraUrls = Object.entries(this.fields)
|
||||
.filter(([field]) => field.startsWith(ExtraUrlFieldName))
|
||||
.map(([, value]) => (value.isProtected ? value.getText() : value))
|
||||
.filter((value) => value);
|
||||
return urls.concat(extraUrls);
|
||||
}
|
||||
|
||||
static fromEntry(entry, group, file) {
|
||||
const model = new EntryModel();
|
||||
model.setEntry(entry, group, file);
|
||||
@@ -651,8 +690,16 @@ class EntryModel extends Model {
|
||||
file.setModified();
|
||||
return model;
|
||||
}
|
||||
|
||||
static newEntryWithFields(group, fields) {
|
||||
const entry = EntryModel.newEntry(group, group.file);
|
||||
for (const [field, value] of Object.entries(fields)) {
|
||||
entry.setField(field, value);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
EntryModel.defineModelProperties({}, { extensions: true });
|
||||
|
||||
export { EntryModel };
|
||||
export { EntryModel, ExtraUrlFieldName };
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { ExternalDeviceModel } from 'models/external/external-device-model';
|
||||
|
||||
class ExternalOtpDeviceModel extends ExternalDeviceModel {
|
||||
open(callback) {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
|
||||
cancelOpen() {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
|
||||
close(callback) {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
|
||||
getOtp(callback) {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
}
|
||||
|
||||
export { ExternalOtpDeviceModel };
|
||||
@@ -16,8 +16,10 @@ const DefaultProperties = {
|
||||
keyFilePath: null,
|
||||
opts: null,
|
||||
backup: null,
|
||||
fingerprint: null,
|
||||
chalResp: null
|
||||
fingerprint: null, // obsolete
|
||||
chalResp: null,
|
||||
encryptedPassword: null,
|
||||
encryptedPasswordDate: null
|
||||
};
|
||||
|
||||
class FileInfoModel extends Model {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import demoFileData from 'demo.kdbx';
|
||||
import { Model } from 'framework/model';
|
||||
import { Events } from 'framework/events';
|
||||
@@ -8,7 +8,8 @@ import { GroupModel } from 'models/group-model';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { IconUrlFormat } from 'util/formatting/icon-url-format';
|
||||
import { Logger } from 'util/logger';
|
||||
import { mapObject } from 'util/fn';
|
||||
import { Locale } from 'util/locale';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
import { ChalRespCalculator } from 'comp/app/chal-resp-calculator';
|
||||
|
||||
const logger = new Logger('file');
|
||||
@@ -31,19 +32,20 @@ class FileModel extends Model {
|
||||
kdbxweb.Kdbx.load(fileData, credentials)
|
||||
.then((db) => {
|
||||
this.db = db;
|
||||
})
|
||||
.then(() => {
|
||||
this.readModel();
|
||||
this.setOpenFile({ passwordLength: password ? password.textLength : 0 });
|
||||
if (keyFileData) {
|
||||
kdbxweb.ByteUtils.zeroBuffer(keyFileData);
|
||||
}
|
||||
this.fixVersion();
|
||||
logger.info(
|
||||
'Opened file ' +
|
||||
this.name +
|
||||
': ' +
|
||||
logger.ts(ts) +
|
||||
', ' +
|
||||
this.kdfArgsToString(db.header) +
|
||||
this.kdfArgsToString(this.db.header) +
|
||||
', ' +
|
||||
Math.round(fileData.byteLength / 1024) +
|
||||
' kB'
|
||||
@@ -90,13 +92,14 @@ class FileModel extends Model {
|
||||
}
|
||||
}
|
||||
|
||||
create(name) {
|
||||
create(name, callback) {
|
||||
const password = kdbxweb.ProtectedValue.fromString('');
|
||||
const credentials = new kdbxweb.Credentials(password);
|
||||
this.db = kdbxweb.Kdbx.create(credentials, name);
|
||||
this.name = name;
|
||||
this.readModel();
|
||||
this.set({ active: true, created: true, name });
|
||||
callback();
|
||||
}
|
||||
|
||||
importWithXml(fileXml, callback) {
|
||||
@@ -107,6 +110,8 @@ class FileModel extends Model {
|
||||
kdbxweb.Kdbx.loadXml(fileXml, credentials)
|
||||
.then((db) => {
|
||||
this.db = db;
|
||||
})
|
||||
.then(() => {
|
||||
this.readModel();
|
||||
this.set({ active: true, created: true });
|
||||
logger.info('Imported file ' + this.name + ': ' + logger.ts(ts));
|
||||
@@ -128,13 +133,16 @@ class FileModel extends Model {
|
||||
const demoFile = kdbxweb.ByteUtils.arrayToBuffer(
|
||||
kdbxweb.ByteUtils.base64ToBytes(demoFileData)
|
||||
);
|
||||
kdbxweb.Kdbx.load(demoFile, credentials).then((db) => {
|
||||
this.db = db;
|
||||
this.name = 'Demo';
|
||||
this.readModel();
|
||||
this.setOpenFile({ passwordLength: 4, demo: true });
|
||||
callback();
|
||||
});
|
||||
kdbxweb.Kdbx.load(demoFile, credentials)
|
||||
.then((db) => {
|
||||
this.db = db;
|
||||
})
|
||||
.then(() => {
|
||||
this.name = 'Demo';
|
||||
this.readModel();
|
||||
this.setOpenFile({ passwordLength: 4, demo: true });
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
setOpenFile(props) {
|
||||
@@ -257,17 +265,6 @@ class FileModel extends Model {
|
||||
});
|
||||
}
|
||||
|
||||
fixVersion() {
|
||||
if (
|
||||
this.db.meta.generator === 'KdbxWeb' &&
|
||||
this.db.header.versionMajor === 4 &&
|
||||
this.db.header.versionMinor === 1
|
||||
) {
|
||||
this.db.header.versionMinor = 0;
|
||||
logger.info('Fixed file version: 4.1 => 4.0');
|
||||
}
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.buildObjectMap();
|
||||
this.readModel();
|
||||
@@ -407,7 +404,7 @@ class FileModel extends Model {
|
||||
createEntryTemplatesGroup() {
|
||||
const rootGroup = this.groups[0];
|
||||
const templatesGroup = GroupModel.newGroup(rootGroup, this);
|
||||
templatesGroup.setName('Templates');
|
||||
templatesGroup.setName(StringFormat.capFirst(Locale.templates));
|
||||
this.db.meta.entryTemplatesGroup = templatesGroup.group.uuid;
|
||||
this.reload();
|
||||
return templatesGroup;
|
||||
@@ -416,7 +413,6 @@ class FileModel extends Model {
|
||||
setModified() {
|
||||
if (!this.demo) {
|
||||
this.set({ modified: true, dirty: true });
|
||||
Events.emit('file-modified');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,9 +483,11 @@ class FileModel extends Model {
|
||||
syncError: error
|
||||
});
|
||||
|
||||
const shouldResetFingerprint = this.passwordChanged && this.fingerprint;
|
||||
if (shouldResetFingerprint && !error) {
|
||||
this.fingerprint = null;
|
||||
if (!error && this.passwordChanged && this.encryptedPassword) {
|
||||
this.set({
|
||||
encryptedPassword: null,
|
||||
encryptedPasswordDate: null
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.open) {
|
||||
@@ -670,16 +668,19 @@ class FileModel extends Model {
|
||||
}
|
||||
|
||||
getCustomIcons() {
|
||||
return mapObject(this.db.meta.customIcons, (customIcon) =>
|
||||
IconUrlFormat.toDataUrl(customIcon)
|
||||
);
|
||||
const customIcons = {};
|
||||
for (const [id, icon] of this.db.meta.customIcons) {
|
||||
customIcons[id] = IconUrlFormat.toDataUrl(icon.data);
|
||||
}
|
||||
return customIcons;
|
||||
}
|
||||
|
||||
addCustomIcon(iconData) {
|
||||
const uuid = kdbxweb.KdbxUuid.random();
|
||||
this.db.meta.customIcons[uuid] = kdbxweb.ByteUtils.arrayToBuffer(
|
||||
kdbxweb.ByteUtils.base64ToBytes(iconData)
|
||||
);
|
||||
this.db.meta.customIcons.set(uuid.id, {
|
||||
data: kdbxweb.ByteUtils.arrayToBuffer(kdbxweb.ByteUtils.base64ToBytes(iconData)),
|
||||
lastModified: new Date()
|
||||
});
|
||||
return uuid.toString();
|
||||
}
|
||||
|
||||
@@ -759,10 +760,17 @@ FileModel.defineModelProperties({
|
||||
keyEncryptionRounds: null,
|
||||
kdfName: null,
|
||||
kdfParameters: null,
|
||||
fingerprint: null,
|
||||
fingerprint: null, // obsolete
|
||||
oldPasswordHash: null,
|
||||
oldKeyFileHash: null,
|
||||
oldKeyChangeDate: null
|
||||
oldKeyChangeDate: null,
|
||||
encryptedPassword: null,
|
||||
encryptedPasswordDate: null,
|
||||
supportsTags: true,
|
||||
supportsColors: true,
|
||||
supportsIcons: true,
|
||||
supportsExpiration: true,
|
||||
defaultGroupHash: ''
|
||||
});
|
||||
|
||||
export { FileModel };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { IconMap } from 'const/icon-map';
|
||||
import { EntryModel } from 'models/entry-model';
|
||||
import { MenuItemModel } from 'models/menu/menu-item-model';
|
||||
@@ -86,7 +86,9 @@ class GroupModel extends MenuItemModel {
|
||||
_buildCustomIcon() {
|
||||
this.customIcon = null;
|
||||
if (this.group.customIcon) {
|
||||
return IconUrlFormat.toDataUrl(this.file.db.meta.customIcons[this.group.customIcon]);
|
||||
return IconUrlFormat.toDataUrl(
|
||||
this.file.db.meta.customIcons.get(this.group.customIcon.id)?.data
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { MenuSectionModel } from 'models/menu/menu-section-model';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
import { Locale } from 'util/locale';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Features } from 'util/features';
|
||||
|
||||
class MenuModel extends Model {
|
||||
constructor() {
|
||||
@@ -113,6 +114,11 @@ class MenuModel extends Model {
|
||||
this.shortcutsSection = new MenuSectionModel([
|
||||
{ locTitle: 'shortcuts', icon: 'keyboard', page: 'shortcuts' }
|
||||
]);
|
||||
if (Features.supportsBrowserExtensions) {
|
||||
this.browserSection = new MenuSectionModel([
|
||||
{ locTitle: 'menuSetBrowser', icon: Features.browserIcon, page: 'browser' }
|
||||
]);
|
||||
}
|
||||
this.pluginsSection = new MenuSectionModel([
|
||||
{ locTitle: 'plugins', icon: 'puzzle-piece', page: 'plugins' }
|
||||
]);
|
||||
@@ -133,6 +139,7 @@ class MenuModel extends Model {
|
||||
[
|
||||
this.generalSection,
|
||||
this.shortcutsSection,
|
||||
this.browserSection,
|
||||
this.pluginsSection,
|
||||
this.devicesSection,
|
||||
this.aboutSection,
|
||||
|
||||
@@ -27,7 +27,7 @@ class MenuSectionModel extends Model {
|
||||
removeByFile(file) {
|
||||
const items = this.items;
|
||||
items.find((item) => {
|
||||
if (item.file === file || item.file === file) {
|
||||
if (item.file === file) {
|
||||
items.remove(item);
|
||||
return true;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ class MenuSectionModel extends Model {
|
||||
replaceByFile(file, newItem) {
|
||||
const items = this.items;
|
||||
items.find((item, ix) => {
|
||||
if (item.file === file || item.file === file) {
|
||||
if (item.file === file) {
|
||||
items[ix] = newItem;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Collection } from 'framework/collection';
|
||||
import { OtpDeviceEntryModel } from './otp-device-entry-model';
|
||||
|
||||
class OtpDeviceEntryCollection extends Collection {
|
||||
static model = OtpDeviceEntryModel;
|
||||
}
|
||||
|
||||
export { OtpDeviceEntryCollection };
|
||||
+9
-4
@@ -1,7 +1,7 @@
|
||||
import { Model } from 'framework/model';
|
||||
import { EntrySearch } from 'util/entry-search';
|
||||
|
||||
class ExternalEntryModel extends Model {
|
||||
class OtpDeviceEntryModel extends Model {
|
||||
tags = [];
|
||||
fields = {};
|
||||
|
||||
@@ -35,6 +35,10 @@ class ExternalEntryModel extends Model {
|
||||
return this.fields;
|
||||
}
|
||||
|
||||
getAllUrls() {
|
||||
return [];
|
||||
}
|
||||
|
||||
getFieldValue(field) {
|
||||
return this.fields[field];
|
||||
}
|
||||
@@ -44,9 +48,10 @@ class ExternalEntryModel extends Model {
|
||||
}
|
||||
}
|
||||
|
||||
ExternalEntryModel.defineModelProperties({
|
||||
OtpDeviceEntryModel.defineModelProperties({
|
||||
id: '',
|
||||
external: true,
|
||||
file: null,
|
||||
entry: true,
|
||||
readOnly: true,
|
||||
device: undefined,
|
||||
deviceSubId: null,
|
||||
@@ -59,4 +64,4 @@ ExternalEntryModel.defineModelProperties({
|
||||
_search: undefined
|
||||
});
|
||||
|
||||
export { ExternalEntryModel };
|
||||
export { OtpDeviceEntryModel };
|
||||
+23
-8
@@ -1,13 +1,11 @@
|
||||
import { Model } from 'framework/model';
|
||||
import { ExternalEntryCollection } from 'collections/external-entry-collection';
|
||||
import { OtpDeviceEntryCollection } from './otp-device-entry-collection';
|
||||
|
||||
class ExternalDeviceModel extends Model {
|
||||
entries = new ExternalEntryCollection();
|
||||
class OtpDeviceModel extends Model {
|
||||
entries = new OtpDeviceEntryCollection();
|
||||
groups = [];
|
||||
entryMap = {};
|
||||
|
||||
close() {}
|
||||
|
||||
forEachEntry(filter, callback) {
|
||||
if (filter.trash || filter.group || filter.tag) {
|
||||
return;
|
||||
@@ -32,11 +30,28 @@ class ExternalDeviceModel extends Model {
|
||||
this.entryMap[entry.id.toLowerCase()] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
open(callback) {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
|
||||
cancelOpen() {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
|
||||
close(callback) {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
|
||||
getOtp(callback) {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
}
|
||||
|
||||
ExternalDeviceModel.defineModelProperties({
|
||||
OtpDeviceModel.defineModelProperties({
|
||||
id: '',
|
||||
external: true,
|
||||
backend: 'otp-device',
|
||||
skipOpenList: true,
|
||||
readOnly: true,
|
||||
active: false,
|
||||
entries: undefined,
|
||||
@@ -47,4 +62,4 @@ ExternalDeviceModel.defineModelProperties({
|
||||
entryMap: undefined
|
||||
});
|
||||
|
||||
export { ExternalDeviceModel };
|
||||
export { OtpDeviceModel };
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
import { ExternalEntryModel } from 'models/external/external-entry-model';
|
||||
import { OtpDeviceEntryModel } from './otp-device-entry-model';
|
||||
|
||||
class ExternalOtpEntryModel extends ExternalEntryModel {
|
||||
class OtpEntryModel extends OtpDeviceEntryModel {
|
||||
constructor(props) {
|
||||
super({
|
||||
...props,
|
||||
@@ -59,10 +59,11 @@ class ExternalOtpEntryModel extends ExternalEntryModel {
|
||||
}
|
||||
}
|
||||
|
||||
ExternalOtpEntryModel.defineModelProperties({
|
||||
OtpEntryModel.defineModelProperties({
|
||||
user: undefined,
|
||||
backend: 'otp-device',
|
||||
otpGenerator: undefined,
|
||||
needsTouch: false
|
||||
});
|
||||
|
||||
export { ExternalOtpEntryModel };
|
||||
export { OtpEntryModel };
|
||||
+5
-4
@@ -1,13 +1,13 @@
|
||||
import { Events } from 'framework/events';
|
||||
import { ExternalOtpDeviceModel } from 'models/external/external-otp-device-model';
|
||||
import { ExternalOtpEntryModel } from 'models/external/external-otp-entry-model';
|
||||
import { OtpDeviceModel } from './otp-device-model';
|
||||
import { OtpEntryModel } from './otp-entry-model';
|
||||
import { Logger } from 'util/logger';
|
||||
import { UsbListener } from 'comp/app/usb-listener';
|
||||
import { YubiKey } from 'comp/app/yubikey';
|
||||
|
||||
const logger = new Logger('yubikey');
|
||||
|
||||
class YubiKeyOtpModel extends ExternalOtpDeviceModel {
|
||||
class YubiKeyOtpModel extends OtpDeviceModel {
|
||||
constructor(props) {
|
||||
super({
|
||||
id: 'yubikey',
|
||||
@@ -67,8 +67,9 @@ class YubiKeyOtpModel extends ExternalOtpDeviceModel {
|
||||
|
||||
for (const code of codes) {
|
||||
this.entries.push(
|
||||
new ExternalOtpEntryModel({
|
||||
new OtpEntryModel({
|
||||
id: this.entryId(code.title, code.user),
|
||||
file: this,
|
||||
device: this,
|
||||
deviceSubId: serial,
|
||||
icon: 'clock',
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Events } from 'framework/events';
|
||||
import { SettingsStore } from 'comp/settings/settings-store';
|
||||
import { Links } from 'const/links';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import BaseLocale from 'locales/base.json';
|
||||
import { Model } from 'framework/model';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
@@ -373,25 +373,27 @@ class Plugin extends Model {
|
||||
};
|
||||
text = `(function(require, module){${text}})(window["${jsVar}"].require,window["${jsVar}"].module);`;
|
||||
const ts = this.logger.ts();
|
||||
const blob = new Blob([text], { type: 'text/javascript' });
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
const elId = 'plugin-js-' + name;
|
||||
const el = this.createElementInHead('script', elId, {
|
||||
src: objectUrl
|
||||
});
|
||||
el.addEventListener('load', () => {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
setTimeout(() => {
|
||||
delete global[jsVar];
|
||||
if (this.module.exports.uninstall) {
|
||||
this.logger.debug('Plugin script installed', this.logger.ts(ts));
|
||||
this.loadPluginSettings();
|
||||
resolve();
|
||||
} else {
|
||||
reject('Plugin script installation failed');
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Note that here we're calling eval to run the plugin code,
|
||||
// previously it was loaded as 'blob:' scheme (see the code below), however:
|
||||
// 1. we need to have eval enabled in our CSP anyway for WASM,
|
||||
// see https://github.com/WebAssembly/content-security-policy/issues/7
|
||||
// 2. we would like to prevent Chrome extensions from injecting scripts to our page,
|
||||
// which is possible to do if we have 'blob:', but they can't call eval
|
||||
// Previous implementation with 'blob:' can be found in git, if we ever need to restore it.
|
||||
|
||||
// eslint-disable-next-line no-eval
|
||||
eval(text);
|
||||
setTimeout(() => {
|
||||
delete global[jsVar];
|
||||
if (this.module.exports.uninstall) {
|
||||
this.logger.debug('Plugin script installed', this.logger.ts(ts));
|
||||
this.loadPluginSettings();
|
||||
resolve();
|
||||
} else {
|
||||
reject('Plugin script installation failed');
|
||||
}
|
||||
}, 0);
|
||||
} catch (e) {
|
||||
this.logger.error('Error installing plugin script', e);
|
||||
reject(e);
|
||||
|
||||
@@ -14,12 +14,13 @@ EntryPresenter.prototype = {
|
||||
this.entry = item;
|
||||
} else if (item.group) {
|
||||
this.group = item;
|
||||
} else if (item.external) {
|
||||
this.entry = item;
|
||||
this.external = true;
|
||||
}
|
||||
return this;
|
||||
},
|
||||
reset() {
|
||||
this.entry = null;
|
||||
this.group = null;
|
||||
},
|
||||
get id() {
|
||||
return this.entry ? this.entry.id : this.group.id;
|
||||
},
|
||||
@@ -71,7 +72,7 @@ EntryPresenter.prototype = {
|
||||
if (!this.entry) {
|
||||
return '[' + Locale.listGroup + ']';
|
||||
}
|
||||
if (this.external) {
|
||||
if (this.entry.backend === 'otp-device') {
|
||||
return this.entry.description;
|
||||
}
|
||||
switch (this.descField) {
|
||||
|
||||
@@ -83,7 +83,9 @@ class StorageDropbox extends StorageBase {
|
||||
pkce: true,
|
||||
width: 600,
|
||||
height: 400,
|
||||
urlParams: { 'token_access_type': 'offline' }
|
||||
urlParams: this.appSettings.shortLivedStorageToken
|
||||
? {}
|
||||
: { 'token_access_type': 'offline' }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const fileWatchers = {};
|
||||
|
||||
class StorageFile extends StorageBase {
|
||||
name = 'file';
|
||||
icon = 'hdd-o';
|
||||
icon = 'hdd';
|
||||
enabled = !!Launcher;
|
||||
system = true;
|
||||
backup = true;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { StorageBase } from 'storage/storage-base';
|
||||
import { Locale } from 'util/locale';
|
||||
import { Features } from 'util/features';
|
||||
import { UrlFormat } from 'util/formatting/url-format';
|
||||
import { GDriveApps } from 'const/cloud-storage-apps';
|
||||
|
||||
const NewFileIdPrefix = 'NewFile:';
|
||||
@@ -27,11 +28,9 @@ class StorageGDrive extends StorageBase {
|
||||
}
|
||||
this.logger.debug('Load', path);
|
||||
const ts = this.logger.ts();
|
||||
const url =
|
||||
this._baseUrl +
|
||||
'/files/{id}/revisions/{rev}?alt=media'
|
||||
.replace('{id}', path)
|
||||
.replace('{rev}', stat.rev);
|
||||
const url = UrlFormat.makeUrl(`${this._baseUrl}/files/${path}/revisions/${stat.rev}`, {
|
||||
'alt': 'media'
|
||||
});
|
||||
this._xhr({
|
||||
url,
|
||||
responseType: 'arraybuffer',
|
||||
@@ -57,7 +56,11 @@ class StorageGDrive extends StorageBase {
|
||||
}
|
||||
this.logger.debug('Stat', path);
|
||||
const ts = this.logger.ts();
|
||||
const url = this._baseUrl + '/files/{id}?fields=headRevisionId'.replace('{id}', path);
|
||||
const url = UrlFormat.makeUrl(`${this._baseUrl}/files/${path}`, {
|
||||
fields: 'headRevisionId',
|
||||
includeItemsFromAllDrives: true,
|
||||
supportsAllDrives: true
|
||||
});
|
||||
this._xhr({
|
||||
url,
|
||||
responseType: 'json',
|
||||
@@ -95,9 +98,12 @@ class StorageGDrive extends StorageBase {
|
||||
let dataType;
|
||||
let dataIsMultipart = false;
|
||||
if (isNew) {
|
||||
url =
|
||||
this._baseUrlUpload +
|
||||
'/files?uploadType=multipart&fields=id,headRevisionId';
|
||||
url = UrlFormat.makeUrl(`${this._baseUrlUpload}/files`, {
|
||||
uploadType: 'multipart',
|
||||
fields: 'id,headRevisionId',
|
||||
includeItemsFromAllDrives: true,
|
||||
supportsAllDrives: true
|
||||
});
|
||||
const fileName = path.replace(NewFileIdPrefix, '') + '.kdbx';
|
||||
const boundary = 'b' + Date.now() + 'x' + Math.round(Math.random() * 1000000);
|
||||
data = [
|
||||
@@ -123,9 +129,12 @@ class StorageGDrive extends StorageBase {
|
||||
dataType = 'multipart/related; boundary="' + boundary + '"';
|
||||
dataIsMultipart = true;
|
||||
} else {
|
||||
url =
|
||||
this._baseUrlUpload +
|
||||
'/files/{id}?uploadType=media&fields=headRevisionId'.replace('{id}', path);
|
||||
url = UrlFormat.makeUrl(`${this._baseUrlUpload}/files/${path}`, {
|
||||
uploadType: 'media',
|
||||
fields: 'headRevisionId',
|
||||
includeItemsFromAllDrives: true,
|
||||
supportsAllDrives: true
|
||||
});
|
||||
}
|
||||
this._xhr({
|
||||
url,
|
||||
@@ -160,59 +169,103 @@ class StorageGDrive extends StorageBase {
|
||||
return callback && callback(err);
|
||||
}
|
||||
this.logger.debug('List');
|
||||
let query =
|
||||
dir === 'shared'
|
||||
? 'sharedWithMe=true'
|
||||
: dir
|
||||
? `"${dir}" in parents`
|
||||
: '"root" in parents';
|
||||
query += ' and trashed=false';
|
||||
const url =
|
||||
this._baseUrl +
|
||||
'/files?fields={fields}&q={q}&pageSize=1000'
|
||||
.replace(
|
||||
'{fields}',
|
||||
encodeURIComponent('files(id,name,mimeType,headRevisionId)')
|
||||
)
|
||||
.replace('{q}', encodeURIComponent(query));
|
||||
|
||||
const ts = this.logger.ts();
|
||||
this._xhr({
|
||||
url,
|
||||
responseType: 'json',
|
||||
success: (response) => {
|
||||
if (!response) {
|
||||
this.logger.error('List error', this.logger.ts(ts));
|
||||
return callback && callback('list error');
|
||||
}
|
||||
this.logger.debug('Listed', this.logger.ts(ts));
|
||||
const fileList = response.files.map((f) => ({
|
||||
name: f.name,
|
||||
path: f.id,
|
||||
rev: f.headRevisionId,
|
||||
dir: f.mimeType === 'application/vnd.google-apps.folder'
|
||||
}));
|
||||
if (!dir) {
|
||||
fileList.unshift({
|
||||
name: Locale.gdriveSharedWithMe,
|
||||
path: 'shared',
|
||||
rev: undefined,
|
||||
|
||||
if (dir === 'drives') {
|
||||
const urlParams = {
|
||||
pageSize: 100
|
||||
};
|
||||
|
||||
const url = UrlFormat.makeUrl(`${this._baseUrl}/drives`, urlParams);
|
||||
|
||||
this._xhr({
|
||||
url,
|
||||
responseType: 'json',
|
||||
success: (response) => {
|
||||
if (!response) {
|
||||
this.logger.error('Drive list error', this.logger.ts(ts));
|
||||
return callback?.('drive list error');
|
||||
}
|
||||
this.logger.debug('Listed drives', this.logger.ts(ts));
|
||||
|
||||
const fileList = response.drives.map((d) => ({
|
||||
name: d.name,
|
||||
path: d.id,
|
||||
dir: true
|
||||
});
|
||||
}));
|
||||
return callback?.(null, fileList);
|
||||
},
|
||||
error: (err) => {
|
||||
this.logger.error('Drive dist error', this.logger.ts(ts), err);
|
||||
return callback?.(err);
|
||||
}
|
||||
return callback && callback(null, fileList);
|
||||
},
|
||||
error: (err) => {
|
||||
this.logger.error('List error', this.logger.ts(ts), err);
|
||||
return callback && callback(err);
|
||||
});
|
||||
} else {
|
||||
let query = 'trashed=false and ';
|
||||
if (dir === 'shared') {
|
||||
query += 'sharedWithMe=true';
|
||||
} else if (dir) {
|
||||
query += `"${dir}" in parents`;
|
||||
} else {
|
||||
query += '"root" in parents';
|
||||
}
|
||||
});
|
||||
|
||||
const urlParams = {
|
||||
fields: 'files(id,name,mimeType,headRevisionId)',
|
||||
q: query,
|
||||
pageSize: 1000,
|
||||
includeItemsFromAllDrives: true,
|
||||
supportsAllDrives: true
|
||||
};
|
||||
|
||||
const url = UrlFormat.makeUrl(`${this._baseUrl}/files`, urlParams);
|
||||
|
||||
this._xhr({
|
||||
url,
|
||||
responseType: 'json',
|
||||
success: (response) => {
|
||||
if (!response) {
|
||||
this.logger.error('List error', this.logger.ts(ts));
|
||||
return callback?.('list error');
|
||||
}
|
||||
this.logger.debug('Listed', this.logger.ts(ts));
|
||||
|
||||
const fileList = response.files.map((f) => ({
|
||||
name: f.name,
|
||||
path: f.id,
|
||||
rev: f.headRevisionId,
|
||||
dir: f.mimeType === 'application/vnd.google-apps.folder'
|
||||
}));
|
||||
if (!dir) {
|
||||
fileList.unshift({
|
||||
name: Locale.gdriveSharedWithMe,
|
||||
path: 'shared',
|
||||
rev: undefined,
|
||||
dir: true
|
||||
});
|
||||
fileList.unshift({
|
||||
name: Locale.gdriveSharedDrives,
|
||||
path: 'drives',
|
||||
rev: undefined,
|
||||
dir: true
|
||||
});
|
||||
}
|
||||
return callback?.(null, fileList);
|
||||
},
|
||||
error: (err) => {
|
||||
this.logger.error('List error', this.logger.ts(ts), err);
|
||||
return callback?.(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
remove(path, callback) {
|
||||
this.logger.debug('Remove', path);
|
||||
const ts = this.logger.ts();
|
||||
const url = this._baseUrl + '/files/{id}'.replace('{id}', path);
|
||||
const url = `${this._baseUrl}/files/${path}`;
|
||||
this._xhr({
|
||||
url,
|
||||
method: 'DELETE',
|
||||
@@ -254,9 +307,7 @@ class StorageGDrive extends StorageBase {
|
||||
width: 600,
|
||||
height: 400,
|
||||
pkce: true,
|
||||
redirectUrlParams: {
|
||||
'access_type': 'offline'
|
||||
}
|
||||
urlParams: this.appSettings.shortLivedStorageToken ? {} : { 'access_type': 'offline' }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,17 +221,31 @@ class StorageOneDrive extends StorageBase {
|
||||
_getOAuthConfig() {
|
||||
let clientId = this.appSettings.onedriveClientId;
|
||||
let clientSecret = this.appSettings.onedriveClientSecret;
|
||||
let tenant = this.appSettings.onedriveTenantId;
|
||||
|
||||
if (!clientId) {
|
||||
if (Features.isLocal) {
|
||||
({ id: clientId, secret: clientSecret } = OneDriveApps.Local);
|
||||
if (Features.isDesktop) {
|
||||
({ id: clientId, secret: clientSecret, tenantId: tenant } = OneDriveApps.Desktop);
|
||||
} else if (Features.isLocal) {
|
||||
({ id: clientId, secret: clientSecret, tenantId: tenant } = OneDriveApps.Local);
|
||||
} else {
|
||||
({ id: clientId, secret: clientSecret } = OneDriveApps.Production);
|
||||
({
|
||||
id: clientId,
|
||||
secret: clientSecret,
|
||||
tenantId: tenant
|
||||
} = OneDriveApps.Production);
|
||||
}
|
||||
}
|
||||
tenant = tenant || 'common';
|
||||
|
||||
let scope = 'files.readwrite';
|
||||
if (!this.appSettings.shortLivedStorageToken) {
|
||||
scope += ' offline_access';
|
||||
}
|
||||
return {
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
||||
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
scope: 'files.readwrite offline_access',
|
||||
url: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
|
||||
tokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
|
||||
scope,
|
||||
clientId,
|
||||
clientSecret,
|
||||
pkce: true,
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
import { StorageBase } from 'storage/storage-base';
|
||||
import { MsTeamsApps } from 'const/cloud-storage-apps';
|
||||
import { Features } from 'util/features';
|
||||
|
||||
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
|
||||
|
||||
// https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true
|
||||
// https://graph.microsoft.com/v1.0/groups?$filter=groupTypes/any(c:c+eq+'Unified')
|
||||
// https://graph.microsoft.com/v1.0/groups?$filter=groupTypes/any(c:c+eq+'Unified')&$orderby=displayName
|
||||
// https://graph.microsoft.com/v1.0/groups?$orderby=displayName
|
||||
// /me/joinedTeams
|
||||
// https://graph.microsoft.com/v1.0/groups/{group id}/drive/root/children
|
||||
|
||||
class StorageTeams extends StorageBase {
|
||||
name = 'msteams';
|
||||
enabled = true;
|
||||
uipos = 50;
|
||||
icon = 'user-friends';
|
||||
|
||||
_graphUrl = 'https://graph.microsoft.com/v1.0';
|
||||
_groupsUrl = `${this._graphUrl}/me/joinedTeams`;
|
||||
_baseUrl = `${this._graphUrl}/groups`;
|
||||
|
||||
getPathForName(fileName) {
|
||||
return '/drive/root:/' + fileName + '.kdbx';
|
||||
}
|
||||
|
||||
genUrlAddress(groupId, path) {
|
||||
if (groupId) {
|
||||
return this._baseUrl + '/' + groupId + (path ? '/' + path.replace(/^\/+/, '') : '');
|
||||
} else {
|
||||
return this._groupsUrl;
|
||||
}
|
||||
}
|
||||
|
||||
genUrl(path) {
|
||||
if (!path) {
|
||||
const groupId = null;
|
||||
const dir = null;
|
||||
const url = this.genUrlAddress(groupId, dir);
|
||||
return [groupId, dir, url];
|
||||
}
|
||||
|
||||
const parts = path.replace(/^\/+/, '').split('/');
|
||||
if (parts.length === 0) {
|
||||
const groupId = null;
|
||||
const dir = null;
|
||||
const url = this.genUrlAddress(groupId, dir);
|
||||
return [groupId, dir, url];
|
||||
} else if (parts.length === 1) {
|
||||
const groupId = parts[0];
|
||||
const dir = null;
|
||||
const url = this.genUrlAddress(groupId, dir);
|
||||
return [groupId, dir, url];
|
||||
} else {
|
||||
path = path.replace(/\/drive\/root\:/, '');
|
||||
const groupId = parts[0];
|
||||
const dir = ('/' + parts.slice(1).join('/')).replace(/^\/+/, '');
|
||||
const url = this.genUrlAddress(groupId, dir);
|
||||
return [groupId, dir, url];
|
||||
}
|
||||
}
|
||||
|
||||
load(path, opts, callback) {
|
||||
this._oauthAuthorize((err) => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
this.logger.debug('Load', path);
|
||||
const ts = this.logger.ts();
|
||||
|
||||
const urlParts = this.genUrl(path);
|
||||
const groupId = urlParts[0];
|
||||
path = urlParts[1];
|
||||
const url = urlParts[2];
|
||||
if (!groupId) {
|
||||
const err = 'no group id defined';
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
||||
this._xhr({
|
||||
url,
|
||||
responseType: 'json',
|
||||
success: (response) => {
|
||||
const downloadUrl = response['@microsoft.graph.downloadUrl'];
|
||||
let rev = response.eTag;
|
||||
if (!downloadUrl || !response.eTag) {
|
||||
this.logger.debug(
|
||||
'Load error',
|
||||
path,
|
||||
'no download url',
|
||||
response,
|
||||
this.logger.ts(ts)
|
||||
);
|
||||
return callback && callback('no download url');
|
||||
}
|
||||
this._xhr({
|
||||
url: downloadUrl,
|
||||
responseType: 'arraybuffer',
|
||||
skipAuth: true,
|
||||
success: (response, xhr) => {
|
||||
rev = xhr.getResponseHeader('ETag') || rev;
|
||||
this.logger.debug('Loaded', path, rev, this.logger.ts(ts));
|
||||
return callback && callback(null, response, { rev });
|
||||
},
|
||||
error: (err) => {
|
||||
this.logger.error('Load error', path, err, this.logger.ts(ts));
|
||||
return callback && callback(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
error: (err) => {
|
||||
this.logger.error('Load error', path, err, this.logger.ts(ts));
|
||||
return callback && callback(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
stat(path, opts, callback) {
|
||||
this._oauthAuthorize((err) => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
this.logger.debug('Stat', path);
|
||||
const ts = this.logger.ts();
|
||||
|
||||
const urlParts = this.genUrl(path);
|
||||
const groupId = urlParts[0];
|
||||
path = urlParts[1];
|
||||
const url = urlParts[2];
|
||||
if (!groupId) {
|
||||
const err = 'no group id defined';
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
||||
this._xhr({
|
||||
url,
|
||||
responseType: 'json',
|
||||
success: (response) => {
|
||||
const rev = response.eTag;
|
||||
if (!rev) {
|
||||
this.logger.error('Stat error', path, 'no eTag', this.logger.ts(ts));
|
||||
return callback && callback('no eTag');
|
||||
}
|
||||
this.logger.debug('Stated', path, rev, this.logger.ts(ts));
|
||||
return callback && callback(null, { rev });
|
||||
},
|
||||
error: (err, xhr) => {
|
||||
if (xhr.status === 404) {
|
||||
this.logger.debug('Stated not found', path, this.logger.ts(ts));
|
||||
return callback && callback({ notFound: true });
|
||||
}
|
||||
this.logger.error('Stat error', path, err, this.logger.ts(ts));
|
||||
return callback && callback(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
save(path, opts, data, callback, rev) {
|
||||
this._oauthAuthorize((err) => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
this.logger.debug('Save', path, rev);
|
||||
const ts = this.logger.ts();
|
||||
|
||||
const urlParts = this.genUrl(path);
|
||||
const groupId = urlParts[0];
|
||||
path = urlParts[1];
|
||||
const url = urlParts[2] + ':/content';
|
||||
if (!groupId) {
|
||||
const err = 'no group id defined';
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
||||
this._xhr({
|
||||
url,
|
||||
method: 'PUT',
|
||||
responseType: 'json',
|
||||
headers: rev ? { 'If-Match': rev } : null,
|
||||
data,
|
||||
statuses: [200, 201, 412],
|
||||
success: (response, xhr) => {
|
||||
rev = response.eTag;
|
||||
if (!rev) {
|
||||
this.logger.error('Save error', path, 'no eTag', this.logger.ts(ts));
|
||||
return callback && callback('no eTag');
|
||||
}
|
||||
if (xhr.status === 412) {
|
||||
this.logger.debug('Save conflict', path, rev, this.logger.ts(ts));
|
||||
return callback && callback({ revConflict: true }, { rev });
|
||||
}
|
||||
this.logger.debug('Saved', path, rev, this.logger.ts(ts));
|
||||
return callback && callback(null, { rev });
|
||||
},
|
||||
error: (err) => {
|
||||
this.logger.error('Save error', path, err, this.logger.ts(ts));
|
||||
return callback && callback(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
list(dir, callback) {
|
||||
this._oauthAuthorize((err) => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
this.logger.debug('List', dir);
|
||||
const ts = this.logger.ts();
|
||||
|
||||
const urlParts = this.genUrl(dir);
|
||||
const groupId = urlParts[0];
|
||||
dir = urlParts[1];
|
||||
const urlPath = groupId ? (dir ? ':/children' : '/drive/root/children') : '';
|
||||
const url = urlParts[2] + urlPath;
|
||||
|
||||
const self = this;
|
||||
self._groupId = groupId;
|
||||
|
||||
this._xhr({
|
||||
url,
|
||||
responseType: 'json',
|
||||
success: (response) => {
|
||||
if (!response || !response.value) {
|
||||
this.logger.error('List error', this.logger.ts(ts), response);
|
||||
return callback && callback('list error');
|
||||
}
|
||||
this.logger.debug('Listed', this.logger.ts(ts));
|
||||
let fileList;
|
||||
if (!self._groupId) {
|
||||
fileList = response.value
|
||||
.filter((f) => f.displayName)
|
||||
.map((f) => ({
|
||||
name: f.displayName,
|
||||
path: '/' + f.id,
|
||||
rev: f.id,
|
||||
dir: true
|
||||
}));
|
||||
} else {
|
||||
fileList = response.value
|
||||
.filter((f) => f.name)
|
||||
.map((f) => ({
|
||||
name: f.name,
|
||||
path: `/${self._groupId}${f.parentReference.path}/${f.name}`,
|
||||
rev: f.eTag,
|
||||
dir: !!f.folder
|
||||
}));
|
||||
}
|
||||
return callback && callback(null, fileList);
|
||||
},
|
||||
error: (err) => {
|
||||
this.logger.error('List error', this.logger.ts(ts), err);
|
||||
return callback && callback(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
remove(path, callback) {
|
||||
this.logger.debug('Remove', path);
|
||||
const ts = this.logger.ts();
|
||||
|
||||
const urlParts = this.genUrl(path);
|
||||
const groupId = urlParts[0];
|
||||
path = urlParts[1];
|
||||
const url = urlParts[2];
|
||||
if (!groupId) {
|
||||
const err = 'no group id defined';
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
||||
this._xhr({
|
||||
url,
|
||||
method: 'DELETE',
|
||||
responseType: 'json',
|
||||
statuses: [200, 204],
|
||||
success: () => {
|
||||
this.logger.debug('Removed', path, this.logger.ts(ts));
|
||||
return callback && callback();
|
||||
},
|
||||
error: (err) => {
|
||||
this.logger.error('Remove error', path, err, this.logger.ts(ts));
|
||||
return callback && callback(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mkdir(path, callback) {
|
||||
this._oauthAuthorize((err) => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
this.logger.debug('Make dir', path);
|
||||
const ts = this.logger.ts();
|
||||
|
||||
const urlParts = this.genUrl(path);
|
||||
const groupId = urlParts[0];
|
||||
path = urlParts[1];
|
||||
const url = urlParts[2] + '/drive/root/children';
|
||||
if (!groupId) {
|
||||
const err = 'no group id defined';
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
||||
const data = JSON.stringify({ name: path.replace('/drive/root:/', ''), folder: {} });
|
||||
this._xhr({
|
||||
url,
|
||||
method: 'POST',
|
||||
responseType: 'json',
|
||||
statuses: [200, 204],
|
||||
data,
|
||||
dataType: 'application/json',
|
||||
success: () => {
|
||||
this.logger.debug('Made dir', path, this.logger.ts(ts));
|
||||
return callback && callback();
|
||||
},
|
||||
error: (err) => {
|
||||
this.logger.error('Make dir error', path, err, this.logger.ts(ts));
|
||||
return callback && callback(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
logout(enabled) {
|
||||
this._oauthRevokeToken();
|
||||
}
|
||||
|
||||
_getOAuthConfig() {
|
||||
let clientId = this.appSettings.msteamsClientId;
|
||||
let clientSecret = this.appSettings.msteamsClientSecret;
|
||||
let tenant = this.appSettings.msteamsTenantId;
|
||||
|
||||
if (!clientId) {
|
||||
if (Features.isDesktop) {
|
||||
({ id: clientId, secret: clientSecret, tenantId: tenant } = MsTeamsApps.Desktop);
|
||||
} else if (Features.isLocal) {
|
||||
({ id: clientId, secret: clientSecret, tenantId: tenant } = MsTeamsApps.Local);
|
||||
} else {
|
||||
({ id: clientId, secret: clientSecret, tenantId: tenant } = MsTeamsApps.Production);
|
||||
}
|
||||
}
|
||||
tenant = tenant || 'common';
|
||||
|
||||
let scope = 'Sites.ReadWrite.All Team.ReadBasic.All';
|
||||
if (!this.appSettings.shortLivedStorageToken) {
|
||||
scope += ' offline_access';
|
||||
}
|
||||
return {
|
||||
url: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
|
||||
tokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
|
||||
scope,
|
||||
clientId,
|
||||
clientSecret,
|
||||
pkce: true,
|
||||
width: 600,
|
||||
height: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { StorageTeams };
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { StorageBase } from 'storage/storage-base';
|
||||
import { Locale } from 'util/locale';
|
||||
|
||||
@@ -209,6 +209,10 @@ class StorageWebDav extends StorageBase {
|
||||
.replace(/[^/]*$/, movePath);
|
||||
}
|
||||
}
|
||||
// prevent double encoding, see #1729
|
||||
const encodedMovePath = /%[A-Z0-9]{2}/.test(movePath)
|
||||
? movePath
|
||||
: encodeURI(movePath);
|
||||
this._request(
|
||||
{
|
||||
...saveOpts,
|
||||
@@ -217,7 +221,7 @@ class StorageWebDav extends StorageBase {
|
||||
path: tmpPath,
|
||||
nostat: true,
|
||||
headers: {
|
||||
Destination: encodeURI(movePath),
|
||||
Destination: encodedMovePath,
|
||||
'Overwrite': 'T'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import { StorageFile } from 'storage/impl/storage-file';
|
||||
import { StorageFileCache } from 'storage/impl/storage-file-cache';
|
||||
import { StorageGDrive } from 'storage/impl/storage-gdrive';
|
||||
import { StorageOneDrive } from 'storage/impl/storage-onedrive';
|
||||
import { StorageTeams } from 'storage/impl/storage-teams';
|
||||
import { StorageWebDav } from 'storage/impl/storage-webdav';
|
||||
import { createOAuthSession } from 'storage/pkce';
|
||||
|
||||
@@ -17,6 +18,7 @@ const ThirdPartyStorage = {
|
||||
dropbox: new StorageDropbox(),
|
||||
gdrive: new StorageGDrive(),
|
||||
onedrive: new StorageOneDrive(),
|
||||
msteams: new StorageTeams(),
|
||||
webdav: new StorageWebDav()
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
|
||||
let newOAuthSession;
|
||||
|
||||
function createOAuthSession() {
|
||||
const session = newOAuthSession;
|
||||
|
||||
const state = kdbxweb.ByteUtils.bytesToHex(kdbxweb.Random.getBytes(64));
|
||||
const codeVerifier = kdbxweb.ByteUtils.bytesToHex(kdbxweb.Random.getBytes(50));
|
||||
const state = kdbxweb.ByteUtils.bytesToHex(kdbxweb.CryptoEngine.random(64));
|
||||
const codeVerifier = kdbxweb.ByteUtils.bytesToHex(kdbxweb.CryptoEngine.random(50));
|
||||
|
||||
const codeVerifierBytes = kdbxweb.ByteUtils.arrayToBuffer(
|
||||
kdbxweb.ByteUtils.stringToBytes(codeVerifier)
|
||||
|
||||
@@ -50,6 +50,10 @@ class StorageBase {
|
||||
|
||||
logout() {}
|
||||
|
||||
deleteStoredToken() {
|
||||
delete this.runtimeData[this.name + 'OAuthToken'];
|
||||
}
|
||||
|
||||
_xhr(config) {
|
||||
this.logger.info('HTTP request', config.method || 'GET', config.url);
|
||||
if (config.data) {
|
||||
@@ -264,11 +268,33 @@ class StorageBase {
|
||||
|
||||
this.logger.debug('OAuth: popup opened');
|
||||
|
||||
const popupClosed = () => {
|
||||
const processWindowMessage = (locationSearch) => {
|
||||
const data = {};
|
||||
for (const [key, value] of new URLSearchParams(locationSearch).entries()) {
|
||||
data[key] = value;
|
||||
}
|
||||
if (data.error) {
|
||||
this.logger.error('OAuth error', data.error, data.error_description);
|
||||
callback('OAuth: ' + data.error);
|
||||
} else if (data.code) {
|
||||
Events.off('popup-closed', popupClosed);
|
||||
window.removeEventListener('message', windowMessage);
|
||||
this._oauthCodeReceived(data, session, callback);
|
||||
} else {
|
||||
this.logger.debug('Skipped OAuth message', data);
|
||||
}
|
||||
};
|
||||
|
||||
const popupClosed = (e) => {
|
||||
Events.off('popup-closed', popupClosed);
|
||||
window.removeEventListener('message', windowMessage);
|
||||
this.logger.error('OAuth error', 'popup closed');
|
||||
callback('OAuth: popup closed');
|
||||
if (e.locationSearch) {
|
||||
// see #1711: mobile Safari in PWA mode can't close the pop-up, but it returns the url
|
||||
processWindowMessage(e.locationSearch);
|
||||
} else {
|
||||
this.logger.error('OAuth error', 'popup closed');
|
||||
callback('OAuth: popup closed');
|
||||
}
|
||||
};
|
||||
|
||||
const windowMessage = (e) => {
|
||||
@@ -283,20 +309,7 @@ class StorageBase {
|
||||
this.logger.debug('Skipped OAuth message for another storage', e.data.storage);
|
||||
return;
|
||||
}
|
||||
const data = {};
|
||||
for (const [key, value] of new URLSearchParams(e.data.search).entries()) {
|
||||
data[key] = value;
|
||||
}
|
||||
if (data.error) {
|
||||
this.logger.error('OAuth error', data.error, data.error_description);
|
||||
callback('OAuth: ' + data.error);
|
||||
} else if (data.code) {
|
||||
Events.off('popup-closed', popupClosed);
|
||||
window.removeEventListener('message', windowMessage);
|
||||
this._oauthCodeReceived(data, session, callback);
|
||||
} else {
|
||||
this.logger.debug('Skipped OAuth message', data);
|
||||
}
|
||||
processWindowMessage(e.data.search);
|
||||
};
|
||||
Events.on('popup-closed', popupClosed);
|
||||
window.addEventListener('message', windowMessage);
|
||||
@@ -306,7 +319,9 @@ class StorageBase {
|
||||
const token = this._oauthMsgToToken(message);
|
||||
if (token && !token.error) {
|
||||
this._oauthToken = token;
|
||||
this.runtimeData[this.name + 'OAuthToken'] = token;
|
||||
if (!this.appSettings.shortLivedStorageToken) {
|
||||
this.runtimeData[this.name + 'OAuthToken'] = token;
|
||||
}
|
||||
this.logger.debug('OAuth token received');
|
||||
}
|
||||
return token;
|
||||
@@ -334,7 +349,9 @@ class StorageBase {
|
||||
|
||||
_oauthGetNewToken(callback) {
|
||||
this._oauthToken.expired = true;
|
||||
this.runtimeData[this.name + 'OAuthToken'] = this._oauthToken;
|
||||
if (!this.appSettings.shortLivedStorageToken) {
|
||||
this.runtimeData[this.name + 'OAuthToken'] = this._oauthToken;
|
||||
}
|
||||
if (this._oauthToken.refreshToken) {
|
||||
this._oauthExchangeRefreshToken(callback);
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import EventEmitter from 'events';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Locale } from 'util/locale';
|
||||
import { KeeWebLogo } from 'const/inline-images';
|
||||
import oauthPageTemplate from 'templates/oauth/complete.hbs';
|
||||
|
||||
const DefaultPort = 48149;
|
||||
const logger = new Logger('storage-oauth-listener');
|
||||
@@ -23,9 +24,9 @@ const StorageOAuthListener = {
|
||||
let resultHandled = false;
|
||||
const server = http.createServer((req, resp) => {
|
||||
resp.writeHead(200, 'OK', {
|
||||
'Content-Type': 'text/plain; charset=UTF-8'
|
||||
'Content-Type': 'text/html; charset=UTF-8'
|
||||
});
|
||||
resp.end(Locale.appBrowserAuthComplete);
|
||||
resp.end(oauthPageTemplate({ logoSrc: KeeWebLogo }));
|
||||
if (!resultHandled) {
|
||||
this.stop();
|
||||
this.handleResult(req.url, listener);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Logger } from 'util/logger';
|
||||
import publicKeyData from 'public-key.pem';
|
||||
import publicKeyDataNew from 'public-key-new.pem';
|
||||
|
||||
@@ -48,6 +48,15 @@ class EntrySearch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filter.otp) {
|
||||
if (
|
||||
!this.model.fields.otp &&
|
||||
!this.model.fields['TOTP Seed'] &&
|
||||
this.model.backend !== 'otp-device'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -127,8 +136,8 @@ class EntrySearch {
|
||||
return context.matches && context.matches.length === find.length;
|
||||
}
|
||||
|
||||
static matchStringMultiLower(str, find, context, lower) {
|
||||
return EntrySearch.matchStringLower(str, find, context, true);
|
||||
static matchStringMultiLower(str, find, context) {
|
||||
return EntrySearch.matchStringMulti(str, find, context, true);
|
||||
}
|
||||
|
||||
static matchRegex(str, regex) {
|
||||
|
||||
@@ -17,13 +17,19 @@ const Features = {
|
||||
!/^http(s?):\/\/((localhost:8085)|((app|beta)\.keeweb\.info))/.test(location.href),
|
||||
isLocal: location.origin.indexOf('localhost') >= 0,
|
||||
|
||||
supportsTitleBarStyles() {
|
||||
get supportsTitleBarStyles() {
|
||||
return isDesktop && (this.isMac || this.isWindows);
|
||||
},
|
||||
get supportsCustomTitleBarAndDraggableWindow() {
|
||||
return isDesktop && this.isMac;
|
||||
},
|
||||
get renderCustomTitleBar() {
|
||||
return isDesktop && this.isWindows;
|
||||
},
|
||||
get hasUnicodeFlags() {
|
||||
return this.isMac;
|
||||
},
|
||||
hasUnicodeFlags() {
|
||||
return this.isMac;
|
||||
},
|
||||
getBrowserCssClass() {
|
||||
get browserCssClass() {
|
||||
if (window.chrome && window.navigator.userAgent.indexOf('Chrome/') > -1) {
|
||||
return 'chrome';
|
||||
}
|
||||
@@ -34,6 +40,45 @@ const Features = {
|
||||
return 'standalone';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
get browserIcon() {
|
||||
if (this._browserIcon) {
|
||||
return this._browserIcon;
|
||||
}
|
||||
|
||||
if (this.isDesktop) {
|
||||
this._browserIcon = this.isMac ? 'safari' : this.isWindows ? 'edge' : 'chrome';
|
||||
} else if (/Gecko\//.test(navigator.userAgent)) {
|
||||
this._browserIcon = 'firefox-browser';
|
||||
} else if (/Edg\//.test(navigator.userAgent)) {
|
||||
this._browserIcon = 'edge';
|
||||
} else if (/Chrome\//.test(navigator.userAgent)) {
|
||||
this._browserIcon = 'chrome';
|
||||
} else if (this.isMac && /Safari\//.test(navigator.userAgent)) {
|
||||
this._browserIcon = 'safari';
|
||||
} else {
|
||||
this._browserIcon = 'window-maximize';
|
||||
}
|
||||
|
||||
return this._browserIcon;
|
||||
},
|
||||
get supportsBrowserExtensions() {
|
||||
return !this.isMobile && (this.isDesktop || this.browserIcon !== 'safari');
|
||||
},
|
||||
get extensionBrowserFamily() {
|
||||
if (Features.isDesktop) {
|
||||
return undefined;
|
||||
} else if (/Gecko\//.test(navigator.userAgent)) {
|
||||
return 'Firefox';
|
||||
} else if (/Edg\//.test(navigator.userAgent)) {
|
||||
return 'Edge';
|
||||
} else if (/Chrome\//.test(navigator.userAgent)) {
|
||||
return 'Chrome';
|
||||
} else if (this.isMac && /Safari\//.test(navigator.userAgent)) {
|
||||
return 'Safari';
|
||||
} else {
|
||||
return 'Chrome';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -75,3 +75,7 @@ export function isEqual(a, b) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function minmax(val, min, max) {
|
||||
return Math.min(max, Math.max(min, val));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
|
||||
const IconUrlFormat = {
|
||||
toDataUrl(iconData) {
|
||||
|
||||
@@ -17,7 +17,7 @@ const MdToHtml = {
|
||||
return '';
|
||||
}
|
||||
const renderer = new MdRenderer();
|
||||
const html = marked(md, { renderer });
|
||||
const html = marked(md, { renderer, breaks: true });
|
||||
const htmlWithoutLineBreaks = html.replace(whiteSpaceRegex, '');
|
||||
const mdWithoutLineBreaks = md.replace(whiteSpaceRegex, '');
|
||||
if (htmlWithoutLineBreaks === mdWithoutLineBreaks) {
|
||||
|
||||
@@ -49,6 +49,8 @@ const PasswordPresenter = {
|
||||
|
||||
const topEl = document.createElement('div');
|
||||
topEl.style.display = 'flex';
|
||||
topEl.style.overflow = 'hidden';
|
||||
topEl.style.textOverflow = 'ellipsis';
|
||||
|
||||
for (const item of items) {
|
||||
const el = document.createElement('div');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user