Compare commits
817 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a02cc2878e | ||
|
|
1426336439 | ||
|
|
d3263c2429 | ||
|
|
4594a539a2 | ||
|
|
cf18c3850e | ||
|
|
53f6100f2c | ||
|
|
26a98823f9 | ||
|
|
5fcc7de0a9 | ||
|
|
8814df7cfa | ||
|
|
b06e8928d2 | ||
|
|
304c0d4bca | ||
|
|
62768d2112 | ||
|
|
dedf9efe54 | ||
|
|
09aafd50b2 | ||
|
|
9669c57f1d | ||
|
|
e2de65f3f3 | ||
|
|
1794763bbb | ||
|
|
a4e9e89518 | ||
|
|
4038bbe6b5 | ||
|
|
09558dab75 | ||
|
|
0298d7f3c6 | ||
|
|
a58360926a | ||
|
|
b3f1868b42 | ||
|
|
86b30fe336 | ||
|
|
e4f471ef28 | ||
|
|
e85c3be9ff | ||
|
|
b3e64dcfb1 | ||
|
|
c0ae0ca567 | ||
|
|
5638e46fab | ||
|
|
b5b79fd298 | ||
|
|
806e094cd5 | ||
|
|
a454c0cd57 | ||
|
|
98a94c3e6b | ||
|
|
b1b24f3114 | ||
|
|
570e3b9932 | ||
|
|
fb2fd080fe | ||
|
|
6a27f13f3c | ||
|
|
382a938b14 | ||
|
|
e1abd03937 | ||
|
|
3e05686342 | ||
|
|
9aa8299720 | ||
|
|
fbc0376443 | ||
|
|
7a0f0d64e0 | ||
|
|
ca675b0815 | ||
|
|
a3e6826b2f | ||
|
|
395318b568 | ||
|
|
d6483f895c | ||
|
|
b740d55260 | ||
|
|
da62f64f34 | ||
|
|
0a9a51c73c | ||
|
|
fcc7437ffa | ||
|
|
18fc0bc54d | ||
|
|
95fe52218e | ||
|
|
fb346f2e2e | ||
|
|
b0804c6dca | ||
|
|
7442fbb522 | ||
|
|
54b9548f8c | ||
|
|
8810c48f97 | ||
|
|
4fd28b3038 | ||
|
|
c624e8e781 | ||
|
|
81bf63f8b0 | ||
|
|
5dd7a2effb | ||
|
|
8484785a17 | ||
|
|
3f5bb2841f | ||
|
|
508dda1685 | ||
|
|
9dc18b4b52 | ||
|
|
9879e29c20 | ||
|
|
59a2975cc0 | ||
|
|
cd98d8b559 | ||
|
|
305f45a03d | ||
|
|
7718d8e906 | ||
|
|
540b3cba6e | ||
|
|
b5574ea37b | ||
|
|
ac13321407 | ||
|
|
47c516ef9a | ||
|
|
20034ab376 | ||
|
|
4fb8b7f8d7 | ||
|
|
38316da137 | ||
|
|
507be36664 | ||
|
|
8e3c87ac55 | ||
|
|
d2048f59a8 | ||
|
|
119a2772ec | ||
|
|
1628567cfc | ||
|
|
f0b8e63897 | ||
|
|
6eb8bdad2e | ||
|
|
d797aacb9a | ||
|
|
e804038b4e | ||
|
|
1f154ce039 | ||
|
|
eb0be440ac | ||
|
|
cfdcdaf092 | ||
|
|
43dafd9130 | ||
|
|
87cdb65d6b | ||
|
|
0a8fe89a92 | ||
|
|
d9f96d4e03 | ||
|
|
ce607ff111 | ||
|
|
2552a2b842 | ||
|
|
01c91d2086 | ||
|
|
e136660e8d | ||
|
|
f411f8cf5a | ||
|
|
da7898446f | ||
|
|
5620c25830 | ||
|
|
fa3f6c8c63 | ||
|
|
7a2f6d1e12 | ||
|
|
eab9771102 | ||
|
|
8267be397f | ||
|
|
4e82464f48 | ||
|
|
f03810e380 | ||
|
|
fb33d672a2 | ||
|
|
311319199e | ||
|
|
17421bae24 | ||
|
|
f9f491c453 | ||
|
|
d7d7a020d3 | ||
|
|
f7548a7076 | ||
|
|
018bb1f982 | ||
|
|
916fe1102f | ||
|
|
9070732d2c | ||
|
|
4f8d998068 | ||
|
|
8b07e919ad | ||
|
|
e4b21c9797 | ||
|
|
0fb7bb3aa5 | ||
|
|
823816517e | ||
|
|
5f8a7feb2a | ||
|
|
9102717053 | ||
|
|
2cc4fb3c5c | ||
|
|
9ab1dae25e | ||
|
|
10cafafeef | ||
|
|
0f1d3fa9fc | ||
|
|
694403e0ce | ||
|
|
8274a96d7c | ||
|
|
c980d52ff0 | ||
|
|
0b6a314f01 | ||
|
|
03992b2061 | ||
|
|
44bf284fe3 | ||
|
|
1ced45029e | ||
|
|
639c08c1f3 | ||
|
|
9e0ef60e00 | ||
|
|
fd4b334d8a | ||
|
|
a8a2bded0a | ||
|
|
ca003cecb2 | ||
|
|
7ad231787b | ||
|
|
17cfded1f0 | ||
|
|
7a2f965951 | ||
|
|
22ef2b48d1 | ||
|
|
4ab35da9cf | ||
|
|
4b6171c9cd | ||
|
|
174b7dcf36 | ||
|
|
a086151b2f | ||
|
|
de4d6ed2fc | ||
|
|
f142f0e575 | ||
|
|
affa9325df | ||
|
|
56e8aa2669 | ||
|
|
1839a43359 | ||
|
|
64d21bcecf | ||
|
|
2db9233ba1 | ||
|
|
d154d31ec2 | ||
|
|
9fe476b742 | ||
|
|
f91396f833 | ||
|
|
beabd14cae | ||
|
|
a2d81deff9 | ||
|
|
0e83d360b5 | ||
|
|
f05fe74f4e | ||
|
|
cc87962474 | ||
|
|
10c32901d8 | ||
|
|
d1dffe8f60 | ||
|
|
04ec0b09fe | ||
|
|
3651fcdcc2 | ||
|
|
feda4104b0 | ||
|
|
8bf8c929fb | ||
|
|
7f824c685e | ||
|
|
7f2f83d32e | ||
|
|
b13e0fefb0 | ||
|
|
dbeba27553 | ||
|
|
90cd9061b4 | ||
|
|
7991cb39a5 | ||
|
|
31c72b8092 | ||
|
|
5e56a473e7 | ||
|
|
497e481d42 | ||
|
|
ade91429e8 | ||
|
|
b9effba813 | ||
|
|
0b1d72d68e | ||
|
|
6c5ad6997d | ||
|
|
4df0c3196f | ||
|
|
b8f82a15a9 | ||
|
|
792cb95718 | ||
|
|
cebd754983 | ||
|
|
e69a4105a1 | ||
|
|
1ff8dce33c | ||
|
|
5b74800cb4 | ||
|
|
199bfc5dca | ||
|
|
5ea7d041b2 | ||
|
|
ad183694bf | ||
|
|
d77fa741c8 | ||
|
|
ec4cf9a457 | ||
|
|
f7e20304b2 | ||
|
|
f51651e45a | ||
|
|
d179d96bde | ||
|
|
c7498f288e | ||
|
|
f691fe2d7f | ||
|
|
6f1c286af3 | ||
|
|
b43e300b7c | ||
|
|
a23f64d539 | ||
|
|
dcfa6d4f9c | ||
|
|
791bc1d643 | ||
|
|
bb178de5d2 | ||
|
|
33e000af6f | ||
|
|
8c1c4c1a03 | ||
|
|
35b628376b | ||
|
|
79bc595e14 | ||
|
|
d9ada9c6ec | ||
|
|
ee361fd95a | ||
|
|
3d517b4434 | ||
|
|
d95ab52c4d | ||
|
|
4d889e31e4 | ||
|
|
dabc022c4f | ||
|
|
3c709fa3ba | ||
|
|
95c2a1a2f4 | ||
|
|
bab2fc5fb8 | ||
|
|
cf94762736 | ||
|
|
94269e1dea | ||
|
|
34b910bbba | ||
|
|
fbcd1797f0 | ||
|
|
cb0e560ed8 | ||
|
|
589ac69594 | ||
|
|
e4dfd3cb2e | ||
|
|
490475fe56 | ||
|
|
65014d72b5 | ||
|
|
2dd4260103 | ||
|
|
7c00262494 | ||
|
|
4ab9e0beb8 | ||
|
|
6608107d70 | ||
|
|
cf08353ebc | ||
|
|
5c73a68d12 | ||
|
|
ef70f04594 | ||
|
|
ec83995838 | ||
|
|
e71de79923 | ||
|
|
20188aecb1 | ||
|
|
b2abca9af1 | ||
|
|
29491f663f | ||
|
|
2d821be5e1 | ||
|
|
08b8e1cf1d | ||
|
|
e1a493acd8 | ||
|
|
c8c0db5cb9 | ||
|
|
417bf07f69 | ||
|
|
17b8eeae96 | ||
|
|
d4a92e9bb9 | ||
|
|
020b9e7c18 | ||
|
|
9b0cec6b2b | ||
|
|
736e931529 | ||
|
|
e84fd891bf | ||
|
|
888701e066 | ||
|
|
b59144e704 | ||
|
|
eae50903f2 | ||
|
|
66e19c9b2c | ||
|
|
dcdb7e68cf | ||
|
|
a363fb264c | ||
|
|
877813cb73 | ||
|
|
0ced99564d | ||
|
|
d6db40d563 | ||
|
|
4591dec171 | ||
|
|
d8b7723a7c | ||
|
|
c7576c52e4 | ||
|
|
678cd45198 | ||
|
|
4b1afadf92 | ||
|
|
0422b29321 | ||
|
|
9989bf02d3 | ||
|
|
c35b7ff3c0 | ||
|
|
38e22ef69c | ||
|
|
d2b7a87bbb | ||
|
|
3208a8c1d9 | ||
|
|
ab8309bdab | ||
|
|
12584db7b5 | ||
|
|
6dac6eb1df | ||
|
|
3a33fd4628 | ||
|
|
e1b9d711d3 | ||
|
|
9e1b53d410 | ||
|
|
ac43d4628d | ||
|
|
33a7d06032 | ||
|
|
a110039125 | ||
|
|
dd654551c1 | ||
|
|
74048295fa | ||
|
|
e5cfdf58af | ||
|
|
1595be8c0d | ||
|
|
925f9f4bc8 | ||
|
|
a3f49ca25c | ||
|
|
177e54f1f2 | ||
|
|
7efa8f5a1c | ||
|
|
18a5efcc98 | ||
|
|
da99de9a44 | ||
|
|
2c8caef9a6 | ||
|
|
63b435ac42 | ||
|
|
898f1d3266 | ||
|
|
c92d52d055 | ||
|
|
486e777210 | ||
|
|
ef4bbef17e | ||
|
|
54226692a0 | ||
|
|
0d5944b95a | ||
|
|
4627ec463a | ||
|
|
ca0bc6ef2a | ||
|
|
e26f7c5e84 | ||
|
|
e2617dbbd8 | ||
|
|
185d660f8b | ||
|
|
a264424afb | ||
|
|
50f0f25e0b | ||
|
|
976b926d2d | ||
|
|
b26fb0e13a | ||
|
|
fa46d45662 | ||
|
|
c56d315eb5 | ||
|
|
222ffece76 | ||
|
|
cb8d99fe5a | ||
|
|
c771001d9f | ||
|
|
7d7f1e708e | ||
|
|
978138df1a | ||
|
|
095af77472 | ||
|
|
5eaa427f09 | ||
|
|
178ac6d19b | ||
|
|
18fb794c75 | ||
|
|
1148a35663 | ||
|
|
043b7dd5ce | ||
|
|
13bf22065f | ||
|
|
0e14e087a5 | ||
|
|
d41fac7a7f | ||
|
|
6abf8cde9e | ||
|
|
17b81de9e1 | ||
|
|
066871b1a3 | ||
|
|
2f2e010796 | ||
|
|
1ab7806356 | ||
|
|
88c00d5a02 | ||
|
|
b887113b50 | ||
|
|
51dbe56f3f | ||
|
|
f4fea342ed | ||
|
|
01bfd5c971 | ||
|
|
2ccc2cbd66 | ||
|
|
143d96af6b | ||
|
|
93391f826c | ||
|
|
0790a5114f | ||
|
|
11058803c7 | ||
|
|
df0ab1add9 | ||
|
|
b564d20627 | ||
|
|
3562298031 | ||
|
|
7706d52e63 | ||
|
|
ff2e92f1ff | ||
|
|
4b2f385e43 | ||
|
|
a15c29bea0 | ||
|
|
a0594bf6d9 | ||
|
|
6e0a213003 | ||
|
|
8024c72db5 | ||
|
|
ecc9ea9ab9 | ||
|
|
b3dbad9a6e | ||
|
|
ccdbd265c8 | ||
|
|
a0659a8971 | ||
|
|
ef4d37f308 | ||
|
|
a49fd0ad33 | ||
|
|
190d2daece | ||
|
|
bfbfeb2e66 | ||
|
|
a11df91aa4 | ||
|
|
4d51348487 | ||
|
|
0e7e1a54f4 | ||
|
|
b7938df35b | ||
|
|
b622bbb284 | ||
|
|
009c51e245 | ||
|
|
21fe296303 | ||
|
|
2a0200f46d | ||
|
|
f5971fb24f | ||
|
|
76c550ff99 | ||
|
|
2d1fec3cb8 | ||
|
|
097f10ec18 | ||
|
|
b63bad55ac | ||
|
|
94c1c01480 | ||
|
|
21053200f0 | ||
|
|
8d65bdf0bf | ||
|
|
e2e3682ca8 | ||
|
|
fc7f4bfc35 | ||
|
|
e2360f2146 | ||
|
|
910edcaea2 | ||
|
|
ffcce029f5 | ||
|
|
b6987ddb90 | ||
|
|
cbeffb850d | ||
|
|
bbcba6e266 | ||
|
|
9a6429ea97 | ||
|
|
52e5ecf905 | ||
|
|
2912422a31 | ||
|
|
e17da2b6c0 | ||
|
|
b276cea24a | ||
|
|
e7ec7e7364 | ||
|
|
46add74e4f | ||
|
|
4e8cd07303 | ||
|
|
5db8906a86 | ||
|
|
927afe44c7 | ||
|
|
30b07a82c7 | ||
|
|
c80ce3d4f8 | ||
|
|
980035fecc | ||
|
|
6af389655c | ||
|
|
eb5c8fac12 | ||
|
|
3f90a9bad3 | ||
|
|
0280e4f30d | ||
|
|
b92e502fb0 | ||
|
|
8cb8a180b7 | ||
|
|
5c9ab337ea | ||
|
|
f6e18818cd | ||
|
|
880205fc59 | ||
|
|
c073511378 | ||
|
|
275454d479 | ||
|
|
a96b31fb99 | ||
|
|
a19bfd421c | ||
|
|
fbae84ba3c | ||
|
|
e63904a53b | ||
|
|
5a989ec257 | ||
|
|
b90f3d7204 | ||
|
|
03c234edec | ||
|
|
af2b1254e8 | ||
|
|
f218fe17d0 | ||
|
|
a2979a4406 | ||
|
|
e4dd3b4ad1 | ||
|
|
aa713a3a57 | ||
|
|
adc9921108 | ||
|
|
f326b14776 | ||
|
|
afaf1f53ab | ||
|
|
23bb50eb86 | ||
|
|
39824c876d | ||
|
|
ca1a7d0900 | ||
|
|
351eff9b80 | ||
|
|
e037011c55 | ||
|
|
c0ce07a2b9 | ||
|
|
94f11a9155 | ||
|
|
dff234ffbe | ||
|
|
223b4d12dc | ||
|
|
b33fb5e1d3 | ||
|
|
3511e7acb2 | ||
|
|
be4a1234c3 | ||
|
|
823014d4e9 | ||
|
|
310d3290fe | ||
|
|
07765cf8ff | ||
|
|
c4a6585cc7 | ||
|
|
d99d991650 | ||
|
|
6e8682e693 | ||
|
|
87eab76574 | ||
|
|
a011948149 | ||
|
|
b9391d87dc | ||
|
|
7e7f0a0d10 | ||
|
|
8e62d70986 | ||
|
|
02e5fa8535 | ||
|
|
2dc8d061e1 | ||
|
|
efca2acbe2 | ||
|
|
029e5c8a1f | ||
|
|
7383b95864 | ||
|
|
5be99f03f3 | ||
|
|
da522d8d58 | ||
|
|
60ac03df04 | ||
|
|
5a22011fe6 | ||
|
|
09f4908def | ||
|
|
8172db9cdb | ||
|
|
05e7e1e739 | ||
|
|
b2c72775dd | ||
|
|
9eb7457097 | ||
|
|
0b7d1dc06f | ||
|
|
2f9d1bf92e | ||
|
|
de405fe60f | ||
|
|
9bb3ee9bc1 | ||
|
|
5ab6a42808 | ||
|
|
9b5286efca | ||
|
|
78e1067c07 | ||
|
|
c19c4ee217 | ||
|
|
5ef26eaf32 | ||
|
|
16843c9d6a | ||
|
|
a21f5c9d53 | ||
|
|
97cea6cdef | ||
|
|
12c6552259 | ||
|
|
04aaeb4cc1 | ||
|
|
7c3e57aa57 | ||
|
|
f90ba3d6d6 | ||
|
|
4fe2a9333e | ||
|
|
bd59fa5ef5 | ||
|
|
989c729834 | ||
|
|
a57a424067 | ||
|
|
755e4f0cd7 | ||
|
|
a2a0539316 | ||
|
|
e8975d04d6 | ||
|
|
d6eac44275 | ||
|
|
ea39e24727 | ||
|
|
5a290b3ad3 | ||
|
|
ce523c51e7 | ||
|
|
5bcde678d1 | ||
|
|
98e74e53f2 | ||
|
|
bde2da820c | ||
|
|
4b2dd40d99 | ||
|
|
4d7f8d1f50 | ||
|
|
8510ee8224 | ||
|
|
7e8e514e7b | ||
|
|
fb3b0d3a49 | ||
|
|
aaacc6d3fa | ||
|
|
3c199f697c | ||
|
|
5c01c3793c | ||
|
|
0ff901ef32 | ||
|
|
08a5d5f6ff | ||
|
|
1fd00def95 | ||
|
|
21a73fa5e6 | ||
|
|
c9b2c3cc36 | ||
|
|
4ee876c134 | ||
|
|
c1c713ef15 | ||
|
|
b926cd5cf2 | ||
|
|
325343c2cf | ||
|
|
d9502ff348 | ||
|
|
585f8f49a7 | ||
|
|
505db999ad | ||
|
|
d402dd5164 | ||
|
|
a93cc2a08f | ||
|
|
0ac8685c63 | ||
|
|
754b2fc40c | ||
|
|
c32a68ed93 | ||
|
|
c269311ef5 | ||
|
|
04c28a9637 | ||
|
|
6830a78e23 | ||
|
|
3a4921344b | ||
|
|
063b77f3df | ||
|
|
61e5f1fde5 | ||
|
|
4f253bfab1 | ||
|
|
c88ca44ff1 | ||
|
|
2d871f2759 | ||
|
|
2a0bac500c | ||
|
|
cedf4de2ba | ||
|
|
d9e1a7ae98 | ||
|
|
80118d579d | ||
|
|
0299c8be99 | ||
|
|
70a7ec54ea | ||
|
|
4876d37287 | ||
|
|
9894ca78f0 | ||
|
|
99a4b4b4e1 | ||
|
|
1169f5bae9 | ||
|
|
a59619c153 | ||
|
|
c4932c4c8e | ||
|
|
cb31699a6e | ||
|
|
41e6e10241 | ||
|
|
ce5c85f592 | ||
|
|
2614514bc6 | ||
|
|
a50c261f13 | ||
|
|
b474193f97 | ||
|
|
27a4f2e8ff | ||
|
|
17024f814a | ||
|
|
d18cc23149 | ||
|
|
047111ffbb | ||
|
|
8c5dbb4197 | ||
|
|
7a6cbab8af | ||
|
|
add6f179a4 | ||
|
|
29d8c41395 | ||
|
|
0ecfa248ee | ||
|
|
748c7c5de1 | ||
|
|
2b5be510b7 | ||
|
|
99ef4e6899 | ||
|
|
59b86a882e | ||
|
|
a6a729189c | ||
|
|
7829e9a3ba | ||
|
|
a2e27f3ced | ||
|
|
9175ce3646 | ||
|
|
0dbdf401eb | ||
|
|
db84078093 | ||
|
|
a0ab8069d2 | ||
|
|
ea068dc23e | ||
|
|
26756cc687 | ||
|
|
171a4e3b48 | ||
|
|
bfd5aef4d9 | ||
|
|
b3c2a48eec | ||
|
|
848cc9e43f | ||
|
|
f61230ff35 | ||
|
|
268740a142 | ||
|
|
abf8d32ff5 | ||
|
|
b769aec7e8 | ||
|
|
84832f011a | ||
|
|
b1dc766ffb | ||
|
|
3274b35468 | ||
|
|
ad76e2d3ce | ||
|
|
aec78d147a | ||
|
|
9333111a99 | ||
|
|
1f0d192fd5 | ||
|
|
21f04259cb | ||
|
|
a62eb20f26 | ||
|
|
b813a4af9d | ||
|
|
09ca411903 | ||
|
|
59c256f8cc | ||
|
|
c0e2a8a6fb | ||
|
|
c15278d3c6 | ||
|
|
7cf3c67432 | ||
|
|
1ef3796709 | ||
|
|
d3527f2aee | ||
|
|
31b3dff98e | ||
|
|
1b9cf02f1a | ||
|
|
a9963c2f43 | ||
|
|
b9bdf92065 | ||
|
|
b66b8f1880 | ||
|
|
356b211754 | ||
|
|
833389c1fc | ||
|
|
f5e30cdff3 | ||
|
|
50f6cd743a | ||
|
|
c4c6e861b5 | ||
|
|
cac293b75b | ||
|
|
2c39ba9d9e | ||
|
|
a1602fa57a | ||
|
|
f6f1adc9f4 | ||
|
|
9fbbba817e | ||
|
|
d973a8a2ca | ||
|
|
70cc044e57 | ||
|
|
f7498e5ba2 | ||
|
|
e103599984 | ||
|
|
40abe0b311 | ||
|
|
8b9e514ce9 | ||
|
|
582f3a6ae4 | ||
|
|
5589c0a80f | ||
|
|
71c67ad637 | ||
|
|
1c7aff0d5b | ||
|
|
9b359abcce | ||
|
|
67de1109fe | ||
|
|
3489567cde | ||
|
|
d69e6c3501 | ||
|
|
01dec5160e | ||
|
|
90c80c9946 | ||
|
|
0b9ca610b7 | ||
|
|
2a2782bc95 | ||
|
|
3b6b02ed19 | ||
|
|
443ae7f40b | ||
|
|
4c91ac44e1 | ||
|
|
42dc98fa45 | ||
|
|
e88bec86fb | ||
|
|
20c8ef7383 | ||
|
|
ff09db6244 | ||
|
|
c15a00d603 | ||
|
|
b18cf31bdc | ||
|
|
3ee1a96bde | ||
|
|
8c470d0e4e | ||
|
|
0826102f6e | ||
|
|
2ff52a0359 | ||
|
|
8d70999498 | ||
|
|
f6a4581dad | ||
|
|
2d74977ed3 | ||
|
|
8b06724b67 | ||
|
|
c4755fa920 | ||
|
|
36f146a71d | ||
|
|
a5515d8520 | ||
|
|
33514c6e25 | ||
|
|
d8a582aaa5 | ||
|
|
a428d33964 | ||
|
|
33639a8955 | ||
|
|
4021f07aec | ||
|
|
67bd456b63 | ||
|
|
78cc5877a3 | ||
|
|
c4bc505e03 | ||
|
|
ad372590a9 | ||
|
|
2771d526be | ||
|
|
263ec4d266 | ||
|
|
f86876328e | ||
|
|
e47167b17e | ||
|
|
5c32cf69e6 | ||
|
|
fd46017c45 | ||
|
|
95d3cca660 | ||
|
|
1fcb5560f0 | ||
|
|
06b3dc27d2 | ||
|
|
d2b683368c | ||
|
|
6b397ccd3f | ||
|
|
32006fe8e1 | ||
|
|
911e157322 | ||
|
|
100b4062bf | ||
|
|
b27e2b7a1c | ||
|
|
7085009667 | ||
|
|
4b6ceab070 | ||
|
|
49af472617 | ||
|
|
d7aeb0d3b3 | ||
|
|
35f8b775f9 | ||
|
|
079df0d6a6 | ||
|
|
36442cb3a2 | ||
|
|
217cd9183e | ||
|
|
e9c2eaebce | ||
|
|
954c790827 | ||
|
|
2d2b476688 | ||
|
|
ab58217cab | ||
|
|
6f0b05c1f6 | ||
|
|
58d35098c0 | ||
|
|
44e71e58e6 | ||
|
|
0e516bc407 | ||
|
|
9fc7b9d755 | ||
|
|
e1ff8fb520 | ||
|
|
adf308ac9f | ||
|
|
7b6abe858c | ||
|
|
6937346e5a | ||
|
|
fc1a354072 | ||
|
|
82464e4129 | ||
|
|
1a126ad743 | ||
|
|
9558194bf7 | ||
|
|
8aa2f0c018 | ||
|
|
d6de310363 | ||
|
|
1fdddb25ec | ||
|
|
a78f6f5ce3 | ||
|
|
2528727d81 | ||
|
|
97bb4dd956 | ||
|
|
3c6819c9e7 | ||
|
|
bb2fc15c07 | ||
|
|
fb129df215 | ||
|
|
050ccb921e | ||
|
|
9b9aae4c0e | ||
|
|
263569b163 | ||
|
|
2702232eea | ||
|
|
8f802b4d38 | ||
|
|
00b4aabb32 | ||
|
|
8ea84dca94 | ||
|
|
bc45b779ef | ||
|
|
c72286623f | ||
|
|
1d3c97fae5 | ||
|
|
17e22b8b93 | ||
|
|
e32714ebaf | ||
|
|
acd81548e6 | ||
|
|
2d71860fa1 | ||
|
|
6930924cc1 | ||
|
|
e5ce253daf | ||
|
|
b9cfe0e300 | ||
|
|
af91c8c9e3 | ||
|
|
08e50e3643 | ||
|
|
885001ec97 | ||
|
|
b2dae09cf1 | ||
|
|
c55d94d969 | ||
|
|
7d035f5604 | ||
|
|
a5f49080a7 | ||
|
|
8a52a85ca3 | ||
|
|
2bfdc99f89 | ||
|
|
e5da621736 | ||
|
|
359f6c455c | ||
|
|
e929af5c99 | ||
|
|
191e61400d | ||
|
|
d311b40faa | ||
|
|
7c7fc41b2c | ||
|
|
5ab0f84d41 | ||
|
|
5917b190eb | ||
|
|
828c7b814c | ||
|
|
6d3f1bbee8 | ||
|
|
85954ab7cc | ||
|
|
822b2f4ae7 | ||
|
|
ed6911a8dc | ||
|
|
d8cfe15373 | ||
|
|
dc4d6d2671 | ||
|
|
eb6b30864f | ||
|
|
1618bb1266 | ||
|
|
54e5aa28bd | ||
|
|
4e72e2ea50 | ||
|
|
134608625f | ||
|
|
11fbaa6be8 | ||
|
|
377dd08c96 | ||
|
|
6a9dfe6dd1 | ||
|
|
59da4e4f99 | ||
|
|
6d917f82d7 | ||
|
|
15780467eb | ||
|
|
1a53871517 | ||
|
|
ef0af3a494 | ||
|
|
ab854befd1 | ||
|
|
3d522c4f2e | ||
|
|
7908da3721 | ||
|
|
f9a3354741 | ||
|
|
fbf4d3b355 | ||
|
|
f1839bcfbf | ||
|
|
895af2d7ad | ||
|
|
b5510da291 | ||
|
|
3083738d00 | ||
|
|
ea84fe82dd | ||
|
|
ea8b7507e4 | ||
|
|
7dfd170983 | ||
|
|
06c7e709e6 | ||
|
|
e0f10670d7 | ||
|
|
b33d2dff4a | ||
|
|
2f1b9ebe0c | ||
|
|
9b024bda31 | ||
|
|
606dcf17c9 | ||
|
|
2fb0c29705 | ||
|
|
d673a7006b | ||
|
|
fbe3a795df | ||
|
|
0c614c3f7e | ||
|
|
ce4bebb8b4 | ||
|
|
a8b4a581e4 | ||
|
|
7d5cc80898 | ||
|
|
7fa6003156 | ||
|
|
87cdff17b8 | ||
|
|
4fbe3a3340 | ||
|
|
c2a8894d9f | ||
|
|
4bb7e224d7 | ||
|
|
c24efff92f | ||
|
|
9dc73c29ff | ||
|
|
c2d599b546 | ||
|
|
574d1d1d69 | ||
|
|
f3ff95cc57 | ||
|
|
4a2f4fdadc | ||
|
|
c2bd39a65f | ||
|
|
529b141f1b | ||
|
|
de643ee51b | ||
|
|
1dd59bc2b6 | ||
|
|
8b80bd4119 | ||
|
|
0ca0ea0e93 | ||
|
|
4f15b2f273 | ||
|
|
63e2384907 | ||
|
|
e9b66ff7fd | ||
|
|
0eff48267f | ||
|
|
4c9f247cf6 | ||
|
|
5aa7552232 | ||
|
|
0dd1a7308a | ||
|
|
315aeafd24 | ||
|
|
74fd2dcf5e | ||
|
|
da66ab2fc6 | ||
|
|
4835fba6dc | ||
|
|
c28b663f56 | ||
|
|
cc2ece53ee | ||
|
|
ccc23e6e52 | ||
|
|
cee5c16aa1 | ||
|
|
84db98515d | ||
|
|
4a24df3395 | ||
|
|
f21054f17f | ||
|
|
e12824008f | ||
|
|
0c24e04b8c | ||
|
|
e66aae1209 | ||
|
|
e652a1eced | ||
|
|
bbfbd43105 | ||
|
|
62bc9643fd | ||
|
|
a36bbb2ffa | ||
|
|
c58e2403ca | ||
|
|
926327156d |
13
.github/ISSUE_TEMPLATE/general_help_request.md
vendored
13
.github/ISSUE_TEMPLATE/general_help_request.md
vendored
@@ -3,18 +3,23 @@ name: General Help Request
|
||||
about: Create a general help request
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
* If you have some error the first thing you should do is to check "Manage » Logs » App Logs"! If there are any errors there - make sure to provide them in the issue.
|
||||
* Have you read FAQ? https://github.com/freescout-helpdesk/freescout/wiki/FAQ
|
||||
* If you have "Whoops something went wrong" error see: https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide#11-troubleshooting
|
||||
* Before creating an issue try to disable all non-official modules and remove all custom code from your FreeScout and see if the issue still persists.
|
||||
* Feature requests for modules should be posted here: https://freescout.net/request-feature/
|
||||
* Docker questions should go here: https://github.com/tiredofit/docker-freescout/issues
|
||||
* Create separate issues for different topics - no need to mix all your problems into one issue.
|
||||
* Questions on issues with payments for modules should be sent by email: https://freescout.net/contact-us/
|
||||
* Don't forget that you are not the project manager. Try to be humble, polite, friendly and positive. Otherwise your account may be banned and it can't be undone.
|
||||
* Mentioning how much you've spent on modules or donated is the way to be banned.
|
||||
* Don't forget that you are not the project manager. Try to be humble, polite, friendly and positive. Otherwise your account may be banned (and it can't be undone).
|
||||
* Mentioning how much you've spent on modules is also the way to be banned.
|
||||
|
||||
Still here? Well clean this out and go ahead :)
|
||||
-->
|
||||
|
||||
PHP version:
|
||||
FreeScout version:
|
||||
FreeScout version:
|
||||
Database: MySQL / PostgreSQL
|
||||
Are you using CloudFlare: Yes / No
|
||||
Are you using non-official modules: Yes / No
|
||||
2
.github/workflows/lint-php.yml
vendored
2
.github/workflows/lint-php.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.1
|
||||
php-version: 8.3
|
||||
tools: phpcs
|
||||
|
||||
- name: Run check
|
||||
|
||||
3
.github/workflows/test-pgsql.yml
vendored
3
.github/workflows/test-pgsql.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['7.3', '7.4', '8.0', '8.1', '8.2']
|
||||
php: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -54,6 +54,7 @@ jobs:
|
||||
DB_PORT: ${{ job.services.postgres.ports[5432] }}
|
||||
|
||||
- name: Run PHP tests
|
||||
if: ${{ matrix.php < 8.4 }}
|
||||
run: php${{ matrix.php }} ./vendor/bin/phpunit
|
||||
env:
|
||||
DB_PORT: ${{ job.services.postgres.ports[5432] }}
|
||||
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['7.3', '7.4', '8.0', '8.1', '8.2']
|
||||
php: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -42,4 +42,5 @@ jobs:
|
||||
php${{ matrix.php }} artisan db:seed --force -n --database=testing
|
||||
|
||||
- name: Run PHP tests
|
||||
if: ${{ matrix.php < 8.4 }}
|
||||
run: php${{ matrix.php }} ./vendor/bin/phpunit
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -33,4 +33,5 @@ Thumbs.db
|
||||
/tools
|
||||
.well-known
|
||||
/resources/lang/module.*
|
||||
.phpunit.result.cache
|
||||
.phpunit.result.cache
|
||||
/.phpunit.cache
|
||||
31
README.md
31
README.md
@@ -2,10 +2,10 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/freescout-helpdesk/freescout/master/public/img/logo-300.png" width="180" height="180" />
|
||||
<img src="https://raw.githubusercontent.com/freescout-help-desk/freescout/master/public/img/logo-300.png" width="180" height="180" />
|
||||
<br/><br/>
|
||||
|
||||
[](https://github.com/freescout-helpdesk/freescout#requirements) [](https://hits.seeyoufarm.com)
|
||||
[](https://github.com/freescout-help-desk/freescout#requirements)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -36,9 +36,11 @@ If you want to support the project feel free to **star this repository**. It hel
|
||||
|
||||
* No limitations on the number of users, tickets, mailboxes, etc.
|
||||
* 100% Mobile-friendly.
|
||||
* Multilingual: English, Chinese, Croatian, Czech, Danish, Dutch, Finnish, French, German, Italian, Japanese, Korean, Norwegian, Persian, Polish, Portuguese, Russian, Spanish, Slovak, Swedish.
|
||||
* Multilingual: English, Chinese, Croatian, Czech, Danish, Dutch, Finnish, French, German, Hungarian, Italian, Japanese, Kazakh, Korean, Norwegian, Persian, Polish, Portuguese, Romanian, Russian, Spanish, Slovak, Swedish, Turkish.
|
||||
* Seamless email integration.
|
||||
* Supports modern Microsoft Exchange authentication.
|
||||
* Fully supports screen readers (for visually impaired).
|
||||
* Built with strong focus on [security](https://freescout.net/security).
|
||||
* Web installer & updater.
|
||||
* Starred conversations.
|
||||
* Forwarding conversations.
|
||||
@@ -57,6 +59,7 @@ If you want to support the project feel free to **star this repository**. It hel
|
||||
* Open tracking.
|
||||
* Editing threads.
|
||||
* Search.
|
||||
* And more…
|
||||
|
||||
Need anything else? Suggest features [here](https://freescout.net/request-feature/).
|
||||
|
||||
@@ -66,19 +69,21 @@ Mobile apps support the same functionality and modules as the web version of you
|
||||
|
||||
<a href="https://freescout.net/android-app/" target="_blank" rel="nofollow"><img alt="Android App" src="https://freescout-helpdesk.github.io/img/apps/android.png" width="200px" /></a> <a href="https://freescout.net/ios-app/" target="_blank" rel="nofollow"><img alt="iOS App" src="https://freescout-helpdesk.github.io/img/apps/ios.png?v=1" width="200px" /></a>
|
||||
|
||||
[MacOS Menu Bar App](https://github.com/jonalaniz/scouter)
|
||||
|
||||
## Requirements
|
||||
|
||||
FreeScout is a pure PHP/MySQL application, so it can be easily deployed even on a shared hosting.
|
||||
FreeScout is a pure PHP/MySQL application, so it can be easily deployed even on a [shared hosting](https://github.com/freescout-help-desk/freescout/wiki/Choosing-a-Server).
|
||||
|
||||
* Nginx / Apache / IIS
|
||||
* PHP 7.1 - 8.2
|
||||
* PHP 7.1 - 8.x
|
||||
* MySQL 5.0+ / MariaDB 5.0+ / PostgreSQL
|
||||
|
||||
There is no minimum system requirements (CPU / RAM) – FreeScout will run on any system.
|
||||
There are no minimum system requirements (CPU / RAM) – FreeScout will run on any system.
|
||||
|
||||
## Installation
|
||||
|
||||
[Installation Guide](https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide)
|
||||
[Installation Guide](https://github.com/freescout-help-desk/freescout/wiki/Installation-Guide)
|
||||
|
||||
Images & one-click installs:
|
||||
|
||||
@@ -86,7 +91,7 @@ Images & one-click installs:
|
||||
* [Softaculous](http://www.softaculous.com/apps/customersupport/FreeScout) (cPanel, Plesk, ISPmanager, H-Sphere, DirectAdmin, InterWorx)
|
||||
* [Fantastico](http://ff3.netenberg.com/visitors/scripts/freescout/view) (cPanel, DirectAdmin, ISP Manager, ISP Config)
|
||||
* [Cloudron](https://cloudron.io/store/net.freescout.cloudronapp.html)
|
||||
* [Ubuntu](https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide#interactive-installation-bash-script-ubuntu) (bash script)
|
||||
* [Ubuntu](https://github.com/freescout-help-desk/freescout/wiki/Installation-Guide#interactive-installation-bash-script-ubuntu) (bash script)
|
||||
|
||||
## Cloud Hosted
|
||||
|
||||
@@ -108,14 +113,14 @@ Images & one-click installs:
|
||||
|
||||
Don't miss news, updates and new modules!
|
||||
|
||||
[Email Newsletter](https://freescout.net/subscribe/) | [Facebook](https://freescout.net/facebook/) | [Twitter](https://freescout.net/twitter/) | [YouTube](https://freescout.net/youtube/) | [RSS](https://freescout.net/feed/)
|
||||
[Email Newsletter](https://freescout.net/subscribe/) | [Facebook](https://freescout.net/facebook/) | [Twitter](https://freescout.net/twitter/) | [YouTube](https://freescout.net/youtube/) | [Telegram](https://freescout.net/telegram/) | [RSS](https://freescout.net/feed/)
|
||||
|
||||
## Contributing
|
||||
|
||||
* [Support the project by leaving a feedback](https://github.com/freescout-helpdesk/freescout/issues/288)
|
||||
* [Development Guide](https://github.com/freescout-helpdesk/freescout/wiki/Development-Guide)
|
||||
* [Todo list](https://github.com/freescout-helpdesk/freescout/labels/help%20wanted)
|
||||
* [Translate](https://github.com/freescout-helpdesk/freescout/wiki/Translate)
|
||||
* [Support the project by leaving a feedback](https://github.com/freescout-help-desk/freescout/issues/288)
|
||||
* [Development Guide](https://github.com/freescout-help-desk/freescout/wiki/Development-Guide)
|
||||
* [Todo list](https://github.com/freescout-help-desk/freescout/labels/help%20wanted)
|
||||
* [Translate](https://github.com/freescout-help-desk/freescout/wiki/Translate)
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
||||
@@ -150,10 +150,14 @@ class Attachment extends Model
|
||||
|
||||
$file_dir .= $i.DIRECTORY_SEPARATOR;
|
||||
|
||||
if ($uploaded_file) {
|
||||
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name, ['disk' => self::DISK]);
|
||||
} else {
|
||||
Storage::disk(self::DISK)->put($file_path, $content);
|
||||
try {
|
||||
if ($uploaded_file) {
|
||||
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name, ['disk' => self::DISK]);
|
||||
} else {
|
||||
Storage::disk(self::DISK)->put($file_path, $content);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e, '[Attachment::saveFileToDisk()]');
|
||||
}
|
||||
|
||||
\Helper::sanitizeUploadedFileData($file_path, \Helper::getPrivateStorage(), $content);
|
||||
@@ -248,7 +252,18 @@ class Attachment extends Model
|
||||
*/
|
||||
public function url()
|
||||
{
|
||||
return Storage::url($this->getStorageFilePath()).'?id='.$this->id.'&token='.$this->getToken();
|
||||
$file_path = $this->getStorageFilePath();
|
||||
|
||||
// URL must contain only forward slashes.
|
||||
$file_path = str_replace(DIRECTORY_SEPARATOR, '/', $file_path);
|
||||
|
||||
$file_url = Storage::url($file_path);
|
||||
|
||||
// Fix percents.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3530
|
||||
$file_url = str_replace('%', '%25', $file_url);
|
||||
|
||||
return $file_url.'?id='.$this->id.'&token='.$this->getToken();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -412,9 +427,8 @@ class Attachment extends Model
|
||||
public function duplicate($thread_id = null)
|
||||
{
|
||||
$new_attachment = $this->replicate();
|
||||
if ($thread_id) {
|
||||
$new_attachment->thread_id = $thread_id;
|
||||
}
|
||||
|
||||
$new_attachment->thread_id = $thread_id;
|
||||
|
||||
$new_attachment->save();
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class CleanTmp extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
shell_exec('find '.\Helper::getTempDir().' -mtime +7 -type f -name '.\Helper::getTempFilePrefix().'* -exec rm -r -f {} \;');
|
||||
\Helper::shellExec('find '.\Helper::getTempDir().' -mtime +7 -type f -name '.\Helper::getTempFilePrefix().'* -exec rm -r -f {} \;');
|
||||
|
||||
$this->comment("Done");
|
||||
}
|
||||
|
||||
@@ -39,6 +39,13 @@ class ClearCache extends Command
|
||||
{
|
||||
$this->call('clear-compiled');
|
||||
$this->call('cache:clear');
|
||||
|
||||
// Remove files from /bootstrap/cache folder.
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4536
|
||||
$files = new \Illuminate\Filesystem\Filesystem;
|
||||
$files->delete($this->laravel->getCachedServicesPath());
|
||||
$files->delete($this->laravel->getCachedPackagesPath());
|
||||
|
||||
$this->call('view:clear');
|
||||
if ($this->option('doNotCacheConfig')) {
|
||||
$this->call('config:clear');
|
||||
@@ -50,6 +57,16 @@ class ClearCache extends Command
|
||||
opcache_invalidate(app()->getCachedConfigPath());
|
||||
}
|
||||
}
|
||||
|
||||
// In FreeScout routes caching does not increase performance
|
||||
// but it increases memory consumption and may lead to problems
|
||||
// during application updating or installing/updating modules.
|
||||
// try {
|
||||
// $this->call('route:cache');
|
||||
// } catch (\Exception $e) {
|
||||
// // Do nothing.
|
||||
// }
|
||||
|
||||
// Regenerate vars to get new data from .env
|
||||
if (!$this->option('doNotGenerateVars')) {
|
||||
$this->call('freescout:generate-vars');
|
||||
|
||||
@@ -18,12 +18,14 @@ use App\Subscription;
|
||||
use App\Thread;
|
||||
use App\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Webklex\IMAP\Client;
|
||||
//use Webklex\IMAP\Client;
|
||||
|
||||
class FetchEmails extends Command
|
||||
{
|
||||
const FWD_AS_CUSTOMER_COMMAND = '@fwd';
|
||||
|
||||
const MAX_SLEEP = 500000;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
@@ -31,7 +33,7 @@ class FetchEmails extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:fetch-emails {--days=3} {--unseen=1} {--identifier=dummy}';
|
||||
protected $signature = 'freescout:fetch-emails {--days=3} {--unseen=1} {--debug=0} {--identifier=dummy} {--mailboxes=0}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -78,11 +80,14 @@ class FetchEmails extends Command
|
||||
{
|
||||
$now = time();
|
||||
$successfully = true;
|
||||
$debug = $this->option('debug');
|
||||
Option::set('fetch_emails_last_run', $now);
|
||||
|
||||
$this->line('['.date('Y-m-d H:i:s').'] Fetching '.($this->option('unseen') ? 'UNREAD' : 'ALL').' emails for the last '.$this->option('days').' days.');
|
||||
if ($debug) {
|
||||
\Config::set('imap.options.debug', true);
|
||||
}
|
||||
|
||||
$this->extra_import = [];
|
||||
$this->line('['.date('Y-m-d H:i:s').'] Fetching '.($this->option('unseen') ? 'UNREAD' : 'ALL').' emails for the last '.$this->option('days').' days.');
|
||||
|
||||
if (Mailbox::getInProtocols() === Mailbox::$in_protocols) {
|
||||
$this->mailboxes = Mailbox::get();
|
||||
@@ -97,39 +102,84 @@ class FetchEmails extends Command
|
||||
// Microseconds: 1 second = 1 000 000 microseconds.
|
||||
$sleep = 20000;
|
||||
|
||||
// Fetches specific mailboxes only, in case the corresponding id is greater than zero.
|
||||
$mailboxIds = array_filter(
|
||||
array_map(
|
||||
'intval',
|
||||
explode(',', $this->option('mailboxes'))
|
||||
),
|
||||
function ($mailboxId) {
|
||||
return $mailboxId > 0;
|
||||
}
|
||||
);
|
||||
|
||||
foreach ($this->mailboxes as $mailbox) {
|
||||
if (!$mailbox->isInActive()) {
|
||||
continue;
|
||||
}
|
||||
if ($mailboxIds !== [] && !in_array($mailbox->id, $mailboxIds, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sleep += 20000;
|
||||
if ($sleep > 500000) {
|
||||
$sleep = 500000;
|
||||
if ($sleep > self::MAX_SLEEP) {
|
||||
$sleep = self::MAX_SLEEP;
|
||||
}
|
||||
|
||||
$this->info('['.date('Y-m-d H:i:s').'] Mailbox: '.$mailbox->name);
|
||||
|
||||
$this->mailbox = $mailbox;
|
||||
$this->extra_import = [];
|
||||
|
||||
$debug_log = '';
|
||||
|
||||
try {
|
||||
$this->fetch($mailbox);
|
||||
$debug_log = $this->executeFetch($mailbox, $debug);
|
||||
} catch (\Exception $e) {
|
||||
$successfully = false;
|
||||
$this->logError('Error: '.$e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')').')';
|
||||
// If mail server starts to block the connection
|
||||
// (when there are many mailboxes for example),
|
||||
// we increase connection sleep time and retry after sleep.
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4227
|
||||
if (trim($e->getMessage()) == 'connection setup failed') {
|
||||
$sleep += 500000;
|
||||
|
||||
usleep(self::MAX_SLEEP);
|
||||
|
||||
try {
|
||||
$debug_log = $this->executeFetch($mailbox, $debug);
|
||||
} catch (\Exception $e) {
|
||||
$successfully = false;
|
||||
$this->logError('Error: '.$e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')').')';
|
||||
}
|
||||
} else {
|
||||
$successfully = false;
|
||||
|
||||
// When emails are fetched via POP3 we have to come through all the
|
||||
// emails and check dates in their headers - it may take some time.
|
||||
// So there can be situations when more than one fetching imports same email.
|
||||
if (!\Str::startsWith($e->getMessage(), 'SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry')
|
||||
) {
|
||||
$this->logError('Error: '.$e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')').')';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($debug && $debug_log) {
|
||||
$this->line($debug_log);
|
||||
}
|
||||
|
||||
// Import emails sent to several mailboxes at once.
|
||||
if (count($this->extra_import)) {
|
||||
$this->line('['.date('Y-m-d H:i:s').'] Importing emails sent to several mailboxes at once: '.count($this->extra_import));
|
||||
foreach ($this->extra_import as $i => $extra_import) {
|
||||
$this->line('['.date('Y-m-d H:i:s').'] '.($i+1).') '.$extra_import['message']->getSubject());
|
||||
$this->processMessage($extra_import['message'], $extra_import['message_id'], $extra_import['mailbox'], [], true);
|
||||
}
|
||||
}
|
||||
|
||||
usleep($sleep);
|
||||
}
|
||||
|
||||
// Import emails sent to several mailboxes at once.
|
||||
if (count($this->extra_import)) {
|
||||
$this->line('['.date('Y-m-d H:i:s').'] Importing emails sent to several mailboxes at once: '.count($this->extra_import));
|
||||
foreach ($this->extra_import as $i => $extra_import) {
|
||||
$this->line('['.date('Y-m-d H:i:s').'] '.($i+1).') '.$extra_import['message']->getSubject());
|
||||
$this->processMessage($extra_import['message'], $extra_import['message_id'], $extra_import['mailbox'], [], true);
|
||||
}
|
||||
}
|
||||
|
||||
if ($successfully && count($this->mailboxes)) {
|
||||
Option::set('fetch_emails_last_successful_run', $now);
|
||||
}
|
||||
@@ -145,14 +195,45 @@ class FetchEmails extends Command
|
||||
$this->mailboxes = [];
|
||||
}
|
||||
|
||||
public function executeFetch($mailbox, $debug)
|
||||
{
|
||||
$debug_log = '';
|
||||
|
||||
if ($debug) {
|
||||
ob_start();
|
||||
}
|
||||
|
||||
$this->fetch($mailbox);
|
||||
|
||||
if ($debug) {
|
||||
$debug_log = ob_get_contents();
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
return $debug_log;
|
||||
}
|
||||
|
||||
public function fetch($mailbox)
|
||||
{
|
||||
$no_charset = false;
|
||||
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
|
||||
// Connect to the Server
|
||||
$client->connect();
|
||||
// Connect to the Server.
|
||||
try {
|
||||
$client->connect();
|
||||
} catch (\Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
|
||||
// POP3 uses LegacyProtocol.php
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/4060
|
||||
if ($error && \Str::startsWith($error, 'Mailbox is empty')) {
|
||||
$this->line('['.date('Y-m-d H:i:s').'] Fetched: 0');
|
||||
return;
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
$folders = [];
|
||||
|
||||
@@ -166,11 +247,13 @@ class FetchEmails extends Command
|
||||
$folder = \MailHelper::getImapFolder($client, $folder_name);
|
||||
} catch (\Exception $e) {
|
||||
// Just log error and continue.
|
||||
$this->error('['.date('Y-m-d H:i:s').'] Could not get mailbox IMAP folder: '.$folder_name);
|
||||
$this->error('['.date('Y-m-d H:i:s').'] IMAP folder ('.$folder_name.') not found on the mail server: '.$e->getMessage());
|
||||
}
|
||||
|
||||
if ($folder) {
|
||||
$folders[] = $folder;
|
||||
} else {
|
||||
$this->line('['.date('Y-m-d H:i:s').'] IMAP folder not found on the mail server: '.$folder_name);
|
||||
}
|
||||
}
|
||||
// try {
|
||||
@@ -184,8 +267,9 @@ class FetchEmails extends Command
|
||||
$this->line('['.date('Y-m-d H:i:s').'] Fetching: '.($unseen ? 'UNREAD' : 'ALL'));
|
||||
}
|
||||
|
||||
$page_size = (int)config('app.fetching_bunch_size');
|
||||
foreach ($folders as $folder) {
|
||||
$this->line('['.date('Y-m-d H:i:s').'] Folder: '.$folder->name);
|
||||
$this->line('['.date('Y-m-d H:i:s').'] Folder: '.($folder->full_name ?? $folder->name));
|
||||
|
||||
// Requesting emails by bunches allows to fetch large amounts of emails
|
||||
// without problems with memory.
|
||||
@@ -203,7 +287,7 @@ class FetchEmails extends Command
|
||||
if ($no_charset) {
|
||||
$messages_query->setCharset(null);
|
||||
}
|
||||
$messages_query->limit(self::PAGE_SIZE, $page);
|
||||
$messages_query->limit($page_size, $page);
|
||||
|
||||
$messages = $messages_query->get();
|
||||
|
||||
@@ -211,7 +295,7 @@ class FetchEmails extends Command
|
||||
$last_error = $client->getLastError();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$last_error = $e->getMessage();
|
||||
$last_error = $e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')'.')';
|
||||
}
|
||||
|
||||
if ($last_error && stristr($last_error, 'The specified charset is not supported')) {
|
||||
@@ -256,7 +340,7 @@ class FetchEmails extends Command
|
||||
$this->processMessage($message, $message_id, $dest_mailbox, $this->mailboxes);
|
||||
}
|
||||
$page++;
|
||||
} while (count($messages) == self::PAGE_SIZE);
|
||||
} while (count($messages) == $page_size);
|
||||
}
|
||||
|
||||
$client->disconnect();
|
||||
@@ -367,7 +451,7 @@ class FetchEmails extends Command
|
||||
$user_id = null;
|
||||
$user = null; // for user reply only
|
||||
$message_from_customer = true;
|
||||
$in_reply_to = $message->getInReplyTo();
|
||||
$in_reply_to = trim($message->getInReplyTo() ?? '', '<>');
|
||||
$references = $message->getReferences();
|
||||
$attachments = $message->getAttachments();
|
||||
$html_body = '';
|
||||
@@ -376,19 +460,21 @@ class FetchEmails extends Command
|
||||
$is_bounce = false;
|
||||
|
||||
// Determine previous Message-ID
|
||||
$prev_message_id = '';
|
||||
$prev_message_ids = array();
|
||||
|
||||
if ($references && !is_array($references)) {
|
||||
$references = array_filter(preg_split('/[, <>]/', $references));
|
||||
}
|
||||
|
||||
if ($in_reply_to) {
|
||||
$prev_message_id = trim($in_reply_to, '<>');
|
||||
} elseif ($references) {
|
||||
if (!is_array($references)) {
|
||||
$references = array_filter(preg_split('/[, <>]/', $references));
|
||||
}
|
||||
// Find first non-empty reference
|
||||
$prev_message_ids[] = $in_reply_to;
|
||||
}
|
||||
if ($references) {
|
||||
// Find non-empty references
|
||||
if (is_array($references)) {
|
||||
foreach ($references as $reference) {
|
||||
if (!empty(trim($reference))) {
|
||||
$prev_message_id = trim($reference);
|
||||
break;
|
||||
$prev_message_ids[] = trim($reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -403,13 +489,10 @@ class FetchEmails extends Command
|
||||
];
|
||||
|
||||
// Try to get previous message ID from marker in body.
|
||||
if (!$prev_message_id || !preg_match('/^('.implode('|', $reply_prefixes).')\-(\d+)\-/', $prev_message_id)) {
|
||||
$html_body = $message->getHTMLBody(false);
|
||||
$marker_message_id = \MailHelper::fetchMessageMarkerValue($html_body);
|
||||
|
||||
if ($marker_message_id) {
|
||||
$prev_message_id = $marker_message_id;
|
||||
}
|
||||
$html_body = $message->getHTMLBody(false);
|
||||
$marker_message_id = \MailHelper::fetchMessageMarkerValue($html_body);
|
||||
if ($marker_message_id) {
|
||||
$prev_message_ids[] = $marker_message_id;
|
||||
}
|
||||
|
||||
// Bounce detection.
|
||||
@@ -478,51 +561,45 @@ class FetchEmails extends Command
|
||||
}
|
||||
}
|
||||
|
||||
// Is it a message from Customer or User replied to the notification
|
||||
preg_match('/^'.\MailHelper::MESSAGE_ID_PREFIX_NOTIFICATION."\-(\d+)\-(\d+)\-/", $prev_message_id, $m);
|
||||
# Try to get the thread traversing the possible prev_message_ids
|
||||
foreach ($prev_message_ids as $prev_message_id) {
|
||||
// Is it a message from Customer or User replied to the notification
|
||||
preg_match('/^'.\MailHelper::MESSAGE_ID_PREFIX_NOTIFICATION."\-(\d+)\-(\d+)\-/", $prev_message_id, $m);
|
||||
|
||||
if (!$is_bounce && !empty($m[1]) && !empty($m[2])) {
|
||||
// Reply from User to the notification
|
||||
$prev_thread = Thread::find($m[1]);
|
||||
$user_id = $m[2];
|
||||
$user = User::find($user_id);
|
||||
$message_from_customer = false;
|
||||
$is_reply = true;
|
||||
if (!$is_bounce && !empty($m[1]) && !empty($m[2])) {
|
||||
// Reply from User to the notification
|
||||
$prev_thread = Thread::find($m[1]);
|
||||
$user_id = $m[2];
|
||||
$user = User::find($user_id);
|
||||
$message_from_customer = false;
|
||||
$is_reply = true;
|
||||
|
||||
if (!$user) {
|
||||
$this->logError('User not found: '.$user_id);
|
||||
$this->setSeen($message, $mailbox);
|
||||
return;
|
||||
}
|
||||
$this->line('['.date('Y-m-d H:i:s').'] Message from: User');
|
||||
} else {
|
||||
// Message from Customer or User replied to his reply to notification
|
||||
$this->line('['.date('Y-m-d H:i:s').'] Message from: Customer');
|
||||
if (!$user) {
|
||||
$this->logError('User not found: '.$user_id);
|
||||
$this->setSeen($message, $mailbox);
|
||||
return;
|
||||
}
|
||||
// Skip auto-replies sent to the email notification on behalf of a user.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/4035
|
||||
if (\MailHelper::isAutoResponder($message_header)) {
|
||||
$this->logError('Skipping an auto-reply to the email notification');
|
||||
$this->setSeen($message, $mailbox);
|
||||
return;
|
||||
}
|
||||
$this->line('['.date('Y-m-d H:i:s').'] Message from: User');
|
||||
} else {
|
||||
// Message from Customer or User replied to his reply to notification
|
||||
$this->line('['.date('Y-m-d H:i:s').'] Message from: Customer');
|
||||
|
||||
if (!$is_bounce) {
|
||||
if ($prev_message_id) {
|
||||
$prev_thread_id = '';
|
||||
if (!$is_bounce) {
|
||||
if ($prev_message_id) {
|
||||
$prev_thread_id = '';
|
||||
|
||||
// Customer replied to the email from user
|
||||
preg_match('/^'.\MailHelper::MESSAGE_ID_PREFIX_REPLY_TO_CUSTOMER."\-(\d+)\-([a-z0-9]+)@/", $prev_message_id, $m);
|
||||
// Simply checking thread_id from message_id was causing an issue when
|
||||
// customer was sending a message from FreeScout - the message was
|
||||
// connected to the wrong conversation.
|
||||
if (!empty($m[1]) && !empty($m[2])) {
|
||||
$message_id_hash = $m[2];
|
||||
if (strlen($message_id_hash) == 16) {
|
||||
if ($message_id_hash == \MailHelper::getMessageIdHash($m[1])) {
|
||||
$prev_thread_id = $m[1];
|
||||
}
|
||||
} else {
|
||||
// Backward compatibility.
|
||||
$prev_thread_id = $m[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Customer replied to the auto reply
|
||||
if (!$prev_thread_id) {
|
||||
preg_match('/^'.\MailHelper::MESSAGE_ID_PREFIX_AUTO_REPLY."\-(\d+)\-([a-z0-9]+)@/", $prev_message_id, $m);
|
||||
// Customer replied to the email from user
|
||||
preg_match('/^'.\MailHelper::MESSAGE_ID_PREFIX_REPLY_TO_CUSTOMER."\-(\d+)\-([a-z0-9]+)@/", $prev_message_id, $m);
|
||||
// Simply checking thread_id from message_id was causing an issue when
|
||||
// customer was sending a message from FreeScout - the message was
|
||||
// connected to the wrong conversation.
|
||||
if (!empty($m[1]) && !empty($m[2])) {
|
||||
$message_id_hash = $m[2];
|
||||
if (strlen($message_id_hash) == 16) {
|
||||
@@ -534,29 +611,48 @@ class FetchEmails extends Command
|
||||
$prev_thread_id = $m[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($prev_thread_id) {
|
||||
$prev_thread = Thread::find($prev_thread_id);
|
||||
} else {
|
||||
// Customer replied to his own message
|
||||
$prev_thread = Thread::where('message_id', $prev_message_id)->first();
|
||||
}
|
||||
// Customer replied to the auto reply
|
||||
if (!$prev_thread_id) {
|
||||
preg_match('/^'.\MailHelper::MESSAGE_ID_PREFIX_AUTO_REPLY."\-(\d+)\-([a-z0-9]+)@/", $prev_message_id, $m);
|
||||
if (!empty($m[1]) && !empty($m[2])) {
|
||||
$message_id_hash = $m[2];
|
||||
if (strlen($message_id_hash) == 16) {
|
||||
if ($message_id_hash == \MailHelper::getMessageIdHash($m[1])) {
|
||||
$prev_thread_id = $m[1];
|
||||
}
|
||||
} else {
|
||||
// Backward compatibility.
|
||||
$prev_thread_id = $m[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reply from user to his reply to the notification
|
||||
if (!$prev_thread
|
||||
&& ($prev_thread = Thread::where('message_id', $prev_message_id)->first())
|
||||
&& $prev_thread->created_by_user_id
|
||||
&& $prev_thread->created_by_user->hasEmail($from)
|
||||
) {
|
||||
$user_id = $user->id;
|
||||
$message_from_customer = false;
|
||||
$is_reply = true;
|
||||
if ($prev_thread_id) {
|
||||
$prev_thread = Thread::find($prev_thread_id);
|
||||
} else {
|
||||
// Customer replied to his own message
|
||||
$prev_thread = Thread::where('message_id', $prev_message_id)->first();
|
||||
}
|
||||
|
||||
// Reply from user to his reply to the notification
|
||||
if (!$prev_thread
|
||||
&& ($prev_thread = Thread::where('message_id', $prev_message_id)->first())
|
||||
&& $prev_thread->created_by_user_id
|
||||
&& $prev_thread->created_by_user->hasEmail($from)
|
||||
) {
|
||||
$user_id = $user->id;
|
||||
$message_from_customer = false;
|
||||
$is_reply = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($prev_thread)) {
|
||||
$is_reply = true;
|
||||
}
|
||||
}
|
||||
|
||||
# If a thread is found, we keep it and break
|
||||
if (!empty($prev_thread)) {
|
||||
$is_reply = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,12 +689,17 @@ class FetchEmails extends Command
|
||||
// Get body and do not replace :cid with images base64
|
||||
$html_body = $message->getHTMLBody(false);
|
||||
}
|
||||
|
||||
$is_html = true;
|
||||
|
||||
if ($html_body) {
|
||||
$body = $this->separateReply($html_body, true, $is_reply);
|
||||
$body = $html_body;
|
||||
} else {
|
||||
$body = $message->getTextBody();
|
||||
$body = $this->separateReply($body, false, $is_reply);
|
||||
$is_html = false;
|
||||
$body = $message->getTextBody() ?? '';
|
||||
$body = htmlspecialchars($body);
|
||||
}
|
||||
|
||||
// We have to fetch absolutely all emails, even with empty body.
|
||||
// if (!$body) {
|
||||
// $this->logError('Message body is empty');
|
||||
@@ -620,23 +721,26 @@ class FetchEmails extends Command
|
||||
|
||||
// It will always return an empty value as it's Bcc.
|
||||
$bcc = $this->formatEmailList($message->getBcc());
|
||||
|
||||
|
||||
// If existing user forwarded customer's email to the mailbox
|
||||
// we are creating a new conversation as if it was sent by the customer.
|
||||
if ($in_reply_to
|
||||
if (// Some mail clients to not add "In-Reply-To" header when forwarding emails.
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4348
|
||||
//$in_reply_to
|
||||
// We should use body here, as entire HTML may contain
|
||||
// email looking things.
|
||||
//&& ($fwd_body = $html_body ?: $message->getTextBody())
|
||||
&& $body
|
||||
$body
|
||||
//&& preg_match("/^(".implode('|', \MailHelper::$fwd_prefixes)."):(.*)/i", $subject, $m)
|
||||
// F:, FW:, FWD:, WG:, De:
|
||||
&& preg_match("/^[[:alpha:]]{1,3}:(.*)/i", $subject, $m)
|
||||
&& preg_match("/^[[:alpha:]]{1,3}\s*:(.*)/i", $subject, $m)
|
||||
// It can be just "Fwd:"
|
||||
//&& !empty($m[1])
|
||||
&& !$user_id && !$is_reply && !$prev_thread
|
||||
// Only if the email has been sent to one mailbox.
|
||||
&& count($to) == 1 && count($cc) == 0
|
||||
&& preg_match("/^[\s]*".self::FWD_AS_CUSTOMER_COMMAND."/su", trim(strip_tags($body)))
|
||||
// We need to replace also any potential <style></style> tags.
|
||||
&& preg_match("/^[\s]*".self::FWD_AS_CUSTOMER_COMMAND."/su", strtolower(trim(\Helper::stripTags($body))))
|
||||
) {
|
||||
// Try to get "From:" from body.
|
||||
$original_sender = $this->getOriginalSenderFromFwd($body);
|
||||
@@ -657,6 +761,12 @@ class FetchEmails extends Command
|
||||
}
|
||||
}
|
||||
|
||||
// separateReply() function may distort original HTML if email
|
||||
// is mentioned as <test@example.org> and it will interpret it as a tag.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/4036
|
||||
|
||||
$body = $this->separateReply($body, $is_html, $is_reply, !$message_from_customer, (($message_from_customer && $prev_thread) ? $prev_thread->getMessageId($mailbox) : ''));
|
||||
|
||||
// Create customers
|
||||
$emails = array_merge(
|
||||
$this->attrToArray($message->getFrom()),
|
||||
@@ -800,7 +910,7 @@ class FetchEmails extends Command
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/2672
|
||||
$body = preg_replace("/[\"']cid:/", '!', $body);
|
||||
// Cut out the command, otherwise it will be recognized as an email.
|
||||
$body = preg_replace("/".self::FWD_AS_CUSTOMER_COMMAND."([\s<]+)/su", '$1', $body);
|
||||
$body = preg_replace("/".self::FWD_AS_CUSTOMER_COMMAND."([\s<]+)/isu", '$1', $body);
|
||||
|
||||
// Looks like email texts may appear in attributes:
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/276
|
||||
@@ -813,6 +923,7 @@ class FetchEmails extends Command
|
||||
$email = $b[1] ?? '';
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/2517
|
||||
$email = preg_replace("#.*<(.*)>.*#", "$1", $email);
|
||||
|
||||
return Email::sanitizeEmail($email);
|
||||
}
|
||||
|
||||
@@ -902,6 +1013,7 @@ class FetchEmails extends Command
|
||||
$now = date('Y-m-d H:i:s');
|
||||
}
|
||||
$conv_cc = $cc;
|
||||
$prev_conv_cc = $conv_cc;
|
||||
|
||||
// Customers are created before with email and name
|
||||
$customer = Customer::create($from);
|
||||
@@ -911,6 +1023,7 @@ class FetchEmails extends Command
|
||||
// If reply came from another customer: change customer, add original as CC.
|
||||
// If FreeScout will not change the customer, the reply will be shown
|
||||
// as coming from the original customer (not the real sender) and cause confusion.
|
||||
// Below after events are fired we roll customer back.
|
||||
if ($conversation->customer_id != $customer->id) {
|
||||
$prev_customer_id = $conversation->customer_id;
|
||||
$prev_customer_email = $conversation->customer_email;
|
||||
@@ -1005,11 +1118,17 @@ class FetchEmails extends Command
|
||||
$body_changed = false;
|
||||
$saved_attachments = $this->saveAttachments($attachments, $thread->id);
|
||||
if ($saved_attachments) {
|
||||
$thread->has_attachments = true;
|
||||
|
||||
// After attachments saved to the disk we can replace cids in body (for PLAIN and HTML body)
|
||||
$thread->body = $this->replaceCidsWithAttachmentUrls($thread->body, $saved_attachments, $conversation, $prev_has_attachments);
|
||||
$body_changed = true;
|
||||
|
||||
foreach ($saved_attachments as $saved_attachment) {
|
||||
if (!$saved_attachment['attachment']->embedded) {
|
||||
$thread->has_attachments = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$new_body = Thread::replaceBase64ImagesWithAttachments($thread->body);
|
||||
@@ -1043,8 +1162,16 @@ class FetchEmails extends Command
|
||||
}
|
||||
|
||||
// Conversation customer changed
|
||||
// if ($prev_customer_id) {
|
||||
// event(new ConversationCustomerChanged($conversation, $prev_customer_id, $prev_customer_email, null, $customer));
|
||||
// }
|
||||
|
||||
// Return original customer back.
|
||||
if ($prev_customer_id) {
|
||||
event(new ConversationCustomerChanged($conversation, $prev_customer_id, $prev_customer_email, null, $customer));
|
||||
$conversation->customer_id = $prev_customer_id;
|
||||
$conversation->customer_email = $prev_customer_email;
|
||||
$conversation->setCc(array_merge($prev_conv_cc, array_diff($to, $mailbox->getEmails())));
|
||||
$conversation->save();
|
||||
}
|
||||
|
||||
return $thread;
|
||||
@@ -1143,11 +1270,16 @@ class FetchEmails extends Command
|
||||
$body_changed = false;
|
||||
$saved_attachments = $this->saveAttachments($attachments, $thread->id);
|
||||
if ($saved_attachments) {
|
||||
$thread->has_attachments = true;
|
||||
|
||||
// After attachments saved to the disk we can replace cids in body (for PLAIN and HTML body)
|
||||
$thread->body = $this->replaceCidsWithAttachmentUrls($thread->body, $saved_attachments, $conversation, $prev_has_attachments);
|
||||
$body_changed = true;
|
||||
|
||||
foreach ($saved_attachments as $saved_attachment) {
|
||||
if (!$saved_attachment['attachment']->embedded) {
|
||||
$thread->has_attachments = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$new_body = Thread::replaceBase64ImagesWithAttachments($thread->body);
|
||||
@@ -1203,7 +1335,7 @@ class FetchEmails extends Command
|
||||
// Fix for Webklex/laravel-imap.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/2782
|
||||
if (\Str::startsWith($name, '=?')) {
|
||||
$name_decoded = \imap_utf8($name);
|
||||
$name_decoded = \MailHelper::imapUtf8($name);
|
||||
|
||||
if ($name_decoded) {
|
||||
return $name_decoded;
|
||||
@@ -1220,7 +1352,7 @@ class FetchEmails extends Command
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function separateReply($body, $is_html, $is_reply)
|
||||
public function separateReply($body, $is_html, $is_reply, $user_reply_to_notification = false, $prev_message_id = '')
|
||||
{
|
||||
$cmp_reply_length_desc = function ($a, $b) {
|
||||
if (mb_strlen($a) == mb_strlen($b)) {
|
||||
@@ -1232,8 +1364,22 @@ class FetchEmails extends Command
|
||||
|
||||
$result = '';
|
||||
|
||||
// Can fix broken HTML, remove sensitive parts and etc.
|
||||
$body = \Eventy::filter('fetch_emails.separate_reply.preprocess_body', $body);
|
||||
|
||||
if ($is_html) {
|
||||
// Extract body content from HTML
|
||||
|
||||
// Proton has it's own unique way of placing replies:
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4537#issuecomment-2629836738
|
||||
if ($is_reply
|
||||
&& ($protonmail_quote_pos = mb_strpos($body, '<div class="protonmail_quote">'))
|
||||
&& ($html_pos = mb_strpos($body, '<html'))
|
||||
&& $protonmail_quote_pos < $html_pos
|
||||
) {
|
||||
$body = mb_substr($body, 0, $protonmail_quote_pos);
|
||||
}
|
||||
|
||||
// Split by <html>
|
||||
$htmls = [];
|
||||
preg_match_all("/<html[^>]*>(.*?)<\/html>/is", $body, $htmls);
|
||||
@@ -1247,7 +1393,7 @@ class FetchEmails extends Command
|
||||
libxml_use_internal_errors(true);
|
||||
//$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
//$dom->loadHTML(\Helper::mbConvertEncodingHtmlEntities($html));
|
||||
$dom->loadHTML(\Symfony\Polyfill\Mbstring\Mbstring::mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
$dom->loadHTML(\Symfony\Polyfill\Mbstring\Mbstring::mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8') ?: $html);
|
||||
libxml_use_internal_errors(false);
|
||||
$bodies = $dom->getElementsByTagName('body');
|
||||
if ($bodies->length == 1) {
|
||||
@@ -1270,12 +1416,31 @@ class FetchEmails extends Command
|
||||
if ($is_reply) {
|
||||
// Check all separators and choose the shortest reply
|
||||
$reply_bodies = [];
|
||||
|
||||
$reply_separators = Mail::$alternative_reply_separators;
|
||||
|
||||
if (!empty($this->mailbox->before_reply)) {
|
||||
$reply_separators[] = $this->mailbox->before_reply;
|
||||
}
|
||||
|
||||
// If user replied to the email notification use only predefined reply separator.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3580
|
||||
if ($user_reply_to_notification && strstr($result, \MailHelper::REPLY_SEPARATOR_NOTIFICATION)) {
|
||||
$reply_separators = [\MailHelper::REPLY_SEPARATOR_NOTIFICATION];
|
||||
}
|
||||
|
||||
// Try to separate customer reply using hashed reply separator.
|
||||
// In this case some extra text may appear below customer's reply:
|
||||
// On Thu, Jan 4, 2024 at 8:41 AM John Doe | Demo <test@example.org> wrote:
|
||||
if (config('app.alternative_reply_separation')) {
|
||||
if (!$user_reply_to_notification && $prev_message_id) {
|
||||
$hashed_reply_separator = \MailHelper::getHashedReplySeparator($prev_message_id);
|
||||
if (strstr($result, $hashed_reply_separator)) {
|
||||
$reply_separators = [$hashed_reply_separator];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($reply_separators as $reply_separator) {
|
||||
if (\Str::startsWith($reply_separator, 'regex:')) {
|
||||
$regex = preg_replace("/^regex:/", '', $reply_separator);
|
||||
@@ -1284,7 +1449,7 @@ class FetchEmails extends Command
|
||||
$parts = explode($reply_separator, $result);
|
||||
}
|
||||
if (count($parts) > 1) {
|
||||
// Check if past contains any real text.
|
||||
// Check if part contains any real text.
|
||||
$text = \Helper::htmlToText($parts[0]);
|
||||
$text = trim($text);
|
||||
$text = preg_replace('/^\s+/mu', '', $text);
|
||||
@@ -1474,7 +1639,8 @@ class FetchEmails extends Command
|
||||
|
||||
public function setSeen($message, $mailbox)
|
||||
{
|
||||
$message->setFlag(['Seen']);
|
||||
$flag = \Eventy::filter('fetch_emails.set_seen_flag', ['Seen'], $message, $mailbox);
|
||||
$message->setFlag($flag);
|
||||
\Eventy::action('fetch_emails.after_set_seen', $message, $mailbox, $this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,8 +53,17 @@ class GenerateVars extends Command
|
||||
|
||||
$content = view('js/vars', $params)->render();
|
||||
|
||||
//$filesystem->put($file_path, $content);
|
||||
// Save vars only if content changed
|
||||
// Escape quotes in json values.
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4369
|
||||
$content = preg_replace_callback(
|
||||
"#(:[ ]*\")(.*)(\"[,\r\n])#",
|
||||
function($v) {
|
||||
return $v[1].str_replace('"', '\"', $v[2]).$v[3];
|
||||
},
|
||||
$content
|
||||
);
|
||||
|
||||
// Save vars only if content has changed.
|
||||
try {
|
||||
if (\Storage::exists('js/vars.js')) {
|
||||
$old_content = \Storage::get('js/vars.js');
|
||||
|
||||
@@ -69,10 +69,11 @@ class ModuleUpdate extends Command
|
||||
|
||||
// Detect if new version is available.
|
||||
foreach ($installed_modules as $module) {
|
||||
if ($module->getAlias() != $dir_module['alias'] || !$module->active()) {
|
||||
if ($module->getAlias() != $dir_module['alias'] /*|| !$module->active()*/) {
|
||||
continue;
|
||||
}
|
||||
if (!empty($dir_module['version']) && version_compare($dir_module['version'], $module->get('version'), '>')) {
|
||||
|
||||
$update_result = \App\Module::updateModule($dir_module['alias']);
|
||||
|
||||
$this->info('['.$update_result['module_name'].' Module'.']');
|
||||
@@ -88,12 +89,75 @@ class ModuleUpdate extends Command
|
||||
if (trim($update_result['output'])) {
|
||||
$this->line(preg_replace("#\n#", "\n> ", '> '.trim($update_result['output'])));
|
||||
}
|
||||
|
||||
|
||||
$counter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update custom modules.
|
||||
// Loop through each installed module.
|
||||
foreach ($installed_modules as $module) {
|
||||
// Skip if the module is an official one
|
||||
if (\App\Module::isOfficial($module->get('authorUrl'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the URL for the latest version of the module
|
||||
$latest_version_number_url = $module->get('latestVersionUrl');
|
||||
if (! $latest_version_number_url) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a new Guzzle HTTP client
|
||||
$client = new \GuzzleHttp\Client();
|
||||
|
||||
try {
|
||||
// Send a GET request to the latest version URL
|
||||
$response = $client->request('GET', $latest_version_number_url, \Helper::setGuzzleDefaultOptions());
|
||||
|
||||
// Get the latest version number from the response body
|
||||
$latest_version = trim((string) $response->getBody());
|
||||
|
||||
if (empty($latest_version)) {
|
||||
continue;
|
||||
} else {
|
||||
// Get the current version of the module
|
||||
$current_version = $module->get('version');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// If there's an exception, skip to the next iteration
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the latest version is greater than the current version
|
||||
if (version_compare($latest_version, $current_version, '>')) {
|
||||
// Update the module
|
||||
$update_result = \App\Module::updateModule($module->getAlias());
|
||||
|
||||
// Print the module name and status
|
||||
$this->info('[' . $update_result['module_name'] . ' Module' . ']');
|
||||
if ($update_result['status'] == 'success') {
|
||||
// If the update was successful, print the success message
|
||||
$this->line($update_result['msg_success']);
|
||||
} else {
|
||||
// If the update failed, print the error message
|
||||
$msg = $update_result['msg'];
|
||||
if ($update_result['download_msg']) {
|
||||
$msg .= ' (' . $update_result['download_msg'] . ')';
|
||||
}
|
||||
$this->error('ERROR: ' . $msg);
|
||||
}
|
||||
// If there's any output from the update, print it
|
||||
if (trim($update_result['output'])) {
|
||||
$this->line(preg_replace("#\n#", "\n> ", '> ' . trim($update_result['output'])));
|
||||
}
|
||||
|
||||
// Increment the counter
|
||||
$counter ++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($module_alias && !$found) {
|
||||
$this->error('Module with the following alias not found: '.$module_alias);
|
||||
} elseif (!$counter) {
|
||||
|
||||
126
app/Console/Commands/ParseEml.php
Normal file
126
app/Console/Commands/ParseEml.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Mailbox;
|
||||
use Webklex\PHPIMAP\IMAP;
|
||||
use Webklex\PHPIMAP\Message;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
|
||||
class ParseEml extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:parse-eml';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Parse EML file';
|
||||
|
||||
/**
|
||||
* Current mailbox.
|
||||
*
|
||||
* Used to process emails sent to multiple mailboxes.
|
||||
*/
|
||||
public $mailbox;
|
||||
|
||||
/**
|
||||
* Used to process emails sent to multiple mailboxes.
|
||||
*/
|
||||
public $mailboxes;
|
||||
|
||||
public $extra_import = [];
|
||||
|
||||
/**
|
||||
* Page size when requesting emails from mail server.
|
||||
*/
|
||||
const PAGE_SIZE = 300;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$email = file_get_contents(storage_path('logs/email.eml'));
|
||||
|
||||
if (!str_contains($email, "\r\n")){
|
||||
$email = str_replace("\n", "\r\n", $email);
|
||||
}
|
||||
|
||||
// $raw_header = substr($email, 0, strpos($email, "\r\n\r\n"));
|
||||
// $raw_body = substr($email, strlen($raw_header)+4);
|
||||
|
||||
// $mailbox = Mailbox::find($this->option('mailbox'));
|
||||
|
||||
// //\Config::set('app.new_fetching_library', 'true');
|
||||
// $client = \MailHelper::getMailboxClient($mailbox);
|
||||
// $client->openFolder("INBOX");
|
||||
|
||||
// $message = Message::make(/*$this->option('uid')*/null, null, $client, $raw_header, $raw_body, [/*0 => "\\Seen"*/], IMAP::ST_UID);
|
||||
|
||||
$manager = new \Webklex\PHPIMAP\ClientManager([
|
||||
// 'options' => [
|
||||
// "debug" => $_ENV["LIVE_MAILBOX_DEBUG"] ?? false,
|
||||
// ],
|
||||
// 'accounts' => [
|
||||
// 'default' => [
|
||||
// 'host' => getenv("LIVE_MAILBOX_HOST"),
|
||||
// 'port' => getenv("LIVE_MAILBOX_PORT"),
|
||||
// 'encryption' => getenv("LIVE_MAILBOX_ENCRYPTION"),
|
||||
// 'validate_cert' => getenv("LIVE_MAILBOX_VALIDATE_CERT"),
|
||||
// 'username' => getenv("LIVE_MAILBOX_USERNAME"),
|
||||
// 'password' => getenv("LIVE_MAILBOX_PASSWORD"),
|
||||
// 'protocol' => 'imap', //might also use imap, [pop3 or nntp (untested)]
|
||||
// ],
|
||||
// ],
|
||||
]);
|
||||
$message = \Webklex\PHPIMAP\Message::fromString($email);
|
||||
|
||||
$this->line('Headers: ');
|
||||
$this->info($message->getHeader()->raw);
|
||||
$this->line('From: ');
|
||||
$this->info(json_encode($message->getFrom()[0] ?? [], JSON_UNESCAPED_UNICODE));
|
||||
$this->line('Reply-To: ');
|
||||
$this->info(json_encode($message->getReplyTo()[0] ?? [], JSON_UNESCAPED_UNICODE));
|
||||
$this->line('In-Reply-To: ');
|
||||
$this->info($message->getInReplyTo());
|
||||
$this->line('References: ');
|
||||
$this->info(json_encode(array_values(array_filter(preg_split('/[, <>]/', $message->getReferences() ?? ''))), JSON_UNESCAPED_UNICODE));
|
||||
$this->line('Date: ');
|
||||
$this->info($message->getDate());
|
||||
$this->line('Subject: ');
|
||||
$this->info($message->getSubject());
|
||||
$this->line('Text Body: ');
|
||||
$this->info($message->getTextBody());
|
||||
$this->line('HTML Body: ');
|
||||
$html_body = $message->getHTMLBody(false) ?? '';
|
||||
$this->info($html_body);
|
||||
|
||||
$attachments = $message->getAttachments();
|
||||
if (count($attachments)) {
|
||||
$this->line('Attachments: ');
|
||||
foreach ($attachments as $attachment) {
|
||||
$this->info('— '.$attachment->getName().(strstr($html_body, 'cid:'.$attachment->id) ? ' (embedded)' : ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ class SendMonitor extends Command
|
||||
// Get SendReplyToCustomer jobs.
|
||||
$pending_jobs = \App\Job::where('queue', 'emails')
|
||||
->where('payload', 'like', '{"displayName":"App\\\\\\\\Jobs\\\\\\\\SendReplyToCustomer"%')
|
||||
->where('created_at', '<', time() - self::CHECK_PERIOD)
|
||||
->where('available_at', '<', time() - self::CHECK_PERIOD)
|
||||
->exists();
|
||||
|
||||
// Check failed_jobs.
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Mailbox;
|
||||
use Webklex\PHPIMAP\IMAP;
|
||||
use Webklex\PHPIMAP\Message;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
|
||||
class TestWebklex extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* --mailbox Any mailbox able to connect via IMAP to its mail server.
|
||||
* --uid Pass any UID from the Webklex/PHP-IMAP fetching output.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:test-webklex {--mailbox=2} {--uid=914}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Test Webklex/php-imap library';
|
||||
|
||||
/**
|
||||
* Current mailbox.
|
||||
*
|
||||
* Used to process emails sent to multiple mailboxes.
|
||||
*/
|
||||
public $mailbox;
|
||||
|
||||
/**
|
||||
* Used to process emails sent to multiple mailboxes.
|
||||
*/
|
||||
public $mailboxes;
|
||||
|
||||
public $extra_import = [];
|
||||
|
||||
/**
|
||||
* Page size when requesting emails from mail server.
|
||||
*/
|
||||
const PAGE_SIZE = 300;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$email = file_get_contents(storage_path('logs/test_webklex.eml'));
|
||||
|
||||
if (!str_contains($email, "\r\n")){
|
||||
$email = str_replace("\n", "\r\n", $email);
|
||||
}
|
||||
|
||||
$raw_header = substr($email, 0, strpos($email, "\r\n\r\n"));
|
||||
$raw_body = substr($email, strlen($raw_header)+8);
|
||||
|
||||
$mailbox = Mailbox::find($this->option('mailbox'));
|
||||
|
||||
\Config::set('app.new_fetching_library', 'true');
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
$client->openFolder("INBOX");
|
||||
|
||||
$message = Message::make($this->option('uid'), null, $client, $raw_header, $raw_body, [0 => "\\Seen"], IMAP::ST_UID);
|
||||
|
||||
$this->line('Headers: ');
|
||||
$this->info($message->getHeader()->raw);
|
||||
$this->line('From: ');
|
||||
$this->info(json_encode($message->getFrom()[0] ?? [], JSON_UNESCAPED_UNICODE));
|
||||
$this->line('Subject: ');
|
||||
$this->info($message->getSubject());
|
||||
$this->line('Text Body: ');
|
||||
$this->info($message->getTextBody());
|
||||
$this->line('HTML Body: ');
|
||||
$this->info($message->getHTMLBody(false));
|
||||
|
||||
$attachments = $message->getAttachments();
|
||||
if (count($attachments)) {
|
||||
$this->line('Attachments: ');
|
||||
foreach ($attachments as $attachment) {
|
||||
$this->info($attachment->getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,10 @@ class Kernel extends ConsoleKernel
|
||||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
// Keep in mind that this function is also called on clearing cache.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3970
|
||||
if (!$this->isScheduleRun() && !\Helper::isRoute('system.cron')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove failed jobs
|
||||
$schedule->command('queue:flush')
|
||||
@@ -100,23 +103,30 @@ class Kernel extends ConsoleKernel
|
||||
}
|
||||
}
|
||||
|
||||
$fetch_unseen = (int)config('app.fetch_unseen');
|
||||
$fetch_command_identifier = \Helper::getWorkerIdentifier('freescout:fetch-emails');
|
||||
$fetch_command_name = 'freescout:fetch-emails --identifier='.$fetch_command_identifier;
|
||||
$fetch_command_name = 'freescout:fetch-emails'
|
||||
. ' --identifier='.$fetch_command_identifier
|
||||
. ' --unseen='.$fetch_unseen;
|
||||
|
||||
// Kill fetch commands running for too long.
|
||||
// In shedule:run this code is executed every time $schedule->command() in this function is executed.
|
||||
if ($this->isScheduleRun() && function_exists('shell_exec')) {
|
||||
if (function_exists('shell_exec')) {
|
||||
$fetch_command_pids = \Helper::getRunningProcesses($fetch_command_identifier);
|
||||
|
||||
// The name of the command here must be exactly the same as below!
|
||||
// Otherwise long fetching will be killed and won't run longer than 1 mintue.
|
||||
$mutex_name = $schedule->command($fetch_command_name)
|
||||
->skip(function () {
|
||||
return true;
|
||||
})
|
||||
->mutexName();
|
||||
// $schedule->command() creates a new mutex, so we need to store mutex name in cache.
|
||||
// $mutex_name = $schedule->command($fetch_command_name)
|
||||
// ->everyMinute() - this also need to be set
|
||||
// ->skip(function () {
|
||||
// return true;
|
||||
// })
|
||||
// ->mutexName();
|
||||
|
||||
// If there is no cache mutext but there are running fetch commands
|
||||
$mutex_name = \Cache::get('fetch_mutex_name') ?? '';
|
||||
|
||||
// If there is no cache mutext but there are running fetch commands
|
||||
// it means the mutex had expired after self::FETCH_MAX_EXECUTION_TIME
|
||||
// and the existing command(s) is running longer than self::FETCH_MAX_EXECUTION_TIME.
|
||||
if (count($fetch_command_pids) > 0 && !\Cache::get($mutex_name)) {
|
||||
@@ -138,15 +148,21 @@ class Kernel extends ConsoleKernel
|
||||
|
||||
// Fetch emails from mailboxes
|
||||
$fetch_command = $schedule->command($fetch_command_name)
|
||||
// withoutOverlapping() option creates a mutex in the cache
|
||||
// withoutOverlapping() option creates a mutex in the cache
|
||||
// which by default expires in 24 hours.
|
||||
// So we are passing an 'expiresAt' parameter to withoutOverlapping() to
|
||||
// prevent fetching from not being executed when fetching command by some reason
|
||||
// does not remove the mutex from the cache.
|
||||
// does not remove the mutex from the cache.
|
||||
->withoutOverlapping($expiresAt = self::FETCH_MAX_EXECUTION_TIME /* minutes */)
|
||||
->sendOutputTo(storage_path().'/logs/fetch-emails.log');
|
||||
|
||||
switch (config('app.fetch_schedule')) {
|
||||
case Mail::FETCH_SCHEDULE_EVERY_TWO_MINUTES:
|
||||
$fetch_command->cron('*/2 * * * *');
|
||||
break;
|
||||
case Mail::FETCH_SCHEDULE_EVERY_THREE_MINUTES:
|
||||
$fetch_command->cron('*/3 * * * *');
|
||||
break;
|
||||
case Mail::FETCH_SCHEDULE_EVERY_FIVE_MINUTES:
|
||||
$fetch_command->everyFiveMinutes();
|
||||
break;
|
||||
@@ -166,6 +182,7 @@ class Kernel extends ConsoleKernel
|
||||
$fetch_command->everyMinute();
|
||||
break;
|
||||
}
|
||||
\Cache::put('fetch_mutex_name', $fetch_command->mutexName(), self::FETCH_MAX_EXECUTION_TIME);
|
||||
|
||||
$schedule = \Eventy::filter('schedule', $schedule);
|
||||
|
||||
@@ -187,10 +204,10 @@ class Kernel extends ConsoleKernel
|
||||
// $schedule->command('queue:work') command below has withoutOverlapping() option,
|
||||
// which works via special mutex stored in the cache preventing several 'queue:work' to work at the same time.
|
||||
// So when the cache is cleared the mutex indicating that the 'queue:work' is running is removed,
|
||||
// and the second 'queue:work' command is launched by cron. When `artisan schedule:run` is executed it sees
|
||||
// and the second 'queue:work' command is launched by cron. When `artisan schedule:run` is executed it sees
|
||||
// that there are two 'queue:work' processes running and kills them.
|
||||
// After one minute 'queue:work' is executed by cron via `artisan schedule:run` and works in the background.
|
||||
if ($this->isScheduleRun() && function_exists('shell_exec')) {
|
||||
if (function_exists('shell_exec')) {
|
||||
$running_commands = \Helper::getRunningProcesses();
|
||||
|
||||
if (count($running_commands) > 1) {
|
||||
@@ -202,7 +219,7 @@ class Kernel extends ConsoleKernel
|
||||
sleep(1);
|
||||
// Check processes again.
|
||||
$worker_pids = \Helper::getRunningProcesses();
|
||||
|
||||
|
||||
if (count($worker_pids) > 1) {
|
||||
// Current process also has to be killed, as otherwise it "stucks"
|
||||
// $current_pid = getmypid();
|
||||
@@ -240,12 +257,12 @@ class Kernel extends ConsoleKernel
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is needed because every time $schedule->command() is executed
|
||||
* This function is needed because every time $schedule->command() is executed
|
||||
* the schedule() is executed also.
|
||||
*/
|
||||
public function isScheduleRun()
|
||||
{
|
||||
if (!\Helper::isConsole()) {
|
||||
if (\Helper::isConsole()) {
|
||||
return true;
|
||||
} else {
|
||||
return !empty($_SERVER['argv']) && in_array('schedule:run', $_SERVER['argv']);
|
||||
|
||||
@@ -57,11 +57,13 @@ class Conversation extends Model
|
||||
const TYPE_EMAIL = 1;
|
||||
const TYPE_PHONE = 2;
|
||||
const TYPE_CHAT = 3;
|
||||
const TYPE_CUSTOM = 4;
|
||||
|
||||
public static $types = [
|
||||
self::TYPE_EMAIL => 'email',
|
||||
self::TYPE_PHONE => 'phone',
|
||||
self::TYPE_CHAT => 'chat',
|
||||
self::TYPE_CUSTOM => 'custom',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -669,7 +671,7 @@ class Conversation extends Model
|
||||
*
|
||||
* @return Conversation
|
||||
*/
|
||||
public function getNearby($mode = 'closest', $folder_id = null)
|
||||
public function getNearby($mode = 'closest', $folder_id = null, $status = null, $prev_if_no_next = false)
|
||||
{
|
||||
$conversation = null;
|
||||
|
||||
@@ -684,21 +686,31 @@ class Conversation extends Model
|
||||
|
||||
$query = \Eventy::filter('conversation.get_nearby_query', $query, $this, $mode, $folder);
|
||||
|
||||
$status_applied = \Eventy::filter('conversation.get_nearby_status', false, $query, $status, $this, $folder);
|
||||
|
||||
if (!$status_applied && $status) {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
$order_bys = $folder->getOrderByArray();
|
||||
|
||||
// Next.
|
||||
if ($mode != 'prev') {
|
||||
// Try to get next conversation
|
||||
$query_next = $query;
|
||||
$query_next = clone $query;
|
||||
foreach ($order_bys as $order_by) {
|
||||
foreach ($order_by as $field => $sort_order) {
|
||||
if (!$this->$field) {
|
||||
continue;
|
||||
}
|
||||
$field_value = $this->$field;
|
||||
if ($field == 'status' && $status !== null) {
|
||||
$field_value = $status;
|
||||
}
|
||||
if ($sort_order == 'asc') {
|
||||
$query_next->where($field, '>=', $this->$field);
|
||||
$query_next->where($field, '>=', $field_value);
|
||||
} else {
|
||||
$query_next->where($field, '<=', $this->$field);
|
||||
$query_next->where($field, '<=', $field_value);
|
||||
}
|
||||
$query_next->orderBy($field, $sort_order);
|
||||
}
|
||||
@@ -706,7 +718,8 @@ class Conversation extends Model
|
||||
$conversation = $query_next->first();
|
||||
}
|
||||
|
||||
if ($conversation || $mode == 'next') {
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3486
|
||||
if ($conversation || ($mode == 'next' && !$prev_if_no_next)) {
|
||||
return $conversation;
|
||||
}
|
||||
|
||||
@@ -717,10 +730,14 @@ class Conversation extends Model
|
||||
if (!$this->$field) {
|
||||
continue;
|
||||
}
|
||||
$field_value = $this->$field;
|
||||
if ($field == 'status' && $status !== null) {
|
||||
$field_value = $status;
|
||||
}
|
||||
if ($sort_order == 'asc') {
|
||||
$query_prev->where($field, '<=', $this->$field);
|
||||
$query_prev->where($field, '<=', $field_value);
|
||||
} else {
|
||||
$query_prev->where($field, '>=', $this->$field);
|
||||
$query_prev->where($field, '>=', $field_value);
|
||||
}
|
||||
$query_prev->orderBy($field, $sort_order == 'asc' ? 'desc' : 'asc');
|
||||
}
|
||||
@@ -732,9 +749,9 @@ class Conversation extends Model
|
||||
/**
|
||||
* Get URL of the next conversation.
|
||||
*/
|
||||
public function urlNext($folder_id = null)
|
||||
public function urlNext($folder_id = null, $status = null, $prev_if_no_next = false)
|
||||
{
|
||||
$next_conversation = $this->getNearby('next', $folder_id);
|
||||
$next_conversation = $this->getNearby('next', $folder_id, $status, $prev_if_no_next);
|
||||
if ($next_conversation) {
|
||||
$url = $next_conversation->url();
|
||||
} else {
|
||||
@@ -782,6 +799,11 @@ class Conversation extends Model
|
||||
|
||||
if (!$mailbox) {
|
||||
$mailbox = $this->mailbox;
|
||||
|
||||
if ($mailbox->id != $this->mailbox_id) {
|
||||
$this->load('mailbox');
|
||||
$mailbox = $this->mailbox;
|
||||
}
|
||||
}
|
||||
|
||||
// Find folder
|
||||
@@ -1065,8 +1087,10 @@ class Conversation extends Model
|
||||
} else {
|
||||
return __('me');
|
||||
}
|
||||
} else {
|
||||
} elseif ($this->user) {
|
||||
return $this->user->getFullName();
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1077,11 +1101,13 @@ class Conversation extends Model
|
||||
{
|
||||
// Get conversations from personal folder
|
||||
if ($folder->type == Folder::TYPE_MINE) {
|
||||
$query_conversations = self::where('user_id', $user_id)
|
||||
->where('mailbox_id', $folder->mailbox_id)
|
||||
$query_conversations = self::where('mailbox_id', $folder->mailbox_id)
|
||||
->whereIn('status', [self::STATUS_ACTIVE, self::STATUS_PENDING])
|
||||
->where('state', self::STATE_PUBLISHED);
|
||||
|
||||
// Applied below.
|
||||
//where('user_id', $user_id)
|
||||
|
||||
// Assigned - do not show my conversations.
|
||||
} elseif ($folder->type == Folder::TYPE_ASSIGNED) {
|
||||
$query_conversations = $folder->conversations()
|
||||
@@ -1113,18 +1139,31 @@ class Conversation extends Model
|
||||
$query_conversations = $folder->conversations()->where('state', self::STATE_PUBLISHED);
|
||||
}
|
||||
|
||||
$assignee_condition_applied = false;
|
||||
|
||||
// If show only assigned to the current user conversations.
|
||||
if (!\Helper::isConsole()
|
||||
&& $user_id
|
||||
&& $user = auth()->user()
|
||||
) {
|
||||
if ($user->id == $user_id
|
||||
&& $user->hasManageMailboxPermission($folder->mailbox_id, Mailbox::ACCESS_PERM_ASSIGNED)
|
||||
) {
|
||||
$query_conversations->where('user_id', '=', $user_id);
|
||||
if ($user->id == $user_id && $user->canSeeOnlyAssignedConversations()) {
|
||||
if ($folder->type != Folder::TYPE_DRAFTS) {
|
||||
$assignee_condition_applied = \Eventy::filter('folder.only_assigned_condition', false, $query_conversations, $user_id);
|
||||
if (!$assignee_condition_applied) {
|
||||
$query_conversations->where('user_id', '=', $user_id);
|
||||
$assignee_condition_applied = true;
|
||||
}
|
||||
} else {
|
||||
$query_conversations->where('user_id', '=', $user_id)
|
||||
->orWhere('created_by_user_id', '=', $user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($folder->type == Folder::TYPE_MINE && !$assignee_condition_applied) {
|
||||
$query_conversations->where('user_id', $user_id);
|
||||
}
|
||||
|
||||
return \Eventy::filter('folder.conversations_query', $query_conversations, $folder, $user_id);
|
||||
}
|
||||
|
||||
@@ -1134,9 +1173,12 @@ class Conversation extends Model
|
||||
*/
|
||||
public function getSignatureProcessed($data = [], $escape = false)
|
||||
{
|
||||
$replaced_text = $this->replaceTextVars( $this->mailbox->signature, $data, $escape );
|
||||
$replaced_text = $this->replaceTextVars($this->mailbox->signature, $data, $escape);
|
||||
|
||||
return \Eventy::filter( 'conversation.signature_processed', $replaced_text, $this, $data, $escape );
|
||||
// https://github.com/freescout-helpdesk/freescout/security/advisories/GHSA-fffc-phh8-5h4v
|
||||
$replaced_text = \Helper::stripDangerousTags($replaced_text);
|
||||
|
||||
return \Eventy::filter('conversation.signature_processed', $replaced_text, $this, $data, $escape);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1238,6 +1280,8 @@ class Conversation extends Model
|
||||
}
|
||||
}
|
||||
|
||||
// Remember original mailbox ID.
|
||||
$this->setMeta('orig_mailbox_id', $this->mailbox_id);
|
||||
// We don't know how to replace $this->mailbox object.
|
||||
$this->mailbox_id = $mailbox->id;
|
||||
// Check assignee.
|
||||
@@ -1630,7 +1674,7 @@ class Conversation extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Get emails which are excluded from CC and BCC.
|
||||
* Get emails which should be excluded from CC and BCC.
|
||||
*/
|
||||
public function getExcludeArray($mailbox = null)
|
||||
{
|
||||
@@ -1661,6 +1705,14 @@ class Conversation extends Model
|
||||
return ($this->type == self::TYPE_PHONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is it as custom conversation.
|
||||
*/
|
||||
public function isCustom()
|
||||
{
|
||||
return ($this->type == self::TYPE_CUSTOM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is it as chat conversation.
|
||||
*/
|
||||
@@ -1722,6 +1774,16 @@ class Conversation extends Model
|
||||
return $viewers;
|
||||
}
|
||||
|
||||
public function changeSubject($new_subject, $user = null)
|
||||
{
|
||||
$prev_subject = $this->subject;
|
||||
|
||||
$this->subject = $new_subject;
|
||||
$this->save();
|
||||
|
||||
\Eventy::action('conversation.subject_changed', $this, $user, $prev_subject);
|
||||
}
|
||||
|
||||
public function changeState($new_state, $user = null)
|
||||
{
|
||||
if (!array_key_exists($new_state, self::$states)) {
|
||||
@@ -1796,9 +1858,9 @@ class Conversation extends Model
|
||||
\Eventy::action('conversation.user_changed', $this, $user, $prev_user_id);
|
||||
}
|
||||
|
||||
public function deleteToFolder($user)
|
||||
public function deleteToFolder($user, $update_folders_counters = true)
|
||||
{
|
||||
$folder_id = $this->getCurrentFolder();
|
||||
//$folder_id = $this->getCurrentFolder();
|
||||
|
||||
$prev_state = $this->state;
|
||||
$this->state = Conversation::STATE_DELETED;
|
||||
@@ -1824,8 +1886,10 @@ class Conversation extends Model
|
||||
// Remove conversation from drafts folder.
|
||||
$this->removeFromFolder(Folder::TYPE_DRAFTS);
|
||||
|
||||
// Recalculate only old and new folders
|
||||
$this->mailbox->updateFoldersCounters();
|
||||
// Recalculate only old and new folders.
|
||||
if ($update_folders_counters) {
|
||||
$this->mailbox->updateFoldersCounters();
|
||||
}
|
||||
|
||||
\Eventy::action('conversation.deleted', $this, $user);
|
||||
\Eventy::action('conversation.state_changed', $this, $user, $prev_state);
|
||||
@@ -2164,6 +2228,8 @@ class Conversation extends Model
|
||||
'order' => 'desc',
|
||||
];
|
||||
|
||||
$result = \Eventy::filter('conversations.table_sorting', $result);
|
||||
|
||||
if (
|
||||
!empty($request->sorting['sort_by']) && !empty($request->sorting['order']) &&
|
||||
in_array($request->sorting['sort_by'], ['subject', 'number', 'date']) &&
|
||||
@@ -2176,7 +2242,7 @@ class Conversation extends Model
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function search($q, $filters, $user = null, $query_conversations = null)
|
||||
public static function search($q, $filters, $user = null, $query_conversations = null, $group_by = [])
|
||||
{
|
||||
$mailbox_ids = [];
|
||||
|
||||
@@ -2189,7 +2255,7 @@ class Conversation extends Model
|
||||
|
||||
// https://github.com/laravel/framework/issues/21242
|
||||
// https://github.com/laravel/framework/pull/27675
|
||||
$query_conversations->groupby('conversations.id');
|
||||
$query_conversations->groupBy(array_merge(['conversations.id'], $group_by));
|
||||
|
||||
if (!empty($filters['mailbox'])) {
|
||||
// Check if the user has access to the mailbox.
|
||||
@@ -2213,12 +2279,18 @@ class Conversation extends Model
|
||||
|
||||
if ($q) {
|
||||
$query_conversations->where(function ($query) use ($like, $filters, $q, $like_op) {
|
||||
|
||||
// It needs to be sanitized to avoid "Numeric value out of range" on PostgreSQL.
|
||||
$q_int = (int)$q;
|
||||
$q_int = $q_int > \Helper::DB_INT_MAX ? \Helper::DB_INT_MAX : $q_int;
|
||||
|
||||
$query->where('conversations.subject', $like_op, $like)
|
||||
->orWhere('conversations.customer_email', $like_op, $like)
|
||||
->orWhere('conversations.'.self::numberFieldName(), (int)$q)
|
||||
->orWhere('conversations.id', (int)$q)
|
||||
->orWhere('conversations.'.self::numberFieldName(), $q_int)
|
||||
->orWhere('conversations.id', $q_int)
|
||||
->orWhere('customers.first_name', $like_op, $like)
|
||||
->orWhere('customers.last_name', $like_op, $like)
|
||||
->orWhere(\Helper::isPgSql() ? \DB::raw('(customers.first_name || \' \' || customers.last_name)') : \DB::raw('CONCAT(customers.first_name, " ", customers.last_name)'), $like_op, $like)
|
||||
->orWhere('threads.body', $like_op, $like)
|
||||
->orWhere('threads.from', $like_op, $like)
|
||||
->orWhere('threads.to', $like_op, $like)
|
||||
@@ -2293,15 +2365,15 @@ class Conversation extends Model
|
||||
$query_conversations->where('conversations.created_at', '<=', date('Y-m-d 23:59:59', strtotime($filters['before'])));
|
||||
}
|
||||
|
||||
// Join tables if needed
|
||||
// Join tables if needed.
|
||||
$query_sql = $query_conversations->toSql();
|
||||
if (!strstr($query_sql, '`threads`.`conversation_id`')) {
|
||||
if (!self::queryContainsStr($query_sql, '`threads`.`conversation_id`')) {
|
||||
$query_conversations->join('threads', function ($join) {
|
||||
$join->on('conversations.id', '=', 'threads.conversation_id');
|
||||
});
|
||||
}
|
||||
|
||||
if (!strstr($query_sql, '`customers`.`id`')) {
|
||||
if (!self::queryContainsStr($query_sql, '`customers`.`id`')) {
|
||||
$query_conversations->leftJoin('customers', 'conversations.customer_id', '=' ,'customers.id');
|
||||
}
|
||||
|
||||
@@ -2408,4 +2480,34 @@ class Conversation extends Model
|
||||
|
||||
return $chats;
|
||||
}
|
||||
|
||||
public static function queryContainsStr($query_str, $substr)
|
||||
{
|
||||
$regex = preg_quote($substr);
|
||||
$regex = str_replace('`', '[`"]', $regex);
|
||||
|
||||
return preg_match('#'.$regex.'#', $query_str);
|
||||
}
|
||||
|
||||
public function userHasAccessToMailbox($user_id)
|
||||
{
|
||||
return MailboxUser::where('mailbox_id', $this->mailbox_id)
|
||||
->where('user_id', $user_id)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function chatShouldStartNew($mailbox = null)
|
||||
{
|
||||
if (!$mailbox) {
|
||||
$mailbox = $this->mailbox;
|
||||
}
|
||||
if (!empty($mailbox->meta['chat_start_new'])
|
||||
&& ($this->status == Conversation::STATUS_CLOSED
|
||||
|| $this->state == Conversation::STATE_DELETED)
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
108
app/Customer.php
108
app/Customer.php
@@ -454,7 +454,7 @@ class Customer extends Model
|
||||
*
|
||||
* @var [type]
|
||||
*/
|
||||
protected $fillable = ['first_name', 'last_name', 'company', 'job_title', 'address', 'city', 'state', 'zip', 'country', 'photo_url', 'age', 'gender', 'notes', 'channel', 'channel_id', 'social_profiles'];
|
||||
protected $fillable = ['first_name', 'last_name', 'company', 'job_title', 'address', 'city', 'state', 'zip', 'country', 'photo_url', 'notes', 'channel', 'channel_id', 'social_profiles'];
|
||||
|
||||
/**
|
||||
* Fields stored as JSON.
|
||||
@@ -501,6 +501,14 @@ class Customer extends Model
|
||||
return Email::select('email')->where('customer_id', $customer_id)->pluck('email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the customer has an email address among his emails.
|
||||
*/
|
||||
public function hasEmail($email_address)
|
||||
{
|
||||
return $this->emails_cached()->where('email', Email::sanitizeEmail($email_address))->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer full name.
|
||||
*
|
||||
@@ -1013,6 +1021,14 @@ class Customer extends Model
|
||||
$result = true;
|
||||
} else {
|
||||
// Update empty fields.
|
||||
|
||||
// Do not set last name if first name is already set (and vise versa).
|
||||
if (!empty($this->first_name) && !empty($data['last_name'])) {
|
||||
unset($data['last_name']);
|
||||
}
|
||||
if (!empty($this->last_name) && !empty($data['first_name'])) {
|
||||
unset($data['first_name']);
|
||||
}
|
||||
foreach ($data as $key => $value) {
|
||||
if (in_array($key, $this->fillable) && empty($this->$key)) {
|
||||
$this->$key = $value;
|
||||
@@ -1271,6 +1287,84 @@ class Customer extends Model
|
||||
return Storage::url(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$file_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge customerю
|
||||
*/
|
||||
public function mergeWith(Customer $customer2)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
$customer2->conversations()->update(['customer_id' => $this->id]);
|
||||
|
||||
// do {
|
||||
// $conversations = $customer2->conversations()->limit(1000);
|
||||
// foreach ($conversations as $conversation) {
|
||||
// $conversation->changeCustomer($customer_email, $this, $user);
|
||||
// }
|
||||
// } while (count($conversations) > 0);
|
||||
|
||||
// Move emails.
|
||||
$customer2->emails()->update(['customer_id' => $this->id]);
|
||||
|
||||
// Merge attributes
|
||||
// if (in_array('phone', $keepAttributes) && !$this->phone) {
|
||||
// $this->phone = $customerToMerge->phone;
|
||||
// }
|
||||
foreach ($this->fillable as $attr_name) {
|
||||
// Skip some attribues.
|
||||
if (in_array($attr_name, ['channel', 'channel_id'])) {
|
||||
continue;
|
||||
}
|
||||
if (!$this->$attr_name) {
|
||||
$this->$attr_name = $customer2->$attr_name;
|
||||
}
|
||||
}
|
||||
|
||||
// Merge Phones.
|
||||
$phones = self::mergeTypeValueLists($this->getPhones(), $customer2->getPhones());
|
||||
if (count($phones) != count($this->getPhones())) {
|
||||
$this->setPhones($phones);
|
||||
}
|
||||
|
||||
// Merge websites.
|
||||
$websites = array_merge($this->getWebsites(), $customer2->getWebsites());
|
||||
$this->setWebsites(array_unique($websites));
|
||||
|
||||
// Merge social profiles.
|
||||
$social = self::mergeTypeValueLists($this->getSocialProfiles(), $customer2->getSocialProfiles());
|
||||
if (count($social) != count($this->getSocialProfiles())) {
|
||||
$this->setSocialProfiles($social);
|
||||
}
|
||||
|
||||
$this->save();
|
||||
|
||||
\Eventy::action('customer.merged', $this, $customer2, $user);
|
||||
|
||||
$customer2->delete();
|
||||
}
|
||||
|
||||
public static function mergeTypeValueLists($list1, $list2)
|
||||
{
|
||||
foreach ($list2 as $data2) {
|
||||
if (empty($data2['type']) || empty($data2['value'])) {
|
||||
continue;
|
||||
}
|
||||
$exists = false;
|
||||
foreach ($list1 as $data) {
|
||||
if (!empty($data['type']) && !empty($data['value'])
|
||||
&& $data['type'] == $data2['type'] && $data['value'] == $data2['value']
|
||||
) {
|
||||
$exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$exists) {
|
||||
$list1[] = $data2;
|
||||
}
|
||||
}
|
||||
return $list1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize and save photo.
|
||||
*/
|
||||
@@ -1529,5 +1623,17 @@ class Customer extends Model
|
||||
return $customers;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dummy customer.
|
||||
*/
|
||||
public static function getDummyCustomer()
|
||||
{
|
||||
$customer = new self();
|
||||
$customer->first_name = __('Customer');
|
||||
$customer->last_name = '';
|
||||
|
||||
return $customer;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,4 +28,9 @@ class FailedJob extends Model
|
||||
{
|
||||
return \App\Job::getPayloadCommand($this->getPayloadDecoded());
|
||||
}
|
||||
|
||||
public static function retry($job_id)
|
||||
{
|
||||
\Artisan::call('queue:retry', ['id' => $job_id]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +224,9 @@ class Folder extends Model
|
||||
public function updateCounters()
|
||||
{
|
||||
if (config('app.update_folder_counters_in_background')) {
|
||||
\App\Jobs\UpdateFolderCounters::dispatch($this);
|
||||
if(!\Illuminate\Support\Facades\Cache::has("folder_update_lock_{$this->id}")) {
|
||||
\App\Jobs\UpdateFolderCounters::dispatch($this);
|
||||
}
|
||||
} else {
|
||||
$this->updateCountersNow();
|
||||
}
|
||||
|
||||
@@ -199,6 +199,25 @@ class ConversationsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$threads = $conversation->threads()->orderBy('created_at', 'desc')->get();
|
||||
|
||||
// Get To for new conversation.
|
||||
$new_conv_to = [];
|
||||
if (empty($threads[0]) || empty($threads[0]->to)) {
|
||||
// Before new conversation To field was stored in $conversation->customer_email.
|
||||
$emails = Conversation::sanitizeEmails($conversation->customer_email);
|
||||
// Get customers info for emails.
|
||||
if (count($emails)) {
|
||||
$new_conv_to = Customer::emailsToCustomers($emails);
|
||||
}
|
||||
} else {
|
||||
$new_conv_to = Customer::emailsToCustomers($threads[0]->getToArray());
|
||||
}
|
||||
|
||||
if (empty($customer) && count($new_conv_to) == 1) {
|
||||
$customer = Customer::getByEmail(array_key_first($new_conv_to));
|
||||
}
|
||||
|
||||
// Previous conversations
|
||||
$prev_conversations = [];
|
||||
if ($customer) {
|
||||
@@ -217,7 +236,24 @@ class ConversationsController extends Controller
|
||||
$template = 'conversations/create';
|
||||
}
|
||||
|
||||
// CC.
|
||||
$exclude_array = $conversation->getExcludeArray($mailbox);
|
||||
$cc = $conversation->getCcArray($exclude_array);
|
||||
|
||||
// If last reply came from customer who was mentioned in CC before,
|
||||
// we need to add this customer as CC.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3613
|
||||
foreach ($threads as $thread) {
|
||||
if ($thread->isUserMessage() && !$thread->isDraft()) {
|
||||
break;
|
||||
}
|
||||
if ($thread->isCustomerMessage()) {
|
||||
if ($thread->customer_id != $conversation->customer_id) {
|
||||
$cc[] = $thread->from;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Get data for creating a phone conversation.
|
||||
$name = [];
|
||||
@@ -282,8 +318,6 @@ class ConversationsController extends Controller
|
||||
|
||||
\Eventy::action('conversation.view.start', $conversation, $request);
|
||||
|
||||
$threads = $conversation->threads()->orderBy('created_at', 'desc')->get();
|
||||
|
||||
// Mailbox aliases.
|
||||
$from_aliases = $conversation->mailbox->getAliases(true, true);
|
||||
$from_alias = '';
|
||||
@@ -324,19 +358,6 @@ class ConversationsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
// Get To for new conversation draft.
|
||||
$new_conv_to = [];
|
||||
if (empty($threads[0]) || empty($threads[0]->to)) {
|
||||
// Before new conversation To field was stored in $conversation->customer_email.
|
||||
$emails = Conversation::sanitizeEmails($conversation->customer_email);
|
||||
// Get customers info for emails.
|
||||
if (count($emails)) {
|
||||
$new_conv_to = Customer::emailsToCustomers($emails);
|
||||
}
|
||||
} else {
|
||||
$new_conv_to = Customer::emailsToCustomers($threads[0]->getToArray());
|
||||
}
|
||||
|
||||
return view($template, [
|
||||
'conversation' => $conversation,
|
||||
'mailbox' => $conversation->mailbox,
|
||||
@@ -348,7 +369,7 @@ class ConversationsController extends Controller
|
||||
'to' => $new_conv_to,
|
||||
'to_customers' => $to_customers,
|
||||
'prev_conversations' => $prev_conversations,
|
||||
'cc' => $conversation->getCcArray($exclude_array),
|
||||
'cc' => $cc,
|
||||
'bcc' => [], //$conversation->getBccArray($exclude_array),
|
||||
// Data for creating a phone conversation.
|
||||
'name' => $name,
|
||||
@@ -382,13 +403,14 @@ class ConversationsController extends Controller
|
||||
|
||||
// Create conversation from thread
|
||||
$thread = null;
|
||||
$attachments = [];
|
||||
if (!empty($request->from_thread_id)) {
|
||||
$orig_thread = Thread::find($request->from_thread_id);
|
||||
if ($orig_thread) {
|
||||
if ($orig_thread && auth()->user()->can('view', $orig_thread->conversation)) {
|
||||
$subject = $orig_thread->conversation->subject;
|
||||
$subject = preg_replace('/^Fwd:/i', 'Re: ', $subject);
|
||||
|
||||
$thread = new \App\Thread();
|
||||
$thread = new Thread();
|
||||
$thread->body = $orig_thread->body;
|
||||
// If this is a forwarded message, try to fetch From
|
||||
preg_match_all("/From:[^<\n]+<([^<\n]+)>/m", html_entity_decode(strip_tags($thread->body)), $m);
|
||||
@@ -401,16 +423,33 @@ class ConversationsController extends Controller
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clone attachments.
|
||||
$orig_attachments = Attachment::where('thread_id', $orig_thread->id)->get();
|
||||
|
||||
if (count($orig_attachments)) {
|
||||
$conversation->has_attachments = true;
|
||||
$thread->has_attachments = true;
|
||||
foreach ($orig_attachments as $attachment) {
|
||||
$attachments[] = $attachment->duplicate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$to = [];
|
||||
|
||||
// Prefill some values.
|
||||
$prefill_to = \App\Email::sanitizeEmail($request->get('to'));
|
||||
if ($prefill_to) {
|
||||
$to = [$prefill_to => $prefill_to];
|
||||
if ($request->get('to')) {
|
||||
$prefill_to_emails = explode(',', $request->get('to'));
|
||||
foreach ($prefill_to_emails as $prefill_to_email) {
|
||||
$prefill_to_email = \App\Email::sanitizeEmail($prefill_to_email);
|
||||
if ($prefill_to_email) {
|
||||
$to[$prefill_to_email] = $prefill_to_email;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$conversation->subject = $subject;
|
||||
|
||||
return view('conversations/create', [
|
||||
@@ -422,6 +461,7 @@ class ConversationsController extends Controller
|
||||
'after_send' => $after_send,
|
||||
'to' => $to,
|
||||
'from_aliases' => $mailbox->getAliases(true, true),
|
||||
'attachments' => $attachments,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -469,7 +509,6 @@ class ConversationsController extends Controller
|
||||
$conversation->user_id = $orign_conv->user_id;
|
||||
$conversation->updateFolder();
|
||||
$conversation->save();
|
||||
|
||||
|
||||
$thread = Thread::createExtended([
|
||||
'conversation_id' => $orig_thread->conversation_id,
|
||||
@@ -481,8 +520,8 @@ class ConversationsController extends Controller
|
||||
'headers' => $orig_thread->headers,
|
||||
'from' => $orig_thread->from,
|
||||
'to' => $orig_thread->to,
|
||||
'cc' => $orig_thread->cc,
|
||||
'bcc' => $orig_thread->bcc,
|
||||
'cc' => $orig_thread->getCcArray(),
|
||||
'bcc' => $orig_thread->getBccArray(),
|
||||
//'attachments' => $attachments,
|
||||
'has_attachments' => $orig_thread->has_attachments,
|
||||
'message_id' => "clone".crc32(microtime()).'-'.$orig_thread->message_id,
|
||||
@@ -682,6 +721,11 @@ class ConversationsController extends Controller
|
||||
$is_phone = true;
|
||||
}
|
||||
|
||||
$is_custom = false;
|
||||
if ($type == Conversation::TYPE_CUSTOM) {
|
||||
$is_custom = true;
|
||||
}
|
||||
|
||||
$is_create = false;
|
||||
if (!empty($request->is_create)) {
|
||||
//if ($new || ($from_draft && $conversation->threads_count == 1)) {
|
||||
@@ -701,7 +745,7 @@ class ConversationsController extends Controller
|
||||
// If reply is being created from draft, there is already thread created
|
||||
$thread = null;
|
||||
$from_draft = false;
|
||||
if ((!$is_note || $is_phone) && !$response['msg'] && !empty($request->thread_id)) {
|
||||
if (( ! $is_note || $is_phone || $is_custom ) && ! $response['msg'] && ! empty($request->thread_id)) {
|
||||
$thread = Thread::find($request->thread_id);
|
||||
if ($thread && (!$conversation || $thread->conversation_id != $conversation->id)) {
|
||||
$response['msg'] = __('Incorrect thread');
|
||||
@@ -727,7 +771,7 @@ class ConversationsController extends Controller
|
||||
'cc' => 'nullable|array',
|
||||
'bcc' => 'nullable|array',
|
||||
]);
|
||||
} else {
|
||||
} elseif ($type === Conversation::TYPE_PHONE) {
|
||||
// Phone conversation.
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string',
|
||||
@@ -736,6 +780,13 @@ class ConversationsController extends Controller
|
||||
'phone' => 'nullable|string',
|
||||
'to_email' => 'nullable|string',
|
||||
]);
|
||||
} elseif ($type === Conversation::TYPE_CUSTOM) {
|
||||
$validation_rules = \Eventy::filter('conversation.custom.validation_rules', [
|
||||
'body' => 'required|string',
|
||||
'cc' => 'nullable|array',
|
||||
'bcc' => 'nullable|array',
|
||||
], $request);
|
||||
$validator = Validator::make($request->all(), $validation_rules);
|
||||
}
|
||||
} else {
|
||||
$validator = Validator::make($request->all(), [
|
||||
@@ -769,7 +820,7 @@ class ConversationsController extends Controller
|
||||
$to_array = Conversation::sanitizeEmails($request->to);
|
||||
}
|
||||
// Check To
|
||||
if (!$response['msg'] && $new && !$is_phone) {
|
||||
if (! $response['msg'] && $new && ! $is_phone && ! $is_custom) {
|
||||
if (!$to_array) {
|
||||
$response['msg'] .= __('Incorrect recipients');
|
||||
}
|
||||
@@ -777,6 +828,7 @@ class ConversationsController extends Controller
|
||||
|
||||
// Check max. message size.
|
||||
if (!$response['msg']) {
|
||||
|
||||
$max_message_size = (int)config('app.max_message_size');
|
||||
if ($max_message_size) {
|
||||
// Todo: take into account conversation history.
|
||||
@@ -784,6 +836,7 @@ class ConversationsController extends Controller
|
||||
|
||||
// Calculate attachments size.
|
||||
$attachments_ids = array_merge($request->attachments ?? [], $request->embeds ?? []);
|
||||
$attachments_ids = $this->decodeAttachmentsIds($attachments_ids);
|
||||
|
||||
if (count($attachments_ids)) {
|
||||
$attachments_to_check = Attachment::select('size')->whereIn('id', $attachments_ids)->get();
|
||||
@@ -815,6 +868,11 @@ class ConversationsController extends Controller
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$status_changed = false;
|
||||
$user_changed = false;
|
||||
// Chat conversations in chat mode can not be undone.
|
||||
$can_undo = true;
|
||||
|
||||
$request_status = (int)$request->status;
|
||||
|
||||
if ($new) {
|
||||
// New conversation
|
||||
$conversation = new Conversation();
|
||||
@@ -827,7 +885,7 @@ class ConversationsController extends Controller
|
||||
$conversation->source_type = Conversation::SOURCE_TYPE_WEB;
|
||||
} else {
|
||||
// Reply or note
|
||||
if ((int) $request->status != (int) $conversation->status) {
|
||||
if ($request_status && $request_status != (int)$conversation->status) {
|
||||
$status_changed = true;
|
||||
}
|
||||
if (!empty($request->subject)) {
|
||||
@@ -865,14 +923,16 @@ class ConversationsController extends Controller
|
||||
|
||||
$customer_email = $phone_customer_data['customer_email'];
|
||||
$customer = $phone_customer_data['customer'];
|
||||
if (!$conversation->customer_id) {
|
||||
if (! $conversation->customer_id) {
|
||||
$conversation->customer_id = $customer->id;
|
||||
}
|
||||
} elseif ($is_custom) {
|
||||
// No customer for custom conversations.
|
||||
} else {
|
||||
// Email or reply to a phone conversation.
|
||||
if (!empty($to_array)) {
|
||||
$customer_email = $to_array[0];
|
||||
} elseif (!$conversation->customer_email
|
||||
} elseif (!$conversation->customer_email
|
||||
&& ($conversation->isEmail() || $conversation->isPhone())
|
||||
&& $conversation->customer_id
|
||||
&& $conversation->customer
|
||||
@@ -894,7 +954,7 @@ class ConversationsController extends Controller
|
||||
|
||||
$prev_status = $conversation->status;
|
||||
|
||||
$conversation->status = $request->status;
|
||||
$conversation->status = $request_status ?: $conversation->status;
|
||||
|
||||
if (($prev_status != $conversation->status || $is_create)
|
||||
&& $conversation->status == Conversation::STATUS_CLOSED
|
||||
@@ -944,13 +1004,18 @@ class ConversationsController extends Controller
|
||||
|
||||
if (!$is_note && !$is_forward) {
|
||||
// Save extra recipients to CC
|
||||
$cc = Conversation::sanitizeEmails($request->cc);
|
||||
if ($is_create && !$is_multiple && count($to_array) > 1) {
|
||||
$conversation->setCc(array_merge(Conversation::sanitizeEmails($request->cc), $to_array));
|
||||
// First recipient becomes To, others - go to CC.
|
||||
$remaining_to = array_diff($to_array, [$to]);
|
||||
$cc = array_diff($cc, [$to]);
|
||||
$cc = array_merge($cc, $remaining_to);
|
||||
$conversation->setCc($cc);
|
||||
} else {
|
||||
if (!$is_multiple) {
|
||||
$conversation->setCc(array_merge(Conversation::sanitizeEmails($request->cc), [$to]));
|
||||
$conversation->setCc(array_diff($cc, [$to]));
|
||||
} else {
|
||||
$conversation->setCc(Conversation::sanitizeEmails($request->cc));
|
||||
$conversation->setCc($cc);
|
||||
}
|
||||
}
|
||||
$conversation->setBcc($request->bcc);
|
||||
@@ -1018,9 +1083,11 @@ class ConversationsController extends Controller
|
||||
$thread->first = true;
|
||||
}
|
||||
$thread->user_id = $conversation->user_id;
|
||||
$thread->status = $request->status;
|
||||
$thread->status = $request_status ?? $conversation->status;
|
||||
$thread->state = Thread::STATE_PUBLISHED;
|
||||
$thread->customer_id = $customer->id;
|
||||
if (!$is_custom) {
|
||||
$thread->customer_id = $customer->id;
|
||||
}
|
||||
$thread->created_by_user_id = auth()->user()->id;
|
||||
$thread->edited_by_user_id = null;
|
||||
$thread->edited_at = null;
|
||||
@@ -1159,7 +1226,9 @@ class ConversationsController extends Controller
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Attachment::whereIn('id', $attachments_info['attachments'])->update(['thread_id' => $thread->id]);
|
||||
Attachment::whereIn('id', $attachments_info['attachments'])
|
||||
->where('thread_id', null)
|
||||
->update(['thread_id' => $thread->id]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1170,13 +1239,17 @@ class ConversationsController extends Controller
|
||||
$user->followConversation($conversation->id);
|
||||
}
|
||||
|
||||
if ($conversation->isChat() && \Helper::isChatMode()) {
|
||||
$can_undo = false;
|
||||
}
|
||||
|
||||
// When user creates a new conversation it may be saved as draft first.
|
||||
if ($is_create) {
|
||||
// New conversation.
|
||||
event(new UserCreatedConversation($conversation, $thread));
|
||||
\Eventy::action('conversation.created_by_user_can_undo', $conversation, $thread);
|
||||
// After Conversation::UNDO_TIMOUT period trigger final event.
|
||||
\Helper::backgroundAction('conversation.created_by_user', [$conversation, $thread], now()->addSeconds(Conversation::UNDO_TIMOUT));
|
||||
\Helper::backgroundAction('conversation.created_by_user', [$conversation, $thread], now()->addSeconds($this->getUndoTimeout($can_undo)));
|
||||
} elseif ($is_forward) {
|
||||
// Forward.
|
||||
// Notifications to users not sent.
|
||||
@@ -1199,7 +1272,7 @@ class ConversationsController extends Controller
|
||||
event(new UserReplied($conversation, $thread));
|
||||
\Eventy::action('conversation.user_replied_can_undo', $conversation, $thread);
|
||||
// After Conversation::UNDO_TIMOUT period trigger final event.
|
||||
\Helper::backgroundAction('conversation.user_replied', [$conversation, $thread], now()->addSeconds(Conversation::UNDO_TIMOUT));
|
||||
\Helper::backgroundAction('conversation.user_replied', [$conversation, $thread], now()->addSeconds($this->getUndoTimeout($can_undo)));
|
||||
}
|
||||
|
||||
// Send new conversation separately to each customer.
|
||||
@@ -1243,6 +1316,9 @@ class ConversationsController extends Controller
|
||||
// Reload the conversation, otherwise Thread observer will be
|
||||
// increasing threads_count for the first conversation.
|
||||
$thread_copy->load('conversation');
|
||||
|
||||
\Eventy::action('thread.before_save_from_request', $thread_copy, $request);
|
||||
|
||||
$thread_copy->push();
|
||||
|
||||
// Copy attachments.
|
||||
@@ -1258,7 +1334,7 @@ class ConversationsController extends Controller
|
||||
event(new UserCreatedConversation($conversation_copy, $thread_copy));
|
||||
\Eventy::action('conversation.created_by_user_can_undo', $conversation_copy, $thread_copy);
|
||||
// After Conversation::UNDO_TIMOUT period trigger final event.
|
||||
\Helper::backgroundAction('conversation.created_by_user', [$conversation_copy, $thread_copy], now()->addSeconds(Conversation::UNDO_TIMOUT));
|
||||
\Helper::backgroundAction('conversation.created_by_user', [$conversation_copy, $thread_copy], now()->addSeconds($this->getUndoTimeout($can_undo)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1277,6 +1353,14 @@ class ConversationsController extends Controller
|
||||
} else {
|
||||
$flash_text = '<strong>'.__('Conversation created').'</strong>';
|
||||
}
|
||||
} elseif ($is_custom) {
|
||||
$flash_type = 'warning';
|
||||
$identifier = \Eventy::filter('conversation.custom.identifier', __('Custom conversation'), $request);
|
||||
if ($show_view_link) {
|
||||
$flash_text = __(':%tag_start%' . $identifier . ' added:%tag_end% :%view_start%View:%a_end%', $flash_vars);
|
||||
} else {
|
||||
$flash_text = '<strong>'.__('%identifier% added',['%identifier%'=>$identifier]).'</strong>';
|
||||
}
|
||||
} elseif ($is_note) {
|
||||
$flash_type = 'warning';
|
||||
if ($show_view_link) {
|
||||
@@ -1293,7 +1377,9 @@ class ConversationsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
\Session::flash('flash_'.$flash_type.'_floating', $flash_text);
|
||||
if ($can_undo) {
|
||||
\Session::flash('flash_'.$flash_type.'_floating', $flash_text);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1310,7 +1396,7 @@ class ConversationsController extends Controller
|
||||
$new = true;
|
||||
if (!$response['msg'] && !empty($request->conversation_id)) {
|
||||
$conversation = Conversation::find($request->conversation_id);
|
||||
if ($conversation && !$user->can('view', $conversation)) {
|
||||
if ($conversation && !$user->can('view', $conversation) && !$user->hasManageMailboxPermission($request->mailbox_id, Mailbox::ACCESS_PERM_ASSIGNED)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
} else {
|
||||
$new = false;
|
||||
@@ -1322,6 +1408,12 @@ class ConversationsController extends Controller
|
||||
$is_create = true;
|
||||
}
|
||||
|
||||
// If new conversation draft has been discarded (by some other user for example).
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3951
|
||||
if (!$response['msg'] && $is_create && !$conversation) {
|
||||
$new = true;
|
||||
}
|
||||
|
||||
$thread = null;
|
||||
$new_thread = true;
|
||||
if (!$response['msg'] && !empty($request->thread_id)) {
|
||||
@@ -1521,7 +1613,9 @@ class ConversationsController extends Controller
|
||||
|
||||
// Set thread_id for uploaded attachments
|
||||
if ($attachments_info['attachments']) {
|
||||
Attachment::whereIn('id', $attachments_info['attachments'])->update(['thread_id' => $thread->id]);
|
||||
Attachment::whereIn('id', $attachments_info['attachments'])
|
||||
->where('thread_id', null)
|
||||
->update(['thread_id' => $thread->id]);
|
||||
}
|
||||
|
||||
// Update folder counter.
|
||||
@@ -1624,7 +1718,7 @@ class ConversationsController extends Controller
|
||||
$attachments = [];
|
||||
foreach ($thread->attachments as $attachment) {
|
||||
$attachments[] = [
|
||||
'id' => $attachment->id,
|
||||
'id' => encrypt($attachment->id),
|
||||
'name' => $attachment->file_name,
|
||||
'size' => $attachment->size,
|
||||
'url' => $attachment->url(),
|
||||
@@ -1663,16 +1757,16 @@ class ConversationsController extends Controller
|
||||
|
||||
if ($conversation->has_attachments) {
|
||||
foreach ($conversation->threads as $thread) {
|
||||
if ($thread->has_attachments) {
|
||||
if ($thread->has_attachments && (!$thread->isDraft() || count($conversation->threads) == 1)) {
|
||||
foreach ($thread->attachments as $attachment) {
|
||||
if ($request->is_forwarding == 'true') {
|
||||
$attachment_copy = $attachment->duplicate($thread->id);
|
||||
$attachment_copy = $attachment->duplicate();
|
||||
} else {
|
||||
$attachment_copy = $attachment;
|
||||
}
|
||||
|
||||
$attachments[] = [
|
||||
'id' => $attachment_copy->id,
|
||||
'id' => encrypt($attachment_copy->id),
|
||||
'name' => $attachment_copy->file_name,
|
||||
'size' => $attachment_copy->size,
|
||||
'url' => $attachment_copy->url(),
|
||||
@@ -1717,6 +1811,14 @@ class ConversationsController extends Controller
|
||||
// Conversations navigation
|
||||
case 'conversations_pagination':
|
||||
if (!empty($request->filter)) {
|
||||
// Filter conversations by Assigned To column in Search.
|
||||
if (!empty($request->params['user_id']) && !empty($request->filter['f'])) {
|
||||
$filter = $request->filter ?? [];
|
||||
$filter['f']['assigned'] = (int)$request->params['user_id'];
|
||||
|
||||
$request->merge(['filter' => $filter]);
|
||||
}
|
||||
|
||||
$response = $this->ajaxConversationsFilter($request, $response, $user);
|
||||
} else {
|
||||
$response = $this->ajaxConversationsPagination($request, $response, $user);
|
||||
@@ -1867,6 +1969,8 @@ class ConversationsController extends Controller
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
$thread->body = \Helper::stripDangerousTags($thread->body);
|
||||
|
||||
$data = [
|
||||
'thread' => $thread
|
||||
];
|
||||
@@ -2002,27 +2106,7 @@ class ConversationsController extends Controller
|
||||
|
||||
if ($conversation->state != Conversation::STATE_DELETED) {
|
||||
// Move to Deleted folder.
|
||||
$conversation->state = Conversation::STATE_DELETED;
|
||||
$conversation->user_updated_at = date('Y-m-d H:i:s');
|
||||
$conversation->updateFolder();
|
||||
$conversation->save();
|
||||
|
||||
// Create lineitem thread
|
||||
$thread = new Thread();
|
||||
$thread->conversation_id = $conversation->id;
|
||||
$thread->user_id = $conversation->user_id;
|
||||
$thread->type = Thread::TYPE_LINEITEM;
|
||||
$thread->state = Thread::STATE_PUBLISHED;
|
||||
$thread->status = Thread::STATUS_NOCHANGE;
|
||||
$thread->action_type = Thread::ACTION_TYPE_DELETED_TICKET;
|
||||
$thread->source_via = Thread::PERSON_USER;
|
||||
$thread->source_type = Thread::SOURCE_TYPE_WEB;
|
||||
$thread->customer_id = $conversation->customer_id;
|
||||
$thread->created_by_user_id = $user->id;
|
||||
$thread->save();
|
||||
|
||||
// Remove conversation from drafts folder.
|
||||
$conversation->removeFromFolder(Folder::TYPE_DRAFTS);
|
||||
$conversation->deleteToFolder($user, false);
|
||||
} else {
|
||||
// Delete forever
|
||||
$conversation->deleteForever();
|
||||
@@ -2137,20 +2221,37 @@ class ConversationsController extends Controller
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
$merge_conversation = Conversation::find($request->merge_conversation_id);
|
||||
if (!empty($request->merge_conversation_id) && is_array($request->merge_conversation_id)) {
|
||||
|
||||
$sigle_conv = count($request->merge_conversation_id) == 1;
|
||||
|
||||
if (!$merge_conversation) {
|
||||
$response['msg'] = __('Conversation not found');
|
||||
}
|
||||
if (!$response['msg'] && !$user->can('view', $merge_conversation)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
foreach ($request->merge_conversation_id as $merge_conversation_id) {
|
||||
$merge_conversation = Conversation::find($merge_conversation_id);
|
||||
|
||||
if (!$response['msg']) {
|
||||
$conversation->mergeConversations($merge_conversation, $user);
|
||||
$response['msg'] = '';
|
||||
|
||||
$response['status'] = 'success';
|
||||
\Session::flash('flash_success_floating', __('Conversations merged'));
|
||||
if (!$merge_conversation) {
|
||||
$response['msg'] = __('Conversation not found');
|
||||
if ($sigle_conv) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$response['msg'] && !$user->can('view', $merge_conversation)) {
|
||||
$response['msg'] = __('Not enough permissions').': #'.$merge_conversation->number;
|
||||
if ($sigle_conv) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
$conversation->mergeConversations($merge_conversation, $user);
|
||||
|
||||
if ($response['status'] != 'success') {
|
||||
\Session::flash('flash_success_floating', __('Conversations merged'));
|
||||
}
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -2163,7 +2264,7 @@ class ConversationsController extends Controller
|
||||
if (!$conversation) {
|
||||
$response['msg'] = __('Conversation not found');
|
||||
}
|
||||
if (!$response['msg'] && !$conversation->mailbox->userHasAccess($user->id)) {
|
||||
if (!$response['msg'] && !$user->can('view', $conversation)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
@@ -2195,7 +2296,7 @@ class ConversationsController extends Controller
|
||||
if (!$conversation) {
|
||||
$response['msg'] = __('Conversation not found');
|
||||
}
|
||||
if (!$response['msg'] && !$conversation->mailbox->userHasAccess($user->id)) {
|
||||
if (!$response['msg'] && !$user->can('update', $conversation)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
@@ -2203,8 +2304,7 @@ class ConversationsController extends Controller
|
||||
$subject = trim($subject);
|
||||
|
||||
if (!$response['msg'] && $subject) {
|
||||
$conversation->subject = $subject;
|
||||
$conversation->save();
|
||||
$conversation->changeSubject($subject, $user);
|
||||
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
@@ -2246,7 +2346,62 @@ class ConversationsController extends Controller
|
||||
])->render();
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'retry_send':
|
||||
$thread = Thread::find($request->thread_id);
|
||||
|
||||
if (!$thread) {
|
||||
$response['msg'] = __('Thread not found');
|
||||
} elseif (!$user->can('view', $thread->conversation)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
$job_id = $thread->getFailedJobId();
|
||||
|
||||
if ($job_id) {
|
||||
\App\FailedJob::retry($job_id);
|
||||
$thread->send_status = SendLog::STATUS_ACCEPTED;
|
||||
$thread->updateSendStatusData(['msg' => '']);
|
||||
$thread->save();
|
||||
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'load_customer_info':
|
||||
$customer = Customer::getByEmail($request->customer_email);
|
||||
|
||||
if ($customer) {
|
||||
// Previous conversations
|
||||
$prev_conversations = [];
|
||||
|
||||
$mailbox = Mailbox::find($request->mailbox_id);
|
||||
|
||||
if ($mailbox && $mailbox->userHasAccess($user->id)) {
|
||||
$conversation_id = (int)$request->conversation_id ?? 0;
|
||||
|
||||
$prev_conversations = $mailbox->conversations()
|
||||
->where('customer_id', $customer->id)
|
||||
->where('id', '<>', $conversation_id)
|
||||
->where('status', '!=', Conversation::STATUS_SPAM)
|
||||
->where('state', Conversation::STATE_PUBLISHED)
|
||||
//->limit(self::PREV_CONVERSATIONS_LIMIT)
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(self::PREV_CONVERSATIONS_LIMIT);
|
||||
}
|
||||
|
||||
$response['html'] = \View::make('conversations/partials/customer_sidebar')->with([
|
||||
'customer' => $customer,
|
||||
'prev_conversations' => $prev_conversations,
|
||||
])->render();
|
||||
$response['status'] = 'success';
|
||||
} else {
|
||||
$response['msg'] = 'Customer not found';
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -2277,6 +2432,8 @@ class ConversationsController extends Controller
|
||||
return $this->ajaxHtmlMoveConv();
|
||||
case 'merge_conv':
|
||||
return $this->ajaxHtmlMergeConv();
|
||||
case 'assignee_filter':
|
||||
return $this->ajaxAssigneeFilter();
|
||||
case 'default_redirect':
|
||||
return $this->ajaxHtmlDefaultRedirect();
|
||||
}
|
||||
@@ -2415,9 +2572,11 @@ class ConversationsController extends Controller
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$mailboxes = \Eventy::filter( 'conversations.move_conv.mailboxes', $user->mailboxesCanView() );
|
||||
|
||||
return view('conversations/ajax_html/move_conv', [
|
||||
'conversation' => $conversation,
|
||||
'mailboxes' => $user->mailboxesCanView(),
|
||||
'mailboxes' => $mailboxes,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -2461,6 +2620,50 @@ class ConversationsController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter conversations by assignee.
|
||||
*/
|
||||
public function ajaxAssigneeFilter()
|
||||
{
|
||||
$users = collect([]);
|
||||
|
||||
$mailbox_id = Input::get('mailbox_id');
|
||||
$user_id = Input::get('user_id');
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
if ($mailbox_id) {
|
||||
|
||||
$mailbox = Mailbox::find($mailbox_id);
|
||||
if (!$mailbox) {
|
||||
abort(404);
|
||||
}
|
||||
if (!$user->can('view', $mailbox)) {
|
||||
\Helper::denyAccess();
|
||||
}
|
||||
// Show users having access to the mailbox.
|
||||
$users = $mailbox->usersAssignable();
|
||||
} else {
|
||||
// Show users from all accessible mailboxes.
|
||||
$mailboxes = $user->mailboxesCanView();
|
||||
foreach ($mailboxes as $mailbox) {
|
||||
$users = $users->merge($mailbox->usersAssignable())->unique('id');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$users->contains('id', $user->id)) {
|
||||
$users[] = $user;
|
||||
}
|
||||
|
||||
// Sort by full name.
|
||||
$users = User::sortUsers($users);
|
||||
|
||||
return view('conversations/ajax_html/assignee_filter', [
|
||||
'users' => $users,
|
||||
'user_id' => $user_id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change default redirect for the mailbox.
|
||||
*/
|
||||
@@ -2522,7 +2725,8 @@ class ConversationsController extends Controller
|
||||
$redirect_url = route('mailboxes.view.folder', ['id' => $conversation->mailbox_id, 'folder_id' => $folder_id]);
|
||||
break;
|
||||
case MailboxUser::AFTER_SEND_NEXT:
|
||||
$redirect_url = $conversation->urlNext(Conversation::getFolderParam());
|
||||
// We need to get not any next conversation, but ACTIVE next conversation.
|
||||
$redirect_url = $conversation->urlNext(Conversation::getFolderParam(), Conversation::STATUS_ACTIVE, true);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -2574,7 +2778,7 @@ class ConversationsController extends Controller
|
||||
if ($attachment) {
|
||||
$response['status'] = 'success';
|
||||
$response['url'] = $attachment->url();
|
||||
$response['attachment_id'] = $attachment->id;
|
||||
$response['attachment_id'] = encrypt($attachment->id);
|
||||
} else {
|
||||
$response['msg'] = __('Error occurred uploading file');
|
||||
}
|
||||
@@ -2609,6 +2813,11 @@ class ConversationsController extends Controller
|
||||
|
||||
if (!$response['msg']) {
|
||||
$query_conversations = Conversation::getQueryByFolder($folder, $user->id);
|
||||
|
||||
if (!empty($request->params['user_id'])) {
|
||||
$query_conversations->where('conversations.user_id', (int)$request->params['user_id']);
|
||||
}
|
||||
|
||||
$conversations = $folder->queryAddOrderBy($query_conversations)->paginate(Conversation::DEFAULT_LIST_SIZE, ['*'], 'page', $request->page);
|
||||
}
|
||||
|
||||
@@ -2617,6 +2826,7 @@ class ConversationsController extends Controller
|
||||
$response['html'] = view('conversations/conversations_table', [
|
||||
'folder' => $folder,
|
||||
'conversations' => $conversations,
|
||||
'params' => $request->params ?? [],
|
||||
])->render();
|
||||
|
||||
return $response;
|
||||
@@ -2792,6 +3002,8 @@ class ConversationsController extends Controller
|
||||
*/
|
||||
public function searchCustomers($request, $user)
|
||||
{
|
||||
$limited_visibility = config('app.limit_user_customer_visibility') && !$user->isAdmin();
|
||||
|
||||
// Get IDs of mailboxes to which user has access
|
||||
$mailbox_ids = $user->mailboxesIdsCanView();
|
||||
|
||||
@@ -2818,9 +3030,9 @@ class ConversationsController extends Controller
|
||||
|
||||
$query->where('customers.first_name', $like_op, $like)
|
||||
->orWhere('customers.last_name', $like_op, $like)
|
||||
->orWhere(\Helper::isPgSql() ? \DB::raw('(customers.first_name || \' \' || customers.last_name)') : \DB::raw('CONCAT(customers.first_name, " ", customers.last_name)'), $like_op, $like)
|
||||
->orWhere('customers.company', $like_op, $like)
|
||||
->orWhere('customers.job_title', $like_op, $like)
|
||||
->orWhere('customers.phones', $like_op, '%"'.\Helper::phoneToNumeric($q).'"%')
|
||||
->orWhere('customers.websites', $like_op, $like)
|
||||
->orWhere('customers.social_profiles', $like_op, $like)
|
||||
->orWhere('customers.address', $like_op, $like)
|
||||
@@ -2828,6 +3040,12 @@ class ConversationsController extends Controller
|
||||
->orWhere('customers.state', $like_op, $like)
|
||||
->orWhere('customers.zip', $like_op, $like)
|
||||
->orWhere('emails.email', $like_op, $like);
|
||||
|
||||
$phone_numeric = \Helper::phoneToNumeric($q);
|
||||
|
||||
if ($phone_numeric) {
|
||||
$query->orWhere('customers.phones', $like_op, '%"'.$phone_numeric.'"%');
|
||||
}
|
||||
});
|
||||
|
||||
if (!empty($filters['mailbox']) && in_array($filters['mailbox'], $mailbox_ids)) {
|
||||
@@ -2836,6 +3054,12 @@ class ConversationsController extends Controller
|
||||
//$join->on('conversations.mailbox_id', '=', $filters['mailbox']);
|
||||
});
|
||||
$query_customers->where('conversations.mailbox_id', '=', $filters['mailbox']);
|
||||
} elseif ($limited_visibility) {
|
||||
// Force only mailboxes the user has access to.
|
||||
$query_customers->join('conversations', function ($join) use ($filters) {
|
||||
$join->on('conversations.customer_id', '=', 'customers.id');
|
||||
});
|
||||
$query_customers->whereIn('conversations.mailbox_id', $mailbox_ids);
|
||||
}
|
||||
|
||||
$query_customers = \Eventy::filter('search.customers.apply_filters', $query_customers, $filters, $q);
|
||||
@@ -2874,6 +3098,7 @@ class ConversationsController extends Controller
|
||||
$response['html'] = view('conversations/conversations_table', [
|
||||
'conversations' => $conversations,
|
||||
'params' => $request->params ?? [],
|
||||
'conversations_filter' => $request->filter['f'] ?? $request->filter ?? [],
|
||||
])->render();
|
||||
|
||||
return $response;
|
||||
@@ -2898,6 +3123,10 @@ class ConversationsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($request->params['user_id'])) {
|
||||
$query_conversations->where('conversations.user_id', (int)$request->params['user_id']);
|
||||
}
|
||||
|
||||
return $query_conversations->paginate(Conversation::DEFAULT_LIST_SIZE);
|
||||
}
|
||||
|
||||
@@ -2910,17 +3139,20 @@ class ConversationsController extends Controller
|
||||
$attachments = [];
|
||||
if (!empty($request->attachments_all)) {
|
||||
$embeds = [];
|
||||
$attachments_all = $this->decodeAttachmentsIds($request->attachments_all);
|
||||
if (!empty($request->attachments)) {
|
||||
$attachments = $request->attachments;
|
||||
$attachments = $this->decodeAttachmentsIds($request->attachments);
|
||||
}
|
||||
if (!empty($request->embeds)) {
|
||||
$embeds = $request->embeds;
|
||||
$embeds = $this->decodeAttachmentsIds($request->embeds);
|
||||
}
|
||||
if (count($attachments) != count($embeds)) {
|
||||
$attachments_to_remove = array_diff($attachments_all, $attachments);
|
||||
$attachments_to_remove = array_diff($attachments_to_remove, $embeds);
|
||||
if (count($attachments)
|
||||
&& count($attachments) != count($embeds)
|
||||
) {
|
||||
$has_attachments = true;
|
||||
}
|
||||
$attachments_to_remove = array_diff($request->attachments_all, $attachments);
|
||||
$attachments_to_remove = array_diff($attachments_to_remove, $embeds);
|
||||
Attachment::deleteByIds($attachments_to_remove);
|
||||
}
|
||||
|
||||
@@ -2930,6 +3162,20 @@ class ConversationsController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
public function decodeAttachmentsIds($attachments_list)
|
||||
{
|
||||
foreach ($attachments_list as $i => $attachment_id) {
|
||||
$attachment_id_decrypted = \Helper::decrypt($attachment_id);
|
||||
if ($attachment_id_decrypted == $attachment_id) {
|
||||
unset($attachments_list[$i]);
|
||||
} else {
|
||||
$attachments_list[$i] = $attachment_id_decrypted;
|
||||
}
|
||||
}
|
||||
|
||||
return $attachments_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo reply.
|
||||
*/
|
||||
@@ -2988,6 +3234,10 @@ class ConversationsController extends Controller
|
||||
if ($thread->first) {
|
||||
// This was a new conversation, move it to drafts
|
||||
$conversation->state = Thread::STATE_DRAFT;
|
||||
|
||||
// Add a record to the conversation_folder table.
|
||||
$conversation->addToFolder(Folder::TYPE_DRAFTS);
|
||||
|
||||
$conversation->updateFolder();
|
||||
$conversation->mailbox->updateFoldersCounters();
|
||||
$folder_id = null;
|
||||
@@ -3031,7 +3281,9 @@ class ConversationsController extends Controller
|
||||
if (!empty($name_parts[1])) {
|
||||
$customer_data['last_name'] = $name_parts[1];
|
||||
}
|
||||
$customer_data['phones'] = [$request_phone];
|
||||
if ($request_phone) {
|
||||
$customer_data['phones'] = [$request_phone];
|
||||
}
|
||||
}
|
||||
|
||||
// Check if name field contains ID of the customer.
|
||||
@@ -3125,4 +3377,13 @@ class ConversationsController extends Controller
|
||||
'mailbox' => $mailbox,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getUndoTimeout($can_undo)
|
||||
{
|
||||
if ($can_undo) {
|
||||
return Conversation::UNDO_TIMOUT;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,12 @@ class CustomersController extends Controller
|
||||
/**
|
||||
* Edit customer.
|
||||
*/
|
||||
public function update($id)
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$customer = Customer::findOrFail($id);
|
||||
|
||||
$this->checkLimitVisibility($customer);
|
||||
|
||||
$customer_emails = $customer->emails;
|
||||
if (count($customer_emails)) {
|
||||
foreach ($customer_emails as $row) {
|
||||
@@ -56,6 +58,8 @@ class CustomersController extends Controller
|
||||
$customer = Customer::findOrFail($id);
|
||||
$flash_message = '';
|
||||
|
||||
$this->checkLimitVisibility($customer);
|
||||
|
||||
// First name or email must be specified
|
||||
$validator = Validator::make($request->all(), [
|
||||
'first_name' => 'nullable|string|max:255|required_without:emails.0',
|
||||
@@ -77,7 +81,7 @@ class CustomersController extends Controller
|
||||
// Photo
|
||||
$validator->after(function ($validator) use ($customer, $request) {
|
||||
if ($request->hasFile('photo_url')) {
|
||||
$path_url = $customer->savePhoto($request->file('photo_url')->getRealPath(), $request->file('photo_url')->getMimeType());
|
||||
$path_url = $customer->savePhoto($request->file('photo_url')->getRealPath() ?: $request->file('photo_url')->getPathname(), $request->file('photo_url')->getMimeType());
|
||||
|
||||
if ($path_url) {
|
||||
$customer->photo_url = $path_url;
|
||||
@@ -126,7 +130,7 @@ class CustomersController extends Controller
|
||||
'email' => $email->email,
|
||||
'tag_email_begin' => '<strong>',
|
||||
'tag_email_end' => '</strong>',
|
||||
'customer' => $email->customer->getFullName(),
|
||||
'customer' => htmlspecialchars($email->customer->getFullName()),
|
||||
'a_begin' => '<strong><a href="'.$email->customer->url().'" target="_blank">',
|
||||
'a_end' => '</a></strong>',
|
||||
]).' ';
|
||||
@@ -147,6 +151,15 @@ class CustomersController extends Controller
|
||||
if (isset($request_data['photo_url'])) {
|
||||
unset($request_data['photo_url']);
|
||||
}
|
||||
$nonfillable_fields = [
|
||||
'channel',
|
||||
'channel_id',
|
||||
];
|
||||
foreach ($nonfillable_fields as $field) {
|
||||
if (isset($request_data[$field])) {
|
||||
unset($request_data[$field]);
|
||||
}
|
||||
}
|
||||
|
||||
$customer->setData($request_data);
|
||||
// Websites
|
||||
@@ -193,6 +206,24 @@ class CustomersController extends Controller
|
||||
return redirect()->route('customers.update', ['id' => $id]);
|
||||
}
|
||||
|
||||
public function checkLimitVisibility($customer)
|
||||
{
|
||||
$user = auth()->user();
|
||||
$limited_visibility = config('app.limit_user_customer_visibility') && !$user->isAdmin();
|
||||
|
||||
if ($limited_visibility) {
|
||||
$mailbox_ids = $user->mailboxesIdsCanView();
|
||||
|
||||
$accesible = Conversation::where('customer_id', $customer->id)
|
||||
->whereIn('conversations.mailbox_id', $mailbox_ids)
|
||||
->exists();
|
||||
|
||||
if (!$accesible) {
|
||||
\Helper::denyAccess();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User mailboxes.
|
||||
*/
|
||||
@@ -233,11 +264,18 @@ class CustomersController extends Controller
|
||||
{
|
||||
$customer = Customer::findOrFail($id);
|
||||
|
||||
$conversations = $customer->conversations()
|
||||
$query = $customer->conversations()
|
||||
->where('customer_id', $customer->id)
|
||||
->whereIn('mailbox_id', auth()->user()->mailboxesIdsCanView())
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(Conversation::DEFAULT_LIST_SIZE);
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
if ($user->canSeeOnlyAssignedConversations()) {
|
||||
$query->where('user_id', '=', $user->id);
|
||||
}
|
||||
|
||||
$conversations = $query->paginate(Conversation::DEFAULT_LIST_SIZE);
|
||||
|
||||
return view('customers/conversations', [
|
||||
'customer' => $customer,
|
||||
@@ -257,6 +295,9 @@ class CustomersController extends Controller
|
||||
|
||||
$q = $request->q;
|
||||
|
||||
$user = auth()->user();
|
||||
$limited_visibility = config('app.limit_user_customer_visibility') && !$user->isAdmin();
|
||||
|
||||
$join_emails = false;
|
||||
if ($request->search_by == 'all' || $request->search_by == 'email' || $request->exclude_email) {
|
||||
$join_emails = true;
|
||||
@@ -279,18 +320,36 @@ class CustomersController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->search_by == 'all' || $request->search_by == 'email') {
|
||||
$customers_query->where('emails.email', 'like', '%'.$q.'%');
|
||||
}
|
||||
if ($request->exclude_email) {
|
||||
$customers_query->where('emails.email', '<>', $request->exclude_email);
|
||||
}
|
||||
if ($request->search_by == 'all' || $request->search_by == 'name') {
|
||||
$customers_query->orWhere('first_name', 'like', '%'.$q.'%')
|
||||
->orWhere('last_name', 'like', '%'.$q.'%');
|
||||
}
|
||||
if ($request->search_by == 'phone') {
|
||||
$customers_query->where('customers.phones', 'like', '%'.$q.'%');
|
||||
$customers_query->where(function ($query) use ($q, $request) {
|
||||
if ($request->search_by == 'all' || $request->search_by == 'email') {
|
||||
$query->where('emails.email', 'like', '%'.$q.'%');
|
||||
}
|
||||
if ($request->exclude_email) {
|
||||
$query->where('emails.email', '<>', $request->exclude_email);
|
||||
}
|
||||
if ($request->exclude_id) {
|
||||
$query->where('customers.id', '<>', $request->exclude_id);
|
||||
}
|
||||
if ($request->search_by == 'all' || $request->search_by == 'name') {
|
||||
$query->orWhere('first_name', 'like', '%'.$q.'%')
|
||||
->orWhere('last_name', 'like', '%'.$q.'%')
|
||||
->orWhere(\Helper::isPgSql() ? \DB::raw('(first_name || \' \' || last_name)') : \DB::raw('CONCAT(first_name, " ", last_name)'), 'like', '%'.$q.'%');
|
||||
}
|
||||
if ($request->search_by == 'phone') {
|
||||
$phone_numeric = \Helper::phoneToNumeric($q);
|
||||
if (!$phone_numeric) {
|
||||
$phone_numeric = $q;
|
||||
}
|
||||
$query->where('customers.phones', 'like', '%'.$phone_numeric.'%');
|
||||
}
|
||||
});
|
||||
|
||||
if ($limited_visibility) {
|
||||
$mailbox_ids = $user->mailboxesIdsCanView();
|
||||
|
||||
$customers_query->join('conversations', 'conversations.customer_id', '=', 'customers.id');
|
||||
$customers_query->whereIn('conversations.mailbox_id', $mailbox_ids);
|
||||
$customers_query->groupby('customers.id');
|
||||
}
|
||||
|
||||
$customers = $customers_query->paginate(20);
|
||||
@@ -311,7 +370,8 @@ class CustomersController extends Controller
|
||||
// Get phone which matches
|
||||
$phones = $customer->getPhones();
|
||||
foreach ($phones as $phone) {
|
||||
if (strstr($phone['value'], $q)) {
|
||||
$phone_numeric = \Helper::phoneToNumeric($q);
|
||||
if (strstr($phone['value'], $q) || strstr($phone['n'] ?? '', $phone_numeric)) {
|
||||
$text = $phone['value'];
|
||||
if ($customer->getFullName()) {
|
||||
$text .= ' — '.$customer->getFullName();
|
||||
@@ -333,7 +393,8 @@ class CustomersController extends Controller
|
||||
if (!empty($request->use_id)) {
|
||||
$id = $customer->id;
|
||||
} else {
|
||||
$id = $customer->email;
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/4057
|
||||
$id = $customer->email ?? $customer->getMainEmail();
|
||||
}
|
||||
}
|
||||
$response['results'][] = [
|
||||
@@ -361,14 +422,21 @@ class CustomersController extends Controller
|
||||
|
||||
switch ($request->action) {
|
||||
|
||||
// Change conversation user
|
||||
// Change conversation customer.
|
||||
case 'create':
|
||||
// First name or email must be specified
|
||||
$validator = Validator::make($request->all(), [
|
||||
$validator_config = [
|
||||
'first_name' => 'required|string|max:255',
|
||||
'last_name' => 'nullable|string|max:255',
|
||||
'email' => 'required|email|unique:emails,email',
|
||||
]);
|
||||
];
|
||||
|
||||
$limited_visibility = config('app.limit_user_customer_visibility') && !$user->isAdmin();
|
||||
if ($limited_visibility) {
|
||||
$validator_config['email'] = 'required|email';
|
||||
}
|
||||
|
||||
// First name or email must be specified.
|
||||
$validator = Validator::make($request->all(), $validator_config);
|
||||
|
||||
if ($validator->fails()) {
|
||||
foreach ($validator->errors()->getMessages()as $errors) {
|
||||
@@ -411,4 +479,41 @@ class CustomersController extends Controller
|
||||
|
||||
return \Response::json($response);
|
||||
}
|
||||
}
|
||||
|
||||
public function merge(Request $request, $id)
|
||||
{
|
||||
$customer = Customer::findOrFail($id);
|
||||
|
||||
// $customers = Customer::where('id', '!=', $id)
|
||||
// ->orderBy('first_name')
|
||||
// ->orderBy('last_name')
|
||||
// ->get();
|
||||
|
||||
return view('customers/merge', ['customer' => $customer]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge handling function.
|
||||
*/
|
||||
public function mergeSave(Request $request, $id)
|
||||
{
|
||||
$request->validate([
|
||||
'customer2_id' => 'required|exists:customers,id',
|
||||
//'keep_attributes' => 'array'
|
||||
]);
|
||||
|
||||
$customer = Customer::findOrFail($id);
|
||||
$customer2 = Customer::find($request->customer2_id);
|
||||
|
||||
// Ensure customers are different
|
||||
if ($id === $customer2->id) {
|
||||
return redirect()->back()->with('error', __('Cannot merge the same customer'));
|
||||
}
|
||||
|
||||
$customer->mergeWith($customer2/*, $request->keep_attributes ?? []*/);
|
||||
|
||||
\Session::flash('flash_success_floating', __('Customers merged successfully'));
|
||||
|
||||
return redirect()->route('customers.update', ['id' => $id]);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
|
||||
use App\Conversation;
|
||||
use App\Folder;
|
||||
use App\Mailbox;
|
||||
use App\MailboxUser;
|
||||
use App\Thread;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -85,8 +86,14 @@ class MailboxesController extends Controller
|
||||
->withInput();
|
||||
}
|
||||
|
||||
$request_data = [
|
||||
'email' => $request->email,
|
||||
'name' => trim(strip_tags($request->name)),
|
||||
'ratings' => $request->ratings ?? 1,
|
||||
];
|
||||
|
||||
$mailbox = new Mailbox();
|
||||
$mailbox->fill($request->all());
|
||||
$mailbox->fill($request_data);
|
||||
|
||||
$mailbox->save();
|
||||
|
||||
@@ -105,6 +112,8 @@ class MailboxesController extends Controller
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$user = auth()->user();
|
||||
|
||||
// Redirect to the accessible page.
|
||||
if (!$user->can('updateSettings', $mailbox) && !$user->can('updateEmailSignature', $mailbox)) {
|
||||
$accessible_route = '';
|
||||
|
||||
@@ -159,11 +168,16 @@ class MailboxesController extends Controller
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
$can_update_settings = $user->can('updateSettings', $mailbox);
|
||||
$can_update_signature = $user->can('updateEmailSignature', $mailbox);
|
||||
|
||||
if (!$user->can('updateSettings', $mailbox) && !$user->can('updateEmailSignature', $mailbox)) {
|
||||
if (!$can_update_settings && !$can_update_signature) {
|
||||
\Helper::denyAccess();
|
||||
}
|
||||
|
||||
$allowed_fields = [];
|
||||
|
||||
if ($user->can('updateSettings', $mailbox)) {
|
||||
|
||||
// Checkboxes
|
||||
@@ -182,7 +196,7 @@ class MailboxesController extends Controller
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:40',
|
||||
'email' => 'required|string|email|max:128|unique:mailboxes,email,'.$id,
|
||||
'aliases' => 'nullable|string|max:255',
|
||||
'aliases' => 'nullable|string',
|
||||
'from_name' => 'required|integer',
|
||||
'from_name_custom' => 'nullable|string|max:128',
|
||||
'ticket_status' => 'required|integer',
|
||||
@@ -203,9 +217,22 @@ class MailboxesController extends Controller
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
}
|
||||
|
||||
$allowed_fields = [
|
||||
'name',
|
||||
'email',
|
||||
'aliases',
|
||||
'aliases_reply',
|
||||
'auto_bcc',
|
||||
'from_name',
|
||||
'from_name_custom',
|
||||
'ticket_status',
|
||||
'ticket_assignee',
|
||||
'before_reply',
|
||||
];
|
||||
}
|
||||
|
||||
if ($user->can('updateEmailSignature', $mailbox)) {
|
||||
if ($can_update_signature) {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'signature' => 'nullable|string',
|
||||
]);
|
||||
@@ -215,11 +242,29 @@ class MailboxesController extends Controller
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
}
|
||||
$allowed_fields[] = 'signature';
|
||||
}
|
||||
|
||||
\Eventy::action('mailbox.settings_before_save', $mailbox, $request);
|
||||
|
||||
$mailbox->fill($request->all());
|
||||
// Leave only allowed fields.
|
||||
$fields = $request->all();
|
||||
foreach ($fields as $field_name => $value) {
|
||||
if (!in_array($field_name, $allowed_fields)) {
|
||||
unset($fields[$field_name]);
|
||||
}
|
||||
}
|
||||
|
||||
$mailbox->fill($fields);
|
||||
|
||||
// Chat: Start a new conversation when receiving a reply to the closed / deleted Chat conversation.
|
||||
if (!empty($request->chat_start_new)) {
|
||||
$mailbox->setMetaParam('chat_start_new', true);
|
||||
} else {
|
||||
$mailbox->removeMetaParam('chat_start_new');
|
||||
}
|
||||
|
||||
$mailbox->signature = \Helper::stripDangerousTags($mailbox->signature);
|
||||
|
||||
$mailbox->save();
|
||||
|
||||
@@ -268,8 +313,24 @@ class MailboxesController extends Controller
|
||||
$this->authorize('updatePermissions', $mailbox);
|
||||
|
||||
$user = auth()->user();
|
||||
$user_ids = \Eventy::filter('mailbox.permission_users', $request->users ?? [], $id);
|
||||
|
||||
// Get pivot data from mailbox_user to pass to sync() not to loose values.
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4339
|
||||
$users_with_settings = [];
|
||||
$mailbox_users = MailboxUser::where('mailbox_id', $id)
|
||||
//->whereIn('user_id', $user_ids)
|
||||
->get()
|
||||
->keyBy('user_id')
|
||||
->toArray();
|
||||
foreach ($user_ids as $user_id) {
|
||||
$users_with_settings[$user_id] = $mailbox_users[$user_id] ?? [];
|
||||
}
|
||||
|
||||
// Settings for admins are being reset here.
|
||||
// So we restore them below.
|
||||
$mailbox->users()->sync($users_with_settings);
|
||||
|
||||
$mailbox->users()->sync(\Eventy::filter('mailbox.permission_users', $request->users, $id) ?: []);
|
||||
$mailbox->syncPersonalFolders($request->users);
|
||||
|
||||
// Save admins settings.
|
||||
@@ -281,6 +342,15 @@ class MailboxesController extends Controller
|
||||
$admin->mailboxes()->attach($id);
|
||||
$mailbox_user = $admin->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||
}
|
||||
// Restore settings for admins.
|
||||
if (!empty($mailbox_users[$admin->id])) {
|
||||
$admin_settings = $mailbox_users[$admin->id];
|
||||
foreach (MailboxUser::$pivot_settings as $pivot_parameter) {
|
||||
if (isset($admin_settings[$pivot_parameter])) {
|
||||
$mailbox_user->settings->$pivot_parameter = $admin_settings[$pivot_parameter];
|
||||
}
|
||||
}
|
||||
}
|
||||
$mailbox_user->settings->hide = (isset($request->managers[$admin->id]['hide']) ? (int)$request->managers[$admin->id]['hide'] : false);
|
||||
$mailbox_user->settings->save();
|
||||
}
|
||||
@@ -560,7 +630,14 @@ class MailboxesController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$mailbox->fill($request->all());
|
||||
$data = [
|
||||
'auto_reply_enabled' => $request->auto_reply_enabled,
|
||||
'auto_reply_subject' => $request->auto_reply_subject,
|
||||
'auto_reply_message' => $request->auto_reply_message,
|
||||
];
|
||||
|
||||
$mailbox->fill($data);
|
||||
$mailbox->auto_reply_message = \Helper::stripDangerousTags($mailbox->auto_reply_message);
|
||||
|
||||
$mailbox->save();
|
||||
|
||||
@@ -622,17 +699,22 @@ class MailboxesController extends Controller
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
$test_result = false;
|
||||
$test_result = [
|
||||
'status' => 'error'
|
||||
];
|
||||
|
||||
try {
|
||||
$test_result = \App\Misc\Mail::sendTestMail($request->to, $mailbox);
|
||||
$test_result = \MailHelper::sendTestMail($request->to, $mailbox);
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = $e->getMessage();
|
||||
$test_result['msg'] = $e->getMessage();
|
||||
}
|
||||
|
||||
if (!$test_result && !$response['msg']) {
|
||||
$response['msg'] = __('Error occurred sending email. Please check your mail server logs for more details.');
|
||||
if ($test_result['status'] == 'error') {
|
||||
$response['msg'] = $test_result['msg']
|
||||
?: __('Error occurred sending email. Please check your mail server logs for more details.');
|
||||
}
|
||||
|
||||
$response['log'] = $test_result['log'] ?? '';
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
@@ -670,13 +752,14 @@ class MailboxesController extends Controller
|
||||
if (!$response['msg'] && !$tested) {
|
||||
$test_result = false;
|
||||
|
||||
try {
|
||||
$test_result = \MailHelper::fetchTest($mailbox);
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = $e->getMessage();
|
||||
}
|
||||
$test_result = \MailHelper::fetchTest($mailbox);
|
||||
|
||||
if (!$test_result && !$response['msg']) {
|
||||
$response['log'] = $test_result['log'] ?? '';
|
||||
|
||||
if ($test_result['result'] != 'success' && $test_result['msg']) {
|
||||
$response['msg'] = $test_result['msg'];
|
||||
}
|
||||
if ($test_result['result'] != 'success' && !$response['msg']) {
|
||||
$response['msg'] = __('Error occurred connecting to the server');
|
||||
}
|
||||
}
|
||||
@@ -706,28 +789,33 @@ class MailboxesController extends Controller
|
||||
|
||||
$imap_folders = $client->getFolders();
|
||||
|
||||
$response['folders'] = [];
|
||||
|
||||
if (count($imap_folders)) {
|
||||
foreach ($imap_folders as $imap_folder) {
|
||||
if (!empty($imap_folder->name)) {
|
||||
$response['folders'][] = $imap_folder->name;
|
||||
}
|
||||
// Maybe we need a recursion here.
|
||||
if (!empty($imap_folder->children)) {
|
||||
foreach ($imap_folder->children as $child_imap_folder) {
|
||||
// Old library.
|
||||
if (!empty($child_imap_folder->fullName)) {
|
||||
$response['folders'][] = $child_imap_folder->fullName;
|
||||
}
|
||||
// New library.
|
||||
if (!empty($child_imap_folder->full_name)) {
|
||||
$response['folders'][] = $child_imap_folder->full_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$response = $this->interateFolders($response, $imap_folders);
|
||||
$response['folders'] = array_values(array_unique($response['folders']));
|
||||
}
|
||||
|
||||
if (count($response['folders'])) {
|
||||
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3933
|
||||
// Exclude duplicate INBOX.Name and Name folders.
|
||||
// $folder_excluded = false;
|
||||
// foreach ($response['folders'] as $i => $folder_name) {
|
||||
// if (\Str::startsWith($folder_name, 'INBOX.')) {
|
||||
// foreach ($response['folders'] as $folder_name2) {
|
||||
// if ($folder_name != $folder_name2 && $folder_name == 'INBOX.'.$folder_name2) {
|
||||
// unset($response['folders'][$i]);
|
||||
// $folder_excluded = true;
|
||||
// continue 2;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if ($folder_excluded) {
|
||||
// $response['folders'] = array_values($response['folders']);
|
||||
// }
|
||||
|
||||
$response['msg_success'] = __('IMAP folders retrieved: '.implode(', ', $response['folders']));
|
||||
} else {
|
||||
$response['msg_success'] = __('Connected, but no IMAP folders found');
|
||||
@@ -780,17 +868,23 @@ class MailboxesController extends Controller
|
||||
|
||||
// Mute notifications
|
||||
case 'mute':
|
||||
if (!$user) {
|
||||
\Helper::denyAccess();
|
||||
}
|
||||
|
||||
$mailbox = Mailbox::find($request->mailbox_id);
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
} elseif (!$user->can('view', $mailbox)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
|
||||
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $mailbox->id)->first();
|
||||
if (!$mailbox_user) {
|
||||
// User may not be connected to the mailbox yet
|
||||
if (!$mailbox_user && $user->isAdmin()) {
|
||||
// Admin may not be connected to the mailbox yet
|
||||
$user->mailboxes()->attach($mailbox->id);
|
||||
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $mailbox->id)->first();
|
||||
}
|
||||
@@ -813,10 +907,35 @@ class MailboxesController extends Controller
|
||||
return \Response::json($response);
|
||||
}
|
||||
|
||||
// Recursively interate over folders.
|
||||
public function interateFolders($response, $imap_folders, $subfolder = false) {
|
||||
foreach ($imap_folders as $imap_folder) {
|
||||
if (!empty($imap_folder->name) && !$subfolder) {
|
||||
$response['folders'][] = $imap_folder->name;
|
||||
}
|
||||
|
||||
// Check for children and recurse.
|
||||
if (!empty($imap_folder->children)) {
|
||||
$response = $this->interateFolders($response, $imap_folder->children, true);
|
||||
}
|
||||
|
||||
// Old library.
|
||||
if (!empty($imap_folder->fullName)) {
|
||||
$response['folders'][] = $imap_folder->fullName;
|
||||
} elseif (!empty($imap_folder->full_name)) {
|
||||
// New library.
|
||||
$response['folders'][] = $imap_folder->full_name;
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function oauth(Request $request)
|
||||
{
|
||||
$mailbox_id = $request->id ?? '';
|
||||
$provider = $request->provider ?? '';
|
||||
$in_out = $request->in_out ?? 'in';
|
||||
|
||||
$state_data = [];
|
||||
if (!empty($request->state)) {
|
||||
@@ -827,6 +946,9 @@ class MailboxesController extends Controller
|
||||
if (!empty($state_data['provider'])) {
|
||||
$provider = $state_data['provider'];
|
||||
}
|
||||
if (!empty($state_data['in_out'])) {
|
||||
$in_out = $state_data['in_out'];
|
||||
}
|
||||
}
|
||||
|
||||
// MS Exchange.
|
||||
@@ -844,10 +966,17 @@ class MailboxesController extends Controller
|
||||
if (empty($mailbox)) {
|
||||
return __('Mailbox not found').': '.$mailbox_id;
|
||||
}
|
||||
if (empty($mailbox->in_username)) {
|
||||
if ($in_out == 'in') {
|
||||
$username = $mailbox->in_username;
|
||||
$password = $mailbox->in_password;
|
||||
} else {
|
||||
$username = $mailbox->out_username;
|
||||
$password = $mailbox->out_password;
|
||||
}
|
||||
if (empty($username)) {
|
||||
return 'Enter oAuth Client ID as Username and save mailbox settings';
|
||||
}
|
||||
if (empty($mailbox->in_password)) {
|
||||
if (empty($password)) {
|
||||
return 'Enter oAuth Client Secret as Password and save mailbox settings';
|
||||
}
|
||||
|
||||
@@ -857,14 +986,16 @@ class MailboxesController extends Controller
|
||||
}
|
||||
|
||||
if (empty($request->code)) {
|
||||
// Start.
|
||||
$state = [
|
||||
'provider' => $provider,
|
||||
'mailbox_id' => $mailbox_id,
|
||||
'state' => crc32($mailbox->in_username.$mailbox->in_password),
|
||||
'in_out' => $in_out,
|
||||
'state' => crc32($username.$password),
|
||||
];
|
||||
$url = \MailHelper::oauthGetAuthorizationUrl(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
'state' => json_encode($state),
|
||||
'client_id' => $mailbox->in_username,
|
||||
'client_id' => $username,
|
||||
]);
|
||||
if ($url) {
|
||||
\Session::put('mailbox_oauth_'.$provider.'_'.$mailbox_id, $state);
|
||||
@@ -885,21 +1016,40 @@ class MailboxesController extends Controller
|
||||
return 'Invalid oAuth state';
|
||||
|
||||
} else {
|
||||
|
||||
// state is set.
|
||||
// Try to get an access token (using the authorization code grant)
|
||||
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
'client_id' => $mailbox->in_username,
|
||||
'client_secret' => $mailbox->in_password,
|
||||
'client_id' => $username,
|
||||
'client_secret' => $password,
|
||||
'code' => $request->code,
|
||||
]);
|
||||
|
||||
if (!empty($token_data['a_token'])) {
|
||||
// Set username and password for the oppozite in_out.
|
||||
if ($in_out == 'in') {
|
||||
if (empty($mailbox->out_server)
|
||||
|| (trim($mailbox->out_server) == \MailHelper::OAUTH_MICROSOFT_SMTP
|
||||
&& (!$mailbox->out_username || $mailbox->out_username == $username))
|
||||
) {
|
||||
$mailbox->out_username = $username;
|
||||
$mailbox->out_password = $password;
|
||||
}
|
||||
//$mailbox->out_method = Mailbox::OUT_METHOD_SMTP;
|
||||
} else {
|
||||
$mailbox->in_username = $username;
|
||||
$mailbox->in_password = $password;
|
||||
}
|
||||
$mailbox->setMetaParam('oauth', $token_data, true);
|
||||
} elseif (!empty($token_data['error'])) {
|
||||
return __('Error occurred').': '.htmlspecialchars($token_data['error']);
|
||||
}
|
||||
|
||||
return redirect()->route('mailboxes.connection.incoming', ['id' => $mailbox_id]);
|
||||
if ($in_out == 'in') {
|
||||
$route = 'mailboxes.connection.incoming';
|
||||
} else {
|
||||
$route = 'mailboxes.connection';
|
||||
}
|
||||
return redirect()->route($route, ['id' => $mailbox_id]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -907,12 +1057,20 @@ class MailboxesController extends Controller
|
||||
{
|
||||
$mailbox_id = $request->id ?? '';
|
||||
$provider = $request->provider ?? '';
|
||||
$in_out = $request->in_out ?? 'in';
|
||||
|
||||
$mailbox = Mailbox::findOrFail($mailbox_id);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
// oAuth Disconnect.
|
||||
$mailbox->removeMetaParam('oauth', true);
|
||||
return \MailHelper::oauthDisconnect($provider, route('mailboxes.connection.incoming', ['id' => $mailbox_id]));
|
||||
|
||||
if ($in_out == 'in') {
|
||||
$route = 'mailboxes.connection.incoming';
|
||||
} else {
|
||||
$route = 'mailboxes.connection';
|
||||
}
|
||||
|
||||
return \MailHelper::oauthDisconnect($provider, route($route, ['id' => $mailbox_id]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,9 @@ class ModulesController extends Controller
|
||||
'installed' => true,
|
||||
'activated' => \App\Module::isLicenseActivated($module->getAlias(), $module->get('authorUrl')),
|
||||
'license' => \App\Module::getLicense($module->getAlias()),
|
||||
// Update configuration for third party modules
|
||||
'latestVersionNumberUrl' => $module->get('latestVersionUrl'),
|
||||
'latestVersionZipUrl' => $module->get('latestVersionZipUrl'),
|
||||
// Determined later
|
||||
'new_version' => '',
|
||||
];
|
||||
@@ -128,7 +131,7 @@ class ModulesController extends Controller
|
||||
$modules_directory[$i_dir]['active'] = \App\Module::isActive($dir_module['alias']);
|
||||
$modules_directory[$i_dir]['activated'] = false;
|
||||
|
||||
// Do not show third-party modules in Modules Derectory.
|
||||
// Do not show third-party modules in Modules Directory.
|
||||
if (\App\Module::isThirdParty($dir_module)) {
|
||||
$third_party_modules[] = $modules_directory[$i_dir];
|
||||
unset($modules_directory[$i_dir]);
|
||||
@@ -138,12 +141,63 @@ class ModulesController extends Controller
|
||||
$modules_directory = [];
|
||||
}
|
||||
|
||||
// Loop through each installed module
|
||||
foreach ($installed_modules as $i_installed => $module) {
|
||||
// Check if the module is an official one
|
||||
if (\App\Module::isOfficial($module['authorUrl'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the URL for the latest version of the module
|
||||
$latest_version_number_url = $module['latestVersionNumberUrl'] ?? null;
|
||||
if (! $latest_version_number_url) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a new Guzzle HTTP client
|
||||
$client = new \GuzzleHttp\Client();
|
||||
|
||||
try {
|
||||
// Send a GET request to the latest version URL
|
||||
$response = $client->request('GET', $latest_version_number_url, \Helper::setGuzzleDefaultOptions());
|
||||
|
||||
// Get the latest version number from the response body
|
||||
$latest_version = trim((string) $response->getBody());
|
||||
|
||||
if (empty($latest_version)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it is the module.json file - try to parse the body.
|
||||
preg_match('#"version":[^"]*"([\d\.]+)"#', $latest_version, $m);
|
||||
if (!empty($m[1])) {
|
||||
$latest_version = $m[1];
|
||||
}
|
||||
|
||||
// Get the current version of the module
|
||||
$current_version = $module['version'];
|
||||
} catch (\Exception $e) {
|
||||
// If there's an exception, skip to the next iteration
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the latest version is greater than the current version
|
||||
if (version_compare($latest_version, $current_version, '>')) {
|
||||
// Update the installed module's version
|
||||
$installed_modules[ $i_installed ]['new_version'] = $latest_version;
|
||||
// Set the flag to indicate that updates are available
|
||||
$updates_available = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check modules symlinks. Somestimes instead of symlinks folders with files appear.
|
||||
|
||||
$invalid_symlinks = \App\Module::checkSymlinks(
|
||||
collect($installed_modules)->where('active', true)->pluck('alias')->toArray()
|
||||
);
|
||||
|
||||
// Sort all modules.
|
||||
asort($all_modules);
|
||||
|
||||
return view('modules/modules', [
|
||||
'installed_modules' => $installed_modules,
|
||||
'modules_directory' => $modules_directory,
|
||||
@@ -328,6 +382,8 @@ class ModulesController extends Controller
|
||||
if (!$response['msg']) {
|
||||
\App\Module::setActive($alias, true);
|
||||
|
||||
$user_locale = app()->getLocale();
|
||||
|
||||
$outputLog = new BufferedOutput();
|
||||
\Artisan::call('freescout:module-install', ['module_alias' => $alias], $outputLog);
|
||||
$output = $outputLog->fetch();
|
||||
@@ -338,6 +394,9 @@ class ModulesController extends Controller
|
||||
$name = $module->getName();
|
||||
}
|
||||
|
||||
// After clearing cache the locale may be not set.
|
||||
\Helper::setUserLocale($user_locale);
|
||||
|
||||
$type = 'danger';
|
||||
$msg = __('Error occurred activating ":name" module', ['name' => $name]);
|
||||
if (session('flashes_floating') && is_array(session('flashes_floating'))) {
|
||||
@@ -387,6 +446,8 @@ class ModulesController extends Controller
|
||||
$alias = $request->alias;
|
||||
\App\Module::setActive($alias, false);
|
||||
|
||||
$user_locale = app()->getLocale();
|
||||
|
||||
$outputLog = new BufferedOutput();
|
||||
\Artisan::call('freescout:clear-cache', [], $outputLog);
|
||||
$output = $outputLog->fetch();
|
||||
@@ -398,6 +459,9 @@ class ModulesController extends Controller
|
||||
$name = $module->getName();
|
||||
}
|
||||
|
||||
// After clearing cache the locale may be not set.
|
||||
\Helper::setUserLocale($user_locale);
|
||||
|
||||
$type = 'danger';
|
||||
$msg = __('Error occurred deactivating :name module', ['name' => $name]);
|
||||
if (strstr($output, 'Configuration cached successfully')) {
|
||||
|
||||
@@ -87,14 +87,15 @@ class OpenController extends Controller
|
||||
->withInput();
|
||||
}
|
||||
|
||||
$request_data = $request->all();
|
||||
// Do not allow user to set his role
|
||||
if (isset($request_data['role'])) {
|
||||
unset($request_data['role']);
|
||||
}
|
||||
if (isset($request_data['photo_url'])) {
|
||||
unset($request_data['photo_url']);
|
||||
}
|
||||
$request_data = [
|
||||
'email' => $request->email,
|
||||
'password' => $request->password,
|
||||
'job_title' => $request->job_title,
|
||||
'phone' => $request->phone,
|
||||
'timezone' => $request->timezone,
|
||||
'time_format' => $request->time_format,
|
||||
];
|
||||
|
||||
$user->fill($request_data);
|
||||
|
||||
$user->password = bcrypt($request->password);
|
||||
@@ -108,7 +109,7 @@ class OpenController extends Controller
|
||||
// Login user
|
||||
Auth::guard()->login($user);
|
||||
|
||||
\Session::flash('flash_success_floating', __('Welcome to :company_name!', ['company_name' => Option::getCompanyName()]));
|
||||
\Session::flash('flash_success_floating', __('Welcome to :company_name!', ['company_name' => htmlspecialchars(Option::getCompanyName())]));
|
||||
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
||||
@@ -394,17 +394,22 @@ class SettingsController extends Controller
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
$test_result = false;
|
||||
$test_result = [
|
||||
'status' => 'error'
|
||||
];
|
||||
|
||||
try {
|
||||
$test_result = \MailHelper::sendTestMail($request->to);
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = $e->getMessage();
|
||||
$test_result['msg'] = $e->getMessage();
|
||||
}
|
||||
|
||||
if (!$test_result && !$response['msg']) {
|
||||
$response['msg'] = __('Error occurred sending email. Please check your mail server logs for more details.');
|
||||
if ($test_result['status'] == 'error') {
|
||||
$response['msg'] = $test_result['msg']
|
||||
?: __('Error occurred sending email. Please check your mail server logs for more details.');
|
||||
}
|
||||
|
||||
$response['log'] = $test_result['log'] ?? '';
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
|
||||
@@ -53,7 +53,7 @@ class SystemController extends Controller
|
||||
// Check if cache files are writable.
|
||||
$non_writable_cache_file = '';
|
||||
if (function_exists('shell_exec')) {
|
||||
$non_writable_cache_file = shell_exec('find '.base_path('storage/framework/cache/data/').' -type f | xargs -I {} sh -c \'[ ! -w "{}" ] && echo {}\' 2>&1 | head -n 1');
|
||||
$non_writable_cache_file = \Helper::shellExec('find '.base_path('storage/framework/cache/data/').' -type f | xargs -I {} sh -c \'[ ! -w "{}" ] && echo {}\' 2>&1 | head -n 1');
|
||||
$non_writable_cache_file = trim($non_writable_cache_file ?? '');
|
||||
// Leave only one line (in case head -n 1 does not work)
|
||||
$non_writable_cache_file = preg_replace("#[\r\n].+#m", '', $non_writable_cache_file);
|
||||
@@ -80,8 +80,8 @@ class SystemController extends Controller
|
||||
$env_is_writable = is_writable(base_path('.env'));
|
||||
|
||||
// Jobs
|
||||
$queued_jobs = \App\Job::orderBy('created_at', 'desc')->get();
|
||||
$failed_jobs = \App\FailedJob::orderBy('failed_at', 'desc')->get();
|
||||
$queued_jobs = \App\Job::orderBy('created_at', 'desc')->limit(100)->get();
|
||||
$failed_jobs = \App\FailedJob::orderBy('failed_at', 'desc')->limit(100)->get();
|
||||
$failed_queues = $failed_jobs->pluck('queue')->unique();
|
||||
|
||||
// Commands
|
||||
@@ -97,7 +97,7 @@ class SystemController extends Controller
|
||||
$running_commands = 0;
|
||||
|
||||
try {
|
||||
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep '{$command_identifier}'"));
|
||||
$processes = preg_split("/[\r\n]/", \Helper::shellExec("ps auxww | grep '{$command_identifier}'"));
|
||||
$pids = [];
|
||||
foreach ($processes as $process) {
|
||||
$process = trim($process);
|
||||
@@ -290,6 +290,7 @@ class SystemController extends Controller
|
||||
$params = [];
|
||||
$params['--days'] = (int)$request->days;
|
||||
$params['--unseen'] = (int)$request->unseen;
|
||||
$params['--debug'] = (int)$request->debug;
|
||||
\Artisan::call('freescout:fetch-emails', $params, $outputLog);
|
||||
break;
|
||||
|
||||
@@ -403,10 +404,10 @@ class SystemController extends Controller
|
||||
$payload = json_decode($job->payload, true);
|
||||
|
||||
if (!empty($payload['data']['command'])) {
|
||||
$html .= '<pre>'.print_r(unserialize($payload['data']['command']), 1).'</pre>';
|
||||
$html .= '<pre>'.\Helper::stripDangerousTags(print_r(unserialize($payload['data']['command']), 1)).'</pre>';
|
||||
}
|
||||
|
||||
$html .= '<pre>'.$job->exception.'</pre>';
|
||||
$html .= '<pre>'.\Helper::stripDangerousTags($job->exception).'</pre>';
|
||||
|
||||
return response($html);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Events\UserDeleted;
|
||||
use App\Folder;
|
||||
use App\Mailbox;
|
||||
use App\Subscription;
|
||||
@@ -86,8 +85,16 @@ class UsersController extends Controller
|
||||
->withInput();
|
||||
}
|
||||
|
||||
$data = [
|
||||
'first_name' => $request->first_name,
|
||||
'last_name' => $request->last_name,
|
||||
'email' => $request->email,
|
||||
];
|
||||
|
||||
$user = new User();
|
||||
$user->fill($request->all());
|
||||
|
||||
$user->fill($data);
|
||||
|
||||
if (!$auth_user->can('changeRole', $user)) {
|
||||
$user->role = User::ROLE_USER;
|
||||
}
|
||||
@@ -161,6 +168,8 @@ class UsersController extends Controller
|
||||
$user = User::findOrFail($id);
|
||||
$this->authorize('update', $user);
|
||||
|
||||
$auth_user = auth()->user();
|
||||
|
||||
// This is also present in PublicController::userSetup
|
||||
$validator = Validator::make($request->all(), [
|
||||
'first_name' => 'required|string|max:20',
|
||||
@@ -213,7 +222,7 @@ class UsersController extends Controller
|
||||
}
|
||||
|
||||
// Save language into session.
|
||||
if (auth()->user()->id == $id && $request->locale) {
|
||||
if ($auth_user->id == $id && $request->locale) {
|
||||
session()->put('user_locale', $request->locale);
|
||||
}
|
||||
|
||||
@@ -222,7 +231,7 @@ class UsersController extends Controller
|
||||
if (isset($request_data['photo_url'])) {
|
||||
unset($request_data['photo_url']);
|
||||
}
|
||||
if (!auth()->user()->can('changeRole', $user)) {
|
||||
if (!$auth_user->can('changeRole', $user)) {
|
||||
unset($request_data['role']);
|
||||
}
|
||||
if ($user->status != User::STATUS_DELETED) {
|
||||
@@ -232,11 +241,43 @@ class UsersController extends Controller
|
||||
$request_data['status'] = User::STATUS_ACTIVE;
|
||||
}
|
||||
}
|
||||
$user->setData($request_data);
|
||||
|
||||
if (empty($request->input('enable_kb_shortcuts'))) {
|
||||
$user->enable_kb_shortcuts = false;
|
||||
// Sanitize $request_data array.
|
||||
// $allowed_fields = [
|
||||
// 'first_name',
|
||||
// 'last_name',
|
||||
// 'email',
|
||||
// 'emails',
|
||||
// 'job_title',
|
||||
// 'phone',
|
||||
// 'locale',
|
||||
// 'timezone'
|
||||
// 'time_format',
|
||||
// 'photo_url',
|
||||
// ];
|
||||
$admin_fields = [
|
||||
'role',
|
||||
'status',
|
||||
'email',
|
||||
];
|
||||
$nonfillable_fields = [
|
||||
'type',
|
||||
'password',
|
||||
];
|
||||
if (!$auth_user->isAdmin()) {
|
||||
foreach ($admin_fields as $field) {
|
||||
if (isset($request_data[$field])) {
|
||||
unset($request_data[$field]);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($nonfillable_fields as $field) {
|
||||
if (isset($request_data[$field])) {
|
||||
unset($request_data[$field]);
|
||||
}
|
||||
}
|
||||
|
||||
$user->setData($request_data);
|
||||
|
||||
$user = \Eventy::filter('user.save_profile', $user, $request);
|
||||
|
||||
@@ -259,6 +300,10 @@ class UsersController extends Controller
|
||||
|
||||
$user = User::findOrFail($id);
|
||||
|
||||
if ($user->isDeleted()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$mailboxes = Mailbox::all();
|
||||
|
||||
$users = $this->getUsersForSidebar($id);
|
||||
@@ -317,6 +362,10 @@ class UsersController extends Controller
|
||||
$user = User::findOrFail($id);
|
||||
$this->authorize('update', $user);
|
||||
|
||||
if ($user->isDeleted()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$subscriptions = $user->subscriptions()->select('medium', 'event')->get();
|
||||
|
||||
$person = '';
|
||||
@@ -479,106 +528,24 @@ class UsersController extends Controller
|
||||
$response['msg'] = __('User not found');
|
||||
} elseif (!$auth_user->can('delete', $user)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
} elseif ($auth_user->id == $user->id) {
|
||||
// Do not allow admin delete himself.
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
// Check if the user is the only one admin
|
||||
if (!$response['msg'] && $user->isAdmin()) {
|
||||
$admins_count = User::where('role', User::ROLE_ADMIN)->count();
|
||||
if ($admins_count < 2) {
|
||||
$response['msg'] = __('Administrator can not be deleted');
|
||||
}
|
||||
}
|
||||
// - not needed as only admin can delete users and
|
||||
// current admin can not delete himself.
|
||||
// if (!$response['msg'] && $user->isAdmin()) {
|
||||
// $admins_count = User::where('role', User::ROLE_ADMIN)->count();
|
||||
// if ($admins_count < 2) {
|
||||
// $response['msg'] = __('Administrator can not be deleted');
|
||||
// }
|
||||
// }
|
||||
|
||||
if (!$response['msg']) {
|
||||
|
||||
// We have to process conversations one by one to move them to Unassigned folder,
|
||||
// as conversations may be in different mailboxes
|
||||
// $user->conversations()->update(['user_id' => null, 'folder_id' => ]);
|
||||
$mailbox_unassigned_folders = [];
|
||||
|
||||
$user->conversations->each(function ($conversation) use ($auth_user, $request) {
|
||||
// We don't fire ConversationUserChanged event to avoid sending notifications to users
|
||||
if (!empty($request->assign_user)
|
||||
&& !empty($request->assign_user[$conversation->mailbox_id])
|
||||
&& (int) $request->assign_user[$conversation->mailbox_id] != -1
|
||||
) {
|
||||
// Set assignee.
|
||||
// In this case conversation stays assigned, just assignee changes.
|
||||
$conversation->user_id = $request->assign_user[$conversation->mailbox_id];
|
||||
|
||||
} else {
|
||||
|
||||
// Make convesation Unassigned.
|
||||
|
||||
// Unset assignee.
|
||||
// Maybe use changeUser() here.
|
||||
$conversation->user_id = null;
|
||||
|
||||
if ($conversation->isPublished()
|
||||
&& ($conversation->isActive() || $conversation->isPending())
|
||||
) {
|
||||
// Change conversation folder to UNASSIGNED.
|
||||
$folder_id = null;
|
||||
if (!empty($mailbox_unassigned_folders[$conversation->mailbox_id])) {
|
||||
$folder_id = $mailbox_unassigned_folders[$conversation->mailbox_id];
|
||||
} else {
|
||||
$folder = $conversation->mailbox->folders()
|
||||
->where('type', Folder::TYPE_UNASSIGNED)
|
||||
->first();
|
||||
|
||||
if ($folder) {
|
||||
$folder_id = $folder->id;
|
||||
$mailbox_unassigned_folders[$conversation->mailbox_id] = $folder_id;
|
||||
}
|
||||
}
|
||||
if ($folder_id) {
|
||||
$conversation->folder_id = $folder_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$conversation->save();
|
||||
|
||||
// Create lineitem thread
|
||||
$thread = new Thread();
|
||||
$thread->conversation_id = $conversation->id;
|
||||
$thread->user_id = $conversation->user_id;
|
||||
$thread->type = Thread::TYPE_LINEITEM;
|
||||
$thread->state = Thread::STATE_PUBLISHED;
|
||||
$thread->status = Thread::STATUS_NOCHANGE;
|
||||
$thread->action_type = Thread::ACTION_TYPE_USER_CHANGED;
|
||||
$thread->source_via = Thread::PERSON_USER;
|
||||
$thread->source_type = Thread::SOURCE_TYPE_WEB;
|
||||
$thread->customer_id = $conversation->customer_id;
|
||||
$thread->created_by_user_id = $auth_user->id;
|
||||
$thread->save();
|
||||
});
|
||||
|
||||
// Recalculate counters for folders
|
||||
//if ($user->isAdmin()) {
|
||||
// Admin has access to all mailboxes
|
||||
Mailbox::all()->each(function ($mailbox) {
|
||||
$mailbox->updateFoldersCounters();
|
||||
});
|
||||
// } else {
|
||||
// $user->mailboxes->each(function ($mailbox) {
|
||||
// $mailbox->updateFoldersCounters();
|
||||
// });
|
||||
// }
|
||||
|
||||
// Disconnect user from mailboxes.
|
||||
$user->mailboxes()->sync([]);
|
||||
$user->folders()->delete();
|
||||
|
||||
$user->status = \App\User::STATUS_DELETED;
|
||||
// Update email.
|
||||
$email_suffix = User::EMAIL_DELETED_SUFFIX.date('YmdHis');
|
||||
// We have to truncate email to avoid "Data too long" error.
|
||||
$user->email = mb_substr($user->email, 0, User::EMAIL_MAX_LENGTH - mb_strlen($email_suffix)).$email_suffix;
|
||||
|
||||
$user->save();
|
||||
|
||||
event(new UserDeleted($user, $auth_user));
|
||||
$user->deleteUser($auth_user, $request->assign_user);
|
||||
|
||||
\Session::flash('flash_success_floating', __('User deleted').': '.$user->getFullName());
|
||||
|
||||
@@ -588,6 +555,7 @@ class UsersController extends Controller
|
||||
|
||||
default:
|
||||
$response['msg'] = 'Unknown action';
|
||||
$response = \Eventy::filter('users.ajax.response_default', $response, $request);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,13 +30,8 @@ class HttpsRedirect {
|
||||
{
|
||||
if (\Helper::isHttps()) {
|
||||
//$request->setTrustedProxies( [ $request->getClientIp() ], array_keys($this->headers));
|
||||
|
||||
if (//!$request->secure()
|
||||
!in_array(strtolower($_SERVER['X_FORWARDED_PROTO'] ?? ''), array('https', 'on', 'ssl', '1'), true)
|
||||
&& strtolower($_SERVER['HTTPS'] ?? '') != 'on'
|
||||
&& ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') != 'https'
|
||||
&& ($_SERVER['HTTP_CF_VISITOR'] ?? '') != '{"scheme":"https"}'
|
||||
) {
|
||||
//!$request->secure()
|
||||
if (!\Helper::isCurrentUrlHttps()) {
|
||||
return redirect()->secure($request->getRequestUri());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,26 @@ use Closure;
|
||||
|
||||
class ResponseHeaders
|
||||
{
|
||||
private $unwanted_headers = [
|
||||
'X-Powered-By',
|
||||
'Server',
|
||||
];
|
||||
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$this->removeUnwantedHeaders($this->unwanted_headers);
|
||||
|
||||
$response = $next($request);
|
||||
|
||||
// Disable caching
|
||||
// Secure headers.
|
||||
//$response->headers->set('Referrer-Policy', 'no-referrer-when-downgrade');
|
||||
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
||||
// $response->headers->set('X-XSS-Protection', '1; mode=block');
|
||||
// $response->headers->set('X-Frame-Options', 'DENY');
|
||||
//$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||
//$response->headers->set('Content-Security-Policy', "style-src 'self'");
|
||||
|
||||
// Disable caching.
|
||||
if (method_exists($response, 'header')) {
|
||||
$response->header('Pragma', 'no-cache');
|
||||
$response->header('Cache-Control', 'no-cache, max-age=0, must-revalidate, no-store');
|
||||
@@ -18,4 +33,11 @@ class ResponseHeaders
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function removeUnwantedHeaders($headerList)
|
||||
{
|
||||
foreach ($headerList as $header) {
|
||||
header_remove($header);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,13 @@ class SendNotificationToUsers implements ShouldQueue
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not send email notifications to support agents
|
||||
// if the email is a bounce and mail server send limit reached.
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4806
|
||||
if ($last_thread->isBounce() && strstr($last_thread->body, 'message limit exceeded')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Limit conversation history
|
||||
if (config('app.email_user_history') == 'last') {
|
||||
$this->threads = $this->threads->slice(0, 2);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Mail\ReplyToCustomer;
|
||||
use App\Customer;
|
||||
use App\SendLog;
|
||||
use App\Thread;
|
||||
use App\Misc\SwiftGetSmtpQueueId;
|
||||
@@ -12,7 +13,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Mail;
|
||||
use Webklex\IMAP\Client;
|
||||
//use Webklex\IMAP\Client;
|
||||
|
||||
class SendReplyToCustomer implements ShouldQueue
|
||||
{
|
||||
@@ -116,6 +117,17 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
return;
|
||||
}
|
||||
|
||||
// After sending an email we are saving it into "IMAP Folder To Save Outgoing Replies".
|
||||
// This process may stuck or make SendReplyToCustomer job die with
|
||||
// "Allowed memory size of NNN bytes exhausted" error.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3632
|
||||
if ($this->attempts() >= 1 &&
|
||||
($this->last_thread->send_status == SendLog::STATUS_ACCEPTED
|
||||
|| $this->last_thread->isSendStatusSuccess())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (count($this->threads) == 1) {
|
||||
$new = true;
|
||||
}
|
||||
@@ -156,6 +168,19 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
// For Outlook threading.
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4922
|
||||
if ($last_customer_thread) {
|
||||
$outloook_thread_index = $last_customer_thread->getHeader('Thread-Index');
|
||||
if (!empty($outloook_thread_index)) {
|
||||
$headers['Thread-Index'] = $outloook_thread_index;
|
||||
$outloook_thread_topic = $last_customer_thread->getHeader('Thread-Topic');
|
||||
if ($outloook_thread_topic) {
|
||||
$headers['Thread-Topic'] = $outloook_thread_topic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Conversation history.
|
||||
$email_conv_history = config('app.email_conv_history');
|
||||
|
||||
@@ -219,6 +244,12 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get customer by email
|
||||
if (!$this->customer) {
|
||||
$this->customer = Customer::getByEmail($this->customer_email);
|
||||
return;
|
||||
}
|
||||
|
||||
$to_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getToArray());
|
||||
$cc_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getCcArray());
|
||||
$bcc_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getBccArray());
|
||||
@@ -266,12 +297,17 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
|
||||
$reply_mail = new ReplyToCustomer($this->conversation, $this->threads, $headers, $mailbox, $subject, $threads_count);
|
||||
|
||||
$smtp_queue_id = null;
|
||||
|
||||
try {
|
||||
Mail::to($to)
|
||||
->cc($cc_array)
|
||||
->bcc($bcc_array)
|
||||
->send($reply_mail);
|
||||
|
||||
$this->last_thread->send_status = SendLog::STATUS_ACCEPTED;
|
||||
$this->last_thread->save();
|
||||
|
||||
$smtp_queue_id = SwiftGetSmtpQueueId::$last_smtp_queue_id;
|
||||
} catch (\Exception $e) {
|
||||
// We come here in case SMTP server unavailable for example
|
||||
@@ -317,7 +353,7 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
|
||||
// If an email has not been sent after 1 hour - show an error message to support agent.
|
||||
if ($this->attempts() >= 3 || $response_code >= 500) {
|
||||
$this->last_thread->send_status = SendLog::STATUS_SEND_ERROR;
|
||||
$this->last_thread->send_status = SendLog::STATUS_SEND_INTERMEDIATE_ERROR;
|
||||
$this->last_thread->updateSendStatusData(['msg' => $error_message]);
|
||||
$this->last_thread->save();
|
||||
}
|
||||
@@ -345,82 +381,98 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
}
|
||||
|
||||
$imap_sent_folder = $mailbox->imap_sent_folder;
|
||||
if ($imap_sent_folder) {
|
||||
|
||||
if ($imap_sent_folder && \MailHelper::$smtp_mime_message) {
|
||||
try {
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
|
||||
$client->connect();
|
||||
|
||||
$envelope['from'] = $mailbox->getMailFrom(null, $this->conversation)['address'];
|
||||
$envelope['to'] = $this->customer_email;
|
||||
$envelope['subject'] = $subject;
|
||||
$envelope['date'] = now()->toRfc2822String();
|
||||
$envelope['message_id'] = $this->message_id;
|
||||
// $mail_from = $mailbox->getMailFrom($this->last_thread->created_by_user ?? null, $this->conversation);
|
||||
|
||||
// CC.
|
||||
if (count($cc_array)) {
|
||||
$envelope['cc'] = implode(',', $cc_array);
|
||||
}
|
||||
// if (!empty($mail_from['name'])) {
|
||||
// $envelope['from'] = '"'.$mail_from['name'].'" <'.$mail_from['address'].'>';
|
||||
// } else {
|
||||
// $envelope['from'] = $mail_from['address'];
|
||||
// }
|
||||
// if (is_array($to) && !empty($to[0]) && !empty($to[0]['name']) && !empty($to[0]['email'])) {
|
||||
// $envelope['to'] = '"'.$to[0]['name'].'" <'.$to[0]['email'].'>';
|
||||
// } else {
|
||||
// $envelope['to'] = $this->customer_email;
|
||||
// }
|
||||
// $envelope['subject'] = $subject;
|
||||
// $envelope['date'] = now()->toRfc2822String();
|
||||
// $envelope['message_id'] = $this->message_id;
|
||||
|
||||
// Get penultimate email Message-Id if reply
|
||||
if (!$new && !empty($last_customer_thread) && $last_customer_thread->message_id) {
|
||||
$envelope['custom_headers'] = [
|
||||
'In-Reply-To: <'.$last_customer_thread->message_id.'>',
|
||||
'References: '.$references,
|
||||
];
|
||||
}
|
||||
// Remove new lines to avoid "imap_mail_compose(): header injection attempt in subject".
|
||||
foreach ($envelope as $i => $envelope_value) {
|
||||
$envelope[$i] = preg_replace("/[\r\n]/", '', $envelope_value);
|
||||
}
|
||||
// // CC.
|
||||
// if (count($cc_array)) {
|
||||
// $envelope['cc'] = implode(',', $cc_array);
|
||||
// }
|
||||
|
||||
$parts = [];
|
||||
// // Get penultimate email Message-Id if reply
|
||||
// if (!$new && !empty($last_customer_thread) && $last_customer_thread->message_id) {
|
||||
// $envelope['custom_headers'] = [
|
||||
// 'In-Reply-To: <'.$last_customer_thread->message_id.'>',
|
||||
// 'References: '.$references,
|
||||
// ];
|
||||
// }
|
||||
// // Remove new lines to avoid "imap_mail_compose(): header injection attempt in subject".
|
||||
// foreach ($envelope as $i => $envelope_value) {
|
||||
// $envelope[$i] = preg_replace("/[\r\n]/", '', $envelope_value);
|
||||
// }
|
||||
|
||||
// Multipart flag.
|
||||
if ($this->last_thread->has_attachments) {
|
||||
$multipart = [];
|
||||
$multipart["type"] = TYPEMULTIPART;
|
||||
$multipart["subtype"] = "alternative";
|
||||
$parts[] = $multipart;
|
||||
}
|
||||
// $parts = [];
|
||||
|
||||
// Body.
|
||||
$part_body['type'] = TYPETEXT;
|
||||
$part_body['subtype'] = 'html';
|
||||
$part_body['contents.data'] = $reply_mail->render();
|
||||
$part_body['charset'] = 'utf-8';
|
||||
// // Multipart flag.
|
||||
// if ($this->last_thread->has_attachments) {
|
||||
// $multipart = [];
|
||||
// $multipart["type"] = \Webklex\PHPIMAP\IMAP::TYPEMULTIPART;
|
||||
// $multipart["subtype"] = "mixed";
|
||||
// // https://github.com/freescout-helpdesk/freescout/issues/3934
|
||||
// //$multipart["subtype"] = "alternative";
|
||||
// $parts[] = $multipart;
|
||||
// }
|
||||
|
||||
$parts[] = $part_body;
|
||||
// // Body.
|
||||
// $part_body['type'] = \Webklex\PHPIMAP\IMAP::TYPETEXT;
|
||||
// $part_body['subtype'] = 'html';
|
||||
// $part_body['contents.data'] = $reply_mail->render();
|
||||
// $part_body['charset'] = 'utf-8';
|
||||
|
||||
// Add attachments.
|
||||
if ($this->last_thread->has_attachments) {
|
||||
// $parts[] = $part_body;
|
||||
|
||||
foreach ($this->last_thread->attachments as $attachment) {
|
||||
// // Add attachments.
|
||||
// if ($this->last_thread->has_attachments) {
|
||||
|
||||
if ($attachment->embedded) {
|
||||
continue;
|
||||
}
|
||||
// foreach ($this->last_thread->attachments as $attachment) {
|
||||
|
||||
if ($attachment->fileExists()) {
|
||||
$part = [];
|
||||
$part["type"] = 'APPLICATION';
|
||||
$part["encoding"] = ENCBASE64;
|
||||
$part["subtype"] = "octet-stream";
|
||||
$part["description"] = $attachment->file_name;
|
||||
$part['disposition.type'] = 'attachment';
|
||||
$part['disposition'] = array('filename' => $attachment->file_name);
|
||||
$part['type.parameters'] = array('name' => $attachment->file_name);
|
||||
$part["description"] = '';
|
||||
$part["contents.data"] = base64_encode($attachment->getFileContents());
|
||||
// if ($attachment->embedded) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// if ($attachment->fileExists()) {
|
||||
// $part = [];
|
||||
// $part["type"] = 'APPLICATION';
|
||||
// $part["encoding"] = \Webklex\PHPIMAP\IMAP::ENCBASE64;
|
||||
// $part["subtype"] = "octet-stream";
|
||||
// $part["description"] = $attachment->file_name;
|
||||
// $part['disposition.type'] = 'attachment';
|
||||
// $part['disposition'] = array('filename' => $attachment->file_name);
|
||||
// $part['type.parameters'] = array('name' => $attachment->file_name);
|
||||
// $part["description"] = '';
|
||||
// $part["contents.data"] = base64_encode($attachment->getFileContents());
|
||||
|
||||
$parts[] = $part;
|
||||
} else {
|
||||
\Log::error('[IMAP Folder To Save Outgoing Replies] Thread: '.$this->last_thread->id.'. Attachment file not find on disk: '.$attachment->getLocalFilePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
// $parts[] = $part;
|
||||
// } else {
|
||||
// \Log::error('[IMAP Folder To Save Outgoing Replies] Thread: '.$this->last_thread->id.'. Attachment file not find on disk: '.$attachment->getLocalFilePath());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
try {
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3502
|
||||
$imap_sent_folder = mb_convert_encoding($imap_sent_folder, "UTF7-IMAP","UTF-8");
|
||||
|
||||
// https://github.com/Webklex/php-imap/issues/380
|
||||
if (method_exists($client, 'getFolderByPath')) {
|
||||
$folder = $client->getFolderByPath($imap_sent_folder);
|
||||
@@ -430,29 +482,35 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
// Get folder method does not work if sent folder has spaces.
|
||||
if ($folder) {
|
||||
try {
|
||||
$save_result = $this->saveEmailToFolder($client, $folder, $envelope, $parts, $bcc_array);
|
||||
//$save_result = $this->saveEmailToFolder($client, $folder, $envelope, $parts, $bcc_array);
|
||||
$save_result = $this->saveEmailToFolder($folder, \MailHelper::$smtp_mime_message);
|
||||
\MailHelper::$smtp_mime_message = '';
|
||||
|
||||
// Sometimes emails with attachments by some reason are not saved.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/2749
|
||||
// if (!$save_result) {
|
||||
// // Save without attachments.
|
||||
// $save_result = $this->saveEmailToFolder($client, $folder, $envelope, [$part_body], $bcc_array);
|
||||
if (!$save_result) {
|
||||
// Save without attachments.
|
||||
$this->saveEmailToFolder($client, $folder, $envelope, [$part_body], $bcc_array);
|
||||
\Log::error($this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder (check folder name and make sure IMAP folder does not have spaces - folders with spaces do not work): '.$imap_sent_folder);
|
||||
}
|
||||
//}
|
||||
} catch (\Exception $e) {
|
||||
// Just log error and continue.
|
||||
\Helper::logException($e, 'Could not save outgoing reply to the IMAP folder: ');
|
||||
\Helper::logException($e, $this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder: ');
|
||||
}
|
||||
} else {
|
||||
\Log::error('Could not save outgoing reply to the IMAP folder (make sure IMAP folder does not have spaces - folders with spaces do not work): '.$imap_sent_folder);
|
||||
\Log::error($this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder (check folder name and make sure IMAP folder does not have spaces - folders with spaces do not work): '.$imap_sent_folder);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Just log error and continue.
|
||||
\Helper::logException($e, 'Could not save outgoing reply to the IMAP folder, IMAP folder not found: '.$imap_sent_folder.' - ');
|
||||
\Helper::logException($e, $this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder, IMAP folder not found: '.$imap_sent_folder.' - ');
|
||||
//$this->saveToSendLog('['.date('Y-m-d H:i:s').'] Could not save outgoing reply to the IMAP folder: '.$imap_sent_folder);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Just log error and continue.
|
||||
//$this->saveToSendLog('['.date('Y-m-d H:i:s').'] Could not get mailbox IMAP folder: '.$imap_sent_folder);
|
||||
\Helper::logException($e, 'Could not save outgoing reply to the IMAP folder: '.$imap_sent_folder.' - ');
|
||||
\Helper::logException($e, $this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder: '.$imap_sent_folder.' - ');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,29 +526,39 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
$this->saveToSendLog('', $smtp_queue_id);
|
||||
}
|
||||
|
||||
// Save an email to IMAP folder.
|
||||
public function saveEmailToFolder($client, $folder, $envelope, $parts, $bcc = [])
|
||||
public function getImapSaveErrorPrefix($mailbox)
|
||||
{
|
||||
$envelope_str = imap_mail_compose($envelope, $parts);
|
||||
|
||||
// Add BCC.
|
||||
// https://stackoverflow.com/questions/47353938/php-imap-append-with-bcc
|
||||
if (!empty($bcc)) {
|
||||
// There will be a "To:" parameter for sure.
|
||||
$to_pos = strpos($envelope_str , "To:");
|
||||
if ($to_pos !== false) {
|
||||
$bcc_str = "Bcc: " . implode(',', $bcc) . "\r\n";
|
||||
$envelope_str = substr_replace($envelope_str , $bcc_str, $to_pos, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (get_class($client) == 'Webklex\PHPIMAP\Client') {
|
||||
return $folder->appendMessage($envelope_str, ['Seen'], now()->format('d-M-Y H:i:s O'));
|
||||
} else {
|
||||
return $folder->appendMessage($envelope_str, '\Seen', now()->format('d-M-Y H:i:s O'));
|
||||
}
|
||||
return '['.$mailbox->name.' » Connection Settings » Fetching Emails » IMAP Folder To Save Outgoing Replies] ';
|
||||
}
|
||||
|
||||
// Save an email to IMAP folder.
|
||||
public function saveEmailToFolder($folder, $message_str)
|
||||
{
|
||||
return $folder->appendMessage($message_str, ['\Seen'], now()->format('d-M-Y H:i:s O'));
|
||||
}
|
||||
|
||||
// public function saveEmailToFolder($client, $folder, $envelope, $parts, $bcc = [])
|
||||
// {
|
||||
// $envelope_str = imap_mail_compose($envelope, $parts);
|
||||
|
||||
// // Add BCC.
|
||||
// // https://stackoverflow.com/questions/47353938/php-imap-append-with-bcc
|
||||
// if (!empty($bcc)) {
|
||||
// // There will be a "To:" parameter for sure.
|
||||
// $to_pos = strpos($envelope_str , "To:");
|
||||
// if ($to_pos !== false) {
|
||||
// $bcc_str = "Bcc: " . implode(',', $bcc) . "\r\n";
|
||||
// $envelope_str = substr_replace($envelope_str , $bcc_str, $to_pos, 0);
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (get_class($client) == 'Webklex\PHPIMAP\Client') {
|
||||
// return $folder->appendMessage($envelope_str, ['\Seen'], now()->format('d-M-Y H:i:s O'));
|
||||
// } else {
|
||||
// return $folder->appendMessage($envelope_str, '\Seen', now()->format('d-M-Y H:i:s O'));
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* The job failed to process.
|
||||
* This method is called after attempts had finished.
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Conversation;
|
||||
use App\ConversationFolder;
|
||||
use App\Folder;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
@@ -15,28 +14,33 @@ class UpdateFolderCounters implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* @var Folder
|
||||
*/
|
||||
private $folder;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
// Cache lock key
|
||||
private $lockKey;
|
||||
|
||||
public function __construct(Folder $folder)
|
||||
{
|
||||
$this->folder = $folder;
|
||||
$this->lockKey = "folder_update_lock_{$folder->id}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->folder->updateCountersNow();
|
||||
// Check if another job is processing this folder
|
||||
if (Cache::has($this->lockKey)) {
|
||||
return; // Exit if already processing
|
||||
}
|
||||
|
||||
// Set lock with a timeout of 5 minutes (adjust as needed)
|
||||
Cache::put($this->lockKey, true, now()->addMinutes(5));
|
||||
|
||||
try {
|
||||
// Perform the folder update logic
|
||||
$this->folder->updateCountersNow();
|
||||
} finally {
|
||||
// Release the lock after processing
|
||||
Cache::forget($this->lockKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ class LogFailedLogin
|
||||
*/
|
||||
public function handle(Failed $event)
|
||||
{
|
||||
\Eventy::action('listeners.failed_login', $event);
|
||||
|
||||
activity()
|
||||
//->causedBy($event->user)
|
||||
->withProperties(['ip' => app('request')->ip(), 'email' => request()->email])
|
||||
|
||||
@@ -81,6 +81,10 @@ class ReplyToCustomer extends Mailable
|
||||
if (!empty($new_headers) || $from_alias) {
|
||||
$mailbox = $this->mailbox;
|
||||
$this->withSwiftMessage(function ($swiftmessage) use ($new_headers, $from_alias, $mailbox, $thread) {
|
||||
\MailHelper::$smtp_mime_message = '';
|
||||
|
||||
$headers = null;
|
||||
|
||||
if (!empty($new_headers)) {
|
||||
if (!empty($new_headers['Message-ID'])) {
|
||||
$swiftmessage->setId($new_headers['Message-ID']);
|
||||
@@ -111,6 +115,10 @@ class ReplyToCustomer extends Mailable
|
||||
$from_alias_name = $mailbox_mail_from['name'];
|
||||
}
|
||||
|
||||
if (!$headers) {
|
||||
$headers = $swiftmessage->getHeaders();
|
||||
}
|
||||
|
||||
$swift_from = $headers->get('From');
|
||||
|
||||
if ($from_alias_name) {
|
||||
@@ -125,15 +133,24 @@ class ReplyToCustomer extends Mailable
|
||||
}
|
||||
}
|
||||
|
||||
\Eventy::action('email.reply_to_customer.swiftmessage', $swiftmessage, $from_alias, $thread, $mailbox);
|
||||
|
||||
if ($mailbox->imap_sent_folder) {
|
||||
\MailHelper::$smtp_mime_message = $swiftmessage->toString();
|
||||
}
|
||||
|
||||
return $swiftmessage;
|
||||
});
|
||||
}
|
||||
|
||||
$template_html = \Eventy::filter('email.reply_to_customer.template_name_html','emails/customer/reply_fancy');
|
||||
$template_text = \Eventy::filter('email.reply_to_customer.template_name_text','emails/customer/reply_fancy_text');
|
||||
|
||||
// from($this->from) Sets only email, name stays empty.
|
||||
// So we set from in Mail::setMailDriver
|
||||
$message = $this->subject($this->subject)
|
||||
->view('emails/customer/reply_fancy')
|
||||
->text('emails/customer/reply_fancy_text');
|
||||
->view($template_html)
|
||||
->text($template_text);
|
||||
|
||||
if ($thread->has_attachments) {
|
||||
foreach ($thread->attachments as $attachment) {
|
||||
|
||||
@@ -91,15 +91,20 @@ class UserNotification extends Mailable
|
||||
});
|
||||
}
|
||||
|
||||
$subject = '[#'.$this->conversation->number.'] '.$this->conversation->subject;
|
||||
$subject = \Eventy::filter('email.user_notification.subject', '[#' . (isset($this->conversation->number) ? $this->conversation->number : '') . '] ' . $this->conversation->subject, $this->conversation);
|
||||
|
||||
$customer = $this->conversation->customer;
|
||||
|
||||
$thread = $this->threads->first();
|
||||
|
||||
$template_html = \Eventy::filter('email.user_notification.template_name_html', 'emails/user/notification');
|
||||
$template_text = \Eventy::filter('email.user_notification.template_name_text', 'emails/user/notification_text');
|
||||
$template_fields = \Eventy::filter('email.user_notification.template_fields', ['customer' => $customer, 'thread' => $thread, 'mailbox' => $this->mailbox]);
|
||||
|
||||
return $this->subject($subject)
|
||||
->from($this->from['address'], $this->from['name'])
|
||||
->view('emails/user/notification', ['customer' => $customer, 'thread' => $thread, 'mailbox' => $this->mailbox])
|
||||
->text('emails/user/notification_text', ['customer' => $customer, 'thread' => $thread, 'mailbox' => $this->mailbox]);
|
||||
->view($template_html, $template_fields)
|
||||
->text($template_text, $template_fields);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,9 @@ class Mailbox extends Model
|
||||
const OUT_ENCRYPTION_NONE = 1;
|
||||
const OUT_ENCRYPTION_SSL = 2;
|
||||
const OUT_ENCRYPTION_TLS = 3;
|
||||
// StartTLS is already included in TLS:
|
||||
// allow_self_signed = true
|
||||
// verify_peer = false
|
||||
|
||||
public static $out_encryptions = [
|
||||
self::OUT_ENCRYPTION_NONE => '',
|
||||
@@ -82,11 +85,16 @@ class Mailbox extends Model
|
||||
const IN_ENCRYPTION_NONE = 1;
|
||||
const IN_ENCRYPTION_SSL = 2;
|
||||
const IN_ENCRYPTION_TLS = 3;
|
||||
// For Webklex/laravel-imap this option is the same as 'none'.
|
||||
// For Webklex/php-imap it works:
|
||||
// https://github.com/Webklex/php-imap/pull/180
|
||||
const IN_ENCRYPTION_STARTTLS = 4;
|
||||
|
||||
public static $in_encryptions = [
|
||||
self::IN_ENCRYPTION_NONE => '',
|
||||
self::IN_ENCRYPTION_SSL => 'ssl',
|
||||
self::IN_ENCRYPTION_TLS => 'tls',
|
||||
self::IN_ENCRYPTION_STARTTLS => 'starttls',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -140,7 +148,7 @@ class Mailbox extends Model
|
||||
*
|
||||
* @var [type]
|
||||
*/
|
||||
protected $fillable = ['name', 'email', 'aliases', 'aliases_reply', 'auto_bcc', 'from_name', 'from_name_custom', 'ticket_status', 'ticket_assignee', 'template', 'before_reply', 'signature', 'out_method', 'out_server', 'out_username', 'out_password', 'out_port', 'out_encryption', 'in_server', 'in_port', 'in_username', 'in_password', 'in_protocol', 'in_encryption', 'in_validate_cert', 'auto_reply_enabled', 'auto_reply_subject', 'auto_reply_message', 'office_hours_enabled', 'ratings', 'ratings_placement', 'ratings_text', 'imap_sent_folder'];
|
||||
protected $fillable = ['name', 'email', 'aliases', 'aliases_reply', 'auto_bcc', 'from_name', 'from_name_custom', 'ticket_status', 'ticket_assignee', 'before_reply', 'signature', 'out_method', 'out_server', 'out_username', 'out_password', 'out_port', 'out_encryption', 'in_server', 'in_port', 'in_username', 'in_password', 'in_protocol', 'in_encryption', 'in_validate_cert', 'auto_reply_enabled', 'auto_reply_subject', 'auto_reply_message', 'office_hours_enabled', 'ratings', 'ratings_placement', 'ratings_text', 'imap_sent_folder'];
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
@@ -969,6 +977,19 @@ class Mailbox extends Model
|
||||
return $this->meta['oauth'][$param] ?? '';
|
||||
}
|
||||
|
||||
public function inOauthEnabled()
|
||||
{
|
||||
return $this->oauthEnabled()
|
||||
&& $this->in_username !== null && !strstr($this->in_username, '@');
|
||||
}
|
||||
|
||||
public function outOauthEnabled()
|
||||
{
|
||||
return $this->oauthEnabled()
|
||||
&& $this->out_username !== null && !strstr($this->out_username, '@')
|
||||
&& $this->out_server !== null && trim($this->out_server) == \MailHelper::OAUTH_MICROSOFT_SMTP;
|
||||
}
|
||||
|
||||
public function setEmailAttribute($value)
|
||||
{
|
||||
if ($value) {
|
||||
|
||||
@@ -20,6 +20,13 @@ class MailboxUser extends Model
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
public static $pivot_settings = [
|
||||
'after_send',
|
||||
'hide',
|
||||
'mute',
|
||||
'access',
|
||||
];
|
||||
|
||||
// Does not work as we receive it via pivot
|
||||
// protected $casts = [
|
||||
// 'access' => 'array',
|
||||
|
||||
178
app/Misc/ConversationActionButtons.php
Normal file
178
app/Misc/ConversationActionButtons.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace App\Misc;
|
||||
|
||||
class ConversationActionButtons
|
||||
{
|
||||
// Location constants
|
||||
const LOCATION_TOOLBAR = 'toolbar';
|
||||
const LOCATION_DROPDOWN = 'dropdown';
|
||||
const LOCATION_BOTH = 'both';
|
||||
|
||||
/**
|
||||
* Get all available conversation actions
|
||||
*/
|
||||
public static function getActions($conversation, $user, $mailbox)
|
||||
{
|
||||
$actions = [
|
||||
// Default toolbar actions
|
||||
'reply' => [
|
||||
'icon' => 'glyphicon-share-alt',
|
||||
'location' => self::LOCATION_TOOLBAR,
|
||||
'label' => __('Reply'),
|
||||
'permission' => function ($conversation) {
|
||||
return ( ! $conversation->isPhone() || ( $conversation->customer && $conversation->customer->getMainEmail() ) )
|
||||
&& \Eventy::filter('conversation.reply_button.enabled', true, $conversation);
|
||||
},
|
||||
'class' => 'conv-reply',
|
||||
'fixed_location' => true,
|
||||
],
|
||||
'note' => [
|
||||
'icon' => 'glyphicon-edit',
|
||||
'location' => self::LOCATION_TOOLBAR,
|
||||
'label' => __('Note'),
|
||||
'permission' => function ($conversation) {
|
||||
return \Eventy::filter('conversation.note_button.enabled', true, $conversation);
|
||||
},
|
||||
'class' => 'conv-add-note',
|
||||
'fixed_location' => true,
|
||||
],
|
||||
'delete' => [
|
||||
'icon' => 'glyphicon-trash',
|
||||
'location' => self::LOCATION_TOOLBAR,
|
||||
'label' => $conversation->state != \App\Conversation::STATE_DELETED ? __('Delete') : __('Delete Forever'),
|
||||
'permission' => function ($conversation, $user) {
|
||||
return $user->can('delete', $conversation);
|
||||
},
|
||||
'class' => $conversation->state != \App\Conversation::STATE_DELETED ? 'conv-delete' : 'conv-delete-forever',
|
||||
'fixed_location' => true,
|
||||
],
|
||||
'delete_mobile' => [
|
||||
'icon' => 'glyphicon-trash',
|
||||
'location' => self::LOCATION_DROPDOWN,
|
||||
'label' => $conversation->state != \App\Conversation::STATE_DELETED ? __('Delete') : __('Delete Forever'),
|
||||
'permission' => function ($conversation, $user) {
|
||||
return $user->can('delete', $conversation);
|
||||
},
|
||||
'class' => $conversation->state != \App\Conversation::STATE_DELETED ? 'conv-delete' : 'conv-delete-forever',
|
||||
'fixed_location' => true,
|
||||
'mobile_only' => true,
|
||||
],
|
||||
|
||||
// Default dropdown actions
|
||||
'follow' => [
|
||||
'icon' => 'glyphicon-bell',
|
||||
'location' => self::LOCATION_DROPDOWN,
|
||||
'label' => __('Follow'),
|
||||
'permission' => function () {
|
||||
return true;
|
||||
},
|
||||
'class' => 'conv-follow',
|
||||
'has_opposite' => true,
|
||||
'opposite' => [
|
||||
'label' => __('Unfollow'),
|
||||
'class' => 'conv-follow',
|
||||
],
|
||||
'fixed_location' => true,
|
||||
],
|
||||
'forward' => [
|
||||
'icon' => 'glyphicon-arrow-right',
|
||||
'location' => self::LOCATION_DROPDOWN,
|
||||
'label' => __('Forward'),
|
||||
'permission' => function () {
|
||||
return true;
|
||||
},
|
||||
'class' => 'conv-forward',
|
||||
'fixed_location' => true,
|
||||
],
|
||||
'merge' => [
|
||||
'icon' => 'glyphicon-indent-left',
|
||||
'location' => self::LOCATION_DROPDOWN,
|
||||
'label' => __('Merge'),
|
||||
'permission' => function ($conversation) {
|
||||
return ! $conversation->isChat();
|
||||
},
|
||||
'class' => '',
|
||||
'url' => function ($conversation) {
|
||||
return route('conversations.ajax_html', array_merge([ 'action' => 'merge_conv' ], \Request::all(), [ 'conversation_id' => $conversation->id ]));
|
||||
},
|
||||
'attrs' => [
|
||||
'data-trigger' => 'modal',
|
||||
'data-modal-title' => __('Merge Conversations'),
|
||||
'data-modal-no-footer' => 'true',
|
||||
'data-modal-on-show' => 'initMergeConv',
|
||||
],
|
||||
'fixed_location' => true,
|
||||
],
|
||||
'move' => [
|
||||
'icon' => 'glyphicon-log-out',
|
||||
'location' => self::LOCATION_DROPDOWN,
|
||||
'label' => __('Move'),
|
||||
'permission' => function ($conversation, $user) {
|
||||
return $user->can('move', \App\Conversation::class);
|
||||
},
|
||||
'class' => '',
|
||||
'url' => function ($conversation) {
|
||||
return route('conversations.ajax_html', array_merge([ 'action' => 'move_conv' ], \Request::all(), [ 'conversation_id' => $conversation->id ]));
|
||||
},
|
||||
'attrs' => [
|
||||
'data-trigger' => 'modal',
|
||||
'data-modal-title' => __('Move Conversation'),
|
||||
'data-modal-no-footer' => 'true',
|
||||
'data-modal-on-show' => 'initMoveConv',
|
||||
],
|
||||
'fixed_location' => true,
|
||||
],
|
||||
'print' => [
|
||||
'icon' => 'glyphicon-print',
|
||||
'location' => self::LOCATION_DROPDOWN,
|
||||
'label' => __('Print'),
|
||||
'class' => '',
|
||||
'permission' => function () {
|
||||
return true;
|
||||
},
|
||||
'url' => function () {
|
||||
return \Request::getRequestUri() . '&print=1';
|
||||
},
|
||||
'attrs' => [
|
||||
'target' => '_blank',
|
||||
],
|
||||
'fixed_location' => true,
|
||||
],
|
||||
];
|
||||
|
||||
// Allow overriding default actions while preserving backwards compatibility
|
||||
$actions = \Eventy::filter('conversation.get_action_buttons', $actions, $conversation, $user, $mailbox);
|
||||
|
||||
// Filter actions based on permissions
|
||||
foreach ($actions as $key => $action) {
|
||||
if (! $action['permission']($conversation, $user)) {
|
||||
unset($actions[ $key ]);
|
||||
}
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get actions for a specific location
|
||||
*/
|
||||
public static function getActionsByLocation($actions, $location)
|
||||
{
|
||||
return array_filter($actions, function ($action) use ($location) {
|
||||
if (! empty($action['fixed_location'])) {
|
||||
// Modified logic for responsive display
|
||||
if ($location === self::LOCATION_TOOLBAR) {
|
||||
// Show in toolbar if it's a toolbar action
|
||||
return $action['location'] === self::LOCATION_TOOLBAR;
|
||||
} elseif ($location === self::LOCATION_DROPDOWN) {
|
||||
// Show in dropdown if explicitly flagged or if it's a dropdown action
|
||||
return $action['location'] === self::LOCATION_DROPDOWN ||
|
||||
( ! empty($action['show_in_dropdown']) && $action['location'] === self::LOCATION_TOOLBAR );
|
||||
}
|
||||
}
|
||||
|
||||
return $action['location'] === $location;
|
||||
});
|
||||
}
|
||||
}
|
||||
27
app/Misc/Functions.php
Normal file
27
app/Misc/Functions.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
if (! function_exists('__j')) {
|
||||
/**
|
||||
* Translate the given message and prepare it for using in JS.
|
||||
*/
|
||||
function __j($key, $replace = [], $locale = null)
|
||||
{
|
||||
$text = __($key, $replace, $locale);
|
||||
|
||||
return strtr($text, [
|
||||
'"' => '"',
|
||||
"'" => ''',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the given message and prepare it for using in HTML.
|
||||
*
|
||||
*/
|
||||
function __h($key, $replace = [], $locale = null)
|
||||
{
|
||||
$text = __($key, $replace, $locale);
|
||||
|
||||
return htmlspecialchars($text);
|
||||
}
|
||||
}
|
||||
@@ -32,14 +32,18 @@ class Helper
|
||||
|
||||
/**
|
||||
* Limit for IN queries.
|
||||
* MariaDB may not work with more than 999 elements in IN clause.
|
||||
* https://github.com/freescout-help-desk/freescout/issues/4623
|
||||
*/
|
||||
const IN_LIMIT = 1000;
|
||||
const IN_LIMIT = 999;
|
||||
|
||||
/**
|
||||
* Permissions for directories.
|
||||
*/
|
||||
const DIR_PERMISSIONS = 0755;
|
||||
|
||||
const DB_INT_MAX = 2147483647;
|
||||
|
||||
public static $csp_nonce = null;
|
||||
|
||||
/**
|
||||
@@ -59,6 +63,8 @@ class Helper
|
||||
'php.*',
|
||||
'sh',
|
||||
'pl',
|
||||
'phtml',
|
||||
'phar',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -324,6 +330,9 @@ class Helper
|
||||
'ja' => ['name' => '日本語',
|
||||
'name_en' => 'Japanese',
|
||||
],
|
||||
'kz' => ['name' => 'қазақ тілі',
|
||||
'name_en' => 'Kazakh',
|
||||
],
|
||||
'ko' => ['name' => '한국어 (韓國語)',
|
||||
'name_en' => 'Korean (Johab)',
|
||||
],
|
||||
@@ -518,19 +527,21 @@ class Helper
|
||||
{
|
||||
// Remove all kinds of spaces after tags.
|
||||
// https://stackoverflow.com/questions/3230623/filter-all-types-of-whitespace-in-php
|
||||
//
|
||||
// Keep in mind that preg_replace() may return NULL if "u" flag is used.
|
||||
$text = preg_replace("/^(.*)>[\r\n]*\s+/mu", '$1>', $text ?? '');
|
||||
|
||||
// Remove <script> and <style> blocks.
|
||||
$text = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $text);
|
||||
$text = preg_replace('#<style(.*?)>(.*?)</style>#is', '', $text);
|
||||
$text = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $text ?? '');
|
||||
$text = preg_replace('#<style(.*?)>(.*?)</style>#is', '', $text ?? '');
|
||||
|
||||
// Remove tags.
|
||||
$text = strip_tags($text);
|
||||
$text = preg_replace('/\s+/mu', ' ', $text);
|
||||
$text = strip_tags($text ?? '');
|
||||
$text = preg_replace('/\s+/mu', ' ', $text ?? '');
|
||||
|
||||
// Trim
|
||||
$text = trim($text);
|
||||
$text = preg_replace('/^\s+/mu', '', $text);
|
||||
$text = trim($text ?? '');
|
||||
$text = preg_replace('/^\s+/mu', '', $text ?? '');
|
||||
|
||||
// Causes "General error: 1366 Incorrect string value"
|
||||
// Remove "undetectable" whitespaces
|
||||
@@ -541,11 +552,60 @@ class Helper
|
||||
// }
|
||||
// $text = urldecode($text);
|
||||
|
||||
$text = trim(preg_replace('/[ ]+/', ' ', $text));
|
||||
$text = trim(preg_replace('/[ ]+/', ' ', $text ?? ''));
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
public static function stripDangerousTags($html, $allowed_tags = [])
|
||||
{
|
||||
// <script src="/storage/attachment/8/1/1/test.js?id=7&token=c4786c4497db3c6254a0c310623a43c3">
|
||||
// <iframe src="/storage/attachment/8/1/1/1.html?id=95&token=3dced8dc80305031b358119f3d156204"></iframe>
|
||||
// <object data="/storage/attachment/8/1/1/1.html?id=95&token=3dced8dc80305031b358119f3d156204" type="text/html"></object>
|
||||
$tags = ['script', 'form', 'iframe', 'object'];
|
||||
$attrs = 'src|data';
|
||||
|
||||
$tags = array_diff($tags, $allowed_tags);
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
$html = preg_replace('#<'.$tag.'(.*?)>(.*?)<\s*/\s*'.$tag.'\s*>#is', '', $html ?? '');
|
||||
|
||||
// Remove unclosed restricted tags.
|
||||
$html = preg_replace('#<'.$tag.'(.*?)>#is', '', $html ?? '');
|
||||
}
|
||||
|
||||
// If some tag is allowed make sure that it does not point to the file on the current server.
|
||||
if (!empty($allowed_tags)) {
|
||||
foreach ($allowed_tags as $tag) {
|
||||
$html = preg_replace_callback('#<'.$tag.'(.*?)>#is',
|
||||
function ($matches) use ($attrs) {
|
||||
preg_match("/(src|data)\s*=\s*['\"]([^'\"]+)['\"]/i", $matches[1], $attr_match);
|
||||
if (!empty($attr_match[2])) {
|
||||
$url = trim($attr_match[2]);
|
||||
$parts = parse_url($url);
|
||||
|
||||
// Remove tag.
|
||||
if (!preg_match("#^(https?:)?//#i", $url)
|
||||
|| empty($parts['host'])
|
||||
|| (strtolower($parts['host']) == strtolower(self::getDomain()))
|
||||
|| preg_match("#/storage/attachment/.*token#", $parts['host'])
|
||||
|| preg_match("#/storage/uploads/.*\.#", $parts['host'])
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return $matches[0];
|
||||
},
|
||||
$html
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preview of the text in a plain form.
|
||||
*/
|
||||
@@ -627,7 +687,7 @@ class Helper
|
||||
/**
|
||||
* Resize image without using Intervention package.
|
||||
*/
|
||||
public static function resizeImage($file, $mime_type, $thumb_width, $thumb_height)
|
||||
public static function resizeImage($file, $mime_type, $thumb_width, $thumb_height, $transparency = false)
|
||||
{
|
||||
list($width, $height) = getimagesize($file);
|
||||
if (!$width) {
|
||||
@@ -637,8 +697,10 @@ class Helper
|
||||
if (preg_match('/png/i', $mime_type)) {
|
||||
$src = imagecreatefrompng($file);
|
||||
|
||||
$kek = imagecolorallocate($src, 255, 255, 255);
|
||||
imagefill($src, 0, 0, $kek);
|
||||
if (!$transparency) {
|
||||
$kek = imagecolorallocate($src, 255, 255, 255);
|
||||
imagefill($src, 0, 0, $kek);
|
||||
}
|
||||
} elseif (preg_match('/gif/i', $mime_type)) {
|
||||
$src = imagecreatefromgif($file);
|
||||
|
||||
@@ -666,6 +728,10 @@ class Helper
|
||||
}
|
||||
|
||||
$thumb = imagecreatetruecolor($thumb_width, $thumb_height);
|
||||
if ($transparency && preg_match('/png/i', $mime_type)) {
|
||||
imagealphablending($thumb, false);
|
||||
imagesavealpha($thumb, true);
|
||||
}
|
||||
// Resize and crop
|
||||
imagecopyresampled($thumb,
|
||||
$src,
|
||||
@@ -749,9 +815,9 @@ class Helper
|
||||
return 'Error: '.$e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')';
|
||||
}
|
||||
|
||||
public static function denyAccess()
|
||||
public static function denyAccess($msg = '')
|
||||
{
|
||||
abort(403, 'This action is unauthorized.');
|
||||
abort(403, $msg ?: 'This action is unauthorized.');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -801,6 +867,21 @@ class Helper
|
||||
\Log::error($prefix.self::formatException($e));
|
||||
}
|
||||
|
||||
public static function encrypt($value, $password = null)
|
||||
{
|
||||
try {
|
||||
if (!$password) {
|
||||
$value = encrypt($value);
|
||||
} else {
|
||||
$value = (new \Illuminate\Encryption\Encrypter(md5($password)))->encrypt($value);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely decrypt.
|
||||
*
|
||||
@@ -808,10 +889,20 @@ class Helper
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public static function decrypt($value)
|
||||
public static function decrypt($value, $password = null, $force_unserialize = false)
|
||||
{
|
||||
try {
|
||||
$value = decrypt($value);
|
||||
if (!$password) {
|
||||
$value = app('encrypter')->decrypt($value, false);
|
||||
} else {
|
||||
$value = (new \Illuminate\Encryption\Encrypter(md5($password)))->decrypt($value, false);
|
||||
}
|
||||
|
||||
// If the value is scalar - unserialize it,
|
||||
// Otherwise - do not, as objects may contain dangerous code.
|
||||
if (!preg_match("^[idsa]:", $value) || $force_unserialize) {
|
||||
$value = unserialize($value);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Do nothing.
|
||||
}
|
||||
@@ -876,6 +967,16 @@ class Helper
|
||||
}
|
||||
}
|
||||
|
||||
public static function setUserLocale($user_locale = '')
|
||||
{
|
||||
if (!$user_locale) {
|
||||
$user_locale = \Eventy::filter('locale', session('user_locale'));
|
||||
}
|
||||
if ($user_locale) {
|
||||
\Helper::setLocale($user_locale);
|
||||
}
|
||||
}
|
||||
|
||||
public static function setLocale($locale)
|
||||
{
|
||||
if (in_array($locale, config('app.locales'))) {
|
||||
@@ -896,14 +997,14 @@ class Helper
|
||||
if (is_string($locale) && isset(self::$locales[$locale])) {
|
||||
$data = self::$locales[$locale];
|
||||
} else {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($param) {
|
||||
if (isset(self::$locales[$locale])) {
|
||||
return self::$locales[$locale][$param];
|
||||
} else {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return $data;
|
||||
@@ -928,6 +1029,8 @@ class Helper
|
||||
$env_path = app()->environmentFilePath();
|
||||
$contents = file_get_contents($env_path);
|
||||
|
||||
$value = preg_replace("#[\r\n\t]#", '', $value);
|
||||
|
||||
if (strstr($value, '"')) {
|
||||
// Escape quotes.
|
||||
$value = '"'.str_replace('"', '\"', $value).'"';
|
||||
@@ -1183,7 +1286,19 @@ class Helper
|
||||
*/
|
||||
public static function strSplitKeepWords($str, $max_length = 75)
|
||||
{
|
||||
$array_words = explode(' ', $str);
|
||||
$space = html_entity_decode(' ');
|
||||
|
||||
$str = strtr($str, [
|
||||
'、' => '、'.$space,
|
||||
'。' => '。'.$space,
|
||||
// '.' => '.'.$space,
|
||||
// ',' => ','.$space,
|
||||
//':' => ':'.$space,
|
||||
// '—' => '—'.$space,
|
||||
// '।' => '।'.$space,
|
||||
]);
|
||||
|
||||
$array_words = explode($space, $str);
|
||||
|
||||
$currentLength = 0;
|
||||
|
||||
@@ -1240,46 +1355,63 @@ class Helper
|
||||
}
|
||||
|
||||
/**
|
||||
* It looks like this is not used anywhere.
|
||||
* Json encode to avoid "Unable to JSON encode payload. Error code: 5"
|
||||
*/
|
||||
// public static function jsonEncodeSafe($value, $options = 0, $depth = 512, $utfErrorFlag = false)
|
||||
// {
|
||||
// $encoded = json_encode($value, $options, $depth);
|
||||
// switch (json_last_error()) {
|
||||
// case JSON_ERROR_NONE:
|
||||
// return $encoded;
|
||||
// // case JSON_ERROR_DEPTH:
|
||||
// // return 'Maximum stack depth exceeded'; // or trigger_error() or throw new Exception()
|
||||
// // case JSON_ERROR_STATE_MISMATCH:
|
||||
// // return 'Underflow or the modes mismatch'; // or trigger_error() or throw new Exception()
|
||||
// // case JSON_ERROR_CTRL_CHAR:
|
||||
// // return 'Unexpected control character found';
|
||||
// // case JSON_ERROR_SYNTAX:
|
||||
// // return 'Syntax error, malformed JSON'; // or trigger_error() or throw new Exception()
|
||||
// case JSON_ERROR_UTF8:
|
||||
// $clean = self::utf8ize($value);
|
||||
// if ($utfErrorFlag) {
|
||||
// //return 'UTF8 encoding error'; // or trigger_error() or throw new Exception()
|
||||
// }
|
||||
// return self::jsonEncodeSafe($clean, $options, $depth, true);
|
||||
// // default:
|
||||
// // return 'Unknown error'; // or trigger_error() or throw new Exception()
|
||||
public static function jsonEncodeSafe($value, $options = 0, $depth = 512, $attempt = 1)
|
||||
{
|
||||
$msg = '';
|
||||
|
||||
$encoded = json_encode($value, $options, $depth);
|
||||
|
||||
// }
|
||||
// }
|
||||
switch (json_last_error()) {
|
||||
case JSON_ERROR_NONE:
|
||||
return $encoded;
|
||||
case JSON_ERROR_DEPTH:
|
||||
$msg = 'Maximum stack depth exceeded';
|
||||
break;
|
||||
case JSON_ERROR_STATE_MISMATCH:
|
||||
$msg = 'Underflow or the modes mismatch';
|
||||
break;
|
||||
case JSON_ERROR_CTRL_CHAR:
|
||||
$msg = 'Unexpected control character found';
|
||||
break;
|
||||
case JSON_ERROR_SYNTAX:
|
||||
$msg = 'Syntax error, malformed JSON';
|
||||
break;
|
||||
case JSON_ERROR_UTF8:
|
||||
$clean = self::utf8ize($value);
|
||||
if ($attempt > 1) {
|
||||
$msg = 'UTF8 encoding error';
|
||||
} else {
|
||||
return self::jsonEncodeSafe($clean, $options, $depth, 2);
|
||||
}
|
||||
break;
|
||||
// default:
|
||||
// return '';
|
||||
}
|
||||
throw new \Exception("Could not encode JSON: ".$msg, 1);
|
||||
//return '';
|
||||
}
|
||||
|
||||
// public static function utf8ize($mixed)
|
||||
// {
|
||||
// if (is_array($mixed)) {
|
||||
// foreach ($mixed as $key => $value) {
|
||||
// $mixed[$key] = self::utf8ize($value);
|
||||
// }
|
||||
// } else if (is_string ($mixed)) {
|
||||
// return utf8_encode($mixed);
|
||||
// }
|
||||
// return $mixed;
|
||||
// }
|
||||
public static function utf8ize($mixed)
|
||||
{
|
||||
if (is_array($mixed)) {
|
||||
foreach ($mixed as $key => $value) {
|
||||
$mixed[$key] = self::utf8ize($value);
|
||||
}
|
||||
} else if (is_string($mixed)) {
|
||||
return self::utf8Encode($mixed);
|
||||
}
|
||||
return $mixed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replacement for utf8_encode().
|
||||
*/
|
||||
public static function utf8Encode($string)
|
||||
{
|
||||
return mb_convert_encoding($string, 'UTF-8', 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if host is available on the port specified.
|
||||
@@ -1351,13 +1483,17 @@ class Helper
|
||||
case 'http':
|
||||
case 'https':
|
||||
//$value = preg_replace_callback('~(?:(https?)://([^\s<]+)|(www\.[^\s<]+?\.[^\s<]+))(?<![\.,:])~i', function ($match) use ($protocol, &$links, $attr) {
|
||||
$value = preg_replace_callback('%(\b(([\w-]+)://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))%s', function ($match) use ($protocol, &$links, $attr) {
|
||||
if ($match[3]) {
|
||||
$protocol = $match[3];
|
||||
//$value = preg_replace_callback('%(\b(([\w-]+)://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))%s', function ($match) use ($protocol, &$links, $attr) {
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3402
|
||||
$nbsp = html_entity_decode(' ');
|
||||
$value = preg_replace_callback('%([>\r\n\s:;\( '.$nbsp.']|^)((([\w-]+)://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))%s', function ($match) use ($protocol, &$links, $attr) {
|
||||
if ($match[4]) {
|
||||
$protocol = $match[4];
|
||||
}
|
||||
$link = $match[1];
|
||||
$link = substr($link, strlen($match[2]));
|
||||
return '<' . array_push($links, "<a $attr href=\"$protocol://$link\">$protocol://$link</a>") . '>';
|
||||
$link = $match[2];
|
||||
$link = substr($link, strlen($match[3]));
|
||||
//return '<' . array_push($links, "<a $attr href=\"$protocol://$link\">$protocol://$link</a>") . '>';
|
||||
return $match[1].'<' . array_push($links, "<a $attr href=\"$protocol://$link\">".$match[2]."</a>") . '>';
|
||||
}, $value) ?: $value;
|
||||
break;
|
||||
case 'mail': $value = preg_replace_callback('~([^\s<>]+?@[^\s<]+?\.[^\s<]+)(?<![\.,:\)])~', function ($match) use (&$links, $attr) { return '<' . array_push($links, "<a $attr href=\"mailto:{$match[1]}\">{$match[1]}</a>") . '>'; }, $value) ?: $value;
|
||||
@@ -1367,7 +1503,7 @@ class Helper
|
||||
}
|
||||
}
|
||||
|
||||
// Insert all link
|
||||
// Insert all links
|
||||
return preg_replace_callback('/<(\d+)>/', function ($match) use (&$links) { return $links[$match[1] - 1]; }, $value ?? '') ?: $value;
|
||||
}
|
||||
|
||||
@@ -1409,7 +1545,7 @@ class Helper
|
||||
$pids = [];
|
||||
|
||||
try {
|
||||
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep '".$search."'"));
|
||||
$processes = preg_split("/[\r\n]/", \Helper::shellExec("ps aux | grep '".$search."'"));
|
||||
foreach ($processes as $process) {
|
||||
$process = trim($process);
|
||||
preg_match("/^[\S]+\s+([\d]+)\s+/", $process, $m);
|
||||
@@ -1537,6 +1673,14 @@ class Helper
|
||||
return self::isPgSql() ? 'ilike' : 'like';
|
||||
}
|
||||
|
||||
// PostgreSQL truncates string if it contains \u0000 symbol starting from this symbol.
|
||||
// https://stackoverflow.com/questions/31671634/handling-unicode-sequences-in-postgresql
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3485
|
||||
public static function sqlSanitizeString($string)
|
||||
{
|
||||
return str_replace(json_decode('"\u0000"'), "", $string);
|
||||
}
|
||||
|
||||
public static function humanFileSize($size, $unit="")
|
||||
{
|
||||
if ((!$unit && $size >= 1<<30) || $unit == "GB") {
|
||||
@@ -1615,24 +1759,31 @@ class Helper
|
||||
|
||||
// 307 - Temporary Redirect.
|
||||
if (!preg_match("/(200|301|302|307)/", $headers[0])) {
|
||||
return false;
|
||||
throw new \Exception('HTTP Status Code: '.$headers[0], 1);
|
||||
//return false;
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
\Helper::setCurlDefaultOptions($ch);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 180);
|
||||
$contents = curl_exec($ch);
|
||||
|
||||
if (curl_errno($ch)) {
|
||||
throw new \Exception(curl_errno($ch).' '.curl_error($ch), 1);
|
||||
$curl_errno = curl_errno($ch);
|
||||
|
||||
if ($curl_errno) {
|
||||
throw new \Exception('Curl Error Number: '.$curl_errno, 1);
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if (!$contents) {
|
||||
return false;
|
||||
if ($contents == '') {
|
||||
$https_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
throw new \Exception('Empty Response. Curl Error Number: '.$curl_errno.'. Response Status Code: '.$https_status, 1);
|
||||
//return false;
|
||||
} else {
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
return $contents;
|
||||
@@ -1686,7 +1837,7 @@ class Helper
|
||||
} elseif ($ext == 'pdf') {
|
||||
// Rename PDF to avoid running embedded JavaScript.
|
||||
if ($uploaded_file && !$contents) {
|
||||
$contents = file_get_contents($uploaded_file->getRealPath());
|
||||
$contents = file_get_contents($uploaded_file->getRealPath() ?: $uploaded_file->getPathname());
|
||||
}
|
||||
if ($contents && strstr($contents, '/JavaScript')) {
|
||||
$file_name = $file_name.'_';
|
||||
@@ -1722,6 +1873,11 @@ class Helper
|
||||
$file_name = mb_convert_encoding($file_name, 'UTF-8', 'UTF-8');
|
||||
$file_name = preg_replace('/[' . $escaped_regex . ']/', '_', $file_name);
|
||||
$file_name = preg_replace("/[\t\r\n]/", '', $file_name);
|
||||
// Remove unprintable characters and invalid unicode characters.
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4681
|
||||
$file_name = preg_replace("#\p{C}+#u", '', $file_name);
|
||||
// https://github.com/freescout-help-desk/freescout/issues/2123#issuecomment-2775392740
|
||||
$file_name = preg_replace("#\p{Cf}+#u", '', $file_name);
|
||||
|
||||
return $file_name;
|
||||
}
|
||||
@@ -1765,7 +1921,33 @@ class Helper
|
||||
|
||||
public static function isHttps($url = '')
|
||||
{
|
||||
return self::getProtocol($url) == 'https';
|
||||
if (\Helper::isInstaller()) {
|
||||
// In the Installer we determine HTTPS from URL.
|
||||
return self::isCurrentUrlHttps();
|
||||
} else {
|
||||
return self::getProtocol($url) == 'https';
|
||||
}
|
||||
}
|
||||
|
||||
public static function isInstaller()
|
||||
{
|
||||
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
$request_uri = preg_replace("#\?.*#", '', $request_uri);
|
||||
|
||||
return strstr($request_uri, '/install/') || preg_match("#/install$#", $request_uri);
|
||||
}
|
||||
|
||||
public static function isCurrentUrlHttps()
|
||||
{
|
||||
if (in_array(strtolower($_SERVER['X_FORWARDED_PROTO'] ?? ''), array('https', 'on', 'ssl', '1'), true)
|
||||
|| strtolower($_SERVER['HTTPS'] ?? '') == 'on'
|
||||
|| ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') == 'https'
|
||||
|| ($_SERVER['HTTP_CF_VISITOR'] ?? '') == '{"scheme":"https"}'
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function fixProtocol($url)
|
||||
@@ -1883,7 +2065,7 @@ class Helper
|
||||
if (self::isConsole() || !function_exists('shell_exec')) {
|
||||
$pcntl_enabled = extension_loaded('pcntl');
|
||||
} else {
|
||||
$pcntl_enabled = preg_match("/enable/m", shell_exec("php -i | grep pcntl") ?? '');
|
||||
$pcntl_enabled = preg_match("/enable/m", \Helper::shellExec("php -i | grep pcntl") ?? '');
|
||||
}
|
||||
$php_extensions['pcntl (console PHP)'] = $pcntl_enabled;
|
||||
|
||||
@@ -1897,7 +2079,13 @@ class Helper
|
||||
'proc_open (PHP)' => function_exists('proc_open'),
|
||||
'fpassthru (PHP)' => function_exists('fpassthru'),
|
||||
'symlink (PHP)' => function_exists('symlink'),
|
||||
'ps (shell)' => function_exists('shell_exec') ? shell_exec('ps') : false,
|
||||
'iconv (PHP)' => function_exists('iconv'),
|
||||
// If posix_isatty() function is not enabled on the server the question in the
|
||||
// console command makes it wait infinitely and be aborted.
|
||||
// Commands should avoid using interctive functions or use special flags.
|
||||
//'posix_isatty (PHP)' => function_exists('posix_isatty'),
|
||||
'pcntl_signal (console PHP)' => function_exists('shell_exec') ? (int)\Helper::shellExec('php -r "echo (int)function_exists(\'pcntl_signal\');"') : false,
|
||||
'ps (shell)' => function_exists('shell_exec') ? \Helper::shellExec('ps') : false,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1923,7 +2111,7 @@ class Helper
|
||||
if (\Option::get('send_emails_problem')) {
|
||||
$flashes[] = [
|
||||
'type' => 'warning',
|
||||
'text' => __('There is a problem processing outgoing mail queue — an admin should check :%a_begin%System Status:%a_end% and :%a_begin_recommendations%Recommendations:%a_end%', ['%a_begin%' => '<a href="'.route('system').'#cron" target="_blank">', '%a_end%' => '</a>', /*'%a_begin_logs%' => '<a href="'.route('logs', ['name' => 'send_errors']).'#cron" target="_blank">',*/ '%a_begin_recommendations%' => '<a href="https://github.com/freescout-helpdesk/freescout/wiki/Background-Jobs" target="_blank">']),
|
||||
'text' => __('There is a problem processing outgoing mail queue — an admin should check :%a_begin%System Status:%a_end% and :%a_begin_recommendations%Recommendations:%a_end%', ['%a_begin%' => '<a href="'.route('system').'#cron" target="_blank">', '%a_end%' => '</a>', /*'%a_begin_logs%' => '<a href="'.route('logs', ['name' => 'send_errors']).'#cron" target="_blank">',*/ '%a_begin_recommendations%' => '<a href="'.config('app.freescout_repo').'/wiki/Background-Jobs" target="_blank">']),
|
||||
'unescaped' => true,
|
||||
];
|
||||
}
|
||||
@@ -1997,7 +2185,7 @@ class Helper
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, config('app.curl_ssl_verifypeer'));
|
||||
}
|
||||
|
||||
public static function setGuzzleDefaultOptions($params)
|
||||
public static function setGuzzleDefaultOptions($params = [])
|
||||
{
|
||||
$default_params = [
|
||||
'timeout' => config('app.curl_timeout'),
|
||||
@@ -2027,9 +2215,28 @@ class Helper
|
||||
|
||||
$nonce = \Helper::cspNonce();
|
||||
|
||||
return "<meta http-equiv=\"Content-Security-Policy\" content=\"script-src 'self' 'nonce-".$nonce."' "
|
||||
.config('app.csp_script_src').' '.\Eventy::filter('csp.script_src', '')."\">";
|
||||
//<meta property=\"csp-nonce\" id=\"csp-nonce\" content=\"".$nonce."\">";
|
||||
$script_src = config('app.csp_script_src').' '.\Eventy::filter('csp.script_src', '');
|
||||
|
||||
$script_domains = '';
|
||||
$scripts = explode(' ', $script_src);
|
||||
|
||||
foreach ($scripts as $url) {
|
||||
$url = trim($url);
|
||||
if (!preg_match("#^(http|//)#", $url)) {
|
||||
$url = '//'.$url;
|
||||
}
|
||||
$parts = parse_url($url);
|
||||
if (!empty($parts['host'])) {
|
||||
$domain = preg_replace("#['\"; \r\n]#", '', $parts['host']);
|
||||
$script_domains .= ' '.$domain;
|
||||
}
|
||||
}
|
||||
|
||||
// frame-src https://recaptcha.net; connect-src https://recaptcha.net;
|
||||
|
||||
return "<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self' ".$script_domains."; img-src * 'self' data:; font-src * 'self' data:; style-src * 'self' 'unsafe-inline'; form-action 'self'; frame-src * 'self'; script-src 'self' 'nonce-".$nonce."' "
|
||||
.$script_src.";"
|
||||
.config('app.csp_custom').\Eventy::filter('csp.custom', '')."\">";
|
||||
}
|
||||
|
||||
public static function cspNonceAttr()
|
||||
@@ -2059,4 +2266,44 @@ class Helper
|
||||
\Session::forget('chat_mode');
|
||||
}
|
||||
}
|
||||
|
||||
public static function detectCloudFlare()
|
||||
{
|
||||
if (!empty($_SERVER['HTTP_CF_IPCOUNTRY'])
|
||||
|| !empty($_SERVER['HTTP_CF_CONNECTING_IP'])
|
||||
|| !empty($_SERVER['HTTP_CF_VISITOR'])
|
||||
|| !empty($_SERVER['HTTP_CF_RAY'])
|
||||
|| ($_SERVER['HTTP_CDN_LOOP'] ?? '') == 'cloudflare'
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Correct format: 2023-12-14 19:21
|
||||
// Datepicker with enableTime option enabled
|
||||
// may return value in different format on iOS Safari: 2023-12-14T11:25
|
||||
public static function sanitizeDatepickerDatetime($datetime)
|
||||
{
|
||||
return str_replace('T', ' ', $datetime);
|
||||
}
|
||||
|
||||
// To catch possible exception:
|
||||
// shell_exec(): Unable to execute
|
||||
public static function shellExec($command)
|
||||
{
|
||||
try {
|
||||
return shell_exec($command);
|
||||
} catch (\Exception $e) {
|
||||
self::logException($e, '\Helper::shellExec() - ');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public static function startsiWith($text, $string)
|
||||
{
|
||||
return (stripos($text, $string) === 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace App\Misc;
|
||||
use App\Mailbox;
|
||||
use App\Option;
|
||||
use App\SendLog;
|
||||
use Webklex\IMAP\Client;
|
||||
//use Webklex\IMAP\Client;
|
||||
|
||||
// todo: rename into MailHelper
|
||||
class Mail
|
||||
@@ -15,6 +15,7 @@ class Mail
|
||||
*/
|
||||
const REPLY_SEPARATOR_HTML = 'fsReplyAbove';
|
||||
const REPLY_SEPARATOR_TEXT = '-- Please reply above this line --';
|
||||
const REPLY_SEPARATOR_NOTIFICATION = 'fsNotifReplyAbove';
|
||||
|
||||
/**
|
||||
* Message-ID prefixes for outgoing emails.
|
||||
@@ -48,6 +49,7 @@ class Mail
|
||||
const FETCH_SCHEDULE_HOURLY = 60;
|
||||
|
||||
const OAUTH_PROVIDER_MICROSOFT = 'ms';
|
||||
const OAUTH_MICROSOFT_SMTP = 'smtp.office365.com';
|
||||
|
||||
/**
|
||||
* If reply is not extracted properly from the incoming email, add here a new separator.
|
||||
@@ -60,7 +62,11 @@ class Mail
|
||||
self::REPLY_SEPARATOR_TEXT, // Our plain text separator
|
||||
|
||||
// Email service providers specific separators.
|
||||
// <div class="gmail_quote" style="font-family:sans-serif;">
|
||||
'<div class="gmail_quote">', // Gmail
|
||||
'<div class="gmail_quote" ', // Gmail
|
||||
'<div class="gmail_quote gmail_quote_container"', // Gmail
|
||||
'<div class="protonmail_quote">', // https://github.com/freescout-help-desk/freescout/issues/4537
|
||||
'<div id="appendonsend"></div>', // Outlook / Live / Hotmail / Microsoft
|
||||
'<div name="quote" ',
|
||||
'yahoo_quoted_', // Yahoo, full: <div id=3D"ydp6h4f5c59yahoo_quoted_2937493705"
|
||||
@@ -68,15 +74,45 @@ class Mail
|
||||
'------------------ Original ------------------', // QQ English
|
||||
'<div id=3D"divRplyFwdMsg" dir=', // Outlook
|
||||
'regex:/<div style="border:none;border\-top:solid \#[A-Z0-9]{6} 1\.0pt;padding:3\.0pt 0in 0in 0in">[^<]*<p class="MsoNormal"><b>/', // MS Outlook
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4629#issuecomment-2870297514
|
||||
'regex:/<div style="border:none;border\-top:solid \#[A-Z0-9]{6} 1\.0pt;padding:3\.0pt 0cm 0cm 0cm">[^<]*<p class="MsoNormal"><b>/', // MS Outlook
|
||||
|
||||
// General separators.
|
||||
'regex:/<blockquote((?!quote)[^>])*>/', // General sepator. Should skip Gmail's <blockquote class="gmail_quote">.
|
||||
//'regex:/<blockquote((?!quote)[^>])*>/', // General sepator. Should skip Gmail's <blockquote class="gmail_quote">.
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4629#issuecomment-2870299221
|
||||
'<blockquote type="cite"',
|
||||
// This is used both for quotes and replies.
|
||||
//'<blockquote class="gmail_quote"',
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4629#issuecomment-2874816496
|
||||
'<div dir="auto" id="mail-editor-reference-message-container">',
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4764
|
||||
'<!--html--><section>',
|
||||
'regex:/<!\-\-html\-\->\s*<section>/',
|
||||
'<!-- originalMessage -->',
|
||||
'‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐',
|
||||
'--------------- Original Message ---------------',
|
||||
'-------- Αρχικό μήνυμα --------', // Greek
|
||||
];
|
||||
|
||||
/**
|
||||
* Used to substitue encoding during mail body decoding
|
||||
* via iconv() or mb_convert_encoding().
|
||||
* https://github.com/freescout-help-desk/freescout/issues/4282
|
||||
*/
|
||||
public static $encoding_substitution = [
|
||||
'iso-2022-jp' => 'iso-2022-jp-ms',
|
||||
'gb2312' => 'gb18030',
|
||||
];
|
||||
|
||||
/**
|
||||
* Used when decoding mime strings.
|
||||
*/
|
||||
public static $mime_encoding_substitution = [
|
||||
'iso-2022-jp' => 'iso-2022-jp-ms',
|
||||
'ks_c_5601-1987' => 'cp949',
|
||||
//'gb2312' => 'gb18030',
|
||||
];
|
||||
|
||||
/**
|
||||
* md5 of the last applied mail config.
|
||||
*/
|
||||
@@ -86,6 +122,11 @@ class Mail
|
||||
* Used to get SMTP queue id when sending emails to customers.
|
||||
*/
|
||||
public static $smtp_queue_id_plugin_registered = false;
|
||||
|
||||
/**
|
||||
* Used to store the last sent email message.
|
||||
*/
|
||||
public static $smtp_mime_message = '';
|
||||
|
||||
/**
|
||||
* Configure mail sending parameters.
|
||||
@@ -97,20 +138,52 @@ class Mail
|
||||
public static function setMailDriver($mailbox = null, $user_from = null, $conversation = null)
|
||||
{
|
||||
if ($mailbox) {
|
||||
// Configure mail driver according to Mailbox settings
|
||||
// Configure mail driver according to Mailbox settings.
|
||||
$oauth = $mailbox->outOauthEnabled();
|
||||
|
||||
// Refresh Access Token.
|
||||
if ($oauth) {
|
||||
if ((strtotime($mailbox->oauthGetParam('issued_on')) + (int)$mailbox->oauthGetParam('expires_in')) < time()) {
|
||||
// Try to get an access token (using the authorization code grant)
|
||||
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
'client_id' => $mailbox->out_username,
|
||||
'client_secret' => $mailbox->out_password,
|
||||
'refresh_token' => $mailbox->oauthGetParam('r_token'),
|
||||
]);
|
||||
|
||||
if (!empty($token_data['a_token'])) {
|
||||
$mailbox->setMetaParam('oauth', $token_data, true);
|
||||
} elseif (!empty($token_data['error'])) {
|
||||
$error_message = 'Error occurred refreshing oAuth Access Token: '.$token_data['error'];
|
||||
\Helper::log(\App\ActivityLog::NAME_EMAILS_SENDING,
|
||||
\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR_TO_CUSTOMER, [
|
||||
'error' => $error_message,
|
||||
'mailbox' => $mailbox->name,
|
||||
]);
|
||||
//throw new \Exception($error_message, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
\Config::set('mail.driver', $mailbox->getMailDriverName());
|
||||
\Config::set('mail.from', $mailbox->getMailFrom($user_from, $conversation));
|
||||
|
||||
// SMTP
|
||||
// SMTP.
|
||||
if ($mailbox->out_method == Mailbox::OUT_METHOD_SMTP) {
|
||||
\Config::set('mail.host', $mailbox->out_server);
|
||||
\Config::set('mail.port', $mailbox->out_port);
|
||||
if (!$mailbox->out_username) {
|
||||
\Config::set('mail.username', null);
|
||||
\Config::set('mail.password', null);
|
||||
if ($oauth) {
|
||||
\Config::set('mail.auth_mode', 'XOAUTH2');
|
||||
\Config::set('mail.username', $mailbox->email);
|
||||
\Config::set('mail.password', $mailbox->oauthGetParam('a_token'));
|
||||
} else {
|
||||
\Config::set('mail.username', $mailbox->out_username);
|
||||
\Config::set('mail.password', $mailbox->out_password);
|
||||
if (!$mailbox->out_username) {
|
||||
\Config::set('mail.username', null);
|
||||
\Config::set('mail.password', null);
|
||||
} else {
|
||||
\Config::set('mail.username', $mailbox->out_username);
|
||||
\Config::set('mail.password', $mailbox->out_password);
|
||||
}
|
||||
}
|
||||
\Config::set('mail.encryption', $mailbox->getOutEncryptionName());
|
||||
}
|
||||
@@ -222,6 +295,33 @@ class Mail
|
||||
|
||||
$vars = \Eventy::filter('mail_vars.replace', $vars, $data);
|
||||
|
||||
/**
|
||||
* Retrieves all mail var codes from the text, including fallback values.
|
||||
*
|
||||
* @link https://regex101.com/r/icWukp/1
|
||||
*/
|
||||
preg_match_all(
|
||||
'#\{%(?<var>[a-zA-Z.]+)(,fallback=(?<fallback>[^}]*))?%\}#',
|
||||
$text,
|
||||
$matches
|
||||
);
|
||||
|
||||
// Add fallback values to the $vars array, if present.
|
||||
foreach($matches['var'] as $i => $var) {
|
||||
$merge_code = "{%{$var}%}";
|
||||
$full_match = $matches[0][$i];
|
||||
$has_fallback = false !== strpos($full_match, ',fallback=');
|
||||
$fallback_val = $has_fallback ? $matches['fallback'][$i] ?? null : null;
|
||||
$merge_val = isset($vars[$merge_code]) ? $vars[$merge_code] : $fallback_val;
|
||||
|
||||
if (null !== $merge_val || true === $remove_non_replaced) {
|
||||
$vars[$full_match] = $merge_val;
|
||||
$vars[$merge_code] = $merge_val;
|
||||
}
|
||||
}
|
||||
|
||||
$vars = \Eventy::filter('mail_vars.replace_after_fallback', $vars, $data);
|
||||
|
||||
if ($escape) {
|
||||
foreach ($vars as $i => $var) {
|
||||
$vars[$i] = htmlspecialchars($var ?? '');
|
||||
@@ -281,14 +381,29 @@ class Mail
|
||||
return Option::get('mail_driver', 'mail');
|
||||
}
|
||||
|
||||
public static function registerSmtpLogger()
|
||||
{
|
||||
$logger = new \Swift_Plugins_Loggers_ArrayLogger();
|
||||
\Mail::getSwiftMailer()->registerPlugin(new \Swift_Plugins_LoggerPlugin($logger));
|
||||
|
||||
return $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send test email from mailbox.
|
||||
*/
|
||||
public static function sendTestMail($to, $mailbox = null)
|
||||
{
|
||||
$result = [
|
||||
'status' => 'success',
|
||||
'msg' => '',
|
||||
'log' => '',
|
||||
];
|
||||
|
||||
if ($mailbox) {
|
||||
// Configure mail driver according to Mailbox settings
|
||||
\MailHelper::setMailDriver($mailbox);
|
||||
$smtp_logger = self::registerSmtpLogger();
|
||||
|
||||
$status_message = '';
|
||||
|
||||
@@ -301,6 +416,7 @@ class Mail
|
||||
} else {
|
||||
// System email
|
||||
\MailHelper::setSystemMailDriver();
|
||||
$smtp_logger = self::registerSmtpLogger();
|
||||
|
||||
$status_message = '';
|
||||
|
||||
@@ -316,15 +432,17 @@ class Mail
|
||||
if (\Mail::failures() || $status_message) {
|
||||
SendLog::log(null, null, $to, SendLog::MAIL_TYPE_TEST, SendLog::STATUS_SEND_ERROR, null, null, $status_message);
|
||||
if ($status_message) {
|
||||
throw new \Exception($status_message, 1);
|
||||
} else {
|
||||
return false;
|
||||
$result['msg'] = $status_message;
|
||||
}
|
||||
$result['status'] = 'error';
|
||||
$result['log'] = $smtp_logger->dump();
|
||||
} else {
|
||||
SendLog::log(null, null, $to, SendLog::MAIL_TYPE_TEST, SendLog::STATUS_ACCEPTED);
|
||||
|
||||
return true;
|
||||
$result['status'] = 'success';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -332,41 +450,64 @@ class Mail
|
||||
*/
|
||||
public static function fetchTest($mailbox)
|
||||
{
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
$result = [
|
||||
'result' => 'success',
|
||||
'msg' => '',
|
||||
'log' => '',
|
||||
];
|
||||
|
||||
// Connect to the Server
|
||||
$client->connect();
|
||||
$client = null;
|
||||
|
||||
// Get folder
|
||||
$folder = $client->getFolder('INBOX');
|
||||
try {
|
||||
\Config::set('imap.options.debug', true);
|
||||
\Webklex\PHPIMAP\Connection\Protocols\ImapProtocol::$output_debug_log = false;
|
||||
\Webklex\PHPIMAP\Connection\Protocols\PopProtocol::$output_debug_log = false;
|
||||
|
||||
if (!$folder) {
|
||||
throw new \Exception('Could not get mailbox folder: INBOX', 1);
|
||||
}
|
||||
// Get unseen messages for a period
|
||||
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->get();
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
|
||||
$last_error = '';
|
||||
if (method_exists($client, 'getLastError')) {
|
||||
$last_error = $client->getLastError();
|
||||
}
|
||||
|
||||
if ($last_error && stristr($last_error, 'The specified charset is not supported')) {
|
||||
// Solution for MS mailboxes.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/176
|
||||
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->setCharset(null)->get();
|
||||
if (count($client->getErrors()) > 1) {
|
||||
$last_error = $client->getLastError();
|
||||
} else {
|
||||
$last_error = null;
|
||||
// Connect to the Server
|
||||
$client->connect();
|
||||
|
||||
// Get folder
|
||||
$folder = $client->getFolder('INBOX');
|
||||
|
||||
if (!$folder) {
|
||||
throw new \Exception('Could not get mailbox folder: INBOX', 1);
|
||||
}
|
||||
// Get unseen messages for a period
|
||||
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->get();
|
||||
|
||||
$last_error = '';
|
||||
if (method_exists($client, 'getLastError')) {
|
||||
$last_error = $client->getLastError();
|
||||
}
|
||||
|
||||
if ($last_error && stristr($last_error, 'The specified charset is not supported')) {
|
||||
// Solution for MS mailboxes.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/176
|
||||
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->setCharset(null)->get();
|
||||
if (count($client->getErrors()) > 1) {
|
||||
$last_error = $client->getLastError();
|
||||
} else {
|
||||
$last_error = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($last_error) {
|
||||
//throw new \Exception($last_error, 1);
|
||||
$result['result'] = 'error';
|
||||
$result['msg'] = $last_error;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$result['result'] = 'error';
|
||||
$result['msg'] = $e->getMessage();
|
||||
}
|
||||
|
||||
if ($last_error) {
|
||||
throw new \Exception($last_error, 1);
|
||||
} else {
|
||||
return true;
|
||||
if ($result['result'] == 'error') {
|
||||
$result['log'] = \Webklex\PHPIMAP\Connection\Protocols\ImapProtocol::getDebugLog();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -501,9 +642,11 @@ class Mail
|
||||
$autoresponder_headers = [
|
||||
'x-autoreply' => '',
|
||||
'x-autorespond' => '',
|
||||
'x-autoresponder' => '',
|
||||
'auto-submitted' => '', // this can be auto-replied, auto-generated, etc.
|
||||
'precedence' => ['auto_reply', 'bulk', 'junk'],
|
||||
'x-precedence' => ['auto_reply', 'bulk', 'junk'],
|
||||
'delivered-to' => ['autoresponder'],
|
||||
'precedence' => ['auto_reply', 'bulk', 'junk', 'list'],
|
||||
'x-precedence' => ['auto_reply', 'bulk', 'junk', 'list'],
|
||||
];
|
||||
$headers = explode("\n", $headers_str ?? '');
|
||||
|
||||
@@ -564,15 +707,35 @@ class Mail
|
||||
*/
|
||||
public static function parseHeaders($headers_str)
|
||||
{
|
||||
try {
|
||||
return imap_rfc822_parse_headers($headers_str);
|
||||
} catch (\Exception $e) {
|
||||
return;
|
||||
//try {
|
||||
//return imap_rfc822_parse_headers($headers_str);
|
||||
//return (new \Webklex\PHPIMAP\Header(''))->rfc822_parse_headers($headers_str);
|
||||
return \Webklex\PHPIMAP\Header::rfc822_parse_headers($headers_str);
|
||||
// } catch (\Exception $e) {
|
||||
// return;
|
||||
// }
|
||||
}
|
||||
|
||||
// Replacement for https://www.php.net/manual/en/function.imap-utf8.php
|
||||
public static function imapUtf8($mime_encoded_text)
|
||||
{
|
||||
if (function_exists('imap_utf8')) {
|
||||
return imap_utf8($mime_encoded_text);
|
||||
} else {
|
||||
return iconv_mime_decode($mime_encoded_text, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8");
|
||||
}
|
||||
}
|
||||
|
||||
public static function getHeader($headers_str, $header)
|
||||
{
|
||||
// Quick check to same resources.
|
||||
if (!stristr($headers_str, $header)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$header = strtolower($header);
|
||||
$header = str_replace('-', '_', $header);
|
||||
|
||||
$headers = self::parseHeaders($headers_str);
|
||||
if (!$headers) {
|
||||
return;
|
||||
@@ -581,7 +744,7 @@ class Mail
|
||||
if (property_exists($headers, $header)) {
|
||||
$value = $headers->$header;
|
||||
} else {
|
||||
return;
|
||||
return '';
|
||||
}
|
||||
switch ($header) {
|
||||
case 'message_id':
|
||||
@@ -598,9 +761,10 @@ class Mail
|
||||
public static function getMailboxClient($mailbox)
|
||||
{
|
||||
$oauth = $mailbox->oauthEnabled();
|
||||
$new_library = config('app.new_fetching_library');
|
||||
/*$new_library = config('app.new_fetching_library');
|
||||
|
||||
if (!$oauth && !$new_library) {
|
||||
if (!$new_library) {
|
||||
// Old.
|
||||
return new \Webklex\IMAP\Client([
|
||||
'host' => $mailbox->in_server,
|
||||
'port' => $mailbox->in_port,
|
||||
@@ -610,71 +774,71 @@ class Mail
|
||||
'password' => $mailbox->in_password,
|
||||
'protocol' => $mailbox->getInProtocolName(),
|
||||
]);
|
||||
} else {*/
|
||||
// New
|
||||
if ($oauth) {
|
||||
\Config::set('imap.accounts.default', [
|
||||
'host' => $mailbox->in_server,
|
||||
'port' => $mailbox->in_port,
|
||||
'encryption' => $mailbox->getInEncryptionName(),
|
||||
'validate_cert' => $mailbox->in_validate_cert,
|
||||
'username' => $mailbox->email,
|
||||
'password' => $mailbox->oauthGetParam('a_token'),
|
||||
'protocol' => $mailbox->getInProtocolName(),
|
||||
'authentication' => 'oauth',
|
||||
]);
|
||||
} else {
|
||||
\Config::set('imap.accounts.default', [
|
||||
'host' => $mailbox->in_server,
|
||||
'port' => $mailbox->in_port,
|
||||
'encryption' => $mailbox->getInEncryptionName(),
|
||||
'validate_cert' => $mailbox->in_validate_cert,
|
||||
// 'username' => $mailbox->email,
|
||||
// 'password' => $mailbox->oauthGetParam('a_token'),
|
||||
// 'protocol' => $mailbox->getInProtocolName(),
|
||||
// 'authentication' => 'oauth',
|
||||
'username' => $mailbox->in_username,
|
||||
'password' => $mailbox->in_password,
|
||||
'protocol' => $mailbox->getInProtocolName(),
|
||||
]);
|
||||
}
|
||||
// To enable debug: /vendor/webklex/php-imap/src/Connection/Protocols
|
||||
// Debug in console
|
||||
if (app()->runningInConsole()) {
|
||||
\Config::set('imap.options.debug', config('app.debug'));
|
||||
}
|
||||
|
||||
if ($oauth) {
|
||||
\Config::set('imap.accounts.default', [
|
||||
'host' => $mailbox->in_server,
|
||||
'port' => $mailbox->in_port,
|
||||
'encryption' => $mailbox->getInEncryptionName(),
|
||||
'validate_cert' => $mailbox->in_validate_cert,
|
||||
'username' => $mailbox->email,
|
||||
'password' => $mailbox->oauthGetParam('a_token'),
|
||||
'protocol' => $mailbox->getInProtocolName(),
|
||||
'authentication' => 'oauth',
|
||||
$cm = new \Webklex\PHPIMAP\ClientManager(config('imap'));
|
||||
|
||||
// Refresh Access Token.
|
||||
if ($oauth) {
|
||||
if ((strtotime($mailbox->oauthGetParam('issued_on')) + (int)$mailbox->oauthGetParam('expires_in')) < time()) {
|
||||
// Try to get an access token (using the authorization code grant)
|
||||
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
'client_id' => $mailbox->in_username,
|
||||
'client_secret' => $mailbox->in_password,
|
||||
'refresh_token' => $mailbox->oauthGetParam('r_token'),
|
||||
]);
|
||||
} else {
|
||||
\Config::set('imap.accounts.default', [
|
||||
'host' => $mailbox->in_server,
|
||||
'port' => $mailbox->in_port,
|
||||
'encryption' => $mailbox->getInEncryptionName(),
|
||||
'validate_cert' => $mailbox->in_validate_cert,
|
||||
// 'username' => $mailbox->email,
|
||||
// 'password' => $mailbox->oauthGetParam('a_token'),
|
||||
// 'protocol' => $mailbox->getInProtocolName(),
|
||||
// 'authentication' => 'oauth',
|
||||
'username' => $mailbox->in_username,
|
||||
'password' => $mailbox->in_password,
|
||||
'protocol' => $mailbox->getInProtocolName(),
|
||||
]);
|
||||
}
|
||||
// To enable debug: /vendor/webklex/php-imap/src/Connection/Protocols
|
||||
// Debug in console
|
||||
if (app()->runningInConsole()) {
|
||||
\Config::set('imap.options.debug', config('app.debug'));
|
||||
}
|
||||
|
||||
$cm = new \Webklex\PHPIMAP\ClientManager(config('imap'));
|
||||
|
||||
// Refresh Access Token.
|
||||
if ($oauth) {
|
||||
if ((strtotime($mailbox->oauthGetParam('issued_on')) + (int)$mailbox->oauthGetParam('expires_in')) < time()) {
|
||||
// Try to get an access token (using the authorization code grant)
|
||||
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
'client_id' => $mailbox->in_username,
|
||||
'client_secret' => $mailbox->in_password,
|
||||
'refresh_token' => $mailbox->oauthGetParam('r_token'),
|
||||
if (!empty($token_data['a_token'])) {
|
||||
$mailbox->setMetaParam('oauth', $token_data, true);
|
||||
} elseif (!empty($token_data['error'])) {
|
||||
$error_message = 'Error occurred refreshing oAuth Access Token: '.$token_data['error'];
|
||||
\Helper::log(\App\ActivityLog::NAME_EMAILS_FETCHING,
|
||||
\App\ActivityLog::DESCRIPTION_EMAILS_FETCHING_ERROR, [
|
||||
'error' => $error_message,
|
||||
'mailbox' => $mailbox->name,
|
||||
]);
|
||||
|
||||
if (!empty($token_data['a_token'])) {
|
||||
$mailbox->setMetaParam('oauth', $token_data, true);
|
||||
} elseif (!empty($token_data['error'])) {
|
||||
$error_message = 'Error occurred refreshing oAuth Access Token: '.$token_data['error'];
|
||||
\Helper::log(\App\ActivityLog::NAME_EMAILS_FETCHING,
|
||||
\App\ActivityLog::DESCRIPTION_EMAILS_FETCHING_ERROR, [
|
||||
'error' => $error_message,
|
||||
'mailbox' => $mailbox->name,
|
||||
]);
|
||||
throw new \Exception($error_message, 1);
|
||||
}
|
||||
throw new \Exception($error_message, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// This makes it authenticate two times.
|
||||
//$cm->setTimeout(60);
|
||||
|
||||
return $cm->account('default');
|
||||
}
|
||||
|
||||
// This makes it authenticate two times.
|
||||
//$cm->setTimeout(60);
|
||||
|
||||
return $cm->account('default');
|
||||
//}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -705,7 +869,7 @@ class Mail
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
$client->connect();
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e, '('.$mailbox->name.') Could not fetch specific message by Message-ID via IMAP:');
|
||||
\Helper::logException($e, '('.$mailbox->name.') Could not fetch specific message by Message-ID:');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -714,9 +878,16 @@ class Mail
|
||||
foreach ($imap_folders as $folder_name) {
|
||||
try {
|
||||
$folder = self::getImapFolder($client, $folder_name);
|
||||
|
||||
if (!$folder) {
|
||||
\Log::error('('.$mailbox->name.') Show Original - folder not found: '.$folder_name);
|
||||
continue;
|
||||
}
|
||||
// Message-ID: <123@123.com>
|
||||
$search_message_id = addcslashes($message_id, '\"');
|
||||
$query = $folder->query()
|
||||
->text('<'.$message_id.'>')
|
||||
//->text('<'.$message_id.'>')
|
||||
->whereMessageId('"<'.$search_message_id.'>"')
|
||||
->leaveUnread()
|
||||
->limit(1);
|
||||
|
||||
@@ -741,10 +912,11 @@ class Mail
|
||||
if ($last_error && stristr($last_error, 'The specified charset is not supported')) {
|
||||
// Solution for MS mailboxes.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/176
|
||||
$query = $folder->query()->text('<'.$message_id.'>')->leaveUnread()->limit(1)->setCharset(null);
|
||||
//$query = $folder->query()->text('<'.$message_id.'>')->leaveUnread()->limit(1)->setCharset(null);
|
||||
$query = $folder->query()->whereMessageId('"<'.$search_message_id.'>"')->leaveUnread()->limit(1)->setCharset(null);
|
||||
if ($message_date) {
|
||||
$query->since($message_date->subDays(7));
|
||||
$query->before($message_date->addDays(7));
|
||||
$query->before($message_date->addDays(14));
|
||||
}
|
||||
$messages = $query->get();
|
||||
$no_charset = true;
|
||||
@@ -896,6 +1068,9 @@ class Mail
|
||||
|
||||
public static function getImapFolder($client, $folder_name)
|
||||
{
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3502
|
||||
$folder_name = mb_convert_encoding($folder_name, "UTF7-IMAP","UTF-8");
|
||||
|
||||
if (method_exists($client, 'getFolderByPath')) {
|
||||
return $client->getFolderByPath($folder_name);
|
||||
} else {
|
||||
@@ -908,12 +1083,19 @@ class Mail
|
||||
*/
|
||||
public static function decodeSubject($subject)
|
||||
{
|
||||
// Sometimes trying to decode non-encoded strings leads
|
||||
// to loosing accents.
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4506
|
||||
if (!strstr($subject, '=?')) {
|
||||
return $subject;
|
||||
}
|
||||
// Remove new lines as iconv_mime_decode() may loose a part separated by new line:
|
||||
// =?utf-8?Q?Gesch=C3=A4ftskonto?= erstellen =?utf-8?Q?f=C3=BCr?=
|
||||
// 249143
|
||||
$subject = preg_replace("/[\r\n]/", '', $subject);
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3185
|
||||
$subject = str_ireplace('=?iso-2022-jp?', '=?iso-2022-jp-ms?', $subject);
|
||||
//$subject = str_ireplace('=?iso-2022-jp?', '=?iso-2022-jp-ms?', $subject);
|
||||
$subject = self::substituteMimeEncoding($subject);
|
||||
|
||||
// Sometimes imap_utf8() can't decode the subject, for example:
|
||||
// =?iso-2022-jp?B?GyRCIXlCaBsoQjEzMhskQjlmISEhViUsITwlRyVzGyhCJhskQiUoJS8lOSVGJWolIiFXQGxMZ0U5JE4kPyRhJE4jURsoQiYbJEIjQSU1JW0lcyEhIVo3bjQpJSglLyU5JUYlaiUiISYlbyE8JS8hWxsoQg==?=
|
||||
@@ -966,7 +1148,7 @@ class Mail
|
||||
|
||||
// Try imap_utf8().
|
||||
// =?iso-2022-jp?B?IBskQiFaSEcyPDpuQ?= =?iso-2022-jp?B?C4wTU1qIVs3Mkp2JSIlLyU3JSItahsoQg==?=
|
||||
$subject_decoded = \imap_utf8($joined_parts);
|
||||
$subject_decoded = self::imapUtf8($joined_parts);
|
||||
|
||||
if ($subject_decoded
|
||||
&& trim($subject_decoded) != trim($joined_parts)
|
||||
@@ -989,7 +1171,7 @@ class Mail
|
||||
// =?iso-2022-jp?B?IBskQiFaSEcyPDpuQC4wTU1qIVs3Mkp2JSIlLyU3JSItahsoQg==?=
|
||||
// =?iso-2022-jp?B?GyRCQGlNVTtZRTkhIT4uTlMbKEI=?=
|
||||
if (self::isNotYetFullyDecoded($subject_decoded)) {
|
||||
$subject_decoded = \imap_utf8($subject);
|
||||
$subject_decoded = self::imapUtf8($subject);
|
||||
}
|
||||
|
||||
// All previous functions could not decode text.
|
||||
@@ -1016,6 +1198,61 @@ class Mail
|
||||
|| \Str::contains($subject_decoded, $invalid_utf_symbols);
|
||||
}
|
||||
|
||||
public static function getHashedReplySeparator($message_id) {
|
||||
$separator = \MailHelper::REPLY_SEPARATOR_HTML;
|
||||
|
||||
if ($message_id) {
|
||||
$separator .= substr(md5($message_id.config('app.key')), 0, 8);
|
||||
}
|
||||
|
||||
return $separator;
|
||||
}
|
||||
|
||||
// Sanitize status message - remove SMTP username and password.
|
||||
public static function sanitizeSmtpStatusMessage($status_message) {
|
||||
$status_message = preg_replace('#(username ")[^"]+(")#', '$1***$2', $status_message ?? '');
|
||||
$status_message = preg_replace("#(Swift_Transport_Esmtp_Auth_LoginAuthenticator\->authenticate\(Object\(Swift_SmtpTransport\), ')[^\']+(', ')[^\']+('\))#", '$1***$2***$3', $status_message ?? '');
|
||||
|
||||
return $status_message;
|
||||
}
|
||||
|
||||
public static function parseEml($content, $mailbox) {
|
||||
if (!str_contains($content, "\r\n")){
|
||||
$content = str_replace("\n", "\r\n", $content);
|
||||
}
|
||||
|
||||
$raw_header = substr($content, 0, strpos($content, "\r\n\r\n"));
|
||||
$raw_body = substr($content, strlen($raw_header)+8);
|
||||
|
||||
//\Config::set('app.new_fetching_library', 'true');
|
||||
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
$client->openFolder("INBOX");
|
||||
|
||||
return \Webklex\PHPIMAP\Message::make(null, null, $client, $raw_header, $raw_body, [], \Webklex\PHPIMAP\IMAP::ST_UID);
|
||||
}
|
||||
|
||||
// Substitue encoding during mail body decoding.
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4282
|
||||
public static function substituteEncoding($encoding)
|
||||
{
|
||||
$encoding = strtolower($encoding);
|
||||
|
||||
if (!empty(self::$encoding_substitution[$encoding])) {
|
||||
return self::$encoding_substitution[$encoding];
|
||||
} else {
|
||||
return $encoding;
|
||||
}
|
||||
}
|
||||
|
||||
public static function substituteMimeEncoding($string)
|
||||
{
|
||||
foreach (self::$mime_encoding_substitution as $from => $into) {
|
||||
$string = str_ireplace('=?'.$from.'?', '=?'.$into.'?', $string);
|
||||
}
|
||||
return $string;
|
||||
}
|
||||
|
||||
// public static function oauthGetProvider($provider_code, $params)
|
||||
// {
|
||||
// $provider = null;
|
||||
|
||||
168
app/Module.php
168
app/Module.php
@@ -401,76 +401,45 @@ class Module extends Model
|
||||
|
||||
// Download new version.
|
||||
if (!$result['msg']) {
|
||||
$params = [
|
||||
'license' => self::getLicense($alias),
|
||||
'module_alias' => $alias,
|
||||
'url' => self::getAppUrl(),
|
||||
];
|
||||
$license_details = WpApi::getVersion($params);
|
||||
// Check if the module's author is officially recognized
|
||||
if (self::isOfficial($module->authorUrl)) {
|
||||
$params = [
|
||||
'license' => self::getLicense($alias),
|
||||
'module_alias' => $alias,
|
||||
'url' => self::getAppUrl(),
|
||||
];
|
||||
$license_details = WpApi::getVersion($params);
|
||||
|
||||
if (WpApi::$lastError) {
|
||||
$result['msg'] = WpApi::$lastError['message'];
|
||||
} elseif (!empty($license_details['code']) && !empty($license_details['message'])) {
|
||||
$result['msg'] = $license_details['message'];
|
||||
} elseif (!empty($license_details['required_app_version']) && !\Helper::checkAppVersion($license_details['required_app_version'])) {
|
||||
$result['msg'] = 'Module requires app version:'.' '.$license_details['required_app_version'];
|
||||
} elseif (!empty($license_details['download_link'])) {
|
||||
// Download module.
|
||||
$module_archive = \Module::getPath().DIRECTORY_SEPARATOR.$alias.'.zip';
|
||||
|
||||
try {
|
||||
\Helper::downloadRemoteFile($license_details['download_link'], $module_archive);
|
||||
} catch (\Exception $e) {
|
||||
$result['msg'] = $e->getMessage();
|
||||
}
|
||||
|
||||
if (!file_exists($module_archive)) {
|
||||
$result['download_error'] = true;
|
||||
if (WpApi::$lastError) {
|
||||
$result['msg'] = WpApi::$lastError['message'];
|
||||
} elseif (!empty($license_details['code']) && !empty($license_details['message'])) {
|
||||
$result['msg'] = $license_details['message'];
|
||||
} elseif (!empty($license_details['required_app_version']) && !\Helper::checkAppVersion($license_details['required_app_version'])) {
|
||||
$result['msg'] = 'Module requires app version:'.' '.$license_details['required_app_version'];
|
||||
} elseif (!empty($license_details['download_link'])) {
|
||||
// If a download link is available, proceed to update the module from the URL
|
||||
$result = self::updateFromUrl($module, $license_details['download_link'], $result);
|
||||
} elseif ($license_details['status'] && $result['msg'] = self::getErrorMessage($license_details['status'])) {
|
||||
//$result['msg'] = ;
|
||||
} else {
|
||||
// Extract.
|
||||
try {
|
||||
// Sometimes by some reason Public folder becomes a symlink leading to itself.
|
||||
// It causes an error during updating process.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/2709
|
||||
$public_folder = $module->getPath().DIRECTORY_SEPARATOR.'Public';
|
||||
try {
|
||||
if (is_link($public_folder)) {
|
||||
unlink($public_folder);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
\Helper::unzip($module_archive, \Module::getPath());
|
||||
} catch (\Exception $e) {
|
||||
$result['msg'] = $e->getMessage();
|
||||
}
|
||||
// Check if extracted module exists.
|
||||
\Module::clearCache();
|
||||
$module = \Module::findByAlias($alias);
|
||||
if (!$module) {
|
||||
$result['download_error'] = true;
|
||||
}
|
||||
$result['msg'] = __('Error occurred').': '.json_encode($license_details);
|
||||
}
|
||||
|
||||
// Remove archive.
|
||||
if (file_exists($module_archive)) {
|
||||
\File::delete($module_archive);
|
||||
}
|
||||
|
||||
if ($result['download_error']) {
|
||||
$result['download_msg'] = __('Error occurred downloading the module. Please :%a_being%download:%a_end% module manually and extract into :folder', ['%a_being%' => '<a href="'.$license_details['download_link'].'" target="_blank">', '%a_end%' => '</a>', 'folder' => '<strong>'.\Module::getPath().'</strong>']);
|
||||
}
|
||||
} elseif ($license_details['status'] && $result['msg'] = self::getErrorMessage($license_details['status'])) {
|
||||
//$result['msg'] = ;
|
||||
} else {
|
||||
$result['msg'] = __('Error occurred').': '.json_encode($license_details);
|
||||
// If the module's author is not officially recognized, check for a direct download link
|
||||
$latest_version_zip_url = $module->latestVersionZipUrl ?? null;
|
||||
|
||||
if (!empty($latest_version_zip_url)) {
|
||||
// Update the module from the provided ZIP URL
|
||||
$result = self::updateFromUrl($module, $module->latestVersionZipUrl, $result);
|
||||
} else {
|
||||
// If no download link is available, set an error message indicating the module cannot be downloaded
|
||||
$result['msg'] = __('Error occurred') . ': module not available for download';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run post-update instructions.
|
||||
if (!$result['msg'] && !$result['download_error']) {
|
||||
|
||||
$output_log = new BufferedOutput();
|
||||
\Artisan::call('freescout:module-install', ['module_alias' => $alias], $output_log);
|
||||
$result['output'] = $output_log->fetch() ?: ' ';
|
||||
@@ -500,6 +469,83 @@ class Module extends Model
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a module from a given URL.
|
||||
*
|
||||
* @param Module $module The module to be updated.
|
||||
* @param string $url The URL where the new version of the module can be downloaded.
|
||||
* @param array $result An associative array to store the result of the update operation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function updateFromUrl($module, $url, $result)
|
||||
{
|
||||
$alias = $module->alias;
|
||||
// Download module.
|
||||
$module_archive = \Module::getPath() . DIRECTORY_SEPARATOR . $alias . '.zip';
|
||||
|
||||
try {
|
||||
\Helper::downloadRemoteFile($url, $module_archive);
|
||||
} catch (\Exception $e) {
|
||||
$result['msg'] = $e->getMessage();
|
||||
}
|
||||
|
||||
if (!file_exists($module_archive)) {
|
||||
$result['download_error'] = true;
|
||||
} else {
|
||||
// Extract.
|
||||
try {
|
||||
$module_path = $module->getPath();
|
||||
|
||||
// Sometimes by some reason Public folder becomes a symlink leading to itself.
|
||||
// It causes an error during updating process.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/2709
|
||||
$public_folder = $module_path.DIRECTORY_SEPARATOR.'Public';
|
||||
try {
|
||||
if (is_link($public_folder)) {
|
||||
unlink($public_folder);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
\Helper::unzip($module_archive, \Module::getPath());
|
||||
|
||||
// Rename the folder if the archive has been downloaded from GitHub:
|
||||
// SomeModule-master >> SomeModule.
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4611
|
||||
$basename = basename($url, '.zip');
|
||||
|
||||
if (file_exists($module_path.'-'.$basename)) {
|
||||
\File::moveDirectory($module_path.'-'.$basename, $module_path, true);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$result['msg'] = $e->getMessage();
|
||||
}
|
||||
// Check if extracted module exists.
|
||||
\Module::clearCache();
|
||||
$module = \Module::findByAlias($alias);
|
||||
if (!$module) {
|
||||
$result['download_error'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove archive.
|
||||
if (file_exists($module_archive)) {
|
||||
\File::delete($module_archive);
|
||||
}
|
||||
|
||||
if ($result['download_error']) {
|
||||
$result['download_msg'] = __('Error occurred downloading the module. Please :%a_being%download:%a_end% module manually and extract into :folder', [
|
||||
'%a_being%' => '<a href="' . $url . '" target="_blank">',
|
||||
'%a_end%' => '</a>',
|
||||
'folder' => '<strong>' . \Module::getPath() . '</strong>',
|
||||
]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function getErrorMessage($code, $result = null)
|
||||
{
|
||||
$msg = '';
|
||||
|
||||
@@ -205,8 +205,16 @@ class Option extends Model
|
||||
*/
|
||||
public static function maybeSerialize($data)
|
||||
{
|
||||
if (is_array($data) || is_object($data)) {
|
||||
return serialize($data);
|
||||
// if (is_array($data) || is_object($data)) {
|
||||
// return serialize($data);
|
||||
// }
|
||||
// We don't use serialize() function as it is not safe.
|
||||
if (is_array($data)) {
|
||||
try {
|
||||
return \Helper::jsonEncodeSafe($data);
|
||||
} catch (\Exception $e) {
|
||||
// Don nothing.
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
@@ -217,6 +225,8 @@ class Option extends Model
|
||||
*/
|
||||
public static function maybeUnserialize($original)
|
||||
{
|
||||
// This is kept jsut for backward compatibility.
|
||||
// Options are not seralized using serialize() anymore.
|
||||
if (self::isSerialized($original)) {
|
||||
try {
|
||||
$original = unserialize($original);
|
||||
@@ -227,6 +237,19 @@ class Option extends Model
|
||||
return $original;
|
||||
}
|
||||
|
||||
if (is_string($original) && !empty($original[0])
|
||||
&& ($original[0] == '{' || $original[0] == '[')
|
||||
) {
|
||||
try {
|
||||
$result = json_decode($original, true);
|
||||
if (is_array($result)) {
|
||||
return $result;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
return $original;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,15 +24,9 @@ class ConversationPolicy
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
} else {
|
||||
if ($conversation->mailbox->users->contains($user)) {
|
||||
if ($conversation->userHasAccessToMailbox($user->id)) {
|
||||
// Maybe user can see only assigned conversations.
|
||||
if ($conversation->user_id != $user->id
|
||||
&& $user->hasManageMailboxPermission($conversation->mailbox_id, Mailbox::ACCESS_PERM_ASSIGNED)
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return $this->checkIsOnlyAssigned($conversation, $user);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -53,13 +47,7 @@ class ConversationPolicy
|
||||
} else {
|
||||
if ($conversation->mailbox->users_cached->contains($user)) {
|
||||
// Maybe user can see only assigned conversations.
|
||||
if ($conversation->user_id != $user->id
|
||||
&& $user->hasManageMailboxPermission($conversation->mailbox_id, Mailbox::ACCESS_PERM_ASSIGNED)
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return $this->checkIsOnlyAssigned($conversation, $user);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -79,8 +67,9 @@ class ConversationPolicy
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
} else {
|
||||
if ($conversation->mailbox->users->contains($user)) {
|
||||
return true;
|
||||
if ($conversation->userHasAccessToMailbox($user->id)) {
|
||||
// Maybe user can see only assigned conversations.
|
||||
return $this->checkIsOnlyAssigned($conversation, $user);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -95,7 +84,18 @@ class ConversationPolicy
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
} else {
|
||||
return $user->hasPermission(User::PERM_DELETE_CONVERSATIONS);
|
||||
if (!$user->hasPermission(User::PERM_DELETE_CONVERSATIONS)) {
|
||||
return false;
|
||||
}
|
||||
if (!$conversation->id) {
|
||||
return true;
|
||||
}
|
||||
if ($conversation->userHasAccessToMailbox($user->id)) {
|
||||
// Maybe user can see only assigned conversations.
|
||||
return $this->checkIsOnlyAssigned($conversation, $user);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,4 +115,17 @@ class ConversationPolicy
|
||||
}
|
||||
return Mailbox::count() > 1;
|
||||
}
|
||||
|
||||
public function checkIsOnlyAssigned($conversation, $user)
|
||||
{
|
||||
// Maybe user can see only assigned conversations.
|
||||
if (!\Eventy::filter('conversation.is_user_assignee', $conversation->user_id == $user->id, $conversation, $user->id)
|
||||
&& $conversation->created_by_user_id != $user->id
|
||||
&& $user->canSeeOnlyAssignedConversations()
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@ class UserPolicy
|
||||
if ($user->isAdmin()
|
||||
|| $user->id == $model->id
|
||||
|| $user->hasPermission(User::PERM_EDIT_USERS)
|
||||
|| $user->canManageMailbox($model->id)
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
|
||||
@@ -41,7 +41,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
// Forse HTTPS if using CloudFlare "Flexible SSL"
|
||||
// https://support.cloudflare.com/hc/en-us/articles/200170416-What-do-the-SSL-options-mean-
|
||||
if (\Helper::getProtocol() == 'https') {
|
||||
if (\Helper::isHttps()) {
|
||||
// $_SERVER['HTTPS'] = 'on';
|
||||
// $_SERVER['SERVER_PORT'] = '443';
|
||||
$this->app['url']->forceScheme('https');
|
||||
|
||||
@@ -14,6 +14,7 @@ class SendLog extends Model
|
||||
* https://documentation.mailgun.com/en/latest/api-events.html#event-types.
|
||||
*/
|
||||
const STATUS_ACCEPTED = 1; // accepted (for delivery)
|
||||
const STATUS_SEND_INTERMEDIATE_ERROR = 10;
|
||||
const STATUS_SEND_ERROR = 2;
|
||||
const STATUS_DELIVERY_SUCCESS = 4;
|
||||
const STATUS_DELIVERY_ERROR = 5; // rejected, failed
|
||||
@@ -42,6 +43,7 @@ class SendLog extends Model
|
||||
* @var [type]
|
||||
*/
|
||||
public static $status_errors = [
|
||||
self::STATUS_SEND_INTERMEDIATE_ERROR,
|
||||
self::STATUS_SEND_ERROR,
|
||||
self::STATUS_DELIVERY_ERROR,
|
||||
];
|
||||
@@ -94,6 +96,9 @@ class SendLog extends Model
|
||||
*/
|
||||
public static function log($thread_id, $message_id, $email, $mail_type, $status, $customer_id = null, $user_id = null, $status_message = null, $smtp_queue_id = null)
|
||||
{
|
||||
// Sanitize status message - remove SMTP username and password.
|
||||
$status_message = \MailHelper::sanitizeSmtpStatusMessage($status_message);
|
||||
|
||||
$send_log = new self();
|
||||
$send_log->thread_id = $thread_id;
|
||||
$send_log->message_id = $message_id;
|
||||
@@ -106,7 +111,12 @@ class SendLog extends Model
|
||||
if ($smtp_queue_id) {
|
||||
$send_log->smtp_queue_id = $smtp_queue_id;
|
||||
}
|
||||
$send_log->save();
|
||||
try {
|
||||
$send_log->save();
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e, 'Error occurred saving a record to `send_logs` table. ');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Mailbox;
|
||||
use App\Notifications\BroadcastNotification;
|
||||
use App\Notifications\WebsiteNotification;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -282,6 +283,13 @@ class Subscription extends Model
|
||||
}
|
||||
}
|
||||
//}
|
||||
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3843#issuecomment-1974811457
|
||||
if ($conversation->user_id != $subscription->user_id
|
||||
&& $subscription->user->canSeeOnlyAssignedConversations()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (\Eventy::filter('subscription.filter_out', false, $subscription, $thread)) {
|
||||
continue;
|
||||
@@ -400,11 +408,12 @@ class Subscription extends Model
|
||||
$thread_id = self::chooseThread($notify_info['threads'])->id;
|
||||
|
||||
foreach ($notify_info['users'] as $user) {
|
||||
$broadcast_id = $thread_id.'_'.$user->id;
|
||||
$mediums = [$medium];
|
||||
if (!empty($broadcasts[$thread_id]['mediums'])) {
|
||||
$mediums = array_unique(array_merge($mediums, $broadcasts[$thread_id]['mediums']));
|
||||
if (!empty($broadcasts[$broadcast_id]['mediums'])) {
|
||||
$mediums = array_unique(array_merge($mediums, $broadcasts[$broadcast_id]['mediums']));
|
||||
}
|
||||
$broadcasts[$thread_id] = [
|
||||
$broadcasts[$broadcast_id] = [
|
||||
'user' => $user,
|
||||
'conversation' => $notify_info['conversation'],
|
||||
'threads' => $notify_info['threads'],
|
||||
@@ -414,7 +423,7 @@ class Subscription extends Model
|
||||
}
|
||||
}
|
||||
// \Notification::sendNow($notify_info['users'], new BroadcastNotification($notify_info['conversation'], $notify_info['threads'][0]));
|
||||
foreach ($broadcasts as $thread_id => $to_broadcast) {
|
||||
foreach ($broadcasts as $broadcast_id => $to_broadcast) {
|
||||
$broadcast_notification = new BroadcastNotification($to_broadcast['conversation'], self::chooseThread($to_broadcast['threads']), $to_broadcast['mediums']);
|
||||
$broadcast_notification->delay($delay);
|
||||
$to_broadcast['user']->notify($broadcast_notification);
|
||||
|
||||
103
app/Thread.php
103
app/Thread.php
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Mailbox;
|
||||
use App\SendLog;
|
||||
use App\Events\ConversationStatusChanged;
|
||||
use App\Events\ConversationUserChanged;
|
||||
use App\Events\UserAddedNote;
|
||||
@@ -309,11 +311,19 @@ class Thread extends Model
|
||||
// Change "background:" to "background-color:".
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/2560
|
||||
// Keep in mind that with large texts preg_replace() may return null.
|
||||
$body = preg_replace("/(<[^<>]+style=[\"'][^\"']*)background: *([^;() ]+;)/", '$1background-color:$2', $body) ?: $body;
|
||||
$body = preg_replace("/(<[^<>]+style=[\"'][^\"']*)background: *([^;() ]+[;\"'])/", '$1background-color:$2', $body) ?: $body;
|
||||
|
||||
// Cut out "collapse" class as it hides elements.
|
||||
$body = preg_replace("/(<[^<>\r\n]+class=([\"'][^\"']* |[\"']))(collapse|hidden)([\"' ])/", '$1$4', $body) ?: $body;
|
||||
|
||||
// Remove only the <!--[if !mso]><!--> and <!--<![endif]--> around the elements.
|
||||
// https://github.com/freescout-helpdesk/freescout/pull/3865#issuecomment-1990758149
|
||||
$body = preg_replace('/<!\-\-\[if [^>]+\]><!\-\->(.*?)<![ ]+\-\-<!\[endif\]\-\->/s', '$1', $body);
|
||||
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3894
|
||||
// Remove <!--[if !mso]><!--> and <!--<![endif]--> comments, preserving the data inside
|
||||
//$body = preg_replace('/(<!\-\-\[if [^>]+\]>|<!\[endif\]\-\->)/', '', $body);
|
||||
|
||||
return \Helper::purifyHtml($body);
|
||||
}
|
||||
|
||||
@@ -563,7 +573,12 @@ class Thread extends Model
|
||||
return \App\User::getDeletedUser();
|
||||
}
|
||||
} else {
|
||||
return $this->created_by_customer;
|
||||
// In some cases the created_by_customer can be empty.
|
||||
if ($this->created_by_customer) {
|
||||
return $this->created_by_customer;
|
||||
} else {
|
||||
return \App\Customer::getDummyCustomer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -649,7 +664,7 @@ class Thread extends Model
|
||||
}
|
||||
} elseif ($this->action_type == self::ACTION_TYPE_CUSTOMER_CHANGED) {
|
||||
if ($conversation_number) {
|
||||
$did_this = __(':person changed the customer to :customer in conversation #:conversation_number', ['customer' => $this->customer->getFullName(true), 'conversation_number' => $conversation_number]);
|
||||
$did_this = __(':person changed the customer to :customer in conversation #:conversation_number', ['customer' => ($this->customer_cached ? $this->customer_cached->getFullName(true) : ''), 'conversation_number' => $conversation_number]);
|
||||
} else {
|
||||
$customer_name = '';
|
||||
if ($this->customer_cached) {
|
||||
@@ -806,7 +821,14 @@ class Thread extends Model
|
||||
$name = $mailbox->name;
|
||||
|
||||
if ($mailbox->from_name == Mailbox::FROM_NAME_CUSTOM && $mailbox->from_name_custom) {
|
||||
$name = $mailbox->from_name_custom;
|
||||
$data = [
|
||||
'mailbox' => $mailbox,
|
||||
'mailbox_from_name' => '', // To avoid recursion.
|
||||
'conversation' => $this->conversation,
|
||||
// If we reach here it means the thread has been created by user.
|
||||
'user' => $this->getCreatedBy(),
|
||||
];
|
||||
$name = \MailHelper::replaceMailVars($mailbox->from_name_custom, $data, false, true);
|
||||
} elseif ($mailbox->from_name == Mailbox::FROM_NAME_USER && $this->getCreatedBy()) {
|
||||
$name = $this->getCreatedBy()->getFirstName(true);
|
||||
}
|
||||
@@ -872,6 +894,9 @@ class Thread extends Model
|
||||
} else {
|
||||
$send_status_data = $new_data;
|
||||
}
|
||||
if (!empty($send_status_data['msg'])) {
|
||||
$send_status_data['msg'] = \MailHelper::sanitizeSmtpStatusMessage($send_status_data['msg']);
|
||||
}
|
||||
$this->send_status_data = \Helper::jsonEncodeUtf8($send_status_data);
|
||||
} else {
|
||||
$this->send_status_data = null;
|
||||
@@ -1331,8 +1356,10 @@ class Thread extends Model
|
||||
}
|
||||
if ($by_user && $this->created_by_user_id == $by_user->id) {
|
||||
$name = __('you');
|
||||
} else {
|
||||
} elseif ($this->created_by_user) {
|
||||
$name = $this->created_by_user->getFullName();
|
||||
} else {
|
||||
$name = '';
|
||||
}
|
||||
|
||||
return $name;
|
||||
@@ -1390,7 +1417,23 @@ class Thread extends Model
|
||||
*/
|
||||
public function fetchBody()
|
||||
{
|
||||
$message = \MailHelper::fetchMessage($this->conversation->mailbox, $this->message_id, $this->getMailDate());
|
||||
$mailbox = null;
|
||||
|
||||
// The conversation may has been moved from another mailbox.
|
||||
if (!empty($this->conversation->meta['orig_mailbox_id'])) {
|
||||
$mailbox = Mailbox::find($this->conversation->meta['orig_mailbox_id']);
|
||||
}
|
||||
|
||||
if (!$mailbox) {
|
||||
$mailbox = $this->conversation->mailbox;
|
||||
}
|
||||
$message = \MailHelper::fetchMessage($mailbox, $this->message_id, $this->getMailDate());
|
||||
|
||||
// Try without limiting by date.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3658
|
||||
if (!$message) {
|
||||
$message = \MailHelper::fetchMessage($mailbox, $this->message_id);
|
||||
}
|
||||
|
||||
if (!$message) {
|
||||
return '';
|
||||
@@ -1410,6 +1453,20 @@ class Thread extends Model
|
||||
return \MailHelper::parseHeaders($this->headers);
|
||||
}
|
||||
|
||||
public function getHeader($header_name)
|
||||
{
|
||||
return \MailHelper::getHeader($this->headers, $header_name);
|
||||
}
|
||||
|
||||
public function getFromHeader()
|
||||
{
|
||||
if (empty($this->headers)) {
|
||||
return '';
|
||||
}
|
||||
preg_match("#From:\s*.*[^\s]*\s*<\s*(.*[^\s])\s*>\s*\n#", $this->headers ?? '', $m);
|
||||
return $m[1] ?? '';
|
||||
}
|
||||
|
||||
public function getMailDate()
|
||||
{
|
||||
$data = $this->parseHeaders();
|
||||
@@ -1517,4 +1574,38 @@ class Thread extends Model
|
||||
$threads = self::sortThreads($threads);
|
||||
return $threads->first();
|
||||
}
|
||||
|
||||
public function canRetrySend()
|
||||
{
|
||||
if ($this->isSendStatusSuccess()) {
|
||||
return false;
|
||||
}
|
||||
// Check if failed_job still exists.
|
||||
if (!$this->getFailedJobId()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isSendStatusSuccess()
|
||||
{
|
||||
// We have not tried to send the email yet.
|
||||
if ((int)$this->send_status == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!in_array($this->send_status, [SendLog::STATUS_SEND_ERROR, SendLog::STATUS_DELIVERY_ERROR, SendLog::STATUS_SEND_INTERMEDIATE_ERROR])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getFailedJobId()
|
||||
{
|
||||
return \App\FailedJob::where('queue', 'emails')
|
||||
->where('payload', 'like', '{"displayName":"App\\\\\\\\Jobs\\\\\\\\SendReplyToCustomer"%{i:0;i:'.$this->id.';%')
|
||||
->value('id');
|
||||
}
|
||||
}
|
||||
|
||||
153
app/User.php
153
app/User.php
@@ -7,6 +7,7 @@
|
||||
namespace App;
|
||||
|
||||
use App\Email;
|
||||
use App\Events\UserDeleted;
|
||||
use App\Follower;
|
||||
use App\Mail\PasswordChanged;
|
||||
use App\Mail\UserInvite;
|
||||
@@ -116,12 +117,20 @@ class User extends Authenticatable
|
||||
*
|
||||
* @var [type]
|
||||
*/
|
||||
protected $fillable = ['role', 'status', 'first_name', 'last_name', 'email', 'password', 'timezone', 'photo_url', 'type', 'emails', 'job_title', 'phone', 'time_format', 'enable_kb_shortcuts', 'locale'];
|
||||
protected $fillable = ['role', 'status', 'first_name', 'last_name', 'email', 'password', 'timezone', 'photo_url', 'type', 'emails', 'job_title', 'phone', 'time_format', 'locale'];
|
||||
|
||||
protected $casts = [
|
||||
'permissions' => 'array',
|
||||
];
|
||||
|
||||
public function __construct(array $attributes = array())
|
||||
{
|
||||
$this->setRawAttributes(array_merge($this->attributes, array(
|
||||
'timezone' => config('app.timezone') ?: User::DEFAULT_TIMEZONE
|
||||
)), true);
|
||||
parent::__construct($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* For array_unique function.
|
||||
*
|
||||
@@ -298,6 +307,7 @@ class User extends Authenticatable
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get IDs of mailboxes to which user has access.
|
||||
*/
|
||||
@@ -542,6 +552,8 @@ class User extends Authenticatable
|
||||
's' => 'ss',
|
||||
'l' => 'cccc',
|
||||
'O' => 'xx',
|
||||
// https://stackoverflow.com/questions/59682843/php-intldateformatter-returns-incorrect-year
|
||||
'Y' => 'y',
|
||||
]);
|
||||
|
||||
// Remove dot from month name.
|
||||
@@ -617,7 +629,7 @@ class User extends Authenticatable
|
||||
}
|
||||
|
||||
if (stripos($dateForHuman, 'just') === false) {
|
||||
return __(':date @ :time', ['date' => $dateForHuman, 'time' => $date->format('H:i')]);
|
||||
return __(':date @ :time', ['date' => $dateForHuman, 'time' => self::dateFormat($date, 'H:i')]);
|
||||
} else {
|
||||
return $dateForHuman;
|
||||
}
|
||||
@@ -858,7 +870,9 @@ class User extends Authenticatable
|
||||
{
|
||||
$real_path = $uploaded_file;
|
||||
if (!is_string($uploaded_file)) {
|
||||
$real_path = $uploaded_file->getRealPath();
|
||||
// Fallback to getPathname() for Windows.
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4105
|
||||
$real_path = $uploaded_file->getRealPath() ?: $uploaded_file->getPathname();
|
||||
$mime_type = $uploaded_file->getMimeType();
|
||||
}
|
||||
|
||||
@@ -986,6 +1000,22 @@ class User extends Authenticatable
|
||||
$data['password'] = \Hash::make($data['password']);
|
||||
}
|
||||
|
||||
// Strip tags.
|
||||
$fields_strip = [
|
||||
'first_name',
|
||||
'last_name',
|
||||
'phone',
|
||||
'timezone',
|
||||
];
|
||||
|
||||
foreach ($fields_strip as $field) {
|
||||
if (in_array($field, array_keys($data))) {
|
||||
if ($data[$field] !== null) {
|
||||
$data[$field] = strip_tags($data[$field]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($replace_data) {
|
||||
$this->fill($data);
|
||||
} else {
|
||||
@@ -1198,4 +1228,121 @@ class User extends Authenticatable
|
||||
{
|
||||
return User::where('type', User::TYPE_ROBOT);
|
||||
}
|
||||
|
||||
// Truncate fields to their max lengths to avoid PostgreSQL error:
|
||||
// SQLSTATE[22001]: String data, right truncated: 7 ERROR: value too long for type character varying(100).
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3489
|
||||
public function setFirstNameAttribute($first_name)
|
||||
{
|
||||
$this->attributes['first_name'] = mb_substr($first_name ?? '', 0, 20);
|
||||
}
|
||||
public function setLastNameAttribute($last_name)
|
||||
{
|
||||
$this->attributes['last_name'] = mb_substr($last_name ?? '', 0, 30);
|
||||
}
|
||||
public function setEmailAttribute($email)
|
||||
{
|
||||
$this->attributes['email'] = mb_substr($email ?? '', 0, 100);
|
||||
}
|
||||
public function setJobTitleAttribute($job_title)
|
||||
{
|
||||
$this->attributes['job_title'] = mb_substr($job_title ?? '', 0, 100);
|
||||
}
|
||||
|
||||
public function canSeeOnlyAssignedConversations()
|
||||
{
|
||||
return $this->hasManageMailboxPermission(0, Mailbox::ACCESS_PERM_ASSIGNED);
|
||||
}
|
||||
|
||||
public function deleteUser($auth_user, $assign_user)
|
||||
{
|
||||
// We have to process conversations one by one to move them to Unassigned folder,
|
||||
// as conversations may be in different mailboxes
|
||||
// $this->conversations()->update(['user_id' => null, 'folder_id' => ]);
|
||||
$mailbox_unassigned_folders = [];
|
||||
|
||||
$this->conversations->each(function ($conversation) use ($auth_user, $assign_user) {
|
||||
// We don't fire ConversationUserChanged event to avoid sending notifications to users
|
||||
if (!empty($assign_user)
|
||||
&& !empty($assign_user[$conversation->mailbox_id])
|
||||
&& (int) $assign_user[$conversation->mailbox_id] != -1
|
||||
) {
|
||||
// Set assignee.
|
||||
// In this case conversation stays assigned, just assignee changes.
|
||||
$conversation->user_id = $assign_user[$conversation->mailbox_id];
|
||||
|
||||
} else {
|
||||
|
||||
// Make convesation Unassigned.
|
||||
|
||||
// Unset assignee.
|
||||
// Maybe use changeUser() here.
|
||||
$conversation->user_id = null;
|
||||
|
||||
if ($conversation->isPublished()
|
||||
&& ($conversation->isActive() || $conversation->isPending())
|
||||
) {
|
||||
// Change conversation folder to UNASSIGNED.
|
||||
$folder_id = null;
|
||||
if (!empty($mailbox_unassigned_folders[$conversation->mailbox_id])) {
|
||||
$folder_id = $mailbox_unassigned_folders[$conversation->mailbox_id];
|
||||
} else {
|
||||
$folder = $conversation->mailbox->folders()
|
||||
->where('type', Folder::TYPE_UNASSIGNED)
|
||||
->first();
|
||||
|
||||
if ($folder) {
|
||||
$folder_id = $folder->id;
|
||||
$mailbox_unassigned_folders[$conversation->mailbox_id] = $folder_id;
|
||||
}
|
||||
}
|
||||
if ($folder_id) {
|
||||
$conversation->folder_id = $folder_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$conversation->save();
|
||||
|
||||
// Create lineitem thread
|
||||
$thread = new Thread();
|
||||
$thread->conversation_id = $conversation->id;
|
||||
$thread->user_id = $conversation->user_id;
|
||||
$thread->type = Thread::TYPE_LINEITEM;
|
||||
$thread->state = Thread::STATE_PUBLISHED;
|
||||
$thread->status = Thread::STATUS_NOCHANGE;
|
||||
$thread->action_type = Thread::ACTION_TYPE_USER_CHANGED;
|
||||
$thread->source_via = Thread::PERSON_USER;
|
||||
$thread->source_type = Thread::SOURCE_TYPE_WEB;
|
||||
$thread->customer_id = $conversation->customer_id;
|
||||
$thread->created_by_user_id = $auth_user->id;
|
||||
$thread->save();
|
||||
});
|
||||
|
||||
// Recalculate counters for folders
|
||||
//if ($this->isAdmin()) {
|
||||
// Admin has access to all mailboxes
|
||||
Mailbox::all()->each(function ($mailbox) {
|
||||
$mailbox->updateFoldersCounters();
|
||||
});
|
||||
// } else {
|
||||
// $this->mailboxes->each(function ($mailbox) {
|
||||
// $mailbox->updateFoldersCounters();
|
||||
// });
|
||||
// }
|
||||
|
||||
// Disconnect user from mailboxes.
|
||||
$this->mailboxes()->sync([]);
|
||||
$this->folders()->delete();
|
||||
|
||||
$this->status = \App\User::STATUS_DELETED;
|
||||
// Update email.
|
||||
$email_suffix = User::EMAIL_DELETED_SUFFIX.date('YmdHis');
|
||||
// We have to truncate email to avoid "Data too long" error.
|
||||
$this->email = mb_substr($this->email, 0, User::EMAIL_MAX_LENGTH - mb_strlen($email_suffix)).$email_suffix;
|
||||
|
||||
$this->save();
|
||||
|
||||
event(new UserDeleted($this, $auth_user));
|
||||
}
|
||||
}
|
||||
|
||||
190
composer.json
190
composer.json
@@ -22,7 +22,6 @@
|
||||
"lord/laroute": "2.4.7",
|
||||
"axn/laravel-laroute": "1.3.0",
|
||||
"html2text/html2text": "4.1.0",
|
||||
"webklex/laravel-imap": "1.2.7",
|
||||
"watson/rememberable": "2.0.4",
|
||||
"nwidart/laravel-modules": "2.7.0",
|
||||
"tormjens/eventy": "0.5.4",
|
||||
@@ -89,12 +88,14 @@
|
||||
"Swift_": "overrides/swiftmailer/swiftmailer/lib/classes",
|
||||
"HTMLPurifier_": "overrides/ezyang/htmlpurifier/library"
|
||||
},
|
||||
"files": [
|
||||
"app/Misc/Functions.php"
|
||||
],
|
||||
"fs-comment": "Above new lines are listed files containing custom updates. Below - only PHP compatibility updates.",
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Modules\\": "Modules/",
|
||||
"Axn\\Laroute\\Routes\\": "overrides/axn/laravel-laroute/src/Routes/",
|
||||
"Webklex\\IMAP\\": "overrides/webklex/laravel-imap/src/IMAP/",
|
||||
"Webklex\\PHPIMAP\\": "overrides/webklex/php-imap/src/",
|
||||
"RachidLaasri\\LaravelInstaller\\Helpers\\": "overrides/rachidlaasri/laravel-installer/src/Helpers/",
|
||||
"RachidLaasri\\LaravelInstaller\\Middleware\\": "overrides/rachidlaasri/laravel-installer/src/Middleware/",
|
||||
@@ -107,6 +108,7 @@
|
||||
"Illuminate\\Foundation\\": "overrides/laravel/framework/src/Illuminate/Foundation/",
|
||||
"Illuminate\\Foundation\\Testing\\": "overrides/laravel/framework/src/Illuminate/Foundation/Testing/",
|
||||
"Illuminate\\Foundation\\Http\\Middleware\\": "overrides/laravel/framework/src/Illuminate/Foundation/Http/Middleware/",
|
||||
"Illuminate\\Foundation\\Bootstrap\\": "overrides/laravel/framework/src/Illuminate/Foundation/Bootstrap/",
|
||||
"Illuminate\\Routing\\": "overrides/laravel/framework/src/Illuminate/Routing/",
|
||||
"Illuminate\\Broadcasting\\Broadcasters\\": "overrides/laravel/framework/src/Illuminate/Broadcasting/Broadcasters/",
|
||||
"Illuminate\\Validation\\Concerns\\": "overrides/laravel/framework/src/Illuminate/Validation/Concerns/",
|
||||
@@ -124,56 +126,97 @@
|
||||
"Symfony\\Component\\HttpFoundation\\File\\MimeType\\": "overrides/symfony/http-foundation/File/MimeType/",
|
||||
"Chumper\\Zipper\\Repositories\\": "overrides/chumper/zipper/src/Chumper/Zipper/Repositories/",
|
||||
"Faker\\Provider\\": "overrides/fzaninotto/faker/src/Faker/Provider/",
|
||||
"Illuminate\\Database\\": "overrides/laravel/framework/src/Illuminate/Database/",
|
||||
|
||||
"Chumper\\Zipper\\": "overrides/chumper/zipper/src/Chumper/Zipper/",
|
||||
"Illuminate\\Support\\": "overrides/laravel/framework/src/Illuminate/Support/",
|
||||
"Illuminate\\Http\\": "overrides/laravel/framework/src/Illuminate/Http/",
|
||||
"Illuminate\\Database\\Eloquent\\": "overrides/laravel/framework/src/Illuminate/Database/Eloquent/",
|
||||
"Illuminate\\Database\\Eloquent\\Concerns\\": "overrides/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/",
|
||||
"Illuminate\\Database\\Eloquent\\Relations\\": "overrides/laravel/framework/src/Illuminate/Database/Eloquent/Relations/",
|
||||
"Illuminate\\Database\\Schema\\": "overrides/laravel/framework/src/Illuminate/Database/Schema/",
|
||||
"Illuminate\\Database\\Query\\": "overrides/laravel/framework/src/Illuminate/Database/Query/",
|
||||
"Illuminate\\Pagination\\": "overrides/laravel/framework/src/Illuminate/Pagination/",
|
||||
"Illuminate\\Session\\": "overrides/laravel/framework/src/Illuminate/Session/",
|
||||
"Illuminate\\Session\\Middleware\\": "overrides/laravel/framework/src/Illuminate/Session/Middleware/",
|
||||
"Illuminate\\Queue\\": "overrides/laravel/framework/src/Illuminate/Queue/",
|
||||
"Carbon\\": "overrides/nesbot/carbon/src/Carbon/",
|
||||
"Illuminate\\Cache\\": "overrides/laravel/framework/src/Illuminate/Cache/",
|
||||
"Illuminate\\Config\\": "overrides/laravel/framework/src/Illuminate/Config/",
|
||||
"Illuminate\\Notifications\\": "overrides/laravel/framework/src/Illuminate/Notifications/",
|
||||
"Illuminate\\Auth\\Access\\": "overrides/laravel/framework/src/Illuminate/Auth/Access/",
|
||||
"Illuminate\\Broadcasting\\": "overrides/laravel/framework/src/Illuminate/Broadcasting/",
|
||||
"Illuminate\\Bus\\": "overrides/laravel/framework/src/Illuminate/Bus/",
|
||||
"Illuminate\\Pipeline\\": "overrides/laravel/framework/src/Illuminate/Pipeline/",
|
||||
"Doctrine\\DBAL\\Driver\\": "overrides/doctrine/dbal/lib/Doctrine/DBAL/Driver/",
|
||||
"Doctrine\\DBAL\\Schema\\": "overrides/doctrine/dbal/lib/Doctrine/DBAL/Schema/",
|
||||
"Doctrine\\DBAL\\Platforms\\": "overrides/doctrine/dbal/lib/Doctrine/DBAL/Platforms/",
|
||||
"Symfony\\Component\\Finder\\Iterator\\": "overrides/symfony/finder/Iterator/",
|
||||
"Symfony\\Component\\Finder\\": "overrides/symfony/finder/",
|
||||
"Symfony\\Component\\Console\\Helper\\": "overrides/symfony/console/Helper/",
|
||||
"Symfony\\Component\\Console\\": "overrides/symfony/console/",
|
||||
"Symfony\\Component\\Console\\Command\\": "overrides/symfony/console/Command/",
|
||||
"Symfony\\Component\\Console\\Output\\": "overrides/symfony/console/Output/",
|
||||
"DebugBar\\": "overrides/maximebf/debugbar/src/DebugBar/",
|
||||
"DebugBar\\DataFormatter\\": "overrides/maximebf/debugbar/src/DataFormatter/DataFormatter/",
|
||||
"DebugBar\\DataCollector\\PDO\\": "overrides/maximebf/debugbar/src/DataFormatter/DataCollector/PDO/",
|
||||
"Illuminate\\Cache\\Console\\": "overrides/laravel/framework/src/Illuminate/Cache/Console/",
|
||||
"Dotenv\\": "overrides/vlucas/phpdotenv/src/",
|
||||
"Illuminate\\View\\": "overrides/laravel/framework/src/Illuminate/View/",
|
||||
"Illuminate\\View\\Compilers\\": "overrides/laravel/framework/src/Illuminate/View/Compilers/",
|
||||
"Illuminate\\View\\Compilers\\Concerns\\": "overrides/laravel/framework/src/Illuminate/View/Compilers/Concerns/",
|
||||
"Illuminate\\View\\Concerns\\": "overrides/laravel/framework/src/Illuminate/View/Concerns/",
|
||||
"Illuminate\\Console\\": "overrides/laravel/framework/src/Illuminate/Console/",
|
||||
"Illuminate\\Log\\": "overrides/laravel/framework/src/Illuminate/Log/",
|
||||
"Illuminate\\Validation\\": "overrides/laravel/framework/src/Illuminate/Validation/",
|
||||
"Symfony\\Component\\Routing\\": "overrides/symfony/routing/",
|
||||
"Symfony\\Component\\VarDumper\\Cloner\\": "overrides/symfony/var-dumper/Cloner/",
|
||||
"Symfony\\Component\\VarDumper\\Dumper\\": "overrides/symfony/var-dumper/Dumper/",
|
||||
"Devfactory\\Minify\\Providers\\": "overrides/devfactory/minify/src/Providers/",
|
||||
"Barryvdh\\Debugbar\\": "overrides/barryvdh/laravel-debugbar/src/",
|
||||
"Barryvdh\\Debugbar\\DataFormatter\\": "overrides/barryvdh/laravel-debugbar/src/DataFormatter/",
|
||||
"Barryvdh\\Debugbar\\DataCollector\\": "overrides/barryvdh/laravel-debugbar/src/DataCollector/",
|
||||
"Symfony\\Component\\Process\\": "overrides/symfony/process/",
|
||||
"Symfony\\Component\\HttpKernel\\": "overrides/symfony/http-kernel/",
|
||||
"Symfony\\Component\\HttpKernel\\Exception\\": "overrides/symfony/http-kernel/Exception/",
|
||||
"Symfony\\Component\\HttpKernel\\HttpCache\\": "overrides/symfony/http-kernel/HttpCache/",
|
||||
"Symfony\\Component\\Debug\\Exception\\": "overrides/symfony/debug/Exception/",
|
||||
"Spatie\\String\\": "overrides/spatie/string/src/",
|
||||
"GuzzleHttp\\": "overrides/guzzlehttp/guzzle/src/",
|
||||
"GuzzleHttp\\Cookie\\": "overrides/guzzlehttp/guzzle/src/Cookie/",
|
||||
"GuzzleHttp\\Handler\\": "overrides/guzzlehttp/guzzle/src/Handler/",
|
||||
"GuzzleHttp\\Psr7\\": "overrides/guzzlehttp/psr7/src/",
|
||||
"GuzzleHttp\\Promise\\": "overrides/guzzlehttp/promises/src/",
|
||||
"Ramsey\\Uuid\\": "overrides/ramsey/uuid/src/",
|
||||
"Rap2hpoutre\\LaravelLogViewer\\": "overrides/rap2hpoutre/laravel-log-viewer/src/Rap2hpoutre/LaravelLogViewer/",
|
||||
"Symfony\\Component\\Console\\Descriptor\\": "overrides/symfony/console/Descriptor",
|
||||
"Symfony\\Component\\Console\\Descriptor\\": "overrides/symfony/console/Descriptor/",
|
||||
"Symfony\\Component\\Console\\Input\\": "overrides/symfony/console/Input/",
|
||||
"Symfony\\Component\\Console\\Formatter\\": "overrides/symfony/console/Formatter/",
|
||||
"Symfony\\Component\\Console\\Exception\\": "overrides/symfony/console/Exception/",
|
||||
"Symfony\\Component\\Console\\Question\\": "overrides/symfony/console/",
|
||||
"Symfony\\Component\\CssSelector\\XPath\\Extension\\": "overrides/symfony/css-selector/XPath/Extension/",
|
||||
"Javoscript\\MacroableModels\\": "overrides/javoscript/laravel-macroable-models/src/"
|
||||
"Symfony\\Component\\Translation\\Loader\\": "overrides/symfony/translation/Loader/",
|
||||
"Symfony\\Component\\Translation\\Formatter\\": "overrides/symfony/translation/Formatter/",
|
||||
"Symfony\\Component\\Translation\\": "overrides/symfony/translation/",
|
||||
"Javoscript\\MacroableModels\\": "overrides/javoscript/laravel-macroable-models/src/",
|
||||
"League\\Flysystem\\": "overrides/league/flysystem/src/",
|
||||
"Whoops\\": "overrides/filp/whoops/src/Whoops/",
|
||||
"Whoops\\Handler\\": "overrides/filp/whoops/src/Whoops/Handler/",
|
||||
"Whoops\\Util\\": "overrides/filp/whoops/src/Whoops/Util/",
|
||||
"Mews\\Purifier\\": "overrides/mews/purifier/src/",
|
||||
"Cron\\": "overrides/mtdowling/cron-expression/src/Cron/",
|
||||
"Psy\\": "overrides/psy/psysh/src/",
|
||||
"Psy\\Input\\": "overrides/psy/psysh/src/Input/",
|
||||
"Psy\\Output\\": "overrides/psy/psysh/src/Output/",
|
||||
"Psy\\Command\\": "overrides/psy/psysh/src/Command/",
|
||||
"Psy\\TimeitCommand\\": "overrides/psy/psysh/src/TimeitCommand/",
|
||||
"Psy\\Command\\ListCommand\\": "overrides/psy/psysh/src/Command/ListCommand/",
|
||||
"Psy\\Exception\\": "overrides/psy/psysh/src/ErrorException/",
|
||||
"Psy\\VarDumper\\": "overrides/psy/psysh/src/VarDumper/"
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"Modules/",
|
||||
"vendor/axn/laravel-laroute/src/Routes/Collection.php",
|
||||
"vendor/webklex/laravel-imap/src/IMAP/Message.php",
|
||||
"vendor/webklex/laravel-imap/src/IMAP/Attachment.php",
|
||||
"vendor/webklex/laravel-imap/src/IMAP/Client.php",
|
||||
"vendor/rachidlaasri/laravel-installer/src/Helpers/RequirementsChecker.php",
|
||||
"vendor/rachidlaasri/laravel-installer/src/Helpers/PermissionsChecker.php",
|
||||
"vendor/rachidlaasri/laravel-installer/src/Helpers/EnvironmentManager.php",
|
||||
@@ -219,24 +262,34 @@
|
||||
"vendor/chumper/zipper/src/Chumper/Zipper/Repositories/ZipRepository.php",
|
||||
"vendor/fzaninotto/faker/src/Faker/Provider/Base.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Database/DetectsLostConnections.php",
|
||||
|
||||
"vendor/laravel/framework/src/Illuminate/Support/Collection.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Http/Request.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Http/RedirectResponse.php",
|
||||
"vendor/symfony/http-foundation/ParameterBag.php",
|
||||
"vendor/symfony/http-foundation/HeaderBag.php",
|
||||
"vendor/symfony/http-foundation/Response.php",
|
||||
"vendor/symfony/http-foundation/AcceptHeader.php",
|
||||
"vendor/symfony/http-foundation/FileBag.php",
|
||||
"vendor/symfony/http-foundation/Request.php",
|
||||
"vendor/symfony/http-foundation/StreamedResponse.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/Relation.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Database/Schema/Blueprint.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Pagination/AbstractPaginator.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Pagination/Paginator.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Pagination/LengthAwarePaginator.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Session/FileSessionHandler.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Queue/Listener.php",
|
||||
"vendor/nesbot/carbon/src/Carbon/Carbon.php",
|
||||
"vendor/nesbot/carbon/src/Carbon/Translator.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Cache/Repository.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Config/Repository.php",
|
||||
"vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php",
|
||||
@@ -252,6 +305,8 @@
|
||||
"vendor/laravel/framework/src/Illuminate/Support/Optional.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Support/Fluent.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factory.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php",
|
||||
"vendor/symfony/finder/Finder.php",
|
||||
"vendor/symfony/finder/Iterator/FilterIterator.php",
|
||||
"vendor/symfony/finder/Iterator/FileTypeFilterIterator.php",
|
||||
@@ -262,12 +317,18 @@
|
||||
"vendor/symfony/finder/Iterator/DepthRangeFilterIterator.php",
|
||||
"vendor/symfony/finder/Iterator/DateRangeFilterIterator.php",
|
||||
"vendor/symfony/console/Helper/HelperSet.php",
|
||||
"vendor/symfony/console/Helper/HelperInterface.php",
|
||||
"vendor/symfony/console/Helper/Table.php",
|
||||
"vendor/symfony/console/Application.php",
|
||||
"vendor/symfony/debug/Exception/FatalErrorException.php",
|
||||
"vendor/maximebf/debugbar/src/DebugBar/DebugBar.php",
|
||||
"vendor/maximebf/debugbar/src/DebugBar/JavascriptRenderer.php",
|
||||
"vendor/maximebf/debugbar/src/DebugBar/DataFormatter/DataFormatter.php",
|
||||
"vendor/maximebf/debugbar/src/DebugBar/DataCollector/PDO/PDOCollector.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Cache/Console/ClearCommand.php",
|
||||
"vendor/vlucas/phpdotenv/src/Loader.php",
|
||||
"vendor/laravel/framework/src/Illuminate/View/View.php",
|
||||
"vendor/laravel/framework/src/Illuminate/View/FileViewFinder.php",
|
||||
"vendor/laravel/framework/src/Illuminate/View/Compilers/Compiler.php",
|
||||
"vendor/laravel/framework/src/Illuminate/View/Compilers/Concerns/CompilesLayouts.php",
|
||||
"vendor/laravel/framework/src/Illuminate/View/Concerns/ManagesLayouts.php",
|
||||
@@ -275,38 +336,151 @@
|
||||
"vendor/symfony/routing/CompiledRoute.php",
|
||||
"vendor/symfony/var-dumper/Cloner/Data.php",
|
||||
"vendor/symfony/var-dumper/Cloner/Stub.php",
|
||||
"vendor/symfony/var-dumper/Cloner/AbstractCloner.php",
|
||||
"vendor/symfony/var-dumper/Dumper/HtmlDumper.php",
|
||||
"vendor/devfactory/minify/src/Providers/BaseProvider.php",
|
||||
"vendor/barryvdh/laravel-debugbar/src/DataFormatter/QueryFormatter.php",
|
||||
"vendor/barryvdh/laravel-debugbar/src/DataCollector/QueryCollector.php",
|
||||
"vendor/symfony/process/Process.php",
|
||||
"vendor/symfony/http-kernel/UriSigner.php",
|
||||
"vendor/symfony/http-kernel/Exception/HttpException.php",
|
||||
"vendor/symfony/http-kernel/Exception/AccessDeniedHttpException.php",
|
||||
"vendor/symfony/http-kernel/Exception/MethodNotAllowedHttpException.php",
|
||||
"vendor/symfony/http-kernel/Exception/NotFoundHttpException.php",
|
||||
"vendor/symfony/http-kernel/Exception/ServiceUnavailableHttpException.php",
|
||||
"vendor/symfony/http-kernel/Exception/UnauthorizedHttpException.php",
|
||||
"vendor/symfony/http-kernel/HttpCache/Store.php",
|
||||
"vendor/barryvdh/laravel-debugbar/src/JavascriptRenderer.php",
|
||||
"vendor/spatie/string/src/Str.php",
|
||||
"vendor/guzzlehttp/guzzle/src/Client.php",
|
||||
"vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php",
|
||||
"vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php",
|
||||
"vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php",
|
||||
"vendor/guzzlehttp/guzzle/src/HandlerStack.php",
|
||||
"vendor/guzzlehttp/guzzle/src/Middleware.php",
|
||||
"vendor/guzzlehttp/psr7/src/LazyOpenStream.php",
|
||||
"vendor/guzzlehttp/psr7/src/Uri.php",
|
||||
"vendor/guzzlehttp/promises/src/RejectedPromise.php",
|
||||
"vendor/guzzlehttp/promises/src/PromiseInterface.php",
|
||||
"vendor/guzzlehttp/promises/src/FulfilledPromise.php",
|
||||
"vendor/guzzlehttp/promises/src/Utils.php",
|
||||
"vendor/guzzlehttp/promises/src/Promise.php",
|
||||
"vendor/ramsey/uuid/src/Uuid.php",
|
||||
"vendor/ramsey/uuid/src/UuidFactory.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Routing/Router.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Console/Application.php",
|
||||
"vendor/rap2hpoutre/laravel-log-viewer/src/Rap2hpoutre/LaravelLogViewer/LaravelLogViewer.php",
|
||||
"vendor/rap2hpoutre/laravel-log-viewer/src/controllers/LogViewerController.php",
|
||||
"vendor/symfony/console/Descriptor/TextDescriptor.php",
|
||||
"vendor/symfony/console/Input/ArrayInput.php",
|
||||
"vendor/symfony/console/Input/StringInput.php",
|
||||
"vendor/symfony/console/Input/Input.php",
|
||||
"vendor/symfony/console/Helper/Helper.php",
|
||||
"vendor/symfony/console/Helper/ProcessHelper.php",
|
||||
"vendor/symfony/console/Command/Command.php",
|
||||
"vendor/symfony/console/Output/Output.php",
|
||||
"vendor/symfony/console/Formatter/OutputFormatterStyleStack.php",
|
||||
"vendor/symfony/console/Exception/CommandNotFoundException.php",
|
||||
"vendor/symfony/console/Question/Question.php",
|
||||
"vendor/symfony/finder/Iterator/SortableIterator.php",
|
||||
"vendor/symfony/css-selector/XPath/Extension/NodeExtension.php",
|
||||
"vendor/symfony/translation/Loader/ArrayLoader.php",
|
||||
"vendor/symfony/translation/Formatter/MessageFormatter.php",
|
||||
"vendor/symfony/translation/Translator.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/NameSync.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/List.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/Length.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php",
|
||||
"vendor/javoscript/laravel-macroable-models/src/MacroableModels.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Attachment.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/EmbeddedFile.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/MailTransport.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/MailTransport.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Message.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/MimePart.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/SendmailTransport.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/SmtpTransport.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/SwiftException.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/TransportException.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/IoException.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/MimePart.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/SimpleHeaderSet.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/SimpleHeaderFactory.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ContentEncoder/Base64ContentEncoder.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ContentEncoder/QpContentEncoder.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/AbstractHeader.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/ParameterizedHeader.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/IdentificationHeader.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/MailboxHeader.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/AbstractSmtpTransport.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/EsmtpTransport.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/MailInvoker.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/MailTransport.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/SimpleMailInvoker.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/StreamBuffer.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/AuthHandler.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Encoder/QpEncoder.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/SimpleKeyCacheInputStream.php",
|
||||
"vendor/natxet/cssmin/src/CssMin.php",
|
||||
"vendor/webklex/php-imap/src/Header.php",
|
||||
"vendor/webklex/php-imap/src/Structure.php",
|
||||
"vendor/webklex/php-imap/src/Message.php",
|
||||
"vendor/webklex/php-imap/src/Attachment.php",
|
||||
"vendor/webklex/php-imap/src/Connection/Protocols/ImapProtocol.php"
|
||||
"vendor/webklex/php-imap/src/Part.php",
|
||||
"vendor/webklex/php-imap/src/Client.php",
|
||||
"vendor/webklex/php-imap/src/Connection/Protocols/ImapProtocol.php",
|
||||
"vendor/webklex/php-imap/src/Connection/Protocols/LegacyProtocol.php",
|
||||
"vendor/webklex/php-imap/src/Query/Query.php",
|
||||
"vendor/webklex/php-imap/src/Query/WhereQuery.php",
|
||||
"vendor/webklex/php-imap/src/ClientManager.php",
|
||||
"vendor/webklex/php-imap/src/Folder.php",
|
||||
"vendor/webklex/php-imap/src/EncodingAliases.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Log/Writer.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Notifications/RoutesNotifications.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Notifications/SendQueuedNotifications.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Notifications/ChannelManager.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Notifications/NotificationSender.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Auth/Access/Gate.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Broadcasting/BroadcastManager.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
|
||||
"vendor/league/flysystem/src/Filesystem.php",
|
||||
"vendor/league/flysystem/src/FilesystemInterface.php",
|
||||
"vendor/league/flysystem/src/FileNotFoundException.php",
|
||||
"vendor/mews/purifier/src/Purifier.php",
|
||||
"vendor/mtdowling/cron-expression/src/Cron/CronExpression.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Mail/Mailer.php",
|
||||
"vendor/filp/whoops/src/Whoops/Run.php",
|
||||
"vendor/filp/whoops/src/Whoops/Handler/PrettyPageHandler.php",
|
||||
"vendor/filp/whoops/src/Whoops/Util/TemplateHelper.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Validation/Factory.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Validation/ValidationRuleParser.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Foundation/Validation/ValidatesRequests.php",
|
||||
"vendor/chumper/zipper/src/Chumper/Zipper/Zipper.php",
|
||||
"vendor/psy/psysh/src/Shell.php",
|
||||
"vendor/psy/psysh/src/CodeCleaner.php",
|
||||
"vendor/psy/psysh/src/Input/FilterOptions.php",
|
||||
"vendor/psy/psysh/src/Input/ShellInput.php",
|
||||
"vendor/psy/psysh/src/Output/ShellOutput.php",
|
||||
"vendor/psy/psysh/src/Command/Command.php",
|
||||
"vendor/psy/psysh/src/Command/ListCommand.php",
|
||||
"vendor/psy/psysh/src/Command/TimeitCommand/TimeitVisitor.php",
|
||||
"vendor/psy/psysh/src/Command/ListCommand/ClassConstantEnumerator.php",
|
||||
"vendor/psy/psysh/src/Command/ListCommand/Enumerator.php",
|
||||
"vendor/psy/psysh/src/Command/ListCommand/ClassEnumerator.php",
|
||||
"vendor/psy/psysh/src/Command/ListCommand/ConstantEnumerator.php",
|
||||
"vendor/psy/psysh/src/Command/ListCommand/FunctionEnumerator.php",
|
||||
"vendor/psy/psysh/src/Exception/ErrorException.php",
|
||||
"vendor/psy/psysh/src/Exception/BreakException.php",
|
||||
"vendor/psy/psysh/src/VarDumper/Dumper.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Foundation/Application.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
|
||||
509
composer.lock
generated
509
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ return [
|
||||
| or any other location as required by the application or its packages.
|
||||
*/
|
||||
|
||||
'version' => '1.8.106',
|
||||
'version' => '1.8.187',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -98,8 +98,8 @@ return [
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
'locales' => ['en', 'zh-CN', 'hr', 'cs', 'da', 'nl', 'fi', 'fr', 'de', 'it', 'ja', 'ko', 'no', 'fa', 'pl', 'pt-PT', 'pt-BR', 'ru', 'es', 'sk', 'sv'],
|
||||
'locales_rtl' => ['fa'],
|
||||
'locales' => ['en', 'ar', 'zh-CN', 'hr', 'cs', 'da', 'nl', 'fi', 'fr', 'de', 'hu', 'it', 'ja', 'kz', 'ko', 'no', 'fa', 'pl', 'pt-PT', 'pt-BR', 'ro', 'ru', 'es', 'sk', 'sv', 'tr'],
|
||||
'locales_rtl' => ['ar', 'fa'],
|
||||
'default_locale' => 'en',
|
||||
|
||||
/*
|
||||
@@ -170,10 +170,10 @@ return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| FreeScout eepository
|
||||
| FreeScout repository
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'freescout_repo' => 'https://github.com/freescout-helpdesk/freescout',
|
||||
'freescout_repo' => 'https://freescout.net/github',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -188,7 +188,7 @@ return [
|
||||
| Checks for new jobs every --sleep seconds.
|
||||
| If --tries is set and job fails it is being processed right away without any delay.
|
||||
| --delay parameter does not work to set delays between retry attempts.
|
||||
| --timeout parameter sets job timeout and is used to avoid queue:work stucking.
|
||||
| --timeout parameter sets job timeout and is used to avoid queue:work freezing.
|
||||
|
|
||||
| Jobs sending emails are retried manually in handle().
|
||||
| Number of retries is set in each job class.
|
||||
@@ -219,6 +219,7 @@ return [
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'fetch_schedule' => env('APP_FETCH_SCHEDULE', 1),
|
||||
'fetch_unseen' => env('APP_FETCH_UNSEEN', 1),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -288,7 +289,7 @@ return [
|
||||
|
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'no_retry_mail_errors' => env('APP_NO_RETRY_MAIL_ERRORS', '(no valid recipients|does not comply with RFC|message file too big)'),
|
||||
'no_retry_mail_errors' => env('APP_NO_RETRY_MAIL_ERRORS', '(no valid recipients|does not comply with RFC|message file too big|malformed address)'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -338,6 +339,26 @@ return [
|
||||
*/
|
||||
'use_mail_date_on_fetching' => env('APP_USE_MAIL_DATE_ON_FETCHING', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Don't add quotes around date in the SINCE IMAP instruction on fetching.
|
||||
| https://github.com/freescout-help-desk/freescout/issues/4175
|
||||
|
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'since_without_quotes_on_fetching' => env('APP_SINCE_WITHOUT_QUOTES_ON_FETCHING', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Emails are fetched in bunches. The larger the bunch's size the more chances
|
||||
| to face "Allowed memory size exhausted" error. The smaller its size the more
|
||||
| connections are made to the mail server and the more time fetching takes.
|
||||
| https://github.com/freescout-help-desk/freescout/issues/4343
|
||||
|
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'fetching_bunch_size' => env('APP_FETCHING_BUNCH_SIZE', 100),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Dashboard path.
|
||||
@@ -392,13 +413,6 @@ return [
|
||||
*/
|
||||
'custom_mail_headers' => env('APP_CUSTOM_MAIL_HEADERS', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Library used to fetch emails: webklex/laravel-imap, webklex/php-imap
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'new_fetching_library' => env('APP_NEW_FETCHING_LIBRARY', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Timeout for curl and GuzzleHttp.
|
||||
@@ -455,6 +469,14 @@ return [
|
||||
*/
|
||||
'show_only_assigned_conversations' => env('APP_SHOW_ONLY_ASSIGNED_CONVERSATIONS', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Limit non-admin users to only see customers with conversations
|
||||
| in mailboxes they are assigned to. This option does not affect admin users.
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'limit_user_customer_visibility' => env('APP_LIMIT_USER_CUSTOMER_VISIBILITY', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| By default X-Frame-Options header is enabled and set to SAMEORIGIN.
|
||||
@@ -472,6 +494,7 @@ return [
|
||||
*/
|
||||
'csp_enabled' => env('APP_CSP_ENABLED', true),
|
||||
'csp_script_src' => env('APP_CSP_SCRIPT_SRC', ''),
|
||||
'csp_custom' => env('APP_CSP_CUSTOM', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -480,6 +503,16 @@ return [
|
||||
*/
|
||||
'cloudflare_is_used' => env('APP_CLOUDFLARE_IS_USED', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| When this option is enabled you may see an extra text below customer's replies, for example:
|
||||
| On Thu, Jan 4, 2024 at 8:41 AM John Doe | Demo <test@example.org> wrote:
|
||||
|
|
||||
| But overall reply separation in this case is more reliable.
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'alternative_reply_separation' => env('APP_ALTERNATIVE_REPLY_SEPARATION', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Autoloaded Service Providers
|
||||
@@ -607,4 +640,4 @@ return [
|
||||
'Updater' => Codedge\Updater\UpdaterFacade::class,
|
||||
],
|
||||
|
||||
];
|
||||
];
|
||||
@@ -36,7 +36,7 @@ return [
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'prefix' => env('DB_TABLE_PREFIX', ''),
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
@@ -49,11 +49,13 @@ return [
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix' => env('DB_TABLE_PREFIX', ''),
|
||||
'strict' => false,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('DB_MYSQL_ATTR_SSL_CA'),
|
||||
PDO::MYSQL_ATTR_SSL_CERT => env('DB_MYSQL_ATTR_SSL_CERT'),
|
||||
PDO::ATTR_PERSISTENT => env('DB_ATTR_PERSISTENT'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
@@ -67,7 +69,7 @@ return [
|
||||
//'port' => env('DB_TEST_PORT', '3306'),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
'prefix' => env('DB_TABLE_PREFIX', ''),
|
||||
//'prefix_indexes' => true,
|
||||
'strict' => false,
|
||||
'engine' => null,
|
||||
@@ -81,7 +83,7 @@ return [
|
||||
'username' => env('DB_TEST_USERNAME', 'freescout-test'),
|
||||
'password' => env('DB_TEST_PASSWORD', 'freescout-test'),
|
||||
'charset' => 'utf8',
|
||||
'prefix' => '',
|
||||
'prefix' => env('DB_TABLE_PREFIX', ''),
|
||||
'schema' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
],
|
||||
@@ -94,9 +96,13 @@ return [
|
||||
'username' => env('DB_USERNAME', 'forge'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => 'utf8',
|
||||
'prefix' => '',
|
||||
'prefix' => env('DB_TABLE_PREFIX', ''),
|
||||
'schema' => 'public',
|
||||
'sslmode' => env('DB_PGSQL_SSLMODE', 'prefer'),
|
||||
'options' => extension_loaded('pdo_pgsql') ? array_filter([
|
||||
PDO::PGSQL_ATTR_DISABLE_PREPARES => env('DB_PGSQL_ATTR_DISABLE_PREPARES'),
|
||||
PDO::ATTR_PERSISTENT => env('DB_ATTR_PERSISTENT'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
@@ -107,7 +113,7 @@ return [
|
||||
'username' => env('DB_USERNAME', 'forge'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => 'utf8',
|
||||
'prefix' => '',
|
||||
'prefix' => env('DB_TABLE_PREFIX', ''),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
@@ -102,7 +102,8 @@ return [
|
||||
*/
|
||||
'options' => [
|
||||
'delimiter' => '/',
|
||||
'fetch' => FT_UID,
|
||||
//'fetch' => FT_UID,
|
||||
'fetch' => 1,
|
||||
'fetch_body' => true,
|
||||
'fetch_attachment' => true,
|
||||
'fetch_flags' => true,
|
||||
@@ -88,6 +88,18 @@ return [
|
||||
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Auth mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option have an effect when set to 'XOAUTH2' only.
|
||||
| In other cases Swiftmailer chooses auth mode automatically.
|
||||
|
|
||||
*/
|
||||
|
||||
'auth_mode' => env('MAIL_AUTH_MODE', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sendmail System Path
|
||||
@@ -120,4 +132,13 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Swiftmailer SMTP timeout.
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
|
||||
'smtp_timeout' => env('MAIL_SMTP_TIMEOUT', 30),
|
||||
|
||||
];
|
||||
|
||||
@@ -24,11 +24,13 @@ return [
|
||||
'settings' => [
|
||||
'default' => [
|
||||
'HTML.Doctype' => 'HTML 4.01 Transitional',
|
||||
'HTML.Allowed' => 'div[style],b,strong,i,em,u,a[href|title],ul,ol,li,p[style],br,span[style],img[width|height|alt|src],table[width|class],tr[bgcolor],td[style|colspan|rowspan|width],th[style|colspan|rowspan],thead,tfoot,tbody,blockquote,pre,s,strike,h1,h2,h3,h4,h5,h6',
|
||||
//'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align',
|
||||
'CSS.AllowedProperties' => 'font-weight,font-style,text-decoration,color,background-color,text-align,border,border-top,border-left,border-bottom,border-right',
|
||||
'HTML.Allowed' => 'div[style],b,strong,i,em,u,a[href|title|style],ul,ol,li,p[style],br,span[style],img[width|height|alt|src|style],table[style|border|bgcolor|cellspacing|cellpadding|border|width|class],tr[style|bgcolor],td[style|colspan|rowspan|width|bgcolor|border|valign|align],th[style|colspan|rowspan],thead,tfoot,tbody,blockquote,pre,s,strike,font[style|color],h1[style],h2[style],h3[style],h4[style],h5[style],h6,center',
|
||||
'CSS.AllowedProperties' => 'display,overflow,border-radius,letter-spacing,white-space,font-size,margin,margin-top,margin-right,margin-bottom,margin-left,background,text-transform,max-width,max-height,width,height,font,padding,padding-top,padding-right,padding-bottom,padding-left,font-family,border-color,font-weight,font-style,text-decoration,color,background-color,text-align,border,border-top,border-left,border-bottom,border-right',
|
||||
'AutoFormat.AutoParagraph' => true,
|
||||
'AutoFormat.RemoveEmpty' => true,
|
||||
'CSS.Proprietary' => true,
|
||||
'CSS.AllowTricky' => true,
|
||||
'CSS.MaxImgLength' => null,
|
||||
'URI.AllowedSchemes' => [
|
||||
'http' => true,
|
||||
'https' => true,
|
||||
|
||||
@@ -41,6 +41,7 @@ return [
|
||||
'repository_vendor' => 'freescout-helpdesk', //env('SELF_UPDATER_REPO_VENDOR', ''),
|
||||
'repository_name' => 'freescout', //env('SELF_UPDATER_REPO_NAME', ''),
|
||||
'repository_url' => '',
|
||||
'github_api_url' => 'https://freescout.net/github-api',
|
||||
'download_path' => storage_path().DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'updater', //env('SELF_UPDATER_DOWNLOAD_PATH', sys_get_temp_dir()),
|
||||
],
|
||||
],
|
||||
|
||||
@@ -188,10 +188,16 @@ return [
|
||||
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||
| do not enable this as other CSRF protection services are in place.
|
||||
|
|
||||
| Supported: "lax", "strict"
|
||||
| Supported: "lax", "strict", "none"
|
||||
|
|
||||
| When set to "lax" for example the SSO auth does not work:
|
||||
| https://github.com/freescout-help-desk/freescout/issues/4750
|
||||
|
|
||||
| If set to "none" the "secure" flag should be set:
|
||||
| https://github.com/freescout-help-desk/freescout/issues/4769
|
||||
|
|
||||
*/
|
||||
|
||||
'same_site' => null,
|
||||
'same_site' => env('SESSION_SAME_SITE', null),
|
||||
|
||||
];
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class ChangeAliasesColumnInMailboxesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('mailboxes', function (Blueprint $table) {
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3525
|
||||
$table->text('aliases')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddIndexToThreadsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4069#issuecomment-2175476858
|
||||
Schema::table('threads', function (Blueprint $table) {
|
||||
$table->index(['created_at']);
|
||||
});
|
||||
// Schema::table('conversations', function (Blueprint $table) {
|
||||
// $table->index(['mailbox_id', 'state', 'status'], 'conversations_id_state_status');
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('threads', function (Blueprint $table) {
|
||||
$table->dropIndex(['created_at']);
|
||||
});
|
||||
// Schema::table('conversations', function (Blueprint $table) {
|
||||
// $table->dropIndex('conversations_id_state_status');
|
||||
// });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class ChangeImapSentFolderColumnInMailboxesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('mailboxes', function (Blueprint $table) {
|
||||
$table->string('imap_sent_folder', 50)->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('mailboxes', function (Blueprint $table) {
|
||||
$table->string('imap_sent_folder', 25)->nullable()->change();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
<?php
|
||||
|
||||
namespace Barryvdh\Debugbar\DataCollector;
|
||||
|
||||
use DebugBar\DataCollector\PDO\PDOCollector;
|
||||
use DebugBar\DataCollector\TimeDataCollector;
|
||||
|
||||
/**
|
||||
* Collects data about SQL statements executed with PDO
|
||||
*/
|
||||
class QueryCollector extends PDOCollector
|
||||
{
|
||||
protected $timeCollector;
|
||||
protected $queries = [];
|
||||
protected $renderSqlWithParams = false;
|
||||
protected $findSource = false;
|
||||
protected $middleware = [];
|
||||
protected $explainQuery = false;
|
||||
protected $explainTypes = ['SELECT']; // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+
|
||||
protected $showHints = false;
|
||||
protected $reflection = [];
|
||||
|
||||
/**
|
||||
* @param TimeDataCollector $timeCollector
|
||||
*/
|
||||
public function __construct(?TimeDataCollector $timeCollector = null)
|
||||
{
|
||||
$this->timeCollector = $timeCollector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the SQL of traced statements with params embedded
|
||||
*
|
||||
* @param boolean $enabled
|
||||
* @param string $quotationChar NOT USED
|
||||
*/
|
||||
public function setRenderSqlWithParams($enabled = true, $quotationChar = "'")
|
||||
{
|
||||
$this->renderSqlWithParams = $enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide the hints in the parameters
|
||||
*
|
||||
* @param boolean $enabled
|
||||
*/
|
||||
public function setShowHints($enabled = true)
|
||||
{
|
||||
$this->showHints = $enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable finding the source
|
||||
*
|
||||
* @param bool $value
|
||||
* @param array $middleware
|
||||
*/
|
||||
public function setFindSource($value, array $middleware)
|
||||
{
|
||||
$this->findSource = (bool) $value;
|
||||
$this->middleware = $middleware;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable the EXPLAIN queries
|
||||
*
|
||||
* @param bool $enabled
|
||||
* @param array|null $types Array of types to explain queries (select/insert/update/delete)
|
||||
*/
|
||||
public function setExplainSource($enabled, $types)
|
||||
{
|
||||
$this->explainQuery = $enabled;
|
||||
if($types){
|
||||
$this->explainTypes = $types;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $bindings
|
||||
* @param float $time
|
||||
* @param \Illuminate\Database\Connection $connection
|
||||
*/
|
||||
public function addQuery($query, $bindings, $time, $connection)
|
||||
{
|
||||
$explainResults = [];
|
||||
$time = $time / 1000;
|
||||
$endTime = microtime(true);
|
||||
$startTime = $endTime - $time;
|
||||
$hints = $this->performQueryAnalysis($query);
|
||||
|
||||
$pdo = $connection->getPdo();
|
||||
$bindings = $connection->prepareBindings($bindings);
|
||||
|
||||
// Run EXPLAIN on this query (if needed)
|
||||
if ($this->explainQuery && preg_match('/^('.implode($this->explainTypes).') /i', $query)) {
|
||||
$statement = $pdo->prepare('EXPLAIN ' . $query);
|
||||
$statement->execute($bindings);
|
||||
$explainResults = $statement->fetchAll(\PDO::FETCH_CLASS);
|
||||
}
|
||||
|
||||
$bindings = $this->getDataFormatter()->checkBindings($bindings);
|
||||
if (!empty($bindings) && $this->renderSqlWithParams) {
|
||||
foreach ($bindings as $key => $binding) {
|
||||
// This regex matches placeholders only, not the question marks,
|
||||
// nested in quotes, while we iterate through the bindings
|
||||
// and substitute placeholders by suitable values.
|
||||
$regex = is_numeric($key)
|
||||
? "/\?(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/"
|
||||
: "/:{$key}(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/";
|
||||
$query = preg_replace($regex, $pdo->quote($binding), $query, 1);
|
||||
}
|
||||
}
|
||||
|
||||
$source = [];
|
||||
|
||||
if ($this->findSource) {
|
||||
try {
|
||||
$source = $this->findSource();
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
}
|
||||
|
||||
$this->queries[] = [
|
||||
'query' => $query,
|
||||
'type' => 'query',
|
||||
'bindings' => $this->getDataFormatter()->escapeBindings($bindings),
|
||||
'time' => $time,
|
||||
'source' => $source,
|
||||
'explain' => $explainResults,
|
||||
'connection' => $connection->getDatabaseName(),
|
||||
'hints' => $this->showHints ? $hints : null,
|
||||
];
|
||||
|
||||
if ($this->timeCollector !== null) {
|
||||
$this->timeCollector->addMeasure($query, $startTime, $endTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Explainer::performQueryAnalysis()
|
||||
*
|
||||
* Perform simple regex analysis on the code
|
||||
*
|
||||
* @package xplain (https://github.com/rap2hpoutre/mysql-xplain-xplain)
|
||||
* @author e-doceo
|
||||
* @copyright 2014
|
||||
* @version $Id$
|
||||
* @access public
|
||||
* @param string $query
|
||||
* @return string
|
||||
*/
|
||||
protected function performQueryAnalysis($query)
|
||||
{
|
||||
$hints = [];
|
||||
if (preg_match('/^\\s*SELECT\\s*`?[a-zA-Z0-9]*`?\\.?\\*/i', $query)) {
|
||||
$hints[] = 'Use <code>SELECT *</code> only if you need all columns from table';
|
||||
}
|
||||
if (preg_match('/ORDER BY RAND()/i', $query)) {
|
||||
$hints[] = '<code>ORDER BY RAND()</code> is slow, try to avoid if you can.
|
||||
You can <a href="http://stackoverflow.com/questions/2663710/how-does-mysqls-order-by-rand-work" target="_blank">read this</a>
|
||||
or <a href="http://stackoverflow.com/questions/1244555/how-can-i-optimize-mysqls-order-by-rand-function" target="_blank">this</a>';
|
||||
}
|
||||
if (strpos($query, '!=') !== false) {
|
||||
$hints[] = 'The <code>!=</code> operator is not standard. Use the <code><></code> operator to test for inequality instead.';
|
||||
}
|
||||
if (stripos($query, 'WHERE') === false && preg_match('/^(SELECT) /i', $query)) {
|
||||
$hints[] = 'The <code>SELECT</code> statement has no <code>WHERE</code> clause and could examine many more rows than intended';
|
||||
}
|
||||
if (preg_match('/LIMIT\\s/i', $query) && stripos($query, 'ORDER BY') === false) {
|
||||
$hints[] = '<code>LIMIT</code> without <code>ORDER BY</code> causes non-deterministic results, depending on the query execution plan';
|
||||
}
|
||||
if (preg_match('/LIKE\\s[\'"](%.*?)[\'"]/i', $query, $matches)) {
|
||||
$hints[] = 'An argument has a leading wildcard character: <code>' . $matches[1]. '</code>.
|
||||
The predicate with this argument is not sargable and cannot use an index if one exists.';
|
||||
}
|
||||
return $hints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a backtrace to search for the origins of the query.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function findSource()
|
||||
{
|
||||
$stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT, 50);
|
||||
|
||||
$sources = [];
|
||||
|
||||
foreach ($stack as $index => $trace) {
|
||||
$sources[] = $this->parseTrace($index, $trace);
|
||||
}
|
||||
|
||||
return array_filter($sources);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a trace element from the backtrace stack.
|
||||
*
|
||||
* @param int $index
|
||||
* @param array $trace
|
||||
* @return object|bool
|
||||
*/
|
||||
protected function parseTrace($index, array $trace)
|
||||
{
|
||||
$frame = (object) [
|
||||
'index' => $index,
|
||||
'namespace' => null,
|
||||
'name' => null,
|
||||
'line' => isset($trace['line']) ? $trace['line'] : '?',
|
||||
];
|
||||
|
||||
if (isset($trace['function']) && $trace['function'] == 'substituteBindings') {
|
||||
$frame->name = 'Route binding';
|
||||
|
||||
return $frame;
|
||||
}
|
||||
|
||||
if (isset($trace['class']) &&
|
||||
isset($trace['file']) &&
|
||||
!$this->fileIsInExcludedPath($trace['file'])
|
||||
) {
|
||||
$file = $trace['file'];
|
||||
|
||||
if (isset($trace['object']) && is_a($trace['object'], 'Twig_Template')) {
|
||||
list($file, $frame->line) = $this->getTwigInfo($trace);
|
||||
} elseif (strpos($file, storage_path()) !== false) {
|
||||
$hash = pathinfo($file, PATHINFO_FILENAME);
|
||||
|
||||
if (! $frame->name = $this->findViewFromHash($hash)) {
|
||||
$frame->name = $hash;
|
||||
}
|
||||
|
||||
$frame->namespace = 'view';
|
||||
|
||||
return $frame;
|
||||
} elseif (strpos($file, 'Middleware') !== false) {
|
||||
$frame->name = $this->findMiddlewareFromFile($file);
|
||||
|
||||
if ($frame->name) {
|
||||
$frame->namespace = 'middleware';
|
||||
} else {
|
||||
$frame->name = $this->normalizeFilename($file);
|
||||
}
|
||||
|
||||
return $frame;
|
||||
}
|
||||
|
||||
$frame->name = $this->normalizeFilename($file);
|
||||
|
||||
return $frame;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given file is to be excluded from analysis
|
||||
*
|
||||
* @param string $file
|
||||
* @return bool
|
||||
*/
|
||||
protected function fileIsInExcludedPath($file)
|
||||
{
|
||||
$excludedPaths = [
|
||||
'/vendor/laravel/framework/src/Illuminate/Database',
|
||||
'/vendor/laravel/framework/src/Illuminate/Events',
|
||||
'/vendor/barryvdh/laravel-debugbar',
|
||||
];
|
||||
|
||||
$normalizedPath = str_replace('\\', '/', $file);
|
||||
|
||||
foreach ($excludedPaths as $excludedPath) {
|
||||
if (strpos($normalizedPath, $excludedPath) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the middleware alias from the file.
|
||||
*
|
||||
* @param string $file
|
||||
* @return string|null
|
||||
*/
|
||||
protected function findMiddlewareFromFile($file)
|
||||
{
|
||||
$filename = pathinfo($file, PATHINFO_FILENAME);
|
||||
|
||||
foreach ($this->middleware as $alias => $class) {
|
||||
if (strpos($class, $filename) !== false) {
|
||||
return $alias;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the template name from the hash.
|
||||
*
|
||||
* @param string $hash
|
||||
* @return null|string
|
||||
*/
|
||||
protected function findViewFromHash($hash)
|
||||
{
|
||||
$finder = app('view')->getFinder();
|
||||
|
||||
if (isset($this->reflection['viewfinderViews'])) {
|
||||
$property = $this->reflection['viewfinderViews'];
|
||||
} else {
|
||||
$reflection = new \ReflectionClass($finder);
|
||||
$property = $reflection->getProperty('views');
|
||||
$property->setAccessible(true);
|
||||
$this->reflection['viewfinderViews'] = $property;
|
||||
}
|
||||
|
||||
foreach ($property->getValue($finder) as $name => $path){
|
||||
if (sha1($path) == $hash || md5($path) == $hash) {
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filename/line from a Twig template trace
|
||||
*
|
||||
* @param array $trace
|
||||
* @return array The file and line
|
||||
*/
|
||||
protected function getTwigInfo($trace)
|
||||
{
|
||||
$file = $trace['object']->getTemplateName();
|
||||
|
||||
if (isset($trace['line'])) {
|
||||
foreach ($trace['object']->getDebugInfo() as $codeLine => $templateLine) {
|
||||
if ($codeLine <= $trace['line']) {
|
||||
return [$file, $templateLine];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [$file, -1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorten the path by removing the relative links and base dir
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
protected function normalizeFilename($path)
|
||||
{
|
||||
if (file_exists($path)) {
|
||||
$path = realpath($path);
|
||||
}
|
||||
return str_replace(base_path(), '', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect a database transaction event.
|
||||
* @param string $event
|
||||
* @param \Illuminate\Database\Connection $connection
|
||||
* @return array
|
||||
*/
|
||||
public function collectTransactionEvent($event, $connection)
|
||||
{
|
||||
$source = [];
|
||||
|
||||
if ($this->findSource) {
|
||||
try {
|
||||
$source = $this->findSource();
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
}
|
||||
|
||||
$this->queries[] = [
|
||||
'query' => $event,
|
||||
'type' => 'transaction',
|
||||
'bindings' => [],
|
||||
'time' => 0,
|
||||
'source' => $source,
|
||||
'explain' => [],
|
||||
'connection' => $connection->getDatabaseName(),
|
||||
'hints' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the queries.
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
$this->queries = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function collect()
|
||||
{
|
||||
$totalTime = 0;
|
||||
$queries = $this->queries;
|
||||
|
||||
$statements = [];
|
||||
foreach ($queries as $query) {
|
||||
$totalTime += $query['time'];
|
||||
|
||||
$statements[] = [
|
||||
'sql' => $this->getDataFormatter()->formatSql($query['query']),
|
||||
'type' => $query['type'],
|
||||
'params' => [],
|
||||
'bindings' => $query['bindings'],
|
||||
'hints' => $query['hints'],
|
||||
'backtrace' => array_values($query['source']),
|
||||
'duration' => $query['time'],
|
||||
'duration_str' => ($query['type'] == 'transaction') ? '' : $this->formatDuration($query['time']),
|
||||
'stmt_id' => $this->getDataFormatter()->formatSource(reset($query['source'])),
|
||||
'connection' => $query['connection'],
|
||||
];
|
||||
|
||||
//Add the results from the explain as new rows
|
||||
foreach($query['explain'] as $explain){
|
||||
$statements[] = [
|
||||
'sql' => ' - EXPLAIN #' . $explain->id . ': `' . $explain->table . '` (' . $explain->select_type . ')',
|
||||
'type' => 'explain',
|
||||
'params' => $explain,
|
||||
'row_count' => $explain->rows,
|
||||
'stmt_id' => $explain->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$nb_statements = array_filter($queries, function ($query) {
|
||||
return $query['type'] == 'query';
|
||||
});
|
||||
|
||||
$data = [
|
||||
'nb_statements' => count($nb_statements),
|
||||
'nb_failed_statements' => 0,
|
||||
'accumulated_duration' => $totalTime,
|
||||
'accumulated_duration_str' => $this->formatDuration($totalTime),
|
||||
'statements' => $statements
|
||||
];
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'queries';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getWidgets()
|
||||
{
|
||||
return [
|
||||
"queries" => [
|
||||
"icon" => "database",
|
||||
"widget" => "PhpDebugBar.Widgets.LaravelSQLQueriesWidget",
|
||||
"map" => "queries",
|
||||
"default" => "[]"
|
||||
],
|
||||
"queries:badge" => [
|
||||
"map" => "queries.nb_statements",
|
||||
"default" => 0
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -31,15 +31,15 @@ class Controller extends BaseController
|
||||
$groups = [''=>'Choose a group'] + $groups;
|
||||
|
||||
$selected_locale = request()->input('locale');
|
||||
if ($selected_locale == 'en') {
|
||||
$selected_locale = '';
|
||||
}
|
||||
// if ($selected_locale == 'en') {
|
||||
// $selected_locale = '';
|
||||
// }
|
||||
if (!$selected_locale) {
|
||||
foreach ($locales as $locale) {
|
||||
if ($locale != 'en') {
|
||||
//if ($locale != 'en') {
|
||||
$selected_locale = $locale;
|
||||
break;
|
||||
}
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ class Controller extends BaseController
|
||||
->with('numTodo', $numTodo)
|
||||
->with('numChanged', $numChanged)
|
||||
->with('editUrl', action('\Barryvdh\TranslationManager\Controller@postEdit', [$group]))
|
||||
->with('deleteEnabled', $this->manager->getConfig('delete_enabled'));
|
||||
->with('deleteEnabled', (bool) $this->manager->getConfig('delete_enabled'));
|
||||
}
|
||||
|
||||
public function getView($group = null)
|
||||
@@ -198,7 +198,8 @@ class Controller extends BaseController
|
||||
|
||||
public function postAddGroup(Request $request)
|
||||
{
|
||||
$group = str_replace(".", '', $request->input('new-group'));
|
||||
//$group = str_replace(".", '', $request->input('new-group'));
|
||||
$group = $this->sanitizeFilename($request->input('new-group'));
|
||||
if ($group)
|
||||
{
|
||||
return redirect()->action('\Barryvdh\TranslationManager\Controller@getView',$group);
|
||||
@@ -212,8 +213,12 @@ class Controller extends BaseController
|
||||
public function postAddLocale(Request $request)
|
||||
{
|
||||
$locales = $this->manager->getLocales();
|
||||
$newLocale = str_replace([], '-', trim($request->input('new-locale')));
|
||||
if (!$newLocale || in_array($newLocale, $locales)) {
|
||||
//$newLocale = str_replace([], '-', trim($request->input('new-locale')));
|
||||
$newLocale = $this->sanitizeFilename($request->input('new-locale'));
|
||||
if (!$newLocale
|
||||
|| in_array($newLocale, $locales)
|
||||
|| !in_array($newLocale, array_keys(\Helper::$locales))
|
||||
) {
|
||||
return redirect()->back();
|
||||
}
|
||||
$this->manager->addLocale($newLocale);
|
||||
@@ -227,4 +232,15 @@ class Controller extends BaseController
|
||||
}
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
protected function sanitizeFilename($input)
|
||||
{
|
||||
$input = \Str::ascii(trim($input));
|
||||
return str_replace(
|
||||
['<', '>', '%', '/', '\\', '.', ' ', '"', "'", '#', '?'],
|
||||
'',
|
||||
$input
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,7 +449,12 @@ class Manager
|
||||
// Modules translations are added here too.
|
||||
foreach ($groupKeys as $key) {
|
||||
// Split the group and item
|
||||
list($group, $item) = explode('.', $key, 2);
|
||||
$parts = explode('.', $key, 2);
|
||||
if (count($parts) < 2) {
|
||||
\Log::warning("Translation key without group: $key");
|
||||
continue;
|
||||
}
|
||||
list($group, $item) = $parts;
|
||||
$this->missingKey('', $group, $item);
|
||||
}
|
||||
|
||||
@@ -578,6 +583,14 @@ class Manager
|
||||
$translations = $groups[$group];
|
||||
// Sort translations alphabetically.
|
||||
ksort($translations);
|
||||
|
||||
// Strips some tags to avoid XSS when translations are inserted via {!! ... !!}.
|
||||
foreach ($translations as $key => $value) {
|
||||
$value = \Helper::stripDangerousTags($value);
|
||||
|
||||
$translations[$key] = $value;
|
||||
}
|
||||
|
||||
$output = json_encode($translations, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE);
|
||||
$this->files->put($path, $output);
|
||||
|
||||
@@ -692,9 +705,10 @@ class Manager
|
||||
|
||||
public function removeLocale($locale)
|
||||
{
|
||||
if (!$locale) {
|
||||
if (!$locale || !in_array($locale, array_keys(\Helper::$locales))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->ignoreLocales = array_merge($this->ignoreLocales, [$locale]);
|
||||
// Only delete from DB.
|
||||
//$this->saveIgnoredLocales();
|
||||
|
||||
@@ -53,7 +53,7 @@ class Zipper
|
||||
*
|
||||
* @param Filesystem $fs
|
||||
*/
|
||||
public function __construct(Filesystem $fs = null)
|
||||
public function __construct(?Filesystem $fs = null)
|
||||
{
|
||||
$this->file = $fs ? $fs : new Filesystem();
|
||||
}
|
||||
@@ -300,9 +300,15 @@ class GithubRepositoryType extends AbstractRepositoryType implements SourceRepos
|
||||
throw new \Exception('No repository specified. Please enter a valid Github repository owner and name in your config.');
|
||||
}
|
||||
|
||||
if (!empty($this->config['github_api_url'])) {
|
||||
$github_api_url = $this->config['github_api_url'];
|
||||
} else {
|
||||
$github_api_url = self::GITHUB_API_URL.'/repos/'.$this->config['repository_vendor'].'/'.$this->config['repository_name'];
|
||||
}
|
||||
|
||||
return $this->client->request(
|
||||
'GET',
|
||||
self::GITHUB_API_URL.'/repos/'.$this->config['repository_vendor'].'/'.$this->config['repository_name'].'/tags', [
|
||||
$github_api_url.'/tags', [
|
||||
'timeout' => config('app.curl_timeout'),
|
||||
'connect_timeout' => config('app.curl_connect_timeout'),
|
||||
'proxy' => config('app.proxy'),
|
||||
|
||||
@@ -58,7 +58,7 @@ abstract class BaseProvider implements Countable
|
||||
/**
|
||||
* @param null $publicPath
|
||||
*/
|
||||
public function __construct($publicPath = null, $config = null, Filesystem $file = null)
|
||||
public function __construct($publicPath = null, $config = null, ?Filesystem $file = null)
|
||||
{
|
||||
$this->file = $file ?: new Filesystem;
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ if (PHP_VERSION_ID >= 80000) {
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function setFetchMode($mode, ...$args)
|
||||
{
|
||||
return $this->doSetFetchMode($mode, ...$args);
|
||||
|
||||
@@ -138,7 +138,7 @@ class HTMLPurifier_Encoder
|
||||
// need to do PHP stuff. 99% of the time, this will be the case.
|
||||
if (preg_match(
|
||||
'/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du',
|
||||
$str
|
||||
$str ?? ''
|
||||
)) {
|
||||
return $str;
|
||||
}
|
||||
@@ -78,7 +78,7 @@ class HTMLPurifier_Length
|
||||
if ($this->n === '0' && $this->unit === false) {
|
||||
return true;
|
||||
}
|
||||
if (!ctype_lower($this->unit)) {
|
||||
if ($this->unit === false || !ctype_lower((string)$this->unit)) {
|
||||
$this->unit = strtolower($this->unit);
|
||||
}
|
||||
if (!isset(HTMLPurifier_Length::$allowedUnits[$this->unit])) {
|
||||
@@ -272,7 +272,11 @@ class HTMLPurifier_Lexer
|
||||
*/
|
||||
protected static function removeIEConditional($string)
|
||||
{
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3894
|
||||
return preg_replace(
|
||||
// This change caused: https://github.com/freescout-helpdesk/freescout/issues/3907#issuecomment-2043126228
|
||||
// '#<!--\[if [^>]+\]>(.*?)<!\[endif\]-->#si', // probably should generalize for all strings
|
||||
// '$1',
|
||||
'#<!--\[if [^>]+\]>.*?<!\[endif\]-->#si', // probably should generalize for all strings
|
||||
'',
|
||||
$string
|
||||
|
||||
@@ -54,7 +54,7 @@ class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer
|
||||
if ($config->get('Core.AggressivelyFixLt')) {
|
||||
$char = '[^a-z!\/]';
|
||||
$comment = "/<!--(.*?)(-->|\z)/is";
|
||||
$html = preg_replace_callback($comment, array($this, 'callbackArmorCommentEntities'), $html);
|
||||
$html = preg_replace_callback($comment, array($this, 'callbackArmorCommentEntities'), $html ?? '');
|
||||
do {
|
||||
$old = $html;
|
||||
$html = preg_replace("/<($char)/i", '<\\1', $html);
|
||||
835
overrides/filp/whoops/src/Whoops/Handler/PrettyPageHandler.php
Normal file
835
overrides/filp/whoops/src/Whoops/Handler/PrettyPageHandler.php
Normal file
@@ -0,0 +1,835 @@
|
||||
<?php
|
||||
/**
|
||||
* Whoops - php errors for cool kids
|
||||
* @author Filipe Dobreira <http://github.com/filp>
|
||||
*/
|
||||
|
||||
namespace Whoops\Handler;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\VarDumper\Cloner\AbstractCloner;
|
||||
use Symfony\Component\VarDumper\Cloner\VarCloner;
|
||||
use UnexpectedValueException;
|
||||
use Whoops\Exception\Formatter;
|
||||
use Whoops\Util\Misc;
|
||||
use Whoops\Util\TemplateHelper;
|
||||
|
||||
class PrettyPageHandler extends Handler
|
||||
{
|
||||
const EDITOR_SUBLIME = "sublime";
|
||||
const EDITOR_TEXTMATE = "textmate";
|
||||
const EDITOR_EMACS = "emacs";
|
||||
const EDITOR_MACVIM = "macvim";
|
||||
const EDITOR_PHPSTORM = "phpstorm";
|
||||
const EDITOR_IDEA = "idea";
|
||||
const EDITOR_VSCODE = "vscode";
|
||||
const EDITOR_ATOM = "atom";
|
||||
const EDITOR_ESPRESSO = "espresso";
|
||||
const EDITOR_XDEBUG = "xdebug";
|
||||
const EDITOR_NETBEANS = "netbeans";
|
||||
|
||||
/**
|
||||
* Search paths to be scanned for resources.
|
||||
*
|
||||
* Stored in the reverse order they're declared.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $searchPaths = [];
|
||||
|
||||
/**
|
||||
* Fast lookup cache for known resource locations.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $resourceCache = [];
|
||||
|
||||
/**
|
||||
* The name of the custom css file.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $customCss = null;
|
||||
|
||||
/**
|
||||
* The name of the custom js file.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $customJs = null;
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
*/
|
||||
private $extraTables = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $handleUnconditionally = false;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $pageTitle = "Whoops! There was an error.";
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
*/
|
||||
private $applicationPaths;
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
*/
|
||||
private $blacklist = [
|
||||
'_GET' => [],
|
||||
'_POST' => [],
|
||||
'_FILES' => [],
|
||||
'_COOKIE' => [],
|
||||
'_SESSION' => [],
|
||||
'_SERVER' => [],
|
||||
'_ENV' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* An identifier for a known IDE/text editor.
|
||||
*
|
||||
* Either a string, or a calalble that resolves a string, that can be used
|
||||
* to open a given file in an editor. If the string contains the special
|
||||
* substrings %file or %line, they will be replaced with the correct data.
|
||||
*
|
||||
* @example
|
||||
* "txmt://open?url=%file&line=%line"
|
||||
*
|
||||
* @var callable|string $editor
|
||||
*/
|
||||
protected $editor;
|
||||
|
||||
/**
|
||||
* A list of known editor strings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $editors = [
|
||||
"sublime" => "subl://open?url=file://%file&line=%line",
|
||||
"textmate" => "txmt://open?url=file://%file&line=%line",
|
||||
"emacs" => "emacs://open?url=file://%file&line=%line",
|
||||
"macvim" => "mvim://open/?url=file://%file&line=%line",
|
||||
"phpstorm" => "phpstorm://open?file=%file&line=%line",
|
||||
"idea" => "idea://open?file=%file&line=%line",
|
||||
"vscode" => "vscode://file/%file:%line",
|
||||
"atom" => "atom://core/open/file?filename=%file&line=%line",
|
||||
"espresso" => "x-espresso://open?filepath=%file&lines=%line",
|
||||
"netbeans" => "netbeans://open/?f=%file:%line",
|
||||
];
|
||||
|
||||
/**
|
||||
* @var TemplateHelper
|
||||
*/
|
||||
protected $templateHelper;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
if (ini_get('xdebug.file_link_format') || get_cfg_var('xdebug.file_link_format')) {
|
||||
// Register editor using xdebug's file_link_format option.
|
||||
$this->editors['xdebug'] = function ($file, $line) {
|
||||
return str_replace(['%f', '%l'], [$file, $line], ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'));
|
||||
};
|
||||
|
||||
// If xdebug is available, use it as default editor.
|
||||
$this->setEditor('xdebug');
|
||||
}
|
||||
|
||||
// Add the default, local resource search path:
|
||||
//$this->searchPaths[] = __DIR__ . "/../Resources";
|
||||
$this->searchPaths[] = __DIR__ . "/../../../../../../vendor/filp/whoops/src/Whoops/Resources";
|
||||
|
||||
// blacklist php provided auth based values
|
||||
$this->blacklist('_SERVER', 'PHP_AUTH_PW');
|
||||
|
||||
$this->templateHelper = new TemplateHelper();
|
||||
|
||||
if (class_exists('Symfony\Component\VarDumper\Cloner\VarCloner')) {
|
||||
$cloner = new VarCloner();
|
||||
// Only dump object internals if a custom caster exists for performance reasons
|
||||
// https://github.com/filp/whoops/pull/404
|
||||
$cloner->addCasters(['*' => function ($obj, $a, $stub, $isNested, $filter = 0) {
|
||||
$class = $stub->class;
|
||||
$classes = [$class => $class] + class_parents($obj) + class_implements($obj);
|
||||
|
||||
foreach ($classes as $class) {
|
||||
if (isset(AbstractCloner::$defaultCasters[$class])) {
|
||||
return $a;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all internals
|
||||
return [];
|
||||
}]);
|
||||
$this->templateHelper->setCloner($cloner);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (!$this->handleUnconditionally()) {
|
||||
// Check conditions for outputting HTML:
|
||||
// @todo: Make this more robust
|
||||
if (PHP_SAPI === 'cli') {
|
||||
// Help users who have been relying on an internal test value
|
||||
// fix their code to the proper method
|
||||
if (isset($_ENV['whoops-test'])) {
|
||||
throw new \Exception(
|
||||
'Use handleUnconditionally instead of whoops-test'
|
||||
.' environment variable'
|
||||
);
|
||||
}
|
||||
|
||||
return Handler::DONE;
|
||||
}
|
||||
}
|
||||
|
||||
$templateFile = $this->getResource("views/layout.html.php");
|
||||
$cssFile = $this->getResource("css/whoops.base.css");
|
||||
$zeptoFile = $this->getResource("js/zepto.min.js");
|
||||
$prismJs = $this->getResource("js/prism.js");
|
||||
$prismCss = $this->getResource("css/prism.css");
|
||||
$clipboard = $this->getResource("js/clipboard.min.js");
|
||||
$jsFile = $this->getResource("js/whoops.base.js");
|
||||
|
||||
if ($this->customCss) {
|
||||
$customCssFile = $this->getResource($this->customCss);
|
||||
}
|
||||
|
||||
if ($this->customJs) {
|
||||
$customJsFile = $this->getResource($this->customJs);
|
||||
}
|
||||
|
||||
$inspector = $this->getInspector();
|
||||
$frames = $this->getExceptionFrames();
|
||||
$code = $this->getExceptionCode();
|
||||
|
||||
// List of variables that will be passed to the layout template.
|
||||
$vars = [
|
||||
"page_title" => $this->getPageTitle(),
|
||||
|
||||
// @todo: Asset compiler
|
||||
"stylesheet" => file_get_contents($cssFile),
|
||||
"zepto" => file_get_contents($zeptoFile),
|
||||
"prismJs" => file_get_contents($prismJs),
|
||||
"prismCss" => file_get_contents($prismCss),
|
||||
"clipboard" => file_get_contents($clipboard),
|
||||
"javascript" => file_get_contents($jsFile),
|
||||
|
||||
// Template paths:
|
||||
"header" => $this->getResource("views/header.html.php"),
|
||||
"header_outer" => $this->getResource("views/header_outer.html.php"),
|
||||
"frame_list" => $this->getResource("views/frame_list.html.php"),
|
||||
"frames_description" => $this->getResource("views/frames_description.html.php"),
|
||||
"frames_container" => $this->getResource("views/frames_container.html.php"),
|
||||
"panel_details" => $this->getResource("views/panel_details.html.php"),
|
||||
"panel_details_outer" => $this->getResource("views/panel_details_outer.html.php"),
|
||||
"panel_left" => $this->getResource("views/panel_left.html.php"),
|
||||
"panel_left_outer" => $this->getResource("views/panel_left_outer.html.php"),
|
||||
"frame_code" => $this->getResource("views/frame_code.html.php"),
|
||||
"env_details" => $this->getResource("views/env_details.html.php"),
|
||||
|
||||
"title" => $this->getPageTitle(),
|
||||
"name" => explode("\\", $inspector->getExceptionName()),
|
||||
"message" => $inspector->getExceptionMessage(),
|
||||
"previousMessages" => $inspector->getPreviousExceptionMessages(),
|
||||
"docref_url" => $inspector->getExceptionDocrefUrl(),
|
||||
"code" => $code,
|
||||
"previousCodes" => $inspector->getPreviousExceptionCodes(),
|
||||
"plain_exception" => Formatter::formatExceptionPlain($inspector),
|
||||
"frames" => $frames,
|
||||
"has_frames" => !!count($frames),
|
||||
"handler" => $this,
|
||||
"handlers" => $this->getRun()->getHandlers(),
|
||||
|
||||
"active_frames_tab" => count($frames) && $frames->offsetGet(0)->isApplication() ? 'application' : 'all',
|
||||
"has_frames_tabs" => $this->getApplicationPaths(),
|
||||
|
||||
"tables" => [
|
||||
"GET Data" => $this->masked($_GET, '_GET'),
|
||||
"POST Data" => $this->masked($_POST, '_POST'),
|
||||
"Files" => isset($_FILES) ? $this->masked($_FILES, '_FILES') : [],
|
||||
"Cookies" => $this->masked($_COOKIE, '_COOKIE'),
|
||||
"Session" => isset($_SESSION) ? $this->masked($_SESSION, '_SESSION') : [],
|
||||
"Server/Request Data" => $this->masked($_SERVER, '_SERVER'),
|
||||
"Environment Variables" => $this->masked($_ENV, '_ENV'),
|
||||
],
|
||||
];
|
||||
|
||||
if (isset($customCssFile)) {
|
||||
$vars["stylesheet"] .= file_get_contents($customCssFile);
|
||||
}
|
||||
|
||||
if (isset($customJsFile)) {
|
||||
$vars["javascript"] .= file_get_contents($customJsFile);
|
||||
}
|
||||
|
||||
// Add extra entries list of data tables:
|
||||
// @todo: Consolidate addDataTable and addDataTableCallback
|
||||
$extraTables = array_map(function ($table) use ($inspector) {
|
||||
return $table instanceof \Closure ? $table($inspector) : $table;
|
||||
}, $this->getDataTables());
|
||||
$vars["tables"] = array_merge($extraTables, $vars["tables"]);
|
||||
|
||||
$plainTextHandler = new PlainTextHandler();
|
||||
$plainTextHandler->setException($this->getException());
|
||||
$plainTextHandler->setInspector($this->getInspector());
|
||||
$vars["preface"] = "<!--\n\n\n" . $this->templateHelper->escape($plainTextHandler->generateResponse()) . "\n\n\n\n\n\n\n\n\n\n\n-->";
|
||||
|
||||
$this->templateHelper->setVariables($vars);
|
||||
$this->templateHelper->render($templateFile);
|
||||
|
||||
return Handler::QUIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stack trace frames of the exception currently being handled.
|
||||
*
|
||||
* @return \Whoops\Exception\FrameCollection
|
||||
*/
|
||||
protected function getExceptionFrames()
|
||||
{
|
||||
$frames = $this->getInspector()->getFrames();
|
||||
|
||||
if ($this->getApplicationPaths()) {
|
||||
foreach ($frames as $frame) {
|
||||
foreach ($this->getApplicationPaths() as $path) {
|
||||
if (strpos($frame->getFile(), $path) === 0) {
|
||||
$frame->setApplication(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $frames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the code of the exception currently being handled.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getExceptionCode()
|
||||
{
|
||||
$exception = $this->getException();
|
||||
|
||||
$code = $exception->getCode();
|
||||
if ($exception instanceof \ErrorException) {
|
||||
// ErrorExceptions wrap the php-error types within the 'severity' property
|
||||
$code = Misc::translateErrorCode($exception->getSeverity());
|
||||
}
|
||||
|
||||
return (string) $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function contentType()
|
||||
{
|
||||
return 'text/html';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an entry to the list of tables displayed in the template.
|
||||
*
|
||||
* The expected data is a simple associative array. Any nested arrays
|
||||
* will be flattened with `print_r`.
|
||||
*
|
||||
* @param string $label
|
||||
* @param array $data
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function addDataTable($label, array $data)
|
||||
{
|
||||
$this->extraTables[$label] = $data;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily adds an entry to the list of tables displayed in the table.
|
||||
*
|
||||
* The supplied callback argument will be called when the error is
|
||||
* rendered, it should produce a simple associative array. Any nested
|
||||
* arrays will be flattened with `print_r`.
|
||||
*
|
||||
* @param string $label
|
||||
* @param callable $callback Callable returning an associative array
|
||||
*
|
||||
* @throws InvalidArgumentException If $callback is not callable
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function addDataTableCallback($label, /* callable */ $callback)
|
||||
{
|
||||
if (!is_callable($callback)) {
|
||||
throw new InvalidArgumentException('Expecting callback argument to be callable');
|
||||
}
|
||||
|
||||
$this->extraTables[$label] = function (?\Whoops\Exception\Inspector $inspector = null) use ($callback) {
|
||||
try {
|
||||
$result = call_user_func($callback, $inspector);
|
||||
|
||||
// Only return the result if it can be iterated over by foreach().
|
||||
return is_array($result) || $result instanceof \Traversable ? $result : [];
|
||||
} catch (\Exception $e) {
|
||||
// Don't allow failure to break the rendering of the original exception.
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the extra data tables registered with this handler.
|
||||
*
|
||||
* Optionally accepts a 'label' parameter, to only return the data table
|
||||
* under that label.
|
||||
*
|
||||
* @param string|null $label
|
||||
*
|
||||
* @return array[]|callable
|
||||
*/
|
||||
public function getDataTables($label = null)
|
||||
{
|
||||
if ($label !== null) {
|
||||
return isset($this->extraTables[$label]) ?
|
||||
$this->extraTables[$label] : [];
|
||||
}
|
||||
|
||||
return $this->extraTables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to handle unconditionally.
|
||||
*
|
||||
* Allows to disable all attempts to dynamically decide whether to handle
|
||||
* or return prematurely. Set this to ensure that the handler will perform,
|
||||
* no matter what.
|
||||
*
|
||||
* @param bool|null $value
|
||||
*
|
||||
* @return bool|static
|
||||
*/
|
||||
public function handleUnconditionally($value = null)
|
||||
{
|
||||
if (func_num_args() == 0) {
|
||||
return $this->handleUnconditionally;
|
||||
}
|
||||
|
||||
$this->handleUnconditionally = (bool) $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an editor resolver.
|
||||
*
|
||||
* Either a string, or a closure that resolves a string, that can be used
|
||||
* to open a given file in an editor. If the string contains the special
|
||||
* substrings %file or %line, they will be replaced with the correct data.
|
||||
*
|
||||
* @example
|
||||
* $run->addEditor('macvim', "mvim://open?url=file://%file&line=%line")
|
||||
* @example
|
||||
* $run->addEditor('remove-it', function($file, $line) {
|
||||
* unlink($file);
|
||||
* return "http://stackoverflow.com";
|
||||
* });
|
||||
*
|
||||
* @param string $identifier
|
||||
* @param string|callable $resolver
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function addEditor($identifier, $resolver)
|
||||
{
|
||||
$this->editors[$identifier] = $resolver;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the editor to use to open referenced files.
|
||||
*
|
||||
* Pass either the name of a configured editor, or a closure that directly
|
||||
* resolves an editor string.
|
||||
*
|
||||
* @example
|
||||
* $run->setEditor(function($file, $line) { return "file:///{$file}"; });
|
||||
* @example
|
||||
* $run->setEditor('sublime');
|
||||
*
|
||||
* @param string|callable $editor
|
||||
*
|
||||
* @throws InvalidArgumentException If invalid argument identifier provided
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function setEditor($editor)
|
||||
{
|
||||
if (!is_callable($editor) && !isset($this->editors[$editor])) {
|
||||
throw new InvalidArgumentException(
|
||||
"Unknown editor identifier: $editor. Known editors:" .
|
||||
implode(",", array_keys($this->editors))
|
||||
);
|
||||
}
|
||||
|
||||
$this->editor = $editor;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the editor href for a given file and line, if available.
|
||||
*
|
||||
* @param string $filePath
|
||||
* @param int $line
|
||||
*
|
||||
* @throws InvalidArgumentException If editor resolver does not return a string
|
||||
*
|
||||
* @return string|bool
|
||||
*/
|
||||
public function getEditorHref($filePath, $line)
|
||||
{
|
||||
$editor = $this->getEditor($filePath, $line);
|
||||
|
||||
if (empty($editor)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that the editor is a string, and replace the
|
||||
// %line and %file placeholders:
|
||||
if (!isset($editor['url']) || !is_string($editor['url'])) {
|
||||
throw new UnexpectedValueException(
|
||||
__METHOD__ . " should always resolve to a string or a valid editor array; got something else instead."
|
||||
);
|
||||
}
|
||||
|
||||
$editor['url'] = str_replace("%line", rawurlencode($line), $editor['url']);
|
||||
$editor['url'] = str_replace("%file", rawurlencode($filePath), $editor['url']);
|
||||
|
||||
return $editor['url'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the editor link should act as an Ajax request.
|
||||
*
|
||||
* @param string $filePath
|
||||
* @param int $line
|
||||
*
|
||||
* @throws UnexpectedValueException If editor resolver does not return a boolean
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getEditorAjax($filePath, $line)
|
||||
{
|
||||
$editor = $this->getEditor($filePath, $line);
|
||||
|
||||
// Check that the ajax is a bool
|
||||
if (!isset($editor['ajax']) || !is_bool($editor['ajax'])) {
|
||||
throw new UnexpectedValueException(
|
||||
__METHOD__ . " should always resolve to a bool; got something else instead."
|
||||
);
|
||||
}
|
||||
return $editor['ajax'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines both the editor and if ajax should be used.
|
||||
*
|
||||
* @param string $filePath
|
||||
* @param int $line
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getEditor($filePath, $line)
|
||||
{
|
||||
if (!$this->editor || (!is_string($this->editor) && !is_callable($this->editor))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_string($this->editor) && isset($this->editors[$this->editor]) && !is_callable($this->editors[$this->editor])) {
|
||||
return [
|
||||
'ajax' => false,
|
||||
'url' => $this->editors[$this->editor],
|
||||
];
|
||||
}
|
||||
|
||||
if (is_callable($this->editor) || (isset($this->editors[$this->editor]) && is_callable($this->editors[$this->editor]))) {
|
||||
if (is_callable($this->editor)) {
|
||||
$callback = call_user_func($this->editor, $filePath, $line);
|
||||
} else {
|
||||
$callback = call_user_func($this->editors[$this->editor], $filePath, $line);
|
||||
}
|
||||
|
||||
if (empty($callback)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_string($callback)) {
|
||||
return [
|
||||
'ajax' => false,
|
||||
'url' => $callback,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'ajax' => isset($callback['ajax']) ? $callback['ajax'] : false,
|
||||
'url' => isset($callback['url']) ? $callback['url'] : $callback,
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the page title.
|
||||
*
|
||||
* @param string $title
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function setPageTitle($title)
|
||||
{
|
||||
$this->pageTitle = (string) $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the page title.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPageTitle()
|
||||
{
|
||||
return $this->pageTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a path to the list of paths to be searched for resources.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @throws InvalidArgumentException If $path is not a valid directory
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function addResourcePath($path)
|
||||
{
|
||||
if (!is_dir($path)) {
|
||||
throw new InvalidArgumentException(
|
||||
"'$path' is not a valid directory"
|
||||
);
|
||||
}
|
||||
|
||||
array_unshift($this->searchPaths, $path);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom css file to be loaded.
|
||||
*
|
||||
* @param string|null $name
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function addCustomCss($name)
|
||||
{
|
||||
$this->customCss = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom js file to be loaded.
|
||||
*
|
||||
* @param string|null $name
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function addCustomJs($name)
|
||||
{
|
||||
$this->customJs = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getResourcePaths()
|
||||
{
|
||||
return $this->searchPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a resource, by its relative path, in all available search paths.
|
||||
*
|
||||
* The search is performed starting at the last search path, and all the
|
||||
* way back to the first, enabling a cascading-type system of overrides for
|
||||
* all resources.
|
||||
*
|
||||
* @param string $resource
|
||||
*
|
||||
* @throws RuntimeException If resource cannot be found in any of the available paths
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getResource($resource)
|
||||
{
|
||||
// If the resource was found before, we can speed things up
|
||||
// by caching its absolute, resolved path:
|
||||
if (isset($this->resourceCache[$resource])) {
|
||||
return $this->resourceCache[$resource];
|
||||
}
|
||||
|
||||
// Search through available search paths, until we find the
|
||||
// resource we're after:
|
||||
foreach ($this->searchPaths as $path) {
|
||||
$fullPath = $path . "/$resource";
|
||||
|
||||
if (is_file($fullPath)) {
|
||||
// Cache the result:
|
||||
$this->resourceCache[$resource] = $fullPath;
|
||||
return $fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
// If we got this far, nothing was found.
|
||||
throw new RuntimeException(
|
||||
"Could not find resource '$resource' in any resource paths."
|
||||
. "(searched: " . join(", ", $this->searchPaths). ")"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getResourcesPath()
|
||||
{
|
||||
$allPaths = $this->getResourcePaths();
|
||||
|
||||
// Compat: return only the first path added
|
||||
return end($allPaths) ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
* @param string $resourcesPath
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function setResourcesPath($resourcesPath)
|
||||
{
|
||||
$this->addResourcePath($resourcesPath);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the application paths.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getApplicationPaths()
|
||||
{
|
||||
return $this->applicationPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the application paths.
|
||||
*
|
||||
* @param array $applicationPaths
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setApplicationPaths($applicationPaths)
|
||||
{
|
||||
$this->applicationPaths = $applicationPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the application root path.
|
||||
*
|
||||
* @param string $applicationRootPath
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setApplicationRootPath($applicationRootPath)
|
||||
{
|
||||
$this->templateHelper->setApplicationRootPath($applicationRootPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* blacklist a sensitive value within one of the superglobal arrays.
|
||||
* Alias for the hideSuperglobalKey method.
|
||||
*
|
||||
* @param string $superGlobalName The name of the superglobal array, e.g. '_GET'
|
||||
* @param string $key The key within the superglobal
|
||||
* @see hideSuperglobalKey
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function blacklist($superGlobalName, $key)
|
||||
{
|
||||
$this->blacklist[$superGlobalName][] = $key;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide a sensitive value within one of the superglobal arrays.
|
||||
*
|
||||
* @param string $superGlobalName The name of the superglobal array, e.g. '_GET'
|
||||
* @param string $key The key within the superglobal
|
||||
* @return static
|
||||
*/
|
||||
public function hideSuperglobalKey($superGlobalName, $key)
|
||||
{
|
||||
return $this->blacklist($superGlobalName, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks all values within the given superGlobal array.
|
||||
*
|
||||
* Blacklisted values will be replaced by a equal length string containing
|
||||
* only '*' characters for string values.
|
||||
* Non-string values will be replaced with a fixed asterisk count.
|
||||
* We intentionally dont rely on $GLOBALS as it depends on the 'auto_globals_jit' php.ini setting.
|
||||
*
|
||||
* @param array|\ArrayAccess $superGlobal One of the superglobal arrays
|
||||
* @param string $superGlobalName The name of the superglobal array, e.g. '_GET'
|
||||
*
|
||||
* @return array $values without sensitive data
|
||||
*/
|
||||
private function masked($superGlobal, $superGlobalName)
|
||||
{
|
||||
$blacklisted = $this->blacklist[$superGlobalName];
|
||||
|
||||
$values = $superGlobal;
|
||||
|
||||
foreach ($blacklisted as $key) {
|
||||
if (isset($superGlobal[$key])) {
|
||||
$values[$key] = str_repeat('*', is_string($superGlobal[$key]) ? strlen($superGlobal[$key]) : 3);
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
}
|
||||
545
overrides/filp/whoops/src/Whoops/Run.php
Normal file
545
overrides/filp/whoops/src/Whoops/Run.php
Normal file
@@ -0,0 +1,545 @@
|
||||
<?php
|
||||
/**
|
||||
* Whoops - php errors for cool kids
|
||||
* @author Filipe Dobreira <http://github.com/filp>
|
||||
*/
|
||||
|
||||
namespace Whoops;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Throwable;
|
||||
use Whoops\Exception\ErrorException;
|
||||
use Whoops\Exception\Inspector;
|
||||
use Whoops\Handler\CallbackHandler;
|
||||
use Whoops\Handler\Handler;
|
||||
use Whoops\Handler\HandlerInterface;
|
||||
use Whoops\Util\Misc;
|
||||
use Whoops\Util\SystemFacade;
|
||||
|
||||
final class Run implements RunInterface
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isRegistered;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $allowQuit = true;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $sendOutput = true;
|
||||
|
||||
/**
|
||||
* @var integer|false
|
||||
*/
|
||||
private $sendHttpCode = 500;
|
||||
|
||||
/**
|
||||
* @var integer|false
|
||||
*/
|
||||
private $sendExitCode = 1;
|
||||
|
||||
/**
|
||||
* @var HandlerInterface[]
|
||||
*/
|
||||
private $handlerStack = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
* @psalm-var list<array{patterns: string, levels: int}>
|
||||
*/
|
||||
private $silencedPatterns = [];
|
||||
|
||||
/**
|
||||
* @var SystemFacade
|
||||
*/
|
||||
private $system;
|
||||
|
||||
/**
|
||||
* In certain scenarios, like in shutdown handler, we can not throw exceptions.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $canThrowExceptions = true;
|
||||
|
||||
public function __construct(?SystemFacade $system = null)
|
||||
{
|
||||
$this->system = $system ?: new SystemFacade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly request your handler runs as the last of all currently registered handlers.
|
||||
*
|
||||
* @param callable|HandlerInterface $handler
|
||||
*
|
||||
* @return Run
|
||||
*/
|
||||
public function appendHandler($handler)
|
||||
{
|
||||
array_unshift($this->handlerStack, $this->resolveHandler($handler));
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly request your handler runs as the first of all currently registered handlers.
|
||||
*
|
||||
* @param callable|HandlerInterface $handler
|
||||
*
|
||||
* @return Run
|
||||
*/
|
||||
public function prependHandler($handler)
|
||||
{
|
||||
return $this->pushHandler($handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register your handler as the last of all currently registered handlers (to be executed first).
|
||||
* Prefer using appendHandler and prependHandler for clarity.
|
||||
*
|
||||
* @param callable|HandlerInterface $handler
|
||||
*
|
||||
* @return Run
|
||||
*
|
||||
* @throws InvalidArgumentException If argument is not callable or instance of HandlerInterface.
|
||||
*/
|
||||
public function pushHandler($handler)
|
||||
{
|
||||
$this->handlerStack[] = $this->resolveHandler($handler);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the last handler pushed to the handler stack.
|
||||
*
|
||||
* @see Run::removeFirstHandler(), Run::removeLastHandler()
|
||||
*
|
||||
* @return HandlerInterface|null
|
||||
*/
|
||||
public function popHandler()
|
||||
{
|
||||
return array_pop($this->handlerStack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the first handler.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function removeFirstHandler()
|
||||
{
|
||||
array_pop($this->handlerStack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the last handler.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function removeLastHandler()
|
||||
{
|
||||
array_shift($this->handlerStack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with all handlers, in the order they were added to the stack.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getHandlers()
|
||||
{
|
||||
return $this->handlerStack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all handlers in the handlerStack, including the default PrettyPage handler.
|
||||
*
|
||||
* @return Run
|
||||
*/
|
||||
public function clearHandlers()
|
||||
{
|
||||
$this->handlerStack = [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers this instance as an error handler.
|
||||
*
|
||||
* @return Run
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
if (!$this->isRegistered) {
|
||||
// Workaround PHP bug 42098
|
||||
// https://bugs.php.net/bug.php?id=42098
|
||||
class_exists("\\Whoops\\Exception\\ErrorException");
|
||||
class_exists("\\Whoops\\Exception\\FrameCollection");
|
||||
class_exists("\\Whoops\\Exception\\Frame");
|
||||
class_exists("\\Whoops\\Exception\\Inspector");
|
||||
|
||||
$this->system->setErrorHandler([$this, self::ERROR_HANDLER]);
|
||||
$this->system->setExceptionHandler([$this, self::EXCEPTION_HANDLER]);
|
||||
$this->system->registerShutdownFunction([$this, self::SHUTDOWN_HANDLER]);
|
||||
|
||||
$this->isRegistered = true;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters all handlers registered by this Whoops\Run instance.
|
||||
*
|
||||
* @return Run
|
||||
*/
|
||||
public function unregister()
|
||||
{
|
||||
if ($this->isRegistered) {
|
||||
$this->system->restoreExceptionHandler();
|
||||
$this->system->restoreErrorHandler();
|
||||
|
||||
$this->isRegistered = false;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should Whoops allow Handlers to force the script to quit?
|
||||
*
|
||||
* @param bool|int $exit
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function allowQuit($exit = null)
|
||||
{
|
||||
if (func_num_args() == 0) {
|
||||
return $this->allowQuit;
|
||||
}
|
||||
|
||||
return $this->allowQuit = (bool) $exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Silence particular errors in particular files.
|
||||
*
|
||||
* @param array|string $patterns List or a single regex pattern to match.
|
||||
* @param int $levels Defaults to E_STRICT | E_DEPRECATED.
|
||||
*
|
||||
* @return Run
|
||||
*/
|
||||
public function silenceErrorsInPaths($patterns, $levels = 10240)
|
||||
{
|
||||
$this->silencedPatterns = array_merge(
|
||||
$this->silencedPatterns,
|
||||
array_map(
|
||||
function ($pattern) use ($levels) {
|
||||
return [
|
||||
"pattern" => $pattern,
|
||||
"levels" => $levels,
|
||||
];
|
||||
},
|
||||
(array) $patterns
|
||||
)
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with silent errors in path configuration.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSilenceErrorsInPaths()
|
||||
{
|
||||
return $this->silencedPatterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should Whoops send HTTP error code to the browser if possible?
|
||||
* Whoops will by default send HTTP code 500, but you may wish to
|
||||
* use 502, 503, or another 5xx family code.
|
||||
*
|
||||
* @param bool|int $code
|
||||
*
|
||||
* @return int|false
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function sendHttpCode($code = null)
|
||||
{
|
||||
if (func_num_args() == 0) {
|
||||
return $this->sendHttpCode;
|
||||
}
|
||||
|
||||
if (!$code) {
|
||||
return $this->sendHttpCode = false;
|
||||
}
|
||||
|
||||
if ($code === true) {
|
||||
$code = 500;
|
||||
}
|
||||
|
||||
if ($code < 400 || 600 <= $code) {
|
||||
throw new InvalidArgumentException(
|
||||
"Invalid status code '$code', must be 4xx or 5xx"
|
||||
);
|
||||
}
|
||||
|
||||
return $this->sendHttpCode = $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should Whoops exit with a specific code on the CLI if possible?
|
||||
* Whoops will exit with 1 by default, but you can specify something else.
|
||||
*
|
||||
* @param int $code
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function sendExitCode($code = null)
|
||||
{
|
||||
if (func_num_args() == 0) {
|
||||
return $this->sendExitCode;
|
||||
}
|
||||
|
||||
if ($code < 0 || 255 <= $code) {
|
||||
throw new InvalidArgumentException(
|
||||
"Invalid status code '$code', must be between 0 and 254"
|
||||
);
|
||||
}
|
||||
|
||||
return $this->sendExitCode = (int) $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should Whoops push output directly to the client?
|
||||
* If this is false, output will be returned by handleException.
|
||||
*
|
||||
* @param bool|int $send
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function writeToOutput($send = null)
|
||||
{
|
||||
if (func_num_args() == 0) {
|
||||
return $this->sendOutput;
|
||||
}
|
||||
|
||||
return $this->sendOutput = (bool) $send;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an exception, ultimately generating a Whoops error page.
|
||||
*
|
||||
* @param Throwable $exception
|
||||
*
|
||||
* @return string Output generated by handlers.
|
||||
*/
|
||||
public function handleException($exception)
|
||||
{
|
||||
// Walk the registered handlers in the reverse order
|
||||
// they were registered, and pass off the exception
|
||||
$inspector = $this->getInspector($exception);
|
||||
|
||||
// Capture output produced while handling the exception,
|
||||
// we might want to send it straight away to the client,
|
||||
// or return it silently.
|
||||
$this->system->startOutputBuffering();
|
||||
|
||||
// Just in case there are no handlers:
|
||||
$handlerResponse = null;
|
||||
$handlerContentType = null;
|
||||
|
||||
try {
|
||||
foreach (array_reverse($this->handlerStack) as $handler) {
|
||||
$handler->setRun($this);
|
||||
$handler->setInspector($inspector);
|
||||
$handler->setException($exception);
|
||||
|
||||
// The HandlerInterface does not require an Exception passed to handle()
|
||||
// and neither of our bundled handlers use it.
|
||||
// However, 3rd party handlers may have already relied on this parameter,
|
||||
// and removing it would be possibly breaking for users.
|
||||
$handlerResponse = $handler->handle($exception);
|
||||
|
||||
// Collect the content type for possible sending in the headers.
|
||||
$handlerContentType = method_exists($handler, 'contentType') ? $handler->contentType() : null;
|
||||
|
||||
if (in_array($handlerResponse, [Handler::LAST_HANDLER, Handler::QUIT])) {
|
||||
// The Handler has handled the exception in some way, and
|
||||
// wishes to quit execution (Handler::QUIT), or skip any
|
||||
// other handlers (Handler::LAST_HANDLER). If $this->allowQuit
|
||||
// is false, Handler::QUIT behaves like Handler::LAST_HANDLER
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$willQuit = $handlerResponse == Handler::QUIT && $this->allowQuit();
|
||||
} finally {
|
||||
$output = $this->system->cleanOutputBuffer();
|
||||
}
|
||||
|
||||
// If we're allowed to, send output generated by handlers directly
|
||||
// to the output, otherwise, and if the script doesn't quit, return
|
||||
// it so that it may be used by the caller
|
||||
if ($this->writeToOutput()) {
|
||||
// @todo Might be able to clean this up a bit better
|
||||
if ($willQuit) {
|
||||
// Cleanup all other output buffers before sending our output:
|
||||
while ($this->system->getOutputBufferLevel() > 0) {
|
||||
$this->system->endOutputBuffering();
|
||||
}
|
||||
|
||||
// Send any headers if needed:
|
||||
if (Misc::canSendHeaders() && $handlerContentType) {
|
||||
header("Content-Type: {$handlerContentType}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->writeToOutputNow($output);
|
||||
}
|
||||
|
||||
if ($willQuit) {
|
||||
// HHVM fix for https://github.com/facebook/hhvm/issues/4055
|
||||
$this->system->flushOutputBuffer();
|
||||
|
||||
$this->system->stopExecution(
|
||||
$this->sendExitCode()
|
||||
);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts generic PHP errors to \ErrorException instances, before passing them off to be handled.
|
||||
*
|
||||
* This method MUST be compatible with set_error_handler.
|
||||
*
|
||||
* @param int $level
|
||||
* @param string $message
|
||||
* @param string|null $file
|
||||
* @param int|null $line
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws ErrorException
|
||||
*/
|
||||
public function handleError($level, $message, $file = null, $line = null)
|
||||
{
|
||||
if ($level & $this->system->getErrorReportingLevel()) {
|
||||
foreach ($this->silencedPatterns as $entry) {
|
||||
$pathMatches = (bool) preg_match($entry["pattern"], $file);
|
||||
$levelMatches = $level & $entry["levels"];
|
||||
if ($pathMatches && $levelMatches) {
|
||||
// Ignore the error, abort handling
|
||||
// See https://github.com/filp/whoops/issues/418
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// XXX we pass $level for the "code" param only for BC reasons.
|
||||
// see https://github.com/filp/whoops/issues/267
|
||||
$exception = new ErrorException($message, /*code*/ $level, /*severity*/ $level, $file, $line);
|
||||
if ($this->canThrowExceptions) {
|
||||
throw $exception;
|
||||
} else {
|
||||
$this->handleException($exception);
|
||||
}
|
||||
// Do not propagate errors which were already handled by Whoops.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Propagate error to the next handler, allows error_get_last() to
|
||||
// work on silenced errors.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Special case to deal with Fatal errors and the like.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handleShutdown()
|
||||
{
|
||||
// If we reached this step, we are in shutdown handler.
|
||||
// An exception thrown in a shutdown handler will not be propagated
|
||||
// to the exception handler. Pass that information along.
|
||||
$this->canThrowExceptions = false;
|
||||
|
||||
$error = $this->system->getLastError();
|
||||
if ($error && Misc::isLevelFatal($error['type'])) {
|
||||
// If there was a fatal error,
|
||||
// it was not handled in handleError yet.
|
||||
$this->allowQuit = false;
|
||||
$this->handleError(
|
||||
$error['type'],
|
||||
$error['message'],
|
||||
$error['file'],
|
||||
$error['line']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Throwable $exception
|
||||
*
|
||||
* @return Inspector
|
||||
*/
|
||||
private function getInspector($exception)
|
||||
{
|
||||
return new Inspector($exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the giving handler.
|
||||
*
|
||||
* @param callable|HandlerInterface $handler
|
||||
*
|
||||
* @return HandlerInterface
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function resolveHandler($handler)
|
||||
{
|
||||
if (is_callable($handler)) {
|
||||
$handler = new CallbackHandler($handler);
|
||||
}
|
||||
|
||||
if (!$handler instanceof HandlerInterface) {
|
||||
throw new InvalidArgumentException(
|
||||
"Handler must be a callable, or instance of "
|
||||
. "Whoops\\Handler\\HandlerInterface"
|
||||
);
|
||||
}
|
||||
|
||||
return $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Echo something to the browser.
|
||||
*
|
||||
* @param string $output
|
||||
*
|
||||
* @return Run
|
||||
*/
|
||||
private function writeToOutputNow($output)
|
||||
{
|
||||
if ($this->sendHttpCode() && Misc::canSendHeaders()) {
|
||||
$this->system->setHttpResponseCode(
|
||||
$this->sendHttpCode()
|
||||
);
|
||||
}
|
||||
|
||||
echo $output;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
352
overrides/filp/whoops/src/Whoops/Util/TemplateHelper.php
Normal file
352
overrides/filp/whoops/src/Whoops/Util/TemplateHelper.php
Normal file
@@ -0,0 +1,352 @@
|
||||
<?php
|
||||
/**
|
||||
* Whoops - php errors for cool kids
|
||||
* @author Filipe Dobreira <http://github.com/filp>
|
||||
*/
|
||||
|
||||
namespace Whoops\Util;
|
||||
|
||||
use Symfony\Component\VarDumper\Caster\Caster;
|
||||
use Symfony\Component\VarDumper\Cloner\AbstractCloner;
|
||||
use Symfony\Component\VarDumper\Cloner\VarCloner;
|
||||
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
|
||||
use Whoops\Exception\Frame;
|
||||
|
||||
/**
|
||||
* Exposes useful tools for working with/in templates
|
||||
*/
|
||||
class TemplateHelper
|
||||
{
|
||||
/**
|
||||
* An array of variables to be passed to all templates
|
||||
* @var array
|
||||
*/
|
||||
private $variables = [];
|
||||
|
||||
/**
|
||||
* @var HtmlDumper
|
||||
*/
|
||||
private $htmlDumper;
|
||||
|
||||
/**
|
||||
* @var HtmlDumperOutput
|
||||
*/
|
||||
private $htmlDumperOutput;
|
||||
|
||||
/**
|
||||
* @var AbstractCloner
|
||||
*/
|
||||
private $cloner;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $applicationRootPath;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// root path for ordinary composer projects
|
||||
$this->applicationRootPath = dirname(dirname(dirname(dirname(dirname(dirname(__DIR__))))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a string for output in an HTML document
|
||||
*
|
||||
* @param string $raw
|
||||
* @return string
|
||||
*/
|
||||
public function escape($raw)
|
||||
{
|
||||
$flags = ENT_QUOTES;
|
||||
|
||||
// HHVM has all constants defined, but only ENT_IGNORE
|
||||
// works at the moment
|
||||
if (defined("ENT_SUBSTITUTE") && !defined("HHVM_VERSION")) {
|
||||
$flags |= ENT_SUBSTITUTE;
|
||||
} else {
|
||||
// This is for 5.3.
|
||||
// The documentation warns of a potential security issue,
|
||||
// but it seems it does not apply in our case, because
|
||||
// we do not blacklist anything anywhere.
|
||||
$flags |= ENT_IGNORE;
|
||||
}
|
||||
|
||||
$raw = str_replace(chr(9), ' ', $raw);
|
||||
|
||||
return htmlspecialchars($raw, $flags, "UTF-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a string for output in an HTML document, but preserves
|
||||
* URIs within it, and converts them to clickable anchor elements.
|
||||
*
|
||||
* @param string $raw
|
||||
* @return string
|
||||
*/
|
||||
public function escapeButPreserveUris($raw)
|
||||
{
|
||||
$escaped = $this->escape($raw);
|
||||
return preg_replace(
|
||||
"@([A-z]+?://([-\w\.]+[-\w])+(:\d+)?(/([\w/_\.#-]*(\?\S+)?[^\.\s])?)?)@",
|
||||
"<a href=\"$1\" target=\"_blank\" rel=\"noreferrer noopener\">$1</a>",
|
||||
$escaped
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure that the given string breaks on the delimiter.
|
||||
*
|
||||
* @param string $delimiter
|
||||
* @param string $s
|
||||
* @return string
|
||||
*/
|
||||
public function breakOnDelimiter($delimiter, $s)
|
||||
{
|
||||
$parts = explode($delimiter, $s);
|
||||
foreach ($parts as &$part) {
|
||||
$part = '<span class="delimiter">' . $part . '</span>';
|
||||
}
|
||||
|
||||
return implode($delimiter, $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the part of the path that all files have in common.
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
public function shorten($path)
|
||||
{
|
||||
if ($this->applicationRootPath != "/") {
|
||||
$path = str_replace($this->applicationRootPath, '…', $path);
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
private function getDumper()
|
||||
{
|
||||
if (!$this->htmlDumper && class_exists('Symfony\Component\VarDumper\Cloner\VarCloner')) {
|
||||
$this->htmlDumperOutput = new HtmlDumperOutput();
|
||||
// re-use the same var-dumper instance, so it won't re-render the global styles/scripts on each dump.
|
||||
$this->htmlDumper = new HtmlDumper($this->htmlDumperOutput);
|
||||
|
||||
$styles = [
|
||||
'default' => 'color:#FFFFFF; line-height:normal; font:12px "Inconsolata", "Fira Mono", "Source Code Pro", Monaco, Consolas, "Lucida Console", monospace !important; word-wrap: break-word; white-space: pre-wrap; position:relative; z-index:99999; word-break: normal',
|
||||
'num' => 'color:#BCD42A',
|
||||
'const' => 'color: #4bb1b1;',
|
||||
'str' => 'color:#BCD42A',
|
||||
'note' => 'color:#ef7c61',
|
||||
'ref' => 'color:#A0A0A0',
|
||||
'public' => 'color:#FFFFFF',
|
||||
'protected' => 'color:#FFFFFF',
|
||||
'private' => 'color:#FFFFFF',
|
||||
'meta' => 'color:#FFFFFF',
|
||||
'key' => 'color:#BCD42A',
|
||||
'index' => 'color:#ef7c61',
|
||||
];
|
||||
$this->htmlDumper->setStyles($styles);
|
||||
}
|
||||
|
||||
return $this->htmlDumper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the given value into a human readable string.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return string
|
||||
*/
|
||||
public function dump($value)
|
||||
{
|
||||
$dumper = $this->getDumper();
|
||||
|
||||
if ($dumper) {
|
||||
// re-use the same DumpOutput instance, so it won't re-render the global styles/scripts on each dump.
|
||||
// exclude verbose information (e.g. exception stack traces)
|
||||
if (class_exists('Symfony\Component\VarDumper\Caster\Caster')) {
|
||||
$cloneVar = $this->getCloner()->cloneVar($value, Caster::EXCLUDE_VERBOSE);
|
||||
// Symfony VarDumper 2.6 Caster class dont exist.
|
||||
} else {
|
||||
$cloneVar = $this->getCloner()->cloneVar($value);
|
||||
}
|
||||
|
||||
$dumper->dump(
|
||||
$cloneVar,
|
||||
$this->htmlDumperOutput
|
||||
);
|
||||
|
||||
$output = $this->htmlDumperOutput->getOutput();
|
||||
$this->htmlDumperOutput->clear();
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
return htmlspecialchars(print_r($value, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the args of the given Frame as a human readable html string
|
||||
*
|
||||
* @param Frame $frame
|
||||
* @return string the rendered html
|
||||
*/
|
||||
public function dumpArgs(Frame $frame)
|
||||
{
|
||||
// we support frame args only when the optional dumper is available
|
||||
if (!$this->getDumper()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$html = '';
|
||||
$numFrames = count($frame->getArgs());
|
||||
|
||||
if ($numFrames > 0) {
|
||||
$html = '<ol class="linenums">';
|
||||
foreach ($frame->getArgs() as $j => $frameArg) {
|
||||
$html .= '<li>'. $this->dump($frameArg) .'</li>';
|
||||
}
|
||||
$html .= '</ol>';
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string to a slug version of itself
|
||||
*
|
||||
* @param string $original
|
||||
* @return string
|
||||
*/
|
||||
public function slug($original)
|
||||
{
|
||||
$slug = str_replace(" ", "-", $original);
|
||||
$slug = preg_replace('/[^\w\d\-\_]/i', '', $slug);
|
||||
return strtolower($slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a template path, render it within its own scope. This
|
||||
* method also accepts an array of additional variables to be
|
||||
* passed to the template.
|
||||
*
|
||||
* @param string $template
|
||||
* @param array $additionalVariables
|
||||
*/
|
||||
public function render($template, ?array $additionalVariables = null)
|
||||
{
|
||||
$variables = $this->getVariables();
|
||||
|
||||
// Pass the helper to the template:
|
||||
$variables["tpl"] = $this;
|
||||
|
||||
if ($additionalVariables !== null) {
|
||||
$variables = array_replace($variables, $additionalVariables);
|
||||
}
|
||||
|
||||
call_user_func(function () {
|
||||
extract(func_get_arg(1));
|
||||
require func_get_arg(0);
|
||||
}, $template, $variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the variables to be passed to all templates rendered
|
||||
* by this template helper.
|
||||
*
|
||||
* @param array $variables
|
||||
*/
|
||||
public function setVariables(array $variables)
|
||||
{
|
||||
$this->variables = $variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a single template variable, by its name:
|
||||
*
|
||||
* @param string $variableName
|
||||
* @param mixed $variableValue
|
||||
*/
|
||||
public function setVariable($variableName, $variableValue)
|
||||
{
|
||||
$this->variables[$variableName] = $variableValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a single template variable, by its name, or
|
||||
* $defaultValue if the variable does not exist
|
||||
*
|
||||
* @param string $variableName
|
||||
* @param mixed $defaultValue
|
||||
* @return mixed
|
||||
*/
|
||||
public function getVariable($variableName, $defaultValue = null)
|
||||
{
|
||||
return isset($this->variables[$variableName]) ?
|
||||
$this->variables[$variableName] : $defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsets a single template variable, by its name
|
||||
*
|
||||
* @param string $variableName
|
||||
*/
|
||||
public function delVariable($variableName)
|
||||
{
|
||||
unset($this->variables[$variableName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all variables for this helper
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getVariables()
|
||||
{
|
||||
return $this->variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cloner used for dumping variables.
|
||||
*
|
||||
* @param AbstractCloner $cloner
|
||||
*/
|
||||
public function setCloner($cloner)
|
||||
{
|
||||
$this->cloner = $cloner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cloner used for dumping variables.
|
||||
*
|
||||
* @return AbstractCloner
|
||||
*/
|
||||
public function getCloner()
|
||||
{
|
||||
if (!$this->cloner) {
|
||||
$this->cloner = new VarCloner();
|
||||
}
|
||||
return $this->cloner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the application root path.
|
||||
*
|
||||
* @param string $applicationRootPath
|
||||
*/
|
||||
public function setApplicationRootPath($applicationRootPath)
|
||||
{
|
||||
$this->applicationRootPath = $applicationRootPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the application root path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getApplicationRootPath()
|
||||
{
|
||||
return $this->applicationRootPath;
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,7 @@ class CurlMultiHandler
|
||||
while ($this->handles || !$queue->isEmpty()) {
|
||||
// If there are no transfers, then sleep for the next delay
|
||||
if (!$this->active && $this->delays) {
|
||||
usleep($this->timeToNext());
|
||||
usleep((int)$this->timeToNext());
|
||||
}
|
||||
$this->tick();
|
||||
}
|
||||
@@ -76,7 +76,7 @@ class StreamHandler
|
||||
array $options,
|
||||
RequestInterface $request,
|
||||
$startTime,
|
||||
ResponseInterface $response = null,
|
||||
?ResponseInterface $response = null,
|
||||
$error = null
|
||||
) {
|
||||
if (isset($options['on_stats'])) {
|
||||
@@ -37,7 +37,7 @@ class HandlerStack
|
||||
*
|
||||
* @return HandlerStack
|
||||
*/
|
||||
public static function create(callable $handler = null)
|
||||
public static function create(?callable $handler = null)
|
||||
{
|
||||
$stack = new self($handler ?: choose_handler());
|
||||
$stack->push(Middleware::httpErrors(), 'http_errors');
|
||||
@@ -51,7 +51,7 @@ class HandlerStack
|
||||
/**
|
||||
* @param callable $handler Underlying HTTP handler.
|
||||
*/
|
||||
public function __construct(callable $handler = null)
|
||||
public function __construct(?callable $handler = null)
|
||||
{
|
||||
$this->handler = $handler;
|
||||
}
|
||||
@@ -122,7 +122,7 @@ final class Middleware
|
||||
*
|
||||
* @return callable Returns a function that accepts the next handler.
|
||||
*/
|
||||
public static function tap(callable $before = null, callable $after = null)
|
||||
public static function tap(?callable $before = null, ?callable $after = null)
|
||||
{
|
||||
return function (callable $handler) use ($before, $after) {
|
||||
return function ($request, array $options) use ($handler, $before, $after) {
|
||||
@@ -165,7 +165,7 @@ final class Middleware
|
||||
*
|
||||
* @return callable Returns a function that accepts the next handler.
|
||||
*/
|
||||
public static function retry(callable $decider, callable $delay = null)
|
||||
public static function retry(?callable $decider, ?callable $delay = null)
|
||||
{
|
||||
return function (callable $handler) use ($decider, $delay) {
|
||||
return new RetryMiddleware($decider, $handler, $delay);
|
||||
@@ -24,8 +24,8 @@ class FulfilledPromise implements PromiseInterface
|
||||
}
|
||||
|
||||
public function then(
|
||||
callable $onFulfilled = null,
|
||||
callable $onRejected = null
|
||||
?callable $onFulfilled = null,
|
||||
?callable $onRejected = null
|
||||
) {
|
||||
// Return itself if there is no onFulfilled function.
|
||||
if (!$onFulfilled) {
|
||||
@@ -21,16 +21,16 @@ class Promise implements PromiseInterface
|
||||
* @param callable $cancelFn Fn that when invoked cancels the promise.
|
||||
*/
|
||||
public function __construct(
|
||||
callable $waitFn = null,
|
||||
callable $cancelFn = null
|
||||
?callable $waitFn = null,
|
||||
?callable $cancelFn = null
|
||||
) {
|
||||
$this->waitFn = $waitFn;
|
||||
$this->cancelFn = $cancelFn;
|
||||
}
|
||||
|
||||
public function then(
|
||||
callable $onFulfilled = null,
|
||||
callable $onRejected = null
|
||||
?callable $onFulfilled = null,
|
||||
?callable $onRejected = null
|
||||
) {
|
||||
if ($this->state === self::PENDING) {
|
||||
$p = new Promise(null, [$this, 'cancel']);
|
||||
@@ -27,8 +27,8 @@ interface PromiseInterface
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function then(
|
||||
callable $onFulfilled = null,
|
||||
callable $onRejected = null
|
||||
?callable $onFulfilled = null,
|
||||
?callable $onRejected = null
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -24,8 +24,8 @@ class RejectedPromise implements PromiseInterface
|
||||
}
|
||||
|
||||
public function then(
|
||||
callable $onFulfilled = null,
|
||||
callable $onRejected = null
|
||||
?callable $onFulfilled = null,
|
||||
?callable $onRejected = null
|
||||
) {
|
||||
// If there's no onRejected callback then just return self.
|
||||
if (!$onRejected) {
|
||||
@@ -21,7 +21,7 @@ final class Utils
|
||||
*
|
||||
* @return TaskQueueInterface
|
||||
*/
|
||||
public static function queue(TaskQueueInterface $assign = null)
|
||||
public static function queue(?TaskQueueInterface $assign = null)
|
||||
{
|
||||
static $queue;
|
||||
|
||||
@@ -288,7 +288,7 @@ class Uri implements UriInterface
|
||||
*
|
||||
* @link https://tools.ietf.org/html/rfc3986#section-4.4
|
||||
*/
|
||||
public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null)
|
||||
public static function isSameDocumentReference(UriInterface $uri, ?UriInterface $base = null)
|
||||
{
|
||||
if ($base !== null) {
|
||||
$uri = UriResolver::resolve($base, $uri);
|
||||
@@ -125,7 +125,7 @@ class Gate implements GateContract
|
||||
* @param array $abilities
|
||||
* @return $this
|
||||
*/
|
||||
public function resource($name, $class, array $abilities = null)
|
||||
public function resource($name, $class, ?array $abilities = null)
|
||||
{
|
||||
$abilities = $abilities ?: [
|
||||
'view' => 'view',
|
||||
@@ -97,7 +97,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
|
||||
public function __construct($name,
|
||||
UserProvider $provider,
|
||||
Session $session,
|
||||
Request $request = null)
|
||||
?Request $request = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->session = $session;
|
||||
|
||||
@@ -56,7 +56,7 @@ class BroadcastManager implements FactoryContract
|
||||
* @param array|null $attributes
|
||||
* @return void
|
||||
*/
|
||||
public function routes(array $attributes = null)
|
||||
public function routes(?array $attributes = null)
|
||||
{
|
||||
if ($this->app->routesAreCached()) {
|
||||
return;
|
||||
@@ -54,7 +54,7 @@ class Dispatcher implements QueueingDispatcher
|
||||
* @param \Closure|null $queueResolver
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Container $container, Closure $queueResolver = null)
|
||||
public function __construct(Container $container, ?Closure $queueResolver = null)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->queueResolver = $queueResolver;
|
||||
@@ -73,7 +73,7 @@ class Application extends SymfonyApplication implements ApplicationContract
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function run(InputInterface $input = null, OutputInterface $output = null)
|
||||
public function run(?InputInterface $input = null, ?OutputInterface $output = null)
|
||||
{
|
||||
$commandName = $this->getCommandName(
|
||||
$input = $input ?: new ArgvInput
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
trait DetectsLostConnections
|
||||
{
|
||||
/**
|
||||
* Determine if the given exception was caused by a lost connection.
|
||||
*
|
||||
* @param \Exception $e
|
||||
* @return bool
|
||||
*/
|
||||
protected function causedByLostConnection(Exception $e)
|
||||
{
|
||||
$message = $e->getMessage();
|
||||
|
||||
return Str::contains($message, [
|
||||
'server has gone away',
|
||||
'no connection to the server',
|
||||
'Lost connection',
|
||||
'is dead or not enabled',
|
||||
'Error while sending',
|
||||
'decryption failed or bad record mac',
|
||||
'server closed the connection unexpectedly',
|
||||
'SSL connection has been closed unexpectedly',
|
||||
'Error writing data to the connection',
|
||||
'Resource deadlock avoided',
|
||||
'Transaction() on null',
|
||||
'child connection forced to terminate due to client_idle_limit',
|
||||
'query_wait_timeout',
|
||||
'reset by peer',
|
||||
'Physical connection is not usable',
|
||||
'TCP Provider: Error code 0x68',
|
||||
'ORA-03114',
|
||||
'Packets out of order. Expected',
|
||||
'Adaptive Server connection failed',
|
||||
'Communication link failure',
|
||||
'connection is no longer usable',
|
||||
'Login timeout expired',
|
||||
'SQLSTATE[HY000] [2002] Connection refused',
|
||||
'running with the --read-only option so it cannot execute this statement',
|
||||
'The connection is broken and recovery is not possible. The connection is marked by the client driver as unrecoverable. No attempt was made to restore the connection.',
|
||||
'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Try again',
|
||||
'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Name or service not known',
|
||||
'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo for',
|
||||
'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: EOF detected',
|
||||
'SQLSTATE[HY000] [2002] Connection timed out',
|
||||
'SSL: Connection timed out',
|
||||
'SQLSTATE[HY000]: General error: 1105 The last transaction was aborted due to Seamless Scaling. Please retry.',
|
||||
'Temporary failure in name resolution',
|
||||
'SSL: Broken pipe',
|
||||
'SQLSTATE[08S01]: Communication link failure',
|
||||
'SQLSTATE[08006] [7] could not connect to server: Connection refused Is the server running on host',
|
||||
'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: No route to host',
|
||||
'The client was disconnected by the server because of inactivity. See wait_timeout and interactive_timeout for configuring this behavior.',
|
||||
'SQLSTATE[08006] [7] could not translate host name',
|
||||
'TCP Provider: Error code 0x274C',
|
||||
'SQLSTATE[HY000] [2002] No such file or directory',
|
||||
'SSL: Operation timed out',
|
||||
'Reason: Server is in script upgrade mode. Only administrator can connect at this time.',
|
||||
'Unknown $curl_error_code: 77',
|
||||
'SSL: Handshake timed out',
|
||||
'SQLSTATE[08006] [7] SSL error: sslv3 alert unexpected message',
|
||||
'SQLSTATE[08006] [7] unrecognized SSL error code:',
|
||||
'SQLSTATE[HY000] [2002] No connection could be made because the target machine actively refused it',
|
||||
'SQLSTATE[HY000] [2002] A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond',
|
||||
'SQLSTATE[HY000] [2002] Network is unreachable',
|
||||
'SQLSTATE[HY000] [2002] The requested address is not valid in its context',
|
||||
'SQLSTATE[HY000] [2002] A socket operation was attempted to an unreachable network',
|
||||
'SQLSTATE[HY000]: General error: 3989',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,7 @@ class Builder
|
||||
* @param array|null $scopes
|
||||
* @return $this
|
||||
*/
|
||||
public function withoutGlobalScopes(array $scopes = null)
|
||||
public function withoutGlobalScopes(?array $scopes = null)
|
||||
{
|
||||
if (is_array($scopes)) {
|
||||
foreach ($scopes as $scope) {
|
||||
@@ -423,7 +423,7 @@ class Builder
|
||||
* @param \Closure|null $callback
|
||||
* @return \Illuminate\Database\Eloquent\Model|static|mixed
|
||||
*/
|
||||
public function firstOr($columns = ['*'], Closure $callback = null)
|
||||
public function firstOr($columns = ['*'], ?Closure $callback = null)
|
||||
{
|
||||
if ($columns instanceof Closure) {
|
||||
$callback = $columns;
|
||||
@@ -18,7 +18,7 @@ trait HasGlobalScopes
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public static function addGlobalScope($scope, Closure $implementation = null)
|
||||
public static function addGlobalScope($scope, ?Closure $implementation = null)
|
||||
{
|
||||
if (is_string($scope) && ! is_null($implementation)) {
|
||||
return static::$globalScopes[static::class][$scope] = $implementation;
|
||||
@@ -21,7 +21,7 @@ trait QueriesRelationships
|
||||
* @param \Closure|null $callback
|
||||
* @return \Illuminate\Database\Eloquent\Builder|static
|
||||
*/
|
||||
public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null)
|
||||
public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', ?Closure $callback = null)
|
||||
{
|
||||
if (strpos($relation, '.') !== false) {
|
||||
return $this->hasNested($relation, $operator, $count, $boolean, $callback);
|
||||
@@ -101,7 +101,7 @@ trait QueriesRelationships
|
||||
* @param \Closure|null $callback
|
||||
* @return \Illuminate\Database\Eloquent\Builder|static
|
||||
*/
|
||||
public function doesntHave($relation, $boolean = 'and', Closure $callback = null)
|
||||
public function doesntHave($relation, $boolean = 'and', ?Closure $callback = null)
|
||||
{
|
||||
return $this->has($relation, '<', 1, $boolean, $callback);
|
||||
}
|
||||
@@ -126,7 +126,7 @@ trait QueriesRelationships
|
||||
* @param int $count
|
||||
* @return \Illuminate\Database\Eloquent\Builder|static
|
||||
*/
|
||||
public function whereHas($relation, Closure $callback = null, $operator = '>=', $count = 1)
|
||||
public function whereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1)
|
||||
{
|
||||
return $this->has($relation, $operator, $count, 'and', $callback);
|
||||
}
|
||||
@@ -140,7 +140,7 @@ trait QueriesRelationships
|
||||
* @param int $count
|
||||
* @return \Illuminate\Database\Eloquent\Builder|static
|
||||
*/
|
||||
public function orWhereHas($relation, Closure $callback = null, $operator = '>=', $count = 1)
|
||||
public function orWhereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1)
|
||||
{
|
||||
return $this->has($relation, $operator, $count, 'or', $callback);
|
||||
}
|
||||
@@ -152,7 +152,7 @@ trait QueriesRelationships
|
||||
* @param \Closure|null $callback
|
||||
* @return \Illuminate\Database\Eloquent\Builder|static
|
||||
*/
|
||||
public function whereDoesntHave($relation, Closure $callback = null)
|
||||
public function whereDoesntHave($relation, ?Closure $callback = null)
|
||||
{
|
||||
return $this->doesntHave($relation, 'and', $callback);
|
||||
}
|
||||
@@ -164,7 +164,7 @@ trait QueriesRelationships
|
||||
* @param \Closure $callback
|
||||
* @return \Illuminate\Database\Eloquent\Builder|static
|
||||
*/
|
||||
public function orWhereDoesntHave($relation, Closure $callback = null)
|
||||
public function orWhereDoesntHave($relation, ?Closure $callback = null)
|
||||
{
|
||||
return $this->doesntHave($relation, 'or', $callback);
|
||||
}
|
||||
@@ -1046,7 +1046,7 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
|
||||
* @param array|null $except
|
||||
* @return \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public function replicate(array $except = null)
|
||||
public function replicate(?array $except = null)
|
||||
{
|
||||
$defaults = [
|
||||
$this->getKeyName(),
|
||||
|
||||
@@ -309,7 +309,7 @@ abstract class Relation
|
||||
* @param bool $merge
|
||||
* @return array
|
||||
*/
|
||||
public static function morphMap(array $map = null, $merge = true)
|
||||
public static function morphMap(?array $map = null, $merge = true)
|
||||
{
|
||||
$map = static::buildMorphMapFromModels($map);
|
||||
|
||||
@@ -327,7 +327,7 @@ abstract class Relation
|
||||
* @param string[]|null $models
|
||||
* @return array|null
|
||||
*/
|
||||
protected static function buildMorphMapFromModels(array $models = null)
|
||||
protected static function buildMorphMapFromModels(?array $models = null)
|
||||
{
|
||||
if (is_null($models) || Arr::isAssoc($models)) {
|
||||
return $models;
|
||||
@@ -201,8 +201,8 @@ class Builder
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(ConnectionInterface $connection,
|
||||
Grammar $grammar = null,
|
||||
Processor $processor = null)
|
||||
?Grammar $grammar = null,
|
||||
?Processor $processor = null)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->grammar = $grammar ?: $connection->getQueryGrammar();
|
||||
@@ -64,7 +64,7 @@ class Blueprint
|
||||
* @param \Closure|null $callback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($table, Closure $callback = null)
|
||||
public function __construct($table, ?Closure $callback = null)
|
||||
{
|
||||
$this->table = $table;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user