Compare commits
700 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d38b06196 | ||
|
|
93c2d125e2 | ||
|
|
a1114e5298 | ||
|
|
13122d1ec9 | ||
|
|
6558b33225 | ||
|
|
2a0c388fc8 | ||
|
|
b4ede885cc | ||
|
|
119cefe516 | ||
|
|
5ff9795ef3 | ||
|
|
5600f94962 | ||
|
|
d457c146a5 | ||
|
|
b4cc0925c2 | ||
|
|
eaebe0176e | ||
|
|
d590a9770d | ||
|
|
596379768a | ||
|
|
b535782a0a | ||
|
|
580e1c87ec | ||
|
|
ba6217a718 | ||
|
|
7bbd8ab209 | ||
|
|
fd96bbabc9 | ||
|
|
e52e01005b | ||
|
|
98abb059b1 | ||
|
|
078516ed03 | ||
|
|
6eb6b98996 | ||
|
|
feccb6a3a9 | ||
|
|
1e39bf1cf7 | ||
|
|
c54a6ac79d | ||
|
|
5afdf5450f | ||
|
|
52c366f57b | ||
|
|
2570d69892 | ||
|
|
51f9249b8a | ||
|
|
a18df93cdb | ||
|
|
c3819e4d21 | ||
|
|
c59b067dbf | ||
|
|
d51301dbcb | ||
|
|
d832f24332 | ||
|
|
0e599be671 | ||
|
|
0ce88a7f0e | ||
|
|
6caaa640a2 | ||
|
|
914474f8cd | ||
|
|
a923946c75 | ||
|
|
ef6ed9a4d4 | ||
|
|
dbebe9d043 | ||
|
|
84b8baee1e | ||
|
|
0d6cb83bab | ||
|
|
fbe4bb99ef | ||
|
|
a0a7a3f3a8 | ||
|
|
0008042ce9 | ||
|
|
baf1e0a226 | ||
|
|
ea7ecbb6e1 | ||
|
|
36dee625f1 | ||
|
|
b46099431e | ||
|
|
2f17b02376 | ||
|
|
444fd2d013 | ||
|
|
b53bd4316b | ||
|
|
331ca48d8d | ||
|
|
1552cfa3fa | ||
|
|
9ce7d54ed0 | ||
|
|
81ec256263 | ||
|
|
8db4d3778e | ||
|
|
6852e7197e | ||
|
|
d1b634a9e7 | ||
|
|
877adf564a | ||
|
|
1425fe1e4f | ||
|
|
ca13d88887 | ||
|
|
ae4344189d | ||
|
|
f1ba934bd9 | ||
|
|
5074c0321f | ||
|
|
0fbafda14a | ||
|
|
c646c8cb86 | ||
|
|
9aff6b0317 | ||
|
|
1e38901e79 | ||
|
|
39cdd2b1fb | ||
|
|
573e849631 | ||
|
|
8b6a0f05fd | ||
|
|
924380a970 | ||
|
|
624c71716c | ||
|
|
10f38bd0cf | ||
|
|
a7f14bf241 | ||
|
|
a6309e2224 | ||
|
|
05b87199ac | ||
|
|
2a4fe6ad4f | ||
|
|
53339df0f5 | ||
|
|
b3d9e39a5d | ||
|
|
3ff35ad534 | ||
|
|
06c893a5b1 | ||
|
|
dfb1d31fae | ||
|
|
ec76a2032e | ||
|
|
a8792c3db9 | ||
|
|
3a77257f50 | ||
|
|
8d63aa0cc0 | ||
|
|
4858f93c2d | ||
|
|
8b5b831430 | ||
|
|
c4b85fab1f | ||
|
|
245e755462 | ||
|
|
416b548bd8 | ||
|
|
9f27584f97 | ||
|
|
7fc3d8c2ee | ||
|
|
ab6672faa0 | ||
|
|
193f5fe007 | ||
|
|
3a5da66bf6 | ||
|
|
6ba1de123c | ||
|
|
f57fc3f7c4 | ||
|
|
b1da571fed | ||
|
|
6eb9bc2388 | ||
|
|
6fb48cdec3 | ||
|
|
0176ef97d9 | ||
|
|
a8008b73f2 | ||
|
|
28af87ef03 | ||
|
|
3a519bd710 | ||
|
|
68fd23aaf3 | ||
|
|
3326138a52 | ||
|
|
1b9fb37d05 | ||
|
|
587a97c2b9 | ||
|
|
c466519e48 | ||
|
|
61b0d6af09 | ||
|
|
ede9c01b38 | ||
|
|
81d84b0c2f | ||
|
|
241c1af156 | ||
|
|
8eb2dfae26 | ||
|
|
9ce6e5c90a | ||
|
|
4b4fc10842 | ||
|
|
0355664f6d | ||
|
|
ff331e9f8d | ||
|
|
a9bd930217 | ||
|
|
ea1bdbbc27 | ||
|
|
0ec6670d19 | ||
|
|
216ded32ec | ||
|
|
c546a16729 | ||
|
|
b930a68b70 | ||
|
|
fb9a39ba3d | ||
|
|
5e5a9ff5da | ||
|
|
b4938b9905 | ||
|
|
a4c0f674ee | ||
|
|
b42cad6fe8 | ||
|
|
4f137dc2bd | ||
|
|
3805bbfdf3 | ||
|
|
5c2b6a5d2e | ||
|
|
504b0bfb4f | ||
|
|
84c14ace46 | ||
|
|
8255b2e721 | ||
|
|
59f2591809 | ||
|
|
ed87b25759 | ||
|
|
0724d1ada2 | ||
|
|
9bdfde5a4b | ||
|
|
390555db36 | ||
|
|
a77cf535ed | ||
|
|
fec70c4ae9 | ||
|
|
aeeb285061 | ||
|
|
f3bf8553cc | ||
|
|
14814ae28a | ||
|
|
64a5df6881 | ||
|
|
b54dd584b6 | ||
|
|
0da2e0e0f2 | ||
|
|
33ecc3a0b9 | ||
|
|
ab76ffe804 | ||
|
|
eaaa64d9de | ||
|
|
74b8bd0e04 | ||
|
|
9426b2dd82 | ||
|
|
bb4c27dfe6 | ||
|
|
d9fe3adad2 | ||
|
|
854cf47ce5 | ||
|
|
36b4c94fb7 | ||
|
|
4c57bf178e | ||
|
|
136d372ebc | ||
|
|
387a1c04dd | ||
|
|
fea5e1e043 | ||
|
|
b38db3725a | ||
|
|
a3931f4667 | ||
|
|
a4e088b55e | ||
|
|
14e2057b0e | ||
|
|
73bec0f89f | ||
|
|
cbd78ac8d6 | ||
|
|
af9de90d0f | ||
|
|
0784359876 | ||
|
|
45e29185c8 | ||
|
|
6c096cce4b | ||
|
|
7cc70c8bd9 | ||
|
|
1eb71197bc | ||
|
|
c26d840c5f | ||
|
|
c7b2968b24 | ||
|
|
ef24ee9e75 | ||
|
|
4993ba7de6 | ||
|
|
498ea7f506 | ||
|
|
5f68020b07 | ||
|
|
2a6182bd36 | ||
|
|
3b1334d904 | ||
|
|
b5a3098c65 | ||
|
|
15b269dd45 | ||
|
|
f131c54fe8 | ||
|
|
1affaceb25 | ||
|
|
9a224d6dc0 | ||
|
|
8a01025266 | ||
|
|
dfcd14df1b | ||
|
|
d788786b2b | ||
|
|
9bef49b672 | ||
|
|
f658268d1f | ||
|
|
939a278cca | ||
|
|
c0a74162f0 | ||
|
|
b843fb1503 | ||
|
|
2d65cb7723 | ||
|
|
f8455c82ca | ||
|
|
6df6cd984d | ||
|
|
317ab96700 | ||
|
|
fece6ff727 | ||
|
|
d331a44c2d | ||
|
|
a7b0711d65 | ||
|
|
f2facda3e6 | ||
|
|
5277c7ab46 | ||
|
|
0a8475fbba | ||
|
|
7f03a64bd1 | ||
|
|
2584403447 | ||
|
|
872a649210 | ||
|
|
cd4717a10b | ||
|
|
09de958730 | ||
|
|
00bc6b1bf3 | ||
|
|
4689926b2d | ||
|
|
03755768d9 | ||
|
|
0f08e5b0b7 | ||
|
|
7c2003629d | ||
|
|
f878766863 | ||
|
|
2a412883ae | ||
|
|
4919051817 | ||
|
|
777e6f0b74 | ||
|
|
7c00e398a6 | ||
|
|
37cf8350ea | ||
|
|
c4ad48de5d | ||
|
|
88a87a3346 | ||
|
|
04f4e61110 | ||
|
|
45d70301f6 | ||
|
|
6228f640ac | ||
|
|
d4e1c22d4f | ||
|
|
a503c1600e | ||
|
|
f489dde1b4 | ||
|
|
895c7236b5 | ||
|
|
9be940a835 | ||
|
|
6dc77c8cdb | ||
|
|
9d0ba875a3 | ||
|
|
842bc7e873 | ||
|
|
e1340dc70c | ||
|
|
177ece7c2d | ||
|
|
ee7e6e5b84 | ||
|
|
a37de02a16 | ||
|
|
1a3326377e | ||
|
|
2b5fa48590 | ||
|
|
3fd1c04fe1 | ||
|
|
c7657ae536 | ||
|
|
736f3e8772 | ||
|
|
89edac8cc8 | ||
|
|
00e22faddc | ||
|
|
d3c140ce35 | ||
|
|
d4aeada1c5 | ||
|
|
1775ce7582 | ||
|
|
070eefcba2 | ||
|
|
64b8c81d09 | ||
|
|
bd034712a5 | ||
|
|
4475e03ebd | ||
|
|
a8d42a9de2 | ||
|
|
48e8501dd0 | ||
|
|
65e12297fa | ||
|
|
ccca0f5954 | ||
|
|
1deee342a5 | ||
|
|
7e17daa68b | ||
|
|
560a4f1d63 | ||
|
|
0caf58648f | ||
|
|
fcdeb542ab | ||
|
|
1c0c670b11 | ||
|
|
3079040e6b | ||
|
|
3dd2ed35f3 | ||
|
|
30a4a6e900 | ||
|
|
45f5b5b4c5 | ||
|
|
3ee111122b | ||
|
|
776c0f2791 | ||
|
|
9870627423 | ||
|
|
88294e2d87 | ||
|
|
555c4a74b6 | ||
|
|
3c0d5f477a | ||
|
|
4baf9dc859 | ||
|
|
8f6c2ec794 | ||
|
|
724de4563e | ||
|
|
3eac42e55f | ||
|
|
104b4bfc70 | ||
|
|
ee39fd796e | ||
|
|
11b298680c | ||
|
|
ea462cb1e6 | ||
|
|
39f1a4f480 | ||
|
|
7d60207bd0 | ||
|
|
a22ff3b766 | ||
|
|
ca30637645 | ||
|
|
cecda2f2c0 | ||
|
|
d1f8cd0a6c | ||
|
|
7bbe0762d7 | ||
|
|
f7dd4a7c24 | ||
|
|
34b744b5ea | ||
|
|
c89454067c | ||
|
|
544689d72b | ||
|
|
9c3ac48755 | ||
|
|
8b12cfa96f | ||
|
|
a0ba67febd | ||
|
|
039a902252 | ||
|
|
7fdc9949d6 | ||
|
|
b5b9e2b4a6 | ||
|
|
2ed8adaef6 | ||
|
|
8b1cd53ff1 | ||
|
|
9bb6c04464 | ||
|
|
10e779b77d | ||
|
|
a11aee294d | ||
|
|
9e45f3fa95 | ||
|
|
0cfbc907b7 | ||
|
|
81cd39c84f | ||
|
|
e4bfb00de1 | ||
|
|
ae37925598 | ||
|
|
73b997a9de | ||
|
|
6924417599 | ||
|
|
86c2ba0220 | ||
|
|
c64a2611b7 | ||
|
|
848d3bcf69 | ||
|
|
781dbe551b | ||
|
|
e5cf0109cd | ||
|
|
aac546d0db | ||
|
|
3fe132c452 | ||
|
|
f58bcc9bd7 | ||
|
|
f1d28b1c6d | ||
|
|
32b0ed8cd8 | ||
|
|
6b53ff7950 | ||
|
|
8c8a7fd120 | ||
|
|
9fd83eba3a | ||
|
|
9fd3cf5ffd | ||
|
|
b7b31e721d | ||
|
|
bb45d37a54 | ||
|
|
66fa10af51 | ||
|
|
e17ad463ec | ||
|
|
62cd40648b | ||
|
|
ffa23dad20 | ||
|
|
b304cdbd9c | ||
|
|
4493b1267b | ||
|
|
41c1edd940 | ||
|
|
6c8b151ae3 | ||
|
|
1105cde711 | ||
|
|
7805cd9428 | ||
|
|
4e570b53d2 | ||
|
|
e014ffd24c | ||
|
|
ef25ee7607 | ||
|
|
215a0ce262 | ||
|
|
8bd22d0942 | ||
|
|
ca95150105 | ||
|
|
30ef1cc2ae | ||
|
|
402992f385 | ||
|
|
32a229aa98 | ||
|
|
ead4f742ba | ||
|
|
263f71a38e | ||
|
|
731be08600 | ||
|
|
55b4bde204 | ||
|
|
ee7ce3d9c3 | ||
|
|
e2c8f7992c | ||
|
|
8a55c29f87 | ||
|
|
343a2b47e1 | ||
|
|
619ce0f620 | ||
|
|
9fb01bc12f | ||
|
|
daee20fdd8 | ||
|
|
ec6043935b | ||
|
|
091ddba965 | ||
|
|
86f9693dc6 | ||
|
|
5290e4997a | ||
|
|
e43cc799f0 | ||
|
|
8c137fb700 | ||
|
|
516b6e71ec | ||
|
|
a09a23e355 | ||
|
|
8dd2ab61df | ||
|
|
427a8aa628 | ||
|
|
64a8abf375 | ||
|
|
8815d0874f | ||
|
|
7efbdfd2a3 | ||
|
|
ca6c1778e5 | ||
|
|
5c239adb57 | ||
|
|
76149d65ef | ||
|
|
e76e91a8ee | ||
|
|
cf83b3ad38 | ||
|
|
23719a232f | ||
|
|
ce8e0b70e4 | ||
|
|
13e75be91b | ||
|
|
34d978a972 | ||
|
|
b20a2ba861 | ||
|
|
8d39a4cc59 | ||
|
|
58c6c2246b | ||
|
|
37f6a3f970 | ||
|
|
34f913c084 | ||
|
|
bab98e3090 | ||
|
|
e15e1d2929 | ||
|
|
1c316688a1 | ||
|
|
2e0e768f1d | ||
|
|
a9f3d9145c | ||
|
|
41af1ab738 | ||
|
|
ea343b7cce | ||
|
|
1a824770e1 | ||
|
|
96794feac8 | ||
|
|
607264dea9 | ||
|
|
6087bc550c | ||
|
|
5418cf2092 | ||
|
|
74274b5cb1 | ||
|
|
c0cd8b16d0 | ||
|
|
f531718a56 | ||
|
|
cb3625e875 | ||
|
|
ca913ba29f | ||
|
|
b7718be42d | ||
|
|
7275d67bc7 | ||
|
|
af83896d92 | ||
|
|
d47fc2b1cc | ||
|
|
b896f70de6 | ||
|
|
2428cc5823 | ||
|
|
00d409d30e | ||
|
|
3ff3ce93aa | ||
|
|
f63fc67d23 | ||
|
|
46c005a02b | ||
|
|
cb52804450 | ||
|
|
8df095817f | ||
|
|
6de7cf4c2c | ||
|
|
1079011fc0 | ||
|
|
f64860441e | ||
|
|
f410d4ec58 | ||
|
|
389c669728 | ||
|
|
10af29a5d7 | ||
|
|
ebbebaab38 | ||
|
|
eac9960a51 | ||
|
|
24fd5075d6 | ||
|
|
e6beeb9baf | ||
|
|
246aaa4dcf | ||
|
|
61263635d9 | ||
|
|
8d9ab037b3 | ||
|
|
460c5fd4a5 | ||
|
|
58fd151393 | ||
|
|
3d7b2394be | ||
|
|
0a4498b22e | ||
|
|
a6d4761a85 | ||
|
|
6afe0efa37 | ||
|
|
d4d9c4242f | ||
|
|
d5853245f7 | ||
|
|
8fbc7eea6a | ||
|
|
f6c8d18495 | ||
|
|
b48bf7eb0b | ||
|
|
5ae737b9bb | ||
|
|
9530965f8c | ||
|
|
7f5f8cb747 | ||
|
|
d2ae93b540 | ||
|
|
0541faa826 | ||
|
|
c17aa40852 | ||
|
|
396d307913 | ||
|
|
8f5f563973 | ||
|
|
9c2d65ffe4 | ||
|
|
fd4acd4298 | ||
|
|
133585f8cd | ||
|
|
ea3e03d212 | ||
|
|
55fbfcb57b | ||
|
|
3c4a20a85f | ||
|
|
c19a5f03e9 | ||
|
|
de620fe602 | ||
|
|
c806721298 | ||
|
|
50dfe93133 | ||
|
|
04c0bb3a00 | ||
|
|
cf8b8ce721 | ||
|
|
58bf538df9 | ||
|
|
97c50c197b | ||
|
|
1d11ee674f | ||
|
|
f7ab9f42d5 | ||
|
|
095dc0118c | ||
|
|
4fc5f5cc17 | ||
|
|
49d91b0551 | ||
|
|
e1d759c4d5 | ||
|
|
5213ff138c | ||
|
|
dc0507e3b8 | ||
|
|
3702a4fb3f | ||
|
|
2832d44362 | ||
|
|
1c42e26a0e | ||
|
|
08b5eea771 | ||
|
|
406b06f284 | ||
|
|
2d8bd7498f | ||
|
|
22db3eca53 | ||
|
|
f85f9036a4 | ||
|
|
46b7899bc7 | ||
|
|
678098d874 | ||
|
|
30ad9cd477 | ||
|
|
ba2d31cd68 | ||
|
|
a4e36d399c | ||
|
|
75c5cfe91e | ||
|
|
320981df67 | ||
|
|
cd3c101246 | ||
|
|
40f16dd2d6 | ||
|
|
6ab019c161 | ||
|
|
2b2f0a524e | ||
|
|
78c4e5c617 | ||
|
|
9ab605e699 | ||
|
|
c02dce7468 | ||
|
|
8c52e042f7 | ||
|
|
bfccc82057 | ||
|
|
75e111cecc | ||
|
|
39d7625882 | ||
|
|
ac3855a4b5 | ||
|
|
03eeb8e2b9 | ||
|
|
977cf9aafb | ||
|
|
dbeae227de | ||
|
|
45f554adf6 | ||
|
|
c145b2d8fd | ||
|
|
24338c2b5d | ||
|
|
bc017ecdb5 | ||
|
|
268a246b19 | ||
|
|
ca02831c08 | ||
|
|
1b4ec3d0e8 | ||
|
|
a1977d9265 | ||
|
|
5c76bd8183 | ||
|
|
7899ae023a | ||
|
|
5784621fec | ||
|
|
cecab4a3c4 | ||
|
|
5e970061b5 | ||
|
|
c2a15b64c7 | ||
|
|
9c280b49d6 | ||
|
|
3568418939 | ||
|
|
e036fd9a1e | ||
|
|
5002bef9f3 | ||
|
|
f1639d4123 | ||
|
|
c92006e638 | ||
|
|
22edf2b42e | ||
|
|
384aad1365 | ||
|
|
50bc1a2133 | ||
|
|
aee25d69a5 | ||
|
|
db913bd385 | ||
|
|
c2be4ebf02 | ||
|
|
f2c018409e | ||
|
|
da6673667e | ||
|
|
9ab5b4f18f | ||
|
|
c00b8e879a | ||
|
|
fdf9e4127a | ||
|
|
e2e6c0f436 | ||
|
|
47df2b4409 | ||
|
|
f58e1c5d62 | ||
|
|
7b60c57653 | ||
|
|
61655ec608 | ||
|
|
e2dcbb6991 | ||
|
|
b4732b0523 | ||
|
|
b6b63c9916 | ||
|
|
48e436f002 | ||
|
|
b943a95536 | ||
|
|
ced0ef4da3 | ||
|
|
a9b5749656 | ||
|
|
5efd9f14dd | ||
|
|
b0e5ac83f2 | ||
|
|
7c1b97b306 | ||
|
|
23a80592e9 | ||
|
|
845e500844 | ||
|
|
0564438c9c | ||
|
|
7ef7dec03c | ||
|
|
03f8813348 | ||
|
|
1301db4935 | ||
|
|
c431d26bb4 | ||
|
|
60c8c8841e | ||
|
|
020fa0d3db | ||
|
|
2223877c6a | ||
|
|
00981554be | ||
|
|
aaee6b0fde | ||
|
|
ccd8445e97 | ||
|
|
b84547b0b6 | ||
|
|
c08a8a8a21 | ||
|
|
4c43e46f68 | ||
|
|
1d8b79023e | ||
|
|
1eadd02354 | ||
|
|
3163972b1e | ||
|
|
5787f9ae0d | ||
|
|
7b26f6654a | ||
|
|
41b19db3f0 | ||
|
|
1b7f41aab3 | ||
|
|
65430abe20 | ||
|
|
9ed39d9c95 | ||
|
|
06e334fa89 | ||
|
|
11c3b2e2b4 | ||
|
|
ce2b1ef3d4 | ||
|
|
1f0a5ed086 | ||
|
|
08b7a18080 | ||
|
|
423a0d74b1 | ||
|
|
39319e5b3d | ||
|
|
d98bbfcdfe | ||
|
|
d149ea0d42 | ||
|
|
21c71c6ed2 | ||
|
|
5d06ee4618 | ||
|
|
ebfc1d89f6 | ||
|
|
d3de159915 | ||
|
|
81d5cb351b | ||
|
|
4c0341e232 | ||
|
|
6e54e8106c | ||
|
|
459520e8ed | ||
|
|
e724ccb927 | ||
|
|
5c5bd41e37 | ||
|
|
1a47825aa6 | ||
|
|
b67f0a6e53 | ||
|
|
e8959f6af3 | ||
|
|
be178da5ca | ||
|
|
e027dcc433 | ||
|
|
79209144d1 | ||
|
|
e685636ebc | ||
|
|
6932ea84cf | ||
|
|
1ba8d37afc | ||
|
|
58f64e65b0 | ||
|
|
75605a3fe9 | ||
|
|
2218aaca75 | ||
|
|
557d79f921 | ||
|
|
12a4cb093b | ||
|
|
40d6ecbdd7 | ||
|
|
edc7993289 | ||
|
|
d7d37c4cce | ||
|
|
551f786464 | ||
|
|
046f432765 | ||
|
|
b1e5386491 | ||
|
|
4009881ad8 | ||
|
|
f8e6c327f1 | ||
|
|
1630459a21 | ||
|
|
d4ec4dd143 | ||
|
|
3a486600b4 | ||
|
|
250f4195d1 | ||
|
|
9b3cd60bed | ||
|
|
5607751671 | ||
|
|
80bcc191d1 | ||
|
|
311089a62e | ||
|
|
21e3a407dd | ||
|
|
f3446599ea | ||
|
|
5d799ecdf4 | ||
|
|
6c2ecad220 | ||
|
|
f10de7af90 | ||
|
|
f07d2c07b9 | ||
|
|
8954f05b6a | ||
|
|
4656cf4cbc | ||
|
|
3decbe7863 | ||
|
|
1eebcd6661 | ||
|
|
d5545b4e82 | ||
|
|
a1a3d450b7 | ||
|
|
ec331a549f | ||
|
|
365a57ea3c | ||
|
|
0f81f5f3ac | ||
|
|
356e156ab2 | ||
|
|
6607facb6e | ||
|
|
1247609af9 | ||
|
|
c71ddade63 | ||
|
|
2318714e25 | ||
|
|
6de79a0bd4 | ||
|
|
f8f5d3dd7d | ||
|
|
abbe1d2d99 | ||
|
|
c6ac4e7452 | ||
|
|
dab490dc27 | ||
|
|
09197da959 | ||
|
|
dd2b9a4309 | ||
|
|
eed1b66884 | ||
|
|
32877dcac0 | ||
|
|
1388829286 | ||
|
|
7123403e4f | ||
|
|
774771cd8e | ||
|
|
8f88a5513a | ||
|
|
5b76b887ac | ||
|
|
42e7b713f3 | ||
|
|
5717bdd427 | ||
|
|
9f2ea7b0f9 | ||
|
|
9cb4408db5 | ||
|
|
8e8d4f4fe1 | ||
|
|
2fa077aec7 | ||
|
|
481a047576 | ||
|
|
c24ee5753e | ||
|
|
22b599e314 | ||
|
|
5e53b1a131 | ||
|
|
6780f29e74 | ||
|
|
a706866844 | ||
|
|
4ee44d4830 | ||
|
|
76bf772568 | ||
|
|
e4f594c2d3 | ||
|
|
246c144513 | ||
|
|
bc154434dd | ||
|
|
681bea0839 | ||
|
|
55f61390cc | ||
|
|
e705ff0f6c | ||
|
|
e00003f68a | ||
|
|
3024d7f50c | ||
|
|
8325226046 | ||
|
|
0dc5a02e4a | ||
|
|
7796d60c1d | ||
|
|
4f3d3cff7c | ||
|
|
29f01c090a | ||
|
|
ca5932ac6f | ||
|
|
ab9a810181 | ||
|
|
bd300c44d3 | ||
|
|
a3f2e24647 | ||
|
|
34c64f14b4 | ||
|
|
e4db87635f | ||
|
|
fdc5b0fa82 | ||
|
|
d9b41f4e5c | ||
|
|
fa8dc24d0e | ||
|
|
265065c1d9 | ||
|
|
a82af69845 | ||
|
|
4576a6ef07 | ||
|
|
55f3dd3c04 | ||
|
|
aef471b10a | ||
|
|
4744975840 | ||
|
|
a80db8a7f7 | ||
|
|
7a5dd0ebeb | ||
|
|
822a05fedb | ||
|
|
d729810a96 |
2
.gitignore
vendored
@@ -7,6 +7,8 @@ setup/cli/modules/importer
|
||||
.vagrant
|
||||
._.DS_Store
|
||||
Vagrantfile
|
||||
# VS Code workspace files
|
||||
*.code-workspace
|
||||
|
||||
# Staging directory used for packaging script
|
||||
stage
|
||||
|
||||
70
README.md
@@ -22,13 +22,14 @@ easy to setup and use. The best part is, it's completely free.
|
||||
Requirements
|
||||
------------
|
||||
* HTTP server running Microsoft® IIS or Apache
|
||||
* PHP version 8.0
|
||||
* PHP version 8.2 - 8.4 (8.4 recommended)
|
||||
* mysqli extension for PHP
|
||||
* MySQL database version 5.5
|
||||
* MySQL database version 5.5 (or greater)
|
||||
|
||||
### Recommendations
|
||||
* gd, gettext, imap, json, mbstring, and xml extensions for PHP
|
||||
* APC module enabled and configured for PHP
|
||||
* ctype, fileinfo, gd, gettext, iconv, imap, intl, json, mbstring,
|
||||
Zend OPcache, phar, xml, xml-dom, and zip extensions for PHP
|
||||
* APCu module enabled and configured for PHP
|
||||
|
||||
Deployment
|
||||
----------
|
||||
@@ -58,33 +59,9 @@ Upgrading
|
||||
---------
|
||||
osTicket supports upgrading from 1.6-rc1 and later versions. As with any
|
||||
upgrade, strongly consider a backup of your attachment files, database, and
|
||||
osTicket codebase before embarking on an upgrade.
|
||||
|
||||
To trigger the update process, fetch the osTicket tarball from either
|
||||
the osTicket [github](http://github.com/osTicket/osTicket/releases) page
|
||||
or from the [osTicket website](https://osticket.com). Extract the tarball
|
||||
into the folder of your osTicket codebase. This can also be accomplished
|
||||
with the zip file, and a FTP client can of course be used to upload the new
|
||||
source code to your server.
|
||||
|
||||
Any way you choose your adventure, when you have your codebase upgraded to
|
||||
osTicket-1.7, visit the /scp page of you ticketing system. The upgrader will
|
||||
be presented and will walk you through the rest of the process. (The couple
|
||||
clicks needed to go through the process are pretty boring to describe).
|
||||
|
||||
### Upgrading from v1.6
|
||||
**WARNING**: If you are upgrading from osTicket 1.6, please ensure that all
|
||||
your files in your upload folder are both readable and writable to your
|
||||
http server software. Unreadable files will not be migrated to the
|
||||
database during the upgrade and will be effectively lost.
|
||||
|
||||
After upgrading, we recommend migrating your attachments to the database or
|
||||
to the new filesystem plugin. Use the `file` command-line applet to perform
|
||||
the migration.
|
||||
|
||||
php manage.php file migrate --backend=6 --to=D
|
||||
|
||||
View the UPGRADING.txt file for other todo items to complete your upgrade.
|
||||
osTicket codebase before embarking on an upgrade. Please review our [Upgrade
|
||||
Guide](https://docs.osticket.com/en/latest/Getting%20Started/Upgrade%20and%20Migration.html)
|
||||
or the [UPGRADING.txt file](UPGRADING.txt) for upgrade instructions.
|
||||
|
||||
Help
|
||||
----
|
||||
@@ -101,7 +78,7 @@ the feature is published in your fork, send a pull request to begin the
|
||||
conversation of integrating your new feature into osTicket.
|
||||
|
||||
### Localization
|
||||
[](http://i18n.osticket.com/project/osticket-official)
|
||||
[](https://crowdin.com/project/osticket-official)
|
||||
|
||||
The interface for osTicket is now completely translatable. Language packs
|
||||
are available on the [download page](https://osticket.com/download). If you
|
||||
@@ -122,18 +99,19 @@ file for the gory details of the General Public License.
|
||||
|
||||
osTicket is supported by several magical open source projects including:
|
||||
|
||||
* [Font-Awesome](http://fortawesome.github.com/Font-Awesome/)
|
||||
* [HTMLawed](http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed)
|
||||
* [jQuery dropdown](http://labs.abeautifulsite.net/jquery-dropdown/)
|
||||
* [jsTimezoneDetect](http://pellepim.bitbucket.org/jstz/)
|
||||
* [mPDF](http://www.mpdf1.com/)
|
||||
* [PasswordHash](http://www.openwall.com/phpass/)
|
||||
* [PEAR](http://pear.php.net/package/PEAR)
|
||||
* [PEAR/Auth_SASL](http://pear.php.net/package/Auth_SASL)
|
||||
* [PEAR/Mail](http://pear.php.net/package/mail)
|
||||
* [PEAR/Net_SMTP](http://pear.php.net/package/Net_SMTP)
|
||||
* [PEAR/Net_Socket](http://pear.php.net/package/Net_Socket)
|
||||
* [PEAR/Serivces_JSON](http://pear.php.net/package/Services_JSON)
|
||||
* [Font-Awesome](https://fontawesome.com/)
|
||||
* [HTMLawed](https://www.bioinformatics.org/phplabware/internal_utilities/htmLawed)
|
||||
* [jQuery dropdown](https://labs.abeautifulsite.net/jquery-dropdown/) (Project Deleted)
|
||||
* [jsTimezoneDetect](https://pellepim.bitbucket.org/jstz/)
|
||||
* [laminas-mail](https://github.com/laminas/laminas-mail)
|
||||
* [mPDF](https://github.com/mpdf/mpdf)
|
||||
* [PasswordHash](https://www.openwall.com/phpass/)
|
||||
* [PEAR](https://pear.php.net/package/PEAR)
|
||||
* [PEAR/Auth_SASL](https://pear.php.net/package/Auth_SASL)
|
||||
* [PEAR/Mail](https://pear.php.net/package/mail)
|
||||
* [PEAR/Net_SMTP](https://pear.php.net/package/Net_SMTP)
|
||||
* [PEAR/Net_Socket](https://pear.php.net/package/Net_Socket)
|
||||
* [PEAR/Serivces_JSON](https://pear.php.net/package/Services_JSON)
|
||||
* [php-gettext](https://launchpad.net/php-gettext/)
|
||||
* [phpseclib](http://phpseclib.sourceforge.net/)
|
||||
* [Spyc](http://github.com/mustangostang/spyc)
|
||||
* [phpseclib](https://phpseclib.sourceforge.net/)
|
||||
* [Spyc](https://github.com/mustangostang/spyc)
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
Welcome to osTicket 1.7
|
||||
=======================
|
||||
Upgrading osTicket
|
||||
==================
|
||||
osTicket supports upgrading from 1.6-rc1 and later versions. As with any
|
||||
upgrade, strongly consider a backup of your attachment files, database, and
|
||||
osTicket codebase before embarking on an upgrade.
|
||||
|
||||
To trigger the update process, fetch the osTicket tarball from either
|
||||
the osTicket [GitHub](https://github.com/osTicket/osTicket/releases) page
|
||||
or from the [osTicket website](https://osticket.com/download). Extract the tarball
|
||||
into the folder of your osTicket codebase. This can also be accomplished
|
||||
with the zip file, and a FTP client can of course be used to upload the new
|
||||
source code to your server.
|
||||
|
||||
Any way you choose your adventure, when you have your codebase upgraded,
|
||||
visit the /scp page of you ticketing system. The upgrader will
|
||||
be presented and will walk you through the rest of the process. (The couple
|
||||
clicks needed to go through the process are pretty boring to describe).
|
||||
|
||||
### Upgrading from v1.6
|
||||
**WARNING**: If you are upgrading from osTicket 1.6, please ensure that all
|
||||
your files in your upload folder are both readable and writable to your
|
||||
http server software. Unreadable files will not be migrated to the
|
||||
database during the upgrade and will be effectively lost.
|
||||
|
||||
After upgrading, we recommend migrating your attachments to the database or
|
||||
to the new filesystem plugin. Use the `file` command-line applet to perform
|
||||
the migration.
|
||||
|
||||
php manage.php file migrate --backend=6 --to=D
|
||||
|
||||
Some tasks are better left to a system administrator rather than a mindless
|
||||
upgrade script. These are those remaining things that we'd rather you take
|
||||
care of:
|
||||
|
||||
343
WHATSNEW.md
@@ -1,3 +1,274 @@
|
||||
osTicket v1.18.4
|
||||
================
|
||||
### Security
|
||||
* security: Latest Patches 06/2026 (52c366f, 5afdf54, c54a6ac, 1e39bf1, feccb6a, 6eb6b98, 078516e, 98abb05, e52e010, fd96bba, 7bbd8ab, ba6217a, 580e1c8, b535782, 5963797, d590a97, eaebe01, b4cc092, d457c14, 5600f94, 5ff9795, 119cefe, b4ede88, 2a0c388, 6558b33)
|
||||
|
||||
osTicket v1.18.3
|
||||
================
|
||||
### Enhancements
|
||||
* mPDF: Upgrade To v8.2.7 (39cdd2b1)
|
||||
* htmLawed: Upgrade To 1.2.15 (877adf56)
|
||||
|
||||
### Security
|
||||
* security: Latest Patches 01/2026 (c646c8cb, d1b634a9, 6852e719, 8db4d377, 1552cfa3, d832f243)
|
||||
|
||||
osTicket v1.18.2
|
||||
================
|
||||
### Improvements
|
||||
* OAuth2: Show Scopes and Enforce Strict Mode (a77cf535, 390555db)
|
||||
|
||||
osTicket v1.18.1
|
||||
================
|
||||
### Improvements
|
||||
* Update upgrade.php (9fd83eba)
|
||||
* Update upgrade.inc.php (8c8a7fd1)
|
||||
* update: PHP Requirements 1.18.x (1c0c670b)
|
||||
* CLI: Make sure manage util can be executed via CLI (0caf5864)
|
||||
* Update raphael-min.js to 2.3.0 (d4aeada1)
|
||||
|
||||
osTicket v1.18
|
||||
==============
|
||||
### Enhancements
|
||||
* update: Laminas-Mail (66fa10af, bb45d37a)
|
||||
|
||||
### Improvements
|
||||
* Database: Change Plugin Name to varchar(255) (aac546d0)
|
||||
* update: MAJOR_VERSION (3eac42e5)
|
||||
|
||||
osTicket v1.17.8
|
||||
================
|
||||
### Security
|
||||
* security: Latest Patches 06/2026 (52c366f, 5afdf54, c54a6ac, 1e39bf1, feccb6a, 6eb6b98, 078516e, 98abb05, e52e010, fd96bba, 7bbd8ab, ba6217a, 580e1c8, b535782, 5963797, d590a97, eaebe01, b4cc092, d457c14, 5600f94, 5ff9795, 119cefe, b4ede88, 2a0c388, 6558b33)
|
||||
|
||||
osTicket v1.17.7
|
||||
================
|
||||
### Enhancements
|
||||
* mPDF: Upgrade To v8.2.7 (39cdd2b1)
|
||||
* htmLawed: Upgrade To 1.2.15 (877adf56)
|
||||
|
||||
### Improvements
|
||||
* join cdata table earlier (81ec2562)
|
||||
* issue: Content Management Errors (2a4fe6ad)
|
||||
* mail: SMTPUTF8 Backwards Compatibility (05b87199)
|
||||
* issue: SVG MIME Type (a6309e22)
|
||||
* Fix Banlist permission check (a7f14bf2)
|
||||
* issue: POP OAuth2 Error REGEX (10f38bd0)
|
||||
* issue: Kazakh Flag CSS (624c7171)
|
||||
* Fixing Argentinian flag (924380a9)
|
||||
* Update class.setup.php (8b6a0f05)
|
||||
* Fixes for Implicitly marking parameters as nullable (573e8496, ca13d888)
|
||||
* issue: Date Column Interval (1e38901e)
|
||||
* issue: PGP + S/MIME Mail Parsing (9aff6b03)
|
||||
* issue: Trim User Name (0fbafda1)
|
||||
* issue: Microsoft UTF-8 BOM (5074c032)
|
||||
* issue: Empty Dept Error On Topic Update (f1ba934b)
|
||||
* Fix typo in 'Add New Instance' title (ae434418)
|
||||
* Update class.file.php (1425fe1e)
|
||||
* issue: Regex OR Operator (331ca48d)
|
||||
* Revert "issue: Guest Session TTL" (c3819e4d)
|
||||
|
||||
### Security
|
||||
* security: Latest Patches 01/2026 (c646c8cb, d1b634a9, 6852e719, 8db4d377, 1552cfa3, d832f243)
|
||||
|
||||
osTicket v1.17.6
|
||||
================
|
||||
### Improvements
|
||||
* Fix PR #6283: DB column value of "local backend" is 'client' and not 'local' (7805cd94)
|
||||
* i18n: Tasks Department ID Missing (00bc6b1b)
|
||||
* issue: Checkbox Template Variable (09de9587)
|
||||
* Fixed mailfetch error message (cd4717a1)
|
||||
* issue: fix square characters being printed when printing tickets that's using languages like Thai (872a6492, 25844034)
|
||||
* issue: Position Styling.. Again (7f03a64b, b843fb15)
|
||||
* mysqli: Multi-Host Failure (0a8475fb)
|
||||
* issue: Task Last Respondent (5277c7ab)
|
||||
* i18n: Schedules getDays() (f2facda3)
|
||||
* issue: Referred Closed Tickets (a7b0711d)
|
||||
* php: Update Prerequisites (d331a44c)
|
||||
* i18n: Lang URL Param (fece6ff7)
|
||||
* mysql: Update Minimum Version (317ab967)
|
||||
* Fix ajax path for AddRemoteCollaborator (6df6cd98)
|
||||
* Fix staff mobile field class (f8455c82)
|
||||
* patches: Latest 05/28/2024 (2d65cb77, 36b4c94f, 854cf47c, d9fe3ada)
|
||||
* upgrade: Laminas-Mail (2.25.1) (c0a74162, 939a278c, f658268d)
|
||||
* hotfix: Fix getRawEmail() (c26d840c, 1eb71197)
|
||||
* issue: Linked Icon annotation (6c096cce)
|
||||
* fix(email): fix case sensitivity in "Action" header retrieval (07843598)
|
||||
* Upgrade mimeDecode.php from v1.48 to v1.5.6 2016-08-28 (cbd78ac8)
|
||||
* mailer: sendmail() Failure (14e2057b)
|
||||
* php: 8.3 Support (a3931f46, b38db372, fea5e1e0, 387a1c04, 136d372e)
|
||||
* issue: Ticket Relations (9426b2dd)
|
||||
* issue: Basic Search Selector (74b8bd0e)
|
||||
* mail: Reapply Memory Optimization (eaaa64d9, ab76ffe8)
|
||||
* php: iconv Recommendation (33ecc3a0)
|
||||
* Ignore VS Code Workspace Files (0da2e0e0)
|
||||
* issue: Email Remote Backend Name (b54dd584)
|
||||
* issue: Safari Response Content Disposition (64a5df68)
|
||||
* api: DueDate and Other Errors (14814ae2)
|
||||
* issue: Multiselect List Export (f3bf8553)
|
||||
* issue: Image Annotation (aeeb2850)
|
||||
* user: List Import (fec70c4a)
|
||||
* issue: Latest SQL Warnings (9bdfde5a)
|
||||
* fields: Variable Name Validation (0724d1ad)
|
||||
* issue: Transfer Empty Comments Var (ed87b257)
|
||||
* update: Composer symfony/process (59f25918)
|
||||
* issue: Client ACL Staff File Download (8255b2e7)
|
||||
* update: PHP Deprecations & Warnings (84c14ace)
|
||||
* issue: User Lookup Umlaut (504b0bfb)
|
||||
* issue: Department Field User Import (5c2b6a5d)
|
||||
* Fix force_https auto-enabling on settings page (3805bbfd)
|
||||
* queue: getTotal Incorrect Counts (4f137dc2)
|
||||
* issue: Referred Tickets Incorrect Queue Counts (b42cad6f)
|
||||
* php: 8.4 Support (a4c0f674, b4938b99, 5e5a9ff5, fb9a39ba)
|
||||
* issue: Canned Response Access (b930a68b)
|
||||
* issue: Excessive Fetching Errors (c546a167)
|
||||
* issue: Plain-text Base64 (216ded32)
|
||||
* i18n: Crowdin API v2 (0ec6670d)
|
||||
* issue: iFrame Logins (3a5da66b)
|
||||
* issue: mimeDecode .eml Attachments (7fc3d8c2)
|
||||
* patches: Latest 11/18/2024 (416b548b, 245e7554)
|
||||
* Revert part of 0784359 commit (ec76a203)
|
||||
|
||||
### Security
|
||||
* security: Latest Vulns 01/2025 (193f5fe0, ab6672fa)
|
||||
|
||||
osTicket v1.17.5
|
||||
================
|
||||
### Improvements
|
||||
* update: README.md PHP & Laminas (4baf9dc8, 555c4a74, 88294e2d, 30a4a6e9)
|
||||
* issue: putenv() Disabled (3c0d5f47)
|
||||
* i18n: Update Signing (98706274, 776c0f27)
|
||||
* i18n: Flags Position & Codes (3ee11112, 45f5b5b4)
|
||||
* mailer: Undefined SmtpAccount::getName() (560a4f1d)
|
||||
* issue: SystemSessionHandler (7e17daa6)
|
||||
* issue: Status List Overflow (1deee342)
|
||||
* faq: Category Notes Images (ccca0f59)
|
||||
* patches: Latest 10/03/2023 (65e12297, 48e8501d, a8d42a9d, 4475e03e, bd034712, 64b8c81d, 070eefcb, 1775ce75)
|
||||
* mail: NoValidateCert (d3c140ce)
|
||||
* issue: Choice/Selection Field Searches (00e22fad)
|
||||
* stubs: Add stubs to pass lint test (895c7236)
|
||||
* issue: Canned Response Inline Images (a503c160)
|
||||
* format: Viewable Images Bug (2a412883)
|
||||
|
||||
### Security
|
||||
* issue: CSP Headers (6228f640)
|
||||
* security: Latest Vulns 09/2023 (04f4e611, 88a87a33, c4ad48de, 37cf8350)
|
||||
* issue: User Account Creation (777e6f0b)
|
||||
|
||||
osTicket v1.17.4
|
||||
================
|
||||
### Improvements
|
||||
* issue: Thread Entry Actions z-index (215a0ce2)
|
||||
* oauth2: Strict Matching Bug (e014ffd2)
|
||||
* Make string translatable (1105cde7)
|
||||
* issue: Inline-Images Canned Responses (4493b126)
|
||||
* issue: Remove Old Login Code (e17ad463)
|
||||
* issue: i18n Audit Exports (45dd7c4f)
|
||||
* Bug: Custom File Upload Field Config (6371269a, 7283ac81, 902b5d35)
|
||||
* issue: Duplicate User Copy/Paste Import (b304cdb)
|
||||
* issue: Relocate Typeahead JS/CSS Files (62cd406)
|
||||
* issue: strftime() Deprecation (3fe132c4)
|
||||
* jquery: Update To 3.7.0 (ffa23da)
|
||||
* issue: glob() Empty Array (c64a2611)
|
||||
* issue: Email Only Attachment (9e45f3fa)
|
||||
* installer: Change Email (a11aee29)
|
||||
|
||||
### Security
|
||||
* security: Latest Vulns 06/2023 (86c2ba02, 69244175, 73b997a, ae37925, e4bfb00)
|
||||
|
||||
osTicket v1.17.3
|
||||
================
|
||||
### Enhancements
|
||||
* oauth2: Strict Matching (7275d67b, b7718be4, ca913ba2, cb3625e8, f531718a, c0cd8b16)
|
||||
* OAuth2 Resource Owner (5c239adb)
|
||||
|
||||
### Improvements
|
||||
* session: Regenerate Session Id (d5853245)
|
||||
* fetcher: Mail Fetcher / Parser Error Handling (d4d9c424, 0a4498b2)
|
||||
* mysql: Support Sidecar Database Proxies (246aaa4d)
|
||||
* Misc. Fixes (e6beeb9b, 24fd5075, eac9960a, 10af29a5)
|
||||
* fetcher: Default Department (f410d4ec)
|
||||
* issue: LDAP Multi-Instance Fatal Error (f6486044)
|
||||
* issue: TicketDenied errno (6de7cf4c)
|
||||
* session: User Logged Out (00d409d3)
|
||||
* Uncaught Error: Call to a member function getId() on bool (af83896d)
|
||||
* Mailer: Inline Images (7efbdfd2, 8815d087, 64a8abf3)
|
||||
|
||||
osTicket v1.17.2
|
||||
================
|
||||
### Improvements
|
||||
* Fix old reference to SessionData (cf8b8ce7)
|
||||
* Email Misc. Fixes (de620fe6, c19a5f03)
|
||||
* session: Memcache Max TTL (ea3e03d2)
|
||||
* mailer: Use Namespaced Mailer (133585f8)
|
||||
|
||||
osTicket v1.17.1
|
||||
================
|
||||
### Enhancements
|
||||
* mail: Add plain:// encryption scheme hint (7c1b97b3, 39d76258)
|
||||
|
||||
### Improvements
|
||||
* mailer: Make Headers Valid Again (c08a8a8a, aaee6b0f, 00981554, 2223877c, 020fa0d3, 60c8c884)
|
||||
* POP3 Configration can not saved (b84547b0, ccd8445e)
|
||||
* fetcher: Errors Handling & Logging (384aad13, 22edf2b4, c92006e6)
|
||||
* upgrader: Email Account Status (aee25d69)
|
||||
* issue: Plugin Config Item Exists (fdf9e412)
|
||||
* mailer: Improvements & Enhancements (845e5008, 23a80592, 977cf9aa)
|
||||
* smtp: Use proper name when saying What's Up (HELO/EHLO) (b0e5ac83)
|
||||
* email: Make sure Email Account is Active 4realz! (5efd9f14, a9b57496)
|
||||
* Session++ (ced0ef4d, b943a955, b6b63c99, f58e1c5d, 47df2b44, e2e6c0f4, 9ab5b4f1, da667366, f2c01840, c2be4ebf, db913bd3)
|
||||
* setup: Use Bootstrap:ini() to initialize setup dir sessions (48e436f0)
|
||||
* issue: Plugin Config Item Exists (fdf9e412)
|
||||
* upgrader: Email Account Status (aee25d69)
|
||||
* fetcher: Errors Handling & Logging (384aad13, 22edf2b4, c92006e6)
|
||||
* fetcher: Mail Fetch Order (f1639d41, 24338c2b)
|
||||
* SMTP: Dont send 'QUIT' on __destruct() (dbeae227)
|
||||
* issue: MTA Typo (03eeb8e2)
|
||||
* sendmail: Strip "To" and "Subject" headers (ac3855a4)
|
||||
* Mail Parse Error Handling (320981df, 75c5cfe9, a4e36d39, ba2d31cd, 30ad9cd4, 678098d8, 46b7899b, f85f9036)
|
||||
* Issue/session revisited (49d91b05, 4fc5f5cc)
|
||||
|
||||
osTicket v1.17
|
||||
==============
|
||||
### Enhancements
|
||||
* email: Set Default Email Message Encoding to UTF-8 (4656cf4c, 8954f05b)
|
||||
* plugins: Multi-Instance Blacklist (56077516)
|
||||
* mailer: Reset FROM Address when SMTP fails (75605a3f)
|
||||
* Issue: File Storage Plugins Blacklist (1ba8d37a)
|
||||
* mail: Catch Possible Exceptions and Errors (81d5cb35)
|
||||
|
||||
### Improvements
|
||||
* Update class.mail.php (d5545b4e)
|
||||
* Mail Fetcher Fixes (1eebcd66, 3decbe78)
|
||||
* 1.17.x Bug Fixes (6c2ecad2, 5d799ecd, 21e3a407)
|
||||
* plugins: Make Plugins Upgradeable Again! (80bcc191)
|
||||
* mailer: Skip Invalid Email Recipients (046f4327, 551f7864, d7d37c4c)
|
||||
* issue: Wrong Variable Order (12a4cb09)
|
||||
* mailer: Skip Missing Attachment Files (2218aaca)
|
||||
* fetcher: TicketDenied Exception + Throwable (58f64e65, 79209144, e027dcc4, be178da5)
|
||||
* files: Catch possible Storage Backends Errors (e685636e)
|
||||
* fetcher: Fail Safely on Email Parse Error (459520e8)
|
||||
* forms: Add getNotice func. to DynamicFormEntry (6e54e810)
|
||||
* bug: Users Password Policy (ebfc1d89)
|
||||
* v1.17 Misc. Fixes (d149ea0d)
|
||||
* plugins: Make Plugin Base Class Play Nice (7b26f665, 5787f9ae, 3163972b)
|
||||
|
||||
osTicket v1.17-rc4
|
||||
==================
|
||||
### Enhancements
|
||||
* v1.17: Bug Fixes & Enhancements (bd300c44, ab9a8101, 3024d7f5, e705ff0f, 681bea08, bc154434, 246c1445, e4f594c2, 76bf7725, 4ee44d48, a7068668, 5e53b1a1, 481a0475, 2fa077ae, 13888292)
|
||||
|
||||
### Improvements
|
||||
* mail: Mixed Mime Message with mixed multiparts (6de79a0b, c71ddade)
|
||||
* 2fa: Fix 2FA Backend Registry (f8f5d3dd, 2318714e)
|
||||
* issue: Dispatcher include_once (dab490dc)
|
||||
* plugins: Audit enable() (7796d60c)
|
||||
* issue: AuthenticationBackend getBkId() (29f01c09)
|
||||
|
||||
osTicket v1.17-rc3
|
||||
==================
|
||||
### Improvements
|
||||
* Oauth2 / Modern Email Auth Latest Fixes (fdc5b0fa, d9b41f4e, fa8dc24d, 265065c1)
|
||||
|
||||
osTicket v1.17-rc2
|
||||
==================
|
||||
### Improvements
|
||||
@@ -8,6 +279,78 @@ osTicket v1.17-rc1
|
||||
### Enhancements
|
||||
* Oauth2 / Modern Email Authentication Support (d99ff6e1-163c28b5)
|
||||
|
||||
osTicket v1.16.6
|
||||
================
|
||||
### Improvements
|
||||
* session: Collab View Ticket (6afe0efa)
|
||||
* issue: Static forTask() (1079011f)
|
||||
* issue: Featured FAQs (8df09581)
|
||||
* print: Hidden Thread Events (f63fc67d)
|
||||
* issue: Does Not Have A Value (d47fc2b1)
|
||||
* issue: Disabled Queues Run Counts (3ff3ce93)
|
||||
* issue: Queue Counts Incorrect (2428cc58)
|
||||
* jquery: Update jQueryUI 1.13.2 (b896f70d)
|
||||
* issue: Department Export (5418cf20)
|
||||
* jquery: Update To 3.6.3 (8d39a4cc)
|
||||
* i18n: make-pot (74274b5c)
|
||||
* update: README PHP Version (6087bc55)
|
||||
* issue: mPDF Font Files (427a8aa6)
|
||||
* issue: FileUpload False (8dd2ab61)
|
||||
* Add translation for "last resort" standard column labels (cb528044)
|
||||
* add translation for task status (46c005a0)
|
||||
* issue: getRealPath() Stub (32a229aa)
|
||||
|
||||
### Security
|
||||
* security: Latest Security Patches 03/2023 (86f9693d, 091ddba9, ec604393, daee20fd, 9fb01bc1, 619ce0f6, 343a2b47, 8a55c29f, e2c8f799, ee7ce3d9, 263f71a3)
|
||||
|
||||
osTicket v1.16.5
|
||||
================
|
||||
### Improvements
|
||||
* Fix trusted proxy wildcard check (04c0bb3a)
|
||||
* issue: Html Typo (50dfe931)
|
||||
* mailparse: Parse filenames with complex format (c8067212)
|
||||
* validator: Email Domain Verification (3c4a20a8)
|
||||
* user: Make sure User has a valid Email on create (55fbfcb5)
|
||||
|
||||
osTicket v1.16.4
|
||||
================
|
||||
### Enhancements
|
||||
* i18n: Default Ticket Queue (7a5dd0eb)
|
||||
* mailparse: thread_entry_recipients to (a82af698)
|
||||
* issue: Unique Variable Names (0dc5a02e)
|
||||
* php: 8.1 Support (22b599e3, c24ee575, dd2b9a43)
|
||||
* issue: Session Cookie Refresh (1630459a)
|
||||
|
||||
### Improvements
|
||||
* oops: jQuery UI Images (822a05fe)
|
||||
* issue: Manually Sorted Topics (a80db8a7)
|
||||
* issue: Queue Export Headings (47449758)
|
||||
* installer: Checking Existing Install (a3f2e246)
|
||||
* Catch storage error when mailing ticket response (ca5932ac)
|
||||
* issue: Staff Ticket Open Session (4f3d3cff)
|
||||
* issue: External Auth Regenerate Session (83252260)
|
||||
* issue: Searching Numbers (e00003f6)
|
||||
* Replace hacky way with ReflectionClass (55f61390)
|
||||
* Issue: Installer YAML (6780f29e)
|
||||
* issue: Fix Upgrader Session Issues (311089a6)
|
||||
* issue: Undefined Variable ORM (4009881a)
|
||||
* v1.16: Misc Bug Fixes (4c0341e2)
|
||||
* issue: User/Agent Password Reset (d3de1599)
|
||||
* issue: imagesx Boolean (5d06ee46)
|
||||
* file: Possible Key Collision (21c71c6e)
|
||||
* cli: User Import (4c43e46f)
|
||||
* issue: Miscellaneous Fixes (50bc1a21, 5002bef9, e036fd9a, 35684189, 9c280b49, c2a15b64, 5e970061)
|
||||
* issue: urldecode Dispatcher (c00b8e87)
|
||||
* issue: Information and SectionBreak Fields On Edit (7b60c576)
|
||||
* issue: Favicons Login Headers (61655ec6)
|
||||
* issue: Schedules stops_on (e2dcbb69)
|
||||
* Issue: Agent Permission to Assign Tickets (b4732b05)
|
||||
* issue: File Upload Size (45f554ad)
|
||||
|
||||
### Security
|
||||
* security: Username XSS (5213ff13)
|
||||
* security: Refresh URL (3702a4fb)
|
||||
|
||||
osTicket v1.16.3
|
||||
================
|
||||
### Improvements
|
||||
|
||||
@@ -125,6 +125,8 @@ elseif ($_POST) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Rotate CSRF on successful POST
|
||||
$ost->getCSRF()->rotate();
|
||||
}
|
||||
|
||||
if ($errors && $user && $user != $thisclient)
|
||||
|
||||
@@ -15,14 +15,20 @@
|
||||
**********************************************************************/
|
||||
file_exists('../main.inc.php') or die('System Error');
|
||||
|
||||
/*
|
||||
* Why API_SESSION ??
|
||||
* It indicates that the session is API - which session handler should handle as
|
||||
* stateless for new sessions.
|
||||
* Existing session continue to work as expected - this it's important for
|
||||
* SSO authentication, which uses /api/auth/* endpoints. Such calls are not
|
||||
* stateless.
|
||||
*
|
||||
*/
|
||||
define('API_SESSION', true);
|
||||
|
||||
// APICALL const.
|
||||
define('APICALL', true);
|
||||
|
||||
// Disable sessions for the API. API should be considered stateless and
|
||||
// shouldn't chew up database records to store sessions
|
||||
if (!defined('DISABLE_SESSION'))
|
||||
define('DISABLE_SESSION', true);
|
||||
|
||||
require_once('../main.inc.php');
|
||||
require_once(INCLUDE_DIR.'class.http.php');
|
||||
require_once(INCLUDE_DIR.'class.api.php');
|
||||
|
||||
@@ -13,15 +13,9 @@
|
||||
|
||||
vim: expandtab sw=4 ts=4 sts=4:
|
||||
**********************************************************************/
|
||||
// Use sessions — it's important for SSO authentication, which uses
|
||||
// /api/auth/ext
|
||||
define('DISABLE_SESSION', false);
|
||||
|
||||
require 'api.inc.php';
|
||||
|
||||
# Include the main api urls
|
||||
require_once INCLUDE_DIR."class.dispatcher.php";
|
||||
|
||||
$dispatcher = patterns('',
|
||||
url_post("^/tickets\.(?P<format>xml|json|email)$", array('api.tickets.php:TicketApiController','create')),
|
||||
url('^/tasks/', patterns('',
|
||||
@@ -29,8 +23,8 @@ $dispatcher = patterns('',
|
||||
))
|
||||
);
|
||||
|
||||
// Send api signal so backend can register endpoints
|
||||
Signal::send('api', $dispatcher);
|
||||
|
||||
# Call the respective function
|
||||
print $dispatcher->resolve(Osticket::get_path_info());
|
||||
?>
|
||||
|
||||
@@ -18,10 +18,10 @@ ini_set('memory_limit', '256M'); //The concern here is having enough mem for ema
|
||||
@chdir(dirname(__FILE__).'/'); //Change dir.
|
||||
require('api.inc.php');
|
||||
|
||||
//Only local piping supported via pipe.php
|
||||
// Only local piping supported via pipe.php
|
||||
if (!osTicket::is_cli())
|
||||
die(__('pipe.php only supports local piping - use http -> api/tickets.email'));
|
||||
|
||||
require_once(INCLUDE_DIR.'api.tickets.php');
|
||||
PipeApiController::process();
|
||||
PipeApiController::process('cli');
|
||||
?>
|
||||
|
||||
@@ -22,8 +22,6 @@ class Bootstrap {
|
||||
|
||||
#Error reporting...Good idea to ENABLE error reporting to a file. i.e display_errors should be set to false
|
||||
$error_reporting = E_ALL & ~E_NOTICE & ~E_WARNING;
|
||||
if (defined('E_STRICT')) # 5.4.0
|
||||
$error_reporting &= ~E_STRICT;
|
||||
if (defined('E_DEPRECATED')) # 5.3.0
|
||||
$error_reporting &= ~(E_DEPRECATED | E_USER_DEPRECATED);
|
||||
error_reporting($error_reporting); //Respect whatever is set in php.ini (sysadmin knows better??)
|
||||
@@ -45,6 +43,24 @@ class Bootstrap {
|
||||
}
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
if (!function_exists('exif_imagetype')) {
|
||||
function exif_imagetype ($filename) {
|
||||
if ((list($width,$height,$type,) = getimagesize($filename)) !== false)
|
||||
return $type;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('exif_imagetype')) {
|
||||
function exif_imagetype ($filename) {
|
||||
if ((list($width,$height,$type,) = getimagesize($filename)) !== false)
|
||||
return $type;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($_SERVER['REMOTE_ADDR']))
|
||||
$_SERVER['REMOTE_ADDR'] = '';
|
||||
}
|
||||
@@ -193,13 +209,22 @@ class Bootstrap {
|
||||
'key' => DBSSLKEY
|
||||
);
|
||||
|
||||
if (!db_connect(DBHOST, DBUSER, DBPASS, $options)) {
|
||||
$ferror=sprintf('Unable to connect to the database — %s',db_connect_error());
|
||||
}elseif(!db_select_database(DBNAME)) {
|
||||
$ferror=sprintf('Unknown or invalid database: %s',DBNAME);
|
||||
$hosts = explode(',', DBHOST);
|
||||
foreach ($hosts as $host) {
|
||||
$ferror = null;
|
||||
try {
|
||||
if (!db_connect($host, DBUSER, DBPASS, $options))
|
||||
$ferror = sprintf('Unable to connect to the database — %s', db_connect_error());
|
||||
elseif (!db_select_database(DBNAME))
|
||||
$ferror = sprintf('Unknown or invalid database: %s', DBNAME);
|
||||
} catch (mysqli_sql_exception $e) {
|
||||
$ferror = sprintf('Database error — %s', $e->getMessage());
|
||||
}
|
||||
// break if no error
|
||||
if (!$ferror) break;
|
||||
}
|
||||
|
||||
if($ferror) //Fatal error
|
||||
if ($ferror) //Fatal error
|
||||
self::croak($ferror);
|
||||
}
|
||||
|
||||
@@ -314,8 +339,8 @@ class Bootstrap {
|
||||
|
||||
static function croak($message) {
|
||||
$msg = $message."\n\n".THISPAGE;
|
||||
Mailer::sendmail(ADMIN_EMAIL, 'osTicket Fatal Error', $msg,
|
||||
sprintf('"osTicket Alerts"<%s>', ADMIN_EMAIL));
|
||||
osTicket\Mail\Mailer::sendmail(ADMIN_EMAIL, 'osTicket Fatal Error', $msg,
|
||||
sprintf('"osTicket Alerts" <%s>', ADMIN_EMAIL));
|
||||
//Display generic error to the user
|
||||
Http::response(500, "<b>Fatal Error:</b> Contact system administrator.");
|
||||
}
|
||||
@@ -342,7 +367,7 @@ define('CLI_DIR', INCLUDE_DIR.'cli/');
|
||||
|
||||
#Current version && schema signature (Changes from version to version)
|
||||
define('GIT_VERSION','$git');
|
||||
define('MAJOR_VERSION', '1.17');
|
||||
define('MAJOR_VERSION', '1.18');
|
||||
define('THIS_VERSION', MAJOR_VERSION.'-git'); //Shown on admin panel
|
||||
//Path separator
|
||||
if(!defined('PATH_SEPARATOR')){
|
||||
@@ -368,7 +393,7 @@ if (!defined('ROOT_PATH') && ($rp = osTicket::get_root_path(dirname(__file__))))
|
||||
Bootstrap::init();
|
||||
|
||||
#CURRENT EXECUTING SCRIPT.
|
||||
define('THISPAGE', Misc::currentURL());
|
||||
define('THISPAGE', Http::url());
|
||||
|
||||
define('DEFAULT_MAX_FILE_UPLOADS', ini_get('max_file_uploads') ?: 5);
|
||||
define('DEFAULT_PRIORITY_ID', 1);
|
||||
|
||||
@@ -67,8 +67,9 @@ if($thisclient && $thisclient->getId() && $thisclient->isValid()){
|
||||
}
|
||||
|
||||
/******* CSRF Protectin *************/
|
||||
// Enforce CSRF protection for POSTS
|
||||
if ($_POST && !$ost->checkCSRFToken()) {
|
||||
// Enforce CSRF protection for state-changing methods
|
||||
if (in_array($_SERVER['REQUEST_METHOD'], ['POST', 'PUT', 'PATCH', 'DELETE'])
|
||||
&& !$ost->checkCSRFToken()) {
|
||||
Http::redirect('index.php');
|
||||
//just incase redirect fails
|
||||
die('Action denied (400)!');
|
||||
@@ -79,6 +80,8 @@ $ost->addExtraHeader('<meta name="csrf_token" content="'.$ost->getCSRFToken().'"
|
||||
|
||||
/* Client specific defaults */
|
||||
define('PAGE_LIMIT', DEFAULT_PAGE_LIMIT);
|
||||
define('SESSION_MAXLIFE', $thisclient ? $thisclient->getMaxIdleTime() :
|
||||
SESSION_TTL);
|
||||
|
||||
require(INCLUDE_DIR.'class.nav.php');
|
||||
$nav = new UserNav($thisclient, 'home');
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
.flag.flag-bi {background-position: -112px -11px}
|
||||
.flag.flag-bj {background-position: -128px -11px}
|
||||
.flag.flag-bm {background-position: -144px -11px}
|
||||
.flag.flag-bn {background-position: -160px -11px}
|
||||
.flag.flag-bn {background-position: -32px -11px}
|
||||
.flag.flag-bo {background-position: -176px -11px}
|
||||
.flag.flag-br {background-position: -192px -11px}
|
||||
.flag.flag-bs {background-position: 0px -11px}
|
||||
@@ -124,6 +124,7 @@
|
||||
.flag.flag-ki {background-position: -80px -77px}
|
||||
.flag.flag-km {background-position: -64px -77px}
|
||||
.flag.flag-kn {background-position: -112px -77px}
|
||||
.flag.flag-ko {background-position: -144px -77px}
|
||||
.flag.flag-kp {background-position: -128px -77px}
|
||||
.flag.flag-kr {background-position: -144px -77px}
|
||||
.flag.flag-kurdistan {background-position: -160px -77px}
|
||||
@@ -186,7 +187,7 @@
|
||||
.flag.flag-pm {background-position: -48px -121px}
|
||||
.flag.flag-pn {background-position: -64px -121px}
|
||||
.flag.flag-pr {background-position: -80px -121px}
|
||||
.flag.flag-ps {background-position: -96px -121px}
|
||||
.flag.flag-ps {background-position: -48px 0px}
|
||||
.flag.flag-pt {background-position: -112px -121px}
|
||||
.flag.flag-pw {background-position: -128px -121px}
|
||||
.flag.flag-py {background-position: -144px -121px}
|
||||
@@ -211,6 +212,7 @@
|
||||
.flag.flag-sn {background-position: -192px -132px}
|
||||
.flag.flag-so {background-position: -208px -132px}
|
||||
.flag.flag-somaliland {background-position: -224px -132px}
|
||||
.flag.flag-sq {background-position: -96px 0px}
|
||||
.flag.flag-sr {background-position: -240px -132px}
|
||||
.flag.flag-ss {background-position: 0 -143px}
|
||||
.flag.flag-st {background-position: -16px -143px}
|
||||
@@ -244,7 +246,7 @@
|
||||
.flag.flag-vc {background-position: -208px -154px}
|
||||
.flag.flag-ve {background-position: -224px -154px}
|
||||
.flag.flag-vg {background-position: -240px -154px}
|
||||
.flag.flag-vi {background-position: 0 -165px}
|
||||
.flag.flag-vi {background-position: -16px -165px}
|
||||
.flag.flag-vn {background-position: -16px -165px}
|
||||
.flag.flag-vu {background-position: -32px -165px}
|
||||
.flag.flag-wales {background-position: -48px -165px}
|
||||
@@ -256,3 +258,5 @@
|
||||
.flag.flag-zanzibar {background-position: -144px -165px}
|
||||
.flag.flag-zm {background-position: -160px -165px}
|
||||
.flag.flag-zw {background-position: -176px -165px}
|
||||
.flag.flag-fil {background-position: 0px -121px}
|
||||
.flag.flag-kk {background-position: -208px -77px}
|
||||
|
||||
|
Before Width: | Height: | Size: 418 B After Width: | Height: | Size: 528 B |
|
Before Width: | Height: | Size: 312 B After Width: | Height: | Size: 436 B |
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 376 B |
|
Before Width: | Height: | Size: 348 B After Width: | Height: | Size: 458 B |
|
Before Width: | Height: | Size: 207 B After Width: | Height: | Size: 317 B |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 278 B After Width: | Height: | Size: 394 B |
|
Before Width: | Height: | Size: 328 B After Width: | Height: | Size: 438 B |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.6 KiB |
BIN
css/ui-lightness/images/ui-icons_444444_256x240.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
css/ui-lightness/images/ui-icons_555555_256x240.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
css/ui-lightness/images/ui-icons_777620_256x240.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
css/ui-lightness/images/ui-icons_777777_256x240.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
css/ui-lightness/images/ui-icons_cc0000_256x240.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.4 KiB |
10
file.php
@@ -45,9 +45,8 @@ if ($cfg->isAuthRequiredForFiles()
|
||||
) {
|
||||
|
||||
// Try and determine if an agent is viewing the page / file
|
||||
if (strpos($_SERVER['HTTP_REFERRER'], ROOT_PATH . 'scp/') !== false) {
|
||||
$_SESSION['_staff']['auth']['dest'] =
|
||||
'/' . ltrim($_SERVER['REQUEST_URI'], '/');
|
||||
if (strpos($_SERVER['HTTP_REFERER'], ROOT_PATH . 'scp/') !== false) {
|
||||
$_SESSION['_staff']['auth']['dest'] = Http::refresh_url();
|
||||
Http::redirect(ROOT_PATH.'scp/login.php');
|
||||
} else {
|
||||
require 'secure.inc.php';
|
||||
@@ -59,7 +58,7 @@ if ($cfg->isAuthRequiredForFiles()
|
||||
// and the user has access to the parent ticket!!
|
||||
if ($file->verifySignature($_GET['signature'], $_GET['expires'])) {
|
||||
try {
|
||||
if (($s = @$_GET['s']) && strpos($file->getType(), 'image/') === 0)
|
||||
if (($s = @$_GET['s']) && $file->isInlineSafeImage())
|
||||
return $file->display($s);
|
||||
|
||||
// Download the file..
|
||||
@@ -68,7 +67,8 @@ if ($file->verifySignature($_GET['signature'], $_GET['expires'])) {
|
||||
$file->download($filename, $disposition, @$_GET['expires']);
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
Http::response(500, 'Unable to find that file: '.$ex->getMessage());
|
||||
$ost->logError(__('File download error'), $ex->getMessage());
|
||||
Http::response(500, __('Unable to retrieve the requested file'));
|
||||
}
|
||||
}
|
||||
// else
|
||||
|
||||
@@ -19,7 +19,12 @@ require_once INCLUDE_DIR.'class.ajax.php';
|
||||
|
||||
class ContentAjaxAPI extends AjaxController {
|
||||
|
||||
function log($id) {
|
||||
function syslog($id) {
|
||||
global $thisstaff;
|
||||
|
||||
// Ensure there is a Staff and that they are an Administrator
|
||||
if (!$thisstaff || !$thisstaff->isAdmin())
|
||||
Http::response(403, 'Access Denied');
|
||||
|
||||
if($id && ($log=Log::lookup($id))) {
|
||||
$content=sprintf('<div
|
||||
@@ -142,8 +147,8 @@ class ContentAjaxAPI extends AjaxController {
|
||||
function manageContent($id, $lang=false) {
|
||||
global $thisstaff, $cfg;
|
||||
|
||||
if (!$thisstaff)
|
||||
Http::response(403, 'Login Required');
|
||||
if (!$thisstaff || !$thisstaff->isAdmin())
|
||||
Http::response(403, 'Access Denied');
|
||||
|
||||
$content = Page::lookup($id, $lang);
|
||||
|
||||
@@ -168,8 +173,8 @@ class ContentAjaxAPI extends AjaxController {
|
||||
function manageNamedContent($type, $lang=false) {
|
||||
global $thisstaff, $cfg;
|
||||
|
||||
if (!$thisstaff)
|
||||
Http::response(403, 'Login Required');
|
||||
if (!$thisstaff || !$thisstaff->isAdmin())
|
||||
Http::response(403, 'Access Denied');
|
||||
|
||||
$langs = $cfg->getSecondaryLanguages();
|
||||
|
||||
@@ -180,10 +185,10 @@ class ContentAjaxAPI extends AjaxController {
|
||||
}
|
||||
|
||||
function updateContent($id) {
|
||||
global $thisstaff;
|
||||
global $thisstaff, $cfg;
|
||||
|
||||
if (!$thisstaff)
|
||||
Http::response(403, 'Login Required');
|
||||
if (!$thisstaff || !$thisstaff->isAdmin())
|
||||
Http::response(403, 'Access Denied');
|
||||
elseif (!($content = Page::lookup($id)))
|
||||
Http::response(404, 'No such content');
|
||||
|
||||
@@ -201,6 +206,7 @@ class ContentAjaxAPI extends AjaxController {
|
||||
}
|
||||
if (!$errors['err'])
|
||||
$errors['err'] = __('Correct any errors below and try again.');
|
||||
$langs = Internationalization::getConfiguredSystemLanguages();
|
||||
$info = $_POST;
|
||||
$errors = Format::htmlchars($errors);
|
||||
include STAFFINC_DIR . 'templates/content-manage.tmpl.php';
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
if(!defined('INCLUDE_DIR')) die('!');
|
||||
|
||||
require_once(INCLUDE_DIR.'class.draft.php');
|
||||
require_once(INCLUDE_DIR.'class.file.php');
|
||||
|
||||
class DraftAjaxAPI extends AjaxController {
|
||||
|
||||
@@ -53,78 +54,80 @@ class DraftAjaxAPI extends AjaxController {
|
||||
static function _uploadInlineImage($draft) {
|
||||
global $cfg;
|
||||
|
||||
if (!isset($_POST['data']) && !isset($_FILES['file']))
|
||||
if (!isset($_FILES['file']))
|
||||
Http::response(422, "File not included properly");
|
||||
|
||||
# Fixup for expected multiple attachments
|
||||
if (isset($_FILES['file'])) {
|
||||
$file = AttachmentFile::format($_FILES['file']);
|
||||
$file = AttachmentFile::format($_FILES['file']);
|
||||
|
||||
# Allow for data-uri uploaded files
|
||||
$fp = fopen($file[0]['tmp_name'], 'rb');
|
||||
if (fread($fp, 5) == 'data:') {
|
||||
$data = 'data:';
|
||||
while ($block = fread($fp, 8192))
|
||||
$data .= $block;
|
||||
$file[0] = Format::parseRfc2397($data);
|
||||
list(,$ext) = explode('/', $file[0]['type'], 2);
|
||||
$file[0] += array(
|
||||
'name' => Misc::randCode(8).'.'.$ext,
|
||||
'size' => strlen($file[0]['data']),
|
||||
);
|
||||
}
|
||||
fclose($fp);
|
||||
|
||||
# TODO: Detect unacceptable attachment extension
|
||||
# TODO: Verify content-type and check file-content to ensure image
|
||||
$type = $file[0]['type'];
|
||||
if (strpos($file[0]['type'], 'image/') !== 0)
|
||||
return Http::response(403,
|
||||
JsonDataEncoder::encode(array(
|
||||
'error' => 'File type is not allowed',
|
||||
))
|
||||
);
|
||||
|
||||
# TODO: Verify file size is acceptable
|
||||
if ($file[0]['size'] > $cfg->getMaxFileSize())
|
||||
return Http::response(403,
|
||||
JsonDataEncoder::encode(array(
|
||||
'error' => 'File is too large',
|
||||
))
|
||||
);
|
||||
|
||||
// Paste uploads in Chrome will have a name of 'blob'
|
||||
if ($file[0]['name'] == 'blob')
|
||||
$file[0]['name'] = 'screenshot-'.Misc::randCode(4);
|
||||
|
||||
$ids = $draft->attachments->upload($file);
|
||||
|
||||
if (!$ids) {
|
||||
if ($file[0]['error']) {
|
||||
return Http::response(403,
|
||||
JsonDataEncoder::encode(array(
|
||||
'error' => $file[0]['error'],
|
||||
))
|
||||
);
|
||||
}
|
||||
else
|
||||
return Http::response(500, 'Unable to attach image');
|
||||
}
|
||||
|
||||
$id = (is_array($ids)) ? $ids[0] : $ids;
|
||||
}
|
||||
else {
|
||||
$type = explode('/', $_POST['contentType']);
|
||||
$info = array(
|
||||
'data' => base64_decode($_POST['data']),
|
||||
'name' => Misc::randCode(10).'.'.$type[1],
|
||||
// TODO: Ensure _POST['contentType']
|
||||
'type' => $_POST['contentType'],
|
||||
// Allow one file at a time.
|
||||
if (count($file) > 1)
|
||||
return Http::response(403,
|
||||
JsonDataEncoder::encode(array(
|
||||
'error' => 'Send one file at a time',
|
||||
))
|
||||
);
|
||||
|
||||
# Allow for data-uri uploaded files
|
||||
$fp = fopen($file[0]['tmp_name'], 'rb');
|
||||
if (fread($fp, 5) == 'data:') {
|
||||
$data = 'data:';
|
||||
while ($block = fread($fp, 8192))
|
||||
$data .= $block;
|
||||
$file[0] = Format::parseRfc2397($data);
|
||||
list(,$ext) = explode('/', $file[0]['type'], 2);
|
||||
$file[0] += array(
|
||||
'name' => Misc::randCode(8).'.'.$ext,
|
||||
'size' => strlen($file[0]['data']),
|
||||
);
|
||||
// TODO: Detect unacceptable filetype
|
||||
// TODO: Verify content-type and check file-content to ensure image
|
||||
$id = $draft->attachments->save($info);
|
||||
}
|
||||
fclose($fp);
|
||||
|
||||
// Check file type to ensure image
|
||||
$type = $file[0]['type'];
|
||||
if (!AttachmentFile::isInlineSafeImageType($file[0]['type']))
|
||||
return Http::response(403,
|
||||
JsonDataEncoder::encode(array(
|
||||
'error' => 'File type is not allowed',
|
||||
))
|
||||
);
|
||||
|
||||
// Check if file is truly an image
|
||||
if (!FileUploadField::isValidFile($file[0]))
|
||||
return Http::response(403,
|
||||
JsonDataEncoder::encode(array(
|
||||
'error' => 'File is not valid',
|
||||
))
|
||||
);
|
||||
|
||||
// Verify file size is acceptable
|
||||
if ($file[0]['size'] > $cfg->getMaxFileSize())
|
||||
return Http::response(403,
|
||||
JsonDataEncoder::encode(array(
|
||||
'error' => 'File is too large',
|
||||
))
|
||||
);
|
||||
|
||||
// Paste uploads in Chrome will have a name of 'blob'
|
||||
if ($file[0]['name'] == 'blob')
|
||||
$file[0]['name'] = 'screenshot-'.Misc::randCode(4);
|
||||
|
||||
$ids = $draft->attachments->upload($file);
|
||||
|
||||
if (!$ids) {
|
||||
if ($file[0]['error']) {
|
||||
return Http::response(403,
|
||||
JsonDataEncoder::encode(array(
|
||||
'error' => $file[0]['error'],
|
||||
))
|
||||
);
|
||||
|
||||
}
|
||||
else
|
||||
return Http::response(500, 'Unable to attach image');
|
||||
}
|
||||
|
||||
$id = (is_array($ids)) ? $ids[0] : $ids;
|
||||
if (!($f = AttachmentFile::lookup($id)))
|
||||
return Http::response(500, 'Unable to attach image');
|
||||
|
||||
@@ -240,6 +243,7 @@ class DraftAjaxAPI extends AjaxController {
|
||||
|
||||
$draft = Draft::create(array(
|
||||
'namespace' => $namespace,
|
||||
'body' => ''
|
||||
));
|
||||
if (!$draft->save())
|
||||
Http::response(500, 'Unable to create draft');
|
||||
@@ -310,7 +314,8 @@ class DraftAjaxAPI extends AjaxController {
|
||||
Http::response(403, "Login required for image upload");
|
||||
|
||||
$draft = Draft::create(array(
|
||||
'namespace' => $namespace
|
||||
'namespace' => $namespace,
|
||||
'body' => ''
|
||||
));
|
||||
if (!$draft->save())
|
||||
Http::response(500, 'Unable to create draft');
|
||||
@@ -363,6 +368,9 @@ class DraftAjaxAPI extends AjaxController {
|
||||
|
||||
$files = array();
|
||||
foreach ($images as $f) {
|
||||
if (!$f->isInlineSafeImage())
|
||||
continue;
|
||||
|
||||
$url = $f->getDownloadUrl();
|
||||
$files[] = array(
|
||||
// Don't send special sizing for thread items 'cause they
|
||||
|
||||
@@ -25,7 +25,6 @@ class EmailAjaxAPI extends AjaxController {
|
||||
$info = $errors = [];
|
||||
if ($_POST && $account->saveAuth($auth, $form, $errors)) {
|
||||
if ($account->isOAuthAuth()
|
||||
&& $account->isEnabled()
|
||||
&& $account->shouldAuthorize()) {
|
||||
Http::response(201, JsonDataEncoder::encode([
|
||||
'redirect' => sprintf('emails.php?id=%d&do=autho&bk=%s',
|
||||
@@ -40,4 +39,21 @@ class EmailAjaxAPI extends AjaxController {
|
||||
$template = sprintf('email-%sauth.tmpl.php', $authtype);
|
||||
include INCLUDE_DIR . "staff/templates/$template";
|
||||
}
|
||||
|
||||
/*
|
||||
* Delete the OAuth2 token
|
||||
*/
|
||||
function deleteToken($id, $type) {
|
||||
// Check to make sure the email exists
|
||||
if (!($email=Email::lookup($id)))
|
||||
Http::response(404, 'Unknown Email');
|
||||
// Get the authentication account
|
||||
if (!($account=$email->getAuthAccount($type)))
|
||||
Http::response(404, 'Unknown Authentication Type');
|
||||
// Destory the account config which will delete the token
|
||||
if ($account->destroyConfig())
|
||||
Http::response(201, __('Token Deleted Successfully.'));
|
||||
// Couldn't delete the Token
|
||||
Http::response(404, 'Unable to delete token.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ class NoteAjaxAPI extends AjaxController {
|
||||
Http::response(403, "Login required");
|
||||
elseif (!($note = QuickNote::lookup($id)))
|
||||
Http::response(205, "Note not found");
|
||||
elseif (($note->staff_id !== $thisstaff->getId())
|
||||
&& !$thisstaff->isAdmin())
|
||||
Http::response(403, "Access Denied");
|
||||
|
||||
Http::response(200, $note->display());
|
||||
}
|
||||
@@ -24,6 +27,9 @@ class NoteAjaxAPI extends AjaxController {
|
||||
Http::response(403, "Login required");
|
||||
elseif (!($note = QuickNote::lookup($id)))
|
||||
Http::response(205, "Note not found");
|
||||
elseif (($note->staff_id !== $thisstaff->getId())
|
||||
&& !$thisstaff->isAdmin())
|
||||
Http::response(403, "Access Denied");
|
||||
elseif (!isset($_POST['note']) || !$_POST['note'])
|
||||
Http::response(422, "Send `note` parameter");
|
||||
|
||||
@@ -41,6 +47,9 @@ class NoteAjaxAPI extends AjaxController {
|
||||
Http::response(403, "Login required");
|
||||
elseif (!($note = QuickNote::lookup($id)))
|
||||
Http::response(205, "Note not found");
|
||||
elseif (($note->staff_id !== $thisstaff->getId())
|
||||
&& !$thisstaff->isAdmin())
|
||||
Http::response(403, "Access Denied");
|
||||
elseif (!$note->delete())
|
||||
Http::response(500, "Unable to remove note");
|
||||
|
||||
@@ -52,6 +61,9 @@ class NoteAjaxAPI extends AjaxController {
|
||||
|
||||
if (!$thisstaff)
|
||||
Http::response(403, "Login required");
|
||||
elseif (!in_array(substr($ext_id, 0, 1), ['O', 'U'])
|
||||
&& !$thisstaff->isAdmin())
|
||||
Http::response(403, "Access Denied");
|
||||
elseif (!isset($_POST['note']) || !$_POST['note'])
|
||||
Http::response(422, "Send `note` parameter");
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class OrgsAjaxAPI extends AjaxController {
|
||||
if (!$_REQUEST['q'])
|
||||
return $this->json_encode(array());
|
||||
|
||||
$q = $_REQUEST['q'];
|
||||
$q = Format::sanitize($_REQUEST['q']);
|
||||
$limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
|
||||
|
||||
if (strlen(Format::searchable($q)) < 3)
|
||||
@@ -55,7 +55,7 @@ class OrgsAjaxAPI extends AjaxController {
|
||||
foreach ($orgs as $O) {
|
||||
list($id, $name) = $O;
|
||||
$matched[] = array('name' => Format::htmlchars($name), 'info' => $name,
|
||||
'id' => $id, '/bin/true' => $_REQUEST['q']);
|
||||
'id' => $id, '/bin/true' => Format::htmlchars($q));
|
||||
}
|
||||
|
||||
return $this->json_encode(array_values($matched));
|
||||
|
||||
@@ -31,7 +31,7 @@ class SearchAjaxAPI extends AjaxController {
|
||||
$search = new AdhocSearch(array(
|
||||
'root' => 'T',
|
||||
'staff_id' => $thisstaff->getId(),
|
||||
'parent_id' => @$_GET['parent_id'] ?: 0,
|
||||
'parent_id' => (int) @$_GET['parent_id'] ?: 0,
|
||||
));
|
||||
if ($search->parent_id) {
|
||||
$search->flags |= SavedSearch::FLAG_INHERIT_COLUMNS;
|
||||
@@ -168,7 +168,7 @@ class SearchAjaxAPI extends AjaxController {
|
||||
'title' => __('Add Queue'),
|
||||
'root' => 'T',
|
||||
'staff_id' => $thisstaff->getId(),
|
||||
'parent_id' => $_GET['pid'],
|
||||
'parent_id' => (int) $_GET['pid'],
|
||||
));
|
||||
$this->_tryAgain($search);
|
||||
}
|
||||
@@ -188,11 +188,16 @@ class SearchAjaxAPI extends AjaxController {
|
||||
'root' => 'T',
|
||||
'staff_id' => $thisstaff->getId()
|
||||
));
|
||||
$new = true;
|
||||
}
|
||||
|
||||
if (false === $this->_saveSearch($search))
|
||||
return;
|
||||
|
||||
//store saved search count when it is created
|
||||
if ($new)
|
||||
$search->counts($thisstaff, false);
|
||||
|
||||
$info = array(
|
||||
'msg' => sprintf('%s %s %s',
|
||||
__('Search'),
|
||||
@@ -346,17 +351,16 @@ class SearchAjaxAPI extends AjaxController {
|
||||
function addCondition() {
|
||||
global $thisstaff;
|
||||
|
||||
if (!$thisstaff) {
|
||||
if (!$thisstaff)
|
||||
Http::response(403, 'Agent login is required');
|
||||
}
|
||||
elseif (!isset($_GET['field']) || !isset($_GET['id'])
|
||||
|| !isset($_GET['object_id'])
|
||||
) {
|
||||
|| !isset($_GET['object_id']))
|
||||
Http::response(400, '`field`, `id`, and `object_id` parameters required');
|
||||
}
|
||||
elseif (!is_numeric($_GET['object_id'])) {
|
||||
elseif (!is_numeric($_GET['object_id']))
|
||||
Http::response(400, '`object_id` should be an integer');
|
||||
}
|
||||
elseif (!is_numeric($_GET['id']))
|
||||
Http::response(400, '`id` should be an integer');
|
||||
|
||||
$fields = SavedSearch::getSearchableFields('Ticket');
|
||||
if (!isset($fields[$_GET['field']])) {
|
||||
Http::response(400, sprintf('%s: No such searchable field'),
|
||||
@@ -382,8 +386,8 @@ class SearchAjaxAPI extends AjaxController {
|
||||
Http::response(400, '`prop` and `condition` parameters required');
|
||||
}
|
||||
|
||||
$prop = $_GET['prop'];
|
||||
$id = $_GET['condition'];
|
||||
$prop = Format::htmlchars($_GET['prop']);
|
||||
$id = Format::htmlchars($_GET['condition']);
|
||||
include STAFFINC_DIR . 'templates/queue-column-condition-prop.tmpl.php';
|
||||
}
|
||||
|
||||
|
||||
@@ -59,10 +59,17 @@ class TasksAjaxAPI extends AjaxController {
|
||||
}
|
||||
|
||||
function triggerThreadAction($task_id, $thread_id, $action) {
|
||||
global $thisstaff;
|
||||
|
||||
if (!($task = Task::lookup($task_id))
|
||||
|| !$task->checkStaffPerm($thisstaff))
|
||||
Http::response(404, 'No such task thread entry');
|
||||
|
||||
$thread = ThreadEntry::lookup($thread_id);
|
||||
if (!$thread)
|
||||
Http::response(404, 'No such task thread entry');
|
||||
if ($thread->getThread()->getObjectId() != $task_id)
|
||||
if ($thread->getThread()->getObjectType() != 'A'
|
||||
|| $thread->getThread()->getObjectId() != $task_id)
|
||||
Http::response(404, 'No such task thread entry');
|
||||
|
||||
$valid = false;
|
||||
|
||||
@@ -23,7 +23,7 @@ include_once INCLUDE_DIR . 'class.thread_actions.php';
|
||||
|
||||
class TicketsAjaxAPI extends AjaxController {
|
||||
|
||||
function lookup() {
|
||||
function lookup($wildcard=false) {
|
||||
global $thisstaff;
|
||||
|
||||
$limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
|
||||
@@ -53,7 +53,7 @@ class TicketsAjaxAPI extends AjaxController {
|
||||
return $this->encode(array());
|
||||
|
||||
global $ost;
|
||||
$hits = $ost->searcher->find($q, $hits, false);
|
||||
$hits = $ost->searcher->find($q, $hits, false, ['boolean' => $wildcard]);
|
||||
|
||||
$T = array();
|
||||
if (preg_match('/\d{2,}[^*]/', $q, $T)) {
|
||||
@@ -62,10 +62,10 @@ class TicketsAjaxAPI extends AjaxController {
|
||||
elseif (preg_match('/(.*@.{2,})|(.{2,}@.*)/', $q, $T)) {
|
||||
$hits = $this->lookupByEmail($limit, $visibility, $hits);
|
||||
}
|
||||
elseif (!count($hits) && preg_match('`\w$`u', $q)) {
|
||||
elseif (!$wildcard && !count($hits) && preg_match('`^[a-zA-Z0-9]+$`u', $q)) {
|
||||
// Do wild-card fulltext search
|
||||
$_REQUEST['q'] = $q.'*';
|
||||
return $this->lookup();
|
||||
return $this->lookup(true);
|
||||
}
|
||||
|
||||
// TODO: Why not aggregate based on relationship?
|
||||
@@ -777,6 +777,8 @@ class TicketsAjaxAPI extends AjaxController {
|
||||
|
||||
if (!($ticket=Ticket::lookup($tid)))
|
||||
Http::response(404, __('No such ticket'));
|
||||
elseif (!$ticket->checkStaffPerm($thisstaff))
|
||||
Http::response(403, __('Permission denied'));
|
||||
elseif (!($field=$ticket->getField($fid)))
|
||||
Http::response(404, __('No such field'));
|
||||
|
||||
@@ -1134,6 +1136,7 @@ class TicketsAjaxAPI extends AjaxController {
|
||||
|
||||
$depts = $tickets->values_flat('dept_id');
|
||||
}
|
||||
//agents in depts $thisstaff can access
|
||||
$members = $thisstaff->getDeptAgents(array('available' => true));
|
||||
|
||||
if ($depts) {
|
||||
@@ -1211,6 +1214,9 @@ class TicketsAjaxAPI extends AjaxController {
|
||||
$f->configure('prompt', $prompt);
|
||||
|
||||
if ($_POST && $form->isValid()) {
|
||||
// we need to clear this relation so we can create a fresh
|
||||
// query that will get roles for all depts this agent can access
|
||||
unset($thisstaff->ht['dept_access']);
|
||||
foreach ($_POST['tids'] as $tid) {
|
||||
if (($t=Ticket::lookup($tid))
|
||||
// Make sure the agent is allowed to
|
||||
@@ -1698,10 +1704,17 @@ class TicketsAjaxAPI extends AjaxController {
|
||||
}
|
||||
|
||||
function triggerThreadAction($ticket_id, $thread_id, $action) {
|
||||
global $thisstaff;
|
||||
|
||||
if (!($ticket = Ticket::lookup($ticket_id))
|
||||
|| !$ticket->checkStaffPerm($thisstaff))
|
||||
Http::response(404, 'No such ticket thread entry');
|
||||
|
||||
$thread = ThreadEntry::lookup($thread_id);
|
||||
if (!$thread)
|
||||
Http::response(404, 'No such ticket thread entry');
|
||||
if ($thread->getThread()->getObjectId() != $ticket_id)
|
||||
if ($thread->getThread()->getObjectType() != 'T'
|
||||
|| $thread->getThread()->getObjectId() != $ticket_id)
|
||||
Http::response(404, 'No such ticket thread entry');
|
||||
|
||||
$valid = false;
|
||||
@@ -2031,7 +2044,7 @@ class TicketsAjaxAPI extends AjaxController {
|
||||
} else {
|
||||
$filename = sprintf('%s Tickets-%s.csv',
|
||||
$queue->getName(),
|
||||
strftime('%Y%m%d'));
|
||||
date('Ymd'));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -34,7 +34,7 @@ class UsersAjaxAPI extends AjaxController {
|
||||
if (!$_REQUEST['q'])
|
||||
return $this->json_encode($matches);
|
||||
|
||||
$q = $_REQUEST['q'];
|
||||
$q = Format::sanitize($_REQUEST['q']);
|
||||
$limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
|
||||
$users=array();
|
||||
$emails=array();
|
||||
@@ -107,7 +107,7 @@ class UsersAjaxAPI extends AjaxController {
|
||||
}
|
||||
$name = Format::htmlchars(new UsersName($name));
|
||||
$matches[] = array('email'=>$email, 'name'=>$name, 'info'=>"$email - $name",
|
||||
"id" => $id, "/bin/true" => $_REQUEST['q']);
|
||||
"id" => $id, "/bin/true" => $q);
|
||||
}
|
||||
usort($matches, function($a, $b) { return strcmp($a['name'], $b['name']); });
|
||||
}
|
||||
@@ -121,7 +121,12 @@ class UsersAjaxAPI extends AjaxController {
|
||||
|
||||
if(!$thisstaff)
|
||||
Http::response(403, 'Login Required');
|
||||
elseif(!($user = User::lookup($id)))
|
||||
|
||||
if (!$thisstaff->hasPerm(User::PERM_DIRECTORY)
|
||||
&& !$thisstaff->hasPerm(User::PERM_EDIT))
|
||||
Http::response(403, 'Permission Denied');
|
||||
|
||||
if(!($user = User::lookup($id)))
|
||||
Http::response(404, 'Unknown user');
|
||||
|
||||
$info = array(
|
||||
@@ -275,6 +280,14 @@ class UsersAjaxAPI extends AjaxController {
|
||||
}
|
||||
|
||||
function getUser($id=false) {
|
||||
global $thisstaff;
|
||||
|
||||
if (!$thisstaff)
|
||||
Http::response(403, 'Login Required');
|
||||
|
||||
if (!$thisstaff->hasPerm(User::PERM_DIRECTORY)
|
||||
&& !$thisstaff->hasPerm(User::PERM_EDIT))
|
||||
Http::response(403, 'Permission Denied');
|
||||
|
||||
if(($user=User::lookup(($id) ? $id : $_REQUEST['id'])))
|
||||
Http::response(201, $user->to_json(), 'application/json');
|
||||
@@ -323,7 +336,7 @@ class UsersAjaxAPI extends AjaxController {
|
||||
elseif (!$bk || !$id)
|
||||
Http::response(422, 'Backend and user id required');
|
||||
elseif (!($backend = AuthenticationBackend::getSearchDirectoryBackend($bk))
|
||||
|| !($user_info = $backend->lookup($id)))
|
||||
|| !($user_info = $backend->lookup(html_entity_decode($id))))
|
||||
Http::response(404, 'User not found');
|
||||
|
||||
$form = UserForm::getUserForm()->getForm($user_info);
|
||||
@@ -422,6 +435,8 @@ class UsersAjaxAPI extends AjaxController {
|
||||
Http::response(403, 'Login Required');
|
||||
elseif (!($user = User::lookup($id)))
|
||||
Http::response(404, 'Unknown user');
|
||||
elseif (!$thisstaff->hasPerm(User::PERM_EDIT))
|
||||
Http::response(403, 'Access Denied');
|
||||
|
||||
$info = array();
|
||||
$info['title'] = sprintf(__('Organization for %s'),
|
||||
@@ -434,6 +449,9 @@ class UsersAjaxAPI extends AjaxController {
|
||||
if (!($org = Organization::lookup($_POST['orgid'])))
|
||||
$info['error'] = __('Unknown organization selected');
|
||||
} else { //Creating new org.
|
||||
if (!$thisstaff->hasPerm(Organization::PERM_CREATE))
|
||||
Http::response(403, 'Permission Denied');
|
||||
|
||||
$form = OrganizationForm::getDefaultForm()->getForm($_POST);
|
||||
if (!($org = Organization::fromForm($form)))
|
||||
$info['error'] = __('Unable to create organization.')
|
||||
|
||||
@@ -3,40 +3,45 @@
|
||||
include_once INCLUDE_DIR.'class.cron.php';
|
||||
|
||||
class CronApiController extends ApiController {
|
||||
|
||||
function execute() {
|
||||
|
||||
if(!($key=$this->requireApiKey()) || !$key->canExecuteCron())
|
||||
if (!($key=$this->requireApiKey()) || !$key->canExecuteCron())
|
||||
return $this->exerr(401, __('API key not authorized'));
|
||||
|
||||
$this->run();
|
||||
}
|
||||
|
||||
/* private */
|
||||
function run() {
|
||||
global $ost;
|
||||
|
||||
protected function run() {
|
||||
Cron::run();
|
||||
|
||||
$ost->logDebug(__('Cron Job'),__('Cron job executed').' ['.$_SERVER['REMOTE_ADDR'].']');
|
||||
// TODO: Add elapsed time to the debug log
|
||||
$this->debug(__('Cron Job'),
|
||||
sprintf('%s [%s]', __('Cron job executed'), $this->getRemoteAddr()));
|
||||
$this->response(200,'Completed');
|
||||
}
|
||||
}
|
||||
|
||||
class LocalCronApiController extends CronApiController {
|
||||
|
||||
function response($code, $resp) {
|
||||
public function isCli() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if($code == 200) //Success - exit silently.
|
||||
protected function getRemoteAddr() {
|
||||
// Local Cron doesn't have IP Addr set
|
||||
return 'CLI';
|
||||
}
|
||||
|
||||
public function response($code, $response) {
|
||||
|
||||
if ($code == 200) //Success - exit silently.
|
||||
exit(0);
|
||||
|
||||
//On error echo the response (error)
|
||||
echo $resp;
|
||||
|
||||
echo $response;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
static function call() {
|
||||
$cron = new LocalCronApiController();
|
||||
$cron = new LocalCronApiController('cli');
|
||||
$cron->run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,15 +43,22 @@ class TicketApiController extends ApiController {
|
||||
foreach ($form->getFields() as $field)
|
||||
$supported[] = $field->get('name');
|
||||
|
||||
if(!strcasecmp($format, 'email')) {
|
||||
$supported = array_merge($supported, array('header', 'mid',
|
||||
'emailId', 'to-email-id', 'ticketId', 'reply-to', 'reply-to-name',
|
||||
'in-reply-to', 'references', 'thread-type', 'system_emails',
|
||||
'mailflags' => array('bounce', 'auto-reply', 'spam', 'viral'),
|
||||
'recipients' => array('*' => array('name', 'email', 'source'))
|
||||
));
|
||||
|
||||
$supported['attachments']['*'][] = 'cid';
|
||||
switch ($format) {
|
||||
case 'email':
|
||||
$supported = array_merge($supported, [
|
||||
'header', 'mid', 'emailId', 'to-email-id', 'ticketId', 'reply-to',
|
||||
'reply-to-name', 'in-reply-to', 'references', 'thread-type', 'system_emails',
|
||||
'mailflags' => ['bounce', 'auto-reply', 'spam', 'viral'],
|
||||
'recipients' => ['*' => ['name', 'email', 'source']]
|
||||
]);
|
||||
$supported['attachments']['*'][] = 'cid';
|
||||
break;
|
||||
case 'json':
|
||||
case 'xml':
|
||||
$supported = array_merge($supported, [
|
||||
'duedate', 'slaId', 'staffId'
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
return $supported;
|
||||
@@ -78,7 +85,7 @@ class TicketApiController extends ApiController {
|
||||
$data['attachments'] = array();
|
||||
|
||||
//Validate attachments: Do error checking... soft fail - set the error and pass on the request.
|
||||
if ($data['attachments'] && is_array($data['attachments'])) {
|
||||
if (isset($data['attachments']) && is_array($data['attachments'])) {
|
||||
foreach($data['attachments'] as &$file) {
|
||||
if ($file['encoding'] && !strcasecmp($file['encoding'], 'base64')) {
|
||||
if(!($file['data'] = base64_decode($file['data'], true)))
|
||||
@@ -105,61 +112,84 @@ class TicketApiController extends ApiController {
|
||||
|
||||
function create($format) {
|
||||
|
||||
if(!($key=$this->requireApiKey()) || !$key->canCreateTickets())
|
||||
if (!($key=$this->requireApiKey()) || !$key->canCreateTickets())
|
||||
return $this->exerr(401, __('API key not authorized'));
|
||||
|
||||
$ticket = null;
|
||||
if(!strcasecmp($format, 'email')) {
|
||||
# Handle remote piped emails - could be a reply...etc.
|
||||
$ticket = $this->processEmail();
|
||||
if (!strcasecmp($format, 'email')) {
|
||||
// Process remotely piped emails - could be a reply...etc.
|
||||
$ticket = $this->processEmailRequest();
|
||||
} else {
|
||||
# Parse request body
|
||||
// Get and Parse request body data for the format
|
||||
$ticket = $this->createTicket($this->getRequest($format));
|
||||
}
|
||||
|
||||
if(!$ticket)
|
||||
return $this->exerr(500, __("Unable to create new ticket: unknown error"));
|
||||
|
||||
$this->response(201, $ticket->getNumber());
|
||||
|
||||
if ($ticket)
|
||||
$this->response(201, $ticket->getNumber());
|
||||
else
|
||||
$this->exerr(500, _S("unknown error"));
|
||||
|
||||
}
|
||||
|
||||
/* private helper functions */
|
||||
|
||||
function createTicket($data) {
|
||||
function createTicket($data, $source = 'API') {
|
||||
|
||||
# Pull off some meta-data
|
||||
$alert = (bool) (isset($data['alert']) ? $data['alert'] : true);
|
||||
$autorespond = (bool) (isset($data['autorespond']) ? $data['autorespond'] : true);
|
||||
|
||||
# Assign default value to source if not defined, or defined as NULL
|
||||
$data['source'] = isset($data['source']) ? $data['source'] : 'API';
|
||||
// Assign default value to source if not defined, or defined as NULL
|
||||
$data['source'] ??= $source;
|
||||
|
||||
# Create the ticket with the data (attempt to anyway)
|
||||
// Create the ticket with the data (attempt to anyway)
|
||||
$errors = array();
|
||||
if (($ticket = Ticket::create($data, $errors, $data['source'],
|
||||
$autorespond, $alert)) && !$errors)
|
||||
return $ticket;
|
||||
|
||||
$ticket = Ticket::create($data, $errors, $data['source'], $autorespond, $alert);
|
||||
# Return errors (?)
|
||||
// Ticket create failed Bigly - got errors?
|
||||
$title = null;
|
||||
// Got errors?
|
||||
if (count($errors)) {
|
||||
if(isset($errors['errno']) && $errors['errno'] == 403)
|
||||
return $this->exerr(403, __('Ticket denied'));
|
||||
else
|
||||
return $this->exerr(
|
||||
400,
|
||||
__("Unable to create new ticket: validation errors").":\n"
|
||||
.Format::array_implode(": ", "\n", $errors)
|
||||
);
|
||||
} elseif (!$ticket) {
|
||||
return $this->exerr(500, __("Unable to create new ticket: unknown error"));
|
||||
// Ticket denied? Say so loudly so it can standout from generic
|
||||
// validation errors
|
||||
if (isset($errors['errno']) && $errors['errno'] == 403) {
|
||||
$title = _S('Ticket denied');
|
||||
$error = sprintf("%s: %s\n\n%s",
|
||||
$title, $data['email'], $errors['err']);
|
||||
} else {
|
||||
// unpack the errors
|
||||
$error = Format::array_implode("\n", "\n", $errors);
|
||||
}
|
||||
} else {
|
||||
// unknown reason - default
|
||||
$error = _S('unknown error');
|
||||
}
|
||||
|
||||
return $ticket;
|
||||
$error = sprintf('%s :%s',
|
||||
_S('Unable to create new ticket'), $error);
|
||||
return $this->exerr($errors['errno'] ?: 500, $error, $title);
|
||||
}
|
||||
|
||||
function processEmail($data=false) {
|
||||
function processEmailRequest() {
|
||||
return $this->processEmail();
|
||||
}
|
||||
|
||||
if (!$data)
|
||||
$data = $this->getEmailRequest();
|
||||
function processEmail($data=false, array $defaults = []) {
|
||||
|
||||
try {
|
||||
if (!$data)
|
||||
$data = $this->getEmailRequest();
|
||||
elseif (!is_array($data))
|
||||
$data = $this->parseEmail($data);
|
||||
} catch (Exception $ex) {
|
||||
throw new EmailParseError($ex->getMessage());
|
||||
}
|
||||
|
||||
$data = array_merge($defaults, $data);
|
||||
$seen = false;
|
||||
if (($entry = ThreadEntry::lookupByEmailHeaders($data, $seen))
|
||||
&& ($message = $entry->postEmail($data))
|
||||
@@ -183,18 +213,34 @@ class TicketApiController extends ApiController {
|
||||
// All emails which do not appear to be part of an existing thread
|
||||
// will always create new "Tickets". All other objects will need to
|
||||
// be created via the web interface or the API
|
||||
return $this->createTicket($data);
|
||||
try {
|
||||
return $this->createTicket($data, 'Email');
|
||||
} catch (TicketApiError $err) {
|
||||
// Check if the ticket was denied by a filter or banlist
|
||||
if ($err->isDenied() && $data['mid']) {
|
||||
// We need to log the Message-Id (mid) so we don't
|
||||
// process the same email again in subsequent fetches
|
||||
$entry = new ThreadEntry();
|
||||
$entry->logEmailHeaders(0, $data['mid']);
|
||||
// throw TicketDenied exception so the caller can handle it
|
||||
// accordingly
|
||||
throw new TicketDenied($err->getMessage());
|
||||
} else {
|
||||
// otherwise rethrow this bad baby as it is!
|
||||
throw $err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Local email piping controller - no API key required!
|
||||
class PipeApiController extends TicketApiController {
|
||||
|
||||
//Overwrite grandparent's (ApiController) response method.
|
||||
// Overwrite grandparent's (ApiController) response method.
|
||||
function response($code, $resp) {
|
||||
|
||||
//Use postfix exit codes - instead of HTTP
|
||||
// It's important to use postfix exit codes for local piping instead
|
||||
// of HTTP's so the piping script can process them accordingly
|
||||
switch($code) {
|
||||
case 201: //Success
|
||||
$exitcode = 0;
|
||||
@@ -219,19 +265,33 @@ class PipeApiController extends TicketApiController {
|
||||
default: //Temp (unknown) failure - retry
|
||||
$exitcode = 75;
|
||||
}
|
||||
|
||||
//echo "$code ($exitcode):$resp";
|
||||
//We're simply exiting - MTA will take care of the rest based on exit code!
|
||||
exit($exitcode);
|
||||
}
|
||||
|
||||
static function process() {
|
||||
$pipe = new PipeApiController();
|
||||
if(($ticket=$pipe->processEmail()))
|
||||
return $pipe->response(201, $ticket->getNumber());
|
||||
static function process($sapi=null) {
|
||||
$pipe = new PipeApiController($sapi);
|
||||
if (($ticket=$pipe->processEmail()))
|
||||
return $pipe->response(201,
|
||||
is_object($ticket) ? $ticket->getNumber() : $ticket);
|
||||
|
||||
return $pipe->exerr(416, __('Request failed - retry again!'));
|
||||
}
|
||||
|
||||
static function local() {
|
||||
return self::process('cli');
|
||||
}
|
||||
}
|
||||
|
||||
class TicketApiError extends Exception {
|
||||
|
||||
// Check if exception is because of denial
|
||||
public function isDenied() {
|
||||
return ($this->getCode() === 403);
|
||||
}
|
||||
}
|
||||
|
||||
class TicketDenied extends Exception {}
|
||||
class EmailParseError extends Exception {}
|
||||
|
||||
?>
|
||||
|
||||
@@ -112,11 +112,11 @@ abstract class TwoFactorAuthenticationBackend extends ServiceRegistry {
|
||||
|| !($class instanceof TwoFactorAuthenticationBackend))
|
||||
return false;
|
||||
|
||||
static::$registry[$class->getId()] = $class;
|
||||
static::$registry[$class->getBkId()] = $class;
|
||||
}
|
||||
|
||||
static function allRegistered() {
|
||||
return self::getRegistry();
|
||||
return array_merge(self::$registry, parent::getRegistry());
|
||||
}
|
||||
|
||||
static function getBackend($id) {
|
||||
@@ -140,13 +140,18 @@ class ExpiredOTP extends Exception {}
|
||||
*/
|
||||
abstract class Staff2FABackend extends TwoFactorAuthenticationBackend {
|
||||
static protected $registry = array();
|
||||
static function allRegistered() {
|
||||
return array_merge(self::$registry, parent::allRegistered());
|
||||
}
|
||||
abstract function send($user);
|
||||
abstract function validate($form, $user);
|
||||
}
|
||||
|
||||
abstract class User2FABackend extends TwoFactorAuthenticationBackend {
|
||||
static protected $registry = array();
|
||||
|
||||
static function allRegistered() {
|
||||
return array_merge(self::$registry, parent::allRegistered());
|
||||
}
|
||||
abstract function send($user);
|
||||
abstract function validate($form, $user);
|
||||
}
|
||||
|
||||
@@ -24,14 +24,19 @@ require_once (INCLUDE_DIR.'class.api.php');
|
||||
* call controller should inherit from this class in order to maintain
|
||||
* consistency.
|
||||
*/
|
||||
class AjaxController extends Controller {
|
||||
class AjaxController extends ApiController {
|
||||
|
||||
function staffOnly() {
|
||||
global $thisstaff;
|
||||
if(!$thisstaff || !$thisstaff->isValid()) {
|
||||
Http::response(401,sprintf(__('Access Denied. IP %s'),$_SERVER['REMOTE_ADDR']));
|
||||
|
||||
if (!$thisstaff || !$thisstaff->isValid()) {
|
||||
$this->exerr(401, sprintf('%s (%s)',
|
||||
__('Access Denied'), $_SERVER['REMOTE_ADDR']));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a PHP array into a JSON-encoded string
|
||||
*/
|
||||
@@ -44,6 +49,6 @@ class AjaxController extends Controller {
|
||||
}
|
||||
|
||||
function get($var, $default=null) {
|
||||
return (isset($_GET[$var])) ? $_GET[$var] : $default;
|
||||
return $_GET[$var] ?: $default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ class API {
|
||||
$sql='INSERT INTO '.API_KEY_TABLE.' SET '.$sql
|
||||
.',created=NOW() '
|
||||
.',ipaddr='.db_input($vars['ipaddr'])
|
||||
.',apikey='.db_input(strtoupper(md5(time().$vars['ipaddr'].md5(Misc::randCode(16)))));
|
||||
.',apikey='.db_input(strtoupper(Misc::randCode(48, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')));
|
||||
|
||||
if(db_query($sql) && ($id=db_insert_id()))
|
||||
return $id;
|
||||
@@ -169,26 +169,49 @@ class API {
|
||||
|
||||
class ApiController extends Controller {
|
||||
|
||||
var $apikey;
|
||||
private $sapi;
|
||||
private $key;
|
||||
|
||||
public function __construct($sapi = null) {
|
||||
// Set API Interface the request is coming from
|
||||
$this->sapi = $sapi ?: (osTicket::is_cli() ? 'cli' : 'http');
|
||||
}
|
||||
|
||||
public function isCli() {
|
||||
return (strcasecmp($this->sapi, 'cli') === 0);
|
||||
}
|
||||
|
||||
private function getInputStream() {
|
||||
return $this->isCli() ? 'php://stdin' : 'php://input';
|
||||
}
|
||||
|
||||
protected function getRemoteAddr() {
|
||||
return $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
|
||||
protected function getApiKey() {
|
||||
return $_SERVER['HTTP_X_API_KEY'];
|
||||
}
|
||||
|
||||
function requireApiKey() {
|
||||
# Validate the API key -- required to be sent via the X-API-Key
|
||||
# header
|
||||
|
||||
if(!($key=$this->getApiKey()))
|
||||
// Require a valid API key sent as X-API-Key HTTP header
|
||||
// see getApiKey method.
|
||||
if (!($key=$this->getKey()))
|
||||
return $this->exerr(401, __('Valid API key required'));
|
||||
elseif (!$key->isActive() || $key->getIPAddr()!=$_SERVER['REMOTE_ADDR'])
|
||||
elseif (!$key->isActive() || $key->getIPAddr() != $this->getRemoteAddr())
|
||||
return $this->exerr(401, __('API key not found/active or source IP not authorized'));
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
function getApiKey() {
|
||||
function getKey() {
|
||||
// Lookup record using sent API Key && IP Addr
|
||||
if (!$this->key
|
||||
&& ($key=$this->getApiKey())
|
||||
&& ($ip=$this->getRemoteAddr()))
|
||||
$this->key = API::lookupByKey($key, $ip);
|
||||
|
||||
if (!$this->apikey && isset($_SERVER['HTTP_X_API_KEY']) && isset($_SERVER['REMOTE_ADDR']))
|
||||
$this->apikey = API::lookupByKey($_SERVER['HTTP_X_API_KEY'], $_SERVER['REMOTE_ADDR']);
|
||||
|
||||
return $this->apikey;
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,20 +219,28 @@ class ApiController extends Controller {
|
||||
* hashtable. For JSON formats, this is mostly a noop, the conversion
|
||||
* work will be done for XML requests
|
||||
*/
|
||||
function getRequest($format) {
|
||||
global $ost;
|
||||
|
||||
$input = osTicket::is_cli()?'php://stdin':'php://input';
|
||||
|
||||
function getRequest($format, $validate=true) {
|
||||
$input = $this->getInputStream();
|
||||
if (!($stream = @fopen($input, 'r')))
|
||||
$this->exerr(400, __("Unable to read request body"));
|
||||
$this->exerr(400, sprintf('%s (%s)',
|
||||
__("Unable to read request body"), $input));
|
||||
|
||||
return $this->parseRequest($stream, $format, $validate);
|
||||
}
|
||||
|
||||
function getEmailRequest() {
|
||||
if (!($data=$this->getRequest('email', false)))
|
||||
$this->exerr(400, __("Unable to read email request"));
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
function parseRequest($stream, $format, $validate=true) {
|
||||
$parser = null;
|
||||
switch(strtolower($format)) {
|
||||
case 'xml':
|
||||
if (!function_exists('xml_parser_create'))
|
||||
$this->exerr(501, __('XML extension not supported'));
|
||||
|
||||
return $this->exerr(501, __('XML extension not supported'));
|
||||
$parser = new ApiXmlDataParser();
|
||||
break;
|
||||
case 'json':
|
||||
@@ -219,23 +250,24 @@ class ApiController extends Controller {
|
||||
$parser = new ApiEmailDataParser();
|
||||
break;
|
||||
default:
|
||||
$this->exerr(415, __('Unsupported data format'));
|
||||
return $this->exerr(415, __('Unsupported data format'));
|
||||
}
|
||||
|
||||
if (!($data = $parser->parse($stream)))
|
||||
if (!($data = $parser->parse($stream)) || !is_array($data)) {
|
||||
$this->exerr(400, $parser->lastError());
|
||||
}
|
||||
|
||||
//Validate structure of the request.
|
||||
$this->validate($data, $format, false);
|
||||
if ($validate && $data)
|
||||
$this->validate($data, $format, false);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
function getEmailRequest() {
|
||||
return $this->getRequest('email');
|
||||
function parseEmail($content) {
|
||||
return $this->parseRequest($content, 'email', false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Structure to validate the request against -- must be overridden to be
|
||||
* useful
|
||||
@@ -259,14 +291,11 @@ class ApiController extends Controller {
|
||||
} elseif (in_array($key, $structure)) {
|
||||
continue;
|
||||
}
|
||||
if ($strict)
|
||||
return $this->exerr(400, sprintf(__("%s: Unexpected data received in API request"), "$prefix$key"));
|
||||
else
|
||||
$ost->logWarning(__('API Unexpected Data'),
|
||||
sprintf(__("%s: Unexpected data received in API request"), "$prefix$key"),
|
||||
false);
|
||||
}
|
||||
|
||||
$error = sprintf(__("%s: Unexpected data received in API request"),
|
||||
"$prefix$key");
|
||||
$this->onError(400, $error, __('Unexpected Data'), !$strict);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -282,37 +311,66 @@ class ApiController extends Controller {
|
||||
$strict);
|
||||
}
|
||||
|
||||
protected function debug($subj, $msg) {
|
||||
return $this->log($subj, $msg, LOG_DEBUG);
|
||||
}
|
||||
|
||||
protected function logError($subj, $msg, $fatal=false) {
|
||||
// If error is not fatal then log it as a warning
|
||||
return $this->log($subj, $msg, $fatal ? LOG_ERR : LOG_WARN);
|
||||
}
|
||||
|
||||
protected function log($title, $msg, $level = LOG_WARN) {
|
||||
global $ost;
|
||||
switch ($level) {
|
||||
case LOG_WARN:
|
||||
case LOG_ERR:
|
||||
// We are disabling email alerts on API errors / warnings
|
||||
// due to potential abuse or loops that might cause email DOS
|
||||
$ost->log($level, $title, $msg, false);
|
||||
break;
|
||||
case LOG_DEBUG:
|
||||
default:
|
||||
$ost->logDebug($title, $msg);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* API error & logging and response!
|
||||
*
|
||||
* final - don not override downstream.
|
||||
*/
|
||||
final public function onError($code, $error, $title = null,
|
||||
$logOnly = false) {
|
||||
|
||||
/* If possible - DO NOT - overwrite the method downstream */
|
||||
function exerr($code, $error='') {
|
||||
global $ost;
|
||||
|
||||
if($error && is_array($error))
|
||||
// Unpack the errors to string if error is an array
|
||||
if ($error && is_array($error))
|
||||
$error = Format::array_implode(": ", "\n", $error);
|
||||
|
||||
//Log the error as a warning - include api key if available.
|
||||
// Log the error
|
||||
$msg = $error;
|
||||
if($_SERVER['HTTP_X_API_KEY'])
|
||||
$msg.="\n*[".$_SERVER['HTTP_X_API_KEY']."]*\n";
|
||||
$ost->logWarning(__('API Error')." ($code)", $msg, false);
|
||||
// TODO: Only include API Key when in debug mode to avoid
|
||||
// potentialialy leaking a valid key in system logs
|
||||
if (($key=$this->getApiKey()))
|
||||
$msg .= sprintf("\n[%s]\n", Format::shroud($key, 8, 16));
|
||||
|
||||
if (PHP_SAPI == 'cli') {
|
||||
fwrite(STDERR, "({$code}) $error\n");
|
||||
}
|
||||
else {
|
||||
$this->response($code, $error); //Responder should exit...
|
||||
}
|
||||
return false;
|
||||
}
|
||||
$title = sprintf('%s (%s)', $title ?: __('API Error'), $code);
|
||||
$this->logError($title, $msg, $logOnly);
|
||||
|
||||
//Default response method - can be overwritten in subclasses.
|
||||
function response($code, $resp) {
|
||||
Http::response($code, $resp);
|
||||
exit();
|
||||
// If the error is not deemed fatal then simply return
|
||||
if ($logOnly)
|
||||
return;
|
||||
|
||||
// If the API Interface is CLI then throw a TicketApiError exception
|
||||
// so the caller can handle the error gracefully.
|
||||
// Note that we set the error code as well
|
||||
if ($this->isCli())
|
||||
throw new TicketApiError($error, $code);
|
||||
|
||||
// Respond and exit since HTTP endpoint requests
|
||||
// are considered stateless
|
||||
$this->response($code, $error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,8 +387,8 @@ class ApiXmlDataParser extends XmlDataParser {
|
||||
function fixup($current) {
|
||||
global $cfg;
|
||||
|
||||
if($current['ticket'])
|
||||
$current = $current['ticket'];
|
||||
if (isset($current['ticket']))
|
||||
$current = $current['ticket'];
|
||||
|
||||
if (!is_array($current))
|
||||
return $current;
|
||||
@@ -436,17 +494,17 @@ class ApiEmailDataParser extends EmailDataParser {
|
||||
function fixup($data) {
|
||||
global $cfg;
|
||||
|
||||
if(!$data) return $data;
|
||||
if (!$data) return $data;
|
||||
|
||||
$data['source'] = 'Email';
|
||||
|
||||
if(!$data['subject'])
|
||||
if (!$data['subject'])
|
||||
$data['subject'] = '[No Subject]';
|
||||
|
||||
if(!$data['emailId'])
|
||||
if (!$data['emailId'])
|
||||
$data['emailId'] = $cfg->getDefaultEmailId();
|
||||
|
||||
if(!$cfg->useEmailPriority())
|
||||
if( !$cfg->useEmailPriority())
|
||||
unset($data['priorityId']);
|
||||
|
||||
return $data;
|
||||
|
||||
@@ -120,10 +120,11 @@ extends InstrumentedList {
|
||||
}
|
||||
$attachments = array();
|
||||
// Format $new for upload() with new name
|
||||
foreach ($ids as $id=>$name) {
|
||||
foreach ($ids as $id=>$value) {
|
||||
if (is_array($value)) list('id' => $id, 'name' => $value) = $value;
|
||||
$attachments[] = array(
|
||||
'id' => $id,
|
||||
'name' => $name
|
||||
'name' => $value
|
||||
);
|
||||
}
|
||||
// Everything remaining in $attachments is truly new
|
||||
@@ -191,7 +192,7 @@ extends InstrumentedList {
|
||||
function getInlines($lang=false) { return $this->_getList(false, true, $lang); }
|
||||
function getSeparates($lang=false) { return $this->_getList(true, false, $lang); }
|
||||
function getAll($lang=false) { return $this->_getList(true, true, $lang); }
|
||||
function count($lang=false) { return count($this->getSeparates($lang)); }
|
||||
function count($lang=false): int { return count($this->getSeparates($lang)); }
|
||||
|
||||
function _getList($separates=false, $inlines=false, $lang=false) {
|
||||
$base = $this;
|
||||
|
||||
@@ -178,7 +178,7 @@ abstract class OAuth2Backend extends ServiceRegistry {
|
||||
|| !($bk instanceof OAuth2Backend))
|
||||
return false;
|
||||
|
||||
static::$registry[$bk->getId()] = $bk;
|
||||
static::$registry[$bk->getBkId()] = $bk;
|
||||
}
|
||||
|
||||
static function allRegistered() {
|
||||
@@ -330,7 +330,7 @@ abstract class AuthenticationBackend extends ServiceRegistry {
|
||||
foreach (static::allRegistered() as $bk) {
|
||||
if ($backends //Allowed backends
|
||||
&& $bk->supportsInteractiveAuthentication()
|
||||
&& !in_array($bk->getId(), $backends))
|
||||
&& !in_array($bk->getBkId(), $backends))
|
||||
// User cannot be authenticated against this backend
|
||||
continue;
|
||||
|
||||
@@ -419,7 +419,7 @@ abstract class AuthenticationBackend extends ServiceRegistry {
|
||||
if ($bk instanceof AuthDirectorySearch)
|
||||
$backends[$bk->getBkId()] = $bk;
|
||||
|
||||
return array_unique($backends);
|
||||
return array_unique($backends, SORT_REGULAR);
|
||||
}
|
||||
|
||||
static function searchUsers($query) {
|
||||
@@ -589,7 +589,7 @@ abstract class StaffAuthenticationBackend extends AuthenticationBackend {
|
||||
if (!($backends=self::getAllowedBackends($staff->getId())))
|
||||
return true; //No restrictions
|
||||
|
||||
return in_array($bk->getId(), array_map('strtolower', $backends));
|
||||
return in_array($bk->getBkId(), array_map('strtolower', $backends));
|
||||
}
|
||||
|
||||
function getPasswordPolicies($user=null) {
|
||||
@@ -597,7 +597,7 @@ abstract class StaffAuthenticationBackend extends AuthenticationBackend {
|
||||
$policies = PasswordPolicy::allActivePolicies();
|
||||
if ($cfg && ($policy = $cfg->getStaffPasswordPolicy())) {
|
||||
foreach ($policies as $P)
|
||||
if ($policy == $P->getId())
|
||||
if ($policy == $P->getBkId())
|
||||
return array($P);
|
||||
}
|
||||
|
||||
@@ -633,8 +633,6 @@ abstract class StaffAuthenticationBackend extends AuthenticationBackend {
|
||||
|| !($authkey=$bk->getAuthKey($staff)))
|
||||
return false;
|
||||
|
||||
// Regenerate session id and refresh (avoid session fixation)
|
||||
$staff->regenerateSession();
|
||||
//Log debug info.
|
||||
$ost->logDebug(_S('Agent Login'),
|
||||
sprintf(_S("%s logged in [%s], via %s"), $staff->getUserName(),
|
||||
@@ -660,6 +658,8 @@ abstract class StaffAuthenticationBackend extends AuthenticationBackend {
|
||||
$authsession['id'] = $staff->getId();
|
||||
$authsession['key'] = $authkey;
|
||||
$authsession['2fa'] = $auth2fa;
|
||||
// Set TIME_BOMB to regenerate the session 10 seconds after login
|
||||
$_SESSION['TIME_BOMB'] = time() + 10;
|
||||
// Set session token
|
||||
$staff->setSessionToken();
|
||||
// Set Auth Key
|
||||
@@ -850,8 +850,6 @@ abstract class UserAuthenticationBackend extends AuthenticationBackend {
|
||||
throw new AccessDenied(__('Account is administratively locked'));
|
||||
}
|
||||
|
||||
// Regenerate session id and refresh (avoid session fixation)
|
||||
$user->regenerateSession();
|
||||
// Tag the user and associated ticket in the SESSION
|
||||
$this->setAuthKey($user, $bk, $authkey);
|
||||
// Set Session Token
|
||||
@@ -869,6 +867,9 @@ abstract class UserAuthenticationBackend extends AuthenticationBackend {
|
||||
$type = array('type' => 'login');
|
||||
Signal::send('person.login', $u, $type);
|
||||
|
||||
// Set TIME_BOMB to regenerate the session 10 seconds after login
|
||||
$_SESSION['TIME_BOMB'] = time() + 10;
|
||||
|
||||
if ($bk->supportsInteractiveAuthentication() && ($acct=$user->getAccount()))
|
||||
$acct->cancelResetTokens();
|
||||
|
||||
@@ -1254,7 +1255,7 @@ class PasswordResetTokenBackend extends StaffAuthenticationBackend {
|
||||
|| $id != $staff->getId())
|
||||
$errors['msg'] = __('Invalid reset token');
|
||||
elseif (!($ts = $_config->lastModified($_POST['token']))
|
||||
&& ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
|
||||
|| ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
|
||||
$errors['msg'] = __('Invalid reset token');
|
||||
elseif (!$staff->forcePasswdRest())
|
||||
$errors['msg'] = __('Unable to reset password');
|
||||
@@ -1481,7 +1482,7 @@ class ClientPasswordResetTokenBackend extends UserAuthenticationBackend {
|
||||
|| $id != 'c'.$client->getId())
|
||||
$errors['msg'] = __('Invalid reset token');
|
||||
elseif (!($ts = $_config->lastModified($_POST['token']))
|
||||
&& ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
|
||||
|| ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
|
||||
$errors['msg'] = __('Invalid reset token');
|
||||
elseif (!$acct->forcePasswdReset())
|
||||
$errors['msg'] = __('Unable to reset password');
|
||||
@@ -1593,7 +1594,7 @@ abstract class PasswordPolicy extends ServiceRegistry {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SessionData::objects()->filter($criteria)->delete();
|
||||
return DatabaseSessionRecord::objects()->filter($criteria)->delete();
|
||||
}
|
||||
}
|
||||
Signal::connect('auth.clean', array('PasswordPolicy', 'cleanSessions'));
|
||||
@@ -1621,9 +1622,13 @@ extends PasswordPolicy {
|
||||
}
|
||||
|
||||
function onSet($passwd, $current) {
|
||||
if (strlen($passwd) < 6) {
|
||||
$pwdlen = strlen($passwd);
|
||||
if ($pwdlen < 6) {
|
||||
throw new BadPassword(
|
||||
__('Password must be at least 6 characters'));
|
||||
} elseif ($pwdlen > 128) {
|
||||
throw new BadPassword(
|
||||
sprintf('%s - %s', __('Password is too long'), __('maximum of 128 characters allowed')));
|
||||
}
|
||||
// XXX: Changing case is technicall changing the password
|
||||
if (0 === strcasecmp($passwd, $current)) {
|
||||
|
||||
@@ -311,6 +311,12 @@ extends VerySimpleModel {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function staffCanAccess($staff) {
|
||||
if (!$staff instanceof Staff)
|
||||
return false;
|
||||
return (!$this->dept || (($role = $staff->getRole($this->dept)) && $role->hasPerm(Canned::PERM_MANAGE)));
|
||||
}
|
||||
}
|
||||
RolePermission::register( /* @trans */ 'Knowledgebase', Canned::getPermissions());
|
||||
|
||||
|
||||
@@ -151,6 +151,19 @@ implements EmailContact, ITicketUser, TemplateVariable {
|
||||
return $this->user->getId();
|
||||
}
|
||||
|
||||
function getEmailAddress() {
|
||||
$emailaddr = (string) $this->getEmail();
|
||||
if (($name=$this->getName()))
|
||||
$emailaddr = sprintf('"%s" <%s>',
|
||||
(string) $name,
|
||||
$emailaddr);
|
||||
return $emailaddr;
|
||||
}
|
||||
|
||||
function __toString() {
|
||||
return $this->getEmailAddress();
|
||||
}
|
||||
|
||||
abstract function getTicketId();
|
||||
abstract function getTicket();
|
||||
}
|
||||
@@ -484,7 +497,7 @@ class ClientAccount extends UserAccount {
|
||||
if ($vars['backend']) {
|
||||
$this->set('backend', $vars['backend']);
|
||||
if ($vars['username'])
|
||||
$this->set('username', $vars['username']);
|
||||
$this->set('username', Format::sanitize($vars['username']));
|
||||
}
|
||||
|
||||
if ($vars['passwd1']) {
|
||||
|
||||
@@ -39,12 +39,6 @@ implements EmailContact, ITicketUser {
|
||||
|
||||
var $active;
|
||||
|
||||
function __toString() {
|
||||
return Format::htmlchars($this->toString());
|
||||
}
|
||||
function toString() {
|
||||
return sprintf('"%s" <%s>', $this->getName(), $this->getEmail());
|
||||
}
|
||||
|
||||
function getId() {
|
||||
return $this->id;
|
||||
@@ -81,6 +75,16 @@ implements EmailContact, ITicketUser {
|
||||
function getEmail() {
|
||||
return $this->user->getEmail();
|
||||
}
|
||||
|
||||
function getEmailAddress() {
|
||||
$emailaddr = (string) $this->getEmail();
|
||||
if (($name=$this->getName()))
|
||||
$emailaddr = sprintf('"%s" <%s>',
|
||||
(string) $name,
|
||||
$emailaddr);
|
||||
return $emailaddr;
|
||||
}
|
||||
|
||||
function getName() {
|
||||
return $this->user->getName();
|
||||
}
|
||||
@@ -135,6 +139,10 @@ implements EmailContact, ITicketUser {
|
||||
return $this->user_id;
|
||||
}
|
||||
|
||||
function __toString() {
|
||||
return Format::htmlchars($this->getEmailAddress());
|
||||
}
|
||||
|
||||
function hasFlag($flag) {
|
||||
return ($this->get('flags', 0) & $flag) != 0;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,12 @@ class Config {
|
||||
}
|
||||
|
||||
function exists($key) {
|
||||
return $this->get($key, null) ? true : false;
|
||||
if ((isset($this->session) && array_key_exists($key, $this->session))
|
||||
|| array_key_exists($key, $this->config)
|
||||
|| array_key_exists($key, $this->defaults))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function set($key, $value) {
|
||||
@@ -190,7 +195,9 @@ extends VerySimpleModel {
|
||||
))->delete();
|
||||
}
|
||||
|
||||
static function getConfigsByNamespace(?string $namespace=null, $key, $value=false) {
|
||||
|
||||
static function getConfigsByNamespace(?string $namespace, $key, $value=false) {
|
||||
|
||||
$filter = array();
|
||||
|
||||
$filter['key'] = $key;
|
||||
|
||||
@@ -23,33 +23,22 @@ abstract class Controller {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* error & logging and response!
|
||||
*
|
||||
*/
|
||||
function exerr($code, $error='') {
|
||||
global $ost;
|
||||
|
||||
if ($error && is_array($error))
|
||||
$error = Format::array_implode(": ", "\n", $error);
|
||||
|
||||
//Log the error as a warning - include api key if available.
|
||||
$msg = $error;
|
||||
if ($_SERVER['HTTP_X_API_KEY'])
|
||||
$msg.="\n*[".$_SERVER['HTTP_X_API_KEY']."]*\n";
|
||||
$ost->logWarning(__('Error')." ($code)", $msg, false);
|
||||
|
||||
if (PHP_SAPI == 'cli') {
|
||||
fwrite(STDERR, "({$code}) $error\n");
|
||||
} else {
|
||||
$this->response($code, $error); //Responder should exit...
|
||||
}
|
||||
return false;
|
||||
// Wrapper for onFatalError
|
||||
public function exerr($code, $error='') {
|
||||
return $this->onFatalError($code, $error);
|
||||
}
|
||||
|
||||
//Default response method - can be overwritten in subclasses.
|
||||
function response($code, $resp) {
|
||||
Http::response($code, $resp);
|
||||
public function onFatalError($code, $error) {
|
||||
$this->onError($code, $error);
|
||||
// On error should exit but we're making doubly sure
|
||||
$this->response($code, $error);
|
||||
exit();
|
||||
}
|
||||
|
||||
protected function response($code, $response) {
|
||||
Http::response($code, $response);
|
||||
exit();
|
||||
}
|
||||
|
||||
abstract function onError($code, $error, $title=null, $logOnly=false);
|
||||
}
|
||||
|
||||
@@ -53,8 +53,7 @@ class Cron {
|
||||
|
||||
static function CleanExpiredSessions() {
|
||||
require_once(INCLUDE_DIR.'class.ostsession.php');
|
||||
$backend = new DbSessionBackend();
|
||||
$backend->cleanup();
|
||||
osTicketSession::cleanup();
|
||||
}
|
||||
|
||||
static function CleanPwResets() {
|
||||
|
||||
@@ -604,7 +604,7 @@ implements TemplateVariable, Searchable {
|
||||
return true;
|
||||
}
|
||||
|
||||
function export($dept, $criteria=null, $filename='') {
|
||||
static function export($dept, $criteria=null, $filename='') {
|
||||
include_once(INCLUDE_DIR.'class.error.php');
|
||||
$members = $dept->getMembers();
|
||||
|
||||
|
||||
@@ -29,10 +29,14 @@ class Dispatcher {
|
||||
function resolve($url, $args=null) {
|
||||
if ($this->file) { $this->lazy_load(); }
|
||||
# Support HTTP method emulation with the _method GET argument
|
||||
if (isset($_GET['_method'])) {
|
||||
$_SERVER['REQUEST_METHOD'] = strtoupper($_GET['_method']);
|
||||
unset($_GET['_method']);
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST'
|
||||
&& isset($_POST['_method'])
|
||||
&& in_array(strtoupper($_POST['_method']), array('PUT', 'PATCH', 'DELETE'))) {
|
||||
$_SERVER['REQUEST_METHOD'] = strtoupper($_POST['_method']);
|
||||
unset($_POST['_method']);
|
||||
}
|
||||
// Decode URL for accurate matching
|
||||
$url = urldecode($url);
|
||||
foreach ($this->urls as $matcher) {
|
||||
if ($matcher->matches($url)) {
|
||||
return $matcher->dispatch($url, $args);
|
||||
@@ -166,7 +170,7 @@ class UrlMatcher {
|
||||
|
||||
if (strpos($class, ":")) {
|
||||
list($file, $class) = explode(":", $class, 2);
|
||||
include $file;
|
||||
include_once $file;
|
||||
}
|
||||
return array($class, $func);
|
||||
}
|
||||
|
||||
@@ -65,9 +65,9 @@ class Draft extends VerySimpleModel {
|
||||
$draft_body = $draft->getBody();
|
||||
}
|
||||
$attrs[] = sprintf('data-draft-original="%s"',
|
||||
Format::htmlchars(Format::viewableImages($original)));
|
||||
Format::viewableImages($original, [], true));
|
||||
|
||||
return array(Format::htmlchars(Format::viewableImages($draft_body)),
|
||||
return array(Format::viewableImages($draft_body, [], true),
|
||||
implode(' ', $attrs));
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ class Draft extends VerySimpleModel {
|
||||
foreach ($files as $F) {
|
||||
$attachments[] = array(
|
||||
'id' => $F->getId(),
|
||||
'name' => $F->getName(),
|
||||
'inline' => true
|
||||
);
|
||||
}
|
||||
|
||||
@@ -914,7 +914,7 @@ class DynamicFormField extends VerySimpleModel {
|
||||
/* `variable` is used for automation. Internally it's called `name` */
|
||||
), "name");
|
||||
}
|
||||
if ($this->get('name') && !preg_match('/^(?!\d)([[:alnum:]]|_|-)+$/u', $this->get('name')))
|
||||
if ($this->get('name') && !preg_match('/^(?!\d)([[:alnum:]]|_)+$/u', $this->get('name')))
|
||||
$this->addError(__(
|
||||
'Invalid character in variable name. Please use letters and numbers only.'
|
||||
), 'name');
|
||||
@@ -1034,6 +1034,10 @@ class DynamicFormEntry extends VerySimpleModel {
|
||||
return $this->form->getInstructions();
|
||||
}
|
||||
|
||||
function getNotice() {
|
||||
return $this->form->getNotice();
|
||||
}
|
||||
|
||||
function getDynamicForm() {
|
||||
return $this->form;
|
||||
}
|
||||
@@ -1211,7 +1215,7 @@ class DynamicFormEntry extends VerySimpleModel {
|
||||
return $entries[$ticket_id];
|
||||
}
|
||||
|
||||
function forTask($id, $force=false) {
|
||||
static function forTask($id, $force=false) {
|
||||
static $entries = array();
|
||||
if (!isset($entries[$id]) || $force) {
|
||||
$stuff = DynamicFormEntry::objects()->filter(array(
|
||||
@@ -1351,7 +1355,7 @@ class DynamicFormEntry extends VerySimpleModel {
|
||||
//use getChanges if getClean returns an empty array
|
||||
$fieldClean = $field->getClean() ?: $field->getChanges();
|
||||
if (is_array($fieldClean) && $fieldClean[0])
|
||||
$fieldClean = json_decode($fieldClean[0], true);
|
||||
$fieldClean = is_string($fieldClean[0]) ? json_decode($fieldClean[0], true) : $fieldClean[0];
|
||||
} else
|
||||
$fieldClean = $field->getClean();
|
||||
|
||||
@@ -1589,7 +1593,7 @@ class SelectionField extends FormField {
|
||||
$selection = array();
|
||||
|
||||
if ($value && !is_array($value))
|
||||
$value = array($value);
|
||||
$value = JsonDataParser::parse($value) ?: array($value);
|
||||
|
||||
if ($value && is_array($value)) {
|
||||
foreach ($value as $k=>$v) {
|
||||
@@ -1629,6 +1633,7 @@ class SelectionField extends FormField {
|
||||
$values = array();
|
||||
$choices = $this->getChoices();
|
||||
foreach (explode(',', $value) as $V) {
|
||||
$V = trim($V);
|
||||
if (isset($choices[$V]))
|
||||
$values[$V] = $choices[$V];
|
||||
}
|
||||
@@ -1729,6 +1734,7 @@ class SelectionField extends FormField {
|
||||
?: __('Unknown or invalid input');
|
||||
}
|
||||
} elseif ($config['typeahead']
|
||||
&& $entry
|
||||
&& ($entered = $this->getWidget()->getEnteredValue())
|
||||
&& !in_array($entered, $entry)
|
||||
&& $entered != $entry) {
|
||||
@@ -1912,7 +1918,7 @@ class SelectionField extends FormField {
|
||||
$name = $name ?: $this->get('name');
|
||||
$val = $value;
|
||||
if ($value && is_array($value))
|
||||
$val = '"?'.implode('("|,|$)|"?', array_keys($value)).'("|,|$)';
|
||||
$val = '"?(?<![0-9])'.implode('("|,|$)|"?(?<![0-9])', array_keys($value)).'("|,|$)';
|
||||
switch ($method) {
|
||||
case '!includes':
|
||||
return Q::not(array("{$name}__regex" => $val));
|
||||
|
||||
@@ -17,6 +17,7 @@ include_once(INCLUDE_DIR.'class.dept.php');
|
||||
include_once(INCLUDE_DIR.'class.mail.php');
|
||||
include_once(INCLUDE_DIR.'class.mailer.php');
|
||||
include_once(INCLUDE_DIR.'class.oauth2.php');
|
||||
include_once(INCLUDE_DIR.'class.mime.php');
|
||||
include_once(INCLUDE_DIR.'class.mailfetch.php');
|
||||
include_once(INCLUDE_DIR.'class.mailparse.php');
|
||||
include_once(INCLUDE_DIR.'api.tickets.php');
|
||||
@@ -73,17 +74,42 @@ class Email extends VerySimpleModel {
|
||||
return $this->getAddress();
|
||||
}
|
||||
|
||||
function stash($key, $data) {
|
||||
// TODO: move stash/restore to a StashableTrait
|
||||
function restore($key, $drop=true) {
|
||||
if (($data = $this->stash($key, null)) && $drop)
|
||||
$this->stash[$key] = null;
|
||||
return $data;
|
||||
}
|
||||
|
||||
function stash($key, $data=null) {
|
||||
if (!isset($this->stash))
|
||||
$this->stash = &$_SESSION[':email'][$this->getId()];
|
||||
|
||||
$this->stash[$key] = $data;
|
||||
// If data is null then stash is being pop-ed
|
||||
if (!isset($data) && $key && isset($this->stash[$key]))
|
||||
return $this->stash[$key];
|
||||
|
||||
// stash data
|
||||
if ($key && $data)
|
||||
$this->stash[$key] = $data;
|
||||
}
|
||||
|
||||
function stashFormData(array $data) {
|
||||
$this->stash('formdata', array_filter($data));
|
||||
}
|
||||
|
||||
function restoreFormData($drop=true) {
|
||||
return $this->restore('formdata', $drop) ?: [];
|
||||
}
|
||||
|
||||
function restoreErrors($drop=true) {
|
||||
return $this->restore('errors', $drop) ?: [];
|
||||
}
|
||||
|
||||
function restoreNotice($drop=true) {
|
||||
return $this->restore('notice', $drop);
|
||||
}
|
||||
|
||||
function getEmail() {
|
||||
return $this->email;
|
||||
}
|
||||
@@ -165,6 +191,10 @@ class Email extends VerySimpleModel {
|
||||
// SMTP Account Info
|
||||
if (($smtp=$this->getSmtpAccount()))
|
||||
$info = array_merge($info, $smtp->getInfo());
|
||||
// Restore stahed formdata (if any)
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'GET'
|
||||
&& ($data=$this->restoreFormData()))
|
||||
$info = array_merge($info, $data);
|
||||
|
||||
return $info;
|
||||
}
|
||||
@@ -305,12 +335,12 @@ class Email extends VerySimpleModel {
|
||||
if ($errors) return false;
|
||||
|
||||
// Update basic settings
|
||||
$this->email = $vars['email'];
|
||||
$this->email = Format::sanitize($vars['email']);
|
||||
$this->name = Format::striptags($vars['name']);
|
||||
$this->dept_id = $vars['dept_id'];
|
||||
$this->priority_id = isset($vars['priority_id']) ? $vars['priority_id'] : '0';
|
||||
$this->topic_id = $vars['topic_id'];
|
||||
$this->noautoresp = $vars['noautoresp'];
|
||||
$this->dept_id = (int) $vars['dept_id'];
|
||||
$this->priority_id = (int) (isset($vars['priority_id']) ? $vars['priority_id'] : 0);
|
||||
$this->topic_id = (int) $vars['topic_id'];
|
||||
$this->noautoresp = (int) $vars['noautoresp'];
|
||||
$this->notes = Format::sanitize($vars['notes']);
|
||||
|
||||
if ($this->save())
|
||||
@@ -393,9 +423,28 @@ class EmailAccount extends VerySimpleModel {
|
||||
private $instance;
|
||||
// If account supports tls or ssl
|
||||
private $encryption = false;
|
||||
// Account settings
|
||||
private $settings;
|
||||
|
||||
public function getAccountOptions() {
|
||||
return new osTicket\Mail\AccountOptions($this);
|
||||
|
||||
public function getAccountSetting($stashed=false) {
|
||||
|
||||
if (!isset($this->settings)) {
|
||||
// Set properties to stashed form data (if any and requested)
|
||||
if ($stashed && ($info=$this->getInfo())) {
|
||||
foreach (['host', 'port', 'protocol'] as $p) {
|
||||
$k = "{$this->type}_$p";
|
||||
if (isset($info[$k]))
|
||||
$this->{$p} = $info[$k];
|
||||
}
|
||||
}
|
||||
$this->settings = new osTicket\Mail\AccountSetting($this);
|
||||
}
|
||||
return $this->settings;
|
||||
}
|
||||
|
||||
public function getHostInfo() {
|
||||
return $this->getAccountSetting()->getHostInfo();
|
||||
}
|
||||
|
||||
public function getHost() {
|
||||
@@ -406,8 +455,8 @@ class EmailAccount extends VerySimpleModel {
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
public function getEncryption() {
|
||||
return $this->encryption;
|
||||
public function getProtocol() {
|
||||
return $this->protocol;
|
||||
}
|
||||
|
||||
public function getNumErrors() {
|
||||
@@ -423,29 +472,48 @@ class EmailAccount extends VerySimpleModel {
|
||||
}
|
||||
|
||||
public function isActive() {
|
||||
return $this->active;
|
||||
return ($this->active && $this->hasCredentials());
|
||||
}
|
||||
|
||||
// **** Don't use it ****
|
||||
// This routine is depricated and will be removed in the future - OAuth2
|
||||
// Plugin uses it to check if the email accout has auth2 backend.
|
||||
public function isEnabled() {
|
||||
return $this->isOAuthAuth();
|
||||
}
|
||||
|
||||
public function isAuthBackendEnabled() {
|
||||
return $this->isOAuthAuth()
|
||||
? (($i=$this->getOAuth2Instance()) && $i->isEnabled())
|
||||
: true;
|
||||
}
|
||||
|
||||
public function isStrict() {
|
||||
return $this->getConfig()->getStrictMatching();
|
||||
}
|
||||
|
||||
public function checkStrictMatching($token=null) {
|
||||
$token ??= $this->getAccessToken();
|
||||
return ($token && $token->isMatch(
|
||||
$this->getEmail()->getEmail(),
|
||||
$this->isStrict()));
|
||||
}
|
||||
|
||||
public function shouldAuthorize() {
|
||||
// check status and make sure it's oauth
|
||||
if (!$this->isEnabled() || !$this->isOAuthAuth())
|
||||
if (!$this->isAuthBackendEnabled() || !$this->isOAuthAuth())
|
||||
return false;
|
||||
|
||||
// No credentials stored or token expired (referesh failed)
|
||||
if (!($cred=$this->getFreshCredentials())
|
||||
|| !($token=$cred->getAccessToken())
|
||||
|| $token->isExpired())
|
||||
return true;
|
||||
return (!($cred=$this->getFreshCredentials())
|
||||
// Get token with signature match - mismatch means config
|
||||
// changed somehow
|
||||
|| !($token=$cred->getAccessToken($this->getConfigSignature()))
|
||||
// Check if expired
|
||||
|| $token->isExpired()
|
||||
// If Strict Matching is enabled ensure the email matches
|
||||
// the Resource Owner
|
||||
|| !$this->checkStrictMatching($token));
|
||||
|
||||
// Signature mismatch - means config changed
|
||||
return strcasecmp($token->getConfigSignature(),
|
||||
$this->getConfigSignature());
|
||||
}
|
||||
|
||||
public function getId() {
|
||||
@@ -469,7 +537,8 @@ class EmailAccount extends VerySimpleModel {
|
||||
$id = sprintf('%s:%d',
|
||||
$this->getAuthBk(), $this->getId());
|
||||
if ($this->isOAuthAuth())
|
||||
$id .= sprintf(':%d', $this->getAuthId());
|
||||
$id .= sprintf(':%d:%b',
|
||||
$this->getAuthId(), $this->isStrict()); #TODO: Remove strict and delegate to email account
|
||||
|
||||
$this->bkId = $id;
|
||||
}
|
||||
@@ -484,6 +553,15 @@ class EmailAccount extends VerySimpleModel {
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
return $this->getEmail()->getName();
|
||||
}
|
||||
|
||||
public function getAccessToken() {
|
||||
$cred = $this->getFreshCredentials();
|
||||
return $cred ? $cred->getAccessToken($this->getConfigSignature()) : null;
|
||||
}
|
||||
|
||||
private function getOAuth2Backend($auth=null) {
|
||||
$auth = $auth ?: $this->getAuthBk();
|
||||
return Oauth2AuthorizationBackend::getBackend($auth);
|
||||
@@ -497,6 +575,7 @@ class EmailAccount extends VerySimpleModel {
|
||||
'name' => sprintf('%s (%s)',
|
||||
$email->getEmail(), $this->getType()),
|
||||
'isactive' => 1,
|
||||
'strict_matching' => $this->isStrict(),
|
||||
'notes' => sprintf(
|
||||
__('OAuth2 Authorization for %s'), $email->getEmail()),
|
||||
];
|
||||
@@ -557,6 +636,11 @@ class EmailAccount extends VerySimpleModel {
|
||||
if (isset($this->ht[$var]))
|
||||
$ht[$this->type.'_'.$var] = $this->ht[$var];
|
||||
}
|
||||
// Add stashed info (if any)
|
||||
if (($data=$this->email->restoreFormData(false)))
|
||||
$ht = array_merge($ht, $data);
|
||||
|
||||
|
||||
return $ht;
|
||||
}
|
||||
|
||||
@@ -566,7 +650,7 @@ class EmailAccount extends VerySimpleModel {
|
||||
$this->getId());
|
||||
}
|
||||
|
||||
private function getConfig() {
|
||||
protected function getConfig() {
|
||||
if (!isset($this->config))
|
||||
$this->config = new EmailAccountConfig($this->getNamespace());
|
||||
return $this->config;
|
||||
@@ -582,8 +666,13 @@ class EmailAccount extends VerySimpleModel {
|
||||
case 'basic':
|
||||
$this->form = $this->getBasicAuthConfigForm($vars,
|
||||
$auth);
|
||||
$setting = $this->getAccountSetting(true);
|
||||
if (!$setting || !$setting->isValid())
|
||||
$this->form->setNotice(
|
||||
__('Host, Port & Protocol Required'));
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
return $this->form;
|
||||
}
|
||||
@@ -597,7 +686,8 @@ class EmailAccount extends VerySimpleModel {
|
||||
switch ($type) {
|
||||
case 'basic':
|
||||
// Set username and password
|
||||
if (!$vars || !$this->updateCredentials($auth, $vars, $errors))
|
||||
if (!$this->updateCredentials($auth, $vars, $errors)
|
||||
&& !isset($errors['err']))
|
||||
$errors['err'] = sprintf('%s %s',
|
||||
__('Error Saving'),
|
||||
__('Authentication'));
|
||||
@@ -617,10 +707,20 @@ class EmailAccount extends VerySimpleModel {
|
||||
if ($this->getAuthId()
|
||||
&& ($i=$bk->getPluginInstance($this->getAuthId()))) {
|
||||
$vars = array_merge($bk->getDefaults(), $vars); #nolint
|
||||
if (!$i->update($vars, $errors))
|
||||
if ($i->update($vars, $errors)) {
|
||||
// Disable account if backend is changed
|
||||
if (strcasecmp($this->auth_bk, $auth))
|
||||
$this->active = 0;
|
||||
// Auth backend can be changed on update
|
||||
$this->auth_bk = $auth;
|
||||
$this->save();
|
||||
// Update Strict Matching
|
||||
$this->getConfig()->setStrictMatching($_POST['strict_matching'] ? 1 : 0);
|
||||
} elseif (!isset($errors['err'])) {
|
||||
$errors['err'] = sprintf('%s %s',
|
||||
__('Error Saving'),
|
||||
__('Authentication'));
|
||||
__('Authentication'));
|
||||
}
|
||||
} else {
|
||||
// Ask the backend to add OAuth2 instance for this account
|
||||
if (($i=$bk->addPluginInstance($vars, $errors))) { #nolint
|
||||
@@ -645,6 +745,10 @@ class EmailAccount extends VerySimpleModel {
|
||||
return $this->logActivity($error);
|
||||
}
|
||||
|
||||
public function hasCredentials() {
|
||||
return ($this->getFreshCredentials());
|
||||
}
|
||||
|
||||
private function getCredentialsVars($auth=null) {
|
||||
$vars = [];
|
||||
if (($cred = $this->getCredentials($auth)))
|
||||
@@ -661,96 +765,206 @@ class EmailAccount extends VerySimpleModel {
|
||||
// Authentication doesn't match - it's getting reconfigured.
|
||||
if ($auth
|
||||
&& strncasecmp($this->getAuthBk(), $auth, strlen($auth))
|
||||
&& strcasecmp($auth, 'none'))
|
||||
&& !in_array($auth, ['none', 'mailbox']))
|
||||
return [];
|
||||
|
||||
if (!isset($this->cred) || $refresh) {
|
||||
$cred = null;
|
||||
$this->cred = $cred = null;
|
||||
$auth = $auth ?: $this->getAuthBk();
|
||||
list($type, $provider) = explode(':', $auth);
|
||||
switch ($type) {
|
||||
case 'mailbox':
|
||||
if (($mb=$this->email->getMailBoxAccount())
|
||||
&& $mb->getAuthBk())
|
||||
$cred = $mb->getCredentials($mb->getAuthBk(), $refresh);
|
||||
break;
|
||||
case 'none':
|
||||
// No authentication required (open replay)
|
||||
$cred = new osTicket\Mail\NoAuthCredentials([
|
||||
'username' => $this->email->getEmail()]);
|
||||
break;
|
||||
case 'basic':
|
||||
if (($c=$this->getConfig())
|
||||
&& ($creds=$c->toArray())
|
||||
&& isset($creds['username'])
|
||||
&& isset($creds['passwd'])) {
|
||||
// Decrypt password
|
||||
$cred = new osTicket\Mail\BasicAuthCredentials([
|
||||
'username' => $creds['username'],
|
||||
// Decrypt password
|
||||
'password' => Crypto::decrypt($creds['passwd'],
|
||||
SECRET_SALT,
|
||||
md5($creds['username'].$this->getNamespace()))
|
||||
]);
|
||||
}
|
||||
break;
|
||||
case 'oauth2':
|
||||
if (($c=$this->getConfig()) && ($creds=$c->toArray())) {
|
||||
// Decrypt Access Token
|
||||
if ($creds['access_token']) {
|
||||
$creds['access_token'] = Crypto::decrypt(
|
||||
$creds['access_token'],
|
||||
SECRET_SALT,
|
||||
md5($creds['resource_owner_email'].$this->getNamespace())
|
||||
);
|
||||
}
|
||||
// Decrypt Referesh Token
|
||||
if ($creds['refresh_token']) {
|
||||
$creds['refresh_token'] = Crypto::decrypt(
|
||||
$creds['refresh_token'],
|
||||
SECRET_SALT,
|
||||
md5($creds['resource_owner_email'].$this->getNamespace())
|
||||
);
|
||||
}
|
||||
$errors = [];
|
||||
$class = 'osTicket\Mail\OAuth2AuthCredentials';
|
||||
try {
|
||||
// Init credentials and see of we need to
|
||||
// refresh the token
|
||||
if (($cred=$class::init($creds))
|
||||
&& ($token=$cred->getToken())
|
||||
&& ($refresh && $token->isExpired())
|
||||
&& ($bk=$this->getOAuth2Backend())
|
||||
&& ($info=$bk->refreshAccessToken( #nolint
|
||||
$token->getRefreshToken(),
|
||||
$this->getBkId(), $errors))
|
||||
&& isset($info['access_token'])
|
||||
&& $this->updateCredentials($auth,
|
||||
// Merge new access token with
|
||||
// already decrypted creds
|
||||
array_merge($creds, $info), $errors
|
||||
)) {
|
||||
return $this->getCredentials($auth, $refresh);
|
||||
} elseif ($errors) {
|
||||
$errors['err'] = $errors['refresh_token']
|
||||
?: __('Referesh Token Expired');
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
$errors['err'] = $ex->getMessage();
|
||||
}
|
||||
if (isset($errors['err']))
|
||||
$this->logError($errors['err']);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Exception(sprintf('%s: %s',
|
||||
$type, __('Unknown Credential Type')));
|
||||
try {
|
||||
switch ($type) {
|
||||
case 'mailbox':
|
||||
$cred = $this->getMailBoxCredentials($refresh);
|
||||
break;
|
||||
case 'none':
|
||||
// No authentication required (open replay)
|
||||
$cred = new osTicket\Mail\NoAuthCredentials([
|
||||
'username' => $this->email->getEmail()]);
|
||||
break;
|
||||
case 'basic':
|
||||
$cred = $this->getBasicAuthCredentials();
|
||||
break;
|
||||
case 'oauth2':
|
||||
$cred = $this->getOAuth2AuthCredentials($provider, $refresh);
|
||||
break;
|
||||
default:
|
||||
throw new Exception(sprintf('%s: %s',
|
||||
$type, __('Unknown Credential Type')));
|
||||
}
|
||||
// Cache the credentials
|
||||
$this->cred = $cred;
|
||||
} catch (Exception $ex) {
|
||||
// Log the error
|
||||
$this->logError(sprintf('%s: %s',
|
||||
__('Credentials'), $ex->getMessage()
|
||||
));
|
||||
}
|
||||
$this->cred = $cred;
|
||||
}
|
||||
return $this->cred;
|
||||
}
|
||||
|
||||
private function getMailBoxCredentials($refresh=false) {
|
||||
if (($mb=$this->email->getMailBoxAccount())
|
||||
&& $mb->getAuthBk())
|
||||
return $mb->getCredentials($mb->getAuthBk(), $refresh);
|
||||
}
|
||||
|
||||
private function getBasicAuthCredentials() {
|
||||
if (($c=$this->getConfig())
|
||||
&& ($creds=$c->toArray())
|
||||
&& isset($creds['username'])
|
||||
&& isset($creds['passwd'])) {
|
||||
return new osTicket\Mail\BasicAuthCredentials([
|
||||
'username' => $creds['username'],
|
||||
// Decrypt password
|
||||
'password' => Crypto::decrypt($creds['passwd'],
|
||||
SECRET_SALT,
|
||||
md5($creds['username'].$this->getNamespace()))
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function updateBasicAuthCredentials($vars, &$errors) {
|
||||
// Get current credentials - we need to re-encrypt
|
||||
// password as username might be changing
|
||||
$creds = $this->getCredentialsVars('basic');
|
||||
// password change?
|
||||
if (!$vars['username']) {
|
||||
$errors['username'] = __('Username Required');
|
||||
} elseif (!$vars['passwd'] && !$creds['password']) {
|
||||
$errors['passwd'] = __('Password Required');
|
||||
} elseif (($setting=$this->getAccountSetting(true))
|
||||
&& !$setting->isValid()) {
|
||||
$errors['err'] = implode(', ', $setting->getErrors());
|
||||
} elseif ($setting && !$errors) {
|
||||
// Validate the credentials
|
||||
try {
|
||||
$cred = new osTicket\Mail\BasicAuthCredentials([
|
||||
'username' => $vars['username'],
|
||||
'password' => $vars['passwd'] ?:
|
||||
$creds['password'],
|
||||
]);
|
||||
if (!$this->validateCredentials($cred))
|
||||
$errors['err'] = __('Invalid Credentials');
|
||||
} catch (Exception $ex) {
|
||||
$errors['err'] = $ex->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$errors) {
|
||||
// Save credentials and get out of here.
|
||||
$info = [
|
||||
// username
|
||||
'username' => $vars['username'],
|
||||
// Encrypt password
|
||||
'passwd' => Crypto::encrypt($vars['passwd'] ?:
|
||||
$creds['password'], SECRET_SALT,
|
||||
md5($vars['username'].$this->getNamespace()))
|
||||
];
|
||||
|
||||
if (!$this->getConfig()->updateInfo($info))
|
||||
$errors['err'] = sprintf('%s: %s',
|
||||
__('BasicAuth'),
|
||||
__('Error saving credentials'));
|
||||
}
|
||||
return !count($errors);
|
||||
}
|
||||
|
||||
private function getOAuth2AuthCredentials($provider, $refresh=false) {
|
||||
if (!($c=$this->getConfig()))
|
||||
return false;
|
||||
|
||||
$creds=$c->toArray();
|
||||
// Decrypt Access Token
|
||||
if ($creds['access_token']) {
|
||||
$creds['access_token'] = Crypto::decrypt(
|
||||
$creds['access_token'],
|
||||
SECRET_SALT,
|
||||
md5($creds['resource_owner_email'].$this->getNamespace())
|
||||
);
|
||||
}
|
||||
|
||||
// Decrypt Referesh Token
|
||||
if ($creds['refresh_token']) {
|
||||
$creds['refresh_token'] = Crypto::decrypt(
|
||||
$creds['refresh_token'],
|
||||
SECRET_SALT,
|
||||
md5($creds['resource_owner_email'].$this->getNamespace())
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Init credentials and see of we need to
|
||||
// refresh the token
|
||||
$errors = [];
|
||||
$auth = sprintf('oauth2:%s', $provider);
|
||||
$class = 'osTicket\Mail\OAuth2AuthCredentials';
|
||||
if (($cred=$class::init($creds))
|
||||
&& ($token=$cred->getToken())
|
||||
&& ($refresh && $token->isExpired())
|
||||
&& ($bk=$this->getOAuth2Backend())
|
||||
&& ($info=$bk->refreshAccessToken( #nolint
|
||||
$token->getRefreshToken(),
|
||||
$this->getBkId(), $errors))
|
||||
&& isset($info['access_token'])
|
||||
&& $this->updateCredentials($auth,
|
||||
// Merge new access token with
|
||||
// already decrypted creds
|
||||
array_merge($creds, $info), $errors
|
||||
)) {
|
||||
return $this->getCredentials($auth, $refresh);
|
||||
} elseif ($errors) {
|
||||
// Throw an exception with the error
|
||||
throw new Exception($errors['refresh_token']
|
||||
?? __('Referesh Token Expired'));
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
// rethrow the exception including above.
|
||||
throw $ex;
|
||||
}
|
||||
return $cred;
|
||||
}
|
||||
|
||||
private function updateOAuth2AuthCredentials($provider, $vars, &$errors) {
|
||||
$err = sprintf('%s_auth_bk', $this->getType());
|
||||
if (!$vars['access_token']) {
|
||||
$errors[$err] = __('Access Token Required');
|
||||
} elseif (!$vars['resource_owner_email']
|
||||
|| !Validator::is_email($vars['resource_owner_email'])) {
|
||||
$errors[$err] = __('Resource Owner Required');
|
||||
} elseif ($this->isStrict()
|
||||
// When in Strict mode Account Email must match resource owner's
|
||||
// email. Strict mode can be disabled for a global admin to
|
||||
// authorized onbehalf of other user accounts or shared mailboxes.
|
||||
&& strcasecmp($this->getEmail()->getEmail(), $vars['resource_owner_email'])) {
|
||||
$errors[$err] = sprintf(__('Strict Mode: Expecting Authorization for %s not %s'),
|
||||
$this->getEmail()->getEmail(),
|
||||
$vars['resource_owner_email']);
|
||||
} elseif (!$errors) {
|
||||
// Encrypt Access Token
|
||||
$vars['access_token'] = Crypto::encrypt(
|
||||
$vars['access_token'],
|
||||
SECRET_SALT,
|
||||
md5($vars['resource_owner_email'].$this->getNamespace()));
|
||||
// Encrypt Referesh Token
|
||||
if ($vars['refresh_token']) {
|
||||
$vars['refresh_token'] = Crypto::encrypt(
|
||||
$vars['refresh_token'],
|
||||
SECRET_SALT,
|
||||
md5($vars['resource_owner_email'].$this->getNamespace())
|
||||
);
|
||||
}
|
||||
$vars['config_signature'] = $this->getConfigSignature();
|
||||
// TODO: Validate
|
||||
if (!$this->getConfig()->updateInfo($vars))
|
||||
$errors['err'] = sprintf('oauth2:%s - %s',
|
||||
Format::htmlchars($provider),
|
||||
__('Error saving credentials'));
|
||||
}
|
||||
return !count($errors);
|
||||
}
|
||||
|
||||
public function updateCredentials($auth, $vars, &$errors) {
|
||||
if (!$vars || $errors)
|
||||
return false;
|
||||
@@ -758,57 +972,12 @@ class EmailAccount extends VerySimpleModel {
|
||||
list($type, $provider) = explode(':', $auth);
|
||||
switch ($type) {
|
||||
case 'basic':
|
||||
// Get current credentials - we need to re-encrypt
|
||||
// password as username might be changing
|
||||
$creds = $this->getCredentialsVars($auth);
|
||||
// password change?
|
||||
if (!$vars['username']) {
|
||||
$errors['username'] = __('Username Required');
|
||||
} elseif (!$vars['passwd'] && !$creds['password']) {
|
||||
$errors['passwd'] = __('Password Required');
|
||||
} elseif (!$errors) {
|
||||
$info = [
|
||||
// username
|
||||
'username' => $vars['username'],
|
||||
// Encrypt password
|
||||
'passwd' => Crypto::encrypt($vars['passwd'] ?:
|
||||
$creds['password'], SECRET_SALT,
|
||||
md5($vars['username'].$this->getNamespace()))
|
||||
];
|
||||
if (!$this->getConfig()->updateInfo($info))
|
||||
$errors['err'] = sprintf('%s: %s',
|
||||
Format::htmlchars($type),
|
||||
__('Error saving credentials'));
|
||||
}
|
||||
if (!($this->updateBasicAuthCredentials($vars, $errors)))
|
||||
return false;
|
||||
break;
|
||||
case 'oauth2':
|
||||
if (!$vars['access_token']) {
|
||||
$errors['access_token'] = __('Access Token Required');
|
||||
} elseif (!$vars['resource_owner_email']
|
||||
|| !Validator::is_email($vars['resource_owner_email'])) {
|
||||
$errors['resource_owner_email'] =
|
||||
__('Resource Owner Required');
|
||||
|
||||
} else {
|
||||
// Encrypt Access Token
|
||||
$vars['access_token'] = Crypto::encrypt(
|
||||
$vars['access_token'],
|
||||
SECRET_SALT,
|
||||
md5($vars['resource_owner_email'].$this->getNamespace()));
|
||||
// Encrypt Referesh Token
|
||||
if ($vars['refresh_token']) {
|
||||
$vars['refresh_token'] = Crypto::encrypt(
|
||||
$vars['refresh_token'],
|
||||
SECRET_SALT,
|
||||
md5($vars['resource_owner_email'].$this->getNamespace())
|
||||
);
|
||||
}
|
||||
$vars['config_signature'] = $this->getConfigSignature();
|
||||
if (!$this->getConfig()->updateInfo($vars))
|
||||
$errors['err'] = sprintf('%s: %s',
|
||||
Format::htmlchars($type),
|
||||
__('Error saving credentials'));
|
||||
}
|
||||
if (!($this->updateOAuth2AuthCredentials($provider, $vars, $errors)))
|
||||
return false;
|
||||
break;
|
||||
default:
|
||||
$errors['err'] = sprintf('%s - %s',
|
||||
@@ -819,17 +988,25 @@ class EmailAccount extends VerySimpleModel {
|
||||
if ($errors)
|
||||
return false;
|
||||
|
||||
// Save the auth backend
|
||||
$this->auth_bk = $auth;
|
||||
// Clear cached credentials
|
||||
$this->creds = null;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/*
|
||||
* Destory the account config
|
||||
*/
|
||||
function destroyConfig() {
|
||||
return $this->getConfig()->destroy();
|
||||
}
|
||||
|
||||
function update($vars, &$errors) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function logActivity($error= null, $now=null) {
|
||||
public function logActivity($error=null, $now=null) {
|
||||
if (isset($error)) {
|
||||
$this->num_errors += 1;
|
||||
$this->last_error_msg = $error;
|
||||
@@ -837,9 +1014,10 @@ class EmailAccount extends VerySimpleModel {
|
||||
} else {
|
||||
$this->num_errors = 0;
|
||||
$this->last_error = null;
|
||||
$this->last_error_msg = null;
|
||||
$this->last_activity = $now ?: SqlFunction::NOW();
|
||||
}
|
||||
$this->save();
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
function save($refetch=false) {
|
||||
@@ -851,7 +1029,7 @@ class EmailAccount extends VerySimpleModel {
|
||||
|
||||
function delete() {
|
||||
// Destroy the Email config
|
||||
$this->getConfig()->destroy();
|
||||
$this->destroyConfig();
|
||||
// Delete the Plugin instance
|
||||
if ($this->isOAuthAuth() && ($i=$this->getOAuth2Instance()))
|
||||
$i->delete();
|
||||
@@ -861,7 +1039,7 @@ class EmailAccount extends VerySimpleModel {
|
||||
|
||||
static function create($ht=false) {
|
||||
$i = new static($ht);
|
||||
$i->active = 0;
|
||||
$i->active = isset($ht['active']) ? $ht['active'] : 0;
|
||||
$i->created = SqlFunction::NOW();
|
||||
return $i;
|
||||
}
|
||||
@@ -895,6 +1073,16 @@ class MailBoxAccount extends EmailAccount {
|
||||
->filter(['type' => 'mailbox']);
|
||||
}
|
||||
|
||||
//TODO: Morhp MailBoxAccount to ImapMailBoxAccount
|
||||
public function isImap() {
|
||||
return (strcasecmp($this->getProtocol(), 'IMAP') === 0);
|
||||
}
|
||||
|
||||
//TODO: Morhp MailBoxAccount to PopMailBoxAccount
|
||||
public function isPop() {
|
||||
return (strcasecmp($this->getProtocol(), 'POP') === 0);
|
||||
}
|
||||
|
||||
public function getProtocol() {
|
||||
return $this->protocol;
|
||||
}
|
||||
@@ -903,9 +1091,13 @@ class MailBoxAccount extends EmailAccount {
|
||||
return $this->folder;
|
||||
}
|
||||
|
||||
public function getFetchFolder() {
|
||||
return $this->getFolder();
|
||||
}
|
||||
|
||||
public function getArchiveFolder() {
|
||||
if ((strcasecmp($this->getProtocol(), 'imap') == 0) && $this->archivefolder)
|
||||
return $this->archivefolder;
|
||||
return ($this->isImap() && $this->archivefolder)
|
||||
? $this->archivefolder : null;
|
||||
}
|
||||
|
||||
public function canDeleteEmails() {
|
||||
@@ -916,17 +1108,22 @@ class MailBoxAccount extends EmailAccount {
|
||||
return $this->fetchmax;
|
||||
}
|
||||
|
||||
public function getMailBox(osTicket\Mail\AuthCredentials $cred=null) {
|
||||
protected function validateCredentials(osTicket\Mail\AuthCredentials $creds) {
|
||||
return $this->getMailBox($creds);
|
||||
}
|
||||
|
||||
public function getMailBox(?osTicket\Mail\AuthCredentials $cred=null) {
|
||||
if (!isset($this->mailbox) || $cred) {
|
||||
$this->cred = $cred ?: $this->getFreshCredentials();
|
||||
$options = $this->getAccountOptions();
|
||||
$setting = $this->getAccountSetting();
|
||||
$setting->setCredentials($this->cred);
|
||||
switch (strtolower($this->getProtocol())) {
|
||||
case 'imap':
|
||||
$mailbox = new osTicket\Mail\Imap($options);
|
||||
$mailbox = new osTicket\Mail\Imap($setting);
|
||||
break;
|
||||
case 'pop3':
|
||||
case 'pop':
|
||||
$mailbox = new osTicket\Mail\Pop3($options);
|
||||
$mailbox = new osTicket\Mail\Pop3($setting);
|
||||
break;
|
||||
default:
|
||||
throw new Exception('Unknown Mail protocol:
|
||||
@@ -939,14 +1136,15 @@ class MailBoxAccount extends EmailAccount {
|
||||
|
||||
public function fetchEmails() {
|
||||
try {
|
||||
$fetcher = new osTicket\Mail\Fetcher($this);
|
||||
$num = $fetcher->processEmails();
|
||||
$this->logLastFetch();
|
||||
return $num;
|
||||
} catch (Exception $ex) {
|
||||
$this->logFetchError($ex->getMessage());
|
||||
// rethrow the exception so caller can handle it
|
||||
throw $ex;
|
||||
$fetcher = new osTicket\Mail\Fetcher($this);
|
||||
return $fetcher->processEmails();
|
||||
} catch (Throwable $t) {
|
||||
// May throw an Exception or Error
|
||||
// Log the message
|
||||
$this->logFetchError($t->getMessage());
|
||||
// rethrow the throwable so caller can handle it
|
||||
throw $t;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1000,6 +1198,7 @@ class MailBoxAccount extends EmailAccount {
|
||||
$this->fetchmax = $vars['mailbox_fetchmax'] ?: 30;
|
||||
$this->postfetch = $vars['mailbox_postfetch'];
|
||||
$this->last_activity = null;
|
||||
$this->last_error_msg = null;
|
||||
$this->num_errors = 0;
|
||||
//Post fetch email handling...
|
||||
switch ($vars['mailbox_postfetch']) {
|
||||
@@ -1020,7 +1219,7 @@ class MailBoxAccount extends EmailAccount {
|
||||
!$mb->hasFolder($this->folder))
|
||||
$errors['mailbox_folder'] = __('Unknown Folder');
|
||||
if ($this->archivefolder
|
||||
&& $this->protocol == 'IMAP'
|
||||
&& $this->isImap()
|
||||
&& !$mb->hasFolder($this->archivefolder)
|
||||
&& !$mb->createFolder($this->archivefolder))
|
||||
$errors['mailbox_archivefolder'] =
|
||||
@@ -1037,7 +1236,11 @@ class MailBoxAccount extends EmailAccount {
|
||||
}
|
||||
|
||||
public function logLastFetch($now=null) {
|
||||
return $this->logActivity($now);
|
||||
return $this->logActivity(null, $now);
|
||||
}
|
||||
|
||||
private function logFetchError($error) {
|
||||
return $this->logActivity($error ?: __('Mail Fetch Error'));
|
||||
}
|
||||
|
||||
public function update($vars, &$errors) {
|
||||
@@ -1081,10 +1284,31 @@ class SmtpAccount extends EmailAccount {
|
||||
->filter(['type' => 'smtp']);
|
||||
}
|
||||
|
||||
public function isMailboxAuth() {
|
||||
return (strcasecmp($this->getAuthBk(), 'mailbox') === 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if using mailbox auth and MailboxAccount exists if so
|
||||
* return the MailboxAccount config, otherwise return it's own
|
||||
* config
|
||||
*/
|
||||
protected function getConfig() {
|
||||
if ($this->isMailboxAuth()
|
||||
&& ($email=$this->getEmail())
|
||||
&& ($account=$email->getMailBoxAccount()))
|
||||
return $account->getConfig();
|
||||
return parent::getConfig();
|
||||
}
|
||||
|
||||
public function allowSpoofing() {
|
||||
return ($this->allow_spoofing);
|
||||
}
|
||||
|
||||
protected function validateCredentials(osTicket\Mail\AuthCredentials $creds) {
|
||||
return $this->getSmtp($creds);
|
||||
}
|
||||
|
||||
public function getSmtpConnection() {
|
||||
$this->smtp = $this->getSmtp();
|
||||
if (!$this->smtp->connect())
|
||||
@@ -1093,15 +1317,18 @@ class SmtpAccount extends EmailAccount {
|
||||
return $this->smtp;
|
||||
}
|
||||
|
||||
public function getSmtp(osTicket\Mail\AuthCredentials $cred=null) {
|
||||
public function getSmtp(?osTicket\Mail\AuthCredentials $cred=null) {
|
||||
if (!isset($this->smtp) || $cred) {
|
||||
$this->cred = $cred ?: $this->getFreshCredentials();
|
||||
$accountOptions = $this->getAccountOptions();
|
||||
$smtpOptions = new osTicket\Mail\SmtpOptions($accountOptions);
|
||||
$smtp = new osTicket\Mail\Smtp($smtpOptions);
|
||||
// Attempt to connect if Credentials are sent
|
||||
if ($cred) $smtp->connect();
|
||||
$this->smtp = $smtp;
|
||||
if ($this->cred) {
|
||||
$setting = $this->getAccountSetting();
|
||||
$setting->setCredentials($this->cred);
|
||||
$smtpOptions = new osTicket\Mail\SmtpOptions($setting);
|
||||
$smtp = new osTicket\Mail\Smtp($smtpOptions);
|
||||
// Attempt to connect now if credentials are sent in
|
||||
if ($cred) $smtp->connect();
|
||||
$this->smtp = $smtp;
|
||||
}
|
||||
}
|
||||
return $this->smtp;
|
||||
}
|
||||
@@ -1127,6 +1354,14 @@ class SmtpAccount extends EmailAccount {
|
||||
&& !($creds=$this->getFreshCredentials($vars['smtp_auth_bk'])))
|
||||
$_errors['smtp_auth_bk'] = __('Configure Authentication');
|
||||
|
||||
// Check if set to active and using mailbox auth, if so check strict
|
||||
// matching.
|
||||
if ($vars['smtp_active'] == 1
|
||||
&& ($vars['smtp_auth_bk'] === 'mailbox')
|
||||
&& (strpos($vars['mailbox_auth_bk'], 'oauth2') === 0)
|
||||
&& !$this->checkStrictMatching())
|
||||
$_errors['smtp_auth_bk'] = sprintf('%s and %s', __('Resource Owner'), __('Email Mismatch'));
|
||||
|
||||
if (!$_errors) {
|
||||
$this->active = $vars['smtp_active'] ? 1 : 0;
|
||||
$this->host = $vars['smtp_host'];
|
||||
@@ -1135,6 +1370,7 @@ class SmtpAccount extends EmailAccount {
|
||||
$this->protocol = 'SMTP';
|
||||
$this->allow_spoofing = $vars['smtp_allow_spoofing'] ? 1 : 0;
|
||||
$this->last_activity = null;
|
||||
$this->last_error_msg = null;
|
||||
$this->num_errors = 0;
|
||||
// If account is active then attempt to authenticate
|
||||
if ($this->active && $creds) {
|
||||
@@ -1171,6 +1407,20 @@ class SmtpAccount extends EmailAccount {
|
||||
*
|
||||
*/
|
||||
class EmailAccountConfig extends Config {
|
||||
/*
|
||||
* Get strict matching (default: true)
|
||||
*/
|
||||
public function getStrictMatching() {
|
||||
return $this->get('strict_matching', true);
|
||||
}
|
||||
|
||||
/*
|
||||
* Set strict matching
|
||||
*/
|
||||
public function setStrictMatching($mode) {
|
||||
return $this->set('strict_matching', !!$mode);
|
||||
}
|
||||
|
||||
public function updateInfo($vars) {
|
||||
return parent::updateAll($vars);
|
||||
}
|
||||
@@ -1181,40 +1431,45 @@ class EmailAccountConfig extends Config {
|
||||
*
|
||||
*/
|
||||
class BasicAuthConfigForm extends AbstractForm {
|
||||
function buildFields() {
|
||||
$passwdhint = '';
|
||||
if (isset($this->_source['passwd']) && $this->_source['passwd'])
|
||||
$passwdhint = __('Enter a new password to change current one');
|
||||
private $account;
|
||||
private $host;
|
||||
private $password;
|
||||
|
||||
function __construct($source=null, $options=array()) {
|
||||
parent::__construct($source, $options);
|
||||
}
|
||||
|
||||
private function getPassword() {
|
||||
return $this->_source['passwd'];
|
||||
}
|
||||
|
||||
function buildFields() {
|
||||
$password = $this->getPassword();
|
||||
return array(
|
||||
'username' => new TextboxField(array(
|
||||
'required' => true,
|
||||
'label' => __('Username'),
|
||||
'configuration' => array(
|
||||
'length' => 0,
|
||||
'autofocus' => true,
|
||||
),
|
||||
)),
|
||||
'passwd' => new PasswordField(array(
|
||||
'label' => __('Password'),
|
||||
'required' => true,
|
||||
'required' => !$password,
|
||||
'validator' => 'noop',
|
||||
'hint' => $passwdhint,
|
||||
'hint' => $password
|
||||
? __('Enter a new password to change current one')
|
||||
: '',
|
||||
'configuration' => array(
|
||||
'length' => 0,
|
||||
'classes' => 'span12',
|
||||
'placeholder' => $passwdhint ?
|
||||
str_repeat('••••••••••••', 2) : __('Password'),
|
||||
'placeholder' => $password
|
||||
? str_repeat('•', strlen($password)*2)
|
||||
: __('Password'),
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
function isValid($include=false) {
|
||||
$self = $this;
|
||||
return parent::isValid(function ($f) use($self) {
|
||||
return !(($f instanceof PasswordField)
|
||||
&& isset($self->_source['passwd'])
|
||||
&& $f->resetErrors());
|
||||
});
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -259,7 +259,7 @@ class Export {
|
||||
|
||||
// Filename or stream to export agents to
|
||||
$filename = $filename ?: sprintf('Agents-%s.csv',
|
||||
strftime('%Y%m%d'));
|
||||
date('Ymd'));
|
||||
Http::download($filename, "text/$how");
|
||||
$depts = Dept::getDepartments(null, true, Dept::DISPLAY_DISABLED);
|
||||
echo self::dumpQuery($agents, array(
|
||||
@@ -296,7 +296,7 @@ static function departmentMembers($dept, $agents, $filename='', $how='csv') {
|
||||
|
||||
// Filename or stream to export depts' agents to
|
||||
$filename = $filename ?: sprintf('%s-%s.csv', $dept->getName(),
|
||||
strftime('%Y%m%d'));
|
||||
date('Ymd'));
|
||||
Http::download($filename, "text/$how");
|
||||
echo self::dumpQuery($agents, array(
|
||||
'::getName' => 'Name',
|
||||
@@ -322,7 +322,7 @@ static function departmentMembers($dept, $agents, $filename='', $how='csv') {
|
||||
exit;
|
||||
}
|
||||
|
||||
static function audits($type, ?string $filename=null, ?string $tableInfo=null, ?string $object=null, ?string $how='csv', ?bool $show_viewed=true, ?array $data=array(), CsvExporter $exporter) {
|
||||
static function audits($type, ?string $filename, ?string $tableInfo, ?string $object, ?string $how='csv', ?bool $show_viewed=true, ?array $data=array(), CsvExporter $exporter) {
|
||||
$headings = array('Description', 'Timestamp', 'IP');
|
||||
switch ($type) {
|
||||
case 'audit':
|
||||
@@ -535,7 +535,7 @@ abstract class Exporter {
|
||||
|| !($email=$cfg->getDefaultEmail()))
|
||||
return false;
|
||||
|
||||
$mailer = new Mailer($email);
|
||||
$mailer = new osTicket\Mail\Mailer($email);
|
||||
$mailer->addFileObject($file);
|
||||
$subject = __("Export");
|
||||
$body = __("Attached is file containing the export you asked us to send you!");
|
||||
@@ -620,7 +620,7 @@ class CsvExporter extends Exporter {
|
||||
}
|
||||
|
||||
function write($data) {
|
||||
fputcsv($this->fp, $this->escape($data), $this->getDelimiter());
|
||||
fputcsv($this->fp, $this->escape($data), $this->getDelimiter(), "\"", "");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -725,7 +725,7 @@ class CsvResultsExporter extends ResultSetExporter {
|
||||
$delimiter = $this->getDelimiter();
|
||||
// Output a UTF-8 BOM (byte order mark)
|
||||
fputs($this->output, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||||
fputcsv($this->output, $this->getHeaders(), $delimiter);
|
||||
fputcsv($this->output, $this->getHeaders(), $delimiter, "\"", "");
|
||||
while ($row=$this->next())
|
||||
fputcsv($this->output, array_map(
|
||||
function($v){
|
||||
@@ -733,7 +733,7 @@ class CsvResultsExporter extends ResultSetExporter {
|
||||
return "'".$v;
|
||||
return $v;
|
||||
}, $row),
|
||||
$delimiter);
|
||||
$delimiter, "\"", "");
|
||||
|
||||
if (!$tmp)
|
||||
fclose($this->output);
|
||||
|
||||
@@ -347,7 +347,7 @@ class FAQ extends VerySimpleModel {
|
||||
|
||||
static function getFeatured() {
|
||||
return self::objects()
|
||||
->filter(array('ispublished__in'=>array(1,2), 'category__ispublic'=>1))
|
||||
->filter(array('ispublished'=>2, 'category__ispublic__in'=>[1,2]))
|
||||
->order_by('-ispublished');
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,19 @@ require_once(INCLUDE_DIR.'class.signal.php');
|
||||
require_once(INCLUDE_DIR.'class.error.php');
|
||||
|
||||
|
||||
/**
|
||||
* FileObject Interface
|
||||
*
|
||||
* Methods File Objects should support
|
||||
**/
|
||||
interface FileObjectInterface {
|
||||
function getUId();
|
||||
function getKey();
|
||||
function getName();
|
||||
function getData();
|
||||
function getMimeType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a file stored in a storage backend. It is generally attached
|
||||
* to something; however company logos, login page backdrops, and other
|
||||
@@ -29,8 +42,8 @@ require_once(INCLUDE_DIR.'class.error.php');
|
||||
* - 'L' => Logo
|
||||
* - 'B' => Backdrop
|
||||
*/
|
||||
class AttachmentFile extends VerySimpleModel {
|
||||
|
||||
class AttachmentFile extends VerySimpleModel
|
||||
implements FileObjectInterface {
|
||||
static $meta = array(
|
||||
'table' => FILE_TABLE,
|
||||
'pk' => array('id'),
|
||||
@@ -71,6 +84,10 @@ class AttachmentFile extends VerySimpleModel {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
function getUId() {
|
||||
return $this->getId();
|
||||
}
|
||||
|
||||
function getType() {
|
||||
return $this->type;
|
||||
}
|
||||
@@ -158,10 +175,14 @@ class AttachmentFile extends VerySimpleModel {
|
||||
}
|
||||
|
||||
function display($scale=false, $ttl=86400) {
|
||||
// Force download if not safe viewable image
|
||||
if (!$this->isInlineSafeImage())
|
||||
return $this->download(false, 'attachment');
|
||||
|
||||
$this->makeCacheable($ttl);
|
||||
|
||||
if ($scale && extension_loaded('gd')) {
|
||||
$image = imagecreatefromstring($this->getData());
|
||||
if ($scale && extension_loaded('gd')
|
||||
&& ($image = imagecreatefromstring($this->getData()))) {
|
||||
$width = imagesx($image);
|
||||
if ($scale <= $width) {
|
||||
$height = imagesy($image);
|
||||
@@ -197,6 +218,12 @@ class AttachmentFile extends VerySimpleModel {
|
||||
'type' => $options['type']))))
|
||||
$options['id'] = $a->getId();
|
||||
|
||||
// Force attachment disposition if not inline-safe image
|
||||
if (isset($options['disposition'])
|
||||
&& strcasecmp($options['disposition'], 'inline') == 0
|
||||
&& !$this->isInlineSafeImage())
|
||||
$options['disposition'] = 'attachment';
|
||||
|
||||
return static::generateDownloadUrl($this->getId(),
|
||||
strtolower($this->getKey()), $this->getSignature(),
|
||||
$options);
|
||||
@@ -250,7 +277,8 @@ class AttachmentFile extends VerySimpleModel {
|
||||
|
||||
$check = static::_genUrlSignature($this->getId(), $this->getKey(),
|
||||
$this->getSignature(), $expires);
|
||||
return $signature == $check;
|
||||
|
||||
return hash_equals($check, $signature);
|
||||
}
|
||||
|
||||
static function _genUrlSignature($id, $key, $signature, $expires) {
|
||||
@@ -270,7 +298,7 @@ class AttachmentFile extends VerySimpleModel {
|
||||
$inline = ($thisstaff ? ($thisstaff->getImageAttachmentView() === 'inline') : false);
|
||||
$disposition = ((($disposition && strcasecmp($disposition, 'inline') == 0)
|
||||
|| $inline)
|
||||
&& strpos($this->getType(), 'image/') !== false)
|
||||
&& $this->isInlineSafeImage())
|
||||
? 'inline' : 'attachment';
|
||||
$ttl = ($expires) ? $expires - Misc::gmtime() : false;
|
||||
$bk = $this->open();
|
||||
@@ -349,6 +377,7 @@ class AttachmentFile extends VerySimpleModel {
|
||||
case IMAGETYPE_GIF:
|
||||
case IMAGETYPE_JPEG:
|
||||
case IMAGETYPE_PNG:
|
||||
case IMAGETYPE_WEBP:
|
||||
break;
|
||||
default:
|
||||
$error = __('Invalid image file type');
|
||||
@@ -370,6 +399,7 @@ class AttachmentFile extends VerySimpleModel {
|
||||
case IMAGETYPE_GIF:
|
||||
case IMAGETYPE_JPEG:
|
||||
case IMAGETYPE_PNG:
|
||||
case IMAGETYPE_WEBP:
|
||||
break;
|
||||
default:
|
||||
$error = __('Invalid image file type');
|
||||
@@ -477,9 +507,11 @@ class AttachmentFile extends VerySimpleModel {
|
||||
elseif ($bk->write($file['data']) && $bk->flush()) {
|
||||
$succeeded = true; break;
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
} catch (Throwable $t) {
|
||||
// Try next backend
|
||||
// Backends can throw an exception or error.
|
||||
// TODO: Log any exceptions and errors for debugging
|
||||
// purposes.
|
||||
}
|
||||
// Fallthrough to default backend if different?
|
||||
}
|
||||
@@ -610,9 +642,16 @@ class AttachmentFile extends VerySimpleModel {
|
||||
static function lookupByHash($hash) {
|
||||
if (isset(static::$keyCache[$hash]))
|
||||
return static::$keyCache[$hash];
|
||||
|
||||
// Cache a negative lookup if no such file exists
|
||||
return parent::lookup(array('key' => $hash));
|
||||
try {
|
||||
return parent::lookup(array('key' => $hash));
|
||||
} catch (ObjectNotUnique $e) {
|
||||
// TODO: Figure out why key collission might be happening AND
|
||||
// make key (hash) unique field in the file table as a
|
||||
// protection measure. For now we're returning null to avoid possible wrong file
|
||||
// being displayed.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static function lookup($id) {
|
||||
@@ -693,6 +732,29 @@ class AttachmentFile extends VerySimpleModel {
|
||||
->filter(array('ft' => 'B'))
|
||||
->order_by('created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a MIME type is safe to render inline as a passive image.
|
||||
*
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
static function isInlineSafeImageType($type) {
|
||||
$type = strtolower(trim((string) $type));
|
||||
|
||||
return strpos($type, 'image/') === 0
|
||||
&& $type !== 'image/svg+xml'
|
||||
&& $type !== 'image/svg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this file is safe to render inline as a passive image.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function isInlineSafeImage() {
|
||||
return static::isInlineSafeImageType($this->getType());
|
||||
}
|
||||
}
|
||||
|
||||
class FileStorageBackend {
|
||||
@@ -1020,14 +1082,29 @@ class OneSixAttachments extends FileStorageBackend {
|
||||
FileStorageBackend::register('6', 'OneSixAttachments');
|
||||
|
||||
// FileObject - wrapper for SplFileObject class
|
||||
class FileObject extends SplFileObject {
|
||||
class FileObject extends SplFileObject
|
||||
implements FileObjectInterface {
|
||||
|
||||
protected $_key;
|
||||
protected $_sig;
|
||||
protected $_filename;
|
||||
|
||||
function __construct($file, $mode='r') {
|
||||
parent::__construct($file, $mode);
|
||||
}
|
||||
|
||||
function getUId() {
|
||||
return $this->getKey();
|
||||
}
|
||||
|
||||
function getKey() {
|
||||
if (!isset($this->_key))
|
||||
list($this->_key, $this->_sig) = AttachmentFile::_getKeyAndHash(
|
||||
$this->getContents());
|
||||
|
||||
return $this->_key;
|
||||
}
|
||||
|
||||
/* This allows us to set REAL file name as opposed to basename of the
|
||||
* FS file in question
|
||||
*/
|
||||
@@ -1039,6 +1116,10 @@ class FileObject extends SplFileObject {
|
||||
return $this->_filename ?: parent::getFilename();
|
||||
}
|
||||
|
||||
function getName() {
|
||||
return $this->getFilename();
|
||||
}
|
||||
|
||||
/*
|
||||
* Set mime type - well formated mime is expected.
|
||||
*/
|
||||
@@ -1047,12 +1128,8 @@ class FileObject extends SplFileObject {
|
||||
}
|
||||
|
||||
function getMimeType() {
|
||||
if (!isset($this->_mimetype)) {
|
||||
// Try to to auto-detect mime type
|
||||
$finfo = new finfo(FILEINFO_MIME);
|
||||
$this->_mimetype = $finfo->buffer($this->getContents(),
|
||||
FILEINFO_MIME_TYPE);
|
||||
}
|
||||
if (!isset($this->_mimetype))
|
||||
$this->_mimetype = self::mime_type($this->getRealPath());
|
||||
|
||||
return $this->_mimetype;
|
||||
}
|
||||
@@ -1068,6 +1145,28 @@ class FileObject extends SplFileObject {
|
||||
function getData() {
|
||||
return $this->getContents();
|
||||
}
|
||||
|
||||
/*
|
||||
* Given a filepath - auto detect the mime type
|
||||
*
|
||||
*/
|
||||
static function mime_type($filepath) {
|
||||
// Try to to auto-detect mime type
|
||||
$type = null;
|
||||
if (function_exists('finfo_open')) {
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$type = finfo_file($finfo, $filepath);
|
||||
finfo_close($finfo);
|
||||
}
|
||||
return $type ?: mime_content_type($filepath);
|
||||
}
|
||||
|
||||
/*
|
||||
* Compare mime type of file content to a given mime
|
||||
*/
|
||||
static function mimecmp($filepath, $mime) {
|
||||
return strcasecmp(self::mime_type($filepath), $mime) !== 0;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
@@ -502,36 +502,34 @@ extends VerySimpleModel {
|
||||
|
||||
if (is_array($rule)) {
|
||||
if($rule["w"] || $rule["h"]) {
|
||||
// Check for REGEX compile errors
|
||||
if (in_array($rule["h"], array('match','not_match'))) {
|
||||
$wrapped = "/".$rule["v"]."/iu";
|
||||
if (false === @preg_match($rule["v"], ' ')
|
||||
&& (false !== @preg_match($wrapped, ' ')))
|
||||
$rule["v"] = $wrapped;
|
||||
// Check for REGEX compile errors
|
||||
if (in_array($rule["h"], array('match','not_match'))) {
|
||||
$wrapped = "/".$rule["v"]."/iu";
|
||||
if (false === @preg_match($rule["v"], ' ')
|
||||
&& (false !== @preg_match($wrapped, ' ')))
|
||||
$rule["v"] = $wrapped;
|
||||
}
|
||||
|
||||
if(!$rule["w"] || !in_array($rule["w"],$matches))
|
||||
$errors["rule_$i"]=__('Invalid match selection');
|
||||
elseif(!$rule["h"] || !in_array($rule["h"],$types))
|
||||
$errors["rule_$i"]=__('Invalid match type selection');
|
||||
elseif(!$rule["v"])
|
||||
$errors["rule_$i"]=__('Value required');
|
||||
elseif($rule["w"]=='email'
|
||||
&& $rule["h"]=='equal'
|
||||
&& !Validator::is_email($rule["v"]))
|
||||
$errors["rule_$i"]=__('Valid email required for the match type');
|
||||
elseif (in_array($rule["h"], array('match','not_match'))
|
||||
&& (false === @preg_match($rule["v"], ' ')))
|
||||
$errors["rule_$i"] = sprintf(__('Regex compile error: (#%s)'),
|
||||
preg_last_error());
|
||||
else //for everything-else...we assume it's valid.
|
||||
$rules[]=array('what'=>$rule["w"],
|
||||
'how'=>$rule["h"],'val'=>trim($rule["v"]));
|
||||
} elseif($rule["v"]) {
|
||||
$errors["rule_$i"]=__('Incomplete selection');
|
||||
}
|
||||
|
||||
if(!$rule["w"] || !in_array($rule["w"],$matches))
|
||||
$errors["rule_$i"]=__('Invalid match selection');
|
||||
elseif(!$rule["h"] || !in_array($rule["h"],$types))
|
||||
$errors["rule_$i"]=__('Invalid match type selection');
|
||||
elseif(!$rule["v"])
|
||||
$errors["rule_$i"]=__('Value required');
|
||||
elseif($rule["w"]=='email'
|
||||
&& $rule["h"]=='equal'
|
||||
&& !Validator::is_email($rule["v"]))
|
||||
$errors["rule_$i"]=__('Valid email required for the match type');
|
||||
elseif (in_array($rule["h"], array('match','not_match'))
|
||||
&& (false === @preg_match($rule["v"], ' ')))
|
||||
$errors["rule_$i"] = sprintf(__('Regex compile error: (#%s)'),
|
||||
preg_last_error());
|
||||
|
||||
|
||||
else //for everything-else...we assume it's valid.
|
||||
$rules[]=array('what'=>$rule["w"],
|
||||
'how'=>$rule["h"],'val'=>trim($rule["v"]));
|
||||
}elseif($rule["v"]) {
|
||||
$errors["rule_$i"]=__('Incomplete selection');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,6 +564,15 @@ extends VerySimpleModel {
|
||||
return parent::save($refetch || $this->dirty);
|
||||
}
|
||||
|
||||
static function __create($vars,&$errors) {
|
||||
$filter = new static($vars);
|
||||
if ($filter->save()) {
|
||||
$filter->save_rules($vars, $errors);
|
||||
$filter->save_actions($filter->getId(), $vars, $errors);
|
||||
return $filter;
|
||||
}
|
||||
}
|
||||
|
||||
static function create($vars,&$errors) {
|
||||
$filter = new static($vars);
|
||||
if ($filter->save())
|
||||
@@ -908,6 +915,7 @@ class TicketFilter {
|
||||
);
|
||||
|
||||
foreach ($auto_headers as $header=>$find) {
|
||||
$header = strtolower($header);
|
||||
if(!isset($headers[$header])) continue;
|
||||
|
||||
$value = strtoupper($headers[$header]);
|
||||
@@ -931,8 +939,7 @@ class TicketFilter {
|
||||
}
|
||||
|
||||
static function isBounce($headers) {
|
||||
|
||||
if($headers && !is_array($headers))
|
||||
if ($headers && !is_array($headers))
|
||||
$headers = Mail_Parse::splitHeaders($headers);
|
||||
|
||||
$bounce_headers = array(
|
||||
@@ -947,6 +954,7 @@ class TicketFilter {
|
||||
);
|
||||
|
||||
foreach ($bounce_headers as $header => $find) {
|
||||
$header = strtolower($header);
|
||||
if(!isset($headers[$header])) continue;
|
||||
|
||||
@list($func, $searches, $pos, $neg) = $find;
|
||||
|
||||
@@ -89,7 +89,7 @@ class FilterAction extends VerySimpleModel {
|
||||
return $this->_impl;
|
||||
}
|
||||
|
||||
static function setFilterFlags(?object $actions=null, $flag, $bool) {
|
||||
static function setFilterFlags(?object $actions, $flag, $bool) {
|
||||
$flag = constant($flag);
|
||||
if ($actions)
|
||||
foreach ($actions as $action)
|
||||
@@ -642,7 +642,7 @@ class FA_SendEmail extends TriggerAction {
|
||||
|
||||
// Honor FROM address settings
|
||||
if (!$config['from'] || !($mailer = Email::lookup($config['from'])))
|
||||
$mailer = new Mailer();
|
||||
$mailer = new osTicket\Mail\Mailer();
|
||||
|
||||
// Allow %{user} in the To: line
|
||||
$replacer = new VariableReplacer();
|
||||
|
||||
@@ -278,6 +278,11 @@ class Format {
|
||||
$props = array();
|
||||
foreach ($styles as $i=>&$s) {
|
||||
@list($prop, $val) = explode(':', $s);
|
||||
// Strip all CSS url() usages in inline styles
|
||||
if (preg_match('/\burl\s*\(/i', $val)) {
|
||||
unset($styles[$i]);
|
||||
continue;
|
||||
}
|
||||
if (isset($props[$prop])) {
|
||||
unset($styles[$i]);
|
||||
continue;
|
||||
@@ -334,9 +339,10 @@ class Format {
|
||||
':<html[^>]+:i', # drop html attributes
|
||||
':<(a|span) (name|style)="(mso-bookmark\:)?_MailEndCompose">(.+)?<\/(a|span)>:', # Drop _MailEndCompose
|
||||
':<div dir=(3D)?"ltr">(.*?)<\/div>(.*):is', # drop Gmail "ltr" attributes
|
||||
':data-cid="[^"]*":', # drop image cid attributes
|
||||
'(position: ?(-webkit-)?(static|relative|fixed|absolute|sticky|initial|inherit);?)', # Position styling
|
||||
':[\x{2002}-\x{200B}]+:u', # unicode spaces
|
||||
),
|
||||
array('', '', '', '', '<html', '$4', '$2 $3', ''),
|
||||
array('', '', '', '', '<html', '$4', '$2 $3', '', ' '),
|
||||
$html);
|
||||
|
||||
// HtmLawed specific config only
|
||||
@@ -346,7 +352,7 @@ class Format {
|
||||
'comment' => 1, //Remove html comments (OUTLOOK LOVE THEM)
|
||||
'tidy' => -1,
|
||||
'elements' => '*-form-input-button',
|
||||
'deny_attribute' => 'id, formaction, action, on*',
|
||||
'deny_attribute' => 'id, formaction, action, srcset, data*, on*',
|
||||
'schemes' => 'href: aim, feed, file, ftp, gopher, http, https, irc, mailto, news, nntp, sftp, ssh, telnet; *:file, http, https; src: cid, http, https, data',
|
||||
'hook_tag' => function($e, $a=0) { return Format::__html_cleanup($e, $a); },
|
||||
);
|
||||
@@ -355,10 +361,13 @@ class Format {
|
||||
if ($cfg)
|
||||
$whitelist = $cfg->getIframeWhitelist();
|
||||
if (!empty($whitelist)) {
|
||||
$whitelist_quoted = array_map(function ($d) {
|
||||
return preg_quote($d, '`');
|
||||
}, $whitelist);
|
||||
$config['elements'] .= '+iframe';
|
||||
$config['spec'] = 'iframe=-*,height,width,type,style,src(match="`^(https?:)?//(www\.)?('
|
||||
.implode('|', $whitelist)
|
||||
.')/?([^@]*)$`i"),frameborder'.($options['spec'] ? '; '.$options['spec'] : '').',allowfullscreen';
|
||||
.implode('|', $whitelist_quoted)
|
||||
.')(\?|/|#)([^@]*)$`i"),frameborder'.($options['spec'] ? '; '.$options['spec'] : '').',allowfullscreen';
|
||||
}
|
||||
|
||||
return Format::html($html, $config);
|
||||
@@ -367,17 +376,17 @@ class Format {
|
||||
static function localizeInlineImages($text) {
|
||||
// Change file.php urls back to content-id's
|
||||
return preg_replace(
|
||||
'`src="(?:https?:/)?(?:/[^/"]+)*?/file\\.php\\?(?:\w+=[^&]+&(?:amp;)?)*?key=([^&]+)[^"]*`',
|
||||
'src="cid:$1', $text);
|
||||
'`<img src="(?:https?:/)?(?:/[^/"]+)*?/file\\.php\\?(?:\w+=[^&"]+&(?:amp;)?)*?key=([^&]+)[^"]*`',
|
||||
'<img src="cid:$1', $text);
|
||||
}
|
||||
|
||||
static function sanitize($text, $striptags=false, $spec=false) {
|
||||
// Localize inline images before sanitizing content
|
||||
$text = self::localizeInlineImages($text);
|
||||
|
||||
//balance and neutralize unsafe tags.
|
||||
$text = Format::safe_html($text, array('spec' => $spec));
|
||||
|
||||
$text = self::localizeInlineImages($text);
|
||||
|
||||
//If requested - strip tags with decoding disabled.
|
||||
return $striptags?Format::striptags($text, false):$text;
|
||||
}
|
||||
@@ -422,6 +431,14 @@ class Format {
|
||||
return htmlspecialchars_decode($var, $flags);
|
||||
}
|
||||
|
||||
static function http_query_string(string $query, ?array $filter = null) {
|
||||
$args = [];
|
||||
parse_str($query, $args);
|
||||
if ($filter && is_array($filter))
|
||||
$args = array_diff_key($args, array_flip($filter));
|
||||
return http_build_query($args);
|
||||
}
|
||||
|
||||
static function input($var) {
|
||||
return Format::htmlchars($var);
|
||||
}
|
||||
@@ -473,16 +490,17 @@ class Format {
|
||||
// Allowed Inline External Image Extensions
|
||||
$allowed = array('gif', 'png', 'jpg', 'jpeg');
|
||||
$exclude = !$cfg->allowExternalImages();
|
||||
$local = false;
|
||||
|
||||
$input = preg_replace_callback('/<img ([^>]*)(src="([^"]+)")([^>]*)\/?>/',
|
||||
function($match) use ($local, $allowed, $exclude, $display) {
|
||||
if (strpos($match[3], 'cid:') !== false)
|
||||
$local = true;
|
||||
$input = preg_replace_callback('/<img\b([^>]*?)\bsrc\s*=\s*(["\'])(.*?)\2([^>]*)\/?>/i',
|
||||
function($match) use ($allowed, $exclude, $display) {
|
||||
$src = $match[3];
|
||||
|
||||
// CID is only valid if it is the prefix of the src value
|
||||
$local = (stripos(ltrim($src), 'cid:') === 0);
|
||||
|
||||
// Split the src URL and get the extension
|
||||
$part = parse_url($match[3], PHP_URL_PATH);
|
||||
$path = explode('.', $part);
|
||||
$part = parse_url($src, PHP_URL_PATH);
|
||||
$path = explode('.', (string) $part);
|
||||
$ext = preg_split('/[^A-Za-z]/', end($path))[0];
|
||||
|
||||
if (!$local && (($exclude && $display) || !in_array($ext, $allowed)))
|
||||
@@ -490,7 +508,8 @@ class Format {
|
||||
else
|
||||
return $match[0];
|
||||
},
|
||||
$input);
|
||||
$input
|
||||
);
|
||||
|
||||
return $input;
|
||||
}
|
||||
@@ -527,7 +546,8 @@ class Format {
|
||||
'/[\x{23E0}-\x{23EF}]/u', # More Buttons
|
||||
'/[\x{2310}-\x{231F}]/u', # Hourglass/Watch
|
||||
'/[\x{1000B6}]/u', # Private Use Area (Plane 16)
|
||||
'/[\x{2322}-\x{232F}]/u' # Keyboard
|
||||
'/[\x{2322}-\x{232F}]/u', # Keyboard
|
||||
'/\x{00B0}|\x{00BA}/u' # Degree Symbol
|
||||
), '', $text);
|
||||
}
|
||||
|
||||
@@ -575,18 +595,22 @@ class Format {
|
||||
}
|
||||
|
||||
|
||||
static function viewableImages($html, $options=array()) {
|
||||
static function viewableImages($html, $options=array(), $format=false) {
|
||||
$cids = $images = array();
|
||||
$options +=array(
|
||||
'disposition' => 'inline');
|
||||
return preg_replace_callback('/"cid:([\w._-]{32})"/',
|
||||
$html = preg_replace_callback('/("|")cid:([\w._-]{32})("|")/',
|
||||
function($match) use ($options, $images) {
|
||||
if (!($file = AttachmentFile::lookup($match[1])))
|
||||
if (!($file = AttachmentFile::lookup($match[2])))
|
||||
return $match[0];
|
||||
|
||||
if (!$file->isInlineSafeImage())
|
||||
return $match[0];
|
||||
|
||||
return sprintf('"%s" data-cid="%s"',
|
||||
$file->getDownloadUrl($options), $match[1]);
|
||||
$file->getDownloadUrl($options), $match[2]);
|
||||
}, $html);
|
||||
return $format ? Format::htmlchars($html, true) : $html;
|
||||
}
|
||||
|
||||
|
||||
@@ -747,6 +771,7 @@ class Format {
|
||||
if ($cfg && $cfg->isForce24HourTime())
|
||||
$format = str_replace('X', 'R', $format);
|
||||
|
||||
// TODO: Deprecated; replace this soon
|
||||
return strftime($format, $timestamp);
|
||||
}
|
||||
|
||||
@@ -1021,7 +1046,7 @@ class Format {
|
||||
|
||||
// Performs Unicode normalization (where possible) and splits words at
|
||||
// difficult word boundaries (for far eastern languages)
|
||||
static function searchable($text, $lang=false) {
|
||||
static function searchable($text, $lang=false, $length=false) {
|
||||
global $cfg;
|
||||
|
||||
if (function_exists('normalizer_normalize')) {
|
||||
@@ -1053,6 +1078,10 @@ class Format {
|
||||
// Drop leading and trailing whitespace
|
||||
$text = trim($text);
|
||||
}
|
||||
|
||||
if ($length && (str_word_count($text) > $length))
|
||||
return null;
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ class Form {
|
||||
var $options = array();
|
||||
var $fields = array();
|
||||
var $title = '';
|
||||
var $notice = '';
|
||||
var $instructions = '';
|
||||
|
||||
var $validators = array();
|
||||
@@ -39,6 +40,8 @@ class Form {
|
||||
$this->title = $options['title'];
|
||||
if (isset($options['instructions']))
|
||||
$this->instructions = $options['instructions'];
|
||||
if (isset($options['notice']))
|
||||
$this->notice = $options['notice'];
|
||||
if (isset($options['id']))
|
||||
$this->id = $options['id'];
|
||||
|
||||
@@ -53,6 +56,10 @@ class Form {
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
function setNotice($notice) {
|
||||
$this->notice = $notice;
|
||||
}
|
||||
|
||||
function data($source) {
|
||||
foreach ($this->fields as $name=>$f)
|
||||
if (isset($source[$name]))
|
||||
@@ -112,6 +119,7 @@ class Form {
|
||||
}
|
||||
|
||||
function getTitle() { return $this->title; }
|
||||
function getNotice() { return $this->notice; }
|
||||
function getInstructions() { return Format::htmldecode($this->instructions); }
|
||||
function getSource() { return $this->_source; }
|
||||
function setSource($source) { $this->_source = $source; }
|
||||
@@ -163,11 +171,12 @@ class Form {
|
||||
* Transform form data to database ready clean data.
|
||||
*
|
||||
*/
|
||||
function to_db($validate=true) {
|
||||
if (!$this->isValid())
|
||||
function to_db($clean=null, $validate=true) {
|
||||
if (!$clean
|
||||
&& !$this->isValid()
|
||||
&& !($clean=$this->getClean($validate)))
|
||||
return false;
|
||||
$data = [];
|
||||
$clean = $this->getClean($validate);
|
||||
foreach ($clean as $name => $val) {
|
||||
if (!($f = $this->getField($name)))
|
||||
continue;
|
||||
@@ -225,6 +234,9 @@ class Form {
|
||||
$this->title = $options['title'];
|
||||
if (isset($options['instructions']))
|
||||
$this->instructions = $options['instructions'];
|
||||
if (isset($options['notice']))
|
||||
$this->notice = $options['notice'];
|
||||
|
||||
$form = $this;
|
||||
$template = $options['template'] ?: 'dynamic-form.tmpl.php';
|
||||
if (isset($options['staff']) && $options['staff'])
|
||||
@@ -457,9 +469,14 @@ implements FormRenderer {
|
||||
?>
|
||||
<table class="<?php echo 'grid form' ?>">
|
||||
<caption><?php echo Format::htmlchars($this->title ?: $form->getTitle()); ?>
|
||||
<div><small><?php echo Format::viewableImages($form->getInstructions()); ?></small></div>
|
||||
</caption>
|
||||
<tbody><tr><?php for ($i=0; $i<12; $i++) echo '<td style="width:8.3333%"/>'; ?></tr></tbody>
|
||||
<div><small><?php echo Format::viewableImages($form->getInstructions()); ?></small></div>
|
||||
<?php
|
||||
if ($form->getNotice())
|
||||
echo sprintf('<div><small><p id="msg_warning">%s</p></small></div>',
|
||||
Format::htmlchars($form->getNotice()));
|
||||
?>
|
||||
</caption>
|
||||
<tbody><tr><?php for ($i=0; $i<12; $i++) echo '<td style="width:8.3333%"/>'; ?></tr></tbody>
|
||||
<?php
|
||||
$row_size = 12;
|
||||
$cols = $row = 0;
|
||||
@@ -1524,7 +1541,7 @@ class TextboxField extends FormField {
|
||||
function($v) use ($config) {
|
||||
$regex = $config['regex'];
|
||||
return @preg_match($regex, $v);
|
||||
}, __('Value does not match required pattern')
|
||||
}, $config['validator-error'] ?? __('Value does not match required pattern')
|
||||
),
|
||||
);
|
||||
// Support configuration forms, as well as GUI-based form fields
|
||||
@@ -1772,6 +1789,10 @@ class BooleanField extends FormField {
|
||||
return ($value) ? __('Yes') : __('No');
|
||||
}
|
||||
|
||||
function asVar($value, $id=false) {
|
||||
return $this->toString($value);
|
||||
}
|
||||
|
||||
function getClean($validate=true) {
|
||||
if (!isset($this->_clean)) {
|
||||
$this->_clean = (isset($this->value))
|
||||
@@ -1846,7 +1867,10 @@ class ChoiceField extends FormField {
|
||||
'id'=>1, 'label'=>__('Choices'), 'required'=>false, 'default'=>'',
|
||||
'hint'=>__('List choices, one per line. To protect against spelling changes, specify key:value names to preserve entries if the list item names change.</br><b>Note:</b> If you have more than two choices, use a List instead.'),
|
||||
'validator'=>'choices',
|
||||
'configuration'=>array('html'=>false)
|
||||
'configuration'=>array(
|
||||
'html' => false,
|
||||
'disabled' => false,
|
||||
)
|
||||
)),
|
||||
'default' => new TextboxField(array(
|
||||
'id'=>3, 'label'=>__('Default'), 'required'=>false, 'default'=>'',
|
||||
@@ -2065,7 +2089,7 @@ class ChoiceField extends FormField {
|
||||
$name = $name ?: $this->get('name');
|
||||
$val = $value;
|
||||
if ($value && is_array($value))
|
||||
$val = '"?'.implode('("|,|$)|"?', array_keys($value)).'("|,|$)';
|
||||
$val = '"?(?<![0-9])'.implode('("|,|$)|"?(?<![0-9])', array_keys($value)).'("|,|$)';
|
||||
switch ($method) {
|
||||
case '!includes':
|
||||
return Q::not(array("{$name}__regex" => $val));
|
||||
@@ -2545,26 +2569,26 @@ class DatetimeField extends FormField {
|
||||
"{$name}__lte" => $right->format('Y-m-d H:i:s'),
|
||||
));
|
||||
case 'ndaysago':
|
||||
$int = $intervals[$value['int'] ?: 'd'] ?: 'DAY';
|
||||
$int = $intervals[(string) $value['int'] ?: 'd'] ?: 'DAY';
|
||||
$interval = new SqlInterval($int, $value['until']);
|
||||
return new Q(array(
|
||||
"{$name}__range" => array($now->minus($interval), $now),
|
||||
));
|
||||
case 'ndays':
|
||||
$int = $intervals[$value['int'] ?: 'd'] ?: 'DAY';
|
||||
$int = $intervals[(string) $value['int'] ?: 'd'] ?: 'DAY';
|
||||
$interval = new SqlInterval($int, $value['until']);
|
||||
return new Q(array(
|
||||
"{$name}__range" => array($now, $now->plus($interval)),
|
||||
));
|
||||
// Distant past and future ranges
|
||||
case 'distpast':
|
||||
$int = $intervals[$value['int'] ?: 'd'] ?: 'DAY';
|
||||
$int = $intervals[(string) $value['int'] ?: 'd'] ?: 'DAY';
|
||||
$interval = new SqlInterval($int, $value['until']);
|
||||
return new Q(array(
|
||||
"{$name}__lte" => $now->minus($interval),
|
||||
));
|
||||
case 'distfut':
|
||||
$int = $intervals[$value['int'] ?: 'd'] ?: 'DAY';
|
||||
$int = $intervals[(string) $value['int'] ?: 'd'] ?: 'DAY';
|
||||
$interval = new SqlInterval($int, $value['until']);
|
||||
return new Q(array(
|
||||
"{$name}__gte" => $now->plus($interval),
|
||||
@@ -2741,6 +2765,14 @@ class SectionBreakField extends FormField {
|
||||
function isBlockLevel() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function isEditableToStaff() {
|
||||
return $this->isVisibleToStaff();
|
||||
}
|
||||
|
||||
function isEditableToUsers() {
|
||||
return $this->isVisibleToUsers();
|
||||
}
|
||||
}
|
||||
|
||||
class ThreadEntryField extends FormField {
|
||||
@@ -3304,7 +3336,7 @@ class DepartmentField extends ChoiceField {
|
||||
|
||||
function getValue() {
|
||||
if (($value = parent::getValue()) && ($id=$this->getClean()))
|
||||
return $value[$id];
|
||||
return is_array($value) ? $value[$id] : $value;
|
||||
}
|
||||
|
||||
function to_php($value, $id=false) {
|
||||
@@ -3848,6 +3880,10 @@ class FileUploadField extends FormField {
|
||||
'hint'=>__('Optionally, enter comma-separated list of additional file types, by extension. (e.g .doc, .pdf).'),
|
||||
'configuration'=>array('html'=>false, 'rows'=>2),
|
||||
)),
|
||||
'strictmimecheck' => new BooleanField([
|
||||
'id' => 4, 'label'=>__('Strict Mime Type Check'), 'required' => false, 'default' => false,
|
||||
'hint' => 'File Mime Type associations is OS dependent',
|
||||
'configuration' => ['desc' => __('Enable strict Mime Type check')]]),
|
||||
'max' => new TextboxField(array(
|
||||
'label'=>__('Maximum Files'),
|
||||
'hint'=>__('Users cannot upload more than this many files.'),
|
||||
@@ -3872,8 +3908,9 @@ class FileUploadField extends FormField {
|
||||
Http::response(400, 'Send one file at a time');
|
||||
$file = array_shift($files);
|
||||
$file['name'] = urldecode($file['name']);
|
||||
$config = $this->getConfiguration();
|
||||
|
||||
if (!$this->isValidFile($file))
|
||||
if (!self::isValidFile($file, $config['strictmimecheck']))
|
||||
Http::response(413, 'Invalid File');
|
||||
|
||||
if (!$bypass && !$this->isValidFileType($file['name'], $file['type']))
|
||||
@@ -3902,10 +3939,11 @@ class FileUploadField extends FormField {
|
||||
if (!$this->isValidFileType($file['name'], $file['type']))
|
||||
throw new FileUploadError(__('File type is not allowed'));
|
||||
|
||||
if (!$this->isValidFile($file))
|
||||
$config = $this->getConfiguration();
|
||||
|
||||
if (!self::isValidFile($file, $config['strictmimecheck']))
|
||||
throw new FileUploadError(__('Invalid File'));
|
||||
|
||||
$config = $this->getConfiguration();
|
||||
if ($file['size'] > $config['size'])
|
||||
throw new FileUploadError(__('File size is too large'));
|
||||
|
||||
@@ -3942,13 +3980,25 @@ class FileUploadField extends FormField {
|
||||
return $F;
|
||||
}
|
||||
|
||||
function isValidFile($file) {
|
||||
/**
|
||||
* Strict mode can be enabled in Admin Panel > Settings > Tickets
|
||||
*
|
||||
* PS: Please note that the a mismatch can happen if the mime types
|
||||
* database is not up to date or a little different compared to what the
|
||||
* browser reports.
|
||||
**/
|
||||
static function isValidFile($file, $strict = false) {
|
||||
// Strict mime check
|
||||
if ($strict
|
||||
&& !empty($file['type'])
|
||||
&& FileObject::mimecmp($file['tmp_name'], $file['type']))
|
||||
return false;
|
||||
|
||||
// Check invalid image hacks
|
||||
if ($file['tmp_name']
|
||||
&& stripos($file['type'], 'image/') === 0
|
||||
&& function_exists('exif_imagetype')
|
||||
&& !exif_imagetype($file['tmp_name']))
|
||||
&& !(exif_imagetype($file['tmp_name'])
|
||||
|| mime_content_type($file['tmp_name']) === 'image/svg+xml'))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
@@ -4005,7 +4055,11 @@ class FileUploadField extends FormField {
|
||||
}
|
||||
|
||||
function getConfiguration() {
|
||||
global $cfg;
|
||||
|
||||
$config = parent::getConfiguration();
|
||||
// If no size present default to system setting
|
||||
$config['size'] ??= $cfg->getMaxFileSize();
|
||||
$_types = self::getFileTypes();
|
||||
$mimetypes = array();
|
||||
$extensions = array();
|
||||
@@ -4065,7 +4119,7 @@ class FileUploadField extends FormField {
|
||||
if (isset($this->attachments) && $this->attachments) {
|
||||
$this->attachments->keepOnlyFileIds($value);
|
||||
}
|
||||
return JsonDataEncoder::encode($value);
|
||||
return JsonDataEncoder::encode($value) ?? NULL;
|
||||
}
|
||||
|
||||
function parse($value) {
|
||||
@@ -4630,6 +4684,8 @@ class ChoicesWidget extends Widget {
|
||||
echo ' data-'.$D.'="'.Format::htmlchars($V).'"';
|
||||
?>
|
||||
data-placeholder="<?php echo Format::htmlchars($prompt); ?>"
|
||||
<?php if ($config['disabled'])
|
||||
echo ' disabled="disabled"'; ?>
|
||||
<?php if ($config['multiselect'])
|
||||
echo ' multiple="multiple"'; ?>>
|
||||
<?php if ($showdefault || (!$have_def && !$config['multiselect'])) { ?>
|
||||
@@ -5192,6 +5248,8 @@ class FileUploadWidget extends Widget {
|
||||
);
|
||||
|
||||
function render($options=array()) {
|
||||
global $ost;
|
||||
|
||||
$config = $this->field->getConfiguration();
|
||||
$name = $this->field->getFormName();
|
||||
$id = substr(md5(spl_object_hash($this)), 10);
|
||||
@@ -5255,7 +5313,7 @@ class FileUploadWidget extends Widget {
|
||||
// Get Form Type
|
||||
$type = $this->field->getForm()->type;
|
||||
// Determine if for Ticket/Task/Custom
|
||||
if ($type) {
|
||||
if ($type && !is_numeric($field_id)) {
|
||||
if ($type == 'T')
|
||||
$field_id = 'ticket/attach';
|
||||
elseif ($type == 'A')
|
||||
@@ -5277,6 +5335,7 @@ class FileUploadWidget extends Widget {
|
||||
url: 'ajax.php/form/upload/<?php echo $field_id; ?>',
|
||||
link: $('#<?php echo $id; ?>').find('a.manual'),
|
||||
paramname: 'upload[]',
|
||||
data: {"__CSRFToken__": "<?php echo $ost->getCSRF()->getToken(); ?>"},
|
||||
fallback_id: 'file-<?php echo $id; ?>',
|
||||
allowedfileextensions: <?php echo JsonDataEncoder::encode(
|
||||
$config['__extensions'] ?: array()); ?>,
|
||||
@@ -5379,6 +5438,14 @@ class FreeTextField extends FormField {
|
||||
return true;
|
||||
}
|
||||
|
||||
function isEditableToStaff() {
|
||||
return $this->isVisibleToStaff();
|
||||
}
|
||||
|
||||
function isEditableToUsers() {
|
||||
return $this->isVisibleToUsers();
|
||||
}
|
||||
|
||||
/* utils */
|
||||
|
||||
function to_config($config) {
|
||||
@@ -6077,7 +6144,7 @@ class TransferForm extends Form {
|
||||
'refer' => new BooleanField(array(
|
||||
'id'=>2, 'label'=>'', 'required'=>false, 'default'=>false,
|
||||
'configuration'=>array(
|
||||
'desc' => 'Maintain referral access to current department')
|
||||
'desc' => __('Maintain referral access to current department'))
|
||||
)),
|
||||
'comments' => new TextareaField(array(
|
||||
'id' => 3,
|
||||
@@ -6119,7 +6186,7 @@ class TransferForm extends Form {
|
||||
break;
|
||||
default:
|
||||
throw new Exception(sprintf(__('%s: Unknown template style %s'),
|
||||
get_class(), $options['template']));
|
||||
get_class($this), $options['template']));
|
||||
}
|
||||
|
||||
$form = $this;
|
||||
|
||||
@@ -113,10 +113,6 @@ class Http {
|
||||
if (false !== strpos($user_agent,'msie')
|
||||
&& false !== strpos($user_agent,'win'))
|
||||
return 'filename='.rawurlencode($filename);
|
||||
elseif (false !== strpos($user_agent, 'safari')
|
||||
&& false === strpos($user_agent, 'chrome'))
|
||||
// Safari and Safari only can handle the filename as is
|
||||
return 'filename='.str_replace(',', '', $filename);
|
||||
else
|
||||
// Use RFC5987
|
||||
return "filename*=UTF-8''".rawurlencode($filename);
|
||||
@@ -151,5 +147,59 @@ class Http {
|
||||
|
||||
return http_build_query($vars, '', $separator);
|
||||
}
|
||||
|
||||
static function domain() {
|
||||
$domain = null;
|
||||
if (isset($_SERVER['HTTP_HOST'])
|
||||
&& strpos($_SERVER['HTTP_HOST'], '.') !== false
|
||||
&& !Validator::is_ip($_SERVER['HTTP_HOST']))
|
||||
// Remote port specification, as it will make an invalid domain
|
||||
list($domain) = explode(':', $_SERVER['HTTP_HOST']);
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
// Get current url
|
||||
static function url() {
|
||||
// Scheme + Host.
|
||||
$https = osTicket::is_https();
|
||||
$url = sprintf('http%s://%s',
|
||||
$https ? 's' : '',
|
||||
$_SERVER['HTTP_HOST']);
|
||||
// Add Port if not 80 and not https
|
||||
$port = osTicket::get_client_port();
|
||||
if ($port && $port != 80 && !$https && !str_contains($url, $port))
|
||||
$url .= ":{$port}";
|
||||
// Path + Query string
|
||||
if (isset($_SERVER['REQUEST_URI']))
|
||||
$url .= $_SERVER['REQUEST_URI'];
|
||||
else
|
||||
$url .= sprintf('%s?$s',
|
||||
$_SERVER['PHP_SELF'], $_SERVER['QUERY_STRING']);
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
// Parse path from given url or the current url
|
||||
static function url_path(?string $url = null, bool $htmlentities = true) {
|
||||
$path = parse_url($url ?: self::url(), PHP_URL_PATH);
|
||||
return ($htmlentities && $path)
|
||||
? htmlentities($path) : $path;
|
||||
}
|
||||
|
||||
// Parse query string from current url
|
||||
static function query_string(?string $url = null) {
|
||||
return parse_url($url ?: self::url(), PHP_URL_QUERY);
|
||||
}
|
||||
|
||||
// Refresh url
|
||||
static function refresh_url(array $queryFilter = []) {
|
||||
$url = self::url();
|
||||
$refresh_url = self::url_path($url);
|
||||
if (($qs=self::query_string($url)))
|
||||
$refresh_url .= sprintf('?%s',
|
||||
Format::http_query_string($qs, $queryFilter));
|
||||
return $refresh_url;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -49,14 +49,14 @@ class Internationalization {
|
||||
function loadDefaultData() {
|
||||
# notrans -- do not translate the contents of this array
|
||||
$models = array(
|
||||
'department.yaml' => 'Dept::__create',
|
||||
'sla.yaml' => 'SLA::__create',
|
||||
'department.yaml' => 'Dept::__create',
|
||||
'form.yaml' => 'DynamicForm::create',
|
||||
'list.yaml' => 'DynamicList::create',
|
||||
// Note that department, sla, and forms are required for
|
||||
// help_topic
|
||||
'help_topic.yaml' => 'Topic::__create',
|
||||
'filter.yaml' => 'Filter::create',
|
||||
'filter.yaml' => 'Filter::__create',
|
||||
'team.yaml' => 'Team::__create',
|
||||
// Organization
|
||||
'organization.yaml' => 'Organization::__create',
|
||||
@@ -182,7 +182,7 @@ class Internationalization {
|
||||
global $thisstaff, $thisclient;
|
||||
|
||||
$langs = self::availableLanguages();
|
||||
$lang = strtolower($lang);
|
||||
$lang = str_replace('-', '_', strtolower($lang));
|
||||
if (isset($langs[$lang])) {
|
||||
$info = &$langs[$lang];
|
||||
if (!isset($info['desc'])) {
|
||||
@@ -213,7 +213,7 @@ class Internationalization {
|
||||
|
||||
static function getLanguageInfo($lang) {
|
||||
$langs = self::availableLanguages();
|
||||
return @$langs[strtolower($lang)] ?: array();
|
||||
return @$langs[str_replace('-', '_', strtolower($lang))] ?: array();
|
||||
}
|
||||
|
||||
static function availableLanguages($base=I18N_DIR) {
|
||||
@@ -251,7 +251,7 @@ class Internationalization {
|
||||
|
||||
static function isLanguageInstalled($code) {
|
||||
$langs = self::availableLanguages();
|
||||
return isset($langs[strtolower($code)]);
|
||||
return isset($langs[strtolower((string) $code)]);
|
||||
}
|
||||
|
||||
static function isLanguageEnabled($code) {
|
||||
|
||||
@@ -31,8 +31,6 @@ class CsvImporter {
|
||||
function __construct($stream) {
|
||||
// File upload
|
||||
if (is_array($stream) && !$stream['error']) {
|
||||
// Properly detect Macintosh style line endings
|
||||
ini_set('auto_detect_line_endings', true);
|
||||
$this->stream = fopen($stream['tmp_name'], 'r');
|
||||
// Skip Byte Order Mark (BOM) if present
|
||||
if (!self::isBOM(fgets($this->stream, 4)))
|
||||
@@ -65,7 +63,7 @@ class CsvImporter {
|
||||
$named_fields[$f->get('name')] = $f;
|
||||
|
||||
// Read the first row and see if it is a header or not
|
||||
if (!($data = fgetcsv($this->stream, 1000, ",")))
|
||||
if (!($data = fgetcsv($this->stream, 1000, ",", "\"", "")))
|
||||
throw new ImportError(__('Whoops. Perhaps you meant to send some CSV records'));
|
||||
|
||||
$headers = array();
|
||||
@@ -172,7 +170,7 @@ implements Iterator {
|
||||
|
||||
function next() {
|
||||
do {
|
||||
if (($csv = fgetcsv($this->stream, 4096, ",")) === false) {
|
||||
if (($csv = fgetcsv($this->stream, 4096, ",", "\"", "")) === false) {
|
||||
// Read error
|
||||
$this->current = false;
|
||||
break;
|
||||
@@ -191,7 +189,7 @@ implements Iterator {
|
||||
foreach ($this->headers as $h => $label) {
|
||||
$f = $this->fields[$h];
|
||||
$f->_errors = array();
|
||||
$T = $f->parse($csv[$i]);
|
||||
$T = $f->parse(trim($csv[$i]));
|
||||
if ($f->validateEntry($T) && $f->errors())
|
||||
throw new ImportDataError(sprintf(__(
|
||||
/* 1 will be a field label, and 2 will be error messages */
|
||||
|
||||
@@ -596,8 +596,6 @@ class DynamicList extends VerySimpleModel implements CustomList {
|
||||
|
||||
function importFromPost($stuff, $extra=array()) {
|
||||
if (is_array($stuff) && !$stuff['error']) {
|
||||
// Properly detect Macintosh style line endings
|
||||
ini_set('auto_detect_line_endings', true);
|
||||
$stream = fopen($stuff['tmp_name'], 'r');
|
||||
}
|
||||
elseif ($stuff) {
|
||||
|
||||
@@ -21,25 +21,73 @@ namespace osTicket\Mail {
|
||||
use Laminas\Mime\Message as MimeMessage;
|
||||
use Laminas\Mime\Mime;
|
||||
use Laminas\Mime\Part as MimePart;
|
||||
use Laminas\Mail\Header;
|
||||
use osTicket\Mail\Header\ReturnPath;
|
||||
use osTicket\Mime\Rfc2231Part;
|
||||
|
||||
class Message extends MailMessage {
|
||||
private $mimeMessage = null;
|
||||
// Charset
|
||||
private $charset = 'utf-8';
|
||||
// Message Id (mid)
|
||||
private $mid;
|
||||
// MimeMessage Parts
|
||||
private $mimeParts = null;
|
||||
// MimeMessage Content
|
||||
private $mimeContent = null;
|
||||
// Default Charset
|
||||
protected $charset = 'utf-8';
|
||||
// Default Encoding (upstream is ASCII)
|
||||
protected $encoding = 'utf-8';
|
||||
|
||||
// Internal flags used to set Content-Type
|
||||
private $hasHtml = false;
|
||||
private $hasAttachments = false;
|
||||
private $hosInlineImages = false;
|
||||
private $hasInlineImages = false;
|
||||
|
||||
public function getMimeMessage() {
|
||||
if (!isset($this->mimeMessage))
|
||||
$this->mimeMessage = new MimeMessage();
|
||||
|
||||
return $this->mimeMessage;
|
||||
public function hasAttachments() {
|
||||
return $this->hasAttachments;
|
||||
}
|
||||
|
||||
public function addHeader($key, $value) {
|
||||
return $this->getHeaders()->addHeaderLine($key, $value);
|
||||
public function hasInlineImages() {
|
||||
return $this->hasInlineImages;
|
||||
}
|
||||
|
||||
public function hasHtml() {
|
||||
return $this->hasHtml;
|
||||
}
|
||||
// Files either attached or inline
|
||||
public function hasFiles() {
|
||||
return ($this->hasAttachments() || $this->hasInlineImages());
|
||||
}
|
||||
|
||||
public function getId() {
|
||||
if (!isset($this->mid)
|
||||
&& ($header=$this->getHeader('message-id')))
|
||||
$this->mid = $header->getId();
|
||||
return $this->mid;
|
||||
}
|
||||
|
||||
public function getMimeMessageParts() {
|
||||
if (!isset($this->mimeParts))
|
||||
$this->mimeParts = new MimeMessage();
|
||||
|
||||
return $this->mimeParts;
|
||||
}
|
||||
|
||||
public function getMimeMessageContent() {
|
||||
if (!isset($this->mimeContent))
|
||||
$this->mimeContent = new ContentMimeMessage();
|
||||
|
||||
return $this->mimeContent;
|
||||
}
|
||||
|
||||
public function getHeader(string $name) {
|
||||
return $this->getHeaders()->getHeader($name);
|
||||
}
|
||||
|
||||
public function addHeader($header, $value=null) {
|
||||
if (isset($value))
|
||||
$this->getHeaders()->addHeaderLine($header, $value);
|
||||
else
|
||||
$this->getHeaders()->addHeader($header);
|
||||
}
|
||||
|
||||
public function addHeaders(array $headers) {
|
||||
@@ -48,16 +96,19 @@ namespace osTicket\Mail {
|
||||
}
|
||||
|
||||
private function addMimePart(MimePart $part) {
|
||||
$this->getMimeMessage()->addPart($part);
|
||||
$this->getMimeMessageParts()->addPart($part);
|
||||
}
|
||||
|
||||
private function addMimeContent(MimePart $part) {
|
||||
$this->getMimeMessageContent()->addPart($part);
|
||||
}
|
||||
|
||||
public function setTextBody($text, $encoding=false) {
|
||||
$part = new MimePart($text);
|
||||
$part->type = Mime::TYPE_TEXT;
|
||||
$part->charset = $this->charset;
|
||||
$part->encoding = $encoding ?: Mime::ENCODING_BASE64;
|
||||
$this->addMimePart($part);
|
||||
//$this->setContentType($alt ? 'multipart/alternative' : 'text/plain');
|
||||
$part->encoding = $encoding;
|
||||
$this->addMimeContent($part);
|
||||
}
|
||||
|
||||
public function setHtmlBody($html, $encoding=false) {
|
||||
@@ -65,41 +116,138 @@ namespace osTicket\Mail {
|
||||
$part->type = Mime::TYPE_HTML;
|
||||
$part->charset = $this->charset;
|
||||
$part->encoding = $encoding ?: Mime::ENCODING_BASE64;
|
||||
$this->addMimePart($part);
|
||||
$this->addMimeContent($part);
|
||||
$this->hasHtml = true;
|
||||
}
|
||||
|
||||
public function addInlineImage($id, $file) {
|
||||
$f = new MimePart($file->getData());
|
||||
$name = $file->getName();
|
||||
$asciiName = self::asciiFallback($name);
|
||||
$f = new Rfc2231Part($file->getData());
|
||||
$f->id = $id;
|
||||
$f->type = $file->getMimeType();
|
||||
$f->filename = $file->getName();
|
||||
$f->disposition = Mime::DISPOSITION_INLINE;
|
||||
$f->encoding = Mime::ENCODING_BASE64;
|
||||
$f->disposition = Mime::DISPOSITION_INLINE;
|
||||
$f->filename = $asciiName;
|
||||
$f->setRawFilename($name);
|
||||
$this->addMimePart($f);
|
||||
$this->hasInlineImages = true;
|
||||
}
|
||||
|
||||
public function addAttachment($file, $name=null) {
|
||||
$f = new MimePart($file->getData());
|
||||
$name = $name ?: $file->getName();
|
||||
$asciiName = self::asciiFallback($name);
|
||||
$f = new Rfc2231Part($file->getData());
|
||||
$f->type = $file->getMimeType();
|
||||
$f->filename = $name ?: $file->getName();
|
||||
$f->disposition = Mime::DISPOSITION_ATTACHMENT;
|
||||
$f->encoding = Mime::ENCODING_BASE64;
|
||||
$f->disposition = Mime::DISPOSITION_ATTACHMENT;
|
||||
$f->filename = $asciiName;
|
||||
$f->setRawFilename($name);
|
||||
$this->addMimePart($f);
|
||||
$this->hasAttachments = true;
|
||||
}
|
||||
|
||||
public function setContentType($type=null) {
|
||||
// We can only set content type for multipart message
|
||||
if ($this->body->isMultiPart()) {
|
||||
// Set Content-Type
|
||||
$contentType = $type ?: 'text/plain';
|
||||
if ($this->hasHtml)
|
||||
$contentType = 'multipart/alternative';
|
||||
if ($this->hasAttachments)
|
||||
$contentType = 'multipart/related';
|
||||
// Expects a valid date e.g date('r')
|
||||
public function setDate(string $date) {
|
||||
$d = new Header\Date($date);
|
||||
// Laminas auto adds Date upstream when any header is added
|
||||
// We're clearing it here to we back that-date up like it's
|
||||
// 99 & 2000 ~ Juvenile
|
||||
$this->getHeaders()->removeHeader('date');
|
||||
$this->addHeader($d);
|
||||
}
|
||||
|
||||
// Please use this method to set Message-Id otherwise it will be
|
||||
// utf-8 endcoded and results is an invalid email & bounces
|
||||
public function setMessageId(string $id) {
|
||||
try {
|
||||
$header = new Header\MessageId();
|
||||
$header->setId($id);
|
||||
$this->addHeader($header);
|
||||
$this->mid = $header->getId();
|
||||
} catch (\Throwable $t) {
|
||||
// Ignore invalid mid. Upstream will generate one
|
||||
}
|
||||
}
|
||||
|
||||
public function getMessageId() {
|
||||
return $this->getId();
|
||||
}
|
||||
|
||||
// Valid email address required or no return "<>" tag
|
||||
public function setReturnPath($email) {
|
||||
try {
|
||||
// Exception is thrown on invalid email address
|
||||
$header = new ReturnPath();
|
||||
$header->addAddress($email);
|
||||
$this->getHeaders()->removeHeader($header->getType());
|
||||
$this->addHeader($header);
|
||||
} catch (\Throwable $t) {
|
||||
// It's not email - perhaps it's a tag?
|
||||
if (!strcmp($email, '<>'))
|
||||
$this->addHeader($header->getFieldName(), $email);
|
||||
// Silently dropping the invalid path
|
||||
}
|
||||
}
|
||||
|
||||
public function addInReplyTo($inReplyTo) {
|
||||
if (!is_array($inReplyTo))
|
||||
$inReplyTo = explode(' ', $inReplyTo);
|
||||
try {
|
||||
$header = new Header\InReplyTo();
|
||||
$header->setIds($inReplyTo); #nolint
|
||||
$this->addHeader($header);
|
||||
} catch (\Throwable $t) {
|
||||
// Mshenzi
|
||||
}
|
||||
}
|
||||
|
||||
public function addReferences($references) {
|
||||
if (!is_array($references))
|
||||
$references = explode(' ', $references);
|
||||
try {
|
||||
$header = new Header\References();
|
||||
$header->setIds($references); #nolint
|
||||
$this->addHeader($header);
|
||||
} catch (\Throwable $t) {
|
||||
// Mshenzi
|
||||
}
|
||||
}
|
||||
|
||||
// Set Message Sender is useful for SendMail transport, its basically -f
|
||||
// parameter in the mail() interface
|
||||
public function setSender($email, $name=null) {
|
||||
try {
|
||||
// Exception is thrown on invalid email address
|
||||
$header = new Header\Sender();
|
||||
$header->setAddress($email, $name); #nolint
|
||||
$this->addHeader($header);
|
||||
} catch (\Throwable $t) {
|
||||
// Silently ignore invalid email sender defaults to FROM
|
||||
// addresses
|
||||
}
|
||||
}
|
||||
|
||||
public function setFrom($email, $name=null) {
|
||||
// We're resetting the body here when FROM address changes - e.g
|
||||
// after failed send attempt while trying multiple SMTP accounts
|
||||
unset($this->body);
|
||||
return parent::setFrom($email, $name);
|
||||
}
|
||||
|
||||
// This is used to set FROM & and clear Sender to a new Email Address
|
||||
public function setOriginator($email, $name=null) {
|
||||
// Set the FROM Header
|
||||
$this->setFrom($email, $name);
|
||||
// Remove Sender Header
|
||||
$this->getHeaders()->removeHeader('sender');
|
||||
}
|
||||
|
||||
public function setContentType($contentType) {
|
||||
// We can only set content type for multipart message
|
||||
if (isset($this->body)
|
||||
&& $this->body->isMultiPart()
|
||||
&& $contentType) {
|
||||
if (($header=$this->getHeaders()->get('Content-Type')))
|
||||
$header->setType($contentType); #nolint
|
||||
else
|
||||
@@ -108,9 +256,67 @@ namespace osTicket\Mail {
|
||||
}
|
||||
|
||||
public function setBody($body=null) {
|
||||
$body = $body ?: $this->getMimeMessage();
|
||||
// We're ignoring $body param on purpose - only added for
|
||||
// upstream compatibility - local interfaces should use
|
||||
// prepare() to set the body
|
||||
$body = $this->getMimeMessageContent();
|
||||
$contentType = $this->hasHtml()
|
||||
? Mime::MULTIPART_ALTERNATIVE
|
||||
: Mime::TYPE_TEXT;
|
||||
// if we have files (inline images or attachments)
|
||||
if ($this->hasFiles()) {
|
||||
// Content MimePart
|
||||
$content = $body->getContentMimePart();
|
||||
// Get attachments parts (inline and files)
|
||||
$parts = $this->getMimeMessageParts()->getParts();
|
||||
// prepend content part to files parts
|
||||
array_unshift($parts, $content);
|
||||
// Create a new Mime Message and set parts
|
||||
$body = new MimeMessage();
|
||||
$body->setParts($parts); #nolint
|
||||
// We we only have inline images then content type is related
|
||||
// otherwise it's mixed.
|
||||
$contentType = $this->hasAttachments()
|
||||
? Mime::MULTIPART_MIXED
|
||||
: Mime::MULTIPART_RELATED;
|
||||
}
|
||||
// Set body beaches
|
||||
parent::setBody($body);
|
||||
$this->setContentType();
|
||||
// Set the content type
|
||||
$this->setContentType($contentType);
|
||||
}
|
||||
|
||||
public function prepare() {
|
||||
if (!isset($this->body))
|
||||
$this->setBody();
|
||||
}
|
||||
|
||||
private static function asciiFallback(string $utf8): string {
|
||||
$ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $utf8);
|
||||
$ascii = $ascii !== false ? $ascii : '';
|
||||
// Sanitize to header-safe ASCII
|
||||
$ascii = preg_replace('/[^A-Za-z0-9._-]+/', '_', $ascii ?? '') ?? '';
|
||||
return $ascii !== '' ? $ascii : $utf8;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// This is a wrapper class for Mime/Message that generates multipart
|
||||
// alternative content when email is multipart
|
||||
class ContentMimeMessage extends MimeMessage {
|
||||
public function getContent() {
|
||||
// unpack content parts to a content mime part
|
||||
return $this->generateMessage(); #nolint
|
||||
}
|
||||
|
||||
public function getContentMimePart($type=null) {
|
||||
$part = new MimePart($this->getContent()); #nolint
|
||||
$part->type = $type ?: Mime::MULTIPART_ALTERNATIVE;
|
||||
// Set the alternate content boundary
|
||||
$part->setBoundary($this->getMime()->boundary()); #nolint
|
||||
// Clear the encoding
|
||||
$part->encoding = "";
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,21 +324,23 @@ namespace osTicket\Mail {
|
||||
use Laminas\Mail\Protocol\Imap as ImapProtocol;
|
||||
use Laminas\Mail\Protocol\Pop3 as Pop3Protocol;
|
||||
trait MailBoxProtocolTrait {
|
||||
final public function init(AccountOptions $accountOptions) {
|
||||
final public function init(AccountSetting $setting) {
|
||||
// Attempt to connect to the mail server
|
||||
$connect = $accountOptions->getConnectioOptions();
|
||||
$connect = $setting->getConnectionConfig();
|
||||
// Let's go Brandon
|
||||
parent::connect($connect['host'], $connect['port'],
|
||||
$connect['ssl']);
|
||||
parent::__construct($connect['host'], $connect['port'],
|
||||
$connect['ssl'], true);
|
||||
// Attempt authentication based on MailBoxAccount settings
|
||||
$auth = $accountOptions->getAuth();
|
||||
$auth = $setting->getAuthCredentials();
|
||||
switch (true) {
|
||||
case $auth instanceof BasicAuthCredentials:
|
||||
if (!$this->basicAuth($auth->getUsername(), $auth->getPassword()))
|
||||
throw new Exception('cannot login, user or password wrong');
|
||||
break;
|
||||
case $auth instanceof OAuth2AuthCredentials:
|
||||
if (!$this->oauth2Auth($auth->getAccessToken()))
|
||||
// Get OAuth2 Authentication Request
|
||||
$authen = $auth->getAuthRequest($setting->getUser());
|
||||
if (!$this->oauth2Auth($authen))
|
||||
throw new Exception('OAuth2 Authentication Error');
|
||||
break;
|
||||
default:
|
||||
@@ -148,14 +356,14 @@ namespace osTicket\Mail {
|
||||
return $this->login($username, $password);
|
||||
}
|
||||
|
||||
abstract public function __construct($accountOptions);
|
||||
abstract private function oauth2Auth(AccessToken $token);
|
||||
abstract public function __construct($accountSetting);
|
||||
abstract protected function oauth2Auth($authen);
|
||||
}
|
||||
|
||||
class ImapMailboxProtocol extends ImapProtocol {
|
||||
use MailBoxProtocolTrait;
|
||||
public function __construct($accountOptions) {
|
||||
$this->init($accountOptions);
|
||||
public function __construct($accountSetting) {
|
||||
$this->init($accountSetting);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -167,9 +375,8 @@ namespace osTicket\Mail {
|
||||
* S: A01 (OK|NO|BAD)
|
||||
* [connection continues...]
|
||||
*/
|
||||
private function oauth2Auth(AccessToken $token) {
|
||||
$this->sendRequest('AUTHENTICATE', ['XOAUTH2',
|
||||
$token->getAuthRequest()]);
|
||||
private function oauth2Auth($authen) {
|
||||
$this->sendRequest('AUTHENTICATE', ['XOAUTH2', $authen]);
|
||||
while (true) {
|
||||
$matches = [];
|
||||
$response = '';
|
||||
@@ -194,8 +401,8 @@ namespace osTicket\Mail {
|
||||
|
||||
class Pop3MailboxProtocol extends Pop3Protocol {
|
||||
use MailBoxProtocolTrait;
|
||||
public function __construct($accountOptions) {
|
||||
$this->init($accountOptions);
|
||||
public function __construct($accountSetting) {
|
||||
$this->init($accountSetting);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -206,19 +413,19 @@ namespace osTicket\Mail {
|
||||
* S: (+OK|-ERR|+ {msg})
|
||||
* [connection continues...]
|
||||
*/
|
||||
public function oauth2Auth(AccessToken $token) {
|
||||
public function oauth2Auth($authen) {
|
||||
$this->sendRequest('AUTH XOAUTH2');
|
||||
while (true) {
|
||||
$response = $this->readLine();
|
||||
$matches = [];
|
||||
if ($response == '+') {
|
||||
// Send xOAuthRequest
|
||||
$this->sendRequest($token->getAuthRequest());
|
||||
$this->sendRequest($authen);
|
||||
} elseif (preg_match("/^\+OK /i", $response)) {
|
||||
return true;
|
||||
} elseif (preg_match('/^-ERR (.*+)$/i',
|
||||
$response, $matches)) {
|
||||
throw new Exception($matches[2]);
|
||||
throw new Exception($matches[1]);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -240,6 +447,14 @@ namespace osTicket\Mail {
|
||||
return trim($result);
|
||||
}
|
||||
|
||||
public function login($user, $password, $tryApop = true) {
|
||||
try {
|
||||
parent::login($user, $password, $tryApop);
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
throw new Exception(__('login failed').': '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MailBoxStorageTrait
|
||||
@@ -248,9 +463,15 @@ namespace osTicket\Mail {
|
||||
use RecursiveIteratorIterator;
|
||||
trait MailBoxStorageTrait {
|
||||
private $folder;
|
||||
private $hostInfo;
|
||||
|
||||
private function init(\MailBoxAccount $mailbox) {
|
||||
$this->folder = $mailbox->getFolder();
|
||||
private function init(AccountSetting $setting) {
|
||||
$this->folder = $setting->getAccount()->getFolder();
|
||||
$this->hostInfo = $setting->getHostInfo();
|
||||
}
|
||||
|
||||
public function getHostInfo() {
|
||||
return $this->hostInfo;
|
||||
}
|
||||
|
||||
private function getFolder() {
|
||||
@@ -271,8 +492,7 @@ namespace osTicket\Mail {
|
||||
public function hasFolder($folder, $rootFolder = null) {
|
||||
$folders = $this->getFolders($rootFolder);
|
||||
if (is_array($folders)
|
||||
&& in_array(strtolower($folder),
|
||||
array_map('strtolower', $folders)))
|
||||
&& in_array(strtolower($folder), $folders))
|
||||
return true;
|
||||
|
||||
// Try selecting the folder.
|
||||
@@ -295,23 +515,52 @@ namespace osTicket\Mail {
|
||||
foreach ($folders as $name => $folder) {
|
||||
if (!$folder->isSelectable()) #nolint
|
||||
continue;
|
||||
$this->folders[] = $folder->getGlobalName(); #nolint
|
||||
$this->folders[] = strtolower($folder->getGlobalName()); #nolint
|
||||
}
|
||||
}
|
||||
return $this->folders;
|
||||
}
|
||||
|
||||
/*
|
||||
* getRawEmail
|
||||
*
|
||||
* Given message number - get full raw email (headers + content)
|
||||
*
|
||||
*/
|
||||
public function getRawEmail(int $i) {
|
||||
return trim($this->getRawHeader($i)) . "\r\n\r\n" . $this->getRawContent($i);
|
||||
}
|
||||
|
||||
/*
|
||||
* move an existing message to a folder
|
||||
*
|
||||
* Caller should catch possible exception
|
||||
*/
|
||||
public function moveMessage($i, $folder) {
|
||||
parent::moveMessage($i, $folder);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove a message from server.
|
||||
*
|
||||
* Caller should catch possible exception
|
||||
*/
|
||||
public function removeMessage($i) {
|
||||
parent::removeMessage($i);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* markAsSeen
|
||||
*/
|
||||
public function markAsSeen($i) {
|
||||
// noop - storage that implement it should define it
|
||||
}
|
||||
public function markAsSeen($i) {
|
||||
// noop - storage that implement it should define it
|
||||
}
|
||||
|
||||
public function expunge() {
|
||||
public function expunge() {
|
||||
// noop - only IMAP
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Imap
|
||||
@@ -320,15 +569,40 @@ namespace osTicket\Mail {
|
||||
use MailBoxStorageTrait;
|
||||
private $folders;
|
||||
|
||||
public function __construct($accountOptions) {
|
||||
$protocol = new ImapMailBoxProtocol($accountOptions);
|
||||
public function __construct($accountSetting) {
|
||||
$protocol = new ImapMailBoxProtocol($accountSetting);
|
||||
parent::__construct($protocol);
|
||||
$this->init($accountOptions->getAccount());
|
||||
$this->init($accountSetting);
|
||||
}
|
||||
|
||||
// Mark message as seen
|
||||
public function markAsSeen($i) {
|
||||
$this->setFlags($i, [Storage::FLAG_SEEN]);
|
||||
try {
|
||||
return $this->setFlags($i, [Storage::FLAG_SEEN]);
|
||||
} catch (\Throwable $t) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a message from server without expunging the mailbox
|
||||
*
|
||||
* Laminas Mail (upstream) auto expunges the mailbox on message
|
||||
* removal or move (copy + remove) - which can cause major issues
|
||||
* for us since we fetcher uses message sequence numbers to fetch
|
||||
* messages / emails.
|
||||
*
|
||||
* We expunge the mailbox at the end if fetch session.
|
||||
*
|
||||
* TODO: Make PR upstream to support calling removeMessage with
|
||||
* a boolean flag i.e removeMessage(int $id, bool $expunge = true)
|
||||
*
|
||||
*/
|
||||
public function removeMessage($i) {
|
||||
if (! $this->protocol->store([Storage::FLAG_DELETED], $i, null, '+')) {
|
||||
throw new Exception('cannot set deleted flag');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Expunge mailbox
|
||||
@@ -341,10 +615,10 @@ namespace osTicket\Mail {
|
||||
class Pop3 extends Pop3Storage {
|
||||
use MailBoxStorageTrait;
|
||||
|
||||
public function __construct($accountOptions) {
|
||||
$protocol = new Pop3MailboxProtocol($accountOptions);
|
||||
public function __construct($accountSetting) {
|
||||
$protocol = new Pop3MailboxProtocol($accountSetting);
|
||||
parent::__construct($protocol);
|
||||
$this->init($accountOptions->getAccount());
|
||||
$this->init($accountSetting);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,11 +648,9 @@ namespace osTicket\Mail {
|
||||
}
|
||||
|
||||
public function sendMessage(Message $message) {
|
||||
// Make sure the body is set
|
||||
if (!isset($message->body))
|
||||
$message->setBody();
|
||||
|
||||
try {
|
||||
// Make sure the body is set
|
||||
$message->prepare();
|
||||
parent::send($message);
|
||||
} catch (\Throwable $ex) {
|
||||
$this->connected = false;
|
||||
@@ -391,25 +663,30 @@ namespace osTicket\Mail {
|
||||
// SmtpOptions
|
||||
use Laminas\Mail\Transport\SmtpOptions as SmtpSettings;
|
||||
class SmtpOptions extends SmtpSettings {
|
||||
public function __construct(AccountOptions $options) {
|
||||
parent::__construct($this->buildOptions($options));
|
||||
public function __construct(AccountSetting $setting) {
|
||||
parent::__construct($this->buildOptions($setting));
|
||||
}
|
||||
|
||||
private function buildOptions(AccountOptions $options) {
|
||||
// Build out SmtpOptions options based on SmtpAccount Settings
|
||||
$config = [];
|
||||
$connect = $options->getConnectioOptions();
|
||||
$auth = $options->getAuth();
|
||||
// Build out SmtpOptions options based on SmtpAccount Settings
|
||||
private function buildOptions(AccountSetting $setting) {
|
||||
// Dont send 'QUIT' on __destruct()
|
||||
$config = [
|
||||
'use_complete_quit' => false,
|
||||
'novalidatecert' => true
|
||||
];
|
||||
$connect = $setting->getConnectionConfig();
|
||||
$auth = $setting->getAuthCredentials();
|
||||
switch (true) {
|
||||
case $auth instanceof NoAuthCredentials:
|
||||
// No Authentication - simply return host and port
|
||||
return [
|
||||
'host' => $connect['host'],
|
||||
'port' => $connect['port']
|
||||
'host' => $connect['host'],
|
||||
'port' => $connect['port'],
|
||||
'name' => $connect['name'],
|
||||
];
|
||||
break;
|
||||
case $auth instanceof BasicAuthCredentials:
|
||||
$config = [
|
||||
$config += [
|
||||
'username' => $auth->getUsername(),
|
||||
'password' => $auth->getPasswd(),
|
||||
'ssl' => $connect['ssl'],
|
||||
@@ -419,7 +696,7 @@ namespace osTicket\Mail {
|
||||
$token = $auth->getAccessToken();
|
||||
if ($token->hasExpired())
|
||||
throw new Exception('Access Token is Expired');
|
||||
$config = [
|
||||
$config += [
|
||||
'xoauth2' => $token->getAuthRequest(),
|
||||
'ssl' => $connect['ssl'],
|
||||
];
|
||||
@@ -429,8 +706,10 @@ namespace osTicket\Mail {
|
||||
}
|
||||
|
||||
return [
|
||||
'host' => $connect['host'],
|
||||
'port' => $connect['port'],
|
||||
'host' => $connect['host'],
|
||||
'port' => $connect['port'],
|
||||
'name' => $connect['name'],
|
||||
'connection_time_limit' => 300, # 5 minutes limit
|
||||
'connection_class' => $auth->getConnectionClass(),
|
||||
'connection_config' => $config
|
||||
];
|
||||
@@ -445,16 +724,15 @@ namespace osTicket\Mail {
|
||||
}
|
||||
|
||||
public function sendMessage(Message $message) {
|
||||
// Make sure the body is set
|
||||
if (!isset($message->body))
|
||||
$message->setBody();
|
||||
|
||||
try {
|
||||
// Make sure the body is set
|
||||
$message->prepare();
|
||||
parent::send($message);
|
||||
return true;
|
||||
} catch (\Throwable $ex) {
|
||||
throw $ex;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -567,8 +845,18 @@ namespace osTicket\Mail {
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function getAccessToken() {
|
||||
return $this->getToken();
|
||||
public function getAuthRequest($user=null) {
|
||||
return $this->getToken()
|
||||
? $this->getToken()->getAuthRequest($user)
|
||||
: null;
|
||||
}
|
||||
|
||||
public function getAccessToken($signature=false) {
|
||||
$token = $this->getToken();
|
||||
// check signature if requested
|
||||
return (!$signature
|
||||
|| !strcmp($signature, $token->getConfigSignature()))
|
||||
? $token : null;
|
||||
}
|
||||
|
||||
public function toArray() {
|
||||
@@ -576,60 +864,88 @@ namespace osTicket\Mail {
|
||||
}
|
||||
}
|
||||
|
||||
// osTicket/Mail/AccountOptions
|
||||
class AccountOptions {
|
||||
// osTicket/Mail/AccountSetting
|
||||
class AccountSetting {
|
||||
private $account;
|
||||
private $connectOptions = [];
|
||||
private $creds;
|
||||
private $connection = [];
|
||||
private $errors = [];
|
||||
|
||||
public function __construct(\EmailAccount $account) {
|
||||
// Set the account
|
||||
$this->account = $account;
|
||||
$this->account = &$account;
|
||||
// Parse Connection Options
|
||||
// We allow scheme to hint for encryption for people using ssl or tls
|
||||
// on nonstandard ports.
|
||||
$host = $account->getHost();
|
||||
$ssl = $account->getEncryption();
|
||||
$port = (int) $account->getPort();
|
||||
$ssl = null;
|
||||
$matches = [];
|
||||
if (preg_match('~^(ssl|tls)://(.*+)$~iu', $host, $matches))
|
||||
list(, $host, $ssl) = $matches;
|
||||
// Set ssl or tls on based on standard ports
|
||||
$port = $account->getPort();
|
||||
if (!$ssl && $port) {
|
||||
if (preg_match('~^(ssl|tls|plain)://(.*+)$~iu',
|
||||
strtolower($host), $matches)) {
|
||||
list(, $ssl, $host) = $matches;
|
||||
// Clear ssl when "plain" connection is being forced without
|
||||
// using port number as the indicator!
|
||||
$ssl = strcmp($ssl, 'plain') ? $ssl : null;
|
||||
// Why would someone use a standard encryption based port
|
||||
// for unencrypted connection is beyond me - but apparently
|
||||
// it's a thing!!
|
||||
} elseif (!$ssl && $port) {
|
||||
// Set ssl or tls on based on standard ports
|
||||
if (in_array($port, [465, 993, 995]))
|
||||
$ssl = 'ssl';
|
||||
elseif (in_array($port, [587]))
|
||||
$ssl = 'tls';
|
||||
}
|
||||
|
||||
$this->connectOptions = [
|
||||
// Set the connection settings
|
||||
$this->connection = [
|
||||
'host' => $host,
|
||||
'port' => (int) $port,
|
||||
'port' => $port,
|
||||
'ssl' => $ssl,
|
||||
'name' => null
|
||||
'protocol' => strtoupper($account->getProtocol()),
|
||||
'name' => self::get_hostname(),
|
||||
];
|
||||
|
||||
// Set errors to null to clear validation
|
||||
$this->errors = null;
|
||||
}
|
||||
|
||||
public function getUser() {
|
||||
return $this->account->getEmail()->getEmail();
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
return $this->connectOptions['name'];
|
||||
return $this->connection['name'];
|
||||
}
|
||||
|
||||
public function getHost() {
|
||||
return $this->connectOptions['host'];
|
||||
return $this->connection['host'];
|
||||
}
|
||||
|
||||
public function getPort() {
|
||||
return $this->connectOptions['port'];
|
||||
return $this->connection['port'];
|
||||
}
|
||||
|
||||
public function getSsl() {
|
||||
return $this->connectOptions['ssl'];
|
||||
return $this->connection['ssl'];
|
||||
}
|
||||
|
||||
public function getProtocol() {
|
||||
return $this->connection['protocol'];
|
||||
}
|
||||
|
||||
public function setCredentials(AuthCredentials $creds) {
|
||||
$this->creds = $creds;
|
||||
}
|
||||
|
||||
public function getCredentials() {
|
||||
return $this->account->getCredentials();
|
||||
if (!isset($this->creds))
|
||||
$this->creds = $this->account->getCredentials();
|
||||
return $this->creds;
|
||||
}
|
||||
|
||||
public function getAuth() {
|
||||
public function getAuthCredentials() {
|
||||
return $this->getCredentials();
|
||||
}
|
||||
|
||||
@@ -637,8 +953,73 @@ namespace osTicket\Mail {
|
||||
return $this->account;
|
||||
}
|
||||
|
||||
public function getConnectioOptions() {
|
||||
return $this->connectOptions;
|
||||
public function getConnectionConfig() {
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
public function getHostInfo() {
|
||||
return $this->describe();
|
||||
}
|
||||
|
||||
public function asArray() {
|
||||
return $this->getConnectionConfig();
|
||||
}
|
||||
|
||||
public function describe() {
|
||||
return sprintf('%s://%s:%s/%s',
|
||||
$this->getSsl() ?: 'plain',
|
||||
$this->getHost(),
|
||||
$this->getPort(),
|
||||
$this->getProtocol());
|
||||
}
|
||||
|
||||
private function validate() {
|
||||
|
||||
if (!isset($this->errors)) {
|
||||
// Set errors to an array to to make sure don't
|
||||
// unneccesarily validate valid info again.
|
||||
$this->errors = [];
|
||||
// We're simply making sure required info are set. True
|
||||
// validation will happen at the protocol connection level
|
||||
$info = $this->getConnectionConfig();
|
||||
foreach (['host', 'port', 'protocol'] as $p ) {
|
||||
if (!isset($info[$p]) || !$info[$p])
|
||||
$this->errors[$p] = sprintf('%s %s',
|
||||
strtoupper($p), __('Required'));
|
||||
}
|
||||
}
|
||||
return !count($this->errors);
|
||||
}
|
||||
|
||||
public function isValid() {
|
||||
return $this->validate();
|
||||
}
|
||||
|
||||
public function getErrors() {
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/*
|
||||
* get_hostname
|
||||
*
|
||||
* Please note that this is different from getHost above
|
||||
*
|
||||
* Here we're getting the hostname to use on HELO/EHLO when
|
||||
* initiating an SMTP connection. It should be a valid hostname with
|
||||
* valid reverse-lookup for better deliverability.
|
||||
*
|
||||
* Perhaps this can be a setting in the future but allowing users
|
||||
* to set it to **anything** will results in more mail issues than just
|
||||
* defaulting to what the OS tells us or localhost for that matter.
|
||||
*
|
||||
* For now, we're simply asking core osTicket to give us the OS hostname
|
||||
* otherwise it will default to localhost which some mail servers frawns
|
||||
* upon since it won't have a valid reverse-lookup.
|
||||
*
|
||||
*/
|
||||
private static function get_hostname() {
|
||||
// We're simply returning what the OS is telling us!
|
||||
return php_uname('n');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -684,10 +1065,34 @@ namespace osTicket\Mail\Protocol\Smtp\Auth {
|
||||
if (!($xoauth2=$this->getAuthRequest()))
|
||||
throw new Exception('XOAUTH2 Required');
|
||||
$this->_send('AUTH XOAUTH2');
|
||||
$this->_expect(234);
|
||||
$this->_expect(334);
|
||||
$this->_send($xoauth2);
|
||||
$this->_expect(235);
|
||||
$this->auth = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace osTicket\Mail\Header {
|
||||
use Laminas\Mail\Header\AbstractAddressList;
|
||||
use Laminas\Mail\Header\HeaderInterface;
|
||||
use Laminas\Mail\Address;
|
||||
|
||||
class ReturnPath extends AbstractAddressList {
|
||||
protected $fieldName = 'Return-Path';
|
||||
protected static $type = 'return-path';
|
||||
|
||||
public function addAddress($email) {
|
||||
$this->getAddressList()->add(new Address($email)); #nolint
|
||||
}
|
||||
|
||||
public function getFieldValue($format = HeaderInterface::FORMAT_RAW) {
|
||||
// We're simply intercepting Value here to add <> to the email
|
||||
return sprintf('<%s>', parent::getFieldValue($format));
|
||||
}
|
||||
|
||||
public function getType() {
|
||||
return self::$type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,35 +18,36 @@
|
||||
namespace osTicket\Mail;
|
||||
|
||||
class Mailer {
|
||||
private $from = null;
|
||||
var $email = null;
|
||||
var $accounts = [];
|
||||
var $smtpAccounts = [];
|
||||
|
||||
var $ht = array();
|
||||
var $attachments = array();
|
||||
var $options = array();
|
||||
var $eol="\n";
|
||||
|
||||
function __construct(\Email $email=null, array $options=array()) {
|
||||
function __construct(?\Email $email=null, array $options=array()) {
|
||||
global $cfg;
|
||||
|
||||
// Get all possible outgoing emails accounts (SMTP) to try
|
||||
if (($email instanceof \Email)
|
||||
&& ($smtp=$email->getSmtpAccount(false))
|
||||
&& $smtp->isActive()) {
|
||||
$this->accounts[$smtp->getId()] = $smtp;
|
||||
$this->smtpAccounts[$smtp->getId()] = $smtp;
|
||||
}
|
||||
|
||||
if ($cfg
|
||||
&& ($smtp=$cfg->getDefaultMTA())
|
||||
&& $smtp->isActive()) {
|
||||
$this->accounts[$smtp->getId()] = $smtp;
|
||||
if ($smtp->allowSpoofing() || !$email)
|
||||
$email = $smtp->getEmail();
|
||||
}
|
||||
|
||||
if (!$email && $cfg && ($email=$cfg->getDefaultEmail())) {
|
||||
if (($smtp=$email->getSmtpAccount(false)) && $smtp->isActive())
|
||||
$this->accounts[$smtp->getId()] = $smtp;
|
||||
if ($cfg) {
|
||||
// Get Default MTA (SMTP)
|
||||
if (($smtp=$cfg->getDefaultMTA()) && $smtp->isActive()) {
|
||||
$this->smtpAccounts[$smtp->getId()] = $smtp;
|
||||
// If email is not set then use Default MTA
|
||||
if (!$email)
|
||||
$email = $smtp->getEmail();
|
||||
} elseif (!$email && $cfg && ($email=$cfg->getDefaultEmail())) {
|
||||
// as last resort we will send via Default Email
|
||||
if (($smtp=$email->getSmtpAccount(false)) && $smtp->isActive())
|
||||
$this->smtpAccounts[$smtp->getId()] = $smtp;
|
||||
}
|
||||
}
|
||||
|
||||
$this->email = $email;
|
||||
@@ -63,7 +64,7 @@ class Mailer {
|
||||
}
|
||||
|
||||
function getSmtpAccounts() {
|
||||
return $this->accounts;
|
||||
return $this->smtpAccounts;
|
||||
}
|
||||
|
||||
function getEmail() {
|
||||
@@ -71,20 +72,36 @@ class Mailer {
|
||||
}
|
||||
|
||||
/* FROM Address */
|
||||
function setFromAddress($from) {
|
||||
$this->ht['from'] = $from;
|
||||
function setFromAddress($from=null, $name=null) {
|
||||
if ($from instanceof \EmailAddress)
|
||||
$this->from = $from;
|
||||
elseif (\Validator::is_email($from)) {
|
||||
$this->from = new \EmailAddress(
|
||||
str_contains($from, '<') ? $from : sprintf('"%s" <%s>', $name ?: '', $from));
|
||||
} elseif (is_string($from))
|
||||
$this->from = new \EmailAddress($from);
|
||||
elseif (($email=$this->getEmail())) {
|
||||
// we're assuming from was null or unexpected monster
|
||||
$address = sprintf('"%s" <%s>',
|
||||
$name ?: $email->getName(),
|
||||
$email->getEmail());
|
||||
$this->from = new \EmailAddress($address);
|
||||
}
|
||||
}
|
||||
|
||||
function getFromAddress($options=array()) {
|
||||
if (!isset($this->from))
|
||||
$this->setFromAddress(null, $options['from_name'] ?: null);
|
||||
|
||||
if (!$this->ht['from'] && ($email=$this->getEmail())) {
|
||||
if (($name = $options['from_name'] ?: $email->getName()))
|
||||
$this->ht['from'] =sprintf('"%s" <%s>', $name, $email->getEmail());
|
||||
else
|
||||
$this->ht['from'] =sprintf('<%s>', $email->getEmail());
|
||||
}
|
||||
return $this->from;
|
||||
}
|
||||
|
||||
return $this->ht['from'];
|
||||
function getFromName() {
|
||||
return $this->getFromAddress()->getName();
|
||||
}
|
||||
|
||||
function getFromEmail() {
|
||||
return $this->getFromAddress()->getEmail();
|
||||
}
|
||||
|
||||
/* attachments */
|
||||
@@ -92,33 +109,42 @@ class Mailer {
|
||||
return $this->attachments;
|
||||
}
|
||||
|
||||
function addAttachment(Attachment $attachment) {
|
||||
// XXX: This looks too assuming; however, the attachment processor
|
||||
// in the ::send() method seems hard coded to expect this format
|
||||
$this->attachments[] = $attachment;
|
||||
function addAttachment(\Attachment $attachment) {
|
||||
$this->attachments[$attachment->getFile()->getUId()] = $attachment;
|
||||
}
|
||||
|
||||
function addAttachmentFile(AttachmentFile $file) {
|
||||
// XXX: This looks too assuming; however, the attachment processor
|
||||
// in the ::send() method seems hard coded to expect this format
|
||||
$this->attachments[] = $file;
|
||||
function addAttachmentFile(\AttachmentFile $file) {
|
||||
$this->attachments[$file->getUId()] = $file;
|
||||
}
|
||||
|
||||
function addFileObject(FileObject $file) {
|
||||
$this->attachments[] = $file;
|
||||
function addFileObject(\FileObject $file) {
|
||||
$this->attachments[$file->getUId()] = $file;
|
||||
}
|
||||
|
||||
function addAttachments($attachments) {
|
||||
foreach ($attachments as $a) {
|
||||
if ($a instanceof Attachment)
|
||||
if ($a instanceof \Attachment)
|
||||
$this->addAttachment($a);
|
||||
elseif ($a instanceof AttachmentFile)
|
||||
elseif ($a instanceof \AttachmentFile)
|
||||
$this->addAttachmentFile($a);
|
||||
elseif ($a instanceof FileObject)
|
||||
elseif ($a instanceof \FileObject)
|
||||
$this->addFileObject($a);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* lookup Attached File by Key
|
||||
*/
|
||||
function getFile(String $key) {
|
||||
foreach ($this->getAttachments() as $uid => $F) {
|
||||
if ($F instanceof \Attachment)
|
||||
$F = $F->getFile();
|
||||
if (strcasecmp($F->getKey(), $key) === 0)
|
||||
return $F;
|
||||
}
|
||||
return \AttachmentFile::lookup($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* getMessageId
|
||||
*
|
||||
@@ -320,18 +346,19 @@ class Mailer {
|
||||
|
||||
$messageId = $this->getMessageId($recipients, $options);
|
||||
$subject = preg_replace("/(\r\n|\r|\n)/s",'', trim($subject));
|
||||
$from = $this->getFromAddress($options);
|
||||
|
||||
// Create new ostTicket/Mail/Message object
|
||||
$message = new Message();
|
||||
// Set basic headers
|
||||
$message->addFrom($this->getFromAddress($options));
|
||||
// Set our custom Message-Id
|
||||
$message->setMessageId($messageId);
|
||||
// Set From Address
|
||||
$message->setFrom($from->getEmail(), $from->getName());
|
||||
// Set Subject
|
||||
$message->setSubject($subject);
|
||||
|
||||
$headers = array (
|
||||
'Date'=> date('D, d M Y H:i:s O'),
|
||||
'Message-ID' => "<{$messageId}>",
|
||||
'X-Mailer' =>'osTicket Mailer',
|
||||
);
|
||||
// Collect Generic Headers
|
||||
$headers = ['X-Mailer' =>'osTicket Mailer'];
|
||||
|
||||
// Add in the options passed to the constructor
|
||||
$options = ($options ?: array()) + $this->options;
|
||||
@@ -381,10 +408,9 @@ class Mailer {
|
||||
|
||||
// Return-Path
|
||||
if (isset($options['nobounce']) && $options['nobounce'])
|
||||
$headers['Return-Path'] = '<>';
|
||||
$message->setReturnPath('<>');
|
||||
elseif ($this->getEmail() instanceof \Email)
|
||||
$headers['Return-Path'] = sprintf('<%s>',
|
||||
$this->getEmail()->getEmail());
|
||||
$message->setReturnPath($this->getEmail()->getEmail());
|
||||
|
||||
// Bulk.
|
||||
if (isset($options['bulk']) && $options['bulk'])
|
||||
@@ -406,16 +432,11 @@ class Mailer {
|
||||
'Auto-Submitted' => 'auto-generated');
|
||||
// In-Reply-To
|
||||
if (isset($options['inreplyto']) && $options['inreplyto'])
|
||||
$headers += array('In-Reply-To' => $options['inreplyto']);
|
||||
$message->addInReplyTo($options['inreplyto']);
|
||||
|
||||
// References
|
||||
if (isset($options['references']) && $options['references']) {
|
||||
if (is_array($options['references']))
|
||||
$headers += array('References' =>
|
||||
implode(' ', $options['references']));
|
||||
else
|
||||
$headers += array('References' => $options['references']);
|
||||
}
|
||||
if (isset($options['references']) && $options['references'])
|
||||
$message->addReferences($options['references']);
|
||||
|
||||
// Add Headers
|
||||
$message->addHeaders($headers);
|
||||
@@ -426,40 +447,49 @@ class Mailer {
|
||||
foreach ($recipients as $recipient) {
|
||||
if ($recipient instanceof \ClientSession)
|
||||
$recipient = $recipient->getSessionUser();
|
||||
switch (true) {
|
||||
case $recipient instanceof \EmailRecipient:
|
||||
$addr = sprintf('"%s" <%s>',
|
||||
$recipient->getName(),
|
||||
$recipient->getEmail());
|
||||
switch ($recipient->getType()) {
|
||||
case 'to':
|
||||
$message->addTo($addr);
|
||||
break;
|
||||
case 'cc':
|
||||
$message->addCc($addr);
|
||||
break;
|
||||
case 'bcc':
|
||||
$message->addBcc($addr);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case $recipient instanceof \TicketOwner:
|
||||
case $recipient instanceof \Staff:
|
||||
$message->addTo(sprintf('"%s" <%s>',
|
||||
$recipient->getName(),
|
||||
$recipient->getEmail()));
|
||||
break;
|
||||
case $recipient instanceof \Collaborator:
|
||||
$message->addCc(sprintf('"%s" <%s>',
|
||||
$recipient->getName(),
|
||||
$recipient->getEmail()));
|
||||
break;
|
||||
case $recipient instanceof \EmailAddress:
|
||||
$message->addTo($recipient->getAddress());
|
||||
break;
|
||||
default:
|
||||
// Assuming email address.
|
||||
$message->addTo($recipient);
|
||||
try {
|
||||
switch (true) {
|
||||
case $recipient instanceof \EmailRecipient:
|
||||
$email = (string) $recipient->getEmail()->getEmail();
|
||||
$name = (string) $recipient->getName();
|
||||
switch ($recipient->getType()) {
|
||||
case 'to':
|
||||
$message->addTo($email, $name);
|
||||
break;
|
||||
case 'cc':
|
||||
$message->addCc($email, $name);
|
||||
break;
|
||||
case 'bcc':
|
||||
$message->addBcc($email, $name);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case $recipient instanceof \TicketOwner:
|
||||
case $recipient instanceof \Staff:
|
||||
$message->addTo((string) $recipient->getEmail(),
|
||||
(string) $recipient->getName());
|
||||
break;
|
||||
case $recipient instanceof \Collaborator:
|
||||
$message->addCc((string) $recipient->getEmail(),
|
||||
(string) $recipient->getName());
|
||||
break;
|
||||
case $recipient instanceof \EmailAddress:
|
||||
$message->addTo((string) $recipient->getEmail(),
|
||||
(string) $recipient->getName());
|
||||
break;
|
||||
default:
|
||||
// Assuming email address.
|
||||
if (is_string($recipient))
|
||||
$message->addTo($recipient);
|
||||
}
|
||||
} catch(\Exception $ex) {
|
||||
$this->logWarning(sprintf("%s1\$s: %2\$s\n\n%3\$s\n",
|
||||
_S("Unable to add email recipient"),
|
||||
($recipient instanceof EmailContact)
|
||||
? $recipient->getEmailAddress()
|
||||
: (string) $recipient,
|
||||
$ex->getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,7 +528,7 @@ class Mailer {
|
||||
if ($isHtml && $cfg && $cfg->isRichTextEnabled()) {
|
||||
// Pick a domain compatible with pear Mail_Mime
|
||||
$matches = array();
|
||||
if (preg_match('#(@[0-9a-zA-Z\-\.]+)#', $this->getFromAddress(), $matches)) {
|
||||
if (preg_match('#(@[0-9a-zA-Z\-\.]+)#', (string) $this->getFromAddress(), $matches)) {
|
||||
$domain = $matches[1];
|
||||
} else {
|
||||
$domain = '@localhost';
|
||||
@@ -508,24 +538,20 @@ class Mailer {
|
||||
$self = $this;
|
||||
$body = preg_replace_callback('/cid:([\w.-]{32})/',
|
||||
function($match) use ($domain, $message, $self) {
|
||||
$file = false;
|
||||
foreach ($self->attachments as $id=>$F) {
|
||||
if ($F instanceof \Attachment)
|
||||
$F = $F->getFile();
|
||||
if (strcasecmp($F->getKey(), $match[1]) === 0) {
|
||||
$file = $F;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$file)
|
||||
// Not attached yet attempt to attach it inline
|
||||
$file = \AttachmentFile::lookup($match[1]);
|
||||
if (!$file)
|
||||
if (!($file=$self->getFile($match[1])))
|
||||
return $match[0];
|
||||
$message->addInlineImage($match[1].$domain, $file);
|
||||
// Don't re-attach the image below
|
||||
unset($self->attachments[$file->getId()]);
|
||||
return $match[0].$domain;
|
||||
|
||||
try {
|
||||
$message->addInlineImage($match[1].$domain, $file);
|
||||
// Don't re-attach the image below
|
||||
unset($self->attachments[$file->getUId()]);
|
||||
return $match[0].$domain;
|
||||
} catch(\Exception $ex) {
|
||||
$self->logWarning(sprintf("%1\$s:%2\$s\n\n%3\$s\n",
|
||||
_S("Unable to retrieve email inline image"),
|
||||
$match[1].$domain,
|
||||
$ex->getMessage()));
|
||||
}
|
||||
}, $body);
|
||||
// Add an HTML body
|
||||
$message->setHtmlBody($body);
|
||||
@@ -533,75 +559,117 @@ class Mailer {
|
||||
//XXX: Attachments
|
||||
if(($attachments=$this->getAttachments())) {
|
||||
foreach($attachments as $file) {
|
||||
// Read the filename from the Attachment if possible
|
||||
if ($file instanceof \Attachment) {
|
||||
$filename = $file->getFilename();
|
||||
$file = $file->getFile();
|
||||
} elseif ($file instanceof \AttachmentFile) {
|
||||
} else {
|
||||
$filename = $file->getName();
|
||||
} elseif ($file instanceof \FileObject) {
|
||||
$filename = $file->getFilename();
|
||||
} else
|
||||
continue;
|
||||
$message->addAttachment($file, $filename);
|
||||
}
|
||||
|
||||
try {
|
||||
$message->addAttachment($file, $filename);
|
||||
} catch(\Exception $ex) {
|
||||
$this->logWarning(sprintf("%1\$s:%2\$s\n\n%3\$s\n",
|
||||
_S("Unable to retrieve email attachment"),
|
||||
$filename,
|
||||
$ex->getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set Body & Content Type
|
||||
$message->setBody($message->getMimeMessage());
|
||||
|
||||
// Try possible SMTP Accounts - connections are cached per request
|
||||
// at the account level.
|
||||
foreach ($this->getSmtpAccounts() ?: [] as $account) {
|
||||
foreach ($this->getSmtpAccounts() ?: [] as $smtpAccount) {
|
||||
try {
|
||||
if (($smtp=$account->getSmtpConnection())
|
||||
// Check if SPOOFING is allowed.
|
||||
if (!$smtpAccount->allowSpoofing()
|
||||
&& strcasecmp($smtpAccount->getEmail()->getEmail(),
|
||||
$this->getFromEmail())) {
|
||||
// If the Account DOES NOT allow spoofing then set the
|
||||
// Sender as the smtp account sending out the email
|
||||
// TODO: Allow Aliases to Spoof parent account by
|
||||
// default.
|
||||
$message->setSender(
|
||||
// get Account Email
|
||||
(string) $smtpAccount->getEmail()->getEmail(),
|
||||
// Try to keep the name if available
|
||||
$this->getFromName() ?: $smtpAccount->getName() ?: $this->getEmail());
|
||||
}
|
||||
// Attempt to send the Message.
|
||||
if (($smtp=$smtpAccount->getSmtpConnection())
|
||||
&& $smtp->sendMessage($message))
|
||||
return $messageId;
|
||||
return $message->getId();
|
||||
} catch (\Exception $ex) {
|
||||
$alert = _S("Unable to email via SMTP")
|
||||
.sprintf(": %1\$s (%2\$s:%3\$d)\n\n%4\$s\n",
|
||||
$account->getEmail()->getEmail(),
|
||||
$account->getHost(),
|
||||
$account->getPort(),
|
||||
$ex->getMessage());
|
||||
$this->logError($alert);
|
||||
continue;
|
||||
// Log the SMTP error
|
||||
$this->logError(sprintf("%1\$s: %2\$s (%3\$s)\n\n%4\$s\n",
|
||||
_S("Unable to email via SMTP"),
|
||||
$smtpAccount->getEmail()->getEmail(),
|
||||
$smtpAccount->getHostInfo(),
|
||||
$ex->getMessage()
|
||||
));
|
||||
}
|
||||
// Attempt Failed: Reset FROM to original email and clear Sender
|
||||
$message->setOriginator($this->getFromEmail(), $this->getFromName());
|
||||
}
|
||||
|
||||
//No SMTP or it failed....use php's native mail function.
|
||||
$args = [];
|
||||
if (isset($options['from_address']))
|
||||
$args[] = '-f '.$options['from_address'];
|
||||
elseif ($this->getEmail())
|
||||
$args = ['-f '.$this->getEmail()->getEmail()];
|
||||
// No SMTP or it FAILED....use Sendmail transport (PHP mail())
|
||||
// Set Sender / Originator
|
||||
if (isset($options['from_address'])) {
|
||||
// This is often set via Mailer::sendmail()
|
||||
$message->setSender($options['from_address']);
|
||||
} elseif (($from=$this->getFromAddress())) {
|
||||
// This should be already set but we're making doubly sure
|
||||
$message->setOriginator($from->getEmail(), $from->getName());
|
||||
}
|
||||
|
||||
try {
|
||||
// ostTicket/Mail/Sendmail transport
|
||||
// ostTicket/Mail/Sendmail transport (mail())
|
||||
// Set extra params as needed
|
||||
// NOTE: Laminas Mail doesn't respect -f param set Sender (above)
|
||||
$args = [];
|
||||
$sendmail = new Sendmail($args);
|
||||
if ($sendmail->sendMessage($message))
|
||||
return $messageId;
|
||||
return $message->getId();
|
||||
} catch (\Exception $ex) {
|
||||
$alert = _S("Unable to email via php mail function")
|
||||
.sprintf("\n\n%1\$s\n", $ex->getMessage());
|
||||
$this->logError($alert);
|
||||
$this->logError(sprintf("%1\$s\n\n%2\$s\n",
|
||||
_S("Unable to email via Sendmail"),
|
||||
$ex->getMessage()
|
||||
));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function log($msg, $type='warning') {
|
||||
global $ost;
|
||||
|
||||
if (!$ost || !$msg)
|
||||
return;
|
||||
|
||||
// No email alerts on email errors
|
||||
switch ($type) {
|
||||
case 'error':
|
||||
return $ost->logError(_S('Mailer Error'), $msg, false);
|
||||
break;
|
||||
case 'warning':
|
||||
default:
|
||||
return $ost->logWarning(_S('Mailer Warning'), $msg, false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function logError($error) {
|
||||
global $ost;
|
||||
//NOTE: Admin alert override - don't email when having email trouble!
|
||||
$ost->logError(_S('Mailer Error'), $error, false);
|
||||
return $this->log($error, 'error');
|
||||
}
|
||||
|
||||
/******* Static functions ************/
|
||||
function logWarning($warning) {
|
||||
return $this->log($warning, 'warning');
|
||||
}
|
||||
|
||||
//Emails using native php mail function - if DB connection doesn't exist.
|
||||
//Don't use this function if you can help it.
|
||||
static function sendmail($to, $subject, $message, $from, $options=null) {
|
||||
static function sendmail($to, $subject, $message, $from=null, $options=null) {
|
||||
$mailer = new Mailer(null, array('notice'=>true, 'nobounce'=>true));
|
||||
$mailer->setFromAddress($from);
|
||||
$mailer->setFromAddress($from, $options['from_name'] ?: null);
|
||||
return $mailer->send($to, $subject, $message, $options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,14 @@
|
||||
namespace osTicket\Mail;
|
||||
|
||||
class Fetcher {
|
||||
var $account;
|
||||
var $mbox;
|
||||
private $account;
|
||||
private $mbox;
|
||||
private $api;
|
||||
|
||||
function __construct(\MailboxAccount $account, $charset='UTF-8') {
|
||||
$this->account = $account;
|
||||
$this->mbox = $account->getMailBox();
|
||||
if ($folder = $this->getFolder())
|
||||
if (($folder = $this->getFetchFolder()))
|
||||
$this->mbox->selectFolder($folder);
|
||||
}
|
||||
|
||||
@@ -31,6 +32,10 @@ class Fetcher {
|
||||
return $this->account->getEmailId();
|
||||
}
|
||||
|
||||
function getEmail() {
|
||||
return $this->account->getEmail();
|
||||
}
|
||||
|
||||
function getEmailAddress() {
|
||||
return $this->account->getEmail()->getAddress();
|
||||
}
|
||||
@@ -39,8 +44,8 @@ class Fetcher {
|
||||
return $this->account->getMaxFetch();
|
||||
}
|
||||
|
||||
function getFolder() {
|
||||
return $this->account->getFolder();
|
||||
function getFetchFolder() {
|
||||
return $this->account->getFetchFolder();
|
||||
}
|
||||
|
||||
function getArchiveFolder() {
|
||||
@@ -51,67 +56,148 @@ class Fetcher {
|
||||
return $this->account->canDeleteEmails();
|
||||
}
|
||||
|
||||
|
||||
function getTicketsApi() {
|
||||
// We're forcing CLI interface - this is absolutely necessary since
|
||||
// Email Fetching is considered a CLI operation regardless of how
|
||||
// it's triggered (cron job / task or autocron)
|
||||
|
||||
// Please note that PHP_SAPI cannot be trusted for installations
|
||||
// using php-fpm or php-cgi binaries for php CLI executable.
|
||||
|
||||
if (!isset($this->api))
|
||||
$this->api = new \TicketApiController('cli');
|
||||
|
||||
return $this->api;
|
||||
}
|
||||
|
||||
function noop() {
|
||||
return ($this->mbox && $this->mbox->noop());
|
||||
}
|
||||
|
||||
function processMessage(int $i, array $defaults = []) {
|
||||
try {
|
||||
// Please note that the returned object could be anything from
|
||||
// ticket, task to thread entry or a boolean.
|
||||
// Don't let TicketApi call fool you!
|
||||
return $this->getTicketsApi()->processEmail(
|
||||
$this->mbox->getRawEmail($i), $defaults);
|
||||
} catch (\TicketDenied $ex) {
|
||||
// If a ticket is denied we're going to report it as processed
|
||||
// so it can be moved out of the Fetch Folder or Deleted based
|
||||
// on the MailBox settings.
|
||||
return true;
|
||||
} catch (\EmailParseError $ex) {
|
||||
// Upstream we try to create a ticket on email parse error - if
|
||||
// it fails then that means we have invalid headers.
|
||||
// For Debug purposes log the parse error + headers as a warning
|
||||
$this->logWarning(sprintf("%s\n\n%s",
|
||||
$ex->getMessage(),
|
||||
$this->mbox->getRawHeader($i)));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function processEmails() {
|
||||
// We need a connection
|
||||
if(!$this->mbox)
|
||||
if (!$this->mbox)
|
||||
return false;
|
||||
|
||||
// Get basic fetch settings
|
||||
$archiveFolder = $this->getArchiveFolder();
|
||||
$delete = $this->canDeleteEmails();
|
||||
$max = $this->getMaxFetch();
|
||||
// Get full message count
|
||||
$messageCount = $this->mbox->countMessages();
|
||||
$deleteFetched = $this->canDeleteEmails();
|
||||
$max = $this->getMaxFetch() ?: 30; // default to 30 if not set
|
||||
|
||||
// Get message count in the Fetch Folder
|
||||
if (!($messageCount = $this->mbox->countMessages()))
|
||||
return 0;
|
||||
|
||||
// If the number of emails in the folder are more than Max Fetch
|
||||
// then process the latest $max emails - this is necessary when
|
||||
// fetched emails are not getting archived or deleted, which might
|
||||
// lead to fetcher being stuck 4ever processing old emails already
|
||||
// fetched
|
||||
if ($messageCount > $max) {
|
||||
// Latest $max messages
|
||||
$messages = range($messageCount-$max, $messageCount);
|
||||
} else {
|
||||
// Create a range of message sequence numbers (msgno) to fetch
|
||||
// starting from the oldest taking max fetch into account
|
||||
$messages = range(1, min($max, $messageCount));
|
||||
}
|
||||
|
||||
$defaults = [
|
||||
'emailId' => $this->getEmailId()
|
||||
];
|
||||
$msgs = $errors = 0;
|
||||
for($i = $messageCount; $i > 0; $i--) { // Process messages in reverse.
|
||||
// Fetch headers
|
||||
$header = $this->mbox->getRawHeader($i);
|
||||
// Fetch content
|
||||
$content = $this->mbox->getRawContent($i);
|
||||
// Create raw stream
|
||||
$stream = $header . $content;
|
||||
// Create new controller and parser
|
||||
$api = new \TicketApiController();
|
||||
$parser = new \EmailDataParser();
|
||||
// Parse the stream
|
||||
$data = $parser->parse($stream);
|
||||
$data['source'] = 'Email';
|
||||
// Okay, let's create the ticket now
|
||||
if ($api->processEmail($data)) {
|
||||
// Mark the message as "Seen" (IMAP only)
|
||||
$this->mbox->markAsSeen($i);
|
||||
// Attempt to move the message else attempt to delete
|
||||
if((!$archiveFolder || !$this->mbox->moveMessage($i, $archiveFolder)) && $delete)
|
||||
$this->mbox->removeMessage($i);
|
||||
|
||||
$msgs++;
|
||||
$errors = 0; // We are only interested in consecutive errors.
|
||||
} else {
|
||||
$errors++;
|
||||
// TODO: Use message UIDs instead of ids
|
||||
foreach ($messages as $i) {
|
||||
try {
|
||||
// Okay, let's try to create a ticket
|
||||
if (($result=$this->processMessage($i, $defaults))) {
|
||||
// Mark the message as "Seen" (IMAP only)
|
||||
$this->mbox->markAsSeen($i);
|
||||
// Attempt to move the message if archive folder is set or
|
||||
if ($archiveFolder)
|
||||
$this->mbox->moveMessage($i, $archiveFolder);
|
||||
elseif ($deleteFetched) // else delete if deletion is desired
|
||||
$this->mbox->removeMessage($i);
|
||||
$msgs++;
|
||||
$errors = 0; // We are only interested in consecutive errors.
|
||||
} else {
|
||||
$errors++;
|
||||
}
|
||||
} catch (\Throwable $t) {
|
||||
// If we have result then exception happened after email
|
||||
// processing and shouldn't count as an error
|
||||
if (!$result)
|
||||
$errors++;
|
||||
// log the exception as a debug message
|
||||
$this->logDebug($t->getMessage());
|
||||
}
|
||||
|
||||
if($max && ($msgs>=$max || $errors>($max*0.8)))
|
||||
break;
|
||||
}
|
||||
$this->mbox->expunge();
|
||||
|
||||
// Warn on excessive errors
|
||||
// Expunge the mailbox
|
||||
$this->mbox->expunge();
|
||||
|
||||
// Warn on excessive errors - when errors are more than email
|
||||
// processed successfully.
|
||||
if ($errors > $msgs) {
|
||||
$warn = sprintf(_S('Excessive errors processing emails for %1$s/%2$s. Please manually check the inbox.'),
|
||||
$this->getHost(), $this->getUsername());
|
||||
$this->log($warn);
|
||||
$warn = sprintf("%s\n\n%s [%d/%d - %d/%d]",
|
||||
// Mailbox Info
|
||||
sprintf(_S('Excessive errors processing emails for %1$s (%2$s).'),
|
||||
$this->mbox->getHostInfo(), $this->getEmail()),
|
||||
// Fetch Folder
|
||||
sprintf('%s (%s)',
|
||||
_S('Please manually check the Fetch Folder'),
|
||||
$this->getFetchFolder()),
|
||||
// Counts - sort of cryptic but useful once we document
|
||||
// what it means
|
||||
$messageCount, $max, $msgs, $errors);
|
||||
$this->logWarning($warn);
|
||||
}
|
||||
|
||||
return $msgs;
|
||||
}
|
||||
|
||||
function log($error) {
|
||||
private function logDebug($msg) {
|
||||
$this->log($msg, LOG_DEBUG);
|
||||
}
|
||||
|
||||
private function logWarning($msg) {
|
||||
$this->log($msg, LOG_WARN);
|
||||
}
|
||||
|
||||
private function log($msg, $level = LOG_WARN) {
|
||||
global $ost;
|
||||
$ost->logWarning(_S('Mail Fetcher'), $error);
|
||||
$subj = _S('Mail Fetcher');
|
||||
switch ($level) {
|
||||
case LOG_WARN:
|
||||
$ost->logWarning($subj, $msg);
|
||||
break;
|
||||
case LOG_DEBUG:
|
||||
default:
|
||||
$ost->logDebug($subj, $msg);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -142,8 +228,10 @@ class Fetcher {
|
||||
'last_activity__isnull' => true,
|
||||
new \Q(['last_activity__lte' => $now->minus($interval)])
|
||||
]);
|
||||
|
||||
$mailboxes = \MailBoxAccount::objects()
|
||||
->filter(['active' => 1, $errors_Q, $fetch_Q]);
|
||||
->filter(['active' => 1, $errors_Q, $fetch_Q])
|
||||
->order_by('last_activity');
|
||||
|
||||
//Get max execution time so we can figure out how long we can fetch
|
||||
// take fetching emails.
|
||||
@@ -153,20 +241,26 @@ class Fetcher {
|
||||
//Start time
|
||||
$start_time = \Misc::micro_time();
|
||||
foreach ($mailboxes as $mailbox) {
|
||||
//Break if we're 80% into max execution time
|
||||
// Check if the mailbox is active 4realz by getting credentials
|
||||
if (!$mailbox->isActive())
|
||||
continue;
|
||||
|
||||
// Break if we're 80% into max execution time
|
||||
if ((\Misc::micro_time()-$start_time) > ($max_time*0.80))
|
||||
break;
|
||||
|
||||
// Try fetching emails
|
||||
try {
|
||||
$mailbox->fetchEmails();
|
||||
} catch (Exception $ex) {
|
||||
} catch (\Throwable $t) {
|
||||
if ($mailbox->getNumErrors() >= $MAXERRORS && $ost) {
|
||||
//We've reached the MAX consecutive errors...will attempt logins at delayed intervals
|
||||
// XXX: Translate me
|
||||
$msg="\nosTicket is having trouble fetching emails from the following mail account: \n".
|
||||
$msg = sprintf("\n %s:\n",
|
||||
_S('osTicket is having trouble fetching emails from the following mail account')).
|
||||
"\n"._S('Email').": ".$mailbox->getEmail()->getAddress().
|
||||
"\n"._S('Host').": ".sprintf('%s:%d',
|
||||
$mailbox->getHost(), $mailbox->getPort()).
|
||||
"\n"._S('Error').": ".$ex->getMessage().
|
||||
"\n"._S('Host Info').": ".$mailbox->getHostInfo().
|
||||
"\n"._S('Error').": ".$t->getMessage().
|
||||
"\n\n ".sprintf(_S('%1$d consecutive errors. Maximum of %2$d allowed'),
|
||||
$mailbox->getNumErrors(), $MAXERRORS).
|
||||
"\n\n ".sprintf(_S('This could be connection issues related to the mail server. Next delayed login attempt in aprox. %d minutes'), $TIMEOUT);
|
||||
|
||||
@@ -27,6 +27,7 @@ class Mail_Parse {
|
||||
var $decode_bodies;
|
||||
|
||||
var $struct;
|
||||
var $headers;
|
||||
|
||||
var $charset ='UTF-8'; //Default charset.
|
||||
|
||||
@@ -35,18 +36,15 @@ class Mail_Parse {
|
||||
function __construct(&$mimeMessage, $charset=null){
|
||||
|
||||
$this->mime_message = &$mimeMessage;
|
||||
|
||||
if($charset)
|
||||
$this->charset = $charset;
|
||||
|
||||
$this->include_bodies = true;
|
||||
$this->decode_headers = false;
|
||||
$this->decode_bodies = true;
|
||||
|
||||
//Desired charset
|
||||
if($charset)
|
||||
if ($charset)
|
||||
$this->charset = $charset;
|
||||
|
||||
$this->splitBodyHeader();
|
||||
$this->notes = array();
|
||||
}
|
||||
|
||||
@@ -61,14 +59,13 @@ class Mail_Parse {
|
||||
$info = array('raw' => &$this->mime_message);
|
||||
Signal::send('mail.received', $this, $info);
|
||||
|
||||
$this->splitBodyHeader();
|
||||
|
||||
$decoder = new Mail_mimeDecode($this->mime_message);
|
||||
$this->struct = $decoder->decode($params);
|
||||
|
||||
if (PEAR::isError($this->struct))
|
||||
return false;
|
||||
|
||||
$this->headers = &$this->struct->headers;
|
||||
$info = array(
|
||||
'raw_header' => &$this->header,
|
||||
'headers' => &$this->struct->headers,
|
||||
@@ -132,14 +129,35 @@ class Mail_Parse {
|
||||
return (count($this->struct->headers) > 1);
|
||||
}
|
||||
|
||||
function getMimeMessage() {
|
||||
return $this->mime_message;
|
||||
}
|
||||
|
||||
function getMimeFile() {
|
||||
$file = [
|
||||
// Use subject if available otherwise use mid.
|
||||
'name' => sprintf('%s.eml',
|
||||
$this->getSubject() ?: $this->getMessageId()),
|
||||
'type' => 'message/rfc822',
|
||||
'data' => $this->getMimeMessage(),
|
||||
];
|
||||
// See if we have transfer encoding
|
||||
if (($encoding=$this->getHeaderEntry('content-transfer-encoding')))
|
||||
$file['encoding'] = $encoding;
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
function splitBodyHeader() {
|
||||
$match = array();
|
||||
if (preg_match("/^(.*?)\r?\n\r?\n./s",
|
||||
$this->mime_message,
|
||||
$match)) {
|
||||
if (!$this->header
|
||||
&& preg_match("/^(.*?)\r?\n\r?\n./s",
|
||||
$this->mime_message,
|
||||
$match)) {
|
||||
$this->header=$match[1];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the header section of an email message with the form of
|
||||
* Header: Value
|
||||
@@ -159,7 +177,7 @@ class Mail_Parse {
|
||||
if (in_array($headers[$i][0], array(" ", "\t"))) {
|
||||
# Continuation from previous header (runon to next line)
|
||||
$j=$i-1; while (!isset($headers[$j]) && $j>0) $j--;
|
||||
$headers[$j] .= " ".ltrim($headers[$i]);
|
||||
$headers[strtolower($j)] .= " ".ltrim($headers[$i]);
|
||||
unset($headers[$i]);
|
||||
} elseif (strlen($headers[$i]) == 0) {
|
||||
unset($headers[$i]);
|
||||
@@ -169,6 +187,7 @@ class Mail_Parse {
|
||||
foreach ($headers as $hdr) {
|
||||
list($name, $val) = explode(": ", $hdr, 2);
|
||||
# Create list of values if header is specified more than once
|
||||
$name = strtolower($name);
|
||||
if (isset($array[$name]) && $as_array) {
|
||||
if (is_array($array[$name])) $array[$name][] = $val;
|
||||
else $array[$name] = array($array[$name], $val);
|
||||
@@ -183,10 +202,15 @@ class Mail_Parse {
|
||||
static function findHeaderEntry($headers, $name, $allEntries=false) {
|
||||
if (!is_array($headers))
|
||||
$headers = self::splitHeaders($headers, $allEntries);
|
||||
foreach ($headers as $key=>$val)
|
||||
if (strcasecmp($key, $name) === 0)
|
||||
return $val;
|
||||
return false;
|
||||
|
||||
$name = strtolower($name);
|
||||
return $headers[$name] ?? false;
|
||||
}
|
||||
|
||||
function getHeaderEntry($name) {
|
||||
return $this->hasHeaders()
|
||||
? self::findHeaderEntry($this->getHeaders(), $name)
|
||||
: false;
|
||||
}
|
||||
|
||||
function getStruct(){
|
||||
@@ -194,13 +218,26 @@ class Mail_Parse {
|
||||
}
|
||||
|
||||
function getHeader() {
|
||||
if(!$this->header) $this->splitBodyHeader();
|
||||
if (!$this->header)
|
||||
$this->splitBodyHeader();
|
||||
|
||||
return $this->header;
|
||||
}
|
||||
|
||||
function getHeaders() {
|
||||
if (!isset($this->headers))
|
||||
$this->headers = self::splitHeaders($this->getHeader());
|
||||
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
function hasHeaders() {
|
||||
return ($this->getHeaders());
|
||||
}
|
||||
|
||||
function getError(){
|
||||
return PEAR::isError($this->struct)?$this->struct->getMessage():'';
|
||||
return PEAR::isError($this->struct)
|
||||
? $this->struct->getMessage() : '';
|
||||
}
|
||||
|
||||
|
||||
@@ -208,68 +245,82 @@ class Mail_Parse {
|
||||
if (!($header = $this->struct->headers['from']))
|
||||
return null;
|
||||
|
||||
if (is_string($header) && (strpos($header, '<') !== false) && (strpos($header, '>') === false))
|
||||
$header = $header.'>';
|
||||
|
||||
return Mail_Parse::parseAddressList($header, $this->charset);
|
||||
}
|
||||
|
||||
function getDeliveredToAddressList() {
|
||||
if (!($header = $this->struct->headers['delivered-to']))
|
||||
return null;
|
||||
|
||||
return Mail_Parse::parseAddressList($header, $this->charset);
|
||||
return self::parseAddressList($this->getHeaderEntry('delivered-to'),
|
||||
$this->charset);
|
||||
}
|
||||
|
||||
function getToAddressList(){
|
||||
// Delivered-to incase it was a BBC mail.
|
||||
$tolist = array();
|
||||
if ($header = $this->struct->headers['to'])
|
||||
$tolist = array_merge($tolist,
|
||||
Mail_Parse::parseAddressList($header, $this->charset));
|
||||
return $tolist ? $tolist : null;
|
||||
return self::parseAddressList($this->getHeaderEntry('to'),
|
||||
$this->charset);
|
||||
}
|
||||
|
||||
function getCcAddressList(){
|
||||
if (!($header = @$this->struct->headers['cc']))
|
||||
return null;
|
||||
|
||||
return Mail_Parse::parseAddressList($header, $this->charset);
|
||||
return self::parseAddressList($this->getHeaderEntry('cc'),
|
||||
$this->charset);
|
||||
}
|
||||
|
||||
function getBccAddressList(){
|
||||
if (!($header = @$this->struct->headers['bcc']))
|
||||
return null;
|
||||
|
||||
return Mail_Parse::parseAddressList($header, $this->charset);
|
||||
return self::parseAddressList($this->getHeaderEntry('bcc'),
|
||||
$this->charset);
|
||||
}
|
||||
|
||||
function getMessageId(){
|
||||
if (($mid = $this->struct->headers['message-id']) && is_array($mid))
|
||||
$mid = array_pop(array_filter($mid));
|
||||
if (!$mid)
|
||||
if (!($mid = $this->getHeaderEntry('message-id')))
|
||||
$mid = sprintf('<%s@local>', md5($this->getHeader()));
|
||||
|
||||
if ($mid && is_array($mid))
|
||||
$mid = array_pop(array_filter($mid));
|
||||
|
||||
return $mid;
|
||||
}
|
||||
|
||||
function getSubject(){
|
||||
return Format::mimedecode($this->struct->headers['subject'], $this->charset);
|
||||
return Format::mimedecode($this->getHeaderEntry('subject'), $this->charset);
|
||||
}
|
||||
|
||||
function getReplyTo() {
|
||||
if (!($header = @$this->struct->headers['reply-to']))
|
||||
return null;
|
||||
return self::parseAddressList($this->getHeaderEntry('reply-to'),
|
||||
$this->charset);
|
||||
}
|
||||
|
||||
return Mail_Parse::parseAddressList($header, $this->charset);
|
||||
function getInReplyTo() {
|
||||
return $this->getHeaderEntry('in-reply-to');
|
||||
}
|
||||
|
||||
function getReferences() {
|
||||
return $this->getHeaderEntry('references');
|
||||
}
|
||||
|
||||
function getRecipients() {
|
||||
$recipients = [];
|
||||
if (($to = $this->getToAddressList()))
|
||||
$recipients['to'] = $to;
|
||||
if (($cc = $this->getCcAddressList()))
|
||||
$recipients['cc'] = $cc;
|
||||
if (($dt = $this->getDeliveredToAddressList()))
|
||||
$recipients['delivered-to'] = $dt;
|
||||
|
||||
return $recipients;
|
||||
}
|
||||
|
||||
function isBounceNotice() {
|
||||
if (!($body = $this->getPart($this->struct, 'message/delivery-status')))
|
||||
if (!$this->struct
|
||||
|| !($body = $this->getPart($this->struct,
|
||||
'message/delivery-status')))
|
||||
return false;
|
||||
|
||||
$info = self::splitHeaders($body);
|
||||
if (!isset($info['Action']))
|
||||
if (!isset($info['action']))
|
||||
return false;
|
||||
|
||||
return strcasecmp($info['Action'], 'failed') === 0;
|
||||
return strcasecmp($info['action'], 'failed') === 0;
|
||||
}
|
||||
|
||||
function getDeliveryStatusMessage() {
|
||||
@@ -292,7 +343,7 @@ class Mail_Parse {
|
||||
// Handle rfc1892 style bounces
|
||||
if (strtolower($ctype) === 'text/rfc822-headers') {
|
||||
$body = $p->body . "\n\nIgnored";
|
||||
$T = new Mail_mimeDecode($body);
|
||||
$T = new Mail_mimeDecode($body, ['attachOnParseError' => false]);
|
||||
if ($struct = $T->decode())
|
||||
return $struct->headers;
|
||||
}
|
||||
@@ -303,6 +354,11 @@ class Mail_Parse {
|
||||
function getBody(){
|
||||
global $cfg;
|
||||
|
||||
// When struct is not set then it might mean decode error - return
|
||||
// empty body
|
||||
if (!$this->struct)
|
||||
return new TextThreadEntryBody('');
|
||||
|
||||
if ($cfg && $cfg->isRichTextEnabled()) {
|
||||
if ($html=$this->getPart($this->struct,'text/html'))
|
||||
$body = new HtmlThreadEntryBody($html);
|
||||
@@ -318,8 +374,7 @@ class Mail_Parse {
|
||||
|
||||
if (!isset($body))
|
||||
$body = new TextThreadEntryBody('');
|
||||
|
||||
if ($cfg && $cfg->stripQuotedReply())
|
||||
elseif ($cfg && $cfg->stripQuotedReply())
|
||||
$body->stripQuotedReply($cfg->getReplySeparator());
|
||||
|
||||
return $body;
|
||||
@@ -443,6 +498,11 @@ class Mail_Parse {
|
||||
&& $part->headers['content-disposition']
|
||||
&& preg_match('/filename="([^"]+)"/', $part->headers['content-disposition'], $matches)) {
|
||||
$filename = Format::mimedecode($matches[1], $this->charset);
|
||||
} elseif (preg_match('/filename\*\d+=([^;]+);/', $part->headers['content-disposition']) === 1) {
|
||||
$filename = '';
|
||||
foreach ($part->d_parameters as $key=>$fname)
|
||||
if (strpos($key, 'filename*') !== false)
|
||||
$filename .= Format::decodeRfc5987($fname);
|
||||
} else {
|
||||
// Not an attachment?
|
||||
return false;
|
||||
@@ -490,8 +550,12 @@ class Mail_Parse {
|
||||
return $files;
|
||||
}
|
||||
|
||||
if($part==null)
|
||||
if($part==null) {
|
||||
$part=$this->getStruct();
|
||||
if (!$part->parts)
|
||||
if($result=$this->getAttachments($part))
|
||||
$files=array_merge($files,$result);
|
||||
}
|
||||
|
||||
if($part->parts){
|
||||
foreach($part->parts as $k=>$p){
|
||||
@@ -510,7 +574,7 @@ class Mail_Parse {
|
||||
// http://msdn.microsoft.com/en-us/library/ee237166(v=exchg.80).aspx
|
||||
$urgency = 4 - $this->tnef->Importance;
|
||||
}
|
||||
elseif ($priority = Mail_Parse::parsePriority($this->getHeader())) {
|
||||
elseif ($priority = self::parsePriority($this->getHeader())) {
|
||||
$urgency = $priority + 1;
|
||||
}
|
||||
if ($urgency) {
|
||||
@@ -522,6 +586,114 @@ class Mail_Parse {
|
||||
}
|
||||
}
|
||||
|
||||
function getHeaderInfo() {
|
||||
if (!$this->hasHeaders())
|
||||
return null;
|
||||
|
||||
$info = [];
|
||||
$info['emailId'] = 0;
|
||||
$info['recipients'] = array();
|
||||
$info['subject'] = $this->getSubject();
|
||||
$info['header'] = $this->getHeader();
|
||||
$info['mid'] = $this->getMessageId();
|
||||
$info['priorityId'] = $this->getPriority();
|
||||
$info['mailflags'] = new ArrayObject();
|
||||
//FROM address: who sent the email.
|
||||
if (($fromlist = $this->getFromAddressList())) {
|
||||
$from=$fromlist[0]; //Default.
|
||||
foreach ($fromlist as $fromobj) {
|
||||
if(!Validator::is_email($fromobj->mailbox.'@'.$fromobj->host)) continue;
|
||||
$from = $fromobj;
|
||||
break;
|
||||
}
|
||||
$info['email'] = $from->mailbox.'@'.$from->host;
|
||||
$info['name'] = trim($from->personal,'"');
|
||||
if($from->comment && $from->comment[0])
|
||||
$info['name'].= ' ('.$from->comment[0].')';
|
||||
|
||||
//Use email address as name when FROM address doesn't have a name.
|
||||
if(!$info['name'] && $info['email'])
|
||||
$info['name'] = $info['email'];
|
||||
}
|
||||
|
||||
/* Scan through the list of addressees (via To, Cc, and Delivered-To headers), and identify
|
||||
* how the mail arrived at the system. One of the mails should be in the system email list.
|
||||
* The recipient list (without the Delivered-To addressees) will be made available to the
|
||||
* ticket filtering system. However, addresses in the Delivered-To header should never be
|
||||
* considered for the collaborator list.
|
||||
*/
|
||||
$recipients = $this->getRecipients();
|
||||
$info['system_emails'] = $info['thread_entry_recipients'] = [];
|
||||
foreach ($recipients as $source => $list) {
|
||||
foreach($list as $addr) {
|
||||
if (!($emailId=Email::getIdByEmail(strtolower($addr->mailbox).'@'.$addr->host))) {
|
||||
//Skip virtual Delivered-To addresses
|
||||
if ($source == 'delivered-to') continue;
|
||||
|
||||
$name = trim(@$addr->personal, '"');
|
||||
$email = strtolower($addr->mailbox).'@'.$addr->host;
|
||||
$info['recipients'][] = array(
|
||||
'source' => sprintf(_S("Email (%s)"), $source),
|
||||
'name' => $name,
|
||||
'email' => $email);
|
||||
|
||||
$info['thread_entry_recipients'][$source][] = sprintf('%s <%s>', $name, $email);
|
||||
} elseif ($emailId) {
|
||||
$info['system_emails'][] = $emailId;
|
||||
$system_email = Email::lookup($emailId);
|
||||
$info['thread_entry_recipients']['to'][] = (string) $system_email;
|
||||
if (!$info['emailId'])
|
||||
$info['emailId'] = $emailId;
|
||||
}
|
||||
}
|
||||
}
|
||||
$info['thread_entry_recipients']['to'] = isset($info['thread_entry_recipients']['to'])
|
||||
? array_unique($info['thread_entry_recipients']['to'])
|
||||
: [];
|
||||
|
||||
/*
|
||||
* In the event that the mail was delivered to the system although none of the system
|
||||
* mail addresses are in the addressee lists, be careful not to include the addressee
|
||||
* in the collaborator list. Therefore, the delivered-to addressees should be flagged so they
|
||||
* are not added to the collaborator list in the ticket creation process.
|
||||
*/
|
||||
if (isset($recipients['delivered-to'])) {
|
||||
foreach ($recipients['delivered-to'] as $addr) {
|
||||
foreach ($info['recipients'] as $i=>$r) {
|
||||
if (strcasecmp($r['email'], $addr->mailbox.'@'.$addr->host) === 0)
|
||||
$info['recipients'][$i]['source'] = 'delivered-to';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//maybe we got BCC'ed??
|
||||
if(isset($recipients['bcc'])) {
|
||||
foreach ($recipients['bcc'] as $addr) {
|
||||
if (($emailId=Email::getIdByEmail($addr->mailbox.'@'.$addr->host))) {
|
||||
$info['system_emails'][] = $emailId;
|
||||
if (!$info['emailId'])
|
||||
$info['emailId'] = $emailId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reply to ?
|
||||
if (($replyto = $this->getReplyTo())
|
||||
&& ($replyto = $replyto[0])) {
|
||||
$info['reply-to'] = $replyto->mailbox.'@'.$replyto->host;
|
||||
if ($replyto->personal)
|
||||
$info['reply-to-name'] = trim($replyto->personal, " \t\n\r\0\x0B\x22");
|
||||
}
|
||||
|
||||
$info['system_emails'] = array_unique($info['system_emails']);
|
||||
$info['to-email-id'] = $info['emailId'];
|
||||
$info['in-reply-to'] = $this->getInReplyTo();
|
||||
$info['references'] = $this->getReferences();
|
||||
$info['mailflags']['bounce'] = TicketFilter::isBounce($info['header']);
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a normalized priority urgency from the email headers received.
|
||||
*
|
||||
@@ -597,11 +769,16 @@ class Mail_Parse {
|
||||
}
|
||||
|
||||
class EmailDataParser {
|
||||
var $stream;
|
||||
var $error;
|
||||
private $options = [
|
||||
'attachOnParseError' => true
|
||||
];
|
||||
private $stream;
|
||||
private $errors = [];
|
||||
|
||||
function __construct($stream=null) {
|
||||
function __construct($stream=null, array $options = []) {
|
||||
$this->stream = $stream;
|
||||
if ($options)
|
||||
$this->options = array_merge($this->options, $options);
|
||||
}
|
||||
|
||||
function parse($stream) {
|
||||
@@ -611,154 +788,68 @@ class EmailDataParser {
|
||||
if(is_resource($stream)) {
|
||||
while(!feof($stream))
|
||||
$contents .= fread($stream, 8192);
|
||||
|
||||
} else {
|
||||
$contents = $stream;
|
||||
}
|
||||
|
||||
$parser= new Mail_Parse($contents);
|
||||
if(!$parser->decode()) //Decode...returns false on decoding errors
|
||||
return $this->err('Email parse failed ['.$parser->getError().']');
|
||||
if (!$parser->decode())
|
||||
return $this->onParseError($parser);
|
||||
|
||||
$data =array();
|
||||
$data['emailId'] = 0;
|
||||
$data['recipients'] = array();
|
||||
$data['subject'] = $parser->getSubject();
|
||||
$data['header'] = $parser->getHeader();
|
||||
$data['mid'] = $parser->getMessageId();
|
||||
$data['priorityId'] = $parser->getPriority();
|
||||
$data['mailflags'] = new ArrayObject();
|
||||
|
||||
//FROM address: who sent the email.
|
||||
if(($fromlist = $parser->getFromAddressList())) {
|
||||
$from=$fromlist[0]; //Default.
|
||||
foreach($fromlist as $fromobj) {
|
||||
if(!Validator::is_email($fromobj->mailbox.'@'.$fromobj->host)) continue;
|
||||
$from = $fromobj;
|
||||
break;
|
||||
}
|
||||
|
||||
$data['email'] = $from->mailbox.'@'.$from->host;
|
||||
|
||||
$data['name'] = trim($from->personal,'"');
|
||||
if($from->comment && $from->comment[0])
|
||||
$data['name'].= ' ('.$from->comment[0].')';
|
||||
|
||||
//Use email address as name when FROM address doesn't have a name.
|
||||
if(!$data['name'] && $data['email'])
|
||||
$data['name'] = $data['email'];
|
||||
}
|
||||
|
||||
/* Scan through the list of addressees (via To, Cc, and Delivered-To headers), and identify
|
||||
* how the mail arrived at the system. One of the mails should be in the system email list.
|
||||
* The recipient list (without the Delivered-To addressees) will be made available to the
|
||||
* ticket filtering system. However, addresses in the Delivered-To header should never be
|
||||
* considered for the collaborator list.
|
||||
*/
|
||||
|
||||
$tolist = array();
|
||||
if (($to = $parser->getToAddressList()))
|
||||
$tolist['to'] = $to;
|
||||
|
||||
if (($cc = $parser->getCcAddressList()))
|
||||
$tolist['cc'] = $cc;
|
||||
|
||||
if (($dt = $parser->getDeliveredToAddressList()))
|
||||
$tolist['delivered-to'] = $dt;
|
||||
|
||||
$data['system_emails'] = array();
|
||||
$data['thread_entry_recipients'] = array();
|
||||
foreach ($tolist as $source => $list) {
|
||||
foreach($list as $addr) {
|
||||
if (!($emailId=Email::getIdByEmail(strtolower($addr->mailbox).'@'.$addr->host))) {
|
||||
//Skip virtual Delivered-To addresses
|
||||
if ($source == 'delivered-to') continue;
|
||||
|
||||
$name = trim(@$addr->personal, '"');
|
||||
$email = strtolower($addr->mailbox).'@'.$addr->host;
|
||||
$data['recipients'][] = array(
|
||||
'source' => sprintf(_S("Email (%s)"), $source),
|
||||
'name' => $name,
|
||||
'email' => $email);
|
||||
|
||||
$data['thread_entry_recipients'][$source][] = sprintf('%s <%s>', $name, $email);
|
||||
} elseif ($emailId) {
|
||||
$data['system_emails'][] = $emailId;
|
||||
$system_email = Email::lookup($emailId);
|
||||
$data['thread_entry_recipients']['to'][] = (string) $system_email;
|
||||
if (!$data['emailId'])
|
||||
$data['emailId'] = $emailId;
|
||||
}
|
||||
}
|
||||
}
|
||||
$data['thread_entry_recipients']['to'] = array_unique($data['thread_entry_recipients']['to']);
|
||||
|
||||
/*
|
||||
* In the event that the mail was delivered to the system although none of the system
|
||||
* mail addresses are in the addressee lists, be careful not to include the addressee
|
||||
* in the collaborator list. Therefore, the delivered-to addressees should be flagged so they
|
||||
* are not added to the collaborator list in the ticket creation process.
|
||||
*/
|
||||
if ($tolist['delivered-to']) {
|
||||
foreach ($tolist['delivered-to'] as $addr) {
|
||||
foreach ($data['recipients'] as $i=>$r) {
|
||||
if (strcasecmp($r['email'], $addr->mailbox.'@'.$addr->host) === 0)
|
||||
$data['recipients'][$i]['source'] = 'delivered-to';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//maybe we got BCC'ed??
|
||||
if($bcc = $parser->getBccAddressList()) {
|
||||
foreach ($bcc as $addr) {
|
||||
if (($emailId=Email::getIdByEmail($addr->mailbox.'@'.$addr->host))) {
|
||||
$data['system_emails'][] = $emailId;
|
||||
if (!$data['emailId'])
|
||||
$data['emailId'] = $emailId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$data['system_emails'] = array_unique($data['system_emails']);
|
||||
if (!($data = $parser->getHeaderInfo()))
|
||||
return false;
|
||||
|
||||
if ($parser->isBounceNotice()) {
|
||||
// Fetch the original References and assign to 'references'
|
||||
if ($headers = $parser->getOriginalMessageHeaders()) {
|
||||
if (($headers = $parser->getOriginalMessageHeaders())) {
|
||||
$data['references'] = $headers['references'];
|
||||
$data['in-reply-to'] = $headers['message-id'] ?: @$headers['in-reply-to'] ?: null;
|
||||
$data['in-reply-to'] = $headers['message-id']
|
||||
?: ($data['in-reply-to'] ?: null);
|
||||
}
|
||||
// Fetch deliver status report
|
||||
$data['message'] = $parser->getDeliveryStatusMessage() ?: $parser->getBody();
|
||||
$data['thread-type'] = 'N';
|
||||
$data['mailflags']['bounce'] = true;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// Typical email
|
||||
$data['message'] = $parser->getBody();
|
||||
$data['in-reply-to'] = @$parser->struct->headers['in-reply-to'];
|
||||
$data['references'] = @$parser->struct->headers['references'];
|
||||
$data['mailflags']['bounce'] = TicketFilter::isBounce($data['header']);
|
||||
}
|
||||
|
||||
$data['to-email-id'] = $data['emailId'];
|
||||
|
||||
if (($replyto = $parser->getReplyTo())) {
|
||||
$replyto = $replyto[0];
|
||||
$data['reply-to'] = $replyto->mailbox.'@'.$replyto->host;
|
||||
if ($replyto->personal)
|
||||
$data['reply-to-name'] = trim($replyto->personal, " \t\n\r\0\x0B\x22");
|
||||
}
|
||||
|
||||
$data['attachments'] = $parser->getAttachments();
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
function err($error) {
|
||||
$this->error = $error;
|
||||
private function attachOnParseError() {
|
||||
return (bool) $this->options['attachOnParseError'];
|
||||
}
|
||||
|
||||
return false;
|
||||
private function onParseError($parser) {
|
||||
$error = sprintf('%s: [%s]',
|
||||
__('Email parse failed'),
|
||||
$parser->getError());
|
||||
if ($this->attachOnParseError()
|
||||
&& $parser
|
||||
// Make sure we can get header info
|
||||
&& ($data=$parser->getHeaderInfo())) {
|
||||
// Text thread entry body with the warning / error
|
||||
$data['message'] = new TextThreadEntryBody($error);
|
||||
// Add Mime message as an Attachment
|
||||
$data['attachments'][] = $parser->getMimeFile();
|
||||
return $data;
|
||||
}
|
||||
return $this->err($error);
|
||||
}
|
||||
|
||||
private function err($error) {
|
||||
// add error and return false
|
||||
return ($this->addError($error) && false);
|
||||
}
|
||||
|
||||
function getErrors() {
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
function addError($error) {
|
||||
$this->errors [] = $error;
|
||||
}
|
||||
|
||||
function getError() {
|
||||
@@ -766,7 +857,7 @@ class EmailDataParser {
|
||||
}
|
||||
|
||||
function lastError() {
|
||||
return $this->error;
|
||||
return end($this->getErrors());
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -183,7 +183,7 @@ abstract class BaseMessageStorage implements MessageStorageBackend {
|
||||
return $messages;
|
||||
}
|
||||
|
||||
function getIterator() {
|
||||
function getIterator(): Traversable {
|
||||
$this->used = true;
|
||||
$messages = $this->load();
|
||||
if ($this->queued) {
|
||||
|
||||
48
include/class.mime.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/**
|
||||
* class.mime.php
|
||||
*
|
||||
* osTicket MIME utilities — extensions for Laminas\Mime components.
|
||||
*
|
||||
* @author JediKev <kevin@enhancesoft.com>
|
||||
* @copyright Copyright (c) osTicket <gpl@osticket.com>
|
||||
*
|
||||
*/
|
||||
namespace osTicket\Mime {
|
||||
use Laminas\Mime\Mime;
|
||||
use Laminas\Mime\Part;
|
||||
|
||||
class Rfc2231Part extends Part {
|
||||
private ?string $rawfilename = null;
|
||||
|
||||
/**
|
||||
* Store the raw (UTF-8) filename for RFC-2231 emission.
|
||||
*/
|
||||
public function setRawFilename(string $name): void {
|
||||
$this->rawfilename = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to append ; filename*=UTF-8''<percent-encoded> to Content-Disposition.
|
||||
* Keep everything else from the parent as-is.
|
||||
*/
|
||||
public function getHeadersArray($eol = Mime::LINEEND): array {
|
||||
$headers = parent::getHeadersArray($eol);
|
||||
if ($this->rawfilename === null || $this->rawfilename === '')
|
||||
return $headers;
|
||||
|
||||
// RFC 2231: UTF-8 then percent-encode (spaces as %20)
|
||||
$encoded = rawurlencode($this->rawfilename);
|
||||
|
||||
foreach ($headers as &$h) {
|
||||
if (strcasecmp($h[0], 'Content-Disposition') === 0) {
|
||||
$h[1] .= "; filename*=UTF-8''{$encoded}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
unset($h);
|
||||
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,29 +226,6 @@ class Misc {
|
||||
return (object) array('start' => $start, 'end' => $end);
|
||||
}
|
||||
|
||||
//Current page
|
||||
static function currentURL() {
|
||||
|
||||
$str = 'http';
|
||||
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
|
||||
$str .='s';
|
||||
}
|
||||
$str .= '://';
|
||||
if (!isset($_SERVER['REQUEST_URI'])) { //IIS???
|
||||
$_SERVER['REQUEST_URI'] = substr($_SERVER['PHP_SELF'],1 );
|
||||
if (isset($_SERVER['QUERY_STRING'])) {
|
||||
$_SERVER['REQUEST_URI'].='?'.$_SERVER['QUERY_STRING'];
|
||||
}
|
||||
}
|
||||
if ($_SERVER['SERVER_PORT']!=80) {
|
||||
$str .= $_SERVER['SERVER_NAME'].':'.$_SERVER['SERVER_PORT'].$_SERVER['REQUEST_URI'];
|
||||
} else {
|
||||
$str .= $_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI'];
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
static function realpath($path) {
|
||||
$rp = realpath($path);
|
||||
return $rp ? $rp : $path;
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace osTicket\OAuth2 {
|
||||
protected $expires;
|
||||
protected $refreshToken;
|
||||
protected $resourceOwnerId;
|
||||
protected $scope;
|
||||
// osTicket specific
|
||||
protected $resourceOwnerEmail;
|
||||
protected $configSignature;
|
||||
@@ -49,6 +50,9 @@ namespace osTicket\OAuth2 {
|
||||
|
||||
if (!empty($options['resource_owner_email']))
|
||||
$this->resourceOwnerEmail = $options['resource_owner_email'];
|
||||
|
||||
if (!empty($options['scope']))
|
||||
$this->scope = $options['scope'];
|
||||
}
|
||||
|
||||
public function getToken() {
|
||||
@@ -79,6 +83,10 @@ namespace osTicket\OAuth2 {
|
||||
return $this->getResourceOwnerEmail();
|
||||
}
|
||||
|
||||
public function getScope() {
|
||||
return $this->scope;
|
||||
}
|
||||
|
||||
public function getConfigSignature() {
|
||||
return $this->configSignature;
|
||||
}
|
||||
@@ -95,14 +103,17 @@ namespace osTicket\OAuth2 {
|
||||
return $this->hasExpired();
|
||||
}
|
||||
|
||||
public function getAuthRequest() {
|
||||
public function isMatch($email, $strict=false) {
|
||||
return (!$strict || strcasecmp($this->getResourceOwnerEmail(), $email) === 0);
|
||||
}
|
||||
|
||||
public function getAuthRequest($user=null) {
|
||||
if ($this->hasExpired())
|
||||
throw new Exception('Access Token is Expired');
|
||||
|
||||
return base64_encode(sprintf("user=%s\1auth=Bearer %s\1\1",
|
||||
$this->getResourceOwner(),
|
||||
$this->getAccessToken()
|
||||
));
|
||||
$user ?? $this->getResourceOwner(),
|
||||
$this->getAccessToken()));
|
||||
}
|
||||
|
||||
public function __toString() {
|
||||
|
||||
@@ -233,16 +233,16 @@ class ModelMeta implements ArrayAccess {
|
||||
return $root;
|
||||
}
|
||||
|
||||
function offsetGet($field) {
|
||||
function offsetGet($field): mixed {
|
||||
return $this->meta[$field];
|
||||
}
|
||||
function offsetSet($field, $what) {
|
||||
function offsetSet($field, $what): void {
|
||||
$this->meta[$field] = $what;
|
||||
}
|
||||
function offsetExists($field) {
|
||||
function offsetExists($field): bool {
|
||||
return isset($this->meta[$field]);
|
||||
}
|
||||
function offsetUnset($field) {
|
||||
function offsetUnset($field): void {
|
||||
throw new Exception('Model MetaData is immutable');
|
||||
}
|
||||
|
||||
@@ -1368,7 +1368,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
|
||||
return $list[0];
|
||||
}
|
||||
|
||||
function count() {
|
||||
function count(): int {
|
||||
// Defer to the iterator if fetching already started
|
||||
if (isset($this->_iterator)) {
|
||||
return $this->_iterator->count();
|
||||
@@ -1562,7 +1562,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
|
||||
}
|
||||
|
||||
// IteratorAggregate interface
|
||||
function getIterator($iterator=false) {
|
||||
function getIterator($iterator=false): Traversable {
|
||||
if (!isset($this->_iterator)) {
|
||||
$class = $iterator ?: $this->getIteratorClass();
|
||||
$it = new $class($this);
|
||||
@@ -1593,16 +1593,16 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
|
||||
}
|
||||
|
||||
// ArrayAccess interface
|
||||
function offsetExists($offset) {
|
||||
function offsetExists($offset): bool {
|
||||
return $this->getIterator()->offsetExists($offset);
|
||||
}
|
||||
function offsetGet($offset) {
|
||||
function offsetGet($offset): mixed {
|
||||
return $this->getIterator()->offsetGet($offset);
|
||||
}
|
||||
function offsetUnset($a) {
|
||||
function offsetUnset($a): void {
|
||||
throw new Exception(__('QuerySet is read-only'));
|
||||
}
|
||||
function offsetSet($a, $b) {
|
||||
function offsetSet($a, $b): void {
|
||||
throw new Exception(__('QuerySet is read-only'));
|
||||
}
|
||||
|
||||
@@ -1667,7 +1667,17 @@ EOF;
|
||||
return $classname;
|
||||
}
|
||||
|
||||
// Fix PHP 8.1.x Deprecation Warnings
|
||||
// Serializable interface will be removed in PHP 9.x
|
||||
function serialize() {
|
||||
return serialize($this->__serialize());
|
||||
}
|
||||
|
||||
function unserialize($data) {
|
||||
$this->__unserialize(unserialize($data));
|
||||
}
|
||||
|
||||
function __serialize() {
|
||||
$info = get_object_vars($this);
|
||||
unset($info['query']);
|
||||
unset($info['limit']);
|
||||
@@ -1675,11 +1685,10 @@ EOF;
|
||||
unset($info['_iterator']);
|
||||
unset($info['count']);
|
||||
unset($info['total']);
|
||||
return serialize($info);
|
||||
return $info;
|
||||
}
|
||||
|
||||
function unserialize($data) {
|
||||
$data = unserialize($data);
|
||||
function __unserialize($data) {
|
||||
foreach ($data as $name => $val) {
|
||||
$this->{$name} = $val;
|
||||
}
|
||||
@@ -1726,27 +1735,27 @@ implements ArrayAccess {
|
||||
$this->inner->rewind();
|
||||
}
|
||||
|
||||
function getIterator() {
|
||||
function getIterator(): Traversable {
|
||||
$this->asArray();
|
||||
return new ArrayIterator($this->storage);
|
||||
}
|
||||
|
||||
function offsetExists($offset) {
|
||||
function offsetExists($offset): bool {
|
||||
$this->fillTo($offset+1);
|
||||
return count($this->storage) > $offset;
|
||||
}
|
||||
function offsetGet($offset) {
|
||||
function offsetGet($offset): mixed {
|
||||
$this->fillTo($offset+1);
|
||||
return $this->storage[$offset];
|
||||
}
|
||||
function offsetUnset($a) {
|
||||
function offsetUnset($a): void {
|
||||
throw new Exception(__('QuerySet is read-only'));
|
||||
}
|
||||
function offsetSet($a, $b) {
|
||||
function offsetSet($a, $b): void {
|
||||
throw new Exception(__('QuerySet is read-only'));
|
||||
}
|
||||
|
||||
function count($mode=COUNT_NORMAL) {
|
||||
function count($mode=COUNT_NORMAL): int {
|
||||
$this->asArray();
|
||||
return count($this->storage);
|
||||
}
|
||||
@@ -1990,7 +1999,7 @@ implements IteratorAggregate {
|
||||
return $model;
|
||||
}
|
||||
|
||||
function getIterator() {
|
||||
function getIterator(): Traversable {
|
||||
$func = ($this->map) ? 'getRow' : 'getArray';
|
||||
$func = array($this->resource, $func);
|
||||
$cache = true;
|
||||
@@ -2019,27 +2028,27 @@ implements Iterator {
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
function rewind() {
|
||||
function rewind(): void {
|
||||
$this->eoi = false;
|
||||
$this->next();
|
||||
}
|
||||
|
||||
function key() {
|
||||
function key(): mixed {
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
function valid() {
|
||||
function valid(): bool {
|
||||
if (!isset($this->eoi))
|
||||
$this->rewind();
|
||||
return !$this->eoi;
|
||||
}
|
||||
|
||||
function current() {
|
||||
function current(): mixed {
|
||||
if ($this->eoi) return false;
|
||||
return $this->current;
|
||||
}
|
||||
|
||||
function next() {
|
||||
function next(): void {
|
||||
try {
|
||||
$cbk = $this->callback;
|
||||
$this->current = $cbk();
|
||||
@@ -2064,7 +2073,7 @@ implements IteratorAggregate {
|
||||
$this->queryset = $queryset;
|
||||
}
|
||||
|
||||
function getIterator() {
|
||||
function getIterator(): Traversable {
|
||||
$this->resource = $this->queryset->getQuery();
|
||||
return new CallbackSimpleIterator(function() {
|
||||
global $StopIteration;
|
||||
@@ -2087,7 +2096,7 @@ implements IteratorAggregate {
|
||||
$this->queryset = $queryset;
|
||||
}
|
||||
|
||||
function getIterator() {
|
||||
function getIterator(): Traversable {
|
||||
$this->resource = $this->queryset->getQuery();
|
||||
return new CallbackSimpleIterator(function() {
|
||||
global $StopIteration;
|
||||
@@ -2162,7 +2171,7 @@ extends ModelResultSet {
|
||||
* Slight edit to the standard iteration method which will skip deleted
|
||||
* items.
|
||||
*/
|
||||
function getIterator() {
|
||||
function getIterator(): Traversable {
|
||||
return new CallbackFilterIterator(parent::getIterator(),
|
||||
function($i) { return !$i->__deleted__; }
|
||||
);
|
||||
@@ -2235,11 +2244,11 @@ extends ModelResultSet {
|
||||
return clone $this->queryset;
|
||||
}
|
||||
|
||||
function offsetUnset($a) {
|
||||
function offsetUnset($a): void {
|
||||
$this->fillTo($a);
|
||||
$this->storage[$a]->delete();
|
||||
}
|
||||
function offsetSet($a, $b) {
|
||||
function offsetSet($a, $b): void {
|
||||
$this->fillTo($a);
|
||||
if ($obj = $this->storage[$a])
|
||||
$obj->delete();
|
||||
@@ -2628,6 +2637,12 @@ class SqlCompiler {
|
||||
|
||||
function getJoins($queryset) {
|
||||
$sql = '';
|
||||
// Move CDATA to top of joins
|
||||
if (array_key_exists("cdata", $this->joins)) {
|
||||
$temp = ['cdata' => $this->joins['cdata']];
|
||||
unset($this->joins['cdata']);
|
||||
$this->joins = $temp + $this->joins;
|
||||
}
|
||||
foreach ($this->joins as $path => $j) {
|
||||
if (!isset($j['sql']))
|
||||
continue;
|
||||
@@ -2862,7 +2877,7 @@ class MySqlCompiler extends SqlCompiler {
|
||||
$table = ($rmeta['view'])
|
||||
// XXX: Support parameters from the nested query
|
||||
? $rmodel::getSqlAddParams($this)
|
||||
: $this->quote($rmeta['table']);
|
||||
: $this->quote($rmeta['table'], true);
|
||||
$base = "{$join}{$table} {$alias}";
|
||||
return array($base, $constraints);
|
||||
}
|
||||
@@ -2951,7 +2966,12 @@ class MySqlCompiler extends SqlCompiler {
|
||||
$q->annotations = false;
|
||||
$exec = $q->getQuery(array('nosort' => true));
|
||||
$exec->sql = 'SELECT COUNT(*) FROM ('.$exec->sql.') __';
|
||||
$row = $exec->getRow();
|
||||
try {
|
||||
$row = $exec->getRow();
|
||||
} catch (mysqli_sql_exception $e) {
|
||||
throw new InconsistentModelException(
|
||||
'Unable to prepare query: '.db_error().' '.$exec->sql);
|
||||
}
|
||||
return is_array($row) ? (int) $row[0] : null;
|
||||
}
|
||||
|
||||
@@ -3621,12 +3641,22 @@ class Q implements Serializable {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Fix PHP 8.1.x Deprecation Warnings
|
||||
// Serializable interface will be removed in PHP 9.x
|
||||
function serialize() {
|
||||
return serialize(array($this->negated, $this->ored, $this->constraints));
|
||||
return serialize($this->__serialize());
|
||||
}
|
||||
|
||||
function unserialize($data) {
|
||||
list($this->negated, $this->ored, $this->constraints) = unserialize($data);
|
||||
$this->__unserialize(unserialize($data));
|
||||
}
|
||||
|
||||
function __serialize() {
|
||||
return array($this->negated, $this->ored, $this->constraints);
|
||||
}
|
||||
|
||||
function __unserialize($data) {
|
||||
list($this->negated, $this->ored, $this->constraints) = $data;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -28,7 +28,6 @@ use Symfony\Component\ClassLoader\UniversalClassLoader_osTicket;
|
||||
define('LOG_WARN',LOG_WARNING);
|
||||
|
||||
class osTicket {
|
||||
|
||||
var $loglevel=array(1=>'Error','Warning','Debug');
|
||||
|
||||
//Page errors.
|
||||
@@ -36,10 +35,6 @@ class osTicket {
|
||||
|
||||
//System
|
||||
var $system;
|
||||
|
||||
|
||||
|
||||
|
||||
var $warning;
|
||||
var $message;
|
||||
|
||||
@@ -59,9 +54,11 @@ class osTicket {
|
||||
require_once(INCLUDE_DIR.'class.company.php');
|
||||
// Load the config
|
||||
$this->config = new OsticketConfig();
|
||||
// Start session (if not disabled)
|
||||
if (!defined('DISABLE_SESSION') || !DISABLE_SESSION)
|
||||
$this->session = osTicketSession::start(SESSION_TTL,
|
||||
|
||||
// Start Session
|
||||
if (!defined('SESSION_SESSID'))
|
||||
define('SESSION_SESSID', 'OSTSESSID');
|
||||
$this->session = osTicketSession::start(SESSION_SESSID, SESSION_TTL,
|
||||
$this->isUpgradePending());
|
||||
// CSRF Token
|
||||
$this->csrf = new CSRF('__CSRFToken__');
|
||||
@@ -240,7 +237,8 @@ class osTicket {
|
||||
if($email) {
|
||||
$email->sendAlert($to, $subject, $message, null, array('text'=>true, 'reply-tag'=>false));
|
||||
} else {//no luck - try the system mail.
|
||||
Mailer::sendmail($to, $subject, $message, '"'.__('osTicket Alerts').sprintf('" <%s>',$to));
|
||||
osTicket\Mail\Mailer::sendmail($to, $subject, $message,
|
||||
'"'.__('osTicket Alerts').sprintf('" <%s>',$to));
|
||||
}
|
||||
|
||||
//log the alert? Watch out for loops here.
|
||||
@@ -322,7 +320,8 @@ class osTicket {
|
||||
.',title='.db_input(Format::sanitize($title, true))
|
||||
.',log_type='.db_input($loglevel[$level])
|
||||
.',log='.db_input(Format::sanitize($message, false))
|
||||
.',ip_address='.db_input($_SERVER['REMOTE_ADDR']);
|
||||
.',ip_address='.db_input($_SERVER['REMOTE_ADDR'])
|
||||
.',logger=""';
|
||||
|
||||
db_query($sql, false);
|
||||
|
||||
@@ -368,10 +367,10 @@ class osTicket {
|
||||
|
||||
static function get_path_info() {
|
||||
if(isset($_SERVER['PATH_INFO']))
|
||||
return $_SERVER['PATH_INFO'];
|
||||
return htmlentities($_SERVER['PATH_INFO']);
|
||||
|
||||
if(isset($_SERVER['ORIG_PATH_INFO']))
|
||||
return $_SERVER['ORIG_PATH_INFO'];
|
||||
return htmlentities($_SERVER['ORIG_PATH_INFO']);
|
||||
|
||||
//TODO: conruct possible path info.
|
||||
|
||||
@@ -535,6 +534,25 @@ class osTicket {
|
||||
$_SERVER['HTTP_HOST'] . ROOT_PATH);
|
||||
}
|
||||
|
||||
/*
|
||||
* get_client_port
|
||||
*
|
||||
* Get client PORT from "Http_X-Forwarded-PORT" if we have trusted
|
||||
* proxies set.
|
||||
* FIXME: Follow trusted proxies chain
|
||||
*
|
||||
*/
|
||||
static function get_client_port($header='HTTP_X_FORWARDED_PORT') {
|
||||
$port = $_SERVER['SERVER_PORT'];
|
||||
// We're just making sure we have Trusted Proxies
|
||||
// FIXME: Validate
|
||||
$proxies = self::getTrustedProxies();
|
||||
if (isset($_SERVER[$header]) && $proxies)
|
||||
$port = $_SERVER[$header];
|
||||
|
||||
return $port;
|
||||
}
|
||||
|
||||
/*
|
||||
* get_client_ip
|
||||
*
|
||||
@@ -587,7 +605,7 @@ class osTicket {
|
||||
if (!$proxies)
|
||||
return false;
|
||||
// Wildcard set - trust all proxies
|
||||
else if ($proxies == '*')
|
||||
else if (in_array('*', $proxies))
|
||||
return true;
|
||||
|
||||
return ($proxies && Validator::check_ip($ip, $proxies));
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
/*********************************************************************
|
||||
class.ostsession.php
|
||||
|
||||
Custom osTicket session handler.
|
||||
osTicket Session Management Backend
|
||||
|
||||
Peter Rotich <peter@osticket.com>
|
||||
Copyright (c) 2006-2013 osTicket
|
||||
Copyright (c) 2022 osTicket
|
||||
http://www.osticket.com
|
||||
|
||||
Released under the GNU General Public License WITHOUT ANY WARRANTY.
|
||||
@@ -13,85 +13,259 @@
|
||||
|
||||
vim: expandtab sw=4 ts=4 sts=4:
|
||||
**********************************************************************/
|
||||
include_once INCLUDE_DIR.'class.session.php';
|
||||
|
||||
class osTicketSession {
|
||||
static $backends = array(
|
||||
'db' => 'DbSessionBackend',
|
||||
'memcache' => 'MemcacheSessionBackend',
|
||||
'system' => 'FallbackSessionBackend',
|
||||
);
|
||||
// Session SSID
|
||||
private $name = 'OSTSESSID';
|
||||
// Session Backend instance
|
||||
private $backend;
|
||||
// Session default TTL
|
||||
private $ttl;
|
||||
|
||||
var $ttl = SESSION_TTL;
|
||||
var $data = '';
|
||||
var $data_hash = '';
|
||||
var $id = '';
|
||||
var $backend;
|
||||
|
||||
function __construct($ttl=0, $checkdbversion=false){
|
||||
$this->ttl = $ttl ?: ini_get('session.gc_maxlifetime') ?: SESSION_TTL;
|
||||
|
||||
// Set osTicket specific session name.
|
||||
session_name('OSTSESSID');
|
||||
|
||||
// Forced cleanup on shutdown
|
||||
register_shutdown_function('session_write_close');
|
||||
|
||||
// Set session cleanup time to match TTL
|
||||
ini_set('session.gc_maxlifetime', $ttl);
|
||||
|
||||
// Skip db version check if version is later than 1.7
|
||||
if ((!defined('MAJOR_VERSION') || $checkdbversion)
|
||||
&& OsticketConfig::getDBVersion())
|
||||
return session_start();
|
||||
|
||||
# Cookies
|
||||
// Avoid setting a cookie domain without a dot, thanks
|
||||
// http://stackoverflow.com/a/1188145
|
||||
$domain = null;
|
||||
if (isset($_SERVER['HTTP_HOST'])
|
||||
&& strpos($_SERVER['HTTP_HOST'], '.') !== false
|
||||
&& !Validator::is_ip($_SERVER['HTTP_HOST']))
|
||||
// Remote port specification, as it will make an invalid domain
|
||||
list($domain) = explode(':', $_SERVER['HTTP_HOST']);
|
||||
|
||||
session_set_cookie_params($ttl, ROOT_PATH, $domain,
|
||||
function __construct($name, $ttl = SESSION_TTL, $checkdbversion = false) {
|
||||
// session name/ssid
|
||||
if ($name && strcmp($this->name, $name))
|
||||
$this->name = $name;
|
||||
// Session ttl cannot exceed php.ini maxlifetime setting
|
||||
$maxlife = ini_get('session.gc_maxlifetime');
|
||||
$this->ttl = min($ttl ?: ($maxlife ?: SESSION_TTL), $maxlife);
|
||||
// Set osTicket specific session name/sessid
|
||||
session_name($this->name);
|
||||
// Set Default cookie Params before we start the session
|
||||
session_set_cookie_params($this->ttl, ROOT_PATH, Http::domain(),
|
||||
osTicket::is_https(), true);
|
||||
|
||||
if (!defined('SESSION_BACKEND'))
|
||||
define('SESSION_BACKEND', 'db');
|
||||
/** Determine Session Backend to use **/
|
||||
if ((!defined('MAJOR_VERSION') || $checkdbversion)
|
||||
&& OsticketConfig::getDBVersion())
|
||||
// Default PHP SessionHandler
|
||||
$bk = 'system';
|
||||
else // default is database
|
||||
$bk = self::session_backend() ?: 'database';
|
||||
|
||||
// Tag the backend with storage backend class
|
||||
switch ($bk) {
|
||||
case 'database':
|
||||
$bk = "$bk:DatabaseSessionStorageBackend";
|
||||
break;
|
||||
case 'memcache':
|
||||
$bk = "$bk:MemcacheSessionStorageBackend";
|
||||
break;
|
||||
case 'memcache.database':
|
||||
// memcache is primary while database is secondary
|
||||
// Database doesn't store data when set as secondary, but
|
||||
// very useful when session data is offloaded to memcache with
|
||||
// database tracking sessions ids and expire time for the
|
||||
// purpose of knowing who is online.
|
||||
$bk = sprintf('%s:%s:%s',
|
||||
'memcache',
|
||||
'MemcacheSessionStorageBackend',
|
||||
'DatabaseSessionStorageBackend');
|
||||
break;
|
||||
case 'database.memcache':
|
||||
// database is primary while memcache is secondary
|
||||
// This setup only makes sense if memcache is being used as
|
||||
// backup backend
|
||||
$bk = sprintf('%s:%s:%s',
|
||||
'database',
|
||||
'DatabaseSessionStorageBackend',
|
||||
'MemcacheSessionStorageBackend');
|
||||
break;
|
||||
case 'system':
|
||||
case 'noop':
|
||||
// system & noop don't require storage backends
|
||||
break;
|
||||
default:
|
||||
// Assume invalid entry - default to system
|
||||
$bk = 'system';
|
||||
}
|
||||
|
||||
/** Session Backend options **/
|
||||
$options = [
|
||||
// Default TTL (maxlife)
|
||||
'session_ttl' => $this->ttl,
|
||||
// It indicates that the session is API session - which session
|
||||
// handler should handle as stateless for new sessions.
|
||||
'api_session' => defined('API_SESSION'),
|
||||
'callbacks' => $this->getCallbacks($bk),
|
||||
];
|
||||
|
||||
// Set MaxLifeTime if defined. This is defined per user so it's
|
||||
// preferred over ttl.
|
||||
if (defined('SESSION_MAXLIFE') && is_numeric(SESSION_MAXLIFE))
|
||||
$options['session_maxlife'] = SESSION_MAXLIFE;
|
||||
|
||||
// If $bk doesn't exit then an Exception will be thrown
|
||||
// Backend is object is turned on success
|
||||
try {
|
||||
$bk = SESSION_BACKEND;
|
||||
if (!class_exists(self::$backends[$bk]))
|
||||
$bk = 'db';
|
||||
$this->backend = new self::$backends[$bk]($this->ttl);
|
||||
}
|
||||
catch (Exception $x) {
|
||||
// Use the database for sessions
|
||||
trigger_error($x->getMessage(), E_USER_WARNING);
|
||||
$this->backend = new self::$backends['db']($this->ttl);
|
||||
// If $bk doesn't exit then an Exception will be thrown
|
||||
// backend object is turned on success
|
||||
$this->backend = osTicket\Session\SessionBackend::register($bk,
|
||||
$options, true);
|
||||
} catch (Throwable $t) {
|
||||
die($t->getMessage());
|
||||
// We're just gonna default to php session handler and hope for
|
||||
// the best.
|
||||
// TODO: Log the error and perhaps rethrow the exception so it
|
||||
// can be fatal?
|
||||
}
|
||||
|
||||
if ($this->backend instanceof SessionBackend) {
|
||||
// Set handlers.
|
||||
session_set_save_handler(
|
||||
array($this->backend, 'open'),
|
||||
array($this->backend, 'close'),
|
||||
array($this->backend, 'read'),
|
||||
array($this->backend, 'write'),
|
||||
array($this->backend, 'destroy'),
|
||||
array($this->backend, 'gc')
|
||||
);
|
||||
}
|
||||
|
||||
// Start the session.
|
||||
// Finally start the damn session.
|
||||
session_start();
|
||||
}
|
||||
|
||||
function regenerate_id(){
|
||||
$oldId = session_id();
|
||||
session_regenerate_id();
|
||||
$this->backend->destroy($oldId);
|
||||
// returns session callbacks we might be interested in monitoring
|
||||
private function getCallbacks($bk) {
|
||||
return [
|
||||
// see onClose routine for details
|
||||
'close' => [$this, 'onClose']
|
||||
];
|
||||
}
|
||||
|
||||
// onClose - is used to signal those interested on changing session
|
||||
// data, to do so, before data is commited.
|
||||
public function onClose($handler) {
|
||||
$i = new ArrayObject(['touched' => false]);
|
||||
Signal::send('session.close', null, $i);
|
||||
return (bool) $i['touched'];
|
||||
}
|
||||
|
||||
/*
|
||||
* session_backend
|
||||
*
|
||||
* Get configured session backend if any.
|
||||
*/
|
||||
static function session_backend($default = false) {
|
||||
// Session Disabled?
|
||||
// This is useful when fetching logos or on CLI - we don't want to update the
|
||||
// session in such cases.
|
||||
if (defined('NOOP_SESSION') || defined('DISABLE_SESSION'))
|
||||
return 'noop'; // Ignore session data
|
||||
|
||||
// No session backend set - return default
|
||||
if (!defined('SESSION_BACKEND'))
|
||||
return $default;
|
||||
|
||||
// Explode backend incase it's chained
|
||||
list($bk, $secondary) = explode('.', SESSION_BACKEND);
|
||||
// Only recongnize supported primary backends
|
||||
switch (strtolower($bk)) {
|
||||
case 'memcache':
|
||||
// Make sure we have memcache servers defined - if not
|
||||
// then break so we can return default.
|
||||
// TODO: Log an error or throw an exception
|
||||
if (!defined('MEMCACHE_SERVERS'))
|
||||
break;
|
||||
// No break on purpose
|
||||
case 'database':
|
||||
case 'system':
|
||||
return strtolower(SESSION_BACKEND);
|
||||
break;
|
||||
default:
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* registered_backend
|
||||
*
|
||||
* get registered backend if any
|
||||
*/
|
||||
static function registered_backend() {
|
||||
global $ost;
|
||||
if ($ost && isset($ost->session))
|
||||
return $ost->session->backend;
|
||||
}
|
||||
|
||||
/*
|
||||
* regenerate($ttl)
|
||||
*
|
||||
* Regenerate current session_id and expire the old one in $ttl seconds
|
||||
* This is preferred over destroying the session immediately just incase
|
||||
* we have pending ajax requests for example
|
||||
*
|
||||
*/
|
||||
static function regenerate(int $ttl=60) {
|
||||
// Make sure session is active and headers are not already sent
|
||||
if (session_status() !== PHP_SESSION_ACTIVE
|
||||
|| headers_sent())
|
||||
return false;
|
||||
|
||||
// Save current ($old) session id
|
||||
$old = session_id();
|
||||
// Expire current session cookie now + ttl
|
||||
// We have to do it here before re regenerate becase renewCookie has
|
||||
// hardcoded session calls to get name & id
|
||||
// FIXME: Maybe bae...
|
||||
self::renewCookie(time(), $ttl);
|
||||
// Regenerate the session
|
||||
// TODO: use session_create_id() instead of session_regenerate_id -
|
||||
// but PHP (even 8) is still bugy and inconsistent when it comes to
|
||||
// using SessionIdInterface::create_sid depening on session settings.
|
||||
session_regenerate_id(false);
|
||||
// get a new session id and force commit
|
||||
// Expire old session now + ttl
|
||||
session_write_close();
|
||||
// Expire old session now + ttl
|
||||
// ::expire() is not a standard session routine so we have to commit
|
||||
// the current "new" session first
|
||||
self::expire($old, $ttl);
|
||||
// Restart the new session
|
||||
session_start();
|
||||
// Return the new session id
|
||||
return session_id();
|
||||
}
|
||||
|
||||
/*
|
||||
* expire session
|
||||
*
|
||||
* Expire session in $ttl from now if the storage backend supports
|
||||
* it otherwise it should destroy it by calling this->destroy($id)
|
||||
*/
|
||||
// Expire session - end is near mb!
|
||||
static function expire($id, int $ttl) {
|
||||
// See if we have a backend to ask to expire the session - otherwise
|
||||
// we destroy session now!
|
||||
if (!($backend=self::registered_backend()))
|
||||
return false;
|
||||
|
||||
// Expire session soonish (now() + $ttl) - end is near mb!
|
||||
return (bool) $backend->expire($id, $ttl);
|
||||
}
|
||||
|
||||
// Aks the backend to destroy a session now
|
||||
static function destroy($id) {
|
||||
// Expire with ttl of 0 destroys the session
|
||||
return (bool) self::expire($id, 0);
|
||||
}
|
||||
|
||||
// Cleanup Expired Sessions
|
||||
static function cleanup() {
|
||||
// get active backend
|
||||
if (!($backend=self::registered_backend()))
|
||||
return false;
|
||||
|
||||
return (bool) $backend->cleanup();
|
||||
}
|
||||
|
||||
static function renewCookie($baseTime=false, $window=false) {
|
||||
global $ost;
|
||||
|
||||
$ttl = $window ?: SESSION_TTL;
|
||||
$expire = ($baseTime ?: time()) + $ttl;
|
||||
$opts = [
|
||||
'expires' => $expire,
|
||||
'path' => ini_get('session.cookie_path'),
|
||||
'domain' => ini_get('session.cookie_domain'),
|
||||
'secure' => ini_get('session.cookie_secure'),
|
||||
'httponly' => ini_get('session.cookie_httponly'),
|
||||
'samesite' => !empty($ost->getConfig()->getAllowIframes()) ? 'None' : 'Strict'
|
||||
];
|
||||
setcookie(session_name(), session_id(), $opts);
|
||||
// Trigger expire update - neeed for secondary handlers that only
|
||||
// log new sessions
|
||||
self::expire(session_id(), $ttl);
|
||||
}
|
||||
|
||||
static function destroyCookie() {
|
||||
@@ -102,252 +276,409 @@ class osTicketSession {
|
||||
ini_get('session.cookie_httponly'));
|
||||
}
|
||||
|
||||
static function renewCookie($baseTime=false, $window=false) {
|
||||
setcookie(session_name(), session_id(),
|
||||
($baseTime ?: time()) + ($window ?: SESSION_TTL),
|
||||
ini_get('session.cookie_path'),
|
||||
ini_get('session.cookie_domain'),
|
||||
ini_get('session.cookie_secure'),
|
||||
ini_get('session.cookie_httponly'));
|
||||
}
|
||||
|
||||
/* helper functions */
|
||||
|
||||
function get_online_users($sec=0){
|
||||
$sql='SELECT user_id FROM '.SESSION_TABLE.' WHERE user_id>0 AND session_expire>NOW()';
|
||||
if($sec)
|
||||
$sql.=" AND TIME_TO_SEC(TIMEDIFF(NOW(),session_updated))<$sec";
|
||||
|
||||
$users=array();
|
||||
if(($res=db_query($sql)) && db_num_rows($res)) {
|
||||
while(list($uid)=db_fetch_row($res))
|
||||
$users[] = $uid;
|
||||
}
|
||||
|
||||
static function get_online_users(int $seconds = 0) {
|
||||
// Authoretative lookup is DatabaseSessionRecords assuming
|
||||
// database is the primary backend or secondary logger
|
||||
$records = DatabaseSessionRecord::active_sessions([
|
||||
'lastseen' => $seconds,
|
||||
'authenticated' => true,
|
||||
]);
|
||||
$users = [];
|
||||
foreach ($records as $record)
|
||||
$users[] = $record->getUserId();
|
||||
return $users;
|
||||
}
|
||||
|
||||
/* ---------- static function ---------- */
|
||||
static function start($ttl=0, $checkdbversion=false) {
|
||||
return new static($ttl, $checkdbversion);
|
||||
static function start($name, $ttl=0, $checkdbversion=false) {
|
||||
return new static($name, $ttl, $checkdbversion);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SessionBackend {
|
||||
var $isnew = false;
|
||||
var $ttl;
|
||||
|
||||
function __construct($ttl=SESSION_TTL) {
|
||||
$this->ttl = $ttl;
|
||||
}
|
||||
|
||||
function open($save_path, $session_name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function getTTL() {
|
||||
return $this->ttl;
|
||||
}
|
||||
|
||||
function write($id, $data) {
|
||||
// Last chance session update
|
||||
$i = new ArrayObject(array('touched' => false));
|
||||
Signal::send('session.close', null, $i);
|
||||
return $this->update($id, $i['touched'] ? session_encode() : $data);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
$this->gc(0);
|
||||
}
|
||||
|
||||
abstract function read($id);
|
||||
abstract function update($id, $data);
|
||||
abstract function destroy($id);
|
||||
abstract function gc($maxlife);
|
||||
}
|
||||
|
||||
class SessionData
|
||||
extends VerySimpleModel {
|
||||
/*
|
||||
* DatabaseSessionRecord
|
||||
*
|
||||
* ORM hook to session table with SessionRecordInterface implementation
|
||||
*/
|
||||
class DatabaseSessionRecord extends VerySimpleModel
|
||||
implements osTicket\Session\SessionRecordInterface {
|
||||
static $meta = array(
|
||||
'table' => SESSION_TABLE,
|
||||
'pk' => array('session_id'),
|
||||
);
|
||||
}
|
||||
|
||||
class DbSessionBackend
|
||||
extends SessionBackend {
|
||||
var $data = null;
|
||||
public function isNew() {
|
||||
return $this->__new__;
|
||||
}
|
||||
|
||||
function read($id) {
|
||||
public function isValid() {
|
||||
// NOTE: We're not enforcing IP match here because it's a user space
|
||||
// setting and checked elsewhere
|
||||
|
||||
// User Agent must remain same for the lifecycle of the session
|
||||
if (isset($this->user_agent)
|
||||
&& strcmp($_SERVER['HTTP_USER_AGENT'], $this->user_agent) !== 0)
|
||||
return false;
|
||||
|
||||
// Make sure id len is 32
|
||||
return (strlen($this->getId()) == 32);
|
||||
}
|
||||
|
||||
public function setId(string $id) {
|
||||
$this->session_id = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getId() {
|
||||
return $this->session_id;
|
||||
}
|
||||
|
||||
public function setData(?string $data = null) {
|
||||
$this->session_data = $data;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getData() {
|
||||
return $this->session_data;
|
||||
}
|
||||
|
||||
public function getExpireTime() {
|
||||
return $this->session_expire;
|
||||
}
|
||||
|
||||
private function setExpire(int $maxlife) {
|
||||
$this->session_expire = SqlFunction::NOW()
|
||||
->plus(SqlInterval::SECOND($maxlife));
|
||||
}
|
||||
|
||||
public function setTTL($ttl) {
|
||||
$this->setExpire($ttl);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function expire(int $ttl) {
|
||||
$this->setTTL($ttl);
|
||||
// Assume it will expire shortly - clear user_id
|
||||
$this->user_id = 0;
|
||||
return $this->commit();
|
||||
}
|
||||
|
||||
public function getUpdateTime() {
|
||||
return $this->session_updated;
|
||||
}
|
||||
|
||||
public function commit() {
|
||||
return ($this->save());
|
||||
}
|
||||
|
||||
public function save($refresh=false) {
|
||||
global $thisstaff;
|
||||
// See if we need to set the user id - should only be set once onlogin
|
||||
if (!$this->user_id && $thisstaff)
|
||||
$this->user_id = $thisstaff->getId();
|
||||
if (count($this->dirty))
|
||||
$this->session_updated = SqlFunction::NOW();
|
||||
return parent::save($refresh);
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
return (parent::delete());
|
||||
}
|
||||
|
||||
public function toArray() {
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'data' => $this->getData(),
|
||||
'updated' => $this->getUpdateTime(),
|
||||
'expires' => $this->getExpireTime(),
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
* lookupRecord
|
||||
*
|
||||
* Given session id - lookup the record
|
||||
*
|
||||
* Possible returns;
|
||||
* false: on lookup failure
|
||||
* null: doesn't exists and autocreate is false
|
||||
* record: fetched or newly created session record
|
||||
*
|
||||
*/
|
||||
public static function lookupRecord($id, $autocreate = false,
|
||||
$backend=null) {
|
||||
// We're doing lookup locally so we can auto-create one if the
|
||||
// session doesn't exist.
|
||||
$record = false;
|
||||
try {
|
||||
$this->data = SessionData::objects()
|
||||
$record = self::objects()
|
||||
->filter(['session_id' => $id])
|
||||
->annotate(array('is_expired' =>
|
||||
new SqlExpr(new Q(array('session_expire__lt' => SqlFunction::NOW())))))
|
||||
->one();
|
||||
|
||||
if ($this->data->is_expired > 0) {
|
||||
if ($record->is_expired > 0) {
|
||||
// session_expire is in the past. Pretend it is expired and
|
||||
// reset the data. This will assist with CSRF issues
|
||||
$this->data->session_data='';
|
||||
$record->session_data = '';
|
||||
}
|
||||
$this->id = $id;
|
||||
}
|
||||
catch (DoesNotExist $e) {
|
||||
$this->data = new SessionData(['session_id' => $id, 'session_data' => '']);
|
||||
// We're auto-creating model (unsaved) when one doesn't exist?
|
||||
$record = ($autocreate && ctype_alnum($id)) ? self::create($id) : null;
|
||||
}
|
||||
catch (OrmException $e) {
|
||||
return false;
|
||||
catch (OrmException | Exception $ex) {
|
||||
// This could happen if more than one record exits in the
|
||||
// database for example.
|
||||
}
|
||||
// Verify the User Agent string
|
||||
if (isset($this->data->user_agent)
|
||||
&& (strcmp($_SERVER['HTTP_USER_AGENT'], $this->data->user_agent) !== 0)) {
|
||||
$this->destroy($id);
|
||||
return false;
|
||||
}
|
||||
return $this->data->session_data;
|
||||
return $record;
|
||||
}
|
||||
|
||||
function update($id, $data){
|
||||
global $thisstaff;
|
||||
static function create($vars) {
|
||||
// We expect ::init($id) or ::init(array $vars)
|
||||
if ($vars && !is_array($vars))
|
||||
$vars = ['session_id' => $vars];
|
||||
elseif (!isset($vars['session_id']))
|
||||
// Session Id is required
|
||||
throw Exception(sprintf('session_id: %s', __('Required')));
|
||||
|
||||
if (defined('DISABLE_SESSION') && $this->data->__new__)
|
||||
return true;
|
||||
// Set User Session Atrributes
|
||||
if (!isset($vars['user_ip']))
|
||||
$vars['user_ip'] = $_SERVER['REMOTE_ADDR'];
|
||||
if (!isset($vars['user_agent']))
|
||||
$vars['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
|
||||
// Default to system SESSION_TTL if session_maxlife is not set
|
||||
$maxlife = $vars['session_maxlife'] ?? SESSION_TTL;
|
||||
// filter out the $vars to just the fields we support - backend can
|
||||
// send way more data than we want.
|
||||
$vars = array_intersect_key($vars,
|
||||
array_flip([
|
||||
'session_id',
|
||||
'session_data',
|
||||
'session_expire',
|
||||
'session_updated',
|
||||
'user_id',
|
||||
'user_ip',
|
||||
'user_agent',
|
||||
]));
|
||||
|
||||
$ttl = $this && method_exists($this, 'getTTL')
|
||||
? $this->getTTL() : SESSION_TTL;
|
||||
|
||||
// Create a session data obj if not loaded.
|
||||
if (!isset($this->data))
|
||||
$this->data = new SessionData(['session_id' => $id]);
|
||||
|
||||
$this->data->session_data = $data;
|
||||
$this->data->session_expire =
|
||||
SqlFunction::NOW()->plus(SqlInterval::SECOND($ttl));
|
||||
$this->data->user_id = $thisstaff ? $thisstaff->getId() : 0;
|
||||
$this->data->user_ip = $_SERVER['REMOTE_ADDR'];
|
||||
$this->data->user_agent = $_SERVER['HTTP_USER_AGENT'];
|
||||
|
||||
return $this->data->save();
|
||||
// Create an instance
|
||||
$record = new self($vars);
|
||||
// set session_expire timestamp based on $maxlife
|
||||
$record->setExpire($maxlife);
|
||||
// Set updated to now()
|
||||
$record->session_updated = SqlFunction::NOW();
|
||||
return $record;
|
||||
}
|
||||
|
||||
function destroy($id){
|
||||
return SessionData::objects()->filter(['session_id' => $id])->delete() ? true : false;
|
||||
static function destroy($id) {
|
||||
return ( (bool) self::objects()
|
||||
->filter(['session_id' => $id])
|
||||
->delete());
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
$this->gc(0);
|
||||
}
|
||||
|
||||
function gc($maxlife){
|
||||
SessionData::objects()->filter([
|
||||
'session_expire__lte' => SqlFunction::NOW()
|
||||
static function cleanupExpired() {
|
||||
return self::objects()->filter([
|
||||
'session_expire__lte' => SqlFunction::NOW()
|
||||
])->delete();
|
||||
}
|
||||
|
||||
static function user_sessions(int $id) {
|
||||
$criteria = ['user_id' => $id];
|
||||
return self::active_sessions($criteria);
|
||||
}
|
||||
|
||||
static function active_sessions(array $criteria = []) {
|
||||
$criteria['active'] = true;
|
||||
return self::sessions($criteria);
|
||||
}
|
||||
|
||||
static function expired_sessions(array $criteria = []) {
|
||||
$criteria['active'] = false;
|
||||
return self::sessions($criteria);
|
||||
}
|
||||
|
||||
static function sessions(array $criteria = []) {
|
||||
// now
|
||||
$now = SqlFunction::NOW();
|
||||
// empty filters
|
||||
$filters = [];
|
||||
// Active or Expired
|
||||
if (isset($criteria['active']) && $criteria['active']) {
|
||||
// Active must not be expired
|
||||
$filters = ['session_expire__gt' => $now];
|
||||
} elseif (isset($criteria['active'])) {
|
||||
// expired session if active is set to false
|
||||
$filters = ['session_expire__lt' => $now];
|
||||
}
|
||||
|
||||
// Authenticated users have user_id set (only Agents at the moment)
|
||||
if (isset($criteria['user_id']) && $criteria['user_id'])
|
||||
$filters['user_id'] = $criteria['user_id'];
|
||||
elseif (isset($criteria['authenticated']) && $criteria['authenticated'])
|
||||
$filters['user_id__gt'] = 0;
|
||||
elseif (isset($criteria['authenticated']))
|
||||
$filters['user_id'] = 0; // Guests only
|
||||
|
||||
// last seen since X seconds
|
||||
if (isset($criteria['lastseen']) && $criteria['lastseen']) {
|
||||
$interval = new SqlInterval('SECOND', $criteria['lastseen']);
|
||||
$filters['session_updated__gte'] = $now->minus($interval);
|
||||
}
|
||||
|
||||
return self::objects()->filter($filters);
|
||||
}
|
||||
}
|
||||
|
||||
class MemcacheSessionBackend
|
||||
extends SessionBackend {
|
||||
var $memcache;
|
||||
var $servers = array();
|
||||
/*
|
||||
* Database Session Storage Backend
|
||||
*
|
||||
*/
|
||||
class DatabaseSessionStorageBackend
|
||||
extends osTicket\Session\AbstractSessionStorageBackend {
|
||||
|
||||
function __construct($ttl) {
|
||||
parent::__construct($ttl);
|
||||
public function expireRecord($id, $ttl) {
|
||||
if (!($record=$this->getRecord($id)))
|
||||
return false;
|
||||
|
||||
if (!extension_loaded('memcache'))
|
||||
throw new Exception('Memcached extension is missing');
|
||||
if (!defined('MEMCACHE_SERVERS'))
|
||||
return (bool) $record->expire($ttl);
|
||||
}
|
||||
|
||||
public function saveRecord($record, $secondary=false) {
|
||||
if ($secondary || !is_a($record, 'DatabaseSessionRecord')) {
|
||||
// We're only recording new sessions without data if we're
|
||||
// secondary backend
|
||||
if ($record->isNew())
|
||||
$this->writeRecord($record->getId(), null);
|
||||
return true;
|
||||
}
|
||||
return (bool) $record->commit();
|
||||
}
|
||||
|
||||
public function writeRecord($id, $data=null) {
|
||||
$record = self:: lookupRecord($id, true);
|
||||
$record->setData($data);
|
||||
return (bool) $record->commit();
|
||||
}
|
||||
|
||||
public function lookupRecord($id, $autocreate) {
|
||||
return DatabaseSessionRecord::lookupRecord($id, $autocreate, $this);
|
||||
}
|
||||
|
||||
public function destroyRecord($id) {
|
||||
return (bool) (DatabaseSessionRecord::destroy($id));
|
||||
}
|
||||
|
||||
public function cleanupExpiredRecords() {
|
||||
return DatabaseSessionRecord::cleanupExpired();
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Memchache Session Storage Backend
|
||||
*
|
||||
*/
|
||||
class MemcacheSessionStorageBackend
|
||||
extends osTicket\Session\AbstractMemcacheSessionStorageBackend {
|
||||
|
||||
public function __construct($options) {
|
||||
// Make sure we have memcache servers
|
||||
$servers = [];
|
||||
if (isset($options['servers']) && is_array($options['servers']))
|
||||
$servers = $options['servers'];
|
||||
elseif (defined('MEMCACHE_SERVERS'))
|
||||
$servers = explode(',', MEMCACHE_SERVERS);
|
||||
// Bro Got Servers or Nah?!
|
||||
if (!count($servers))
|
||||
throw new Exception('MEMCACHE_SERVERS must be defined');
|
||||
|
||||
$servers = explode(',', MEMCACHE_SERVERS);
|
||||
$this->memcache = new Memcache();
|
||||
|
||||
foreach ($servers as $S) {
|
||||
@list($host, $port) = explode(':', $S);
|
||||
if (strpos($host, '/') !== false)
|
||||
// Use port '0' for unix sockets
|
||||
$port = 0;
|
||||
else
|
||||
$port = $port ?: ini_get('memcache.default_port') ?: 11211;
|
||||
$this->servers[] = array(trim($host), (int) trim($port));
|
||||
// FIXME: Crash or warn if invalid $host or $port
|
||||
}
|
||||
$options['servers'] = $servers;
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
function getKey($id) {
|
||||
return sha1($id.SECRET_SALT);
|
||||
public function saveRecord($record, $secondary = false) {
|
||||
if ($secondary || !is_a($record, 'MemcacheSessionRecord')) {
|
||||
if (!$record->isNew())
|
||||
return true;
|
||||
}
|
||||
return $this->writeRecord($record->getId(), $record->getData());
|
||||
}
|
||||
|
||||
function read($id) {
|
||||
$key = $this->getKey($id);
|
||||
|
||||
// Try distributed read first
|
||||
foreach ($this->servers as $S) {
|
||||
list($host, $port) = $S;
|
||||
$this->memcache->addServer($host, $port);
|
||||
}
|
||||
$data = $this->memcache->get($key);
|
||||
|
||||
// Read from other servers on failure
|
||||
if ($data === false && count($this->servers) > 1) {
|
||||
foreach ($this->servers as $S) {
|
||||
list($host, $port) = $S;
|
||||
$this->memcache->pconnect($host, $port);
|
||||
if ($data = $this->memcache->get($key))
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// No session data on record -- new session
|
||||
$this->isnew = $data === false;
|
||||
|
||||
return $data ?: '';
|
||||
public function writeRecord($id, $data) {
|
||||
return $this->set($id, $data);
|
||||
}
|
||||
|
||||
function update($id, $data) {
|
||||
if (defined('DISABLE_SESSION') && $this->isnew)
|
||||
return true;
|
||||
|
||||
$key = $this->getKey($id);
|
||||
foreach ($this->servers as $S) {
|
||||
list($host, $port) = $S;
|
||||
$this->memcache->pconnect($host, $port);
|
||||
if (!$this->memcache->replace($key, $data, 0, $this->getTTL()));
|
||||
$this->memcache->set($key, $data, 0, $this->getTTL());
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
function destroy($id) {
|
||||
$key = $this->getKey($id);
|
||||
foreach ($this->servers as $S) {
|
||||
list($host, $port) = $S;
|
||||
$this->memcache->pconnect($host, $port);
|
||||
$this->memcache->replace($key, '', 0, 1);
|
||||
$this->memcache->delete($key, 0);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function gc($maxlife) {
|
||||
// Memcache does this automatically
|
||||
public function lookupRecord($id, $autocreate = false) {
|
||||
if (!($data = $this->get($id)) && !$autocreate)
|
||||
return false;
|
||||
return new MemcacheSessionRecord([
|
||||
'id' => $id,
|
||||
'new' => $data === false,
|
||||
'data' => $data,
|
||||
], $this);
|
||||
}
|
||||
}
|
||||
|
||||
class FallbackSessionBackend {
|
||||
// Use default PHP settings, with some edits for best experience
|
||||
function __construct() {
|
||||
// FIXME: Consider extra possible security tweaks such as adjusting
|
||||
// the session.save_path
|
||||
class MemcacheSessionRecord
|
||||
implements osTicket\Session\SessionRecordInterface {
|
||||
private $backend;
|
||||
private $data;
|
||||
|
||||
public function __construct(array $ht, $backend) {
|
||||
// Make sure we have an id
|
||||
if (!isset($ht['id']) || !$ht['id'])
|
||||
throw new Exception('Session id required');
|
||||
// Set backend
|
||||
$this->backend = $backend;
|
||||
// Set the data as array object
|
||||
$this->data = new ArrayObject($ht, ArrayObject::ARRAY_AS_PROPS);
|
||||
}
|
||||
|
||||
public function isNew() {
|
||||
return ($this->data->new);
|
||||
}
|
||||
|
||||
public function isValid() {
|
||||
return (strlen($this->getId()) == 32);
|
||||
}
|
||||
|
||||
public function getId() {
|
||||
return $this->data->id;
|
||||
}
|
||||
|
||||
public function setId(string $id) {
|
||||
$this->data->id = $id;
|
||||
}
|
||||
|
||||
public function getData() {
|
||||
return $this->data->data;
|
||||
}
|
||||
|
||||
public function setData(?string $data = null) {
|
||||
$this->data->data = $data;
|
||||
}
|
||||
|
||||
public function setTTl(int $ttl) {
|
||||
$this->data->ttl = $ttl;
|
||||
}
|
||||
|
||||
// Unsupported call but required
|
||||
public function expire(int $ttl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function commit() {
|
||||
return $this->backend
|
||||
? $this->backend->saveRecord($this)
|
||||
: false;
|
||||
}
|
||||
|
||||
public function toArray() {
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'data' => $this->getData(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function create($vars) {
|
||||
return new self($vars);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
@@ -234,10 +234,7 @@ class Page extends VerySimpleModel {
|
||||
try {
|
||||
return self::objects()->filter(array('type'=>$type))->one();
|
||||
}
|
||||
catch (DoesNotExist $ex) {
|
||||
return null;
|
||||
}
|
||||
catch (InconsistentModelException $ex) {
|
||||
catch (DoesNotExist | InconsistentModelException $ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,29 +26,32 @@ class mPDFWithLocalImages extends Mpdf {
|
||||
$self = $this;
|
||||
$images = $cids = array();
|
||||
// Try and get information for all the files in one query
|
||||
if (preg_match_all('/"cid:([\w._-]{32})"/', $html, $cids)) {
|
||||
if (preg_match_all('/\bsrc\s*=\s*(["\'])\s*cid:\s*([\w._-]{32})\s*\1/i', $html, $cids)) {
|
||||
foreach (AttachmentFile::objects()
|
||||
->filter(array('key__in' => $cids[1]))
|
||||
->filter(array('key__in' => $cids[2]))
|
||||
as $file
|
||||
) {
|
||||
$images[strtolower($file->getKey())] = $file;
|
||||
}
|
||||
}
|
||||
$args[0] = preg_replace_callback('/"cid:([\w.-]{32})"/',
|
||||
$args[0] = preg_replace_callback('/\bsrc\s*=\s*(["\'])\s*cid:\s*([\w._-]{32})\s*\1/i',
|
||||
function($match) use ($self, $images, &$filenumber) {
|
||||
if (!($file = @$images[strtolower($match[1])]))
|
||||
return $match[0];
|
||||
if (!($file = @$images[strtolower($match[2])]))
|
||||
return $match[0]; // leave unchanged if not resolvable
|
||||
$key = "__attached_file_".$filenumber++;
|
||||
$self->imageVars[$key] = $file->getData();
|
||||
return 'var:'.$key;
|
||||
// Preserve attribute formatting: src="var:..."
|
||||
return 'src='.$match[1].'var:'.$key.$match[1];
|
||||
},
|
||||
$html
|
||||
);
|
||||
// unregister phar stream to mitigate vulnerability in mpdf library
|
||||
@stream_wrapper_unregister('phar');
|
||||
@stream_wrapper_unregister('php');
|
||||
call_user_func_array(array('parent', 'WriteHtml'), $args);
|
||||
// restore phar stream
|
||||
@stream_wrapper_restore('phar');
|
||||
@stream_wrapper_restore('php');
|
||||
}
|
||||
|
||||
function output($name = '', $dest = '') {
|
||||
@@ -74,7 +77,7 @@ class Ticket2PDF extends mPDFWithLocalImages
|
||||
$this->includenotes = $notes;
|
||||
$this->includeevents = $events;
|
||||
|
||||
parent::__construct(['mode' => 'utf-8', 'format' => $psize, 'tempDir'=>sys_get_temp_dir()]);
|
||||
parent::__construct(['mode' => 'utf-8', 'format' => $psize, 'tempDir'=>sys_get_temp_dir(), 'autoLangToFont' => true, 'autoScriptToLang' => true]);
|
||||
|
||||
$this->_print();
|
||||
}
|
||||
@@ -98,7 +101,7 @@ class Ticket2PDF extends mPDFWithLocalImages
|
||||
return;
|
||||
$html = ob_get_clean();
|
||||
|
||||
$this->autoScriptToLang;
|
||||
$this->autoScriptToLang = true;
|
||||
$this->WriteHtml($html, 0, true, true);
|
||||
}
|
||||
}
|
||||
@@ -115,7 +118,7 @@ class Task2PDF extends mPDFWithLocalImages {
|
||||
$this->task = $task;
|
||||
$this->options = $options;
|
||||
|
||||
parent::__construct(['mode' => 'utf-8', 'format' => $this->options['psize'], 'tempDir'=>sys_get_temp_dir()]);
|
||||
parent::__construct(['mode' => 'utf-8', 'format' => $this->options['psize'], 'tempDir'=>sys_get_temp_dir(), 'autoLangToFont' => true, 'autoScriptToLang' => true]);
|
||||
$this->_print();
|
||||
}
|
||||
|
||||
@@ -128,7 +131,7 @@ class Task2PDF extends mPDFWithLocalImages {
|
||||
ob_start();
|
||||
include STAFFINC_DIR.'templates/task-print.tmpl.php';
|
||||
$html = ob_get_clean();
|
||||
$this->autoScriptToLang;
|
||||
$this->autoScriptToLang = true;
|
||||
$this->WriteHtml($html, 0, true, true);
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ abstract class PluginConfig extends Config {
|
||||
foreach ($this->getOptions() as $name => $field) {
|
||||
if ($this->exists($name)) {
|
||||
$val = $this->get($name);
|
||||
$this->_config[$name] = $field->to_php($val) ?: $val;
|
||||
$this->_config[$name] = $field->to_php($val);
|
||||
} elseif (($default = $field->get('default')))
|
||||
$this->_config[$name] = $default;
|
||||
}
|
||||
@@ -50,6 +50,17 @@ abstract class PluginConfig extends Config {
|
||||
return array();
|
||||
}
|
||||
|
||||
// getFields is same as getOptions but can be used downstream to filter
|
||||
// or modify fields based on settings
|
||||
function getFields() {
|
||||
return $this->getOptions();
|
||||
}
|
||||
|
||||
// Returns form options like title and notices as accepted by SimpleForm
|
||||
function getFormOptions() {
|
||||
return array();
|
||||
}
|
||||
|
||||
function getInfo() {
|
||||
return array_merge(parent::getInfo(), $this->_config);
|
||||
}
|
||||
@@ -58,7 +69,7 @@ abstract class PluginConfig extends Config {
|
||||
return $this->instance;
|
||||
}
|
||||
|
||||
function setInstance(PluginInstance $i=null) {
|
||||
function setInstance(?PluginInstance $i=null) {
|
||||
$this->instance = $i;
|
||||
}
|
||||
|
||||
@@ -72,7 +83,8 @@ abstract class PluginConfig extends Config {
|
||||
*/
|
||||
function getForm($vars=[]) {
|
||||
if (!isset($this->form)) {
|
||||
$this->form = new SimpleForm($this->getOptions());
|
||||
$this->form = new SimpleForm($this->getFields(),
|
||||
null, $this->getFormOptions());
|
||||
// set data if any
|
||||
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
|
||||
// defaults + current info
|
||||
@@ -104,17 +116,22 @@ abstract class PluginConfig extends Config {
|
||||
* returns (true | false)
|
||||
*
|
||||
*/
|
||||
function store(SimpleForm $form = null, &$errors=array()) {
|
||||
function store(?SimpleForm $form = null, &$errors=array()) {
|
||||
|
||||
if ($this->hasCustomConfig())
|
||||
return $this->saveConfig($form, $errors);
|
||||
|
||||
$form = $form ?: $this->getForm();
|
||||
if (($data=$form->to_db())
|
||||
&& $this->pre_save($data, $errors)
|
||||
&& count($errors) === 0)
|
||||
return $this->updateAll($data);
|
||||
try {
|
||||
if ($this->hasCustomConfig())
|
||||
return $this->saveConfig($form, $errors);
|
||||
|
||||
$form = $form ?: $this->getForm();
|
||||
if (($clean=$form->getClean())
|
||||
&& $this->pre_save($clean, $errors)
|
||||
&& count($errors) === 0
|
||||
&& ($data=$form->to_db($clean)))
|
||||
return $this->updateAll($data);
|
||||
} catch (Throwable $t) {
|
||||
if (!isset($errors['err']))
|
||||
$errors['err'] = $t->getMessage();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -351,6 +368,8 @@ class PluginManager {
|
||||
* registered in the plugin registry -- the %plugin table.
|
||||
*/
|
||||
function install($path) {
|
||||
// Fixup $path to ensure we are only installing from the plugin dir
|
||||
$path = 'plugins/' . basename($path);
|
||||
$is_phar = substr($path, strlen($path) - 5) == '.phar';
|
||||
if (!($info = $this->getInfoForPath(INCLUDE_DIR . $path, $is_phar)))
|
||||
return false;
|
||||
@@ -366,6 +385,8 @@ class PluginManager {
|
||||
];
|
||||
$p = Plugin::create($vars);
|
||||
if ($p->save(true)) {
|
||||
// TODO: Trigger enable() for the plugin (for now)
|
||||
$p->getImpl()->enable();
|
||||
static::clearCache();
|
||||
return $p;
|
||||
}
|
||||
@@ -562,10 +583,12 @@ class Plugin extends VerySimpleModel {
|
||||
function __onload() {
|
||||
$this->info = PluginManager::getInfoForPath(INCLUDE_DIR.$this->ht['install_path'],
|
||||
$this->isPhar());
|
||||
// Auto upgrade / downgrade plugins on-load - if we have version
|
||||
// mismatch. See upgrade routine for more information
|
||||
if ($this->info['version'] && $this->getVersion() != $this->info['verion'])
|
||||
$this->upgrade();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* useModalConfig
|
||||
*
|
||||
@@ -578,6 +601,72 @@ class Plugin extends VerySimpleModel {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* canAddInstance
|
||||
*
|
||||
* Multi-instance plugins can add unlimited instances, otherwise only
|
||||
* one plugin instance is allowed.
|
||||
*
|
||||
*/
|
||||
function canAddInstance() {
|
||||
|
||||
// No instances yet
|
||||
if (!$this->getNumInstances())
|
||||
return true;
|
||||
|
||||
// We have at least one instance already.
|
||||
if (!$this->isMultiInstance())
|
||||
return false;
|
||||
|
||||
// Some Plugins DO Not or SHOULDN'T support multiple instances due
|
||||
// design issues or simply because of the fact that it doesn't make
|
||||
// sense to do so.
|
||||
|
||||
// TODO: The Black List is to be removed down the road once plugins
|
||||
// are forced to declare core osTicket version and if it supports
|
||||
// multiple instances.
|
||||
$blackList =[
|
||||
// 2FA Plugin up to v0.3 cannot handle multiple instances
|
||||
'Auth2FAPlugin' => 0.3,
|
||||
// It doesn't make sense for Audit Plugin to have
|
||||
// multiple instances
|
||||
'AuditPlugin' => '*',
|
||||
// File storage plugins don't currently support multiple instances
|
||||
'S3StoragePlugin' => '*',
|
||||
'FsStoragePlugin' => '*',
|
||||
];
|
||||
foreach ($blackList as $c => $v) {
|
||||
if (is_a($this, $c) && ($v == '*' || $this->getVersion() <= $v))
|
||||
return false;
|
||||
}
|
||||
// Yes, let's make instances - Genesis 9:7
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Get Namespace of the instance otherwise return plugin's namespace
|
||||
*
|
||||
*/
|
||||
function getNamespace() {
|
||||
if (($c=$this->getConfig()) && ($i=$c->getInstance()))
|
||||
return $i->getNamespace();
|
||||
|
||||
return sprintf('plugin.%d', $this->getId());
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
* isMultiInstance
|
||||
*
|
||||
* Indicates if the plugin supports multiple instances
|
||||
* Default is true unless overwritten downstream.
|
||||
*
|
||||
*/
|
||||
|
||||
function isMultiInstance() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* getNewInstanceOptions
|
||||
*
|
||||
@@ -691,14 +780,14 @@ class Plugin extends VerySimpleModel {
|
||||
return $this->config_class;
|
||||
}
|
||||
|
||||
function getConfig(PluginInstance $instance = null, $defaults = []) {
|
||||
function getConfig(?PluginInstance $instance = null, $defaults = []) {
|
||||
if ((!isset($this->config) || $instance)
|
||||
&& ($class=$this->getConfigClass())) {
|
||||
// We cache instance config for side loading purposes on
|
||||
// plugin instantace boostrapping
|
||||
if ($instance)
|
||||
// Config for an instance.
|
||||
$this->config = $instance->getConfig($class, $defaults);
|
||||
$this->config = $instance->getConfig($defaults);
|
||||
else
|
||||
// New instance config
|
||||
$this->config = new $class(null, $defaults);
|
||||
@@ -750,6 +839,35 @@ class Plugin extends VerySimpleModel {
|
||||
parent::delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* upgrade
|
||||
*
|
||||
* Plugins can implement upgrade / downgrade process downsteam as needed.
|
||||
*
|
||||
*/
|
||||
function upgrade(&$errors=[]) {
|
||||
if ($this->pre_upgrade($errors) === false)
|
||||
return false;
|
||||
|
||||
// For now we're just updating the version if we have a mismatch
|
||||
// The true version is what is packaged with the plugin
|
||||
if ($this->getVersion() != $this->info['version'])
|
||||
$this->version = $this->info['version'];
|
||||
|
||||
$this->save();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* pre_upgrade
|
||||
*
|
||||
* Hook function to veto the upgrade request. Return boolean
|
||||
* FALSE if the upgrade operation should be aborted.
|
||||
*/
|
||||
function pre_upgrade(&$errors) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* uninstall
|
||||
*
|
||||
@@ -777,6 +895,10 @@ class Plugin extends VerySimpleModel {
|
||||
return true;
|
||||
}
|
||||
|
||||
function enable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function: __
|
||||
*
|
||||
@@ -894,6 +1016,8 @@ class PluginInstance extends VerySimpleModel {
|
||||
),
|
||||
);
|
||||
|
||||
// Config class that plugin can set.
|
||||
private $config_class = null;
|
||||
// Plugin Config for the instance
|
||||
var $_config;
|
||||
var $_form;
|
||||
@@ -948,13 +1072,22 @@ class PluginInstance extends VerySimpleModel {
|
||||
$this->setFlag(self::FLAG_ENABLED, $status);
|
||||
}
|
||||
|
||||
function getConfig($class=null, $defaults=[]) {
|
||||
$class = $class ?: $this->getPlugin()->getConfigClass();
|
||||
function setConfigClass($class) {
|
||||
$this->config_class = $class;
|
||||
// Clear current config so it can be reloaded
|
||||
$this->_config = null;
|
||||
}
|
||||
|
||||
function getConfigClass() {
|
||||
return $this->config_class ?: $this->getPlugin()->getConfigClass();
|
||||
}
|
||||
|
||||
function getConfig($defaults=[]) {
|
||||
$class = $this->getConfigClass();
|
||||
if (!isset($this->_config) && $class) {
|
||||
$this->_config = new $class($this->getNamespace(), $defaults);
|
||||
$this->_config->setInstance($this);
|
||||
}
|
||||
|
||||
return $this->_config;
|
||||
}
|
||||
|
||||
@@ -966,7 +1099,12 @@ class PluginInstance extends VerySimpleModel {
|
||||
}
|
||||
|
||||
function getSignature() {
|
||||
return md5(json_encode(ksort($this->getConfiguration())));
|
||||
// get Config
|
||||
$config = $this->getConfiguration() ?: [];
|
||||
// Key sort to normalize config - key order matters with md5
|
||||
ksort($config);
|
||||
// Json encode and md5
|
||||
return md5(json_encode($config));
|
||||
}
|
||||
|
||||
function getNamespace() {
|
||||
|
||||
@@ -692,7 +692,7 @@ class CustomQueue extends VerySimpleModel {
|
||||
foreach (array(
|
||||
QueueColumn::placeholder(array(
|
||||
"id" => 1,
|
||||
"heading" => "Number",
|
||||
"heading" => __("Number"),
|
||||
"primary" => 'number',
|
||||
"width" => 85,
|
||||
"bits" => QueueColumn::FLAG_SORTABLE,
|
||||
@@ -702,7 +702,7 @@ class CustomQueue extends VerySimpleModel {
|
||||
)),
|
||||
QueueColumn::placeholder(array(
|
||||
"id" => 2,
|
||||
"heading" => "Created",
|
||||
"heading" => __("Created"),
|
||||
"primary" => 'created',
|
||||
"filter" => 'date:full',
|
||||
"truncate" =>'wrap',
|
||||
@@ -711,7 +711,7 @@ class CustomQueue extends VerySimpleModel {
|
||||
)),
|
||||
QueueColumn::placeholder(array(
|
||||
"id" => 3,
|
||||
"heading" => "Subject",
|
||||
"heading" => __("Subject"),
|
||||
"primary" => 'cdata__subject',
|
||||
"width" => 250,
|
||||
"bits" => QueueColumn::FLAG_SORTABLE,
|
||||
@@ -722,21 +722,21 @@ class CustomQueue extends VerySimpleModel {
|
||||
)),
|
||||
QueueColumn::placeholder(array(
|
||||
"id" => 4,
|
||||
"heading" => "From",
|
||||
"heading" => __("From"),
|
||||
"primary" => 'user__name',
|
||||
"width" => 150,
|
||||
"bits" => QueueColumn::FLAG_SORTABLE,
|
||||
)),
|
||||
QueueColumn::placeholder(array(
|
||||
"id" => 5,
|
||||
"heading" => "Priority",
|
||||
"heading" => __("Priority"),
|
||||
"primary" => 'cdata__priority',
|
||||
"width" => 120,
|
||||
"bits" => QueueColumn::FLAG_SORTABLE,
|
||||
)),
|
||||
QueueColumn::placeholder(array(
|
||||
"id" => 8,
|
||||
"heading" => "Assignee",
|
||||
"heading" => __("Assignee"),
|
||||
"primary" => 'assignee',
|
||||
"width" => 100,
|
||||
"bits" => QueueColumn::FLAG_SORTABLE,
|
||||
@@ -973,12 +973,13 @@ class CustomQueue extends VerySimpleModel {
|
||||
// Consider keyword searching
|
||||
if ($name === ':keywords') {
|
||||
global $ost;
|
||||
$qs = $ost->searcher->find($value, $qs, false);
|
||||
$qs = $ost->searcher->find($value, $qs, false, ['boolean' => false]);
|
||||
}
|
||||
else {
|
||||
$nullable = ($method === 'nset') ? false : null;
|
||||
// XXX: Move getOrmPath to be more of a utility
|
||||
// Ensure the special join is created to support custom data joins
|
||||
$name = @static::getOrmPath($name, $qs);
|
||||
$name = @static::getOrmPath($name, $qs, $nullable);
|
||||
|
||||
if (preg_match('/__answers!\d+__/', $name)) {
|
||||
$qs->annotate(array($name => SqlAggregate::MAX($name)));
|
||||
@@ -1185,7 +1186,7 @@ class CustomQueue extends VerySimpleModel {
|
||||
continue;
|
||||
}
|
||||
|
||||
$f->set('heading', $heading);
|
||||
$f->set('heading', isset($fields[$key]['heading']) ? $fields[$key]['heading'] : $heading);
|
||||
$f->set('sort', array_search($key, $order)+1);
|
||||
unset($new[$key]);
|
||||
}
|
||||
@@ -1466,7 +1467,7 @@ class CustomQueue extends VerySimpleModel {
|
||||
return $for_parent($pid);
|
||||
}
|
||||
|
||||
static function getOrmPath($name, $query=null) {
|
||||
static function getOrmPath($name, $query=null, $nullable=null) {
|
||||
// Special case for custom data `__answers!id__value`. Only add the
|
||||
// join and constraint on the query the first pass, when the query
|
||||
// being mangled is received.
|
||||
@@ -1481,6 +1482,10 @@ class CustomQueue extends VerySimpleModel {
|
||||
$root = $query->model;
|
||||
$meta = $root::getMeta()->getByPath($path[1]);
|
||||
$joins = $meta['joins'];
|
||||
// If the method is 'nset' (ie. IS NULL) we want to force normal
|
||||
// JOIN instead of LEFT JOIN to get proper results
|
||||
if (isset($nullable))
|
||||
$joins['answers']['null'] = $nullable;
|
||||
if (!isset($joins[$path[2]])) {
|
||||
$meta->addJoin($path[2], $joins['answers']);
|
||||
}
|
||||
@@ -1593,7 +1598,7 @@ abstract class QueueColumnAnnotation {
|
||||
}
|
||||
|
||||
function getClassName() {
|
||||
return @$this->config['c'] ?: get_class();
|
||||
return @$this->config['c'] ?: get_class($this);
|
||||
}
|
||||
|
||||
static function getAnnotations($root) {
|
||||
@@ -1840,7 +1845,7 @@ extends QueueColumnAnnotation {
|
||||
function getDecoration($row, $text) {
|
||||
$flags = $row['flags'];
|
||||
$linked = ($flags & Ticket::FLAG_LINKED) != 0;
|
||||
if ($linked && $_REQUEST['a'] == 'search')
|
||||
if ($linked)
|
||||
return '<i class="icon-link"></i>';
|
||||
}
|
||||
|
||||
|
||||
@@ -23,14 +23,21 @@ RolePermission::register(/* @trans */ 'Miscellaneous', ReportModel::getPermissio
|
||||
class OverviewReport {
|
||||
var $start;
|
||||
var $end;
|
||||
static $end_choices = [
|
||||
'now' => 'Up to today',
|
||||
'+7 days' => 'One Week',
|
||||
'+14 days' => 'Two Weeks',
|
||||
'+1 month' => 'One Month',
|
||||
'+3 months' => 'One Quarter'
|
||||
];
|
||||
|
||||
var $format;
|
||||
|
||||
function __construct($start, $end='now', $format=null) {
|
||||
global $cfg;
|
||||
|
||||
$this->start = $start;
|
||||
$this->end = $end;
|
||||
$this->start = Format::sanitize($start);
|
||||
$this->end = array_key_exists($end, self::$end_choices) ? $end : 'now';
|
||||
$this->format = $format ?: $cfg->getDateFormat(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -156,9 +156,10 @@ class Role extends RoleModel {
|
||||
}
|
||||
|
||||
function update($vars, &$errors) {
|
||||
if (!$vars['name'])
|
||||
$name = Format::sanitize($vars['name']);
|
||||
if (!$name)
|
||||
$errors['name'] = __('Name required');
|
||||
elseif (($r=Role::lookup(array('name'=>$vars['name'])))
|
||||
elseif (($r=Role::lookup(array('name'=>$name)))
|
||||
&& $r->getId() != $vars['id'])
|
||||
$errors['name'] = __('Name already in use');
|
||||
elseif (!$vars['perms'] || !count($vars['perms']))
|
||||
@@ -167,8 +168,8 @@ class Role extends RoleModel {
|
||||
if ($errors)
|
||||
return false;
|
||||
|
||||
$this->name = $vars['name'];
|
||||
$this->notes = $vars['notes'];
|
||||
$this->name = $name;
|
||||
$this->notes = Format::sanitize($vars['notes']);
|
||||
|
||||
$this->updatePerms($vars['perms'], $errors);
|
||||
|
||||
@@ -321,7 +322,7 @@ class RolePermission {
|
||||
}
|
||||
|
||||
function getInfo() {
|
||||
return $this->perms;
|
||||
return $this->perms ?: [];
|
||||
}
|
||||
|
||||
function merge($perms) {
|
||||
|
||||
@@ -955,7 +955,7 @@ class ScheduleEntry extends VerySimpleModel {
|
||||
// range at the moment - assuming ends on the same day.
|
||||
$source['ends_at'] = $ends->format('h:i a');
|
||||
if (($stops=$this->getStopsDatetime()))
|
||||
$source['stops_on'] = $stops->setTimestamp();
|
||||
$source['stops_on'] = $stops->getTimestamp();
|
||||
|
||||
// See if time spans all day.
|
||||
if ($this->isFullDay())
|
||||
@@ -1049,13 +1049,15 @@ class ScheduleEntry extends VerySimpleModel {
|
||||
}
|
||||
|
||||
static function getDays() {
|
||||
static $translated = false;
|
||||
if (!$translated) {
|
||||
foreach (static::$days as $k=>$v)
|
||||
static::$days[$k] = __($v);
|
||||
}
|
||||
return self::$days;
|
||||
}
|
||||
|
||||
return static::$days;
|
||||
static function getTranslatedDays() {
|
||||
static $translated = null;
|
||||
if (!isset($translated))
|
||||
$translated = array_map(function ($d) { return __($d); }, static::getDays());
|
||||
|
||||
return $translated;
|
||||
}
|
||||
|
||||
static function getWeeks() {
|
||||
@@ -1176,7 +1178,7 @@ extends AbstractForm {
|
||||
'default' => "",
|
||||
'layout' => new GridFluidCell(6),
|
||||
'label' => __('Day of the Week'),
|
||||
'choices' => ScheduleEntry::getDays() + array(
|
||||
'choices' => ScheduleEntry::getTranslatedDays() + array(
|
||||
'weekdays' => sprintf('%s (%s)',
|
||||
__('Weekdays'), __('Mon-Fri')),
|
||||
'weekends' => sprintf('%s (%s)',
|
||||
@@ -1212,7 +1214,7 @@ extends AbstractForm {
|
||||
'default' => "",
|
||||
'layout' => new GridFluidCell(6),
|
||||
'label' => __('Day'),
|
||||
'choices' => ScheduleEntry::getDays(),
|
||||
'choices' => ScheduleEntry::getTranslatedDays(),
|
||||
'validator-error' => __('Selection required'),
|
||||
'visibility' => new VisibilityConstraint(
|
||||
new Q(array('monthly__neq'=>'day')),
|
||||
@@ -1241,7 +1243,7 @@ extends AbstractForm {
|
||||
'default' => '',
|
||||
'layout' => new GridFluidCell(4),
|
||||
'label' => __('Day'),
|
||||
'choices' => ScheduleEntry::getDays(),
|
||||
'choices' => ScheduleEntry::getTranslatedDays(),
|
||||
'validator-error' => __('Selection required'),
|
||||
'configuration'=>array('prompt'=>__('Day')),
|
||||
'visibility' => new VisibilityConstraint(
|
||||
|
||||
@@ -44,7 +44,7 @@ abstract class SearchBackend {
|
||||
);
|
||||
|
||||
abstract function update($model, $id, $content, $new=false, $attrs=array());
|
||||
abstract function find($query, QuerySet $criteria, $addRelevance=true);
|
||||
abstract function find($query, QuerySet $criteria, $addRelevance=true, $options=array());
|
||||
|
||||
static function register($backend=false) {
|
||||
$backend = $backend ?: get_called_class();
|
||||
@@ -79,9 +79,9 @@ class SearchInterface {
|
||||
$this->bootstrap();
|
||||
}
|
||||
|
||||
function find($query, QuerySet $criteria, $addRelevance=true) {
|
||||
function find($query, QuerySet $criteria, $addRelevance=true, $options=array()) {
|
||||
$query = Format::searchable($query);
|
||||
return $this->backend->find($query, $criteria, $addRelevance);
|
||||
return $this->backend->find($query, $criteria, $addRelevance, $options);
|
||||
}
|
||||
|
||||
function update($model, $id, $content, $new=false, $attrs=array()) {
|
||||
@@ -327,7 +327,7 @@ class MysqlSearchBackend extends SearchBackend {
|
||||
return implode('', $results);
|
||||
}
|
||||
|
||||
function find($query, QuerySet $criteria, $addRelevance=true) {
|
||||
function find($query, QuerySet $criteria, $addRelevance=true, $options=array()) {
|
||||
global $thisstaff;
|
||||
|
||||
// MySQL usually doesn't handle words shorter than three letters
|
||||
@@ -339,6 +339,10 @@ class MysqlSearchBackend extends SearchBackend {
|
||||
|
||||
$mode = ' IN NATURAL LANGUAGE MODE';
|
||||
|
||||
// Allow boolean full-text syntax to be disabled
|
||||
$allow_boolean = !array_key_exists('boolean', $options)
|
||||
|| $options['boolean'];
|
||||
|
||||
// According to the MySQL full text boolean mode, this grammar is
|
||||
// assumed:
|
||||
// see http://dev.mysql.com/doc/refman/5.6/en/fulltext-boolean.html
|
||||
@@ -359,7 +363,8 @@ class MysqlSearchBackend extends SearchBackend {
|
||||
// Require the use of at least one operator and conform to the
|
||||
// boolean mode grammar
|
||||
$T = array();
|
||||
if (preg_match('`(^|\s)["()<>~+-]`u', $query, $T)
|
||||
if ($allow_boolean
|
||||
&& preg_match('`(^|\s)["()<>~+-]`u', $query, $T)
|
||||
&& preg_match("`^{$BOOLEAN}$`u", $query, $T)
|
||||
) {
|
||||
// If using boolean operators, search in boolean mode. This regex
|
||||
@@ -370,6 +375,13 @@ class MysqlSearchBackend extends SearchBackend {
|
||||
}
|
||||
#elseif (count(explode(' ', $query)) == 1)
|
||||
# $mode = ' WITH QUERY EXPANSION';
|
||||
|
||||
// Sanitize query to avoid possible SQL injection via parameter markers
|
||||
// This regex matches one or more colons followed by one or more digits,
|
||||
// and then replaces the match with only the digits (i.e. stripping the colon(s)).
|
||||
$query = preg_replace('/:+(\d+)/', '$1', $query);
|
||||
|
||||
// escape query and using it as search
|
||||
$search = 'MATCH (Z1.title, Z1.content) AGAINST ('.db_input($query).$mode.')';
|
||||
|
||||
switch ($criteria->model) {
|
||||
@@ -467,7 +479,7 @@ class MysqlSearchBackend extends SearchBackend {
|
||||
* not indexed in the _search table and add it to the index.
|
||||
*/
|
||||
function IndexOldStuff() {
|
||||
$class = get_class();
|
||||
$class = get_class($this);
|
||||
$auto_create = function($db_error) use ($class) {
|
||||
|
||||
if ($db_error != 1146)
|
||||
@@ -901,7 +913,10 @@ class SavedQueue extends CustomQueue {
|
||||
$query = $this->getQuery();
|
||||
if ($agent)
|
||||
$query = $agent->applyVisibility($query);
|
||||
$query->limit(false)->offset(false)->order_by(false);
|
||||
$query->filter(Q::any([
|
||||
'ticket_pid__isnull' => true,
|
||||
'flags__hasbit' => Ticket::FLAG_LINKED
|
||||
]))->limit(false)->offset(false)->order_by(false);
|
||||
try {
|
||||
return $query->count();
|
||||
} catch (Exception $e) {
|
||||
@@ -941,6 +956,9 @@ class SavedQueue extends CustomQueue {
|
||||
->filter(Q::any(array(
|
||||
'flags__hasbit' => CustomQueue::FLAG_QUEUE,
|
||||
'staff_id' => $agent->getId(),
|
||||
)))
|
||||
->filter(Q::not(array(
|
||||
'flags__hasbit' => CustomQueue::FLAG_DISABLED,
|
||||
)));
|
||||
|
||||
if ($criteria && is_array($criteria))
|
||||
@@ -984,7 +1002,13 @@ class SavedQueue extends CustomQueue {
|
||||
}
|
||||
|
||||
if ($Q->constraints && !$empty) {
|
||||
$expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)), new SqlField('ticket_id'));
|
||||
$constraints = $Q->constraints;
|
||||
// Add path_constraints to get the correct counts
|
||||
foreach ($Q->path_constraints as $pc) {
|
||||
if (!empty($pc[0]->constraints))
|
||||
$constraints[] = $pc[0];
|
||||
}
|
||||
$expr = SqlCase::N()->when(new SqlExpr(new Q($constraints)), new SqlField('ticket_id'));
|
||||
$query->aggregate(array(
|
||||
"q{$queue->id}" => SqlAggregate::COUNT($expr, true)
|
||||
));
|
||||
@@ -1151,6 +1175,7 @@ class AdvancedSearchSelectionField extends ChoiceField {
|
||||
switch ($method) {
|
||||
case 'includes':
|
||||
case '!includes':
|
||||
if (!$value) return;
|
||||
$Q = new Q();
|
||||
if (count($value) > 1)
|
||||
$Q->add(array("{$name}__in" => array_keys($value)));
|
||||
|
||||
578
include/class.session.php
Normal file
@@ -0,0 +1,578 @@
|
||||
<?php
|
||||
/*********************************************************************
|
||||
class.session.php
|
||||
|
||||
osTicket core Session Handlers & Backends
|
||||
|
||||
Peter Rotich <peter@osticket.com>
|
||||
Copyright (c) 2022 osTicket
|
||||
http://www.osticket.com
|
||||
|
||||
Released under the GNU General Public License WITHOUT ANY WARRANTY.
|
||||
See LICENSE.TXT for details.
|
||||
|
||||
vim: expandtab sw=4 ts=4 sts=4:
|
||||
**********************************************************************/
|
||||
|
||||
namespace osTicket\Session {
|
||||
// Extend global Exception
|
||||
class Exception extends \Exception {}
|
||||
|
||||
// Session backend factory
|
||||
// TODO: Allow plugins to register backends.
|
||||
class SessionBackend {
|
||||
static private $backends = [
|
||||
// Database is the default backend for osTicket
|
||||
'database' => ':AbstractSessionStorageBackend',
|
||||
// Session data are stored in memcache saving database the pain
|
||||
// of storing and retriving the records
|
||||
'memcache' => ':AbstractMemcacheSessionStorageBackend',
|
||||
// Noop/Null SessionHandler doesn't care about sh*t
|
||||
'noop' => 'NoopSessionStorageBackend',
|
||||
// Default SessionHandler
|
||||
'system' => 'SystemSessionHandler',
|
||||
];
|
||||
|
||||
static public function getBackends() {
|
||||
return self::$backends;
|
||||
}
|
||||
|
||||
static public function getBackend(string $bk) {
|
||||
return (($backends=self::getBackends())
|
||||
&& isset($backends[$bk]))
|
||||
? $backends[$bk]
|
||||
: null;
|
||||
}
|
||||
|
||||
static public function has(string $bk) {
|
||||
return (self::getBackend($bk));
|
||||
}
|
||||
|
||||
static public function factory(string $bk, array $options = []) {
|
||||
list ($bk, $handler, $secondary) = explode(':', $bk);
|
||||
if (!($backend=self::getBackend($bk)))
|
||||
throw new Exception('Unknown Session Backend: '.$bk);
|
||||
|
||||
if ($backend[0] == ':') {
|
||||
// External implementation required
|
||||
if (!$handler)
|
||||
throw new Exception(sprintf(
|
||||
'%s: requires storage backend driver',
|
||||
$bk));
|
||||
// External handler needs global namespacing
|
||||
$handler ="\\$handler";
|
||||
// handler must implement parent
|
||||
$impl = substr($backend, 1);
|
||||
} elseif ($handler) {
|
||||
$handler ="\\$handler";
|
||||
} else {
|
||||
// local handler is being used force this namespace
|
||||
$handler = sprintf('%s\%s', __NAMESPACE__, $backend);
|
||||
$impl = ($bk == 'system') ? 'SystemSessionHandler' : 'AbstractSessionHandler';
|
||||
}
|
||||
|
||||
// Make sure handler / backend class exits
|
||||
if (!$handler
|
||||
|| !class_exists($handler))
|
||||
throw new Exception(sprintf('unknown storage backend - [%s]',
|
||||
$handler));
|
||||
|
||||
// Set secondary backend with global namespacing if it is
|
||||
// chained to primary backend ($bk). Primary backend will
|
||||
// validate and instanciate it if it can support the interface
|
||||
// of the said backend.
|
||||
if ($secondary)
|
||||
$options['secondary'] = "\\$secondary";
|
||||
|
||||
$sessionHandler = new $handler($options);
|
||||
// Make sure the handler implements the right interface
|
||||
$impl = sprintf('%s\%s', __NAMESPACE__, $impl);
|
||||
if ($impl && !is_a($sessionHandler, $impl))
|
||||
throw new Exception(sprintf('%s: must implement %s',
|
||||
$handler, $impl));
|
||||
|
||||
return $sessionHandler;
|
||||
}
|
||||
|
||||
static public function register(string $bk, array $options = [], bool
|
||||
$register_shutdown = true) {
|
||||
if (($handler=self::factory($bk, $options))
|
||||
&& session_set_save_handler($handler,
|
||||
$register_shutdown))
|
||||
return $handler;
|
||||
}
|
||||
}
|
||||
|
||||
interface SessionRecordInterface {
|
||||
// Basic setters and getters
|
||||
function getId();
|
||||
function setId(string $id);
|
||||
function getData();
|
||||
function setData(string $data);
|
||||
function setTTL(int $ttl);
|
||||
// expire in ttl & commit
|
||||
function expire(int $ttl);
|
||||
// commit / save the record to storage
|
||||
function commit();
|
||||
// Checkers
|
||||
function isNew();
|
||||
function isValid();
|
||||
// to array [id, data] expected - other attributes can be returned
|
||||
// as well dependin on the storage backend
|
||||
function toArray();
|
||||
}
|
||||
|
||||
abstract class AbstractSessionHandler implements
|
||||
\SessionHandlerInterface,
|
||||
\SessionUpdateTimestampHandlerInterface {
|
||||
// Options
|
||||
protected $options;
|
||||
// Callback registry
|
||||
private $callbacks;
|
||||
// Flags
|
||||
private $isnew = false;
|
||||
private $isapi = false;
|
||||
// maxlife & ttl
|
||||
private $ttl;
|
||||
private $maxlife;
|
||||
// Secondary backend
|
||||
protected $secondary = null;
|
||||
|
||||
public function __construct($options = []) {
|
||||
// Set flags based on passed in options
|
||||
$this->options = $options;
|
||||
|
||||
// Default TTL
|
||||
$this->ttl = $this->options['session_ttl'] ??
|
||||
ini_get('session.gc_maxlifetime');
|
||||
|
||||
// Dynamic maxlife (MLT)
|
||||
if (isset($this->options['session_maxlife']))
|
||||
$this->maxlife = $this->options['session_maxlife'];
|
||||
|
||||
// API Session Flag
|
||||
if (isset($this->options['api_session']))
|
||||
$this->isapi = $this->options['api_session'];
|
||||
|
||||
// callbacks
|
||||
if (isset($options['callbacks'])) {
|
||||
$this->callbacks = $options['callbacks'];
|
||||
unset($options['callbacks']);
|
||||
}
|
||||
|
||||
// Set Secondary Backend if any
|
||||
if (isset($options['secondary']))
|
||||
$this->setSecondaryBackend($options['secondary']);
|
||||
}
|
||||
|
||||
/*
|
||||
* set a seconday backend... for now it's private but will be public
|
||||
* down the road.
|
||||
*/
|
||||
private function setSecondaryBackend($backend, $options = null) {
|
||||
// Ignore invalid backend
|
||||
if (!$backend
|
||||
// class exists
|
||||
|| !class_exists($backend)
|
||||
// Not same backend as handler
|
||||
|| !strcasecmp(get_class($this), $backend))
|
||||
return false;
|
||||
|
||||
// Init Secondary handler if set and valid
|
||||
$options = $options ?? $this->options;
|
||||
unset($options['secondary']); //unset secondary to avoid loop.
|
||||
$this->secondary = new $backend($options);
|
||||
// Make sure it's truly a storage backend and not a Ye!
|
||||
if (!is_a($this->secondary,
|
||||
__NAMESPACE__.'\AbstractSessionStorageBackend'))
|
||||
$this->secondary = null; // nah...
|
||||
|
||||
return ($this->secondary);
|
||||
}
|
||||
|
||||
/*
|
||||
* API Sessions are Stateless and new sessions shouldn't be created
|
||||
* when this flag is turned on.
|
||||
*
|
||||
*/
|
||||
protected function isApiSession() {
|
||||
return ($this->isapi);
|
||||
}
|
||||
|
||||
/*
|
||||
* onEvent
|
||||
*
|
||||
* Storage Backends that's interested in monitoring / reporting on
|
||||
* events can implement this routine.
|
||||
*
|
||||
*/
|
||||
protected function onEvent($event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handles callback for registered event listeners
|
||||
protected function callbackOn($event) {
|
||||
if (!$this->callbacks
|
||||
|| !isset($this->callbacks[$event]))
|
||||
return false;
|
||||
|
||||
return (($collable=$this->callbacks[$event])
|
||||
&& is_callable($collable))
|
||||
? call_user_func($collable, $this)
|
||||
: false;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* pre_save
|
||||
*
|
||||
* This is a hook called by session storage backends before saving a
|
||||
* session record.
|
||||
*
|
||||
*/
|
||||
public function pre_save(SessionRecordInterface $record) {
|
||||
// We're NOT creating new API Sessions since API is stateless.
|
||||
// However existing sessions are updated, allowing for External
|
||||
// Authentication / Authorization to be processed via API endpoints.
|
||||
if ($record->isNew() && $this->isApiSession())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Default osTicket TTL is the default Session's Maxlife
|
||||
*/
|
||||
public function getTTL() {
|
||||
return $this->ttl;
|
||||
}
|
||||
|
||||
/*
|
||||
* Maxlife is based on logged in user (if any)
|
||||
*
|
||||
*/
|
||||
public function getMaxlife() {
|
||||
// Prefer maxlife defined based on settings for the
|
||||
// current session user - otherwise use default ttl
|
||||
if (!isset($this->maxlife))
|
||||
$this->maxlife = (defined('SESSION_MAXLIFE')
|
||||
&& is_numeric(SESSION_MAXLIFE))
|
||||
? SESSION_MAXLIFE : $this->getTTL();
|
||||
|
||||
return $this->maxlife;
|
||||
}
|
||||
|
||||
public function setMaxLife(int $maxlife) {
|
||||
$this->maxlife = $maxlife;
|
||||
}
|
||||
|
||||
public function open($save_path, $session_name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function close() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function write($id, $data) {
|
||||
// Last chance session update - returns true if update is done
|
||||
if ($this->onEvent('close'))
|
||||
$data = session_encode();
|
||||
return $this->update($id, $data);
|
||||
}
|
||||
|
||||
public function validateId($id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function updateTimestamp($id, $data) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function gc($maxlife) {
|
||||
$this->cleanup();
|
||||
}
|
||||
|
||||
abstract function read($id);
|
||||
abstract function update($id, $data);
|
||||
abstract function expire($id, $ttl);
|
||||
abstract function destroy($id);
|
||||
abstract function cleanup();
|
||||
}
|
||||
|
||||
abstract class AbstractSessionStorageBackend extends AbstractSessionHandler {
|
||||
// Record we cache between read & update/write
|
||||
private $record;
|
||||
|
||||
protected function onEvent($event) {
|
||||
return $this->callbackOn($event);
|
||||
}
|
||||
|
||||
public function getRecord($id, $autocreate = false) {
|
||||
if (!isset($this->record)
|
||||
|| !is_object($this->record)
|
||||
// Mismatch here means new session id
|
||||
|| strcmp($id, $this->record->getId()))
|
||||
$this->record = static::lookupRecord($id, $autocreate, $this);
|
||||
|
||||
return $this->record;
|
||||
}
|
||||
|
||||
// This is the wrapper for to ask the backend to lookup or
|
||||
// create/init a record when not found
|
||||
protected function getRecordOrCreate($id) {
|
||||
return $this->getRecord($id, true);
|
||||
}
|
||||
|
||||
public function read($id) {
|
||||
// we're auto creating the record if it doesn't exist so we can
|
||||
// have a new cached recoed on write/update.
|
||||
return (($record = $this->getRecordOrCreate($id))
|
||||
&& !$record->isNew())
|
||||
? $record->getData()
|
||||
: '';
|
||||
}
|
||||
|
||||
public function update($id, $data) {
|
||||
if (!($record = $this->getRecord($id)))
|
||||
return false;
|
||||
|
||||
// Upstream backend can overwrite saving the record via pre_save hook
|
||||
// depending on the type of session or class of user etc.
|
||||
if ($this->pre_save($record) === false)
|
||||
return true; // record is being ignored
|
||||
|
||||
// Set id & data
|
||||
$record->setId($id);
|
||||
$record->setData($data);
|
||||
// Ask backend to save the record
|
||||
if (!$this->saveRecord($record))
|
||||
return false;
|
||||
|
||||
// See if we need to send the record to secondary backend to
|
||||
// send the record to for logging or audit reasons or whatever!
|
||||
try {
|
||||
if (isset($this->secondary))
|
||||
$this->secondary->saveRecord($record, true);
|
||||
} catch (\Trowable $t) {
|
||||
// Ignore any BS!
|
||||
}
|
||||
// clear cache
|
||||
$this->record = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function expire($id, $ttl) {
|
||||
// Destroy session record if expire is now.
|
||||
if ($ttl == 0)
|
||||
return $this->destroy($id);
|
||||
|
||||
if (!$this->expireRecord($id, $ttl))
|
||||
return false;
|
||||
|
||||
try {
|
||||
if (isset($this->secondary))
|
||||
$this->secondary->expireRecord($id, $ttl);
|
||||
} catch (\Trowable $t) {
|
||||
// Ignore any BS!
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function destroy($id) {
|
||||
if (!($this->destroyRecord($id)))
|
||||
return false;
|
||||
|
||||
try {
|
||||
if (isset($this->secondary))
|
||||
$this->secondary->destroyRecord($id);
|
||||
} catch (\Trowable $t) {
|
||||
// Ignore any BS!
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function cleanup() {
|
||||
$this->cleanupExpiredRecords();
|
||||
try {
|
||||
if (isset($this->secondary))
|
||||
$this->secondary->cleanupExpiredRecords();
|
||||
} catch (\Trowable $t) {
|
||||
// Ignore any BS!
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Backend must implement lookup method to return a record that
|
||||
// implements SessionRecordInterface.
|
||||
abstract function lookupRecord($id, $autocreate);
|
||||
// save record
|
||||
abstract function saveRecord($record, $secondary = false);
|
||||
// writeRecord is useful when replicating records without the need
|
||||
// to transcode it when backends are different
|
||||
abstract function writeRecord($id, $data);
|
||||
// expireRecord
|
||||
abstract function expireRecord($id, $ttl);
|
||||
// Backend should implement destroyRecord that takes $id to avoid
|
||||
// the need to do record lookup
|
||||
abstract function destroyRecord($id);
|
||||
// Clear expired records - backend knows best how
|
||||
abstract function cleanupExpiredRecords();
|
||||
}
|
||||
|
||||
abstract class AbstractMemcacheSessionStorageBackend
|
||||
extends AbstractSessionStorageBackend {
|
||||
private $memcache;
|
||||
private $servers = [];
|
||||
|
||||
public function __construct($options) {
|
||||
parent::__construct($options);
|
||||
// Make sure we have memcache module installed
|
||||
if (!extension_loaded('memcache'))
|
||||
throw new Exception('Memcache extension is missing');
|
||||
|
||||
// Require servers to be defined
|
||||
if (!isset($options['servers'])
|
||||
|| !is_array($options['servers'])
|
||||
|| !count($options['servers']))
|
||||
throw new Exception('Memcache severs required');
|
||||
|
||||
// Init Memchache module
|
||||
$this->memcache = new \Memcache();
|
||||
|
||||
// Add servers
|
||||
foreach ($options['servers'] as $server) {
|
||||
list($host, $port) = explode(':', $server);
|
||||
// Use port '0' for unix sockets
|
||||
if (strpos($host, '/') !== false)
|
||||
$port = 0;
|
||||
elseif (!$port)
|
||||
$port = ini_get('memcache.default_port') ?: 11211;
|
||||
$this->addServer(trim($host), (int) trim($port));
|
||||
}
|
||||
|
||||
if (!$this->getNumServers())
|
||||
throw new Exception('At least one memcache severs required');
|
||||
|
||||
// Init Memchache module
|
||||
$this->memcache = new \Memcache();
|
||||
}
|
||||
|
||||
protected function addServer($host, $port) {
|
||||
// TODO: Support add options
|
||||
// FIXME: Crash or warn if invalid $host or $port
|
||||
// Cache Servers locally
|
||||
$this->servers[] = [$host, $port];
|
||||
// Add Server
|
||||
$this->memcache->addServer($host, $port);
|
||||
}
|
||||
|
||||
protected function getNumServers() {
|
||||
return count($this->getServers());
|
||||
}
|
||||
|
||||
protected function getServers() {
|
||||
return $this->servers;
|
||||
}
|
||||
|
||||
protected function getKey($id) {
|
||||
return sha1($id.SECRET_SALT);
|
||||
}
|
||||
|
||||
protected function get($id) {
|
||||
// get key
|
||||
$key = $this->getKey($id);
|
||||
// Attempt distributed read
|
||||
$data = $this->memcache->get($key);
|
||||
// Read from other servers on failure
|
||||
if ($data === false
|
||||
&& $this->getNumServers()) {
|
||||
foreach ($this->getServers() as $server) {
|
||||
list($host, $port) = $server;
|
||||
$this->memcache->pconnect($host, $port);
|
||||
if ($data = $this->memcache->get($key))
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function set($id, $data) {
|
||||
// Since memchache takes care of carbage collection internally
|
||||
// we want to make sure we set data to expire based on the session's
|
||||
// maxidletime (if available) otherwise it defailts to SESSION_TTL.
|
||||
// Memchache has a maximum ttl of 30 days
|
||||
$ttl = min($this->getMaxlife(), 2592000);
|
||||
$key = $this->getKey($id);
|
||||
foreach ($this->getServers() as $server) {
|
||||
list($host, $port) = $server;
|
||||
$this->memcache->pconnect($host, $port);
|
||||
if (!$this->memcache->replace($key, $data, 0, $ttl))
|
||||
$this->memcache->set($key, $data, 0, $ttl);
|
||||
}
|
||||
// FIXME: Return false if we fail to write to at least one server
|
||||
return true;
|
||||
}
|
||||
|
||||
public function expireRecord($id, $ttl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function destroyRecord($id) {
|
||||
$key = $this->getKey($id);
|
||||
foreach ($this->getServers() as $server) {
|
||||
list($host, $port) = $server;
|
||||
$this->memcache->pconnect($host, $port);
|
||||
$this->memcache->replace($key, '', 0, 1);
|
||||
$this->memcache->delete($key, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function cleanupExpiredRecords() {
|
||||
// Memcache does this automatically
|
||||
return true;
|
||||
}
|
||||
|
||||
abstract function writeRecord($id, $data);
|
||||
abstract function saveRecord($record, $secondary = false);
|
||||
}
|
||||
|
||||
/*
|
||||
* NoopSessionHandler
|
||||
*
|
||||
* Use this session handler when you don't care about session data.
|
||||
*
|
||||
*/
|
||||
class NoopSessionStorageBackend extends AbstractSessionHandler {
|
||||
public function read($id) {
|
||||
return "";
|
||||
}
|
||||
|
||||
public function update($id, $data) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function expire($id, $ttl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function destroy($id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function gc($maxlife) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function cleanup() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate everything to PHP Default SessionHandler
|
||||
class SystemSessionHandler extends \SessionHandler {
|
||||
public function __construct() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@ class SetupWizard {
|
||||
|
||||
//Mimimum requirements
|
||||
static protected $prereq = array(
|
||||
'php' => '8.0',
|
||||
'mysql' => '5.0');
|
||||
'php' => '8.2',
|
||||
'mysql' => '5.5');
|
||||
|
||||
//Version info - same as the latest version.
|
||||
|
||||
|
||||
@@ -330,6 +330,15 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
function getEmailAddress() {
|
||||
$emailaddr = (string) $this->getEmail();
|
||||
if (($name=$this->getName()))
|
||||
$emailaddr = sprintf('"%s" <%s>',
|
||||
(string) $name,
|
||||
$emailaddr);
|
||||
return $emailaddr;
|
||||
}
|
||||
|
||||
function getAvatar($size=null) {
|
||||
global $cfg;
|
||||
$source = $cfg->getStaffAvatarSource();
|
||||
@@ -704,7 +713,7 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
|
||||
'child_thread__referrals__team__team_id__in' => $teams)));
|
||||
$assigned->add($childRefTeam);
|
||||
}
|
||||
$visibility = Q::any(new Q(array('status__state'=>'open', $assigned)));
|
||||
$visibility = Q::any(new Q(array('status__state__in'=>['open', 'closed'], $assigned)));
|
||||
// -- If access is limited to assigned only, return assigned
|
||||
if ($this->isAccessLimited())
|
||||
return $visibility;
|
||||
@@ -729,7 +738,7 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
|
||||
}
|
||||
|
||||
function applyVisibility($query, $exclude_archived=false) {
|
||||
return $query->filter($this->getTicketsVisibility($exclude_archived));
|
||||
return $query->filter($this->getTicketsVisibility($exclude_archived))->distinct('ticket_id');
|
||||
}
|
||||
|
||||
function applyDeptVisibility($qs) {
|
||||
@@ -1494,6 +1503,7 @@ extends AbstractForm {
|
||||
'required' => true,
|
||||
'configuration' => array(
|
||||
'classes' => 'span12',
|
||||
'length' => '128',
|
||||
),
|
||||
'visibility' => new VisibilityConstraint(
|
||||
new Q(array('welcome_email' => false)),
|
||||
@@ -1513,6 +1523,7 @@ extends AbstractForm {
|
||||
'required' => true,
|
||||
'configuration' => array(
|
||||
'classes' => 'span12',
|
||||
'length' => '128',
|
||||
),
|
||||
'visibility' => new VisibilityConstraint(
|
||||
new Q(array('welcome_email' => false)),
|
||||
@@ -1556,6 +1567,7 @@ extends AbstractForm {
|
||||
'required' => true,
|
||||
'configuration' => array(
|
||||
'autofocus' => true,
|
||||
'length' => '128',
|
||||
),
|
||||
'validator' => 'noop',
|
||||
)),
|
||||
@@ -1563,6 +1575,9 @@ extends AbstractForm {
|
||||
'label' => __('Enter a new password'),
|
||||
'placeholder' => __('New Password'),
|
||||
'required' => true,
|
||||
'configuration' => array(
|
||||
'length' => '128',
|
||||
),
|
||||
'validator' => '',
|
||||
'validators' => function($self, $v) {
|
||||
try {
|
||||
@@ -1575,6 +1590,9 @@ extends AbstractForm {
|
||||
'passwd2' => new PasswordField(array(
|
||||
'placeholder' => __('Confirm Password'),
|
||||
'required' => true,
|
||||
'configuration' => array(
|
||||
'length' => '128',
|
||||
),
|
||||
'validator' => '',
|
||||
'validators' => function($self, $v) {
|
||||
try {
|
||||
|
||||
@@ -350,23 +350,21 @@ class Task extends TaskModel implements RestrictedAccess, Threadable {
|
||||
}
|
||||
|
||||
function getLastRespondent() {
|
||||
|
||||
if (!isset($this->lastrespondent)) {
|
||||
$this->lastrespondent = Staff::objects()
|
||||
->filter(array(
|
||||
'staff_id' => static::objects()
|
||||
'staff_id' => $this->getThread()->entries
|
||||
->filter(array(
|
||||
'thread__entries__type' => 'R',
|
||||
'thread__entries__staff_id__gt' => 0
|
||||
'type' => 'R',
|
||||
'staff_id__gt' => 0
|
||||
))
|
||||
->values_flat('thread__entries__staff_id')
|
||||
->order_by('-thread__entries__id')
|
||||
->limit('1,1')
|
||||
->values_flat('staff_id')
|
||||
->order_by('-id')
|
||||
->limit('1')
|
||||
))
|
||||
->first()
|
||||
?: false;
|
||||
}
|
||||
|
||||
return $this->lastrespondent;
|
||||
}
|
||||
|
||||
|
||||
@@ -897,8 +897,8 @@ implements TemplateVariable {
|
||||
// a response to an existing thread entry
|
||||
if ($ost)
|
||||
$ost->log(LOG_ERR, _S('Email loop detected'), sprintf(
|
||||
_S('It appears as though <%s> is being used as a forwarded or fetched email account and is also being used as a user / system account. Please correct the loop or seek technical assistance.'),
|
||||
$mailinfo['email']),
|
||||
_S('It appears as though %s is being used as a forwarded or fetched email account and is also being used as a user / system account. Please correct the loop or seek technical assistance.\n\n%s'),
|
||||
$mailinfo['email'], $mailinfo['header']),
|
||||
|
||||
// This is quite intentional -- don't continue the loop
|
||||
false,
|
||||
@@ -1665,13 +1665,14 @@ implements TemplateVariable {
|
||||
|
||||
$entry = new static(array(
|
||||
'created' => SqlFunction::NOW(),
|
||||
'updated' => SqlFunction::NOW(),
|
||||
'type' => $vars['type'],
|
||||
'thread_id' => $vars['threadId'],
|
||||
'title' => Format::strip_emoticons(Format::sanitize($vars['title'], true)),
|
||||
'format' => $vars['body']->getType(),
|
||||
'staff_id' => $vars['staffId'],
|
||||
'user_id' => $vars['userId'],
|
||||
'poster' => $poster,
|
||||
'poster' => Format::sanitize($poster),
|
||||
'source' => $vars['source'],
|
||||
'flags' => $vars['flags'] ?: 0,
|
||||
));
|
||||
@@ -1756,6 +1757,10 @@ implements TemplateVariable {
|
||||
$files[$i]['inline'] = true;
|
||||
}
|
||||
foreach ($entry->normalizeFileInfo($files) as $F) {
|
||||
if (!empty($F['inline'])
|
||||
&& (!isset($F['file']) || !$F['file']->isInlineSafeImage()))
|
||||
$F['inline'] = false;
|
||||
|
||||
// Deduplicate on the `key` attribute. The key is
|
||||
// necessary for the CID rewrite below
|
||||
$attached_files[$F['key']] = $F;
|
||||
@@ -1768,6 +1773,9 @@ implements TemplateVariable {
|
||||
// discarded, only the unique hash-code (key) will be available to
|
||||
// retrieve the image later
|
||||
foreach ($attached_files as $key => $a) {
|
||||
if (empty($a['inline']))
|
||||
continue;
|
||||
|
||||
if (isset($a['cid']) && $a['cid']) {
|
||||
$body = preg_replace('/src=("|\'|\b)(?:cid:)?'
|
||||
. preg_quote($a['cid'], '/').'\1/i',
|
||||
@@ -2115,6 +2123,7 @@ class ThreadEvent extends VerySimpleModel {
|
||||
case 'timestamp':
|
||||
$timeFormat = null;
|
||||
if ($mode != self::MODE_CLIENT && $thisstaff
|
||||
&& (!isset($_REQUEST['a']) || $_REQUEST['a']!='print')
|
||||
&& !strcasecmp($thisstaff->datetime_format,
|
||||
'relative')) {
|
||||
$timeFormat = function ($timestamp) {
|
||||
@@ -2200,8 +2209,8 @@ class ThreadEvent extends VerySimpleModel {
|
||||
|
||||
$inst = self::create(array(
|
||||
'thread_type' => ObjectModel::OBJECT_TYPE_TICKET,
|
||||
'staff_id' => $staff,
|
||||
'team_id' => $ticket->getTeamId(),
|
||||
'staff_id' => $staff ?: 0,
|
||||
'team_id' => $ticket->getTeamId() ?: 0,
|
||||
'dept_id' => $ticket->getDeptId(),
|
||||
'topic_id' => $ticket->getTopicId(),
|
||||
), $user);
|
||||
@@ -2211,8 +2220,8 @@ class ThreadEvent extends VerySimpleModel {
|
||||
static function forTask($task, $state, $user=false) {
|
||||
$inst = self::create(array(
|
||||
'thread_type' => ObjectModel::OBJECT_TYPE_TASK,
|
||||
'staff_id' => $task->getStaffId(),
|
||||
'team_id' => $task->getTeamId(),
|
||||
'staff_id' => $task->getStaffId() ?: 0,
|
||||
'team_id' => $task->getTeamId() ?: 0,
|
||||
'dept_id' => $task->getDeptId(),
|
||||
), $user);
|
||||
return $inst;
|
||||
@@ -2285,7 +2294,7 @@ class Event extends VerySimpleModel {
|
||||
static function getStates($dropdown=false) {
|
||||
$names = array();
|
||||
if ($dropdown)
|
||||
$names = array(__('All'));
|
||||
$names = array('All');
|
||||
|
||||
$events = self::objects()->values_flat('name');
|
||||
foreach ($events as $val)
|
||||
|
||||
@@ -715,7 +715,7 @@ implements RestrictedAccess, Threadable, Searchable {
|
||||
if (!$lock->delete())
|
||||
return false;
|
||||
|
||||
$this->lock = null;
|
||||
$this->lock = 0;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
@@ -2725,7 +2725,7 @@ implements RestrictedAccess, Threadable, Searchable {
|
||||
&& ($msg=$tpl->getTransferAlertMsgTemplate())
|
||||
) {
|
||||
$msg = $this->replaceVars($msg->asArray(),
|
||||
array('comments' => $note, 'staff' => $thisstaff));
|
||||
array('comments' => $note ?: '', 'staff' => $thisstaff));
|
||||
// Recipients
|
||||
$recipients = array();
|
||||
// Assigned staff or team... if any
|
||||
@@ -4237,7 +4237,8 @@ implements RestrictedAccess, Threadable, Searchable {
|
||||
$errors += $form->errors();
|
||||
|
||||
if ($vars['topicId']) {
|
||||
if (($topic=Topic::lookup($vars['topicId']))
|
||||
if (is_numeric($vars['topicId'])
|
||||
&& ($topic=Topic::lookup((int) $vars['topicId']))
|
||||
&& $topic->isActive()) {
|
||||
foreach ($topic_forms as $topic_form) {
|
||||
$TF = $topic_form->getForm($vars);
|
||||
@@ -4369,7 +4370,7 @@ implements RestrictedAccess, Threadable, Searchable {
|
||||
$ticket->email_id = $vars['emailId'];
|
||||
|
||||
//Make sure the origin is staff - avoid firebug hack!
|
||||
if ($vars['duedate'] && !strcasecmp($origin,'staff'))
|
||||
if ($vars['duedate'] && in_array(strtolower($origin), ['staff', 'api']))
|
||||
$ticket->duedate = date('Y-m-d G:i',
|
||||
Misc::dbtime($vars['duedate']));
|
||||
|
||||
@@ -4650,7 +4651,6 @@ implements RestrictedAccess, Threadable, Searchable {
|
||||
// post response - if any
|
||||
$response = null;
|
||||
if ($vars['response'] && $role->hasPerm(Ticket::PERM_REPLY)) {
|
||||
$vars['response'] = $ticket->replaceVars($vars['response']);
|
||||
// $vars['cannedatachments'] contains the attachments placed on
|
||||
// the response form.
|
||||
$response = $ticket->postReply($vars, $errors, ($alert &&
|
||||
|
||||
@@ -667,12 +667,20 @@ class Translation extends gettext_reader implements Serializable {
|
||||
}
|
||||
|
||||
|
||||
// Fix PHP 8.1.x Deprecation Warnings
|
||||
// Serializable interface will be removed in PHP 9.x
|
||||
function serialize() {
|
||||
return serialize(array($this->charset, $this->encode, $this->cache_translations));
|
||||
return serialize($this->__serialize());
|
||||
}
|
||||
function unserialize($what) {
|
||||
list($this->charset, $this->encode, $this->cache_translations)
|
||||
= unserialize($what);
|
||||
$this->__unserialize(unserialize($what));
|
||||
}
|
||||
|
||||
function __serialize() {
|
||||
return array($this->charset, $this->encode, $this->cache_translations);
|
||||
}
|
||||
function __unserialize($what) {
|
||||
list($this->charset, $this->encode, $this->cache_translations) = $what;
|
||||
$this->short_circuit = ! $this->enable_cache
|
||||
= 0 < $this->cache_translations ? count($this->cache_translations) : 1;
|
||||
}
|
||||
@@ -768,7 +776,9 @@ class TextDomain {
|
||||
return;
|
||||
|
||||
// Define locale for C-libraries
|
||||
putenv('LC_ALL=' . $info['code']);
|
||||
// Check if putenv() is available - some hosts disable it
|
||||
if (function_exists('putenv'))
|
||||
putenv('LC_ALL=' . $info['code']);
|
||||
self::setLocale(LC_ALL, $info['code']);
|
||||
}
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ class StreamUpgrader extends SetupWizard {
|
||||
if($email) {
|
||||
$email->sendAlert($thisstaff->getEmail(), $subject, $error);
|
||||
} else {//no luck - try the system mail.
|
||||
Mailer::sendmail($thisstaff->getEmail(), $subject, $error,
|
||||
osTicket\Mail\Mailer::sendmail($thisstaff->getEmail(), $subject, $error,
|
||||
'"'._S('osTicket Alerts')."\" <{$thisstaff->getEmail()}>");
|
||||
}
|
||||
|
||||
|
||||
@@ -220,7 +220,11 @@ implements TemplateVariable, Searchable {
|
||||
static function fromVars($vars, $create=true, $update=false) {
|
||||
// Try and lookup by email address
|
||||
$user = static::lookupByEmail($vars['email']);
|
||||
if (!$user && $create) {
|
||||
if (!$user
|
||||
// can create user?
|
||||
&& $create
|
||||
// Make sure at least email is valid
|
||||
&& Validator::is_email($vars['email'])) {
|
||||
$name = $vars['name'];
|
||||
if (is_array($name))
|
||||
$name = implode(', ', $name);
|
||||
@@ -228,7 +232,7 @@ implements TemplateVariable, Searchable {
|
||||
list($name) = explode('@', $vars['email'], 2);
|
||||
|
||||
$user = new User(array(
|
||||
'name' => Format::htmldecode(Format::sanitize($name, false)),
|
||||
'name' => trim(Format::htmldecode(Format::sanitize($name, false))),
|
||||
'created' => new SqlFunction('NOW'),
|
||||
'updated' => new SqlFunction('NOW'),
|
||||
//XXX: Do plain create once the cause
|
||||
@@ -254,8 +258,7 @@ implements TemplateVariable, Searchable {
|
||||
$type = array('type' => 'created');
|
||||
Signal::send('object.created', $user, $type);
|
||||
Signal::send('user.created', $user);
|
||||
}
|
||||
elseif ($update) {
|
||||
} elseif ($update && $user) {
|
||||
$errors = array();
|
||||
$user->updateInfo($vars, $errors, true);
|
||||
}
|
||||
@@ -586,7 +589,7 @@ implements TemplateVariable, Searchable {
|
||||
$type = array('type' => 'edited', 'key' => 'Name');
|
||||
Signal::send('object.edited', $this, $type);
|
||||
}
|
||||
$this->name = $name;
|
||||
$this->name = trim($name);
|
||||
}
|
||||
|
||||
// Email address field
|
||||
@@ -737,6 +740,8 @@ implements TemplateVariable {
|
||||
$this->address = sprintf('"%s" <%s>',
|
||||
$this->getName(),
|
||||
$this->email);
|
||||
else
|
||||
$this->address = $this->email;
|
||||
}
|
||||
|
||||
function __toString() {
|
||||
@@ -745,7 +750,7 @@ implements TemplateVariable {
|
||||
|
||||
function getVar($what) {
|
||||
|
||||
if (!$this->_info)
|
||||
if (!isset($this->_info))
|
||||
return '';
|
||||
|
||||
switch ($what) {
|
||||
@@ -760,7 +765,11 @@ implements TemplateVariable {
|
||||
}
|
||||
|
||||
function getAddress() {
|
||||
return $this->address ?: $this->email;
|
||||
return $this->address ?: $this->getEmail();
|
||||
}
|
||||
|
||||
function getEmail() {
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
function getHost() {
|
||||
@@ -1320,7 +1329,7 @@ class UserAccount extends VerySimpleModel {
|
||||
}
|
||||
|
||||
$this->set('timezone', $vars['timezone']);
|
||||
$this->set('username', $vars['username']);
|
||||
$this->set('username', Format::sanitize($vars['username']));
|
||||
|
||||
if ($vars['passwd1']) {
|
||||
$this->setPassword($vars['passwd1']);
|
||||
@@ -1398,7 +1407,7 @@ class UserAccount extends VerySimpleModel {
|
||||
));
|
||||
|
||||
if ($vars['username'] && strcasecmp($vars['username'], $user->getEmail()))
|
||||
$account->set('username', $vars['username']);
|
||||
$account->set('username', Format::sanitize($vars['username']));
|
||||
|
||||
if ($vars['passwd1'] && !$vars['sendemail']) {
|
||||
$account->set('passwd', Passwd::hash($vars['passwd1']));
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
/*********************************************************************
|
||||
class.usersession.php
|
||||
|
||||
User (client and staff) sessions handle.
|
||||
User (client and staff) sessions manager
|
||||
|
||||
User-Space session management, not to confused with Session Storage
|
||||
Backends.
|
||||
|
||||
Peter Rotich <peter@osticket.com>
|
||||
Copyright (c) 2006-2013 osTicket
|
||||
Copyright (c) 2022 osTicket
|
||||
http://www.osticket.com
|
||||
|
||||
Released under the GNU General Public License WITHOUT ANY WARRANTY.
|
||||
@@ -52,9 +55,12 @@ class UserSession {
|
||||
}
|
||||
|
||||
function sessionToken(){
|
||||
// Please note that user-space token is not meant to be secure at all
|
||||
// we're simply encoding stuff we want to track as we refresh the
|
||||
// session.
|
||||
$time = time();
|
||||
$hash = md5($time.SESSION_SECRET.$this->userID);
|
||||
$token = "$hash:$time:".MD5($this->ip);
|
||||
$token = "$hash:$time:".MD5($this->getIP());
|
||||
return $token;
|
||||
}
|
||||
|
||||
@@ -66,35 +72,31 @@ class UserSession {
|
||||
return $expire;
|
||||
}
|
||||
|
||||
function isvalidSession($htoken,$maxidletime=0,$checkip=false){
|
||||
global $cfgi;
|
||||
function isvalidSession($htoken, $maxidletime=0, $checkip=false){
|
||||
global $cfg;
|
||||
|
||||
// Compare session ids
|
||||
if (strcmp($this->getSessionId(), session_id()))
|
||||
return false;
|
||||
|
||||
// Is the session invalidated?
|
||||
if (isset($_SESSION['KAPUT']) && $_SESSION['KAPUT'] < time())
|
||||
return (session_destroy() && false);
|
||||
|
||||
$token = rawurldecode($htoken);
|
||||
// Check if we got what we expected....
|
||||
if ($token && !strstr($token,":"))
|
||||
return false;
|
||||
|
||||
// Get the goodies
|
||||
list($hash, $expire, $ip) = explode(":",$token);
|
||||
list($hash, $expire, $ip) = explode(':', $token);
|
||||
|
||||
// Make sure the session hash is valid
|
||||
if ((md5($expire . SESSION_SECRET . $this->userID) != $hash))
|
||||
if (!hash_equals(md5($expire . SESSION_SECRET . $this->userID), $hash))
|
||||
return false;
|
||||
|
||||
// is it expired??
|
||||
if ($maxidletime && ((time()-$expire)>$maxidletime))
|
||||
if ($maxidletime && ((time()-$expire) > $maxidletime))
|
||||
return false;
|
||||
|
||||
#Make sure IP is still same ( proxy access??????)
|
||||
if ($checkip && strcmp($ip, MD5($this->ip)))
|
||||
// Make sure IP is still same - if requested
|
||||
if ($checkip && strcmp($ip, MD5($this->getIP())))
|
||||
return false;
|
||||
|
||||
$this->validated = true;
|
||||
@@ -102,54 +104,13 @@ class UserSession {
|
||||
return true;
|
||||
}
|
||||
|
||||
function regenerateSession($destroy=false) {
|
||||
// Delayed kaput time for current session
|
||||
$_SESSION['KAPUT'] = time() + 60;
|
||||
// Save the session id as old
|
||||
$old = session_id();
|
||||
// Regenerate the session without destroying data
|
||||
session_regenerate_id(false);
|
||||
// Get new session id and close
|
||||
$new = session_id();
|
||||
session_write_close();
|
||||
// Start new session
|
||||
session_id($new);
|
||||
session_start();
|
||||
$this->session_id = $new;
|
||||
// Make sure new session is not set to KAPUT
|
||||
unset($_SESSION['KAPUT']);
|
||||
// Destroy ?
|
||||
if ($destroy) {
|
||||
// Destrory old session
|
||||
$this->destroySession($old);
|
||||
// Restore new session
|
||||
session_id($new);
|
||||
session_start();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function destroySession($id) {
|
||||
// Close current session
|
||||
session_write_close();
|
||||
// Start target session
|
||||
session_id($id);
|
||||
session_start();
|
||||
// Destroy session
|
||||
session_destroy();
|
||||
session_write_close();
|
||||
return true;
|
||||
}
|
||||
|
||||
function isValid() {
|
||||
return ($this->validated);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
trait UserSessionTrait {
|
||||
// Session Object
|
||||
// User Session Object
|
||||
var $session;
|
||||
// Session Token
|
||||
var $token;
|
||||
@@ -160,20 +121,60 @@ trait UserSessionTrait {
|
||||
// User class
|
||||
var $class = '';
|
||||
|
||||
function refreshSession($force=false) {
|
||||
$time = $this->session->getLastUpdate($this->token);
|
||||
// Deadband session token updates to once / 30-seconds
|
||||
if (!$force && time() - $time < 30)
|
||||
return;
|
||||
|
||||
$this->token = $this->getSessionToken();
|
||||
osTicketSession::renewCookie($time, $this->maxidletime);
|
||||
public function getMaxIdleTime() {
|
||||
return $this->maxidletime ;
|
||||
}
|
||||
|
||||
function regenerateSession($destroy=false) {
|
||||
$this->session->regenerateSession($destroy);
|
||||
// Set cookie for the new session id.
|
||||
$this->refreshSession(true);
|
||||
function refreshSession($refreshRate=60) {
|
||||
// Check Time To Die (TTD) if any - OLD people.. I mean sessions,
|
||||
// must die! Don't fight it bro!
|
||||
if (isset($_SESSION['TTD']) && $_SESSION['TTD'] < time()) {
|
||||
error_log(sprintf('Session %s with TTD %s was used',
|
||||
session_id(), $_SESSION['TTD']));
|
||||
return (session_destroy() && false);
|
||||
}
|
||||
|
||||
// If TIME_BOMB is set and less than the current time we need to regenerate
|
||||
// session id to help mitigate session fixation attacks.
|
||||
// Only regenerate on GET to avoid invalidating data in-flight on a
|
||||
// POST request
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET'
|
||||
&& isset($_SESSION['TIME_BOMB'])
|
||||
&& ($_SESSION['TIME_BOMB'] < time())
|
||||
&& ($id=$this->regenerateSession())) {
|
||||
// unset timer and set next one based on maxlife for the user or
|
||||
// 24 hrs later
|
||||
// TODO: Make regenerate frequency configurable in 2032 /j
|
||||
// PS: Living and dying and the stories that are true Secrets to
|
||||
// a good life is knowing when you're through ~ time bomb
|
||||
$ttl = ($this->getMaxIdleTime() ?: 86400);
|
||||
$_SESSION['TIME_BOMB'] = time() + $ttl;
|
||||
// Set new id locally
|
||||
$this->session_id = $id;
|
||||
// Force cookie renewal NOW!
|
||||
$refreshRate = -1;
|
||||
}
|
||||
|
||||
// Deadband session token updates to once / 30-seconds
|
||||
$updated = $this->session->getLastUpdate($this->token);
|
||||
if ($updated + $refreshRate < time()) {
|
||||
// Renew the session token
|
||||
$this->token = $this->getSessionToken();
|
||||
// Update the expire time for the session cookie
|
||||
osTicketSession::renewCookie(time(), $this->getMaxIdleTime());
|
||||
}
|
||||
}
|
||||
|
||||
function regenerateSession(int $ttl = 120) {
|
||||
// Set TTD (Time To Die) on current session
|
||||
// If ttl is 0 then session is destroyed immediatetly
|
||||
$_SESSION['TTD'] = time() + $ttl; // now + ttl
|
||||
if (($id=osTicketSession::regenerate($ttl)))
|
||||
$this->session->session_id = $id;
|
||||
// unset TTD on the new session - new life my boy!
|
||||
unset($_SESSION['TTD']);
|
||||
return $id;
|
||||
}
|
||||
|
||||
function getSession() {
|
||||
@@ -188,6 +189,7 @@ trait UserSessionTrait {
|
||||
// Assign memory to token variable
|
||||
$this->token = &$_SESSION[':token'][$this->class];
|
||||
// Set token
|
||||
$token = $token ?: $this->token;
|
||||
$this->token = $token ?: $this->getSessionToken();
|
||||
}
|
||||
|
||||
@@ -198,7 +200,7 @@ trait UserSessionTrait {
|
||||
function isValidSession() {
|
||||
return ($this->getId()
|
||||
&& $this->session->isvalidSession($this->token,
|
||||
$this->maxidletime, $this->checkip));
|
||||
$this->getMaxIdleTime(), $this->checkip));
|
||||
}
|
||||
|
||||
abstract function isValid();
|
||||
@@ -212,7 +214,7 @@ class ClientSession extends EndUser {
|
||||
parent::__construct($user);
|
||||
$this->class ='client';
|
||||
// XXX: Change the key to user-id
|
||||
$this->session = new UserSession($user->getId());
|
||||
$this->session = new UserSession($user->getUserId());
|
||||
$this->setSessionToken();
|
||||
$this->maxidletime = $cfg->getClientTimeout();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ interface EmailContact {
|
||||
function getUserId();
|
||||
function getName();
|
||||
function getEmail();
|
||||
function getEmailAddress();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +16,7 @@ class EmailRecipient
|
||||
implements EmailContact {
|
||||
protected $contact;
|
||||
protected $type;
|
||||
protected $address;
|
||||
|
||||
function __construct(EmailContact $contact, $type='to') {
|
||||
$this->contact = $contact;
|
||||
@@ -37,6 +39,16 @@ implements EmailContact {
|
||||
return $this->contact->getEmail();
|
||||
}
|
||||
|
||||
function getEmailAddress() {
|
||||
if (!isset($this->address)) {
|
||||
$this->address = (string) $this->getEmail();
|
||||
if (($name=$this->getName()))
|
||||
$this->address = sprintf('"%s" <%s>',
|
||||
(string) $name, $this->address);
|
||||
}
|
||||
return $this->address;
|
||||
}
|
||||
|
||||
function getName() {
|
||||
return $this->contact->getName();
|
||||
}
|
||||
@@ -44,6 +56,11 @@ implements EmailContact {
|
||||
function getType() {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
function __toString() {
|
||||
return (string) $this->getEmailAddress();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class BaseList
|
||||
@@ -81,12 +98,12 @@ implements IteratorAggregate, Countable {
|
||||
}
|
||||
|
||||
// IteratorAggregate
|
||||
function getIterator() {
|
||||
function getIterator(): Traversable {
|
||||
return new ArrayIterator($this->storage);
|
||||
}
|
||||
|
||||
// Countable
|
||||
function count($mode=COUNT_NORMAL) {
|
||||
function count($mode=COUNT_NORMAL): int {
|
||||
return count($this->storage, $mode);
|
||||
}
|
||||
|
||||
@@ -178,7 +195,7 @@ implements ArrayAccess, Serializable {
|
||||
}
|
||||
|
||||
// ArrayAccess
|
||||
function offsetGet($offset) {
|
||||
function offsetGet($offset): mixed {
|
||||
if (!is_int($offset))
|
||||
throw new InvalidArgumentException('List indices should be integers');
|
||||
elseif ($offset < 0)
|
||||
@@ -187,10 +204,11 @@ implements ArrayAccess, Serializable {
|
||||
throw new OutOfBoundsException('List index out of range');
|
||||
return $this->storage[$offset];
|
||||
}
|
||||
function offsetSet($offset, $value) {
|
||||
if ($offset === null)
|
||||
return $this->storage[] = $value;
|
||||
elseif (!is_int($offset))
|
||||
function offsetSet($offset, $value): void {
|
||||
if ($offset === null) {
|
||||
$this->storage[] = $value;
|
||||
return;
|
||||
} elseif (!is_int($offset))
|
||||
throw new InvalidArgumentException('List indices should be integers');
|
||||
elseif ($offset < 0)
|
||||
$offset += count($this->storage);
|
||||
@@ -200,14 +218,14 @@ implements ArrayAccess, Serializable {
|
||||
|
||||
$this->storage[$offset] = $value;
|
||||
}
|
||||
function offsetExists($offset) {
|
||||
function offsetExists($offset): bool {
|
||||
if (!is_int($offset))
|
||||
throw new InvalidArgumentException('List indices should be integers');
|
||||
elseif ($offset < 0)
|
||||
$offset += count($this->storage);
|
||||
return isset($this->storage[$offset]);
|
||||
}
|
||||
function offsetUnset($offset) {
|
||||
function offsetUnset($offset): void {
|
||||
if (!is_int($offset))
|
||||
throw new InvalidArgumentException('List indices should be integers');
|
||||
elseif ($offset < 0)
|
||||
@@ -215,12 +233,22 @@ implements ArrayAccess, Serializable {
|
||||
unset($this->storage[$offset]);
|
||||
}
|
||||
|
||||
// Serializable
|
||||
// Fix PHP 8.1.x Deprecation Warnings
|
||||
// Serializable interface will be removed in PHP 9.x
|
||||
function serialize() {
|
||||
return serialize($this->storage);
|
||||
return serialize($this->__serialize());
|
||||
}
|
||||
|
||||
function unserialize($what) {
|
||||
$this->storage = unserialize($what);
|
||||
$this->__unserialize(unserialize($what));
|
||||
}
|
||||
|
||||
// Serializable
|
||||
function __serialize() {
|
||||
return $this->storage;
|
||||
}
|
||||
function __unserialize($what) {
|
||||
$this->storage = $what;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||