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 | ||
|
|
c313de9f93 | ||
|
|
09b00f3137 | ||
|
|
8342336e8d | ||
|
|
fe245dc95e | ||
|
|
3001afd9db | ||
|
|
48b5c8d4e7 | ||
|
|
21dc4e758f | ||
|
|
5c5fc0271e | ||
|
|
082c6bee93 | ||
|
|
c1d354a6fb | ||
|
|
eab683ec71 | ||
|
|
cd2b87b317 | ||
|
|
4abe63c00f | ||
|
|
7e4e61e005 | ||
|
|
db38e015f8 | ||
|
|
ff20d2a554 | ||
|
|
cb7df205bb | ||
|
|
dfe2c42172 | ||
|
|
4854c0c100 | ||
|
|
33bdf124c2 | ||
|
|
2d752ef144 | ||
|
|
d258bce037 | ||
|
|
21ff37d82e | ||
|
|
312b08ca99 | ||
|
|
a47f0d0ae5 | ||
|
|
c1a6268c2d | ||
|
|
65b6fe2e2e | ||
|
|
0f29e94272 | ||
|
|
dd9b6a1943 | ||
|
|
a45bc13248 | ||
|
|
b7b5caac9d | ||
|
|
0eb5ebab0d | ||
|
|
f34b1011c8 | ||
|
|
33049c21e7 | ||
|
|
6c12822279 | ||
|
|
8a3ff150a6 | ||
|
|
de4bfdf9e0 | ||
|
|
461a45dc51 | ||
|
|
2b24d716a9 | ||
|
|
36b6fd80b8 | ||
|
|
f8cbf51842 | ||
|
|
17fbf9188d | ||
|
|
ec5bacd951 | ||
|
|
56c8649040 | ||
|
|
435eb5779f | ||
|
|
a83e04fe4d | ||
|
|
bdbff334a5 | ||
|
|
f70eb35f69 | ||
|
|
f1a5aba485 | ||
|
|
f8e454dab3 | ||
|
|
9988454af1 | ||
|
|
e0b65bbebf | ||
|
|
09fd37820b | ||
|
|
360194c627 | ||
|
|
21768b1b59 | ||
|
|
325d521f49 | ||
|
|
2deb241ff2 | ||
|
|
a2194435e6 | ||
|
|
448e173f7e | ||
|
|
4b52280549 | ||
|
|
9e09f7b7e7 | ||
|
|
83293d909e | ||
|
|
176553a674 | ||
|
|
a4b22b6c88 | ||
|
|
fb6bcdf5c9 | ||
|
|
776c75d823 | ||
|
|
143f7c6a7a | ||
|
|
b520c58ef4 | ||
|
|
3c47adbf32 | ||
|
|
835acff637 | ||
|
|
a08e3c7d27 | ||
|
|
d9258745d0 | ||
|
|
70bd4f1c4e | ||
|
|
ab3860cdc9 | ||
|
|
9185ef9a55 | ||
|
|
e6826b176e | ||
|
|
f5bb29e61a | ||
|
|
d0d013e046 | ||
|
|
5002c2ce67 | ||
|
|
8e3b218a74 | ||
|
|
51bd80faf4 | ||
|
|
b4fe0af53a | ||
|
|
2ba9a03e2c | ||
|
|
7394754b6f | ||
|
|
b41b291e60 | ||
|
|
0f582e3248 | ||
|
|
b90e84be42 | ||
|
|
ef627c9530 | ||
|
|
b3f021a577 | ||
|
|
96263c11ec | ||
|
|
b2a6e1180c | ||
|
|
817b44b152 | ||
|
|
701bb4caa4 | ||
|
|
a1b8682316 | ||
|
|
8c386dbe57 | ||
|
|
d8e49d993a | ||
|
|
93310eb029 | ||
|
|
f215c26a63 | ||
|
|
b5fddeb627 | ||
|
|
81d4c5c403 | ||
|
|
bf7dec2437 | ||
|
|
efa9b5f9a7 | ||
|
|
1f862a37e9 | ||
|
|
e8a491dba5 | ||
|
|
aa899b8096 | ||
|
|
8de55c450a | ||
|
|
1fafa68793 | ||
|
|
e69219697a | ||
|
|
08a0f6e587 | ||
|
|
7a26d80255 | ||
|
|
4171848655 | ||
|
|
a7193fd783 | ||
|
|
ccce95d4fe | ||
|
|
b9f37c22d9 | ||
|
|
3b3c95e57c | ||
|
|
fadea36232 | ||
|
|
99b687244c | ||
|
|
813b26270f | ||
|
|
b715507dd0 | ||
|
|
0cb502ca67 | ||
|
|
e5073eb65f | ||
|
|
c3b55cd7fa | ||
|
|
e9ef1bd698 | ||
|
|
045fe99158 | ||
|
|
dad95033c0 | ||
|
|
02e4d52db6 | ||
|
|
1f83f34e55 | ||
|
|
1ffb45433c | ||
|
|
b6780dff03 | ||
|
|
5b91dcb272 | ||
|
|
099f1ca9d0 | ||
|
|
c73f81a80a | ||
|
|
177b150c95 | ||
|
|
489eb5598c | ||
|
|
ad571c1e27 | ||
|
|
9827b871fc | ||
|
|
004436e868 | ||
|
|
008b615903 | ||
|
|
451b465e4f | ||
|
|
c91e5de961 | ||
|
|
8251423d95 | ||
|
|
65f66ac60c | ||
|
|
94caa1fca3 | ||
|
|
f7d1708550 | ||
|
|
b888f9da32 | ||
|
|
4d678184f3 | ||
|
|
a2bf157b01 | ||
|
|
1b279119cb | ||
|
|
2075e82381 | ||
|
|
9f60ca8ece | ||
|
|
d6867fb62e | ||
|
|
07e918965b | ||
|
|
24bcc758e7 | ||
|
|
7ae64b0552 | ||
|
|
38e46f6d85 | ||
|
|
530316c25f | ||
|
|
cc750df077 | ||
|
|
3c85694fc2 | ||
|
|
955a1346ff | ||
|
|
8e4566fe5f | ||
|
|
20623e4c42 | ||
|
|
cac31d3350 | ||
|
|
746b56ac57 | ||
|
|
bc140b3fb5 | ||
|
|
5d15411d3d | ||
|
|
c6ce694dfe | ||
|
|
16d303d966 | ||
|
|
f422dbd3db | ||
|
|
5125a44170 | ||
|
|
6e7f2c570f | ||
|
|
db31e65cdf | ||
|
|
3cd9bf6505 | ||
|
|
3b05f6267e | ||
|
|
3e5b0bff4f | ||
|
|
c9cb15d7b7 | ||
|
|
89415c86a4 | ||
|
|
4cd515b19f | ||
|
|
730e2c5ba5 | ||
|
|
3735a6689c | ||
|
|
15c81d8f35 | ||
|
|
154c780281 | ||
|
|
ec9422d22d | ||
|
|
101e8eaa24 | ||
|
|
6a31bba69c | ||
|
|
96ee68dee7 | ||
|
|
5a94c81b11 | ||
|
|
cf8aa6f9a4 | ||
|
|
20e201795e | ||
|
|
e95902d2bb | ||
|
|
79a3b39248 | ||
|
|
c08a0eb8d1 | ||
|
|
a280258d4f | ||
|
|
7985a7a522 | ||
|
|
6ba2e16a91 | ||
|
|
f34ebe2fd3 | ||
|
|
ab3d4a056f | ||
|
|
c88a7c000b | ||
|
|
53b700aa70 | ||
|
|
66c219f6d1 | ||
|
|
06dd153955 | ||
|
|
88a0e63894 | ||
|
|
cac7e53ea8 | ||
|
|
3e390388eb | ||
|
|
2a1b01c0aa | ||
|
|
5a535bb07a | ||
|
|
235ceae228 | ||
|
|
46710aead4 | ||
|
|
277d443d4c | ||
|
|
770c25a273 | ||
|
|
c935de3ec1 | ||
|
|
8350123839 | ||
|
|
02adb81ae0 | ||
|
|
cf26427e68 | ||
|
|
6e357e93ec | ||
|
|
1c8f3932fb | ||
|
|
75671a0a8a | ||
|
|
58296dd896 | ||
|
|
2fff34dc61 | ||
|
|
851ade7e24 | ||
|
|
13585f5317 | ||
|
|
5d0ab36f2c | ||
|
|
be46cf9a25 | ||
|
|
ffdc438091 | ||
|
|
52741cf46a | ||
|
|
9263caed59 | ||
|
|
aa5a9fc6d2 | ||
|
|
316518d63b | ||
|
|
03c44d76c9 | ||
|
|
346d6cb119 | ||
|
|
dae3d57d1e | ||
|
|
aa65cef103 | ||
|
|
e19c923538 | ||
|
|
ec292705b3 | ||
|
|
d36588d148 | ||
|
|
32c171d05b | ||
|
|
f031be4ce2 | ||
|
|
d3ebcabec4 | ||
|
|
f683f14a30 | ||
|
|
0a311e87e4 | ||
|
|
d42ce3e7bb | ||
|
|
4c5d7eb762 | ||
|
|
1d58779d90 | ||
|
|
1434d81d3e | ||
|
|
b44d060fac | ||
|
|
7c89ebc047 | ||
|
|
d140427c94 | ||
|
|
2abdf3693c | ||
|
|
8d1eafe2bc | ||
|
|
510f982579 | ||
|
|
63e6be9867 | ||
|
|
0a2957449f | ||
|
|
ee2ba047ab | ||
|
|
a8590c1b90 | ||
|
|
374f4bc116 | ||
|
|
76fa1f6c9f | ||
|
|
dd68aa342a | ||
|
|
21d614aa97 | ||
|
|
f4e4bbbb8f | ||
|
|
d30c9d9d90 | ||
|
|
e0884a4eec | ||
|
|
446c7f3ab5 | ||
|
|
48a6189fe7 | ||
|
|
e568d06b02 | ||
|
|
243c962667 | ||
|
|
908510ff0b | ||
|
|
d6aab0c470 | ||
|
|
8d2640d99d | ||
|
|
d45748cc32 | ||
|
|
2693dd9366 | ||
|
|
dcfa070176 | ||
|
|
eb577d61d5 | ||
|
|
f79bb90b69 | ||
|
|
4591268063 | ||
|
|
7e849934ff | ||
|
|
92fb322f7b | ||
|
|
abc154738c | ||
|
|
63850f75af | ||
|
|
e0498e5e87 | ||
|
|
232f0b8d69 | ||
|
|
d410dbb17d | ||
|
|
e7c3d0441c | ||
|
|
0119d69641 | ||
|
|
a21fdf50bb | ||
|
|
8fdc5e6d94 | ||
|
|
41e81c2d7a | ||
|
|
10d2f5e641 | ||
|
|
cdfcc6493c | ||
|
|
6b26f6d93f | ||
|
|
a11cf38378 | ||
|
|
7d061c4aaf | ||
|
|
152fcccf60 | ||
|
|
159104a50a | ||
|
|
66b2b77791 | ||
|
|
36ade29e46 | ||
|
|
61c44057cb | ||
|
|
fd9dc1a801 | ||
|
|
57b813196f | ||
|
|
eaf429de3d | ||
|
|
45923d034e | ||
|
|
fbdfacd14d | ||
|
|
a55ddda6cf | ||
|
|
6c66d0791a | ||
|
|
c19d427f3a | ||
|
|
a078081347 | ||
|
|
3a042f25c2 | ||
|
|
6d781a6358 | ||
|
|
349ec59df7 | ||
|
|
833aa3c748 | ||
|
|
eca5b1c7a0 | ||
|
|
0aa925f95d | ||
|
|
2f214b25c7 | ||
|
|
38c3a46395 | ||
|
|
cbb9a29c8d | ||
|
|
dddc4f235e | ||
|
|
c537d0f464 | ||
|
|
8be588afdf | ||
|
|
5b6c707de5 | ||
|
|
8b6dd322a7 | ||
|
|
dfdbd8b092 | ||
|
|
4fad58b1e9 | ||
|
|
6003b21f6a | ||
|
|
963f09057c | ||
|
|
b185c3e102 | ||
|
|
272365dde7 | ||
|
|
da91254cf0 | ||
|
|
70642b464e | ||
|
|
035a4485b7 | ||
|
|
73ac7496e2 | ||
|
|
40d816f86d | ||
|
|
c513fb06e7 | ||
|
|
dec0b4d171 | ||
|
|
03ed25f617 | ||
|
|
487afe799e | ||
|
|
d15366ee24 | ||
|
|
816c7776f7 | ||
|
|
f5eadc5d9e | ||
|
|
8e02c0bd10 | ||
|
|
614f61ce29 | ||
|
|
6c80d50290 | ||
|
|
5492e53ba9 | ||
|
|
e795a317cc | ||
|
|
8eb7366a4e | ||
|
|
b85eab9d10 | ||
|
|
09b0920337 | ||
|
|
502860ac5e | ||
|
|
68d845ff3d | ||
|
|
b2e69b60a6 | ||
|
|
8015da09e8 | ||
|
|
06ed20f59c | ||
|
|
a003d8bfe6 | ||
|
|
468aa28449 | ||
|
|
e0615c751e | ||
|
|
81e4a0eff9 | ||
|
|
b48ece91b2 | ||
|
|
912d2b1e0a | ||
|
|
c97f45e3e5 | ||
|
|
de4230e0d0 | ||
|
|
79ca627e7c | ||
|
|
52045ad2a6 | ||
|
|
a6a9554868 | ||
|
|
d92229095d | ||
|
|
1d1ce0c322 | ||
|
|
cb45eb4b3c | ||
|
|
0b31150a43 | ||
|
|
b626152fda | ||
|
|
3cc2cedaa5 | ||
|
|
e2fb54f88d | ||
|
|
1f334ab3dc | ||
|
|
c39933c051 | ||
|
|
b6719e8972 | ||
|
|
cfaa765858 | ||
|
|
8477df7e5c | ||
|
|
40f3ccc112 | ||
|
|
a40c116617 | ||
|
|
c97219311f | ||
|
|
49d618f066 | ||
|
|
129a77ae7c | ||
|
|
b2d13db01f | ||
|
|
c76cebc469 | ||
|
|
668bb9b5d5 | ||
|
|
ec82af7a68 | ||
|
|
73b7880d73 | ||
|
|
a584eecb70 | ||
|
|
cd0a25f910 | ||
|
|
6cf2bf109c |
@@ -6,6 +6,9 @@
|
||||
"useBuiltIns": true
|
||||
}],
|
||||
"@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,5 +15,5 @@ trim_trailing_whitespace = true
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.nsh]
|
||||
[{*.nsh,*.yml,*.yaml,*.json}]
|
||||
indent_size = 2
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"plugins": ["prettier", "import"],
|
||||
"plugins": ["prettier", "import", "babel"],
|
||||
"extends": ["standard", "eslint:recommended", "plugin:prettier/recommended"],
|
||||
"rules": {
|
||||
"semi": ["error", "always"],
|
||||
@@ -15,7 +15,7 @@
|
||||
"no-useless-escape": "off",
|
||||
"no-var": "error",
|
||||
"prefer-const": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"no-unused-expressions": "off",
|
||||
"strict": ["error", "never"],
|
||||
"no-mixed-operators": "off",
|
||||
"prefer-promise-reject-errors": "off",
|
||||
@@ -48,8 +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"
|
||||
"import/no-default-export": "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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: Bug report
|
||||
about: Create a report to help us improve KeeWeb
|
||||
title: ''
|
||||
labels: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -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
|
||||
@@ -2,7 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
FROM snapcore/snapcraft:stable
|
||||
|
||||
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_15.x | sudo -E bash -
|
||||
RUN apt-get install -y nodejs
|
||||
RUN npm i -g grunt-cli
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -0,0 +1,5 @@
|
||||
name: 'Linux Build'
|
||||
description: 'Build KeeWeb on Linux in Docker'
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'Dockerfile'
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
cd /github/workspace
|
||||
npm ci
|
||||
grunt desktop-linux
|
||||
@@ -0,0 +1,404 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# git-restore-mtime - Change mtime of files based on commit date of last change
|
||||
#
|
||||
# Copyright (C) 2012 Rodrigo Silva (MestreLion) <linux@rodrigosilva.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. See <http://www.gnu.org/licenses/gpl.html>
|
||||
#
|
||||
"""
|
||||
Change the modification time (mtime) of all files in work tree, based on the
|
||||
date of the most recent commit that modified the file.
|
||||
|
||||
Useful prior to generating release tarballs, so each file is archived with a
|
||||
date that is similar to the date when the file was actually last modified,
|
||||
assuming the actual modification date and its commit date are close.
|
||||
|
||||
By default ignores all ignored and untracked files, and also refuses to work
|
||||
on trees with uncommitted changes.
|
||||
"""
|
||||
|
||||
# TODO:
|
||||
# - Add -z on git whatchanged/ls-files so we don't deal with filename decode/OS normalization
|
||||
# - When Python is bumped to 3.7, use text instead of universal_newlines on subprocess
|
||||
# - Update "Statistics for some large projects" with modern hardware and repositories.
|
||||
# - Create a README.md for git-restore-mtime alone. It deserves extensive documentation
|
||||
# - Move Statistics there
|
||||
|
||||
# FIXME:
|
||||
# - When current dir is outside the worktree, e.g. using --work-tree, `git ls-files`
|
||||
# assume any relative pathspecs are to worktree root, not the current dir. As such,
|
||||
# relative pathspecs may not work.
|
||||
# - Renames and mode changes should not change file mtime:
|
||||
# - Must check on status 'R100' and mode changes with same blobs
|
||||
# - Should require status to be (A, C, M, R<100, T). D will never be processed as
|
||||
# filelist is a subset of lsfileslist.
|
||||
# - Check file (A, D) for directory mtime is not sufficient:
|
||||
# - Renames also change dir mtime, unless rename was on a parent dir
|
||||
# - If most recent change of all files in a dir was a [M]odification,
|
||||
# dir might not be touched at all.
|
||||
# - Dirs containing only subdirectories but no direct files will also
|
||||
# not be touched. They're files' [grand]parent dir, but never their dirname().
|
||||
# - Some solutions:
|
||||
# - After files done, perform some dir processing for missing dirs, finding latest
|
||||
# file (A, D, R)
|
||||
# - Simple approach: dir mtime is most recent child (dir or file) mtime
|
||||
# - Use a virtual concept of "created at most at" to fill missing info, bubble up
|
||||
# to parents and grandparents
|
||||
# - When handling [grand]parent dirs, stay inside <pathspec>
|
||||
# - Better handling of merge commits. `-m` is plain *wrong*. `-c/--cc` is perfect, but
|
||||
# painfully slow. First pass without merge commits is not accurate. Maybe add a new
|
||||
# `--accurate` mode for `--cc`?
|
||||
|
||||
if __name__ != "__main__":
|
||||
raise ImportError("{} should not be used as a module.".format(__name__))
|
||||
|
||||
import subprocess, shlex
|
||||
import sys, os.path
|
||||
import logging
|
||||
import argparse
|
||||
import time
|
||||
|
||||
|
||||
# Update symlinks only if the OS supports not following them
|
||||
UPDATE_SYMLINKS = bool(os.utime in getattr(os, 'supports_follow_symlinks', []))
|
||||
STEPMISSING = 100
|
||||
|
||||
|
||||
# Command-line interface ######################################################
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="""Restore original modification time of files based on the date of the
|
||||
most recent commit that modified them. Useful when generating release tarballs.""")
|
||||
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--quiet', '-q', dest='loglevel',
|
||||
action="store_const", const=logging.WARNING, default=logging.INFO,
|
||||
help="Suppress informative messages and summary statistics.")
|
||||
group.add_argument('--verbose', '-v', action="count",
|
||||
help="Print additional information for each processed file.")
|
||||
|
||||
parser.add_argument('--force', '-f', action="store_true",
|
||||
help="Force execution on trees with uncommitted changes.")
|
||||
|
||||
parser.add_argument('--merge', '-m', action="store_true",
|
||||
help="""Include merge commits. Leads to more recent mtimes and more files per
|
||||
commit, thus with the same mtime (which may or may not be what you want). Including
|
||||
merge commits may lead to less commits being evaluated (all files are found sooner),
|
||||
which improves performance, sometimes substantially. But since merge commits are
|
||||
usually huge, processing them may also take longer, sometimes substantially.
|
||||
By default merge logs are only used for files missing from regular commit logs.""")
|
||||
|
||||
parser.add_argument('--first-parent', action="store_true",
|
||||
help="""Consider only the first parent, the "main branch", when parsing merge
|
||||
commit logs. Only effective when merge commits are included in the log, either
|
||||
by --merge or to find missing files after first log parse. See --skip-missing.""")
|
||||
|
||||
parser.add_argument('--skip-missing', '-s',
|
||||
action="store_false", default=True, dest="missing",
|
||||
help="""Do not try to find missing files. If some files were not found in regular
|
||||
commit logs, by default it re-tries using merge commit logs for these files (if
|
||||
--merge was not already used). This option disables this behavior, which may slightly
|
||||
improve performance, but files found only in merge commits will not be updated.""")
|
||||
|
||||
parser.add_argument('--no-directories', '-D',
|
||||
action="store_false", default=True, dest='dirs',
|
||||
help="""Do not update directory mtime for files created, renamed or deleted in it.
|
||||
Note: just modifying a file will not update its directory mtime.""")
|
||||
|
||||
parser.add_argument('--test', '-t', action="store_true", default=False,
|
||||
help="Test run: do not actually update any file")
|
||||
|
||||
parser.add_argument('--commit-time', '-c',
|
||||
action='store_true', default=False, dest='commit_time',
|
||||
help="Use commit time instead of author time")
|
||||
|
||||
parser.add_argument('pathspec', nargs='*', metavar='PATH',
|
||||
help="""Only modify paths matching PATH, directories or files, relative to current
|
||||
directory. Default is to modify all files handled by git, ignoring untracked files
|
||||
and submodules.""")
|
||||
|
||||
parser.add_argument('--work-tree', dest='workdir',
|
||||
help="Path to the work tree, if not current directory or one of its parents.")
|
||||
|
||||
parser.add_argument('--git-dir', dest='gitdir',
|
||||
help="Path to the git repository, if not the default <work-tree-root>/.git")
|
||||
|
||||
parser.add_argument('--skip-older-than', metavar='SECONDS', type=int,
|
||||
help="""Do not modify files that are older than %(metavar)s.
|
||||
It can significantly improve performance if fewer files are processed.
|
||||
Useful on CI builds, which can eventually switch workspace to different branch,
|
||||
but mostly performs builds on the same one (e.g. master).
|
||||
""")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
# Helper functions ############################################################
|
||||
|
||||
def setup_logging(args):
|
||||
TRACE = logging.DEBUG // 2
|
||||
logging.Logger.trace = lambda _, m, *a, **k: _.log(TRACE, m, *a, **k)
|
||||
level = (args.verbose and max(TRACE, logging.DEBUG // args.verbose)) or args.loglevel
|
||||
logging.basicConfig(level=level, format='%(message)s')
|
||||
return logging.getLogger()
|
||||
|
||||
|
||||
def normalize(path):
|
||||
"""Normalize paths from git, handling non-ASCII characters.
|
||||
|
||||
Git for Windows, as of v1.7.10, stores paths as UTF-8 normalization form C. If path
|
||||
contains non-ASCII or non-printable chars it outputs the UTF-8 in octal-escaped
|
||||
notation, double-quoting the whole path. Double-quotes and backslashes are also escaped.
|
||||
|
||||
https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath
|
||||
https://github.com/msysgit/msysgit/wiki/Git-for-Windows-Unicode-Support
|
||||
https://github.com/git/git/blob/master/Documentation/i18n.txt
|
||||
|
||||
Example on git output, this function reverts this:
|
||||
r'back\slash_double"quote_açaí' -> r'"back\\slash_double\"quote_a\303\247a\303\255"'
|
||||
"""
|
||||
if path and path[0] == '"':
|
||||
# Python 2: path = path[1:-1].decode("string-escape")
|
||||
# Python 3: https://stackoverflow.com/a/46650050/624066
|
||||
path = (path[1:-1] # Remove enclosing double quotes
|
||||
.encode('latin1') # Convert to bytes, required 'unicode-escape'
|
||||
.decode('unicode-escape') # Perform the actual octal-escaping decode
|
||||
.encode('latin1') # 1:1 mapping to bytes, forming UTF-8 encoding
|
||||
.decode('utf8')) # Decode from UTF-8
|
||||
# Make sure the slash matches the OS; for Windows we need a backslash
|
||||
return os.path.normpath(path)
|
||||
|
||||
|
||||
if UPDATE_SYMLINKS:
|
||||
def touch(path, mtime, test=False):
|
||||
"""The actual mtime update"""
|
||||
if test: return
|
||||
os.utime(path, (mtime, mtime), follow_symlinks=False)
|
||||
else:
|
||||
def touch(path, mtime, test=False):
|
||||
"""The actual mtime update"""
|
||||
if test: return
|
||||
os.utime(path, (mtime, mtime))
|
||||
|
||||
|
||||
def isodate(secs):
|
||||
return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(secs))
|
||||
|
||||
|
||||
# Git class and parselog(), the heart of the script ###########################
|
||||
|
||||
class Git():
|
||||
def __init__(self, workdir=None, gitdir=None):
|
||||
self.gitcmd = ['git']
|
||||
if workdir: self.gitcmd.extend(('--work-tree', workdir))
|
||||
if gitdir : self.gitcmd.extend(('--git-dir', gitdir))
|
||||
self.workdir, self.gitdir = self._repodirs()
|
||||
|
||||
def ls_files(self, pathlist=None):
|
||||
return (normalize(_) for _ in self._run('ls-files --full-name', pathlist))
|
||||
|
||||
def is_dirty(self):
|
||||
return bool(self._run('diff --no-ext-diff --quiet', output=False))
|
||||
|
||||
def log(self, merge=False, first_parent=False, commit_time=False, pathlist=None):
|
||||
cmd = 'whatchanged --pretty={}'.format('%ct' if commit_time else '%at')
|
||||
if merge: cmd += ' -m'
|
||||
if first_parent: cmd += ' --first-parent'
|
||||
return self._run(cmd, pathlist)
|
||||
|
||||
def _repodirs(self):
|
||||
return (os.path.normpath(_) for _ in
|
||||
self._run('rev-parse --show-toplevel --absolute-git-dir', check=True))
|
||||
|
||||
def _run(self, cmdstr, pathlist=None, output=True, check=False):
|
||||
cmdlist = self.gitcmd + shlex.split(cmdstr)
|
||||
if pathlist:
|
||||
cmdlist.append('--')
|
||||
cmdlist.extend(pathlist)
|
||||
log.trace("Executing: %s", ' '.join(cmdlist))
|
||||
if not output:
|
||||
return subprocess.call(cmdlist)
|
||||
if check:
|
||||
try:
|
||||
stdout = subprocess.check_output(cmdlist, universal_newlines=True)
|
||||
return stdout.splitlines()
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise self.Error(e.returncode, e.cmd, e.output, e.stderr)
|
||||
self.proc = subprocess.Popen(cmdlist, stdout=subprocess.PIPE, universal_newlines=True)
|
||||
return (_.strip() for _ in self.proc.stdout)
|
||||
|
||||
class Error(subprocess.CalledProcessError): pass
|
||||
|
||||
|
||||
def parselog(filelist, dirlist, stats, git, merge=False, filterlist=None):
|
||||
mtime = 0
|
||||
for line in git.log(merge, args.first_parent, args.commit_time, filterlist):
|
||||
stats['loglines'] += 1
|
||||
|
||||
# Blank line between Date and list of files
|
||||
if not line: continue
|
||||
|
||||
# File line
|
||||
if line[0] == ':': # Faster than line.startswith(':')
|
||||
# If line describes a rename, linetok has three tokens, otherwise two
|
||||
linetok = line.split('\t')
|
||||
status = linetok[0]
|
||||
file = linetok[-1]
|
||||
|
||||
# Handles non-ASCII chars and OS path separator
|
||||
file = normalize(file)
|
||||
|
||||
if file in filelist:
|
||||
stats['files'] -= 1
|
||||
log.debug("%d\t%d\t%d\t%s\t%s",
|
||||
stats['loglines'], stats['commits'], stats['files'],
|
||||
isodate(mtime), file)
|
||||
filelist.remove(file)
|
||||
try:
|
||||
touch(os.path.join(git.workdir, file), mtime, args.test)
|
||||
stats['touches'] += 1
|
||||
except Exception as e:
|
||||
log.error("ERROR: %s", e)
|
||||
stats['errors'] += 1
|
||||
|
||||
if args.dirs:
|
||||
dirname = os.path.dirname(file)
|
||||
if status[-1] in ('A', 'D') and dirname in dirlist:
|
||||
log.debug("%d\t%d\t-\t%s\t%s",
|
||||
stats['loglines'], stats['commits'],
|
||||
isodate(mtime), "{}/".format(dirname or '.'))
|
||||
dirlist.remove(dirname)
|
||||
try:
|
||||
touch(os.path.join(git.workdir, dirname), mtime, args.test)
|
||||
stats['dirtouches'] += 1
|
||||
except Exception as e:
|
||||
log.error("ERROR: %s", e)
|
||||
stats['direrrors'] += 1
|
||||
|
||||
# Date line
|
||||
else:
|
||||
stats['commits'] += 1
|
||||
mtime = int(line)
|
||||
|
||||
# All files done?
|
||||
if not stats['files']:
|
||||
git.proc.terminate() # hackish, but does the job. Not needed anyway
|
||||
return
|
||||
|
||||
|
||||
# Main Logic ##################################################################
|
||||
|
||||
def main():
|
||||
start = time.time() # yes, Wall time. CPU time is not realistic for users.
|
||||
stats = {_: 0 for _ in ('loglines', 'commits', 'touches', 'errors', 'dirtouches', 'direrrors')}
|
||||
|
||||
# First things first: Where and Who are we?
|
||||
try:
|
||||
git = Git(args.workdir, args.gitdir)
|
||||
except Git.Error as e:
|
||||
# Not in a git repository, and git already informed user on stderr. So we just...
|
||||
return e.returncode
|
||||
|
||||
# Do not work on dirty repositories, unless --force
|
||||
if not args.force and git.is_dirty():
|
||||
log.critical(
|
||||
"ERROR: There are local changes in the working directory.\n"
|
||||
"This could lead to undesirable results for modified files.\n"
|
||||
"Please, commit your changes (or use --force) and try again.\n"
|
||||
"Aborting")
|
||||
return 1
|
||||
|
||||
# Get the files managed by git and build file and dir list to be processed
|
||||
filelist = set()
|
||||
dirlist = set()
|
||||
if UPDATE_SYMLINKS and not args.skip_older_than:
|
||||
filelist = set(git.ls_files(args.pathspec))
|
||||
dirlist = set(os.path.dirname(_) for _ in filelist)
|
||||
else:
|
||||
for path in git.ls_files(args.pathspec):
|
||||
fullpath = os.path.join(git.workdir, path)
|
||||
|
||||
# Symlink (to file, to dir or broken - git handles the same way)
|
||||
if not UPDATE_SYMLINKS and os.path.islink(fullpath):
|
||||
log.warning("WARNING: Skipping symlink, OS does not support update: %s", path)
|
||||
continue
|
||||
|
||||
# skip files which are older than given threshold
|
||||
if args.skip_older_than and start - os.path.getmtime(fullpath) > args.skip_older_than:
|
||||
continue
|
||||
|
||||
# Always add them relative to worktree root
|
||||
filelist.add(path)
|
||||
dirlist.add(os.path.dirname(path))
|
||||
|
||||
stats['totalfiles'] = stats['files'] = len(filelist)
|
||||
log.info("{0:,} files to be processed in work dir".format(stats['totalfiles']))
|
||||
|
||||
if not filelist:
|
||||
# Nothing to do. Exit silently and without errors, just like git does
|
||||
return
|
||||
|
||||
# Process the log until all files are 'touched'
|
||||
log.debug("Line #\tLog #\tF.Left\tModification Time\tFile Name")
|
||||
parselog(filelist, dirlist, stats, git, args.merge, args.pathspec)
|
||||
|
||||
# Missing files
|
||||
if filelist:
|
||||
# Try to find them in merge logs, if not done already
|
||||
# (usually HUGE, thus MUCH slower!)
|
||||
if args.missing and not args.merge:
|
||||
filterlist = list(filelist)
|
||||
for i in range(0, len(filterlist), STEPMISSING):
|
||||
parselog(filelist, dirlist, stats, git,
|
||||
merge=True, filterlist=filterlist[i:i+STEPMISSING])
|
||||
|
||||
# Still missing some?
|
||||
for file in filelist:
|
||||
log.warning("WARNING: not found in log: %s", file)
|
||||
|
||||
# Final statistics
|
||||
# Suggestion: use git-log --before=mtime to brag about skipped log entries
|
||||
log.info(
|
||||
"Statistics:\n"
|
||||
"{:13,.2f} seconds\n"
|
||||
"{:13,} log lines processed\n"
|
||||
"{:13,} commits evaluated"
|
||||
"".format(time.time()-start, stats['loglines'], stats['commits']))
|
||||
|
||||
if args.dirs:
|
||||
if stats['direrrors']: log.info("{:13,} directory update errors".format(stats['direrrors']))
|
||||
log.info("{:13,} directories updated".format(stats['dirtouches']))
|
||||
|
||||
if stats['touches'] != stats['totalfiles']: log.info("{:13,} files".format(stats['totalfiles']))
|
||||
if stats['files']: log.info("{:13,} files missing".format(stats['files']))
|
||||
if stats['errors']: log.info("{:13,} file update errors".format(stats['errors']))
|
||||
|
||||
log.info("{:13,} files updated".format(stats['touches']))
|
||||
|
||||
if args.test:
|
||||
log.info("TEST RUN - No files modified!")
|
||||
|
||||
|
||||
args = parse_args()
|
||||
log = setup_logging(args)
|
||||
log.trace("Arguments: %s", args)
|
||||
|
||||
# UI done, it's show time!
|
||||
try:
|
||||
sys.exit(main())
|
||||
except KeyboardInterrupt:
|
||||
log.info("Aborting")
|
||||
sys.exit(-1)
|
||||
@@ -0,0 +1,426 @@
|
||||
name: Build
|
||||
on:
|
||||
push:
|
||||
tags: [ '*' ]
|
||||
jobs:
|
||||
|
||||
web:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get current git tag
|
||||
id: get_tag
|
||||
uses: keeweb/get-git-tag@v3.0.2
|
||||
with:
|
||||
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
|
||||
tagRegexGroup: 1
|
||||
- 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
|
||||
run: npm test
|
||||
- name: Grunt
|
||||
run: grunt
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: dist
|
||||
- name: Write secrets
|
||||
env:
|
||||
VIRUS_TOTAL: ${{ secrets.VIRUS_TOTAL }}
|
||||
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
|
||||
needs:
|
||||
- web
|
||||
steps:
|
||||
- name: Get current git tag
|
||||
id: get_tag
|
||||
uses: keeweb/get-git-tag@v3.0.2
|
||||
with:
|
||||
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
|
||||
tagRegexGroup: 1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: keeweb/keeweb
|
||||
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: dist
|
||||
- name: Write secrets
|
||||
env:
|
||||
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
|
||||
KEEWEB_SIGN: ${{ secrets.KEEWEB_SIGN }}
|
||||
run: |
|
||||
mkdir keys
|
||||
echo "$PRIVATE_KEY" > keys/private-key.pem
|
||||
echo "$KEEWEB_SIGN" > keys/keeweb-sign.json
|
||||
- name: Build in Docker
|
||||
uses: ./.github/actions/linux-build
|
||||
- name: Upload AppImage artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.AppImage
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.AppImage
|
||||
- name: Upload snap artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.snap
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.snap
|
||||
- name: Upload deb artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.deb
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.deb
|
||||
- name: Upload zip artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.zip
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.zip
|
||||
- name: Upload rpm artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
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
|
||||
|
||||
darwin:
|
||||
runs-on: macos-latest
|
||||
needs:
|
||||
- web
|
||||
steps:
|
||||
- name: Get current git tag
|
||||
id: get_tag
|
||||
uses: keeweb/get-git-tag@v3.0.2
|
||||
with:
|
||||
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
|
||||
tagRegexGroup: 1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: keeweb/keeweb
|
||||
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: dist
|
||||
- name: Upgrade Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install npm modules
|
||||
run: npm ci
|
||||
- name: Install grunt
|
||||
run: sudo npm i -g grunt-cli
|
||||
- name: Write secrets
|
||||
env:
|
||||
CODESIGN: ${{ secrets.CODESIGN }}
|
||||
APPLE_DEPLOY_PASSWORD: ${{ secrets.APPLE_DEPLOY_PASSWORD }}
|
||||
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
||||
APPLE_PROVISIONING_PROFILE: ${{ secrets.APPLE_PROVISIONING_PROFILE }}
|
||||
run: |
|
||||
mkdir keys
|
||||
echo "$CODESIGN" > keys/codesign.json
|
||||
xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u "$APPLE_ID_USERNAME" -p "$APPLE_DEPLOY_PASSWORD"
|
||||
echo "$APPLE_PROVISIONING_PROFILE" | base64 -d > keys/keeweb.provisionprofile
|
||||
- name: Import certificates
|
||||
uses: keeweb/import-codesign-certs@v1
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
- name: Grunt
|
||||
run: grunt --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.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
|
||||
needs:
|
||||
- web
|
||||
steps:
|
||||
- name: Get current git tag
|
||||
id: get_tag
|
||||
uses: keeweb/get-git-tag@v3.0.2
|
||||
with:
|
||||
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
|
||||
tagRegexGroup: 1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: keeweb/keeweb
|
||||
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: dist
|
||||
- name: Upgrade Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install npm modules
|
||||
run: npm ci
|
||||
- name: Install grunt
|
||||
run: npm i -g grunt-cli
|
||||
- name: Write secrets
|
||||
env:
|
||||
CODESIGN: ${{ secrets.CODESIGN }}
|
||||
MS_CODESIGN_PRIVATE_KEY_CLIENT: ${{ secrets.MS_CODESIGN_PRIVATE_KEY_CLIENT }}
|
||||
MS_CODESIGN_PUBLIC_KEY_CLIENT: ${{ secrets.MS_CODESIGN_PUBLIC_KEY_CLIENT }}
|
||||
MS_CODESIGN_PUBLIC_KEY_SERVER: ${{ secrets.MS_CODESIGN_PUBLIC_KEY_SERVER }}
|
||||
run: |
|
||||
mkdir keys
|
||||
echo $Env:CODESIGN > keys/codesign.json
|
||||
mkdir keys/code-signing
|
||||
echo $Env:MS_CODESIGN_PRIVATE_KEY_CLIENT > keys/code-signing/private-key-client.pem
|
||||
echo $Env:MS_CODESIGN_PUBLIC_KEY_CLIENT > keys/code-signing/public-key-client.pem
|
||||
echo $Env:MS_CODESIGN_PUBLIC_KEY_SERVER > keys/code-signing/public-key-server.pem
|
||||
- name: Grunt
|
||||
run: grunt desktop-win32 ${{ github.repository != 'keeweb/keeweb' && '--skip-sign' || '' }}
|
||||
- name: Upload ia32 exe artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.exe
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.exe
|
||||
- name: Upload ia32 zip artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.zip
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.zip
|
||||
- name: Upload x64 exe artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.exe
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.exe
|
||||
- name: Upload x64 zip artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.zip
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.zip
|
||||
- name: Upload arm64 exe artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.exe
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.exe
|
||||
- name: Upload arm64 zip artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- linux
|
||||
- darwin
|
||||
- win32
|
||||
steps:
|
||||
- name: Get current git tag
|
||||
id: get_tag
|
||||
uses: keeweb/get-git-tag@v3.0.2
|
||||
with:
|
||||
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
|
||||
tagRegexGroup: 1
|
||||
- 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
|
||||
- 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
|
||||
- name: Download html artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: html
|
||||
- name: Download linux.AppImage artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.AppImage
|
||||
path: assets
|
||||
- name: Download linux.snap artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.snap
|
||||
path: assets
|
||||
- name: Download linux.deb artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.deb
|
||||
path: assets
|
||||
- name: Download linux.zip artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.zip
|
||||
path: assets
|
||||
- name: Download linux.rpm artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
|
||||
path: assets
|
||||
- name: Download darwin.x64.dmg artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
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
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.exe
|
||||
path: assets
|
||||
- name: Download win32.ia32.zip artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.zip
|
||||
path: assets
|
||||
- name: Download win32.x64.exe artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.exe
|
||||
path: assets
|
||||
- name: Download win32.x64.zip artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.zip
|
||||
path: assets
|
||||
- name: Download win32.arm64.exe artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.exe
|
||||
path: assets
|
||||
- name: Download win32.arm64.zip artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
||||
path: assets
|
||||
- name: Zip html
|
||||
working-directory: html
|
||||
run: zip -vr ../assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.html.zip .
|
||||
- name: Copy assets to dist
|
||||
run: mkdir -p keeweb/dist/desktop && cp assets/* keeweb/dist/desktop
|
||||
- name: Write secrets
|
||||
env:
|
||||
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
|
||||
KEEWEB_SIGN: ${{ secrets.KEEWEB_SIGN }}
|
||||
working-directory: keeweb
|
||||
run: |
|
||||
mkdir keys
|
||||
echo "$PRIVATE_KEY" > keys/private-key.pem
|
||||
echo "$KEEWEB_SIGN" > keys/keeweb-sign.json
|
||||
- name: Grunt
|
||||
working-directory: keeweb
|
||||
run: grunt finish-release
|
||||
- name: Copy signatures to assets
|
||||
run: cp keeweb/dist/desktop/Verify.sign.sha256 assets
|
||||
- name: Copy checksums to assets
|
||||
run: cp keeweb/dist/desktop/Verify.sha256 assets
|
||||
- name: Login to DockerHub Registry
|
||||
env:
|
||||
DOCKERHUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
run: echo $DOCKERHUB_ACCESS_TOKEN | docker login -u $DOCKERHUB_USERNAME --password-stdin
|
||||
- name: Copy dist to the Docker context
|
||||
run: cp -r html keeweb/package/docker/dist
|
||||
- name: Build the Docker image
|
||||
working-directory: keeweb
|
||||
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 --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: 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:
|
||||
ref: gh-pages
|
||||
path: gh-pages
|
||||
fetch-depth: 0
|
||||
- name: Commit dist to gh-pages
|
||||
working-directory: gh-pages
|
||||
run: |
|
||||
git rm -r '*'
|
||||
cp -r ../html/* .
|
||||
mkdir -p .github/workflows
|
||||
echo $GITHUB_SHA > build.txt
|
||||
date >> build.txt
|
||||
git add .
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git commit -am v${{ steps.get_tag.outputs.tag }}
|
||||
- name: Push gh-pages
|
||||
uses: keeweb/github-push-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: gh-pages
|
||||
directory: gh-pages
|
||||
- name: Restore git mtime
|
||||
working-directory: gh-pages
|
||||
run: python3 ../keeweb/.github/actions/scripts/git-restore-mtime.py
|
||||
- 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,22 +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_email: ${{ secrets.GCP_SA_EMAIL }}
|
||||
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/
|
||||
@@ -3,7 +3,7 @@ misc.xml
|
||||
.DS_Store
|
||||
node_modules/
|
||||
*.log
|
||||
dist/
|
||||
/dist/
|
||||
tmp/
|
||||
coverage/
|
||||
keys
|
||||
@@ -14,6 +14,6 @@ obj/
|
||||
xcuserdata/
|
||||
*.suo
|
||||
.idea/
|
||||
.vscode
|
||||
.vscode/
|
||||
*.iml
|
||||
test/dist/
|
||||
|
||||
@@ -2,24 +2,41 @@
|
||||
|
||||
Thanks for taking the time to contribute! :gift:
|
||||
|
||||
There are different ways to help the project, you will find a summary here.
|
||||
|
||||
## Code of conduct
|
||||
|
||||
This project and everyone participating in it is governed by the [KeeWeb Code of Conduct](https://github.com/keeweb/keeweb/blob/master/.github/CODE_OF_CONDUCT.md#readme). By participating, you are expected to uphold this code. Please report unacceptable behavior to antelle.net@gmail.com.
|
||||
This project and everyone participating in it is governed by the [KeeWeb Code of Conduct](https://github.com/keeweb/keeweb/blob/master/.github/CODE_OF_CONDUCT.md#readme).
|
||||
By participating, you are expected to uphold this code. Please report unacceptable behavior to antelle.net@gmail.com.
|
||||
|
||||
## Issues
|
||||
|
||||
First, let's check if it's [already there](https://github.com/keeweb/keeweb/issues).
|
||||
Perhaps, someone has already reported it?
|
||||
|
||||
[Click here](https://github.com/keeweb/keeweb/issues/new) to create a new issue.
|
||||
[Click here](https://github.com/keeweb/keeweb/issues/new/choose) to create a new issue.
|
||||
When creating issues, please answer some questions asked on that page.
|
||||
They will help to understand, reproduce, and fix your issue.
|
||||
|
||||
## Vulnerabilities
|
||||
|
||||
We would appreciate [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure).
|
||||
If you would like to report a vulnerability, the preferred way to do so is [contacting the developer directly](mailto:antelle.net@gmail.com).
|
||||
|
||||
## Questions, great ideas, etc...
|
||||
|
||||
You can [create an issue](https://github.com/keeweb/keeweb/issues/new) to discuss any question.
|
||||
You can [create an issue](https://github.com/keeweb/keeweb/issues/new/choose) to discuss any topic.
|
||||
Indeed, questions on that page are not applicable in this case.
|
||||
|
||||
## Development
|
||||
|
||||
We don't accept (and in foreseeable future, we won't accept) anyone as a project collaborator directly.
|
||||
Instead, the code should be contributed via pull requests. To get started with development, take a look at these pages:
|
||||
- [Architecture](https://github.com/keeweb/keeweb/wiki/Architecture)
|
||||
- [Engineering](https://github.com/keeweb/keeweb/wiki/Engineering)
|
||||
- [Unsupported Features](https://github.com/keeweb/keeweb/wiki/Unsupported%20Features)
|
||||
- [Pull Request Guidelines](https://github.com/keeweb/keeweb/blob/master/.github/PULL_REQUEST_TEMPLATE.md#readme)
|
||||
|
||||
## Pull requests
|
||||
|
||||
Please follow [this guide](https://github.com/keeweb/keeweb/blob/master/.github/PULL_REQUEST_TEMPLATE.md#readme).
|
||||
@@ -27,4 +44,23 @@ Please follow [this guide](https://github.com/keeweb/keeweb/blob/master/.github/
|
||||
## Docs (Wiki pages)
|
||||
|
||||
KeeWeb has [Wiki pages](https://github.com/keeweb/keeweb/wiki), however it's not possible to contribute with pull requests.
|
||||
If you would like to improve it, please [open an issue](https://github.com/keeweb/keeweb/issues/new) and submit your changes in Markdown format.
|
||||
If you would like to improve it, please [open an issue](https://github.com/keeweb/keeweb/issues/new/choose) and submit your changes in Markdown format.
|
||||
|
||||
## Donations
|
||||
|
||||
There are two ways of donating:
|
||||
- join as backer on [OpenCollective](https://opencollective.com/keeweb#support);
|
||||
- sponsor the developer directly on [GitHub](https://github.com/sponsors/antelle).
|
||||
|
||||
Please note: donation does not imply any type of service contract.
|
||||
|
||||
## Translations
|
||||
|
||||
All KeeWeb translations are contributed by the community. To improve a translation or add a missing one,
|
||||
join us on [OneSky](https://keeweb.oneskyapp.com).
|
||||
|
||||
## PR
|
||||
|
||||
We would be happy to support you if you decide to post, blog, or review the project,
|
||||
or provide any other kind of PR. Please [contact the developer](mailto:antelle.net@gmail.com)
|
||||
if you have anything to share. You can also poke us on [Twitter](https://twitter.com/kee_web).
|
||||
|
||||
@@ -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');
|
||||
@@ -10,7 +11,7 @@ const pkg = require('./package.json');
|
||||
|
||||
debug.enable('electron-notarize');
|
||||
|
||||
module.exports = function(grunt) {
|
||||
module.exports = function (grunt) {
|
||||
require('time-grunt')(grunt);
|
||||
require('load-grunt-tasks')(grunt);
|
||||
|
||||
@@ -24,51 +25,72 @@ module.exports = function(grunt) {
|
||||
|
||||
const dt = date.toISOString().replace(/T.*/, '');
|
||||
const year = date.getFullYear();
|
||||
const minElectronVersionForUpdate = '8.0.0';
|
||||
const zipCommentPlaceholderPart = 'zip_comment_placeholder_that_will_be_replaced_with_hash';
|
||||
const zipCommentPlaceholder =
|
||||
zipCommentPlaceholderPart + '.'.repeat(512 - zipCommentPlaceholderPart.length);
|
||||
const electronVersion = pkg.dependencies.electron.replace(/^\D/, '');
|
||||
const skipSign = grunt.option('skip-sign');
|
||||
const getCodeSignConfig = () =>
|
||||
skipSign ? { identities: {} } : require('./keys/codesign.json');
|
||||
|
||||
const skipCodeSigning = grunt.option('no-sign');
|
||||
let codeSignConfig;
|
||||
|
||||
if (!skipCodeSigning) {
|
||||
let sha = grunt.option('commit-sha');
|
||||
if (!sha) {
|
||||
try {
|
||||
codeSignConfig = require('../keys/codesign');
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
'Unable to load code signing config from ../keys/codesign.\n' +
|
||||
'This is needed for production builds targeting macOS.\n' +
|
||||
'For development builds, run with the `--no-sign` arg to skip code signing,\n' +
|
||||
'e.g. `npm start -- --no-sign`'
|
||||
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'
|
||||
};
|
||||
|
||||
grunt.initConfig({
|
||||
gitinfo: {
|
||||
branch: {
|
||||
current: {
|
||||
SHA: 'Current HEAD SHA',
|
||||
shortSHA: 'Current HEAD short SHA',
|
||||
name: 'Current branch name',
|
||||
lastCommitTime: 'Last commit time'
|
||||
}
|
||||
const windowsAppVersionString = {
|
||||
CompanyName: 'KeeWeb',
|
||||
FileDescription: pkg.description,
|
||||
OriginalFilename: 'KeeWeb.exe',
|
||||
ProductName: 'KeeWeb',
|
||||
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: {} },
|
||||
clean: {
|
||||
dist: ['dist', 'tmp'],
|
||||
desktop: ['tmp/desktop', 'dist/desktop'],
|
||||
cordova: ['tmp/cordova', 'dist/cordova']
|
||||
desktop: ['tmp/desktop', 'dist/desktop']
|
||||
},
|
||||
copy: {
|
||||
html: {
|
||||
@@ -76,14 +98,11 @@ module.exports = function(grunt) {
|
||||
dest: 'tmp/index.html',
|
||||
nonull: true
|
||||
},
|
||||
'html-dist': {
|
||||
src: 'tmp/app.html',
|
||||
dest: 'dist/index.html',
|
||||
nonull: true
|
||||
},
|
||||
favicon: {
|
||||
src: 'app/favicon.png',
|
||||
dest: 'tmp/favicon.png',
|
||||
'content-dist': {
|
||||
cwd: 'app/content/',
|
||||
src: '**',
|
||||
dest: 'dist/',
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
icons: {
|
||||
@@ -114,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',
|
||||
@@ -133,27 +145,23 @@ module.exports = function(grunt) {
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
'desktop-update': {
|
||||
cwd: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Resources/',
|
||||
src: 'app.asar',
|
||||
dest: 'tmp/desktop/update/',
|
||||
'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',
|
||||
expand: true,
|
||||
nonull: true
|
||||
nonull: true,
|
||||
options: { mode: true }
|
||||
},
|
||||
'desktop-update-helper': {
|
||||
src: ['helper/darwin/KeeWebHelper', 'helper/win32/KeeWebHelper.exe'],
|
||||
dest: 'tmp/desktop/update/',
|
||||
nonull: true
|
||||
},
|
||||
'desktop-windows-helper-ia32': {
|
||||
src: 'helper/win32/KeeWebHelper.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-ia32/Resources/',
|
||||
nonull: true
|
||||
},
|
||||
'desktop-windows-helper-x64': {
|
||||
src: 'helper/win32/KeeWebHelper.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-x64/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',
|
||||
@@ -165,13 +173,48 @@ module.exports = function(grunt) {
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.win.ia32.exe`,
|
||||
nonull: true
|
||||
},
|
||||
'desktop-win32-dist-arm64': {
|
||||
src: 'tmp/desktop/KeeWeb.win.arm64.exe',
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.win.arm64.exe`,
|
||||
nonull: true
|
||||
},
|
||||
'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/',
|
||||
nonull: true
|
||||
},
|
||||
'native-modules-win32-ia32': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-modules/*-win32-ia32.node',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-ia32/resources/',
|
||||
nonull: true
|
||||
},
|
||||
'native-modules-win32-arm64': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-modules/*-win32-arm64.node',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-arm64/resources/',
|
||||
nonull: true
|
||||
},
|
||||
'native-modules-linux-x64': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-modules/*-linux-x64.node',
|
||||
dest: 'tmp/desktop/keeweb-linux-x64/resources/',
|
||||
nonull: true
|
||||
},
|
||||
'electron-builder-dist-linux-rpm': {
|
||||
src: `tmp/desktop/electron-builder/keeweb-${pkg.version}.x86_64.rpm`,
|
||||
src: `tmp/desktop/electron-builder/KeeWeb-${pkg.version}.x86_64.rpm`,
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.linux.x86_64.rpm`,
|
||||
nonull: true
|
||||
},
|
||||
'electron-builder-dist-linux-snap': {
|
||||
src: `tmp/desktop/electron-builder/keeweb_${pkg.version}_amd64.snap`,
|
||||
src: `tmp/desktop/electron-builder/KeeWeb_${pkg.version}_amd64.snap`,
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.linux.snap`,
|
||||
nonull: true
|
||||
},
|
||||
@@ -179,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: {
|
||||
@@ -186,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: {
|
||||
@@ -199,12 +289,12 @@ module.exports = function(grunt) {
|
||||
algo: 'sha512',
|
||||
expected: {
|
||||
style: 1,
|
||||
script: 3
|
||||
script: 1
|
||||
}
|
||||
},
|
||||
app: {
|
||||
src: 'tmp/app.html',
|
||||
dest: 'tmp/app.html'
|
||||
dest: 'dist/index.html'
|
||||
}
|
||||
},
|
||||
htmlmin: {
|
||||
@@ -219,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 }] },
|
||||
@@ -255,17 +345,6 @@ module.exports = function(grunt) {
|
||||
]
|
||||
},
|
||||
files: { 'tmp/desktop/app/main.js': 'desktop/main.js' }
|
||||
},
|
||||
'cordova-html': {
|
||||
options: {
|
||||
replacements: [
|
||||
{
|
||||
pattern: '<script',
|
||||
replacement: '<script src="cordova.js"></script><script'
|
||||
}
|
||||
]
|
||||
},
|
||||
files: { 'tmp/cordova/app/index.html': 'dist/index.html' }
|
||||
}
|
||||
},
|
||||
webpack: {
|
||||
@@ -280,7 +359,10 @@ module.exports = function(grunt) {
|
||||
sha: 'dev'
|
||||
}),
|
||||
publicPath: '/',
|
||||
contentBase: path.resolve(__dirname, 'tmp'),
|
||||
contentBase: [
|
||||
path.resolve(__dirname, 'tmp'),
|
||||
path.resolve(__dirname, 'app/content')
|
||||
],
|
||||
progress: false
|
||||
},
|
||||
js: {
|
||||
@@ -298,97 +380,68 @@ 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',
|
||||
...(codeSignConfig
|
||||
? {
|
||||
osxSign: {
|
||||
identity: codeSignConfig.identities.app,
|
||||
hardenedRuntime: true,
|
||||
entitlements: 'package/osx/entitlements.mac.plist',
|
||||
'entitlements-inherit': 'package/osx/entitlements.mac.plist',
|
||||
'gatekeeper-assess': false
|
||||
},
|
||||
osxNotarize: {
|
||||
appleId: codeSignConfig.appleId,
|
||||
appleIdPassword: '@keychain:AC_PASSWORD',
|
||||
ascProvider: codeSignConfig.teamId
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
afterCopy: [
|
||||
(buildPath, electronVersion, platform, arch, callback) => {
|
||||
if (path.basename(buildPath) !== 'app') {
|
||||
throw new Error('Bad build path: ' + buildPath);
|
||||
}
|
||||
const resPath = path.dirname(buildPath);
|
||||
if (path.basename(resPath) !== 'Resources') {
|
||||
throw new Error('Bad Resources path: ' + resPath);
|
||||
}
|
||||
const helperTargetPath = path.join(
|
||||
resPath,
|
||||
'helper/darwin/KeeWebHelper'
|
||||
);
|
||||
const helperSourcePath = path.join(
|
||||
__dirname,
|
||||
'helper/darwin/KeeWebHelper'
|
||||
);
|
||||
fs.copySync(helperSourcePath, helperTargetPath);
|
||||
|
||||
const contentsPath = path.dirname(resPath);
|
||||
if (path.basename(contentsPath) !== 'Contents') {
|
||||
throw new Error('Bad Contents path: ' + contentsPath);
|
||||
}
|
||||
const installerSourcePath = path.join(
|
||||
__dirname,
|
||||
'package/osx/KeeWeb Installer.app'
|
||||
);
|
||||
const installerTargetPath = path.join(
|
||||
contentsPath,
|
||||
'Installer/KeeWeb Installer.app'
|
||||
);
|
||||
fs.copySync(installerSourcePath, installerTargetPath);
|
||||
|
||||
callback();
|
||||
}
|
||||
]
|
||||
extendInfo: 'package/osx/extend.plist'
|
||||
}
|
||||
},
|
||||
win32: {
|
||||
'darwin-arm64': {
|
||||
options: {
|
||||
platform: 'darwin',
|
||||
arch: 'arm64',
|
||||
icon: 'graphics/icon.icns',
|
||||
appBundleId: 'net.antelle.keeweb',
|
||||
appCategoryType: 'public.app-category.productivity',
|
||||
extendInfo: 'package/osx/extend.plist'
|
||||
}
|
||||
},
|
||||
'win32-x64': {
|
||||
options: {
|
||||
platform: 'win32',
|
||||
arch: ['ia32', 'x64'],
|
||||
arch: 'x64',
|
||||
icon: 'graphics/icon.ico',
|
||||
buildVersion: pkg.version,
|
||||
'version-string': {
|
||||
CompanyName: 'KeeWeb',
|
||||
FileDescription: pkg.description,
|
||||
OriginalFilename: 'KeeWeb.exe',
|
||||
ProductName: 'KeeWeb',
|
||||
InternalName: 'KeeWeb'
|
||||
}
|
||||
'version-string': windowsAppVersionString
|
||||
}
|
||||
},
|
||||
'win32-ia32': {
|
||||
options: {
|
||||
platform: 'win32',
|
||||
arch: 'ia32',
|
||||
icon: 'graphics/icon.ico',
|
||||
buildVersion: pkg.version,
|
||||
'version-string': windowsAppVersionString
|
||||
}
|
||||
},
|
||||
'win32-arm64': {
|
||||
options: {
|
||||
platform: 'win32',
|
||||
arch: 'arm64',
|
||||
icon: 'graphics/icon.ico',
|
||||
buildVersion: pkg.version,
|
||||
'version-string': windowsAppVersionString
|
||||
}
|
||||
}
|
||||
},
|
||||
'electron-builder': {
|
||||
linux: {
|
||||
options: {
|
||||
publish: 'never',
|
||||
targets: 'linux',
|
||||
prepackaged: 'tmp/desktop/keeweb-linux-x64',
|
||||
config: {
|
||||
@@ -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 }]
|
||||
@@ -438,6 +502,10 @@ module.exports = function(grunt) {
|
||||
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.ia32.zip` },
|
||||
files: [{ cwd: 'tmp/desktop/KeeWeb-win32-ia32', src: '**', expand: true }]
|
||||
},
|
||||
'win32-arm64': {
|
||||
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.arm64.zip` },
|
||||
files: [{ cwd: 'tmp/desktop/KeeWeb-win32-arm64', src: '**', expand: true }]
|
||||
},
|
||||
'linux-x64': {
|
||||
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.linux.x64.zip` },
|
||||
files: [
|
||||
@@ -447,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
|
||||
}
|
||||
},
|
||||
@@ -505,6 +559,20 @@ module.exports = function(grunt) {
|
||||
arch: 'ia32',
|
||||
output: 'tmp/desktop/KeeWeb-win32-ia32/uninst.exe'
|
||||
}
|
||||
},
|
||||
'win32-arm64': {
|
||||
options: {
|
||||
installScript: 'package/nsis/main.nsi',
|
||||
arch: 'arm64',
|
||||
output: 'tmp/desktop/KeeWeb.win.arm64.exe'
|
||||
}
|
||||
},
|
||||
'win32-un-arm64': {
|
||||
options: {
|
||||
installScript: 'package/nsis/main-un.nsi',
|
||||
arch: 'arm64',
|
||||
output: 'tmp/desktop/KeeWeb-win32-arm64/uninst.exe'
|
||||
}
|
||||
}
|
||||
},
|
||||
chmod: {
|
||||
@@ -524,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': {
|
||||
@@ -536,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'
|
||||
}
|
||||
@@ -559,45 +625,66 @@ module.exports = function(grunt) {
|
||||
]
|
||||
}
|
||||
},
|
||||
'sign-archive': {
|
||||
'desktop-update': {
|
||||
'osx-sign': {
|
||||
options: {
|
||||
get identity() {
|
||||
return getCodeSignConfig().identities.app;
|
||||
},
|
||||
hardenedRuntime: true,
|
||||
entitlements: 'package/osx/entitlements.plist',
|
||||
'entitlements-inherit': 'package/osx/entitlements-inherit.plist',
|
||||
'gatekeeper-assess': false
|
||||
},
|
||||
'desktop-x64': {
|
||||
options: {
|
||||
file: 'dist/desktop/UpdateDesktop.zip',
|
||||
signature: zipCommentPlaceholder
|
||||
}
|
||||
'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'
|
||||
}
|
||||
},
|
||||
'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'
|
||||
notarize: {
|
||||
options: {
|
||||
appBundleId: 'net.antelle.keeweb',
|
||||
get appleId() {
|
||||
return getCodeSignConfig().appleId;
|
||||
},
|
||||
appleIdPassword: '@keychain:AC_PASSWORD',
|
||||
get ascProvider() {
|
||||
return getCodeSignConfig().teamId;
|
||||
}
|
||||
},
|
||||
'desktop-x64': {
|
||||
src: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app'
|
||||
},
|
||||
'desktop-arm64': {
|
||||
src: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app'
|
||||
}
|
||||
},
|
||||
'sign-exe': {
|
||||
options: {
|
||||
// algo: 'SHA256',
|
||||
url: pkg.homepage,
|
||||
...codeSignConfig.windows
|
||||
get windows() {
|
||||
return getCodeSignConfig().windows;
|
||||
},
|
||||
get certHash() {
|
||||
return getCodeSignConfig().microsoftCertHash;
|
||||
}
|
||||
},
|
||||
'win32-build-x64': {
|
||||
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',
|
||||
@@ -610,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',
|
||||
@@ -618,6 +707,20 @@ module.exports = function(grunt) {
|
||||
}
|
||||
}
|
||||
},
|
||||
'win32-build-arm64': {
|
||||
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',
|
||||
'tmp/desktop/KeeWeb-win32-arm64/libGLESv2.dll':
|
||||
'ANGLE libGLESv2 Dynamic Link Library'
|
||||
}
|
||||
}
|
||||
},
|
||||
'win32-uninst-x64': {
|
||||
options: {
|
||||
files: {
|
||||
@@ -632,6 +735,13 @@ module.exports = function(grunt) {
|
||||
}
|
||||
}
|
||||
},
|
||||
'win32-uninst-arm64': {
|
||||
options: {
|
||||
files: {
|
||||
'tmp/desktop/KeeWeb-win32-arm64/uninst.exe': 'KeeWeb Uninstaller'
|
||||
}
|
||||
}
|
||||
},
|
||||
'win32-installer-x64': {
|
||||
options: {
|
||||
files: {
|
||||
@@ -645,6 +755,13 @@ module.exports = function(grunt) {
|
||||
'tmp/desktop/KeeWeb.win.ia32.exe': 'KeeWeb Setup'
|
||||
}
|
||||
}
|
||||
},
|
||||
'win32-installer-arm64': {
|
||||
options: {
|
||||
files: {
|
||||
'tmp/desktop/KeeWeb.win.arm64.exe': 'KeeWeb Setup'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'sign-dist': {
|
||||
@@ -653,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-*']
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -665,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
|
||||
|
||||
@@ -5,7 +5,7 @@ The app can run either in browser, or as a desktop app.
|
||||
|
||||

|
||||
|
||||
# Quick Links
|
||||
## Quick Links
|
||||
|
||||
Apps: [Web](https://app.keeweb.info/), [Desktop](https://github.com/keeweb/keeweb/releases/latest)
|
||||
Timeline: [Release Notes](release-notes.md), [TODO](https://github.com/keeweb/keeweb/wiki/TODO)
|
||||
@@ -14,11 +14,11 @@ Website: [keeweb.info](https://keeweb.info)
|
||||
Twitter: [kee_web](https://twitter.com/kee_web)
|
||||
Donate: [OpenCollective](https://opencollective.com/keeweb#support), [GitHub](https://github.com/sponsors/antelle)
|
||||
|
||||
# Status
|
||||
## Status
|
||||
|
||||
The app is quite stable now. Basic stuff, as well as more advanced operations, should be rather reliable.
|
||||
|
||||
# Self-hosting
|
||||
## Self-hosting
|
||||
|
||||
Everything you need to host this app on your server is any static file server. The app is a single HTML file + a service worker (optionally; for offline access).
|
||||
You can download the latest distribution files from [gh-pages](https://github.com/keeweb/keeweb/archive/gh-pages.zip) branch.
|
||||
@@ -33,17 +33,18 @@ docker run --name keeweb -d -p 443:443 -p 80:80 -v $EXT_DIR:/etc/nginx/external/
|
||||
|
||||
To make Dropbox work in your self-hosted app, [go to this Wiki page](https://github.com/keeweb/keeweb/wiki/Dropbox-and-GDrive).
|
||||
|
||||
# Building
|
||||
## Building
|
||||
|
||||
The easiest way to clone all KeeWeb repos is:
|
||||
```bash
|
||||
curl https://raw.githubusercontent.com/keeweb/keeweb/develop/dev-env.sh | bash -
|
||||
```
|
||||
|
||||
The app can be built with grunt: `grunt` (html file will be in `dist/`).
|
||||
Desktop apps are built with `grunt desktop`. This works only in macOS as it builds dmg; requires wine.
|
||||
Also, a hardware token is required.
|
||||
To run Electron app without building an installer, build the app with `grunt` and start it this way:
|
||||
The app can be built with grunt: `grunt` (html files will be in `dist/`).
|
||||
Desktop apps are built with `grunt desktop`. This requires some magic and currently works only on CI,
|
||||
you can find more details in [the GitHub Actions workflow](.github/workflows/build.yaml).
|
||||
|
||||
To run the desktop (electron) app without building an installer, build the app with `grunt` and start it this way:
|
||||
```bash
|
||||
npm run dev
|
||||
npm run electron
|
||||
@@ -54,18 +55,28 @@ For debug build:
|
||||
1. run `npm run dev`
|
||||
2. open `http://localhost:8085`
|
||||
|
||||
# Contributing
|
||||
To build desktop apps, use these goals, the result can be found in `tmp`:
|
||||
|
||||
```
|
||||
npm run dev-desktop-macos
|
||||
npm run dev-desktop-windows
|
||||
npm run dev-desktop-linux
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Please read contribution guidelines [for pull requests](.github/PULL_REQUEST_TEMPLATE.md).
|
||||
Here's a [list of issues](https://github.com/keeweb/keeweb/labels/help%20wanted) where your help would be very welcome.
|
||||
Also you can help by [translating KeeWeb](https://keeweb.oneskyapp.com) to your language.
|
||||
|
||||
### Important notes for pull requests
|
||||
Other ways of contribution can be found [on this page](CONTRIBUTING.md).
|
||||
|
||||
#### Important notes for pull requests
|
||||
|
||||
- please branch from `develop`, not `master`
|
||||
- don't edit translation files except base.json, they will be replaced
|
||||
|
||||
# Donations
|
||||
## Donations
|
||||
|
||||
KeeWeb is not free to develop. It takes time, requires paid code signing certificates and domains.
|
||||
You can help the project or say "thank you" with this button:
|
||||
@@ -75,7 +86,15 @@ You can also sponsor the developer directly [on GitHub](https://github.com/spons
|
||||
|
||||
Please note: donation does not imply any type of service contract.
|
||||
|
||||
## Thank you
|
||||
|
||||
# License
|
||||
Notable contributions to KeeWeb:
|
||||
|
||||
- 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)
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/keeweb/keeweb/blob/master/LICENSE)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Only the latest version of the product is supported. This version can be accessed at https://app.keeweb.info
|
||||
and downloaded from https://keeweb.info or GitHub releases ([latest release link](https://github.com/keeweb/keeweb/releases/latest)).
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We would appreciate [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure).
|
||||
If you would like to report a vulnerability, the preferred way to do so is [contacting the developer directly](mailto:antelle.net@gmail.com).
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Not Found</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, 'BlinkMacSystemFont', 'Raleway', 'Helvetica Neue',
|
||||
'Helvetica', 'Arial', sans-serif;
|
||||
font-feature-settings: 'liga' 0;
|
||||
text-align: center;
|
||||
background: #f5f5f5;
|
||||
font-weight: 300;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3em;
|
||||
margin-top: 10vh;
|
||||
}
|
||||
p {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>404 Not Found</h1>
|
||||
<p>Sorry, the page you're looking for was not found.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>KeeWeb</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.opener.postMessage(
|
||||
{ storage: 'dropbox', search: location.search },
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>KeeWeb</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.opener.postMessage(
|
||||
{ storage: 'gdrive', search: location.search },
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>KeeWeb</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.opener.postMessage(
|
||||
{ storage: 'onedrive', 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 |
@@ -3,8 +3,22 @@
|
||||
<head lang="en">
|
||||
<meta charset="UTF-8" />
|
||||
<title>KeeWeb</title>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="
|
||||
default-src 'self';
|
||||
font-src data:;
|
||||
script-src 'self' 'unsafe-eval';
|
||||
style-src 'self' blob:;
|
||||
connect-src 'self' ws: https:;
|
||||
child-src 'self' blob:;
|
||||
worker-src 'self' blob:;
|
||||
img-src 'self' data: blob: https://services.keeweb.info/;
|
||||
object-src 'none';
|
||||
form-action 'none';
|
||||
"
|
||||
/>
|
||||
<meta name="application-name" content="KeeWeb" />
|
||||
<meta name="kw-signature" content="" />
|
||||
<meta name="kw-config" content="(no-config)" />
|
||||
<meta
|
||||
name="viewport"
|
||||
@@ -16,20 +30,6 @@
|
||||
<meta name="theme-color" content="#6386ec" />
|
||||
<meta name="msapplication-config" content="browserconfig.xml" />
|
||||
<meta name="msapplication-TileColor" content="#6386ec" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="
|
||||
default-src 'self';
|
||||
font-src data:;
|
||||
script-src 'self' 'unsafe-eval';
|
||||
style-src 'self' blob:;
|
||||
connect-src 'self' ws: https:;
|
||||
child-src blob:;
|
||||
img-src 'self' data:;
|
||||
object-src 'none';
|
||||
form-action 'none';
|
||||
"
|
||||
/>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="icons/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png" />
|
||||
@@ -71,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 |
@@ -5,10 +5,12 @@ import { AppRightsChecker } from 'comp/app/app-rights-checker';
|
||||
import { ExportApi } from 'comp/app/export-api';
|
||||
import { SingleInstanceChecker } from 'comp/app/single-instance-checker';
|
||||
import { Updater } from 'comp/app/updater';
|
||||
import { AuthReceiver } from 'comp/browser/auth-receiver';
|
||||
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';
|
||||
@@ -25,6 +27,8 @@ import { KdbxwebInit } from 'util/kdbxweb/kdbxweb-init';
|
||||
import { Locale } from 'util/locale';
|
||||
import { AppView } from 'views/app-view';
|
||||
import 'hbs-helpers';
|
||||
import { AutoType } from './auto-type';
|
||||
import { Storage } from './storage';
|
||||
|
||||
StartProfiler.milestone('loading modules');
|
||||
|
||||
@@ -33,11 +37,6 @@ const ready = (Launcher && Launcher.ready) || $;
|
||||
ready(() => {
|
||||
StartProfiler.milestone('document ready');
|
||||
|
||||
if (AuthReceiver.receive()) {
|
||||
return;
|
||||
}
|
||||
StartProfiler.milestone('checking auth');
|
||||
|
||||
const appModel = new AppModel();
|
||||
StartProfiler.milestone('creating app model');
|
||||
|
||||
@@ -46,9 +45,11 @@ ready(() => {
|
||||
.then(initModules)
|
||||
.then(loadRemoteConfig)
|
||||
.then(ensureCanRun)
|
||||
.then(initStorage)
|
||||
.then(initUsbListener)
|
||||
.then(showApp)
|
||||
.then(postInit)
|
||||
.catch(e => {
|
||||
.catch((e) => {
|
||||
appModel.appLogger.error('Error starting app', e);
|
||||
});
|
||||
|
||||
@@ -59,10 +60,11 @@ ready(() => {
|
||||
);
|
||||
}
|
||||
return FeatureTester.test()
|
||||
.catch(e => {
|
||||
.catch((e) => {
|
||||
Alerts.error({
|
||||
header: Locale.appSettingsError,
|
||||
body: Locale.appNotSupportedError + '<br/><br/>' + e,
|
||||
body: Locale.appNotSupportedError,
|
||||
pre: e,
|
||||
buttons: [],
|
||||
esc: false,
|
||||
enter: false,
|
||||
@@ -88,10 +90,12 @@ ready(() => {
|
||||
|
||||
function initModules() {
|
||||
KeyHandler.init();
|
||||
IdleTracker.init();
|
||||
PopupNotifier.init();
|
||||
KdbxwebInit.init();
|
||||
FocusDetector.init();
|
||||
AutoType.init();
|
||||
ThemeWatcher.init();
|
||||
SettingsManager.init();
|
||||
window.kw = ExportApi;
|
||||
return PluginManager.init().then(() => {
|
||||
StartProfiler.milestone('initializing modules');
|
||||
@@ -112,15 +116,15 @@ 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 => {
|
||||
.catch((e) => {
|
||||
if (!appModel.settings.cacheConfigSettings) {
|
||||
showSettingsLoadError();
|
||||
throw e;
|
||||
@@ -133,6 +137,18 @@ ready(() => {
|
||||
});
|
||||
}
|
||||
|
||||
function initStorage() {
|
||||
for (const prv of Object.values(Storage)) {
|
||||
prv.init();
|
||||
}
|
||||
StartProfiler.milestone('initializing storage');
|
||||
}
|
||||
|
||||
function initUsbListener() {
|
||||
UsbListener.init();
|
||||
StartProfiler.milestone('starting usb');
|
||||
}
|
||||
|
||||
function showApp() {
|
||||
return Promise.resolve().then(() => {
|
||||
const skipHttpsWarning =
|
||||
@@ -140,14 +156,14 @@ ready(() => {
|
||||
const protocolIsInsecure = ['https:', 'file:', 'app:'].indexOf(location.protocol) < 0;
|
||||
const hostIsInsecure = location.hostname !== 'localhost';
|
||||
if (protocolIsInsecure && hostIsInsecure && !skipHttpsWarning) {
|
||||
return new Promise(resolve => {
|
||||
return new Promise((resolve) => {
|
||||
Alerts.error({
|
||||
header: Locale.appSecWarn,
|
||||
icon: 'user-secret',
|
||||
esc: false,
|
||||
enter: false,
|
||||
click: false,
|
||||
body: Locale.appSecWarnBody1 + '<br/><br/>' + Locale.appSecWarnBody2,
|
||||
body: Locale.appSecWarnBody1 + '\n\n' + Locale.appSecWarnBody2,
|
||||
buttons: [{ result: '', title: Locale.appSecWarnBtn, error: true }],
|
||||
complete: () => {
|
||||
showView();
|
||||
@@ -157,21 +173,23 @@ ready(() => {
|
||||
});
|
||||
} else {
|
||||
showView();
|
||||
return new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function postInit() {
|
||||
Updater.init();
|
||||
SingleInstanceChecker.init();
|
||||
AppRightsChecker.init();
|
||||
setTimeout(() => PluginManager.runAutoUpdate(), Timeouts.AutoUpdatePluginsAfterStart);
|
||||
setTimeout(() => {
|
||||
Updater.init();
|
||||
SingleInstanceChecker.init();
|
||||
AppRightsChecker.init();
|
||||
IdleTracker.init();
|
||||
BrowserExtensionConnector.init(appModel);
|
||||
PluginManager.runAutoUpdate();
|
||||
}, Timeouts.AutoUpdatePluginsAfterStart);
|
||||
}
|
||||
|
||||
function showView() {
|
||||
appModel.prepare();
|
||||
StartProfiler.milestone('preparing app model');
|
||||
|
||||
new AppView(appModel).render();
|
||||
StartProfiler.milestone('first view rendering');
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const AutoTypeEmitterFactory = {
|
||||
create(callback) {
|
||||
if (Launcher && Launcher.autoTypeSupported) {
|
||||
const { AutoTypeEmitter } = require('./emitter/auto-type-emitter-' +
|
||||
Launcher.platform());
|
||||
return new AutoTypeEmitter(callback);
|
||||
}
|
||||
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 };
|
||||
@@ -11,7 +11,7 @@ const MaxSteps = 1000;
|
||||
const MaxCopy = 2;
|
||||
const FakeCharAlphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz123456789O0oIl';
|
||||
|
||||
const AutoTypeObfuscator = function(chars) {
|
||||
const AutoTypeObfuscator = function (chars) {
|
||||
this.chars = chars;
|
||||
this.inputChars = [];
|
||||
this.inputCursor = 0;
|
||||
@@ -21,7 +21,7 @@ const AutoTypeObfuscator = function(chars) {
|
||||
this.copyCount = 0;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.obfuscate = function() {
|
||||
AutoTypeObfuscator.prototype.obfuscate = function () {
|
||||
while (!this.finished()) {
|
||||
this.step();
|
||||
if (this.stepCount++ > MaxSteps) {
|
||||
@@ -35,16 +35,16 @@ AutoTypeObfuscator.prototype.obfuscate = function() {
|
||||
return this.ops;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.finished = function() {
|
||||
AutoTypeObfuscator.prototype.finished = function () {
|
||||
return (
|
||||
this.chars.length === this.inputChars.length &&
|
||||
this.chars.every(function(ch, ix) {
|
||||
this.chars.every(function (ch, ix) {
|
||||
return this.inputChars[ix].ch === ch;
|
||||
}, this)
|
||||
);
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.step = function() {
|
||||
AutoTypeObfuscator.prototype.step = function () {
|
||||
const isFake = this.stepCount < MaxFakeOps && Math.random() > this.stepCount / MaxFakeOps;
|
||||
if (isFake) {
|
||||
this.stepFake();
|
||||
@@ -52,11 +52,11 @@ AutoTypeObfuscator.prototype.step = function() {
|
||||
this.stepReal();
|
||||
}
|
||||
if (logger.getLevel() >= Logger.Level.Debug) {
|
||||
logger.debug('value', this.inputChars.map(ic => ic.ch).join(''));
|
||||
logger.debug('value', this.inputChars.map((ic) => ic.ch).join(''));
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.stepFake = function() {
|
||||
AutoTypeObfuscator.prototype.stepFake = function () {
|
||||
const pos = Math.floor(Math.random() * (this.inputChars.length + 1));
|
||||
const ch = FakeCharAlphabet[Math.floor(Math.random() * FakeCharAlphabet.length)];
|
||||
logger.info('step.fake', pos, ch);
|
||||
@@ -85,7 +85,7 @@ AutoTypeObfuscator.prototype.stepFake = function() {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.stepReal = function() {
|
||||
AutoTypeObfuscator.prototype.stepReal = function () {
|
||||
const possibleActions = [];
|
||||
const inputRealPositions = [];
|
||||
let i;
|
||||
@@ -134,7 +134,7 @@ AutoTypeObfuscator.prototype.stepReal = function() {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.moveToPos = function(pos) {
|
||||
AutoTypeObfuscator.prototype.moveToPos = function (pos) {
|
||||
logger.debug('moveToPos', pos);
|
||||
while (this.inputCursor > pos) {
|
||||
this.moveLeft();
|
||||
@@ -144,21 +144,21 @@ AutoTypeObfuscator.prototype.moveToPos = function(pos) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.moveLeft = function() {
|
||||
AutoTypeObfuscator.prototype.moveLeft = function () {
|
||||
logger.debug('moveLeft');
|
||||
this.ops.push({ type: 'key', value: 'left' });
|
||||
this.inputCursor--;
|
||||
this.inputSel = 0;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.moveRight = function() {
|
||||
AutoTypeObfuscator.prototype.moveRight = function () {
|
||||
logger.debug('moveRight');
|
||||
this.ops.push({ type: 'key', value: 'right' });
|
||||
this.inputCursor++;
|
||||
this.inputSel = 0;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.inputChar = function(ch) {
|
||||
AutoTypeObfuscator.prototype.inputChar = function (ch) {
|
||||
logger.debug('inputChar', ch);
|
||||
this.ops.push({ type: 'text', value: ch });
|
||||
this.inputChars.splice(this.inputCursor, this.inputSel, { ch });
|
||||
@@ -166,7 +166,7 @@ AutoTypeObfuscator.prototype.inputChar = function(ch) {
|
||||
this.inputSel = 0;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.copyPaste = function(ch) {
|
||||
AutoTypeObfuscator.prototype.copyPaste = function (ch) {
|
||||
logger.debug('copyPaste', ch);
|
||||
this.ops.push({ type: 'cmd', value: 'copyPaste', arg: ch });
|
||||
this.inputChars.splice(this.inputCursor, this.inputSel, { ch });
|
||||
@@ -174,7 +174,7 @@ AutoTypeObfuscator.prototype.copyPaste = function(ch) {
|
||||
this.inputSel = 0;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.selectText = function(backward, count) {
|
||||
AutoTypeObfuscator.prototype.selectText = function (backward, count) {
|
||||
logger.debug('selectText', backward ? 'left' : 'right', count);
|
||||
const ops = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
@@ -192,7 +192,7 @@ AutoTypeObfuscator.prototype.selectText = function(backward, count) {
|
||||
this.inputSel = count;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.deleteText = function(backward) {
|
||||
AutoTypeObfuscator.prototype.deleteText = function (backward) {
|
||||
logger.debug('deleteText', backward ? 'left' : 'right');
|
||||
this.ops.push({ type: 'key', value: backward ? 'bs' : 'del' });
|
||||
if (this.inputSel) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AutoTypeRunner } from 'auto-type/auto-type-runner';
|
||||
|
||||
const AutoTypeParser = function(sequence) {
|
||||
const AutoTypeParser = function (sequence) {
|
||||
this.sequence = sequence;
|
||||
this.ix = 0;
|
||||
this.states = [];
|
||||
@@ -8,7 +8,7 @@ const AutoTypeParser = function(sequence) {
|
||||
|
||||
AutoTypeParser.opRegex = /^(.*?)(?:([\s:=])[\s:=]*(.*))?$/;
|
||||
|
||||
AutoTypeParser.prototype.parse = function() {
|
||||
AutoTypeParser.prototype.parse = function () {
|
||||
const len = this.sequence.length;
|
||||
this.pushState();
|
||||
while (this.ix < len) {
|
||||
@@ -45,14 +45,14 @@ AutoTypeParser.prototype.parse = function() {
|
||||
return new AutoTypeRunner(this.state().ops);
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.pushState = function() {
|
||||
AutoTypeParser.prototype.pushState = function () {
|
||||
this.states.unshift({
|
||||
modifiers: null,
|
||||
ops: []
|
||||
});
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.popState = function() {
|
||||
AutoTypeParser.prototype.popState = function () {
|
||||
if (this.states.length <= 1) {
|
||||
throw 'Unexpected ")" at index ' + this.ix;
|
||||
}
|
||||
@@ -60,11 +60,11 @@ AutoTypeParser.prototype.popState = function() {
|
||||
this.addState(state);
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.state = function() {
|
||||
AutoTypeParser.prototype.state = function () {
|
||||
return this.states[0];
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.readOp = function() {
|
||||
AutoTypeParser.prototype.readOp = function () {
|
||||
const toIx = this.sequence.indexOf('}', this.ix + 2);
|
||||
if (toIx < 0) {
|
||||
throw 'Mismatched "{" at index ' + this.ix;
|
||||
@@ -79,7 +79,7 @@ AutoTypeParser.prototype.readOp = function() {
|
||||
this.addOp(op, sep, arg);
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.readModifier = function(modifier) {
|
||||
AutoTypeParser.prototype.readModifier = function (modifier) {
|
||||
const state = this.state();
|
||||
if (!state.modifiers) {
|
||||
state.modifiers = {};
|
||||
@@ -91,14 +91,14 @@ AutoTypeParser.prototype.readModifier = function(modifier) {
|
||||
state.modifiers[modifier] = true;
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.resetModifiers = function() {
|
||||
AutoTypeParser.prototype.resetModifiers = function () {
|
||||
const state = this.state();
|
||||
const modifiers = state.modifiers;
|
||||
state.modifiers = null;
|
||||
return modifiers;
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.addState = function(state) {
|
||||
AutoTypeParser.prototype.addState = function (state) {
|
||||
this.state().ops.push({
|
||||
type: 'group',
|
||||
value: state.ops,
|
||||
@@ -106,7 +106,7 @@ AutoTypeParser.prototype.addState = function(state) {
|
||||
});
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.addChar = function(ch) {
|
||||
AutoTypeParser.prototype.addChar = function (ch) {
|
||||
this.state().ops.push({
|
||||
type: 'text',
|
||||
value: ch,
|
||||
@@ -114,7 +114,7 @@ AutoTypeParser.prototype.addChar = function(ch) {
|
||||
});
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.addOp = function(op, sep, arg) {
|
||||
AutoTypeParser.prototype.addOp = function (op, sep, arg) {
|
||||
this.state().ops.push({
|
||||
type: 'op',
|
||||
value: op,
|
||||
|
||||
@@ -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,10 +6,10 @@ 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) {
|
||||
const AutoTypeRunner = function (ops) {
|
||||
this.ops = ops;
|
||||
this.pendingResolvesCount = 0;
|
||||
this.entry = null;
|
||||
@@ -142,8 +142,9 @@ AutoTypeRunner.Substitutions = {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.resolve = function(entry, callback) {
|
||||
AutoTypeRunner.prototype.resolve = function (entry, context, callback) {
|
||||
this.entry = entry;
|
||||
this.context = context;
|
||||
try {
|
||||
this.resolveOps(this.ops);
|
||||
if (!this.pendingResolvesCount) {
|
||||
@@ -156,7 +157,7 @@ AutoTypeRunner.prototype.resolve = function(entry, callback) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.resolveOps = function(ops) {
|
||||
AutoTypeRunner.prototype.resolveOps = function (ops) {
|
||||
for (let i = 0, len = ops.length; i < len; i++) {
|
||||
const op = ops[i];
|
||||
if (op.type === 'group') {
|
||||
@@ -167,7 +168,7 @@ AutoTypeRunner.prototype.resolveOps = function(ops) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.resolveOp = function(op) {
|
||||
AutoTypeRunner.prototype.resolveOp = function (op) {
|
||||
if (op.value.length === 1 && !op.sep) {
|
||||
// {x}
|
||||
op.type = 'text';
|
||||
@@ -203,6 +204,11 @@ AutoTypeRunner.prototype.resolveOp = function(op) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.context?.resolved?.[lowerValue]) {
|
||||
op.type = 'text';
|
||||
op.value = this.context.resolved[lowerValue];
|
||||
return;
|
||||
}
|
||||
const substitution = AutoTypeRunner.Substitutions[lowerValue];
|
||||
if (substitution) {
|
||||
// {title}
|
||||
@@ -218,7 +224,7 @@ AutoTypeRunner.prototype.resolveOp = function(op) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.tryParseCommand = function(op) {
|
||||
AutoTypeRunner.prototype.tryParseCommand = function (op) {
|
||||
switch (op.value.toLowerCase()) {
|
||||
case 'clearfield':
|
||||
// {CLEARFIELD}
|
||||
@@ -233,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;
|
||||
@@ -257,7 +263,7 @@ AutoTypeRunner.prototype.tryParseCommand = function(op) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.getEntryFieldKeys = function(field, op) {
|
||||
AutoTypeRunner.prototype.getEntryFieldKeys = function (field, op) {
|
||||
if (!field || !this.entry) {
|
||||
return '';
|
||||
}
|
||||
@@ -268,7 +274,7 @@ AutoTypeRunner.prototype.getEntryFieldKeys = function(field, op) {
|
||||
if (value.isProtected) {
|
||||
op.type = 'group';
|
||||
const ops = [];
|
||||
value.forEachChar(ch => {
|
||||
value.forEachChar((ch) => {
|
||||
if (ch === 10 || ch === 13) {
|
||||
ops.push({ type: 'key', value: 'enter' });
|
||||
} else {
|
||||
@@ -283,7 +289,7 @@ AutoTypeRunner.prototype.getEntryFieldKeys = function(field, op) {
|
||||
}
|
||||
op.type = 'group';
|
||||
const partsOps = [];
|
||||
parts.forEach(part => {
|
||||
parts.forEach((part) => {
|
||||
if (partsOps.length) {
|
||||
partsOps.push({ type: 'key', value: 'enter' });
|
||||
}
|
||||
@@ -295,11 +301,11 @@ AutoTypeRunner.prototype.getEntryFieldKeys = function(field, op) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.getEntryGroupName = function() {
|
||||
AutoTypeRunner.prototype.getEntryGroupName = function () {
|
||||
return this.entry && this.entry.group.title;
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.dt = function(part) {
|
||||
AutoTypeRunner.prototype.dt = function (part) {
|
||||
switch (part) {
|
||||
case 'simple':
|
||||
return (
|
||||
@@ -327,7 +333,7 @@ AutoTypeRunner.prototype.dt = function(part) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.udt = function(part) {
|
||||
AutoTypeRunner.prototype.udt = function (part) {
|
||||
switch (part) {
|
||||
case 'simple':
|
||||
return (
|
||||
@@ -355,7 +361,7 @@ AutoTypeRunner.prototype.udt = function(part) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.getOtp = function(op) {
|
||||
AutoTypeRunner.prototype.getOtp = function (op) {
|
||||
if (!this.entry) {
|
||||
return '';
|
||||
}
|
||||
@@ -363,13 +369,13 @@ AutoTypeRunner.prototype.getOtp = function(op) {
|
||||
if (!this.entry.otpGenerator) {
|
||||
return '';
|
||||
}
|
||||
this.entry.otpGenerator.next(otp => {
|
||||
this.pendingResolved(op, otp, otp ? undefined : 'OTP error');
|
||||
this.entry.otpGenerator.next((err, otp) => {
|
||||
this.pendingResolved(op, otp, err);
|
||||
});
|
||||
return AutoTypeRunner.PendingResolve;
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.pendingResolved = function(op, value, error) {
|
||||
AutoTypeRunner.prototype.pendingResolved = function (op, value, error) {
|
||||
const wasPending = op.value === AutoTypeRunner.PendingResolve;
|
||||
if (value) {
|
||||
op.value = value;
|
||||
@@ -384,11 +390,11 @@ AutoTypeRunner.prototype.pendingResolved = function(op, value, error) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.obfuscate = function() {
|
||||
AutoTypeRunner.prototype.obfuscate = function () {
|
||||
this.obfuscateOps(this.ops);
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.obfuscateOps = function(ops) {
|
||||
AutoTypeRunner.prototype.obfuscateOps = function (ops) {
|
||||
for (let i = 0, len = ops.length; i < len; i++) {
|
||||
const op = ops[i];
|
||||
if (op.mod) {
|
||||
@@ -397,7 +403,7 @@ AutoTypeRunner.prototype.obfuscateOps = function(ops) {
|
||||
if (op.type === 'text') {
|
||||
this.obfuscateOp(op);
|
||||
} else if (op.type === 'group') {
|
||||
const onlyText = op.value.every(grOp => grOp.type === 'text' && !grOp.mod);
|
||||
const onlyText = op.value.every((grOp) => grOp.type === 'text' && !grOp.mod);
|
||||
if (onlyText) {
|
||||
this.obfuscateOp(op);
|
||||
} else {
|
||||
@@ -407,7 +413,7 @@ AutoTypeRunner.prototype.obfuscateOps = function(ops) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.obfuscateOp = function(op) {
|
||||
AutoTypeRunner.prototype.obfuscateOp = function (op) {
|
||||
let letters = [];
|
||||
if (op.type === 'text') {
|
||||
if (!op.value || op.value.length <= 1) {
|
||||
@@ -415,7 +421,7 @@ AutoTypeRunner.prototype.obfuscateOp = function(op) {
|
||||
}
|
||||
letters = op.value.split('');
|
||||
} else {
|
||||
op.value.forEach(grOp => letters.push(...grOp.value.split('')));
|
||||
op.value.forEach((grOp) => letters.push(...grOp.value.split('')));
|
||||
}
|
||||
if (letters.length <= 1) {
|
||||
return;
|
||||
@@ -425,8 +431,8 @@ AutoTypeRunner.prototype.obfuscateOp = function(op) {
|
||||
op.type = 'group';
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.run = function(callback) {
|
||||
this.emitter = AutoTypeEmitterFactory.create(this.emitNext.bind(this));
|
||||
AutoTypeRunner.prototype.run = function (callback, windowId) {
|
||||
this.emitter = new AutoTypeEmitter(this.emitNext.bind(this), windowId);
|
||||
this.emitterState = {
|
||||
callback,
|
||||
stack: [],
|
||||
@@ -436,10 +442,10 @@ AutoTypeRunner.prototype.run = function(callback) {
|
||||
activeMod: {},
|
||||
finished: null
|
||||
};
|
||||
this.emitNext();
|
||||
this.emitter.begin();
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.emitNext = function(err) {
|
||||
AutoTypeRunner.prototype.emitNext = function (err) {
|
||||
if (err) {
|
||||
this.emitterState.finished = true;
|
||||
this.emitterState.callback(err);
|
||||
@@ -512,8 +518,8 @@ AutoTypeRunner.prototype.emitNext = function(err) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.setEmitterMod = function(addedMod) {
|
||||
Object.keys(addedMod).forEach(function(mod) {
|
||||
AutoTypeRunner.prototype.setEmitterMod = function (addedMod) {
|
||||
Object.keys(addedMod).forEach(function (mod) {
|
||||
if (addedMod[mod] && !this.emitterState.activeMod[mod]) {
|
||||
emitterLogger.debug('mod', mod, true);
|
||||
this.emitter.setMod(mod, true);
|
||||
@@ -522,8 +528,8 @@ AutoTypeRunner.prototype.setEmitterMod = function(addedMod) {
|
||||
}, this);
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.resetEmitterMod = function(targetState) {
|
||||
Object.keys(this.emitterState.activeMod).forEach(function(mod) {
|
||||
AutoTypeRunner.prototype.resetEmitterMod = function (targetState) {
|
||||
Object.keys(this.emitterState.activeMod).forEach(function (mod) {
|
||||
if (this.emitterState.activeMod[mod] && !targetState[mod]) {
|
||||
emitterLogger.debug('mod', mod, false);
|
||||
this.emitter.setMod(mod, false);
|
||||
|
||||
@@ -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,147 +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) {
|
||||
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('keyup ctrl alt shift t');
|
||||
Object.keys(this.mod).forEach(mod => {
|
||||
this.pendingScript.push('keydown ' + ModMap[mod]);
|
||||
});
|
||||
text.split('').forEach(char => {
|
||||
this.pendingScript.push('key U' + char.charCodeAt(0).toString(16));
|
||||
});
|
||||
Object.keys(this.mod).forEach(mod => {
|
||||
this.pendingScript.push('keyup ' + ModMap[mod]);
|
||||
});
|
||||
this.waitComplete();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.key = function(key) {
|
||||
if (typeof key !== 'number') {
|
||||
if (!KeyMap[key]) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
key = KeyMap[key].toString(16);
|
||||
}
|
||||
this.pendingScript.push('key --clearmodifiers ' + this.modString() + key);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.copyPaste = function(text) {
|
||||
this.pendingScript.push('sleep 0.5');
|
||||
Launcher.setClipboardText(text);
|
||||
this.pendingScript.push('key --clearmodifiers shift+Insert');
|
||||
this.pendingScript.push('sleep 0.5');
|
||||
this.waitComplete();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.wait = function(time) {
|
||||
this.pendingScript.push('sleep ' + time / 1000);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.waitComplete = function(callback) {
|
||||
if (this.pendingScript.length) {
|
||||
const script = this.pendingScript.join(' ');
|
||||
this.pendingScript.length = 0;
|
||||
this.runScript(script, callback);
|
||||
} else {
|
||||
this.callback();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.modString = function() {
|
||||
let mod = '';
|
||||
Object.keys(this.mod).forEach(key => {
|
||||
mod += key + '+';
|
||||
});
|
||||
return mod;
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.runScript = function(script, callback) {
|
||||
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,39 +1,38 @@
|
||||
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(appModel) {
|
||||
init() {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
this.appModel = appModel;
|
||||
Events.on('auto-type', e => this.handleEvent(e));
|
||||
Events.on('main-window-blur', e => this.resetPendingEvent(e));
|
||||
Events.on('main-window-will-close', e => this.resetPendingEvent(e));
|
||||
Events.on('closed-open-view', e => this.processPendingEvent(e));
|
||||
Events.on('auto-type', (e) => this.handleEvent(e));
|
||||
},
|
||||
|
||||
handleEvent(e) {
|
||||
const entry = (e && e.entry) || null;
|
||||
const sequence = (e && e.sequence) || null;
|
||||
const context = (e && e.context) || null;
|
||||
logger.debug('Auto type event', entry);
|
||||
if (this.running) {
|
||||
logger.debug('Already running, skipping event');
|
||||
@@ -41,7 +40,7 @@ const AutoType = {
|
||||
}
|
||||
if (entry) {
|
||||
this.hideWindow(() => {
|
||||
this.runAndHandleResult({ entry, sequence });
|
||||
this.runAndHandleResult({ entry, sequence, context });
|
||||
});
|
||||
} else {
|
||||
if (this.selectEntryView) {
|
||||
@@ -58,12 +57,19 @@ const AutoType = {
|
||||
}
|
||||
},
|
||||
|
||||
runAndHandleResult(result) {
|
||||
this.run(result, err => {
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -73,16 +79,17 @@ const AutoType = {
|
||||
}
|
||||
},
|
||||
|
||||
run(result, callback) {
|
||||
run(result, windowId, callback) {
|
||||
this.running = true;
|
||||
const sequence = result.sequence || result.entry.getEffectiveAutoTypeSeq();
|
||||
const context = result.context;
|
||||
logger.debug('Start', sequence);
|
||||
const ts = logger.ts();
|
||||
try {
|
||||
const parser = new AutoTypeParser(sequence);
|
||||
const runner = parser.parse();
|
||||
logger.debug('Parsed', this.printOps(runner.ops));
|
||||
runner.resolve(result.entry, err => {
|
||||
runner.resolve(result.entry, context, (err) => {
|
||||
if (err) {
|
||||
this.running = false;
|
||||
logger.error('Resolve error', err);
|
||||
@@ -99,7 +106,7 @@ const AutoType = {
|
||||
}
|
||||
logger.debug('Obfuscated');
|
||||
}
|
||||
runner.run(err => {
|
||||
runner.run((err) => {
|
||||
this.running = false;
|
||||
if (err) {
|
||||
logger.error('Run error', err);
|
||||
@@ -107,7 +114,7 @@ const AutoType = {
|
||||
}
|
||||
logger.debug('Complete', logger.ts(ts));
|
||||
return callback && callback();
|
||||
});
|
||||
}, windowId);
|
||||
});
|
||||
} catch (ex) {
|
||||
this.running = false;
|
||||
@@ -120,7 +127,7 @@ const AutoType = {
|
||||
try {
|
||||
const parser = new AutoTypeParser(sequence);
|
||||
const runner = parser.parse();
|
||||
runner.resolve(entry, callback);
|
||||
runner.resolve(entry, null, callback);
|
||||
} catch (ex) {
|
||||
return callback(ex);
|
||||
}
|
||||
@@ -157,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;
|
||||
}
|
||||
@@ -185,14 +191,14 @@ const AutoType = {
|
||||
logger.debug('Error during active window check, something is wrong', err);
|
||||
return callback(false);
|
||||
}
|
||||
if (activeWindowInfo.id !== windowInfo.id) {
|
||||
if (activeWindowInfo.id !== windowInfo.id && !this.supportsEventsWithWindowId) {
|
||||
logger.info(
|
||||
`Active window doesn't match: ID is different. ` +
|
||||
`Expected ${windowInfo.id}, got ${activeWindowInfo.id}`
|
||||
);
|
||||
return callback(false, activeWindowInfo);
|
||||
}
|
||||
if (activeWindowInfo.url !== windowInfo.url) {
|
||||
if (activeWindowInfo.url !== windowInfo.url && !this.supportsEventsWithWindowId) {
|
||||
logger.info(
|
||||
`Active window doesn't match: url is different. ` +
|
||||
`Expected "${windowInfo.url}", got "${activeWindowInfo.url}"`
|
||||
@@ -205,74 +211,101 @@ const AutoType = {
|
||||
},
|
||||
|
||||
selectEntryAndRun() {
|
||||
this.getActiveWindowInfo((e, windowInfo) => {
|
||||
const filter = new AutoTypeFilter(windowInfo, this.appModel);
|
||||
this.getActiveWindowInfo(async (e, windowInfo) => {
|
||||
const filter = new SelectEntryFilter(
|
||||
windowInfo,
|
||||
AppModel.instance,
|
||||
AppModel.instance.files,
|
||||
{
|
||||
autoType: true
|
||||
}
|
||||
);
|
||||
const evt = { filter, windowInfo };
|
||||
if (!this.appModel.files.hasOpenFiles()) {
|
||||
this.pendingEvent = evt;
|
||||
if (!AppModel.instance.files.hasOpenFiles()) {
|
||||
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] });
|
||||
this.runAndHandleResult({ entry: entries[0] }, evt.windowInfo.id);
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.focusMainWindow();
|
||||
evt.filter.ignoreWindowInfo = true;
|
||||
this.selectEntryView = new AutoTypeSelectView({ filter: evt.filter });
|
||||
this.selectEntryView.on('result', result => {
|
||||
|
||||
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);
|
||||
this.runAndHandleResult(result, evt.windowInfo.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
this.selectEntryView.render();
|
||||
this.selectEntryView.on('show-open-files', () => {
|
||||
this.selectEntryView.on('show-open-files', async () => {
|
||||
this.selectEntryView.hide();
|
||||
Events.emit('open-file');
|
||||
});
|
||||
},
|
||||
|
||||
resetPendingEvent() {
|
||||
if (this.pendingEvent) {
|
||||
this.pendingEvent = null;
|
||||
logger.debug('auto-type event cancelled');
|
||||
}
|
||||
},
|
||||
|
||||
processPendingEvent() {
|
||||
if (this.selectEntryView) {
|
||||
try {
|
||||
await AppModel.instance.unlockAnyFile('autoTypeUnlockMessage');
|
||||
} catch {
|
||||
this.selectEntryView.emit('result', undefined);
|
||||
return;
|
||||
}
|
||||
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,23 +1,23 @@
|
||||
import { Collection } from 'framework/collection';
|
||||
import { FileModel } from 'models/file-model';
|
||||
import { Model } from 'framework/model';
|
||||
|
||||
class FileCollection extends Collection {
|
||||
static model = FileModel;
|
||||
static model = Model;
|
||||
|
||||
hasOpenFiles() {
|
||||
return this.some(file => file.active);
|
||||
return this.some((file) => file.active);
|
||||
}
|
||||
|
||||
hasUnsavedFiles() {
|
||||
return this.some(file => file.modified);
|
||||
return this.some((file) => file.modified);
|
||||
}
|
||||
|
||||
hasDirtyFiles() {
|
||||
return this.some(file => file.dirty);
|
||||
return this.some((file) => file.dirty);
|
||||
}
|
||||
|
||||
getByName(name) {
|
||||
return this.find(file => file.name.toLowerCase() === name.toLowerCase());
|
||||
return this.find((file) => file.name.toLowerCase() === name.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ class FileInfoCollection extends Collection {
|
||||
static model = FileInfoModel;
|
||||
|
||||
load() {
|
||||
return SettingsStore.load('file-info').then(data => {
|
||||
return SettingsStore.load('file-info').then((data) => {
|
||||
if (data) {
|
||||
for (const item of data) {
|
||||
this.push(new FileInfoModel(item));
|
||||
@@ -20,7 +20,7 @@ class FileInfoCollection extends Collection {
|
||||
}
|
||||
|
||||
getMatch(storage, name, path) {
|
||||
return this.find(fi => {
|
||||
return this.find((fi) => {
|
||||
return (
|
||||
(fi.storage || '') === (storage || '') &&
|
||||
(fi.name || '') === (name || '') &&
|
||||
@@ -30,7 +30,7 @@ class FileInfoCollection extends Collection {
|
||||
}
|
||||
|
||||
getByName(name) {
|
||||
return this.find(file => file.name.toLowerCase() === name.toLowerCase());
|
||||
return this.find((file) => file.name.toLowerCase() === name.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const AppRightsChecker = {
|
||||
if (!Launcher.getAppPath().startsWith(this.AppPath)) {
|
||||
return;
|
||||
}
|
||||
this.needRunInstaller(needRun => {
|
||||
this.needRunInstaller((needRun) => {
|
||||
if (needRun) {
|
||||
this.showAlert();
|
||||
this.runInstaller();
|
||||
@@ -26,7 +26,7 @@ const AppRightsChecker = {
|
||||
},
|
||||
|
||||
needRunInstaller(callback) {
|
||||
Launcher.statFile(this.AppPath, stat => {
|
||||
Launcher.statFile(this.AppPath, (stat) => {
|
||||
const folderIsRoot = stat && stat.uid === 0;
|
||||
callback(!folderIsRoot);
|
||||
});
|
||||
@@ -38,15 +38,15 @@ const AppRightsChecker = {
|
||||
icon: 'lock',
|
||||
header: Locale.appRightsAlert,
|
||||
body:
|
||||
Locale.appRightsAlertBody1.replace('{}', `<code>${this.AppPath}</code>`) +
|
||||
'<br/>' +
|
||||
Locale.appRightsAlertBody2 +
|
||||
`: <pre>${command}</pre>`,
|
||||
Locale.appRightsAlertBody1.replace('{}', this.AppPath) +
|
||||
'\n' +
|
||||
Locale.appRightsAlertBody2,
|
||||
pre: command,
|
||||
buttons: [
|
||||
{ result: 'skip', title: Locale.alertDoNotAsk, error: true },
|
||||
Alerts.buttons.ok
|
||||
],
|
||||
success: result => {
|
||||
success: (result) => {
|
||||
if (result === 'skip') {
|
||||
this.dontAskAnymore();
|
||||
}
|
||||
@@ -58,8 +58,9 @@ const AppRightsChecker = {
|
||||
runInstaller() {
|
||||
Launcher.spawn({
|
||||
cmd: this.AppPath + '/Contents/Installer/KeeWeb Installer.app/Contents/MacOS/applet',
|
||||
args: ['--install'],
|
||||
complete: () => {
|
||||
this.needRunInstaller(needRun => {
|
||||
this.needRunInstaller((needRun) => {
|
||||
if (this.alert && !needRun) {
|
||||
this.alert.closeWithResult('cancel');
|
||||
}
|
||||
@@ -69,7 +70,7 @@ const AppRightsChecker = {
|
||||
},
|
||||
|
||||
dontAskAnymore() {
|
||||
this.needRunInstaller(needRun => {
|
||||
this.needRunInstaller((needRun) => {
|
||||
if (needRun) {
|
||||
AppSettingsModel.skipFolderRightsWarning = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import { Events } from 'framework/events';
|
||||
import { Logger } from 'util/logger';
|
||||
import { YubiKey } from 'comp/app/yubikey';
|
||||
import { UsbListener } from 'comp/app/usb-listener';
|
||||
import { Alerts } from 'comp/ui/alerts';
|
||||
import { Locale } from 'util/locale';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
|
||||
const logger = new Logger('chal-resp');
|
||||
|
||||
const ChalRespCalculator = {
|
||||
cache: {},
|
||||
|
||||
getCacheKey(params) {
|
||||
return `${params.vid}:${params.pid}:${params.serial}:${params.slot}`;
|
||||
},
|
||||
|
||||
build(params) {
|
||||
if (!params) {
|
||||
return null;
|
||||
}
|
||||
return (challenge) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
challenge = Buffer.from(challenge);
|
||||
const hexChallenge = challenge.toString('hex');
|
||||
|
||||
const cacheKey = this.getCacheKey(params);
|
||||
const respFromCache = this.cache[cacheKey]?.[hexChallenge];
|
||||
if (respFromCache) {
|
||||
logger.debug('Found ChalResp in cache');
|
||||
return resolve(Buffer.from(respFromCache, 'hex'));
|
||||
}
|
||||
|
||||
if (!AppSettingsModel.enableUsb) {
|
||||
logger.debug('USB is disabled');
|
||||
Alerts.error({
|
||||
header: Locale.yubiKeyDisabledErrorHeader,
|
||||
body: Locale.yubiKeyDisabledErrorBody,
|
||||
complete() {
|
||||
const err = new Error(Locale.yubiKeyDisabledErrorHeader);
|
||||
err.userCanceled = true;
|
||||
err.ykError = true;
|
||||
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('Calculating ChalResp using a YubiKey', params);
|
||||
|
||||
this._calc(params, challenge, (err, response) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
_calc(params, challenge, callback) {
|
||||
let touchAlert = null;
|
||||
let userCanceled = false;
|
||||
|
||||
YubiKey.calculateChalResp(params, challenge, (err, response) => {
|
||||
if (userCanceled) {
|
||||
userCanceled = false;
|
||||
return;
|
||||
}
|
||||
if (touchAlert) {
|
||||
touchAlert.closeWithoutResult();
|
||||
touchAlert = null;
|
||||
}
|
||||
if (err) {
|
||||
if (err.noKey) {
|
||||
logger.info('YubiKey ChalResp: no key');
|
||||
this._showNoKeyAlert(params.serial, (err) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
this._calc(params, challenge, callback);
|
||||
});
|
||||
return;
|
||||
} else if (err.touchRequested) {
|
||||
logger.info('YubiKey ChalResp: touch requested');
|
||||
touchAlert = this._showTouchAlert(params.serial, (err) => {
|
||||
touchAlert = null;
|
||||
userCanceled = true;
|
||||
|
||||
logger.info('YubiKey ChalResp canceled');
|
||||
YubiKey.cancelChalResp();
|
||||
|
||||
callback(err);
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
logger.error('YubiKey ChalResp error', err);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const cacheKey = this.getCacheKey(params);
|
||||
if (!this.cache[cacheKey]) {
|
||||
this.cache[cacheKey] = {};
|
||||
}
|
||||
|
||||
const hexChallenge = challenge.toString('hex');
|
||||
this.cache[cacheKey][hexChallenge] = response.toString('hex');
|
||||
|
||||
logger.info('Calculated YubiKey ChalResp');
|
||||
callback(null, response);
|
||||
});
|
||||
},
|
||||
|
||||
_showNoKeyAlert(serial, callback) {
|
||||
Launcher.showMainWindow();
|
||||
|
||||
let noKeyAlert = null;
|
||||
let deviceEnumerationTimer;
|
||||
|
||||
const onUsbDevicesChanged = () => {
|
||||
if (!UsbListener.attachedYubiKeys) {
|
||||
return;
|
||||
}
|
||||
deviceEnumerationTimer = setTimeout(() => {
|
||||
YubiKey.list((err, list) => {
|
||||
if (err) {
|
||||
logger.error('YubiKey list error', err);
|
||||
return;
|
||||
}
|
||||
const isAttached = list.some((yk) => yk.serial === serial);
|
||||
logger.info(isAttached ? 'YubiKey found' : 'YubiKey not found');
|
||||
if (isAttached) {
|
||||
Events.off('usb-devices-changed', onUsbDevicesChanged);
|
||||
if (noKeyAlert) {
|
||||
noKeyAlert.closeWithoutResult();
|
||||
}
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}, Timeouts.ExternalDeviceAfterReconnect);
|
||||
};
|
||||
|
||||
Events.on('usb-devices-changed', onUsbDevicesChanged);
|
||||
|
||||
noKeyAlert = Alerts.alert({
|
||||
header: Locale.yubiKeyNoKeyHeader,
|
||||
body: Locale.yubiKeyNoKeyBody.replace('{}', serial),
|
||||
buttons: [Alerts.buttons.cancel],
|
||||
icon: 'usb-token',
|
||||
cancel: () => {
|
||||
logger.info('No key alert closed');
|
||||
|
||||
clearTimeout(deviceEnumerationTimer);
|
||||
Events.off('usb-devices-changed', onUsbDevicesChanged);
|
||||
|
||||
const err = new Error('User canceled the YubiKey no key prompt');
|
||||
err.userCanceled = true;
|
||||
err.ykError = true;
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_showTouchAlert(serial, callback) {
|
||||
Launcher.showMainWindow();
|
||||
|
||||
return Alerts.alert({
|
||||
header: Locale.yubiKeyTouchRequestedHeader,
|
||||
body: Locale.yubiKeyTouchRequestedBody.replace('{}', serial),
|
||||
buttons: [Alerts.buttons.cancel],
|
||||
icon: 'usb-token',
|
||||
cancel: () => {
|
||||
logger.info('Touch alert closed');
|
||||
|
||||
const err = new Error('User canceled the YubiKey touch prompt');
|
||||
err.userCanceled = true;
|
||||
err.ykError = true;
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
clearCache(params) {
|
||||
delete this.cache[this.getCacheKey(params)];
|
||||
}
|
||||
};
|
||||
|
||||
export { ChalRespCalculator };
|
||||
@@ -3,19 +3,19 @@ import { UrlFormat } from 'util/formatting/url-format';
|
||||
|
||||
const ChooserAppKey = 'qp7ctun6qt5n9d6';
|
||||
|
||||
const DropboxChooser = function(callback) {
|
||||
const DropboxChooser = function (callback) {
|
||||
this.cb = callback;
|
||||
this.onMessage = this.onMessage.bind(this);
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.callback = function(err, res) {
|
||||
DropboxChooser.prototype.callback = function (err, res) {
|
||||
if (this.cb) {
|
||||
this.cb(err, res);
|
||||
}
|
||||
this.cb = null;
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.choose = function() {
|
||||
DropboxChooser.prototype.choose = function () {
|
||||
const windowFeatures = 'width=640,height=552,left=357,top=100,resizable=yes,location=yes';
|
||||
const url = this.buildUrl();
|
||||
this.popup = window.open(url, 'dropbox', windowFeatures);
|
||||
@@ -26,7 +26,7 @@ DropboxChooser.prototype.choose = function() {
|
||||
this.closeInt = setInterval(this.checkClose.bind(this), 200);
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.buildUrl = function() {
|
||||
DropboxChooser.prototype.buildUrl = function () {
|
||||
return UrlFormat.makeUrl('https://www.dropbox.com/chooser', {
|
||||
origin: window.location.protocol + '//' + window.location.host,
|
||||
'app_key': AppSettingsModel.dropboxAppKey || ChooserAppKey,
|
||||
@@ -40,8 +40,8 @@ DropboxChooser.prototype.buildUrl = function() {
|
||||
});
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.onMessage = function(e) {
|
||||
if (e.source !== this.popup) {
|
||||
DropboxChooser.prototype.onMessage = function (e) {
|
||||
if (e.source !== this.popup || e.origin !== 'https://www.dropbox.com') {
|
||||
return;
|
||||
}
|
||||
const data = JSON.parse(e.data);
|
||||
@@ -69,7 +69,7 @@ DropboxChooser.prototype.onMessage = function(e) {
|
||||
}
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.checkClose = function() {
|
||||
DropboxChooser.prototype.checkClose = function () {
|
||||
if (this.popup.closed) {
|
||||
clearInterval(this.closeInt);
|
||||
window.removeEventListener('message', this.onMessage);
|
||||
@@ -79,7 +79,7 @@ DropboxChooser.prototype.checkClose = function() {
|
||||
}
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.success = function(params) {
|
||||
DropboxChooser.prototype.success = function (params) {
|
||||
if (!params || !params[0] || !params[0].link || params[0].is_dir) {
|
||||
return this.callback('bad result');
|
||||
}
|
||||
@@ -87,7 +87,7 @@ DropboxChooser.prototype.success = function(params) {
|
||||
this.readFile(this.result.link);
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.readFile = function(url) {
|
||||
DropboxChooser.prototype.readFile = function (url) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.addEventListener('load', () => {
|
||||
this.callback(null, { name: this.result.name, data: xhr.response });
|
||||
|
||||
@@ -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,
|
||||
@@ -67,16 +79,16 @@ const GeneratorPresets = {
|
||||
|
||||
get all() {
|
||||
let presets = this.builtIn;
|
||||
presets.forEach(preset => {
|
||||
presets.forEach((preset) => {
|
||||
preset.builtIn = true;
|
||||
});
|
||||
const setting = AppSettingsModel.generatorPresets;
|
||||
if (setting) {
|
||||
if (setting.user) {
|
||||
presets = presets.concat(setting.user.map(item => ({ ...item })));
|
||||
presets = presets.concat(setting.user.map((item) => ({ ...item })));
|
||||
}
|
||||
let hasDefault = false;
|
||||
presets.forEach(preset => {
|
||||
presets.forEach((preset) => {
|
||||
if (setting.disabled && setting.disabled[preset.name]) {
|
||||
preset.disabled = true;
|
||||
}
|
||||
@@ -93,7 +105,7 @@ const GeneratorPresets = {
|
||||
},
|
||||
|
||||
get enabled() {
|
||||
const allPresets = this.all.filter(preset => !preset.disabled);
|
||||
const allPresets = this.all.filter((preset) => !preset.disabled);
|
||||
if (!allPresets.length) {
|
||||
allPresets.push(this.defaultPreset);
|
||||
}
|
||||
@@ -110,7 +122,7 @@ const GeneratorPresets = {
|
||||
|
||||
add(preset) {
|
||||
const setting = this.getOrCreateSetting();
|
||||
if (preset.name && !setting.user.filter(p => p.name === preset.name).length) {
|
||||
if (preset.name && !setting.user.filter((p) => p.name === preset.name).length) {
|
||||
setting.user.push(preset);
|
||||
this.save(setting);
|
||||
}
|
||||
@@ -118,13 +130,13 @@ const GeneratorPresets = {
|
||||
|
||||
remove(name) {
|
||||
const setting = this.getOrCreateSetting();
|
||||
setting.user = setting.user.filter(p => p.name !== name);
|
||||
setting.user = setting.user.filter((p) => p.name !== name);
|
||||
this.save(setting);
|
||||
},
|
||||
|
||||
setPreset(name, props) {
|
||||
const setting = this.getOrCreateSetting();
|
||||
const preset = setting.user.filter(p => p.name === name)[0];
|
||||
const preset = setting.user.filter((p) => p.name === name)[0];
|
||||
if (preset) {
|
||||
Object.assign(preset, props);
|
||||
this.save(setting);
|
||||
|
||||
@@ -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 };
|
||||
@@ -56,7 +56,7 @@ const Shortcuts = {
|
||||
}
|
||||
return shortcutValue
|
||||
.split(/\+/g)
|
||||
.map(part => {
|
||||
.map((part) => {
|
||||
switch (part) {
|
||||
case 'Ctrl':
|
||||
return this.ctrlShortcutSymbol(formatting);
|
||||
@@ -85,7 +85,7 @@ const Shortcuts = {
|
||||
return Features.isMac ? '⌃' : this.formatShortcut(Locale.ctrlKey, formatting);
|
||||
},
|
||||
formatShortcut(shortcut, formatting) {
|
||||
return formatting ? `<span class="thin">${shortcut} + </span>` : `${shortcut}+`;
|
||||
return formatting ? `${shortcut} + ` : `${shortcut}+`;
|
||||
},
|
||||
globalShortcutText(type, formatting) {
|
||||
return this.presentShortcut(this.globalShortcut(type), formatting);
|
||||
|
||||
@@ -19,22 +19,20 @@ const StartProfiler = {
|
||||
operations.unshift({ name: 'fetching', elapsed: networkTime });
|
||||
|
||||
const time = Math.round(performance.now());
|
||||
const details = operations.map(op => `${op.name}=${Math.round(op.elapsed)}ms`).join(', ');
|
||||
let message = `Started in ${time}ms: ${details}.`;
|
||||
|
||||
if (this.appProfile) {
|
||||
message += ` Electron app started in ${this.appProfile.totalTime}ms: `;
|
||||
message +=
|
||||
this.appProfile.timings
|
||||
.map(op => `${op.name}=${Math.round(op.elapsed)}ms`)
|
||||
.join(', ') + '.';
|
||||
}
|
||||
|
||||
logger.info(message);
|
||||
this.printReport('App', operations, time);
|
||||
},
|
||||
|
||||
reportAppProfile(data) {
|
||||
this.appProfile = data;
|
||||
this.printReport('Electron app', data.timings, data.totalTime);
|
||||
},
|
||||
|
||||
printReport(name, operations, totalTime) {
|
||||
const message =
|
||||
`${name} started in ${totalTime}ms: ` +
|
||||
operations.map((op) => `${op.name}=${Math.round(op.elapsed)}ms`).join(', ');
|
||||
|
||||
logger.info(message);
|
||||
},
|
||||
|
||||
getNetworkTime() {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
},
|
||||
|
||||
@@ -43,7 +43,7 @@ const Updater = {
|
||||
if (!Launcher && navigator.serviceWorker && !RuntimeInfo.beta && !RuntimeInfo.devMode) {
|
||||
navigator.serviceWorker
|
||||
.register('service-worker.js')
|
||||
.then(reg => {
|
||||
.then((reg) => {
|
||||
logger.info('Service worker registered');
|
||||
reg.addEventListener('updatefound', () => {
|
||||
if (reg.active) {
|
||||
@@ -52,7 +52,7 @@ const Updater = {
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
.catch((e) => {
|
||||
logger.error('Failed to register a service worker', e);
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
@@ -145,7 +143,7 @@ const Updater = {
|
||||
UpdateModel.set({ updateStatus: 'found' });
|
||||
}
|
||||
},
|
||||
error: e => {
|
||||
error: (e) => {
|
||||
logger.error('Update check error', e);
|
||||
UpdateModel.set({
|
||||
status: 'error',
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Events } from 'framework/events';
|
||||
import { Logger } from 'util/logger';
|
||||
import { NativeModules } from 'comp/launcher/native-modules';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { Features } from 'util/features';
|
||||
|
||||
const logger = new Logger('usb-listener');
|
||||
|
||||
const UsbListener = {
|
||||
supported: Features.isDesktop,
|
||||
attachedYubiKeys: 0,
|
||||
|
||||
init() {
|
||||
if (!this.supported) {
|
||||
return;
|
||||
}
|
||||
|
||||
Events.on('native-modules-yubikeys', (e) => {
|
||||
if (e.numYubiKeys !== this.attachedYubiKeys) {
|
||||
logger.debug(`YubiKeys changed ${this.attachedYubiKeys} => ${e.numYubiKeys}`);
|
||||
this.attachedYubiKeys = e.numYubiKeys;
|
||||
Events.emit('usb-devices-changed');
|
||||
}
|
||||
});
|
||||
|
||||
AppSettingsModel.on('change:enableUsb', (model, enabled) => {
|
||||
if (enabled) {
|
||||
this.start();
|
||||
} else {
|
||||
this.stop();
|
||||
}
|
||||
});
|
||||
|
||||
if (AppSettingsModel.enableUsb) {
|
||||
this.start();
|
||||
}
|
||||
},
|
||||
|
||||
start() {
|
||||
logger.info('Starting USB listener');
|
||||
|
||||
if (this.usb) {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
try {
|
||||
NativeModules.startUsbListener();
|
||||
} catch (e) {
|
||||
logger.error('Error starting USB listener', e);
|
||||
}
|
||||
},
|
||||
|
||||
stop() {
|
||||
logger.info('Stopping USB listener');
|
||||
|
||||
try {
|
||||
NativeModules.stopUsbListener();
|
||||
} catch (e) {
|
||||
logger.error('Error stopping USB listener', e);
|
||||
}
|
||||
|
||||
if (this.attachedYubiKeys) {
|
||||
this.attachedYubiKeys = 0;
|
||||
Events.emit('usb-devices-changed');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { UsbListener };
|
||||
@@ -0,0 +1,291 @@
|
||||
import { Events } from 'framework/events';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { NativeModules } from 'comp/launcher/native-modules';
|
||||
import { Logger } from 'util/logger';
|
||||
import { UsbListener } from 'comp/app/usb-listener';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
import { YubiKeyProductIds, YubiKeyChallengeSize } from 'const/hardware';
|
||||
import { Locale } from 'util/locale';
|
||||
|
||||
const logger = new Logger('yubikey');
|
||||
|
||||
const YubiKey = {
|
||||
ykmanStatus: undefined,
|
||||
process: null,
|
||||
aborted: false,
|
||||
|
||||
cmd() {
|
||||
if (this._cmd) {
|
||||
return this._cmd;
|
||||
}
|
||||
const macYkmanPath = '/usr/local/bin/ykman';
|
||||
if (Launcher.platform() === 'darwin' && Launcher.fileExistsSync(macYkmanPath)) {
|
||||
this._cmd = macYkmanPath;
|
||||
} else {
|
||||
this._cmd = 'ykman';
|
||||
}
|
||||
return this._cmd;
|
||||
},
|
||||
|
||||
checkToolStatus() {
|
||||
if (this.ykmanStatus === 'ok') {
|
||||
return Promise.resolve(this.ykmanStatus);
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this.ykmanStatus = 'checking';
|
||||
this._cmd = undefined;
|
||||
Launcher.spawn({
|
||||
cmd: this.cmd(),
|
||||
args: ['-v'],
|
||||
noStdOutLogging: true,
|
||||
complete: (err, stdout, code) => {
|
||||
if (err || code !== 0) {
|
||||
this.ykmanStatus = 'error';
|
||||
} else {
|
||||
this.ykmanStatus = 'ok';
|
||||
}
|
||||
resolve(this.ykmanStatus);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
abort() {
|
||||
logger.info('Aborting');
|
||||
if (this.process) {
|
||||
logger.info('Killing the process');
|
||||
try {
|
||||
this.process.kill();
|
||||
} catch {}
|
||||
}
|
||||
this.aborted = true;
|
||||
this.process = null;
|
||||
},
|
||||
|
||||
list(callback) {
|
||||
NativeModules.getYubiKeys({})
|
||||
.then((yubiKeys) => {
|
||||
yubiKeys = yubiKeys.map(({ serial, vid, pid, version, slots }) => {
|
||||
return {
|
||||
vid,
|
||||
pid,
|
||||
serial,
|
||||
slots,
|
||||
fullName: this.getKeyFullName(pid, version, serial)
|
||||
};
|
||||
});
|
||||
callback(null, yubiKeys);
|
||||
})
|
||||
.catch(callback);
|
||||
},
|
||||
|
||||
getKeyFullName(pid, version, serial) {
|
||||
let name = 'YubiKey';
|
||||
if (YubiKeyProductIds.Gen1.includes(pid)) {
|
||||
name += ' Gen 1';
|
||||
} else if (YubiKeyProductIds.NEO.includes(pid)) {
|
||||
name += ' NEO';
|
||||
} else if (YubiKeyProductIds.YK4.includes(pid)) {
|
||||
if (version >= '5.1.0') {
|
||||
name += ' 5';
|
||||
}
|
||||
}
|
||||
return `${name} ${serial}`;
|
||||
},
|
||||
|
||||
listWithYkman(callback) {
|
||||
this._listWithYkman(callback, true);
|
||||
},
|
||||
|
||||
_listWithYkman(callback, canRetry) {
|
||||
if (this.process) {
|
||||
return callback('Already in progress');
|
||||
}
|
||||
this.aborted = false;
|
||||
|
||||
logger.info('Listing YubiKeys');
|
||||
|
||||
if (!UsbListener.attachedYubiKeys) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
this.process = Launcher.spawn({
|
||||
cmd: this.cmd(),
|
||||
args: ['list'],
|
||||
noStdOutLogging: true,
|
||||
complete: (err, stdout) => {
|
||||
this.process = null;
|
||||
|
||||
if (this.aborted) {
|
||||
return callback('Aborted');
|
||||
}
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const yubiKeysIncludingEmpty = stdout
|
||||
.trim()
|
||||
.split(/\n/g)
|
||||
.map((line) => {
|
||||
const fullName = line;
|
||||
const serial = (line.match(/\d{5,}$/g) || [])[0];
|
||||
return { fullName, serial };
|
||||
});
|
||||
|
||||
const yubiKeys = yubiKeysIncludingEmpty.filter((s) => s.serial);
|
||||
|
||||
if (
|
||||
yubiKeysIncludingEmpty.length === 1 &&
|
||||
yubiKeys.length === 0 &&
|
||||
stdout.startsWith('YubiKey') &&
|
||||
stdout.includes('CCID') &&
|
||||
!stdout.includes('Serial')
|
||||
) {
|
||||
logger.info('The YubiKey is probably stuck');
|
||||
if (!AppSettingsModel.yubiKeyStuckWorkaround) {
|
||||
return callback(Locale.yubiKeyStuckError);
|
||||
}
|
||||
if (canRetry) {
|
||||
return this._repairStuckYubiKey(callback);
|
||||
}
|
||||
}
|
||||
|
||||
if (!yubiKeys.length) {
|
||||
return callback('No YubiKeys returned by "ykman list"');
|
||||
}
|
||||
|
||||
callback(null, yubiKeys);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_repairStuckYubiKey(callback) {
|
||||
logger.info('Repairing a stuck YubiKey');
|
||||
|
||||
let openTimeout;
|
||||
const countYubiKeys = UsbListener.attachedYubiKeys;
|
||||
const onDevicesChangedDuringRepair = () => {
|
||||
if (UsbListener.attachedYubiKeys === countYubiKeys) {
|
||||
logger.info('YubiKey was reconnected');
|
||||
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
|
||||
clearTimeout(openTimeout);
|
||||
this.aborted = false;
|
||||
setTimeout(() => {
|
||||
this._listWithYkman(callback, false);
|
||||
}, Timeouts.ExternalDeviceAfterReconnect);
|
||||
}
|
||||
};
|
||||
Events.on('usb-devices-changed', onDevicesChangedDuringRepair);
|
||||
|
||||
Launcher.spawn({
|
||||
cmd: this.cmd(),
|
||||
args: ['config', 'usb', '-e', 'oath', '-f'],
|
||||
noStdOutLogging: true,
|
||||
complete: (err) => {
|
||||
logger.info('Repair complete', err ? 'with error' : 'OK');
|
||||
if (err) {
|
||||
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
|
||||
return callback(`YubiKey repair error: ${err}`);
|
||||
}
|
||||
openTimeout = setTimeout(() => {
|
||||
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
|
||||
}, Timeouts.ExternalDeviceReconnect);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getOtpCodes(serial, callback) {
|
||||
if (this.process) {
|
||||
return callback('Already in progress');
|
||||
}
|
||||
this.aborted = false;
|
||||
|
||||
this.process = Launcher.spawn({
|
||||
cmd: this.cmd(),
|
||||
args: ['-d', serial, 'oath', 'accounts', 'code'],
|
||||
noStdOutLogging: true,
|
||||
throwOnStdErr: true,
|
||||
complete: (err, stdout) => {
|
||||
this.process = null;
|
||||
|
||||
if (this.aborted) {
|
||||
return callback('Aborted');
|
||||
}
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const codes = [];
|
||||
|
||||
for (const line of stdout.split('\n')) {
|
||||
const match = line.match(/^(.*?):(.*?)\s+(.*)$/);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
const [, title, user, code] = match;
|
||||
const needsTouch = !code.match(/^\d+$/);
|
||||
|
||||
codes.push({
|
||||
title,
|
||||
user,
|
||||
needsTouch
|
||||
});
|
||||
}
|
||||
|
||||
callback(null, codes);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getOtp(serial, entry, callback) {
|
||||
return Launcher.spawn({
|
||||
cmd: this.cmd(),
|
||||
args: ['-d', serial, 'oath', 'accounts', 'code', '--single', entry],
|
||||
noStdOutLogging: true,
|
||||
complete: (err, stdout) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
const otp = stdout.trim();
|
||||
callback(null, otp);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
calculateChalResp(chalResp, challenge, callback) {
|
||||
const { vid, pid, serial, slot } = chalResp;
|
||||
const yubiKey = { vid, pid, serial };
|
||||
|
||||
challenge = Buffer.from(challenge);
|
||||
|
||||
// https://github.com/Yubico/yubikey-personalization-gui/issues/86
|
||||
// https://github.com/keepassxreboot/keepassxc/blob/develop/src/keys/drivers/YubiKey.cpp#L318
|
||||
|
||||
const padLen = YubiKeyChallengeSize - challenge.byteLength;
|
||||
|
||||
const paddedChallenge = Buffer.alloc(YubiKeyChallengeSize, padLen);
|
||||
challenge.copy(paddedChallenge);
|
||||
|
||||
NativeModules.yubiKeyChallengeResponse(
|
||||
yubiKey,
|
||||
[...paddedChallenge],
|
||||
slot,
|
||||
(err, result) => {
|
||||
if (result) {
|
||||
result = Buffer.from(result);
|
||||
}
|
||||
if (err) {
|
||||
err.ykError = true;
|
||||
}
|
||||
return callback(err, result);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
cancelChalResp() {
|
||||
NativeModules.yubiKeyCancelChallengeResponse();
|
||||
}
|
||||
};
|
||||
|
||||
export { YubiKey };
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Features } from 'util/features';
|
||||
|
||||
const AuthReceiver = {
|
||||
receive() {
|
||||
if (!Features.isPopup) {
|
||||
return false;
|
||||
}
|
||||
const opener = window.opener || window.parent;
|
||||
const message = this.urlArgsToMessage(window.location.href);
|
||||
const hasKeys = Object.keys(message).filter(key => key !== 'config').length > 0;
|
||||
if (!hasKeys) {
|
||||
return false;
|
||||
}
|
||||
opener.postMessage(message, window.location.origin);
|
||||
window.close();
|
||||
return true;
|
||||
},
|
||||
|
||||
urlArgsToMessage(url) {
|
||||
const message = {};
|
||||
url.split(/[?#&]/g).forEach(part => {
|
||||
const parts = part.split('=');
|
||||
if (parts.length === 2) {
|
||||
message[parts[0]] = parts[1];
|
||||
}
|
||||
});
|
||||
return message;
|
||||
}
|
||||
};
|
||||
|
||||
export { AuthReceiver };
|
||||
@@ -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');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -44,7 +44,7 @@ class KeyHandler {
|
||||
offKey(key, handler, thisArg) {
|
||||
if (this.shortcuts[key]) {
|
||||
this.shortcuts[key] = this.shortcuts[key].filter(
|
||||
sh => sh.handler !== handler || sh.thisArg !== thisArg
|
||||
(sh) => sh.handler !== handler || sh.thisArg !== thisArg
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Events } from 'framework/events';
|
||||
import { AuthReceiver } from 'comp/browser/auth-receiver';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Alerts } from 'comp/ui/alerts';
|
||||
import { Links } from 'const/links';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
import { Locale } from 'util/locale';
|
||||
import { Logger } from 'util/logger';
|
||||
import { noop } from 'util/fn';
|
||||
|
||||
const PopupNotifier = {
|
||||
logger: null,
|
||||
@@ -14,10 +13,10 @@ const PopupNotifier = {
|
||||
this.logger = new Logger('popup-notifier');
|
||||
|
||||
if (Launcher) {
|
||||
window.open = this._openLauncherWindow.bind(this);
|
||||
window.open = noop;
|
||||
} else {
|
||||
const windowOpen = window.open;
|
||||
window.open = function(...args) {
|
||||
window.open = function (...args) {
|
||||
const win = windowOpen.apply(window, args);
|
||||
if (win) {
|
||||
PopupNotifier.deferCheckClosed(win);
|
||||
@@ -35,100 +34,6 @@ const PopupNotifier = {
|
||||
}
|
||||
},
|
||||
|
||||
_openLauncherWindow(url, title, settings) {
|
||||
const opts = { show: false };
|
||||
if (settings) {
|
||||
const settingsObj = {};
|
||||
settings.split(',').forEach(part => {
|
||||
const parts = part.split('=');
|
||||
settingsObj[parts[0].trim()] = parts[1].trim();
|
||||
});
|
||||
if (settingsObj.width) {
|
||||
opts.width = +settingsObj.width;
|
||||
}
|
||||
if (settingsObj.height) {
|
||||
opts.height = +settingsObj.height;
|
||||
}
|
||||
if (settingsObj.top) {
|
||||
opts.y = +settingsObj.top;
|
||||
}
|
||||
if (settingsObj.left) {
|
||||
opts.x = +settingsObj.left;
|
||||
}
|
||||
}
|
||||
let win = Launcher.openWindow(opts);
|
||||
win.webContents.on('will-redirect', (e, url) => {
|
||||
if (PopupNotifier.isOwnUrl(url)) {
|
||||
win.webContents.stop();
|
||||
win.close();
|
||||
PopupNotifier.processReturnToApp(url);
|
||||
}
|
||||
});
|
||||
win.webContents.on('will-navigate', (e, url) => {
|
||||
if (PopupNotifier.isOwnUrl(url)) {
|
||||
e.preventDefault();
|
||||
win.close();
|
||||
PopupNotifier.processReturnToApp(url);
|
||||
}
|
||||
});
|
||||
win.webContents.on('crashed', (e, killed) => {
|
||||
this.logger.debug('crashed', e, killed);
|
||||
this.deferCheckClosed(win);
|
||||
win.close();
|
||||
win = null;
|
||||
});
|
||||
win.webContents.on(
|
||||
'did-fail-load',
|
||||
(e, errorCode, errorDescription, validatedUrl, isMainFrame) => {
|
||||
this.logger.debug(
|
||||
'did-fail-load',
|
||||
e,
|
||||
errorCode,
|
||||
errorDescription,
|
||||
validatedUrl,
|
||||
isMainFrame
|
||||
);
|
||||
this.deferCheckClosed(win);
|
||||
win.close();
|
||||
win = null;
|
||||
}
|
||||
);
|
||||
win.once('page-title-updated', () => {
|
||||
setTimeout(() => {
|
||||
if (win) {
|
||||
win.show();
|
||||
win.focus();
|
||||
}
|
||||
}, Timeouts.PopupWaitTime);
|
||||
});
|
||||
win.on('closed', () => {
|
||||
setTimeout(
|
||||
PopupNotifier.triggerClosed.bind(PopupNotifier, win),
|
||||
Timeouts.CheckWindowClosed
|
||||
);
|
||||
win = null;
|
||||
});
|
||||
win.loadURL(url);
|
||||
Events.emit('popup-opened', win);
|
||||
return win;
|
||||
},
|
||||
|
||||
isOwnUrl(url) {
|
||||
return (
|
||||
url.lastIndexOf(Links.WebApp, 0) === 0 ||
|
||||
url.lastIndexOf(location.origin + location.pathname, 0) === 0
|
||||
);
|
||||
},
|
||||
|
||||
processReturnToApp(url) {
|
||||
const returnMessage = AuthReceiver.urlArgsToMessage(url);
|
||||
if (Object.keys(returnMessage).length > 0) {
|
||||
const evt = new Event('message');
|
||||
evt.data = returnMessage;
|
||||
window.dispatchEvent(evt);
|
||||
}
|
||||
},
|
||||
|
||||
deferCheckClosed(win) {
|
||||
setTimeout(PopupNotifier.checkClosed.bind(PopupNotifier, win), Timeouts.CheckWindowClosed);
|
||||
},
|
||||
@@ -140,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,6 +1,6 @@
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
|
||||
const SecureInput = function() {
|
||||
const SecureInput = function () {
|
||||
this.el = null;
|
||||
this.minChar = 0x1400 + Math.round(Math.random() * 100);
|
||||
this.maxLen = 1024;
|
||||
@@ -9,13 +9,13 @@ const SecureInput = function() {
|
||||
this.salt = new Uint32Array(0);
|
||||
};
|
||||
|
||||
SecureInput.prototype.setElement = function(el) {
|
||||
SecureInput.prototype.setElement = function (el) {
|
||||
this.el = el;
|
||||
this.el.val(this.pseudoValue);
|
||||
this.el.on('input', this._input.bind(this));
|
||||
};
|
||||
|
||||
SecureInput.prototype.reset = function() {
|
||||
SecureInput.prototype.reset = function () {
|
||||
this.el = null;
|
||||
this.length = 0;
|
||||
this.pseudoValue = '';
|
||||
@@ -28,7 +28,7 @@ SecureInput.prototype.reset = function() {
|
||||
this.salt = new Uint32Array(0);
|
||||
};
|
||||
|
||||
SecureInput.prototype._input = function() {
|
||||
SecureInput.prototype._input = function () {
|
||||
const selStart = this.el[0].selectionStart;
|
||||
const value = this.el.val();
|
||||
let newPs = '';
|
||||
@@ -63,11 +63,11 @@ SecureInput.prototype._input = function() {
|
||||
this.el[0].selectionEnd = selStart;
|
||||
};
|
||||
|
||||
SecureInput.prototype._getChar = function(ix) {
|
||||
SecureInput.prototype._getChar = function (ix) {
|
||||
return String.fromCharCode(this.minChar + ix);
|
||||
};
|
||||
|
||||
SecureInput.prototype._isSpecialChar = function(ch) {
|
||||
SecureInput.prototype._isSpecialChar = function (ch) {
|
||||
return ch >= this.minChar && ch <= this.minChar + this.maxLen;
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
@@ -27,7 +45,7 @@ const Transport = {
|
||||
logger.info('GET ' + config.url);
|
||||
const opts = Launcher.req('url').parse(config.url);
|
||||
opts.headers = { 'User-Agent': navigator.userAgent };
|
||||
Launcher.resolveProxy(config.url, proxy => {
|
||||
Launcher.resolveProxy(config.url, (proxy) => {
|
||||
logger.info(
|
||||
'Request to ' +
|
||||
config.url +
|
||||
@@ -41,7 +59,7 @@ const Transport = {
|
||||
opts.path = config.url;
|
||||
}
|
||||
Launcher.req(proto)
|
||||
.get(opts, res => {
|
||||
.get(opts, (res) => {
|
||||
logger.info('Response from ' + config.url + ': ' + res.statusCode);
|
||||
if (res.statusCode === 200) {
|
||||
if (config.file) {
|
||||
@@ -52,19 +70,26 @@ const Transport = {
|
||||
config.success(tmpFile);
|
||||
});
|
||||
});
|
||||
file.on('error', err => {
|
||||
file.on('error', (err) => {
|
||||
config.error(err);
|
||||
});
|
||||
} else {
|
||||
let data = [];
|
||||
res.on('data', chunk => {
|
||||
res.on('data', (chunk) => {
|
||||
data.push(chunk);
|
||||
});
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -79,7 +104,7 @@ const Transport = {
|
||||
config.error('HTTP status ' + res.statusCode);
|
||||
}
|
||||
})
|
||||
.on('error', e => {
|
||||
.on('error', (e) => {
|
||||
logger.error('Cannot GET ' + config.url, e);
|
||||
if (tmpFile) {
|
||||
fs.unlink(tmpFile, noop);
|
||||
|
||||
@@ -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,12 +1,10 @@
|
||||
/* 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 { MdToHtml } from 'util/formatting/md-to-html';
|
||||
import { DateFormat } from 'comp/i18n/date-format';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
import { Locale } from 'util/locale';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
|
||||
const Templates = {
|
||||
db: require('templates/export/db.hbs'),
|
||||
@@ -17,7 +15,7 @@ const FieldMapping = [
|
||||
{ name: 'UserName', locStr: 'user' },
|
||||
{ name: 'Password', locStr: 'password', protect: true },
|
||||
{ name: 'URL', locStr: 'website' },
|
||||
{ name: 'Notes', locStr: 'notes', markdown: true }
|
||||
{ name: 'Notes', locStr: 'notes' }
|
||||
];
|
||||
|
||||
const KnownFields = { 'Title': true };
|
||||
@@ -33,41 +31,34 @@ function walkGroup(db, group, parents) {
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
const self = group.entries.map(entry => walkEntry(db, entry, parents)).join('\n');
|
||||
const children = group.groups.map(childGroup => walkGroup(db, childGroup, parents)).join('\n');
|
||||
const self = group.entries.map((entry) => walkEntry(db, entry, parents)).join('\n');
|
||||
const children = group.groups
|
||||
.map((childGroup) => walkGroup(db, childGroup, parents))
|
||||
.join('\n');
|
||||
return self + children;
|
||||
}
|
||||
|
||||
function walkEntry(db, entry, parents) {
|
||||
const path = parents.map(group => group.name).join(' / ');
|
||||
const path = parents.map((group) => group.name).join(' / ');
|
||||
const fields = [];
|
||||
for (const field of FieldMapping) {
|
||||
let value = entryField(entry, field.name);
|
||||
const value = entryField(entry, field.name);
|
||||
if (value) {
|
||||
let html = false;
|
||||
if (field.markdown && AppSettingsModel.useMarkdown) {
|
||||
const converted = MdToHtml.convert(value);
|
||||
if (converted !== value) {
|
||||
value = converted;
|
||||
html = true;
|
||||
}
|
||||
}
|
||||
fields.push({
|
||||
title: StringFormat.capFirst(Locale[field.locStr]),
|
||||
value,
|
||||
protect: field.protect,
|
||||
html
|
||||
protect: field.protect
|
||||
});
|
||||
}
|
||||
}
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -78,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;
|
||||
@@ -89,7 +80,7 @@ function walkEntry(db, entry, parents) {
|
||||
}
|
||||
return { name, data };
|
||||
})
|
||||
.filter(att => att.name && att.data);
|
||||
.filter((att) => att.name && att.data);
|
||||
|
||||
return Templates.entry({
|
||||
path,
|
||||
@@ -104,19 +95,19 @@ 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 || '';
|
||||
}
|
||||
|
||||
const KdbxToHtml = {
|
||||
convert(db, options) {
|
||||
const content = db.groups.map(group => walkGroup(db, group, [])).join('\n');
|
||||
const content = db.groups.map((group) => walkGroup(db, group, [])).join('\n');
|
||||
return Templates.db({
|
||||
name: options.name,
|
||||
date: DateFormat.dtStr(Date.now()),
|
||||
appLink: Links.Homepage,
|
||||
appVersion: RuntimeInfo.version,
|
||||
content
|
||||
contentHtml: content
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Otp } from 'util/data/otp';
|
||||
import { Features } from 'util/features';
|
||||
import { Locale } from 'util/locale';
|
||||
import { Logger } from 'util/logger';
|
||||
import { escape } from 'util/fn';
|
||||
|
||||
const logger = new Logger('otp-qr-reader');
|
||||
|
||||
@@ -22,17 +21,11 @@ class OtpQrReader {
|
||||
read() {
|
||||
let screenshotKey = Shortcuts.screenshotToClipboardShortcut();
|
||||
if (screenshotKey) {
|
||||
screenshotKey = Locale.detSetupOtpAlertBodyWith.replace(
|
||||
'{}',
|
||||
'<code>' + screenshotKey + '</code>'
|
||||
);
|
||||
screenshotKey = Locale.detSetupOtpAlertBodyWith.replace('{}', screenshotKey);
|
||||
}
|
||||
const pasteKey = Features.isMobile
|
||||
? ''
|
||||
: Locale.detSetupOtpAlertBodyWith.replace(
|
||||
'{}',
|
||||
'<code>' + Shortcuts.actionShortcutSymbol() + 'V</code>'
|
||||
);
|
||||
: Locale.detSetupOtpAlertBodyWith.replace('{}', Shortcuts.actionShortcutSymbol() + 'V');
|
||||
this.startListenClipoard();
|
||||
const buttons = [
|
||||
{ result: 'manually', title: Locale.detSetupOtpManualButton, silent: true },
|
||||
@@ -53,12 +46,12 @@ class OtpQrReader {
|
||||
Locale.detSetupOtpAlertBody2.replace('{}', screenshotKey || ''),
|
||||
line3,
|
||||
Locale.detSetupOtpAlertBody4
|
||||
].join('<br/>'),
|
||||
].join('\n'),
|
||||
esc: '',
|
||||
click: '',
|
||||
enter: '',
|
||||
buttons,
|
||||
complete: res => {
|
||||
complete: (res) => {
|
||||
this.alert = null;
|
||||
this.stopListenClipboard();
|
||||
if (res === 'select') {
|
||||
@@ -101,7 +94,7 @@ class OtpQrReader {
|
||||
|
||||
pasteEvent(e) {
|
||||
const item = [...e.clipboardData.items].find(
|
||||
item => item.kind === 'file' && item.type.indexOf('image') !== -1
|
||||
(item) => item.kind === 'file' && item.type.indexOf('image') !== -1
|
||||
);
|
||||
if (!item) {
|
||||
logger.debug('Paste without file');
|
||||
@@ -141,11 +134,8 @@ class OtpQrReader {
|
||||
logger.error('Error parsing QR code', err);
|
||||
Alerts.error({
|
||||
header: Locale.detOtpQrWrong,
|
||||
body:
|
||||
Locale.detOtpQrWrongBody +
|
||||
'<pre class="modal__pre">' +
|
||||
escape(err.toString()) +
|
||||
'</pre>'
|
||||
body: Locale.detOtpQrWrongBody,
|
||||
pre: err.toString()
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -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 };
|
||||
@@ -2,8 +2,6 @@ let Launcher;
|
||||
|
||||
if (window.process && window.process.versions && window.process.versions.electron) {
|
||||
Launcher = require('./launcher-electron').Launcher;
|
||||
} else if (window.cordova) {
|
||||
Launcher = require('./launcher-cordova').Launcher;
|
||||
}
|
||||
|
||||
export { Launcher };
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
/* global FingerprintAuth */
|
||||
|
||||
import { Events } from 'framework/events';
|
||||
|
||||
const Launcher = {
|
||||
name: 'cordova',
|
||||
version: window.cordova.platformVersion,
|
||||
autoTypeSupported: false,
|
||||
thirdPartyStoragesSupported: false,
|
||||
clipboardSupported: false,
|
||||
ready(callback) {
|
||||
document.addEventListener('deviceready', callback, false);
|
||||
document.addEventListener(
|
||||
'pause',
|
||||
() => {
|
||||
Events.emit('app-minimized');
|
||||
},
|
||||
false
|
||||
);
|
||||
},
|
||||
platform() {
|
||||
return 'cordova';
|
||||
},
|
||||
openLink(href) {
|
||||
window.open(href, '_system');
|
||||
},
|
||||
devTools: false,
|
||||
openDevTools() {},
|
||||
getSaveFileName(defaultPath, callback) {
|
||||
/* skip in cordova */
|
||||
},
|
||||
getDataPath(...args) {
|
||||
const storagePath = window.cordova.file.externalDataDirectory;
|
||||
return [storagePath].concat(Array.from(args)).filter(s => !!s);
|
||||
},
|
||||
getUserDataPath(fileName) {
|
||||
return this.getDataPath('userdata', fileName).join('/');
|
||||
},
|
||||
getTempPath(fileName) {
|
||||
return this.getDataPath('temp', fileName).join('/');
|
||||
},
|
||||
getDocumentsPath(fileName) {
|
||||
return this.getDataPath('documents', fileName).join('/');
|
||||
},
|
||||
getAppPath(fileName) {
|
||||
return this.getDataPath(fileName).join('/');
|
||||
},
|
||||
getWorkDirPath(fileName) {
|
||||
return this.getDataPath(fileName).join('/');
|
||||
},
|
||||
joinPath(...parts) {
|
||||
return [...parts].join('/');
|
||||
},
|
||||
writeFile(path, data, callback) {
|
||||
const createFile = filePath => {
|
||||
window.resolveLocalFileSystemURL(
|
||||
filePath.dir,
|
||||
dir => {
|
||||
dir.getFile(filePath.file, { create: true }, writeFile);
|
||||
},
|
||||
callback,
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
const writeFile = fileEntry => {
|
||||
fileEntry.createWriter(fileWriter => {
|
||||
fileWriter.onerror = callback;
|
||||
fileWriter.onwriteend = () => callback();
|
||||
fileWriter.write(data);
|
||||
}, callback);
|
||||
};
|
||||
|
||||
if (path.startsWith('cdvfile://')) {
|
||||
// then file exists
|
||||
window.resolveLocalFileSystemURL(path, writeFile, callback, callback);
|
||||
} else {
|
||||
// create file on sd card
|
||||
const filePath = this.parsePath(path);
|
||||
this.mkdir(filePath.dir, () => {
|
||||
createFile(filePath);
|
||||
});
|
||||
}
|
||||
},
|
||||
readFile(path, encoding, callback) {
|
||||
window.resolveLocalFileSystemURL(
|
||||
path,
|
||||
fileEntry => {
|
||||
fileEntry.file(
|
||||
file => {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = callback;
|
||||
reader.onloadend = () => {
|
||||
const contents = new Uint8Array(reader.result);
|
||||
callback(
|
||||
encoding ? String.fromCharCode.apply(null, contents) : contents
|
||||
);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
},
|
||||
err => callback(undefined, err)
|
||||
);
|
||||
},
|
||||
err => callback(undefined, err)
|
||||
);
|
||||
},
|
||||
fileExists(path, callback) {
|
||||
window.resolveLocalFileSystemURL(
|
||||
path,
|
||||
fileEntry => callback(true),
|
||||
() => callback(false)
|
||||
);
|
||||
},
|
||||
deleteFile(path, callback) {
|
||||
window.resolveLocalFileSystemURL(
|
||||
path,
|
||||
fileEntry => {
|
||||
fileEntry.remove(callback, callback, callback);
|
||||
},
|
||||
callback
|
||||
);
|
||||
},
|
||||
statFile(path, callback) {
|
||||
window.resolveLocalFileSystemURL(
|
||||
path,
|
||||
fileEntry => {
|
||||
fileEntry.file(
|
||||
file => {
|
||||
callback({
|
||||
ctime: new Date(file.lastModified),
|
||||
mtime: new Date(file.lastModified)
|
||||
});
|
||||
},
|
||||
err => callback(undefined, err)
|
||||
);
|
||||
},
|
||||
err => callback(undefined, err)
|
||||
);
|
||||
},
|
||||
mkdir(dir, callback) {
|
||||
const basePath = this.getDataPath().join('/');
|
||||
const createDir = (dirEntry, path, callback) => {
|
||||
const name = path.shift();
|
||||
dirEntry.getDirectory(
|
||||
name,
|
||||
{ create: true },
|
||||
dirEntry => {
|
||||
if (path.length) {
|
||||
// there is more to create
|
||||
createDir(dirEntry, path, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
const localPath = dir
|
||||
.replace(basePath, '')
|
||||
.split('/')
|
||||
.filter(s => !!s);
|
||||
|
||||
if (localPath.length) {
|
||||
window.resolveLocalFileSystemURL(
|
||||
basePath,
|
||||
dirEntry => {
|
||||
createDir(dirEntry, localPath, callback);
|
||||
},
|
||||
callback
|
||||
);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
parsePath(fileName) {
|
||||
const parts = fileName.split('/');
|
||||
|
||||
return {
|
||||
path: fileName,
|
||||
file: parts.pop(),
|
||||
dir: parts.join('/')
|
||||
};
|
||||
},
|
||||
createFsWatcher(path) {
|
||||
return null; // not in android with content provider
|
||||
},
|
||||
ensureRunnable(path) {},
|
||||
preventExit(e) {
|
||||
e.returnValue = false;
|
||||
return false;
|
||||
},
|
||||
exit() {
|
||||
this.hideApp();
|
||||
},
|
||||
requestExit() {
|
||||
/* skip in cordova */
|
||||
},
|
||||
requestRestart() {
|
||||
window.location.reload();
|
||||
},
|
||||
cancelRestart() {
|
||||
/* skip in cordova */
|
||||
},
|
||||
setClipboardText(text) {},
|
||||
getClipboardText() {},
|
||||
clearClipboardText() {},
|
||||
minimizeApp() {
|
||||
this.hideApp();
|
||||
},
|
||||
canMinimize() {
|
||||
return false;
|
||||
},
|
||||
canDetectOsSleep() {
|
||||
return false;
|
||||
},
|
||||
updaterEnabled() {
|
||||
return false;
|
||||
},
|
||||
// getMainWindow() {},
|
||||
resolveProxy(url, callback) {
|
||||
/* skip in cordova */
|
||||
},
|
||||
openWindow(opts) {
|
||||
/* skip in cordova */
|
||||
},
|
||||
hideApp() {
|
||||
/* skip in cordova */
|
||||
},
|
||||
isAppFocused() {
|
||||
return false; /* skip in cordova */
|
||||
},
|
||||
showMainWindow() {
|
||||
/* skip in cordova */
|
||||
},
|
||||
// spawn(config) {},
|
||||
openFileChooser(callback) {
|
||||
const onFileSelected = function(selected) {
|
||||
window.resolveLocalFileSystemURL(selected.uri, fileEntry => {
|
||||
fileEntry.file(file => {
|
||||
file.path = file.localURL;
|
||||
file.name = selected.name;
|
||||
callback(null, file);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.cordova.exec(onFileSelected, callback, 'FileChooser', 'choose');
|
||||
},
|
||||
setGlobalShortcuts(appSettings) {},
|
||||
fingerprints: {
|
||||
config: {
|
||||
disableBackup: true,
|
||||
clientId: 'keeweb'
|
||||
},
|
||||
|
||||
register(fileId, password, callback) {
|
||||
FingerprintAuth.isAvailable(result => {
|
||||
if (!result.isAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const encryptConfig = {
|
||||
...this.config,
|
||||
username: fileId,
|
||||
password: password.getText()
|
||||
};
|
||||
|
||||
FingerprintAuth.encrypt(encryptConfig, result => {
|
||||
callback(result.token);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
auth(fileId, token, callback) {
|
||||
if (!token) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
const decryptConfig = { ...this.config, username: fileId, token };
|
||||
|
||||
FingerprintAuth.decrypt(decryptConfig, result => {
|
||||
callback(result.password);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { Launcher };
|
||||
@@ -17,6 +17,9 @@ const Launcher = {
|
||||
platform() {
|
||||
return process.platform;
|
||||
},
|
||||
arch() {
|
||||
return process.arch;
|
||||
},
|
||||
electron() {
|
||||
return this.req('electron');
|
||||
},
|
||||
@@ -33,9 +36,7 @@ const Launcher = {
|
||||
},
|
||||
devTools: true,
|
||||
openDevTools() {
|
||||
this.electron()
|
||||
.remote.getCurrentWindow()
|
||||
.openDevTools({ mode: 'bottom' });
|
||||
this.electron().remote.getCurrentWindow().webContents.openDevTools({ mode: 'bottom' });
|
||||
},
|
||||
getSaveFileName(defaultPath, callback) {
|
||||
if (defaultPath) {
|
||||
@@ -48,18 +49,24 @@ const Launcher = {
|
||||
defaultPath,
|
||||
filters: [{ name: Locale.launcherFileFilter, extensions: ['kdbx'] }]
|
||||
})
|
||||
.then(res => callback(res.filePath));
|
||||
.then((res) => callback(res.filePath));
|
||||
},
|
||||
getUserDataPath(fileName) {
|
||||
if (!this.userDataPath) {
|
||||
const realUserDataPath = this.remoteApp().getPath('userData');
|
||||
const suffixReplacementRegex = /[\\/]temp[\\/]\d+\.\d+[\\/]?$/;
|
||||
this.userDataPath = realUserDataPath.replace(suffixReplacementRegex, '');
|
||||
this.userDataPath = this.remoteApp().getPath('userData');
|
||||
}
|
||||
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 || '');
|
||||
@@ -85,7 +92,12 @@ const Launcher = {
|
||||
});
|
||||
},
|
||||
fileExists(path, callback) {
|
||||
this.req('fs').exists(path, callback);
|
||||
const fs = this.req('fs');
|
||||
fs.access(path, fs.constants.F_OK, (err) => callback(!err));
|
||||
},
|
||||
fileExistsSync(path) {
|
||||
const fs = this.req('fs');
|
||||
return !fs.accessSync(path, fs.constants.F_OK);
|
||||
},
|
||||
deleteFile(path, callback) {
|
||||
this.req('fs').unlink(path, callback || noop);
|
||||
@@ -98,8 +110,8 @@ const Launcher = {
|
||||
const path = this.req('path');
|
||||
const stack = [];
|
||||
|
||||
const collect = function(dir, stack, callback) {
|
||||
fs.exists(dir, exists => {
|
||||
const collect = function (dir, stack, callback) {
|
||||
fs.exists(dir, (exists) => {
|
||||
if (exists) {
|
||||
return callback();
|
||||
}
|
||||
@@ -114,12 +126,12 @@ const Launcher = {
|
||||
});
|
||||
};
|
||||
|
||||
const create = function(stack, callback) {
|
||||
const create = function (stack, callback) {
|
||||
if (!stack.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
fs.mkdir(stack.shift(), err => (err ? callback(err) : create(stack, callback)));
|
||||
fs.mkdir(stack.shift(), (err) => (err ? callback(err) : create(stack, callback)));
|
||||
};
|
||||
|
||||
collect(dir, stack, () => create(stack, callback));
|
||||
@@ -135,16 +147,11 @@ const Launcher = {
|
||||
createFsWatcher(path) {
|
||||
return this.req('fs').watch(path, { persistent: false });
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
loadConfig(name) {
|
||||
return this.remoteApp().loadConfig(name);
|
||||
},
|
||||
saveConfig(name, data) {
|
||||
return this.remoteApp().saveConfig(name, data);
|
||||
},
|
||||
preventExit(e) {
|
||||
e.returnValue = false;
|
||||
@@ -156,18 +163,19 @@ const Launcher = {
|
||||
},
|
||||
requestExit() {
|
||||
const app = this.remoteApp();
|
||||
if (this.restartPending) {
|
||||
app.restartApp();
|
||||
app.setHookBeforeQuitEvent(false);
|
||||
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);
|
||||
@@ -176,7 +184,14 @@ const Launcher = {
|
||||
return this.electron().clipboard.readText();
|
||||
},
|
||||
clearClipboardText() {
|
||||
return this.electron().clipboard.clear();
|
||||
const { clipboard } = this.electron();
|
||||
clipboard.clear();
|
||||
if (process.platform === 'linux') {
|
||||
clipboard.clear('selection');
|
||||
}
|
||||
},
|
||||
quitOnRealQuitEventIfMinimizeOnQuitIsEnabled() {
|
||||
return !!this.pendingUpdateFile;
|
||||
},
|
||||
minimizeApp() {
|
||||
this.remoteApp().minimizeApp({
|
||||
@@ -184,14 +199,11 @@ const Launcher = {
|
||||
quit: Locale.menuQuitApp.replace('{}', 'KeeWeb')
|
||||
});
|
||||
},
|
||||
canMinimize() {
|
||||
return process.platform !== 'darwin';
|
||||
},
|
||||
canDetectOsSleep() {
|
||||
return process.platform !== 'linux';
|
||||
},
|
||||
updaterEnabled() {
|
||||
return this.electron().remote.process.argv.indexOf('--disable-updater') === -1;
|
||||
return process.platform !== 'linux';
|
||||
},
|
||||
getMainWindow() {
|
||||
return this.remoteApp().getMainWindow();
|
||||
@@ -199,80 +211,54 @@ const Launcher = {
|
||||
resolveProxy(url, callback) {
|
||||
const window = this.getMainWindow();
|
||||
const session = window.webContents.session;
|
||||
session.resolveProxy(url).then(proxy => {
|
||||
session.resolveProxy(url).then((proxy) => {
|
||||
const match = /^proxy\s+([\w\.]+):(\d+)+\s*/i.exec(proxy);
|
||||
proxy = match && match[1] ? { host: match[1], port: +match[2] } : null;
|
||||
callback(proxy);
|
||||
});
|
||||
},
|
||||
openWindow(opts) {
|
||||
return this.remoteApp().openWindow(opts);
|
||||
},
|
||||
hideApp() {
|
||||
const app = this.remoteApp();
|
||||
if (this.canMinimize()) {
|
||||
app.minimizeThenHideIfInTray();
|
||||
} else {
|
||||
if (this.platform() === 'darwin') {
|
||||
app.hide();
|
||||
} else {
|
||||
app.minimizeThenHideIfInTray();
|
||||
}
|
||||
},
|
||||
isAppFocused() {
|
||||
return !!this.electron().remote.BrowserWindow.getFocusedWindow();
|
||||
},
|
||||
showMainWindow() {
|
||||
const win = this.getMainWindow();
|
||||
win.show();
|
||||
win.focus();
|
||||
win.restore();
|
||||
this.remoteApp().showAndFocusMainWindow();
|
||||
},
|
||||
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');
|
||||
});
|
||||
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) {
|
||||
logger.error(msg + '\n' + stdout + '\n' + stderr);
|
||||
} else {
|
||||
logger.info(msg + (stdout ? '\n' + stdout : ''));
|
||||
}
|
||||
if (complete) {
|
||||
complete(code ? '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;
|
||||
@@ -290,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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -297,11 +295,12 @@ 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));
|
||||
Events.on('start-profile', (data) => StartProfiler.reportAppProfile(data));
|
||||
|
||||
window.launcherOpen = file => Launcher.openFile(file);
|
||||
window.launcherOpen = (file) => Launcher.openFile(file);
|
||||
if (window.launcherOpenedFile) {
|
||||
logger.info('Open file request', window.launcherOpenedFile);
|
||||
Launcher.openFile(window.launcherOpenedFile);
|
||||
@@ -317,6 +316,15 @@ Events.on('app-ready', () =>
|
||||
}, 0)
|
||||
);
|
||||
|
||||
global.Events = Events;
|
||||
if (process.platform === 'darwin') {
|
||||
Launcher.remoteApp().setHookBeforeQuitEvent(true);
|
||||
}
|
||||
|
||||
Launcher.remoteApp().on('remote-app-event', (e) => {
|
||||
if (window.debugRemoteAppEvents) {
|
||||
logger.debug('remote-app-event', e.name);
|
||||
}
|
||||
Events.emit(e.name, e.data);
|
||||
});
|
||||
|
||||
export { Launcher };
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Events } from 'framework/events';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
|
||||
let NativeModules;
|
||||
|
||||
if (Launcher) {
|
||||
const logger = new Logger('native-module-connector');
|
||||
|
||||
let hostRunning = false;
|
||||
let hostStartPromise;
|
||||
let callId = 0;
|
||||
let promises = {};
|
||||
let ykChalRespCallbacks = {};
|
||||
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
ipcRenderer.on('nativeModuleCallback', (e, msg) => NativeModules.hostCallback(msg));
|
||||
ipcRenderer.on('nativeModuleHostError', (e, err) => NativeModules.hostError(err));
|
||||
ipcRenderer.on('nativeModuleHostExit', (e, { code, sig }) => NativeModules.hostExit(code, sig));
|
||||
ipcRenderer.on('nativeModuleHostDisconnect', () => NativeModules.hostDisconnect());
|
||||
ipcRenderer.on('log', (e, ...args) => NativeModules.log(...args));
|
||||
|
||||
const handlers = {
|
||||
yubikeys(numYubiKeys) {
|
||||
Events.emit('native-modules-yubikeys', { numYubiKeys });
|
||||
},
|
||||
|
||||
log(...args) {
|
||||
logger.info('Message from host', ...args);
|
||||
},
|
||||
|
||||
result({ callId, result, error }) {
|
||||
const promise = promises[callId];
|
||||
if (promise) {
|
||||
delete promises[callId];
|
||||
if (error) {
|
||||
logger.error('Received an error', promise.cmd, error);
|
||||
promise.reject(error);
|
||||
} else {
|
||||
promise.resolve(result);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
yubiKeyChallengeResponseResult({ callbackId, error, result }) {
|
||||
const callback = ykChalRespCallbacks[callbackId];
|
||||
if (callback) {
|
||||
const willBeCalledAgain = error && error.touchRequested;
|
||||
if (!willBeCalledAgain) {
|
||||
delete ykChalRespCallbacks[callbackId];
|
||||
}
|
||||
callback(error, result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NativeModules = {
|
||||
startHost() {
|
||||
if (hostRunning) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (hostStartPromise) {
|
||||
return hostStartPromise;
|
||||
}
|
||||
|
||||
logger.debug('Starting native module host');
|
||||
|
||||
hostStartPromise = this.callNoWait('start').then(() => {
|
||||
hostStartPromise = undefined;
|
||||
hostRunning = true;
|
||||
|
||||
if (this.usbListenerRunning) {
|
||||
return this.call('startUsbListener');
|
||||
}
|
||||
});
|
||||
|
||||
return hostStartPromise;
|
||||
},
|
||||
|
||||
hostError(e) {
|
||||
logger.error('Host error', e);
|
||||
},
|
||||
|
||||
hostDisconnect() {
|
||||
logger.error('Host disconnected');
|
||||
},
|
||||
|
||||
hostExit(code, sig) {
|
||||
logger.error(`Host exited with code ${code} and signal ${sig}`);
|
||||
|
||||
hostRunning = false;
|
||||
|
||||
const err = new Error('Native module host crashed');
|
||||
|
||||
for (const promise of Object.values(promises)) {
|
||||
promise.reject(err);
|
||||
}
|
||||
promises = {};
|
||||
|
||||
for (const callback of Object.values(ykChalRespCallbacks)) {
|
||||
callback(err);
|
||||
}
|
||||
ykChalRespCallbacks = {};
|
||||
|
||||
if (code !== 0) {
|
||||
this.autoRestartHost();
|
||||
}
|
||||
},
|
||||
|
||||
hostCallback(message) {
|
||||
const { cmd, args } = message;
|
||||
// logger.debug('Callback', cmd, args);
|
||||
if (handlers[cmd]) {
|
||||
handlers[cmd](...args);
|
||||
} else {
|
||||
logger.error('No callback', cmd);
|
||||
}
|
||||
},
|
||||
|
||||
log(name, level, ...args) {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
const logger = new Logger(name);
|
||||
logger[level](...args);
|
||||
},
|
||||
|
||||
autoRestartHost() {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
this.startHost();
|
||||
} catch (e) {
|
||||
logger.error('Native module host failed to auto-restart', e);
|
||||
}
|
||||
}, Timeouts.NativeModuleHostRestartTime);
|
||||
},
|
||||
|
||||
call(cmd, ...args) {
|
||||
return this.startHost().then(() => this.callNoWait(cmd, ...args));
|
||||
},
|
||||
|
||||
callNoWait(cmd, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
callId++;
|
||||
if (callId === Number.MAX_SAFE_INTEGER) {
|
||||
callId = 1;
|
||||
}
|
||||
// logger.debug('Call', cmd, args, callId);
|
||||
promises[callId] = { cmd, resolve, reject };
|
||||
|
||||
ipcRenderer.send('nativeModuleCall', { cmd, args, callId });
|
||||
});
|
||||
},
|
||||
|
||||
startUsbListener() {
|
||||
this.call('startUsbListener');
|
||||
this.usbListenerRunning = true;
|
||||
},
|
||||
|
||||
stopUsbListener() {
|
||||
this.usbListenerRunning = false;
|
||||
if (hostRunning) {
|
||||
this.call('stopUsbListener');
|
||||
}
|
||||
},
|
||||
|
||||
getYubiKeys(config) {
|
||||
return this.call('getYubiKeys', config);
|
||||
},
|
||||
|
||||
yubiKeyChallengeResponse(yubiKey, challenge, slot, callback) {
|
||||
ykChalRespCallbacks[callId] = callback;
|
||||
return this.call('yubiKeyChallengeResponse', yubiKey, challenge, slot, callId);
|
||||
},
|
||||
|
||||
yubiKeyCancelChallengeResponse() {
|
||||
if (hostRunning) {
|
||||
this.call('yubiKeyCancelChallengeResponse');
|
||||
}
|
||||
},
|
||||
|
||||
argon2(password, salt, options) {
|
||||
return this.call('argon2', password, salt, options);
|
||||
},
|
||||
|
||||
hardwareCryptoDeleteKey: async () => {
|
||||
await ipcRenderer.invoke('hardwareCryptoDeleteKey');
|
||||
},
|
||||
|
||||
hardwareEncrypt: async (value) => {
|
||||
const { data, salt } = await ipcRenderer.invoke('hardwareEncrypt', value.dataAndSalt());
|
||||
return new kdbxweb.ProtectedValue(data, salt);
|
||||
},
|
||||
|
||||
hardwareDecrypt: async (value, touchIdPrompt) => {
|
||||
const { data, salt } = await ipcRenderer.invoke(
|
||||
'hardwareDecrypt',
|
||||
value.dataAndSalt(),
|
||||
touchIdPrompt
|
||||
);
|
||||
return new kdbxweb.ProtectedValue(data, salt);
|
||||
},
|
||||
|
||||
kbdGetActiveWindow(options) {
|
||||
return this.call('kbdGetActiveWindow', options);
|
||||
},
|
||||
|
||||
kbdGetActivePid() {
|
||||
return this.call('kbdGetActivePid');
|
||||
},
|
||||
|
||||
kbdShowWindow(win) {
|
||||
return this.call('kbdShowWindow', win);
|
||||
},
|
||||
|
||||
kbdText(str) {
|
||||
return this.call('kbdText', str);
|
||||
},
|
||||
|
||||
kbdTextAsKeys(str, mods) {
|
||||
return this.call('kbdTextAsKeys', str, mods);
|
||||
},
|
||||
|
||||
kbdKeyPress(code, modifiers) {
|
||||
return this.call('kbdKeyPress', code, modifiers);
|
||||
},
|
||||
|
||||
kbdShortcut(code, modifiers) {
|
||||
return this.call('kbdShortcut', code, modifiers);
|
||||
},
|
||||
|
||||
kbdKeyMoveWithModifier(down, modifiers) {
|
||||
return this.call('kbdKeyMoveWithModifier', down, modifiers);
|
||||
},
|
||||
|
||||
kbdKeyPressWithCharacter(character, code, modifiers) {
|
||||
return this.call('kbdKeyPressWithCharacter', character, code, modifiers);
|
||||
},
|
||||
|
||||
kbdEnsureModifierNotPressed() {
|
||||
return this.call('kbdEnsureModifierNotPressed');
|
||||
}
|
||||
};
|
||||
|
||||
global.NativeModules = NativeModules;
|
||||
}
|
||||
|
||||
export { NativeModules };
|
||||
@@ -1,68 +1,147 @@
|
||||
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) {
|
||||
if (settings.theme) {
|
||||
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());
|
||||
}
|
||||
} catch (ex) {}
|
||||
},
|
||||
|
||||
getDefaultTheme() {
|
||||
return 'dark';
|
||||
},
|
||||
|
||||
setTheme(theme) {
|
||||
if (!theme) {
|
||||
if (this.activeTheme) {
|
||||
return;
|
||||
}
|
||||
theme = this.getDefaultTheme();
|
||||
}
|
||||
for (const cls of document.body.classList) {
|
||||
if (/^th-/.test(cls)) {
|
||||
document.body.classList.remove(cls);
|
||||
}
|
||||
}
|
||||
if (AppSettingsModel.autoSwitchTheme) {
|
||||
theme = this.selectDarkOrLightTheme(theme);
|
||||
}
|
||||
document.body.classList.add(this.getThemeClass(theme));
|
||||
const metaThemeColor = document.head.querySelector('meta[name=theme-color]');
|
||||
if (metaThemeColor) {
|
||||
metaThemeColor.content = window.getComputedStyle(document.body).backgroundColor;
|
||||
}
|
||||
this.activeTheme = theme;
|
||||
logger.debug('Theme changed', theme);
|
||||
Events.emit('theme-applied');
|
||||
},
|
||||
|
||||
getThemeClass(theme) {
|
||||
return 'th-' + theme;
|
||||
},
|
||||
|
||||
selectDarkOrLightTheme(theme) {
|
||||
for (const config of this.autoSwitchedThemes) {
|
||||
if (config.light === theme || config.dark === theme) {
|
||||
return ThemeWatcher.dark ? config.dark : config.light;
|
||||
}
|
||||
}
|
||||
return theme;
|
||||
},
|
||||
|
||||
darkModeChanged() {
|
||||
if (AppSettingsModel.autoSwitchTheme) {
|
||||
for (const config of this.autoSwitchedThemes) {
|
||||
if (config.light === this.activeTheme || config.dark === this.activeTheme) {
|
||||
const newTheme = ThemeWatcher.dark ? config.dark : config.light;
|
||||
logger.debug('Setting theme triggered by system settings change', newTheme);
|
||||
this.setTheme(newTheme);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setFontSize(fontSize) {
|
||||
const defaultFontSize = Features.isMobile ? 14 : 12;
|
||||
document.documentElement.style.fontSize = defaultFontSize + (fontSize || 0) * 2 + 'px';
|
||||
@@ -73,7 +152,7 @@ const SettingsManager = {
|
||||
return;
|
||||
}
|
||||
let localeValues;
|
||||
if (loc !== 'en') {
|
||||
if (loc !== 'en-US') {
|
||||
if (this.customLocales[loc]) {
|
||||
localeValues = this.customLocales[loc];
|
||||
} else {
|
||||
@@ -86,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;
|
||||
}
|
||||
|
||||
@@ -5,57 +5,31 @@ import { Logger } from 'util/logger';
|
||||
const logger = new Logger('settings');
|
||||
|
||||
const SettingsStore = {
|
||||
fileName(key) {
|
||||
return `${key}.json`;
|
||||
},
|
||||
|
||||
load(key) {
|
||||
return new Promise(resolve => {
|
||||
if (Launcher) {
|
||||
const settingsFile = Launcher.getUserDataPath(this.fileName(key));
|
||||
Launcher.fileExists(settingsFile, exists => {
|
||||
if (exists) {
|
||||
Launcher.readFile(settingsFile, 'utf8', data => {
|
||||
return this.parseData(key, data, resolve);
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const data = localStorage[StringFormat.camelCase(key)];
|
||||
return this.parseData(key, data, resolve);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
parseData(key, data, resolve) {
|
||||
try {
|
||||
if (data) {
|
||||
return resolve(JSON.parse(data));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Error loading ' + key, e);
|
||||
resolve();
|
||||
let loadPromise;
|
||||
if (Launcher) {
|
||||
loadPromise = Launcher.loadConfig(key);
|
||||
} else {
|
||||
loadPromise = Promise.resolve().then(() => {
|
||||
return localStorage[StringFormat.camelCase(key)];
|
||||
});
|
||||
}
|
||||
return loadPromise
|
||||
.then((data) => (data ? JSON.parse(data) : null))
|
||||
.catch((err) => {
|
||||
logger.error(`Error loading ${key}`, err);
|
||||
});
|
||||
},
|
||||
|
||||
save(key, data) {
|
||||
return new Promise(resolve => {
|
||||
if (Launcher) {
|
||||
const settingsFile = Launcher.getUserDataPath(this.fileName(key));
|
||||
data = JSON.stringify(data);
|
||||
Launcher.writeFile(settingsFile, data, err => {
|
||||
if (err) {
|
||||
logger.error(`Error saving ${key}`, err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
} else if (typeof localStorage !== 'undefined') {
|
||||
if (Launcher) {
|
||||
return Launcher.saveConfig(key, JSON.stringify(data)).catch((err) => {
|
||||
logger.error(`Error saving ${key}`, err);
|
||||
});
|
||||
}
|
||||
return Promise.resolve().then(() => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage[StringFormat.camelCase(key)] = JSON.stringify(data);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,8 +50,7 @@ const Alerts = {
|
||||
Alerts.alertDisplayed = true;
|
||||
const view = new ModalView(config);
|
||||
view.render();
|
||||
view.on('result', (res, check) => {
|
||||
Alerts.alertDisplayed = false;
|
||||
view.once('result', (res, check) => {
|
||||
if (res && config.success) {
|
||||
config.success(res, check);
|
||||
}
|
||||
@@ -50,6 +61,9 @@ const Alerts = {
|
||||
config.complete(res, check);
|
||||
}
|
||||
});
|
||||
view.on('will-close', () => {
|
||||
Alerts.alertDisplayed = false;
|
||||
});
|
||||
return view;
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// Secrets are not really secrets and are supposed to be embedded in the app code according to
|
||||
// the Google's guide: https://developers.google.com/identity/protocols/oauth2#installed
|
||||
// The process results in a client ID and, in some cases, a client secret,
|
||||
// which you embed in the source code of your application.
|
||||
// (In this context, the client secret is obviously not treated as a secret.)
|
||||
|
||||
const DropboxApps = {
|
||||
AppFolder: { id: 'qp7ctun6qt5n9d6', secret: '07s5r4ck1uvlj6a' },
|
||||
FullDropbox: { id: 'eor7hvv6u6oslq9', secret: 'ez04o1iwf6yprq3' }
|
||||
};
|
||||
|
||||
const GDriveApps = {
|
||||
Local: {
|
||||
id: '783608538594-36tkdh8iscrq8t8dq87gghubnhivhjp5.apps.googleusercontent.com',
|
||||
secret: 'yAtyfc9TIQ9GyQgQmo3i0HAP'
|
||||
},
|
||||
Production: {
|
||||
id: '847548101761-koqkji474gp3i2gn3k5omipbfju7pbt1.apps.googleusercontent.com',
|
||||
secret: '42HeSBybXDZjvweotq4o4CkJ'
|
||||
},
|
||||
Desktop: {
|
||||
id: '847548101761-h2pcl2p6m1tssnlqm0vrm33crlveccbr.apps.googleusercontent.com',
|
||||
secret: 'nTSCiqXtUNmURIIdASaC1TJK'
|
||||
}
|
||||
};
|
||||
|
||||
const OneDriveApps = {
|
||||
Local: {
|
||||
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-'
|
||||
}
|
||||
};
|
||||
|
||||
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 };
|
||||
@@ -0,0 +1,99 @@
|
||||
const DefaultAppSettings = {
|
||||
theme: null, // UI theme
|
||||
autoSwitchTheme: false, // automatically switch between light and dark theme
|
||||
locale: null, // user interface language
|
||||
expandGroups: true, // show entries from all subgroups
|
||||
listViewWidth: null, // width of the entry list representation
|
||||
menuViewWidth: null, // width of the left menu
|
||||
tagsViewHeight: null, // tags menu section height
|
||||
autoUpdate: 'install', // auto-update options: "install", "check", ""
|
||||
clipboardSeconds: 0, // number of seconds after which the clipboard will be cleared
|
||||
autoSave: true, // auto-save open files
|
||||
autoSaveInterval: 0, // interval between performing automatic sync, minutes, -1: on every change
|
||||
rememberKeyFiles: 'path', // remember keyfiles selected on the Open screen
|
||||
idleMinutes: 15, // app lock timeout after inactivity, minutes
|
||||
minimizeOnClose: false, // minimise the app instead of closing
|
||||
minimizeOnFieldCopy: false, // minimise the app on copy
|
||||
tableView: false, // view entries as a table instead of list
|
||||
colorfulIcons: false, // use colorful custom icons instead of grayscale
|
||||
useMarkdown: true, // use Markdown in Notes field
|
||||
directAutotype: true, // if only one matching entry is found, select that one automatically
|
||||
autoTypeTitleFilterEnabled: true, // enable the title filtering in auto-type by default
|
||||
titlebarStyle: 'default', // window titlebar style
|
||||
lockOnMinimize: true, // lock the app when it's minimized
|
||||
lockOnCopy: false, // lock the app after a password was copied
|
||||
lockOnAutoType: false, // lock the app after performing auto-type
|
||||
lockOnOsLock: false, // lock the app when the computer is locked
|
||||
helpTipCopyShown: false, // disable the tooltip about copying fields
|
||||
templateHelpShown: false, // disable the tooltip about entry templates
|
||||
skipOpenLocalWarn: false, // disable the warning about opening a local file
|
||||
hideEmptyFields: false, // hide empty fields in entries
|
||||
skipHttpsWarning: false, // disable the non-HTTPS warning
|
||||
demoOpened: false, // hide the demo button inside the More... menu
|
||||
fontSize: 0, // font size: 0, 1, 2
|
||||
tableViewColumns: null, // columns displayed in the table view
|
||||
generatorPresets: null, // presets used in the password generator
|
||||
generatorHidePassword: false, // hide password in the generator
|
||||
cacheConfigSettings: false, // cache config settings and use them if the config can't be loaded
|
||||
allowIframes: false, // allow displaying the app in IFrames
|
||||
useGroupIconForEntries: false, // automatically use group icon when creating new entries
|
||||
enableUsb: true, // enable interaction with USB devices
|
||||
fieldLabelDblClickAutoType: false, // trigger auto-type by doubleclicking field label
|
||||
auditPasswords: true, // enable password audit
|
||||
auditPasswordEntropy: true, // show warnings for weak passwords
|
||||
excludePinsFromAudit: true, // exclude PIN codes from audit
|
||||
checkPasswordsOnHIBP: false, // check passwords on Have I Been Pwned
|
||||
auditPasswordAge: 0, // show warnings about old passwords, number of years, 0 = disabled
|
||||
deviceOwnerAuth: null, // Touch ID: null / 'memory' / 'file'
|
||||
deviceOwnerAuthTimeoutMinutes: 0, // how often master password is required with Touch ID
|
||||
disableOfflineStorage: false, // don't cache loaded files in offline storage
|
||||
shortLivedStorageToken: false, // short-lived sessions in cloud storage providers
|
||||
extensionFocusIfLocked: true, // focus KeeWeb if a browser extension tries to connect while KeeWeb is locked
|
||||
extensionFocusIfEmpty: true, // show the entry selection screen if there's no match found by URL
|
||||
|
||||
yubiKeyShowIcon: true, // show an icon to open OTP codes from YubiKey
|
||||
yubiKeyAutoOpen: false, // auto-load one-time codes when there are open files
|
||||
yubiKeyMatchEntries: true, // show matching one-time codes in entries
|
||||
yubiKeyShowChalResp: true, // show YubiKey challenge-response option
|
||||
yubiKeyRememberChalResp: false, // remember YubiKey challenge-response codes while the app is open
|
||||
yubiKeyStuckWorkaround: false, // enable the workaround for stuck YubiKeys
|
||||
|
||||
canOpen: true, // can select and open new files
|
||||
canOpenDemo: true, // can open a demo file
|
||||
canOpenSettings: true, // can go to settings
|
||||
canCreate: true, // can create new files
|
||||
canImportXml: true, // can import files from XML
|
||||
canImportCsv: true, // can import files from CSV
|
||||
canRemoveLatest: true, // can remove files from the recent file list
|
||||
canExportXml: true, // can export files as XML
|
||||
canExportHtml: true, // can export files as HTML
|
||||
canSaveTo: true, // can save existing files to filesystem
|
||||
canOpenStorage: true, // can open files from cloud storage providers
|
||||
canOpenGenerator: true, // can open password generator
|
||||
canOpenOtpDevice: true, // can open OTP codes from USB tokens
|
||||
|
||||
dropbox: true, // enable Dropbox integration
|
||||
dropboxFolder: null, // default folder path
|
||||
dropboxAppKey: null, // custom Dropbox app key
|
||||
dropboxSecret: null, // custom Dropbox app secret
|
||||
|
||||
webdav: true, // enable WebDAV integration
|
||||
webdavSaveMethod: 'move', // how to save files with WebDAV: "move" or "put"
|
||||
webdavStatReload: false, // WebDAV: reload the file instead of relying on Last-Modified
|
||||
|
||||
gdrive: true, // enable Google Drive integration
|
||||
gdriveClientId: null, // custom Google Drive client id
|
||||
gdriveClientSecret: null, // custom Google Drive client secret
|
||||
|
||||
onedrive: true, // enable OneDrive integration
|
||||
onedriveClientId: null, // custom OneDrive client id
|
||||
onedriveClientSecret: null, // custom OneDrive client secret
|
||||
onedriveTenantId: null, // custom OneDrive tenant id
|
||||
|
||||
msteams: false, // enable Microsoft Teams integration
|
||||
msteamsClientId: null, // custom Microsoft Teams client id
|
||||
msteamsClientSecret: null, // custom Microsoft Teams client secret
|
||||
msteamsTenantId: null // custom Microsoft Teams tenant id
|
||||
};
|
||||
|
||||
export { DefaultAppSettings };
|
||||
@@ -0,0 +1,12 @@
|
||||
const BuiltInFields = [
|
||||
'Title',
|
||||
'Password',
|
||||
'UserName',
|
||||
'URL',
|
||||
'Notes',
|
||||
'TOTP Seed',
|
||||
'TOTP Settings',
|
||||
'_etm_template_uuid'
|
||||
];
|
||||
|
||||
export { BuiltInFields };
|
||||