Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfc6201285 | ||
|
|
1930c265c8 | ||
|
|
1c434d7c79 | ||
|
|
c0c75ac8a8 | ||
|
|
4b7124aa57 | ||
|
|
93cee76b4f | ||
|
|
1673580dfa | ||
|
|
bf0e94582d | ||
|
|
29ae7e8ed2 | ||
|
|
9b2c40a078 | ||
|
|
f0bb382449 | ||
|
|
062ddb3f72 | ||
|
|
208a1b551e | ||
|
|
f8af64e45b | ||
|
|
98268f21c0 | ||
|
|
dcbe8d70da | ||
|
|
1d61c6ca03 | ||
|
|
433f0acf3c | ||
|
|
070573e68a | ||
|
|
946d7769f1 | ||
|
|
f0426a3ec2 | ||
|
|
b2d67818a8 | ||
|
|
c0b159d824 | ||
|
|
d590318153 | ||
|
|
5816ca9c4d | ||
|
|
6627e8c0ec | ||
|
|
379e28e8a4 | ||
|
|
4fb25451a2 | ||
|
|
f59b846643 | ||
|
|
28d7dfeb67 | ||
|
|
6cc39d2107 | ||
|
|
a92a01095b | ||
|
|
3f4cddd2a2 | ||
|
|
aa734ea976 | ||
|
|
30628004fd | ||
|
|
a6e87b52fd | ||
|
|
337ef44b56 | ||
|
|
70b4effde8 | ||
|
|
9e4c0f5c47 | ||
|
|
535cdc72f7 | ||
|
|
8d258ecab2 | ||
|
|
3079e28f6f | ||
|
|
4970290614 | ||
|
|
e348310602 | ||
|
|
7abbd2f253 | ||
|
|
c56698492e | ||
|
|
bc4c33fcbb | ||
|
|
05d5ac960c | ||
|
|
80331401b5 | ||
|
|
b4459e1247 | ||
|
|
706c82d3ca | ||
|
|
0e1b086096 | ||
|
|
80359c98f5 | ||
|
|
034e0ec2e3 | ||
|
|
cc6839d13e | ||
|
|
0ce436c2e9 | ||
|
|
966d814281 | ||
|
|
c983bc10ab | ||
|
|
d603398da0 | ||
|
|
1404d2688f | ||
|
|
c72158cf0d | ||
|
|
1e6ed1308f | ||
|
|
fae241373a | ||
|
|
2bf4cd83df | ||
|
|
efd3290ee8 | ||
|
|
aa729d9af8 | ||
|
|
2d6d3658a1 | ||
|
|
0cbdd6281a | ||
|
|
a845a42d0d | ||
|
|
60329e086a | ||
|
|
a3174b96d7 | ||
|
|
29c00066bc | ||
|
|
b90ffc4d3f | ||
|
|
7e030320f5 | ||
|
|
c96bcad32d | ||
|
|
a655cce195 | ||
|
|
aafb0e7964 | ||
|
|
288f7f117f | ||
|
|
125ac2eda8 | ||
|
|
ea80bfae07 | ||
|
|
5e727a3547 | ||
|
|
6672b60d0f | ||
|
|
09fe1a4649 | ||
|
|
e4fa4cc413 | ||
|
|
0be60475cf | ||
|
|
b7e53185b0 | ||
|
|
cd8c76b3eb | ||
|
|
a1d7a1fe48 | ||
|
|
c48153819f | ||
|
|
d2d5590887 | ||
|
|
65b319e127 | ||
|
|
b34c0dd34e | ||
|
|
ddc1a371bf | ||
|
|
a0c0b7b195 | ||
|
|
b07cea9049 | ||
|
|
991388d4cd | ||
|
|
7adc77c823 | ||
|
|
550f93d4ae | ||
|
|
b43187e5b8 | ||
|
|
6230710bb0 | ||
|
|
ff119cbc0a | ||
|
|
2a2ec713af | ||
|
|
41eb560d1a | ||
|
|
f7b9b91144 | ||
|
|
97ab5d066e | ||
|
|
448e19d28c | ||
|
|
391f2c9c00 | ||
|
|
18524ad9eb | ||
|
|
732c35625b | ||
|
|
95857450c2 | ||
|
|
98ea7a542d | ||
|
|
338f6d47d1 | ||
|
|
fdb492c572 | ||
|
|
ce3ff8c3d8 | ||
|
|
1789d9bb67 | ||
|
|
1883b527a4 | ||
|
|
2f4a9d450a | ||
|
|
12fc33a15e | ||
|
|
e4693ecf61 | ||
|
|
c15d709db2 | ||
|
|
e09040b0fb | ||
|
|
4477c3fc2c | ||
|
|
f52a00a156 | ||
|
|
946a063993 | ||
|
|
d3fcbcb6a5 | ||
|
|
f91cfc0f79 | ||
|
|
f0bbc54fd5 | ||
|
|
3ca5f94b9b | ||
|
|
7a35ebec73 | ||
|
|
66d3623f90 | ||
|
|
8c9badd440 | ||
|
|
e0d47a474d | ||
|
|
1a8aff0594 | ||
|
|
cf5e01c213 | ||
|
|
ffae956e21 | ||
|
|
5c4cb7ff6d | ||
|
|
7794c8b89b | ||
|
|
bb78e8e8a7 | ||
|
|
00a2a95adc | ||
|
|
e55dd47f6e | ||
|
|
7b10bda0a4 | ||
|
|
d5b9aefa01 | ||
|
|
cdc3f5e1df | ||
|
|
8fc6f49fb4 | ||
|
|
29e5c14009 | ||
|
|
fe9ff205b9 | ||
|
|
87b59eccee | ||
|
|
42cb5a6c36 | ||
|
|
9826965803 | ||
|
|
47153e41d3 | ||
|
|
677b428e63 | ||
|
|
15936fcef0 | ||
|
|
b2e828e7ef | ||
|
|
ac6e5a3ae8 | ||
|
|
3722477eea | ||
|
|
72d0f76acc | ||
|
|
b7333574aa | ||
|
|
5caccbd1f5 | ||
|
|
698b559935 | ||
|
|
36d5e8f730 | ||
|
|
233721bf51 | ||
|
|
822602a38f | ||
|
|
e69dbf697c | ||
|
|
184d71a1b9 | ||
|
|
ccb9b03287 | ||
|
|
dd4ad6d72b | ||
|
|
195e2cc8f0 | ||
|
|
ef573336d8 | ||
|
|
9377a849f6 | ||
|
|
00ccbf4be2 | ||
|
|
00159d26dc | ||
|
|
b4f93adf05 | ||
|
|
5bbc9003f4 | ||
|
|
c4be926b7c | ||
|
|
147e3911f1 | ||
|
|
c7b814fc01 | ||
|
|
8525571afc | ||
|
|
5c5de91672 | ||
|
|
7dbb0a2e8f | ||
|
|
11b54ffdf9 | ||
|
|
5d76ac33dd | ||
|
|
8e92bf3766 | ||
|
|
56517eec62 | ||
|
|
ed56f0733e | ||
|
|
3fd799ee74 | ||
|
|
4ed8a67eda | ||
|
|
cbb9a8dbd0 | ||
|
|
4280b84459 | ||
|
|
e06e9ac1f8 | ||
|
|
7bea4ad282 | ||
|
|
316c14d845 | ||
|
|
d17be63312 | ||
|
|
2c2253db31 | ||
|
|
a3128e56a3 | ||
|
|
de0cf0ce67 | ||
|
|
e3f33b9d4c | ||
|
|
27143e2663 | ||
|
|
566e2fb35d | ||
|
|
dc5ada1c04 | ||
|
|
603e698f5a | ||
|
|
f893dcb19a | ||
|
|
27b116798e | ||
|
|
ae932e5710 | ||
|
|
bab5a979be | ||
|
|
1b8c9a3e0c | ||
|
|
99d0fa30ed | ||
|
|
5cc146481b | ||
|
|
102da5cba1 | ||
|
|
d374eecb18 | ||
|
|
08d31c0ffe | ||
|
|
c62e40465a | ||
|
|
5367f32d5c | ||
|
|
ad20698a42 | ||
|
|
e865d80a63 | ||
|
|
81a311e7fa | ||
|
|
5397fa8fd3 | ||
|
|
b9e08cd9e3 | ||
|
|
fe86cd6320 | ||
|
|
bba07bb83a | ||
|
|
83a53ee1f0 | ||
|
|
23ba363a0d | ||
|
|
101aee81c0 | ||
|
|
fa60d02129 | ||
|
|
6a9e5fd801 | ||
|
|
12ae7845ec | ||
|
|
045e8ee96f | ||
|
|
993f75b14e | ||
|
|
b48f3746bc | ||
|
|
dcc477acbc | ||
|
|
5f4f007b31 | ||
|
|
9d4788cb7c | ||
|
|
5a1380f9ef | ||
|
|
0d82aa26f3 | ||
|
|
05f2ec19f2 | ||
|
|
25c3c9c565 | ||
|
|
d6e7e3d4a0 | ||
|
|
6148112904 | ||
|
|
ba338e9282 | ||
|
|
2e132690d1 | ||
|
|
8f1b6ca3b7 | ||
|
|
85d0252972 | ||
|
|
077f72a0c5 | ||
|
|
25bec513f3 | ||
|
|
0a6378d84c | ||
|
|
1e6034ce37 | ||
|
|
ef6e5ebcbe | ||
|
|
19a42b7463 | ||
|
|
9d0e608454 | ||
|
|
bd9a98dbfd | ||
|
|
0ffca3dfe4 | ||
|
|
01958559ee | ||
|
|
bb4aef206a | ||
|
|
e74227b0db | ||
|
|
a47aec1cc4 | ||
|
|
cd9d8cde3e | ||
|
|
d4f68d5ec7 | ||
|
|
a910cfcb35 | ||
|
|
962de503fd | ||
|
|
f40e72c882 | ||
|
|
b7993e1d0e | ||
|
|
d6e5e73e82 | ||
|
|
1794f87f99 | ||
|
|
81c7d57f30 | ||
|
|
b4f0149bd8 | ||
|
|
87ec06eef0 | ||
|
|
c764548214 | ||
|
|
c78af7c3ad | ||
|
|
f9cd0d4e29 | ||
|
|
ee03435ca7 | ||
|
|
a9b83256c6 | ||
|
|
7086ddcf58 | ||
|
|
26d226bb17 | ||
|
|
dc4742bf5c | ||
|
|
933df5fae4 | ||
|
|
4b17ac19dc | ||
|
|
43f6a4f499 | ||
|
|
076b99676d | ||
|
|
b3dfa05a4a | ||
|
|
c5946b1a0b | ||
|
|
58a85eabdf | ||
|
|
8a1d9b96dd | ||
|
|
36a3d2f766 | ||
|
|
68010076f9 | ||
|
|
92da783e82 | ||
|
|
7d1e343d25 | ||
|
|
84d17b7255 | ||
|
|
64e06a32ec | ||
|
|
808b4a687d | ||
|
|
954f2c235b | ||
|
|
51a7bbbfb8 | ||
|
|
9b5f5857bb | ||
|
|
e66f52a8ce | ||
|
|
8023bcea96 | ||
|
|
dfb7491503 | ||
|
|
b43d9c963a | ||
|
|
51c0637134 | ||
|
|
b16c23190b | ||
|
|
fddf2892cd | ||
|
|
53b5715da0 | ||
|
|
3a79fcb366 | ||
|
|
4df034ac55 | ||
|
|
a0bb04fe3a | ||
|
|
a923f28a8d | ||
|
|
78642f1c9b | ||
|
|
879ba17760 | ||
|
|
40b0643eaa | ||
|
|
0a776c6a72 | ||
|
|
e7a8d5232f | ||
|
|
f8ec47bfdb | ||
|
|
4e84121553 | ||
|
|
6ba3d44fa1 |
@@ -7,6 +7,8 @@
|
||||
}],
|
||||
"@babel/plugin-proposal-optional-catch-binding",
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
"@babel/plugin-proposal-logical-assignment-operators",
|
||||
"@babel/plugin-external-helpers"
|
||||
]
|
||||
}
|
||||
|
||||
+1
-4
@@ -15,8 +15,5 @@ trim_trailing_whitespace = true
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.nsh]
|
||||
indent_size = 2
|
||||
|
||||
[{*.yml,*.yaml}]
|
||||
[{*.nsh,*.yml,*.yaml,*.json}]
|
||||
indent_size = 2
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
"import/no-webpack-loader-syntax": "off",
|
||||
"import/no-relative-parent-imports": "error",
|
||||
"import/first": "error",
|
||||
"import/no-namespace": "error",
|
||||
"import/no-default-export": "error",
|
||||
"babel/no-unused-expressions": "error",
|
||||
"node/no-callback-literal": "off"
|
||||
|
||||
@@ -24,7 +24,7 @@ A clear and concise description of what you expected to happen.
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment**
|
||||
Your user-agent (from Settings/Help section)
|
||||
Please copy all version information from Settings/Help
|
||||
|
||||
**Kdbx File**
|
||||
Does it happen on Demo or New database?
|
||||
|
||||
@@ -1,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();
|
||||
+32
-163
@@ -18,6 +18,11 @@ jobs:
|
||||
with:
|
||||
repository: keeweb/keeweb
|
||||
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
|
||||
- name: Upgrade Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install npm modules
|
||||
run: npm ci
|
||||
- name: Test
|
||||
@@ -117,6 +122,11 @@ jobs:
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: dist
|
||||
- name: Upgrade Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install npm modules
|
||||
run: npm ci
|
||||
- name: Install grunt
|
||||
@@ -171,6 +181,11 @@ jobs:
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: dist
|
||||
- name: Upgrade Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install npm modules
|
||||
run: npm ci
|
||||
- name: Install grunt
|
||||
@@ -246,6 +261,11 @@ jobs:
|
||||
repository: keeweb/keeweb
|
||||
path: keeweb
|
||||
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
|
||||
- name: Upgrade Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install npm modules
|
||||
working-directory: keeweb
|
||||
run: npm ci
|
||||
@@ -349,173 +369,22 @@ jobs:
|
||||
run: cp -r html keeweb/package/docker/dist
|
||||
- name: Build the Docker image
|
||||
working-directory: keeweb
|
||||
run: docker build -t antelle/keeweb:latest package/docker
|
||||
- name: Tag the Docker image
|
||||
run: docker tag antelle/keeweb:latest antelle/keeweb:${{ steps.get_tag.outputs.tag }}
|
||||
- name: Push the Docker image to the registry
|
||||
run: docker build -t antelle/keeweb:latest -t antelle/keeweb:${{ steps.get_tag.outputs.tag }} package/docker
|
||||
- name: Push the Docker image to dockerhub
|
||||
if: ${{ github.repository == 'keeweb/keeweb' }}
|
||||
run: docker push antelle/keeweb
|
||||
- name: Create a GitHub release
|
||||
id: create_release
|
||||
uses: actions/create-release@latest
|
||||
run: docker push --all-tags antelle/keeweb
|
||||
- name: Extract release notes
|
||||
run: node util/extract-release-notes.js --version=${{ steps.get_tag.outputs.tag }} --output=tmp/release-body.md
|
||||
working-directory: keeweb
|
||||
- name: Publish GitHub release
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Desktop apps v${{ steps.get_tag.outputs.tag }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
- name: Upload html asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.html.zip
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html.zip
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload linux.AppImage asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.AppImage
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.AppImage
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload linux.snap asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.snap
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.snap
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload linux.deb asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.deb
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.deb
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload linux.zip asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.zip
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.zip
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload linux.rpm asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload darwin.x64.dmg asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.x64.dmg
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.x64.dmg
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload darwin.arm64.dmg asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.arm64.dmg
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.arm64.dmg
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.ia32.exe asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.exe
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.exe
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.ia32.zip asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.zip
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.zip
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.x64.exe asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.exe
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.exe
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.x64.zip asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.zip
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.zip
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.arm64.exe asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.exe
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.exe
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.arm64.zip asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload verify.sign asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/Verify.sign.sha256
|
||||
asset_name: Verify.sign.sha256
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload verify.sha asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/Verify.sha256
|
||||
asset_name: Verify.sha256
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Publish the GitHub release
|
||||
uses: ./keeweb/.github/actions/publish-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
version: ${{ steps.get_tag.outputs.tag }}
|
||||
release_id: ${{ steps.create_release.outputs.id }}
|
||||
tag_name: v${{ steps.get_tag.outputs.tag }}
|
||||
name: Desktop apps v${{ steps.get_tag.outputs.tag }}
|
||||
files: assets/*
|
||||
body_path: keeweb/tmp/release-body.md
|
||||
- name: Checkout gh-pages
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
|
||||
+1
-1
@@ -14,6 +14,6 @@ obj/
|
||||
xcuserdata/
|
||||
*.suo
|
||||
.idea/
|
||||
.vscode
|
||||
.vscode/
|
||||
*.iml
|
||||
test/dist/
|
||||
|
||||
+49
-28
@@ -47,7 +47,8 @@ module.exports = function (grunt) {
|
||||
const webpackOptions = {
|
||||
date,
|
||||
beta: !!grunt.option('beta'),
|
||||
sha
|
||||
sha,
|
||||
appleTeamId: '3LE7JZ657W'
|
||||
};
|
||||
|
||||
const windowsAppVersionString = {
|
||||
@@ -144,18 +145,6 @@ module.exports = function (grunt) {
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
'desktop-darwin-helper-x64': {
|
||||
src: 'helper/darwin/KeeWebHelper',
|
||||
dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Resources/',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'desktop-darwin-helper-arm64': {
|
||||
src: 'helper/darwin/KeeWebHelper',
|
||||
dest: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/Resources/',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'desktop-darwin-installer-helper-x64': {
|
||||
cwd: 'tmp/desktop/KeeWeb Installer.app',
|
||||
src: '**',
|
||||
@@ -174,21 +163,6 @@ module.exports = function (grunt) {
|
||||
nonull: true,
|
||||
options: { mode: true }
|
||||
},
|
||||
'desktop-windows-helper-x64': {
|
||||
src: 'helper/win32/KeeWebHelper.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-x64/Resources/',
|
||||
nonull: true
|
||||
},
|
||||
'desktop-windows-helper-ia32': {
|
||||
src: 'helper/win32/KeeWebHelper.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-ia32/Resources/',
|
||||
nonull: true
|
||||
},
|
||||
'desktop-windows-helper-arm64': {
|
||||
src: 'helper/win32/KeeWebHelper.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-arm64/Resources/',
|
||||
nonull: true
|
||||
},
|
||||
'desktop-win32-dist-x64': {
|
||||
src: 'tmp/desktop/KeeWeb.win.x64.exe',
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.win.x64.exe`,
|
||||
@@ -253,6 +227,47 @@ module.exports = function (grunt) {
|
||||
src: 'graphics/icon.icns',
|
||||
dest: 'tmp/desktop/KeeWeb Installer.app/Contents/Resources/applet.icns',
|
||||
nonull: true
|
||||
},
|
||||
'native-messaging-host-darwin-x64': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/darwin-x64/keeweb-native-messaging-host',
|
||||
dest:
|
||||
'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/MacOS/util/keeweb-native-messaging-host',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'native-messaging-host-darwin-arm64': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/darwin-arm64/keeweb-native-messaging-host',
|
||||
dest:
|
||||
'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/MacOS/util/keeweb-native-messaging-host',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'native-messaging-host-linux-x64': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/linux-x64/keeweb-native-messaging-host',
|
||||
dest: 'tmp/desktop/keeweb-linux-x64/keeweb-native-messaging-host',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'native-messaging-host-win32-x64': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/win32-x64/keeweb-native-messaging-host.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-x64/keeweb-native-messaging-host.exe',
|
||||
nonull: true
|
||||
},
|
||||
'native-messaging-host-win32-ia32': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/win32-ia32/keeweb-native-messaging-host.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-ia32/keeweb-native-messaging-host.exe',
|
||||
nonull: true
|
||||
},
|
||||
'native-messaging-host-win32-arm64': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/win32-arm64/keeweb-native-messaging-host.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-arm64/keeweb-native-messaging-host.exe',
|
||||
nonull: true
|
||||
}
|
||||
},
|
||||
eslint: {
|
||||
@@ -668,6 +683,8 @@ module.exports = function (grunt) {
|
||||
options: {
|
||||
files: {
|
||||
'tmp/desktop/KeeWeb-win32-x64/KeeWeb.exe': 'KeeWeb',
|
||||
'tmp/desktop/KeeWeb-win32-x64/keeweb-native-messaging-host.exe':
|
||||
'KeeWeb Native Messaging Host',
|
||||
'tmp/desktop/KeeWeb-win32-x64/ffmpeg.dll': '',
|
||||
'tmp/desktop/KeeWeb-win32-x64/libEGL.dll':
|
||||
'ANGLE libEGL Dynamic Link Library',
|
||||
@@ -680,6 +697,8 @@ module.exports = function (grunt) {
|
||||
options: {
|
||||
files: {
|
||||
'tmp/desktop/KeeWeb-win32-ia32/KeeWeb.exe': 'KeeWeb',
|
||||
'tmp/desktop/KeeWeb-win32-ia32/keeweb-native-messaging-host.exe':
|
||||
'KeeWeb Native Messaging Host',
|
||||
'tmp/desktop/KeeWeb-win32-ia32/ffmpeg.dll': '',
|
||||
'tmp/desktop/KeeWeb-win32-ia32/libEGL.dll':
|
||||
'ANGLE libEGL Dynamic Link Library',
|
||||
@@ -692,6 +711,8 @@ module.exports = function (grunt) {
|
||||
options: {
|
||||
files: {
|
||||
'tmp/desktop/KeeWeb-win32-arm64/KeeWeb.exe': 'KeeWeb',
|
||||
'tmp/desktop/KeeWeb-win32-arm64/keeweb-native-messaging-host.exe':
|
||||
'KeeWeb Native Messaging Host',
|
||||
'tmp/desktop/KeeWeb-win32-arm64/ffmpeg.dll': '',
|
||||
'tmp/desktop/KeeWeb-win32-arm64/libEGL.dll':
|
||||
'ANGLE libEGL Dynamic Link Library',
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>KeeWeb</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.opener.postMessage(
|
||||
{ storage: 'teams', search: location.search },
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
content="
|
||||
default-src 'self';
|
||||
font-src data:;
|
||||
script-src 'self' blob: 'unsafe-eval';
|
||||
script-src 'self' 'unsafe-eval';
|
||||
style-src 'self' blob:;
|
||||
connect-src 'self' ws: https:;
|
||||
child-src 'self' blob:;
|
||||
|
||||
+12
-5
@@ -6,6 +6,7 @@ import { ExportApi } from 'comp/app/export-api';
|
||||
import { SingleInstanceChecker } from 'comp/app/single-instance-checker';
|
||||
import { Updater } from 'comp/app/updater';
|
||||
import { UsbListener } from 'comp/app/usb-listener';
|
||||
import { BrowserExtensionConnector } from 'comp/extension/browser-extension-connector';
|
||||
import { FeatureTester } from 'comp/browser/feature-tester';
|
||||
import { FocusDetector } from 'comp/browser/focus-detector';
|
||||
import { IdleTracker } from 'comp/browser/idle-tracker';
|
||||
@@ -45,6 +46,7 @@ ready(() => {
|
||||
.then(loadRemoteConfig)
|
||||
.then(ensureCanRun)
|
||||
.then(initStorage)
|
||||
.then(initUsbListener)
|
||||
.then(showApp)
|
||||
.then(postInit)
|
||||
.catch((e) => {
|
||||
@@ -142,6 +144,11 @@ ready(() => {
|
||||
StartProfiler.milestone('initializing storage');
|
||||
}
|
||||
|
||||
function initUsbListener() {
|
||||
UsbListener.init();
|
||||
StartProfiler.milestone('starting usb');
|
||||
}
|
||||
|
||||
function showApp() {
|
||||
return Promise.resolve().then(() => {
|
||||
const skipHttpsWarning =
|
||||
@@ -172,12 +179,12 @@ ready(() => {
|
||||
}
|
||||
|
||||
function postInit() {
|
||||
Updater.init();
|
||||
SingleInstanceChecker.init();
|
||||
AppRightsChecker.init();
|
||||
IdleTracker.init();
|
||||
UsbListener.init();
|
||||
setTimeout(() => {
|
||||
Updater.init();
|
||||
SingleInstanceChecker.init();
|
||||
AppRightsChecker.init();
|
||||
IdleTracker.init();
|
||||
BrowserExtensionConnector.init(appModel);
|
||||
PluginManager.runAutoUpdate();
|
||||
}, Timeouts.AutoUpdatePluginsAfterStart);
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { AutoTypeEmitter } from 'auto-type/auto-type-emitter';
|
||||
|
||||
const AutoTypeEmitterFactory = {
|
||||
create(callback, windowId) {
|
||||
if (Launcher && Launcher.autoTypeSupported) {
|
||||
if (AppSettingsModel.useLegacyAutoType) {
|
||||
const { AutoTypeEmitter } = require('./emitter/auto-type-emitter-' +
|
||||
Launcher.platform());
|
||||
return new AutoTypeEmitter(callback, windowId);
|
||||
}
|
||||
return new AutoTypeEmitter(callback, windowId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export { AutoTypeEmitterFactory };
|
||||
@@ -1,90 +0,0 @@
|
||||
import { SearchResultCollection } from 'collections/search-result-collection';
|
||||
import { Ranking } from 'util/data/ranking';
|
||||
|
||||
const urlPartsRegex = /^(\w+:\/\/)?(?:(?:www|wwws|secure)\.)?([^\/]+)\/?(.*)/;
|
||||
|
||||
const AutoTypeFilter = function (windowInfo, appModel) {
|
||||
this.title = windowInfo.title;
|
||||
this.url = windowInfo.url;
|
||||
this.text = '';
|
||||
this.ignoreWindowInfo = false;
|
||||
this.appModel = appModel;
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.getEntries = function () {
|
||||
const filter = {
|
||||
text: this.text,
|
||||
autoType: true
|
||||
};
|
||||
this.prepareFilter();
|
||||
let entries = this.appModel.getEntriesByFilter(filter).map((e) => [e, this.getEntryRank(e)]);
|
||||
if (!this.ignoreWindowInfo) {
|
||||
entries = entries.filter((e) => e[1]);
|
||||
}
|
||||
entries = entries.sort((x, y) =>
|
||||
x[1] === y[1] ? x[0].title.localeCompare(y[0].title) : y[1] - x[1]
|
||||
);
|
||||
entries = entries.map((p) => p[0]);
|
||||
return new SearchResultCollection(entries, { comparator: 'none' });
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.hasWindowInfo = function () {
|
||||
return this.title || this.url;
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.prepareFilter = function () {
|
||||
this.titleLower = this.title ? this.title.toLowerCase() : null;
|
||||
this.urlLower = this.url ? this.url.toLowerCase() : null;
|
||||
this.urlParts = this.url ? urlPartsRegex.exec(this.urlLower) : null;
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.getEntryRank = function (entry) {
|
||||
let rank = 0;
|
||||
if (this.titleLower && entry.title) {
|
||||
rank += Ranking.getStringRank(entry.title.toLowerCase(), this.titleLower);
|
||||
}
|
||||
if (this.urlParts) {
|
||||
if (entry.url) {
|
||||
const entryUrlParts = urlPartsRegex.exec(entry.url.toLowerCase());
|
||||
if (entryUrlParts) {
|
||||
const [, scheme, domain, path] = entryUrlParts;
|
||||
const [, thisScheme, thisDomain, thisPath] = this.urlParts;
|
||||
if (domain === thisDomain || thisDomain.indexOf('.' + domain) > 0) {
|
||||
if (domain === thisDomain) {
|
||||
rank += 20;
|
||||
} else {
|
||||
rank += 10;
|
||||
}
|
||||
if (path === thisPath) {
|
||||
rank += 10;
|
||||
} else if (path && thisPath) {
|
||||
if (path.lastIndexOf(thisPath, 0) === 0) {
|
||||
rank += 5;
|
||||
} else if (thisPath.lastIndexOf(path, 0) === 0) {
|
||||
rank += 3;
|
||||
}
|
||||
}
|
||||
if (scheme === thisScheme) {
|
||||
rank += 1;
|
||||
}
|
||||
} else {
|
||||
if (entry.searchText.indexOf(this.urlLower) >= 0) {
|
||||
// the url is in some field; include it
|
||||
rank += 5;
|
||||
} else {
|
||||
// another domain; don't show this record at all, ignore title match
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (entry.searchText.indexOf(this.urlLower) >= 0) {
|
||||
// the url is in some field; include it
|
||||
rank += 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
return rank;
|
||||
};
|
||||
|
||||
export { AutoTypeFilter };
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { AutoTypeHelper } from 'auto-type/auto-type-helper';
|
||||
|
||||
const AutoTypeHelperFactory = {
|
||||
create() {
|
||||
if (Launcher && Launcher.autoTypeSupported) {
|
||||
if (AppSettingsModel.useLegacyAutoType) {
|
||||
const { AutoTypeHelper } = require('./helper/auto-type-helper-' +
|
||||
Launcher.platform());
|
||||
return new AutoTypeHelper();
|
||||
}
|
||||
return new AutoTypeHelper();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export { AutoTypeHelperFactory };
|
||||
@@ -1,8 +1,7 @@
|
||||
import { AutoTypeEmitterFactory } from 'auto-type/auto-type-emitter-factory';
|
||||
import { AutoTypeEmitter } from 'auto-type/auto-type-emitter';
|
||||
import { AutoTypeObfuscator } from 'auto-type/auto-type-obfuscator';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
import { Logger } from 'util/logger';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
|
||||
const emitterLogger = new Logger(
|
||||
'auto-type-emitter',
|
||||
@@ -433,9 +432,7 @@ AutoTypeRunner.prototype.obfuscateOp = function (op) {
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.run = function (callback, windowId) {
|
||||
const emitterType = AppSettingsModel.useLegacyAutoType ? 'legacy' : 'native';
|
||||
emitterLogger.info(`Using ${emitterType} auto-type emitter`);
|
||||
this.emitter = AutoTypeEmitterFactory.create(this.emitNext.bind(this), windowId);
|
||||
this.emitter = new AutoTypeEmitter(this.emitNext.bind(this), windowId);
|
||||
this.emitterState = {
|
||||
callback,
|
||||
stack: [],
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
import { AutoTypeNativeHelper } from 'auto-type/helper/auto-type-native-helper';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
// http://eastmanreference.com/complete-list-of-applescript-key-codes/
|
||||
const KeyMap = {
|
||||
tab: 48,
|
||||
enter: 36,
|
||||
space: 49,
|
||||
up: 126,
|
||||
down: 125,
|
||||
left: 123,
|
||||
right: 124,
|
||||
home: 115,
|
||||
end: 119,
|
||||
pgup: 116,
|
||||
pgdn: 121,
|
||||
ins: 114,
|
||||
del: 117,
|
||||
bs: 51,
|
||||
esc: 53,
|
||||
win: 55,
|
||||
rwin: 55,
|
||||
f1: 122,
|
||||
f2: 120,
|
||||
f3: 99,
|
||||
f4: 118,
|
||||
f5: 96,
|
||||
f6: 97,
|
||||
f7: 98,
|
||||
f8: 100,
|
||||
f9: 101,
|
||||
f10: 109,
|
||||
f11: 103,
|
||||
f12: 111,
|
||||
f13: 105,
|
||||
f14: 107,
|
||||
f15: 113,
|
||||
f16: 106,
|
||||
add: 69,
|
||||
subtract: 78,
|
||||
multiply: 67,
|
||||
divide: 75,
|
||||
n0: 82,
|
||||
n1: 83,
|
||||
n2: 84,
|
||||
n3: 85,
|
||||
n4: 86,
|
||||
n5: 87,
|
||||
n6: 88,
|
||||
n7: 89,
|
||||
n8: 91,
|
||||
n9: 92
|
||||
};
|
||||
|
||||
const ModMap = {
|
||||
'^': '@',
|
||||
'+': '+',
|
||||
'%': '%',
|
||||
'^^': '^'
|
||||
};
|
||||
|
||||
const AutoTypeEmitter = function (callback) {
|
||||
this.callback = callback;
|
||||
this.mod = {};
|
||||
this.pendingScript = [];
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.begin = function () {
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
|
||||
if (enabled) {
|
||||
this.mod[ModMap[mod]] = true;
|
||||
} else {
|
||||
delete this.mod[ModMap[mod]];
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.text = function (text) {
|
||||
this.pendingScript.push('text ' + this.modString() + ' ' + text);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.key = function (key) {
|
||||
if (typeof key !== 'number') {
|
||||
if (!KeyMap[key]) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
key = KeyMap[key];
|
||||
}
|
||||
this.pendingScript.push('key ' + this.modString() + key);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.copyPaste = function (text) {
|
||||
this.pendingScript.push('copypaste ' + text);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.wait = function (time) {
|
||||
this.pendingScript.push('wait ' + time);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.waitComplete = function () {
|
||||
if (this.pendingScript.length) {
|
||||
const script = this.pendingScript.join('\n');
|
||||
this.pendingScript.length = 0;
|
||||
this.runScript(script);
|
||||
} else {
|
||||
this.callback();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setDelay = function (delay) {
|
||||
this.delay = delay || 0;
|
||||
this.callback('Not implemented');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.modString = function () {
|
||||
return Object.keys(this.mod).join('');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.runScript = function (script) {
|
||||
Launcher.spawn({
|
||||
cmd: AutoTypeNativeHelper.getHelperPath(),
|
||||
data: script,
|
||||
complete: this.callback
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeEmitter };
|
||||
@@ -1,162 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Locale } from 'util/locale';
|
||||
|
||||
// https://cgit.freedesktop.org/xorg/proto/x11proto/plain/keysymdef.h
|
||||
const KeyMap = {
|
||||
tab: 'Tab',
|
||||
enter: 'KP_Enter',
|
||||
space: 'KP_Space',
|
||||
up: 'Up',
|
||||
down: 'Down',
|
||||
left: 'Left',
|
||||
right: 'Right',
|
||||
home: 'Home',
|
||||
end: 'End',
|
||||
pgup: 'Page_Up',
|
||||
pgdn: 'Page_Down',
|
||||
ins: 'Insert',
|
||||
del: 'Delete',
|
||||
bs: 'BackSpace',
|
||||
esc: 'Escape',
|
||||
win: 'Meta_L',
|
||||
rwin: 'Meta_R',
|
||||
f1: 'F1',
|
||||
f2: 'F2',
|
||||
f3: 'F3',
|
||||
f4: 'F4',
|
||||
f5: 'F5',
|
||||
f6: 'F6',
|
||||
f7: 'F7',
|
||||
f8: 'F8',
|
||||
f9: 'F9',
|
||||
f10: 'F10',
|
||||
f11: 'F11',
|
||||
f12: 'F12',
|
||||
f13: 'F13',
|
||||
f14: 'F14',
|
||||
f15: 'F15',
|
||||
f16: 'F16',
|
||||
add: 'KP_Add',
|
||||
subtract: 'KP_Subtract',
|
||||
multiply: 'KP_Multiply',
|
||||
divide: 'KP_Divide',
|
||||
n0: 'KP_0',
|
||||
n1: 'KP_1',
|
||||
n2: 'KP_2',
|
||||
n3: 'KP_3',
|
||||
n4: 'KP_4',
|
||||
n5: 'KP_5',
|
||||
n6: 'KP_6',
|
||||
n7: 'KP_7',
|
||||
n8: 'KP_8',
|
||||
n9: 'KP_9'
|
||||
};
|
||||
|
||||
const ModMap = {
|
||||
'^': 'ctrl',
|
||||
'+': 'shift',
|
||||
'%': 'alt',
|
||||
'^^': 'ctrl'
|
||||
};
|
||||
|
||||
const AutoTypeEmitter = function (callback, windowId) {
|
||||
this.callback = callback;
|
||||
this.mod = {};
|
||||
this.pendingScript = [];
|
||||
if (typeof windowId !== 'undefined' && windowId) {
|
||||
this.pendingScript.push('windowactivate --sync ' + windowId);
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.begin = function () {
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
|
||||
if (enabled) {
|
||||
this.mod[ModMap[mod]] = true;
|
||||
} else {
|
||||
delete this.mod[ModMap[mod]];
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.text = function (text) {
|
||||
this.pendingScript.push('keyup ctrl alt shift t');
|
||||
Object.keys(this.mod).forEach((mod) => {
|
||||
this.pendingScript.push('keydown ' + ModMap[mod]);
|
||||
});
|
||||
text.split('').forEach((char) => {
|
||||
this.pendingScript.push('key ' + 'U' + char.charCodeAt(0).toString(16));
|
||||
});
|
||||
Object.keys(this.mod).forEach((mod) => {
|
||||
this.pendingScript.push('keyup ' + ModMap[mod]);
|
||||
});
|
||||
this.waitComplete();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.key = function (key) {
|
||||
const isSpecialKey = typeof key !== 'number';
|
||||
if (isSpecialKey) {
|
||||
if (!KeyMap[key]) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
key = KeyMap[key].toString();
|
||||
}
|
||||
this.pendingScript.push('key --clearmodifiers ' + this.modString() + key);
|
||||
if (isSpecialKey) {
|
||||
this.waitComplete();
|
||||
} else {
|
||||
this.callback();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.copyPaste = function (text) {
|
||||
this.pendingScript.push('sleep 0.5');
|
||||
Launcher.setClipboardText(text);
|
||||
this.pendingScript.push('key --clearmodifiers ' + 'shift+Insert');
|
||||
this.pendingScript.push('sleep 0.5');
|
||||
this.waitComplete();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.wait = function (time) {
|
||||
this.pendingScript.push('sleep ' + time / 1000);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.waitComplete = function (callback) {
|
||||
if (this.pendingScript.length) {
|
||||
const script = this.pendingScript.join(' ');
|
||||
this.pendingScript.length = 0;
|
||||
this.runScript(script, callback);
|
||||
} else {
|
||||
this.callback();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.modString = function () {
|
||||
let mod = '';
|
||||
Object.keys(this.mod).forEach((key) => {
|
||||
mod += key + '+';
|
||||
});
|
||||
return mod;
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.runScript = function (script, callback) {
|
||||
// xdotool doesn't like it when stdin doesn't end with a linebreak
|
||||
// see https://github.com/keeweb/keeweb/issues/1409
|
||||
const data = script + '\n';
|
||||
Launcher.spawn({
|
||||
cmd: 'xdotool',
|
||||
args: ['-'],
|
||||
data,
|
||||
complete: (err, stdout, code) => {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
err = Locale.autoTypeErrorNotInstalled.replace('{}', 'xdotool');
|
||||
}
|
||||
const cb = callback || this.callback;
|
||||
cb(err, stdout, code);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeEmitter };
|
||||
@@ -1,135 +0,0 @@
|
||||
import { AutoTypeNativeHelper } from 'auto-type/helper/auto-type-native-helper';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||
const KeyMap = {
|
||||
tab: 0x09,
|
||||
enter: 0x0d,
|
||||
space: 0x20,
|
||||
up: 0x26,
|
||||
down: 0x28,
|
||||
left: 0x25,
|
||||
right: 0x27,
|
||||
home: 0x24,
|
||||
end: 0x23,
|
||||
pgup: 0x21,
|
||||
pgdn: 0x22,
|
||||
ins: 0x2d,
|
||||
del: 0x2e,
|
||||
bs: 0x08,
|
||||
esc: 0x1b,
|
||||
win: 0x5b,
|
||||
rwin: 0x5c,
|
||||
f1: 0x70,
|
||||
f2: 0x71,
|
||||
f3: 0x72,
|
||||
f4: 0x73,
|
||||
f5: 0x74,
|
||||
f6: 0x75,
|
||||
f7: 0x76,
|
||||
f8: 0x77,
|
||||
f9: 0x78,
|
||||
f10: 0x79,
|
||||
f11: 0x7a,
|
||||
f12: 0x7b,
|
||||
f13: 0x7c,
|
||||
f14: 0x7d,
|
||||
f15: 0x7e,
|
||||
f16: 0x7f,
|
||||
add: 0x6b,
|
||||
subtract: 0x6d,
|
||||
multiply: 0x6a,
|
||||
divide: 0x6f,
|
||||
n0: 0x30,
|
||||
n1: 0x31,
|
||||
n2: 0x32,
|
||||
n3: 0x33,
|
||||
n4: 0x34,
|
||||
n5: 0x35,
|
||||
n6: 0x36,
|
||||
n7: 0x37,
|
||||
n8: 0x38,
|
||||
n9: 0x39
|
||||
};
|
||||
|
||||
const ModMap = {
|
||||
'^': '^',
|
||||
'+': '+',
|
||||
'%': '%',
|
||||
'^^': '^'
|
||||
};
|
||||
|
||||
const AutoTypeEmitter = function (callback) {
|
||||
this.callback = callback;
|
||||
this.mod = {};
|
||||
this.pendingScript = [];
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.begin = function () {
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
|
||||
if (enabled) {
|
||||
this.mod[ModMap[mod]] = true;
|
||||
} else {
|
||||
delete this.mod[ModMap[mod]];
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.text = function (text) {
|
||||
if (text) {
|
||||
this.pendingScript.push('text ' + this.modStr() + ' ' + text);
|
||||
}
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.key = function (key) {
|
||||
if (typeof key !== 'number') {
|
||||
if (!KeyMap[key]) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
key = KeyMap[key];
|
||||
}
|
||||
this.pendingScript.push('key ' + this.modStr() + key);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.copyPaste = function (text) {
|
||||
this.pendingScript.push('copypaste ' + text);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.wait = function (time) {
|
||||
this.pendingScript.push('wait ' + time);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.waitComplete = function () {
|
||||
if (this.pendingScript.length) {
|
||||
const script = this.pendingScript.join('\n');
|
||||
this.pendingScript.length = 0;
|
||||
this.runScript(script);
|
||||
} else {
|
||||
this.callback();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setDelay = function (delay) {
|
||||
this.delay = delay || 0;
|
||||
this.callback('Not implemented');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.modStr = function () {
|
||||
return Object.keys(this.mod).join('');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.runScript = function (script) {
|
||||
Launcher.spawn({
|
||||
cmd: AutoTypeNativeHelper.getHelperPath(),
|
||||
data: script,
|
||||
complete: this.callback
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeEmitter };
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const ForeMostAppScript =
|
||||
'tell application "System Events"\n' +
|
||||
' set frontAppName to name of first process whose frontmost is true\n' +
|
||||
' set frontAppId to id of first process whose frontmost is true\n' +
|
||||
'end tell\n' +
|
||||
'"" & frontAppId & " " & frontAppName';
|
||||
const ChromeScript =
|
||||
'tell application "{}" to set appUrl to URL of active tab of front window\n' +
|
||||
'tell application "{}" to set appTitle to title of active tab of front window\n' +
|
||||
'return appUrl & "\n" & appTitle';
|
||||
const SafariScript =
|
||||
'tell application "{}" to set appUrl to URL of front document\n' +
|
||||
'tell application "{}" to set appTitle to name of front document\n' +
|
||||
'return appUrl & "\n" & appTitle';
|
||||
const OtherAppsScript =
|
||||
'tell application "System Events"\n' +
|
||||
' tell process "{}"\n' +
|
||||
' tell (1st window whose value of attribute "AXMain" is true)\n' +
|
||||
' set windowTitle to value of attribute "AXTitle"\n' +
|
||||
' end tell\n' +
|
||||
' end tell\n' +
|
||||
'end tell';
|
||||
|
||||
const AutoTypeHelper = function () {};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowInfo = function (callback) {
|
||||
AutoTypeHelper.exec(ForeMostAppScript, (err, out) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
const output = out.trim();
|
||||
const spaceIx = output.indexOf(' ');
|
||||
let id = '',
|
||||
appName = '';
|
||||
if (spaceIx >= 0) {
|
||||
id = output.substr(0, spaceIx);
|
||||
appName = output.substr(spaceIx + 1).trim();
|
||||
}
|
||||
// getting urls and titles from Chrome or Safari:
|
||||
// - will suit in 90% cases
|
||||
// - does not require assistive access
|
||||
// - allows to get url
|
||||
if (['Google Chrome', 'Chromium', 'Google Chrome Canary'].indexOf(appName) >= 0) {
|
||||
AutoTypeHelper.exec(ChromeScript.replace(/\{}/g, appName), (err, out) => {
|
||||
if (err) {
|
||||
return callback(err, { id });
|
||||
}
|
||||
const parts = out.split('\n');
|
||||
return callback(null, {
|
||||
id,
|
||||
url: parts[0].trim(),
|
||||
title: (parts[1] || '').trim()
|
||||
});
|
||||
});
|
||||
} else if (['Safari', 'Webkit'].indexOf(appName) >= 0) {
|
||||
AutoTypeHelper.exec(SafariScript.replace(/\{}/g, appName), (err, out) => {
|
||||
if (err) {
|
||||
return callback(err, { id });
|
||||
}
|
||||
const parts = out.split('\n');
|
||||
return callback(null, {
|
||||
id,
|
||||
url: parts[0].trim(),
|
||||
title: (parts[1] || '').trim()
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// special cases are not available. this method may ask the user about assistive access
|
||||
AutoTypeHelper.exec(OtherAppsScript.replace(/\{}/g, appName), (err, out) => {
|
||||
if (err) {
|
||||
return callback(err, { id });
|
||||
}
|
||||
return callback(null, {
|
||||
id,
|
||||
title: out.trim()
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
AutoTypeHelper.exec = function (script, callback) {
|
||||
Launcher.spawn({
|
||||
cmd: 'osascript',
|
||||
args: ['-e', script],
|
||||
complete: callback
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeHelper };
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const AutoTypeHelper = function () {};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowInfo = function (callback) {
|
||||
Launcher.spawn({
|
||||
cmd: 'xdotool',
|
||||
args: ['getactivewindow', 'getwindowname', 'getactivewindow'],
|
||||
complete(err, out) {
|
||||
let windowInfo;
|
||||
if (out) {
|
||||
const [title, id] = out.trim().split('\n');
|
||||
windowInfo = {
|
||||
id,
|
||||
title
|
||||
};
|
||||
}
|
||||
return callback(err, windowInfo);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeHelper };
|
||||
@@ -1,25 +0,0 @@
|
||||
import { AutoTypeNativeHelper } from 'auto-type/helper/auto-type-native-helper';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const AutoTypeHelper = function () {};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowInfo = function (callback) {
|
||||
Launcher.spawn({
|
||||
cmd: AutoTypeNativeHelper.getHelperPath(),
|
||||
args: ['--window-info'],
|
||||
complete(err, out) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
const [id, title, url] = out.trim().split('\n');
|
||||
const windowInfo = {
|
||||
id,
|
||||
title,
|
||||
url
|
||||
};
|
||||
return callback(null, windowInfo);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeHelper };
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Logger } from 'util/logger';
|
||||
|
||||
const logger = new Logger('auto-type');
|
||||
|
||||
const AutoTypeNativeHelper = {
|
||||
getHelperPath() {
|
||||
if (this._helperPath) {
|
||||
return this._helperPath;
|
||||
}
|
||||
const ext = process.platform === 'win32' ? '.exe' : '';
|
||||
const helperRelPath = `helper/${process.platform}/KeeWebHelper${ext}`;
|
||||
const helperPath = Launcher.getAppPath(helperRelPath);
|
||||
Launcher.ensureRunnable(helperPath);
|
||||
logger.debug('Using auto-type helper', helperPath);
|
||||
this._helperPath = helperPath;
|
||||
return helperPath;
|
||||
}
|
||||
};
|
||||
|
||||
export { AutoTypeNativeHelper };
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Events } from 'framework/events';
|
||||
import { AutoTypeFilter } from 'auto-type/auto-type-filter';
|
||||
import { AutoTypeHelperFactory } from 'auto-type/auto-type-helper-factory';
|
||||
import { AutoTypeHelper } from 'auto-type/auto-type-helper';
|
||||
import { AutoTypeParser } from 'auto-type/auto-type-parser';
|
||||
import { SelectEntryFilter } from 'comp/app/select-entry-filter';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Features } from 'util/features';
|
||||
import { Alerts } from 'comp/ui/alerts';
|
||||
@@ -11,7 +11,7 @@ import { AppModel } from 'models/app-model';
|
||||
import { Locale } from 'util/locale';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Links } from 'const/links';
|
||||
import { AutoTypeSelectView } from 'views/auto-type/auto-type-select-view';
|
||||
import { SelectEntryView } from 'views/select/select-entry-view';
|
||||
|
||||
const logger = new Logger('auto-type');
|
||||
const clearTextAutoTypeLog = !!localStorage.debugAutoType;
|
||||
@@ -20,7 +20,6 @@ const AutoType = {
|
||||
enabled: !!(Launcher && Launcher.autoTypeSupported),
|
||||
supportsEventsWithWindowId: !!(Launcher && Launcher.platform() === 'linux'),
|
||||
selectEntryView: false,
|
||||
pendingEvent: null,
|
||||
running: false,
|
||||
|
||||
init() {
|
||||
@@ -28,10 +27,6 @@ const AutoType = {
|
||||
return;
|
||||
}
|
||||
Events.on('auto-type', (e) => this.handleEvent(e));
|
||||
Events.on('main-window-blur', (e) => this.mainWindowBlur(e));
|
||||
Events.on('main-window-focus', (e) => this.mainWindowFocus(e));
|
||||
Events.on('main-window-will-close', (e) => this.mainWindowWillClose(e));
|
||||
Events.on('closed-open-view', (e) => this.processPendingEvent(e));
|
||||
},
|
||||
|
||||
handleEvent(e) {
|
||||
@@ -168,9 +163,8 @@ const AutoType = {
|
||||
},
|
||||
|
||||
getActiveWindowInfo(callback) {
|
||||
const helperType = AppSettingsModel.useLegacyAutoType ? 'legacy' : 'native';
|
||||
logger.debug(`Getting window info using ${helperType} helper`);
|
||||
const helper = AutoTypeHelperFactory.create();
|
||||
logger.debug('Getting window info');
|
||||
const helper = new AutoTypeHelper();
|
||||
return helper.getActiveWindowInfo((err, windowInfo) => {
|
||||
if (err) {
|
||||
logger.error('Error getting window info', err);
|
||||
@@ -217,25 +211,42 @@ const AutoType = {
|
||||
},
|
||||
|
||||
selectEntryAndRun() {
|
||||
this.getActiveWindowInfo((e, windowInfo) => {
|
||||
const filter = new AutoTypeFilter(windowInfo, AppModel.instance);
|
||||
this.getActiveWindowInfo(async (e, windowInfo) => {
|
||||
const filter = new SelectEntryFilter(
|
||||
windowInfo,
|
||||
AppModel.instance,
|
||||
AppModel.instance.files,
|
||||
{
|
||||
autoType: true
|
||||
}
|
||||
);
|
||||
const evt = { filter, windowInfo };
|
||||
if (!AppModel.instance.files.hasOpenFiles()) {
|
||||
this.pendingEvent = evt;
|
||||
logger.debug('auto-type event delayed');
|
||||
this.focusMainWindow();
|
||||
} else {
|
||||
this.processEventWithFilter(evt);
|
||||
try {
|
||||
await AppModel.instance.unlockAnyFile('autoTypeUnlockMessage');
|
||||
} catch {
|
||||
logger.debug('auto-type event canceled');
|
||||
return;
|
||||
}
|
||||
if (this.selectEntryView) {
|
||||
this.selectEntryView.show();
|
||||
}
|
||||
}
|
||||
logger.debug('processing auto-type event');
|
||||
this.processEventWithFilter(evt);
|
||||
});
|
||||
},
|
||||
|
||||
focusMainWindow() {
|
||||
setTimeout(() => Launcher.showMainWindow(), Timeouts.RedrawInactiveWindow);
|
||||
if (!Launcher.isAppFocused()) {
|
||||
setTimeout(() => Launcher.showMainWindow(), Timeouts.RedrawInactiveWindow);
|
||||
}
|
||||
},
|
||||
|
||||
processEventWithFilter(evt) {
|
||||
const entries = evt.filter.getEntries();
|
||||
let entries = evt.filter.getEntries();
|
||||
if (entries.length === 1 && AppSettingsModel.directAutotype) {
|
||||
this.hideWindow(() => {
|
||||
this.runAndHandleResult({ entry: entries[0] }, evt.windowInfo.id);
|
||||
@@ -243,8 +254,31 @@ const AutoType = {
|
||||
return;
|
||||
}
|
||||
this.focusMainWindow();
|
||||
evt.filter.ignoreWindowInfo = true;
|
||||
this.selectEntryView = new AutoTypeSelectView({ filter: evt.filter });
|
||||
|
||||
if (entries.length === 0) {
|
||||
if (evt.filter.useUrl) {
|
||||
evt.filter.useUrl = false;
|
||||
if (evt.filter.title && AppSettingsModel.autoTypeTitleFilterEnabled) {
|
||||
evt.filter.useTitle = true;
|
||||
}
|
||||
}
|
||||
entries = evt.filter.getEntries();
|
||||
if (entries.length === 0 && evt.filter.useTitle) {
|
||||
evt.filter.useTitle = false;
|
||||
}
|
||||
}
|
||||
|
||||
const humanReadableTarget = evt.filter.title || evt.filter.url;
|
||||
const topMessage = humanReadableTarget
|
||||
? Locale.autoTypeMsgMatchedByWindow.replace('{}', humanReadableTarget)
|
||||
: Locale.autoTypeMsgNoWindow;
|
||||
|
||||
this.selectEntryView = new SelectEntryView({
|
||||
isAutoType: true,
|
||||
itemOptions: true,
|
||||
filter: evt.filter,
|
||||
topMessage
|
||||
});
|
||||
this.selectEntryView.on('result', (result) => {
|
||||
logger.debug('Entry selected', result);
|
||||
this.selectEntryView.off('result');
|
||||
@@ -261,55 +295,17 @@ const AutoType = {
|
||||
});
|
||||
});
|
||||
this.selectEntryView.render();
|
||||
this.selectEntryView.on('show-open-files', () => {
|
||||
this.selectEntryView.on('show-open-files', async () => {
|
||||
this.selectEntryView.hide();
|
||||
Events.emit('open-file');
|
||||
});
|
||||
},
|
||||
|
||||
mainWindowBlur() {
|
||||
this.mainWindowBlurTimer = setTimeout(() => {
|
||||
// macOS emits focus-blur-focus event in a row when triggering auto-type from minimized state
|
||||
delete this.mainWindowBlurTimer;
|
||||
this.resetPendingEvent();
|
||||
if (this.selectEntryView) {
|
||||
try {
|
||||
await AppModel.instance.unlockAnyFile('autoTypeUnlockMessage');
|
||||
} catch {
|
||||
this.selectEntryView.emit('result', undefined);
|
||||
return;
|
||||
}
|
||||
}, Timeouts.AutoTypeWindowFocusAfterBlur);
|
||||
},
|
||||
|
||||
mainWindowFocus() {
|
||||
if (this.mainWindowBlurTimer) {
|
||||
clearTimeout(this.mainWindowBlurTimer);
|
||||
this.mainWindowBlurTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
mainWindowWillClose() {
|
||||
this.resetPendingEvent();
|
||||
if (this.selectEntryView) {
|
||||
this.selectEntryView.emit('result', undefined);
|
||||
}
|
||||
},
|
||||
|
||||
resetPendingEvent() {
|
||||
if (this.pendingEvent) {
|
||||
this.pendingEvent = null;
|
||||
logger.debug('auto-type event canceled');
|
||||
}
|
||||
},
|
||||
|
||||
processPendingEvent() {
|
||||
if (this.selectEntryView) {
|
||||
this.selectEntryView.show();
|
||||
}
|
||||
if (!this.pendingEvent) {
|
||||
return;
|
||||
}
|
||||
logger.debug('processing pending auto-type event');
|
||||
const evt = this.pendingEvent;
|
||||
this.pendingEvent = null;
|
||||
this.processEventWithFilter(evt);
|
||||
this.selectEntryView.render();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Collection } from 'framework/collection';
|
||||
import { ExternalEntryModel } from 'models/external/external-entry-model';
|
||||
|
||||
class ExternalEntryCollection extends Collection {
|
||||
static model = ExternalEntryModel;
|
||||
}
|
||||
|
||||
export { ExternalEntryCollection };
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { RuntimeDataModel } from 'models/runtime-data-model';
|
||||
|
||||
const ExportApi = {
|
||||
settings: {
|
||||
@@ -11,6 +12,17 @@ const ExportApi = {
|
||||
del(key) {
|
||||
delete AppSettingsModel[key];
|
||||
}
|
||||
},
|
||||
runtimeData: {
|
||||
get(key) {
|
||||
return key ? RuntimeDataModel[key] : { ...RuntimeDataModel };
|
||||
},
|
||||
set(key, value) {
|
||||
RuntimeDataModel[key] = value;
|
||||
},
|
||||
del(key) {
|
||||
delete RuntimeDataModel[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,18 @@ const GeneratorPresets = {
|
||||
};
|
||||
},
|
||||
|
||||
get browserExtensionPreset() {
|
||||
return {
|
||||
name: 'BrowserExtension',
|
||||
length: 20,
|
||||
upper: true,
|
||||
lower: true,
|
||||
special: true,
|
||||
brackets: true,
|
||||
ambiguous: true
|
||||
};
|
||||
},
|
||||
|
||||
get builtIn() {
|
||||
return [
|
||||
this.defaultPreset,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Logger } from 'util/logger';
|
||||
|
||||
const logger = new Logger('online-password-checker');
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { SearchResultCollection } from 'collections/search-result-collection';
|
||||
import { Ranking } from 'util/data/ranking';
|
||||
|
||||
const urlPartsRegex = /^(\w+:\/\/)?(?:(?:www|wwws|secure)\.)?([^\/]+)\/?(.*)/;
|
||||
|
||||
class SelectEntryFilter {
|
||||
constructor(windowInfo, appModel, files, filterOptions) {
|
||||
this.title = windowInfo.title;
|
||||
this.useTitle = !!windowInfo.title && !windowInfo.url;
|
||||
this.url = windowInfo.url;
|
||||
this.useUrl = !!windowInfo.url;
|
||||
this.subdomains = true;
|
||||
this.text = '';
|
||||
this.appModel = appModel;
|
||||
this.files = files;
|
||||
this.filterOptions = filterOptions;
|
||||
}
|
||||
|
||||
getEntries() {
|
||||
const filter = {
|
||||
text: this.text,
|
||||
...this.filterOptions
|
||||
};
|
||||
let entries = this.appModel
|
||||
.getEntriesByFilter(filter, this.files)
|
||||
.map((e) => [e, this._getEntryRank(e)]);
|
||||
if (this.useUrl || this.useTitle) {
|
||||
entries = entries.filter((e) => e[1]);
|
||||
}
|
||||
entries = entries.sort((x, y) =>
|
||||
x[1] === y[1] ? x[0].title.localeCompare(y[0].title) : y[1] - x[1]
|
||||
);
|
||||
entries = entries.map((p) => p[0]);
|
||||
return new SearchResultCollection(entries, { comparator: 'none' });
|
||||
}
|
||||
|
||||
_getEntryRank(entry) {
|
||||
let titleRank = 0;
|
||||
let urlRank = 0;
|
||||
|
||||
if (this.useTitle && this.title && entry.title) {
|
||||
titleRank = Ranking.getStringRank(entry.title.toLowerCase(), this.title.toLowerCase());
|
||||
if (!titleRank) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.useUrl && this.url) {
|
||||
const searchUrlLower = this.url.toLowerCase();
|
||||
const searchUrlParts = urlPartsRegex.exec(searchUrlLower);
|
||||
|
||||
for (const url of entry.getAllUrls()) {
|
||||
const entryUrlParts = urlPartsRegex.exec(url.toLowerCase());
|
||||
if (entryUrlParts) {
|
||||
const [, scheme, domain, path] = entryUrlParts;
|
||||
const [, searchScheme, searchDomain, searchPath] = searchUrlParts;
|
||||
if (
|
||||
domain === searchDomain ||
|
||||
(this.subdomains && searchDomain.indexOf('.' + domain) > 0)
|
||||
) {
|
||||
if (domain === searchDomain) {
|
||||
urlRank += 20;
|
||||
} else {
|
||||
urlRank += 10;
|
||||
}
|
||||
if (path === searchPath) {
|
||||
urlRank += 10;
|
||||
} else if (path && searchPath) {
|
||||
if (path.lastIndexOf(searchPath, 0) === 0) {
|
||||
urlRank += 5;
|
||||
} else if (searchPath.lastIndexOf(path, 0) === 0) {
|
||||
urlRank += 3;
|
||||
}
|
||||
}
|
||||
if (scheme === searchScheme) {
|
||||
urlRank += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.searchText.includes(searchUrlLower)) {
|
||||
// the url is in some field; include it
|
||||
urlRank += 5;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.useTitle && !titleRank) {
|
||||
return 0;
|
||||
}
|
||||
if (this.useUrl && !urlRank) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return titleRank + urlRank;
|
||||
}
|
||||
}
|
||||
|
||||
export { SelectEntryFilter };
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Events } from 'framework/events';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { Transport } from 'comp/browser/transport';
|
||||
|
||||
@@ -209,6 +209,10 @@ const YubiKey = {
|
||||
complete: (err, stdout) => {
|
||||
this.process = null;
|
||||
|
||||
if (window.debugYubiKey) {
|
||||
logger.info('received codes', err, stdout);
|
||||
}
|
||||
|
||||
if (this.aborted) {
|
||||
return callback('Aborted');
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
|
||||
const SecureInput = function () {
|
||||
this.el = null;
|
||||
@@ -79,7 +79,7 @@ Object.defineProperty(SecureInput.prototype, 'value', {
|
||||
const len = pseudoValue.length;
|
||||
let byteLength = 0;
|
||||
const valueBytes = new Uint8Array(len * 4);
|
||||
const saltBytes = kdbxweb.Random.getBytes(len * 4);
|
||||
const saltBytes = kdbxweb.CryptoEngine.random(len * 4);
|
||||
let ch;
|
||||
let bytes;
|
||||
for (let i = 0; i < len; i++) {
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Logger } from 'util/logger';
|
||||
import { ProtocolImpl } from './protocol-impl';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { Features } from 'util/features';
|
||||
|
||||
const WebConnectionInfo = {
|
||||
connectionId: 1,
|
||||
extensionName: 'KeeWeb Connect',
|
||||
supportsNotifications: true
|
||||
};
|
||||
|
||||
const SupportedExtensions = [
|
||||
{ alias: 'KWC', name: 'KeeWeb Connect' },
|
||||
{ alias: 'KPXC', name: 'KeePassXC-Browser' }
|
||||
];
|
||||
const SupportedBrowsers = ['Chrome', 'Firefox', 'Edge', 'Other'];
|
||||
if (Features.isMac) {
|
||||
SupportedBrowsers.unshift('Safari');
|
||||
}
|
||||
|
||||
const logger = new Logger('browser-extension-connector');
|
||||
if (!localStorage.debugBrowserExtension) {
|
||||
logger.level = Logger.Level.Info;
|
||||
}
|
||||
|
||||
const connections = new Map();
|
||||
const pendingBrowserMessages = [];
|
||||
let processingBrowserMessage = false;
|
||||
|
||||
const BrowserExtensionConnector = {
|
||||
started: false,
|
||||
logger,
|
||||
|
||||
init(appModel) {
|
||||
const sendEvent = this.sendEvent.bind(this);
|
||||
ProtocolImpl.init({ appModel, logger, sendEvent });
|
||||
|
||||
this.browserWindowMessage = this.browserWindowMessage.bind(this);
|
||||
|
||||
if (Launcher) {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
|
||||
ipcRenderer.on('browserExtensionSocketConnected', (e, socketId, connectionInfo) =>
|
||||
this.socketConnected(socketId, connectionInfo)
|
||||
);
|
||||
ipcRenderer.on('browserExtensionSocketClosed', (e, socketId) =>
|
||||
this.socketClosed(socketId)
|
||||
);
|
||||
ipcRenderer.on('browserExtensionSocketRequest', (e, socketId, request) =>
|
||||
this.socketRequest(socketId, request)
|
||||
);
|
||||
|
||||
AppSettingsModel.on('change', () => this.appSettingsChanged());
|
||||
}
|
||||
|
||||
if (this.isEnabled()) {
|
||||
this.start();
|
||||
}
|
||||
},
|
||||
|
||||
start() {
|
||||
if (Launcher) {
|
||||
this.startDesktopAppListener();
|
||||
} else {
|
||||
this.startWebMessageListener();
|
||||
}
|
||||
|
||||
this.started = true;
|
||||
},
|
||||
|
||||
stop() {
|
||||
if (Launcher) {
|
||||
this.stopDesktopAppListener();
|
||||
} else {
|
||||
this.stopWebMessageListener();
|
||||
}
|
||||
|
||||
ProtocolImpl.cleanup();
|
||||
connections.clear();
|
||||
|
||||
this.started = false;
|
||||
},
|
||||
|
||||
appSettingsChanged() {
|
||||
if (this.isEnabled()) {
|
||||
if (!this.started) {
|
||||
this.start();
|
||||
}
|
||||
} else if (this.started) {
|
||||
this.stop();
|
||||
}
|
||||
},
|
||||
|
||||
isEnabled() {
|
||||
if (!Launcher) {
|
||||
return true;
|
||||
}
|
||||
for (const ext of SupportedExtensions) {
|
||||
for (const browser of SupportedBrowsers) {
|
||||
if (AppSettingsModel[`extensionEnabled${ext.alias}${browser}`]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
startWebMessageListener() {
|
||||
window.addEventListener('message', this.browserWindowMessage);
|
||||
logger.info('Started');
|
||||
},
|
||||
|
||||
stopWebMessageListener() {
|
||||
window.removeEventListener('message', this.browserWindowMessage);
|
||||
},
|
||||
|
||||
enable(browser, extension, enabled) {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorEnable', browser, extension, enabled);
|
||||
},
|
||||
|
||||
async startDesktopAppListener() {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorStart', {
|
||||
appleTeamId: RuntimeInfo.appleTeamId
|
||||
});
|
||||
},
|
||||
|
||||
stopDesktopAppListener() {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorStop');
|
||||
},
|
||||
|
||||
browserWindowMessage(e) {
|
||||
if (e.origin !== location.origin) {
|
||||
return;
|
||||
}
|
||||
if (e.source !== window) {
|
||||
return;
|
||||
}
|
||||
if (e?.data?.kwConnect !== 'request') {
|
||||
return;
|
||||
}
|
||||
logger.debug('Extension -> KeeWeb', e.data);
|
||||
pendingBrowserMessages.push(e.data);
|
||||
this.processBrowserMessages();
|
||||
},
|
||||
|
||||
async processBrowserMessages() {
|
||||
if (!pendingBrowserMessages.length || processingBrowserMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connections.has(WebConnectionInfo.connectionId)) {
|
||||
connections.set(WebConnectionInfo.connectionId, WebConnectionInfo);
|
||||
}
|
||||
|
||||
processingBrowserMessage = true;
|
||||
|
||||
const request = pendingBrowserMessages.shift();
|
||||
|
||||
const response = await ProtocolImpl.handleRequest(request, WebConnectionInfo);
|
||||
|
||||
processingBrowserMessage = false;
|
||||
|
||||
if (response) {
|
||||
this.sendWebResponse(response);
|
||||
}
|
||||
|
||||
this.processBrowserMessages();
|
||||
},
|
||||
|
||||
sendWebResponse(response) {
|
||||
logger.debug('KeeWeb -> Extension', response);
|
||||
response.kwConnect = 'response';
|
||||
postMessage(response, window.location.origin);
|
||||
},
|
||||
|
||||
sendSocketEvent(data) {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorSocketEvent', data);
|
||||
},
|
||||
|
||||
sendSocketResult(socketId, data) {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorSocketResult', socketId, data);
|
||||
},
|
||||
|
||||
sendEvent(data) {
|
||||
if (!this.isEnabled() || !connections.size) {
|
||||
return;
|
||||
}
|
||||
if (Launcher) {
|
||||
this.sendSocketEvent(data);
|
||||
} else {
|
||||
this.sendWebResponse(data);
|
||||
}
|
||||
},
|
||||
|
||||
socketConnected(socketId, connectionInfo) {
|
||||
connections.set(socketId, connectionInfo);
|
||||
},
|
||||
|
||||
socketClosed(socketId) {
|
||||
connections.delete(socketId);
|
||||
ProtocolImpl.deleteConnection(socketId);
|
||||
},
|
||||
|
||||
async socketRequest(socketId, request) {
|
||||
let result;
|
||||
|
||||
const connectionInfo = connections.get(socketId);
|
||||
if (connectionInfo) {
|
||||
result = await ProtocolImpl.handleRequest(request, connectionInfo);
|
||||
} else {
|
||||
const message = `Connection not found: ${socketId}`;
|
||||
result = ProtocolImpl.errorToResponse({ message }, request);
|
||||
}
|
||||
|
||||
this.sendSocketResult(socketId, result);
|
||||
},
|
||||
|
||||
get sessions() {
|
||||
return ProtocolImpl.sessions;
|
||||
},
|
||||
|
||||
terminateConnection(connectionId) {
|
||||
connectionId = +connectionId;
|
||||
if (Launcher) {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorCloseSocket', connectionId);
|
||||
} else {
|
||||
ProtocolImpl.deleteConnection(connectionId);
|
||||
}
|
||||
},
|
||||
|
||||
getClientPermissions(clientId) {
|
||||
return ProtocolImpl.getClientPermissions(clientId);
|
||||
},
|
||||
|
||||
setClientPermissions(clientId, permissions) {
|
||||
ProtocolImpl.setClientPermissions(clientId, permissions);
|
||||
}
|
||||
};
|
||||
|
||||
export { BrowserExtensionConnector, SupportedExtensions, SupportedBrowsers };
|
||||
@@ -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 || '';
|
||||
}
|
||||
|
||||
|
||||
@@ -153,17 +153,6 @@ const Launcher = {
|
||||
saveConfig(name, data) {
|
||||
return this.remoteApp().saveConfig(name, data);
|
||||
},
|
||||
ensureRunnable(path) {
|
||||
if (process.platform !== 'win32') {
|
||||
const fs = this.req('fs');
|
||||
const stat = fs.statSync(path);
|
||||
if ((stat.mode & 0o0111) === 0) {
|
||||
const mode = stat.mode | 0o0100;
|
||||
logger.info(`chmod 0${mode.toString(8)} ${path}`);
|
||||
fs.chmodSync(path, mode);
|
||||
}
|
||||
}
|
||||
},
|
||||
preventExit(e) {
|
||||
e.returnValue = false;
|
||||
return false;
|
||||
@@ -202,7 +191,7 @@ const Launcher = {
|
||||
}
|
||||
},
|
||||
quitOnRealQuitEventIfMinimizeOnQuitIsEnabled() {
|
||||
return !!(this.platform() === 'darwin' || this.pendingUpdateFile);
|
||||
return !!this.pendingUpdateFile;
|
||||
},
|
||||
minimizeApp() {
|
||||
this.remoteApp().minimizeApp({
|
||||
@@ -244,57 +233,32 @@ const Launcher = {
|
||||
},
|
||||
spawn(config) {
|
||||
const ts = logger.ts();
|
||||
let complete = config.complete;
|
||||
const ps = this.req('child_process').spawn(config.cmd, config.args);
|
||||
[ps.stdin, ps.stdout, ps.stderr].forEach((s) => s.setEncoding('utf-8'));
|
||||
let stderr = '';
|
||||
let stdout = '';
|
||||
ps.stderr.on('data', (d) => {
|
||||
stderr += d.toString('utf-8');
|
||||
if (config.throwOnStdErr) {
|
||||
try {
|
||||
ps.kill();
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
ps.stdout.on('data', (d) => {
|
||||
stdout += d.toString('utf-8');
|
||||
});
|
||||
ps.on('close', (code) => {
|
||||
stdout = stdout.trim();
|
||||
stderr = stderr.trim();
|
||||
const msg = 'spawn ' + config.cmd + ': ' + code + ', ' + logger.ts(ts);
|
||||
if (code !== 0) {
|
||||
logger.error(msg + '\n' + stdout + '\n' + stderr);
|
||||
} else {
|
||||
logger.info(msg + (stdout && !config.noStdOutLogging ? '\n' + stdout : ''));
|
||||
}
|
||||
if (complete) {
|
||||
complete(code !== 0 ? 'Exit code ' + code : null, stdout, code);
|
||||
const { ipcRenderer } = this.electron();
|
||||
let { complete } = config;
|
||||
delete config.complete;
|
||||
ipcRenderer
|
||||
.invoke('spawnProcess', config)
|
||||
.then((res) => {
|
||||
if (res.err) {
|
||||
logger.error('spawn error: ' + config.cmd + ', ' + logger.ts(ts), res.err);
|
||||
complete?.(res.err);
|
||||
} else {
|
||||
const code = res.code;
|
||||
const stdout = res.stdout || '';
|
||||
const stderr = res.stderr || '';
|
||||
const msg = 'spawn ' + config.cmd + ': ' + code + ', ' + logger.ts(ts);
|
||||
if (code !== 0) {
|
||||
logger.error(msg + '\n' + stdout + '\n' + stderr);
|
||||
} else {
|
||||
logger.info(msg + (stdout && !config.noStdOutLogging ? '\n' + stdout : ''));
|
||||
}
|
||||
complete?.(code !== 0 ? 'Exit code ' + code : null, stdout, code);
|
||||
}
|
||||
complete = null;
|
||||
}
|
||||
});
|
||||
ps.on('error', (err) => {
|
||||
logger.error('spawn error: ' + config.cmd + ', ' + logger.ts(ts), err);
|
||||
if (complete) {
|
||||
complete(err);
|
||||
complete = null;
|
||||
}
|
||||
});
|
||||
if (config.data) {
|
||||
try {
|
||||
ps.stdin.end(config.data);
|
||||
} catch (e) {
|
||||
logger.error('spawn write error', e);
|
||||
}
|
||||
}
|
||||
process.nextTick(() => {
|
||||
// it should work without destroy, but a process doesn't get launched
|
||||
// xubuntu-desktop 19.04 / xfce
|
||||
// see https://github.com/keeweb/keeweb/issues/1234
|
||||
ps.stdin.destroy();
|
||||
});
|
||||
return ps;
|
||||
})
|
||||
.catch((err) => {
|
||||
complete?.(err);
|
||||
});
|
||||
},
|
||||
checkOpenFiles() {
|
||||
this.readyToOpenFiles = true;
|
||||
@@ -335,7 +299,6 @@ Events.on('launcher-maximize', () => setTimeout(() => Events.emit('app-maximized
|
||||
Events.on('launcher-unmaximize', () => setTimeout(() => Events.emit('app-unmaximized'), 0));
|
||||
Events.on('launcher-started-minimized', () => setTimeout(() => Launcher.minimizeApp(), 0));
|
||||
Events.on('start-profile', (data) => StartProfiler.reportAppProfile(data));
|
||||
Events.on('log', (e) => new Logger(e.category || 'remote-app')[e.method || 'info'](e.message));
|
||||
|
||||
window.launcherOpen = (file) => Launcher.openFile(file);
|
||||
if (window.launcherOpenedFile) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Events } from 'framework/events';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
@@ -20,6 +20,7 @@ if (Launcher) {
|
||||
ipcRenderer.on('nativeModuleHostError', (e, err) => NativeModules.hostError(err));
|
||||
ipcRenderer.on('nativeModuleHostExit', (e, { code, sig }) => NativeModules.hostExit(code, sig));
|
||||
ipcRenderer.on('nativeModuleHostDisconnect', () => NativeModules.hostDisconnect());
|
||||
ipcRenderer.on('log', (e, ...args) => NativeModules.log(...args));
|
||||
|
||||
const handlers = {
|
||||
yubikeys(numYubiKeys) {
|
||||
@@ -118,6 +119,14 @@ if (Launcher) {
|
||||
}
|
||||
},
|
||||
|
||||
log(name, level, ...args) {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
const logger = new Logger(name);
|
||||
logger[level](...args);
|
||||
},
|
||||
|
||||
autoRestartHost() {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
@@ -176,6 +185,10 @@ if (Launcher) {
|
||||
return this.call('argon2', password, salt, options);
|
||||
},
|
||||
|
||||
hardwareCryptoDeleteKey: async () => {
|
||||
await ipcRenderer.invoke('hardwareCryptoDeleteKey');
|
||||
},
|
||||
|
||||
hardwareEncrypt: async (value) => {
|
||||
const { data, salt } = await ipcRenderer.invoke('hardwareEncrypt', value.dataAndSalt());
|
||||
return new kdbxweb.ProtectedValue(data, salt);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Locale } from 'util/locale';
|
||||
import { ThemeWatcher } from 'comp/browser/theme-watcher';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const logger = new Logger('settings-manager');
|
||||
|
||||
@@ -164,6 +165,20 @@ const SettingsManager = {
|
||||
Object.assign(Locale, this.neutralLocale, localeValues);
|
||||
this.activeLocale = loc;
|
||||
Events.emit('set-locale', loc);
|
||||
|
||||
if (Launcher) {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
const localeValuesForDesktopApp = {};
|
||||
for (const [key, value] of Object.entries(Locale)) {
|
||||
if (key.startsWith('sysMenu')) {
|
||||
localeValuesForDesktopApp[key] = value;
|
||||
}
|
||||
}
|
||||
ipcRenderer.invoke('setLocale', {
|
||||
locale: loc,
|
||||
...localeValuesForDesktopApp
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getBrowserLocale() {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -9,7 +9,7 @@ const DefaultAppSettings = {
|
||||
autoUpdate: 'install', // auto-update options: "install", "check", ""
|
||||
clipboardSeconds: 0, // number of seconds after which the clipboard will be cleared
|
||||
autoSave: true, // auto-save open files
|
||||
autoSaveInterval: 0, // interval between performing automatic sync, minutes
|
||||
autoSaveInterval: 0, // interval between performing automatic sync, minutes, -1: on every change
|
||||
rememberKeyFiles: 'path', // remember keyfiles selected on the Open screen
|
||||
idleMinutes: 15, // app lock timeout after inactivity, minutes
|
||||
minimizeOnClose: false, // minimise the app instead of closing
|
||||
@@ -18,6 +18,7 @@ const DefaultAppSettings = {
|
||||
colorfulIcons: false, // use colorful custom icons instead of grayscale
|
||||
useMarkdown: true, // use Markdown in Notes field
|
||||
directAutotype: true, // if only one matching entry is found, select that one automatically
|
||||
autoTypeTitleFilterEnabled: true, // enable the title filtering in auto-type by default
|
||||
titlebarStyle: 'default', // window titlebar style
|
||||
lockOnMinimize: true, // lock the app when it's minimized
|
||||
lockOnCopy: false, // lock the app after a password was copied
|
||||
@@ -43,10 +44,12 @@ const DefaultAppSettings = {
|
||||
excludePinsFromAudit: true, // exclude PIN codes from audit
|
||||
checkPasswordsOnHIBP: false, // check passwords on Have I Been Pwned
|
||||
auditPasswordAge: 0, // show warnings about old passwords, number of years, 0 = disabled
|
||||
useLegacyAutoType: false, // use legacy auto-type engine (will be removed in future versions)
|
||||
deviceOwnerAuth: null, // Touch ID: null / 'memory' / 'file'
|
||||
deviceOwnerAuthTimeoutMinutes: 0, // how often master password is required with Touch ID
|
||||
disableOfflineStorage: false, // don't cache loaded files in offline storage
|
||||
shortLivedStorageToken: false, // short-lived sessions in cloud storage providers
|
||||
extensionFocusIfLocked: true, // focus KeeWeb if a browser extension tries to connect while KeeWeb is locked
|
||||
extensionFocusIfEmpty: true, // show the entry selection screen if there's no match found by URL
|
||||
|
||||
yubiKeyShowIcon: true, // show an icon to open OTP codes from YubiKey
|
||||
yubiKeyAutoOpen: false, // auto-load one-time codes when there are open files
|
||||
@@ -84,7 +87,13 @@ const DefaultAppSettings = {
|
||||
|
||||
onedrive: true, // enable OneDrive integration
|
||||
onedriveClientId: null, // custom OneDrive client id
|
||||
onedriveClientSecret: null // custom OneDrive client secret
|
||||
onedriveClientSecret: null, // custom OneDrive client secret
|
||||
onedriveTenantId: null, // custom OneDrive tenant id
|
||||
|
||||
msteams: false, // enable Microsoft Teams integration
|
||||
msteamsClientId: null, // custom Microsoft Teams client id
|
||||
msteamsClientSecret: null, // custom Microsoft Teams client secret
|
||||
msteamsTenantId: null // custom Microsoft Teams tenant id
|
||||
};
|
||||
|
||||
export { DefaultAppSettings };
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const KnownAppVersions = {
|
||||
KeePassXC: '2.6.4'
|
||||
};
|
||||
|
||||
export { KnownAppVersions };
|
||||
@@ -20,7 +20,17 @@ const Links = {
|
||||
YubiKeyManual: 'https://github.com/keeweb/keeweb/wiki/YubiKey',
|
||||
YubiKeyManagerInstall: 'https://github.com/Yubico/yubikey-manager#installation',
|
||||
HaveIBeenPwned: 'https://haveibeenpwned.com',
|
||||
HaveIBeenPwnedPrivacy: 'https://haveibeenpwned.com/Passwords'
|
||||
HaveIBeenPwnedPrivacy: 'https://haveibeenpwned.com/Passwords',
|
||||
ExtensionHelpForOtherBrowsers:
|
||||
'https://github.com/keeweb/keeweb/wiki/Browser-AutoFill#other-browsers',
|
||||
ExtensionHelpForKPXC:
|
||||
'https://github.com/keeweb/keeweb/wiki/Browser-AutoFill#keepassxc-browser',
|
||||
KWCForChrome:
|
||||
'https://chrome.google.com/webstore/detail/keeweb-connect/pikpfmjfkekaeinceagbebpfkmkdlcjk',
|
||||
KWCForFirefox: 'https://addons.mozilla.org/firefox/addon/keeweb-connect/',
|
||||
KWCForEdge:
|
||||
'https://microsoftedge.microsoft.com/addons/detail/keewebconnect/nmggpehkjmeaeocmaijenpejbepckinm',
|
||||
KWCForSafari: 'https://apps.apple.com/app/keeweb-connect/id1565748094'
|
||||
};
|
||||
|
||||
export { Links };
|
||||
|
||||
@@ -3,7 +3,8 @@ const RuntimeInfo = {
|
||||
beta: !!'@@BETA',
|
||||
buildDate: '@@DATE',
|
||||
commit: '@@COMMIT',
|
||||
devMode: '@@DEVMODE'
|
||||
devMode: '@@DEVMODE',
|
||||
appleTeamId: '@@APPLE_TEAM_ID'
|
||||
};
|
||||
|
||||
export { RuntimeInfo };
|
||||
|
||||
@@ -18,7 +18,8 @@ const Timeouts = {
|
||||
FieldLabelDoubleClick: 300,
|
||||
NativeModuleHostRestartTime: 3000,
|
||||
FastAnimation: 100,
|
||||
AutoTypeCopyPaste: 300
|
||||
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() {
|
||||
|
||||
+776
-715
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",
|
||||
@@ -56,6 +57,22 @@
|
||||
"menuItemCollapsed": "Doppelklick zum Aufklappen",
|
||||
"menuRestoreApp": "{} öffnen",
|
||||
"menuQuitApp": "{} beenden",
|
||||
"sysMenuAboutKeeWeb": "Über {}",
|
||||
"sysMenuServices": "Dienste",
|
||||
"sysMenuHide": "{} ausblenden",
|
||||
"sysMenuHideOthers": "Andere ausblenden",
|
||||
"sysMenuUnhide": "Alle anzeigen",
|
||||
"sysMenuQuit": "{} beenden",
|
||||
"sysMenuEdit": "Bearbeiten",
|
||||
"sysMenuUndo": "Rückgängig",
|
||||
"sysMenuRedo": "Wiederherstellen",
|
||||
"sysMenuCut": "Ausschneiden",
|
||||
"sysMenuCopy": "Kopieren",
|
||||
"sysMenuPaste": "Einfügen",
|
||||
"sysMenuSelectAll": "Alles auswählen",
|
||||
"sysMenuWindow": "Fenster",
|
||||
"sysMenuMinimize": "Minimieren",
|
||||
"sysMenuClose": "Fenster schließen",
|
||||
"alertYes": "Ja",
|
||||
"alertNo": "Nein",
|
||||
"alertOk": "OK",
|
||||
@@ -228,7 +245,7 @@
|
||||
"detHistoryDiscardChangesAlertBody": "Ungespeicherte Änderungen gehen dabei unwiderruflich verloren.",
|
||||
"detBackToList": "zurück zur Liste",
|
||||
"detSetIconColor": "Farbe ändern",
|
||||
"detSetIcon": "Icon ändern",
|
||||
"detSetIcon": "Symbol ändern",
|
||||
"detDropAttachments": "Anhänge hier ablegen",
|
||||
"detDelEntry": "Löschen",
|
||||
"detDelEntryPerm": "Dauerhaft löschen",
|
||||
@@ -295,6 +312,16 @@
|
||||
"detRevealField": "Anzeigen",
|
||||
"detHideField": "Verbergen",
|
||||
"detAutoTypeField": "Auto-Type",
|
||||
"detIssuesHideTooltip": "Diese Warnung verstecken",
|
||||
"detIssueWeakPassword": "Das Passwort ist schwach, es wird empfohlen es zu ändern.",
|
||||
"detIssuePoorPassword": "Das Passwort ist schwach, es wird dringend empfohlen es zu ändern.",
|
||||
"detIssuePwnedPassword": "Dieses Passwort wurde laut {} in einem Datenleck gefunden, es wird empfohlen das Passwort zu ändern.",
|
||||
"detIssuePasswordCheckError": "Es gab einen Fehler bei der online-überprüfung des Passwort.",
|
||||
"detIssueOldPassword": "Das Passwort ist alt.",
|
||||
"detIssueCloseAlertHeader": "Verstecke Passwortmeldungen",
|
||||
"detIssueCloseAlertBody": "Es gibt verschiedene Möglichkeiten diese Warnung zu verstecken:",
|
||||
"detIssueCloseAlertEntry": "Nicht für diesen Eintrag anzeigen.",
|
||||
"detIssueCloseAlertSettings": "Globale Einstellungen anpassen",
|
||||
"autoTypeEntryFields": "Feld-Platzhalter",
|
||||
"autoTypeModifiers": "Hilfstasten",
|
||||
"autoTypeKeys": "Tasten",
|
||||
@@ -325,6 +352,8 @@
|
||||
"appSaveError": "Fehler beim Speichern",
|
||||
"appSaveErrorBody": "Automatisches Speichern fehlgeschlagen",
|
||||
"appSaveErrorBodyMul": "Automatisches Speichern fehlgeschlagen:",
|
||||
"appSaveErrorExitLoseChanges": "Schließen und alle Änderungen verlieren",
|
||||
"appSaveErrorExitLoseChangesBody": "Du kannst entweder die App schließen und alle Änderungen verlieren, oder gehe in die Dateieinstellungen und exportiere die Daten",
|
||||
"appSettingsError": "Fehler beim Laden der App",
|
||||
"appSettingsErrorBody": "Es gab einen Fehler beim Laden der App-Einstellungen. Bitte überprüfen Sie die App-URL oder kontaktieren Sie Ihren Systemadministrator.",
|
||||
"appNotSupportedError": "Ihr Browser unterstützt einige wichtige Funktionen nicht, die für diese App benötigt werden.",
|
||||
@@ -360,12 +389,26 @@
|
||||
"setGenDownloadAndRestart": "Update herunterladen und App neu starten",
|
||||
"setGenAppearance": "Oberfläche",
|
||||
"setGenTheme": "Theme",
|
||||
"setGenThemeDefault": "Standard",
|
||||
"setGenThemeDark": "Dunkel",
|
||||
"setGenThemeLight": "Hell",
|
||||
"setGenThemeBlue": "Flach blau",
|
||||
"setGenThemeFb": "Mattblau",
|
||||
"setGenThemeBl": "Hell blau",
|
||||
"setGenThemeBrown": "Brownie",
|
||||
"setGenThemeDb": "Dunkelbraun",
|
||||
"setGenThemeLb": "Hell braun",
|
||||
"setGenThemeTerminal": "Terminal",
|
||||
"setGenThemeTe": "Terminal",
|
||||
"setGenThemeLt": "Hell Terminal",
|
||||
"setGenThemeHighContrast": "Hoher Kontrast",
|
||||
"setGenThemeHc": "Hoher Kontrast",
|
||||
"setGenThemeDc": "Dunkel Kontrast",
|
||||
"setGenThemeSol": "Solarized",
|
||||
"setGenThemeSd": "Solarisiert dunkel",
|
||||
"setGenThemeSl": "Solarisiert hell",
|
||||
"setGenMoreThemes": "Mehr Themes",
|
||||
"setGenAutoSwitchTheme": "Automatisch zwischen hellem und dunklem Thema umschalten, sofern möglich",
|
||||
"setGenLocale": "Sprache",
|
||||
"setGenLocOther": "Andere Sprachen sind als Plugins verfügbar.",
|
||||
"setGenFontSize": "Schriftgröße",
|
||||
@@ -408,6 +451,7 @@
|
||||
"setGenLockAutoType": "Bei Auto-Type",
|
||||
"setGenLockOrSleep": "Bei Aktivierung von Bildschirmsperre oder Ruhezustand",
|
||||
"setGenStorage": "Synchronisierung",
|
||||
"setGenDisableOfflineStorage": "Geladene Dateien nicht in Offline-Speicher zwischenspeichern",
|
||||
"setGenStorageLogout": "Abmelden",
|
||||
"setGenShowAdvanced": "Erweiterte Einstellungen anzeigen",
|
||||
"setGenDevTools": "Entwicklerwerkzeuge anzeigen",
|
||||
@@ -422,6 +466,16 @@
|
||||
"setGenTouchIdMemory": "Nur mit Fingerabdruck entsperren, wenn KeeWeb aktiv ist",
|
||||
"setGenTouchIdFile": "Benutze immer den Fingerabdruck anstatt das Master-Passwort",
|
||||
"setGenTouchIdPass": "Benötige Master-Passwort nach",
|
||||
"setGenAudit": "Audit",
|
||||
"setGenAuditPasswords": "Warnungen für schwache Passwörter anzeigen",
|
||||
"setGenAuditPasswordEntropy": "Passwortlänge und Zufälligkeit prüfen",
|
||||
"setGenExcludePinsFromAudit": "Kurze Pin-Codes niemals prüfen (z. B. 12345)",
|
||||
"setGenHelpHIBP": "KeeWeb kann überprüfen ob deine Passwörter bei einem früheren Datenleck veröffentlicht wurden. Deine Passwörter können nicht beim übertragen ausgelesen werden, aber die Anzahl der überprüften Passwörter kann möglicherweise entdeckt werden. {} kannst du mehr über den Datenschutz beim Nutzen dieses Dienstes erfahren. Wenn diese Option aktiviert ist überprüft KeeWeb deine Passwörter automatisch.",
|
||||
"setGenHelpHIBPLink": "hier",
|
||||
"setGenAuditPasswordAge": "Alte Passwörter",
|
||||
"setGenAuditPasswordAgeOff": "Zeige keine Warnungen über alte Passwörter",
|
||||
"setGenAuditPasswordAgeOneYear": "Zeige Warnungen für Passwörter die über ein Jahr alt sind",
|
||||
"setGenAuditPasswordAgeYears": "Zeige Warnungen für Passwörter die über {} Jahre alt sind",
|
||||
"setFilePath": "Dateipfad",
|
||||
"setFileStorage": "Diese Datei wird von {} geladen.",
|
||||
"setFileIntl": "Diese Datei ist im internen App-Speicher abgelegt",
|
||||
@@ -635,5 +689,7 @@
|
||||
"yubiKeyTouchRequestedHeader": "Bitte den YubiKey berühren",
|
||||
"yubiKeyTouchRequestedBody": "Bitte den YubiKey mit der Seriennummer {} berühren",
|
||||
"yubiKeyDisabledErrorHeader": "USB ist deaktiviert",
|
||||
"yubiKeyDisabledErrorBody": "YubiKey ist zum Öffnen dieser Datei erforderlich. Bitte USB-Geräte in den Einstellungen aktivieren."
|
||||
"yubiKeyDisabledErrorBody": "YubiKey ist zum Öffnen dieser Datei erforderlich. Bitte USB-Geräte in den Einstellungen aktivieren.",
|
||||
"yubiKeyErrorWithCode": "YubiKey-Fehler, Code {}.",
|
||||
"bioOpenAuthPrompt": "„{}“ öffnen"
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
"or": "ou",
|
||||
"history": "historique",
|
||||
"template": "modèle",
|
||||
"templates": "templates",
|
||||
"notImplemented": "Non Implémenté",
|
||||
"saveChanges": "Sauvegarder les modifications",
|
||||
"discardChanges": "Annuler les modifications",
|
||||
@@ -46,16 +47,33 @@
|
||||
"menuColors": "Couleurs",
|
||||
"menuTrash": "Corbeille",
|
||||
"menuSetGeneral": "Général",
|
||||
"menuSetBrowser": "Navigateur",
|
||||
"menuSetAbout": "À propos",
|
||||
"menuSetDevices": "Appareils",
|
||||
"menuAlertNoTags": "Aucun tag",
|
||||
"menuAlertNoTagsBody": "Vous pouvez ajouter de nouveaux tags en éditant les champs, dans la section Tags.",
|
||||
"menuEmptyTrash": "Vider la Corbeille",
|
||||
"menuEmptyTrashAlert": "Vider la Corbeille?",
|
||||
"menuEmptyTrashAlert": "Vider la Corbeille ?",
|
||||
"menuEmptyTrashAlertBody": "Vous ne pourrez plus récupérer ces éléments",
|
||||
"menuItemCollapsed": "Double-cliquez pour déplier",
|
||||
"menuRestoreApp": "Ouvrir {}",
|
||||
"menuQuitApp": "Quitter {}",
|
||||
"sysMenuAboutKeeWeb": "A propos {}",
|
||||
"sysMenuServices": "Services",
|
||||
"sysMenuHide": "Cacher {}",
|
||||
"sysMenuHideOthers": "Cacher autres",
|
||||
"sysMenuUnhide": "Montrer tout",
|
||||
"sysMenuQuit": "Quitter {}",
|
||||
"sysMenuEdit": "Editer",
|
||||
"sysMenuUndo": "Annuler",
|
||||
"sysMenuRedo": "Répéter",
|
||||
"sysMenuCut": "Couper",
|
||||
"sysMenuCopy": "Copier",
|
||||
"sysMenuPaste": "Coller",
|
||||
"sysMenuSelectAll": "Tout sélectionner",
|
||||
"sysMenuWindow": "Fenêtre",
|
||||
"sysMenuMinimize": "Réduire",
|
||||
"sysMenuClose": "Fermer fenêtre",
|
||||
"alertYes": "Oui",
|
||||
"alertNo": "Non",
|
||||
"alertOk": "OK",
|
||||
@@ -64,6 +82,8 @@
|
||||
"alertCopy": "Copier",
|
||||
"alertClose": "Fermer",
|
||||
"alertDoNotAsk": "Ne plus me le redemander",
|
||||
"alertAllow": "Autoriser",
|
||||
"alertDeny": "Refuser",
|
||||
"appBeta": "ATTENTION: version beta, aperçu seulement",
|
||||
"footerOpen": "Ouvrir/Nouveau",
|
||||
"footerSyncError": "Erreur de synchronisation",
|
||||
@@ -72,7 +92,7 @@
|
||||
"genLen": "Longueur",
|
||||
"genNewPass": "Nouveau mot de passe",
|
||||
"genPresetDefault": "réglages par défaut",
|
||||
"genPresetDerived": "identique à l'ancien mot de passe",
|
||||
"genPresetDerived": "similaire à l'ancien mot de passe",
|
||||
"genPresetPronounceable": "prononçable",
|
||||
"genPresetMed": "longueur moyenne",
|
||||
"genPresetLong": "long",
|
||||
@@ -96,7 +116,7 @@
|
||||
"tagExists": "Ce tag existe déjà",
|
||||
"tagExistsBody": "Un tag existe déjà avec ce nom. Merci de choisir un autre nom.",
|
||||
"tagBadName": "Nom invalide",
|
||||
"tagBadNameBody": "Tag name cannot contain characters {}. Please remove them.",
|
||||
"tagBadNameBody": "Les noms de Tag ne peuvent contenir les caractères {}. Veuillez les supprimer.",
|
||||
"genPsTitle": "Préréglages du Générateur",
|
||||
"genPsCreate": "Nouveau préréglage",
|
||||
"genPsDelete": "Supprimer préréglage",
|
||||
@@ -123,6 +143,7 @@
|
||||
"keyChangeMessageExpired": "La clé maître pour cette base de donnée est expirée. Merci de saisir une nouvelle clé.",
|
||||
"keyChangeRepeatPassword": "Mot de passe, encore une fois",
|
||||
"keyEnter": "Entrée",
|
||||
"keyEsc": "Esc",
|
||||
"iconFavTitle": "Télécharger et utiliser le favicon du site web",
|
||||
"iconSelCustom": "Sélectionner une icône personnalisée",
|
||||
"listEmptyTitle": "Vide",
|
||||
@@ -193,6 +214,7 @@
|
||||
"openConfigErrorNotFound": "Aucun fichier trouvé",
|
||||
"openError": "Erreur",
|
||||
"openErrorDescription": "Une erreur est survenue à l'ouverture du fichier",
|
||||
"openErrorDescriptionMaybeTouchIdChanged": "L'erreur ressemble à ce qu'y arrive habituellement quand la configuration de Touch ID a été changée. Par exemple, vous avez ajouté ou supprimé un doigt supplémentaire. Si c'est le cas, allez dans Paramètres, désactiver Touch ID et activer le de nouveau.",
|
||||
"openErrorFileNotFound": "Fichier non trouvé",
|
||||
"openListErrorBody": "Erreur au chargement de la liste des fichiers",
|
||||
"openShowAllFiles": "Tous les fichiers",
|
||||
@@ -246,10 +268,11 @@
|
||||
"detDelToTrashBody": "L'entrée sera déplacée dans la corbeille.",
|
||||
"detFieldCopied": "Copié",
|
||||
"detFieldCopiedTime": "Copié pendant {} secondes",
|
||||
"detCopyHint": "You can copy field value by clicking its title",
|
||||
"detCopyHint": "Vous pouvez copier la valeur du champ en cliquant sur ton titre",
|
||||
"detMore": "plus",
|
||||
"detClickToAddField": "cliquez pour ajouter un nouveau champ",
|
||||
"detMenuAddNewField": "Ajouter nouveau champ",
|
||||
"detMenuAddNewWebsite": "Ajouter un autre site",
|
||||
"detMenuShowEmpty": "Montrer les champs vides",
|
||||
"detMenuHideEmpty": "Cacher les champs vides",
|
||||
"detMenuAddField": "Ajouter {}",
|
||||
@@ -301,13 +324,18 @@
|
||||
"detIssuePwnedPassword": "Ce mot de passe a été exposé à une faille selon {}, il est recommandé de le changer",
|
||||
"detIssuePasswordCheckError": "Une erreur est survenue en vérifiant la force du mot de passe en ligne",
|
||||
"detIssueOldPassword": "Ce mot de passe est vieux",
|
||||
"detIssueCloseAlertHeader": "Cacher les problèmes de mots de passe",
|
||||
"detIssueCloseAlertBody": "Il y a plusieurs façon de cacher ce message :",
|
||||
"detIssueCloseAlertEntry": "Ne plus montrer pour cette entrée",
|
||||
"detIssueCloseAlertSettings": "Modifier les paramètres globaux",
|
||||
"autoTypeEntryFields": "Champs",
|
||||
"autoTypeModifiers": "Touches modificatrices",
|
||||
"autoTypeKeys": "Clés",
|
||||
"autoTypeLink": "plus...",
|
||||
"autoTypeError": "Erreur saisie auto",
|
||||
"autoTypeErrorGeneric": "Il y a eu une erreur lors de la saisie automatique : {}",
|
||||
"autoTypeErrorGlobal": "Pour utiliser un raccourci, sélectionner l'application là où vous voulez saisir le mot de passe",
|
||||
"autoTypeErrorAccessibilityMacOS": "Nous avons essayé d'envoyer les frappes de caractères à cette application mais cela ne semble pas fonctionner parce qu'il manque des permissions. Cliquer ici pour en lire plus.",
|
||||
"autoTypeErrorGlobal": "Pour utiliser un raccourci global, sélectionner l'application là où vous voulez saisir le mot de passe",
|
||||
"autoTypeErrorNotInstalled": "{} n'est pas installé",
|
||||
"autoTypeHeader": "Saisie auto : Selectionner",
|
||||
"autoTypeMsgNoWindow": "Nous n'avons pas pu obtenir le titre de la fenêtre active, commencez à taper pour rechercher",
|
||||
@@ -318,6 +346,7 @@
|
||||
"autoTypeSelectionHintOpt": "Entrez seulement le compte",
|
||||
"autoTypeSelectionHintShift": "Autres champs",
|
||||
"autoTypeSelectionOtp": "code à usage unique",
|
||||
"autoTypeUnlockMessage": "Déverrouiller pour saisir automatiquement",
|
||||
"appSecWarn": "Non sécurisé !",
|
||||
"appSecWarnBody1": "Vous avez chargé cette appli avec une connexion non sécurisée. Quelqu'un peut vous observer et voler vos mots de passe. Nous vous recommandons fortement d'arrêter cette connexion, à moins que vous ne compreniez exactement ce que vous faites.",
|
||||
"appSecWarnBody2": "Oui, votre base est chiffrée mais personne ne peut garantir que l'application n'a pas été modifiée avant d'arriver à vous.",
|
||||
@@ -424,12 +453,15 @@
|
||||
"setGenClearSeconds": "Dans {} secondes",
|
||||
"setGenClearMinute": "Dans une minute",
|
||||
"setGenMinInstead": "Réduire l'application au lieu de fermer",
|
||||
"setGenMinOnFieldCopy": "Réduire sur copie d'un champ",
|
||||
"setGenLock": "Verrou Auto",
|
||||
"setGenLockMinimize": "quand l'appli est en arrière-plan",
|
||||
"setGenLockCopy": "sur copie de m.d.p.",
|
||||
"setGenLockAutoType": "sur saisie auto",
|
||||
"setGenLockOrSleep": "Quand l'ordinateur est verrouillé ou mis en sommeil",
|
||||
"setGenStorage": "Stockage",
|
||||
"setGenDisableOfflineStorage": "Ne pas mettre en cache des fichiers d'un stockage offline",
|
||||
"setGenShortLivedStorageToken": "Utiliser des sessions de courte-durée avec les fournisseurs de stockage dans le cloud",
|
||||
"setGenStorageLogout": "Déconnexion",
|
||||
"setGenShowAdvanced": "Afficher les paramètres avancés",
|
||||
"setGenDevTools": "Afficher les outils de développement",
|
||||
@@ -441,7 +473,7 @@
|
||||
"setGenFieldLabelDblClickAutoType": "Remplissage auto par double clic sur les noms de champ",
|
||||
"setGenTouchId": "Touch ID",
|
||||
"setGenTouchIdDisabled": "Ne pas utiliser Touch ID",
|
||||
"setGenTouchIdMemory": "Dévérouiller avec Touch ID uniquement quand Keeweb est lancé",
|
||||
"setGenTouchIdMemory": "Dévérouiller avec Touch ID uniquement quand KeeWeb est lancé",
|
||||
"setGenTouchIdFile": "Toujours utiliser Touch ID à la place du mot de passe",
|
||||
"setGenTouchIdPass": "Exige le mot de passe après",
|
||||
"setGenAudit": "Audit",
|
||||
@@ -589,6 +621,38 @@
|
||||
"setPlTranslateLink": "traduire l'appli dans votre langue",
|
||||
"setPlAutoUpdate": "Mise à jour automatique",
|
||||
"setPlLoadGallery": "Charger le catalogue",
|
||||
"setBrowserTitle": "Navigateur",
|
||||
"setBrowserIntroDesktop": "KeeWeb peut entrer des mots de passe en utilisant la saisie automatique, cependant, une extension peut être un moyen plus pratique de le faire. KeeWeb prend en charge deux extensions de navigateur:",
|
||||
"setBrowserIntroKeeWebConnect": "l'extension officielle KeeWeb est construite avec les fonctionnalités de KeeWeb, mais vous risquez de manquer certaines fonctionnalités avancées que l'on peut trouver dans l'autre extension.",
|
||||
"setBrowserIntroKeePassXcBrowser": "cette extension a été développée pour KeePassXC, elle existe depuis un moment et est assez fiable. L'extension ne fait pas partie de KeeWeb et il peut y avoir des problèmes de compatibilité.",
|
||||
"setBrowserIntroWeb": "Installez notre extension de navigateur pour remplir automatiquement les mots de passe de KeeWeb sur différentes pages. L'extension de navigateur se connectera à un onglet KeeWeb de votre navigateur pour récupérer les mots de passe. Cliquez ici pour télécharger l'extension:",
|
||||
"setBrowserNotEnabled": "L'intégration du navigateur n'est pas activée, les extensions ne pourront pas se connecter à KeeWeb. Utilisez les cases à cocher ci-dessous pour l'activer:",
|
||||
"setBrowserEnablePerBrowser": "Activer l'intégration dans le navigateur en cochant ci dessous:",
|
||||
"setBrowserFocusIfLocked": "Ouvrir KeeWeb si le navigateur essaye de se connecter alors que aucun fichier n'est ouvert",
|
||||
"setBrowserFocusIfEmpty": "Afficher la liste si aucune correspondance n'est trouvée avec l'URL",
|
||||
"setBrowserOtherBrowsers": "Autres navigateurs",
|
||||
"setBrowserExtensionFor": "pour {}",
|
||||
"setBrowserExtensionNotSupported": "Non supporté",
|
||||
"setBrowserExtensionHelp": "Comment installer ?",
|
||||
"setBrowserExtensionInstall": "Installer l'application",
|
||||
"setBrowserExtensionKPXCWarnHeader": "{} cessera de fonctionner",
|
||||
"setBrowserExtensionKPXCWarnBody1": "Malheureusement, il n'est pas possible de connecter une extension à plusieurs applications. Si vous connectez l'extension à KeeWeb, nous remplacerons son association d'application, ce qui signifie que l'intégration avec {} cessera de fonctionner. Même si vous décochez cette case, l'association avec {} ne sera pas restaurée. Pour le faire fonctionner à nouveau, configurez l'intégration du navigateur dans les paramètres de {}.",
|
||||
"setBrowserExtensionKPXCWarnBody2": "Configurer l'extension pour utiliser KeeWeb?",
|
||||
"setBrowserSessions": "Sessions",
|
||||
"setBrowserSessionsEmpty": "Aucune session connectée",
|
||||
"setBrowserSessionsIntro": "Ces extensions sont connectées à KeeWeb",
|
||||
"setBrowserSessionsActiveTooltip": "Session active",
|
||||
"setBrowserSessionsActiveText": "Cette session est active. Elle peut échanger des données avec KeeWeb selon les permissions:",
|
||||
"setBrowserSessionsInactiveTooltip": "session inactive",
|
||||
"setBrowserSessionsInactiveText": "Cette session est inactive. L'extension est connectée à KeeWeb, cependant, elle n'a pas essayé d'échanger des données. Lorsque l'extension demande quelque chose, vous pourrez choisir ce que vous souhaitez partager.",
|
||||
"setBrowserSessionsDeniedTooltip": "Accès interdit",
|
||||
"setBrowserSessionsDeniedText": "Cette session est inactive. L'extension est connectée à KeeWeb mais vous avez refusé l'accès aux données.",
|
||||
"setBrowserSessionsConnectedDate": "Connecté",
|
||||
"setBrowserSessionsTerminate": "Clôturer la session",
|
||||
"setBrowserSessionsAccessToFiles": "Autoriser l'accès aux fichiers",
|
||||
"setBrowserSessionsNoFileAccess": "L'extension n'a accès à aucun fichier, elle ne pourra pas récupérer les mots de passe de KeeWeb.",
|
||||
"setBrowserSessionsPasswordsRead": "Accès aux mot de passe",
|
||||
"setBrowserSessionsPasswordsWritten": "Mots de passe enregistrés",
|
||||
"setDevicesTitle": "Appareils",
|
||||
"setDevicesEnableUsb": "Autoriser les interactions avec les appareils USB",
|
||||
"setDevicesYubiKeyIntro": "Il est recommandé de lire {} avant d'utiliser une YubiKey.",
|
||||
@@ -623,12 +687,12 @@
|
||||
"setHelpProblems2": "ou {}",
|
||||
"setHelpOpenIssue": "faire connaître le problème sur GitHub",
|
||||
"setHelpContactLink": "contactez le développeur directement",
|
||||
"setHelpAppInfo": "Information",
|
||||
"setHelpAppInfo": "Informations",
|
||||
"setHelpOtherPlatforms": "Autres plateformes",
|
||||
"setHelpDesktopApps": "Applis Desktop",
|
||||
"setHelpWebApp": "Appli web",
|
||||
"setHelpUpdates": "Mises à jour",
|
||||
"setHelpTwitter": "Appli twitter",
|
||||
"setHelpTwitter": "Twitter",
|
||||
"dropboxSetupDesc": "Une configuration particulière est nécessaire pour utiliser Dropbox dans une application auto-hébergée. Merci de créer votre propre application Dropbox et d'inscrire sa clé ci-dessous.",
|
||||
"dropboxAppKey": "Clé Dropbox",
|
||||
"dropboxAppKeyDesc": "Copier la clé de votre appli Dropbox (Réglages développeur)",
|
||||
@@ -644,6 +708,8 @@
|
||||
"dropboxLinkFull": "Tout Dropbox ou n'importe quel dossier",
|
||||
"dropboxLinkCustom": "Votre appli Dropbox",
|
||||
"gdriveSharedWithMe": "Partagé avec moi",
|
||||
"gdriveSharedDrives": "Lecteurs partagés",
|
||||
"gdriveTeamDrives": "Team drives",
|
||||
"webdavSaveMethod": "Méthode de sauvegarde",
|
||||
"webdavSaveMove": "Envoyer un fichier temporaire et le déplacer",
|
||||
"webdavSavePut": "Écraser le fichier kdbx avec PUT",
|
||||
@@ -672,5 +738,39 @@
|
||||
"yubiKeyDisabledErrorHeader": "L'USB est désactivé",
|
||||
"yubiKeyDisabledErrorBody": "Yubikey est nécessaire pour ouvrir ce fichier, merci d'activer les appareils USB dans les paramètres",
|
||||
"yubiKeyErrorWithCode": "Erreur Yubikey code {}.",
|
||||
"bioOpenAuthPrompt": "ouvrir \"{}\""
|
||||
"bioOpenAuthPrompt": "ouvrir \"{}\"",
|
||||
"extensionErrorNoOpenFiles": "Aucun fichier ouvert",
|
||||
"extensionErrorUserRejected": "La requête a été refusée",
|
||||
"extensionErrorNoMatches": "Aucune correspondance",
|
||||
"extensionErrorAlertDisplayed": "Impossible de poser une question maintenant car une autre boîte de dialogue est affichée, veuillez réessayer",
|
||||
"extensionConnectHeader": "Échange de données avec l'extension",
|
||||
"extensionConnectIntro": "Une extension de navigateur qui a pour nom {} essaie d'échanger des données avec KeeWeb.",
|
||||
"extensionConnectUnknownActivity": "KeeWeb ne vérifie pas que l'application connectée est ce qu'elle prétend être. N'approuvez la demande que si vous en connaissez l'origine.",
|
||||
"extensionConnectFiles": "Lors de cette session, autoriser l'accès à:",
|
||||
"extensionConnectAllOtherFiles": "Tous les autres fichiers",
|
||||
"extensionConnectAllFiles": "Tous les fichiers",
|
||||
"extensionConnectAskGet": "Demandez avant d'envoyer les mots de passe à l'extension:",
|
||||
"extensionConnectAskGetMultiple": "s'il y a plus d'une correspondance",
|
||||
"extensionConnectAskGetAlways": "Toujours",
|
||||
"extensionConnectAskSave": "Demandez avant d'enregistrer de nouveaux mots de passe dans KeeWeb:",
|
||||
"extensionConnectAskSaveAlways": "Toujours",
|
||||
"extensionConnectAskSaveAuto": "quand ce n'est pas possible de sauvegarder automatiquement",
|
||||
"extensionConnectSettingsAreForSession": "Les paramètres que vous sélectionnez ici ne sont valides que pour la session en cours. Vous pouvez afficher et gérer les sessions dans les paramètres de KeeWeb.",
|
||||
"extensionUnlockMessage": "Déverrouiller pour connecter l'extension du navigateur",
|
||||
"extensionNewGroupHeader": "Nouveau groupe",
|
||||
"extensionNewGroupBody": "{} essaye de créer un nouveau groupe. Autoriser cela?",
|
||||
"extensionNewGroupPath": "Dossier du groupe",
|
||||
"extensionNewGroupFile": "Ce groupe sera créé dans:",
|
||||
"extensionSaveEntryHeader": "Sauvegarder mot de passe",
|
||||
"extensionSaveEntryBody": "{} essaye de sauvegarder un mot de passe. Autoriser cela?",
|
||||
"extensionSaveEntryAuto": "Sauvegarder les autres mots de passe automatiquement lors de cette session",
|
||||
"extensionSaveEntryNewGroup": "nouveau groupe",
|
||||
"extensionSelectPasswordFor": "Sélectionner un mot de passe pour {}",
|
||||
"selectEntryHeader": "Sélectionner une entrée",
|
||||
"selectEntryEnterHint": "Utiliser la ligne sélectionnée",
|
||||
"selectEntryTypingHint": "Ecrire pour filtrer",
|
||||
"selectEntryContains": "Contient le texte",
|
||||
"selectEntrySubdomains": "Sous-domaines",
|
||||
"selectEntryFieldHeader": "Sélectionnez un champ",
|
||||
"selectEntryFieldTouch": "Appuyez sur le bouton de votre appareil pour générer un code à usage unique."
|
||||
}
|
||||
+134
-16
@@ -12,11 +12,12 @@ import { EntryModel } from 'models/entry-model';
|
||||
import { FileInfoModel } from 'models/file-info-model';
|
||||
import { FileModel } from 'models/file-model';
|
||||
import { GroupModel } from 'models/group-model';
|
||||
import { YubiKeyOtpModel } from 'models/external/yubikey-otp-model';
|
||||
import { YubiKeyOtpModel } from 'models/otp-device/yubikey-otp-model';
|
||||
import { MenuModel } from 'models/menu/menu-model';
|
||||
import { PluginManager } from 'plugins/plugin-manager';
|
||||
import { Features } from 'util/features';
|
||||
import { DateFormat } from 'comp/i18n/date-format';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { UrlFormat } from 'util/formatting/url-format';
|
||||
import { IdGenerator } from 'util/generators/id-generator';
|
||||
import { Locale } from 'util/locale';
|
||||
@@ -38,6 +39,9 @@ class AppModel {
|
||||
advancedSearch = null;
|
||||
attachedYubiKeysCount = 0;
|
||||
memoryPasswordStorage = {};
|
||||
fileUnlockPromise = null;
|
||||
hardwareDecryptInProgress = false;
|
||||
mainWindowBlurTimer = null;
|
||||
|
||||
constructor() {
|
||||
Events.on('refresh', this.refresh.bind(this));
|
||||
@@ -48,6 +52,11 @@ class AppModel {
|
||||
Events.on('select-entry', this.selectEntry.bind(this));
|
||||
Events.on('unset-keyfile', this.unsetKeyFile.bind(this));
|
||||
Events.on('usb-devices-changed', this.usbDevicesChanged.bind(this));
|
||||
Events.on('main-window-blur', this.mainWindowBlur.bind(this));
|
||||
Events.on('main-window-focus', this.mainWindowFocus.bind(this));
|
||||
Events.on('main-window-will-close', this.mainWindowWillClose.bind(this));
|
||||
Events.on('hardware-decrypt-started', this.hardwareDecryptStarted.bind(this));
|
||||
Events.on('hardware-decrypt-finished', this.hardwareDecryptFinished.bind(this));
|
||||
|
||||
this.appLogger = new Logger('app');
|
||||
AppModel.instance = this;
|
||||
@@ -167,10 +176,29 @@ class AppModel {
|
||||
page: 'file',
|
||||
file
|
||||
});
|
||||
|
||||
this.refresh();
|
||||
|
||||
file.on('reload', this.reloadFile.bind(this));
|
||||
file.on('change', () => Events.emit('file-changed', file));
|
||||
file.on('change', () => {
|
||||
Events.emit('file-changed', file);
|
||||
});
|
||||
file.on('ejected', () => this.closeFile(file));
|
||||
file.on('change:dirty', (file, dirty) => {
|
||||
if (dirty && this.settings.autoSaveInterval === -1) {
|
||||
this.syncFile(file);
|
||||
}
|
||||
});
|
||||
|
||||
Events.emit('file-opened');
|
||||
|
||||
if (this.fileUnlockPromise) {
|
||||
this.appLogger.info('Running pending file unlock operation');
|
||||
this.fileUnlockPromise.resolve(file);
|
||||
this.fileUnlockPromise = null;
|
||||
Events.emit('unlock-message-changed', null);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -232,6 +260,9 @@ class AppModel {
|
||||
}
|
||||
|
||||
closeAllFiles() {
|
||||
if (!this.files.hasOpenFiles()) {
|
||||
return;
|
||||
}
|
||||
for (const file of this.files) {
|
||||
file.close();
|
||||
this.fileClosed(file);
|
||||
@@ -244,6 +275,7 @@ class AppModel {
|
||||
this.tags.splice(0, this.tags.length);
|
||||
this.filter = {};
|
||||
this.menu.select({ item: this.menu.allItemsItem });
|
||||
Events.emit('all-files-closed');
|
||||
}
|
||||
|
||||
closeFile(file) {
|
||||
@@ -254,6 +286,7 @@ class AppModel {
|
||||
this.menu.groupsSection.removeByFile(file);
|
||||
this.menu.filesSection.removeByFile(file);
|
||||
this.menu.select({ item: this.menu.allItemsSection.items[0] });
|
||||
Events.emit('one-file-closed');
|
||||
}
|
||||
|
||||
emptyTrash() {
|
||||
@@ -295,7 +328,7 @@ class AppModel {
|
||||
}
|
||||
|
||||
getEntries() {
|
||||
const entries = this.getEntriesByFilter(this.filter);
|
||||
const entries = this.getEntriesByFilter(this.filter, this.files);
|
||||
entries.sortEntries(this.sort, this.filter);
|
||||
if (this.filter.trash) {
|
||||
this.addTrashGroups(entries);
|
||||
@@ -303,16 +336,16 @@ class AppModel {
|
||||
return entries;
|
||||
}
|
||||
|
||||
getEntriesByFilter(filter) {
|
||||
getEntriesByFilter(filter, files) {
|
||||
const preparedFilter = this.prepareFilter(filter);
|
||||
const entries = new SearchResultCollection();
|
||||
|
||||
const devicesToMatchOtpEntries = this.files.filter((file) => file.external);
|
||||
const devicesToMatchOtpEntries = files.filter((file) => file.backend === 'otp-device');
|
||||
|
||||
const matchedOtpEntrySet = this.settings.yubiKeyMatchEntries ? new Set() : undefined;
|
||||
|
||||
this.files
|
||||
.filter((file) => !file.external)
|
||||
files
|
||||
.filter((file) => file.backend !== 'otp-device')
|
||||
.forEach((file) => {
|
||||
file.forEachEntry(preparedFilter, (entry) => {
|
||||
if (matchedOtpEntrySet) {
|
||||
@@ -427,7 +460,7 @@ class AppModel {
|
||||
|
||||
createNewEntry(args) {
|
||||
const sel = this.getFirstSelectedGroupForCreation();
|
||||
if (args && args.template) {
|
||||
if (args?.template) {
|
||||
if (sel.file !== args.template.file) {
|
||||
sel.file = args.template.file;
|
||||
sel.group = args.template.file.groups[0];
|
||||
@@ -443,11 +476,21 @@ class AppModel {
|
||||
}
|
||||
}
|
||||
|
||||
createNewEntryWithFields(group, fields) {
|
||||
return EntryModel.newEntryWithFields(group, fields);
|
||||
}
|
||||
|
||||
createNewGroup() {
|
||||
const sel = this.getFirstSelectedGroupForCreation();
|
||||
return GroupModel.newGroup(sel.group, sel.file);
|
||||
}
|
||||
|
||||
createNewGroupWithName(group, file, name) {
|
||||
const newGroup = GroupModel.newGroup(group, file);
|
||||
newGroup.setName(name);
|
||||
return newGroup;
|
||||
}
|
||||
|
||||
createNewTemplateEntry() {
|
||||
const file = this.getFirstSelectedGroupForCreation().file;
|
||||
const group = file.getEntryTemplatesGroup() || file.createEntryTemplatesGroup();
|
||||
@@ -466,7 +509,7 @@ class AppModel {
|
||||
}
|
||||
}
|
||||
|
||||
createNewFile(name) {
|
||||
createNewFile(name, callback) {
|
||||
if (!name) {
|
||||
for (let i = 0; ; i++) {
|
||||
name = Locale.openNewFile + (i || '');
|
||||
@@ -476,9 +519,10 @@ class AppModel {
|
||||
}
|
||||
}
|
||||
const newFile = new FileModel({ id: IdGenerator.uuid() });
|
||||
newFile.create(name);
|
||||
this.addFile(newFile);
|
||||
return newFile;
|
||||
newFile.create(name, () => {
|
||||
this.addFile(newFile);
|
||||
callback?.(newFile);
|
||||
});
|
||||
}
|
||||
|
||||
openFile(params, callback) {
|
||||
@@ -653,7 +697,7 @@ class AppModel {
|
||||
let needLoadKeyFile = false;
|
||||
if (!params.keyFileData && fileInfo && fileInfo.keyFileName) {
|
||||
params.keyFileName = fileInfo.keyFileName;
|
||||
if (this.settings.rememberKeyFiles === 'data') {
|
||||
if (this.settings.rememberKeyFiles === 'data' && fileInfo.keyFileHash) {
|
||||
params.keyFileData = FileModel.createKeyFileWithHash(fileInfo.keyFileHash);
|
||||
} else if (this.settings.rememberKeyFiles === 'path' && fileInfo.keyFilePath) {
|
||||
params.keyFilePath = fileInfo.keyFilePath;
|
||||
@@ -827,7 +871,10 @@ class AppModel {
|
||||
this.scheduleBackupFile(file, data);
|
||||
}
|
||||
if (this.settings.yubiKeyAutoOpen) {
|
||||
if (this.attachedYubiKeysCount > 0 && !this.files.some((f) => f.external)) {
|
||||
if (
|
||||
this.attachedYubiKeysCount > 0 &&
|
||||
!this.files.some((f) => f.backend === 'otp-device')
|
||||
) {
|
||||
this.tryOpenOtpDeviceInBackground();
|
||||
}
|
||||
}
|
||||
@@ -1271,7 +1318,9 @@ class AppModel {
|
||||
}
|
||||
|
||||
const isNewYubiKey = UsbListener.attachedYubiKeys > attachedYubiKeysCount;
|
||||
const hasOpenFiles = this.files.some((file) => file.active && !file.external);
|
||||
const hasOpenFiles = this.files.some(
|
||||
(file) => file.active && file.backend !== 'otp-device'
|
||||
);
|
||||
|
||||
if (isNewYubiKey && hasOpenFiles && !this.openingOtpDevice) {
|
||||
this.tryOpenOtpDeviceInBackground();
|
||||
@@ -1303,7 +1352,7 @@ class AppModel {
|
||||
return null;
|
||||
}
|
||||
for (const file of this.files) {
|
||||
if (file.external) {
|
||||
if (file.backend === 'otp-device') {
|
||||
const matchingEntry = file.getMatchingEntry(entry);
|
||||
if (matchingEntry) {
|
||||
return matchingEntry;
|
||||
@@ -1402,6 +1451,75 @@ class AppModel {
|
||||
this.memoryPasswordStorage = {};
|
||||
}
|
||||
}
|
||||
|
||||
unlockAnyFile(unlockRes, timeout) {
|
||||
this.rejectPendingFileUnlockPromise('Replaced with a new operation');
|
||||
Events.emit('show-open-view');
|
||||
return new Promise((resolve, reject) => {
|
||||
this.fileUnlockPromise = { resolve, reject, unlockRes };
|
||||
if (timeout) {
|
||||
const timer = setTimeout(
|
||||
() => this.rejectPendingFileUnlockPromise('Timeout'),
|
||||
timeout
|
||||
);
|
||||
this.fileUnlockPromise.resolve = (res) => {
|
||||
clearTimeout(timer);
|
||||
resolve(res);
|
||||
};
|
||||
this.fileUnlockPromise.reject = (err) => {
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
};
|
||||
}
|
||||
this.appLogger.info('Pending file unlock operation is set');
|
||||
Events.emit('unlock-message-changed', unlockRes);
|
||||
});
|
||||
}
|
||||
|
||||
get unlockMessageRes() {
|
||||
return this.fileUnlockPromise?.unlockRes;
|
||||
}
|
||||
|
||||
rejectPendingFileUnlockPromise(reason) {
|
||||
if (this.fileUnlockPromise) {
|
||||
this.appLogger.info('Cancel pending file unlock operation', reason);
|
||||
this.fileUnlockPromise.reject(new Error(reason));
|
||||
this.fileUnlockPromise = null;
|
||||
Events.emit('unlock-message-changed', null);
|
||||
}
|
||||
}
|
||||
|
||||
mainWindowBlur() {
|
||||
if (!this.hardwareDecryptInProgress) {
|
||||
this.mainWindowBlurTimer = setTimeout(() => {
|
||||
// macOS emits focus-blur-focus event in a row when triggering auto-type from minimized state
|
||||
delete this.mainWindowBlurTimer;
|
||||
this.rejectPendingFileUnlockPromise('Main window blur');
|
||||
}, Timeouts.AutoTypeWindowFocusAfterBlur);
|
||||
}
|
||||
}
|
||||
|
||||
mainWindowFocus() {
|
||||
if (this.mainWindowBlurTimer) {
|
||||
clearTimeout(this.mainWindowBlurTimer);
|
||||
this.mainWindowBlurTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
mainWindowWillClose() {
|
||||
this.rejectPendingFileUnlockPromise('Main window will close');
|
||||
}
|
||||
|
||||
hardwareDecryptStarted() {
|
||||
this.hardwareDecryptInProgress = true;
|
||||
}
|
||||
|
||||
hardwareDecryptFinished() {
|
||||
this.hardwareDecryptInProgress = false;
|
||||
if (!Launcher.isAppFocused()) {
|
||||
this.rejectPendingFileUnlockPromise('App is not focused after hardware decrypt');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { AppModel };
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -345,20 +351,20 @@ class EntryModel extends Model {
|
||||
}
|
||||
|
||||
hasField(field) {
|
||||
return Object.prototype.hasOwnProperty.call(this.entry.fields, field);
|
||||
return this.entry.fields.has(field);
|
||||
}
|
||||
|
||||
addAttachment(name, data) {
|
||||
this._entryModified();
|
||||
return this.file.db.createBinary(data).then((binaryRef) => {
|
||||
this.entry.binaries[name] = binaryRef;
|
||||
this.entry.binaries.set(name, binaryRef);
|
||||
this._fillByEntry();
|
||||
});
|
||||
}
|
||||
|
||||
removeAttachment(name) {
|
||||
this._entryModified();
|
||||
delete this.entry.binaries[name];
|
||||
this.entry.binaries.delete(name);
|
||||
this._fillByEntry();
|
||||
}
|
||||
|
||||
@@ -388,8 +394,8 @@ class EntryModel extends Model {
|
||||
this.entry.pushHistory();
|
||||
this.unsaved = true;
|
||||
this.file.setModified();
|
||||
this.entry.fields = {};
|
||||
this.entry.binaries = {};
|
||||
this.entry.fields = new Map();
|
||||
this.entry.binaries = new Map();
|
||||
this.entry.copyFrom(historyEntry);
|
||||
this._entryModified();
|
||||
this._fillByEntry();
|
||||
@@ -400,8 +406,8 @@ class EntryModel extends Model {
|
||||
this.unsaved = false;
|
||||
const historyEntry = this.entry.history[this.entry.history.length - 1];
|
||||
this.entry.removeHistory(this.entry.history.length - 1);
|
||||
this.entry.fields = {};
|
||||
this.entry.binaries = {};
|
||||
this.entry.fields = new Map();
|
||||
this.entry.binaries = new Map();
|
||||
this.entry.copyFrom(historyEntry);
|
||||
this._fillByEntry();
|
||||
}
|
||||
@@ -474,14 +480,14 @@ class EntryModel extends Model {
|
||||
otpUrl = Otp.makeUrl(args.key, args.step, args.size);
|
||||
}
|
||||
}
|
||||
} else if (this.entry.fields['TOTP Seed']) {
|
||||
} else if (this.entry.fields.get('TOTP Seed')) {
|
||||
// TrayTOTP plugin format
|
||||
let secret = this.entry.fields['TOTP Seed'];
|
||||
let secret = this.entry.fields.get('TOTP Seed');
|
||||
if (secret.isProtected) {
|
||||
secret = secret.getText();
|
||||
}
|
||||
if (secret) {
|
||||
let settings = this.entry.fields['TOTP Settings'];
|
||||
let settings = this.entry.fields.get('TOTP Settings');
|
||||
if (settings && settings.isProtected) {
|
||||
settings = settings.getText();
|
||||
}
|
||||
@@ -505,7 +511,7 @@ class EntryModel extends Model {
|
||||
}
|
||||
try {
|
||||
this.otpGenerator = Otp.parseUrl(otpUrl);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
this.otpGenerator = null;
|
||||
}
|
||||
} else {
|
||||
@@ -520,8 +526,8 @@ class EntryModel extends Model {
|
||||
|
||||
setOtpUrl(url) {
|
||||
this.setField('otp', url ? kdbxweb.ProtectedValue.fromString(url) : undefined);
|
||||
delete this.entry.fields['TOTP Seed'];
|
||||
delete this.entry.fields['TOTP Settings'];
|
||||
this.entry.fields.delete('TOTP Seed');
|
||||
this.entry.fields.delete('TOTP Settings');
|
||||
}
|
||||
|
||||
getEffectiveEnableAutoType() {
|
||||
@@ -572,7 +578,7 @@ class EntryModel extends Model {
|
||||
newEntry.entry.uuid = uuid;
|
||||
newEntry.entry.times.update();
|
||||
newEntry.entry.times.creationTime = newEntry.entry.times.lastModTime;
|
||||
newEntry.entry.fields.Title = this.title + nameSuffix;
|
||||
newEntry.entry.fields.set('Title', this.title + nameSuffix);
|
||||
newEntry._fillByEntry();
|
||||
this.file.reload();
|
||||
return newEntry;
|
||||
@@ -584,7 +590,7 @@ class EntryModel extends Model {
|
||||
this.entry.uuid = uuid;
|
||||
this.entry.times.update();
|
||||
this.entry.times.creationTime = this.entry.times.lastModTime;
|
||||
this.entry.fields.Title = '';
|
||||
this.entry.fields.set('Title', '');
|
||||
this._fillByEntry();
|
||||
}
|
||||
|
||||
@@ -610,7 +616,7 @@ class EntryModel extends Model {
|
||||
const allFields = Object.keys(fieldWeights).concat(Object.keys(this.fields));
|
||||
|
||||
return allFields.reduce((rank, fieldName) => {
|
||||
const val = this.entry.fields[fieldName];
|
||||
const val = this.entry.fields.get(fieldName);
|
||||
if (!val) {
|
||||
return rank;
|
||||
}
|
||||
@@ -628,17 +634,38 @@ class EntryModel extends Model {
|
||||
}
|
||||
|
||||
canCheckPasswordIssues() {
|
||||
return !this.entry.customData?.IgnorePwIssues;
|
||||
return !this.entry.customData?.has('IgnorePwIssues');
|
||||
}
|
||||
|
||||
setIgnorePasswordIssues() {
|
||||
if (!this.entry.customData) {
|
||||
this.entry.customData = {};
|
||||
this.entry.customData = new Map();
|
||||
}
|
||||
this.entry.customData.IgnorePwIssues = '1';
|
||||
this.entry.customData.set('IgnorePwIssues', '1');
|
||||
this._entryModified();
|
||||
}
|
||||
|
||||
getNextUrlFieldName() {
|
||||
const takenFields = new Set(
|
||||
[...this.entry.fields.keys()].filter((f) => f.startsWith(ExtraUrlFieldName))
|
||||
);
|
||||
for (let i = 0; ; i++) {
|
||||
const fieldName = i ? `${ExtraUrlFieldName}_${i}` : ExtraUrlFieldName;
|
||||
if (!takenFields.has(fieldName)) {
|
||||
return fieldName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAllUrls() {
|
||||
const urls = this.url ? [this.url] : [];
|
||||
const extraUrls = Object.entries(this.fields)
|
||||
.filter(([field]) => field.startsWith(ExtraUrlFieldName))
|
||||
.map(([, value]) => (value.isProtected ? value.getText() : value))
|
||||
.filter((value) => value);
|
||||
return urls.concat(extraUrls);
|
||||
}
|
||||
|
||||
static fromEntry(entry, group, file) {
|
||||
const model = new EntryModel();
|
||||
model.setEntry(entry, group, file);
|
||||
@@ -663,8 +690,16 @@ class EntryModel extends Model {
|
||||
file.setModified();
|
||||
return model;
|
||||
}
|
||||
|
||||
static newEntryWithFields(group, fields) {
|
||||
const entry = EntryModel.newEntry(group, group.file);
|
||||
for (const [field, value] of Object.entries(fields)) {
|
||||
entry.setField(field, value);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
EntryModel.defineModelProperties({}, { extensions: true });
|
||||
|
||||
export { EntryModel };
|
||||
export { EntryModel, ExtraUrlFieldName };
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { ExternalDeviceModel } from 'models/external/external-device-model';
|
||||
|
||||
class ExternalOtpDeviceModel extends ExternalDeviceModel {
|
||||
open(callback) {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
|
||||
cancelOpen() {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
|
||||
close(callback) {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
|
||||
getOtp(callback) {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
}
|
||||
|
||||
export { ExternalOtpDeviceModel };
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import demoFileData from 'demo.kdbx';
|
||||
import { Model } from 'framework/model';
|
||||
import { Events } from 'framework/events';
|
||||
@@ -8,7 +8,8 @@ import { GroupModel } from 'models/group-model';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { IconUrlFormat } from 'util/formatting/icon-url-format';
|
||||
import { Logger } from 'util/logger';
|
||||
import { mapObject } from 'util/fn';
|
||||
import { Locale } from 'util/locale';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
import { ChalRespCalculator } from 'comp/app/chal-resp-calculator';
|
||||
|
||||
const logger = new Logger('file');
|
||||
@@ -31,19 +32,20 @@ class FileModel extends Model {
|
||||
kdbxweb.Kdbx.load(fileData, credentials)
|
||||
.then((db) => {
|
||||
this.db = db;
|
||||
})
|
||||
.then(() => {
|
||||
this.readModel();
|
||||
this.setOpenFile({ passwordLength: password ? password.textLength : 0 });
|
||||
if (keyFileData) {
|
||||
kdbxweb.ByteUtils.zeroBuffer(keyFileData);
|
||||
}
|
||||
this.fixVersion();
|
||||
logger.info(
|
||||
'Opened file ' +
|
||||
this.name +
|
||||
': ' +
|
||||
logger.ts(ts) +
|
||||
', ' +
|
||||
this.kdfArgsToString(db.header) +
|
||||
this.kdfArgsToString(this.db.header) +
|
||||
', ' +
|
||||
Math.round(fileData.byteLength / 1024) +
|
||||
' kB'
|
||||
@@ -90,13 +92,14 @@ class FileModel extends Model {
|
||||
}
|
||||
}
|
||||
|
||||
create(name) {
|
||||
create(name, callback) {
|
||||
const password = kdbxweb.ProtectedValue.fromString('');
|
||||
const credentials = new kdbxweb.Credentials(password);
|
||||
this.db = kdbxweb.Kdbx.create(credentials, name);
|
||||
this.name = name;
|
||||
this.readModel();
|
||||
this.set({ active: true, created: true, name });
|
||||
callback();
|
||||
}
|
||||
|
||||
importWithXml(fileXml, callback) {
|
||||
@@ -107,6 +110,8 @@ class FileModel extends Model {
|
||||
kdbxweb.Kdbx.loadXml(fileXml, credentials)
|
||||
.then((db) => {
|
||||
this.db = db;
|
||||
})
|
||||
.then(() => {
|
||||
this.readModel();
|
||||
this.set({ active: true, created: true });
|
||||
logger.info('Imported file ' + this.name + ': ' + logger.ts(ts));
|
||||
@@ -128,13 +133,16 @@ class FileModel extends Model {
|
||||
const demoFile = kdbxweb.ByteUtils.arrayToBuffer(
|
||||
kdbxweb.ByteUtils.base64ToBytes(demoFileData)
|
||||
);
|
||||
kdbxweb.Kdbx.load(demoFile, credentials).then((db) => {
|
||||
this.db = db;
|
||||
this.name = 'Demo';
|
||||
this.readModel();
|
||||
this.setOpenFile({ passwordLength: 4, demo: true });
|
||||
callback();
|
||||
});
|
||||
kdbxweb.Kdbx.load(demoFile, credentials)
|
||||
.then((db) => {
|
||||
this.db = db;
|
||||
})
|
||||
.then(() => {
|
||||
this.name = 'Demo';
|
||||
this.readModel();
|
||||
this.setOpenFile({ passwordLength: 4, demo: true });
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
setOpenFile(props) {
|
||||
@@ -257,17 +265,6 @@ class FileModel extends Model {
|
||||
});
|
||||
}
|
||||
|
||||
fixVersion() {
|
||||
if (
|
||||
this.db.meta.generator === 'KdbxWeb' &&
|
||||
this.db.header.versionMajor === 4 &&
|
||||
this.db.header.versionMinor === 1
|
||||
) {
|
||||
this.db.header.versionMinor = 0;
|
||||
logger.info('Fixed file version: 4.1 => 4.0');
|
||||
}
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.buildObjectMap();
|
||||
this.readModel();
|
||||
@@ -407,7 +404,7 @@ class FileModel extends Model {
|
||||
createEntryTemplatesGroup() {
|
||||
const rootGroup = this.groups[0];
|
||||
const templatesGroup = GroupModel.newGroup(rootGroup, this);
|
||||
templatesGroup.setName('Templates');
|
||||
templatesGroup.setName(StringFormat.capFirst(Locale.templates));
|
||||
this.db.meta.entryTemplatesGroup = templatesGroup.group.uuid;
|
||||
this.reload();
|
||||
return templatesGroup;
|
||||
@@ -416,7 +413,6 @@ class FileModel extends Model {
|
||||
setModified() {
|
||||
if (!this.demo) {
|
||||
this.set({ modified: true, dirty: true });
|
||||
Events.emit('file-modified');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -672,16 +668,19 @@ class FileModel extends Model {
|
||||
}
|
||||
|
||||
getCustomIcons() {
|
||||
return mapObject(this.db.meta.customIcons, (customIcon) =>
|
||||
IconUrlFormat.toDataUrl(customIcon)
|
||||
);
|
||||
const customIcons = {};
|
||||
for (const [id, icon] of this.db.meta.customIcons) {
|
||||
customIcons[id] = IconUrlFormat.toDataUrl(icon.data);
|
||||
}
|
||||
return customIcons;
|
||||
}
|
||||
|
||||
addCustomIcon(iconData) {
|
||||
const uuid = kdbxweb.KdbxUuid.random();
|
||||
this.db.meta.customIcons[uuid] = kdbxweb.ByteUtils.arrayToBuffer(
|
||||
kdbxweb.ByteUtils.base64ToBytes(iconData)
|
||||
);
|
||||
this.db.meta.customIcons.set(uuid.id, {
|
||||
data: kdbxweb.ByteUtils.arrayToBuffer(kdbxweb.ByteUtils.base64ToBytes(iconData)),
|
||||
lastModified: new Date()
|
||||
});
|
||||
return uuid.toString();
|
||||
}
|
||||
|
||||
@@ -766,7 +765,12 @@ FileModel.defineModelProperties({
|
||||
oldKeyFileHash: null,
|
||||
oldKeyChangeDate: null,
|
||||
encryptedPassword: null,
|
||||
encryptedPasswordDate: null
|
||||
encryptedPasswordDate: null,
|
||||
supportsTags: true,
|
||||
supportsColors: true,
|
||||
supportsIcons: true,
|
||||
supportsExpiration: true,
|
||||
defaultGroupHash: ''
|
||||
});
|
||||
|
||||
export { FileModel };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { IconMap } from 'const/icon-map';
|
||||
import { EntryModel } from 'models/entry-model';
|
||||
import { MenuItemModel } from 'models/menu/menu-item-model';
|
||||
@@ -86,7 +86,9 @@ class GroupModel extends MenuItemModel {
|
||||
_buildCustomIcon() {
|
||||
this.customIcon = null;
|
||||
if (this.group.customIcon) {
|
||||
return IconUrlFormat.toDataUrl(this.file.db.meta.customIcons[this.group.customIcon]);
|
||||
return IconUrlFormat.toDataUrl(
|
||||
this.file.db.meta.customIcons.get(this.group.customIcon.id)?.data
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { MenuSectionModel } from 'models/menu/menu-section-model';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
import { Locale } from 'util/locale';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Features } from 'util/features';
|
||||
|
||||
class MenuModel extends Model {
|
||||
constructor() {
|
||||
@@ -113,6 +114,11 @@ class MenuModel extends Model {
|
||||
this.shortcutsSection = new MenuSectionModel([
|
||||
{ locTitle: 'shortcuts', icon: 'keyboard', page: 'shortcuts' }
|
||||
]);
|
||||
if (Features.supportsBrowserExtensions) {
|
||||
this.browserSection = new MenuSectionModel([
|
||||
{ locTitle: 'menuSetBrowser', icon: Features.browserIcon, page: 'browser' }
|
||||
]);
|
||||
}
|
||||
this.pluginsSection = new MenuSectionModel([
|
||||
{ locTitle: 'plugins', icon: 'puzzle-piece', page: 'plugins' }
|
||||
]);
|
||||
@@ -133,6 +139,7 @@ class MenuModel extends Model {
|
||||
[
|
||||
this.generalSection,
|
||||
this.shortcutsSection,
|
||||
this.browserSection,
|
||||
this.pluginsSection,
|
||||
this.devicesSection,
|
||||
this.aboutSection,
|
||||
|
||||
@@ -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' }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,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,19 +17,19 @@ const Features = {
|
||||
!/^http(s?):\/\/((localhost:8085)|((app|beta)\.keeweb\.info))/.test(location.href),
|
||||
isLocal: location.origin.indexOf('localhost') >= 0,
|
||||
|
||||
supportsTitleBarStyles() {
|
||||
get supportsTitleBarStyles() {
|
||||
return isDesktop && (this.isMac || this.isWindows);
|
||||
},
|
||||
supportsCustomTitleBarAndDraggableWindow() {
|
||||
get supportsCustomTitleBarAndDraggableWindow() {
|
||||
return isDesktop && this.isMac;
|
||||
},
|
||||
renderCustomTitleBar() {
|
||||
get renderCustomTitleBar() {
|
||||
return isDesktop && this.isWindows;
|
||||
},
|
||||
hasUnicodeFlags() {
|
||||
get hasUnicodeFlags() {
|
||||
return this.isMac;
|
||||
},
|
||||
getBrowserCssClass() {
|
||||
get browserCssClass() {
|
||||
if (window.chrome && window.navigator.userAgent.indexOf('Chrome/') > -1) {
|
||||
return 'chrome';
|
||||
}
|
||||
@@ -40,6 +40,45 @@ const Features = {
|
||||
return 'standalone';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
get browserIcon() {
|
||||
if (this._browserIcon) {
|
||||
return this._browserIcon;
|
||||
}
|
||||
|
||||
if (this.isDesktop) {
|
||||
this._browserIcon = this.isMac ? 'safari' : this.isWindows ? 'edge' : 'chrome';
|
||||
} else if (/Gecko\//.test(navigator.userAgent)) {
|
||||
this._browserIcon = 'firefox-browser';
|
||||
} else if (/Edg\//.test(navigator.userAgent)) {
|
||||
this._browserIcon = 'edge';
|
||||
} else if (/Chrome\//.test(navigator.userAgent)) {
|
||||
this._browserIcon = 'chrome';
|
||||
} else if (this.isMac && /Safari\//.test(navigator.userAgent)) {
|
||||
this._browserIcon = 'safari';
|
||||
} else {
|
||||
this._browserIcon = 'window-maximize';
|
||||
}
|
||||
|
||||
return this._browserIcon;
|
||||
},
|
||||
get supportsBrowserExtensions() {
|
||||
return !this.isMobile && (this.isDesktop || this.browserIcon !== 'safari');
|
||||
},
|
||||
get extensionBrowserFamily() {
|
||||
if (Features.isDesktop) {
|
||||
return undefined;
|
||||
} else if (/Gecko\//.test(navigator.userAgent)) {
|
||||
return 'Firefox';
|
||||
} else if (/Edg\//.test(navigator.userAgent)) {
|
||||
return 'Edge';
|
||||
} else if (/Chrome\//.test(navigator.userAgent)) {
|
||||
return 'Chrome';
|
||||
} else if (this.isMac && /Safari\//.test(navigator.userAgent)) {
|
||||
return 'Safari';
|
||||
} else {
|
||||
return 'Chrome';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
|
||||
const IconUrlFormat = {
|
||||
toDataUrl(iconData) {
|
||||
|
||||
@@ -2,6 +2,7 @@ const UrlFormat = {
|
||||
multiSlashRegex: /\/{2,}/g,
|
||||
lastPartRegex: /[\/\\]?[^\/\\]+$/,
|
||||
kdbxEndRegex: /\.kdbx$/i,
|
||||
maxShortPresentableUrlLength: 60,
|
||||
|
||||
getDataFileName(url) {
|
||||
const ix = url.lastIndexOf('/');
|
||||
@@ -35,6 +36,36 @@ const UrlFormat = {
|
||||
return Object.entries(params)
|
||||
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
|
||||
.join('&');
|
||||
},
|
||||
|
||||
presentAsShortUrl(url) {
|
||||
if (url.length <= this.maxShortPresentableUrlLength) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const [beforeHash] = url.split('#', 1);
|
||||
if (beforeHash.length <= this.maxShortPresentableUrlLength) {
|
||||
return beforeHash + '#…';
|
||||
}
|
||||
|
||||
const [beforeQuestionMark] = url.split('?', 1);
|
||||
if (beforeQuestionMark.length <= this.maxShortPresentableUrlLength) {
|
||||
return beforeQuestionMark + '?…';
|
||||
}
|
||||
|
||||
const parsed = new URL(beforeQuestionMark);
|
||||
const pathParts = parsed.pathname.split('/');
|
||||
|
||||
while (pathParts.length > 1) {
|
||||
pathParts.pop();
|
||||
parsed.pathname = pathParts.join('/');
|
||||
const res = parsed.toString();
|
||||
if (res.length < this.maxShortPresentableUrlLength) {
|
||||
return res + '/…';
|
||||
}
|
||||
}
|
||||
|
||||
return parsed + '…';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { phonetic } from 'util/generators/phonetic';
|
||||
import { shuffle } from 'util/fn';
|
||||
|
||||
const CharRanges = {
|
||||
upper: 'ABCDEFGHJKLMNPQRSTUVWXYZ',
|
||||
@@ -41,18 +42,41 @@ const PasswordGenerator = {
|
||||
}
|
||||
const rangesByPatternChar = {
|
||||
...DefaultCharRangesByPattern,
|
||||
'X': ranges.join(''),
|
||||
'I': opts.include || ''
|
||||
};
|
||||
const pattern = opts.pattern || 'X';
|
||||
const randomBytes = kdbxweb.Random.getBytes(opts.length);
|
||||
|
||||
let countDefaultChars = 0;
|
||||
for (let i = 0; i < opts.length; i++) {
|
||||
const patternChar = pattern[i % pattern.length];
|
||||
if (patternChar === 'X') {
|
||||
countDefaultChars++;
|
||||
}
|
||||
}
|
||||
|
||||
const rangeIxRandomBytes = kdbxweb.CryptoEngine.random(countDefaultChars);
|
||||
const rangeCharRandomBytes = kdbxweb.CryptoEngine.random(countDefaultChars);
|
||||
const defaultRangeGeneratedChars = [];
|
||||
for (let i = 0; i < countDefaultChars; i++) {
|
||||
const rangeIx = i < ranges.length ? i : rangeIxRandomBytes[i] % ranges.length;
|
||||
const range = ranges[rangeIx];
|
||||
const char = range[rangeCharRandomBytes[i] % range.length];
|
||||
defaultRangeGeneratedChars.push(char);
|
||||
}
|
||||
shuffle(defaultRangeGeneratedChars);
|
||||
|
||||
const randomBytes = kdbxweb.CryptoEngine.random(opts.length);
|
||||
const chars = [];
|
||||
for (let i = 0; i < opts.length; i++) {
|
||||
const rand = Math.round(Math.random() * 1000) + randomBytes[i];
|
||||
const patternChar = pattern[i % pattern.length];
|
||||
const range = rangesByPatternChar[patternChar];
|
||||
const char = range ? range[rand % range.length] : patternChar;
|
||||
chars.push(char);
|
||||
if (patternChar === 'X') {
|
||||
chars.push(defaultRangeGeneratedChars.pop());
|
||||
} else {
|
||||
const range = rangesByPatternChar[patternChar];
|
||||
const char = range ? range[rand % range.length] : patternChar;
|
||||
chars.push(char);
|
||||
}
|
||||
}
|
||||
return chars.join('');
|
||||
},
|
||||
|
||||
@@ -312,7 +312,8 @@ function generate(options) {
|
||||
word: '',
|
||||
opts: options
|
||||
};
|
||||
while (wordObj.word.length < length) {
|
||||
const safeMaxLength = length + 5;
|
||||
while (wordObj.word.length < safeMaxLength) {
|
||||
addSyllable(wordObj);
|
||||
}
|
||||
return postProcess(wordObj).substr(0, length);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Features } from 'util/features';
|
||||
import { NativeModules } from 'comp/launcher/native-modules';
|
||||
@@ -7,7 +7,7 @@ const logger = new Logger('argon2');
|
||||
|
||||
const KdbxwebInit = {
|
||||
init() {
|
||||
kdbxweb.CryptoEngine.argon2 = (...args) => this.argon2(...args);
|
||||
kdbxweb.CryptoEngine.setArgon2Impl((...args) => this.argon2(...args));
|
||||
},
|
||||
|
||||
argon2(password, salt, memory, iterations, length, parallelism, type, version) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
|
||||
const ExpectedFieldRefChars = '{REF:0@I:00000000000000000000000000000000}'.split('');
|
||||
const ExpectedFieldRefByteLength = ExpectedFieldRefChars.length;
|
||||
@@ -6,8 +6,8 @@ const ExpectedFieldRefByteLength = ExpectedFieldRefChars.length;
|
||||
kdbxweb.ProtectedValue.prototype.isProtected = true;
|
||||
|
||||
kdbxweb.ProtectedValue.prototype.forEachChar = function (fn) {
|
||||
const value = this._value;
|
||||
const salt = this._salt;
|
||||
const value = this.value;
|
||||
const salt = this.salt;
|
||||
let b, b1, b2, b3;
|
||||
for (let i = 0, len = value.length; i < len; i++) {
|
||||
b = value[i] ^ salt[i];
|
||||
@@ -154,7 +154,7 @@ kdbxweb.ProtectedValue.prototype.equals = function (other) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < len; i++) {
|
||||
if ((this._value[i] ^ this._salt[i]) !== (other._value[i] ^ other._salt[i])) {
|
||||
if ((this.value[i] ^ this.salt[i]) !== (other.value[i] ^ other.salt[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -175,14 +175,14 @@ kdbxweb.ProtectedValue.prototype.isFieldReference = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
const RandomSalt = kdbxweb.Random.getBytes(128);
|
||||
const RandomSalt = kdbxweb.CryptoEngine.random(128);
|
||||
|
||||
kdbxweb.ProtectedValue.prototype.saltedValue = function () {
|
||||
if (!this.byteLength) {
|
||||
return 0;
|
||||
}
|
||||
const value = this._value;
|
||||
const salt = this._salt;
|
||||
const value = this.value;
|
||||
const salt = this.salt;
|
||||
let salted = '';
|
||||
for (let i = 0, len = value.length; i < len; i++) {
|
||||
const byte = value[i] ^ salt[i];
|
||||
@@ -193,8 +193,8 @@ kdbxweb.ProtectedValue.prototype.saltedValue = function () {
|
||||
|
||||
kdbxweb.ProtectedValue.prototype.dataAndSalt = function () {
|
||||
return {
|
||||
data: [...this._value],
|
||||
salt: [...this._salt]
|
||||
data: [...this.value],
|
||||
salt: [...this.salt]
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ class AppView extends View {
|
||||
this.views.list.dragView = this.views.listDrag;
|
||||
this.views.details = new DetailsView(undefined, { ownParent: true });
|
||||
this.views.details.appModel = this.model;
|
||||
if (this.titlebarStyle !== 'default' && Features.renderCustomTitleBar()) {
|
||||
if (this.titlebarStyle !== 'default' && Features.renderCustomTitleBar) {
|
||||
this.views.titlebar = new TitlebarView(this.model);
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ class AppView extends View {
|
||||
this.listenTo(Events, 'toggle-settings', this.toggleSettings);
|
||||
this.listenTo(Events, 'toggle-menu', this.toggleMenu);
|
||||
this.listenTo(Events, 'toggle-details', this.toggleDetails);
|
||||
this.listenTo(Events, 'show-open-view', this.showOpenIfNotThere);
|
||||
this.listenTo(Events, 'edit-group', this.editGroup);
|
||||
this.listenTo(Events, 'edit-tag', this.editTag);
|
||||
this.listenTo(Events, 'edit-generator-presets', this.editGeneratorPresets);
|
||||
@@ -119,13 +120,13 @@ class AppView extends View {
|
||||
}
|
||||
|
||||
setWindowClass() {
|
||||
const getBrowserCssClass = Features.getBrowserCssClass();
|
||||
if (getBrowserCssClass) {
|
||||
document.body.classList.add(getBrowserCssClass);
|
||||
const browserCssClass = Features.browserCssClass;
|
||||
if (browserCssClass) {
|
||||
document.body.classList.add(browserCssClass);
|
||||
}
|
||||
if (this.titlebarStyle !== 'default') {
|
||||
document.body.classList.add('titlebar-' + this.titlebarStyle);
|
||||
if (Features.renderCustomTitleBar()) {
|
||||
if (Features.renderCustomTitleBar) {
|
||||
document.body.classList.add('titlebar-custom');
|
||||
}
|
||||
}
|
||||
@@ -138,7 +139,7 @@ class AppView extends View {
|
||||
super.render({
|
||||
beta: this.model.isBeta,
|
||||
titlebarStyle: this.titlebarStyle,
|
||||
customTitlebar: Features.renderCustomTitleBar()
|
||||
customTitlebar: Features.renderCustomTitleBar
|
||||
});
|
||||
this.panelEl = this.$el.find('.app__panel:first');
|
||||
this.views.listWrap.render();
|
||||
@@ -171,9 +172,6 @@ class AppView extends View {
|
||||
this.views.open.on('close', () => {
|
||||
this.showEntries();
|
||||
});
|
||||
this.views.open.on('remove', () => {
|
||||
Events.emit('closed-open-view');
|
||||
});
|
||||
}
|
||||
|
||||
showLastOpenFile() {
|
||||
@@ -202,7 +200,6 @@ class AppView extends View {
|
||||
this.views.menu.show();
|
||||
this.views.menuDrag.$el.parent().show();
|
||||
this.views.listWrap.show();
|
||||
this.views.list.show();
|
||||
this.views.listDrag.show();
|
||||
this.views.details.show();
|
||||
this.views.footer.show();
|
||||
@@ -211,6 +208,8 @@ class AppView extends View {
|
||||
this.hideSettings();
|
||||
this.hideKeyChange();
|
||||
this.hideImportCsv();
|
||||
|
||||
this.views.list.show();
|
||||
}
|
||||
|
||||
hideOpenFile() {
|
||||
@@ -351,7 +350,6 @@ class AppView extends View {
|
||||
launcherBeforeQuit() {
|
||||
// this is currently called only on macos
|
||||
const event = {
|
||||
fromBeforeQuit: true,
|
||||
preventDefault() {}
|
||||
};
|
||||
const result = this.beforeUnload(event);
|
||||
@@ -372,24 +370,22 @@ class AppView extends View {
|
||||
}
|
||||
|
||||
let minimizeInsteadOfClose = this.model.settings.minimizeOnClose;
|
||||
if (e.fromBeforeQuit) {
|
||||
if (Launcher.quitOnRealQuitEventIfMinimizeOnQuitIsEnabled()) {
|
||||
minimizeInsteadOfClose = false;
|
||||
}
|
||||
if (Launcher?.quitOnRealQuitEventIfMinimizeOnQuitIsEnabled()) {
|
||||
minimizeInsteadOfClose = false;
|
||||
}
|
||||
|
||||
if (this.model.files.hasDirtyFiles()) {
|
||||
const exit = () => {
|
||||
if (minimizeInsteadOfClose) {
|
||||
Launcher.minimizeApp();
|
||||
} else {
|
||||
Launcher.exit();
|
||||
}
|
||||
};
|
||||
if (Launcher && Launcher.exitRequested) {
|
||||
return;
|
||||
}
|
||||
if (Launcher) {
|
||||
const exit = () => {
|
||||
if (minimizeInsteadOfClose) {
|
||||
Launcher.minimizeApp();
|
||||
} else {
|
||||
Launcher.exit();
|
||||
}
|
||||
};
|
||||
if (Launcher.exitRequested) {
|
||||
return;
|
||||
}
|
||||
if (!this.exitAlertShown) {
|
||||
if (this.model.settings.autoSave) {
|
||||
this.saveAndLock(
|
||||
@@ -614,8 +610,11 @@ class AppView extends View {
|
||||
}
|
||||
|
||||
closeAllFilesAndShowFirst() {
|
||||
if (!this.model.files.hasOpenFiles()) {
|
||||
return;
|
||||
}
|
||||
let fileToShow = this.model.files.find(
|
||||
(file) => !file.demo && !file.created && !file.external
|
||||
(file) => !file.demo && !file.created && !file.skipOpenList
|
||||
);
|
||||
this.model.closeAllFiles();
|
||||
if (!fileToShow) {
|
||||
@@ -650,7 +649,7 @@ class AppView extends View {
|
||||
if (this.autoSaveTimer) {
|
||||
clearInterval(this.autoSaveTimer);
|
||||
}
|
||||
if (this.model.settings.autoSaveInterval) {
|
||||
if (this.model.settings.autoSaveInterval > 0) {
|
||||
this.autoSaveTimer = setInterval(
|
||||
this.saveAll.bind(this),
|
||||
this.model.settings.autoSaveInterval * 1000 * 60
|
||||
@@ -723,6 +722,12 @@ class AppView extends View {
|
||||
this.views.menu.switchVisibility(false);
|
||||
}
|
||||
|
||||
showOpenIfNotThere() {
|
||||
if (!this.views.open) {
|
||||
this.showLastOpenFile();
|
||||
}
|
||||
}
|
||||
|
||||
editGroup(group) {
|
||||
if (group && !(this.views.panel instanceof GrpView)) {
|
||||
this.showEditGroup(group);
|
||||
@@ -832,7 +837,10 @@ class AppView extends View {
|
||||
extLinkClick(e) {
|
||||
if (Launcher) {
|
||||
e.preventDefault();
|
||||
Launcher.openLink(e.target.href);
|
||||
const link = e.target.closest('a');
|
||||
if (link?.href) {
|
||||
Launcher.openLink(link.href);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { FieldViewDate } from 'views/fields/field-view-date';
|
||||
import { FieldViewHistory } from 'views/fields/field-view-history';
|
||||
import { FieldViewCustom } from 'views/fields/field-view-custom';
|
||||
import { FieldViewReadOnlyWithOptions } from 'views/fields/field-view-read-only-with-options';
|
||||
import { ExtraUrlFieldName } from 'models/entry-model';
|
||||
|
||||
function createDetailsFields(detailsView) {
|
||||
const model = detailsView.model;
|
||||
@@ -21,7 +22,7 @@ function createDetailsFields(detailsView) {
|
||||
const fieldViews = [];
|
||||
const fieldViewsAside = [];
|
||||
|
||||
if (model.external) {
|
||||
if (model.backend === 'otp-device') {
|
||||
fieldViewsAside.push(
|
||||
new FieldViewReadOnly({
|
||||
name: 'Device',
|
||||
@@ -73,15 +74,27 @@ function createDetailsFields(detailsView) {
|
||||
})
|
||||
);
|
||||
} else {
|
||||
fieldViewsAside.push(
|
||||
new FieldViewReadOnly({
|
||||
name: 'File',
|
||||
title: StringFormat.capFirst(Locale.file),
|
||||
value() {
|
||||
return model.fileName;
|
||||
}
|
||||
})
|
||||
);
|
||||
if (model.backend) {
|
||||
fieldViewsAside.push(
|
||||
new FieldViewReadOnly({
|
||||
name: 'Storage',
|
||||
title: StringFormat.capFirst(Locale.storage),
|
||||
value() {
|
||||
return model.fileName;
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
fieldViewsAside.push(
|
||||
new FieldViewReadOnly({
|
||||
name: 'File',
|
||||
title: StringFormat.capFirst(Locale.file),
|
||||
value() {
|
||||
return model.fileName;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
fieldViews.push(
|
||||
new FieldViewAutocomplete({
|
||||
@@ -127,26 +140,30 @@ function createDetailsFields(detailsView) {
|
||||
sequence: '{NOTES}'
|
||||
})
|
||||
);
|
||||
fieldViews.push(
|
||||
new FieldViewTags({
|
||||
name: 'Tags',
|
||||
title: StringFormat.capFirst(Locale.tags),
|
||||
tags: AppModel.instance.tags,
|
||||
value() {
|
||||
return model.tags;
|
||||
}
|
||||
})
|
||||
);
|
||||
fieldViews.push(
|
||||
new FieldViewDate({
|
||||
name: 'Expires',
|
||||
title: Locale.detExpires,
|
||||
lessThanNow: '(' + Locale.detExpired + ')',
|
||||
value() {
|
||||
return model.expires;
|
||||
}
|
||||
})
|
||||
);
|
||||
if (model.file.supportsTags) {
|
||||
fieldViews.push(
|
||||
new FieldViewTags({
|
||||
name: 'Tags',
|
||||
title: StringFormat.capFirst(Locale.tags),
|
||||
tags: AppModel.instance.tags,
|
||||
value() {
|
||||
return model.tags;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
if (model.file.supportsExpiration) {
|
||||
fieldViews.push(
|
||||
new FieldViewDate({
|
||||
name: 'Expires',
|
||||
title: Locale.detExpires,
|
||||
lessThanNow: '(' + Locale.detExpired + ')',
|
||||
value() {
|
||||
return model.expires;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
fieldViewsAside.push(
|
||||
new FieldViewReadOnly({
|
||||
name: 'Group',
|
||||
@@ -159,33 +176,39 @@ function createDetailsFields(detailsView) {
|
||||
}
|
||||
})
|
||||
);
|
||||
fieldViewsAside.push(
|
||||
new FieldViewReadOnly({
|
||||
name: 'Created',
|
||||
title: Locale.detCreated,
|
||||
value() {
|
||||
return DateFormat.dtStr(model.created);
|
||||
}
|
||||
})
|
||||
);
|
||||
fieldViewsAside.push(
|
||||
new FieldViewReadOnly({
|
||||
name: 'Updated',
|
||||
title: Locale.detUpdated,
|
||||
value() {
|
||||
return DateFormat.dtStr(model.updated);
|
||||
}
|
||||
})
|
||||
);
|
||||
fieldViewsAside.push(
|
||||
new FieldViewHistory({
|
||||
name: 'History',
|
||||
title: StringFormat.capFirst(Locale.history),
|
||||
value() {
|
||||
return { length: model.historyLength, unsaved: model.unsaved };
|
||||
}
|
||||
})
|
||||
);
|
||||
if (model.created) {
|
||||
fieldViewsAside.push(
|
||||
new FieldViewReadOnly({
|
||||
name: 'Created',
|
||||
title: Locale.detCreated,
|
||||
value() {
|
||||
return DateFormat.dtStr(model.created);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
if (model.updated) {
|
||||
fieldViewsAside.push(
|
||||
new FieldViewReadOnly({
|
||||
name: 'Updated',
|
||||
title: Locale.detUpdated,
|
||||
value() {
|
||||
return DateFormat.dtStr(model.updated);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!model.backend) {
|
||||
fieldViewsAside.push(
|
||||
new FieldViewHistory({
|
||||
name: 'History',
|
||||
title: StringFormat.capFirst(Locale.history),
|
||||
value() {
|
||||
return { length: model.historyLength, unsaved: model.unsaved };
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
if (otpEntry) {
|
||||
fieldViews.push(
|
||||
new FieldViewOtp({
|
||||
@@ -207,7 +230,7 @@ function createDetailsFields(detailsView) {
|
||||
fieldViews.push(
|
||||
new FieldViewOtp({
|
||||
name: '$' + field,
|
||||
title: field,
|
||||
title: Locale.detOtpField,
|
||||
value() {
|
||||
return model.otpGenerator;
|
||||
},
|
||||
@@ -216,17 +239,32 @@ function createDetailsFields(detailsView) {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
fieldViews.push(
|
||||
new FieldViewCustom({
|
||||
name: '$' + field,
|
||||
title: field,
|
||||
multiline: true,
|
||||
value() {
|
||||
return model.fields[field];
|
||||
},
|
||||
sequence: `{S:${field}}`
|
||||
})
|
||||
);
|
||||
const isUrl = field.startsWith(ExtraUrlFieldName);
|
||||
if (isUrl) {
|
||||
fieldViews.push(
|
||||
new FieldViewUrl({
|
||||
name: '$' + field,
|
||||
title: StringFormat.capFirst(Locale.website),
|
||||
isExtraUrl: true,
|
||||
value() {
|
||||
return model.fields[field];
|
||||
},
|
||||
sequence: `{S:${field}}`
|
||||
})
|
||||
);
|
||||
} else {
|
||||
fieldViews.push(
|
||||
new FieldViewCustom({
|
||||
name: '$' + field,
|
||||
title: field,
|
||||
multiline: true,
|
||||
value() {
|
||||
return model.fields[field];
|
||||
},
|
||||
sequence: `{S:${field}}`
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,4 +272,32 @@ function createDetailsFields(detailsView) {
|
||||
return { fieldViews, fieldViewsAside };
|
||||
}
|
||||
|
||||
export { createDetailsFields };
|
||||
function createNewCustomField(newFieldTitle, newFieldOptions, model) {
|
||||
const isUrl = newFieldTitle.startsWith(ExtraUrlFieldName);
|
||||
|
||||
if (isUrl) {
|
||||
return new FieldViewUrl(
|
||||
{
|
||||
name: '$' + newFieldTitle,
|
||||
title: StringFormat.capFirst(Locale.website),
|
||||
newField: newFieldTitle,
|
||||
isExtraUrl: true,
|
||||
value: () => model.fields[newFieldTitle]
|
||||
},
|
||||
newFieldOptions
|
||||
);
|
||||
} else {
|
||||
return new FieldViewCustom(
|
||||
{
|
||||
name: '$' + newFieldTitle,
|
||||
title: newFieldTitle,
|
||||
newField: newFieldTitle,
|
||||
multiline: true,
|
||||
value: () => ''
|
||||
},
|
||||
newFieldOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { createDetailsFields, createNewCustomField };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { View } from 'framework/views/view';
|
||||
import { Events } from 'framework/events';
|
||||
import { AutoType } from 'auto-type';
|
||||
@@ -22,7 +22,7 @@ import { DetailsAutoTypeView } from 'views/details/details-auto-type-view';
|
||||
import { DetailsHistoryView } from 'views/details/details-history-view';
|
||||
import { DetailsIssuesView } from 'views/details/details-issues-view';
|
||||
import { DropdownView } from 'views/dropdown-view';
|
||||
import { createDetailsFields } from 'views/details/details-fields';
|
||||
import { createDetailsFields, createNewCustomField } from 'views/details/details-fields';
|
||||
import { FieldViewCustom } from 'views/fields/field-view-custom';
|
||||
import { IconSelectView } from 'views/icon-select-view';
|
||||
import { isEqual } from 'util/fn';
|
||||
@@ -121,6 +121,9 @@ class DetailsView extends View {
|
||||
}
|
||||
const model = {
|
||||
deleted: this.appModel.filter.trash,
|
||||
canEditColor: this.model.file.supportsColors && !this.model.readOnly,
|
||||
canEditIcon: this.model.file.supportsIcons && !this.model.readOnly,
|
||||
showButtons: !this.model.backend && !this.model.readOnly,
|
||||
...this.model
|
||||
};
|
||||
this.template = template;
|
||||
@@ -185,7 +188,7 @@ class DetailsView extends View {
|
||||
|
||||
this.fieldViews = fieldViews.concat(fieldViewsAside);
|
||||
|
||||
if (!this.model.external) {
|
||||
if (!this.model.backend) {
|
||||
this.moreView = new DetailsAddFieldView();
|
||||
this.moreView.render();
|
||||
this.moreView.on('add-field', this.addNewField.bind(this));
|
||||
@@ -193,10 +196,10 @@ class DetailsView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
addNewField() {
|
||||
addNewField(title) {
|
||||
this.moreView.remove();
|
||||
this.moreView = null;
|
||||
let newFieldTitle = Locale.detNetField;
|
||||
let newFieldTitle = title || Locale.detNetField;
|
||||
if (this.model.fields[newFieldTitle]) {
|
||||
for (let i = 1; ; i++) {
|
||||
const newFieldTitleVariant = newFieldTitle + i;
|
||||
@@ -206,20 +209,15 @@ class DetailsView extends View {
|
||||
}
|
||||
}
|
||||
}
|
||||
const fieldView = new FieldViewCustom(
|
||||
{
|
||||
name: '$' + newFieldTitle,
|
||||
title: newFieldTitle,
|
||||
newField: newFieldTitle,
|
||||
multiline: true,
|
||||
value() {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
const fieldView = createNewCustomField(
|
||||
newFieldTitle,
|
||||
{
|
||||
parent: this.$el.find('.details__body-fields')[0]
|
||||
}
|
||||
},
|
||||
this.model
|
||||
);
|
||||
|
||||
fieldView.on('change', this.fieldChanged.bind(this));
|
||||
fieldView.render();
|
||||
fieldView.edit();
|
||||
@@ -252,6 +250,13 @@ class DetailsView extends View {
|
||||
icon: 'plus',
|
||||
text: Locale.detMenuAddNewField
|
||||
});
|
||||
if (this.model.url) {
|
||||
moreOptions.push({
|
||||
value: 'add-website',
|
||||
icon: 'plus',
|
||||
text: Locale.detMenuAddNewWebsite
|
||||
});
|
||||
}
|
||||
moreOptions.push({
|
||||
value: 'toggle-empty',
|
||||
icon: 'eye',
|
||||
@@ -263,6 +268,13 @@ class DetailsView extends View {
|
||||
icon: 'plus',
|
||||
text: Locale.detMenuAddNewField
|
||||
});
|
||||
if (this.model.url) {
|
||||
moreOptions.push({
|
||||
value: 'add-website',
|
||||
icon: 'plus',
|
||||
text: Locale.detMenuAddNewWebsite
|
||||
});
|
||||
}
|
||||
moreOptions.push({
|
||||
value: 'toggle-empty',
|
||||
icon: 'eye-slash',
|
||||
@@ -300,6 +312,9 @@ class DetailsView extends View {
|
||||
case 'add-new':
|
||||
this.addNewField();
|
||||
break;
|
||||
case 'add-website':
|
||||
this.addNewField(this.model.getNextUrlFieldName());
|
||||
break;
|
||||
case 'toggle-empty': {
|
||||
const hideEmptyFields = AppSettingsModel.hideEmptyFields;
|
||||
AppSettingsModel.hideEmptyFields = !hideEmptyFields;
|
||||
@@ -363,7 +378,7 @@ class DetailsView extends View {
|
||||
}
|
||||
|
||||
toggleIcons() {
|
||||
if (this.model.external) {
|
||||
if (this.model.backend) {
|
||||
return;
|
||||
}
|
||||
if (this.views.sub && this.views.sub instanceof IconSelectView) {
|
||||
@@ -464,7 +479,7 @@ class DetailsView extends View {
|
||||
}
|
||||
|
||||
this.model.initOtpGenerator?.();
|
||||
if (this.model.external) {
|
||||
if (this.model.backend === 'otp-device') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -496,7 +511,7 @@ class DetailsView extends View {
|
||||
if (!this.model) {
|
||||
return;
|
||||
}
|
||||
if (this.model.external) {
|
||||
if (this.model.backend === 'otp-device') {
|
||||
this.copyOtp();
|
||||
e.preventDefault();
|
||||
}
|
||||
@@ -520,7 +535,7 @@ class DetailsView extends View {
|
||||
|
||||
copyOtp() {
|
||||
const otpField = this.getFieldView('$otp');
|
||||
if (this.model.external) {
|
||||
if (this.model.backend === 'otp-device') {
|
||||
if (!otpField) {
|
||||
return false;
|
||||
}
|
||||
@@ -600,10 +615,14 @@ class DetailsView extends View {
|
||||
}
|
||||
this.entryUpdated(true);
|
||||
this.fieldViews.forEach(function (fieldView, ix) {
|
||||
// TODO: render the view instead
|
||||
if (
|
||||
fieldView instanceof FieldViewCustom &&
|
||||
!fieldView.model.newField &&
|
||||
!this.model.hasField(fieldView.model.title)
|
||||
(fieldView instanceof FieldViewCustom &&
|
||||
!fieldView.model.newField &&
|
||||
!this.model.hasField(fieldView.model.title)) ||
|
||||
(fieldView.model.isExtraUrl &&
|
||||
!fieldView.model.newField &&
|
||||
!this.model.hasField(fieldView.model.name.replace('$', '')))
|
||||
) {
|
||||
fieldView.remove();
|
||||
this.fieldViews.splice(ix, 1);
|
||||
@@ -715,7 +734,7 @@ class DetailsView extends View {
|
||||
}
|
||||
|
||||
editTitle() {
|
||||
if (this.model.external) {
|
||||
if (this.model.backend === 'otp-device') {
|
||||
return;
|
||||
}
|
||||
const input = $('<input/>')
|
||||
@@ -875,7 +894,7 @@ class DetailsView extends View {
|
||||
const canCopy = document.queryCommandSupported('copy');
|
||||
const options = [];
|
||||
if (canCopy) {
|
||||
if (this.model.external) {
|
||||
if (this.model.backend === 'otp-device') {
|
||||
options.push({
|
||||
value: 'det-copy-otp',
|
||||
icon: 'copy',
|
||||
@@ -894,7 +913,7 @@ class DetailsView extends View {
|
||||
text: Locale.detMenuCopyUser
|
||||
});
|
||||
}
|
||||
if (!this.model.external) {
|
||||
if (!this.model.backend) {
|
||||
options.push({ value: 'det-add-new', icon: 'plus', text: Locale.detMenuAddNewField });
|
||||
options.push({ value: 'det-clone', icon: 'clone', text: Locale.detClone });
|
||||
if (canCopy) {
|
||||
@@ -985,7 +1004,8 @@ class DetailsView extends View {
|
||||
|
||||
autoType(sequence) {
|
||||
const entry = this.model;
|
||||
const hasOtp = sequence?.includes('{TOTP}') || (entry.external && !sequence);
|
||||
const hasOtp =
|
||||
sequence?.includes('{TOTP}') || (entry.backend === 'otp-device' && !sequence);
|
||||
if (hasOtp) {
|
||||
const otpField = this.getFieldView('$otp');
|
||||
otpField.refreshOtp((err) => {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { View } from 'framework/views/view';
|
||||
import template from 'templates/extension/extension-connect.hbs';
|
||||
|
||||
class ExtensionConnectView extends View {
|
||||
template = template;
|
||||
|
||||
events = {
|
||||
'change #extension-connect__ask-get': 'askGetChanged',
|
||||
'change .extension-connect__file-check': 'fileChecked'
|
||||
};
|
||||
|
||||
constructor(model) {
|
||||
super(model);
|
||||
this.config = {
|
||||
askGet: this.model.askGet,
|
||||
allFiles: this.model.allFiles,
|
||||
files: this.model.files.filter((f) => f.checked).map((f) => f.id)
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
super.render({
|
||||
...this.model,
|
||||
...this.config,
|
||||
files: this.model.files.map((f) => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
checked: this.config.files.includes(f.id)
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
fileChecked(e) {
|
||||
const fileId = e.target.dataset.file;
|
||||
const checked = e.target.checked;
|
||||
|
||||
if (fileId === 'all') {
|
||||
this.config.allFiles = checked;
|
||||
this.config.files = this.model.files.map((f) => f.id);
|
||||
} else {
|
||||
if (checked) {
|
||||
this.config.files.push(fileId);
|
||||
} else {
|
||||
this.config.files = this.config.files.filter((f) => f !== fileId);
|
||||
this.config.allFiles = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.render();
|
||||
|
||||
const atLeastOneFileSelected = this.config.files.length > 0 || this.config.allFiles;
|
||||
|
||||
const allowButton = document.querySelector('.modal button[data-result=yes]');
|
||||
allowButton.classList.toggle('hide', !atLeastOneFileSelected);
|
||||
}
|
||||
|
||||
askGetChanged(e) {
|
||||
this.config.askGet = e.target.value;
|
||||
}
|
||||
}
|
||||
|
||||
export { ExtensionConnectView };
|
||||
@@ -0,0 +1,26 @@
|
||||
import { View } from 'framework/views/view';
|
||||
import template from 'templates/extension/extension-create-group.hbs';
|
||||
|
||||
class ExtensionCreateGroupView extends View {
|
||||
template = template;
|
||||
|
||||
events = {
|
||||
'change #extension-create-group__file': 'fileChanged'
|
||||
};
|
||||
|
||||
constructor(model) {
|
||||
super(model);
|
||||
|
||||
this.selectedFile = model.files.find((f) => f.selected).id;
|
||||
}
|
||||
|
||||
render() {
|
||||
super.render(this.model);
|
||||
}
|
||||
|
||||
fileChanged(e) {
|
||||
this.selectedFile = e.target.value;
|
||||
}
|
||||
}
|
||||
|
||||
export { ExtensionCreateGroupView };
|
||||
@@ -0,0 +1,38 @@
|
||||
import { View } from 'framework/views/view';
|
||||
import template from 'templates/extension/extension-save-entry.hbs';
|
||||
|
||||
class ExtensionSaveEntryView extends View {
|
||||
template = template;
|
||||
|
||||
events = {
|
||||
'change #extension-save-entry__auto': 'autoChanged',
|
||||
'change #extension-save-entry__group': 'groupChanged'
|
||||
};
|
||||
|
||||
constructor(model) {
|
||||
super(model);
|
||||
|
||||
const selectedGroup = model.allGroups.find((g) => g.selected);
|
||||
this.config = {
|
||||
askSave: model.askSave || 'always',
|
||||
groupId: selectedGroup.id,
|
||||
fileId: selectedGroup.fileId
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
super.render(this.model);
|
||||
}
|
||||
|
||||
autoChanged(e) {
|
||||
this.config.askSave = e.target.checked ? 'auto' : 'always';
|
||||
}
|
||||
|
||||
groupChanged(e) {
|
||||
const option = e.target.options[e.target.selectedIndex];
|
||||
this.config.groupId = option.value;
|
||||
this.config.fileId = option.dataset.file;
|
||||
}
|
||||
}
|
||||
|
||||
export { ExtensionSaveEntryView };
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Keys } from 'const/keys';
|
||||
import { Locale } from 'util/locale';
|
||||
import { Tip } from 'util/ui/tip';
|
||||
@@ -38,7 +38,7 @@ class FieldViewCustom extends FieldViewText {
|
||||
endEdit(newVal, extra) {
|
||||
this.$el.removeClass('details__field--can-edit-title');
|
||||
extra = { ...extra };
|
||||
if (this.model.titleChanged || this.model.newField) {
|
||||
if (this.model.titleChanged) {
|
||||
extra.newField = this.model.title;
|
||||
}
|
||||
if (!this.editing) {
|
||||
@@ -96,6 +96,7 @@ class FieldViewCustom extends FieldViewText {
|
||||
|
||||
fieldLabelClick(e) {
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
if (this.model.newField) {
|
||||
this.startEditTitle(true);
|
||||
} else if (this.editing) {
|
||||
|
||||
@@ -5,7 +5,7 @@ class FieldViewReadOnly extends FieldView {
|
||||
readonly = true;
|
||||
|
||||
renderValue(value) {
|
||||
value = value.isProtected ? new Array(value.textLength + 1).join('•') : escape(value);
|
||||
value = value?.isProtected ? new Array(value.textLength + 1).join('•') : escape(value);
|
||||
value = value.replace(/\n/g, '<br/>');
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Events } from 'framework/events';
|
||||
import { KeyHandler } from 'comp/browser/key-handler';
|
||||
import { Keys } from 'const/keys';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { View } from 'framework/views/view';
|
||||
import { Events } from 'framework/events';
|
||||
import { CopyPaste } from 'comp/browser/copy-paste';
|
||||
@@ -188,6 +188,10 @@ class FieldView extends View {
|
||||
}
|
||||
const protectedEqual =
|
||||
(newVal && newVal.isProtected) === (this.value && this.value.isProtected);
|
||||
if (!extra?.newField && this.model.newField) {
|
||||
extra ??= {};
|
||||
extra.newField = this.model.newField;
|
||||
}
|
||||
const nameChanged = extra && extra.newField;
|
||||
let arg;
|
||||
if (newVal !== undefined && (!textEqual || !protectedEqual || nameChanged)) {
|
||||
|
||||
@@ -175,10 +175,12 @@ class GeneratorView extends View {
|
||||
}
|
||||
|
||||
btnOkClick() {
|
||||
if (!CopyPaste.simpleCopy) {
|
||||
CopyPaste.createHiddenInput(this.password);
|
||||
if (this.model.copy) {
|
||||
if (!CopyPaste.simpleCopy) {
|
||||
CopyPaste.createHiddenInput(this.password);
|
||||
}
|
||||
CopyPaste.copy(this.password);
|
||||
}
|
||||
CopyPaste.copy(this.password);
|
||||
this.emit('result', this.password);
|
||||
this.remove();
|
||||
}
|
||||
|
||||
@@ -108,7 +108,12 @@ class IconSelectView extends View {
|
||||
return url;
|
||||
}
|
||||
|
||||
selectIcon() {
|
||||
selectIcon(e) {
|
||||
const btn = e.target.closest('.icon-select__icon-select');
|
||||
if (btn.classList.contains('icon-select__icon--custom-selected')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$el.find('.icon-select__file-input').click();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { View } from 'framework/views/view';
|
||||
import { Scrollable } from 'framework/views/scrollable';
|
||||
import template from 'templates/import-csv.hbs';
|
||||
@@ -145,31 +145,41 @@ class ImportCsvView extends View {
|
||||
|
||||
runImport() {
|
||||
let group = this.targetGroup;
|
||||
let file = group ? group.file : undefined;
|
||||
if (!group) {
|
||||
|
||||
let filePromise;
|
||||
if (group) {
|
||||
filePromise = Promise.resolve(group.file);
|
||||
} else {
|
||||
const fileName = this.fileName.replace(/\.csv$/i, '');
|
||||
file = this.appModel.createNewFile(fileName);
|
||||
group = file.groups[0];
|
||||
filePromise = new Promise((resolve) => this.appModel.createNewFile(fileName, resolve));
|
||||
}
|
||||
for (const row of this.model.rows) {
|
||||
const newEntry = EntryModel.newEntry(group, file);
|
||||
for (let ix = 0; ix < row.length; ix++) {
|
||||
let value = row[ix];
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
const mapping = this.fieldMapping[ix];
|
||||
if (mapping.type === 'ignore' || !mapping.field) {
|
||||
continue;
|
||||
}
|
||||
if (mapping.field === 'Password') {
|
||||
value = kdbxweb.ProtectedValue.fromString(value);
|
||||
}
|
||||
newEntry.setField(mapping.field, value);
|
||||
|
||||
filePromise.then((file) => {
|
||||
if (!group) {
|
||||
group = file.groups[0];
|
||||
}
|
||||
}
|
||||
file.reload();
|
||||
this.emit('done');
|
||||
|
||||
for (const row of this.model.rows) {
|
||||
const newEntry = EntryModel.newEntry(group, file);
|
||||
for (let ix = 0; ix < row.length; ix++) {
|
||||
let value = row[ix];
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
const mapping = this.fieldMapping[ix];
|
||||
if (mapping.type === 'ignore' || !mapping.field) {
|
||||
continue;
|
||||
}
|
||||
if (mapping.field === 'Password') {
|
||||
value = kdbxweb.ProtectedValue.fromString(value);
|
||||
}
|
||||
newEntry.setField(mapping.field, value);
|
||||
}
|
||||
}
|
||||
|
||||
file.reload();
|
||||
this.emit('done');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+124
-12
@@ -68,12 +68,20 @@ class ListView extends View {
|
||||
this.readTableColumnsEnabled();
|
||||
|
||||
this.items = new SearchResultCollection();
|
||||
this.renderedItems = new Map();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isVisible()) {
|
||||
this.pendingRender = true;
|
||||
return;
|
||||
}
|
||||
this.pendingRender = false;
|
||||
|
||||
if (!this.itemsEl) {
|
||||
super.render();
|
||||
this.itemsEl = this.$el.find('.list__items>.scroller');
|
||||
this.itemsEl.on('scroll', () => this.renderVisibleItems());
|
||||
this.views.search.render();
|
||||
this.setTableView();
|
||||
|
||||
@@ -84,9 +92,9 @@ class ListView extends View {
|
||||
});
|
||||
}
|
||||
if (this.items.length) {
|
||||
const itemTemplate = this.getItemTemplate();
|
||||
const itemsTemplate = this.getItemsTemplate();
|
||||
const noColor = AppSettingsModel.colorfulIcons ? '' : 'grayscale';
|
||||
|
||||
const presenter = new EntryPresenter(
|
||||
this.getDescField(),
|
||||
noColor,
|
||||
@@ -99,16 +107,29 @@ class ListView extends View {
|
||||
}
|
||||
});
|
||||
presenter.columns = columns;
|
||||
let itemsHtml = '';
|
||||
this.items.forEach((item) => {
|
||||
presenter.present(item);
|
||||
itemsHtml += itemTemplate(presenter, DefaultTemplateOptions);
|
||||
}, this);
|
||||
this.presenter = presenter;
|
||||
|
||||
presenter.present(this.items[0]);
|
||||
const itemTemplate = this.getItemTemplate();
|
||||
const itemsHtml = itemTemplate(presenter, DefaultTemplateOptions);
|
||||
presenter.reset();
|
||||
|
||||
const html = itemsTemplate(
|
||||
{ itemsHtml, columns: this.tableColumns },
|
||||
DefaultTemplateOptions
|
||||
);
|
||||
this.itemsEl.html(html);
|
||||
this.itemsContainerEl = this.itemsEl.find('.list__items-container:first')[0];
|
||||
|
||||
const firstListItem = this.itemsContainerEl.firstElementChild;
|
||||
this.itemHeight = firstListItem.getBoundingClientRect().height;
|
||||
|
||||
this.renderedItems = new Map([[0, firstListItem]]);
|
||||
|
||||
const totalHeight = this.itemHeight * this.items.length;
|
||||
this.itemsContainerEl.style.minHeight = totalHeight + 'px';
|
||||
|
||||
this.renderVisibleItems();
|
||||
} else {
|
||||
this.itemsEl.html(this.emptyTemplate({}, DefaultTemplateOptions));
|
||||
}
|
||||
@@ -117,16 +138,12 @@ class ListView extends View {
|
||||
|
||||
getItemsTemplate() {
|
||||
if (this.model.settings.tableView) {
|
||||
return require('templates/list-table.hbs');
|
||||
return require('templates/list-mode-table.hbs');
|
||||
} else {
|
||||
return this.renderPlainItems;
|
||||
return require('templates/list-mode-list.hbs');
|
||||
}
|
||||
}
|
||||
|
||||
renderPlainItems(data) {
|
||||
return data.itemsHtml;
|
||||
}
|
||||
|
||||
getItemTemplate() {
|
||||
if (this.model.settings.tableView) {
|
||||
return require('templates/list-item-table.hbs');
|
||||
@@ -135,6 +152,90 @@ class ListView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
renderVisibleItems() {
|
||||
if (!this.isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollEl = this.itemsEl[0];
|
||||
const rect = scrollEl.getBoundingClientRect();
|
||||
|
||||
const pxTop = scrollEl.scrollTop;
|
||||
const pxHeight = rect.height;
|
||||
const itemHeight = this.itemHeight;
|
||||
const renderedItems = this.renderedItems;
|
||||
|
||||
let firstIx = Math.max(0, Math.floor(pxTop / itemHeight));
|
||||
let lastIx = Math.min(this.items.length - 1, Math.ceil((pxTop + pxHeight) / itemHeight));
|
||||
|
||||
const visibleCount = lastIx - firstIx;
|
||||
firstIx = Math.max(0, firstIx - visibleCount);
|
||||
lastIx = Math.min(this.items.length - 1, lastIx + visibleCount);
|
||||
|
||||
const itemTemplate = this.getItemTemplate();
|
||||
const presenter = this.presenter;
|
||||
|
||||
let itemsHtml = '';
|
||||
const renderedIndices = [];
|
||||
|
||||
for (let ix = firstIx; ix <= lastIx; ix++) {
|
||||
const item = this.items[ix];
|
||||
if (renderedItems.has(ix)) {
|
||||
continue;
|
||||
}
|
||||
presenter.present(item);
|
||||
itemsHtml += itemTemplate(presenter, DefaultTemplateOptions);
|
||||
renderedIndices.push(ix);
|
||||
}
|
||||
presenter.reset();
|
||||
|
||||
const tempEl = document.createElement('div');
|
||||
tempEl.innerHTML = itemsHtml;
|
||||
const renderedElements = [...tempEl.children];
|
||||
|
||||
for (let i = 0; i < renderedElements.length; i++) {
|
||||
const el = renderedElements[i];
|
||||
const ix = renderedIndices[i];
|
||||
this.itemsContainerEl.append(el);
|
||||
el.style.top = ix * itemHeight + 'px';
|
||||
renderedItems.set(ix, el);
|
||||
}
|
||||
|
||||
const maxRenderedItems = visibleCount * 5;
|
||||
|
||||
if (renderedItems.size > maxRenderedItems) {
|
||||
for (const [ix, el] of this.renderedItems) {
|
||||
if (ix < firstIx || ix > lastIx) {
|
||||
el.remove();
|
||||
renderedItems.delete(ix);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ensureItemRendered(ix) {
|
||||
if (this.renderedItems.has(ix)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = this.items[ix];
|
||||
const itemTemplate = this.getItemTemplate();
|
||||
|
||||
this.presenter.present(item);
|
||||
const itemHtml = itemTemplate(this.presenter, DefaultTemplateOptions);
|
||||
this.presenter.reset();
|
||||
|
||||
const tempEl = document.createElement('div');
|
||||
tempEl.innerHTML = itemHtml;
|
||||
|
||||
const [el] = tempEl.children;
|
||||
|
||||
this.itemsContainerEl.append(el);
|
||||
el.style.top = ix * this.itemHeight + 'px';
|
||||
|
||||
this.renderedItems.set(ix, el);
|
||||
}
|
||||
|
||||
getDescField() {
|
||||
return this.model.sort.replace('-', '');
|
||||
}
|
||||
@@ -203,7 +304,12 @@ class ListView extends View {
|
||||
}
|
||||
|
||||
selectItem(item) {
|
||||
this.presenter.activeEntryId = item.id;
|
||||
this.model.activeEntryId = item.id;
|
||||
|
||||
const ix = this.items.indexOf(item);
|
||||
this.ensureItemRendered(ix);
|
||||
|
||||
Events.emit('entry-selected', item);
|
||||
this.itemsEl.find('.list__item--active').removeClass('list__item--active');
|
||||
const itemEl = document.getElementById(item.id);
|
||||
@@ -220,6 +326,9 @@ class ListView extends View {
|
||||
|
||||
viewShown() {
|
||||
this.views.search.show();
|
||||
if (this.pendingRender) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
viewHidden() {
|
||||
@@ -248,6 +357,7 @@ class ListView extends View {
|
||||
viewResized(size) {
|
||||
this.setSize(size);
|
||||
this.throttleSetViewSizeSetting(size);
|
||||
this.renderVisibleItems();
|
||||
}
|
||||
|
||||
throttleSetViewSizeSetting = throttle((size) => {
|
||||
@@ -256,6 +366,7 @@ class ListView extends View {
|
||||
|
||||
filterChanged(filter) {
|
||||
this.items = filter.entries;
|
||||
this.renderedItems = new Map();
|
||||
this.render();
|
||||
}
|
||||
|
||||
@@ -263,6 +374,7 @@ class ListView extends View {
|
||||
const scrollTop = this.itemsEl[0].scrollTop;
|
||||
this.render();
|
||||
this.itemsEl[0].scrollTop = scrollTop;
|
||||
this.renderVisibleItems();
|
||||
}
|
||||
|
||||
itemDragStart(e) {
|
||||
|
||||
@@ -41,7 +41,7 @@ class ModalView extends View {
|
||||
document.activeElement.blur();
|
||||
}, 20);
|
||||
if (this.model.view) {
|
||||
this.model.view.parent = '.modal__body';
|
||||
this.model.view.parent = this.el.querySelector('.modal__body');
|
||||
this.model.view.render();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { View } from 'framework/views/view';
|
||||
import { Events } from 'framework/events';
|
||||
import { Storage } from 'storage';
|
||||
@@ -52,6 +52,7 @@ class OpenView extends View {
|
||||
'click .open__settings-yubikey': 'selectYubiKeyChalResp',
|
||||
'click .open__last-item': 'openLast',
|
||||
'click .open__icon-generate': 'toggleGenerator',
|
||||
'click .open__message-cancel-btn': 'openMessageCancelClick',
|
||||
dragover: 'dragover',
|
||||
dragleave: 'dragleave',
|
||||
drop: 'drop'
|
||||
@@ -76,6 +77,7 @@ class OpenView extends View {
|
||||
this.onKey(Keys.DOM_VK_UP, this.moveOpenFileSelectionUp, null, 'open');
|
||||
this.listenTo(Events, 'main-window-focus', this.windowFocused.bind(this));
|
||||
this.listenTo(Events, 'usb-devices-changed', this.usbDevicesChanged.bind(this));
|
||||
this.listenTo(Events, 'unlock-message-changed', this.unlockMessageChanged.bind(this));
|
||||
this.once('remove', () => {
|
||||
this.passwordInput.reset();
|
||||
});
|
||||
@@ -118,6 +120,7 @@ class OpenView extends View {
|
||||
canOpenKeyFromDropbox: !Launcher && Storage.dropbox.enabled,
|
||||
demoOpened: this.model.settings.demoOpened,
|
||||
storageProviders,
|
||||
unlockMessageRes: this.model.unlockMessageRes,
|
||||
canOpen: this.model.settings.canOpen,
|
||||
canOpenDemo: this.model.settings.canOpenDemo,
|
||||
canOpenSettings: this.model.settings.canOpenSettings,
|
||||
@@ -211,11 +214,17 @@ class OpenView extends View {
|
||||
fileSelected(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
this.processFile(file, (success) => {
|
||||
if (success && !file.path && this.reading === 'fileData') {
|
||||
this.showLocalFileAlert();
|
||||
}
|
||||
});
|
||||
if (this.model.settings.canImportCsv && /\.csv$/.test(file.name)) {
|
||||
Events.emit('import-csv-requested', file);
|
||||
} else if (this.model.settings.canImportXml && /\.xml$/.test(file.name)) {
|
||||
this.setFile(file, null, this.showLocalFileAlert.bind(this));
|
||||
} else {
|
||||
this.processFile(file, (success) => {
|
||||
if (success && !file.path && this.reading === 'fileData') {
|
||||
this.showLocalFileAlert();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -674,16 +683,24 @@ class OpenView extends View {
|
||||
const encryptedPassword = kdbxweb.ProtectedValue.fromBase64(
|
||||
this.encryptedPassword.value
|
||||
);
|
||||
Events.emit('hardware-decrypt-started');
|
||||
NativeModules.hardwareDecrypt(encryptedPassword, touchIdPrompt)
|
||||
.then((password) => {
|
||||
Events.emit('hardware-decrypt-finished');
|
||||
|
||||
this.params.password = password;
|
||||
this.params.encryptedPassword = this.encryptedPassword;
|
||||
this.model.openFile(this.params, (err) => this.openDbComplete(err));
|
||||
})
|
||||
.catch((err) => {
|
||||
Events.emit('hardware-decrypt-finished');
|
||||
|
||||
if (err.message.includes('User refused')) {
|
||||
err.userCanceled = true;
|
||||
} else if (err.message.includes('SecKeyCreateDecryptedData')) {
|
||||
err.maybeTouchIdChanged = true;
|
||||
}
|
||||
logger.error('Error in hardware decryption', err);
|
||||
this.openDbComplete(err);
|
||||
});
|
||||
} else {
|
||||
@@ -712,9 +729,13 @@ class OpenView extends View {
|
||||
if (err.notFound) {
|
||||
err = Locale.openErrorFileNotFound;
|
||||
}
|
||||
let alertBody = Locale.openErrorDescription;
|
||||
if (err.maybeTouchIdChanged) {
|
||||
alertBody += '\n' + Locale.openErrorDescriptionMaybeTouchIdChanged;
|
||||
}
|
||||
Alerts.error({
|
||||
header: Locale.openError,
|
||||
body: Locale.openErrorDescription,
|
||||
body: alertBody,
|
||||
pre: this.errorToString(err)
|
||||
});
|
||||
}
|
||||
@@ -1141,6 +1162,20 @@ class OpenView extends View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unlockMessageChanged(unlockMessageRes) {
|
||||
const messageEl = this.el.querySelector('.open__message');
|
||||
messageEl.classList.toggle('hide', !unlockMessageRes);
|
||||
|
||||
if (unlockMessageRes) {
|
||||
const contentEl = this.el.querySelector('.open__message-content');
|
||||
contentEl.innerText = Locale[unlockMessageRes];
|
||||
}
|
||||
}
|
||||
|
||||
openMessageCancelClick() {
|
||||
this.model.rejectPendingFileUnlockPromise('User canceled');
|
||||
}
|
||||
}
|
||||
|
||||
export { OpenView };
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user