Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43d5279acf | ||
|
|
e674607fce | ||
|
|
2e6239f637 | ||
|
|
bcb8ddc17b | ||
|
|
cd619582ec | ||
|
|
3b49562f64 | ||
|
|
674ed2cc72 | ||
|
|
5621fd8a06 | ||
|
|
af27e0b111 | ||
|
|
5a78e12f7d | ||
|
|
47b19d1d74 | ||
|
|
070573e68a | ||
|
|
946d7769f1 | ||
|
|
f0426a3ec2 | ||
|
|
b2d67818a8 | ||
|
|
c0b159d824 | ||
|
|
d7e11e20c1 | ||
|
|
d590318153 | ||
|
|
5816ca9c4d | ||
|
|
6627e8c0ec | ||
|
|
379e28e8a4 | ||
|
|
4fb25451a2 | ||
|
|
58a6a97130 | ||
|
|
f59b846643 | ||
|
|
28d7dfeb67 | ||
|
|
6cc39d2107 | ||
|
|
a92a01095b | ||
|
|
3f4cddd2a2 | ||
|
|
aa734ea976 | ||
|
|
30628004fd | ||
|
|
a6e87b52fd | ||
|
|
6d06d825e5 | ||
|
|
0800d5f33a | ||
|
|
337ef44b56 | ||
|
|
70b4effde8 | ||
|
|
9e4c0f5c47 | ||
|
|
535cdc72f7 | ||
|
|
8d258ecab2 | ||
|
|
3079e28f6f | ||
|
|
4970290614 | ||
|
|
6929931cae | ||
|
|
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 | ||
|
|
ace18f9d95 | ||
|
|
c4ec91f61a | ||
|
|
a4881419df | ||
|
|
839359a794 | ||
|
|
d4d9b7c7a7 | ||
|
|
d4526c0c8a | ||
|
|
78f0bc6f02 | ||
|
|
ad3f5afeba | ||
|
|
7c4326939a | ||
|
|
e5428b67b8 | ||
|
|
59616bd8af | ||
|
|
fdc9ac95d2 | ||
|
|
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 | ||
|
|
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 | ||
|
|
a9c2da3fdb | ||
|
|
2ec37c3ece | ||
|
|
ef812c5697 | ||
|
|
54a1860398 | ||
|
|
014b6b2319 | ||
|
|
33763ab865 | ||
|
|
5f5ea892a3 | ||
|
|
2cad70dae0 | ||
|
|
fb56ba2f0d | ||
|
|
fc737b1f51 | ||
|
|
4f068008bd | ||
|
|
953f6dc61f | ||
|
|
01fac1f0ca | ||
|
|
1796ac743d | ||
|
|
60329b3cab | ||
|
|
f489eca5c2 | ||
|
|
1617cae518 | ||
|
|
6c7b99af58 | ||
|
|
ddd242b9df | ||
|
|
c24fe2806c | ||
|
|
8da0a2e239 | ||
|
|
8aff64ce47 | ||
|
|
677f1ce75f | ||
|
|
f99ff95a57 | ||
|
|
39a33bd77a | ||
|
|
f184915849 | ||
|
|
683b0bc4e6 | ||
|
|
8a07162e6b | ||
|
|
4097d81b8a | ||
|
|
bf2d72745a | ||
|
|
acc60e6297 | ||
|
|
f8fda777db | ||
|
|
e955c3ddc1 |
@@ -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?
|
||||
|
||||
@@ -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();
|
||||
+40
-194
@@ -18,12 +18,22 @@ 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
|
||||
run: npm test
|
||||
- name: Grunt
|
||||
run: grunt
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: dist
|
||||
- name: Write secrets
|
||||
env:
|
||||
VIRUS_TOTAL: ${{ secrets.VIRUS_TOTAL }}
|
||||
@@ -33,11 +43,6 @@ jobs:
|
||||
- name: Check on VirusTotal
|
||||
run: grunt virustotal
|
||||
if: ${{ github.repository == 'keeweb/keeweb' }}
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: dist
|
||||
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -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/
|
||||
|
||||
+120
-100
@@ -25,21 +25,30 @@ 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 = () =>
|
||||
skipSign ? { identities: {} } : require('./keys/codesign.json');
|
||||
|
||||
const sha = execSync('git rev-parse --short HEAD').toString('utf8').trim();
|
||||
let sha = grunt.option('commit-sha');
|
||||
if (!sha) {
|
||||
try {
|
||||
sha = execSync('git rev-parse --short HEAD').toString('utf8').trim();
|
||||
} catch (e) {
|
||||
grunt.warn(
|
||||
"Cannot get commit sha from git. It's recommended to build KeeWeb from a git repo " +
|
||||
'because commit sha is displayed in the UI, however if you would like to build from a folder, ' +
|
||||
'you can override what will be displayed in the UI with --commit-sha=xxx.'
|
||||
);
|
||||
}
|
||||
}
|
||||
grunt.log.writeln(`Building KeeWeb v${pkg.version} (${sha})`);
|
||||
|
||||
const webpackOptions = {
|
||||
date,
|
||||
beta: !!grunt.option('beta'),
|
||||
sha
|
||||
sha,
|
||||
appleTeamId: '3LE7JZ657W'
|
||||
};
|
||||
|
||||
const windowsAppVersionString = {
|
||||
@@ -68,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: {
|
||||
@@ -127,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',
|
||||
@@ -161,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',
|
||||
@@ -169,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`,
|
||||
@@ -243,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: {
|
||||
@@ -250,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: {
|
||||
@@ -263,7 +289,7 @@ module.exports = function (grunt) {
|
||||
algo: 'sha512',
|
||||
expected: {
|
||||
style: 1,
|
||||
script: 2
|
||||
script: 1
|
||||
}
|
||||
},
|
||||
app: {
|
||||
@@ -283,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 }] },
|
||||
@@ -437,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 }]
|
||||
@@ -565,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'
|
||||
}
|
||||
@@ -588,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: {
|
||||
@@ -666,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',
|
||||
@@ -678,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',
|
||||
@@ -690,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',
|
||||
@@ -747,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
-2
@@ -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:;
|
||||
@@ -72,7 +72,6 @@
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<link rel="stylesheet" href="css/app.css?__inline=true" />
|
||||
<script src="js/app.js?__inline=true"></script>
|
||||
<script src="js/runtime.js?__inline=true"></script>
|
||||
</head>
|
||||
<body class="th-d">
|
||||
<noscript>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
CACHE MANIFEST
|
||||
|
||||
# YYYY-MM-DD:v0.0.0
|
||||
# updmin:v0.0.0
|
||||
|
||||
NETWORK:
|
||||
*
|
||||
+17
-7
@@ -6,9 +6,11 @@ import { ExportApi } from 'comp/app/export-api';
|
||||
import { SingleInstanceChecker } from 'comp/app/single-instance-checker';
|
||||
import { Updater } from 'comp/app/updater';
|
||||
import { UsbListener } from 'comp/app/usb-listener';
|
||||
import { BrowserExtensionConnector } from 'comp/extension/browser-extension-connector';
|
||||
import { FeatureTester } from 'comp/browser/feature-tester';
|
||||
import { FocusDetector } from 'comp/browser/focus-detector';
|
||||
import { IdleTracker } from 'comp/browser/idle-tracker';
|
||||
import { ThemeWatcher } from 'comp/browser/theme-watcher';
|
||||
import { KeyHandler } from 'comp/browser/key-handler';
|
||||
import { PopupNotifier } from 'comp/browser/popup-notifier';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
@@ -44,6 +46,7 @@ ready(() => {
|
||||
.then(loadRemoteConfig)
|
||||
.then(ensureCanRun)
|
||||
.then(initStorage)
|
||||
.then(initUsbListener)
|
||||
.then(showApp)
|
||||
.then(postInit)
|
||||
.catch((e) => {
|
||||
@@ -91,6 +94,8 @@ ready(() => {
|
||||
KdbxwebInit.init();
|
||||
FocusDetector.init();
|
||||
AutoType.init();
|
||||
ThemeWatcher.init();
|
||||
SettingsManager.init();
|
||||
window.kw = ExportApi;
|
||||
return PluginManager.init().then(() => {
|
||||
StartProfiler.milestone('initializing modules');
|
||||
@@ -111,13 +116,13 @@ ready(() => {
|
||||
function loadRemoteConfig() {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
SettingsManager.setBySettings(appModel.settings);
|
||||
SettingsManager.setBySettings();
|
||||
const configParam = getConfigParam();
|
||||
if (configParam) {
|
||||
return appModel
|
||||
.loadConfig(configParam)
|
||||
.then(() => {
|
||||
SettingsManager.setBySettings(appModel.settings);
|
||||
SettingsManager.setBySettings();
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!appModel.settings.cacheConfigSettings) {
|
||||
@@ -139,6 +144,11 @@ ready(() => {
|
||||
StartProfiler.milestone('initializing storage');
|
||||
}
|
||||
|
||||
function initUsbListener() {
|
||||
UsbListener.init();
|
||||
StartProfiler.milestone('starting usb');
|
||||
}
|
||||
|
||||
function showApp() {
|
||||
return Promise.resolve().then(() => {
|
||||
const skipHttpsWarning =
|
||||
@@ -169,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,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Logger } from 'util/logger';
|
||||
|
||||
const logger = new Logger('online-password-checker');
|
||||
|
||||
const exposedPasswords = {};
|
||||
|
||||
function checkIfPasswordIsExposedOnline(password) {
|
||||
if (!password || !password.isProtected || !password.byteLength) {
|
||||
return false;
|
||||
}
|
||||
const saltedValue = password.saltedValue();
|
||||
const cached = exposedPasswords[saltedValue];
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
const passwordBytes = password.getBinary();
|
||||
return crypto.subtle
|
||||
.digest({ name: 'SHA-1' }, passwordBytes)
|
||||
.then((sha1) => {
|
||||
kdbxweb.ByteUtils.zeroBuffer(passwordBytes);
|
||||
sha1 = kdbxweb.ByteUtils.bytesToHex(sha1).toUpperCase();
|
||||
const shaFirst = sha1.substr(0, 5);
|
||||
return fetch(`https://api.pwnedpasswords.com/range/${shaFirst}`)
|
||||
.then((response) => response.text())
|
||||
.then((response) => {
|
||||
const isPresent = response.includes(sha1.substr(5));
|
||||
exposedPasswords[saltedValue] = isPresent;
|
||||
return isPresent;
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error('Error checking password online', e);
|
||||
});
|
||||
}
|
||||
|
||||
export { checkIfPasswordIsExposedOnline };
|
||||
@@ -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,7 +203,7 @@ const YubiKey = {
|
||||
|
||||
this.process = Launcher.spawn({
|
||||
cmd: this.cmd(),
|
||||
args: ['-d', serial, 'oath', 'code'],
|
||||
args: ['-d', serial, 'oath', 'accounts', 'code'],
|
||||
noStdOutLogging: true,
|
||||
throwOnStdErr: true,
|
||||
complete: (err, stdout) => {
|
||||
@@ -241,7 +241,7 @@ const YubiKey = {
|
||||
getOtp(serial, entry, callback) {
|
||||
return Launcher.spawn({
|
||||
cmd: this.cmd(),
|
||||
args: ['-d', serial, 'oath', 'code', '--single', entry],
|
||||
args: ['-d', serial, 'oath', 'accounts', 'code', '--single', entry],
|
||||
noStdOutLogging: true,
|
||||
complete: (err, stdout) => {
|
||||
if (err) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
|
||||
const SecureInput = function () {
|
||||
this.el = null;
|
||||
@@ -79,7 +79,7 @@ Object.defineProperty(SecureInput.prototype, 'value', {
|
||||
const len = pseudoValue.length;
|
||||
let byteLength = 0;
|
||||
const valueBytes = new Uint8Array(len * 4);
|
||||
const saltBytes = kdbxweb.Random.getBytes(len * 4);
|
||||
const saltBytes = kdbxweb.CryptoEngine.random(len * 4);
|
||||
let ch;
|
||||
let bytes;
|
||||
for (let i = 0; i < len; i++) {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Events } from 'framework/events';
|
||||
|
||||
const ThemeWatcher = {
|
||||
dark: false,
|
||||
|
||||
init() {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
if (mediaQuery && mediaQuery.addEventListener) {
|
||||
mediaQuery.addEventListener('change', (e) => {
|
||||
const dark = e.matches;
|
||||
this.dark = dark;
|
||||
Events.emit('dark-mode-changed', { dark });
|
||||
});
|
||||
}
|
||||
this.dark = !!mediaQuery.matches;
|
||||
}
|
||||
};
|
||||
|
||||
export { ThemeWatcher };
|
||||
@@ -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 };
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Events } from 'framework/events';
|
||||
import { Features } from 'util/features';
|
||||
import { Locale } from 'util/locale';
|
||||
import { ThemeWatcher } from 'comp/browser/theme-watcher';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const logger = new Logger('settings-manager');
|
||||
|
||||
const SettingsManager = {
|
||||
neutralLocale: null,
|
||||
@@ -16,23 +22,65 @@ const SettingsManager = {
|
||||
allThemes: {
|
||||
dark: 'setGenThemeDark',
|
||||
light: 'setGenThemeLight',
|
||||
fb: 'setGenThemeFb',
|
||||
db: 'setGenThemeDb',
|
||||
sd: 'setGenThemeSd',
|
||||
sl: 'setGenThemeSl',
|
||||
fb: 'setGenThemeFb',
|
||||
bl: 'setGenThemeBl',
|
||||
db: 'setGenThemeDb',
|
||||
lb: 'setGenThemeLb',
|
||||
te: 'setGenThemeTe',
|
||||
lt: 'setGenThemeLt',
|
||||
dc: 'setGenThemeDc',
|
||||
hc: 'setGenThemeHc'
|
||||
},
|
||||
|
||||
// changing something here? don't forget about desktop/app.js
|
||||
autoSwitchedThemes: [
|
||||
{
|
||||
name: 'setGenThemeDefault',
|
||||
dark: 'dark',
|
||||
light: 'light'
|
||||
},
|
||||
{
|
||||
name: 'setGenThemeSol',
|
||||
dark: 'sd',
|
||||
light: 'sl'
|
||||
},
|
||||
{
|
||||
name: 'setGenThemeBlue',
|
||||
dark: 'fb',
|
||||
light: 'bl'
|
||||
},
|
||||
{
|
||||
name: 'setGenThemeBrown',
|
||||
dark: 'db',
|
||||
light: 'lb'
|
||||
},
|
||||
{
|
||||
name: 'setGenThemeTerminal',
|
||||
dark: 'te',
|
||||
light: 'lt'
|
||||
},
|
||||
{
|
||||
name: 'setGenThemeHighContrast',
|
||||
dark: 'dc',
|
||||
light: 'hc'
|
||||
}
|
||||
],
|
||||
|
||||
customLocales: {},
|
||||
|
||||
setBySettings(settings) {
|
||||
this.setTheme(settings.theme);
|
||||
this.setFontSize(settings.fontSize);
|
||||
const locale = settings.locale;
|
||||
init() {
|
||||
Events.on('dark-mode-changed', () => this.darkModeChanged());
|
||||
},
|
||||
|
||||
setBySettings() {
|
||||
this.setTheme(AppSettingsModel.theme);
|
||||
this.setFontSize(AppSettingsModel.fontSize);
|
||||
const locale = AppSettingsModel.locale;
|
||||
try {
|
||||
if (locale) {
|
||||
this.setLocale(settings.locale);
|
||||
this.setLocale(AppSettingsModel.locale);
|
||||
} else {
|
||||
this.setLocale(this.getBrowserLocale());
|
||||
}
|
||||
@@ -55,18 +103,45 @@ const SettingsManager = {
|
||||
document.body.classList.remove(cls);
|
||||
}
|
||||
}
|
||||
if (AppSettingsModel.autoSwitchTheme) {
|
||||
theme = this.selectDarkOrLightTheme(theme);
|
||||
}
|
||||
document.body.classList.add(this.getThemeClass(theme));
|
||||
const metaThemeColor = document.head.querySelector('meta[name=theme-color]');
|
||||
if (metaThemeColor) {
|
||||
metaThemeColor.content = window.getComputedStyle(document.body).backgroundColor;
|
||||
}
|
||||
this.activeTheme = theme;
|
||||
logger.debug('Theme changed', theme);
|
||||
Events.emit('theme-applied');
|
||||
},
|
||||
|
||||
getThemeClass(theme) {
|
||||
return 'th-' + theme;
|
||||
},
|
||||
|
||||
selectDarkOrLightTheme(theme) {
|
||||
for (const config of this.autoSwitchedThemes) {
|
||||
if (config.light === theme || config.dark === theme) {
|
||||
return ThemeWatcher.dark ? config.dark : config.light;
|
||||
}
|
||||
}
|
||||
return theme;
|
||||
},
|
||||
|
||||
darkModeChanged() {
|
||||
if (AppSettingsModel.autoSwitchTheme) {
|
||||
for (const config of this.autoSwitchedThemes) {
|
||||
if (config.light === this.activeTheme || config.dark === this.activeTheme) {
|
||||
const newTheme = ThemeWatcher.dark ? config.dark : config.light;
|
||||
logger.debug('Setting theme triggered by system settings change', newTheme);
|
||||
this.setTheme(newTheme);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setFontSize(fontSize) {
|
||||
const defaultFontSize = Features.isMobile ? 14 : 12;
|
||||
document.documentElement.style.fontSize = defaultFontSize + (fontSize || 0) * 2 + 'px';
|
||||
@@ -90,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;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -37,4 +37,17 @@ const OneDriveApps = {
|
||||
}
|
||||
};
|
||||
|
||||
export { DropboxApps, GDriveApps, OneDriveApps };
|
||||
const MsTeamsApps = {
|
||||
Local: {
|
||||
id: '8fbe2245-13d5-446f-bedc-74c3b2e1f635'
|
||||
},
|
||||
Production: {
|
||||
id: '8fbe2245-13d5-446f-bedc-74c3b2e1f635'
|
||||
},
|
||||
Desktop: {
|
||||
id: '8fbe2245-13d5-446f-bedc-74c3b2e1f635',
|
||||
secret: 'F02~HYaWs-~7MndJcVRtv9~h-50Brk_9ho'
|
||||
}
|
||||
};
|
||||
|
||||
export { DropboxApps, GDriveApps, OneDriveApps, MsTeamsApps };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const DefaultAppSettings = {
|
||||
theme: null, // UI theme
|
||||
autoSwitchTheme: false, // automatically switch between light and dark theme
|
||||
locale: null, // user interface language
|
||||
expandGroups: true, // show entries from all subgroups
|
||||
listViewWidth: null, // width of the entry list representation
|
||||
@@ -8,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
|
||||
@@ -36,6 +39,17 @@ const DefaultAppSettings = {
|
||||
useGroupIconForEntries: false, // automatically use group icon when creating new entries
|
||||
enableUsb: true, // enable interaction with USB devices
|
||||
fieldLabelDblClickAutoType: false, // trigger auto-type by doubleclicking field label
|
||||
auditPasswords: true, // enable password audit
|
||||
auditPasswordEntropy: true, // show warnings for weak passwords
|
||||
excludePinsFromAudit: true, // exclude PIN codes from audit
|
||||
checkPasswordsOnHIBP: false, // check passwords on Have I Been Pwned
|
||||
auditPasswordAge: 0, // show warnings about old passwords, number of years, 0 = disabled
|
||||
deviceOwnerAuth: null, // Touch ID: null / 'memory' / 'file'
|
||||
deviceOwnerAuthTimeoutMinutes: 0, // how often master password is required with Touch ID
|
||||
disableOfflineStorage: false, // don't cache loaded files in offline storage
|
||||
shortLivedStorageToken: false, // short-lived sessions in cloud storage providers
|
||||
extensionFocusIfLocked: true, // focus KeeWeb if a browser extension tries to connect while KeeWeb is locked
|
||||
extensionFocusIfEmpty: true, // show the entry selection screen if there's no match found by URL
|
||||
|
||||
yubiKeyShowIcon: true, // show an icon to open OTP codes from YubiKey
|
||||
yubiKeyAutoOpen: false, // auto-load one-time codes when there are open files
|
||||
@@ -73,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,17 +7,33 @@ 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',
|
||||
PluginDevelopStart: 'https://github.com/keeweb/keeweb/wiki/Plugins',
|
||||
YubiKeyManual: 'https://github.com/keeweb/keeweb/wiki/YubiKey',
|
||||
YubiKeyManagerInstall: 'https://github.com/Yubico/yubikey-manager#installation'
|
||||
YubiKeyManagerInstall: 'https://github.com/Yubico/yubikey-manager#installation',
|
||||
HaveIBeenPwned: 'https://haveibeenpwned.com',
|
||||
HaveIBeenPwnedPrivacy: 'https://haveibeenpwned.com/Passwords',
|
||||
ExtensionHelpForOtherBrowsers:
|
||||
'https://github.com/keeweb/keeweb/wiki/Browser-AutoFill#other-browsers',
|
||||
KWCForChrome:
|
||||
'https://chrome.google.com/webstore/detail/keeweb-connect/pikpfmjfkekaeinceagbebpfkmkdlcjk',
|
||||
KWCForFirefox: 'https://addons.mozilla.org/firefox/addon/keeweb-connect/',
|
||||
KWCForEdge:
|
||||
'https://microsoftedge.microsoft.com/addons/detail/keewebconnect/nmggpehkjmeaeocmaijenpejbepckinm',
|
||||
KWCForSafari: 'https://apps.apple.com/app/keeweb-connect/id1565748094',
|
||||
KPXCForChrome:
|
||||
'https://chrome.google.com/webstore/detail/keepassxc-browser/oboonakemofpalcgghocfoadofidjkkk',
|
||||
KPXCForFirefox: 'https://addons.mozilla.org/firefox/addon/keepassxc-browser/',
|
||||
KPXCForEdge:
|
||||
'https://microsoftedge.microsoft.com/addons/detail/keepassxcbrowser/pdffhmdngciaglkoonimfcmckehcpafo'
|
||||
};
|
||||
|
||||
export { Links };
|
||||
|
||||
@@ -3,7 +3,8 @@ const RuntimeInfo = {
|
||||
beta: !!'@@BETA',
|
||||
buildDate: '@@DATE',
|
||||
commit: '@@COMMIT',
|
||||
devMode: '@@DEVMODE'
|
||||
devMode: '@@DEVMODE',
|
||||
appleTeamId: '@@APPLE_TEAM_ID'
|
||||
};
|
||||
|
||||
export { RuntimeInfo };
|
||||
|
||||
@@ -16,7 +16,10 @@ const Timeouts = {
|
||||
ExternalDeviceReconnect: 3000,
|
||||
ExternalDeviceAfterReconnect: 1000,
|
||||
FieldLabelDoubleClick: 300,
|
||||
NativeModuleHostRestartTime: 3000
|
||||
NativeModuleHostRestartTime: 3000,
|
||||
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
-663
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"
|
||||
}
|
||||
@@ -26,6 +26,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",
|
||||
@@ -42,11 +51,27 @@
|
||||
"menuAlertNoTags": "Aucun tag",
|
||||
"menuAlertNoTagsBody": "Vous pouvez ajouter de nouveaux tags en éditant les champs, dans la section Tags.",
|
||||
"menuEmptyTrash": "Vider la Corbeille",
|
||||
"menuEmptyTrashAlert": "Vider la Corbeille?",
|
||||
"menuEmptyTrashAlert": "Vider la Corbeille ?",
|
||||
"menuEmptyTrashAlertBody": "Vous ne pourrez plus récupérer ces éléments",
|
||||
"menuItemCollapsed": "Double-cliquez pour déplier",
|
||||
"menuRestoreApp": "Ouvrir {}",
|
||||
"menuQuitApp": "Quitter {}",
|
||||
"sysMenuAboutKeeWeb": "A propos {}",
|
||||
"sysMenuServices": "Services",
|
||||
"sysMenuHide": "Cacher {}",
|
||||
"sysMenuHideOthers": "Cacher autres",
|
||||
"sysMenuUnhide": "Montrer tous",
|
||||
"sysMenuQuit": "Quitter {}",
|
||||
"sysMenuEdit": "Editer",
|
||||
"sysMenuUndo": "Annuler",
|
||||
"sysMenuRedo": "Répéter",
|
||||
"sysMenuCut": "Couper",
|
||||
"sysMenuCopy": "Copier",
|
||||
"sysMenuPaste": "Coller",
|
||||
"sysMenuSelectAll": "Tout sélectionner",
|
||||
"sysMenuWindow": "Fenêtre",
|
||||
"sysMenuMinimize": "Réduire",
|
||||
"sysMenuClose": "Fermer fenêtre",
|
||||
"alertYes": "Oui",
|
||||
"alertNo": "Non",
|
||||
"alertOk": "OK",
|
||||
@@ -63,7 +88,7 @@
|
||||
"genLen": "Longueur",
|
||||
"genNewPass": "Nouveau mot de passe",
|
||||
"genPresetDefault": "réglages par défaut",
|
||||
"genPresetDerived": "identique à l'ancien mot de passe",
|
||||
"genPresetDerived": "similaire à l'ancien mot de passe",
|
||||
"genPresetPronounceable": "prononçable",
|
||||
"genPresetMed": "longueur moyenne",
|
||||
"genPresetLong": "long",
|
||||
@@ -87,7 +112,7 @@
|
||||
"tagExists": "Ce tag existe déjà",
|
||||
"tagExistsBody": "Un tag existe déjà avec ce nom. Merci de choisir un autre nom.",
|
||||
"tagBadName": "Nom invalide",
|
||||
"tagBadNameBody": "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",
|
||||
@@ -122,7 +147,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 +209,7 @@
|
||||
"openConfigErrorNotFound": "Aucun fichier trouvé",
|
||||
"openError": "Erreur",
|
||||
"openErrorDescription": "Une erreur est survenue à l'ouverture du fichier",
|
||||
"openErrorDescriptionMaybeTouchIdChanged": "L'erreur ressemble à ce qu'y arrive habituellement quand la configuration de Touch ID a été changée. Par exemple, vous avez ajouté ou supprimé un doigt supplémentaire. Si c'est le cas, allez dans Paramètres, désactiver Touch ID et activer le de nouveau.",
|
||||
"openErrorFileNotFound": "Fichier non trouvé",
|
||||
"openListErrorBody": "Erreur au chargement de la liste des fichiers",
|
||||
"openShowAllFiles": "Tous les fichiers",
|
||||
@@ -237,7 +263,7 @@
|
||||
"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",
|
||||
@@ -286,13 +312,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",
|
||||
@@ -349,7 +386,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 +446,15 @@
|
||||
"setGenClearSeconds": "Dans {} secondes",
|
||||
"setGenClearMinute": "Dans une minute",
|
||||
"setGenMinInstead": "Réduire l'application au lieu de fermer",
|
||||
"setGenMinOnFieldCopy": "Réduire sur copie d'un champ",
|
||||
"setGenLock": "Verrou Auto",
|
||||
"setGenLockMinimize": "quand l'appli est en arrière-plan",
|
||||
"setGenLockCopy": "sur copie de m.d.p.",
|
||||
"setGenLockAutoType": "sur saisie auto",
|
||||
"setGenLockOrSleep": "Quand l'ordinateur est verrouillé ou mis en sommeil",
|
||||
"setGenStorage": "Stockage",
|
||||
"setGenDisableOfflineStorage": "Ne pas mettre en cache des fichiers d'un stockage offline",
|
||||
"setGenShortLivedStorageToken": "Utiliser des sessions de courte-durée avec les fournisseurs de stockage dans le cloud",
|
||||
"setGenStorageLogout": "Déconnexion",
|
||||
"setGenShowAdvanced": "Afficher les paramètres avancés",
|
||||
"setGenDevTools": "Afficher les outils de développement",
|
||||
@@ -424,6 +464,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",
|
||||
@@ -615,7 +671,7 @@
|
||||
"gdriveSharedWithMe": "Partagé avec moi",
|
||||
"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 +696,6 @@
|
||||
"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 \"{}\""
|
||||
}
|
||||
+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() {
|
||||
@@ -77,41 +78,47 @@ class MenuModel extends Model {
|
||||
locTitle: 'setGenAppearance',
|
||||
icon: '0',
|
||||
page: 'general',
|
||||
section: 'appearance',
|
||||
active: true
|
||||
section: 'appearance'
|
||||
},
|
||||
{
|
||||
locTitle: 'setGenFunction',
|
||||
icon: '0',
|
||||
page: 'general',
|
||||
section: 'function',
|
||||
active: true
|
||||
section: 'function'
|
||||
},
|
||||
{
|
||||
locTitle: 'setGenAudit',
|
||||
icon: '0',
|
||||
page: 'general',
|
||||
section: 'audit'
|
||||
},
|
||||
{
|
||||
locTitle: 'setGenLock',
|
||||
icon: '0',
|
||||
page: 'general',
|
||||
section: 'lock',
|
||||
active: true
|
||||
section: 'lock'
|
||||
},
|
||||
{
|
||||
locTitle: 'setGenStorage',
|
||||
icon: '0',
|
||||
page: 'general',
|
||||
section: 'storage',
|
||||
active: true
|
||||
section: 'storage'
|
||||
},
|
||||
{
|
||||
locTitle: 'advanced',
|
||||
icon: '0',
|
||||
page: 'general',
|
||||
section: 'advanced',
|
||||
active: true
|
||||
section: 'advanced'
|
||||
}
|
||||
]);
|
||||
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' }
|
||||
]);
|
||||
@@ -132,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,19 +221,31 @@ class StorageOneDrive extends StorageBase {
|
||||
_getOAuthConfig() {
|
||||
let clientId = this.appSettings.onedriveClientId;
|
||||
let clientSecret = this.appSettings.onedriveClientSecret;
|
||||
let tenant = this.appSettings.onedriveTenantId;
|
||||
|
||||
if (!clientId) {
|
||||
if (Features.isDesktop) {
|
||||
({ id: clientId, secret: clientSecret } = OneDriveApps.Desktop);
|
||||
({ id: clientId, secret: clientSecret, tenantId: tenant } = OneDriveApps.Desktop);
|
||||
} else if (Features.isLocal) {
|
||||
({ id: clientId, secret: clientSecret } = OneDriveApps.Local);
|
||||
({ id: clientId, secret: clientSecret, tenantId: tenant } = OneDriveApps.Local);
|
||||
} else {
|
||||
({ id: clientId, secret: clientSecret } = OneDriveApps.Production);
|
||||
({
|
||||
id: clientId,
|
||||
secret: clientSecret,
|
||||
tenantId: tenant
|
||||
} = OneDriveApps.Production);
|
||||
}
|
||||
}
|
||||
tenant = tenant || 'common';
|
||||
|
||||
let scope = 'files.readwrite';
|
||||
if (!this.appSettings.shortLivedStorageToken) {
|
||||
scope += ' offline_access';
|
||||
}
|
||||
return {
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
||||
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
scope: 'files.readwrite offline_access',
|
||||
url: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
|
||||
tokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
|
||||
scope,
|
||||
clientId,
|
||||
clientSecret,
|
||||
pkce: true,
|
||||
|
||||
@@ -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) {
|
||||
@@ -315,7 +319,9 @@ class StorageBase {
|
||||
const token = this._oauthMsgToToken(message);
|
||||
if (token && !token.error) {
|
||||
this._oauthToken = token;
|
||||
this.runtimeData[this.name + 'OAuthToken'] = token;
|
||||
if (!this.appSettings.shortLivedStorageToken) {
|
||||
this.runtimeData[this.name + 'OAuthToken'] = token;
|
||||
}
|
||||
this.logger.debug('OAuth token received');
|
||||
}
|
||||
return token;
|
||||
@@ -343,7 +349,9 @@ class StorageBase {
|
||||
|
||||
_oauthGetNewToken(callback) {
|
||||
this._oauthToken.expired = true;
|
||||
this.runtimeData[this.name + 'OAuthToken'] = this._oauthToken;
|
||||
if (!this.appSettings.shortLivedStorageToken) {
|
||||
this.runtimeData[this.name + 'OAuthToken'] = this._oauthToken;
|
||||
}
|
||||
if (this._oauthToken.refreshToken) {
|
||||
this._oauthExchangeRefreshToken(callback);
|
||||
} else {
|
||||
|
||||
@@ -1,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);
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Password strength level estimation according to OWASP password recommendations and entropy
|
||||
* https://auth0.com/docs/connections/database/password-strength
|
||||
*/
|
||||
|
||||
const PasswordStrengthLevel = {
|
||||
None: 0,
|
||||
Low: 1,
|
||||
Good: 2
|
||||
};
|
||||
|
||||
const charClasses = new Uint8Array(128);
|
||||
for (let i = 48 /* '0' */; i <= 57 /* '9' */; i++) {
|
||||
charClasses[i] = 1;
|
||||
}
|
||||
for (let i = 97 /* 'a' */; i <= 122 /* 'z' */; i++) {
|
||||
charClasses[i] = 2;
|
||||
}
|
||||
for (let i = 65 /* 'A' */; i <= 90 /* 'Z' */; i++) {
|
||||
charClasses[i] = 3;
|
||||
}
|
||||
|
||||
const symbolsPerCharClass = new Uint8Array([
|
||||
95 /* ASCII symbols */,
|
||||
10 /* digits */,
|
||||
26 /* lowercase letters */,
|
||||
26 /* uppercase letters */
|
||||
]);
|
||||
|
||||
function passwordStrength(password) {
|
||||
if (!password || !password.isProtected) {
|
||||
throw new TypeError('Bad password type');
|
||||
}
|
||||
|
||||
if (!password.byteLength) {
|
||||
return { level: PasswordStrengthLevel.None, length: 0 };
|
||||
}
|
||||
|
||||
let length = 0;
|
||||
const countByClass = [0, 0, 0, 0];
|
||||
let isSingleChar = true;
|
||||
let prevCharCode = -1;
|
||||
password.forEachChar((charCode) => {
|
||||
const charClass = charCode < charClasses.length ? charClasses[charCode] : 0;
|
||||
countByClass[charClass]++;
|
||||
length++;
|
||||
if (isSingleChar) {
|
||||
if (charCode !== prevCharCode) {
|
||||
if (prevCharCode === -1) {
|
||||
prevCharCode = charCode;
|
||||
} else {
|
||||
isSingleChar = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const onlyDigits = countByClass[1] === length;
|
||||
|
||||
if (length < 6) {
|
||||
return { level: PasswordStrengthLevel.None, length, onlyDigits };
|
||||
}
|
||||
|
||||
if (isSingleChar) {
|
||||
return { level: PasswordStrengthLevel.None, length, onlyDigits };
|
||||
}
|
||||
|
||||
if (length < 8) {
|
||||
return { level: PasswordStrengthLevel.Low, length, onlyDigits };
|
||||
}
|
||||
|
||||
let alphabetSize = 0;
|
||||
for (let i = 0; i < countByClass.length; i++) {
|
||||
if (countByClass[i] > 0) {
|
||||
alphabetSize += symbolsPerCharClass[i];
|
||||
}
|
||||
}
|
||||
|
||||
const entropy = Math.log2(Math.pow(alphabetSize, length));
|
||||
|
||||
const level = entropy < 60 ? PasswordStrengthLevel.Low : PasswordStrengthLevel.Good;
|
||||
return { length, level, onlyDigits };
|
||||
}
|
||||
|
||||
export { PasswordStrengthLevel, passwordStrength };
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -29,6 +29,10 @@ const StringFormat = {
|
||||
|
||||
pascalCase(str) {
|
||||
return this.capFirst(str.replace(this.camelCaseRegex, (match) => match[1].toUpperCase()));
|
||||
},
|
||||
|
||||
replaceVersion(str, replacement) {
|
||||
return str.replace(/\d+\.\d+\.\d+/g, replacement);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user