Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43d5279acf | ||
|
|
e674607fce | ||
|
|
2e6239f637 | ||
|
|
bcb8ddc17b | ||
|
|
cd619582ec | ||
|
|
3b49562f64 | ||
|
|
674ed2cc72 | ||
|
|
5621fd8a06 | ||
|
|
af27e0b111 | ||
|
|
5a78e12f7d | ||
|
|
47b19d1d74 | ||
|
|
070573e68a | ||
|
|
946d7769f1 | ||
|
|
f0426a3ec2 | ||
|
|
b2d67818a8 | ||
|
|
c0b159d824 | ||
|
|
d7e11e20c1 | ||
|
|
d590318153 | ||
|
|
5816ca9c4d | ||
|
|
6627e8c0ec | ||
|
|
379e28e8a4 | ||
|
|
4fb25451a2 | ||
|
|
58a6a97130 | ||
|
|
f59b846643 | ||
|
|
28d7dfeb67 | ||
|
|
6cc39d2107 | ||
|
|
a92a01095b | ||
|
|
3f4cddd2a2 | ||
|
|
aa734ea976 | ||
|
|
30628004fd | ||
|
|
a6e87b52fd | ||
|
|
6d06d825e5 | ||
|
|
0800d5f33a | ||
|
|
337ef44b56 | ||
|
|
70b4effde8 | ||
|
|
9e4c0f5c47 | ||
|
|
535cdc72f7 | ||
|
|
8d258ecab2 | ||
|
|
3079e28f6f | ||
|
|
4970290614 | ||
|
|
6929931cae | ||
|
|
e348310602 | ||
|
|
7abbd2f253 | ||
|
|
c56698492e | ||
|
|
bc4c33fcbb | ||
|
|
05d5ac960c | ||
|
|
80331401b5 | ||
|
|
b4459e1247 | ||
|
|
706c82d3ca | ||
|
|
0e1b086096 | ||
|
|
80359c98f5 | ||
|
|
034e0ec2e3 | ||
|
|
cc6839d13e | ||
|
|
0ce436c2e9 | ||
|
|
966d814281 | ||
|
|
c983bc10ab | ||
|
|
d603398da0 | ||
|
|
1404d2688f | ||
|
|
c72158cf0d | ||
|
|
1e6ed1308f | ||
|
|
fae241373a | ||
|
|
2bf4cd83df | ||
|
|
efd3290ee8 | ||
|
|
aa729d9af8 | ||
|
|
2d6d3658a1 | ||
|
|
0cbdd6281a | ||
|
|
a845a42d0d | ||
|
|
60329e086a | ||
|
|
a3174b96d7 | ||
|
|
29c00066bc | ||
|
|
b90ffc4d3f | ||
|
|
7e030320f5 | ||
|
|
c96bcad32d | ||
|
|
a655cce195 | ||
|
|
aafb0e7964 | ||
|
|
288f7f117f | ||
|
|
125ac2eda8 | ||
|
|
ea80bfae07 | ||
|
|
5e727a3547 | ||
|
|
6672b60d0f | ||
|
|
09fe1a4649 | ||
|
|
e4fa4cc413 | ||
|
|
0be60475cf | ||
|
|
b7e53185b0 | ||
|
|
cd8c76b3eb | ||
|
|
a1d7a1fe48 | ||
|
|
c48153819f | ||
|
|
d2d5590887 | ||
|
|
65b319e127 | ||
|
|
b34c0dd34e | ||
|
|
ddc1a371bf | ||
|
|
a0c0b7b195 | ||
|
|
b07cea9049 | ||
|
|
991388d4cd | ||
|
|
7adc77c823 | ||
|
|
550f93d4ae | ||
|
|
b43187e5b8 | ||
|
|
6230710bb0 | ||
|
|
ff119cbc0a | ||
|
|
2a2ec713af | ||
|
|
41eb560d1a | ||
|
|
f7b9b91144 | ||
|
|
97ab5d066e | ||
|
|
448e19d28c | ||
|
|
391f2c9c00 | ||
|
|
18524ad9eb | ||
|
|
732c35625b | ||
|
|
95857450c2 | ||
|
|
98ea7a542d | ||
|
|
338f6d47d1 | ||
|
|
fdb492c572 | ||
|
|
ce3ff8c3d8 | ||
|
|
1789d9bb67 | ||
|
|
1883b527a4 | ||
|
|
2f4a9d450a | ||
|
|
12fc33a15e | ||
|
|
e4693ecf61 | ||
|
|
c15d709db2 | ||
|
|
e09040b0fb | ||
|
|
4477c3fc2c | ||
|
|
f52a00a156 | ||
|
|
946a063993 | ||
|
|
d3fcbcb6a5 | ||
|
|
f91cfc0f79 | ||
|
|
f0bbc54fd5 | ||
|
|
3ca5f94b9b | ||
|
|
7a35ebec73 | ||
|
|
66d3623f90 | ||
|
|
8c9badd440 | ||
|
|
e0d47a474d | ||
|
|
1a8aff0594 | ||
|
|
cf5e01c213 | ||
|
|
ffae956e21 | ||
|
|
5c4cb7ff6d | ||
|
|
7794c8b89b | ||
|
|
bb78e8e8a7 | ||
|
|
00a2a95adc | ||
|
|
e55dd47f6e | ||
|
|
7b10bda0a4 | ||
|
|
d5b9aefa01 | ||
|
|
cdc3f5e1df | ||
|
|
8fc6f49fb4 | ||
|
|
29e5c14009 | ||
|
|
fe9ff205b9 | ||
|
|
87b59eccee | ||
|
|
42cb5a6c36 | ||
|
|
9826965803 | ||
|
|
47153e41d3 | ||
|
|
677b428e63 | ||
|
|
15936fcef0 | ||
|
|
b2e828e7ef | ||
|
|
ac6e5a3ae8 | ||
|
|
3722477eea | ||
|
|
72d0f76acc | ||
|
|
b7333574aa | ||
|
|
5caccbd1f5 | ||
|
|
698b559935 | ||
|
|
36d5e8f730 | ||
|
|
233721bf51 | ||
|
|
822602a38f | ||
|
|
e69dbf697c | ||
|
|
184d71a1b9 | ||
|
|
ccb9b03287 | ||
|
|
dd4ad6d72b | ||
|
|
195e2cc8f0 | ||
|
|
ef573336d8 | ||
|
|
9377a849f6 | ||
|
|
00ccbf4be2 | ||
|
|
00159d26dc | ||
|
|
b4f93adf05 | ||
|
|
5bbc9003f4 | ||
|
|
c4be926b7c | ||
|
|
147e3911f1 | ||
|
|
c7b814fc01 | ||
|
|
8525571afc | ||
|
|
5c5de91672 | ||
|
|
7dbb0a2e8f | ||
|
|
11b54ffdf9 | ||
|
|
5d76ac33dd | ||
|
|
8e92bf3766 | ||
|
|
56517eec62 | ||
|
|
ed56f0733e | ||
|
|
3fd799ee74 | ||
|
|
4ed8a67eda | ||
|
|
cbb9a8dbd0 | ||
|
|
4280b84459 | ||
|
|
e06e9ac1f8 | ||
|
|
7bea4ad282 | ||
|
|
316c14d845 | ||
|
|
d17be63312 | ||
|
|
2c2253db31 | ||
|
|
a3128e56a3 | ||
|
|
de0cf0ce67 | ||
|
|
e3f33b9d4c | ||
|
|
27143e2663 | ||
|
|
566e2fb35d | ||
|
|
dc5ada1c04 | ||
|
|
603e698f5a | ||
|
|
f893dcb19a | ||
|
|
27b116798e | ||
|
|
ae932e5710 | ||
|
|
bab5a979be | ||
|
|
1b8c9a3e0c | ||
|
|
99d0fa30ed | ||
|
|
5cc146481b | ||
|
|
102da5cba1 | ||
|
|
d374eecb18 | ||
|
|
08d31c0ffe | ||
|
|
c62e40465a | ||
|
|
5367f32d5c | ||
|
|
ad20698a42 | ||
|
|
e865d80a63 | ||
|
|
81a311e7fa | ||
|
|
5397fa8fd3 | ||
|
|
b9e08cd9e3 | ||
|
|
fe86cd6320 | ||
|
|
bba07bb83a | ||
|
|
83a53ee1f0 | ||
|
|
23ba363a0d | ||
|
|
101aee81c0 | ||
|
|
fa60d02129 | ||
|
|
6a9e5fd801 | ||
|
|
12ae7845ec | ||
|
|
045e8ee96f | ||
|
|
993f75b14e | ||
|
|
b48f3746bc | ||
|
|
dcc477acbc | ||
|
|
5f4f007b31 | ||
|
|
9d4788cb7c | ||
|
|
5a1380f9ef | ||
|
|
0d82aa26f3 | ||
|
|
05f2ec19f2 | ||
|
|
25c3c9c565 | ||
|
|
d6e7e3d4a0 | ||
|
|
6148112904 | ||
|
|
ba338e9282 | ||
|
|
2e132690d1 | ||
|
|
8f1b6ca3b7 | ||
|
|
85d0252972 | ||
|
|
077f72a0c5 | ||
|
|
25bec513f3 | ||
|
|
0a6378d84c | ||
|
|
1e6034ce37 | ||
|
|
ef6e5ebcbe | ||
|
|
19a42b7463 | ||
|
|
9d0e608454 | ||
|
|
bd9a98dbfd | ||
|
|
0ffca3dfe4 | ||
|
|
01958559ee | ||
|
|
bb4aef206a | ||
|
|
e74227b0db | ||
|
|
a47aec1cc4 | ||
|
|
cd9d8cde3e | ||
|
|
d4f68d5ec7 | ||
|
|
a910cfcb35 | ||
|
|
962de503fd | ||
|
|
f40e72c882 | ||
|
|
b7993e1d0e | ||
|
|
d6e5e73e82 | ||
|
|
1794f87f99 | ||
|
|
81c7d57f30 | ||
|
|
b4f0149bd8 | ||
|
|
87ec06eef0 | ||
|
|
c764548214 | ||
|
|
c78af7c3ad | ||
|
|
f9cd0d4e29 | ||
|
|
ee03435ca7 | ||
|
|
a9b83256c6 | ||
|
|
7086ddcf58 | ||
|
|
26d226bb17 | ||
|
|
dc4742bf5c | ||
|
|
933df5fae4 | ||
|
|
4b17ac19dc | ||
|
|
43f6a4f499 | ||
|
|
076b99676d | ||
|
|
b3dfa05a4a | ||
|
|
c5946b1a0b | ||
|
|
58a85eabdf | ||
|
|
8a1d9b96dd | ||
|
|
36a3d2f766 | ||
|
|
68010076f9 | ||
|
|
92da783e82 | ||
|
|
7d1e343d25 | ||
|
|
84d17b7255 | ||
|
|
64e06a32ec | ||
|
|
4ee87e197d | ||
|
|
cace667b26 | ||
|
|
67d3e28af7 | ||
|
|
3c5f4e7b84 | ||
|
|
8f913c737c | ||
|
|
8ba147f52f | ||
|
|
9cfbd24b76 | ||
|
|
259abf6d25 | ||
|
|
8c59838a6f | ||
|
|
f7d24c6379 | ||
|
|
0f5466e075 | ||
|
|
7ab0e83f55 | ||
|
|
dc02ede8d7 | ||
|
|
42e484827f | ||
|
|
576c8f97bd | ||
|
|
808b4a687d | ||
|
|
954f2c235b | ||
|
|
51a7bbbfb8 | ||
|
|
9b5f5857bb | ||
|
|
e66f52a8ce | ||
|
|
8023bcea96 | ||
|
|
dfb7491503 | ||
|
|
b43d9c963a | ||
|
|
51c0637134 | ||
|
|
b16c23190b | ||
|
|
fddf2892cd | ||
|
|
53b5715da0 | ||
|
|
3a79fcb366 | ||
|
|
4df034ac55 | ||
|
|
a0bb04fe3a | ||
|
|
a923f28a8d | ||
|
|
78642f1c9b | ||
|
|
f8a85af859 | ||
|
|
bdb5bd0428 | ||
|
|
9e07f55c12 | ||
|
|
6a3e129ed5 | ||
|
|
879ba17760 | ||
|
|
0d2107e72a | ||
|
|
a23a47fded | ||
|
|
7737a29cca | ||
|
|
34278b3bff | ||
|
|
40b0643eaa | ||
|
|
ae7f5fbff2 | ||
|
|
0a776c6a72 | ||
|
|
5caecb1c8f | ||
|
|
0def3f8b2d | ||
|
|
1593acd8e9 | ||
|
|
da5553364a | ||
|
|
b7c03490d8 | ||
|
|
b8dd7b0f71 | ||
|
|
e7a8d5232f | ||
|
|
c638fa38e8 | ||
|
|
f74b054f01 | ||
|
|
b7af33edae | ||
|
|
166211b6f8 | ||
|
|
f8ec47bfdb | ||
|
|
4e84121553 | ||
|
|
ccf4ce8e58 | ||
|
|
6ba3d44fa1 | ||
|
|
c03ed76fec | ||
|
|
31214b10e7 | ||
|
|
5bbc324b2e | ||
|
|
7dcb22950e | ||
|
|
3c06f1f1a3 | ||
|
|
6bcf5161b1 | ||
|
|
ccc6765e2e | ||
|
|
14d40d4e2d | ||
|
|
75d563b320 | ||
|
|
2fdd53b55e | ||
|
|
4ac16a6701 | ||
|
|
e998788d25 | ||
|
|
e23b80b11b | ||
|
|
7eb884dcb9 | ||
|
|
92397adbb3 | ||
|
|
828485789f | ||
|
|
2f84474216 | ||
|
|
762aab397d | ||
|
|
b03cb4590f | ||
|
|
5d40831dd7 | ||
|
|
0d291afb05 | ||
|
|
e107c5c108 | ||
|
|
44905e674d | ||
|
|
5b790e711a | ||
|
|
1a9c54b237 | ||
|
|
0f4434a361 | ||
|
|
7e2823903b | ||
|
|
92937dbb86 | ||
|
|
0868efe095 | ||
|
|
97374f63f9 | ||
|
|
69f7b02ce6 | ||
|
|
b5ee682a41 | ||
|
|
70f82bdfd8 | ||
|
|
e70185560b | ||
|
|
be4be0e7e8 | ||
|
|
b9a5c64113 | ||
|
|
215f398547 | ||
|
|
ace18f9d95 | ||
|
|
c4ec91f61a | ||
|
|
a4881419df | ||
|
|
839359a794 | ||
|
|
d4d9b7c7a7 | ||
|
|
d4526c0c8a | ||
|
|
78f0bc6f02 | ||
|
|
ad3f5afeba | ||
|
|
7c4326939a | ||
|
|
e5428b67b8 | ||
|
|
59616bd8af | ||
|
|
fdc9ac95d2 | ||
|
|
a8f0d1af62 | ||
|
|
f6e377f222 | ||
|
|
c43d7ab92a | ||
|
|
0464e1de5c | ||
|
|
7a7381eac0 | ||
|
|
1379db1e92 | ||
|
|
ee10894f5f | ||
|
|
87d760708d | ||
|
|
ec7155809b | ||
|
|
5af7837468 | ||
|
|
d3f79af2ab | ||
|
|
a6c0fc3984 | ||
|
|
56604de7f1 | ||
|
|
d5e6199775 | ||
|
|
15920abcf7 | ||
|
|
88205d716a | ||
|
|
fa1a16f140 | ||
|
|
4f8075ca6c | ||
|
|
2615d0644d | ||
|
|
d2686937fd | ||
|
|
b60307e28b | ||
|
|
08d49ddc54 | ||
|
|
4cdc80e530 | ||
|
|
acd360efb9 | ||
|
|
87ad89beeb | ||
|
|
c2663ca7d8 | ||
|
|
4e58285cc2 | ||
|
|
ef354fb8af | ||
|
|
79ce535d4a | ||
|
|
50c43c4408 | ||
|
|
8fdec5aadc | ||
|
|
73eccd5308 | ||
|
|
4449677e36 | ||
|
|
8dc3a4b51f | ||
|
|
cd2de3a1db | ||
|
|
1fa0eebc3b | ||
|
|
e13002bab3 | ||
|
|
a750e80ea1 | ||
|
|
66e1b0a089 | ||
|
|
00c0b3f88c | ||
|
|
674311cb4b | ||
|
|
f54b8e00e1 | ||
|
|
c1beac5d0d | ||
|
|
7123345341 | ||
|
|
0e2475e667 | ||
|
|
acf1f1146d | ||
|
|
d136fa4fdf | ||
|
|
61067391b6 | ||
|
|
affebb1ba4 | ||
|
|
cbffa3d4ef | ||
|
|
6ef5ae2517 | ||
|
|
9701496897 | ||
|
|
22d6a3011f | ||
|
|
d22ddd99f6 | ||
|
|
4e8648fca0 | ||
|
|
9f49c357e9 | ||
|
|
d318467039 | ||
|
|
b05c25ac1a | ||
|
|
5dd0d51514 | ||
|
|
c7574a6b83 | ||
|
|
ab64e9343b | ||
|
|
b82cb0e849 | ||
|
|
c44a8fab42 | ||
|
|
9a75eb0a22 | ||
|
|
f994278a8b | ||
|
|
bd882d0e17 | ||
|
|
fddf62409a | ||
|
|
a8967ac764 | ||
|
|
20bc254a42 | ||
|
|
e298d5fbd8 | ||
|
|
ff54c2070d | ||
|
|
e092caa2ef | ||
|
|
feb44be9d0 | ||
|
|
a43a2e8ce2 | ||
|
|
6fcf77829d | ||
|
|
733833e8a4 | ||
|
|
890f998fe0 | ||
|
|
28c708f599 | ||
|
|
f1e536cbcf | ||
|
|
8dc7ad4f01 | ||
|
|
43c8976282 | ||
|
|
8ee63ccc24 | ||
|
|
2900fe07bf | ||
|
|
1d572d1fa9 | ||
|
|
21b8e9ae8f | ||
|
|
cb77b7fc1e | ||
|
|
763141592f | ||
|
|
09fcddfad2 | ||
|
|
b8ce1b5cc1 | ||
|
|
6a34fffcc4 | ||
|
|
75fa636366 | ||
|
|
bee96ebf15 | ||
|
|
9ec23fac5d | ||
|
|
2a7cd3574d | ||
|
|
4f96629e22 | ||
|
|
74bd270423 | ||
|
|
0469f28a54 | ||
|
|
a9c2da3fdb | ||
|
|
2ec37c3ece | ||
|
|
ef812c5697 | ||
|
|
8ab4e7c433 | ||
|
|
23bb30be92 | ||
|
|
a13d44d560 | ||
|
|
42b5b002cc | ||
|
|
f889488915 | ||
|
|
57f3a287c2 | ||
|
|
143b1a73f7 | ||
|
|
54a1860398 | ||
|
|
014b6b2319 | ||
|
|
33763ab865 | ||
|
|
5f5ea892a3 | ||
|
|
a3fc2b2cdd | ||
|
|
2cad70dae0 | ||
|
|
fb56ba2f0d | ||
|
|
7c2b7a09f8 | ||
|
|
abf5531fc5 | ||
|
|
18775e3cea | ||
|
|
9d5e7be105 | ||
|
|
eed647c867 | ||
|
|
072271bbe6 | ||
|
|
a84499d7d4 | ||
|
|
c06db5a2b7 | ||
|
|
fc737b1f51 | ||
|
|
4f068008bd | ||
|
|
953f6dc61f | ||
|
|
01fac1f0ca | ||
|
|
1796ac743d | ||
|
|
60329b3cab | ||
|
|
f489eca5c2 | ||
|
|
1617cae518 | ||
|
|
6c7b99af58 | ||
|
|
ddd242b9df | ||
|
|
c24fe2806c | ||
|
|
8da0a2e239 | ||
|
|
8aff64ce47 | ||
|
|
677f1ce75f | ||
|
|
f99ff95a57 | ||
|
|
7a522acdd9 | ||
|
|
9888a0cf5b | ||
|
|
9b586f7059 | ||
|
|
90666cea99 | ||
|
|
cabf0d388d | ||
|
|
39a33bd77a | ||
|
|
f184915849 | ||
|
|
683b0bc4e6 | ||
|
|
212042556f | ||
|
|
cc4aa6fc1a | ||
|
|
b4b6ef3206 | ||
|
|
ccfd5126bc | ||
|
|
b733b43f8b | ||
|
|
3c0693fdd9 | ||
|
|
938f2be487 | ||
|
|
9f2a09cc97 | ||
|
|
27d5fc4cba | ||
|
|
251b763ea9 | ||
|
|
8a07162e6b | ||
|
|
4097d81b8a | ||
|
|
bf2d72745a | ||
|
|
acc60e6297 | ||
|
|
211b058686 | ||
|
|
95c077a0ba | ||
|
|
0b70017780 | ||
|
|
513e258869 | ||
|
|
7cd046ecc0 | ||
|
|
220a27e030 | ||
|
|
3728fbc367 | ||
|
|
6d41c71542 | ||
|
|
a1b4dde3e3 | ||
|
|
677586e882 | ||
|
|
3177a87690 | ||
|
|
f8fda777db | ||
|
|
e955c3ddc1 | ||
|
|
7e36418276 | ||
|
|
e5616dffa7 | ||
|
|
be3087d2cc | ||
|
|
396721fce6 | ||
|
|
d49af938d9 | ||
|
|
d2ae8522b5 | ||
|
|
a3e27709d0 | ||
|
|
a245553cad | ||
|
|
8bcdc6d109 | ||
|
|
8a1b28a793 | ||
|
|
90b236e6f9 | ||
|
|
804b8cf17b | ||
|
|
ed3eefbbfc | ||
|
|
51564056a1 | ||
|
|
4884caadd4 | ||
|
|
e129e52e01 | ||
|
|
893a0f43c1 | ||
|
|
083cbe9840 | ||
|
|
a7fcbae65f | ||
|
|
7ae80c2e7c | ||
|
|
f1ede2b876 | ||
|
|
8c51adbca1 | ||
|
|
446f293da0 | ||
|
|
bbd7f9c6d6 | ||
|
|
9e5fb90bd6 | ||
|
|
2ed735ad95 | ||
|
|
00be6f0181 | ||
|
|
29a4d367f9 | ||
|
|
df43c61785 | ||
|
|
464c49257a | ||
|
|
bd4a6d0eee | ||
|
|
9410fc6a8c | ||
|
|
bd400c0797 | ||
|
|
54c721c265 | ||
|
|
94cfa4f3d4 | ||
|
|
f21ef7f444 | ||
|
|
66a0cf4782 | ||
|
|
52f27ce743 | ||
|
|
57296ba313 | ||
|
|
47dc1727df | ||
|
|
cd9fe46fdb | ||
|
|
775cdf237c | ||
|
|
63367998bf | ||
|
|
5d3e110a9e | ||
|
|
42267eaef8 | ||
|
|
b716599c90 | ||
|
|
6d80de9216 | ||
|
|
212dc99cec | ||
|
|
6f4f8ec1ea | ||
|
|
24c64daa49 | ||
|
|
bdd9a51d25 | ||
|
|
f27862b036 | ||
|
|
f7e9f725b0 | ||
|
|
4ee430284b | ||
|
|
96458048fc | ||
|
|
a31f9d5a50 | ||
|
|
33d5f0b5bb | ||
|
|
ec43f0a549 | ||
|
|
8e0dc326cb | ||
|
|
9994c87446 | ||
|
|
647ce573b0 | ||
|
|
1ca4ead313 | ||
|
|
1ef5ffab52 | ||
|
|
f4f2ddfb3c | ||
|
|
650b4805be | ||
|
|
22754f32ec | ||
|
|
85a67b533a | ||
|
|
974dbd224a | ||
|
|
b8c7a8b936 | ||
|
|
86844064db | ||
|
|
0c9b9a1b97 | ||
|
|
0a9846b86f | ||
|
|
96ee17b8cc | ||
|
|
4b65792585 | ||
|
|
32b871e338 | ||
|
|
46ddb725db | ||
|
|
d0fdada9f7 | ||
|
|
a343853b38 | ||
|
|
136fbc2f77 | ||
|
|
25bc5643b1 | ||
|
|
a0c03363a7 | ||
|
|
8dd7a97769 | ||
|
|
f70dfaaa28 | ||
|
|
01ceb6dc37 | ||
|
|
d56e50b29d | ||
|
|
0b2c722f61 | ||
|
|
26990f38dd | ||
|
|
b2d87efc99 | ||
|
|
63b7b32e9c | ||
|
|
8925a48822 | ||
|
|
8ad47181a9 | ||
|
|
c1b9ec7a77 | ||
|
|
6daf34e261 | ||
|
|
fc5c865b18 | ||
|
|
083306a1cd | ||
|
|
5ebacbc347 | ||
|
|
08c4adfd46 | ||
|
|
27f87ad09f | ||
|
|
d0b33ca0c1 | ||
|
|
bb44c7cb3e | ||
|
|
66e366d9d0 | ||
|
|
31390eac80 | ||
|
|
543a71f6f2 | ||
|
|
5d29b79dab | ||
|
|
207ff77778 | ||
|
|
b85d25614b | ||
|
|
d1bcdca0d3 | ||
|
|
ffb68b1651 | ||
|
|
49d46cfedc | ||
|
|
9c18e2877b | ||
|
|
a1da5ceabe | ||
|
|
759b92bfd6 | ||
|
|
b4f3d19259 | ||
|
|
35e3271839 | ||
|
|
90da86ebb8 | ||
|
|
ad978d2ed3 | ||
|
|
7140a2d894 | ||
|
|
eb86603b47 | ||
|
|
1e1117c28b | ||
|
|
f3de0d8391 | ||
|
|
4749be20b4 | ||
|
|
3eea00bf6d | ||
|
|
d301287fdc | ||
|
|
07f4090498 | ||
|
|
067d3778d8 | ||
|
|
daa250e8fa | ||
|
|
91f992df39 | ||
|
|
ecaf9a37da | ||
|
|
31cc2fbdd4 | ||
|
|
6e30687919 | ||
|
|
dbf212f0df | ||
|
|
af49645f8d | ||
|
|
59bed7593d | ||
|
|
43ca723c75 | ||
|
|
855370074f | ||
|
|
84f707d6cd | ||
|
|
17be9cf266 | ||
|
|
0e3d01ade6 | ||
|
|
02fe2b1754 | ||
|
|
a0c553b3a9 | ||
|
|
62ea631269 | ||
|
|
b32d842394 | ||
|
|
0c38b5b9f1 | ||
|
|
dd0b20634c | ||
|
|
f8319408f1 | ||
|
|
c451e92a9c | ||
|
|
36cf70202d | ||
|
|
39fe83fdd1 | ||
|
|
cc5a465dc3 | ||
|
|
2018f04bdf | ||
|
|
f0e79eab95 | ||
|
|
fc1b2fbbaa | ||
|
|
a8e89da5e9 | ||
|
|
e369a6b7be | ||
|
|
ae4d4398cd | ||
|
|
5781d20615 | ||
|
|
a6e9521435 | ||
|
|
1e72711bc3 | ||
|
|
1f793e2d00 | ||
|
|
af8d94d102 | ||
|
|
a424c3286a | ||
|
|
5f9b33506a | ||
|
|
819b10e1f6 | ||
|
|
0c170766e5 | ||
|
|
f7a0025b97 | ||
|
|
22776e9088 | ||
|
|
d23d393c39 | ||
|
|
bbf6e796f8 | ||
|
|
1c3c194b3a | ||
|
|
0cc633c5c1 | ||
|
|
9a8f1ad508 | ||
|
|
65f09c6535 | ||
|
|
d7db5d2c77 | ||
|
|
8205b01c65 | ||
|
|
d7ee936b27 | ||
|
|
d9ae6f312f | ||
|
|
aea9a513b4 | ||
|
|
d3b65f6280 | ||
|
|
3bf53aa87f | ||
|
|
5081df4f2f | ||
|
|
b91dbdaa8f |
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,9 +48,9 @@
|
||||
"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"
|
||||
"babel/no-unused-expressions": "error",
|
||||
"node/no-callback-literal": "off"
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: antelle
|
||||
github: [keeweb, antelle]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: keeweb
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: true
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
name: Discussion
|
||||
about: Discuss any topic or idea
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -4,7 +4,7 @@ COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y build-essential git-core unzip curl pkg-config rpm
|
||||
RUN curl -sL https://deb.nodesource.com/setup_13.x | sudo -E bash -
|
||||
RUN curl -sL https://deb.nodesource.com/setup_15.x | sudo -E bash -
|
||||
RUN apt-get install -y nodejs
|
||||
RUN npm i -g grunt-cli
|
||||
|
||||
|
||||
@@ -2,7 +2,4 @@
|
||||
|
||||
cd /github/workspace
|
||||
npm ci
|
||||
cd desktop
|
||||
npm ci
|
||||
cd /github/workspace
|
||||
grunt desktop-linux
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
name: 'Publish release'
|
||||
description: 'Publish a release and update the release notes'
|
||||
inputs:
|
||||
version:
|
||||
description: 'Release version'
|
||||
required: true
|
||||
id:
|
||||
description: 'Release ID'
|
||||
required: true
|
||||
runs:
|
||||
using: 'node12'
|
||||
main: 'dist/index.js'
|
||||
@@ -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,48 +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). ' +
|
||||
'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();
|
||||
@@ -9,14 +9,20 @@ jobs:
|
||||
steps:
|
||||
- name: Get current git tag
|
||||
id: get_tag
|
||||
uses: keeweb/get-tag@v2
|
||||
uses: keeweb/get-git-tag@v3.0.2
|
||||
with:
|
||||
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
|
||||
tagRegexGroup: 1
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
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
|
||||
@@ -28,6 +34,15 @@ jobs:
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: dist
|
||||
- name: Write secrets
|
||||
env:
|
||||
VIRUS_TOTAL: ${{ secrets.VIRUS_TOTAL }}
|
||||
run: |
|
||||
mkdir keys
|
||||
echo "$VIRUS_TOTAL" > keys/virus-total.json
|
||||
- name: Check on VirusTotal
|
||||
run: grunt virustotal
|
||||
if: ${{ github.repository == 'keeweb/keeweb' }}
|
||||
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -36,11 +51,12 @@ jobs:
|
||||
steps:
|
||||
- name: Get current git tag
|
||||
id: get_tag
|
||||
uses: keeweb/get-tag@v2
|
||||
uses: keeweb/get-git-tag@v3.0.2
|
||||
with:
|
||||
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
|
||||
tagRegexGroup: 1
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: keeweb/keeweb
|
||||
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
|
||||
@@ -84,11 +100,6 @@ jobs:
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
|
||||
- name: Upload update artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: UpdateDesktop.zip
|
||||
path: dist/desktop/UpdateDesktop.zip
|
||||
|
||||
darwin:
|
||||
runs-on: macos-latest
|
||||
@@ -97,11 +108,12 @@ jobs:
|
||||
steps:
|
||||
- name: Get current git tag
|
||||
id: get_tag
|
||||
uses: keeweb/get-tag@v2
|
||||
uses: keeweb/get-git-tag@v3.0.2
|
||||
with:
|
||||
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
|
||||
tagRegexGroup: 1
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: keeweb/keeweb
|
||||
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
|
||||
@@ -110,11 +122,13 @@ jobs:
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: dist
|
||||
- name: Upgrade Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install npm modules
|
||||
run: npm ci
|
||||
- name: Install desktop npm modules
|
||||
working-directory: desktop
|
||||
run: npm ci
|
||||
- name: Install grunt
|
||||
run: sudo npm i -g grunt-cli
|
||||
- name: Write secrets
|
||||
@@ -122,21 +136,29 @@ jobs:
|
||||
CODESIGN: ${{ secrets.CODESIGN }}
|
||||
APPLE_DEPLOY_PASSWORD: ${{ secrets.APPLE_DEPLOY_PASSWORD }}
|
||||
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
||||
APPLE_PROVISIONING_PROFILE: ${{ secrets.APPLE_PROVISIONING_PROFILE }}
|
||||
run: |
|
||||
mkdir keys
|
||||
echo "$CODESIGN" > keys/codesign.json
|
||||
xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u "$APPLE_ID_USERNAME" -p "$APPLE_DEPLOY_PASSWORD"
|
||||
- uses: keeweb/import-codesign-certs@v1
|
||||
echo "$APPLE_PROVISIONING_PROFILE" | base64 -d > keys/keeweb.provisionprofile
|
||||
- name: Import certificates
|
||||
uses: keeweb/import-codesign-certs@v1
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
- name: Grunt
|
||||
run: grunt desktop-darwin ${{ github.repository != 'keeweb/keeweb' && '--skip-sign' || '' }}
|
||||
- name: Upload dmg artifact
|
||||
run: grunt --max-old-space-size=4096 desktop-darwin ${{ github.repository != 'keeweb/keeweb' && '--skip-sign' || '' }}
|
||||
- name: Upload x64 dmg artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.dmg
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.dmg
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.x64.dmg
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.x64.dmg
|
||||
- name: Upload arm64 dmg artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.arm64.dmg
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.arm64.dmg
|
||||
|
||||
win32:
|
||||
runs-on: windows-latest
|
||||
@@ -145,11 +167,12 @@ jobs:
|
||||
steps:
|
||||
- name: Get current git tag
|
||||
id: get_tag
|
||||
uses: keeweb/get-tag@v2
|
||||
uses: keeweb/get-git-tag@v3.0.2
|
||||
with:
|
||||
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
|
||||
tagRegexGroup: 1
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: keeweb/keeweb
|
||||
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
|
||||
@@ -158,11 +181,13 @@ jobs:
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: dist
|
||||
- name: Upgrade Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install npm modules
|
||||
run: npm ci
|
||||
- name: Install desktop npm modules
|
||||
working-directory: desktop
|
||||
run: npm ci
|
||||
- name: Install grunt
|
||||
run: npm i -g grunt-cli
|
||||
- name: Write secrets
|
||||
@@ -220,20 +245,27 @@ jobs:
|
||||
steps:
|
||||
- name: Get current git tag
|
||||
id: get_tag
|
||||
uses: keeweb/get-tag@v2
|
||||
uses: keeweb/get-git-tag@v3.0.2
|
||||
with:
|
||||
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
|
||||
tagRegexGroup: 1
|
||||
- uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
|
||||
- name: Setup GCloud
|
||||
uses: google-github-actions/setup-gcloud@master
|
||||
with:
|
||||
version: '285.0.0'
|
||||
service_account_key: ${{ secrets.GCP_SA_KEY }}
|
||||
export_default_credentials: true
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
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
|
||||
@@ -267,10 +299,15 @@ jobs:
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
|
||||
path: assets
|
||||
- name: Download darwin.dmg artifact
|
||||
- name: Download darwin.x64.dmg artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.dmg
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.x64.dmg
|
||||
path: assets
|
||||
- name: Download darwin.arm64.dmg artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.arm64.dmg
|
||||
path: assets
|
||||
- name: Download win32.ia32.exe artifact
|
||||
uses: actions/download-artifact@v1
|
||||
@@ -302,11 +339,6 @@ jobs:
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
||||
path: assets
|
||||
- name: Download update artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: UpdateDesktop.zip
|
||||
path: assets
|
||||
- name: Zip html
|
||||
working-directory: html
|
||||
run: zip -vr ../assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.html.zip .
|
||||
@@ -337,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.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.dmg
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.dmg
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.ia32.exe asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.exe
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.exe
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.ia32.zip asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.zip
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.zip
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.x64.exe asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.exe
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.exe
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.x64.zip asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.zip
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.zip
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.arm64.exe asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.exe
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.exe
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload win32.arm64.zip asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload update asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/UpdateDesktop.zip
|
||||
asset_name: UpdateDesktop.zip
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload verify.sign asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/Verify.sign.sha256
|
||||
asset_name: Verify.sign.sha256
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload verify.sha asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: assets/Verify.sha256
|
||||
asset_name: Verify.sha256
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Publish the GitHub release
|
||||
uses: ./keeweb/.github/actions/publish-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
version: ${{ steps.get_tag.outputs.tag }}
|
||||
release_id: ${{ steps.create_release.outputs.id }}
|
||||
tag_name: v${{ steps.get_tag.outputs.tag }}
|
||||
name: Desktop apps v${{ steps.get_tag.outputs.tag }}
|
||||
files: assets/*
|
||||
body_path: keeweb/tmp/release-body.md
|
||||
- name: Checkout gh-pages
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
@@ -534,3 +415,12 @@ jobs:
|
||||
- name: Sync the website
|
||||
if: ${{ github.repository == 'keeweb/keeweb' }}
|
||||
run: gsutil -m rsync -r -d -x "^\." gh-pages gs://app.keeweb.info/
|
||||
- name: Purge CloudFlare cache
|
||||
if: ${{ github.repository == 'keeweb/keeweb' }}
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
run: |
|
||||
curl -sS -X POST "https://api.cloudflare.com/client/v4/zones/dbda8e7e454d23edb08d4bc3b3f6f872/purge_cache" \
|
||||
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"purge_everything":true}'
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
name: Deploy
|
||||
on:
|
||||
push:
|
||||
branches: [ 'gh-pages' ]
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: 'gh-pages'
|
||||
- uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
|
||||
with:
|
||||
version: '285.0.0'
|
||||
service_account_key: ${{ secrets.GCP_SA_KEY }}
|
||||
export_default_credentials: true
|
||||
- name: Restore git mtime
|
||||
run: curl -Ss https://raw.githubusercontent.com/MestreLion/git-tools/f3cc70b73200154d027554714c354f35a08680ed/git-restore-mtime | python3
|
||||
- name: Sync
|
||||
run: gsutil -m rsync -r -d -x "^\.|^[a-f\d-]{36}$|^README\.md$" . gs://app.keeweb.info/
|
||||
@@ -14,6 +14,6 @@ obj/
|
||||
xcuserdata/
|
||||
*.suo
|
||||
.idea/
|
||||
.vscode
|
||||
.vscode/
|
||||
*.iml
|
||||
test/dist/
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const debug = require('debug');
|
||||
|
||||
const webpackConfig = require('./build/webpack.config');
|
||||
@@ -24,21 +25,30 @@ module.exports = function (grunt) {
|
||||
|
||||
const dt = date.toISOString().replace(/T.*/, '');
|
||||
const year = date.getFullYear();
|
||||
const minElectronVersionForUpdate = '9.0.1';
|
||||
const zipCommentPlaceholderPart = 'zip_comment_placeholder_that_will_be_replaced_with_hash';
|
||||
const zipCommentPlaceholder =
|
||||
zipCommentPlaceholderPart + '.'.repeat(512 - zipCommentPlaceholderPart.length);
|
||||
const electronVersion = pkg.dependencies.electron.replace(/^\D/, '');
|
||||
const skipSign = grunt.option('skip-sign');
|
||||
const getCodeSignConfig = () =>
|
||||
skipSign ? { identities: {} } : require('./keys/codesign.json');
|
||||
|
||||
let sha = grunt.option('commit-sha');
|
||||
if (!sha) {
|
||||
try {
|
||||
sha = execSync('git rev-parse --short HEAD').toString('utf8').trim();
|
||||
} catch (e) {
|
||||
grunt.warn(
|
||||
"Cannot get commit sha from git. It's recommended to build KeeWeb from a git repo " +
|
||||
'because commit sha is displayed in the UI, however if you would like to build from a folder, ' +
|
||||
'you can override what will be displayed in the UI with --commit-sha=xxx.'
|
||||
);
|
||||
}
|
||||
}
|
||||
grunt.log.writeln(`Building KeeWeb v${pkg.version} (${sha})`);
|
||||
|
||||
const webpackOptions = {
|
||||
date,
|
||||
beta: !!grunt.option('beta'),
|
||||
get sha() {
|
||||
return grunt.config.get('gitinfo.local.branch.current.shortSHA');
|
||||
}
|
||||
sha,
|
||||
appleTeamId: '3LE7JZ657W'
|
||||
};
|
||||
|
||||
const windowsAppVersionString = {
|
||||
@@ -49,18 +59,35 @@ module.exports = function (grunt) {
|
||||
InternalName: 'KeeWeb'
|
||||
};
|
||||
|
||||
const appdmgOptions = (arch) => ({
|
||||
title: 'KeeWeb',
|
||||
icon: 'graphics/icon.icns',
|
||||
background: 'graphics/dmg-background.png',
|
||||
'background-color': '#E0E6F9',
|
||||
'icon-size': 80,
|
||||
window: { size: { width: 658, height: 498 } },
|
||||
contents: [
|
||||
{ x: 438, y: 344, type: 'link', path: '/Applications' },
|
||||
{
|
||||
x: 192,
|
||||
y: 344,
|
||||
type: 'file',
|
||||
path: `tmp/desktop/KeeWeb-darwin-${arch}/KeeWeb.app`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const linuxDependencies = [
|
||||
'libappindicator1',
|
||||
'libgconf-2-4',
|
||||
'gnome-keyring',
|
||||
'libxtst6',
|
||||
'libx11-6',
|
||||
'libatspi2.0-0'
|
||||
];
|
||||
|
||||
grunt.initConfig({
|
||||
noop: { noop: {} },
|
||||
gitinfo: {
|
||||
branch: {
|
||||
current: {
|
||||
SHA: 'Current HEAD SHA',
|
||||
shortSHA: 'Current HEAD short SHA',
|
||||
name: 'Current branch name',
|
||||
lastCommitTime: 'Last commit time'
|
||||
}
|
||||
}
|
||||
},
|
||||
clean: {
|
||||
dist: ['dist', 'tmp'],
|
||||
desktop: ['tmp/desktop', 'dist/desktop']
|
||||
@@ -78,11 +105,6 @@ module.exports = function (grunt) {
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
favicon: {
|
||||
src: 'app/favicon.png',
|
||||
dest: 'tmp/favicon.png',
|
||||
nonull: true
|
||||
},
|
||||
icons: {
|
||||
cwd: 'app/icons/',
|
||||
src: ['*.png', '*.svg'],
|
||||
@@ -111,13 +133,6 @@ module.exports = function (grunt) {
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
fonts: {
|
||||
src: 'node_modules/font-awesome/fonts/fontawesome-webfont.*',
|
||||
dest: 'tmp/fonts/',
|
||||
nonull: true,
|
||||
expand: true,
|
||||
flatten: true
|
||||
},
|
||||
'desktop-html': {
|
||||
src: 'dist/index.html',
|
||||
dest: 'tmp/desktop/app/index.html',
|
||||
@@ -130,26 +145,8 @@ module.exports = function (grunt) {
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
'desktop-update': {
|
||||
cwd: 'tmp/desktop/keeweb-linux-x64/resources/',
|
||||
src: 'app.asar',
|
||||
dest: 'tmp/desktop/update/',
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
'desktop-update-helper': {
|
||||
src: ['helper/darwin/KeeWebHelper', 'helper/win32/KeeWebHelper.exe'],
|
||||
dest: 'tmp/desktop/update/',
|
||||
nonull: true
|
||||
},
|
||||
'desktop-darwin-helper': {
|
||||
src: 'helper/darwin/KeeWebHelper',
|
||||
dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Resources/',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'desktop-darwin-installer-helper': {
|
||||
cwd: 'package/osx/KeeWeb Installer.app',
|
||||
'desktop-darwin-installer-helper-x64': {
|
||||
cwd: 'tmp/desktop/KeeWeb Installer.app',
|
||||
src: '**',
|
||||
dest:
|
||||
'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
|
||||
@@ -157,20 +154,14 @@ 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-darwin-installer-helper-arm64': {
|
||||
cwd: 'tmp/desktop/KeeWeb Installer.app',
|
||||
src: '**',
|
||||
dest:
|
||||
'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
|
||||
expand: true,
|
||||
nonull: true,
|
||||
options: { mode: true }
|
||||
},
|
||||
'desktop-win32-dist-x64': {
|
||||
src: 'tmp/desktop/KeeWeb.win.x64.exe',
|
||||
@@ -187,11 +178,16 @@ module.exports = function (grunt) {
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.win.arm64.exe`,
|
||||
nonull: true
|
||||
},
|
||||
'native-modules-darwin': {
|
||||
'native-modules-darwin-x64': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-modules/*-darwin-x64.node',
|
||||
dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Resources/',
|
||||
nonull: true
|
||||
},
|
||||
'native-modules-darwin-arm64': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-modules/*-darwin-arm64.node',
|
||||
dest: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/Resources/',
|
||||
nonull: true
|
||||
},
|
||||
'native-modules-win32-x64': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-modules/*-win32-x64.node',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-x64/resources/',
|
||||
@@ -226,6 +222,52 @@ module.exports = function (grunt) {
|
||||
src: `tmp/desktop/electron-builder/keeweb-${pkg.version}.AppImage`,
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.linux.AppImage`,
|
||||
nonull: true
|
||||
},
|
||||
'darwin-installer-icon': {
|
||||
src: 'graphics/icon.icns',
|
||||
dest: 'tmp/desktop/KeeWeb Installer.app/Contents/Resources/applet.icns',
|
||||
nonull: true
|
||||
},
|
||||
'native-messaging-host-darwin-x64': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/darwin-x64/keeweb-native-messaging-host',
|
||||
dest:
|
||||
'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/MacOS/util/keeweb-native-messaging-host',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'native-messaging-host-darwin-arm64': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/darwin-arm64/keeweb-native-messaging-host',
|
||||
dest:
|
||||
'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/MacOS/util/keeweb-native-messaging-host',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'native-messaging-host-linux-x64': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/linux-x64/keeweb-native-messaging-host',
|
||||
dest: 'tmp/desktop/keeweb-linux-x64/keeweb-native-messaging-host',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'native-messaging-host-win32-x64': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/win32-x64/keeweb-native-messaging-host.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-x64/keeweb-native-messaging-host.exe',
|
||||
nonull: true
|
||||
},
|
||||
'native-messaging-host-win32-ia32': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/win32-ia32/keeweb-native-messaging-host.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-ia32/keeweb-native-messaging-host.exe',
|
||||
nonull: true
|
||||
},
|
||||
'native-messaging-host-win32-arm64': {
|
||||
src:
|
||||
'node_modules/@keeweb/keeweb-native-messaging-host/win32-arm64/keeweb-native-messaging-host.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-arm64/keeweb-native-messaging-host.exe',
|
||||
nonull: true
|
||||
}
|
||||
},
|
||||
eslint: {
|
||||
@@ -233,7 +275,8 @@ module.exports = function (grunt) {
|
||||
desktop: ['desktop/**/*.js', '!desktop/node_modules/**'],
|
||||
build: ['Gruntfile.js', 'grunt.*.js', 'build/**/*.js', 'webpack.config.js'],
|
||||
plugins: ['plugins/**/*.js'],
|
||||
util: ['util/**/*.js']
|
||||
util: ['util/**/*.js'],
|
||||
installer: ['package/osx/installer.js']
|
||||
},
|
||||
inline: {
|
||||
app: {
|
||||
@@ -246,7 +289,7 @@ module.exports = function (grunt) {
|
||||
algo: 'sha512',
|
||||
expected: {
|
||||
style: 1,
|
||||
script: 3
|
||||
script: 1
|
||||
}
|
||||
},
|
||||
app: {
|
||||
@@ -266,20 +309,20 @@ module.exports = function (grunt) {
|
||||
}
|
||||
},
|
||||
'string-replace': {
|
||||
manifest: {
|
||||
'update-manifest': {
|
||||
options: {
|
||||
replacements: [
|
||||
{
|
||||
pattern: '# YYYY-MM-DD:v0.0.0',
|
||||
replacement: '# ' + dt + ':v' + pkg.version
|
||||
pattern: /"version":\s*".*?"/,
|
||||
replacement: `"version": "${pkg.version}"`
|
||||
},
|
||||
{
|
||||
pattern: '# updmin:v0.0.0',
|
||||
replacement: '# updmin:v' + minElectronVersionForUpdate
|
||||
pattern: /"date":\s*".*?"/,
|
||||
replacement: `"date": "${dt}"`
|
||||
}
|
||||
]
|
||||
},
|
||||
files: { 'dist/manifest.appcache': 'app/manifest.appcache' }
|
||||
files: { 'dist/update.json': 'app/update.json' }
|
||||
},
|
||||
'service-worker': {
|
||||
options: { replacements: [{ pattern: '0.0.0', replacement: pkg.version }] },
|
||||
@@ -337,20 +380,30 @@ module.exports = function (grunt) {
|
||||
asar: true,
|
||||
appCopyright: `Copyright © ${year} Antelle`,
|
||||
appVersion: pkg.version,
|
||||
buildVersion: '<%= gitinfo.local.branch.current.shortSHA %>'
|
||||
buildVersion: sha
|
||||
},
|
||||
linux: {
|
||||
options: {
|
||||
name: 'keeweb',
|
||||
platform: 'linux',
|
||||
arch: ['x64'],
|
||||
arch: 'x64',
|
||||
icon: 'graphics/icon.ico'
|
||||
}
|
||||
},
|
||||
darwin: {
|
||||
'darwin-x64': {
|
||||
options: {
|
||||
platform: 'darwin',
|
||||
arch: ['x64'],
|
||||
arch: 'x64',
|
||||
icon: 'graphics/icon.icns',
|
||||
appBundleId: 'net.antelle.keeweb',
|
||||
appCategoryType: 'public.app-category.productivity',
|
||||
extendInfo: 'package/osx/extend.plist'
|
||||
}
|
||||
},
|
||||
'darwin-arm64': {
|
||||
options: {
|
||||
platform: 'darwin',
|
||||
arch: 'arm64',
|
||||
icon: 'graphics/icon.icns',
|
||||
appBundleId: 'net.antelle.keeweb',
|
||||
appCategoryType: 'public.app-category.productivity',
|
||||
@@ -410,26 +463,37 @@ module.exports = function (grunt) {
|
||||
category: 'Utility'
|
||||
},
|
||||
rpm: {
|
||||
// depends: ['libappindicator1', 'libgconf-2-4']
|
||||
// depends: linuxDependencies
|
||||
},
|
||||
snap: {
|
||||
stagePackages: ['libappindicator1', 'libgconf-2-4']
|
||||
stagePackages: linuxDependencies
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'electron-patch': {
|
||||
'win32-x64': 'tmp/desktop/KeeWeb-win32-x64/KeeWeb.exe',
|
||||
'win32-ia32': 'tmp/desktop/KeeWeb-win32-ia32/KeeWeb.exe',
|
||||
'win32-arm64': 'tmp/desktop/KeeWeb-win32-arm64/KeeWeb.exe',
|
||||
'darwin-x64': 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app',
|
||||
'darwin-arm64': 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app',
|
||||
'linux': 'tmp/desktop/KeeWeb-linux-x64/keeweb'
|
||||
},
|
||||
osacompile: {
|
||||
options: {
|
||||
language: 'JavaScript'
|
||||
},
|
||||
installer: {
|
||||
files: {
|
||||
'tmp/desktop/KeeWeb Installer.app': 'package/osx/installer.js'
|
||||
}
|
||||
}
|
||||
},
|
||||
compress: {
|
||||
options: {
|
||||
level: 6
|
||||
},
|
||||
'desktop-update': {
|
||||
options: {
|
||||
archive: 'dist/desktop/UpdateDesktop.zip',
|
||||
comment: zipCommentPlaceholder
|
||||
},
|
||||
files: [{ cwd: 'tmp/desktop/update', src: '**', expand: true, nonull: true }]
|
||||
},
|
||||
'win32-x64': {
|
||||
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.x64.zip` },
|
||||
files: [{ cwd: 'tmp/desktop/KeeWeb-win32-x64', src: '**', expand: true }]
|
||||
@@ -451,34 +515,20 @@ module.exports = function (grunt) {
|
||||
}
|
||||
},
|
||||
appdmg: {
|
||||
options: {
|
||||
title: 'KeeWeb',
|
||||
icon: 'graphics/icon.icns',
|
||||
background: 'graphics/background.png',
|
||||
'background-color': '#E0E6F9',
|
||||
'icon-size': 80,
|
||||
window: { size: { width: 658, height: 498 } },
|
||||
contents: [
|
||||
{ x: 438, y: 344, type: 'link', path: '/Applications' },
|
||||
{
|
||||
x: 192,
|
||||
y: 344,
|
||||
type: 'file',
|
||||
path: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app'
|
||||
}
|
||||
]
|
||||
x64: {
|
||||
options: appdmgOptions('x64'),
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.mac.x64.dmg`
|
||||
},
|
||||
app: {
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.mac.dmg`
|
||||
arm64: {
|
||||
options: appdmgOptions('arm64'),
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.mac.arm64.dmg`
|
||||
}
|
||||
},
|
||||
nsis: {
|
||||
options: {
|
||||
vars: {
|
||||
version: pkg.version,
|
||||
rev() {
|
||||
return grunt.config.get('gitinfo.local.branch.current.shortSHA');
|
||||
},
|
||||
rev: sha,
|
||||
homepage: pkg.homepage
|
||||
}
|
||||
},
|
||||
@@ -542,9 +592,7 @@ module.exports = function (grunt) {
|
||||
description: pkg.description,
|
||||
author: pkg.author,
|
||||
homepage: pkg.homepage,
|
||||
rev() {
|
||||
return grunt.config.get('gitinfo.local.branch.current.shortSHA');
|
||||
}
|
||||
rev: sha
|
||||
}
|
||||
},
|
||||
'linux-x64': {
|
||||
@@ -554,7 +602,7 @@ module.exports = function (grunt) {
|
||||
pkgName: `KeeWeb-${pkg.version}.linux.x64.deb`,
|
||||
targetDir: 'dist/desktop',
|
||||
appName: 'KeeWeb',
|
||||
depends: 'libappindicator1, libgconf-2-4',
|
||||
depends: linuxDependencies.join(', '),
|
||||
scripts: {
|
||||
postinst: 'package/deb/scripts/postinst'
|
||||
}
|
||||
@@ -577,47 +625,30 @@ module.exports = function (grunt) {
|
||||
]
|
||||
}
|
||||
},
|
||||
'sign-archive': {
|
||||
'desktop-update': {
|
||||
options: {
|
||||
file: 'dist/desktop/UpdateDesktop.zip',
|
||||
signature: zipCommentPlaceholder
|
||||
}
|
||||
}
|
||||
},
|
||||
'sign-desktop-files': {
|
||||
'desktop-update': {
|
||||
options: {
|
||||
path: 'tmp/desktop/update'
|
||||
}
|
||||
}
|
||||
},
|
||||
'validate-desktop-update': {
|
||||
desktop: {
|
||||
options: {
|
||||
file: 'dist/desktop/UpdateDesktop.zip',
|
||||
expected: [
|
||||
'app.asar',
|
||||
'helper/darwin/KeeWebHelper',
|
||||
'helper/win32/KeeWebHelper.exe'
|
||||
],
|
||||
expectedCount: 7,
|
||||
publicKey: 'app/resources/public-key.pem'
|
||||
}
|
||||
}
|
||||
},
|
||||
'osx-sign': {
|
||||
options: {
|
||||
get identity() {
|
||||
return getCodeSignConfig().identities.app;
|
||||
},
|
||||
hardenedRuntime: true,
|
||||
entitlements: 'package/osx/entitlements.mac.plist',
|
||||
'entitlements-inherit': 'package/osx/entitlements.mac.plist',
|
||||
entitlements: 'package/osx/entitlements.plist',
|
||||
'entitlements-inherit': 'package/osx/entitlements-inherit.plist',
|
||||
'gatekeeper-assess': false
|
||||
},
|
||||
desktop: {
|
||||
'desktop-x64': {
|
||||
options: {
|
||||
'provisioning-profile': './keys/keeweb.provisionprofile'
|
||||
},
|
||||
src: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app'
|
||||
},
|
||||
'desktop-arm64': {
|
||||
options: {
|
||||
'provisioning-profile': './keys/keeweb.provisionprofile'
|
||||
},
|
||||
src: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app'
|
||||
},
|
||||
'installer': {
|
||||
src: 'tmp/desktop/KeeWeb Installer.app'
|
||||
}
|
||||
},
|
||||
notarize: {
|
||||
@@ -631,8 +662,11 @@ module.exports = function (grunt) {
|
||||
return getCodeSignConfig().teamId;
|
||||
}
|
||||
},
|
||||
desktop: {
|
||||
'desktop-x64': {
|
||||
src: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app'
|
||||
},
|
||||
'desktop-arm64': {
|
||||
src: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app'
|
||||
}
|
||||
},
|
||||
'sign-exe': {
|
||||
@@ -649,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',
|
||||
@@ -661,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',
|
||||
@@ -673,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',
|
||||
@@ -730,10 +770,7 @@ module.exports = function (grunt) {
|
||||
sign: 'dist/desktop/Verify.sign.sha256'
|
||||
},
|
||||
files: {
|
||||
'dist/desktop/Verify.sha256': [
|
||||
'dist/desktop/KeeWeb-*',
|
||||
'dist/desktop/UpdateDesktop.zip'
|
||||
]
|
||||
'dist/desktop/Verify.sha256': ['dist/desktop/KeeWeb-*']
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -742,6 +779,16 @@ module.exports = function (grunt) {
|
||||
headless: true
|
||||
},
|
||||
default: 'test/runner.html'
|
||||
},
|
||||
virustotal: {
|
||||
options: {
|
||||
prefix: `keeweb.v${pkg.version}-${sha}.`,
|
||||
timeout: 10 * 60 * 1000,
|
||||
get apiKey() {
|
||||
return require('./keys/virus-total.json').apiKey;
|
||||
}
|
||||
},
|
||||
html: 'dist/index.html'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Antelle
|
||||
Copyright (c) 2021 Antelle https://antelle.net
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -90,7 +90,7 @@ Please note: donation does not imply any type of service contract.
|
||||
|
||||
Notable contributions to KeeWeb:
|
||||
|
||||
- Florian Reuschel ([@Loilo](https://github.com/Loilo)): [German translation](http://keeweb.oneskyapp.com/collaboration/translate/project/project/173183/language/550)
|
||||
- Florian Reuschel ([@Loilo](https://github.com/Loilo)): [German translation](https://keeweb.oneskyapp.com/collaboration/translate/project/project/173183/language/550)
|
||||
- Dennis Ploeger ([@dploeger](https://github.com/dploeger)): [auto-type improvements](https://github.com/keeweb/keeweb/pulls?q=is%3Apr+is%3Aclosed+author%3Adploeger)
|
||||
- Hackmanit ([hackmanit.de](https://www.hackmanit.de)): [penetration test](https://www.hackmanit.de/en/blog-en/104-pro-bono-penetration-test-keeweb)
|
||||
- Peter Bittner ([@bittner](https://github.com/bittner)): [Wikipedia article](https://en.wikipedia.org/wiki/KeeWeb)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>KeeWeb</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.opener.postMessage(
|
||||
{ storage: 'teams', search: location.search },
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 685 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1 +1 @@
|
||||
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="922.667" height="922.667" viewBox="0 0 692.000000 692.000000"><path d="M266.9 57.4c-.2.2-4 .6-8.3 1-4.4.4-8.2.8-8.5 1-.3.2-2.7.7-5.3 1-5.5.7-21.6 4.8-29.8 7.5-46.3 15.4-93.3 48.9-126.5 90.1-25.7 31.8-46.8 74.8-52.9 107.4-.5 2.8-1.5 8.3-2.1 12.1-1.9 11.6-2.6 35.9-1.3 45 .3 2.7.9 7 1.2 9.5 3.3 27.4 16.9 55.4 36.1 74.5 12.1 12.1 27.3 22.7 40 27.8 3.9 1.6 7.9 3.3 9 3.7 1.1.5 4.3 1.5 7 2.4 4.4 1.3 7.1 1.9 18.5 4.1 12.1 2.4 42.4 2.4 56.5 0 1.7-.3 4.4-.7 6-1 1.7-.3 4.1-.7 5.5-1 1.4-.3 3.7-.7 5.1-1 4.5-.8 19.1-5.2 28.4-8.6 16.8-6.1 42.9-20.2 59.4-32.2l5.3-3.9 130.7 130.5C567.7 654 571.7 657.8 577.5 660c6.2 2.2 15.3 2.9 21.4 1.6 12.8-2.7 27.8-15.7 34.7-30.1 2.5-5.3 2.9-7.1 2.9-15-.1-15.8 1.1-14.3-49.6-65.1l-44-44 18.1-18.2 18.1-18.1 6.7 7.1c20.7 21.9 32.6 32.8 38.6 35.3 5.6 2.4 11.5-1.2 26.5-15.8 16.1-15.8 21.8-23.8 20-28.6-.9-2.3-4.6-7.8-7.4-10.7-.5-.6-4.1-4.8-8-9.2-7.2-8.3-64.7-65.9-91-91.1-8.2-7.9-16-14.7-17.3-15.2-6.8-2.6-12.6.8-28.4 16.3-13.1 12.9-19.8 21.8-19.8 26.3 0 5.6 13.3 21 37.4 43.4l5.8 5.4-18.1 18.1-18.1 18.1-67-67c-36.9-36.8-67-67.3-67-67.7 0-.4 3-5.1 6.7-10.4 16.1-23.5 30.3-53.8 35.8-76.4.7-2.8 4.3-22.5 5.3-28.5 1.1-6.5.8-41.1-.3-48.5-1.4-9.2-6.4-26.8-10-35.3-9.9-23.3-28.4-44.9-50.5-58.8-21.8-13.8-47.4-20.5-78.6-20.7-7.3-.1-13.3 0-13.5.2zm22 74c12.1 2.8 22.6 7.6 31.1 14.4 13.3 10.7 23.7 27.4 26.3 42.7 6 34.4-9.5 65.3-40.5 80.7-16.1 7.9-40.9 8.5-58.1 1.3-4.4-1.8-5.2-1.9-4-.3 1.1 1.4 4 9.8 4.8 13.9 5.3 25.7-.6 47.7-17.7 66-8.9 9.6-19.7 16.4-31.3 19.8-9.7 2.9-11.7 3.1-22.2 3.2-24.8 0-47.2-12.3-61-33.3-15.4-23.7-15.3-56.4.4-79.3 11.1-16.2 26.6-27.1 44.3-31.1 14.4-3.2 32.9-1.7 43.9 3.7 2 .9 3.7 1.6 3.8 1.5.2-.2-.8-3.4-2.2-7.3-3.4-9.7-3.7-11.4-4-23.3-.4-17.1 3.8-30.7 13.8-44 11.4-15.2 26.5-24.8 44.7-28.4 6.2-1.2 22.9-1.4 27.9-.2z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-64 -64 640 640"><path d="M136.736 512c-4.892-.008-10.322-2.061-15.92-4.997-11.941-6.265-20.13-14.185-13.293-27.218l166.101-316.638c-1.972-1.022-3.619-1.88-4.743-2.469-59.892-31.418-86.463-77.87-64.952-118.877 24.726-47.135 101.304-55.69 171.042-19.107 69.738 36.584 106.227 104.451 81.501 151.586-21.264 40.536-76.784 44.151-132.61 14.866-1.538-.807-4.033-2.103-6.857-3.567L150.768 502.471c-3.632 6.924-8.488 9.538-14.032 9.529zm-30.582-67.835l-3.144-1.649-.7-.366-42.546-22.32v-.001l-10.256-5.38c-1.738-.911-2.407-3.057-1.496-4.794l13.465-25.667c.906-1.728 3.036-2.399 4.768-1.509l.028.013 20.513 10.761c1.736.911 3.883.241 4.794-1.496l13.466-25.667c.911-1.737.24-3.883-1.497-4.794l-10.256-5.382-10.256-5.38c-1.737-.911-2.406-3.057-1.495-4.794l13.464-25.669c.911-1.737 3.058-2.406 4.794-1.494l10.258 5.38 26.697 14.005 16.548 8.681c.007.003.014.007.021.011l3.125 1.639-14.142 26.957-.973 1.855c0 .001-.001.003-.002.004l-20.063 38.245-2.041 3.888-11.424 21.779c-.002.004-.005.009-.008.014zm67.232-128.162l-3.145-1.651-14.938-7.837-11.794-6.187c-1.737-.911-2.408-3.057-1.497-4.794l13.466-25.667c.911-1.737 3.058-2.408 4.794-1.497l11.795 6.188 14.939 7.837c.007.003.014.007.021.01l3.125 1.64-1.651 3.145-6.731 12.834-6.733 12.833c-.003.005-.006.008-.009.013zm219.778-151.914c8.857.016 15.784-3.552 20.386-12.326 14.027-26.737-11.92-67.987-57.953-92.135-46.032-24.148-94.719-22.049-108.746 4.688-14.026 26.737 24.987 44.293 71.02 68.441 30.928 16.224 57.156 31.296 75.293 31.332z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1007 KiB After Width: | Height: | Size: 1013 KiB |
|
Before Width: | Height: | Size: 947 KiB After Width: | Height: | Size: 960 KiB |
|
Before Width: | Height: | Size: 1020 KiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 322 KiB |
|
Before Width: | Height: | Size: 405 KiB After Width: | Height: | Size: 408 KiB |
@@ -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:;
|
||||
@@ -19,7 +19,6 @@
|
||||
"
|
||||
/>
|
||||
<meta name="application-name" content="KeeWeb" />
|
||||
<meta name="kw-signature" content="" />
|
||||
<meta name="kw-config" content="(no-config)" />
|
||||
<meta
|
||||
name="viewport"
|
||||
@@ -72,9 +71,7 @@
|
||||
/>
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<link rel="stylesheet" href="css/app.css?__inline=true" />
|
||||
<script src="js/vendor.js?__inline=true"></script>
|
||||
<script src="js/app.js?__inline=true"></script>
|
||||
<script src="js/runtime.js?__inline=true"></script>
|
||||
</head>
|
||||
<body class="th-d">
|
||||
<noscript>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
CACHE MANIFEST
|
||||
|
||||
# YYYY-MM-DD:v0.0.0
|
||||
# updmin:v0.0.0
|
||||
|
||||
NETWORK:
|
||||
*
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M339 314.9L175.4 32h161.2l163.6 282.9H339zm-137.5 23.6L120.9 480h310.5L512 338.5H201.5zM154.1 67.4L0 338.5 80.6 480 237 208.8 154.1 67.4z"/></svg>
|
||||
|
Before Width: | Height: | Size: 217 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="256" height="256" version="1.1" viewBox="0 0 256 256"><g transform="translate(296.64282,-100.61434)"><g transform="translate(222.85714,-11.428576)"><g transform="matrix(0.83394139,0,0,0.83394139,-86.101383,10.950635)"><path d="m-419.5 365.94c-18.48-4.62-28.77-19.31-28.81-41.1-0.01-6.97 0.49-10.31 2.23-14.79 4.26-10.99 15.55-19.27 30.41-22.33 7.39-1.52 9.67-3.15 9.67-6.92 0-1.18 0.88-4.71 1.95-7.83 4.88-14.2 13.93-26.03 23.59-30.87 10.11-5.07 15.22-6.21 27.45-6.14 17.38 0.09 26.04 3.86 38.17 16.6l6.67 7 5.97-2.07c28.91-10.01 57.73 7.03 60.06 35.49l0.64 7.79 5.69 2.04c16.26 5.83 23.9 18.06 22.52 36.04-0.91 11.76-6.4 21.15-15.11 25.81l-4.09 2.19-91 0.18c-69.93 0.13-92.16-0.11-96-1.07zM-487.72 353.36" fill="#000"/><path d="m-487.72 353.36c-10.79-2.56-22.22-12.09-27.58-22.99-3.04-6.18-3.2-7.09-3.2-18.03 0-10.4 0.26-12.07 2.68-17.23 5.1-10.89 14.88-18.75 27.15-21.84 2.59-0.65 5.02-1.69 5.41-2.31 0.38-0.62 0.81-4 0.95-7.5 0.85-21.78 15.15-40.97 35.1-47.14 10.78-3.33 24.33-2.51 36.05 2.18 3.72 1.49 3.3 1.81 11.16-8.5 4.65-6.1 14.05-13.68 21.74-17.55 8.3-4.17 16.94-6.09 27.26-6.07 28.86 0.07 53.73 18.12 62.92 45.67 2.94 8.8 2.79 11.27-0.67 11.34-1.51 0.03-5.85 0.86-9.63 1.85l-6.88 1.79-6.28-6.28c-17.7-17.7-46.59-21.53-71.15-9.42-9.81 4.84-17.7 11.78-23.65 20.83-4.25 6.45-9.66 18.48-9.66 21.47 0 2.12-1.72 3.18-9.05 5.58-22.69 7.44-35.94 24.63-35.93 46.62 0 8 2.06 17.8 4.93 23.41 1.08 2.11 1.68 4.13 1.34 4.47-0.88 0.88-29.11 0.58-33.01-0.35z" /></g></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1 +0,0 @@
|
||||
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M465.69 311.917V200.083H363.175v111.834zm-69.897-18.639a4.66 4.66 0 01-4.659-4.659v-65.237a4.66 4.66 0 014.659-4.66h37.278a4.66 4.66 0 014.66 4.66v65.237a4.66 4.66 0 01-4.66 4.66h-37.278z"/><path d="M400.453 251.34v-23.299h27.959v23.299zm0 32.618v-23.299h27.959v23.299zM46.31 187.656v136.688c0 8.564 6.968 15.532 15.532 15.532h21.746V204.742a4.66 4.66 0 019.319 0v135.134h245.415c8.565 0 15.533-6.968 15.533-15.532V187.656c0-8.564-6.968-15.532-15.533-15.532H61.842c-8.565 0-15.532 6.968-15.532 15.532zm241.936 68.6c.122 9.637-7.529 17.293-17.166 17.416-9.359-.354-17.016-8.006-16.662-17.366-.122-9.636 7.529-17.293 16.889-16.938 9.637-.124 17.293 7.528 16.939 16.888zm-41.921-28.528a4.658 4.658 0 011.679 6.372c-3.865 6.631-5.909 14.266-5.909 22.082v.014c-.143 8.152 1.746 15.601 5.603 22.206a4.66 4.66 0 01-8.048 4.7c-4.73-8.099-7.057-17.196-6.873-26.984v-.021c0-9.381 2.48-18.637 7.176-26.69a4.657 4.657 0 016.372-1.679zm-23.7-17.48a4.659 4.659 0 011.395 6.441c-7.725 11.993-11.78 25.622-11.613 39.406-.164 14.287 3.794 28.031 11.327 39.791a4.66 4.66 0 01-7.849 5.027c-8.511-13.288-12.909-28.806-12.799-44.815-.13-15.503 4.387-30.927 13.099-44.455a4.66 4.66 0 016.44-1.395zm-22.433-16.741a4.656 4.656 0 011.272 6.466c-11.136 16.59-17.074 35.936-17.174 55.945-.244 20.163 5.497 39.619 16.606 56.34a4.66 4.66 0 01-7.762 5.158c-11.901-17.914-18.172-38.71-18.172-60.224 0-.452.003-.901.008-1.354.108-21.815 6.594-42.941 18.756-61.059a4.66 4.66 0 016.466-1.272z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -6,9 +6,11 @@ import { ExportApi } from 'comp/app/export-api';
|
||||
import { SingleInstanceChecker } from 'comp/app/single-instance-checker';
|
||||
import { Updater } from 'comp/app/updater';
|
||||
import { UsbListener } from 'comp/app/usb-listener';
|
||||
import { BrowserExtensionConnector } from 'comp/extension/browser-extension-connector';
|
||||
import { FeatureTester } from 'comp/browser/feature-tester';
|
||||
import { FocusDetector } from 'comp/browser/focus-detector';
|
||||
import { IdleTracker } from 'comp/browser/idle-tracker';
|
||||
import { ThemeWatcher } from 'comp/browser/theme-watcher';
|
||||
import { KeyHandler } from 'comp/browser/key-handler';
|
||||
import { PopupNotifier } from 'comp/browser/popup-notifier';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
@@ -44,6 +46,7 @@ ready(() => {
|
||||
.then(loadRemoteConfig)
|
||||
.then(ensureCanRun)
|
||||
.then(initStorage)
|
||||
.then(initUsbListener)
|
||||
.then(showApp)
|
||||
.then(postInit)
|
||||
.catch((e) => {
|
||||
@@ -91,6 +94,8 @@ ready(() => {
|
||||
KdbxwebInit.init();
|
||||
FocusDetector.init();
|
||||
AutoType.init();
|
||||
ThemeWatcher.init();
|
||||
SettingsManager.init();
|
||||
window.kw = ExportApi;
|
||||
return PluginManager.init().then(() => {
|
||||
StartProfiler.milestone('initializing modules');
|
||||
@@ -111,13 +116,13 @@ ready(() => {
|
||||
function loadRemoteConfig() {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
SettingsManager.setBySettings(appModel.settings);
|
||||
SettingsManager.setBySettings();
|
||||
const configParam = getConfigParam();
|
||||
if (configParam) {
|
||||
return appModel
|
||||
.loadConfig(configParam)
|
||||
.then(() => {
|
||||
SettingsManager.setBySettings(appModel.settings);
|
||||
SettingsManager.setBySettings();
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!appModel.settings.cacheConfigSettings) {
|
||||
@@ -139,6 +144,11 @@ ready(() => {
|
||||
StartProfiler.milestone('initializing storage');
|
||||
}
|
||||
|
||||
function initUsbListener() {
|
||||
UsbListener.init();
|
||||
StartProfiler.milestone('starting usb');
|
||||
}
|
||||
|
||||
function showApp() {
|
||||
return Promise.resolve().then(() => {
|
||||
const skipHttpsWarning =
|
||||
@@ -169,12 +179,12 @@ ready(() => {
|
||||
}
|
||||
|
||||
function postInit() {
|
||||
Updater.init();
|
||||
SingleInstanceChecker.init();
|
||||
AppRightsChecker.init();
|
||||
IdleTracker.init();
|
||||
UsbListener.init();
|
||||
setTimeout(() => {
|
||||
Updater.init();
|
||||
SingleInstanceChecker.init();
|
||||
AppRightsChecker.init();
|
||||
IdleTracker.init();
|
||||
BrowserExtensionConnector.init(appModel);
|
||||
PluginManager.runAutoUpdate();
|
||||
}, Timeouts.AutoUpdatePluginsAfterStart);
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const AutoTypeEmitterFactory = {
|
||||
create(callback, windowId) {
|
||||
if (Launcher && Launcher.autoTypeSupported) {
|
||||
const { AutoTypeEmitter } = require('./emitter/auto-type-emitter-' +
|
||||
Launcher.platform());
|
||||
return new AutoTypeEmitter(callback, windowId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export { AutoTypeEmitterFactory };
|
||||
@@ -0,0 +1,160 @@
|
||||
import { Features } from 'util/features';
|
||||
import { NativeModules } from 'comp/launcher/native-modules';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
|
||||
const logger = new Logger('auto-type-emitter');
|
||||
|
||||
const KeyMap = {
|
||||
tab: 'Tab',
|
||||
enter: 'Return',
|
||||
space: 'Space',
|
||||
up: 'UpArrow',
|
||||
down: 'DownArrow',
|
||||
left: 'LeftArrow',
|
||||
right: 'RightArrow',
|
||||
home: 'Home',
|
||||
end: 'End',
|
||||
pgup: 'PageUp',
|
||||
pgdn: 'PageDown',
|
||||
ins: 'Insert',
|
||||
del: 'ForwardDelete',
|
||||
bs: 'BackwardDelete',
|
||||
esc: 'Escape',
|
||||
win: 'Meta',
|
||||
rwin: 'RightMeta',
|
||||
f1: 'F1',
|
||||
f2: 'F2',
|
||||
f3: 'F3',
|
||||
f4: 'F4',
|
||||
f5: 'F5',
|
||||
f6: 'F6',
|
||||
f7: 'F7',
|
||||
f8: 'F8',
|
||||
f9: 'F9',
|
||||
f10: 'F10',
|
||||
f11: 'F11',
|
||||
f12: 'F12',
|
||||
f13: 'F13',
|
||||
f14: 'F14',
|
||||
f15: 'F15',
|
||||
f16: 'F16',
|
||||
add: 'KeypadPlus',
|
||||
subtract: 'KeypadMinus',
|
||||
multiply: 'KeypadMultiply',
|
||||
divide: 'KeypadDivide',
|
||||
n0: 'D0',
|
||||
n1: 'D1',
|
||||
n2: 'D2',
|
||||
n3: 'D3',
|
||||
n4: 'D4',
|
||||
n5: 'D5',
|
||||
n6: 'D6',
|
||||
n7: 'D7',
|
||||
n8: 'D8',
|
||||
n9: 'D9'
|
||||
};
|
||||
|
||||
const ModMap = {
|
||||
'^': Features.isMac ? 'Command' : 'Ctrl',
|
||||
'+': 'Shift',
|
||||
'%': 'Alt',
|
||||
'^^': 'Ctrl'
|
||||
};
|
||||
|
||||
class AutoTypeEmitter {
|
||||
constructor(callback) {
|
||||
this.callback = callback;
|
||||
this.mod = {};
|
||||
}
|
||||
|
||||
begin() {
|
||||
this.withCallback(NativeModules.kbdEnsureModifierNotPressed());
|
||||
}
|
||||
|
||||
setMod(mod, enabled) {
|
||||
const nativeMod = ModMap[mod];
|
||||
if (!nativeMod) {
|
||||
return this.callback(`Bad modifier: ${mod}`);
|
||||
}
|
||||
NativeModules.kbdKeyMoveWithModifier(!!enabled, [nativeMod]).catch((e) => {
|
||||
logger.error('Error moving modifier', mod, enabled ? 'down' : 'up', e);
|
||||
});
|
||||
if (enabled) {
|
||||
this.mod[nativeMod] = true;
|
||||
} else {
|
||||
delete this.mod[nativeMod];
|
||||
}
|
||||
}
|
||||
|
||||
text(str) {
|
||||
if (!str) {
|
||||
return this.withCallback(Promise.resolve());
|
||||
}
|
||||
const mods = Object.keys(this.mod);
|
||||
if (mods.length) {
|
||||
this.withCallback(NativeModules.kbdTextAsKeys(str, mods));
|
||||
} else {
|
||||
this.withCallback(NativeModules.kbdText(str));
|
||||
}
|
||||
}
|
||||
|
||||
key(key) {
|
||||
const mods = Object.keys(this.mod);
|
||||
if (typeof key === 'number') {
|
||||
this.withCallback(NativeModules.kbdKeyPressWithCharacter(0, key, mods));
|
||||
} else {
|
||||
if (!KeyMap[key]) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
const code = KeyMap[key];
|
||||
this.withCallback(NativeModules.kbdKeyPress(code, mods));
|
||||
}
|
||||
}
|
||||
|
||||
copyPaste(text) {
|
||||
setTimeout(() => {
|
||||
Launcher.setClipboardText(text);
|
||||
setTimeout(() => {
|
||||
this.withCallback(NativeModules.kbdShortcut('V'));
|
||||
}, Timeouts.AutoTypeCopyPaste);
|
||||
}, Timeouts.AutoTypeCopyPaste);
|
||||
}
|
||||
|
||||
wait(time) {
|
||||
setTimeout(() => this.withCallback(Promise.resolve()), time);
|
||||
}
|
||||
|
||||
waitComplete() {
|
||||
this.withCallback(Promise.resolve());
|
||||
}
|
||||
|
||||
setDelay() {
|
||||
this.callback('Not implemented');
|
||||
}
|
||||
|
||||
withCallback(promise) {
|
||||
promise
|
||||
.then(() => {
|
||||
try {
|
||||
this.callback();
|
||||
} catch (err) {
|
||||
logger.error('Callback error', err);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
const keyPressFailed = err.message === 'Key press failed';
|
||||
if (keyPressFailed) {
|
||||
err.keyPressFailed = true;
|
||||
}
|
||||
try {
|
||||
this.callback(err);
|
||||
} catch (err) {
|
||||
logger.error('Callback error', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { AutoTypeEmitter };
|
||||
@@ -1,90 +0,0 @@
|
||||
import { SearchResultCollection } from 'collections/search-result-collection';
|
||||
import { Ranking } from 'util/data/ranking';
|
||||
|
||||
const urlPartsRegex = /^(\w+:\/\/)?(?:(?:www|wwws|secure)\.)?([^\/]+)\/?(.*)/;
|
||||
|
||||
const AutoTypeFilter = function (windowInfo, appModel) {
|
||||
this.title = windowInfo.title;
|
||||
this.url = windowInfo.url;
|
||||
this.text = '';
|
||||
this.ignoreWindowInfo = false;
|
||||
this.appModel = appModel;
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.getEntries = function () {
|
||||
const filter = {
|
||||
text: this.text,
|
||||
autoType: true
|
||||
};
|
||||
this.prepareFilter();
|
||||
let entries = this.appModel.getEntriesByFilter(filter).map((e) => [e, this.getEntryRank(e)]);
|
||||
if (!this.ignoreWindowInfo) {
|
||||
entries = entries.filter((e) => e[1]);
|
||||
}
|
||||
entries = entries.sort((x, y) =>
|
||||
x[1] === y[1] ? x[0].title.localeCompare(y[0].title) : y[1] - x[1]
|
||||
);
|
||||
entries = entries.map((p) => p[0]);
|
||||
return new SearchResultCollection(entries, { comparator: 'none' });
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.hasWindowInfo = function () {
|
||||
return this.title || this.url;
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.prepareFilter = function () {
|
||||
this.titleLower = this.title ? this.title.toLowerCase() : null;
|
||||
this.urlLower = this.url ? this.url.toLowerCase() : null;
|
||||
this.urlParts = this.url ? urlPartsRegex.exec(this.urlLower) : null;
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.getEntryRank = function (entry) {
|
||||
let rank = 0;
|
||||
if (this.titleLower && entry.title) {
|
||||
rank += Ranking.getStringRank(entry.title.toLowerCase(), this.titleLower);
|
||||
}
|
||||
if (this.urlParts) {
|
||||
if (entry.url) {
|
||||
const entryUrlParts = urlPartsRegex.exec(entry.url.toLowerCase());
|
||||
if (entryUrlParts) {
|
||||
const [, scheme, domain, path] = entryUrlParts;
|
||||
const [, thisScheme, thisDomain, thisPath] = this.urlParts;
|
||||
if (domain === thisDomain || thisDomain.indexOf('.' + domain) > 0) {
|
||||
if (domain === thisDomain) {
|
||||
rank += 20;
|
||||
} else {
|
||||
rank += 10;
|
||||
}
|
||||
if (path === thisPath) {
|
||||
rank += 10;
|
||||
} else if (path && thisPath) {
|
||||
if (path.lastIndexOf(thisPath, 0) === 0) {
|
||||
rank += 5;
|
||||
} else if (thisPath.lastIndexOf(path, 0) === 0) {
|
||||
rank += 3;
|
||||
}
|
||||
}
|
||||
if (scheme === thisScheme) {
|
||||
rank += 1;
|
||||
}
|
||||
} else {
|
||||
if (entry.searchText.indexOf(this.urlLower) >= 0) {
|
||||
// the url is in some field; include it
|
||||
rank += 5;
|
||||
} else {
|
||||
// another domain; don't show this record at all, ignore title match
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (entry.searchText.indexOf(this.urlLower) >= 0) {
|
||||
// the url is in some field; include it
|
||||
rank += 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
return rank;
|
||||
};
|
||||
|
||||
export { AutoTypeFilter };
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const AutoTypeHelperFactory = {
|
||||
create() {
|
||||
if (Launcher && Launcher.autoTypeSupported) {
|
||||
const { AutoTypeHelper } = require('./helper/auto-type-helper-' + Launcher.platform());
|
||||
return new AutoTypeHelper();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export { AutoTypeHelperFactory };
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NativeModules } from 'comp/launcher/native-modules';
|
||||
|
||||
class AutoTypeHelper {
|
||||
getActiveWindowInfo(callback) {
|
||||
NativeModules.kbdGetActiveWindow({
|
||||
getWindowTitle: true,
|
||||
getBrowserUrl: true
|
||||
})
|
||||
.then((win) => {
|
||||
callback(undefined, win);
|
||||
})
|
||||
.catch((err) => callback(err));
|
||||
}
|
||||
}
|
||||
|
||||
export { AutoTypeHelper };
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AutoTypeEmitterFactory } from 'auto-type/auto-type-emitter-factory';
|
||||
import { AutoTypeEmitter } from 'auto-type/auto-type-emitter';
|
||||
import { AutoTypeObfuscator } from 'auto-type/auto-type-obfuscator';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
import { Logger } from 'util/logger';
|
||||
@@ -6,7 +6,7 @@ import { Logger } from 'util/logger';
|
||||
const emitterLogger = new Logger(
|
||||
'auto-type-emitter',
|
||||
undefined,
|
||||
localStorage.debugAutoType ? Logger.Level.All : Logger.Level.Warn
|
||||
localStorage.debugAutoType ? Logger.Level.All : Logger.Level.Info
|
||||
);
|
||||
|
||||
const AutoTypeRunner = function (ops) {
|
||||
@@ -239,7 +239,7 @@ AutoTypeRunner.prototype.tryParseCommand = function (op) {
|
||||
// {VKEY 10} {VKEY 0x1F}
|
||||
op.type = 'key';
|
||||
op.value = parseInt(op.arg);
|
||||
if (isNaN(op.value) || op.value <= 0) {
|
||||
if (isNaN(op.value) || op.value < 0) {
|
||||
throw 'Bad vkey: ' + op.arg;
|
||||
}
|
||||
return true;
|
||||
@@ -432,7 +432,7 @@ AutoTypeRunner.prototype.obfuscateOp = function (op) {
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.run = function (callback, windowId) {
|
||||
this.emitter = AutoTypeEmitterFactory.create(this.emitNext.bind(this), windowId);
|
||||
this.emitter = new AutoTypeEmitter(this.emitNext.bind(this), windowId);
|
||||
this.emitterState = {
|
||||
callback,
|
||||
stack: [],
|
||||
@@ -442,7 +442,7 @@ AutoTypeRunner.prototype.run = function (callback, windowId) {
|
||||
activeMod: {},
|
||||
finished: null
|
||||
};
|
||||
this.emitNext();
|
||||
this.emitter.begin();
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.emitNext = function (err) {
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import { AutoTypeNativeHelper } from 'auto-type/helper/auto-type-native-helper';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
// http://eastmanreference.com/complete-list-of-applescript-key-codes/
|
||||
const KeyMap = {
|
||||
tab: 48,
|
||||
enter: 36,
|
||||
space: 49,
|
||||
up: 126,
|
||||
down: 125,
|
||||
left: 123,
|
||||
right: 124,
|
||||
home: 115,
|
||||
end: 119,
|
||||
pgup: 116,
|
||||
pgdn: 121,
|
||||
ins: 114,
|
||||
del: 117,
|
||||
bs: 51,
|
||||
esc: 53,
|
||||
win: 55,
|
||||
rwin: 55,
|
||||
f1: 122,
|
||||
f2: 120,
|
||||
f3: 99,
|
||||
f4: 118,
|
||||
f5: 96,
|
||||
f6: 97,
|
||||
f7: 98,
|
||||
f8: 100,
|
||||
f9: 101,
|
||||
f10: 109,
|
||||
f11: 103,
|
||||
f12: 111,
|
||||
f13: 105,
|
||||
f14: 107,
|
||||
f15: 113,
|
||||
f16: 106,
|
||||
add: 69,
|
||||
subtract: 78,
|
||||
multiply: 67,
|
||||
divide: 75,
|
||||
n0: 82,
|
||||
n1: 83,
|
||||
n2: 84,
|
||||
n3: 85,
|
||||
n4: 86,
|
||||
n5: 87,
|
||||
n6: 88,
|
||||
n7: 89,
|
||||
n8: 91,
|
||||
n9: 92
|
||||
};
|
||||
|
||||
const ModMap = {
|
||||
'^': '@',
|
||||
'+': '+',
|
||||
'%': '%',
|
||||
'^^': '^'
|
||||
};
|
||||
|
||||
const AutoTypeEmitter = function (callback) {
|
||||
this.callback = callback;
|
||||
this.mod = {};
|
||||
this.pendingScript = [];
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
|
||||
if (enabled) {
|
||||
this.mod[ModMap[mod]] = true;
|
||||
} else {
|
||||
delete this.mod[ModMap[mod]];
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.text = function (text) {
|
||||
this.pendingScript.push('text ' + this.modString() + ' ' + text);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.key = function (key) {
|
||||
if (typeof key !== 'number') {
|
||||
if (!KeyMap[key]) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
key = KeyMap[key];
|
||||
}
|
||||
this.pendingScript.push('key ' + this.modString() + key);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.copyPaste = function (text) {
|
||||
this.pendingScript.push('copypaste ' + text);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.wait = function (time) {
|
||||
this.pendingScript.push('wait ' + time);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.waitComplete = function () {
|
||||
if (this.pendingScript.length) {
|
||||
const script = this.pendingScript.join('\n');
|
||||
this.pendingScript.length = 0;
|
||||
this.runScript(script);
|
||||
} else {
|
||||
this.callback();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setDelay = function (delay) {
|
||||
this.delay = delay || 0;
|
||||
this.callback('Not implemented');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.modString = function () {
|
||||
return Object.keys(this.mod).join('');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.runScript = function (script) {
|
||||
Launcher.spawn({
|
||||
cmd: AutoTypeNativeHelper.getHelperPath(),
|
||||
data: script,
|
||||
complete: this.callback
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeEmitter };
|
||||
@@ -1,161 +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;
|
||||
if (typeof windowId !== 'undefined' && windowId) {
|
||||
this.windowParameter = '--window ' + windowId + ' ';
|
||||
} else {
|
||||
this.windowParameter = '';
|
||||
}
|
||||
this.mod = {};
|
||||
this.pendingScript = [];
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
|
||||
if (enabled) {
|
||||
this.mod[ModMap[mod]] = true;
|
||||
} else {
|
||||
delete this.mod[ModMap[mod]];
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.text = function (text) {
|
||||
this.pendingScript.push('keyup ctrl alt shift t');
|
||||
Object.keys(this.mod).forEach((mod) => {
|
||||
this.pendingScript.push('keydown ' + this.windowParameter + ModMap[mod]);
|
||||
});
|
||||
text.split('').forEach((char) => {
|
||||
this.pendingScript.push(
|
||||
'key ' + this.windowParameter + 'U' + char.charCodeAt(0).toString(16)
|
||||
);
|
||||
});
|
||||
Object.keys(this.mod).forEach((mod) => {
|
||||
this.pendingScript.push('keyup ' + this.windowParameter + 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.windowParameter + 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 ' + this.windowParameter + '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) {
|
||||
Launcher.spawn({
|
||||
cmd: 'xdotool',
|
||||
args: ['-'],
|
||||
data: script,
|
||||
complete: (err, stdout, code) => {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
err = Locale.autoTypeErrorNotInstalled.replace('{}', 'xdotool');
|
||||
}
|
||||
const cb = callback || this.callback;
|
||||
cb(err, stdout, code);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeEmitter };
|
||||
@@ -1,131 +0,0 @@
|
||||
import { AutoTypeNativeHelper } from 'auto-type/helper/auto-type-native-helper';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||
const KeyMap = {
|
||||
tab: 0x09,
|
||||
enter: 0x0d,
|
||||
space: 0x20,
|
||||
up: 0x26,
|
||||
down: 0x28,
|
||||
left: 0x25,
|
||||
right: 0x27,
|
||||
home: 0x24,
|
||||
end: 0x23,
|
||||
pgup: 0x21,
|
||||
pgdn: 0x22,
|
||||
ins: 0x2d,
|
||||
del: 0x2e,
|
||||
bs: 0x08,
|
||||
esc: 0x1b,
|
||||
win: 0x5b,
|
||||
rwin: 0x5c,
|
||||
f1: 0x70,
|
||||
f2: 0x71,
|
||||
f3: 0x72,
|
||||
f4: 0x73,
|
||||
f5: 0x74,
|
||||
f6: 0x75,
|
||||
f7: 0x76,
|
||||
f8: 0x77,
|
||||
f9: 0x78,
|
||||
f10: 0x79,
|
||||
f11: 0x7a,
|
||||
f12: 0x7b,
|
||||
f13: 0x7c,
|
||||
f14: 0x7d,
|
||||
f15: 0x7e,
|
||||
f16: 0x7f,
|
||||
add: 0x6b,
|
||||
subtract: 0x6d,
|
||||
multiply: 0x6a,
|
||||
divide: 0x6f,
|
||||
n0: 0x30,
|
||||
n1: 0x31,
|
||||
n2: 0x32,
|
||||
n3: 0x33,
|
||||
n4: 0x34,
|
||||
n5: 0x35,
|
||||
n6: 0x36,
|
||||
n7: 0x37,
|
||||
n8: 0x38,
|
||||
n9: 0x39
|
||||
};
|
||||
|
||||
const ModMap = {
|
||||
'^': '^',
|
||||
'+': '+',
|
||||
'%': '%',
|
||||
'^^': '^'
|
||||
};
|
||||
|
||||
const AutoTypeEmitter = function (callback) {
|
||||
this.callback = callback;
|
||||
this.mod = {};
|
||||
this.pendingScript = [];
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
|
||||
if (enabled) {
|
||||
this.mod[ModMap[mod]] = true;
|
||||
} else {
|
||||
delete this.mod[ModMap[mod]];
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.text = function (text) {
|
||||
if (text) {
|
||||
this.pendingScript.push('text ' + this.modStr() + ' ' + text);
|
||||
}
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.key = function (key) {
|
||||
if (typeof key !== 'number') {
|
||||
if (!KeyMap[key]) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
key = KeyMap[key];
|
||||
}
|
||||
this.pendingScript.push('key ' + this.modStr() + key);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.copyPaste = function (text) {
|
||||
this.pendingScript.push('copypaste ' + text);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.wait = function (time) {
|
||||
this.pendingScript.push('wait ' + time);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.waitComplete = function () {
|
||||
if (this.pendingScript.length) {
|
||||
const script = this.pendingScript.join('\n');
|
||||
this.pendingScript.length = 0;
|
||||
this.runScript(script);
|
||||
} else {
|
||||
this.callback();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setDelay = function (delay) {
|
||||
this.delay = delay || 0;
|
||||
this.callback('Not implemented');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.modStr = function () {
|
||||
return Object.keys(this.mod).join('');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.runScript = function (script) {
|
||||
Launcher.spawn({
|
||||
cmd: AutoTypeNativeHelper.getHelperPath(),
|
||||
data: script,
|
||||
complete: this.callback
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeEmitter };
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const ForeMostAppScript =
|
||||
'tell application "System Events"\n' +
|
||||
' set frontAppName to name of first process whose frontmost is true\n' +
|
||||
' set frontAppId to id of first process whose frontmost is true\n' +
|
||||
'end tell\n' +
|
||||
'"" & frontAppId & " " & frontAppName';
|
||||
const ChromeScript =
|
||||
'tell application "{}" to set appUrl to URL of active tab of front window\n' +
|
||||
'tell application "{}" to set appTitle to title of active tab of front window\n' +
|
||||
'return appUrl & "\n" & appTitle';
|
||||
const SafariScript =
|
||||
'tell application "{}" to set appUrl to URL of front document\n' +
|
||||
'tell application "{}" to set appTitle to name of front document\n' +
|
||||
'return appUrl & "\n" & appTitle';
|
||||
const OtherAppsScript =
|
||||
'tell application "System Events"\n' +
|
||||
' tell process "{}"\n' +
|
||||
' tell (1st window whose value of attribute "AXMain" is true)\n' +
|
||||
' set windowTitle to value of attribute "AXTitle"\n' +
|
||||
' end tell\n' +
|
||||
' end tell\n' +
|
||||
'end tell';
|
||||
|
||||
const AutoTypeHelper = function () {};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowInfo = function (callback) {
|
||||
AutoTypeHelper.exec(ForeMostAppScript, (err, out) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
const output = out.trim();
|
||||
const spaceIx = output.indexOf(' ');
|
||||
let id = '',
|
||||
appName = '';
|
||||
if (spaceIx >= 0) {
|
||||
id = output.substr(0, spaceIx);
|
||||
appName = output.substr(spaceIx + 1).trim();
|
||||
}
|
||||
// getting urls and titles from Chrome or Safari:
|
||||
// - will suit in 90% cases
|
||||
// - does not require assistive access
|
||||
// - allows to get url
|
||||
if (['Google Chrome', 'Chromium', 'Google Chrome Canary'].indexOf(appName) >= 0) {
|
||||
AutoTypeHelper.exec(ChromeScript.replace(/\{}/g, appName), (err, out) => {
|
||||
if (err) {
|
||||
return callback(err, { id });
|
||||
}
|
||||
const parts = out.split('\n');
|
||||
return callback(null, {
|
||||
id,
|
||||
url: parts[0].trim(),
|
||||
title: (parts[1] || '').trim()
|
||||
});
|
||||
});
|
||||
} else if (['Safari', 'Webkit'].indexOf(appName) >= 0) {
|
||||
AutoTypeHelper.exec(SafariScript.replace(/\{}/g, appName), (err, out) => {
|
||||
if (err) {
|
||||
return callback(err, { id });
|
||||
}
|
||||
const parts = out.split('\n');
|
||||
return callback(null, {
|
||||
id,
|
||||
url: parts[0].trim(),
|
||||
title: (parts[1] || '').trim()
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// special cases are not available. this method may ask the user about assistive access
|
||||
AutoTypeHelper.exec(OtherAppsScript.replace(/\{}/g, appName), (err, out) => {
|
||||
if (err) {
|
||||
return callback(err, { id });
|
||||
}
|
||||
return callback(null, {
|
||||
id,
|
||||
title: out.trim()
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
AutoTypeHelper.exec = function (script, callback) {
|
||||
Launcher.spawn({
|
||||
cmd: 'osascript',
|
||||
args: ['-e', script],
|
||||
complete: callback
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeHelper };
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const AutoTypeHelper = function () {};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowInfo = function (callback) {
|
||||
Launcher.spawn({
|
||||
cmd: 'xdotool',
|
||||
args: ['getactivewindow', 'getwindowname', 'getactivewindow'],
|
||||
complete(err, out) {
|
||||
let windowInfo;
|
||||
if (out) {
|
||||
const [title, id] = out.trim().split('\n');
|
||||
windowInfo = {
|
||||
id,
|
||||
title
|
||||
};
|
||||
}
|
||||
return callback(err, windowInfo);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeHelper };
|
||||
@@ -1,25 +0,0 @@
|
||||
import { AutoTypeNativeHelper } from 'auto-type/helper/auto-type-native-helper';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const AutoTypeHelper = function () {};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowInfo = function (callback) {
|
||||
Launcher.spawn({
|
||||
cmd: AutoTypeNativeHelper.getHelperPath(),
|
||||
args: ['--window-info'],
|
||||
complete(err, out) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
const [id, title, url] = out.trim().split('\n');
|
||||
const windowInfo = {
|
||||
id,
|
||||
title,
|
||||
url
|
||||
};
|
||||
return callback(null, windowInfo);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export { AutoTypeHelper };
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Logger } from 'util/logger';
|
||||
|
||||
const logger = new Logger('auto-type');
|
||||
|
||||
const AutoTypeNativeHelper = {
|
||||
getHelperPath() {
|
||||
if (this._helperPath) {
|
||||
return this._helperPath;
|
||||
}
|
||||
const ext = process.platform === 'win32' ? '.exe' : '';
|
||||
const helperRelPath = `helper/${process.platform}/KeeWebHelper${ext}`;
|
||||
const helperPath = Launcher.getAppPath(helperRelPath);
|
||||
Launcher.ensureRunnable(helperPath);
|
||||
logger.debug('Using auto-type helper', helperPath);
|
||||
this._helperPath = helperPath;
|
||||
return helperPath;
|
||||
}
|
||||
};
|
||||
|
||||
export { AutoTypeNativeHelper };
|
||||
@@ -1,25 +1,25 @@
|
||||
import { Events } from 'framework/events';
|
||||
import { AutoTypeFilter } from 'auto-type/auto-type-filter';
|
||||
import { AutoTypeHelperFactory } from 'auto-type/auto-type-helper-factory';
|
||||
import { AutoTypeHelper } from 'auto-type/auto-type-helper';
|
||||
import { AutoTypeParser } from 'auto-type/auto-type-parser';
|
||||
import { SelectEntryFilter } from 'comp/app/select-entry-filter';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Features } from 'util/features';
|
||||
import { Alerts } from 'comp/ui/alerts';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { AppModel } from 'models/app-model';
|
||||
import { Locale } from 'util/locale';
|
||||
import { Logger } from 'util/logger';
|
||||
import { AutoTypeSelectView } from 'views/auto-type/auto-type-select-view';
|
||||
import { Links } from 'const/links';
|
||||
import { SelectEntryView } from 'views/select/select-entry-view';
|
||||
|
||||
const logger = new Logger('auto-type');
|
||||
const clearTextAutoTypeLog = !!localStorage.debugAutoType;
|
||||
|
||||
const AutoType = {
|
||||
helper: AutoTypeHelperFactory.create(),
|
||||
enabled: !!(Launcher && Launcher.autoTypeSupported),
|
||||
supportsEventsWithWindowId: !!(Launcher && Launcher.platform() === 'linux'),
|
||||
selectEntryView: false,
|
||||
pendingEvent: null,
|
||||
running: false,
|
||||
|
||||
init() {
|
||||
@@ -27,10 +27,6 @@ const AutoType = {
|
||||
return;
|
||||
}
|
||||
Events.on('auto-type', (e) => this.handleEvent(e));
|
||||
Events.on('main-window-blur', (e) => this.mainWindowBlur(e));
|
||||
Events.on('main-window-focus', (e) => this.mainWindowFocus(e));
|
||||
Events.on('main-window-will-close', (e) => this.mainWindowWillClose(e));
|
||||
Events.on('closed-open-view', (e) => this.processPendingEvent(e));
|
||||
},
|
||||
|
||||
handleEvent(e) {
|
||||
@@ -64,9 +60,16 @@ const AutoType = {
|
||||
runAndHandleResult(result, windowId) {
|
||||
this.run(result, windowId, (err) => {
|
||||
if (err) {
|
||||
let body = Locale.autoTypeErrorGeneric.replace('{}', err.message || err.toString());
|
||||
let link;
|
||||
if (err.keyPressFailed && Features.isMac) {
|
||||
body = Locale.autoTypeErrorAccessibilityMacOS;
|
||||
link = Links.AutoTypeMacOS;
|
||||
}
|
||||
Alerts.error({
|
||||
header: Locale.autoTypeError,
|
||||
body: Locale.autoTypeErrorGeneric.replace('{}', err.toString())
|
||||
body,
|
||||
link
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -161,15 +164,14 @@ const AutoType = {
|
||||
|
||||
getActiveWindowInfo(callback) {
|
||||
logger.debug('Getting window info');
|
||||
return this.helper.getActiveWindowInfo((err, windowInfo) => {
|
||||
const helper = new AutoTypeHelper();
|
||||
return helper.getActiveWindowInfo((err, windowInfo) => {
|
||||
if (err) {
|
||||
logger.error('Error getting window info', err);
|
||||
} else {
|
||||
if (!windowInfo.url) {
|
||||
// try to find a URL in the title
|
||||
const urlMatcher = new RegExp(
|
||||
'https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,4}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)'
|
||||
);
|
||||
const urlMatcher = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\\+.~#?&\/=]*)/;
|
||||
const urlMatches = urlMatcher.exec(windowInfo.title);
|
||||
windowInfo.url = urlMatches && urlMatches.length > 0 ? urlMatches[0] : null;
|
||||
}
|
||||
@@ -209,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);
|
||||
@@ -235,15 +254,38 @@ 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');
|
||||
this.selectEntryView.remove();
|
||||
this.selectEntryView = null;
|
||||
this.hideWindow(() => {
|
||||
if (result) {
|
||||
if (result?.entry) {
|
||||
this.activeWindowMatches(evt.windowInfo, (matches, activeWindowInfo) => {
|
||||
if (matches) {
|
||||
this.runAndHandleResult(result, evt.windowInfo.id);
|
||||
@@ -253,55 +295,17 @@ const AutoType = {
|
||||
});
|
||||
});
|
||||
this.selectEntryView.render();
|
||||
this.selectEntryView.on('show-open-files', () => {
|
||||
this.selectEntryView.on('show-open-files', async () => {
|
||||
this.selectEntryView.hide();
|
||||
Events.emit('open-file');
|
||||
});
|
||||
},
|
||||
|
||||
mainWindowBlur() {
|
||||
this.mainWindowBlurTimer = setTimeout(() => {
|
||||
// macOS emits focus-blur-focus event in a row when triggering auto-type from minimized state
|
||||
delete this.mainWindowBlurTimer;
|
||||
this.resetPendingEvent();
|
||||
if (this.selectEntryView) {
|
||||
try {
|
||||
await AppModel.instance.unlockAnyFile('autoTypeUnlockMessage');
|
||||
} catch {
|
||||
this.selectEntryView.emit('result', undefined);
|
||||
return;
|
||||
}
|
||||
}, Timeouts.AutoTypeWindowFocusAfterBlur);
|
||||
},
|
||||
|
||||
mainWindowFocus() {
|
||||
if (this.mainWindowBlurTimer) {
|
||||
clearTimeout(this.mainWindowBlurTimer);
|
||||
this.mainWindowBlurTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
mainWindowWillClose() {
|
||||
this.resetPendingEvent();
|
||||
if (this.selectEntryView) {
|
||||
this.selectEntryView.emit('result', undefined);
|
||||
}
|
||||
},
|
||||
|
||||
resetPendingEvent() {
|
||||
if (this.pendingEvent) {
|
||||
this.pendingEvent = null;
|
||||
logger.debug('auto-type event canceled');
|
||||
}
|
||||
},
|
||||
|
||||
processPendingEvent() {
|
||||
if (this.selectEntryView) {
|
||||
this.selectEntryView.show();
|
||||
}
|
||||
if (!this.pendingEvent) {
|
||||
return;
|
||||
}
|
||||
logger.debug('processing pending auto-type event');
|
||||
const evt = this.pendingEvent;
|
||||
this.pendingEvent = null;
|
||||
this.processEventWithFilter(evt);
|
||||
this.selectEntryView.render();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Collection } from 'framework/collection';
|
||||
import { ExternalEntryModel } from 'models/external/external-entry-model';
|
||||
|
||||
class ExternalEntryCollection extends Collection {
|
||||
static model = ExternalEntryModel;
|
||||
}
|
||||
|
||||
export { ExternalEntryCollection };
|
||||
@@ -58,6 +58,7 @@ const AppRightsChecker = {
|
||||
runInstaller() {
|
||||
Launcher.spawn({
|
||||
cmd: this.AppPath + '/Contents/Installer/KeeWeb Installer.app/Contents/MacOS/applet',
|
||||
args: ['--install'],
|
||||
complete: () => {
|
||||
this.needRunInstaller((needRun) => {
|
||||
if (this.alert && !needRun) {
|
||||
|
||||
@@ -150,7 +150,7 @@ const ChalRespCalculator = {
|
||||
header: Locale.yubiKeyNoKeyHeader,
|
||||
body: Locale.yubiKeyNoKeyBody.replace('{}', serial),
|
||||
buttons: [Alerts.buttons.cancel],
|
||||
iconSvg: 'usb-token',
|
||||
icon: 'usb-token',
|
||||
cancel: () => {
|
||||
logger.info('No key alert closed');
|
||||
|
||||
@@ -173,7 +173,7 @@ const ChalRespCalculator = {
|
||||
header: Locale.yubiKeyTouchRequestedHeader,
|
||||
body: Locale.yubiKeyTouchRequestedBody.replace('{}', serial),
|
||||
buttons: [Alerts.buttons.cancel],
|
||||
iconSvg: 'usb-token',
|
||||
icon: 'usb-token',
|
||||
cancel: () => {
|
||||
logger.info('Touch alert closed');
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { RuntimeDataModel } from 'models/runtime-data-model';
|
||||
|
||||
const ExportApi = {
|
||||
settings: {
|
||||
@@ -11,6 +12,17 @@ const ExportApi = {
|
||||
del(key) {
|
||||
delete AppSettingsModel[key];
|
||||
}
|
||||
},
|
||||
runtimeData: {
|
||||
get(key) {
|
||||
return key ? RuntimeDataModel[key] : { ...RuntimeDataModel };
|
||||
},
|
||||
set(key, value) {
|
||||
RuntimeDataModel[key] = value;
|
||||
},
|
||||
del(key) {
|
||||
delete RuntimeDataModel[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,18 @@ const GeneratorPresets = {
|
||||
};
|
||||
},
|
||||
|
||||
get browserExtensionPreset() {
|
||||
return {
|
||||
name: 'BrowserExtension',
|
||||
length: 20,
|
||||
upper: true,
|
||||
lower: true,
|
||||
special: true,
|
||||
brackets: true,
|
||||
ambiguous: true
|
||||
};
|
||||
},
|
||||
|
||||
get builtIn() {
|
||||
return [
|
||||
this.defaultPreset,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Logger } from 'util/logger';
|
||||
|
||||
const logger = new Logger('online-password-checker');
|
||||
|
||||
const exposedPasswords = {};
|
||||
|
||||
function checkIfPasswordIsExposedOnline(password) {
|
||||
if (!password || !password.isProtected || !password.byteLength) {
|
||||
return false;
|
||||
}
|
||||
const saltedValue = password.saltedValue();
|
||||
const cached = exposedPasswords[saltedValue];
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
const passwordBytes = password.getBinary();
|
||||
return crypto.subtle
|
||||
.digest({ name: 'SHA-1' }, passwordBytes)
|
||||
.then((sha1) => {
|
||||
kdbxweb.ByteUtils.zeroBuffer(passwordBytes);
|
||||
sha1 = kdbxweb.ByteUtils.bytesToHex(sha1).toUpperCase();
|
||||
const shaFirst = sha1.substr(0, 5);
|
||||
return fetch(`https://api.pwnedpasswords.com/range/${shaFirst}`)
|
||||
.then((response) => response.text())
|
||||
.then((response) => {
|
||||
const isPresent = response.includes(sha1.substr(5));
|
||||
exposedPasswords[saltedValue] = isPresent;
|
||||
return isPresent;
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error('Error checking password online', e);
|
||||
});
|
||||
}
|
||||
|
||||
export { checkIfPasswordIsExposedOnline };
|
||||
@@ -0,0 +1,99 @@
|
||||
import { SearchResultCollection } from 'collections/search-result-collection';
|
||||
import { Ranking } from 'util/data/ranking';
|
||||
|
||||
const urlPartsRegex = /^(\w+:\/\/)?(?:(?:www|wwws|secure)\.)?([^\/]+)\/?(.*)/;
|
||||
|
||||
class SelectEntryFilter {
|
||||
constructor(windowInfo, appModel, files, filterOptions) {
|
||||
this.title = windowInfo.title;
|
||||
this.useTitle = !!windowInfo.title && !windowInfo.url;
|
||||
this.url = windowInfo.url;
|
||||
this.useUrl = !!windowInfo.url;
|
||||
this.subdomains = true;
|
||||
this.text = '';
|
||||
this.appModel = appModel;
|
||||
this.files = files;
|
||||
this.filterOptions = filterOptions;
|
||||
}
|
||||
|
||||
getEntries() {
|
||||
const filter = {
|
||||
text: this.text,
|
||||
...this.filterOptions
|
||||
};
|
||||
let entries = this.appModel
|
||||
.getEntriesByFilter(filter, this.files)
|
||||
.map((e) => [e, this._getEntryRank(e)]);
|
||||
if (this.useUrl || this.useTitle) {
|
||||
entries = entries.filter((e) => e[1]);
|
||||
}
|
||||
entries = entries.sort((x, y) =>
|
||||
x[1] === y[1] ? x[0].title.localeCompare(y[0].title) : y[1] - x[1]
|
||||
);
|
||||
entries = entries.map((p) => p[0]);
|
||||
return new SearchResultCollection(entries, { comparator: 'none' });
|
||||
}
|
||||
|
||||
_getEntryRank(entry) {
|
||||
let titleRank = 0;
|
||||
let urlRank = 0;
|
||||
|
||||
if (this.useTitle && this.title && entry.title) {
|
||||
titleRank = Ranking.getStringRank(entry.title.toLowerCase(), this.title.toLowerCase());
|
||||
if (!titleRank) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.useUrl && this.url) {
|
||||
const searchUrlLower = this.url.toLowerCase();
|
||||
const searchUrlParts = urlPartsRegex.exec(searchUrlLower);
|
||||
|
||||
for (const url of entry.getAllUrls()) {
|
||||
const entryUrlParts = urlPartsRegex.exec(url.toLowerCase());
|
||||
if (entryUrlParts) {
|
||||
const [, scheme, domain, path] = entryUrlParts;
|
||||
const [, searchScheme, searchDomain, searchPath] = searchUrlParts;
|
||||
if (
|
||||
domain === searchDomain ||
|
||||
(this.subdomains && searchDomain.indexOf('.' + domain) > 0)
|
||||
) {
|
||||
if (domain === searchDomain) {
|
||||
urlRank += 20;
|
||||
} else {
|
||||
urlRank += 10;
|
||||
}
|
||||
if (path === searchPath) {
|
||||
urlRank += 10;
|
||||
} else if (path && searchPath) {
|
||||
if (path.lastIndexOf(searchPath, 0) === 0) {
|
||||
urlRank += 5;
|
||||
} else if (searchPath.lastIndexOf(path, 0) === 0) {
|
||||
urlRank += 3;
|
||||
}
|
||||
}
|
||||
if (scheme === searchScheme) {
|
||||
urlRank += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.searchText.includes(searchUrlLower)) {
|
||||
// the url is in some field; include it
|
||||
urlRank += 5;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.useTitle && !titleRank) {
|
||||
return 0;
|
||||
}
|
||||
if (this.useUrl && !urlRank) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return titleRank + urlRank;
|
||||
}
|
||||
}
|
||||
|
||||
export { SelectEntryFilter };
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Events } from 'framework/events';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { Transport } from 'comp/browser/transport';
|
||||
@@ -15,10 +16,9 @@ const Updater = {
|
||||
UpdateInterval: 1000 * 60 * 60 * 24,
|
||||
MinUpdateTimeout: 500,
|
||||
MinUpdateSize: 10000,
|
||||
UpdateCheckFiles: ['app.asar'],
|
||||
nextCheckTimeout: null,
|
||||
updateCheckDate: new Date(0),
|
||||
enabled: Launcher && Launcher.updaterEnabled(),
|
||||
enabled: Launcher?.updaterEnabled(),
|
||||
|
||||
getAutoUpdateType() {
|
||||
if (!this.enabled) {
|
||||
@@ -34,7 +34,7 @@ const Updater = {
|
||||
updateInProgress() {
|
||||
return (
|
||||
UpdateModel.status === 'checking' ||
|
||||
['downloading', 'extracting'].indexOf(UpdateModel.updateStatus) >= 0
|
||||
['downloading', 'extracting', 'updating'].indexOf(UpdateModel.updateStatus) >= 0
|
||||
);
|
||||
},
|
||||
|
||||
@@ -97,13 +97,12 @@ const Updater = {
|
||||
}
|
||||
logger.info('Checking for update...');
|
||||
Transport.httpGet({
|
||||
url: Links.Manifest,
|
||||
utf8: true,
|
||||
success: (data) => {
|
||||
url: Links.UpdateJson,
|
||||
json: true,
|
||||
success: (updateJson) => {
|
||||
const dt = new Date();
|
||||
const match = data.match(/#\s*(\d+\-\d+\-\d+):v([\d+\.\w]+)/);
|
||||
logger.info('Update check: ' + (match ? match[0] : 'unknown'));
|
||||
if (!match) {
|
||||
logger.info('Update check: ' + (updateJson.version || 'unknown'));
|
||||
if (!updateJson.version) {
|
||||
const errMsg = 'No version info found';
|
||||
UpdateModel.set({
|
||||
status: 'error',
|
||||
@@ -114,16 +113,15 @@ const Updater = {
|
||||
this.scheduleNextCheck();
|
||||
return;
|
||||
}
|
||||
const updateMinVersionMatch = data.match(/#\s*updmin:v([\d+\.\w]+)/);
|
||||
const prevLastVersion = UpdateModel.lastVersion;
|
||||
UpdateModel.set({
|
||||
status: 'ok',
|
||||
lastCheckDate: dt,
|
||||
lastSuccessCheckDate: dt,
|
||||
lastVersionReleaseDate: new Date(match[1]),
|
||||
lastVersion: match[2],
|
||||
lastVersionReleaseDate: new Date(updateJson.date),
|
||||
lastVersion: updateJson.version,
|
||||
lastCheckError: null,
|
||||
lastCheckUpdMin: updateMinVersionMatch ? updateMinVersionMatch[1] : null
|
||||
lastCheckUpdMin: updateJson.minVersion || null
|
||||
});
|
||||
UpdateModel.save();
|
||||
this.scheduleNextCheck();
|
||||
@@ -161,7 +159,7 @@ const Updater = {
|
||||
canAutoUpdate() {
|
||||
const minLauncherVersion = UpdateModel.lastCheckUpdMin;
|
||||
if (minLauncherVersion) {
|
||||
const cmp = SemVer.compareVersions(Launcher.version, minLauncherVersion);
|
||||
const cmp = SemVer.compareVersions(RuntimeInfo.version, minLauncherVersion);
|
||||
if (cmp < 0) {
|
||||
UpdateModel.set({ updateStatus: 'ready', updateManual: true });
|
||||
return false;
|
||||
@@ -182,28 +180,55 @@ const Updater = {
|
||||
}
|
||||
UpdateModel.set({ updateStatus: 'downloading', updateError: null });
|
||||
logger.info('Downloading update', ver);
|
||||
const updateAssetName = this.getUpdateAssetName(ver);
|
||||
if (!updateAssetName) {
|
||||
logger.error('Empty updater asset name for', Launcher.platform(), Launcher.arch());
|
||||
return;
|
||||
}
|
||||
const updateUrlBasePath = Links.UpdateBasePath.replace('{ver}', ver);
|
||||
const updateAssetUrl = updateUrlBasePath + updateAssetName;
|
||||
Transport.httpGet({
|
||||
url: Links.UpdateDesktop.replace('{ver}', ver),
|
||||
file: 'KeeWeb-' + ver + '.zip',
|
||||
cache: !startedByUser,
|
||||
success: (filePath) => {
|
||||
UpdateModel.set({ updateStatus: 'extracting' });
|
||||
logger.info('Extracting update file', this.UpdateCheckFiles, filePath);
|
||||
this.extractAppUpdate(filePath, (err) => {
|
||||
if (err) {
|
||||
logger.error('Error extracting update', err);
|
||||
url: updateAssetUrl,
|
||||
file: updateAssetName,
|
||||
cleanupOldFiles: true,
|
||||
cache: true,
|
||||
success: (assetFilePath) => {
|
||||
logger.info('Downloading update signatures');
|
||||
Transport.httpGet({
|
||||
url: updateUrlBasePath + 'Verify.sign.sha256',
|
||||
text: true,
|
||||
file: updateAssetName + '.sign',
|
||||
cleanupOldFiles: true,
|
||||
cache: true,
|
||||
success: (assetFileSignaturePath) => {
|
||||
this.verifySignature(assetFilePath, updateAssetName, (err, valid) => {
|
||||
if (err || !valid) {
|
||||
UpdateModel.set({
|
||||
updateStatus: 'error',
|
||||
updateError: err
|
||||
? 'Error verifying update signature'
|
||||
: 'Invalid update signature'
|
||||
});
|
||||
Launcher.deleteFile(assetFilePath);
|
||||
Launcher.deleteFile(assetFileSignaturePath);
|
||||
return;
|
||||
}
|
||||
logger.info('Update is ready', assetFilePath);
|
||||
UpdateModel.set({ updateStatus: 'ready', updateError: null });
|
||||
if (!startedByUser) {
|
||||
Events.emit('update-app');
|
||||
}
|
||||
if (typeof successCallback === 'function') {
|
||||
successCallback();
|
||||
}
|
||||
});
|
||||
},
|
||||
error(e) {
|
||||
logger.error('Error downloading update signatures', e);
|
||||
UpdateModel.set({
|
||||
updateStatus: 'error',
|
||||
updateError: 'Error extracting update'
|
||||
updateError: 'Error downloading update signatures'
|
||||
});
|
||||
} else {
|
||||
UpdateModel.set({ updateStatus: 'ready', updateError: null });
|
||||
if (!startedByUser) {
|
||||
Events.emit('update-app');
|
||||
}
|
||||
if (typeof successCallback === 'function') {
|
||||
successCallback();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -217,61 +242,64 @@ const Updater = {
|
||||
});
|
||||
},
|
||||
|
||||
extractAppUpdate(updateFile, cb) {
|
||||
const expectedFiles = this.UpdateCheckFiles;
|
||||
const appPath = Launcher.getUserDataPath();
|
||||
const StreamZip = Launcher.req('node-stream-zip');
|
||||
StreamZip.setFs(Launcher.req('original-fs'));
|
||||
const zip = new StreamZip({ file: updateFile, storeEntries: true });
|
||||
zip.on('error', cb);
|
||||
zip.on('ready', () => {
|
||||
const containsAll = expectedFiles.every((expFile) => {
|
||||
const entry = zip.entry(expFile);
|
||||
return entry && entry.isFile;
|
||||
verifySignature(assetFilePath, assetName, callback) {
|
||||
logger.info('Verifying update signature', assetName);
|
||||
const fs = Launcher.req('fs');
|
||||
const signaturesTxt = fs.readFileSync(assetFilePath + '.sign', 'utf8');
|
||||
const assetSignatureLine = signaturesTxt
|
||||
.split('\n')
|
||||
.find((line) => line.endsWith(assetName));
|
||||
if (!assetSignatureLine) {
|
||||
logger.error('Signature not found for asset', assetName);
|
||||
callback('Asset signature not found');
|
||||
return;
|
||||
}
|
||||
const signature = kdbxweb.ByteUtils.hexToBytes(assetSignatureLine.split(' ')[0]);
|
||||
const fileBytes = fs.readFileSync(assetFilePath);
|
||||
SignatureVerifier.verify(fileBytes, signature)
|
||||
.catch((e) => {
|
||||
logger.error('Error verifying signature', e);
|
||||
callback('Error verifying signature');
|
||||
})
|
||||
.then((valid) => {
|
||||
logger.info(`Update asset signature is ${valid ? 'valid' : 'invalid'}`);
|
||||
callback(undefined, valid);
|
||||
});
|
||||
if (!containsAll) {
|
||||
return cb('Bad archive');
|
||||
}
|
||||
this.validateArchiveSignature(updateFile, zip)
|
||||
.then(() => {
|
||||
zip.extract(null, appPath, (err) => {
|
||||
zip.close();
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
Launcher.deleteFile(updateFile);
|
||||
cb();
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
return cb('Invalid archive: ' + e);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
validateArchiveSignature(archivePath, zip) {
|
||||
if (!zip.comment) {
|
||||
return Promise.reject('No comment in ZIP');
|
||||
getUpdateAssetName(ver) {
|
||||
const platform = Launcher.platform();
|
||||
const arch = Launcher.arch();
|
||||
switch (platform) {
|
||||
case 'win32':
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
return `KeeWeb-${ver}.win.x64.exe`;
|
||||
case 'ia32':
|
||||
return `KeeWeb-${ver}.win.ia32.exe`;
|
||||
case 'arm64':
|
||||
return `KeeWeb-${ver}.win.arm64.exe`;
|
||||
}
|
||||
break;
|
||||
case 'darwin':
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
return `KeeWeb-${ver}.mac.x64.dmg`;
|
||||
case 'arm64':
|
||||
return `KeeWeb-${ver}.mac.arm64.dmg`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (zip.comment.length !== 512) {
|
||||
return Promise.reject('Bad comment length in ZIP: ' + zip.comment.length);
|
||||
}
|
||||
try {
|
||||
const zipFileData = Launcher.req('fs').readFileSync(archivePath);
|
||||
const dataToVerify = zipFileData.slice(0, zip.centralDirectory.headerOffset + 22);
|
||||
const signature = window.Buffer.from(zip.comment, 'hex');
|
||||
return SignatureVerifier.verify(dataToVerify, signature)
|
||||
.catch(() => {
|
||||
throw new Error('Error verifying signature');
|
||||
})
|
||||
.then((isValid) => {
|
||||
if (!isValid) {
|
||||
throw new Error('Invalid signature');
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
return Promise.reject(err.toString());
|
||||
return undefined;
|
||||
},
|
||||
|
||||
installAndRestart() {
|
||||
if (!Launcher) {
|
||||
return;
|
||||
}
|
||||
const updateAssetName = this.getUpdateAssetName(UpdateModel.lastVersion);
|
||||
const updateFilePath = Transport.cacheFilePath(updateAssetName);
|
||||
Launcher.requestRestartAndUpdate(updateFilePath);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ const YubiKey = {
|
||||
|
||||
this.process = Launcher.spawn({
|
||||
cmd: this.cmd(),
|
||||
args: ['-d', serial, 'oath', 'code'],
|
||||
args: ['-d', serial, 'oath', 'accounts', 'code'],
|
||||
noStdOutLogging: true,
|
||||
throwOnStdErr: true,
|
||||
complete: (err, stdout) => {
|
||||
@@ -241,7 +241,7 @@ const YubiKey = {
|
||||
getOtp(serial, entry, callback) {
|
||||
return Launcher.spawn({
|
||||
cmd: this.cmd(),
|
||||
args: ['-d', serial, 'oath', 'code', '--single', entry],
|
||||
args: ['-d', serial, 'oath', 'accounts', 'code', '--single', entry],
|
||||
noStdOutLogging: true,
|
||||
complete: (err, stdout) => {
|
||||
if (err) {
|
||||
|
||||
@@ -4,7 +4,8 @@ const FeatureTester = {
|
||||
test() {
|
||||
return Promise.resolve()
|
||||
.then(() => this.checkWebAssembly())
|
||||
.then(() => this.checkLocalStorage());
|
||||
.then(() => this.checkLocalStorage())
|
||||
.then(() => this.checkWebCrypto());
|
||||
},
|
||||
|
||||
checkWebAssembly() {
|
||||
@@ -28,6 +29,12 @@ const FeatureTester = {
|
||||
} catch (e) {
|
||||
throw 'LocalStorage is not supported';
|
||||
}
|
||||
},
|
||||
|
||||
checkWebCrypto() {
|
||||
if (!global.crypto.subtle) {
|
||||
throw 'WebCrypto is not supported';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ const IdleTracker = {
|
||||
const idleMinutes = (Date.now() - this.actionTime) / 1000 / 60;
|
||||
const maxIdleMinutes = AppSettingsModel.idleMinutes;
|
||||
if (maxIdleMinutes && idleMinutes > maxIdleMinutes) {
|
||||
Events.emit('before-user-idle');
|
||||
Events.emit('user-idle');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -45,12 +45,28 @@ const PopupNotifier = {
|
||||
Timeouts.CheckWindowClosed
|
||||
);
|
||||
} else {
|
||||
const loc = PopupNotifier.tryGetLocationSearch(win);
|
||||
if (loc) {
|
||||
try {
|
||||
win.close();
|
||||
} catch {}
|
||||
PopupNotifier.triggerClosed(win, loc);
|
||||
return;
|
||||
}
|
||||
PopupNotifier.deferCheckClosed(win);
|
||||
}
|
||||
},
|
||||
|
||||
triggerClosed(win) {
|
||||
Events.emit('popup-closed', win);
|
||||
tryGetLocationSearch(win) {
|
||||
try {
|
||||
if (win.location.host === location.host) {
|
||||
return win.location.search;
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
|
||||
triggerClosed(window, locationSearch) {
|
||||
Events.emit('popup-closed', { window, locationSearch });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
|
||||
const SecureInput = function () {
|
||||
this.el = null;
|
||||
@@ -79,11 +79,12 @@ 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++) {
|
||||
ch = String.fromCharCode(pseudoValue.charCodeAt(i) ^ salt[i]);
|
||||
const pseudoCharCode = pseudoValue.charCodeAt(i);
|
||||
ch = String.fromCharCode(salt[i] ^ pseudoCharCode);
|
||||
bytes = kdbxweb.ByteUtils.stringToBytes(ch);
|
||||
for (let j = 0; j < bytes.length; j++) {
|
||||
valueBytes[byteLength] = bytes[j] ^ saltBytes[byteLength];
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Events } from 'framework/events';
|
||||
|
||||
const ThemeWatcher = {
|
||||
dark: false,
|
||||
|
||||
init() {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
if (mediaQuery && mediaQuery.addEventListener) {
|
||||
mediaQuery.addEventListener('change', (e) => {
|
||||
const dark = e.matches;
|
||||
this.dark = dark;
|
||||
Events.emit('dark-mode-changed', { dark });
|
||||
});
|
||||
}
|
||||
this.dark = !!mediaQuery.matches;
|
||||
}
|
||||
};
|
||||
|
||||
export { ThemeWatcher };
|
||||
@@ -1,15 +1,33 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Logger } from 'util/logger';
|
||||
import { noop } from 'util/fn';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
|
||||
const logger = new Logger('transport');
|
||||
|
||||
const Transport = {
|
||||
cacheFilePath(fileName) {
|
||||
return Launcher.getTempPath(fileName);
|
||||
},
|
||||
|
||||
httpGet(config) {
|
||||
let tmpFile;
|
||||
const fs = Launcher.req('fs');
|
||||
if (config.file) {
|
||||
tmpFile = Launcher.getTempPath(config.file);
|
||||
const baseTempPath = Launcher.getTempPath();
|
||||
if (config.cleanupOldFiles) {
|
||||
const allFiles = fs.readdirSync(baseTempPath);
|
||||
for (const file of allFiles) {
|
||||
if (
|
||||
file !== config.file &&
|
||||
StringFormat.replaceVersion(file, '0') ===
|
||||
StringFormat.replaceVersion(config.file, '0')
|
||||
) {
|
||||
fs.unlinkSync(Launcher.joinPath(baseTempPath, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
tmpFile = Launcher.joinPath(baseTempPath, config.file);
|
||||
if (fs.existsSync(tmpFile)) {
|
||||
try {
|
||||
if (config.cache && fs.statSync(tmpFile).size > 0) {
|
||||
@@ -62,9 +80,16 @@ const Transport = {
|
||||
});
|
||||
res.on('end', () => {
|
||||
data = window.Buffer.concat(data);
|
||||
if (config.utf8) {
|
||||
if (config.text || config.json) {
|
||||
data = data.toString('utf8');
|
||||
}
|
||||
if (config.json) {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
config.error('Error parsing JSON: ' + e.message);
|
||||
}
|
||||
}
|
||||
config.success(data);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Logger } from 'util/logger';
|
||||
import { ProtocolImpl } from './protocol-impl';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { Features } from 'util/features';
|
||||
|
||||
const WebConnectionInfo = {
|
||||
connectionId: 1,
|
||||
extensionName: 'KeeWeb Connect',
|
||||
supportsNotifications: true
|
||||
};
|
||||
|
||||
const SupportedExtensions = [
|
||||
{ alias: 'KWC', name: 'KeeWeb Connect' },
|
||||
{ alias: 'KPXC', name: 'KeePassXC-Browser' }
|
||||
];
|
||||
const SupportedBrowsers = ['Chrome', 'Firefox', 'Edge', 'Other'];
|
||||
if (Features.isMac) {
|
||||
SupportedBrowsers.unshift('Safari');
|
||||
}
|
||||
|
||||
const logger = new Logger('browser-extension-connector');
|
||||
if (!localStorage.debugBrowserExtension) {
|
||||
logger.level = Logger.Level.Info;
|
||||
}
|
||||
|
||||
const connections = new Map();
|
||||
const pendingBrowserMessages = [];
|
||||
let processingBrowserMessage = false;
|
||||
|
||||
const BrowserExtensionConnector = {
|
||||
started: false,
|
||||
logger,
|
||||
|
||||
init(appModel) {
|
||||
const sendEvent = this.sendEvent.bind(this);
|
||||
ProtocolImpl.init({ appModel, logger, sendEvent });
|
||||
|
||||
this.browserWindowMessage = this.browserWindowMessage.bind(this);
|
||||
|
||||
if (Launcher) {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
|
||||
ipcRenderer.on('browserExtensionSocketConnected', (e, socketId, connectionInfo) =>
|
||||
this.socketConnected(socketId, connectionInfo)
|
||||
);
|
||||
ipcRenderer.on('browserExtensionSocketClosed', (e, socketId) =>
|
||||
this.socketClosed(socketId)
|
||||
);
|
||||
ipcRenderer.on('browserExtensionSocketRequest', (e, socketId, request) =>
|
||||
this.socketRequest(socketId, request)
|
||||
);
|
||||
|
||||
AppSettingsModel.on('change', () => this.appSettingsChanged());
|
||||
}
|
||||
|
||||
if (this.isEnabled()) {
|
||||
this.start();
|
||||
}
|
||||
},
|
||||
|
||||
start() {
|
||||
if (Launcher) {
|
||||
this.startDesktopAppListener();
|
||||
} else {
|
||||
this.startWebMessageListener();
|
||||
}
|
||||
|
||||
this.started = true;
|
||||
},
|
||||
|
||||
stop() {
|
||||
if (Launcher) {
|
||||
this.stopDesktopAppListener();
|
||||
} else {
|
||||
this.stopWebMessageListener();
|
||||
}
|
||||
|
||||
ProtocolImpl.cleanup();
|
||||
connections.clear();
|
||||
|
||||
this.started = false;
|
||||
},
|
||||
|
||||
appSettingsChanged() {
|
||||
if (this.isEnabled()) {
|
||||
if (!this.started) {
|
||||
this.start();
|
||||
}
|
||||
} else if (this.started) {
|
||||
this.stop();
|
||||
}
|
||||
},
|
||||
|
||||
isEnabled() {
|
||||
if (!Launcher) {
|
||||
return true;
|
||||
}
|
||||
for (const ext of SupportedExtensions) {
|
||||
for (const browser of SupportedBrowsers) {
|
||||
if (AppSettingsModel[`extensionEnabled${ext.alias}${browser}`]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
startWebMessageListener() {
|
||||
window.addEventListener('message', this.browserWindowMessage);
|
||||
logger.info('Started');
|
||||
},
|
||||
|
||||
stopWebMessageListener() {
|
||||
window.removeEventListener('message', this.browserWindowMessage);
|
||||
},
|
||||
|
||||
enable(browser, extension, enabled) {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorEnable', browser, extension, enabled);
|
||||
},
|
||||
|
||||
async startDesktopAppListener() {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorStart', {
|
||||
appleTeamId: RuntimeInfo.appleTeamId
|
||||
});
|
||||
},
|
||||
|
||||
stopDesktopAppListener() {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorStop');
|
||||
},
|
||||
|
||||
browserWindowMessage(e) {
|
||||
if (e.origin !== location.origin) {
|
||||
return;
|
||||
}
|
||||
if (e.source !== window) {
|
||||
return;
|
||||
}
|
||||
if (e?.data?.kwConnect !== 'request') {
|
||||
return;
|
||||
}
|
||||
logger.debug('Extension -> KeeWeb', e.data);
|
||||
pendingBrowserMessages.push(e.data);
|
||||
this.processBrowserMessages();
|
||||
},
|
||||
|
||||
async processBrowserMessages() {
|
||||
if (!pendingBrowserMessages.length || processingBrowserMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connections.has(WebConnectionInfo.connectionId)) {
|
||||
connections.set(WebConnectionInfo.connectionId, WebConnectionInfo);
|
||||
}
|
||||
|
||||
processingBrowserMessage = true;
|
||||
|
||||
const request = pendingBrowserMessages.shift();
|
||||
|
||||
const response = await ProtocolImpl.handleRequest(request, WebConnectionInfo);
|
||||
|
||||
processingBrowserMessage = false;
|
||||
|
||||
if (response) {
|
||||
this.sendWebResponse(response);
|
||||
}
|
||||
|
||||
this.processBrowserMessages();
|
||||
},
|
||||
|
||||
sendWebResponse(response) {
|
||||
logger.debug('KeeWeb -> Extension', response);
|
||||
response.kwConnect = 'response';
|
||||
postMessage(response, window.location.origin);
|
||||
},
|
||||
|
||||
sendSocketEvent(data) {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorSocketEvent', data);
|
||||
},
|
||||
|
||||
sendSocketResult(socketId, data) {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorSocketResult', socketId, data);
|
||||
},
|
||||
|
||||
sendEvent(data) {
|
||||
if (!this.isEnabled() || !connections.size) {
|
||||
return;
|
||||
}
|
||||
if (Launcher) {
|
||||
this.sendSocketEvent(data);
|
||||
} else {
|
||||
this.sendWebResponse(data);
|
||||
}
|
||||
},
|
||||
|
||||
socketConnected(socketId, connectionInfo) {
|
||||
connections.set(socketId, connectionInfo);
|
||||
},
|
||||
|
||||
socketClosed(socketId) {
|
||||
connections.delete(socketId);
|
||||
ProtocolImpl.deleteConnection(socketId);
|
||||
},
|
||||
|
||||
async socketRequest(socketId, request) {
|
||||
let result;
|
||||
|
||||
const connectionInfo = connections.get(socketId);
|
||||
if (connectionInfo) {
|
||||
result = await ProtocolImpl.handleRequest(request, connectionInfo);
|
||||
} else {
|
||||
const message = `Connection not found: ${socketId}`;
|
||||
result = ProtocolImpl.errorToResponse({ message }, request);
|
||||
}
|
||||
|
||||
this.sendSocketResult(socketId, result);
|
||||
},
|
||||
|
||||
get sessions() {
|
||||
return ProtocolImpl.sessions;
|
||||
},
|
||||
|
||||
terminateConnection(connectionId) {
|
||||
connectionId = +connectionId;
|
||||
if (Launcher) {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.invoke('browserExtensionConnectorCloseSocket', connectionId);
|
||||
} else {
|
||||
ProtocolImpl.deleteConnection(connectionId);
|
||||
}
|
||||
},
|
||||
|
||||
getClientPermissions(clientId) {
|
||||
return ProtocolImpl.getClientPermissions(clientId);
|
||||
},
|
||||
|
||||
setClientPermissions(clientId, permissions) {
|
||||
ProtocolImpl.setClientPermissions(clientId, permissions);
|
||||
}
|
||||
};
|
||||
|
||||
export { BrowserExtensionConnector, SupportedExtensions, SupportedBrowsers };
|
||||
@@ -0,0 +1,940 @@
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Events } from 'framework/events';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { box as tweetnaclBox } from 'tweetnacl';
|
||||
import { PasswordGenerator } from 'util/generators/password-generator';
|
||||
import { GeneratorPresets } from 'comp/app/generator-presets';
|
||||
import { Alerts } from 'comp/ui/alerts';
|
||||
import { Locale } from 'util/locale';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { KnownAppVersions } from 'const/known-app-versions';
|
||||
import { ExtensionConnectView } from 'views/extension/extension-connect-view';
|
||||
import { ExtensionCreateGroupView } from 'views/extension/extension-create-group-view';
|
||||
import { ExtensionSaveEntryView } from 'views/extension/extension-save-entry-view';
|
||||
import { RuntimeDataModel } from 'models/runtime-data-model';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
import { SelectEntryView } from 'views/select/select-entry-view';
|
||||
import { SelectEntryFieldView } from 'views/select/select-entry-field-view';
|
||||
import { SelectEntryFilter } from 'comp/app/select-entry-filter';
|
||||
|
||||
const KeeWebAssociationId = 'KeeWeb';
|
||||
const KeeWebHash = '398d9c782ec76ae9e9877c2321cbda2b31fc6d18ccf0fed5ca4bd746bab4d64a'; // sha256('KeeWeb')
|
||||
const ExtensionGroupIconId = 1;
|
||||
const DefaultExtensionGroupName = 'Browser';
|
||||
const ExtensionGroupNames = new Set(['KeePassXC-Browser Passwords', DefaultExtensionGroupName]);
|
||||
|
||||
const Errors = {
|
||||
noOpenFiles: {
|
||||
message: Locale.extensionErrorNoOpenFiles,
|
||||
code: '1'
|
||||
},
|
||||
userRejected: {
|
||||
message: Locale.extensionErrorUserRejected,
|
||||
code: '6'
|
||||
},
|
||||
noMatches: {
|
||||
message: Locale.extensionErrorNoMatches,
|
||||
code: '15'
|
||||
}
|
||||
};
|
||||
|
||||
const connectedClients = new Map();
|
||||
|
||||
let logger;
|
||||
let appModel;
|
||||
let sendEvent;
|
||||
|
||||
function setupListeners() {
|
||||
Events.on('file-opened', () => {
|
||||
sendEvent({ action: 'database-unlocked' });
|
||||
});
|
||||
Events.on('one-file-closed', () => {
|
||||
if (!appModel.files.hasOpenFiles()) {
|
||||
sendEvent({ action: 'database-locked' });
|
||||
}
|
||||
});
|
||||
Events.on('all-files-closed', () => {
|
||||
sendEvent({ action: 'database-locked' });
|
||||
});
|
||||
}
|
||||
|
||||
function incrementNonce(nonce) {
|
||||
// from libsodium/utils.c, like it is in KeePassXC
|
||||
let i = 0;
|
||||
let c = 1;
|
||||
for (; i < nonce.length; ++i) {
|
||||
c += nonce[i];
|
||||
nonce[i] = c;
|
||||
c >>= 8;
|
||||
}
|
||||
}
|
||||
|
||||
function getClient(request) {
|
||||
if (!request.clientID) {
|
||||
throw new Error('Empty clientID');
|
||||
}
|
||||
const client = connectedClients.get(request.clientID);
|
||||
if (!client) {
|
||||
throw new Error(`Client not connected: ${request.clientID}`);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
function decryptRequest(request) {
|
||||
const client = getClient(request);
|
||||
|
||||
if (!request.nonce) {
|
||||
throw new Error('Empty nonce');
|
||||
}
|
||||
if (!request.message) {
|
||||
throw new Error('Empty message');
|
||||
}
|
||||
|
||||
const nonce = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
|
||||
const message = kdbxweb.ByteUtils.base64ToBytes(request.message);
|
||||
|
||||
const data = tweetnaclBox.open(message, nonce, client.publicKey, client.keys.secretKey);
|
||||
if (!data) {
|
||||
throw new Error('Failed to decrypt data');
|
||||
}
|
||||
|
||||
const json = new TextDecoder().decode(data);
|
||||
const payload = JSON.parse(json);
|
||||
|
||||
logger.debug('Extension -> KeeWeb -> (decrypted)', payload);
|
||||
|
||||
if (!payload) {
|
||||
throw new Error('Empty request payload');
|
||||
}
|
||||
if (payload.action !== request.action) {
|
||||
throw new Error(`Bad action in decrypted payload`);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function encryptResponse(request, payload) {
|
||||
logger.debug('KeeWeb -> Extension (decrypted)', payload);
|
||||
|
||||
const nonceBytes = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
|
||||
incrementNonce(nonceBytes);
|
||||
const nonce = kdbxweb.ByteUtils.bytesToBase64(nonceBytes);
|
||||
|
||||
const client = getClient(request);
|
||||
|
||||
payload.nonce = nonce;
|
||||
|
||||
const json = JSON.stringify(payload);
|
||||
const data = new TextEncoder().encode(json);
|
||||
|
||||
const encrypted = tweetnaclBox(data, nonceBytes, client.publicKey, client.keys.secretKey);
|
||||
|
||||
const message = kdbxweb.ByteUtils.bytesToBase64(encrypted);
|
||||
|
||||
return {
|
||||
action: request.action,
|
||||
message,
|
||||
nonce
|
||||
};
|
||||
}
|
||||
|
||||
function makeError(def) {
|
||||
const e = new Error(def.message);
|
||||
e.code = def.code;
|
||||
return e;
|
||||
}
|
||||
|
||||
function ensureAtLeastOneFileIsOpen() {
|
||||
if (!appModel.files.hasOpenFiles()) {
|
||||
throw makeError(Errors.noOpenFiles);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkContentRequestPermissions(request) {
|
||||
if (!appModel.files.hasOpenFiles()) {
|
||||
if (AppSettingsModel.extensionFocusIfLocked) {
|
||||
try {
|
||||
focusKeeWeb();
|
||||
await appModel.unlockAnyFile(
|
||||
'extensionUnlockMessage',
|
||||
Timeouts.KeeWebConnectRequest
|
||||
);
|
||||
} catch {
|
||||
throw makeError(Errors.noOpenFiles);
|
||||
}
|
||||
} else {
|
||||
throw makeError(Errors.noOpenFiles);
|
||||
}
|
||||
}
|
||||
|
||||
const client = getClient(request);
|
||||
if (client.permissions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Alerts.alertDisplayed) {
|
||||
throw new Error(Locale.extensionErrorAlertDisplayed);
|
||||
}
|
||||
|
||||
focusKeeWeb();
|
||||
|
||||
const config = RuntimeDataModel.extensionConnectConfig;
|
||||
const files = appModel.files.map((f) => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
checked: !config || config.allFiles || config.files.includes(f.id)
|
||||
}));
|
||||
if (!files.some((f) => f.checked)) {
|
||||
for (const f of files) {
|
||||
f.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
const extensionConnectView = new ExtensionConnectView({
|
||||
extensionName: getHumanReadableExtensionName(client),
|
||||
identityVerified: !Launcher,
|
||||
files,
|
||||
allFiles: config?.allFiles ?? true,
|
||||
askGet: config?.askGet || 'multiple'
|
||||
});
|
||||
|
||||
try {
|
||||
await alertWithTimeout({
|
||||
header: Locale.extensionConnectHeader,
|
||||
icon: 'exchange-alt',
|
||||
buttons: [Alerts.buttons.allow, Alerts.buttons.deny],
|
||||
view: extensionConnectView,
|
||||
wide: true,
|
||||
opaque: true
|
||||
});
|
||||
} catch (e) {
|
||||
client.permissionsDenied = true;
|
||||
Events.emit('browser-extension-sessions-changed');
|
||||
throw e;
|
||||
}
|
||||
|
||||
RuntimeDataModel.extensionConnectConfig = extensionConnectView.config;
|
||||
client.permissions = extensionConnectView.config;
|
||||
Events.emit('browser-extension-sessions-changed');
|
||||
}
|
||||
|
||||
function alertWithTimeout(config) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let inactivityTimer = 0;
|
||||
|
||||
const alert = Alerts.alert({
|
||||
...config,
|
||||
enter: 'yes',
|
||||
esc: '',
|
||||
success: (res) => {
|
||||
clearTimeout(inactivityTimer);
|
||||
resolve(res);
|
||||
},
|
||||
cancel: () => {
|
||||
clearTimeout(inactivityTimer);
|
||||
reject(makeError(Errors.userRejected));
|
||||
}
|
||||
});
|
||||
|
||||
inactivityTimer = setTimeout(() => {
|
||||
alert.closeWithResult('');
|
||||
}, Timeouts.KeeWebConnectRequest);
|
||||
});
|
||||
}
|
||||
|
||||
function getAvailableFiles(request) {
|
||||
const client = getClient(request);
|
||||
if (!client.permissions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = appModel.files.filter(
|
||||
(file) =>
|
||||
file.active &&
|
||||
(client.permissions.allFiles || client.permissions.files.includes(file.id))
|
||||
);
|
||||
if (!files.length) {
|
||||
throw makeError(Errors.noOpenFiles);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function getVersion(request) {
|
||||
return isKeePassXcBrowser(request) ? KnownAppVersions.KeePassXC : RuntimeInfo.version;
|
||||
}
|
||||
|
||||
function isKeeWebConnect(request) {
|
||||
return getClient(request).connection.extensionName === 'KeeWeb Connect';
|
||||
}
|
||||
|
||||
function isKeePassXcBrowser(request) {
|
||||
return getClient(request).connection.extensionName === 'KeePassXC-Browser';
|
||||
}
|
||||
|
||||
function getHumanReadableExtensionName(client) {
|
||||
return client.connection.appName
|
||||
? `${client.connection.extensionName} (${client.connection.appName})`
|
||||
: client.connection.extensionName;
|
||||
}
|
||||
|
||||
function focusKeeWeb() {
|
||||
logger.debug('Focus KeeWeb');
|
||||
if (Launcher) {
|
||||
if (!Launcher.isAppFocused()) {
|
||||
Launcher.showMainWindow();
|
||||
}
|
||||
} else {
|
||||
sendEvent({ action: 'attention-required' });
|
||||
}
|
||||
}
|
||||
|
||||
async function findEntry(request, returnIfOneMatch, filterOptions) {
|
||||
const payload = decryptRequest(request);
|
||||
await checkContentRequestPermissions(request);
|
||||
|
||||
if (!payload.url) {
|
||||
throw new Error('Empty url');
|
||||
}
|
||||
|
||||
const files = getAvailableFiles(request);
|
||||
const client = getClient(request);
|
||||
|
||||
const filter = new SelectEntryFilter(
|
||||
{ url: payload.url, title: payload.title },
|
||||
appModel,
|
||||
files,
|
||||
filterOptions
|
||||
);
|
||||
filter.subdomains = false;
|
||||
|
||||
let entries = filter.getEntries();
|
||||
|
||||
filter.subdomains = true;
|
||||
|
||||
let entry;
|
||||
|
||||
if (entries.length) {
|
||||
if (entries.length === 1 && returnIfOneMatch && client.permissions.askGet === 'multiple') {
|
||||
entry = entries[0];
|
||||
}
|
||||
} else {
|
||||
entries = filter.getEntries();
|
||||
|
||||
if (!entries.length) {
|
||||
if (AppSettingsModel.extensionFocusIfEmpty) {
|
||||
filter.useUrl = false;
|
||||
if (filter.title && AppSettingsModel.autoTypeTitleFilterEnabled) {
|
||||
filter.useTitle = true;
|
||||
entries = filter.getEntries();
|
||||
if (!entries.length) {
|
||||
filter.useTitle = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw makeError(Errors.noMatches);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!entry) {
|
||||
const extName = getHumanReadableExtensionName(client);
|
||||
const topMessage = Locale.extensionSelectPasswordFor.replace('{}', extName);
|
||||
const selectEntryView = new SelectEntryView({ filter, topMessage });
|
||||
|
||||
focusKeeWeb();
|
||||
|
||||
const inactivityTimer = setTimeout(() => {
|
||||
selectEntryView.emit('result', undefined);
|
||||
}, Timeouts.KeeWebConnectRequest);
|
||||
|
||||
const result = await selectEntryView.showAndGetResult();
|
||||
|
||||
clearTimeout(inactivityTimer);
|
||||
|
||||
entry = result?.entry;
|
||||
if (!entry) {
|
||||
throw makeError(Errors.userRejected);
|
||||
}
|
||||
}
|
||||
|
||||
client.stats.passwordsRead++;
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
const ProtocolHandlers = {
|
||||
'ping'({ data }) {
|
||||
return { data };
|
||||
},
|
||||
|
||||
'change-public-keys'(request, connection) {
|
||||
let { publicKey, version, clientID: clientId } = request;
|
||||
|
||||
if (connectedClients.has(clientId)) {
|
||||
throw new Error('Changing keys is not allowed');
|
||||
}
|
||||
|
||||
if (!Launcher) {
|
||||
// on web there can be only one connected client
|
||||
connectedClients.clear();
|
||||
}
|
||||
|
||||
const keys = tweetnaclBox.keyPair();
|
||||
publicKey = kdbxweb.ByteUtils.base64ToBytes(publicKey);
|
||||
|
||||
const stats = {
|
||||
connectedDate: new Date(),
|
||||
passwordsRead: 0,
|
||||
passwordsWritten: 0
|
||||
};
|
||||
|
||||
connectedClients.set(clientId, { connection, publicKey, version, keys, stats });
|
||||
|
||||
Events.emit('browser-extension-sessions-changed');
|
||||
|
||||
logger.info('New client key created', clientId, version);
|
||||
|
||||
const nonceBytes = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
|
||||
incrementNonce(nonceBytes);
|
||||
const nonce = kdbxweb.ByteUtils.bytesToBase64(nonceBytes);
|
||||
|
||||
return {
|
||||
action: 'change-public-keys',
|
||||
version: getVersion(request),
|
||||
publicKey: kdbxweb.ByteUtils.bytesToBase64(keys.publicKey),
|
||||
nonce,
|
||||
success: 'true',
|
||||
...(isKeeWebConnect(request) ? { appName: 'KeeWeb' } : undefined)
|
||||
};
|
||||
},
|
||||
|
||||
async 'get-databasehash'(request) {
|
||||
decryptRequest(request);
|
||||
|
||||
if (request.triggerUnlock) {
|
||||
await checkContentRequestPermissions(request);
|
||||
} else {
|
||||
ensureAtLeastOneFileIsOpen();
|
||||
}
|
||||
|
||||
return encryptResponse(request, {
|
||||
hash: KeeWebHash,
|
||||
success: 'true',
|
||||
version: getVersion(request)
|
||||
});
|
||||
},
|
||||
|
||||
'generate-password'(request) {
|
||||
const password = PasswordGenerator.generate(GeneratorPresets.browserExtensionPreset);
|
||||
|
||||
return encryptResponse(request, {
|
||||
version: getVersion(request),
|
||||
success: 'true',
|
||||
entries: [{ password }]
|
||||
});
|
||||
},
|
||||
|
||||
'lock-database'(request) {
|
||||
decryptRequest(request);
|
||||
ensureAtLeastOneFileIsOpen();
|
||||
|
||||
Events.emit('lock-workspace');
|
||||
|
||||
if (Alerts.alertDisplayed) {
|
||||
focusKeeWeb();
|
||||
}
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request)
|
||||
});
|
||||
},
|
||||
|
||||
'associate'(request) {
|
||||
decryptRequest(request);
|
||||
ensureAtLeastOneFileIsOpen();
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
hash: KeeWebHash,
|
||||
id: KeeWebAssociationId
|
||||
});
|
||||
},
|
||||
|
||||
'test-associate'(request) {
|
||||
const payload = decryptRequest(request);
|
||||
// ensureAtLeastOneFileIsOpen();
|
||||
|
||||
if (payload.id !== KeeWebAssociationId) {
|
||||
throw makeError(Errors.noOpenFiles);
|
||||
}
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
hash: KeeWebHash,
|
||||
id: payload.id
|
||||
});
|
||||
},
|
||||
|
||||
async 'get-logins'(request) {
|
||||
const entry = await findEntry(request, true);
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
hash: KeeWebHash,
|
||||
count: 1,
|
||||
entries: [
|
||||
{
|
||||
group: entry.group.title,
|
||||
login: entry.user || '',
|
||||
name: entry.title || '',
|
||||
password: entry.password?.getText() || '',
|
||||
skipAutoSubmit: 'false',
|
||||
stringFields: [],
|
||||
uuid: kdbxweb.ByteUtils.bytesToHex(entry.entry.uuid.bytes)
|
||||
}
|
||||
],
|
||||
id: ''
|
||||
});
|
||||
},
|
||||
|
||||
async 'get-totp-by-url'(request) {
|
||||
const entry = await findEntry(request, true, { otp: true });
|
||||
|
||||
entry.initOtpGenerator();
|
||||
|
||||
if (!entry.otpGenerator) {
|
||||
throw makeError(Errors.noMatches);
|
||||
}
|
||||
|
||||
let selectEntryFieldView;
|
||||
if (entry.needsTouch) {
|
||||
selectEntryFieldView = new SelectEntryFieldView({
|
||||
needsTouch: true,
|
||||
deviceShortName: entry.device.shortName
|
||||
});
|
||||
selectEntryFieldView.render();
|
||||
}
|
||||
|
||||
const otpPromise = new Promise((resolve, reject) => {
|
||||
selectEntryFieldView?.on('result', () => reject(makeError(Errors.userRejected)));
|
||||
entry.otpGenerator.next((err, otp) => {
|
||||
if (otp) {
|
||||
resolve(otp);
|
||||
} else {
|
||||
reject(err || makeError(Errors.userRejected));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let totp;
|
||||
try {
|
||||
totp = await otpPromise;
|
||||
} finally {
|
||||
selectEntryFieldView?.remove();
|
||||
}
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
totp
|
||||
});
|
||||
},
|
||||
|
||||
async 'get-any-field'(request) {
|
||||
const entry = await findEntry(request, false);
|
||||
|
||||
const selectEntryFieldView = new SelectEntryFieldView({
|
||||
entry
|
||||
});
|
||||
const inactivityTimer = setTimeout(() => {
|
||||
selectEntryFieldView.emit('result', undefined);
|
||||
}, Timeouts.KeeWebConnectRequest);
|
||||
|
||||
const field = await selectEntryFieldView.showAndGetResult();
|
||||
|
||||
clearTimeout(inactivityTimer);
|
||||
|
||||
if (!field) {
|
||||
throw makeError(Errors.userRejected);
|
||||
}
|
||||
|
||||
let value = entry.getAllFields()[field];
|
||||
if (value.isProtected) {
|
||||
value = value.getText();
|
||||
}
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
field,
|
||||
value
|
||||
});
|
||||
},
|
||||
|
||||
async 'get-totp'(request) {
|
||||
decryptRequest(request);
|
||||
await checkContentRequestPermissions(request);
|
||||
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
|
||||
async 'set-login'(request) {
|
||||
const payload = decryptRequest(request);
|
||||
await checkContentRequestPermissions(request);
|
||||
|
||||
focusKeeWeb();
|
||||
|
||||
if (!payload.url) {
|
||||
throw new Error('Empty url');
|
||||
}
|
||||
const url = new URL(payload.url);
|
||||
|
||||
const files = getAvailableFiles(request);
|
||||
const client = getClient(request);
|
||||
|
||||
let selectedGroup;
|
||||
|
||||
let entryToUpdate;
|
||||
if (payload.uuid) {
|
||||
for (const file of files) {
|
||||
const entryId = kdbxweb.ByteUtils.bytesToBase64(
|
||||
kdbxweb.ByteUtils.hexToBytes(payload.uuid)
|
||||
);
|
||||
const foundEntry = file.getEntry(file.subId(entryId));
|
||||
if (foundEntry) {
|
||||
if (entryToUpdate) {
|
||||
throw new Error('Two entries with the same ID found');
|
||||
} else {
|
||||
entryToUpdate = foundEntry;
|
||||
selectedGroup = foundEntry.group;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!entryToUpdate) {
|
||||
throw new Error('Updated entry not found');
|
||||
}
|
||||
}
|
||||
|
||||
if (client.permissions.askSave === 'auto' && client.permissions.saveTo && !selectedGroup) {
|
||||
const file = files.find((f) => f.id === client.permissions.saveTo.fileId);
|
||||
selectedGroup = file?.getGroup(client.permissions.saveTo.groupId);
|
||||
}
|
||||
|
||||
if (client.permissions.askSave !== 'auto' || !selectedGroup) {
|
||||
if (!selectedGroup && RuntimeDataModel.extensionSaveConfig) {
|
||||
const file = files.find(
|
||||
(f) => f.id === RuntimeDataModel.extensionSaveConfig.fileId
|
||||
);
|
||||
selectedGroup = file?.getGroup(RuntimeDataModel.extensionSaveConfig.groupId);
|
||||
}
|
||||
|
||||
const allGroups = [];
|
||||
for (const file of files) {
|
||||
file.forEachGroup((group) => {
|
||||
const spaces = [];
|
||||
for (let parent = group; parent.parentGroup; parent = parent.parentGroup) {
|
||||
spaces.push(' ', ' ');
|
||||
}
|
||||
|
||||
if (
|
||||
!selectedGroup &&
|
||||
group.iconId === ExtensionGroupIconId &&
|
||||
ExtensionGroupNames.has(group.title)
|
||||
) {
|
||||
selectedGroup = group;
|
||||
}
|
||||
|
||||
allGroups.push({
|
||||
id: group.id,
|
||||
fileId: file.id,
|
||||
spaces,
|
||||
title: group.title,
|
||||
selected: group.id === selectedGroup?.id
|
||||
});
|
||||
});
|
||||
}
|
||||
if (!selectedGroup) {
|
||||
allGroups.splice(1, 0, {
|
||||
id: '',
|
||||
fileId: files[0].id,
|
||||
spaces: [' ', ' '],
|
||||
title: `${DefaultExtensionGroupName} (${Locale.extensionSaveEntryNewGroup})`,
|
||||
selected: true
|
||||
});
|
||||
}
|
||||
|
||||
const saveEntryView = new ExtensionSaveEntryView({
|
||||
extensionName: getHumanReadableExtensionName(client),
|
||||
url: payload.url,
|
||||
user: payload.login,
|
||||
askSave: RuntimeDataModel.extensionSaveConfig?.askSave || 'always',
|
||||
update: !!entryToUpdate,
|
||||
allGroups
|
||||
});
|
||||
|
||||
await alertWithTimeout({
|
||||
header: Locale.extensionSaveEntryHeader,
|
||||
icon: 'plus',
|
||||
buttons: [Alerts.buttons.allow, Alerts.buttons.deny],
|
||||
view: saveEntryView
|
||||
});
|
||||
|
||||
const config = { ...saveEntryView.config };
|
||||
if (!entryToUpdate) {
|
||||
if (config.groupId) {
|
||||
const file = files.find((f) => f.id === config.fileId);
|
||||
selectedGroup = file.getGroup(config.groupId);
|
||||
} else {
|
||||
selectedGroup = appModel.createNewGroupWithName(
|
||||
files[0].groups[0],
|
||||
files[0],
|
||||
DefaultExtensionGroupName
|
||||
);
|
||||
selectedGroup.setIcon(ExtensionGroupIconId);
|
||||
config.groupId = selectedGroup.id;
|
||||
}
|
||||
|
||||
RuntimeDataModel.extensionSaveConfig = config;
|
||||
client.permissions.saveTo = { fileId: config.fileId, groupId: config.groupId };
|
||||
}
|
||||
|
||||
client.permissions.askSave = config.askSave;
|
||||
}
|
||||
|
||||
const entryFields = {
|
||||
Title: url.hostname,
|
||||
UserName: payload.login,
|
||||
Password: kdbxweb.ProtectedValue.fromString(payload.password || ''),
|
||||
URL: payload.url
|
||||
};
|
||||
|
||||
if (entryToUpdate) {
|
||||
for (const [field, value] of Object.entries(entryFields)) {
|
||||
if (value) {
|
||||
entryToUpdate.setField(field, value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
appModel.createNewEntryWithFields(selectedGroup, entryFields);
|
||||
}
|
||||
|
||||
client.stats.passwordsWritten++;
|
||||
|
||||
Events.emit('browser-extension-sessions-changed');
|
||||
Events.emit('refresh');
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
count: null,
|
||||
entries: null,
|
||||
hash: KeeWebHash
|
||||
});
|
||||
},
|
||||
|
||||
async 'get-database-groups'(request) {
|
||||
decryptRequest(request);
|
||||
await checkContentRequestPermissions(request);
|
||||
|
||||
const makeGroups = (group) => {
|
||||
const res = {
|
||||
name: group.title,
|
||||
uuid: kdbxweb.ByteUtils.bytesToHex(group.group.uuid.bytes),
|
||||
children: []
|
||||
};
|
||||
for (const subGroup of group.items) {
|
||||
if (subGroup.matches()) {
|
||||
res.children.push(makeGroups(subGroup));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
const groups = [];
|
||||
for (const file of getAvailableFiles(request)) {
|
||||
for (const group of file.groups) {
|
||||
groups.push(makeGroups(group));
|
||||
}
|
||||
}
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
groups: { groups }
|
||||
});
|
||||
},
|
||||
|
||||
async 'create-new-group'(request) {
|
||||
const payload = decryptRequest(request);
|
||||
await checkContentRequestPermissions(request);
|
||||
|
||||
if (!payload.groupName) {
|
||||
throw new Error('No groupName');
|
||||
}
|
||||
|
||||
const groupNames = payload.groupName
|
||||
.split('/')
|
||||
.map((g) => g.trim())
|
||||
.filter((g) => g);
|
||||
|
||||
if (!groupNames.length) {
|
||||
throw new Error('Empty group path');
|
||||
}
|
||||
|
||||
const files = getAvailableFiles(request);
|
||||
|
||||
for (const file of files) {
|
||||
for (const rootGroup of file.groups) {
|
||||
let foundGroup = rootGroup;
|
||||
const pendingGroups = [...groupNames];
|
||||
while (pendingGroups.length && foundGroup) {
|
||||
const title = pendingGroups.shift();
|
||||
foundGroup = foundGroup.items.find((g) => g.title === title);
|
||||
}
|
||||
if (foundGroup) {
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
name: foundGroup.title,
|
||||
uuid: kdbxweb.ByteUtils.bytesToHex(foundGroup.group.uuid.bytes)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const client = getClient(request);
|
||||
const createGroupView = new ExtensionCreateGroupView({
|
||||
extensionName: getHumanReadableExtensionName(client),
|
||||
groupPath: groupNames.join(' / '),
|
||||
files: files.map((f, ix) => ({ id: f.id, name: f.name, selected: ix === 0 }))
|
||||
});
|
||||
|
||||
await alertWithTimeout({
|
||||
header: Locale.extensionNewGroupHeader,
|
||||
icon: 'folder-plus',
|
||||
buttons: [Alerts.buttons.allow, Alerts.buttons.deny],
|
||||
view: createGroupView
|
||||
});
|
||||
|
||||
const selectedFile = files.find((f) => f.id === createGroupView.selectedFile);
|
||||
|
||||
let newGroup = selectedFile.groups[0];
|
||||
const pendingGroups = [...groupNames];
|
||||
|
||||
while (pendingGroups.length) {
|
||||
const title = pendingGroups.shift();
|
||||
const item = newGroup.items.find((g) => g.title === title);
|
||||
if (item) {
|
||||
newGroup = item;
|
||||
} else {
|
||||
newGroup = appModel.createNewGroupWithName(newGroup, selectedFile, title);
|
||||
}
|
||||
}
|
||||
|
||||
return encryptResponse(request, {
|
||||
success: 'true',
|
||||
version: getVersion(request),
|
||||
name: newGroup.title,
|
||||
uuid: kdbxweb.ByteUtils.bytesToHex(newGroup.group.uuid.bytes)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const ProtocolImpl = {
|
||||
init(vars) {
|
||||
appModel = vars.appModel;
|
||||
logger = vars.logger;
|
||||
sendEvent = vars.sendEvent;
|
||||
|
||||
setupListeners();
|
||||
},
|
||||
|
||||
cleanup() {
|
||||
const wasNotEmpty = connectedClients.size;
|
||||
|
||||
connectedClients.clear();
|
||||
|
||||
if (wasNotEmpty) {
|
||||
Events.emit('browser-extension-sessions-changed');
|
||||
}
|
||||
},
|
||||
|
||||
deleteConnection(connectionId) {
|
||||
for (const [clientId, client] of connectedClients.entries()) {
|
||||
if (client.connection.connectionId === connectionId) {
|
||||
connectedClients.delete(clientId);
|
||||
}
|
||||
}
|
||||
Events.emit('browser-extension-sessions-changed');
|
||||
},
|
||||
|
||||
getClientPermissions(clientId) {
|
||||
return connectedClients.get(clientId)?.permissions;
|
||||
},
|
||||
|
||||
setClientPermissions(clientId, permissions) {
|
||||
const client = connectedClients.get(clientId);
|
||||
if (client?.permissions) {
|
||||
client.permissions = { ...client.permissions, ...permissions };
|
||||
}
|
||||
},
|
||||
|
||||
errorToResponse(e, request) {
|
||||
return {
|
||||
action: request?.action,
|
||||
error: e.message || 'Unknown error',
|
||||
errorCode: e.code || 0
|
||||
};
|
||||
},
|
||||
|
||||
async handleRequest(request, connectionInfo) {
|
||||
const appWindowWasFocused = Launcher?.isAppFocused();
|
||||
|
||||
let result;
|
||||
try {
|
||||
const handler = ProtocolHandlers[request.action];
|
||||
if (!handler) {
|
||||
throw new Error(`Handler not found: ${request.action}`);
|
||||
}
|
||||
result = await handler(request, connectionInfo);
|
||||
if (!result) {
|
||||
throw new Error(`Handler returned an empty result: ${request.action}`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!e.code) {
|
||||
logger.error(`Error in handler ${request.action}`, e);
|
||||
}
|
||||
result = this.errorToResponse(e, request);
|
||||
}
|
||||
|
||||
if (!appWindowWasFocused && Launcher?.isAppFocused()) {
|
||||
Launcher.hideApp();
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
get sessions() {
|
||||
return [...connectedClients.entries()]
|
||||
.map(([clientId, client]) => ({
|
||||
clientId,
|
||||
connectionId: client.connection.connectionId,
|
||||
appName: client.connection.appName,
|
||||
extensionName: client.connection.extensionName,
|
||||
connectedDate: client.stats.connectedDate,
|
||||
passwordsRead: client.stats.passwordsRead,
|
||||
passwordsWritten: client.stats.passwordsWritten,
|
||||
permissions: client.permissions,
|
||||
permissionsDenied: client.permissionsDenied
|
||||
}))
|
||||
.sort((x, y) => y.connectedDate - x.connectedDate);
|
||||
}
|
||||
};
|
||||
|
||||
export { ProtocolImpl };
|
||||
@@ -1,8 +1,8 @@
|
||||
/* 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 'util/formatting/date-format';
|
||||
import { DateFormat } from 'comp/i18n/date-format';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
import { Locale } from 'util/locale';
|
||||
|
||||
@@ -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 || '';
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { SettingsManager } from 'comp/settings/settings-manager';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
|
||||
const DateFormat = {
|
||||
months() {
|
||||
const format = new Intl.DateTimeFormat(SettingsManager.activeLocale, { month: 'long' });
|
||||
const months = [];
|
||||
for (let month = 0; month < 12; month++) {
|
||||
months.push(format.format(new Date(2008, month)));
|
||||
}
|
||||
return months;
|
||||
},
|
||||
|
||||
weekDays() {
|
||||
const format = new Intl.DateTimeFormat(SettingsManager.activeLocale, { weekday: 'long' });
|
||||
const weekdays = [];
|
||||
for (let day = 1; day < 8; day++) {
|
||||
weekdays.push(format.format(new Date(2007, 9, 6 + day)));
|
||||
}
|
||||
return weekdays;
|
||||
},
|
||||
|
||||
shortWeekDays() {
|
||||
const format = new Intl.DateTimeFormat(SettingsManager.activeLocale, { weekday: 'short' });
|
||||
const weekdays = [];
|
||||
for (let day = 1; day < 8; day++) {
|
||||
weekdays.push(format.format(new Date(2007, 9, 6 + day)));
|
||||
}
|
||||
return weekdays;
|
||||
},
|
||||
|
||||
dtStr(dt) {
|
||||
if (typeof dt === 'number') {
|
||||
dt = new Date(dt);
|
||||
}
|
||||
return dt
|
||||
? new Intl.DateTimeFormat(SettingsManager.activeLocale, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'medium'
|
||||
}).format(dt)
|
||||
: '';
|
||||
},
|
||||
|
||||
dStr(dt) {
|
||||
if (typeof dt === 'number') {
|
||||
dt = new Date(dt);
|
||||
}
|
||||
return dt
|
||||
? new Intl.DateTimeFormat(SettingsManager.activeLocale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}).format(dt)
|
||||
: '';
|
||||
},
|
||||
|
||||
dtStrFs(dt) {
|
||||
if (typeof dt === 'number') {
|
||||
dt = new Date(dt);
|
||||
}
|
||||
return dt
|
||||
? dt.getFullYear() +
|
||||
'-' +
|
||||
StringFormat.pad(dt.getMonth() + 1, 2) +
|
||||
'-' +
|
||||
StringFormat.pad(dt.getDate(), 2) +
|
||||
'T' +
|
||||
StringFormat.pad(dt.getHours(), 2) +
|
||||
'-' +
|
||||
StringFormat.pad(dt.getMinutes(), 2) +
|
||||
'-' +
|
||||
StringFormat.pad(dt.getSeconds(), 2)
|
||||
: '';
|
||||
}
|
||||
};
|
||||
|
||||
export { DateFormat };
|
||||
@@ -17,6 +17,9 @@ const Launcher = {
|
||||
platform() {
|
||||
return process.platform;
|
||||
},
|
||||
arch() {
|
||||
return process.arch;
|
||||
},
|
||||
electron() {
|
||||
return this.req('electron');
|
||||
},
|
||||
@@ -55,7 +58,15 @@ const Launcher = {
|
||||
return this.joinPath(this.userDataPath, fileName || '');
|
||||
},
|
||||
getTempPath(fileName) {
|
||||
return this.joinPath(this.remoteApp().getPath('temp'), fileName || '');
|
||||
let tempPath = this.joinPath(this.remoteApp().getPath('temp'), 'KeeWeb');
|
||||
const fs = this.req('fs');
|
||||
if (!fs.existsSync(tempPath)) {
|
||||
fs.mkdirSync(tempPath);
|
||||
}
|
||||
if (fileName) {
|
||||
tempPath = this.joinPath(tempPath, fileName);
|
||||
}
|
||||
return tempPath;
|
||||
},
|
||||
getDocumentsPath(fileName) {
|
||||
return this.joinPath(this.remoteApp().getPath('documents'), fileName || '');
|
||||
@@ -142,17 +153,6 @@ const Launcher = {
|
||||
saveConfig(name, data) {
|
||||
return this.remoteApp().saveConfig(name, data);
|
||||
},
|
||||
ensureRunnable(path) {
|
||||
if (process.platform !== 'win32') {
|
||||
const fs = this.req('fs');
|
||||
const stat = fs.statSync(path);
|
||||
if ((stat.mode & 0o0111) === 0) {
|
||||
const mode = stat.mode | 0o0100;
|
||||
logger.info(`chmod 0${mode.toString(8)} ${path}`);
|
||||
fs.chmodSync(path, mode);
|
||||
}
|
||||
}
|
||||
},
|
||||
preventExit(e) {
|
||||
e.returnValue = false;
|
||||
return false;
|
||||
@@ -164,18 +164,18 @@ const Launcher = {
|
||||
requestExit() {
|
||||
const app = this.remoteApp();
|
||||
app.setHookBeforeQuitEvent(false);
|
||||
if (this.restartPending) {
|
||||
app.restartApp();
|
||||
if (this.pendingUpdateFile) {
|
||||
app.restartAndUpdate(this.pendingUpdateFile);
|
||||
} else {
|
||||
app.quit();
|
||||
}
|
||||
},
|
||||
requestRestart() {
|
||||
this.restartPending = true;
|
||||
requestRestartAndUpdate(updateFilePath) {
|
||||
this.pendingUpdateFile = updateFilePath;
|
||||
this.requestExit();
|
||||
},
|
||||
cancelRestart() {
|
||||
this.restartPending = false;
|
||||
this.pendingUpdateFile = undefined;
|
||||
},
|
||||
setClipboardText(text) {
|
||||
return this.electron().clipboard.writeText(text);
|
||||
@@ -191,7 +191,7 @@ const Launcher = {
|
||||
}
|
||||
},
|
||||
quitOnRealQuitEventIfMinimizeOnQuitIsEnabled() {
|
||||
return this.platform() === 'darwin';
|
||||
return !!this.pendingUpdateFile;
|
||||
},
|
||||
minimizeApp() {
|
||||
this.remoteApp().minimizeApp({
|
||||
@@ -203,7 +203,7 @@ const Launcher = {
|
||||
return process.platform !== 'linux';
|
||||
},
|
||||
updaterEnabled() {
|
||||
return this.electron().remote.process.argv.indexOf('--disable-updater') === -1;
|
||||
return process.platform !== 'linux';
|
||||
},
|
||||
getMainWindow() {
|
||||
return this.remoteApp().getMainWindow();
|
||||
@@ -233,57 +233,32 @@ const Launcher = {
|
||||
},
|
||||
spawn(config) {
|
||||
const ts = logger.ts();
|
||||
let complete = config.complete;
|
||||
const ps = this.req('child_process').spawn(config.cmd, config.args);
|
||||
[ps.stdin, ps.stdout, ps.stderr].forEach((s) => s.setEncoding('utf-8'));
|
||||
let stderr = '';
|
||||
let stdout = '';
|
||||
ps.stderr.on('data', (d) => {
|
||||
stderr += d.toString('utf-8');
|
||||
if (config.throwOnStdErr) {
|
||||
try {
|
||||
ps.kill();
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
ps.stdout.on('data', (d) => {
|
||||
stdout += d.toString('utf-8');
|
||||
});
|
||||
ps.on('close', (code) => {
|
||||
stdout = stdout.trim();
|
||||
stderr = stderr.trim();
|
||||
const msg = 'spawn ' + config.cmd + ': ' + code + ', ' + logger.ts(ts);
|
||||
if (code !== 0) {
|
||||
logger.error(msg + '\n' + stdout + '\n' + stderr);
|
||||
} else {
|
||||
logger.info(msg + (stdout && !config.noStdOutLogging ? '\n' + stdout : ''));
|
||||
}
|
||||
if (complete) {
|
||||
complete(code !== 0 ? 'Exit code ' + code : null, stdout, code);
|
||||
const { ipcRenderer } = this.electron();
|
||||
let { complete } = config;
|
||||
delete config.complete;
|
||||
ipcRenderer
|
||||
.invoke('spawnProcess', config)
|
||||
.then((res) => {
|
||||
if (res.err) {
|
||||
logger.error('spawn error: ' + config.cmd + ', ' + logger.ts(ts), res.err);
|
||||
complete?.(res.err);
|
||||
} else {
|
||||
const code = res.code;
|
||||
const stdout = res.stdout || '';
|
||||
const stderr = res.stderr || '';
|
||||
const msg = 'spawn ' + config.cmd + ': ' + code + ', ' + logger.ts(ts);
|
||||
if (code !== 0) {
|
||||
logger.error(msg + '\n' + stdout + '\n' + stderr);
|
||||
} else {
|
||||
logger.info(msg + (stdout && !config.noStdOutLogging ? '\n' + stdout : ''));
|
||||
}
|
||||
complete?.(code !== 0 ? 'Exit code ' + code : null, stdout, code);
|
||||
}
|
||||
complete = null;
|
||||
}
|
||||
});
|
||||
ps.on('error', (err) => {
|
||||
logger.error('spawn error: ' + config.cmd + ', ' + logger.ts(ts), err);
|
||||
if (complete) {
|
||||
complete(err);
|
||||
complete = null;
|
||||
}
|
||||
});
|
||||
if (config.data) {
|
||||
try {
|
||||
ps.stdin.end(config.data);
|
||||
} catch (e) {
|
||||
logger.error('spawn write error', e);
|
||||
}
|
||||
}
|
||||
process.nextTick(() => {
|
||||
// it should work without destroy, but a process doesn't get launched
|
||||
// xubuntu-desktop 19.04 / xfce
|
||||
// see https://github.com/keeweb/keeweb/issues/1234
|
||||
ps.stdin.destroy();
|
||||
});
|
||||
return ps;
|
||||
})
|
||||
.catch((err) => {
|
||||
complete?.(err);
|
||||
});
|
||||
},
|
||||
checkOpenFiles() {
|
||||
this.readyToOpenFiles = true;
|
||||
@@ -301,6 +276,18 @@ const Launcher = {
|
||||
},
|
||||
setGlobalShortcuts(appSettings) {
|
||||
this.remoteApp().setGlobalShortcuts(appSettings);
|
||||
},
|
||||
minimizeMainWindow() {
|
||||
this.getMainWindow().minimize();
|
||||
},
|
||||
maximizeMainWindow() {
|
||||
this.getMainWindow().maximize();
|
||||
},
|
||||
restoreMainWindow() {
|
||||
this.getMainWindow().restore();
|
||||
},
|
||||
mainWindowMaximized() {
|
||||
return this.getMainWindow().isMaximized();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -308,9 +295,10 @@ Events.on('launcher-exit-request', () => {
|
||||
setTimeout(() => Launcher.exit(), 0);
|
||||
});
|
||||
Events.on('launcher-minimize', () => setTimeout(() => Events.emit('app-minimized'), 0));
|
||||
Events.on('launcher-maximize', () => setTimeout(() => Events.emit('app-maximized'), 0));
|
||||
Events.on('launcher-unmaximize', () => setTimeout(() => Events.emit('app-unmaximized'), 0));
|
||||
Events.on('launcher-started-minimized', () => setTimeout(() => Launcher.minimizeApp(), 0));
|
||||
Events.on('start-profile', (data) => StartProfiler.reportAppProfile(data));
|
||||
Events.on('log', (e) => new Logger(e.category || 'remote-app')[e.method || 'info'](e.message));
|
||||
|
||||
window.launcherOpen = (file) => Launcher.openFile(file);
|
||||
if (window.launcherOpenedFile) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Events } from 'framework/events';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
@@ -8,11 +9,19 @@ let NativeModules;
|
||||
if (Launcher) {
|
||||
const logger = new Logger('native-module-connector');
|
||||
|
||||
let host;
|
||||
let hostRunning = false;
|
||||
let hostStartPromise;
|
||||
let callId = 0;
|
||||
let promises = {};
|
||||
let ykChalRespCallbacks = {};
|
||||
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.on('nativeModuleCallback', (e, msg) => NativeModules.hostCallback(msg));
|
||||
ipcRenderer.on('nativeModuleHostError', (e, err) => NativeModules.hostError(err));
|
||||
ipcRenderer.on('nativeModuleHostExit', (e, { code, sig }) => NativeModules.hostExit(code, sig));
|
||||
ipcRenderer.on('nativeModuleHostDisconnect', () => NativeModules.hostDisconnect());
|
||||
ipcRenderer.on('log', (e, ...args) => NativeModules.log(...args));
|
||||
|
||||
const handlers = {
|
||||
yubikeys(numYubiKeys) {
|
||||
Events.emit('native-modules-yubikeys', { numYubiKeys });
|
||||
@@ -35,7 +44,7 @@ if (Launcher) {
|
||||
}
|
||||
},
|
||||
|
||||
'yk-chal-resp-result'({ callbackId, error, result }) {
|
||||
yubiKeyChallengeResponseResult({ callbackId, error, result }) {
|
||||
const callback = ykChalRespCallbacks[callbackId];
|
||||
if (callback) {
|
||||
const willBeCalledAgain = error && error.touchRequested;
|
||||
@@ -49,39 +58,39 @@ if (Launcher) {
|
||||
|
||||
NativeModules = {
|
||||
startHost() {
|
||||
if (host) {
|
||||
return;
|
||||
if (hostRunning) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (hostStartPromise) {
|
||||
return hostStartPromise;
|
||||
}
|
||||
|
||||
logger.debug('Starting native module host');
|
||||
|
||||
const path = Launcher.req('path');
|
||||
const appContentRoot = Launcher.remoteApp().getAppContentRoot();
|
||||
const mainModulePath = path.join(appContentRoot, 'native-module-host.js');
|
||||
hostStartPromise = this.callNoWait('start').then(() => {
|
||||
hostStartPromise = undefined;
|
||||
hostRunning = true;
|
||||
|
||||
const { fork } = Launcher.req('child_process');
|
||||
if (this.usbListenerRunning) {
|
||||
return this.call('startUsbListener');
|
||||
}
|
||||
});
|
||||
|
||||
host = fork(mainModulePath);
|
||||
|
||||
host.on('message', (message) => this.hostCallback(message));
|
||||
|
||||
host.on('error', (e) => this.hostError(e));
|
||||
host.on('exit', (code, sig) => this.hostExit(code, sig));
|
||||
|
||||
this.call('init', Launcher.remoteApp().getAppMainRoot());
|
||||
|
||||
if (this.usbListenerRunning) {
|
||||
this.call('start-usb');
|
||||
}
|
||||
return hostStartPromise;
|
||||
},
|
||||
|
||||
hostError(e) {
|
||||
logger.error('Host error', e);
|
||||
},
|
||||
|
||||
hostDisconnect() {
|
||||
logger.error('Host disconnected');
|
||||
},
|
||||
|
||||
hostExit(code, sig) {
|
||||
logger.error(`Host exited with code ${code} and signal ${sig}`);
|
||||
host = null;
|
||||
|
||||
hostRunning = false;
|
||||
|
||||
const err = new Error('Native module host crashed');
|
||||
|
||||
@@ -110,6 +119,14 @@ if (Launcher) {
|
||||
}
|
||||
},
|
||||
|
||||
log(name, level, ...args) {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
const logger = new Logger(name);
|
||||
logger[level](...args);
|
||||
},
|
||||
|
||||
autoRestartHost() {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
@@ -121,56 +138,113 @@ if (Launcher) {
|
||||
},
|
||||
|
||||
call(cmd, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!host) {
|
||||
try {
|
||||
this.startHost();
|
||||
} catch (e) {
|
||||
return reject(e);
|
||||
}
|
||||
}
|
||||
return this.startHost().then(() => this.callNoWait(cmd, ...args));
|
||||
},
|
||||
|
||||
callNoWait(cmd, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
callId++;
|
||||
if (callId === Number.MAX_SAFE_INTEGER) {
|
||||
callId = 1;
|
||||
}
|
||||
// logger.debug('Call', cmd, args, callId);
|
||||
promises[callId] = { cmd, resolve, reject };
|
||||
host.send({ cmd, args, callId });
|
||||
|
||||
ipcRenderer.send('nativeModuleCall', { cmd, args, callId });
|
||||
});
|
||||
},
|
||||
|
||||
startUsbListener() {
|
||||
this.call('start-usb');
|
||||
this.call('startUsbListener');
|
||||
this.usbListenerRunning = true;
|
||||
},
|
||||
|
||||
stopUsbListener() {
|
||||
this.usbListenerRunning = false;
|
||||
if (host) {
|
||||
this.call('stop-usb');
|
||||
if (hostRunning) {
|
||||
this.call('stopUsbListener');
|
||||
}
|
||||
},
|
||||
|
||||
getYubiKeys(config) {
|
||||
return this.call('get-yubikeys', config);
|
||||
return this.call('getYubiKeys', config);
|
||||
},
|
||||
|
||||
yubiKeyChallengeResponse(yubiKey, challenge, slot, callback) {
|
||||
ykChalRespCallbacks[callId] = callback;
|
||||
return this.call('yk-chal-resp', yubiKey, challenge, slot, callId);
|
||||
return this.call('yubiKeyChallengeResponse', yubiKey, challenge, slot, callId);
|
||||
},
|
||||
|
||||
yubiKeyCancelChallengeResponse() {
|
||||
if (host) {
|
||||
this.call('yk-cancel-chal-resp');
|
||||
if (hostRunning) {
|
||||
this.call('yubiKeyCancelChallengeResponse');
|
||||
}
|
||||
},
|
||||
|
||||
argon2(password, salt, options) {
|
||||
return this.call('argon2', password, salt, options);
|
||||
},
|
||||
|
||||
hardwareCryptoDeleteKey: async () => {
|
||||
await ipcRenderer.invoke('hardwareCryptoDeleteKey');
|
||||
},
|
||||
|
||||
hardwareEncrypt: async (value) => {
|
||||
const { data, salt } = await ipcRenderer.invoke('hardwareEncrypt', value.dataAndSalt());
|
||||
return new kdbxweb.ProtectedValue(data, salt);
|
||||
},
|
||||
|
||||
hardwareDecrypt: async (value, touchIdPrompt) => {
|
||||
const { data, salt } = await ipcRenderer.invoke(
|
||||
'hardwareDecrypt',
|
||||
value.dataAndSalt(),
|
||||
touchIdPrompt
|
||||
);
|
||||
return new kdbxweb.ProtectedValue(data, salt);
|
||||
},
|
||||
|
||||
kbdGetActiveWindow(options) {
|
||||
return this.call('kbdGetActiveWindow', options);
|
||||
},
|
||||
|
||||
kbdGetActivePid() {
|
||||
return this.call('kbdGetActivePid');
|
||||
},
|
||||
|
||||
kbdShowWindow(win) {
|
||||
return this.call('kbdShowWindow', win);
|
||||
},
|
||||
|
||||
kbdText(str) {
|
||||
return this.call('kbdText', str);
|
||||
},
|
||||
|
||||
kbdTextAsKeys(str, mods) {
|
||||
return this.call('kbdTextAsKeys', str, mods);
|
||||
},
|
||||
|
||||
kbdKeyPress(code, modifiers) {
|
||||
return this.call('kbdKeyPress', code, modifiers);
|
||||
},
|
||||
|
||||
kbdShortcut(code, modifiers) {
|
||||
return this.call('kbdShortcut', code, modifiers);
|
||||
},
|
||||
|
||||
kbdKeyMoveWithModifier(down, modifiers) {
|
||||
return this.call('kbdKeyMoveWithModifier', down, modifiers);
|
||||
},
|
||||
|
||||
kbdKeyPressWithCharacter(character, code, modifiers) {
|
||||
return this.call('kbdKeyPressWithCharacter', character, code, modifiers);
|
||||
},
|
||||
|
||||
kbdEnsureModifierNotPressed() {
|
||||
return this.call('kbdEnsureModifierNotPressed');
|
||||
}
|
||||
};
|
||||
|
||||
global.NativeModules = NativeModules;
|
||||
}
|
||||
|
||||
export { NativeModules };
|
||||
|
||||
@@ -1,44 +1,86 @@
|
||||
import { Events } from 'framework/events';
|
||||
import { Features } from 'util/features';
|
||||
import { Locale } from 'util/locale';
|
||||
import { ThemeWatcher } from 'comp/browser/theme-watcher';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const appleThemes = {
|
||||
macdark: 'setGenThemeMacDark'
|
||||
};
|
||||
|
||||
const extraThemes = Features.isMac || Features.isiOS ? appleThemes : {};
|
||||
const logger = new Logger('settings-manager');
|
||||
|
||||
const SettingsManager = {
|
||||
neutralLocale: null,
|
||||
activeLocale: 'en',
|
||||
activeLocale: 'en-US',
|
||||
activeTheme: null,
|
||||
|
||||
allLocales: {
|
||||
'en': 'English',
|
||||
'en-US': 'English',
|
||||
'de-DE': 'Deutsch',
|
||||
'fr-FR': 'Français'
|
||||
},
|
||||
|
||||
allThemes: {
|
||||
fb: 'setGenThemeFb',
|
||||
db: 'setGenThemeDb',
|
||||
dark: 'setGenThemeDark',
|
||||
light: 'setGenThemeLight',
|
||||
sd: 'setGenThemeSd',
|
||||
sl: 'setGenThemeSl',
|
||||
wh: 'setGenThemeWh',
|
||||
fb: 'setGenThemeFb',
|
||||
bl: 'setGenThemeBl',
|
||||
db: 'setGenThemeDb',
|
||||
lb: 'setGenThemeLb',
|
||||
te: 'setGenThemeTe',
|
||||
hc: 'setGenThemeHc',
|
||||
...extraThemes
|
||||
lt: 'setGenThemeLt',
|
||||
dc: 'setGenThemeDc',
|
||||
hc: 'setGenThemeHc'
|
||||
},
|
||||
|
||||
// changing something here? don't forget about desktop/app.js
|
||||
autoSwitchedThemes: [
|
||||
{
|
||||
name: 'setGenThemeDefault',
|
||||
dark: 'dark',
|
||||
light: 'light'
|
||||
},
|
||||
{
|
||||
name: 'setGenThemeSol',
|
||||
dark: 'sd',
|
||||
light: 'sl'
|
||||
},
|
||||
{
|
||||
name: 'setGenThemeBlue',
|
||||
dark: 'fb',
|
||||
light: 'bl'
|
||||
},
|
||||
{
|
||||
name: 'setGenThemeBrown',
|
||||
dark: 'db',
|
||||
light: 'lb'
|
||||
},
|
||||
{
|
||||
name: 'setGenThemeTerminal',
|
||||
dark: 'te',
|
||||
light: 'lt'
|
||||
},
|
||||
{
|
||||
name: 'setGenThemeHighContrast',
|
||||
dark: 'dc',
|
||||
light: 'hc'
|
||||
}
|
||||
],
|
||||
|
||||
customLocales: {},
|
||||
|
||||
setBySettings(settings) {
|
||||
this.setTheme(settings.theme);
|
||||
this.setFontSize(settings.fontSize);
|
||||
const locale = settings.locale;
|
||||
init() {
|
||||
Events.on('dark-mode-changed', () => this.darkModeChanged());
|
||||
},
|
||||
|
||||
setBySettings() {
|
||||
this.setTheme(AppSettingsModel.theme);
|
||||
this.setFontSize(AppSettingsModel.fontSize);
|
||||
const locale = AppSettingsModel.locale;
|
||||
try {
|
||||
if (locale) {
|
||||
this.setLocale(settings.locale);
|
||||
this.setLocale(AppSettingsModel.locale);
|
||||
} else {
|
||||
this.setLocale(this.getBrowserLocale());
|
||||
}
|
||||
@@ -46,7 +88,7 @@ const SettingsManager = {
|
||||
},
|
||||
|
||||
getDefaultTheme() {
|
||||
return Features.isMac ? 'macdark' : 'fb';
|
||||
return 'dark';
|
||||
},
|
||||
|
||||
setTheme(theme) {
|
||||
@@ -61,18 +103,45 @@ const SettingsManager = {
|
||||
document.body.classList.remove(cls);
|
||||
}
|
||||
}
|
||||
if (AppSettingsModel.autoSwitchTheme) {
|
||||
theme = this.selectDarkOrLightTheme(theme);
|
||||
}
|
||||
document.body.classList.add(this.getThemeClass(theme));
|
||||
const metaThemeColor = document.head.querySelector('meta[name=theme-color]');
|
||||
if (metaThemeColor) {
|
||||
metaThemeColor.content = window.getComputedStyle(document.body).backgroundColor;
|
||||
}
|
||||
this.activeTheme = theme;
|
||||
logger.debug('Theme changed', theme);
|
||||
Events.emit('theme-applied');
|
||||
},
|
||||
|
||||
getThemeClass(theme) {
|
||||
return 'th-' + theme;
|
||||
},
|
||||
|
||||
selectDarkOrLightTheme(theme) {
|
||||
for (const config of this.autoSwitchedThemes) {
|
||||
if (config.light === theme || config.dark === theme) {
|
||||
return ThemeWatcher.dark ? config.dark : config.light;
|
||||
}
|
||||
}
|
||||
return theme;
|
||||
},
|
||||
|
||||
darkModeChanged() {
|
||||
if (AppSettingsModel.autoSwitchTheme) {
|
||||
for (const config of this.autoSwitchedThemes) {
|
||||
if (config.light === this.activeTheme || config.dark === this.activeTheme) {
|
||||
const newTheme = ThemeWatcher.dark ? config.dark : config.light;
|
||||
logger.debug('Setting theme triggered by system settings change', newTheme);
|
||||
this.setTheme(newTheme);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setFontSize(fontSize) {
|
||||
const defaultFontSize = Features.isMobile ? 14 : 12;
|
||||
document.documentElement.style.fontSize = defaultFontSize + (fontSize || 0) * 2 + 'px';
|
||||
@@ -83,7 +152,7 @@ const SettingsManager = {
|
||||
return;
|
||||
}
|
||||
let localeValues;
|
||||
if (loc !== 'en') {
|
||||
if (loc !== 'en-US') {
|
||||
if (this.customLocales[loc]) {
|
||||
localeValues = this.customLocales[loc];
|
||||
} else {
|
||||
@@ -96,12 +165,26 @@ 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() {
|
||||
const language = (navigator.languages && navigator.languages[0]) || navigator.language;
|
||||
if (language && language.lastIndexOf('en', 0) === 0) {
|
||||
return 'en';
|
||||
if (language && language.startsWith('en')) {
|
||||
return 'en-US';
|
||||
}
|
||||
return language;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -38,7 +50,7 @@ const Alerts = {
|
||||
Alerts.alertDisplayed = true;
|
||||
const view = new ModalView(config);
|
||||
view.render();
|
||||
view.on('result', (res, check) => {
|
||||
view.once('result', (res, check) => {
|
||||
if (res && config.success) {
|
||||
config.success(res, check);
|
||||
}
|
||||
|
||||
@@ -26,13 +26,28 @@ const GDriveApps = {
|
||||
|
||||
const OneDriveApps = {
|
||||
Local: {
|
||||
id: 'b97c53d5-db5b-4124-aab9-d39195293815',
|
||||
secret: 'V9b6:iJU]N7cImE1f_OLNjqZJDBnumR?'
|
||||
id: 'b97c53d5-db5b-4124-aab9-d39195293815'
|
||||
},
|
||||
Production: {
|
||||
id: 'bbc74d1b-3a9c-46e6-9da4-4c645e830923'
|
||||
},
|
||||
Desktop: {
|
||||
id: 'bbc74d1b-3a9c-46e6-9da4-4c645e830923',
|
||||
secret: 'aOMJaktJEAs_Tmh]fx4iQ[Zd3mp3KK7-'
|
||||
}
|
||||
};
|
||||
|
||||
export { DropboxApps, GDriveApps, OneDriveApps };
|
||||
const MsTeamsApps = {
|
||||
Local: {
|
||||
id: '8fbe2245-13d5-446f-bedc-74c3b2e1f635'
|
||||
},
|
||||
Production: {
|
||||
id: '8fbe2245-13d5-446f-bedc-74c3b2e1f635'
|
||||
},
|
||||
Desktop: {
|
||||
id: '8fbe2245-13d5-446f-bedc-74c3b2e1f635',
|
||||
secret: 'F02~HYaWs-~7MndJcVRtv9~h-50Brk_9ho'
|
||||
}
|
||||
};
|
||||
|
||||
export { DropboxApps, GDriveApps, OneDriveApps, MsTeamsApps };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const DefaultAppSettings = {
|
||||
theme: null, // UI theme
|
||||
autoSwitchTheme: false, // automatically switch between light and dark theme
|
||||
locale: null, // user interface language
|
||||
expandGroups: true, // show entries from all subgroups
|
||||
listViewWidth: null, // width of the entry list representation
|
||||
@@ -8,14 +9,16 @@ const DefaultAppSettings = {
|
||||
autoUpdate: 'install', // auto-update options: "install", "check", ""
|
||||
clipboardSeconds: 0, // number of seconds after which the clipboard will be cleared
|
||||
autoSave: true, // auto-save open files
|
||||
autoSaveInterval: 0, // interval between performing automatic sync, minutes
|
||||
rememberKeyFiles: false, // remember keyfiles selected on the Open screen
|
||||
autoSaveInterval: 0, // interval between performing automatic sync, minutes, -1: on every change
|
||||
rememberKeyFiles: 'path', // remember keyfiles selected on the Open screen
|
||||
idleMinutes: 15, // app lock timeout after inactivity, minutes
|
||||
minimizeOnClose: false, // minimise the app instead of closing
|
||||
minimizeOnFieldCopy: false, // minimise the app on copy
|
||||
tableView: false, // view entries as a table instead of list
|
||||
colorfulIcons: false, // use colorful custom icons instead of grayscale
|
||||
useMarkdown: true, // use Markdown in Notes field
|
||||
directAutotype: true, // if only one matching entry is found, select that one automatically
|
||||
autoTypeTitleFilterEnabled: true, // enable the title filtering in auto-type by default
|
||||
titlebarStyle: 'default', // window titlebar style
|
||||
lockOnMinimize: true, // lock the app when it's minimized
|
||||
lockOnCopy: false, // lock the app after a password was copied
|
||||
@@ -35,8 +38,18 @@ const DefaultAppSettings = {
|
||||
allowIframes: false, // allow displaying the app in IFrames
|
||||
useGroupIconForEntries: false, // automatically use group icon when creating new entries
|
||||
enableUsb: true, // enable interaction with USB devices
|
||||
nativeArgon2: true, // use native argon2 module
|
||||
fieldLabelDblClickAutoType: false, // trigger auto-type by doubleclicking field label
|
||||
auditPasswords: true, // enable password audit
|
||||
auditPasswordEntropy: true, // show warnings for weak passwords
|
||||
excludePinsFromAudit: true, // exclude PIN codes from audit
|
||||
checkPasswordsOnHIBP: false, // check passwords on Have I Been Pwned
|
||||
auditPasswordAge: 0, // show warnings about old passwords, number of years, 0 = disabled
|
||||
deviceOwnerAuth: null, // Touch ID: null / 'memory' / 'file'
|
||||
deviceOwnerAuthTimeoutMinutes: 0, // how often master password is required with Touch ID
|
||||
disableOfflineStorage: false, // don't cache loaded files in offline storage
|
||||
shortLivedStorageToken: false, // short-lived sessions in cloud storage providers
|
||||
extensionFocusIfLocked: true, // focus KeeWeb if a browser extension tries to connect while KeeWeb is locked
|
||||
extensionFocusIfEmpty: true, // show the entry selection screen if there's no match found by URL
|
||||
|
||||
yubiKeyShowIcon: true, // show an icon to open OTP codes from YubiKey
|
||||
yubiKeyAutoOpen: false, // auto-load one-time codes when there are open files
|
||||
@@ -66,6 +79,7 @@ const DefaultAppSettings = {
|
||||
|
||||
webdav: true, // enable WebDAV integration
|
||||
webdavSaveMethod: 'move', // how to save files with WebDAV: "move" or "put"
|
||||
webdavStatReload: false, // WebDAV: reload the file instead of relying on Last-Modified
|
||||
|
||||
gdrive: true, // enable Google Drive integration
|
||||
gdriveClientId: null, // custom Google Drive client id
|
||||
@@ -73,7 +87,13 @@ const DefaultAppSettings = {
|
||||
|
||||
onedrive: true, // enable OneDrive integration
|
||||
onedriveClientId: null, // custom OneDrive client id
|
||||
onedriveClientSecret: null // custom OneDrive client secret
|
||||
onedriveClientSecret: null, // custom OneDrive client secret
|
||||
onedriveTenantId: null, // custom OneDrive tenant id
|
||||
|
||||
msteams: false, // enable Microsoft Teams integration
|
||||
msteamsClientId: null, // custom Microsoft Teams client id
|
||||
msteamsClientSecret: null, // custom Microsoft Teams client secret
|
||||
msteamsTenantId: null // custom Microsoft Teams tenant id
|
||||
};
|
||||
|
||||
export { DefaultAppSettings };
|
||||
|
||||
@@ -3,12 +3,12 @@ const IconMap = [
|
||||
'globe',
|
||||
'exclamation-triangle',
|
||||
'server',
|
||||
'thumb-tack',
|
||||
'comments-o',
|
||||
'thumbtack',
|
||||
'comments',
|
||||
'puzzle-piece',
|
||||
'pencil-square-o',
|
||||
'edit',
|
||||
'plug',
|
||||
'newspaper-o',
|
||||
'address-card',
|
||||
'paperclip',
|
||||
'camera',
|
||||
'wifi',
|
||||
@@ -18,32 +18,32 @@ const IconMap = [
|
||||
'certificate',
|
||||
'bullseye',
|
||||
'desktop',
|
||||
'envelope-o',
|
||||
'envelope',
|
||||
'cog',
|
||||
'clipboard',
|
||||
'paper-plane-o',
|
||||
'television',
|
||||
'paper-plane',
|
||||
'newspaper',
|
||||
'bolt',
|
||||
'inbox',
|
||||
'floppy-o',
|
||||
'hdd-o',
|
||||
'dot-circle-o',
|
||||
'expeditedssl',
|
||||
'save',
|
||||
'hdd',
|
||||
'dot-circle',
|
||||
'user-lock',
|
||||
'terminal',
|
||||
'print',
|
||||
'map-signs',
|
||||
'project-diagram',
|
||||
'flag-checkered',
|
||||
'wrench',
|
||||
'laptop',
|
||||
'archive',
|
||||
'credit-card',
|
||||
'windows',
|
||||
'clock-o',
|
||||
'clock',
|
||||
'search',
|
||||
'flask',
|
||||
'gamepad',
|
||||
'trash-o',
|
||||
'sticky-note-o',
|
||||
'trash',
|
||||
'sticky-note',
|
||||
'ban',
|
||||
'question-circle',
|
||||
'cube',
|
||||
@@ -53,20 +53,20 @@ const IconMap = [
|
||||
'unlock-alt',
|
||||
'lock',
|
||||
'check',
|
||||
'pencil',
|
||||
'picture-o',
|
||||
'pencil-alt',
|
||||
'image',
|
||||
'book',
|
||||
'list-alt',
|
||||
'user-secret',
|
||||
'cutlery',
|
||||
'utensils',
|
||||
'home',
|
||||
'star-o',
|
||||
'star',
|
||||
'linux',
|
||||
'map-pin',
|
||||
'apple',
|
||||
'wikipedia-w',
|
||||
'usd',
|
||||
'calendar',
|
||||
'dollar-sign',
|
||||
'signature',
|
||||
'mobile'
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const KeeWebLogo =
|
||||
'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxkZWZzPjxyYWRpYWxHcmFkaWVudCBpZD0iYSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGN4PSI2NjMuMTMzIiBjeT0iMTM4LjYwMSIgcj0iODY0LjU2OCIgZng9IjY2My4xMzMiIGZ5PSIxMzguNjAxIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM5MmJmZjUiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMDQxYzgiLz48L3JhZGlhbEdyYWRpZW50PjwvZGVmcz48cGF0aCBkPSJNOTIzIDM1Ny42MjhjMC05Ljc4MiAwLTE5LjU2NC0uMDU2LTI5LjM0OC0uMDUtOC4yNC0uMTQ0LTE2LjQ4LS4zNjgtMjQuNzE2YTM1OC43MjQgMzU4LjcyNCAwIDAwLTQuNzM2LTUzLjgxMSAxODEuNDk3IDE4MS40OTcgMCAwMC0xNi44NTgtNTEuMTMzIDE3Mi4wNjYgMTcyLjA2NiAwIDAwLTc1LjIzNi03NS4yIDE4MS43MDIgMTgxLjcwMiAwIDAwLTUxLjE4OC0xNi44NiAzNTkuNjI2IDM1OS42MjYgMCAwMC01My44Mi00LjczYy04LjI0Mi0uMjMtMTYuNDg2LS4zMTgtMjQuNzM4LS4zNjgtOS43OC0uMDYyLTE5LjU2OC0uMDYyLTI5LjM1Ni0uMDYyTDU1MyAxMDFoLTg1bC0xMTEuNjM4LjRjLTkuOCAwLTE5LjYxNCAwLTI5LjQyLjA1NC04LjI2Mi4wNS0xNi41Mi4xNDQtMjQuNzc4LjM2OGEzNjAuODA4IDM2MC44MDggMCAwMC01My45NDggNC43NDIgMTgyLjI3MiAxODIuMjcyIDAgMDAtNTEuMjU4IDE2Ljg0OCAxNzIuMjMgMTcyLjIzIDAgMDAtNzUuMzg2IDc1LjE4OCAxODEuMjM4IDE4MS4yMzggMCAwMC0xNi45IDUxLjE2IDM1OC4zMTIgMzU4LjMxMiAwIDAwLTQuNzQyIDUzLjhjLS4yMjIgOC4yNC0uMzE4IDE2LjQ4LS4zNjggMjQuNzItLjA2IDkuNzg0LS41NjIgMjEuOTM2LS41NjIgMzEuNzJ2MTk2bC41MDggMTEyLjQyOGMwIDkuOCAwIDE5LjYuMDU0IDI5LjM4OC4wNSA4LjI1Mi4xNDYgMTYuNTAyLjM2OCAyNC43NWEzNTkuMDQ2IDM1OS4wNDYgMCAwMDQuNzQ2IDUzLjg4NSAxODEuNjY4IDE4MS42NjggMCAwMDE2Ljg5IDUxLjIwMSAxNzIuMzU2IDE3Mi4zNTYgMCAwMDc1LjM4MiA3NS4zMDIgMTgyLjEwNSAxODIuMTA1IDAgMDA1MS4yODggMTYuODggMzYwLjIwNyAzNjAuMjA3IDAgMDA1My45MjQgNC43MzZjOC4yNTguMjI0IDE2LjUxOC4zMTggMjQuNzguMzY4IDkuOC4wNiAxOS42MTIuMDU2IDI5LjQyLjA1NmgzMTAuMjg0YzkuNzg4IDAgMTkuNTc2IDAgMjkuMzY0LS4wNTQgOC4yNDQtLjA1IDE2LjQ4OC0uMTQ0IDI0LjczLS4zNjhhMzU5LjEgMzU5LjEgMCAwMDUzLjg0LTQuNzQgMTgxLjUxOCAxODEuNTE4IDAgMDA1MS4xNi0xNi44NzIgMTcyLjE5NSAxNzIuMTk1IDAgMDA3NS4yNC03NS4yOTggMTgxLjk1IDE4MS45NSAwIDAwMTYuODY2LTUxLjIzIDM1OS44MTUgMzU5LjgxNSAwIDAwNC43MzItNTMuODY0Yy4yMjQtOC4yNS4zMTgtMTYuNS4zNjgtMjQuNzUuMDYtOS44LjA1Ni0xOS42LjA1Ni0yOS4zODh2LTMxMC44IiBmaWxsPSJ1cmwoI2EpIi8+PHBhdGggZD0iTTk5IDEwMWg4MjR2ODI0SDk5eiIgZmlsbD0ibm9uZSIvPjxnIGZpbGw9IiNmZmYiPjxwYXRoIGQ9Ik0zNzkuMTk2IDgxMy4yNzNjLTUuOTM5LS4wMS0xMi41My0yLjUwMi0xOS4zMjYtNi4wNjctMTQuNDk3LTcuNjA1LTI0LjQzOS0xNy4yMi0xNi4xMzktMzMuMDQyTDU0NS4zOCAzODkuNzYxYTE1MTguMzEgMTUxOC4zMSAwIDAxLTUuNzU4LTIuOTk4Yy03Mi43MDktMzguMTQyLTEwNC45NjctOTQuNTM1LTc4Ljg1Mi0xNDQuMzE4IDMwLjAxOC01Ny4yMjMgMTIyLjk4NC02Ny42MDggMjA3LjY0Ny0yMy4xOTYgODQuNjYzIDQ0LjQxMyAxMjguOTYxIDEyNi44MDUgOTguOTQzIDE4NC4wMjgtMjUuODE1IDQ5LjIxMS05My4yMTcgNTMuNTk5LTE2MC45OSAxOC4wNDctMS44NjctLjk4LTQuODk2LTIuNTUzLTguMzI0LTQuMzNMMzk2LjIzMSA4MDEuNzA1Yy00LjQwOSA4LjQwNS0xMC4zMDQgMTEuNTc5LTE3LjAzNSAxMS41Njh6bS0zNy4xMjctODIuMzUybC0zLjgxNi0yLjAwMi0uODUtLjQ0NS01MS42NTItMjcuMDk2di0uMDAybC0xMi40NTEtNi41MzFhNC4zMSA0LjMxIDAgMDEtMS44MTUtNS44MmwxNi4zNDYtMzEuMTZhNC4zMTIgNC4zMTIgMCAwMTUuNzg5LTEuODMybC4wMzMuMDE1IDI0LjkwMyAxMy4wNjVhNC4zMTEgNC4zMTEgMCAwMDUuODItMS44MTdsMTYuMzQ4LTMxLjE2YTQuMzEgNC4zMSAwIDAwLTEuODE3LTUuODJsLTEyLjQ1MS02LjUzMy0xMi40NTEtNi41MzJhNC4zMSA0LjMxIDAgMDEtMS44MTUtNS44MmwxNi4zNDYtMzEuMTYyYTQuMzEgNC4zMSAwIDAxNS44Mi0xLjgxNGwxMi40NTMgNi41MzEgMzIuNDExIDE3LjAwMiAyMC4wODkgMTAuNTM5LjAyNi4wMTQgMy43OTMgMS45OS0xNy4xNjggMzIuNzI2LTEuMTgyIDIuMjUyYS4wMDguMDA4IDAgMDEtLjAwMi4wMDRsLTI0LjM1NyA0Ni40My0yLjQ3NyA0LjcyMS0xMy44NjkgMjYuNDM5LS4wMS4wMTgtMS45OTQgMy44ek00MjMuNjkgNTc1LjMzbC0zLjgxOC0yLjAwNC0xOC4xMzUtOS41MTQtMTQuMzE4LTcuNTEyYTQuMzEgNC4zMSAwIDAxLTEuODE3LTUuODJsMTYuMzQ4LTMxLjE2YTQuMzExIDQuMzExIDAgMDE1LjgyLTEuODE3bDE0LjMxOSA3LjUxMiAxOC4xMzYgOS41MTQuMDI2LjAxMyAzLjc5MyAxLjk5MS0yLjAwNCAzLjgxOC04LjE3MiAxNS41OC04LjE3NCAxNS41OC0uMDEuMDE2LTEuOTk0IDMuODAzem0yNjYuODEzLTE4NC40MjZjMTAuNzUzLjAyIDE5LjE2My00LjMxMiAyNC43NS0xNC45NjMgMTcuMDI4LTMyLjQ2LTE0LjQ3Mi04Mi41MzgtNzAuMzU2LTExMS44NTQtNTUuODg0LTI5LjMxNi0xMTQuOTkxLTI2Ljc2OC0xMzIuMDE5IDUuNjkyLTE3LjAyOCAzMi40NTkgMzAuMzM0IDUzLjc3MiA4Ni4yMTkgODMuMDg4IDM3LjU0NyAxOS42OTYgNjkuMzg4IDM3Ljk5NCA5MS40MDYgMzguMDM3eiIvPjxwYXRoIGQ9Ik00MjMuNjkxIDU3NS4zMjlsMTkuMDkxIDEwLjAxNSAyMC4zNTMtMzguNzk3LTE5LjA5MS0xMC4wMTV6TTM0Mi4wNyA3MzAuOTIybDE5LjA5MSAxMC4wMTUgNjEuMDU3LTExNi4zOTItMTkuMDkxLTEwLjAxNXoiIGZpbGwtb3BhY2l0eT0iLjIiLz48L2c+PC9zdmc+';
|
||||
@@ -0,0 +1,5 @@
|
||||
const KnownAppVersions = {
|
||||
KeePassXC: '2.6.4'
|
||||
};
|
||||
|
||||
export { KnownAppVersions };
|
||||
@@ -6,17 +6,34 @@ const Links = {
|
||||
BetaWebApp: 'https://beta.keeweb.info',
|
||||
License: 'https://github.com/keeweb/keeweb/blob/master/LICENSE',
|
||||
LicenseApache: 'https://opensource.org/licenses/Apache-2.0',
|
||||
UpdateDesktop: 'https://github.com/keeweb/keeweb/releases/download/v{ver}/UpdateDesktop.zip',
|
||||
LicenseLinkCCBY40: 'https://creativecommons.org/licenses/by/4.0/',
|
||||
UpdateBasePath: 'https://github.com/keeweb/keeweb/releases/download/v{ver}/',
|
||||
ReleaseNotes: 'https://github.com/keeweb/keeweb/blob/master/release-notes.md#release-notes',
|
||||
SelfHostedDropbox: 'https://github.com/keeweb/keeweb#self-hosting',
|
||||
Manifest: 'https://app.keeweb.info/manifest.appcache',
|
||||
UpdateJson: 'https://app.keeweb.info/update.json',
|
||||
AutoType: 'https://github.com/keeweb/keeweb/wiki/Auto-Type',
|
||||
AutoTypeMacOS: 'https://github.com/keeweb/keeweb/wiki/Auto-Type#macos',
|
||||
Translation: 'https://keeweb.oneskyapp.com/',
|
||||
Donation: 'https://opencollective.com/keeweb#support',
|
||||
Plugins: 'https://plugins.keeweb.info',
|
||||
PluginDevelopStart: 'https://github.com/keeweb/keeweb/wiki/Plugins',
|
||||
YubiKeyManual: 'https://github.com/keeweb/keeweb/wiki/YubiKey',
|
||||
YubiKeyManagerInstall: 'https://github.com/Yubico/yubikey-manager#installation'
|
||||
YubiKeyManagerInstall: 'https://github.com/Yubico/yubikey-manager#installation',
|
||||
HaveIBeenPwned: 'https://haveibeenpwned.com',
|
||||
HaveIBeenPwnedPrivacy: 'https://haveibeenpwned.com/Passwords',
|
||||
ExtensionHelpForOtherBrowsers:
|
||||
'https://github.com/keeweb/keeweb/wiki/Browser-AutoFill#other-browsers',
|
||||
KWCForChrome:
|
||||
'https://chrome.google.com/webstore/detail/keeweb-connect/pikpfmjfkekaeinceagbebpfkmkdlcjk',
|
||||
KWCForFirefox: 'https://addons.mozilla.org/firefox/addon/keeweb-connect/',
|
||||
KWCForEdge:
|
||||
'https://microsoftedge.microsoft.com/addons/detail/keewebconnect/nmggpehkjmeaeocmaijenpejbepckinm',
|
||||
KWCForSafari: 'https://apps.apple.com/app/keeweb-connect/id1565748094',
|
||||
KPXCForChrome:
|
||||
'https://chrome.google.com/webstore/detail/keepassxc-browser/oboonakemofpalcgghocfoadofidjkkk',
|
||||
KPXCForFirefox: 'https://addons.mozilla.org/firefox/addon/keepassxc-browser/',
|
||||
KPXCForEdge:
|
||||
'https://microsoftedge.microsoft.com/addons/detail/keepassxcbrowser/pdffhmdngciaglkoonimfcmckehcpafo'
|
||||
};
|
||||
|
||||
export { Links };
|
||||
|
||||
@@ -3,7 +3,8 @@ const RuntimeInfo = {
|
||||
beta: !!'@@BETA',
|
||||
buildDate: '@@DATE',
|
||||
commit: '@@COMMIT',
|
||||
devMode: '@@DEVMODE'
|
||||
devMode: '@@DEVMODE',
|
||||
appleTeamId: '@@APPLE_TEAM_ID'
|
||||
};
|
||||
|
||||
export { RuntimeInfo };
|
||||
|
||||
@@ -16,7 +16,10 @@ const Timeouts = {
|
||||
ExternalDeviceReconnect: 3000,
|
||||
ExternalDeviceAfterReconnect: 1000,
|
||||
FieldLabelDoubleClick: 300,
|
||||
NativeModuleHostRestartTime: 3000
|
||||
NativeModuleHostRestartTime: 3000,
|
||||
FastAnimation: 100,
|
||||
AutoTypeCopyPaste: 300,
|
||||
KeeWebConnectRequest: 60000
|
||||
};
|
||||
|
||||
export { Timeouts };
|
||||
|
||||
@@ -25,7 +25,7 @@ const Resizable = {
|
||||
if (dragInfo.auto !== undefined) {
|
||||
this.$el.css(dragInfo.prop, dragInfo.auto);
|
||||
} else {
|
||||
this.$el.css(dragInfo.prop, 'auto');
|
||||
this.$el.css(dragInfo.prop, '');
|
||||
}
|
||||
this.fixSize(dragInfo);
|
||||
this.emit('view-resize', null);
|
||||
@@ -55,7 +55,7 @@ const Resizable = {
|
||||
const propLower = prop.toLowerCase();
|
||||
const min = this.getSizeProp('min' + prop);
|
||||
const max = this.getSizeProp('max' + prop);
|
||||
const auto = this.getSizeProp('auto' + prop) || 'auto';
|
||||
const auto = this.getSizeProp('auto' + prop);
|
||||
const startSize = this.$el[propLower]();
|
||||
return { startSize, prop: propLower, min, max, auto };
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@ class View extends EventEmitter {
|
||||
model = undefined;
|
||||
options = {};
|
||||
views = {};
|
||||
hidden = false;
|
||||
hidden = undefined;
|
||||
removed = false;
|
||||
modal = undefined;
|
||||
eventListeners = {};
|
||||
@@ -239,6 +239,10 @@ class View extends EventEmitter {
|
||||
if (visible === undefined) {
|
||||
visible = this.hidden;
|
||||
}
|
||||
if (this.hidden === !visible) {
|
||||
this.debugLogger?.debug('Toggle: noop', visible);
|
||||
return;
|
||||
}
|
||||
this.hidden = !visible;
|
||||
if (this.modal) {
|
||||
if (visible) {
|
||||
@@ -247,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);
|
||||
@@ -255,10 +258,11 @@ class View extends EventEmitter {
|
||||
Tip.hideTips(this.el);
|
||||
}
|
||||
}
|
||||
this.emit(visible ? 'show' : 'hide');
|
||||
}
|
||||
|
||||
isHidden() {
|
||||
return this.hidden;
|
||||
return !!this.hidden;
|
||||
}
|
||||
|
||||
isVisible() {
|
||||
|
||||
@@ -4,4 +4,3 @@ import 'hbs-helpers/ifeq';
|
||||
import 'hbs-helpers/ifneq';
|
||||
import 'hbs-helpers/ifemptyoreq';
|
||||
import 'hbs-helpers/res';
|
||||
import 'hbs-helpers/svg';
|
||||
|
||||
@@ -12,10 +12,14 @@ Handlebars.registerHelper('res', function (key, options) {
|
||||
return value;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('Res', (key) => {
|
||||
Handlebars.registerHelper('Res', (key, options) => {
|
||||
let value = Locale[key];
|
||||
if (value) {
|
||||
value = value[0].toUpperCase() + value.substr(1);
|
||||
const ix = value.indexOf('{}');
|
||||
if (ix >= 0) {
|
||||
value = value.replace('{}', options.fn(this));
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import Handlebars from 'hbs';
|
||||
|
||||
Handlebars.registerHelper('svg', (name, cls) => {
|
||||
const icon = require(`svg/${name}.svg`).default;
|
||||
if (typeof cls === 'string') {
|
||||
return `<svg class="${cls}"` + icon.substr(4);
|
||||
}
|
||||
return icon;
|
||||
});
|
||||
@@ -1,50 +1,4 @@
|
||||
{
|
||||
"months": [
|
||||
"Januar",
|
||||
"Februar",
|
||||
"März",
|
||||
"April",
|
||||
"Mai",
|
||||
"Juni",
|
||||
"Juli",
|
||||
"August",
|
||||
"September",
|
||||
"Oktober",
|
||||
"November",
|
||||
"Dezember"
|
||||
],
|
||||
"monthsShort": [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mär",
|
||||
"Apr",
|
||||
"Mai",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Okt",
|
||||
"Nov",
|
||||
"Dez"
|
||||
],
|
||||
"weekdays": [
|
||||
"Sonntag",
|
||||
"Montag",
|
||||
"Dienstag",
|
||||
"Mittwoch",
|
||||
"Donnerstag",
|
||||
"Freitag",
|
||||
"Samstag"
|
||||
],
|
||||
"weekdaysShort": [
|
||||
"So",
|
||||
"Mo",
|
||||
"Di",
|
||||
"Mi",
|
||||
"Do",
|
||||
"Fr",
|
||||
"Sa"
|
||||
],
|
||||
"retToApp": "zurück zur App",
|
||||
"name": "Name",
|
||||
"icon": "Icon",
|
||||
@@ -56,10 +10,11 @@
|
||||
"notes": "Notizen",
|
||||
"entry": "Eintrag",
|
||||
"group": "Gruppe",
|
||||
"noTitle": "Keinen Titel",
|
||||
"noTitle": "Kein Titel",
|
||||
"or": "oder",
|
||||
"history": "Verlauf",
|
||||
"template": "Template",
|
||||
"template": "Vorlage",
|
||||
"templates": "Vorlagen",
|
||||
"notImplemented": "Nicht implementiert",
|
||||
"saveChanges": "Änderungen speichern",
|
||||
"discardChanges": "Änderungen verwerfen",
|
||||
@@ -72,6 +27,15 @@
|
||||
"shiftKey": "Umschalt",
|
||||
"altKey": "Alt",
|
||||
"error": "Fehler",
|
||||
"oneMinute": "Eine Minute",
|
||||
"minutes": "{} Minuten",
|
||||
"oneHour": "Eine Stunde",
|
||||
"hours": "{} Stunden",
|
||||
"oneDay": "Ein Tag",
|
||||
"days": "{} Tage",
|
||||
"oneWeek": "Eine Woche",
|
||||
"oneMonth": "Ein Monat",
|
||||
"oneYear": "Ein Jahr",
|
||||
"cache": "Cache",
|
||||
"file": "Datei",
|
||||
"device": "Gerät",
|
||||
@@ -86,13 +50,29 @@
|
||||
"menuSetAbout": "Info",
|
||||
"menuSetDevices": "Geräte",
|
||||
"menuAlertNoTags": "Keine Tags",
|
||||
"menuAlertNoTagsBody": "Neue Tags können durch das Bearbeiten von Einträgen (Abschnitt “Tags”) hinzugefügt werden.",
|
||||
"menuEmptyTrash": "Leere Papierkorb",
|
||||
"menuAlertNoTagsBody": "Neue Tags können durch das Bearbeiten von Einträgen (Abschnitt „Tags“) hinzugefügt werden.",
|
||||
"menuEmptyTrash": "Papierkorb leeren",
|
||||
"menuEmptyTrashAlert": "Papierkorb leeren?",
|
||||
"menuEmptyTrashAlertBody": "Die Einträge können nicht wiederhergestellt werden",
|
||||
"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",
|
||||
@@ -109,21 +89,21 @@
|
||||
"genLen": "Länge",
|
||||
"genNewPass": "Neues Passwort",
|
||||
"genPresetDefault": "Standardeinstellung",
|
||||
"genPresetDerived": "unverändert",
|
||||
"genPresetDerived": "Wie das alte Passwort",
|
||||
"genPresetPronounceable": "aussprechbar",
|
||||
"genPresetMed": "durchschnittlich lang",
|
||||
"genPresetMed": "Durchschnittliche Länge",
|
||||
"genPresetLong": "lang",
|
||||
"genPresetPin4": "4-stellige PIN",
|
||||
"genPresetMac": "MAC-Adresse",
|
||||
"genPresetHash128": "128-Bit Hash",
|
||||
"genPresetHash256": "256-Bit Hash",
|
||||
"genPresetHash128": "128-Bit-Hash",
|
||||
"genPresetHash256": "256-Bit-Hash",
|
||||
"genHidePass": "Passwort ausblenden",
|
||||
"genShowPass": "Passwort einblenden",
|
||||
"grpTitle": "Gruppe",
|
||||
"grpSearch": "Suche für Einträge in dieser Gruppe aktivieren",
|
||||
"grpAutoType": "Auto-Type aktivieren",
|
||||
"grpAutoTypeSeq": "Auto-Type Abfolge",
|
||||
"grpAutoTypeSeqDefault": "Standard Auto-Type Abfolge verwenden",
|
||||
"grpAutoTypeSeq": "Auto-Type-Sequenz",
|
||||
"grpAutoTypeSeqDefault": "Standard-Auto-Type-Sequenz verwenden",
|
||||
"grpTrash": "Gruppe und alle enthaltenen Einträge löschen",
|
||||
"tagTitle": "Tag",
|
||||
"tagTrash": "Diesen Tag von allen Einträgen entfernen",
|
||||
@@ -131,14 +111,14 @@
|
||||
"tagTrashQuestion": "Diesen Tag von allen Einträgen entfernen?",
|
||||
"tagTrashQuestionBody": "Der Tag wird von allen Einträgen entfernt. Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"tagExists": "Tag existiert bereits",
|
||||
"tagExistsBody": "Ein Tag mit diesem Namen ist bereits vorhanden. Bitte wählen Sie einen anderen Namen.",
|
||||
"tagExistsBody": "Ein Tag mit diesem Namen ist bereits vorhanden. Bitte einen anderen Namen wählen.",
|
||||
"tagBadName": "Ungültiger Name",
|
||||
"tagBadNameBody": "Der Tag-Name darf nicht die folgenden Zeichen enthalten: {}. Bitte entfernen Sie sie.",
|
||||
"tagBadNameBody": "Der Tag-Name darf folgende Zeichen nicht enthalten: {}. Bitte diese entfernen.",
|
||||
"genPsTitle": "Generator-Voreinstellungen",
|
||||
"genPsCreate": "Neue Voreinstellung",
|
||||
"genPsDelete": "Voreinstellung löschen",
|
||||
"genPsNew": "Voreinstellung",
|
||||
"genPsEnabled": "In Voreinstellungs-Liste anzeigen",
|
||||
"genPsEnabled": "In Voreinstellungsliste anzeigen",
|
||||
"genPsDefault": "Standardmäßig ausgewählt",
|
||||
"genPsDefaultLength": "Standardlänge",
|
||||
"genPsUpper": "Lateinische Großbuchstaben",
|
||||
@@ -151,16 +131,16 @@
|
||||
"genPsInclude": "Zusätzliche Symbole angeben",
|
||||
"genPsExample": "Beispiel für ein generiertes Passwort",
|
||||
"genPsPattern": "Muster",
|
||||
"genPsPatternHelp": "Mit Mustern können eigene Regeln zur Auswahl von Zeichen festgelegt werden. Beispielsweise werden mit 1-AA Passwörter generiert, die mit einer Ziffer starten, gefolgt von einem Bindestrich und zwei Großbuchstaben. Die folgenden Symbole stehen zur Verfügung:",
|
||||
"genPsPatternHelp": "Mit Mustern können eigene Regeln zur Auswahl von Zeichen festgelegt werden. Beispielsweise werden mit 1-AA Passwörter generiert, die mit einer Ziffer starten, gefolgt von einem Bindestrich und zwei Großbuchstaben. Folgende Symbole stehen zur Verfügung:",
|
||||
"genPsAllRanges": "Alle Zeichen",
|
||||
"genPsIncluded": "Zusätzliche oben hinzugefügte Zeichen",
|
||||
"keyChangeTitleRemote": "Hauptschlüssel geändert",
|
||||
"keyChangeMessageRemote": "Der Hauptschlüssel für diese Datenbank wurde geändert. Bitte geben Sie das neue Passwort ein.",
|
||||
"keyChangeMessageRemote": "Der Hauptschlüssel für diese Datenbank wurde geändert. Bitte das neue Passwort eingeben.",
|
||||
"keyChangeTitleExpired": "Hauptschlüssel abgelaufen",
|
||||
"keyChangeMessageExpired": "Der Hauptschlüssel für diese Datenkbank ist abgelaufen. Bitte geben Sie einen neuen Schlüssel ein.",
|
||||
"keyChangeRepeatPassword": "Passwort (Wiederholung)",
|
||||
"keyEnter": "Enter",
|
||||
"iconFavTitle": "Website-Icon herunterladen und verwenden",
|
||||
"iconFavTitle": "Website-Symbol herunterladen und verwenden",
|
||||
"iconSelCustom": "Eigenes Icon auswählen",
|
||||
"listEmptyTitle": "Leer",
|
||||
"listEmptyAdd": "Zum Anlegen neuer Einträge den obigen {} Button benutzen",
|
||||
@@ -168,9 +148,9 @@
|
||||
"listNoWebsite": "keine Website",
|
||||
"listNoUser": "kein Benutzer",
|
||||
"listNoAttachments": "keine Anhänge",
|
||||
"listAddTemplateHeader": "Templates",
|
||||
"listAddTemplateBody1": "Templates ermöglichen es, Einträge mit nur einem Klick zu erstellen. Fügen Sie etwas zum Template-Eintrag hinzu und klicken Sie auf {}, um dieses Template zu benutzen.",
|
||||
"listAddTemplateBody2": "Sie können Ihre Templates in der Gruppe “{}” finden.",
|
||||
"listAddTemplateHeader": "Vorlagen",
|
||||
"listAddTemplateBody1": "Vorlagen ermöglichen es, Einträge mit nur einem Klick zu erstellen. Fügen Sie etwas zum Vorlage-Eintrag hinzu und klicken Sie auf {}, um diese Vorlage zu verwenden.",
|
||||
"listAddTemplateBody2": "Sie können Ihre Vorlagen in der Gruppe “{}” finden.",
|
||||
"searchAddNew": "Hinzufügen",
|
||||
"searchSort": "Sortierung",
|
||||
"searchCreated": "Erstelldatum",
|
||||
@@ -202,17 +182,17 @@
|
||||
"openKeyFile": "Schlüsseldatei",
|
||||
"openKeyFileDropbox": "(von Dropbox)",
|
||||
"openDropHere": "Dateien hier ablegen",
|
||||
"openFailedRead": "Konnte die Datei nicht lesen",
|
||||
"openFailedRead": "Datei konnte nicht gelesen werden",
|
||||
"openNothingFound": "Nichts gefunden",
|
||||
"openNothingFoundBody": "Es konnten keine kompatiblen Dateien gefunden werden.",
|
||||
"openSelectFile": "Datei auswählen",
|
||||
"openSelectFileBody": "Wählen Sie eine Datei zum Öffnen aus",
|
||||
"openPassFor": "Passwort für",
|
||||
"openRemoveLastQuestion": "Lokale Datei löschen?",
|
||||
"openRemoveLastQuestionBody": "Die Datei, die Sie löschen möchten, ist nur innerhalb der App gespeichert. Möchten Sie sie unwiederbringlich löschen?",
|
||||
"openRemoveLastQuestionBody": "Die Datei, die Sie löschen möchten, ist nur innerhalb der App gespeichert. Möchten Sie diese unwiederbringlich löschen?",
|
||||
"openRemoveLastQuestionModBody": "Die Datei, die Sie löschen möchten, beinhaltet lokale Änderungen. Möchten Sie diese Änderungen verwerfen und die Datei löschen?",
|
||||
"openLocalFile": "Lokale Datei",
|
||||
"openLocalFileBody": "Sie sind dabei, eine Datei zu öffnen, die innerhalb der App gespeichert wird. Wenn Sie an dieser Datei Änderungen vornehmen, werden diese nicht im Dateisystem gespeichert. Um die Datei mit den Änderungen zu erhalten, können Sie sie aus den Einstellungen heraus exportieren.",
|
||||
"openLocalFileBody": "Sie sind dabei, eine Datei zu öffnen, die innerhalb der App gespeichert wird. Wenn Sie an dieser Datei Änderungen vornehmen, werden diese nicht im Dateisystem gespeichert. Um die Datei mit den Änderungen zu erhalten, können Sie diese aus den Einstellungen heraus exportieren.",
|
||||
"openLocalFileDontShow": "Nicht wieder anzeigen",
|
||||
"openWrongFile": "Inkompatibles Dateiformat",
|
||||
"openWrongFileBody": "Dieses Dateiformat ist nicht kompatibel. Die App unterstützt KeePass-Datenbanken im kdbx-Format.",
|
||||
@@ -221,10 +201,10 @@
|
||||
"openUrl": "URL",
|
||||
"openUrlDesc": "https://server/pfad/datei.kdbx, oder einfach datei.kdbx",
|
||||
"openUser": "Benutzername",
|
||||
"openUserDesc": "WebDAV Server Benutzername (falls erforderlich)",
|
||||
"openUserDesc": "WebDAV-Server-Benutzername (falls erforderlich)",
|
||||
"openUserPlaceholder": "kein Benutzername",
|
||||
"openPass": "Passwort",
|
||||
"openPassDesc": "WebDAV Server-Passwort (nicht das KDBX-Datei-Passwort)",
|
||||
"openPassDesc": "WebDAV-Server-Passwort (nicht das KDBX-Datei-Passwort)",
|
||||
"openPassPlaceholder": "kein Passwort",
|
||||
"openConfigError": "Fehler: {}",
|
||||
"openConfigErrorNotFound": "Datei nicht gefunden",
|
||||
@@ -238,12 +218,13 @@
|
||||
"openChalRespLoading": "Liste mit YubiKeys wird geladen",
|
||||
"openChalRespSelectYubiKey": "Wählen Sie den YubiKey aus, den Sie benutzen wollen",
|
||||
"openChalRespErrorEmpty": "Keine YubiKeys gefunden.",
|
||||
"openChalRespErrorEmptyMac": "Verwenden Sie dieses Feature zum ersten Mal auf MacOS? KeeWeb muss in Ihren Sicherheitseinstellung im Input-Monitoring-Abschnitt hinzugefügt werden.",
|
||||
"detAttDownload": "Umschalttaste + Klick auf den Anhang-Button zum Herunterladen oder",
|
||||
"detAttDelToRemove": "Backspace zum entfernen",
|
||||
"detAttSave": "Speichern unter...",
|
||||
"detAttDelToRemove": "Backspace zum Entfernen",
|
||||
"detAttSave": "Speichern unter …",
|
||||
"detEmpty": "Ihre Passwörter werden hier angezeigt",
|
||||
"detGroupRestore": "Ziehen Sie diese Gruppe in eine andere Gruppe als den Papierkorb, um sie wiederherzustellen.",
|
||||
"detHistoryClickPoint": "Klicken Sie auf einen der Punkte im Verlauf um den entsprechenden Zustand des Eintrags anzuzeigen",
|
||||
"detHistoryClickPoint": "Klicken Sie auf einen der Punkte im Verlauf, um den entsprechenden Zustand des Eintrags anzuzeigen",
|
||||
"detHistoryReturn": "zurück zum Eintrag",
|
||||
"detHistoryRevert": "Auf diese Version zurücksetzen",
|
||||
"detHistoryDel": "Version löschen",
|
||||
@@ -264,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",
|
||||
@@ -277,7 +258,7 @@
|
||||
"detAttachments": "Anhänge",
|
||||
"detDelFromTrash": "Aus dem Papierkorb löschen?",
|
||||
"detDelFromTrashBody": "Es wird keine Möglichkeit zur Wiederherstellung geben.",
|
||||
"detDelFromTrashBodyHint": "Um alle Dateien aus dem Papierkorb zu entfernen, klicken Sie auf das “Papierkorb” leeren-Icon im Papierkorb-Menüpunkt.",
|
||||
"detDelFromTrashBodyHint": "Um alle Dateien aus dem Papierkorb zu entfernen, klicken Sie auf das „Papierkorb leeren“-Symbol im Papierkorb-Menüpunkt.",
|
||||
"detDelToTrash": "Diesen Eintrag löschen?",
|
||||
"detDelToTrashBody": "Der Eintrag wird in den Papierkorb verschoben.",
|
||||
"detFieldCopied": "Kopiert",
|
||||
@@ -299,12 +280,12 @@
|
||||
"detAutoType": "Auto-Type",
|
||||
"detAutoTypeSettings": "Auto-Type Einstellungen",
|
||||
"detAutoTypeEnabled": "Auto-Type für diesen Eintrag aktivieren",
|
||||
"detAutoTypeSequence": "Tastensequenz",
|
||||
"detAutoTypeSequence": "Tastenfolge",
|
||||
"detAutoTypeInput": "Eingabe",
|
||||
"detAutoTypeShortcutsDesc": "{} oder {} während die App inaktiv ist",
|
||||
"detAutoTypeObfuscation": "Echte Tastenanschläge mit zufälligen mischen",
|
||||
"detAutoTypeWindow": "Fenster",
|
||||
"detAutoTypeInputWindow": "Fenster-Titel",
|
||||
"detAutoTypeInputWindow": "Fenstertitel",
|
||||
"detSetupOtpAlert": "QR-Code einscannen",
|
||||
"detSetupOtpAlertBody": "Bitte kopieren Sie den QR-Code, der auf der Autorisierungsseite angezeigt wird.",
|
||||
"detSetupOtpAlertBody1": "1. Gehen Sie zur Autorisierungsseite",
|
||||
@@ -317,25 +298,35 @@
|
||||
"detSetupOtpAlertBodyWith": "mit {}",
|
||||
"detOtpImageError": "Fehler beim Einlesen des Bildes",
|
||||
"detOtpImageErrorBody": "Entschuldigung, wir konnten das Bildformat nicht lesen. Bitte kontaktieren Sie den Hersteller mit Einzelheiten zu diesem Fehler.",
|
||||
"detOtpImageReading": "QR-Code wird eingelesen...",
|
||||
"detOtpImageReading": "QR-Code wird eingelesen …",
|
||||
"detOtpQrError": "QR-Code Lesefehler",
|
||||
"detOtpQrErrorBody": "Der QR-Code konnte leider nicht eingelesen werden. Bitte versuchen Sie es noch einmal oder kontaktieren Sie den Hersteller mit Einzelheiten zum aufgetretenen Fehler.",
|
||||
"detOtpQrWrong": "Fehlerhafter QR-Code",
|
||||
"detOtpQrWrongBody": "Ihr QR-Code wurde erfolgreich eingelesen, enthält jedoch keinen Einmal-Code.",
|
||||
"detOtpField": "Einmal-Code",
|
||||
"detOtpClickToTouch": "Zum Erzeugen hier klicken",
|
||||
"detOtpGenerating": "Wird erzeugt...",
|
||||
"detOtpGenerating": "Wird erzeugt …",
|
||||
"detOtpTouch": "Berühren Sie Ihren {}",
|
||||
"detLockField": "Dieses Feld sperren, damit der Inhalt nicht durchsuchbar ist und angezeigt wird. Um ihn anzuzeigen, muss er angeklickt werden.",
|
||||
"detUnlockField": "Dieses Feld entsperren. Dies führt dazu, dass der Inhalt direkt angezeigt wird und durchsuchbar ist.",
|
||||
"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",
|
||||
"autoTypeLink": "mehr...",
|
||||
"autoTypeError": "Auto-Type Fehler",
|
||||
"autoTypeLink": "mehr …",
|
||||
"autoTypeError": "Auto-Type-Fehler",
|
||||
"autoTypeErrorGeneric": "Während der Ausführung von Auto-Type ist ein Fehler aufgetreten: {}",
|
||||
"autoTypeErrorGlobal": "Um globale Tastenkürzel zu nutzen, wechseln Sie bitte zu der Anwendung, in der Sie das Passwort eingeben wollen.",
|
||||
"autoTypeErrorNotInstalled": "{} ist nicht installiert",
|
||||
@@ -343,7 +334,7 @@
|
||||
"autoTypeMsgNoWindow": "Wir konnten den Titel des aktiven Fensters leider nicht auslesen. Fangen Sie einfach an zu tippen, um nach dem richtigen Eintrag zu suchen.",
|
||||
"autoTypeMsgMatchedByWindow": "Passwort auswählen für {}",
|
||||
"autoTypeNoMatches": "keine passenden Einträge",
|
||||
"autoTypeSelectionHint": "Die Auto-Type Abfolge eintippen",
|
||||
"autoTypeSelectionHint": "Die Auto-Type-Sequenz eintippen",
|
||||
"autoTypeSelectionHintAction": "Nur das Passwort eintippen",
|
||||
"autoTypeSelectionHintOpt": "Nur den Benutzernamen eintippen",
|
||||
"autoTypeSelectionHintShift": "Andere Felder",
|
||||
@@ -361,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.",
|
||||
@@ -375,12 +368,12 @@
|
||||
"setGenNewVersion": "Eine neue Version der App ist verfügbar und wurde heruntergeladen",
|
||||
"setGenReleaseNotes": "Versionshinweise anzeigen",
|
||||
"setGenReloadToUpdate": "Seite aktualisieren, um das Update durchzuführen",
|
||||
"setGenUpdateManual": "Eine neue Version ist verfügbar. Sie wird nach Updates suchen und sie automatisch installieren, ein automatisches Update von Ihrer Version ist aber nicht möglich.",
|
||||
"setGenUpdateManual": "Eine neue Version ist verfügbar. Sie wird nach Updates suchen und diese automatisch installieren, ein automatisches Update Ihrer Version ist aber nicht möglich.",
|
||||
"setGenDownloadUpdate": "Update herunterladen",
|
||||
"setGenUpdateAuto": "Automatisch herunterladen und installieren",
|
||||
"setGenUpdateCheck": "Auf Updates überprüfen aber nicht installieren",
|
||||
"setGenUpdateCheck": "Auf Updates überprüfen, aber nicht installieren",
|
||||
"setGenNoUpdate": "Nie nach Updates suchen",
|
||||
"setGenUpdateChecking": "Suche nach Updates",
|
||||
"setGenUpdateChecking": "Updates werden gesucht",
|
||||
"setGenCheckUpdate": "Nach Updates suchen",
|
||||
"setGenErrorChecking": "Updatesuche fehlgeschlagen",
|
||||
"setGenLastCheckSuccess": "Letzte erfolgreiche Überprüfung am {}",
|
||||
@@ -388,37 +381,49 @@
|
||||
"setGenCheckedAt": "Überprüft am",
|
||||
"setGenLatestVer": "Sie haben die aktuellste Version",
|
||||
"setGenNewVer": "Neue Version {} verfügbar, veröffentlicht am",
|
||||
"setGenDownloadingUpdate": "Update wird heruntergeladen...",
|
||||
"setGenExtractingUpdate": "Update wird entpackt...",
|
||||
"setGenDownloadingUpdate": "Update wird heruntergeladen …",
|
||||
"setGenExtractingUpdate": "Update wird entpackt …",
|
||||
"setGenCheckErr": "Beim Herunterladen der neuen Version ist ein Fehler aufgetreten",
|
||||
"setGenNeverChecked": "Nie nach Updates gesucht",
|
||||
"setGenRestartToUpdate": "App neu starten, um das Update durchzuführen",
|
||||
"setGenDownloadAndRestart": "Update herunterladen und App neu starten",
|
||||
"setGenAppearance": "Oberfläche",
|
||||
"setGenTheme": "Theme",
|
||||
"setGenThemeFb": "Flat blue",
|
||||
"setGenThemeDb": "Dark brown",
|
||||
"setGenThemeWh": "White",
|
||||
"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",
|
||||
"setGenThemeHc": "High contrast",
|
||||
"setGenThemeSd": "Solarized dark",
|
||||
"setGenThemeSl": "Solarized light",
|
||||
"setGenThemeMacDark": "macOS Dunkel",
|
||||
"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",
|
||||
"setGenFontSizeNormal": "Standard",
|
||||
"setGenFontSizeLarge": "Groß",
|
||||
"setGenFontSizeLargest": "Größte",
|
||||
"setGenTitlebarStyle": "Fenster-Stil (erfordert Neustart des Programms)",
|
||||
"setGenTitlebarStyle": "Fensterstil (erfordert Neustart des Programms)",
|
||||
"setGenTitlebarStyleDefault": "Standard",
|
||||
"setGenTitlebarStyleHidden": "Eigener Titel",
|
||||
"setGenTitlebarStyleHiddenInset": "Eigener Titel, verschiebbares Fenster",
|
||||
"setGenShowSubgroups": "Einträge aus allen Untergruppen anzeigen",
|
||||
"setGenTableView": "Einträge in Tabellenansicht anzeigen",
|
||||
"setGenColorfulIcons": "Eigene Icons in der Listenansicht farbig anzeigen",
|
||||
"setGenColorfulIcons": "Eigene Symbole in der Listenansicht farbig anzeigen",
|
||||
"setGenUseMarkdown": "Markdown in Notizen",
|
||||
"setGenUseGroupIconForEntries": "Für neue Einträge automatisch das Gruppen-Icon verwenden",
|
||||
"setGenUseGroupIconForEntries": "Für neue Einträge automatisch das Gruppensymbol verwenden",
|
||||
"setGenDirectAutotype": "Wenn nur ein passender Eintrag gefunden wird, diesen automatisch für Auto-Type auswählen",
|
||||
"setGenFunction": "Arbeitsweise",
|
||||
"setGenAutoSyncOnClose": "Beim Schließen speichern und synchronisieren",
|
||||
@@ -446,22 +451,38 @@
|
||||
"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",
|
||||
"setGenTryBeta": "Beta-Version bis zum Schließen der App ausprobieren",
|
||||
"setGenTryBeta": "Betaversion bis zum Schließen der App ausprobieren",
|
||||
"setGenTryBetaWarning": "Ungespeicherte Dateien",
|
||||
"setGenTryBetaWarningBody": "Bitte speichern Sie alle Dateien und Klicken Sie erneut auf diesen Button",
|
||||
"setGenTryBetaWarningBody": "Bitte speichern Sie alle Dateien und klicken erneut auf diese Schaltfläche",
|
||||
"setGenShowAppLogs": "App-Logs anzeigen",
|
||||
"setGenReloadApp": "App neu laden",
|
||||
"setGenFieldLabelDblClickAutoType": "Auto-Type durch anklicken von Beschriftungen aktivieren",
|
||||
"setGenFieldLabelDblClickAutoType": "Auto-Type durch Anklicken von Beschriftungen aktivieren",
|
||||
"setGenTouchId": "Fingerabdruck",
|
||||
"setGenTouchIdDisabled": "Fingerabdruck nicht benutzen",
|
||||
"setGenTouchIdMemory": "Nur mit Fingerabdruck entsperren, wenn KeeWeb aktiv ist",
|
||||
"setGenTouchIdFile": "Benutze immer den Fingerabdruck anstatt das Master-Passwort",
|
||||
"setGenTouchIdPass": "Benötige Master-Passwort nach",
|
||||
"setGenAudit": "Audit",
|
||||
"setGenAuditPasswords": "Warnungen für schwache Passwörter anzeigen",
|
||||
"setGenAuditPasswordEntropy": "Passwortlänge und Zufälligkeit prüfen",
|
||||
"setGenExcludePinsFromAudit": "Kurze Pin-Codes niemals prüfen (z. B. 12345)",
|
||||
"setGenHelpHIBP": "KeeWeb kann überprüfen ob deine Passwörter bei einem früheren Datenleck veröffentlicht wurden. Deine Passwörter können nicht beim übertragen ausgelesen werden, aber die Anzahl der überprüften Passwörter kann möglicherweise entdeckt werden. {} kannst du mehr über den Datenschutz beim Nutzen dieses Dienstes erfahren. Wenn diese Option aktiviert ist überprüft KeeWeb deine Passwörter automatisch.",
|
||||
"setGenHelpHIBPLink": "hier",
|
||||
"setGenAuditPasswordAge": "Alte Passwörter",
|
||||
"setGenAuditPasswordAgeOff": "Zeige keine Warnungen über alte Passwörter",
|
||||
"setGenAuditPasswordAgeOneYear": "Zeige Warnungen für Passwörter die über ein Jahr alt sind",
|
||||
"setGenAuditPasswordAgeYears": "Zeige Warnungen für Passwörter die über {} Jahre alt sind",
|
||||
"setFilePath": "Dateipfad",
|
||||
"setFileStorage": "Diese Datei wird von {} geladen.",
|
||||
"setFileIntl": "Diese Datei ist im internen App-Speicher abgelegt",
|
||||
"setFileLocalHint": "Sie wollen nahtlos mit lokalen Dateien arbeiten?",
|
||||
"setFileDownloadApp": "Laden Sie die Desktop-App herunter",
|
||||
"setFileSave": "Speichern",
|
||||
"setFileSaveTo": "Speichern unter...",
|
||||
"setFileSaveTo": "Speichern unter …",
|
||||
"setFileClose": "Sperren",
|
||||
"setFileSync": "Synchronisierung",
|
||||
"setFileSyncVerb": "Synchronisieren",
|
||||
@@ -475,7 +496,7 @@
|
||||
"setFileConfirmPass": "Masterpasswort bestätigen",
|
||||
"setFilePassChange": "Um das Passwort zu ändern, geben Sie es im Bestätigungsfeld an",
|
||||
"setFilePassChanged": "Das Passwort wurde geändert. Lassen Sie das Feld leer, um das bisherige Passwort zu behalten.",
|
||||
"setFilePassNotMatch": "Die Passwörter stimmen nicht überein, bitte geben Sie sie erneut ein",
|
||||
"setFilePassNotMatch": "Die Passwörter stimmen nicht überein, bitte geben Sie diese erneut ein",
|
||||
"setFileKeyFile": "Schlüsseldatei",
|
||||
"setFileSelKeyFile": "Schlüsseldatei auswählen",
|
||||
"setFileNames": "Namen",
|
||||
@@ -492,7 +513,7 @@
|
||||
"setFileBackupPath": "Sicherungspfad",
|
||||
"setFileBackupTime": "Sicherungen anlegen",
|
||||
"setFileBackupNow": "Jetzt sichern",
|
||||
"setFileBackupNowWorking": "Sichern...",
|
||||
"setFileBackupNowWorking": "Ausführung läuft …",
|
||||
"setFileBackupError": "Sicherungsfehler",
|
||||
"setFileBackupErrorDescription": "Fehler beim Schreiben der Sicherungsdatei",
|
||||
"setFileBackupErrorIsDir": "Der Sicherungspfad ist ungültig",
|
||||
@@ -512,9 +533,9 @@
|
||||
"setFileUseGenKeyFile": "Generierte Schlüsseldatei benutzen",
|
||||
"setFileUseOldKeyFile": "Alte Schlüsseldatei benutzen",
|
||||
"setFileGenKeyFile": "Neue Schlüsseldatei generieren",
|
||||
"setFileDontUseKeyFile": "Keine Schlüsseldatei benutzen",
|
||||
"setFileDontUseKeyFile": "Keine Schlüsseldatei verwenden",
|
||||
"setFileEmptyPass": "Leeres Passwort",
|
||||
"setFileEmptyPassBody": "Eine Datenbank ohne Passwort zu speichern macht sie vollkommen ungeschützt. Möchten Sie sie trotzdem speichern?",
|
||||
"setFileEmptyPassBody": "Eine Datenbank ohne Passwort zu speichern, macht diese vollkommen ungeschützt. Möchten Sie dies trotzdem tun?",
|
||||
"setFileSaveError": "Fehler beim Speichern",
|
||||
"setFileSaveErrorBody": "Schreibfehler beim Speichern",
|
||||
"setFileAlreadyExists": "Existiert bereits",
|
||||
@@ -529,14 +550,16 @@
|
||||
"setFileDeviceIntro": "Einmal-Codes aus diesem {} werden in der App angezeigt.",
|
||||
"setFileDeviceSettings": "Einstellungen",
|
||||
"setFileYubiKey": "YubiKey",
|
||||
"setFileDontUseYubiKey": "Keinen YubiKey benutzen",
|
||||
"setFileDontUseYubiKey": "Keinen YubiKey verwenden",
|
||||
"setFileRefreshYubiKeyList": "Liste aktualisieren",
|
||||
"setFileYubiKeyHeader": "YubiKey",
|
||||
"setFileYubiKeyBody": "Es ist gefährlich, einen YubiKey als Teil des Hauptschlüssels zu verwenden. Falls etwas schief geht, besteht möglicherweise kein Zugriff mehr auf die Passwörter. Haben Sie ein Backup Ihrer Datei erstellt, bevor diese Einstellung geändert wurde?",
|
||||
"setFileYubiKeyErrorEmptyMac": "Scheinbar wurde ein YubiKey angeschlossen, er ist aber nicht sichtbar. KeeWeb muss in den Sicherheitseinstellungen im Abschnitt Input-Monitoring hinzugefügt werden, um auf YubiKeys zugreifen zu können.",
|
||||
"setShTitle": "Tastenkürzel",
|
||||
"setShShowAll": "alle Einträge anzeigen",
|
||||
"setShColors": "mit Farben markierte Einträge anzeigen",
|
||||
"setShTrash": "Papierkorb öffnen",
|
||||
"setShFind": "Suchfeld aktivieren; oder: einfach anfangen, den Suchbegriff zu tippen",
|
||||
"setShFind": "Suchen oder einfach beginnen zu tippen",
|
||||
"setShClearSearch": "Suchfeld leeren",
|
||||
"setShCopyPass": "Passwort bzw. ausgewähltes Feld kopieren",
|
||||
"setShCopyUser": "Benutzername kopieren",
|
||||
@@ -545,8 +568,8 @@
|
||||
"setShPrev": "zum vorherigen Eintrag gehen",
|
||||
"setShNext": "zum nächsten Eintrag gehen",
|
||||
"setShCreateEntry": "Neuer Eintrag",
|
||||
"setShOpen": "öffnen / neu",
|
||||
"setShSave": "alle Dateien speichern",
|
||||
"setShOpen": "Öffnen / Neu",
|
||||
"setShSave": "Alle Dateien speichern",
|
||||
"setShGen": "Passwort generieren",
|
||||
"setShSet": "App-Einstellungen",
|
||||
"setShCopyPassGlobal": "Passwort kopieren",
|
||||
@@ -556,12 +579,12 @@
|
||||
"setShRestoreApp": "{} öffnen",
|
||||
"setShGlobal": "Diese systemweiten Tastenkürzel funktionieren auch, wenn die App im Hintergrund läuft. Klicken Sie auf einen Eintrag, um ein eigenes Kürzel festzulegen.",
|
||||
"setShLock": "Datenbank sperren",
|
||||
"setShEdit": "Drücken Sie eine Tastenkombination, um diese als Kürzel zu setzen",
|
||||
"setShEdit": "Drücken Sie eine Tastenkombination, um diese als Kürzel zu festzulegen",
|
||||
"setPlInstallTitle": "Neue Plugins installieren",
|
||||
"setPlInstallDesc": "KeeWeb-Plugins fügen Funktionen, Themes und Sprachen zu KeeWeb hinzu. Plugins werden mit denselben Rechten ausgeführt wie KeeWeb selbst, sie können auf all Ihre Passwörter zugreifen oder diese ändern. Installieren Sie niemals Plugins, denen Sie nicht vertrauen.",
|
||||
"setPlInstallLabel": "Plugin-URL",
|
||||
"setPlInstallBtn": "Installieren",
|
||||
"setPlInstallBtnProgress": "Installiere",
|
||||
"setPlInstallBtnProgress": "Installation läuft",
|
||||
"setPlUninstallBtn": "Deinstallieren",
|
||||
"setPlDisableBtn": "Deaktivieren",
|
||||
"setPlEnableBtn": "Aktivieren",
|
||||
@@ -575,7 +598,7 @@
|
||||
"setPlLoadTime": "geladen in {}",
|
||||
"setPlLastUpdate": "Letzte Prüfung auf Updates",
|
||||
"setPlLoadError": "Fehler beim Laden des Plugins",
|
||||
"setPlGalleryLoading": "Plugins werden geladen, bitte warten Sie einen Moment",
|
||||
"setPlGalleryLoading": "Plugins werden geladen, bitte einen Moment warten",
|
||||
"setPlGalleryLoadError": "Fehler beim Laden der Plugins",
|
||||
"setPlInstallUrlTitle": "Plugin von einer URL hinzufügen",
|
||||
"setPlInstallUrlDesc": "Wenn das Plugin nicht in der Galerie verfügbar ist, können Sie es manuell von einer URL installieren",
|
||||
@@ -587,19 +610,35 @@
|
||||
"setPlTranslateLink": "übersetzen Sie die App in Ihre Sprache",
|
||||
"setPlAutoUpdate": "Automatisch aktualisieren",
|
||||
"setPlLoadGallery": "Plugin-Galerie laden",
|
||||
"setDevicesTitle": "Geräte",
|
||||
"setDevicesEnableUsb": "Interaktion mit USB-Geräten aktivieren",
|
||||
"setDevicesYubiKeyIntro": "Es wird empfehlen {} zu lesen, bevor ein YubiKey verwendet wird.",
|
||||
"setDevicesYubiKeyIntroLink": "dieses Dokument",
|
||||
"setDevicesYubiKeyToolsDesc": "Um YubiKey in diesem Modus verwenden zu können, müssen Sie vorher das Tool {} installieren.",
|
||||
"setDevicesYubiKeyToolsDesc2": "{}, um mehr Informationen über dieses Tool zu erhalten.",
|
||||
"setDevicesYubiKeyToolsDescLink": "Hier klicken",
|
||||
"setDevicesYubiKeyToolsStatusChecking": "Prüfung, ob {} installiert ist",
|
||||
"setDevicesYubiKeyToolsStatusOk": "{} ist installiert",
|
||||
"setDevicesYubiKeyToolsStatusError": "{} ist nicht installiert oder funktioniert nicht korrekt",
|
||||
"setDevicesYubiKeyOtpTitle": "Einmal-Codes",
|
||||
"setDevicesYubiKeyOtpDesc": "Ein YubiKey kann genutzt werden, um Einmal-Codes für verschiedene Dienste zu erzeugen.",
|
||||
"setDevicesYubiKeyOtpShowIcon": "Das Symbol auf der Startseite anzeigen",
|
||||
"setDevicesYubiKeyOtpAutoOpen": "Einmal-Codes automatisch laden, wenn Dateien geöffnet sind",
|
||||
"setDevicesYubiKeyOtpMatchEntries": "Passende Einmal-Codes in den Einträgen anzeigen",
|
||||
"setDevicesYubiKeyChalRespTitle": "Challenge-Response",
|
||||
"setDevicesYubiKeyChalRespDesc": "Es ist auch möglich, einen YubiKey im Challenge-Response-Modus zu verwenden, um einen Teil des Private Schlüssels, der zum Verschlüsseln der Dateien verwendet wird, auf dem YubiKey zu speichern.",
|
||||
"setDevicesYubiKeyChalRespShow": "Optionen zum Verwenden eines YubiKeys beim Öffnen von Dateien anzeigen",
|
||||
"setDevicesYubiKeyStuckWorkaround": "YubiKey entfernen und neu verbinden, falls der Ladeprozess hängt",
|
||||
"setDevicesYubiKeyRememberChalResp": "Challenge-Response-Einstellungen merken, während die App geöffnet ist",
|
||||
"setAboutTitle": "Über",
|
||||
"setAboutBuilt": "Diese App wurde mit den folgenden Werkzeugen erstellt",
|
||||
"setAboutBuilt": "Diese App wurde mit folgenden Werkzeugen erstellt",
|
||||
"setAboutLic": "Lizenz",
|
||||
"setAboutLicComment": "Die App selbst und alle enthaltenen Komponenten, die nicht Public Domain sind, stehen unter der MIT-Lizenz.",
|
||||
"setAboutFirst": "Dies ist eine Open-Source App, erstellt von {}",
|
||||
"setAboutSecond": "und lizenziert unter {}.",
|
||||
"setAboutSource": "Quellcode und Tickets befinden sich auf {}.",
|
||||
"setHelpFormat": "Dateiformat",
|
||||
"setHelpFormatBody": "Dies ist eine Portierung der {} App, programmiert mit Webtechnologien. Sie ist kompatibel mit Dateien im KeePass-Format (kdbx). Sie können solche Dateien (Passwort-Datenbanken) entweder mit KeePass oder mit dieser App erstellen. Das Dateiformat ist zu 100% kompatibel und sollte in beiden Anwendungen nutzbar sein.",
|
||||
"setHelpFormatBody": "Dies ist eine Portierung der App {}, programmiert mit Webtechnologien. Sie ist kompatibel mit Dateien im KeePass-Format (kdbx). Sie können solche Dateien (Passwort-Datenbanken) entweder mit KeePass oder mit dieser App erstellen. Das Dateiformat ist zu 100 % kompatibel und sollte in beiden Anwendungen nutzbar sein.",
|
||||
"setHelpProblems": "Probleme?",
|
||||
"setHelpProblems1": "Falls etwas schief geht, {}",
|
||||
"setHelpProblems2": "oder {}",
|
||||
@@ -612,14 +651,15 @@
|
||||
"setHelpUpdates": "Neuigkeiten",
|
||||
"setHelpTwitter": "Twitter",
|
||||
"dropboxSetupDesc": "Etwas Konfiguration ist notwendig, um Dropbox in einer selbst gehosteten Web-App zu verwenden. Bitte erstellen Sie eine eigene Dropbox-App und tragen Sie ihren App-Schlüssel unten ein.",
|
||||
"dropboxAppKey": "Dropbox App-Schlüssel",
|
||||
"dropboxAppKey": "Dropbox-App-Schlüssel",
|
||||
"dropboxAppKeyDesc": "Kopieren Sie den Schlüssel aus Ihrer Dropbox-App (Entwicklereinstellungen)",
|
||||
"dropboxAppSecret": "Dropbox App Secret",
|
||||
"dropboxAppSecretDesc": "Das App Secret ist neben dem App Key zu finden",
|
||||
"dropboxAppKeyHint": "Ihr App-Schlüssel",
|
||||
"dropboxAppSecret": "Dropbox-App-Secret",
|
||||
"dropboxAppSecretDesc": "Das App-Secret ist neben dem App-Schlüssel zu finden",
|
||||
"dropboxFolder": "App-Ordner",
|
||||
"dropboxFolderDesc": "Wenn Ihre App mit der gesamten Dropbox verlinkt ist (anstatt mit einem bestimmten Ordner), geben Sie hier den Pfad zum Ordner mit Ihren KDBX-Dateien an.",
|
||||
"dropboxFolderSettingsDesc": "Wählen Sie einen Ordner in Ihrer Dropbox, in dem die Dateien gespeichert werden (standardmäßig das Stammverzeichnis)",
|
||||
"dropboxFolderPlaceholder": "Standard-Ordner",
|
||||
"dropboxFolderPlaceholder": "Standardordner",
|
||||
"dropboxLink": "App verlinken mit",
|
||||
"dropboxLinkApp": "App-Ordner (Apps/KeeWeb)",
|
||||
"dropboxLinkFull": "Gesamte Dropbox oder beliebiger Ordner",
|
||||
@@ -628,10 +668,10 @@
|
||||
"webdavSaveMethod": "Speichermethode",
|
||||
"webdavSaveMove": "Eine temporäre Datei hochladen und verschieben",
|
||||
"webdavSavePut": "kdbx-Datei mittels PUT überschreiben",
|
||||
"launcherSave": "Passwort-Datenbank speichern",
|
||||
"launcherSave": "Passwortdatenbank speichern",
|
||||
"launcherFileFilter": "KeePass-Dateien",
|
||||
"authPopupRequired": "Pop-Ups blockiert",
|
||||
"authPopupRequiredBody": "Bitte erlauben Sie Pop-Ups in Ihrem Browser.",
|
||||
"authPopupRequired": "Pop-ups blockiert",
|
||||
"authPopupRequiredBody": "Bitte erlauben Sie Pop-ups in Ihrem Browser.",
|
||||
"exportFileInfo": "Dateiinformationen",
|
||||
"exportHtmlName": "Name",
|
||||
"exportHtmlDate": "Exportdatum",
|
||||
@@ -641,5 +681,15 @@
|
||||
"importCsvRun": "Importieren",
|
||||
"importIgnoreField": "Ignorieren",
|
||||
"importTo": "Einträge werden importiert nach:",
|
||||
"importNewFile": "Neue Datei"
|
||||
"importNewFile": "Neue Datei",
|
||||
"yubiKeyStuckError": "Der YubiKey scheint nicht mehr zu funktionieren. Auto-Repair kann in den App-Einstellungen aktiviert werden.",
|
||||
"yubiKeyNoKeyHeader": "YubiKey benötigt",
|
||||
"yubiKeyNoKeyBody": "Bitte den YubiKey mit der Seriennummer {} anschließen",
|
||||
"yubiKeySlot": "Anschluss",
|
||||
"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.",
|
||||
"yubiKeyErrorWithCode": "YubiKey-Fehler, Code {}.",
|
||||
"bioOpenAuthPrompt": "„{}“ öffnen"
|
||||
}
|
||||
@@ -1,50 +1,4 @@
|
||||
{
|
||||
"months": [
|
||||
"Janvier",
|
||||
"Février",
|
||||
"Mars",
|
||||
"Avril",
|
||||
"Mai",
|
||||
"Juin",
|
||||
"Juillet",
|
||||
"Août",
|
||||
"Septembre",
|
||||
"Octobre",
|
||||
"Novembre",
|
||||
"Décembre"
|
||||
],
|
||||
"monthsShort": [
|
||||
"Janv",
|
||||
"Févr",
|
||||
"Mars",
|
||||
"Avr",
|
||||
"Mai",
|
||||
"Juin",
|
||||
"Juil",
|
||||
"Août",
|
||||
"Sept",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Déc"
|
||||
],
|
||||
"weekdays": [
|
||||
"Dimanche",
|
||||
"Lundi",
|
||||
"Mardi",
|
||||
"Mercredi",
|
||||
"Jeudi",
|
||||
"Vendredi",
|
||||
"Samedi"
|
||||
],
|
||||
"weekdaysShort": [
|
||||
"Dim",
|
||||
"Lun",
|
||||
"Mar",
|
||||
"Mer",
|
||||
"Jeu",
|
||||
"Ven",
|
||||
"Sam"
|
||||
],
|
||||
"retToApp": "retour vers l'appli",
|
||||
"name": "nom",
|
||||
"icon": "icône",
|
||||
@@ -72,6 +26,15 @@
|
||||
"shiftKey": "shift",
|
||||
"altKey": "alt",
|
||||
"error": "erreur",
|
||||
"oneMinute": "une minute",
|
||||
"minutes": "{} minutes",
|
||||
"oneHour": "une heure",
|
||||
"hours": "{} heures",
|
||||
"oneDay": "un jour",
|
||||
"days": "{} jours",
|
||||
"oneWeek": "une semaine",
|
||||
"oneMonth": "un mois",
|
||||
"oneYear": "une année",
|
||||
"cache": "cache",
|
||||
"file": "fichier",
|
||||
"device": "appareil",
|
||||
@@ -88,11 +51,27 @@
|
||||
"menuAlertNoTags": "Aucun tag",
|
||||
"menuAlertNoTagsBody": "Vous pouvez ajouter de nouveaux tags en éditant les champs, dans la section Tags.",
|
||||
"menuEmptyTrash": "Vider la Corbeille",
|
||||
"menuEmptyTrashAlert": "Vider la Corbeille?",
|
||||
"menuEmptyTrashAlert": "Vider la Corbeille ?",
|
||||
"menuEmptyTrashAlertBody": "Vous ne pourrez plus récupérer ces éléments",
|
||||
"menuItemCollapsed": "Double-cliquez pour déplier",
|
||||
"menuRestoreApp": "Ouvrir {}",
|
||||
"menuQuitApp": "Quitter {}",
|
||||
"sysMenuAboutKeeWeb": "A propos {}",
|
||||
"sysMenuServices": "Services",
|
||||
"sysMenuHide": "Cacher {}",
|
||||
"sysMenuHideOthers": "Cacher autres",
|
||||
"sysMenuUnhide": "Montrer tous",
|
||||
"sysMenuQuit": "Quitter {}",
|
||||
"sysMenuEdit": "Editer",
|
||||
"sysMenuUndo": "Annuler",
|
||||
"sysMenuRedo": "Répéter",
|
||||
"sysMenuCut": "Couper",
|
||||
"sysMenuCopy": "Copier",
|
||||
"sysMenuPaste": "Coller",
|
||||
"sysMenuSelectAll": "Tout sélectionner",
|
||||
"sysMenuWindow": "Fenêtre",
|
||||
"sysMenuMinimize": "Réduire",
|
||||
"sysMenuClose": "Fermer fenêtre",
|
||||
"alertYes": "Oui",
|
||||
"alertNo": "Non",
|
||||
"alertOk": "OK",
|
||||
@@ -109,7 +88,7 @@
|
||||
"genLen": "Longueur",
|
||||
"genNewPass": "Nouveau mot de passe",
|
||||
"genPresetDefault": "réglages par défaut",
|
||||
"genPresetDerived": "identique à l'ancien mot de passe",
|
||||
"genPresetDerived": "similaire à l'ancien mot de passe",
|
||||
"genPresetPronounceable": "prononçable",
|
||||
"genPresetMed": "longueur moyenne",
|
||||
"genPresetLong": "long",
|
||||
@@ -133,7 +112,7 @@
|
||||
"tagExists": "Ce tag existe déjà",
|
||||
"tagExistsBody": "Un tag existe déjà avec ce nom. Merci de choisir un autre nom.",
|
||||
"tagBadName": "Nom invalide",
|
||||
"tagBadNameBody": "Un nom de tag ne peut pas contenir les caractères {}. Merci de les supprimer.",
|
||||
"tagBadNameBody": "Les noms de Tag ne peuvent contenir les caractères {}. Veuillez les supprimer.",
|
||||
"genPsTitle": "Préréglages du Générateur",
|
||||
"genPsCreate": "Nouveau préréglage",
|
||||
"genPsDelete": "Supprimer préréglage",
|
||||
@@ -168,7 +147,7 @@
|
||||
"listNoWebsite": "aucun site web",
|
||||
"listNoUser": "aucun utilisateur",
|
||||
"listNoAttachments": "aucune pièce-jointe",
|
||||
"listAddTemplateHeader": "Templates",
|
||||
"listAddTemplateHeader": "Modèles",
|
||||
"listAddTemplateBody1": "Les modèles (templates) vous permettent de créer des nouvelles entrées en un clic. Ajouter quelque chose à l'entrée du modèle et ensuite cliquer de nouveau sur {} pour utiliser ce modèle.",
|
||||
"listAddTemplateBody2": "Vous pouvez toujours retrouver vos modèles dans le groupe {}.",
|
||||
"searchAddNew": "Ajouter Nouveau",
|
||||
@@ -230,6 +209,7 @@
|
||||
"openConfigErrorNotFound": "Aucun fichier trouvé",
|
||||
"openError": "Erreur",
|
||||
"openErrorDescription": "Une erreur est survenue à l'ouverture du fichier",
|
||||
"openErrorDescriptionMaybeTouchIdChanged": "L'erreur ressemble à ce qu'y arrive habituellement quand la configuration de Touch ID a été changée. Par exemple, vous avez ajouté ou supprimé un doigt supplémentaire. Si c'est le cas, allez dans Paramètres, désactiver Touch ID et activer le de nouveau.",
|
||||
"openErrorFileNotFound": "Fichier non trouvé",
|
||||
"openListErrorBody": "Erreur au chargement de la liste des fichiers",
|
||||
"openShowAllFiles": "Tous les fichiers",
|
||||
@@ -283,7 +263,7 @@
|
||||
"detDelToTrashBody": "L'entrée sera déplacée dans la corbeille.",
|
||||
"detFieldCopied": "Copié",
|
||||
"detFieldCopiedTime": "Copié pendant {} secondes",
|
||||
"detCopyHint": "Vous pouvez copier la valeur du champ en cliquant sur son titre",
|
||||
"detCopyHint": "Vous pouvez copier la valeur du champ en cliquant sur ton titre",
|
||||
"detMore": "plus",
|
||||
"detClickToAddField": "cliquez pour ajouter un nouveau champ",
|
||||
"detMenuAddNewField": "Ajouter nouveau champ",
|
||||
@@ -322,7 +302,7 @@
|
||||
"detOtpQrError": "Erreur lecture du QR code",
|
||||
"detOtpQrErrorBody": "Désolé, le QR code n'a pas pu être lu, essayez à nouveau ou contacter les auteurs de l'application avec le détail de l'erreur.",
|
||||
"detOtpQrWrong": "QR code incorrect",
|
||||
"detOtpQrWrongBody": "Votre QR code a été scanné avec succès mais il ne contient pas d'information pour un mot de passe à usage unique.",
|
||||
"detOtpQrWrongBody": "Votre code QR code a été scanné avec succès mais il ne contient pas d'information pour un code à usage unique.",
|
||||
"detOtpField": "Code à usage unique",
|
||||
"detOtpClickToTouch": "Cliquer pour générer",
|
||||
"detOtpGenerating": "Création...",
|
||||
@@ -332,13 +312,24 @@
|
||||
"detRevealField": "Révéler",
|
||||
"detHideField": "Cacher",
|
||||
"detAutoTypeField": "Saisie auto",
|
||||
"detIssuesHideTooltip": "Cacher cet avertissement",
|
||||
"detIssueWeakPassword": "Ce mot de passe est faible, nous vous recommandons de le changer",
|
||||
"detIssuePoorPassword": "Ce mot de passe est très faible, nous vous recommandons très fort de le changer",
|
||||
"detIssuePwnedPassword": "Ce mot de passe a été exposé à une faille selon {}, il est recommandé de le changer",
|
||||
"detIssuePasswordCheckError": "Une erreur est survenue en vérifiant la force du mot de passe en ligne",
|
||||
"detIssueOldPassword": "Ce mot de passe est vieux",
|
||||
"detIssueCloseAlertHeader": "Cacher les problèmes de mots de passe",
|
||||
"detIssueCloseAlertBody": "Il y a plusieurs façon de cacher ce message :",
|
||||
"detIssueCloseAlertEntry": "Ne plus montrer pour cette entrée",
|
||||
"detIssueCloseAlertSettings": "Modifier les paramètres globaux",
|
||||
"autoTypeEntryFields": "Champs",
|
||||
"autoTypeModifiers": "Touches modificatrices",
|
||||
"autoTypeKeys": "Clés",
|
||||
"autoTypeLink": "plus...",
|
||||
"autoTypeError": "Erreur saisie auto",
|
||||
"autoTypeErrorGeneric": "Il y a eu une erreur lors de la saisie automatique : {}",
|
||||
"autoTypeErrorGlobal": "Pour utiliser un raccourci, sélectionner l'application là où vous voulez saisir le mot de passe",
|
||||
"autoTypeErrorAccessibilityMacOS": "Nous avons essayé d'envoyer les frappes de caractères à cette application mais cela ne semble pas fonctionner parce qu'il manque des permissions. Cliquer ici pour en lire plus.",
|
||||
"autoTypeErrorGlobal": "Pour utiliser un raccourci global, sélectionner l'application là où vous voulez saisir le mot de passe",
|
||||
"autoTypeErrorNotInstalled": "{} n'est pas installé",
|
||||
"autoTypeHeader": "Saisie auto : Selectionner",
|
||||
"autoTypeMsgNoWindow": "Nous n'avons pas pu obtenir le titre de la fenêtre active, commencez à taper pour rechercher",
|
||||
@@ -362,6 +353,8 @@
|
||||
"appSaveError": "Sauver l'erreur",
|
||||
"appSaveErrorBody": "Échec sauvegarde auto du fichier",
|
||||
"appSaveErrorBodyMul": "Échec sauvegarde auto des fichiers",
|
||||
"appSaveErrorExitLoseChanges": "Afsluiten en alle aanpassingen kwijtraken",
|
||||
"appSaveErrorExitLoseChangesBody": "Je kunt de app afsluiten en alle wijzigingen kwijtraken, of je kunt de bestandsinstellingen gebruiken om gegevens te exporteren",
|
||||
"appSettingsError": "Erreur lancement appli",
|
||||
"appSettingsErrorBody": "Il y a eu une erreur lors du chargement des paramètres. Merci de revérifier l'URL de l'application ou contacter votre administrateur.",
|
||||
"appNotSupportedError": "Votre navigateur n'est pas compatible avec certaines fonctionnalités importantes que nous utilisons.",
|
||||
@@ -393,18 +386,30 @@
|
||||
"setGenExtractingUpdate": "Décompression de la mise à jour...",
|
||||
"setGenCheckErr": "Une erreur est intervenue durant le téléchargement de la mise à jour",
|
||||
"setGenNeverChecked": "Ne jamais vérifier les mises à jour",
|
||||
"setGenRestartToUpdate": "Redémarrer pour mettre à jour",
|
||||
"setGenRestartToUpdate": "Redémarrer KeeWeb pour mettre à jour",
|
||||
"setGenDownloadAndRestart": "Télécharger la mise à jour et redémarrer",
|
||||
"setGenAppearance": "Apparence",
|
||||
"setGenTheme": "Thème",
|
||||
"setGenThemeFb": "Bleu plat",
|
||||
"setGenThemeDefault": "Défaut",
|
||||
"setGenThemeDark": "Sombre",
|
||||
"setGenThemeLight": "Clair",
|
||||
"setGenThemeBlue": "Bleu",
|
||||
"setGenThemeFb": "Bleu foncé",
|
||||
"setGenThemeBl": "Bleu clair",
|
||||
"setGenThemeBrown": "Brownie",
|
||||
"setGenThemeDb": "Marron foncé",
|
||||
"setGenThemeWh": "Blanc",
|
||||
"setGenThemeLb": "Brun clair",
|
||||
"setGenThemeTerminal": "Terminal",
|
||||
"setGenThemeTe": "Terminal",
|
||||
"setGenThemeLt": "Terminal clair",
|
||||
"setGenThemeHighContrast": "Contraste élevé",
|
||||
"setGenThemeHc": "Contraste élevé",
|
||||
"setGenThemeDc": "Contraste foncé",
|
||||
"setGenThemeSol": "Solarisé",
|
||||
"setGenThemeSd": "Solarized dark",
|
||||
"setGenThemeSl": "Solarized light",
|
||||
"setGenThemeMacDark": "Mode sombre de macOS",
|
||||
"setGenMoreThemes": "Plus de thèmes",
|
||||
"setGenAutoSwitchTheme": "Basculer automatiquement entre les thèmes clairs et foncés lorsque possible",
|
||||
"setGenLocale": "Langue",
|
||||
"setGenLocOther": "d'autres langues sont disponibles en tant que modules",
|
||||
"setGenFontSize": "Taille de police",
|
||||
@@ -441,12 +446,15 @@
|
||||
"setGenClearSeconds": "Dans {} secondes",
|
||||
"setGenClearMinute": "Dans une minute",
|
||||
"setGenMinInstead": "Réduire l'application au lieu de fermer",
|
||||
"setGenMinOnFieldCopy": "Réduire sur copie d'un champ",
|
||||
"setGenLock": "Verrou Auto",
|
||||
"setGenLockMinimize": "quand l'appli est en arrière-plan",
|
||||
"setGenLockCopy": "sur copie de m.d.p.",
|
||||
"setGenLockAutoType": "sur saisie auto",
|
||||
"setGenLockOrSleep": "Quand l'ordinateur est verrouillé ou mis en sommeil",
|
||||
"setGenStorage": "Stockage",
|
||||
"setGenDisableOfflineStorage": "Ne pas mettre en cache des fichiers d'un stockage offline",
|
||||
"setGenShortLivedStorageToken": "Utiliser des sessions de courte-durée avec les fournisseurs de stockage dans le cloud",
|
||||
"setGenStorageLogout": "Déconnexion",
|
||||
"setGenShowAdvanced": "Afficher les paramètres avancés",
|
||||
"setGenDevTools": "Afficher les outils de développement",
|
||||
@@ -456,6 +464,22 @@
|
||||
"setGenShowAppLogs": "Voir les logs",
|
||||
"setGenReloadApp": "Recharger l'application",
|
||||
"setGenFieldLabelDblClickAutoType": "Remplissage auto par double clic sur les noms de champ",
|
||||
"setGenTouchId": "Touch ID",
|
||||
"setGenTouchIdDisabled": "Ne pas utiliser Touch ID",
|
||||
"setGenTouchIdMemory": "Dévérouiller avec Touch ID uniquement quand KeeWeb est lancé",
|
||||
"setGenTouchIdFile": "Toujours utiliser Touch ID à la place du mot de passe",
|
||||
"setGenTouchIdPass": "Exige le mot de passe après",
|
||||
"setGenAudit": "Audit",
|
||||
"setGenAuditPasswords": "Voir des avertissements sur la force du mot de passe",
|
||||
"setGenAuditPasswordEntropy": "Vérifier la longueur et l'aléas du mot de passe",
|
||||
"setGenExcludePinsFromAudit": "Ne jamais vérifier les codes PIN courts, comme 123456",
|
||||
"setGenCheckPasswordsOnHIBP": "Vérifier les mots de passe en utilisant un service en ligne {}",
|
||||
"setGenHelpHIBP": "KeeWeb peut vérifier si vos mots de passes ont été précédemment exposés à une faille de sécurité en utilisant un service en ligne. Votre mot de passe ne peut pas être récupéré à partir de données envoyées en ligne, cependant le nombre de mots de passes vérifiés peut être exposé. Plus d'informations sur votre sécurité en utilisant ce service peuvent être trouvées {}. Si cette option est activée, KeeWeb vérifiera automatiquement vos mots de passes là.",
|
||||
"setGenHelpHIBPLink": "ici",
|
||||
"setGenAuditPasswordAge": "Vieux mots de passe",
|
||||
"setGenAuditPasswordAgeOff": "Ne pas afficher d'avertissements à propos des vieux mots de passe",
|
||||
"setGenAuditPasswordAgeOneYear": "Afficher des avertissements pour les mots de passe plus vieux qu'un an",
|
||||
"setGenAuditPasswordAgeYears": "Afficher des avertissement pour les mots de passe plus vieux que {} ans",
|
||||
"setFilePath": "Chemin",
|
||||
"setFileStorage": "Le fichier est ouvert de {}.",
|
||||
"setFileIntl": "Le fichier est conservé dans le stockage interne de l'application",
|
||||
@@ -512,10 +536,10 @@
|
||||
"setFileUseKeyFile": "Utiliser un fichier clé",
|
||||
"setFileUseGenKeyFile": "Utiliser le fichier clé généré",
|
||||
"setFileUseOldKeyFile": "Utiliser l'ancien fichier clé",
|
||||
"setFileGenKeyFile": "Générer nouveau fichier clé",
|
||||
"setFileGenKeyFile": "Générer un nouveau fichier clé",
|
||||
"setFileDontUseKeyFile": "Ne pas utiliser de fichier clé",
|
||||
"setFileEmptyPass": "Mot de passe vide",
|
||||
"setFileEmptyPassBody": "Sauvegarder la base de données avec un mode de passe vide la laisse sans aucune protection. Voulez-vous vraiment faire cela ?",
|
||||
"setFileEmptyPassBody": "Sauvegarder la base de données avec un mot de passe vide la laisse sans aucune protection. Voulez-vous vraiment faire cela ?",
|
||||
"setFileSaveError": "Erreur sauvegarde",
|
||||
"setFileSaveErrorBody": "Erreur sauvegarde dans le fichier",
|
||||
"setFileAlreadyExists": "Existe déjà",
|
||||
@@ -613,7 +637,7 @@
|
||||
"setAboutTitle": "À propos",
|
||||
"setAboutBuilt": "Cette appli est construite avec ces outils géniaux",
|
||||
"setAboutLic": "Licence",
|
||||
"setAboutLicComment": "Cette application et tous les composants qu'elle contient qui ne sont pas dans le domaine public sont sous licence MIT",
|
||||
"setAboutLicComment": "Cette application et tous les composants qu'elle contient qui ne sont pas dans le domaine public sont sous licence MIT, sauf si autrement spécifié",
|
||||
"setAboutFirst": "Ceci est une application open-source créée par {}",
|
||||
"setAboutSecond": " et sous licence {}.",
|
||||
"setAboutSource": "Le code source et les problèmes sont sur {}.",
|
||||
@@ -647,7 +671,9 @@
|
||||
"gdriveSharedWithMe": "Partagé avec moi",
|
||||
"webdavSaveMethod": "Méthode de sauvegarde",
|
||||
"webdavSaveMove": "Envoyer un fichier temporaire et le déplacer",
|
||||
"webdavSavePut": "Ecraser le fichier kdbx avec PUT",
|
||||
"webdavSavePut": "Écraser le fichier kdbx avec PUT",
|
||||
"webdavNoLastModified": "L'entête HTTP \"Last-Modified\" est absent",
|
||||
"webdavStatReload": "Toujours recharger le fichier au lieu de se fier à l'entête HTTP \"Last-Modified\"",
|
||||
"launcherSave": "Sauvegarder base des mots de passe",
|
||||
"launcherFileFilter": "Fichiers Keepass",
|
||||
"authPopupRequired": "Les pop-up sont bloquées",
|
||||
@@ -667,5 +693,9 @@
|
||||
"yubiKeyNoKeyBody": "Merci d'insérer votre YubiKey avec le numéro de série {}",
|
||||
"yubiKeySlot": "port",
|
||||
"yubiKeyTouchRequestedHeader": "Touchez votre YubiKey",
|
||||
"yubiKeyTouchRequestedBody": "Merci de toucher votre YubiKey avec le numéro de série {}"
|
||||
"yubiKeyTouchRequestedBody": "Merci de toucher votre YubiKey avec le numéro de série {}",
|
||||
"yubiKeyDisabledErrorHeader": "L'USB est désactivé",
|
||||
"yubiKeyDisabledErrorBody": "Yubikey est nécessaire pour ouvrir ce fichier, merci d'activer les appareils USB dans les paramètres",
|
||||
"yubiKeyErrorWithCode": "Erreur Yubikey code {}.",
|
||||
"bioOpenAuthPrompt": "ouvrir \"{}\""
|
||||
}
|
||||
@@ -4,19 +4,20 @@ import { SearchResultCollection } from 'collections/search-result-collection';
|
||||
import { FileCollection } from 'collections/file-collection';
|
||||
import { FileInfoCollection } from 'collections/file-info-collection';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { UsbListener } from 'comp/app/usb-listener';
|
||||
import { NativeModules } from 'comp/launcher/native-modules';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { EntryModel } from 'models/entry-model';
|
||||
import { FileInfoModel } from 'models/file-info-model';
|
||||
import { FileModel } from 'models/file-model';
|
||||
import { GroupModel } from 'models/group-model';
|
||||
import { YubiKeyOtpModel } from 'models/external/yubikey-otp-model';
|
||||
import { YubiKeyOtpModel } from 'models/otp-device/yubikey-otp-model';
|
||||
import { MenuModel } from 'models/menu/menu-model';
|
||||
import { PluginManager } from 'plugins/plugin-manager';
|
||||
import { Features } from 'util/features';
|
||||
import { DateFormat } from 'util/formatting/date-format';
|
||||
import { DateFormat } from 'comp/i18n/date-format';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { UrlFormat } from 'util/formatting/url-format';
|
||||
import { IdGenerator } from 'util/generators/id-generator';
|
||||
import { Locale } from 'util/locale';
|
||||
@@ -37,6 +38,10 @@ class AppModel {
|
||||
isBeta = RuntimeInfo.beta;
|
||||
advancedSearch = null;
|
||||
attachedYubiKeysCount = 0;
|
||||
memoryPasswordStorage = {};
|
||||
fileUnlockPromise = null;
|
||||
hardwareDecryptInProgress = false;
|
||||
mainWindowBlurTimer = null;
|
||||
|
||||
constructor() {
|
||||
Events.on('refresh', this.refresh.bind(this));
|
||||
@@ -47,6 +52,11 @@ class AppModel {
|
||||
Events.on('select-entry', this.selectEntry.bind(this));
|
||||
Events.on('unset-keyfile', this.unsetKeyFile.bind(this));
|
||||
Events.on('usb-devices-changed', this.usbDevicesChanged.bind(this));
|
||||
Events.on('main-window-blur', this.mainWindowBlur.bind(this));
|
||||
Events.on('main-window-focus', this.mainWindowFocus.bind(this));
|
||||
Events.on('main-window-will-close', this.mainWindowWillClose.bind(this));
|
||||
Events.on('hardware-decrypt-started', this.hardwareDecryptStarted.bind(this));
|
||||
Events.on('hardware-decrypt-finished', this.hardwareDecryptFinished.bind(this));
|
||||
|
||||
this.appLogger = new Logger('app');
|
||||
AppModel.instance = this;
|
||||
@@ -166,10 +176,29 @@ class AppModel {
|
||||
page: 'file',
|
||||
file
|
||||
});
|
||||
|
||||
this.refresh();
|
||||
|
||||
file.on('reload', this.reloadFile.bind(this));
|
||||
file.on('change', () => Events.emit('file-changed', file));
|
||||
file.on('change', () => {
|
||||
Events.emit('file-changed', file);
|
||||
});
|
||||
file.on('ejected', () => this.closeFile(file));
|
||||
file.on('change:dirty', (file, dirty) => {
|
||||
if (dirty && this.settings.autoSaveInterval === -1) {
|
||||
this.syncFile(file);
|
||||
}
|
||||
});
|
||||
|
||||
Events.emit('file-opened');
|
||||
|
||||
if (this.fileUnlockPromise) {
|
||||
this.appLogger.info('Running pending file unlock operation');
|
||||
this.fileUnlockPromise.resolve(file);
|
||||
this.fileUnlockPromise = null;
|
||||
Events.emit('unlock-message-changed', null);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -231,6 +260,9 @@ class AppModel {
|
||||
}
|
||||
|
||||
closeAllFiles() {
|
||||
if (!this.files.hasOpenFiles()) {
|
||||
return;
|
||||
}
|
||||
for (const file of this.files) {
|
||||
file.close();
|
||||
this.fileClosed(file);
|
||||
@@ -243,6 +275,7 @@ class AppModel {
|
||||
this.tags.splice(0, this.tags.length);
|
||||
this.filter = {};
|
||||
this.menu.select({ item: this.menu.allItemsItem });
|
||||
Events.emit('all-files-closed');
|
||||
}
|
||||
|
||||
closeFile(file) {
|
||||
@@ -253,6 +286,7 @@ class AppModel {
|
||||
this.menu.groupsSection.removeByFile(file);
|
||||
this.menu.filesSection.removeByFile(file);
|
||||
this.menu.select({ item: this.menu.allItemsSection.items[0] });
|
||||
Events.emit('one-file-closed');
|
||||
}
|
||||
|
||||
emptyTrash() {
|
||||
@@ -294,7 +328,7 @@ class AppModel {
|
||||
}
|
||||
|
||||
getEntries() {
|
||||
const entries = this.getEntriesByFilter(this.filter);
|
||||
const entries = this.getEntriesByFilter(this.filter, this.files);
|
||||
entries.sortEntries(this.sort, this.filter);
|
||||
if (this.filter.trash) {
|
||||
this.addTrashGroups(entries);
|
||||
@@ -302,16 +336,16 @@ class AppModel {
|
||||
return entries;
|
||||
}
|
||||
|
||||
getEntriesByFilter(filter) {
|
||||
getEntriesByFilter(filter, files) {
|
||||
const preparedFilter = this.prepareFilter(filter);
|
||||
const entries = new SearchResultCollection();
|
||||
|
||||
const devicesToMatchOtpEntries = this.files.filter((file) => file.external);
|
||||
const devicesToMatchOtpEntries = files.filter((file) => file.backend === 'otp-device');
|
||||
|
||||
const matchedOtpEntrySet = this.settings.yubiKeyMatchEntries ? new Set() : undefined;
|
||||
|
||||
this.files
|
||||
.filter((file) => !file.external)
|
||||
files
|
||||
.filter((file) => file.backend !== 'otp-device')
|
||||
.forEach((file) => {
|
||||
file.forEachEntry(preparedFilter, (entry) => {
|
||||
if (matchedOtpEntrySet) {
|
||||
@@ -426,7 +460,7 @@ class AppModel {
|
||||
|
||||
createNewEntry(args) {
|
||||
const sel = this.getFirstSelectedGroupForCreation();
|
||||
if (args && args.template) {
|
||||
if (args?.template) {
|
||||
if (sel.file !== args.template.file) {
|
||||
sel.file = args.template.file;
|
||||
sel.group = args.template.file.groups[0];
|
||||
@@ -442,11 +476,21 @@ class AppModel {
|
||||
}
|
||||
}
|
||||
|
||||
createNewEntryWithFields(group, fields) {
|
||||
return EntryModel.newEntryWithFields(group, fields);
|
||||
}
|
||||
|
||||
createNewGroup() {
|
||||
const sel = this.getFirstSelectedGroupForCreation();
|
||||
return GroupModel.newGroup(sel.group, sel.file);
|
||||
}
|
||||
|
||||
createNewGroupWithName(group, file, name) {
|
||||
const newGroup = GroupModel.newGroup(group, file);
|
||||
newGroup.setName(name);
|
||||
return newGroup;
|
||||
}
|
||||
|
||||
createNewTemplateEntry() {
|
||||
const file = this.getFirstSelectedGroupForCreation().file;
|
||||
const group = file.getEntryTemplatesGroup() || file.createEntryTemplatesGroup();
|
||||
@@ -465,7 +509,7 @@ class AppModel {
|
||||
}
|
||||
}
|
||||
|
||||
createNewFile(name) {
|
||||
createNewFile(name, callback) {
|
||||
if (!name) {
|
||||
for (let i = 0; ; i++) {
|
||||
name = Locale.openNewFile + (i || '');
|
||||
@@ -475,9 +519,10 @@ class AppModel {
|
||||
}
|
||||
}
|
||||
const newFile = new FileModel({ id: IdGenerator.uuid() });
|
||||
newFile.create(name);
|
||||
this.addFile(newFile);
|
||||
return newFile;
|
||||
newFile.create(name, () => {
|
||||
this.addFile(newFile);
|
||||
callback?.(newFile);
|
||||
});
|
||||
}
|
||||
|
||||
openFile(params, callback) {
|
||||
@@ -525,7 +570,8 @@ class AppModel {
|
||||
fileInfo &&
|
||||
fileInfo.openDate &&
|
||||
fileInfo.rev === params.rev &&
|
||||
fileInfo.storage !== 'file'
|
||||
fileInfo.storage !== 'file' &&
|
||||
!this.settings.disableOfflineStorage
|
||||
) {
|
||||
logger.info('Open file from cache because it is latest');
|
||||
this.openFileFromCache(
|
||||
@@ -546,7 +592,12 @@ class AppModel {
|
||||
},
|
||||
fileInfo
|
||||
);
|
||||
} else if (!fileInfo || !fileInfo.openDate || params.storage === 'file') {
|
||||
} else if (
|
||||
!fileInfo ||
|
||||
!fileInfo.openDate ||
|
||||
params.storage === 'file' ||
|
||||
this.settings.disableOfflineStorage
|
||||
) {
|
||||
this.openFileFromStorage(params, callback, fileInfo, logger);
|
||||
} else {
|
||||
logger.info('Open file from cache, will sync after load', params.storage);
|
||||
@@ -594,7 +645,7 @@ class AppModel {
|
||||
logger.info('Load from storage');
|
||||
storage.load(params.path, params.opts, (err, data, stat) => {
|
||||
if (err) {
|
||||
if (fileInfo && fileInfo.openDate) {
|
||||
if (fileInfo && fileInfo.openDate && !this.settings.disableOfflineStorage) {
|
||||
logger.info('Open file from cache because of storage load error', err);
|
||||
this.openFileFromCache(params, callback, fileInfo);
|
||||
} else {
|
||||
@@ -618,7 +669,8 @@ class AppModel {
|
||||
!noCache &&
|
||||
fileInfo &&
|
||||
storage.name !== 'file' &&
|
||||
(err || (stat && stat.rev === cacheRev))
|
||||
(err || (stat && stat.rev === cacheRev)) &&
|
||||
!this.settings.disableOfflineStorage
|
||||
) {
|
||||
logger.info(
|
||||
'Open file from cache because ' + (err ? 'stat error' : 'it is latest'),
|
||||
@@ -645,7 +697,7 @@ class AppModel {
|
||||
let needLoadKeyFile = false;
|
||||
if (!params.keyFileData && fileInfo && fileInfo.keyFileName) {
|
||||
params.keyFileName = fileInfo.keyFileName;
|
||||
if (this.settings.rememberKeyFiles === 'data') {
|
||||
if (this.settings.rememberKeyFiles === 'data' && fileInfo.keyFileHash) {
|
||||
params.keyFileData = FileModel.createKeyFileWithHash(fileInfo.keyFileHash);
|
||||
} else if (this.settings.rememberKeyFiles === 'path' && fileInfo.keyFilePath) {
|
||||
params.keyFilePath = fileInfo.keyFilePath;
|
||||
@@ -663,10 +715,13 @@ class AppModel {
|
||||
path: params.path,
|
||||
keyFileName: params.keyFileName,
|
||||
keyFilePath: params.keyFilePath,
|
||||
backup: (fileInfo && fileInfo.backup) || null,
|
||||
fingerprint: (fileInfo && fileInfo.fingerprint) || null,
|
||||
backup: fileInfo?.backup || null,
|
||||
chalResp: params.chalResp
|
||||
});
|
||||
if (params.encryptedPassword) {
|
||||
file.encryptedPassword = fileInfo.encryptedPassword;
|
||||
file.encryptedPasswordDate = fileInfo?.encryptedPasswordDate || new Date();
|
||||
}
|
||||
const openComplete = (err) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
@@ -685,7 +740,7 @@ class AppModel {
|
||||
if (fileInfo) {
|
||||
file.syncDate = fileInfo.syncDate;
|
||||
}
|
||||
if (updateCacheOnSuccess) {
|
||||
if (updateCacheOnSuccess && !this.settings.disableOfflineStorage) {
|
||||
logger.info('Save loaded file to cache');
|
||||
Storage.cache.save(file.id, null, params.fileData);
|
||||
}
|
||||
@@ -755,7 +810,6 @@ class AppModel {
|
||||
syncDate: file.syncDate || dt,
|
||||
openDate: dt,
|
||||
backup: file.backup,
|
||||
fingerprint: file.fingerprint,
|
||||
chalResp: file.chalResp
|
||||
});
|
||||
switch (this.settings.rememberKeyFiles) {
|
||||
@@ -771,6 +825,14 @@ class AppModel {
|
||||
keyFilePath: file.keyFilePath || null
|
||||
});
|
||||
}
|
||||
if (this.settings.deviceOwnerAuth === 'file' && file.encryptedPassword) {
|
||||
const maxDate = new Date(file.encryptedPasswordDate);
|
||||
maxDate.setMinutes(maxDate.getMinutes() + this.settings.deviceOwnerAuthTimeoutMinutes);
|
||||
if (maxDate > new Date()) {
|
||||
fileInfo.encryptedPassword = file.encryptedPassword;
|
||||
fileInfo.encryptedPasswordDate = file.encryptedPasswordDate;
|
||||
}
|
||||
}
|
||||
this.fileInfos.remove(file.id);
|
||||
this.fileInfos.unshift(fileInfo);
|
||||
this.fileInfos.save();
|
||||
@@ -808,14 +870,17 @@ class AppModel {
|
||||
if (data && backup && backup.enabled && backup.pending) {
|
||||
this.scheduleBackupFile(file, data);
|
||||
}
|
||||
if (params) {
|
||||
this.saveFileFingerprint(file, params.password);
|
||||
}
|
||||
if (this.settings.yubiKeyAutoOpen) {
|
||||
if (this.attachedYubiKeysCount > 0 && !this.files.some((f) => f.external)) {
|
||||
if (
|
||||
this.attachedYubiKeysCount > 0 &&
|
||||
!this.files.some((f) => f.backend === 'otp-device')
|
||||
) {
|
||||
this.tryOpenOtpDeviceInBackground();
|
||||
}
|
||||
}
|
||||
if (this.settings.deviceOwnerAuth) {
|
||||
this.saveEncryptedPassword(file, params);
|
||||
}
|
||||
}
|
||||
|
||||
fileClosed(file) {
|
||||
@@ -858,7 +923,7 @@ class AppModel {
|
||||
path = Storage[storage].getPathForName(file.name);
|
||||
}
|
||||
const optionsForLogging = { ...options };
|
||||
if (optionsForLogging && optionsForLogging.opts && optionsForLogging.opts.password) {
|
||||
if (optionsForLogging.opts && optionsForLogging.opts.password) {
|
||||
optionsForLogging.opts = { ...optionsForLogging.opts };
|
||||
optionsForLogging.opts.password = '***';
|
||||
}
|
||||
@@ -965,6 +1030,10 @@ class AppModel {
|
||||
logger.info('Updated sync date, saving modified file');
|
||||
saveToCacheAndStorage();
|
||||
} else if (file.dirty) {
|
||||
if (this.settings.disableOfflineStorage) {
|
||||
logger.info('File is dirty and cache is disabled');
|
||||
return complete(err);
|
||||
}
|
||||
logger.info('Saving not modified dirty file to cache');
|
||||
Storage.cache.save(fileInfo.id, null, data, (err) => {
|
||||
if (err) {
|
||||
@@ -1027,6 +1096,9 @@ class AppModel {
|
||||
} else if (!file.dirty) {
|
||||
logger.info('Saving to storage, skip cache because not dirty');
|
||||
saveToStorage(data);
|
||||
} else if (this.settings.disableOfflineStorage) {
|
||||
logger.info('Saving to storage because cache is disabled');
|
||||
saveToStorage(data);
|
||||
} else {
|
||||
logger.info('Saving to cache');
|
||||
Storage.cache.save(fileInfo.id, null, data, (err) => {
|
||||
@@ -1050,6 +1122,10 @@ class AppModel {
|
||||
logger.info('File does not exist in storage, creating');
|
||||
saveToCacheAndStorage();
|
||||
} else if (file.dirty) {
|
||||
if (this.settings.disableOfflineStorage) {
|
||||
logger.info('Stat error, dirty, cache is disabled', err || 'no error');
|
||||
return complete(err);
|
||||
}
|
||||
logger.info('Stat error, dirty, save to cache', err || 'no error');
|
||||
file.getData((data, e) => {
|
||||
if (e) {
|
||||
@@ -1087,6 +1163,14 @@ class AppModel {
|
||||
}
|
||||
}
|
||||
|
||||
deleteAllCachedFiles() {
|
||||
for (const fileInfo of this.fileInfos) {
|
||||
if (fileInfo.storage && !fileInfo.modified) {
|
||||
Storage.cache.remove(fileInfo.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearStoredKeyFiles() {
|
||||
for (const fileInfo of this.fileInfos) {
|
||||
fileInfo.set({
|
||||
@@ -1224,18 +1308,6 @@ class AppModel {
|
||||
}
|
||||
}
|
||||
|
||||
saveFileFingerprint(file, password) {
|
||||
if (Launcher && Launcher.fingerprints && !file.fingerprint) {
|
||||
const fileInfo = this.fileInfos.get(file.id);
|
||||
Launcher.fingerprints.register(file.id, password, (token) => {
|
||||
if (token) {
|
||||
fileInfo.fingerprint = token;
|
||||
this.fileInfos.save();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
usbDevicesChanged() {
|
||||
const attachedYubiKeysCount = this.attachedYubiKeysCount;
|
||||
|
||||
@@ -1246,7 +1318,9 @@ class AppModel {
|
||||
}
|
||||
|
||||
const isNewYubiKey = UsbListener.attachedYubiKeys > attachedYubiKeysCount;
|
||||
const hasOpenFiles = this.files.some((file) => file.active && !file.external);
|
||||
const hasOpenFiles = this.files.some(
|
||||
(file) => file.active && file.backend !== 'otp-device'
|
||||
);
|
||||
|
||||
if (isNewYubiKey && hasOpenFiles && !this.openingOtpDevice) {
|
||||
this.tryOpenOtpDeviceInBackground();
|
||||
@@ -1278,7 +1352,7 @@ class AppModel {
|
||||
return null;
|
||||
}
|
||||
for (const file of this.files) {
|
||||
if (file.external) {
|
||||
if (file.backend === 'otp-device') {
|
||||
const matchingEntry = file.getMatchingEntry(entry);
|
||||
if (matchingEntry) {
|
||||
return matchingEntry;
|
||||
@@ -1286,6 +1360,166 @@ class AppModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveEncryptedPassword(file, params) {
|
||||
if (!this.settings.deviceOwnerAuth || params.encryptedPassword) {
|
||||
return;
|
||||
}
|
||||
NativeModules.hardwareEncrypt(params.password)
|
||||
.then((encryptedPassword) => {
|
||||
encryptedPassword = encryptedPassword.toBase64();
|
||||
const fileInfo = this.fileInfos.get(file.id);
|
||||
const encryptedPasswordDate = new Date();
|
||||
file.encryptedPassword = encryptedPassword;
|
||||
file.encryptedPasswordDate = encryptedPasswordDate;
|
||||
if (this.settings.deviceOwnerAuth === 'file') {
|
||||
fileInfo.encryptedPassword = encryptedPassword;
|
||||
fileInfo.encryptedPasswordDate = encryptedPasswordDate;
|
||||
this.fileInfos.save();
|
||||
} else if (this.settings.deviceOwnerAuth === 'memory') {
|
||||
this.memoryPasswordStorage[file.id] = {
|
||||
value: encryptedPassword,
|
||||
date: encryptedPasswordDate
|
||||
};
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
file.encryptedPassword = null;
|
||||
file.encryptedPasswordDate = null;
|
||||
delete this.memoryPasswordStorage[file.id];
|
||||
this.appLogger.error('Error encrypting password', e);
|
||||
});
|
||||
}
|
||||
|
||||
getMemoryPassword(fileId) {
|
||||
return this.memoryPasswordStorage[fileId];
|
||||
}
|
||||
|
||||
checkEncryptedPasswordsStorage() {
|
||||
if (this.settings.deviceOwnerAuth === 'file') {
|
||||
let changed = false;
|
||||
for (const fileInfo of this.fileInfos) {
|
||||
if (this.memoryPasswordStorage[fileInfo.id]) {
|
||||
fileInfo.encryptedPassword = this.memoryPasswordStorage[fileInfo.id].value;
|
||||
fileInfo.encryptedPasswordDate = this.memoryPasswordStorage[fileInfo.id].date;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
this.fileInfos.save();
|
||||
}
|
||||
for (const file of this.files) {
|
||||
if (this.memoryPasswordStorage[file.id]) {
|
||||
file.encryptedPassword = this.memoryPasswordStorage[file.id].value;
|
||||
file.encryptedPasswordDate = this.memoryPasswordStorage[file.id].date;
|
||||
}
|
||||
}
|
||||
} else if (this.settings.deviceOwnerAuth === 'memory') {
|
||||
let changed = false;
|
||||
for (const fileInfo of this.fileInfos) {
|
||||
if (fileInfo.encryptedPassword) {
|
||||
this.memoryPasswordStorage[fileInfo.id] = {
|
||||
value: fileInfo.encryptedPassword,
|
||||
date: fileInfo.encryptedPasswordDate
|
||||
};
|
||||
fileInfo.encryptedPassword = null;
|
||||
fileInfo.encryptedPasswordDate = null;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
this.fileInfos.save();
|
||||
}
|
||||
} else {
|
||||
let changed = false;
|
||||
for (const fileInfo of this.fileInfos) {
|
||||
if (fileInfo.encryptedPassword) {
|
||||
fileInfo.encryptedPassword = null;
|
||||
fileInfo.encryptedPasswordDate = null;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
this.fileInfos.save();
|
||||
}
|
||||
for (const file of this.files) {
|
||||
if (file.encryptedPassword) {
|
||||
file.encryptedPassword = null;
|
||||
file.encryptedPasswordDate = null;
|
||||
}
|
||||
}
|
||||
this.memoryPasswordStorage = {};
|
||||
}
|
||||
}
|
||||
|
||||
unlockAnyFile(unlockRes, timeout) {
|
||||
this.rejectPendingFileUnlockPromise('Replaced with a new operation');
|
||||
Events.emit('show-open-view');
|
||||
return new Promise((resolve, reject) => {
|
||||
this.fileUnlockPromise = { resolve, reject, unlockRes };
|
||||
if (timeout) {
|
||||
const timer = setTimeout(
|
||||
() => this.rejectPendingFileUnlockPromise('Timeout'),
|
||||
timeout
|
||||
);
|
||||
this.fileUnlockPromise.resolve = (res) => {
|
||||
clearTimeout(timer);
|
||||
resolve(res);
|
||||
};
|
||||
this.fileUnlockPromise.reject = (err) => {
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
};
|
||||
}
|
||||
this.appLogger.info('Pending file unlock operation is set');
|
||||
Events.emit('unlock-message-changed', unlockRes);
|
||||
});
|
||||
}
|
||||
|
||||
get unlockMessageRes() {
|
||||
return this.fileUnlockPromise?.unlockRes;
|
||||
}
|
||||
|
||||
rejectPendingFileUnlockPromise(reason) {
|
||||
if (this.fileUnlockPromise) {
|
||||
this.appLogger.info('Cancel pending file unlock operation', reason);
|
||||
this.fileUnlockPromise.reject(new Error(reason));
|
||||
this.fileUnlockPromise = null;
|
||||
Events.emit('unlock-message-changed', null);
|
||||
}
|
||||
}
|
||||
|
||||
mainWindowBlur() {
|
||||
if (!this.hardwareDecryptInProgress) {
|
||||
this.mainWindowBlurTimer = setTimeout(() => {
|
||||
// macOS emits focus-blur-focus event in a row when triggering auto-type from minimized state
|
||||
delete this.mainWindowBlurTimer;
|
||||
this.rejectPendingFileUnlockPromise('Main window blur');
|
||||
}, Timeouts.AutoTypeWindowFocusAfterBlur);
|
||||
}
|
||||
}
|
||||
|
||||
mainWindowFocus() {
|
||||
if (this.mainWindowBlurTimer) {
|
||||
clearTimeout(this.mainWindowBlurTimer);
|
||||
this.mainWindowBlurTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
mainWindowWillClose() {
|
||||
this.rejectPendingFileUnlockPromise('Main window will close');
|
||||
}
|
||||
|
||||
hardwareDecryptStarted() {
|
||||
this.hardwareDecryptInProgress = true;
|
||||
}
|
||||
|
||||
hardwareDecryptFinished() {
|
||||
this.hardwareDecryptInProgress = false;
|
||||
if (!Launcher.isAppFocused()) {
|
||||
this.rejectPendingFileUnlockPromise('App is not focused after hardware decrypt');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { AppModel };
|
||||
|
||||
@@ -21,6 +21,15 @@ class AppSettingsModel extends Model {
|
||||
if (data.rememberKeyFiles === true) {
|
||||
data.rememberKeyFiles = 'data';
|
||||
}
|
||||
if (data.locale === 'en') {
|
||||
data.locale = 'en-US';
|
||||
}
|
||||
if (data.theme === 'macdark') {
|
||||
data.theme = 'dark';
|
||||
}
|
||||
if (data.theme === 'wh') {
|
||||
data.theme = 'light';
|
||||
}
|
||||
}
|
||||
|
||||
save() {
|
||||
|
||||