Compare commits
1832 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8053591031 | ||
|
|
3d2fcd020d | ||
|
|
3d7fd68fa8 | ||
|
|
1a011d9ea4 | ||
|
|
57a943edec | ||
|
|
85e9dc53f2 | ||
|
|
a682d8f7df | ||
|
|
bdc1073a28 | ||
|
|
ac79c57de4 | ||
|
|
1b70e45a1c | ||
|
|
97d0c470fb | ||
|
|
c92589da2a | ||
|
|
3dffd231ae | ||
|
|
f429bdef8e | ||
|
|
11f223ab04 | ||
|
|
2ccf407bd9 | ||
|
|
a172d476f5 | ||
|
|
f37303e8a0 | ||
|
|
60016fd532 | ||
|
|
ab3de1f60b | ||
|
|
6014509dd2 | ||
|
|
4e856ea783 | ||
|
|
23aeefbcbd | ||
|
|
b84afb99a5 | ||
|
|
2dada15292 | ||
|
|
a87e2199f0 | ||
|
|
0e85c73a77 | ||
|
|
d7ef54a37e | ||
|
|
ed47a5e3c1 | ||
|
|
27698ebea0 | ||
|
|
3bcdcd9284 | ||
|
|
640379e4d7 | ||
|
|
47b4dfb4b8 | ||
|
|
da2ea1cd1b | ||
|
|
12e80d60b6 | ||
|
|
141fd8320e | ||
|
|
a9ac930cc0 | ||
|
|
344f06178f | ||
|
|
0f03f4fffd | ||
|
|
24ef816c2e | ||
|
|
516e02be1d | ||
|
|
3c13551a7a | ||
|
|
efcf05ad8b | ||
|
|
26b7e1c5c3 | ||
|
|
2e71180f7c | ||
|
|
a42e0db899 | ||
|
|
3c7dda7118 | ||
|
|
740d8aa706 | ||
|
|
e82a1c2b81 | ||
|
|
7ea7795913 | ||
|
|
2589aa1da9 | ||
|
|
18295e83fb | ||
|
|
768c4561a9 | ||
|
|
dcb7e7715f | ||
|
|
767b20f059 | ||
|
|
66fba5f20a | ||
|
|
921186e2e0 | ||
|
|
5d41b6908c | ||
|
|
fede8d633a | ||
|
|
182f49f758 | ||
|
|
91c914fd14 | ||
|
|
c6ea0a85cb | ||
|
|
0a6e0a100c | ||
|
|
d4b81c4cdb | ||
|
|
2b4f67befe | ||
|
|
56597bbd29 | ||
|
|
c0fc874ce4 | ||
|
|
d26ab69b18 | ||
|
|
346148ae60 | ||
|
|
8e9bcc6040 | ||
|
|
f4c76467bb | ||
|
|
26a8d8511f | ||
|
|
1873dc6a20 | ||
|
|
8fa94d8452 | ||
|
|
d62088fa1f | ||
|
|
03d8e2e0e8 | ||
|
|
4397e0c756 | ||
|
|
42b23f6e3a | ||
|
|
501b0719cc | ||
|
|
1a7289a395 | ||
|
|
6153db71c8 | ||
|
|
5a7eae76d7 | ||
|
|
cc8e5da17b | ||
|
|
54d44ba87a | ||
|
|
64c46e3941 | ||
|
|
2458539c6f | ||
|
|
c56b9a0573 | ||
|
|
eeb00763d3 | ||
|
|
800a484acc | ||
|
|
b3e444be47 | ||
|
|
ff025b94d2 | ||
|
|
96986e8a22 | ||
|
|
2d0c72d5bb | ||
|
|
384ff40d88 | ||
|
|
8dca28adeb | ||
|
|
1f6a95c4a7 | ||
|
|
3493f16035 | ||
|
|
97d12be54d | ||
|
|
19d84bb479 | ||
|
|
2a29e3194e | ||
|
|
4b8904f335 | ||
|
|
a9d43435e9 | ||
|
|
9884a767ff | ||
|
|
aa1dba6241 | ||
|
|
ba9293a598 | ||
|
|
7a15e55da2 | ||
|
|
77a851cbe0 | ||
|
|
595eaa7a20 | ||
|
|
a66b6d019e | ||
|
|
b6257aa21c | ||
|
|
1a46c7f001 | ||
|
|
8ddbe18543 | ||
|
|
5c8de33eb6 | ||
|
|
7c663cacf8 | ||
|
|
1bedf8394d | ||
|
|
df0ca8b740 | ||
|
|
ced188f286 | ||
|
|
96fa6b5507 | ||
|
|
c19600bc7c | ||
|
|
cbddefcda2 | ||
|
|
a06d2f4c81 | ||
|
|
929fc80e66 | ||
|
|
56f2f1053b | ||
|
|
3f1ec34dea | ||
|
|
ddc4f9d452 | ||
|
|
14ae10536f | ||
|
|
a9a3863d05 | ||
|
|
9e8ebe17ea | ||
|
|
9cca472d29 | ||
|
|
44390a6280 | ||
|
|
345cdb2702 | ||
|
|
a2adf76962 | ||
|
|
3d477f6296 | ||
|
|
b0469ed5bc | ||
|
|
63f5b92a3c | ||
|
|
a5d03df940 | ||
|
|
2b07bcbca7 | ||
|
|
d55bfd6495 | ||
|
|
8210891675 | ||
|
|
beeeadf34a | ||
|
|
1f8b2098cc | ||
|
|
669ebf79e1 | ||
|
|
8a0077c94d | ||
|
|
a2910deed3 | ||
|
|
039623097d | ||
|
|
833158b0de | ||
|
|
d1598d61ac | ||
|
|
672149d3f7 | ||
|
|
5db4c5ced2 | ||
|
|
8f07621946 | ||
|
|
3235e60f72 | ||
|
|
566640d287 | ||
|
|
c10686e546 | ||
|
|
fb1ac61d64 | ||
|
|
813bc84575 | ||
|
|
323e9d7a20 | ||
|
|
f134a0288e | ||
|
|
7324f15c50 | ||
|
|
b8286e7875 | ||
|
|
1afb66ce0b | ||
|
|
ec4670757e | ||
|
|
92a9e7cb78 | ||
|
|
dd8e8274a1 | ||
|
|
b010996391 | ||
|
|
5bbe2b9d73 | ||
|
|
c2336d9ca8 | ||
|
|
1b29571ba9 | ||
|
|
810d2e3924 | ||
|
|
55d24994df | ||
|
|
f0bba4b77b | ||
|
|
eefc7fd153 | ||
|
|
f24b2e5040 | ||
|
|
d96c180631 | ||
|
|
f78933e2a4 | ||
|
|
b30b996e9c | ||
|
|
a51cb49225 | ||
|
|
f4924c3140 | ||
|
|
c1b08383ef | ||
|
|
59651ee24d | ||
|
|
11b79b2b48 | ||
|
|
ac03db6380 | ||
|
|
5d590d598e | ||
|
|
17a157eb60 | ||
|
|
2446a94d50 | ||
|
|
53240b6360 | ||
|
|
b4b09213f7 | ||
|
|
cdf6fd07cf | ||
|
|
a0c5748c59 | ||
|
|
d599c63816 | ||
|
|
c4fb834cd0 | ||
|
|
f605cf7e85 | ||
|
|
fa06b7148e | ||
|
|
f17755aad8 | ||
|
|
e4fdab0a19 | ||
|
|
94af31c686 | ||
|
|
28902b4c9f | ||
|
|
e57ed73f98 | ||
|
|
bf1eb5985a | ||
|
|
2188ce8a4e | ||
|
|
33767a590a | ||
|
|
352469b662 | ||
|
|
01b8da7cc2 | ||
|
|
f9b517c849 | ||
|
|
962418a9ee | ||
|
|
3353d57c84 | ||
|
|
0ed7db3b0c | ||
|
|
5ad58e5b25 | ||
|
|
e28f174f20 | ||
|
|
4a8aae7aff | ||
|
|
e780a863cf | ||
|
|
86c2724ceb | ||
|
|
6677cf39a9 | ||
|
|
0bac1ae3c4 | ||
|
|
a152d8bf06 | ||
|
|
badba682a0 | ||
|
|
90ae9e2a7e | ||
|
|
69cbede2f4 | ||
|
|
754c3d7e75 | ||
|
|
3814a2d3e9 | ||
|
|
97fb5678bc | ||
|
|
964249c1d4 | ||
|
|
607ae55db7 | ||
|
|
b7dae29b5f | ||
|
|
87d48a3428 | ||
|
|
82a8876209 | ||
|
|
325ad1914b | ||
|
|
d1bcef4df7 | ||
|
|
7c0ef24bfd | ||
|
|
038c3610ac | ||
|
|
62ea562f26 | ||
|
|
3f17e9b811 | ||
|
|
c08c077b8f | ||
|
|
b5efc28b0f | ||
|
|
ab55a5fe19 | ||
|
|
f5e493e5dc | ||
|
|
e6ab5eba07 | ||
|
|
d0f1b621fe | ||
|
|
79aca06be8 | ||
|
|
9e7f2e22dd | ||
|
|
d11c9c83f0 | ||
|
|
9e0622f91f | ||
|
|
ea7e30b9cc | ||
|
|
9c52ea8eca | ||
|
|
79db2920ca | ||
|
|
df9370a37f | ||
|
|
416d07fcce | ||
|
|
5384a32d58 | ||
|
|
18e9679553 | ||
|
|
c0cef1b178 | ||
|
|
045c1833e3 | ||
|
|
d4f8134de4 | ||
|
|
78596eb4c9 | ||
|
|
380a275248 | ||
|
|
a0b98cabf1 | ||
|
|
2ccc6dd141 | ||
|
|
a52336dc01 | ||
|
|
d70ae69dae | ||
|
|
5eeed269b6 | ||
|
|
eedc9e8f94 | ||
|
|
950af7b268 | ||
|
|
5f9e02012a | ||
|
|
548517b906 | ||
|
|
c29f902483 | ||
|
|
46919ba5a3 | ||
|
|
c4a4f33633 | ||
|
|
5176f0456f | ||
|
|
fb3e2635f5 | ||
|
|
380d1269d0 | ||
|
|
069e82c097 | ||
|
|
9a836a01fd | ||
|
|
eaffa01452 | ||
|
|
169fa21966 | ||
|
|
20e0dae7a3 | ||
|
|
358db187be | ||
|
|
fe17e3c4bb | ||
|
|
60e0d3c40c | ||
|
|
1753a6f654 | ||
|
|
d41e0e6c8c | ||
|
|
6f09d91a50 | ||
|
|
75c6008acc | ||
|
|
7c581cc7bb | ||
|
|
0715f008ca | ||
|
|
61f4c3760b | ||
|
|
ad1a464335 | ||
|
|
76ca9a1153 | ||
|
|
461a786ebc | ||
|
|
170c282f3e | ||
|
|
c7480c81b2 | ||
|
|
de62a0c11b | ||
|
|
dc35f63586 | ||
|
|
f5d7808792 | ||
|
|
1bf03b619a | ||
|
|
e109f5782c | ||
|
|
3c57cf0f65 | ||
|
|
475917c110 | ||
|
|
3b7ece1790 | ||
|
|
2a6c5faa79 | ||
|
|
7446cce387 | ||
|
|
15bb477e84 | ||
|
|
83071f3a1d | ||
|
|
eb579823db | ||
|
|
fe702bcaea | ||
|
|
0d87a6ea93 | ||
|
|
d9135c134b | ||
|
|
eb33b0c3c7 | ||
|
|
93471f4342 | ||
|
|
0db0bfdfd4 | ||
|
|
92e4dbcaa8 | ||
|
|
15443d155b | ||
|
|
21283962c4 | ||
|
|
1816aa2761 | ||
|
|
3d862b03eb | ||
|
|
411fca2ab3 | ||
|
|
8190907e39 | ||
|
|
05fbc0b80d | ||
|
|
347397f4da | ||
|
|
1d0e37950d | ||
|
|
7828df20fc | ||
|
|
a72f6b964f | ||
|
|
950dc05843 | ||
|
|
6a242635f7 | ||
|
|
dff1bd0d40 | ||
|
|
d678390b17 | ||
|
|
579b0bc5ef | ||
|
|
23b01416d7 | ||
|
|
3266b33b05 | ||
|
|
ad686553e5 | ||
|
|
e289f5acbd | ||
|
|
1573f73a0e | ||
|
|
efcac585ef | ||
|
|
89757bc7dd | ||
|
|
04e8391ca2 | ||
|
|
7f6bda6c76 | ||
|
|
b3b5063b50 | ||
|
|
a79d9aa094 | ||
|
|
b8f3200f8f | ||
|
|
b702566cb0 | ||
|
|
69b4d0a322 | ||
|
|
b2d17e3639 | ||
|
|
29ef585c31 | ||
|
|
d3d1c4ccb9 | ||
|
|
f53a260dff | ||
|
|
e8e86d1bf4 | ||
|
|
284d8f07fa | ||
|
|
aca1d26a76 | ||
|
|
3ef5c28bac | ||
|
|
de5c9f6047 | ||
|
|
b4b93af96a | ||
|
|
9d52e8853e | ||
|
|
2369c9b3be | ||
|
|
8ee0699b3e | ||
|
|
fcb18e56f0 | ||
|
|
14bbab0969 | ||
|
|
d84fe65460 | ||
|
|
047b8d1433 | ||
|
|
6e699a27ab | ||
|
|
fd45d20876 | ||
|
|
f977ff30ea | ||
|
|
27946aabae | ||
|
|
acdc935f70 | ||
|
|
28cd928a1b | ||
|
|
405f09ff00 | ||
|
|
9c1ac14bf1 | ||
|
|
920afd6045 | ||
|
|
60912da17c | ||
|
|
68eff16f64 | ||
|
|
475eb0565d | ||
|
|
cc58b6739b | ||
|
|
3f66a76727 | ||
|
|
deaec751b0 | ||
|
|
baff0ab7e4 | ||
|
|
45a56f6ca3 | ||
|
|
8a14756cea | ||
|
|
c39b0f1e02 | ||
|
|
27d8706023 | ||
|
|
ef434e5491 | ||
|
|
88c960dfad | ||
|
|
d028dee724 | ||
|
|
d17a9e2ef1 | ||
|
|
85399792fb | ||
|
|
a02ed17233 | ||
|
|
3dc5f1c843 | ||
|
|
2a077ecf81 | ||
|
|
1ae5fe8cc3 | ||
|
|
658ac6b759 | ||
|
|
08973f9b50 | ||
|
|
1abd3e2aa7 | ||
|
|
49929dd4a0 | ||
|
|
15cce31886 | ||
|
|
0302556635 | ||
|
|
1ae8ff4b99 | ||
|
|
ca75a702ca | ||
|
|
14096aeaf8 | ||
|
|
b9ebef7817 | ||
|
|
b4f6f8f2d0 | ||
|
|
d27b2f9727 | ||
|
|
fd0e58e957 | ||
|
|
eaf08c60d4 | ||
|
|
6e900d111c | ||
|
|
cf6a192ccc | ||
|
|
515a8d2b54 | ||
|
|
4018604028 | ||
|
|
55d70f8454 | ||
|
|
fd08b5d4ee | ||
|
|
f7cd18d4ad | ||
|
|
078e098b28 | ||
|
|
b2f761ef76 | ||
|
|
cdd894b50c | ||
|
|
323524ac1b | ||
|
|
bc04ca3db7 | ||
|
|
434d2c76e0 | ||
|
|
009aecc72a | ||
|
|
ddd7885038 | ||
|
|
a46a7f5429 | ||
|
|
fe5e7011cc | ||
|
|
e12da9e82d | ||
|
|
dddf4d12df | ||
|
|
96033cbc78 | ||
|
|
b821d3fd09 | ||
|
|
1b6dcb00e7 | ||
|
|
938e4e1ad7 | ||
|
|
cd99b04507 | ||
|
|
ba49cde9f6 | ||
|
|
3f82e25074 | ||
|
|
f132f88059 | ||
|
|
38ff9e748a | ||
|
|
c299e0c3e2 | ||
|
|
d4127889c3 | ||
|
|
994ead0e3e | ||
|
|
3e5900b1e0 | ||
|
|
f579c43b29 | ||
|
|
f2fee6779d | ||
|
|
4abb7ecc46 | ||
|
|
7e8fa56bf5 | ||
|
|
ce02ba86db | ||
|
|
7fcb585268 | ||
|
|
e143280c6c | ||
|
|
f82d36afb0 | ||
|
|
e14d899ac4 | ||
|
|
6a16cbabd2 | ||
|
|
e52711d592 | ||
|
|
abc01538f9 | ||
|
|
6eaf4359b5 | ||
|
|
728959332b | ||
|
|
d58a0981c4 | ||
|
|
23cdcffb12 | ||
|
|
944363765a | ||
|
|
ace3bdbb8f | ||
|
|
7926b9fd0f | ||
|
|
4617b34478 | ||
|
|
7fcda1f0e7 | ||
|
|
a72842cafe | ||
|
|
8ffbfd5653 | ||
|
|
955681886f | ||
|
|
066656adf8 | ||
|
|
a5db2a49a8 | ||
|
|
4a5dd111d7 | ||
|
|
73d260c2db | ||
|
|
c86e895ea9 | ||
|
|
208bdb28fe | ||
|
|
70b3b2f75b | ||
|
|
8f35603493 | ||
|
|
1c592e6713 | ||
|
|
b0d22fc035 | ||
|
|
7f5c9dd4ef | ||
|
|
76e2a3ef46 | ||
|
|
9c48b0054f | ||
|
|
72db97df79 | ||
|
|
a8b8b49dce | ||
|
|
26f9939bff | ||
|
|
37ec94c157 | ||
|
|
b336b2e54e | ||
|
|
91a0cecffa | ||
|
|
a59204cd99 | ||
|
|
cd61c3fd8d | ||
|
|
34082cb122 | ||
|
|
70d92cef40 | ||
|
|
b3c1a97f3d | ||
|
|
f3aa63aa13 | ||
|
|
27c8753b3a | ||
|
|
d352007cd2 | ||
|
|
3fe95f4362 | ||
|
|
f13a201c56 | ||
|
|
d4ca8c77b5 | ||
|
|
920690e504 | ||
|
|
282eebce76 | ||
|
|
abeddf8c65 | ||
|
|
826cafc3a9 | ||
|
|
04422376f2 | ||
|
|
f88f805f28 | ||
|
|
bfe4ec2818 | ||
|
|
9e07d636a8 | ||
|
|
b10211dcf9 | ||
|
|
9a7c88efbf | ||
|
|
a0750dddfc | ||
|
|
90b0100048 | ||
|
|
bbb5763b8c | ||
|
|
b029070d94 | ||
|
|
499b2dc748 | ||
|
|
153f1cbd31 | ||
|
|
9247f96679 | ||
|
|
d18d138f73 | ||
|
|
36f0686cf4 | ||
|
|
963e637931 | ||
|
|
570d5bc60b | ||
|
|
4c37af4976 | ||
|
|
f09fbdba72 | ||
|
|
17cd4b0460 | ||
|
|
02e234a627 | ||
|
|
79c13c7f31 | ||
|
|
c86ae98d04 | ||
|
|
aec129f6bb | ||
|
|
b7d6e06530 | ||
|
|
d1fc654e36 | ||
|
|
1e99dbe589 | ||
|
|
80a5f6188b | ||
|
|
265728dc38 | ||
|
|
863a29dbe6 | ||
|
|
b28330ed56 | ||
|
|
4ec7666985 | ||
|
|
63cf643300 | ||
|
|
85c167a5f4 | ||
|
|
1f4fbbd5f8 | ||
|
|
d9405428d6 | ||
|
|
91280c2177 | ||
|
|
9abba00fe1 | ||
|
|
cd2b36e5c7 | ||
|
|
8552d7e825 | ||
|
|
5e1e9399cc | ||
|
|
6a5e4af0f3 | ||
|
|
38b31fce3c | ||
|
|
c5fa140d07 | ||
|
|
e13f1cf73a | ||
|
|
03ff3f7257 | ||
|
|
4c990f43a7 | ||
|
|
75497aa031 | ||
|
|
a8c69d1cec | ||
|
|
dc4c1b9b2f | ||
|
|
1ef654578e | ||
|
|
a610fe2fd2 | ||
|
|
97cbb8a03f | ||
|
|
0e28127851 | ||
|
|
c7c41805d8 | ||
|
|
6cef45d3bd | ||
|
|
ded05a5408 | ||
|
|
361a4cc242 | ||
|
|
6cfed6c229 | ||
|
|
e8d62ab190 | ||
|
|
7a027c4149 | ||
|
|
425de91c57 | ||
|
|
d50aef3832 | ||
|
|
afe2b1b8ce | ||
|
|
566fff6c0c | ||
|
|
88b9c926c4 | ||
|
|
2db13a7ec1 | ||
|
|
4d6b740f30 | ||
|
|
9a8025777b | ||
|
|
715bc2ecbd | ||
|
|
e05c456f99 | ||
|
|
3c4459bbac | ||
|
|
cd128c5683 | ||
|
|
b4cfec133d | ||
|
|
408c5351bf | ||
|
|
6fd295211b | ||
|
|
6e9db6d7c9 | ||
|
|
cd4964401f | ||
|
|
7572febd8d | ||
|
|
90180b890d | ||
|
|
1e6b0ac6a9 | ||
|
|
6ab9b5fba7 | ||
|
|
b9cbf749be | ||
|
|
ea7de1c085 | ||
|
|
eaa681dfc3 | ||
|
|
2d79c3df9f | ||
|
|
649134bd22 | ||
|
|
abf3192476 | ||
|
|
b7a2312994 | ||
|
|
4d6d16a58b | ||
|
|
4d389dcfa3 | ||
|
|
01fa48df18 | ||
|
|
3fedf57b72 | ||
|
|
3140c2e90c | ||
|
|
5fbab18684 | ||
|
|
1fc7c4d53d | ||
|
|
2f14216e49 | ||
|
|
ae602a2f6f | ||
|
|
08c701a3eb | ||
|
|
40cebf61c2 | ||
|
|
c6fa18fe8d | ||
|
|
3598d5efdf | ||
|
|
0b305ec22f | ||
|
|
fe694de119 | ||
|
|
74bb03f92e | ||
|
|
3446abd7d8 | ||
|
|
ed0cc6c9be | ||
|
|
ef8d6ddf07 | ||
|
|
3a3dd05393 | ||
|
|
f6417a3bd5 | ||
|
|
49df4425ea | ||
|
|
758129d035 | ||
|
|
608e6e7a1d | ||
|
|
6cffdba578 | ||
|
|
9b64cc270a | ||
|
|
cc5526f7f3 | ||
|
|
77ed94a2e8 | ||
|
|
54657e5dc7 | ||
|
|
62bc3dd614 | ||
|
|
ec7f10d119 | ||
|
|
50f22ccf25 | ||
|
|
9ae4a5053a | ||
|
|
807bddeace | ||
|
|
74ce6ca353 | ||
|
|
cb10380498 | ||
|
|
490f850834 | ||
|
|
9bd75acdba | ||
|
|
e8e8bd3262 | ||
|
|
55411f7979 | ||
|
|
40e8d39f75 | ||
|
|
bd81680cdb | ||
|
|
fa557969df | ||
|
|
2a5f92eaf2 | ||
|
|
7cd5dd9929 | ||
|
|
a6f18efbc7 | ||
|
|
fedaa33741 | ||
|
|
1eadd06f9d | ||
|
|
4f1c2731bf | ||
|
|
00b52794aa | ||
|
|
f9c2799526 | ||
|
|
bd1278aab4 | ||
|
|
4f8063214f | ||
|
|
fcdf2fba0e | ||
|
|
a8479fb315 | ||
|
|
9fc473efb1 | ||
|
|
712785dc44 | ||
|
|
f904057944 | ||
|
|
4cc750ec0f | ||
|
|
f97f43f49b | ||
|
|
826e42085c | ||
|
|
4e555f3198 | ||
|
|
df5d23abbc | ||
|
|
8041f6b268 | ||
|
|
f22e2d6d93 | ||
|
|
d770b218f2 | ||
|
|
93d640206f | ||
|
|
19d66a3cb2 | ||
|
|
9819668246 | ||
|
|
944a29814b | ||
|
|
b09ef174ab | ||
|
|
aebf33c2f0 | ||
|
|
5e59b7b561 | ||
|
|
f47efb8f5f | ||
|
|
c562b9525a | ||
|
|
06593afb95 | ||
|
|
9d09869f80 | ||
|
|
32ecbfc718 | ||
|
|
ae6fdeef29 | ||
|
|
bbd0cdee78 | ||
|
|
76ab984e0b | ||
|
|
c61c9c86c8 | ||
|
|
90e29f8b43 | ||
|
|
e7ab1e4296 | ||
|
|
e5017a2031 | ||
|
|
02ff35a306 | ||
|
|
c3b90d7952 | ||
|
|
f9ba58c6f1 | ||
|
|
69da2dbd18 | ||
|
|
308b4a9ecd | ||
|
|
1256f02c13 | ||
|
|
b3ee050eb3 | ||
|
|
ead26a92c4 | ||
|
|
2e4dca4d89 | ||
|
|
b4d1e3eaff | ||
|
|
8a5fa4f158 | ||
|
|
fcacca3495 | ||
|
|
6e71478598 | ||
|
|
e982a8a799 | ||
|
|
b591947fac | ||
|
|
6ed171fc8f | ||
|
|
48f68f82dd | ||
|
|
70fbc0c24a | ||
|
|
27049809ef | ||
|
|
5b4ff8b5c2 | ||
|
|
0f581ad9cb | ||
|
|
edf9163ca1 | ||
|
|
83b6e82c1d | ||
|
|
c0e54d44c4 | ||
|
|
03f20e7ec7 | ||
|
|
c1e9d01099 | ||
|
|
0f4f2674f7 | ||
|
|
6e752bba1c | ||
|
|
077d6ba8a4 | ||
|
|
0e3b9d9c1d | ||
|
|
21edb60c16 | ||
|
|
b38cd59b42 | ||
|
|
fbf88be324 | ||
|
|
cfa228083d | ||
|
|
b9b36e31d2 | ||
|
|
f25259e78b | ||
|
|
7e24125fe3 | ||
|
|
e237f76b0f | ||
|
|
a5c9a8cea6 | ||
|
|
7dca17f2aa | ||
|
|
2befc5c172 | ||
|
|
8a667cb8a2 | ||
|
|
fde1e96104 | ||
|
|
13ad09022b | ||
|
|
7c248803dd | ||
|
|
b4b440f871 | ||
|
|
86c29a89c2 | ||
|
|
ac3255efb5 | ||
|
|
24acd8e2fe | ||
|
|
a243d7a0ff | ||
|
|
530715af17 | ||
|
|
10e2c0c20f | ||
|
|
17b1de0825 | ||
|
|
882478edb1 | ||
|
|
a5e3343f06 | ||
|
|
168e4427dc | ||
|
|
069a44774e | ||
|
|
e67d60d088 | ||
|
|
d1239e2e22 | ||
|
|
3e1ee77c14 | ||
|
|
973f1386d0 | ||
|
|
077ed36a4a | ||
|
|
1841ee32f6 | ||
|
|
d16a1938c2 | ||
|
|
28f3c3be62 | ||
|
|
1d2e0fed0e | ||
|
|
065048ee46 | ||
|
|
465a982080 | ||
|
|
8827fae5f3 | ||
|
|
3461a2d36c | ||
|
|
19891a39e4 | ||
|
|
b751959f42 | ||
|
|
721a599ed3 | ||
|
|
9caf10774b | ||
|
|
b1dd37025f | ||
|
|
9b49a321bb | ||
|
|
f8b0214519 | ||
|
|
4bcc9dbc3a | ||
|
|
4b44f3c718 | ||
|
|
230ce5e405 | ||
|
|
ca2bc58881 | ||
|
|
8271ccb005 | ||
|
|
e766a9a581 | ||
|
|
b66a0a9b12 | ||
|
|
fb7733f2e1 | ||
|
|
d4fe8943a4 | ||
|
|
c3d6e845d0 | ||
|
|
ba7b0843df | ||
|
|
5bbd6d474a | ||
|
|
e3d1d2760a | ||
|
|
4489e171db | ||
|
|
9c32bf2328 | ||
|
|
1c687f84b7 | ||
|
|
a574687253 | ||
|
|
b7464a3742 | ||
|
|
f319d2f498 | ||
|
|
61bb42ae3a | ||
|
|
e5bd0dfe49 | ||
|
|
f30adeb613 | ||
|
|
ca173694f8 | ||
|
|
9b04008545 | ||
|
|
83ca847312 | ||
|
|
19dcb55980 | ||
|
|
f1f1e19670 | ||
|
|
1bb23e2198 | ||
|
|
adfd7baa46 | ||
|
|
c0b33e1ea5 | ||
|
|
066bb84e5c | ||
|
|
e230ee77fd | ||
|
|
e113373eb6 | ||
|
|
dd602cd3b6 | ||
|
|
ce029693cf | ||
|
|
497af1fe00 | ||
|
|
b5e5bc0b01 | ||
|
|
051b1875e7 | ||
|
|
a673f4a7f2 | ||
|
|
bd1804589a | ||
|
|
26409139a0 | ||
|
|
62e44488a9 | ||
|
|
5def82024d | ||
|
|
cdb6b6d8f8 | ||
|
|
4705c3ec8c | ||
|
|
ded547a296 | ||
|
|
c61655bb08 | ||
|
|
eb25eeef76 | ||
|
|
1de015a3ab | ||
|
|
b83dbbe5d4 | ||
|
|
ed82534030 | ||
|
|
94481421ac | ||
|
|
3463563e42 | ||
|
|
b6fac871e0 | ||
|
|
6221193d8c | ||
|
|
83f14010b1 | ||
|
|
adb6f8327d | ||
|
|
bb7ede7998 | ||
|
|
b6103a61cd | ||
|
|
8ac4b2c7dd | ||
|
|
ed15907e69 | ||
|
|
022f358e16 | ||
|
|
661ce426dd | ||
|
|
068b89fe72 | ||
|
|
0924ebe33d | ||
|
|
97b7429da5 | ||
|
|
cc1c392418 | ||
|
|
1e8ce0bebf | ||
|
|
badcdf78c1 | ||
|
|
026bf60751 | ||
|
|
6aee85a591 | ||
|
|
f8e150c65d | ||
|
|
694d0ac27c | ||
|
|
2ad683f7dd | ||
|
|
da367ee93f | ||
|
|
190d2304bc | ||
|
|
bdc39b25b7 | ||
|
|
9b17434c64 | ||
|
|
65426df448 | ||
|
|
05977c7792 | ||
|
|
804e92e9b7 | ||
|
|
02420b5e94 | ||
|
|
b3f7016515 | ||
|
|
96c7166a26 | ||
|
|
4262a1b639 | ||
|
|
ad9316377d | ||
|
|
c282f9c2d4 | ||
|
|
52a8ce60d9 | ||
|
|
517a87438e | ||
|
|
cf4d03b772 | ||
|
|
8877be24b5 | ||
|
|
1bbe9e19de | ||
|
|
6728f7c796 | ||
|
|
5488f4ba3d | ||
|
|
026b4aa7cf | ||
|
|
ab21227bee | ||
|
|
de0afbabde | ||
|
|
ef033e477c | ||
|
|
35b2cbc490 | ||
|
|
ecc031422e | ||
|
|
7451223e09 | ||
|
|
9a1ae415ce | ||
|
|
e09fe00563 | ||
|
|
0d8ae55422 | ||
|
|
ee508a7927 | ||
|
|
ec8b282767 | ||
|
|
face17249d | ||
|
|
ce182c9fa5 | ||
|
|
b25a59e748 | ||
|
|
3b0c50329a | ||
|
|
420245e66a | ||
|
|
dd9db08431 | ||
|
|
f8122d5fb3 | ||
|
|
90975c31d2 | ||
|
|
8dd630b43b | ||
|
|
25a41858fd | ||
|
|
d20986949b | ||
|
|
bc60281379 | ||
|
|
84b3df80e2 | ||
|
|
8923315995 | ||
|
|
528b4584ac | ||
|
|
c14d2c3867 | ||
|
|
be3081de42 | ||
|
|
c46e0d9811 | ||
|
|
f00df1c17a | ||
|
|
684f8769a1 | ||
|
|
956888956e | ||
|
|
d8de1f86cf | ||
|
|
4be4b84622 | ||
|
|
ce1fbb5e05 | ||
|
|
c29a7cd693 | ||
|
|
b69361abf2 | ||
|
|
e7e50bee35 | ||
|
|
2cef319a32 | ||
|
|
9df7c18181 | ||
|
|
b4229070e6 | ||
|
|
c11632f8ef | ||
|
|
972cd5e645 | ||
|
|
87e606699d | ||
|
|
5da4dee1cb | ||
|
|
0d44809496 | ||
|
|
74e0108d76 | ||
|
|
243fe9c756 | ||
|
|
276dff3ae1 | ||
|
|
e1ed469ec9 | ||
|
|
d60ac945fc | ||
|
|
82d0f81498 | ||
|
|
f176c1f117 | ||
|
|
341f4c006f | ||
|
|
a86721f992 | ||
|
|
d50805da83 | ||
|
|
7532cec613 | ||
|
|
6ef4e44c88 | ||
|
|
c147acedda | ||
|
|
b60c4e24e6 | ||
|
|
b4d8c82e04 | ||
|
|
4c197400da | ||
|
|
b422c44603 | ||
|
|
8fd7a5ad8f | ||
|
|
5cfcf77c32 | ||
|
|
e15a40f7d9 | ||
|
|
62905aa073 | ||
|
|
b98d3f7be2 | ||
|
|
1808a0473c | ||
|
|
1d8d609307 | ||
|
|
c8f9645eae | ||
|
|
0b83f75e14 | ||
|
|
ee2199afce | ||
|
|
b7a3719815 | ||
|
|
2b7a5c31d4 | ||
|
|
015c72292f | ||
|
|
303619d060 | ||
|
|
219c8c0ccc | ||
|
|
d3158cc8a1 | ||
|
|
d4c69004cc | ||
|
|
c0cd027904 | ||
|
|
12675a1586 | ||
|
|
a51d89f559 | ||
|
|
2c4d41f8da | ||
|
|
6491fc3ddd | ||
|
|
a51189e6f6 | ||
|
|
3bdee559e6 | ||
|
|
de67e11704 | ||
|
|
180380339d | ||
|
|
1d7be34787 | ||
|
|
d8e3c1a390 | ||
|
|
c4bf136fbd | ||
|
|
dc567ea94f | ||
|
|
b4133dbb7e | ||
|
|
3c8f2855bf | ||
|
|
9583a2a9e9 | ||
|
|
37646b4f63 | ||
|
|
96adf59413 | ||
|
|
6fadc6f4f8 | ||
|
|
4a2be8898d | ||
|
|
8bfbc93cf8 | ||
|
|
cb139b11ca | ||
|
|
e1e3ccc599 | ||
|
|
00e7d0793a | ||
|
|
6a0e2528db | ||
|
|
c612810a2a | ||
|
|
21243fd7a0 | ||
|
|
ca1b010529 | ||
|
|
a2d7297382 | ||
|
|
6052103ebf | ||
|
|
383cbe953f | ||
|
|
75d20f7e0f | ||
|
|
9fe7644f0a | ||
|
|
863c41d550 | ||
|
|
5280f71e15 | ||
|
|
df7b0772c8 | ||
|
|
4ea63506a5 | ||
|
|
fae1253df6 | ||
|
|
d9e074762b | ||
|
|
096cd4f9b9 | ||
|
|
39f6c8959e | ||
|
|
3824bcb359 | ||
|
|
3b56ce55ed | ||
|
|
31b8c24b4b | ||
|
|
a0b97b51c9 | ||
|
|
f45925d34d | ||
|
|
90a34c1295 | ||
|
|
42717759b6 | ||
|
|
695ca73817 | ||
|
|
f21a95940e | ||
|
|
7fe21927c9 | ||
|
|
8338777318 | ||
|
|
e2c1b65e9e | ||
|
|
e4f397d593 | ||
|
|
c0893aa500 | ||
|
|
726d78d4f8 | ||
|
|
918e31483b | ||
|
|
54960a9a99 | ||
|
|
71feae1ee1 | ||
|
|
317bf35c9c | ||
|
|
73b8fa039f | ||
|
|
8e1412c014 | ||
|
|
8cc777013c | ||
|
|
750f98648d | ||
|
|
995f36075a | ||
|
|
74a56142f6 | ||
|
|
cd9fc3e5a3 | ||
|
|
c8001a615e | ||
|
|
7be3266543 | ||
|
|
d0f013c536 | ||
|
|
d8f36cd716 | ||
|
|
9718d42c13 | ||
|
|
ed46dca3da | ||
|
|
07f40b5c15 | ||
|
|
e004968328 | ||
|
|
4a71d6fa73 | ||
|
|
b29db2c76c | ||
|
|
a817e001db | ||
|
|
35341a9caf | ||
|
|
7e469d6bea | ||
|
|
c1a5999471 | ||
|
|
9a193be6f3 | ||
|
|
9464f46bf6 | ||
|
|
f24a12ef45 | ||
|
|
f36252ac87 | ||
|
|
2653bf49d0 | ||
|
|
781b51ea88 | ||
|
|
36147e7b2f | ||
|
|
8842a097ab | ||
|
|
2a1445c060 | ||
|
|
bd24a34f8f | ||
|
|
fcee9b2003 | ||
|
|
923a2db2d0 | ||
|
|
59c2b6fa0f | ||
|
|
9984c4453f | ||
|
|
b439c8db3b | ||
|
|
cb828d577e | ||
|
|
4b48841caa | ||
|
|
adf422687a | ||
|
|
3a99faa85c | ||
|
|
e245659e5f | ||
|
|
17d85512a2 | ||
|
|
727d967823 | ||
|
|
54a51d9d82 | ||
|
|
5a0289a4b6 | ||
|
|
790181a720 | ||
|
|
a440b8d3bc | ||
|
|
e64025fb30 | ||
|
|
904d1650cd | ||
|
|
4765d96cec | ||
|
|
6a85c9588f | ||
|
|
7a6157fd12 | ||
|
|
f8cf6f9fb6 | ||
|
|
2cd39eb5c9 | ||
|
|
f823d7527d | ||
|
|
70732b7f94 | ||
|
|
6bf9858c94 | ||
|
|
f46c9847ce | ||
|
|
7a6480a5a1 | ||
|
|
6e1b95ff59 | ||
|
|
faf39b0e9c | ||
|
|
bae93001f0 | ||
|
|
a55985b415 | ||
|
|
794242f42f | ||
|
|
16846cfa26 | ||
|
|
a59e55642b | ||
|
|
78b074499b | ||
|
|
83851ba366 | ||
|
|
740f684324 | ||
|
|
f5240ed605 | ||
|
|
1d32c0f2cd | ||
|
|
a0d251eb9a | ||
|
|
6ecda5b99b | ||
|
|
61d4c735b3 | ||
|
|
d083f4bdf9 | ||
|
|
3175fd6518 | ||
|
|
9586c67d53 | ||
|
|
8db93a4ee4 | ||
|
|
585f35546f | ||
|
|
4eaadab2bc | ||
|
|
7e3781a662 | ||
|
|
286ac988c7 | ||
|
|
deaa8a47be | ||
|
|
b1d0ae9fee | ||
|
|
d69a5bfcda | ||
|
|
b63dfb136f | ||
|
|
aa58d5fe75 | ||
|
|
aab1c3e85b | ||
|
|
cbd00ca192 | ||
|
|
3e4392d4ad | ||
|
|
f5d85b8594 | ||
|
|
b1966a2a85 | ||
|
|
0516ebaa57 | ||
|
|
679fa7546c | ||
|
|
e76891b280 | ||
|
|
4d5da851ab | ||
|
|
d8c848cfae | ||
|
|
2df4c024f4 | ||
|
|
24d4971abe | ||
|
|
cc32cc8257 | ||
|
|
bd52e9c72d | ||
|
|
40b62be293 | ||
|
|
3db52a6c41 | ||
|
|
43f7ebb225 | ||
|
|
bd070a8af9 | ||
|
|
8e379b14f1 | ||
|
|
2d0cb64ec3 | ||
|
|
6291f48263 | ||
|
|
f103bda6fe | ||
|
|
7ea54b6f03 | ||
|
|
f4d04f6691 | ||
|
|
a2efc65226 | ||
|
|
c5117c7898 | ||
|
|
86761c405c | ||
|
|
4fc419ba74 | ||
|
|
8686012302 | ||
|
|
90f3f4b56f | ||
|
|
19edef364f | ||
|
|
ce307ddc45 | ||
|
|
948de91102 | ||
|
|
ab751c3b27 | ||
|
|
d01df11a21 | ||
|
|
0e4c9efdbe | ||
|
|
80438e991c | ||
|
|
d0e224dbe1 | ||
|
|
53138e405e | ||
|
|
a3a1e0d621 | ||
|
|
92efaab611 | ||
|
|
a87156690b | ||
|
|
fa01192bd3 | ||
|
|
0c2d9900f3 | ||
|
|
ebd34d0d0a | ||
|
|
f3ea9bcb47 | ||
|
|
33c5f74bf3 | ||
|
|
babb1f3956 | ||
|
|
c0b48208e6 | ||
|
|
b00b830f21 | ||
|
|
a24c56b527 | ||
|
|
bd1f4b06bc | ||
|
|
c3cbeebf91 | ||
|
|
bba3f071ee | ||
|
|
eaf5bb245b | ||
|
|
52bc01dede | ||
|
|
1b3020d99a | ||
|
|
046f0ce79d | ||
|
|
79c40bc505 | ||
|
|
c822777ed3 | ||
|
|
29ebb5ad72 | ||
|
|
4cdf9d9b63 | ||
|
|
120f81e3f5 | ||
|
|
0c70621935 | ||
|
|
9efa70a0ee | ||
|
|
b5dd14758b | ||
|
|
1cd8881088 | ||
|
|
e0654d8e08 | ||
|
|
b30ffc19cd | ||
|
|
3f0b64775f | ||
|
|
02bfc295e6 | ||
|
|
fcf957db9c | ||
|
|
fe62c7fcd4 | ||
|
|
a15e87f23c | ||
|
|
4375c1ecd3 | ||
|
|
d8d8ea3bd0 | ||
|
|
613c888d47 | ||
|
|
4c665f3749 | ||
|
|
307fe595f0 | ||
|
|
341d4ebf24 | ||
|
|
3adde31a47 | ||
|
|
6acd19977c | ||
|
|
df9bddb85f | ||
|
|
3d6928fcdc | ||
|
|
f242090f8d | ||
|
|
dd071d4926 | ||
|
|
e38ccdee96 | ||
|
|
7da26f92bf | ||
|
|
14c04c8ce1 | ||
|
|
5e928270bb | ||
|
|
9661a938c5 | ||
|
|
6ca806f733 | ||
|
|
ebb8a1c60f | ||
|
|
01d8d8b34d | ||
|
|
17f280708e | ||
|
|
3a0e3b780a | ||
|
|
3e1ca23348 | ||
|
|
22317cd092 | ||
|
|
9410d282cc | ||
|
|
3e732ddbe6 | ||
|
|
e4a7c2713f | ||
|
|
32e40ed930 | ||
|
|
1406b3566a | ||
|
|
7022a5a992 | ||
|
|
ef98cec45a | ||
|
|
c0477f52f5 | ||
|
|
111a0dcdca | ||
|
|
0113623294 | ||
|
|
c73857425a | ||
|
|
a53374f94f | ||
|
|
d0f771a78e | ||
|
|
0682433113 | ||
|
|
9f84983680 | ||
|
|
025b1e4a61 | ||
|
|
5fbcf19389 | ||
|
|
0b4f4f915a | ||
|
|
9c7cce6d07 | ||
|
|
fd3ae64d9d | ||
|
|
ece4e9f3e1 | ||
|
|
fda982c7d1 | ||
|
|
a4acf8caa7 | ||
|
|
da08b93e25 | ||
|
|
1f8229ca5c | ||
|
|
4afdbb94cf | ||
|
|
d591ec05c0 | ||
|
|
9c971a0387 | ||
|
|
e78d935a54 | ||
|
|
1304f59bef | ||
|
|
7a1aa3fb2a | ||
|
|
712fd47880 | ||
|
|
802c5cf20d | ||
|
|
8d2036636f | ||
|
|
b0923098e4 | ||
|
|
9673871c48 | ||
|
|
d08d2ab786 | ||
|
|
47608f26d5 | ||
|
|
6dee385660 | ||
|
|
ec7d82d663 | ||
|
|
567b47f793 | ||
|
|
f2603c030d | ||
|
|
2d24ea0e43 | ||
|
|
7f70f51c34 | ||
|
|
a5c3424d17 | ||
|
|
9cb3d2fde6 | ||
|
|
8526ba4d04 | ||
|
|
ffcd58ac46 | ||
|
|
1d02f1362c | ||
|
|
278a636398 | ||
|
|
831ca7c3cb | ||
|
|
5604cf1356 | ||
|
|
b339b89c4b | ||
|
|
cca24c8cbd | ||
|
|
67cc30305b | ||
|
|
53102be531 | ||
|
|
6b85d2398c | ||
|
|
7fd5ee818a | ||
|
|
04f149c072 | ||
|
|
c3edfd8acf | ||
|
|
9a7fc7c65c | ||
|
|
cc131cbf60 | ||
|
|
2c4e415a66 | ||
|
|
9bc4c3a210 | ||
|
|
b9198e5b28 | ||
|
|
67a5c44d06 | ||
|
|
39485f692c | ||
|
|
6a7c2dd778 | ||
|
|
20750ebddc | ||
|
|
b8cbcb8619 | ||
|
|
37fb2b1fb3 | ||
|
|
6950394054 | ||
|
|
a0ee00bc2a | ||
|
|
dd58bdc545 | ||
|
|
67d46cda28 | ||
|
|
06e04e1333 | ||
|
|
e8b745384c | ||
|
|
5397175314 | ||
|
|
62636d73dc | ||
|
|
374498b445 | ||
|
|
d63bfacc10 | ||
|
|
9d697647c3 | ||
|
|
e674b71081 | ||
|
|
2b9e94b596 | ||
|
|
3411683e47 | ||
|
|
e2ca7d73a6 | ||
|
|
f2f5a9c488 | ||
|
|
b0063d2c2e | ||
|
|
c55162e745 | ||
|
|
7e590b2179 | ||
|
|
d157e702c1 | ||
|
|
5681f4b74a | ||
|
|
2344917ce7 | ||
|
|
e42ed6f581 | ||
|
|
3633bb68d7 | ||
|
|
c178a7890e | ||
|
|
7fea640a84 | ||
|
|
dea9c2e998 | ||
|
|
53a97f07b8 | ||
|
|
e0ceb2bcd7 | ||
|
|
f57611c1c9 | ||
|
|
b5ab58790a | ||
|
|
7c5ca832e8 | ||
|
|
491d68088e | ||
|
|
c478556c67 | ||
|
|
83d5b26922 | ||
|
|
4810cea1ab | ||
|
|
73657915a3 | ||
|
|
cceb140eb1 | ||
|
|
f147b4ef5f | ||
|
|
102c1fcd96 | ||
|
|
48e89b36c2 | ||
|
|
d1386ee8f3 | ||
|
|
1c32dad7d5 | ||
|
|
362e9cf1e6 | ||
|
|
d13640c466 | ||
|
|
36ace2bb7e | ||
|
|
533a482d2c | ||
|
|
14cbb06bdb | ||
|
|
c2fb36a6d1 | ||
|
|
0888a08c12 | ||
|
|
8a6c81c607 | ||
|
|
90bd50c38e | ||
|
|
aad11d5d76 | ||
|
|
b9c140160c | ||
|
|
497efafabf | ||
|
|
3e78fdc737 | ||
|
|
a8b045d95b | ||
|
|
ab9c5f68b8 | ||
|
|
dc2ffe6393 | ||
|
|
7807a68a74 | ||
|
|
99d11db2ed | ||
|
|
e1346ce833 | ||
|
|
a4e0fc66b4 | ||
|
|
046a00aaa0 | ||
|
|
08fe70bd69 | ||
|
|
df9b151d98 | ||
|
|
ba12b17e94 | ||
|
|
911d18ed31 | ||
|
|
b16e61af1a | ||
|
|
41ad6bf3e0 | ||
|
|
c16916455d | ||
|
|
83376e0eac | ||
|
|
95c2e1fea9 | ||
|
|
b38e0a4938 | ||
|
|
5a3b15abcb | ||
|
|
e28ad0cc33 | ||
|
|
9e2e10f7fb | ||
|
|
06f9842a3a | ||
|
|
d477b34d27 | ||
|
|
999f2ab3b0 | ||
|
|
99f5f4a2ad | ||
|
|
5d79e3c833 | ||
|
|
29f1f86f03 | ||
|
|
7ea5f15371 | ||
|
|
a7fd5288a1 | ||
|
|
3c0c61cb97 | ||
|
|
771e9cd92b | ||
|
|
e4548ac167 | ||
|
|
c5e28eea7a | ||
|
|
a0e187d1c7 | ||
|
|
650c9029e9 | ||
|
|
611cb37878 | ||
|
|
2d54d40149 | ||
|
|
58493c77c1 | ||
|
|
78f28e8ed3 | ||
|
|
5625442913 | ||
|
|
6961d8d9ac | ||
|
|
24b36271fe | ||
|
|
0bb3ec0d02 | ||
|
|
53ba824106 | ||
|
|
396942fd17 | ||
|
|
8a92c6c44e | ||
|
|
0151b7af4f | ||
|
|
06ccb07c09 | ||
|
|
25a00f5585 | ||
|
|
2b0156fb13 | ||
|
|
88aac4accf | ||
|
|
171be5630c | ||
|
|
c4b6011dd9 | ||
|
|
b90e68f23c | ||
|
|
61fdf8150b | ||
|
|
09ffb53bed | ||
|
|
a0ee0a7387 | ||
|
|
5f048f67bb | ||
|
|
6b8a915609 | ||
|
|
d6aad6f97e | ||
|
|
38485396ff | ||
|
|
6e37cdb862 | ||
|
|
fc82c1a1a0 | ||
|
|
db9103c568 | ||
|
|
77ab67edcd | ||
|
|
f24bd71576 | ||
|
|
5638b3b0ae | ||
|
|
76a1dbd8f1 | ||
|
|
71464ab1fe | ||
|
|
6698957104 | ||
|
|
8eb47b8b69 | ||
|
|
7e9edd5d21 | ||
|
|
12c11e88d5 | ||
|
|
f653dbb8f9 | ||
|
|
9e469414b3 | ||
|
|
0d3b88c33b | ||
|
|
57ae95616f | ||
|
|
b6f5c240e0 | ||
|
|
93810fa81a | ||
|
|
a4fdf03dbd | ||
|
|
4d058110e0 | ||
|
|
fffedb608b | ||
|
|
a20b142014 | ||
|
|
9f672322e0 | ||
|
|
1c8c1a0ba9 | ||
|
|
bfbdb3681b | ||
|
|
b9f3d8736e | ||
|
|
290d6d8f54 | ||
|
|
0e470df4b6 | ||
|
|
68588d1bb2 | ||
|
|
3cd7d390c1 | ||
|
|
a0bf3c4d1b | ||
|
|
2cb9e0efbd | ||
|
|
5b81cc24e7 | ||
|
|
b6c53c8795 | ||
|
|
0aef81100f | ||
|
|
137e7c0b7b | ||
|
|
b87892dc9b | ||
|
|
5a640831e2 | ||
|
|
c73b11f62c | ||
|
|
12cd3c0af7 | ||
|
|
51a50a44a4 | ||
|
|
1e3c024b87 | ||
|
|
1c0c1db43b | ||
|
|
fac3713af4 | ||
|
|
a709f67a00 | ||
|
|
790aea27db | ||
|
|
f0af4ea870 | ||
|
|
153ecd726e | ||
|
|
dff86e679c | ||
|
|
2bb93938f9 | ||
|
|
48e16a54d3 | ||
|
|
676652fa09 | ||
|
|
294df88809 | ||
|
|
d815de5131 | ||
|
|
32a156c3a6 | ||
|
|
ed77f937f4 | ||
|
|
7070331706 | ||
|
|
3f82986e2c | ||
|
|
7dfbaad817 | ||
|
|
4b8a38d7fe | ||
|
|
fee5e94239 | ||
|
|
57d89b371d | ||
|
|
e5ecc679f0 | ||
|
|
8e32f5db50 | ||
|
|
edde3c4507 | ||
|
|
ae4e35ef65 | ||
|
|
b8633c7711 | ||
|
|
cbccc495a9 | ||
|
|
9d0702800c | ||
|
|
04b28811a4 | ||
|
|
bb7c6db8fb | ||
|
|
adf3f8f21d | ||
|
|
8e0b388fd9 | ||
|
|
d466e62649 | ||
|
|
307742fb81 | ||
|
|
b6c76dd6ed | ||
|
|
e5fb71cf24 | ||
|
|
951765a711 | ||
|
|
2165cd8be8 | ||
|
|
c66dcf1bb6 | ||
|
|
30941fc4c0 | ||
|
|
d7d279c7b6 | ||
|
|
6afaa4a809 | ||
|
|
99a0d69269 | ||
|
|
0db95d4f15 | ||
|
|
676c431bb2 | ||
|
|
d969b567b3 | ||
|
|
46dfca7865 | ||
|
|
e9c19e26ba | ||
|
|
a2d9b608d1 | ||
|
|
6f64c14ef1 | ||
|
|
4b492d0013 | ||
|
|
02d682c50d | ||
|
|
033283810f | ||
|
|
35a02a6816 | ||
|
|
ade6beaffc | ||
|
|
e377d5143e | ||
|
|
2ff723f54b | ||
|
|
504398e5d7 | ||
|
|
af57cd0020 | ||
|
|
399a815526 | ||
|
|
ff2591550e | ||
|
|
34e3c8d6d9 | ||
|
|
8f4d942ced | ||
|
|
587623a329 | ||
|
|
bce1220229 | ||
|
|
e4c10ccd73 | ||
|
|
5e3cba47da | ||
|
|
6bf0210a63 | ||
|
|
ad50d26e0c | ||
|
|
941d5171d3 | ||
|
|
8713a9cb97 | ||
|
|
d434181fd6 | ||
|
|
287ca5cbc2 | ||
|
|
6670ff99cc | ||
|
|
cbd71b7dd9 | ||
|
|
ce2c06af06 | ||
|
|
fa62292633 | ||
|
|
3c6325d7a0 | ||
|
|
c90e2f9e27 | ||
|
|
705538582f | ||
|
|
2c974afecc | ||
|
|
f96a78cb82 | ||
|
|
b77e268441 | ||
|
|
b17fccfed0 | ||
|
|
0ec8a2a942 | ||
|
|
24629b8fb4 | ||
|
|
9526c6714a | ||
|
|
2471c37fd8 | ||
|
|
cc78c0e560 | ||
|
|
2cb7d81229 | ||
|
|
476f120683 | ||
|
|
72638891fe | ||
|
|
16a7edc0ca | ||
|
|
2fc1b7d3e0 | ||
|
|
7069b8e178 | ||
|
|
0cb9961501 | ||
|
|
33dc39d4ad | ||
|
|
5246e77c60 | ||
|
|
def039f591 | ||
|
|
b91e1924e0 | ||
|
|
da25e7c359 | ||
|
|
2a860590d8 | ||
|
|
0be7557204 | ||
|
|
46b596af8c | ||
|
|
90b9254ffb | ||
|
|
c74bd56eb3 | ||
|
|
d2cdca1f6a | ||
|
|
6c584b5c70 | ||
|
|
b9d8540f23 | ||
|
|
8adc269058 | ||
|
|
416b56aed8 | ||
|
|
bd268fd315 | ||
|
|
9f391b59fc | ||
|
|
277b683d98 | ||
|
|
150da8a98d | ||
|
|
4ea0897e3c | ||
|
|
278ecff4ec | ||
|
|
326d37cf1c | ||
|
|
3c704eec2f | ||
|
|
9b5e4acda0 | ||
|
|
456cb8491f | ||
|
|
d65ce800af | ||
|
|
a236eb2e11 | ||
|
|
39c6242f3e | ||
|
|
d1e5604e79 | ||
|
|
098d3572d6 | ||
|
|
d61878a761 | ||
|
|
575b8e43ad | ||
|
|
22eda49187 | ||
|
|
0a52c0b542 | ||
|
|
aa1c26a125 | ||
|
|
30bbef8285 | ||
|
|
74e8e51a43 | ||
|
|
db64fd2aeb | ||
|
|
fc267329d4 | ||
|
|
fe54b5af4b | ||
|
|
677bca506f | ||
|
|
4371399b41 | ||
|
|
284f0835da | ||
|
|
2b687f3334 | ||
|
|
cf1e89f6ef | ||
|
|
057b814e91 | ||
|
|
2022bf021e | ||
|
|
7b1cb4d29a | ||
|
|
81e80d30f6 | ||
|
|
77bcdd76c8 | ||
|
|
49cced03c2 | ||
|
|
1ed2448c7d | ||
|
|
e497c95ff1 | ||
|
|
5fd1ea35fc | ||
|
|
f54e61dae2 | ||
|
|
a6fe1ecf87 | ||
|
|
e5bfe957e2 | ||
|
|
36d52d45ef | ||
|
|
148e89e231 | ||
|
|
19fba8da7a | ||
|
|
1d69e0052c | ||
|
|
0022c3ca15 | ||
|
|
6d3ec46c61 | ||
|
|
211768717e | ||
|
|
8a47b94cdb | ||
|
|
d7a3829a4d | ||
|
|
a125a8d7b7 | ||
|
|
9b2495edce | ||
|
|
fd815acf72 | ||
|
|
6b2071e405 | ||
|
|
503bd57d5e | ||
|
|
d8fbf21579 | ||
|
|
9145463243 | ||
|
|
0052b0b62c | ||
|
|
7f25e75f50 | ||
|
|
46add87243 | ||
|
|
78eed5c405 | ||
|
|
574d6bbf8c | ||
|
|
021b118916 | ||
|
|
53fc5969db | ||
|
|
338c89c53b | ||
|
|
8713e19d78 | ||
|
|
9a769154d7 | ||
|
|
ab707bd365 | ||
|
|
b6c1daa25e | ||
|
|
72c6806fa7 | ||
|
|
d74af3e5c0 | ||
|
|
adeb8c08f7 | ||
|
|
794bdc2c40 | ||
|
|
3b575c10d1 | ||
|
|
7bba857251 | ||
|
|
b59d8a663c | ||
|
|
680245e0a9 | ||
|
|
da54367245 | ||
|
|
2b529eb70a | ||
|
|
9c19bca5d9 | ||
|
|
2b6ac6a375 | ||
|
|
7e4aa4552c | ||
|
|
50a8c8db8a | ||
|
|
84685d1972 | ||
|
|
caf8b26202 | ||
|
|
946132e4b5 | ||
|
|
b9b829db7f | ||
|
|
02bd76ac57 | ||
|
|
9d79f4aea4 | ||
|
|
793aaac2e1 | ||
|
|
1d8f79f3a2 | ||
|
|
8e978b7b17 | ||
|
|
b5325c9641 | ||
|
|
414ab76cb4 | ||
|
|
ed06f38ddd | ||
|
|
bf7e38a164 | ||
|
|
c4d58214b6 | ||
|
|
84006b6923 | ||
|
|
f69fe6a2e0 | ||
|
|
d4e3e73c0d | ||
|
|
35e56b4464 | ||
|
|
6f8c002349 | ||
|
|
40771d962e | ||
|
|
2e0567ef87 | ||
|
|
ef24f2fdf8 | ||
|
|
456aeb3b8c | ||
|
|
4e0a73623e | ||
|
|
f0498fdd7f | ||
|
|
d79446bf56 | ||
|
|
acb4610b22 | ||
|
|
d597dee6f7 | ||
|
|
7f5c7c3eaf | ||
|
|
002bac584b | ||
|
|
120131998e | ||
|
|
c5cdcdfce2 | ||
|
|
c7210de18f | ||
|
|
d3ce3e8722 | ||
|
|
0e79c82013 | ||
|
|
7c8a25cd4c | ||
|
|
9b83083bc3 | ||
|
|
bd063db247 | ||
|
|
65d4e4b8fe | ||
|
|
2373771be8 | ||
|
|
4d753b60d3 | ||
|
|
c73ff62369 | ||
|
|
570a9cca91 | ||
|
|
101ea0ff9a | ||
|
|
40026f4809 | ||
|
|
8c44e581f3 | ||
|
|
7eb9caa96c | ||
|
|
5c495fd1a5 | ||
|
|
471920a0ba | ||
|
|
1a365ae247 | ||
|
|
16b7e407a0 | ||
|
|
9bdca967bb | ||
|
|
79bd2f132c | ||
|
|
f42b3c009e | ||
|
|
80a765cf0c | ||
|
|
c97ec28ecd | ||
|
|
f5ba15269d | ||
|
|
51f5149cb6 | ||
|
|
e08476b3f8 | ||
|
|
bcdf977753 | ||
|
|
233dafa67b | ||
|
|
8a3af78076 | ||
|
|
f3e616b4a8 | ||
|
|
33a1374aea | ||
|
|
39cd69c439 | ||
|
|
bbe628e82f | ||
|
|
a375f96969 | ||
|
|
4682e2df3f | ||
|
|
ba8d7daf53 | ||
|
|
3a4d459872 | ||
|
|
60ee9ff640 | ||
|
|
b673f1ef4f | ||
|
|
d8828cab65 | ||
|
|
52fb8f11a7 | ||
|
|
05f13135a4 | ||
|
|
700fa91850 | ||
|
|
6ba2f882e7 | ||
|
|
f025df8c8c | ||
|
|
79ff47410e | ||
|
|
9e0453e90a | ||
|
|
88c317faba | ||
|
|
5e8e37a22e | ||
|
|
cb57f184b0 | ||
|
|
6273c6e79d | ||
|
|
a2a132eecc | ||
|
|
5b07fbeb3a | ||
|
|
a57f621dfe | ||
|
|
01d25b1a23 | ||
|
|
1bd75719fe | ||
|
|
b37c39b853 | ||
|
|
9bd0dda3dd | ||
|
|
0885022b15 | ||
|
|
0c413261b4 | ||
|
|
b1f87e64e5 | ||
|
|
0ee49fd9e2 | ||
|
|
32662c4964 | ||
|
|
a59d52339e | ||
|
|
447c40483c | ||
|
|
6bc85bf399 | ||
|
|
de7da536bf | ||
|
|
d6ead86d46 | ||
|
|
0a34644369 | ||
|
|
5120af7953 | ||
|
|
67668a6227 | ||
|
|
700f140a0e | ||
|
|
448db8f273 | ||
|
|
549a36e7a2 | ||
|
|
2cf4f914e6 | ||
|
|
51d06959ba | ||
|
|
d4fcaf8a5c | ||
|
|
599e45a479 | ||
|
|
a79fb0e74b | ||
|
|
286158b177 | ||
|
|
cd88e1aa67 | ||
|
|
57aec4e132 | ||
|
|
42eff0f61b | ||
|
|
f39dd27fab | ||
|
|
aaabffb9aa | ||
|
|
7ae110c866 | ||
|
|
332b95965c | ||
|
|
463a9e059d | ||
|
|
7670ed52ce | ||
|
|
dd36c6efb3 | ||
|
|
3e02679738 | ||
|
|
bc16a732b4 | ||
|
|
2d32550714 | ||
|
|
de9f0e1be5 | ||
|
|
00659e33fb | ||
|
|
aaba49adf7 | ||
|
|
cad35f5dd7 | ||
|
|
20cb166ce8 | ||
|
|
9f6da13a57 | ||
|
|
d509613d60 | ||
|
|
cfc7345a9a | ||
|
|
5d06587318 | ||
|
|
6ae8ba4237 | ||
|
|
1f4c2fef7e | ||
|
|
2ccdbc1aa9 | ||
|
|
df2fd5bb19 | ||
|
|
5ab12c9a23 | ||
|
|
d7ea0764e6 | ||
|
|
d05ef0115d | ||
|
|
91726bad33 | ||
|
|
89aac1833d | ||
|
|
423f1de5c0 | ||
|
|
a606ee1861 | ||
|
|
30e45c0558 | ||
|
|
70704c2bef | ||
|
|
8e359eda60 | ||
|
|
fdf1bea596 | ||
|
|
141635a2a7 | ||
|
|
a18c02bdec | ||
|
|
99e6711b44 | ||
|
|
237c2e2c32 | ||
|
|
7b0a1bfad4 | ||
|
|
2fb503323b | ||
|
|
0d9a332b89 | ||
|
|
474f186354 | ||
|
|
1ccc5d2a3f | ||
|
|
28861aec41 | ||
|
|
030fc9e97d | ||
|
|
b20e3e3bbb | ||
|
|
0b7657268c | ||
|
|
ec2a3d116f | ||
|
|
d2f7e5c975 | ||
|
|
81f319dfea | ||
|
|
c5de9bece3 | ||
|
|
c62a87af43 | ||
|
|
b53cfe4b57 | ||
|
|
fa902b3dec | ||
|
|
e3e7d281ef | ||
|
|
4e993a1ebb | ||
|
|
913a521c77 | ||
|
|
fbe7c28b26 | ||
|
|
10e9172d9d | ||
|
|
8907232133 | ||
|
|
ee3151d085 | ||
|
|
c39c1b4d75 | ||
|
|
46a36fe444 | ||
|
|
65eb9bda98 | ||
|
|
0c64c7d08c | ||
|
|
e5515c20b3 | ||
|
|
4793a7666d | ||
|
|
3e02ea636c | ||
|
|
0dae0c9146 | ||
|
|
060d24c95e | ||
|
|
556660e6c1 | ||
|
|
c1be80b5d1 | ||
|
|
255a8fd3c6 | ||
|
|
a29d832a56 | ||
|
|
c6cabafb20 | ||
|
|
d2219b1673 | ||
|
|
62d4a553d3 | ||
|
|
ff938c2239 | ||
|
|
2599e8293b | ||
|
|
6d8c5eeae0 | ||
|
|
1c086a6c68 | ||
|
|
3e8e6f7095 | ||
|
|
a22e4538ab | ||
|
|
b323086c09 | ||
|
|
b09d957b12 | ||
|
|
30ab811ba2 | ||
|
|
6aca1bea71 | ||
|
|
3287172473 | ||
|
|
d8acd413f4 | ||
|
|
06cdae9a87 | ||
|
|
0e5e88d8ff | ||
|
|
390fad48fb | ||
|
|
5dec3b4e2d | ||
|
|
340b277204 | ||
|
|
66930b7408 | ||
|
|
cfc59939f7 | ||
|
|
f2dedc0dfe | ||
|
|
5708909360 | ||
|
|
3f6a5fd2aa | ||
|
|
aa994eb2a5 | ||
|
|
41bef2e90e | ||
|
|
d1ba487fcd | ||
|
|
adab9fc7a8 | ||
|
|
52942138f3 | ||
|
|
f16ec8422a | ||
|
|
5b94ae8aa4 | ||
|
|
116c826262 | ||
|
|
bf8460fc91 | ||
|
|
63156fb3b4 | ||
|
|
d99636afb5 | ||
|
|
c7745d424e | ||
|
|
91bad70bdf | ||
|
|
536a80d1c1 | ||
|
|
79957c1e58 | ||
|
|
fae04cb1eb | ||
|
|
2a3df36798 | ||
|
|
2ada8c787b | ||
|
|
80e1e47d1b | ||
|
|
21412e8047 | ||
|
|
6346437754 | ||
|
|
a2e35bacbd | ||
|
|
074f98f375 | ||
|
|
321987beb4 | ||
|
|
9a36e88451 | ||
|
|
cd421c7afc | ||
|
|
f1214bcaba | ||
|
|
f1b460be65 | ||
|
|
93bc42bbc0 | ||
|
|
913f7d12f4 | ||
|
|
2db883812e | ||
|
|
9a1bcac262 | ||
|
|
51a25ab804 | ||
|
|
80ae8f117f |
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.json]
|
||||
insert_final_newline = false
|
||||
12
.env.example
12
.env.example
@@ -9,9 +9,12 @@
|
||||
# Application URL
|
||||
APP_URL=https://example.com
|
||||
|
||||
# Use HTTP protocol and redirect to HTTPS
|
||||
# Use HTTPS protocol and redirect to HTTPS
|
||||
#APP_FORCE_HTTPS=true
|
||||
|
||||
# If you are using HTTPS, feel free to uncomment this line to improve security
|
||||
#SESSION_SECURE_COOKIE=true
|
||||
|
||||
# Timezones: https://github.com/freescout-helpdesk/freescout/wiki/PHP-Timezones
|
||||
# Comment it to use default timezone from php.ini
|
||||
#APP_TIMEZONE=Europe/London
|
||||
@@ -19,9 +22,10 @@ APP_URL=https://example.com
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=freescout
|
||||
DB_USERNAME=freescout
|
||||
DB_PASSWORD=XXX
|
||||
DB_DATABASE=
|
||||
DB_USERNAME=
|
||||
# Maximum password length is 50 characters
|
||||
DB_PASSWORD=
|
||||
|
||||
# Run the following console command to generate the key: php artisan key:generate
|
||||
# Otherwise application will show the following error: "Whoops, looks like something went wrong"
|
||||
|
||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
patreon: FreeScout
|
||||
12
.github/ISSUE_TEMPLATE/general_help_request.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE/general_help_request.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: General Help Request
|
||||
about: Create a general help request
|
||||
|
||||
---
|
||||
|
||||
* If you have "Whoops something went wrong" error see: https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide#11-troubleshooting
|
||||
* 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
|
||||
* Questions on issues with payments for modules should be sent by email: https://freescout.net/contact-us/
|
||||
|
||||
Still here? Well clean this out and go ahead :)
|
||||
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
Keep in mind that pull requests should be sent to the `master` branch! See https://github.com/freescout-helpdesk/freescout/wiki/Development-Guide#github-workflow
|
||||
|
||||
Now you can delete this text and type the description of your pull request...
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -27,6 +27,9 @@ Thumbs.db
|
||||
/Modules
|
||||
/Modules/**/.git
|
||||
/public/modules
|
||||
/public/docs
|
||||
/storage/.ignore_locales
|
||||
/storage/.installed
|
||||
/tools
|
||||
/tools
|
||||
.well-known
|
||||
/resources/lang/module.*
|
||||
@@ -1,4 +1,9 @@
|
||||
# On some hostings it is impossible to change web root directory
|
||||
# so we rewrite all web requests into /public folder
|
||||
RewriteEngine on
|
||||
RewriteRule (.*) /public/$1 [L]
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine on
|
||||
# On some hostings it does not work as "public/$1", but works as "/public/$1".
|
||||
# But we can't write it as "/public/$1" because in this case it does not work
|
||||
# when application is installed in subdirectory.
|
||||
RewriteRule (.*) public/$1 [L]
|
||||
</IfModule>
|
||||
@@ -3,10 +3,13 @@ language: php
|
||||
php:
|
||||
- 7.0
|
||||
- 7.2
|
||||
- 7.4
|
||||
- 8.0
|
||||
|
||||
before_script:
|
||||
- cp .env.travis .env
|
||||
- mysql -e 'create database homestead_test;'
|
||||
|
||||
script:
|
||||
- vendor/bin/phpunit
|
||||
# phpunit testing is disabled as dist vendor is cleared by removing unneeded files
|
||||
#script:
|
||||
# - vendor/bin/phpunit
|
||||
83
README.md
83
README.md
@@ -5,70 +5,109 @@
|
||||
<img src="https://raw.githubusercontent.com/freescout-helpdesk/freescout/master/public/img/logo-300.png" width="180" height="180" />
|
||||
<br/><br/>
|
||||
|
||||
[](https://github.com/freescout-helpdesk/freescout#requirements) [](https://github.com/freescout-helpdesk/freescout#requirements)
|
||||
[](https://freescout-helpdesk.github.io/img/badges/travis.svg) [](https://gitter.im/freescout-helpdesk/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](http://hits.dwyl.io/freescout-helpdesk/freescout)
|
||||
[](https://github.com/freescout-helpdesk/freescout#requirements) [](https://hits.seeyoufarm.com)
|
||||
|
||||
</div>
|
||||
|
||||
**FreeScout** is the super lightweight free open source help desk and shared inbox written in PHP7 (Laravel 5.5 framework) – self hosted clone of HelpScout. Now you can enjoy [free Zendesk & Help Scout](https://freescout.net) without giving up privacy or locking yourself into a service you don't control.
|
||||
|
||||
FreeScout has been developed from scratch and is not using any copyrighted Help Scout materials to comply with GitHub's [DMCA Policy](https://help.github.com/articles/dmca-takedown-policy/) (trademark, logos, icons, CSS, JS, HTML files are copyrighted; idea, look and feel cannot be copyrighted).
|
||||
**FreeScout** is the super lightweight free open source help desk and shared inbox written in PHP7+ (Laravel framework) – self hosted clone of HelpScout. Now you can enjoy free Zendesk & Help Scout without giving up privacy or locking yourself into a service you don't control. FreeScout has been developed from scratch and is not using any copyrighted Help Scout or Zendesk materials.
|
||||
|
||||

|
||||
|
||||
## Table of Contents
|
||||
* [Features](#features)
|
||||
* [Demo](#demo)
|
||||
* [Mobile Apps](#mobile-apps)
|
||||
* [Requirements](#requirements)
|
||||
* [News & Updates](#news--updates)
|
||||
* [Installation](#installation)
|
||||
* [Cloud Hosted](#cloud-hosted)
|
||||
* [Modules](#modules)
|
||||
* [Tools & Integrations](#tools--integrations)
|
||||
* [News & Updates](#news--updates)
|
||||
* [Contributing](#contributing)
|
||||
* [Screenshots](#screenshots)
|
||||
|
||||
## Features
|
||||
|
||||

|
||||
|
||||
* No limitations on the number of users, tickets, mailboxes, etc.
|
||||
* 100% Mobile-friendly.
|
||||
* Multilingual.
|
||||
* Multilingual (English, Croatian, Danish, Dutch, French, German, Japanese, Italian, Persian, Polish, Portuguese, Russian, Spanish, Slovak, Swedish).
|
||||
* Seamless email integration.
|
||||
* Web installer & updater.
|
||||
* Starred conversations.
|
||||
* Forwarding conversations.
|
||||
* Merging conversations.
|
||||
* Moving conversations between mailboxes.
|
||||
* Phone conversations.
|
||||
* Sending new conversations to multiple recipients at once.
|
||||
* Collision detection – notice is shown when two agents open the same conversation.
|
||||
* Push notifications.
|
||||
* Following a conversation.
|
||||
* Auto reply.
|
||||
* Notes.
|
||||
* Internal notes.
|
||||
* Pasting screenshots from the clipboard into the reply area.
|
||||
* Dragging images into the reply area.
|
||||
* Configuring notifications on a per user basis.
|
||||
* Open tracking.
|
||||
* Editing threads.
|
||||
* Search.
|
||||
* Edit/hide threads.
|
||||
|
||||
Need anything else? Suggest features [here](https://feedback.userreport.com/25a3cb5f-e4bd-4470-b6f3-79fcfaa8e90f/#ideas/popular)
|
||||
Need anything else? Suggest features [here](https://freescout.net/request-feature/).
|
||||
|
||||
## Demo
|
||||
|
||||
**[Live Demo](https://demo.freescout.net)**
|
||||
|
||||
## Mobile Apps
|
||||
|
||||
Mobile apps support the same functionality and modules as the web version of your FreeScout installation. Both support agents and administrators can use mobile apps.
|
||||
|
||||
<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>
|
||||
|
||||
## Requirements
|
||||
|
||||
FreeScout is a pure PHP/MySQL application, so it can be easily deployed even on a shared hosting.
|
||||
|
||||
* Nginx / Apache / IIS
|
||||
* PHP 7.0+
|
||||
* MySQL/MariaDB 5.0+
|
||||
|
||||
## News & Updates
|
||||
|
||||
If you would like to be notified about new releases, news and updates you can subscribe [here](https://feedburner.google.com/fb/a/mailverify?uri=freescout)
|
||||
* PHP 7.1 - 8.1
|
||||
* MySQL 5.0+ / MariaDB 5.0+ / PostgreSQL
|
||||
|
||||
## Installation
|
||||
|
||||
[Installation Guide](https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide)
|
||||
|
||||
Images & one-click installs:
|
||||
|
||||
* [Docker Image](http://freescout.net/docker/)
|
||||
* [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)
|
||||
|
||||
## Cloud Hosted
|
||||
|
||||
[Cloud Hosted FreeScout](https://freescout.net/cloud-hosted/)
|
||||
|
||||
## Modules
|
||||
|
||||
FreeScout [Modules](https://github.com/freescout-helpdesk/freescout/wiki/FreeScout-Modules)
|
||||
* [Official Modules](https://freescout.net/modules/)
|
||||
* [Community Modules](https://freescout.net/community-modules/)
|
||||
|
||||
## Tools & Integrations
|
||||
|
||||
* [API](https://api-docs.freescout.net/)
|
||||
* [Migrate to FreeScout](http://freescout.net/migrate/) (from any help desk)
|
||||
* [Zapier](https://freescout.net/zapier/)
|
||||
* [Integromat](https://freescout.net/integromat/)
|
||||
|
||||
## News & Updates
|
||||
|
||||
If you would like to be notified about important news and updates you can subscribe [here](https://freescout.net/subscribe/)
|
||||
|
||||
## 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/projects/1)
|
||||
* [Todo list](https://github.com/freescout-helpdesk/freescout/labels/help%20wanted)
|
||||
* [Translate](https://github.com/freescout-helpdesk/freescout/wiki/Translate)
|
||||
* [Support project by leaving a feedback](https://github.com/freescout-helpdesk/freescout/wiki#support-project)
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
||||
5
SECURITY.md
Normal file
5
SECURITY.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Email at support@freescout.net
|
||||
@@ -13,6 +13,15 @@ class ActivityLog extends Activity
|
||||
const NAME_SYSTEM = 'system';
|
||||
const NAME_APP_LOGS = 'app';
|
||||
|
||||
public static $available_logs = [
|
||||
self::NAME_USER,
|
||||
self::NAME_OUT_EMAILS,
|
||||
self::NAME_EMAILS_SENDING,
|
||||
self::NAME_EMAILS_FETCHING,
|
||||
self::NAME_SYSTEM,
|
||||
self::NAME_APP_LOGS,
|
||||
];
|
||||
|
||||
const DESCRIPTION_USER_LOGIN = 'login';
|
||||
const DESCRIPTION_USER_LOGOUT = 'logout';
|
||||
const DESCRIPTION_USER_REGISTER = 'register';
|
||||
@@ -87,7 +96,7 @@ class ActivityLog extends Activity
|
||||
case self::NAME_APP_LOGS:
|
||||
return __('App Logs');
|
||||
default:
|
||||
return ucfirst($log_name);
|
||||
return ucwords(str_replace('_', ' ', $log_name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +104,32 @@ class ActivityLog extends Activity
|
||||
{
|
||||
$col = str_replace('_', ' ', $col);
|
||||
$col = ucfirst($col);
|
||||
|
||||
return $col;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get log names.
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public static function getLogNames()
|
||||
{
|
||||
return self::select('log_name')->distinct()->pluck('log_name')->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available log names.
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public static function getAvailableLogs($check_existing = true)
|
||||
{
|
||||
$available_logs = self::$available_logs;
|
||||
if ($check_existing) {
|
||||
$available_logs = array_merge($available_logs, self::getLogNames());
|
||||
}
|
||||
|
||||
return array_unique(\Eventy::filter('activity_log.available_logs', self::$available_logs));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ class Attachment extends Model
|
||||
|
||||
const DIRECTORY = 'attachment';
|
||||
|
||||
CONST DISK = 'private';
|
||||
|
||||
// https://github.com/Webklex/laravel-imap/blob/master/src/IMAP/Attachment.php
|
||||
public static $types = [
|
||||
'message' => self::TYPE_MESSAGE,
|
||||
@@ -32,13 +34,8 @@ class Attachment extends Model
|
||||
'other' => self::TYPE_OTHER,
|
||||
];
|
||||
|
||||
/**
|
||||
* Files with such extensions are being renamed on upload.
|
||||
*/
|
||||
public static $restricted_extensions = [
|
||||
'php.*',
|
||||
'sh',
|
||||
'pl',
|
||||
public static $type_extensions = [
|
||||
self::TYPE_VIDEO => ['flv', 'mp4', 'm3u8', 'ts', '3gp', 'mov', 'avi', 'wmv']
|
||||
];
|
||||
|
||||
public $timestamps = false;
|
||||
@@ -60,30 +57,63 @@ class Attachment extends Model
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check extension.
|
||||
$extension = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
|
||||
if (preg_match('/('.implode('|', self::$restricted_extensions).')/', $extension)) {
|
||||
// Add underscore to the extension if file has restricted extension.
|
||||
$file_name = $file_name.'_';
|
||||
}
|
||||
$orig_extension = pathinfo($file_name, PATHINFO_EXTENSION);
|
||||
|
||||
// Add underscore to the extension if file has restricted extension.
|
||||
$file_name = \Helper::sanitizeUploadedFileName($file_name);
|
||||
|
||||
// Replace some symbols in file name.
|
||||
// Gmail can not load image if it contains spaces.
|
||||
$file_name = str_replace(' ', '-', $file_name);
|
||||
$file_name = preg_replace('/[ #\/]/', '-', $file_name);
|
||||
|
||||
if (!$file_name) {
|
||||
if (!$orig_extension) {
|
||||
preg_match("/.*\/([^\/]+)$/", $mime_type, $m);
|
||||
if (!empty($m[1])) {
|
||||
$orig_extension = $m[1];
|
||||
}
|
||||
}
|
||||
$file_name = uniqid();
|
||||
if ($orig_extension) {
|
||||
$file_name .= '.'.$orig_extension;
|
||||
}
|
||||
}
|
||||
|
||||
if (strlen($file_name) > 255) {
|
||||
$without_ext = pathinfo($file_name, PATHINFO_FILENAME);
|
||||
$extension = pathinfo($file_name, PATHINFO_EXTENSION);
|
||||
// 125 because file name may have unicode symbols.
|
||||
$file_name = \Helper::substrUnicode($without_ext, 0, 125-strlen($extension)-1);
|
||||
$file_name .= '.'.$extension;
|
||||
}
|
||||
|
||||
if (!$type) {
|
||||
$type = self::detectType($mime_type);
|
||||
$type = self::detectType($mime_type, $orig_extension);
|
||||
}
|
||||
|
||||
$attachment = new self();
|
||||
$attachment->thread_id = $thread_id;
|
||||
$attachment->user_id = $user_id;
|
||||
$attachment->user_id = $user_id;
|
||||
$attachment->file_name = $file_name;
|
||||
$attachment->mime_type = $mime_type;
|
||||
$attachment->type = $type;
|
||||
$attachment->embedded = $embedded;
|
||||
$attachment->type = $type;
|
||||
$attachment->embedded = $embedded;
|
||||
$attachment->save();
|
||||
|
||||
$file_info = self::saveFileToDisk($attachment, $file_name, $content, $uploaded_file);
|
||||
|
||||
$attachment->file_dir = $file_info['file_dir'];
|
||||
$attachment->size = Storage::disk(self::DISK)->size($file_info['file_path']);
|
||||
$attachment->save();
|
||||
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save file to the disk and return file_dir.
|
||||
*/
|
||||
public static function saveFileToDisk($attachment, $file_name, $content, $uploaded_file)
|
||||
{
|
||||
// Save file from content or copy file.
|
||||
// We have to keep file name as is, so if file exists we create extra folder.
|
||||
// Examples: 1/2/3
|
||||
@@ -93,26 +123,25 @@ class Attachment extends Model
|
||||
do {
|
||||
$i++;
|
||||
$file_path = self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir.$i.DIRECTORY_SEPARATOR.$file_name;
|
||||
} while (Storage::exists($file_path));
|
||||
} while (Storage::disk(self::DISK)->exists($file_path));
|
||||
|
||||
$file_dir .= $i.DIRECTORY_SEPARATOR;
|
||||
|
||||
if ($uploaded_file) {
|
||||
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name);
|
||||
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name, ['disk' => self::DISK]);
|
||||
} else {
|
||||
Storage::put($file_path, $content);
|
||||
Storage::disk(self::DISK)->put($file_path, $content);
|
||||
}
|
||||
|
||||
$attachment->file_dir = $file_dir;
|
||||
$attachment->size = Storage::size($file_path);
|
||||
$attachment->save();
|
||||
|
||||
return $attachment;
|
||||
return [
|
||||
'file_dir' => $file_dir,
|
||||
'file_path' => $file_path,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file path.
|
||||
* Examples: 1/2, 1/3
|
||||
* Examples: 1/2, 1/3.
|
||||
*
|
||||
* @param int $id
|
||||
*
|
||||
@@ -144,17 +173,23 @@ class Attachment extends Model
|
||||
|
||||
/**
|
||||
* Detect attachment type by it's mime type.
|
||||
*
|
||||
* @param string $mime_type
|
||||
*
|
||||
* @param string $mime_type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function detectType($mime_type)
|
||||
public static function detectType($mime_type, $extension = '')
|
||||
{
|
||||
if (preg_match("/^text\//", $mime_type)) {
|
||||
return self::TYPE_TEXT;
|
||||
} elseif (preg_match("/^message\//", $mime_type)) {
|
||||
return self::TYPE_MESSAGE;
|
||||
} elseif (preg_match("/^application\//", $mime_type)) {
|
||||
// This is tricky mime type.
|
||||
// For .mp4 mime type can be application/octet-stream
|
||||
if (!empty($extension) && in_array(strtolower($extension), self::$type_extensions[self::TYPE_VIDEO])) {
|
||||
return self::TYPE_VIDEO;
|
||||
}
|
||||
return self::TYPE_APPLICATION;
|
||||
} elseif (preg_match("/^audio\//", $mime_type)) {
|
||||
return self::TYPE_AUDIO;
|
||||
@@ -188,7 +223,40 @@ class Attachment extends Model
|
||||
*/
|
||||
public function url()
|
||||
{
|
||||
return Storage::url($this->getStorageFilePath());
|
||||
return Storage::url($this->getStorageFilePath()).'?id='.$this->id.'&token='.$this->getToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hashed security token for the attachment.
|
||||
*/
|
||||
public function getToken()
|
||||
{
|
||||
// \Hash::make() may contain . and / symbols which may cause problems.
|
||||
return md5(config('app.key').$this->id.$this->size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs the current Attachment as download
|
||||
*/
|
||||
public function download($view = false)
|
||||
{
|
||||
$headers = [];
|
||||
// #533
|
||||
//return $this->getDisk()->download($this->getStorageFilePath(), \Str::ascii($this->file_name));
|
||||
if ($view) {
|
||||
$headers['Content-Disposition'] = '';
|
||||
}
|
||||
$file_name = $this->file_name;
|
||||
|
||||
if ($file_name == "RFC822"){
|
||||
$file_name = $file_name.'.eml';
|
||||
}
|
||||
|
||||
return $this->getDisk()->download($this->getStorageFilePath(), $file_name, $headers);
|
||||
}
|
||||
|
||||
private function getDisk() {
|
||||
return Storage::disk(self::DISK);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,14 +269,24 @@ class Attachment extends Model
|
||||
return self::formatBytes($this->size);
|
||||
}
|
||||
|
||||
/**
|
||||
* attachment/...
|
||||
*/
|
||||
public function getStorageFilePath()
|
||||
{
|
||||
return self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name;
|
||||
}
|
||||
|
||||
public function getLocalFilePath()
|
||||
/**
|
||||
* /var/html/storage/app/attachment/...
|
||||
*/
|
||||
public function getLocalFilePath($full = true)
|
||||
{
|
||||
return Storage::path(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
|
||||
if ($full) {
|
||||
return $this->getDisk()->path(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
|
||||
} else {
|
||||
return DIRECTORY_SEPARATOR.'storage'.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name;
|
||||
}
|
||||
}
|
||||
|
||||
public static function formatBytes($size, $precision = 0)
|
||||
@@ -227,23 +305,105 @@ class Attachment extends Model
|
||||
/**
|
||||
* Delete attachments from disk and DB.
|
||||
* Embeds are not taken into account.
|
||||
*
|
||||
* @param array $attachments
|
||||
*
|
||||
* @param array $attachments
|
||||
*/
|
||||
public static function deleteByIds($attachment_ids)
|
||||
{
|
||||
if (!count($attachment_ids)) {
|
||||
return;
|
||||
}
|
||||
$attachments = Attachment::whereIn('id', $attachment_ids)->get();
|
||||
$attachments = self::whereIn('id', $attachment_ids)->get();
|
||||
|
||||
// Delete from disk
|
||||
self::deleteForever($attachments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete attachments by thread IDs.
|
||||
*/
|
||||
public static function deleteByThreadIds($thread_ids)
|
||||
{
|
||||
if (!count($thread_ids)) {
|
||||
return;
|
||||
}
|
||||
$attachments = self::whereIn('thread_id', $thread_ids)->get();
|
||||
|
||||
// Delete from disk
|
||||
self::deleteForever($attachments);
|
||||
}
|
||||
|
||||
public static function deleteForever($attachments)
|
||||
{
|
||||
// Delete from disk
|
||||
foreach ($attachments as $attachment) {
|
||||
echo $attachment->getStorageFilePath().' ';
|
||||
Storage::delete($attachment->getStorageFilePath());
|
||||
$attachment->getDisk()->delete($attachment->getStorageFilePath());
|
||||
}
|
||||
|
||||
// Delete from DB
|
||||
Attachment::whereIn('id', $attachment_ids)->delete();
|
||||
self::whereIn('id', $attachments->pluck('id')->toArray())->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete attachments and update Thread & Conversation.
|
||||
*/
|
||||
public static function deleteAttachments($attachments)
|
||||
{
|
||||
if (!$attachments instanceof \Illuminate\Support\Collection) {
|
||||
$attachments = collect($attachments);
|
||||
}
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
if ($attachment->thread_id && $attachment->thread
|
||||
&& count($attachment->thread->attachments) <= 1
|
||||
) {
|
||||
$attachment->thread->has_attachments = false;
|
||||
$attachment->thread->save();
|
||||
// Update conversation.
|
||||
$conversation = $attachment->thread->conversation;
|
||||
foreach ($conversation->threads as $thread) {
|
||||
if ($thread->has_attachments) {
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
$conversation->has_attachments = false;
|
||||
$conversation->save();
|
||||
}
|
||||
}
|
||||
Attachment::deleteForever($attachments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of the attachment and it's file.
|
||||
*/
|
||||
public function duplicate($thread_id)
|
||||
{
|
||||
$new_attachment = $this->replicate();
|
||||
$new_attachment->thread_id = $thread_id;
|
||||
|
||||
$new_attachment->save();
|
||||
|
||||
try {
|
||||
$attachment_file = new \Illuminate\Http\UploadedFile(
|
||||
$this->getLocalFilePath(), $this->file_name,
|
||||
null, null, true
|
||||
);
|
||||
|
||||
$file_info = Attachment::saveFileToDisk($new_attachment, $new_attachment->file_name, '', $attachment_file);
|
||||
|
||||
if (!empty($file_info['file_dir'])) {
|
||||
$new_attachment->file_dir = $file_info['file_dir'];
|
||||
$new_attachment->save();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e);
|
||||
}
|
||||
|
||||
return $new_attachment;
|
||||
}
|
||||
|
||||
public function getFileContents()
|
||||
{
|
||||
return $this->getDisk()->get($this->getStorageFilePath());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,13 @@
|
||||
* Created by PhpStorm.
|
||||
* User: leemason
|
||||
* Date: 31/10/15
|
||||
* Time: 00:20
|
||||
* Time: 00:20.
|
||||
*/
|
||||
|
||||
namespace App\Broadcasting\Broadcasters;
|
||||
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Broadcasting\Broadcasters\Broadcaster;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
@@ -43,21 +40,22 @@ class PolycastBroadcaster extends Broadcaster
|
||||
|
||||
// insert the new event
|
||||
\DB::table('polycast_events')->insert([
|
||||
'channels' => json_encode($channels),
|
||||
'event' => $event,
|
||||
'payload' => json_encode($payload),
|
||||
'created_at' => Carbon::now()->toDateTimeString()
|
||||
'channels' => json_encode($channels),
|
||||
'event' => $event,
|
||||
'payload' => json_encode($payload),
|
||||
'created_at' => Carbon::now()->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the incoming request for a given channel.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function auth($request) {
|
||||
|
||||
public function auth($request)
|
||||
{
|
||||
// For connect request
|
||||
if (empty($request->channels)) {
|
||||
return true;
|
||||
@@ -67,19 +65,20 @@ class PolycastBroadcaster extends Broadcaster
|
||||
foreach ($request->channels as $channel_name => $channel_info) {
|
||||
// Copied from Illuminate\Broadcasting\Broadcasters\PusherBroadcaster
|
||||
if (Str::startsWith($channel_name, ['private-', 'presence-']) &&
|
||||
! $request->user()) {
|
||||
echo 1;
|
||||
exit();
|
||||
throw new AccessDeniedHttpException;
|
||||
!$request->user()) {
|
||||
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$channelName = Str::startsWith($channel_name, 'private-')
|
||||
? Str::replaceFirst('private-', '', $channel_name)
|
||||
: Str::replaceFirst('presence-', '', $channel_name);
|
||||
// This throws an exception
|
||||
parent::verifyUserCanAccessChannel(
|
||||
$request, $channelName
|
||||
);
|
||||
if (Str::startsWith($channel_name, ['private-', 'presence-'])) {
|
||||
$channelName = Str::startsWith($channel_name, 'private-')
|
||||
? Str::replaceFirst('private-', '', $channel_name)
|
||||
: Str::replaceFirst('presence-', '', $channel_name);
|
||||
// This throws an exception if needed.
|
||||
parent::verifyUserCanAccessChannel(
|
||||
$request, $channelName
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -88,11 +87,13 @@ class PolycastBroadcaster extends Broadcaster
|
||||
/**
|
||||
* Return the valid authentication response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param mixed $result
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param mixed $result
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function validAuthenticationResponse($request, $result) {
|
||||
public function validAuthenticationResponse($request, $result)
|
||||
{
|
||||
// By some reason this is never called
|
||||
return false;
|
||||
|
||||
@@ -107,20 +108,20 @@ class PolycastBroadcaster extends Broadcaster
|
||||
// ]]);
|
||||
}
|
||||
|
||||
public function isDeferred()
|
||||
{
|
||||
public function isDeferred()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Created as there was an error:
|
||||
* "Call to undefined method App\Broadcasting\Broadcasters\PolycastBroadcaster::channel()"
|
||||
*
|
||||
* It is called from routes/channels.php
|
||||
*/
|
||||
/*
|
||||
* Created as there was an error:
|
||||
* "Call to undefined method App\Broadcasting\Broadcasters\PolycastBroadcaster::channel()"
|
||||
*
|
||||
* It is called from routes/channels.php
|
||||
*/
|
||||
// public function channel($channel, callable $callback)
|
||||
// {
|
||||
// return true;
|
||||
// //return (int) $user->id === (int) $id;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,17 @@
|
||||
namespace App\Channels;
|
||||
|
||||
use App\Events\RealtimeBroadcastNotificationCreated;
|
||||
use RuntimeException;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Notifications\Channels\BroadcastChannel;
|
||||
use Illuminate\Notifications\Messages\BroadcastMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class RealtimeBroadcastChannel extends BroadcastChannel
|
||||
{
|
||||
/**
|
||||
* Send the given notification immediately using non-quable event.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @param \Illuminate\Notifications\Notification $notification
|
||||
* @param mixed $notifiable
|
||||
* @param \Illuminate\Notifications\Notification $notification
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function send($notifiable, Notification $notification)
|
||||
|
||||
117
app/Console/Commands/CheckConvViewers.php
Normal file
117
app/Console/Commands/CheckConvViewers.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CheckConvViewers extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:check-conv-viewers';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Check if user finished viewing conversation';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
// Check dates in cache.
|
||||
$cache_key = 'conv_view';
|
||||
$cache_data = \Cache::get($cache_key);
|
||||
|
||||
if (empty($cache_data) || !is_array($cache_data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = Carbon::now();
|
||||
$need_update = false;
|
||||
foreach ($cache_data as $conversation_id => $conv_data) {
|
||||
if (empty($conv_data) || !is_array($conv_data)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($conv_data as $user_id => $data) {
|
||||
|
||||
if (!isset($data['t']) || !isset($data['r'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$view_date = Carbon::createFromFormat('Y-m-d H:i:s', $data['t']);
|
||||
|
||||
if ($view_date && $now->diffInSeconds($view_date) > 25) {
|
||||
// Remove user from viewers.
|
||||
unset($cache_data[$conversation_id][$user_id]);
|
||||
if (empty($cache_data[$conversation_id])) {
|
||||
unset($cache_data[$conversation_id]);
|
||||
}
|
||||
$need_update = true;
|
||||
|
||||
\Cache::forget('conv_view_'.$user_id.'_'.$conversation_id);
|
||||
|
||||
// Create event to let other users know that user finished viewing conversation.
|
||||
$notification_data = [
|
||||
'conversation_id' => $conversation_id,
|
||||
'user_id' => $user_id,
|
||||
];
|
||||
event(new \App\Events\RealtimeConvViewFinish($notification_data));
|
||||
|
||||
\Eventy::action('conversation.view.finish', $conversation_id, $user_id, $now->diffInSeconds($view_date));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($need_update) {
|
||||
// Update conversation cache data.
|
||||
\Cache::put($cache_key, $cache_data, 20 /*minutes*/);
|
||||
}
|
||||
/*$cache_key = 'conv_view_'.$this->user_id.'_'.$this->conversation_id;
|
||||
$cache_data = \Cache::get($cache_key);
|
||||
|
||||
if (!isset($cache_data['t']) || !isset($cache_data['r'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$view_date = Carbon::createFromFormat('Y-m-d H:i:s', $cache_data['t']);
|
||||
$now = Carbon::now();
|
||||
|
||||
if ($view_date && $now->diffInSeconds($view_date) > 30) {
|
||||
$cache_key = 'conv_view';
|
||||
if (!empty($cache_data[$this->conversation_id]) && !empty($cache_data[$this->conversation_id][$this->user_id])) {
|
||||
// Remove user from viewers.
|
||||
unset($cache_data[$this->conversation_id][$this->user_id]);
|
||||
|
||||
// Update conversation cache data.
|
||||
\Cache::put($cache_key, $cache_data, 1);
|
||||
}
|
||||
|
||||
// Create event to let other users know that user finished viewing conversation.
|
||||
$notification_data = [
|
||||
'conversation_id' => $conversation->id,
|
||||
'user_id' => $user->id,
|
||||
];
|
||||
event(new \App\Events\RealtimeConvViewFinish($notification_data));
|
||||
}*/
|
||||
}
|
||||
}
|
||||
46
app/Console/Commands/CleanSendLog.php
Normal file
46
app/Console/Commands/CleanSendLog.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CleanSendLog extends Command
|
||||
{
|
||||
const PERIOD = '-6 months';
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:clean-send-log';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Delete old records from send log.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$logs = \App\SendLog::where('created_at', '<', \Carbon\Carbon::now()->modify(self::PERIOD))->delete();
|
||||
|
||||
$this->info('['.date('Y-m-d H:i:s').'] Deleted send logs: '.self::PERIOD);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ class ClearCache extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:clear-cache {--doNotCacheConfig}';
|
||||
protected $signature = 'freescout:clear-cache {--doNotCacheConfig} {--doNotGenerateVars}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -44,6 +44,15 @@ class ClearCache extends Command
|
||||
$this->call('config:clear');
|
||||
} else {
|
||||
$this->call('config:cache');
|
||||
// Laravel users `require` function to include config.php
|
||||
// If opcache is being used for few seconds config.php is being cached.
|
||||
if (function_exists('opcache_invalidate')) {
|
||||
opcache_invalidate(app()->getCachedConfigPath());
|
||||
}
|
||||
}
|
||||
// Regenerate vars to get new data from .env
|
||||
if (!$this->option('doNotGenerateVars')) {
|
||||
$this->call('freescout:generate-vars');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class CreateUser extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:create-user';
|
||||
protected $signature = 'freescout:create-user {--role=} {--firstName=} {--lastName=} {--email=} {--password=}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -46,40 +46,57 @@ class CreateUser extends Command
|
||||
).'.model'
|
||||
);
|
||||
$user = new $class();
|
||||
$fillables = ['role', 'first_name', 'last_name', 'email', 'password'];
|
||||
foreach ($fillables as $key => $fillable) {
|
||||
if ($fillable == 'password') {
|
||||
$user->$fillable = \Hash::make($this->secret(($key + 1).'/'.count($fillables)." User $fillable"));
|
||||
} elseif ($fillable == 'role') {
|
||||
$user->$fillable = $this->ask(($key + 1).'/'.count($fillables)." User $fillable (admin/user)", 'admin');
|
||||
if (!$user->$fillable) {
|
||||
$user->$fillable = 'admin';
|
||||
}
|
||||
|
||||
while (!in_array($user->$fillable, User::$roles)) {
|
||||
$this->error('Incorrect role');
|
||||
$user->$fillable = $this->ask('Please enter valid role');
|
||||
}
|
||||
$user->$fillable = array_flip(User::$roles)[$user->$fillable];
|
||||
} else {
|
||||
$user->$fillable = $this->ask(($key + 1).'/'.count($fillables)." User $fillable");
|
||||
|
||||
if ($fillable == 'email') {
|
||||
while (!filter_var($user->$fillable, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->error('Incorrect email address');
|
||||
$user->$fillable = $this->ask('Please enter valid email address');
|
||||
}
|
||||
}
|
||||
$user->role = $this->option('role');
|
||||
if ($user->role ) {
|
||||
if (!in_array($user->role , User::$roles)) {
|
||||
$this->error('Invalid role');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$user->role = $this->ask('User role (admin/user)', 'admin');
|
||||
while (!in_array($user->role, User::$roles)) {
|
||||
$this->error('Invalid role');
|
||||
$user->role = $this->ask('Please enter valid role');
|
||||
}
|
||||
}
|
||||
$user->role = array_flip(User::$roles)[$user->role];
|
||||
|
||||
$user->first_name = $this->option('firstName') ? $this->option('firstName') : $this->ask('User first name');
|
||||
$user->last_name = $this->option('lastName') ? $this->option('lastName') : $this->ask('User last name');
|
||||
|
||||
$user->email = $this->option('email');
|
||||
if ($user->email) {
|
||||
if (!filter_var($user->email, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->error('Invalid email address');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$user->email = $this->ask('User email address');
|
||||
while (!filter_var($user->email, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->error('Incorrect email address');
|
||||
$user->email = $this->ask('Please enter valid email address');
|
||||
}
|
||||
}
|
||||
|
||||
$user->password = \Hash::make($this->option('password') ? $this->option('password') : $this->secret('User password'));
|
||||
|
||||
if ($this->confirm('Do you want to create the user?', true)) {
|
||||
if ($user->isAdmin()) {
|
||||
$user->invite_state = User::INVITE_STATE_ACTIVATED;
|
||||
}
|
||||
$user->save();
|
||||
$this->info("User created (id: {$user->id})");
|
||||
|
||||
try {
|
||||
$user->save();
|
||||
} catch (\Exception $e) {
|
||||
$this->line($e->getMessage());
|
||||
$this->error('User already exists.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('User created with id: '.$user->id);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -39,12 +39,16 @@ class FetchMonitor extends Command
|
||||
{
|
||||
$now = time();
|
||||
|
||||
$last_successful_run = \Option::get('fetch_emails_last_successful_run');
|
||||
if ($last_successful_run && $last_successful_run < $now - \Option::get('alert_fetch_period')*60) {
|
||||
$options = \Option::getOptions([
|
||||
'alert_fetch_period',
|
||||
'fetch_emails_last_successful_run',
|
||||
]);
|
||||
|
||||
$last_successful_run = $options['fetch_emails_last_successful_run'];
|
||||
if ($last_successful_run && $last_successful_run < $now - ((config('app.fetch_schedule') * 60) + ($options['alert_fetch_period'] * 60))) {
|
||||
$mins_ago = floor(($now - $last_successful_run) / 60);
|
||||
|
||||
$text = 'There are some problems fetching emails: last time emails were successfully fetched <strong>'.$mins_ago.' minutes ago</strong>. Please check <a href="'.route('logs', ['name' => 'fetch_errors']).'">fetching logs</a> and <a href="'.route('system').'#cron">make sure</a> that the flollowing cron task is running: <code>php artisan schedule:run</code>';
|
||||
$text = 'There are some problems fetching emails: last time emails were successfully fetched <strong>'.$mins_ago.' minutes ago</strong>. Please check <a href="'.route('logs', ['name' => 'fetch_errors']).'">fetching logs</a> and <a href="'.route('system').'#cron">make sure</a> that the following cron task is running: <code>php artisan schedule:run</code>';
|
||||
|
||||
if (\Option::get('alert_fetch') && !\Option::get('alert_fetch_sent')) {
|
||||
// We send alert only once
|
||||
|
||||
@@ -43,18 +43,33 @@ class GenerateVars extends Command
|
||||
{
|
||||
try {
|
||||
$params = [
|
||||
'locales' => config('app.locales'),
|
||||
'locales' => \Helper::getAllLocales(),
|
||||
];
|
||||
|
||||
$filesystem = new Filesystem();
|
||||
//$filesystem = new Filesystem();
|
||||
|
||||
$file_path = public_path('js/vars.js');
|
||||
//$file_path = public_path('js/vars.js');
|
||||
$file_path = storage_path('app/public/js/vars.js');
|
||||
|
||||
$compiled = view('js/vars', $params)->render();
|
||||
$content = view('js/vars', $params)->render();
|
||||
|
||||
$filesystem->put($file_path, $compiled);
|
||||
|
||||
$this->info("Created: {$file_path}");
|
||||
//$filesystem->put($file_path, $content);
|
||||
// Save vars only if content changed
|
||||
try {
|
||||
if (\Storage::exists('js/vars.js')) {
|
||||
$old_content = \Storage::get('js/vars.js');
|
||||
if ($content != $old_content) {
|
||||
\Storage::put('js/vars.js', $content);
|
||||
}
|
||||
} else {
|
||||
\Storage::put('js/vars.js', $content);
|
||||
}
|
||||
$this->info("Created: ".substr($file_path, strlen(base_path())+1));
|
||||
} catch (\Exception $e) {
|
||||
$msg = "Error occured saving /storage/app/public/js/vars.js. ".\Helper::formatException($e);
|
||||
\Log::error($msg);
|
||||
$this->error($msg);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
|
||||
61
app/Console/Commands/LogoutUsers.php
Normal file
61
app/Console/Commands/LogoutUsers.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class LogoutUsers extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:logout-users';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Logout all users';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
// Remove files from storage/frameworks/sessions
|
||||
$files = \File::files(storage_path('framework/sessions'));
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach ($files as $file) {
|
||||
try {
|
||||
$deleted = \File::delete($file->getPathname());
|
||||
if ($deleted) {
|
||||
$count++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
$this->line('Deleted sessions: '.$count);
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
96
app/Console/Commands/LogsMonitor.php
Normal file
96
app/Console/Commands/LogsMonitor.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class LogsMonitor extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:logs-monitor';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Send new log records by email';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$now = \Carbon\Carbon::now();
|
||||
|
||||
$options = \Option::getOptions([
|
||||
'alert_logs_names',
|
||||
'alert_logs_period',
|
||||
], [
|
||||
'alert_logs_period' => config('app.alert_logs_period'),
|
||||
]);
|
||||
|
||||
if (!$options['alert_logs_names']) {
|
||||
$this->error('['.date('Y-m-d H:i:s').'] No logs to monitor selected');
|
||||
|
||||
return;
|
||||
}
|
||||
if (!$options['alert_logs_period']) {
|
||||
$this->error('['.date('Y-m-d H:i:s').'] No logs monitoring period set');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$logs = \App\ActivityLog::whereIn('log_name', $options['alert_logs_names'])
|
||||
->where('created_at', '>=', \Carbon\Carbon::now()->modify('-1 '.$options['alert_logs_period'])->toDateTimeString())
|
||||
->where('created_at', '<', $now->toDateTimeString())
|
||||
->get();
|
||||
|
||||
if (!count($logs)) {
|
||||
$this->line('['.date('Y-m-d H:i:s').'] No new log records found for the last '.$options['alert_logs_period']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$names = $logs->pluck('log_name')->unique()->toArray();
|
||||
$text = 'Logs having new records for the last '.$options['alert_logs_period'].':<ul>';
|
||||
foreach ($names as $name) {
|
||||
$text .= '<li>';
|
||||
$text .= '<strong>'.\App\ActivityLog::getLogTitle($name).'</strong>';
|
||||
$text .= '</li>';
|
||||
}
|
||||
$text .= '</ul>';
|
||||
|
||||
foreach ($names as $name) {
|
||||
$text .= '<br/><strong>'.\App\ActivityLog::getLogTitle($name).'</strong><br/>';
|
||||
foreach ($logs as $log) {
|
||||
if ($log->log_name != $name) {
|
||||
continue;
|
||||
}
|
||||
$text .= '● ['.$log->created_at.'] '.$log->getEventDescription().' <code>'.$log->properties.'</code><br/>';
|
||||
}
|
||||
}
|
||||
// Send alert.
|
||||
\MailHelper::sendAlertMail($text, 'Logs Monitoring');
|
||||
|
||||
$this->line($text);
|
||||
|
||||
$this->info('['.date('Y-m-d H:i:s').'] Monitoring finished');
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ class ModuleBuild extends Command
|
||||
}
|
||||
if (!$modules_aliases) {
|
||||
$this->error('No modules found');
|
||||
|
||||
return;
|
||||
}
|
||||
$all = true;
|
||||
@@ -70,6 +71,7 @@ class ModuleBuild extends Command
|
||||
$module = \Module::findByAlias($module_alias);
|
||||
if (!$module) {
|
||||
$this->error('Module with the specified alias not found: '.$module_alias);
|
||||
|
||||
return;
|
||||
}
|
||||
$this->buildModule($module);
|
||||
@@ -84,6 +86,7 @@ class ModuleBuild extends Command
|
||||
$public_symlink = public_path('modules').DIRECTORY_SEPARATOR.$module->alias;
|
||||
if (!file_exists($public_symlink)) {
|
||||
$this->error('Public symlink ['.$public_symlink.'] not found. Run module installation command first: php artisan freescout:module-install');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -108,7 +111,6 @@ class ModuleBuild extends Command
|
||||
}
|
||||
|
||||
$this->info("Created: {$file_path}");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
|
||||
102
app/Console/Commands/ModuleCheckLicenses.php
Normal file
102
app/Console/Commands/ModuleCheckLicenses.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Misc\WpApi;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ModuleCheckLicenses extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:module-check-licenses';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Check licenses for modules';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
// Get active official modules and check validity of their licenses
|
||||
$modules = \Module::getActive();
|
||||
|
||||
$this->info('Active modules found: '.count($modules));
|
||||
|
||||
$params = [
|
||||
'url' => \App\Module::getAppUrl(),
|
||||
'data' => [],
|
||||
];
|
||||
|
||||
foreach ($modules as $module) {
|
||||
$license = $module->getLicense();
|
||||
|
||||
if (!$module->isOfficial() || !$license) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data['license'] = $license;
|
||||
$data['module_alias'] = $module->getAlias();
|
||||
|
||||
$params['data'][] = $data;
|
||||
}
|
||||
|
||||
$result = WpApi::checkLicenses($params);
|
||||
|
||||
if (!empty($result['statuses'])) {
|
||||
foreach ($modules as $module) {
|
||||
$module_alias = $module->getAlias();
|
||||
|
||||
foreach ($result['statuses'] as $result_module_alias => $status) {
|
||||
if ($result_module_alias != $module_alias) {
|
||||
continue;
|
||||
}
|
||||
if (!empty($status) && $status != 'valid') {
|
||||
$msg = 'Module '.$module->getName().' has been deactivated due to invalid license: '.$status;
|
||||
|
||||
$this->error($module->getName().': '.$msg);
|
||||
|
||||
// Deactive module
|
||||
\App\Module::deactiveModule($module->getAlias(), true);
|
||||
|
||||
// Inform admin
|
||||
\Log::error($msg);
|
||||
activity()
|
||||
->withProperties([
|
||||
'error' => $msg,
|
||||
])
|
||||
->useLog(\App\ActivityLog::NAME_SYSTEM)
|
||||
->log(\App\ActivityLog::DESCRIPTION_SYSTEM_ERROR);
|
||||
} else {
|
||||
$this->info($module->getName().': OK');
|
||||
}
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$this->info($module->getName().': Unknown status');
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Checking licenses finished');
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
/**
|
||||
* php artisan freescout:module-install modulealias
|
||||
* php artisan freescout:module-install modulealias.
|
||||
*/
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
@@ -56,6 +57,7 @@ class ModuleInstall extends Command
|
||||
}
|
||||
if (!$modules_aliases) {
|
||||
$this->error('No modules found');
|
||||
|
||||
return;
|
||||
}
|
||||
$install_all = $this->confirm('You have not specified a module alias, would you like to install all available modules ('.implode(', ', $modules_aliases).')?');
|
||||
@@ -74,6 +76,7 @@ class ModuleInstall extends Command
|
||||
$module = \Module::findByAlias($module_alias);
|
||||
if (!$module) {
|
||||
$this->error('Module with the specified alias not found: '.$module_alias);
|
||||
|
||||
return;
|
||||
}
|
||||
$this->call('module:migrate', ['module' => $module->getName(), '--force' => true]);
|
||||
@@ -91,7 +94,7 @@ class ModuleInstall extends Command
|
||||
if (file_exists($from)) {
|
||||
return $this->info('Public symlink already exists');
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
symlink($to, $from);
|
||||
} catch (\Exception $e) {
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Axn\Laroute\Routes\Collection as Routes;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ModuleLaroute extends Command
|
||||
{
|
||||
@@ -31,8 +30,8 @@ class ModuleLaroute extends Command
|
||||
{
|
||||
$app = app();
|
||||
|
||||
$this->config = $app['config'];
|
||||
$this->generator = $app->make('Lord\Laroute\Generators\GeneratorInterface');
|
||||
$this->config = $app['config'];
|
||||
$this->generator = $app->make('Lord\Laroute\Generators\GeneratorInterface');
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -58,6 +57,7 @@ class ModuleLaroute extends Command
|
||||
}
|
||||
if (!$modules_aliases) {
|
||||
$this->error('No modules found');
|
||||
|
||||
return;
|
||||
}
|
||||
$all = true;
|
||||
@@ -75,6 +75,7 @@ class ModuleLaroute extends Command
|
||||
$module = \Module::findByAlias($module_alias);
|
||||
if (!$module) {
|
||||
$this->error('Module with the specified alias not found: '.$module_alias);
|
||||
|
||||
return;
|
||||
}
|
||||
$this->generateModuleRoutes($module);
|
||||
@@ -88,6 +89,7 @@ class ModuleLaroute extends Command
|
||||
$public_symlink = public_path('modules').DIRECTORY_SEPARATOR.$module->getAlias();
|
||||
if (!file_exists($public_symlink)) {
|
||||
$this->error('Public symlink ['.$public_symlink.'] not found. Run module installation command first: php artisan freescout:module-install');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,16 +125,15 @@ class ModuleLaroute extends Command
|
||||
*/
|
||||
protected function getTemplateData()
|
||||
{
|
||||
$namespace = $this->getOptionOrConfig('namespace');
|
||||
$routes = $this->routes->toJSON();
|
||||
$absolute = $this->config->get('laroute.absolute', false);
|
||||
$rootUrl = $this->config->get('app.url', '');
|
||||
$prefix = $this->config->get('laroute.prefix', '');
|
||||
$namespace = $this->getOptionOrConfig('namespace');
|
||||
$routes = $this->routes->toJSON();
|
||||
$absolute = $this->config->get('laroute.absolute', false);
|
||||
$rootUrl = $this->config->get('app.url', '');
|
||||
$prefix = $this->config->get('laroute.prefix', '');
|
||||
|
||||
return compact('namespace', 'routes', 'absolute', 'rootUrl', 'prefix');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the path where the file will be generated.
|
||||
*
|
||||
@@ -140,7 +141,7 @@ class ModuleLaroute extends Command
|
||||
*/
|
||||
protected function getFileGenerationPath($module_alias)
|
||||
{
|
||||
$path = 'public/modules/'.$module_alias.'/js';
|
||||
$path = 'public/modules/'.$module_alias.'/js';
|
||||
$filename = 'laroute'; //$this->getOptionOrConfig('filename');
|
||||
|
||||
return "{$path}/{$filename}.js";
|
||||
|
||||
@@ -43,11 +43,12 @@ class Update extends Command
|
||||
if (!$this->confirmToProceed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ini_set('memory_limit', '128M');
|
||||
|
||||
if (\Updater::isNewVersionAvailable(config('app.version'))) {
|
||||
$this->info('Updating... This may take several minutes');
|
||||
|
||||
try {
|
||||
// Script may fail here and stop with the error:
|
||||
// PHP Fatal error: Allowed memory size of 94371840 bytes exhausted
|
||||
|
||||
46
app/Console/Commands/UpdateFolderCounters.php
Normal file
46
app/Console/Commands/UpdateFolderCounters.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class UpdateFolderCounters extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:update-folder-counters';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Update counters for all folders';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
foreach (\App\Folder::get() as $folder) {
|
||||
$folder->updateCounters();
|
||||
$this->line('Updated counters for folder: '.$folder->id);
|
||||
}
|
||||
$this->info('Updating finished');
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ namespace App\Console;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
use App\Misc\Mail;
|
||||
use App\Option;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
@@ -27,9 +29,11 @@ class Kernel extends ConsoleKernel
|
||||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
// Keep in mind that this function is also called on clearing cache.
|
||||
|
||||
// Remove failed jobs
|
||||
$schedule->command('queue:flush')
|
||||
->daily();
|
||||
->weekly();
|
||||
|
||||
// Restart processing queued jobs (just in case)
|
||||
$schedule->command('queue:restart')
|
||||
@@ -39,43 +43,138 @@ class Kernel extends ConsoleKernel
|
||||
->everyMinute()
|
||||
->withoutOverlapping();
|
||||
|
||||
// Fetch emails from mailboxes
|
||||
$schedule->command('freescout:fetch-emails')
|
||||
$schedule->command('freescout:update-folder-counters')
|
||||
->hourly();
|
||||
|
||||
$crc = crc32(config('app.key'));
|
||||
$schedule->command('freescout:module-check-licenses')
|
||||
->cron((int)($crc % 59).' '.(int)($crc % 23).' * * *');
|
||||
|
||||
// Check if user finished viewing conversation.
|
||||
$schedule->command('freescout:check-conv-viewers')
|
||||
->everyMinute()
|
||||
->withoutOverlapping();
|
||||
|
||||
$schedule->command('freescout:clean-send-log')
|
||||
->monthly();
|
||||
|
||||
// Logs monitoring.
|
||||
$alert_logs_period = config('app.alert_logs_period');
|
||||
if (config('app.alert_logs') && $alert_logs_period) {
|
||||
$logs_cron = '';
|
||||
switch ($alert_logs_period) {
|
||||
case 'hour':
|
||||
$logs_cron = '0 * * * *';
|
||||
break;
|
||||
case 'day':
|
||||
$logs_cron = '0 0 * * *';
|
||||
break;
|
||||
case 'week':
|
||||
$logs_cron = '0 0 * * 0';
|
||||
break;
|
||||
case 'month':
|
||||
$logs_cron = '0 0 1 * *';
|
||||
break;
|
||||
}
|
||||
if ($logs_cron) {
|
||||
$schedule->command('freescout:logs-monitor')
|
||||
->cron($logs_cron)
|
||||
->withoutOverlapping();
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch emails from mailboxes
|
||||
$fetch_command = $schedule->command('freescout:fetch-emails')
|
||||
->withoutOverlapping()
|
||||
->sendOutputTo(storage_path().'/logs/fetch-emails.log');
|
||||
|
||||
switch (config('app.fetch_schedule')) {
|
||||
case Mail::FETCH_SCHEDULE_EVERY_FIVE_MINUTES:
|
||||
$fetch_command->everyFiveMinutes();
|
||||
break;
|
||||
case Mail::FETCH_SCHEDULE_EVERY_TEN_MINUTES:
|
||||
$fetch_command->everyTenMinutes();
|
||||
break;
|
||||
case Mail::FETCH_SCHEDULE_EVERY_FIFTEEN_MINUTES:
|
||||
$fetch_command->everyFifteenMinutes();
|
||||
break;
|
||||
case Mail::FETCH_SCHEDULE_EVERY_THIRTY_MINUTES:
|
||||
$fetch_command->everyThirtyMinutes();
|
||||
break;
|
||||
case Mail::FETCH_SCHEDULE_HOURLY:
|
||||
$fetch_command->Hourly();
|
||||
break;
|
||||
default:
|
||||
$fetch_command->everyMinute();
|
||||
break;
|
||||
}
|
||||
|
||||
$schedule = \Eventy::filter('schedule', $schedule);
|
||||
|
||||
// Command runs as subprocess and sets cache mutex. If schedule:run command is killed
|
||||
// subprocess does not clear the mutex and it stays in the cache until cache:clear is executed.
|
||||
// By default, the lock will expire after 24 hours.
|
||||
//
|
||||
// cache:clear clears the mutex, but sometimes process continues running, so we need to kill it.
|
||||
|
||||
|
||||
if (function_exists('shell_exec')) {
|
||||
$running_commands = 0;
|
||||
try {
|
||||
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep 'queue:work'"));
|
||||
foreach ($processes as $process) {
|
||||
preg_match("/^[\S]+\s+([\d]+)\s+/", $process, $m);
|
||||
if (!preg_match("/(sh \-c|grep )/", $process) && !empty($m[1])) {
|
||||
$running_commands++;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Do nothing
|
||||
}
|
||||
if ($running_commands > 1) {
|
||||
$running_commands = $this->getRunningQueueProcesses();
|
||||
|
||||
if (count($running_commands) > 1) {
|
||||
// Stop all queue:work processes.
|
||||
// queue:work command is stopped by settings a cache key
|
||||
\Cache::forever('illuminate:queue:restart', Carbon::now()->getTimestamp());
|
||||
\Helper::queueWorkRestart();
|
||||
// Sometimes processes stuck and just continue running, so we need to kill them.
|
||||
// Sleep to let processes stop.
|
||||
sleep(1);
|
||||
// Check processes again.
|
||||
$worker_pids = $this->getRunningQueueProcesses();
|
||||
|
||||
if (count($worker_pids) > 1) {
|
||||
// Current process also has to be killed, as otherwise it "stucks"
|
||||
// $current_pid = getmypid();
|
||||
// foreach ($worker_pids as $i => $pid) {
|
||||
// if ($pid == $current_pid) {
|
||||
// unset($worker_pids[$i]);
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
shell_exec('kill '.implode(' | kill ', $worker_pids));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$schedule->command('queue:work', Config('app.queue_work_params'))
|
||||
$queue_work_params = Config('app.queue_work_params');
|
||||
// Add identifier to avoid conflicts with other FreeScout instances on the same server.
|
||||
$queue_work_params['--queue'] .= ','.\Helper::getWorkerIdentifier();
|
||||
|
||||
$schedule->command('queue:work', $queue_work_params)
|
||||
->everyMinute()
|
||||
->withoutOverlapping()
|
||||
->sendOutputTo(storage_path().'/logs/queue-jobs.log');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pids of the queue:work processes.
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
protected function getRunningQueueProcesses()
|
||||
{
|
||||
$pids = [];
|
||||
|
||||
try {
|
||||
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep '".\Helper::getWorkerIdentifier()."'"));
|
||||
foreach ($processes as $process) {
|
||||
preg_match("/^[\S]+\s+([\d]+)\s+/", $process, $m);
|
||||
if (!preg_match("/(sh \-c|grep )/", $process) && !empty($m[1])) {
|
||||
$pids[] = $m[1];
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Do nothing
|
||||
}
|
||||
return $pids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*
|
||||
@@ -85,6 +184,13 @@ class Kernel extends ConsoleKernel
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
// Swiftmailer uses $_SERVER['SERVER_NAME'] in transport_deps.php
|
||||
// to set the host for EHLO command, if it is empty it uses [127.0.0.1].
|
||||
// G Suite sometimes rejects emails with EHLO [127.0.0.1].
|
||||
if (empty($_SERVER['SERVER_NAME'])) {
|
||||
$_SERVER['SERVER_NAME'] = parse_url(config('app.url'), PHP_URL_HOST);
|
||||
}
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
|
||||
1355
app/Conversation.php
1355
app/Conversation.php
File diff suppressed because it is too large
Load Diff
@@ -3,11 +3,10 @@
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Input;
|
||||
|
||||
class ConversationFolder extends Model
|
||||
{
|
||||
/**
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
@@ -15,4 +14,4 @@ class ConversationFolder extends Model
|
||||
protected $table = 'conversation_folder';
|
||||
|
||||
protected $fillable = ['folder_id', 'conversation_id'];
|
||||
}
|
||||
}
|
||||
|
||||
798
app/Customer.php
798
app/Customer.php
@@ -4,14 +4,20 @@ namespace App;
|
||||
|
||||
use App\Email;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Watson\Rememberable\Rememberable;
|
||||
|
||||
class Customer extends Model
|
||||
{
|
||||
use Rememberable;
|
||||
|
||||
// This is obligatory.
|
||||
public $rememberCacheDriver = 'array';
|
||||
|
||||
const PHOTO_DIRECTORY = 'customers';
|
||||
const PHOTO_SIZE = 64; // px
|
||||
const PHOTO_QUALITY = 77;
|
||||
|
||||
/**
|
||||
* Genders.
|
||||
*/
|
||||
@@ -46,7 +52,7 @@ class Customer extends Model
|
||||
self::PHONE_TYPE_HOME => 'home',
|
||||
self::PHONE_TYPE_OTHER => 'other',
|
||||
self::PHONE_TYPE_MOBILE => 'mobile',
|
||||
self::PHONE_TYPE_FAX => 'Fax',
|
||||
self::PHONE_TYPE_FAX => 'fax',
|
||||
self::PHONE_TYPE_PAGER => 'pager',
|
||||
];
|
||||
|
||||
@@ -79,51 +85,52 @@ class Customer extends Model
|
||||
/**
|
||||
* Chat types.
|
||||
*/
|
||||
const CHAT_TYPE_AIM = 1;
|
||||
const CHAT_TYPE_GTALK = 2;
|
||||
const CHAT_TYPE_ICQ = 3;
|
||||
const CHAT_TYPE_XMPP = 4;
|
||||
const CHAT_TYPE_MSN = 5;
|
||||
const CHAT_TYPE_SKYPE = 6;
|
||||
const CHAT_TYPE_YAHOO = 7;
|
||||
const CHAT_TYPE_QQ = 8;
|
||||
const CHAT_TYPE_WECHAT = 10;
|
||||
const CHAT_TYPE_OTHER = 9;
|
||||
// const CHAT_TYPE_AIM = 1;
|
||||
// const CHAT_TYPE_GTALK = 2;
|
||||
// const CHAT_TYPE_ICQ = 3;
|
||||
// const CHAT_TYPE_XMPP = 4;
|
||||
// const CHAT_TYPE_MSN = 5;
|
||||
// const CHAT_TYPE_SKYPE = 6;
|
||||
// const CHAT_TYPE_YAHOO = 7;
|
||||
// const CHAT_TYPE_QQ = 8;
|
||||
// const CHAT_TYPE_WECHAT = 10;
|
||||
// const CHAT_TYPE_OTHER = 9;
|
||||
|
||||
/**
|
||||
* For API.
|
||||
*/
|
||||
public static $chat_types = [
|
||||
self::CHAT_TYPE_AIM => 'aim',
|
||||
self::CHAT_TYPE_GTALK => 'gtalk',
|
||||
self::CHAT_TYPE_ICQ => 'icq',
|
||||
self::CHAT_TYPE_XMPP => 'xmpp',
|
||||
self::CHAT_TYPE_MSN => 'msn',
|
||||
self::CHAT_TYPE_SKYPE => 'skype',
|
||||
self::CHAT_TYPE_YAHOO => 'yahoo',
|
||||
self::CHAT_TYPE_QQ => 'qq',
|
||||
self::CHAT_TYPE_WECHAT => 'wechat', // Extra
|
||||
self::CHAT_TYPE_OTHER => 'other',
|
||||
];
|
||||
// public static $chat_types = [
|
||||
// self::CHAT_TYPE_AIM => 'aim',
|
||||
// self::CHAT_TYPE_GTALK => 'gtalk',
|
||||
// self::CHAT_TYPE_ICQ => 'icq',
|
||||
// self::CHAT_TYPE_XMPP => 'xmpp',
|
||||
// self::CHAT_TYPE_MSN => 'msn',
|
||||
// self::CHAT_TYPE_SKYPE => 'skype',
|
||||
// self::CHAT_TYPE_YAHOO => 'yahoo',
|
||||
// self::CHAT_TYPE_QQ => 'qq',
|
||||
// self::CHAT_TYPE_WECHAT => 'wechat', // Extra
|
||||
// self::CHAT_TYPE_OTHER => 'other',
|
||||
// ];
|
||||
|
||||
public static $chat_type_names = [
|
||||
self::CHAT_TYPE_AIM => 'AIM',
|
||||
self::CHAT_TYPE_GTALK => 'Google+',
|
||||
self::CHAT_TYPE_ICQ => 'ICQ',
|
||||
self::CHAT_TYPE_XMPP => 'XMPP',
|
||||
self::CHAT_TYPE_MSN => 'MSN',
|
||||
self::CHAT_TYPE_SKYPE => 'Skype',
|
||||
self::CHAT_TYPE_YAHOO => 'Yahoo',
|
||||
self::CHAT_TYPE_QQ => 'QQ',
|
||||
self::CHAT_TYPE_WECHAT => 'WeChat', // Extra
|
||||
self::CHAT_TYPE_OTHER => 'Other',
|
||||
];
|
||||
// public static $chat_type_names = [
|
||||
// self::CHAT_TYPE_AIM => 'AIM',
|
||||
// self::CHAT_TYPE_GTALK => 'Google+',
|
||||
// self::CHAT_TYPE_ICQ => 'ICQ',
|
||||
// self::CHAT_TYPE_XMPP => 'XMPP',
|
||||
// self::CHAT_TYPE_MSN => 'MSN',
|
||||
// self::CHAT_TYPE_SKYPE => 'Skype',
|
||||
// self::CHAT_TYPE_YAHOO => 'Yahoo',
|
||||
// self::CHAT_TYPE_QQ => 'QQ',
|
||||
// self::CHAT_TYPE_WECHAT => 'WeChat', // Extra
|
||||
// self::CHAT_TYPE_OTHER => 'Other',
|
||||
// ];
|
||||
|
||||
/**
|
||||
* Social types.
|
||||
*/
|
||||
const SOCIAL_TYPE_TWITTER = 1;
|
||||
const SOCIAL_TYPE_FACEBOOK = 2;
|
||||
const SOCIAL_TYPE_TELEGRAM = 14;
|
||||
const SOCIAL_TYPE_LINKEDIN = 3;
|
||||
const SOCIAL_TYPE_ABOUTME = 4;
|
||||
const SOCIAL_TYPE_GOOGLE = 5;
|
||||
@@ -139,6 +146,7 @@ class Customer extends Model
|
||||
public static $social_types = [
|
||||
self::SOCIAL_TYPE_TWITTER => 'twitter',
|
||||
self::SOCIAL_TYPE_FACEBOOK => 'facebook',
|
||||
self::SOCIAL_TYPE_TELEGRAM => 'telegram',
|
||||
self::SOCIAL_TYPE_LINKEDIN => 'linkedin',
|
||||
self::SOCIAL_TYPE_ABOUTME => 'aboutme',
|
||||
self::SOCIAL_TYPE_GOOGLE => 'google',
|
||||
@@ -155,6 +163,7 @@ class Customer extends Model
|
||||
public static $social_type_names = [
|
||||
self::SOCIAL_TYPE_TWITTER => 'Twitter',
|
||||
self::SOCIAL_TYPE_FACEBOOK => 'Facebook',
|
||||
self::SOCIAL_TYPE_TELEGRAM => 'Telegram',
|
||||
self::SOCIAL_TYPE_LINKEDIN => 'Linkedin',
|
||||
self::SOCIAL_TYPE_ABOUTME => 'About.me',
|
||||
self::SOCIAL_TYPE_GOOGLE => 'Google',
|
||||
@@ -168,6 +177,13 @@ class Customer extends Model
|
||||
self::SOCIAL_TYPE_OTHER => 'Other',
|
||||
];
|
||||
|
||||
/**
|
||||
* Search filters.
|
||||
*/
|
||||
public static $search_filters = [
|
||||
'mailbox',
|
||||
];
|
||||
|
||||
/**
|
||||
* Countries list.
|
||||
*/
|
||||
@@ -424,6 +440,10 @@ class Customer extends Model
|
||||
'ZW' => 'Zimbabwe',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'meta' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Attributes which are not fillable using fill() method.
|
||||
*/
|
||||
@@ -434,7 +454,12 @@ class Customer extends Model
|
||||
*
|
||||
* @var [type]
|
||||
*/
|
||||
protected $fillable = ['first_name', 'last_name', 'company', 'job_title', 'address', 'city', 'state', 'zip', 'country'];
|
||||
protected $fillable = ['first_name', 'last_name', 'company', 'job_title', 'address', 'city', 'state', 'zip', 'country', 'photo_url', 'age', 'gender', 'notes', 'channel', 'channel_id', 'social_profiles'];
|
||||
|
||||
/**
|
||||
* Fields stored as JSON.
|
||||
*/
|
||||
protected $json_fields = ['phones', 'websites', 'social_profiles'];
|
||||
|
||||
/**
|
||||
* Get customer emails.
|
||||
@@ -444,6 +469,14 @@ class Customer extends Model
|
||||
return $this->hasMany('App\Email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer emails.
|
||||
*/
|
||||
public function emails_cached()
|
||||
{
|
||||
return $this->hasMany('App\Email')->rememberForever();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer conversations.
|
||||
*/
|
||||
@@ -457,7 +490,15 @@ class Customer extends Model
|
||||
*/
|
||||
public function getMainEmail()
|
||||
{
|
||||
return $this->emails()->first()->email;
|
||||
return optional($this->emails_cached()->first())->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get main email.
|
||||
*/
|
||||
public static function getMainEmailStatic($customer_id)
|
||||
{
|
||||
return Email::select('email')->where('customer_id', $customer_id)->pluck('email');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -465,7 +506,7 @@ class Customer extends Model
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFullName($email_if_empty = false)
|
||||
public function getFullName($email_if_empty = false, $first_part_from_email = false)
|
||||
{
|
||||
if ($this->first_name && $this->last_name) {
|
||||
return $this->first_name.' '.$this->last_name;
|
||||
@@ -474,7 +515,12 @@ class Customer extends Model
|
||||
} elseif (!$this->first_name && $this->last_name) {
|
||||
return $this->last_name;
|
||||
} elseif ($email_if_empty) {
|
||||
return $this->getMainEmail();
|
||||
$email = $this->getMainEmail();
|
||||
if ($first_part_from_email) {
|
||||
return $this->getNameFromEmail($email);
|
||||
} else {
|
||||
return $email;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
@@ -501,11 +547,13 @@ class Customer extends Model
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNameFromEmail()
|
||||
public function getNameFromEmail($email = '')
|
||||
{
|
||||
$email = $this->emails()->first();
|
||||
if (!$email) {
|
||||
$email = optional($this->emails_cached()->first())->email;
|
||||
}
|
||||
if ($email) {
|
||||
return explode('@', $email->email)[0];
|
||||
return explode('@', $email)[0];
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
@@ -558,15 +606,38 @@ class Customer extends Model
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new email to customer.
|
||||
*/
|
||||
public function addEmail($email_address, $check_if_exists = false)
|
||||
{
|
||||
// Check if email already exists and belongs to another customer.
|
||||
if ($check_if_exists) {
|
||||
$email = Email::where('email', $email_address)->first();
|
||||
if ($email && !empty($email->customer_id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$new_email = new Email(['email' => $email_address]);
|
||||
$this->emails()->save($new_email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customers phones as array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPhones()
|
||||
public function getPhones($dummy_if_empty = false)
|
||||
{
|
||||
if ($this->phones) {
|
||||
return json_decode($this->phones);
|
||||
$phones = json_decode($this->phones ?? '', true);
|
||||
|
||||
if (is_array($phones) && count($phones)) {
|
||||
return $phones;
|
||||
} elseif ($dummy_if_empty) {
|
||||
return [[
|
||||
'type' => self::PHONE_TYPE_WORK,
|
||||
'value' => '',
|
||||
]];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
@@ -579,7 +650,19 @@ class Customer extends Model
|
||||
*/
|
||||
public function setPhones(array $phones_array)
|
||||
{
|
||||
$this->phones = json_encode(self::formatPhones($phones));
|
||||
$phones_array = self::formatPhones($phones_array);
|
||||
|
||||
// Remove dubplicates.
|
||||
$list = [];
|
||||
foreach ($phones_array as $i => $data) {
|
||||
if (in_array($data['value'], $list)) {
|
||||
unset($phones_array[$i]);
|
||||
} else {
|
||||
$list[] = $data['value'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->phones = \Helper::jsonEncodeUtf8($phones_array);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -592,16 +675,54 @@ class Customer extends Model
|
||||
public static function formatPhones(array $phones_array)
|
||||
{
|
||||
$phones = [];
|
||||
|
||||
foreach ($phones_array as $phone) {
|
||||
if (!empty($phone['value']) && !empty($phone['type']) && in_array($phone['type'], array_keys(self::$phone_types))) {
|
||||
if (is_array($phone)) {
|
||||
if (!empty($phone['value'])) {
|
||||
if (empty($phone['type']) || !in_array($phone['type'], array_keys(self::$phone_types))) {
|
||||
$phone['type'] = self::PHONE_TYPE_WORK;
|
||||
}
|
||||
$phones[] = [
|
||||
'value' => (string) $phone['value'],
|
||||
'type' => (int) $phone['type'],
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$phones[] = [
|
||||
'value' => (string) $phone['value'],
|
||||
'type' => (int) $phone['type'],
|
||||
'value' => (string) $phone,
|
||||
'type' => self::PHONE_TYPE_WORK
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return json_encode($phones);
|
||||
return $phones;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add website.
|
||||
*/
|
||||
public function addPhone($phone, $type = self::PHONE_TYPE_WORK)
|
||||
{
|
||||
if (is_string($phone)) {
|
||||
$this->setPhones(array_merge(
|
||||
$this->getPhones(),
|
||||
[['value' => $phone, 'type' => $type]]
|
||||
));
|
||||
} else {
|
||||
$this->setPhones(array_merge(
|
||||
$this->getPhones(),
|
||||
[$phone]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find customer by phone number.
|
||||
*/
|
||||
public static function findByPhone($phone)
|
||||
{
|
||||
$phone = trim($phone);
|
||||
return Customer::where('phones', 'LIKE', '%"'.$phone.'"%')->first();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -609,10 +730,17 @@ class Customer extends Model
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSocialProfiles()
|
||||
public function getSocialProfiles($dummy_if_empty = false)
|
||||
{
|
||||
if ($this->social_profiles) {
|
||||
return json_decode($this->social_profiles);
|
||||
$social_profiles = json_decode($this->social_profiles ?? '', true);
|
||||
|
||||
if (is_array($social_profiles) && count($social_profiles)) {
|
||||
return json_decode($this->social_profiles, true);
|
||||
} elseif ($dummy_if_empty) {
|
||||
return [[
|
||||
'type' => '',
|
||||
'value' => '',
|
||||
]];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
@@ -625,8 +753,9 @@ class Customer extends Model
|
||||
*/
|
||||
public function getWebsites($dummy_if_empty = false)
|
||||
{
|
||||
if ($this->websites) {
|
||||
return json_decode($this->websites);
|
||||
$websites = json_decode($this->websites ?? '', true);
|
||||
if (is_array($websites) && count($websites)) {
|
||||
return $websites;
|
||||
} elseif ($dummy_if_empty) {
|
||||
return [''];
|
||||
} else {
|
||||
@@ -643,17 +772,106 @@ class Customer extends Model
|
||||
{
|
||||
$websites = [];
|
||||
foreach ($websites_array as $key => $value) {
|
||||
$value = filter_var((string) $value, FILTER_SANITIZE_URL);
|
||||
// FILTER_SANITIZE_URL cuts some symbols.
|
||||
//$value = filter_var((string) $value, FILTER_SANITIZE_URL);
|
||||
if (isset($value['value'])) {
|
||||
$value = $value['value'];
|
||||
}
|
||||
if (!$value || preg_match("/^http(s)?:?\/?\/?$/i", $value)) {
|
||||
continue;
|
||||
}
|
||||
if (!preg_match("/http(s)?:\/\//i", $value)) {
|
||||
$value = 'http://'.$value;
|
||||
}
|
||||
$websites[] = (string) $value;
|
||||
}
|
||||
$this->websites = json_encode($websites);
|
||||
$this->websites = \Helper::jsonEncodeUtf8(array_unique($websites));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create customer or get existing.
|
||||
* Add website.
|
||||
*/
|
||||
public function addWebsite($website)
|
||||
{
|
||||
$websites = $this->getWebsites();
|
||||
if (isset($website['value'])) {
|
||||
$website = $website['value'];
|
||||
}
|
||||
array_push($websites, $website);
|
||||
$this->setWebsites($websites);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize social profiles.
|
||||
*
|
||||
* @param array $list [description]
|
||||
*
|
||||
* @return array [description]
|
||||
*/
|
||||
public static function formatSocialProfiles(array $list)
|
||||
{
|
||||
$social_profiles = [];
|
||||
foreach ($list as $social_profile) {
|
||||
if (is_array($social_profile)) {
|
||||
if (!empty($social_profile['value']) && !empty($social_profile['type'])) {
|
||||
|
||||
$type = null;
|
||||
|
||||
if (is_numeric($social_profile['type']) && in_array($social_profile['type'], array_keys(self::$social_types))) {
|
||||
$type = (int)$social_profile['type'];
|
||||
} else {
|
||||
// Find type.
|
||||
foreach (self::$social_types as $type_id => $type_name) {
|
||||
if ($type_name == strtolower($social_profile['type'])) {
|
||||
$type = $type_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$social_profiles[] = [
|
||||
'value' => (string) $social_profile['value'],
|
||||
'type' => $type,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$social_profiles[] = [
|
||||
'value' => (string) $social_profile,
|
||||
'type' => self::SOCIAL_TYPE_OTHER,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $social_profiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set social profiles as JSON.
|
||||
*
|
||||
* @param array $websites_array
|
||||
*/
|
||||
public function setSocialProfiles(array $sp_array)
|
||||
{
|
||||
$sp_array = self::formatSocialProfiles($sp_array);
|
||||
|
||||
// Remove dubplicates.
|
||||
$list = [];
|
||||
foreach ($sp_array as $i => $data) {
|
||||
if (in_array($data['value'], $list)) {
|
||||
unset($sp_array[$i]);
|
||||
} else {
|
||||
$list[] = $data['value'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->social_profiles = \Helper::jsonEncodeUtf8($sp_array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create customer or get existing and fill empty fields.
|
||||
*
|
||||
* @param string $email
|
||||
* @param array $data [description]
|
||||
@@ -662,6 +880,8 @@ class Customer extends Model
|
||||
*/
|
||||
public static function create($email, $data = [])
|
||||
{
|
||||
$new = false;
|
||||
|
||||
$email = Email::sanitizeEmail($email);
|
||||
if (!$email) {
|
||||
return null;
|
||||
@@ -670,27 +890,194 @@ class Customer extends Model
|
||||
if ($email_obj) {
|
||||
$customer = $email_obj->customer;
|
||||
|
||||
if (empty($customer->first_name) && !empty($data['first_name'])) {
|
||||
// In case somehow the email has no customer.
|
||||
if (!$customer) {
|
||||
// Customer will be saved and connected to the email later.
|
||||
$customer = new self();
|
||||
}
|
||||
|
||||
// Update name if empty.
|
||||
/*if (empty($customer->first_name) && !empty($data['first_name'])) {
|
||||
$customer->first_name = $data['first_name'];
|
||||
if (empty($customer->last_name) && !empty($data['last_name'])) {
|
||||
$customer->last_name = $data['last_name'];
|
||||
}
|
||||
$customer->save();
|
||||
}
|
||||
}*/
|
||||
} else {
|
||||
$customer = new self();
|
||||
$email_obj = new Email();
|
||||
$email_obj->email = $email;
|
||||
|
||||
$customer->fill($data);
|
||||
$new = true;
|
||||
}
|
||||
|
||||
// Set empty fields
|
||||
if ($customer->setData($data, false) || !$customer->id) {
|
||||
$customer->save();
|
||||
}
|
||||
|
||||
if (empty($email_obj->id) || !$email_obj->customer_id) {
|
||||
$email_obj->customer()->associate($customer);
|
||||
$email_obj->save();
|
||||
if (empty($email_obj->id) || !$email_obj->customer_id || $email_obj->customer_id != $customer->id) {
|
||||
// Email may have been set in setData().
|
||||
$save_email = true;
|
||||
if (!empty($data['emails']) && is_array($data['emails'])) {
|
||||
foreach ($data['emails'] as $data_email) {
|
||||
if (is_string($data_email) && $data_email == $email) {
|
||||
$save_email = false;
|
||||
break;
|
||||
}
|
||||
if (is_array($data_email) && !empty($data_email['value']) && $data_email['value'] == $email) {
|
||||
$save_email = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($save_email) {
|
||||
$email_obj->customer()->associate($customer);
|
||||
$email_obj->save();
|
||||
}
|
||||
}
|
||||
|
||||
// Todo: check phone uniqueness.
|
||||
|
||||
if ($new) {
|
||||
\Eventy::action('customer.created', $customer);
|
||||
}
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set empty fields.
|
||||
*/
|
||||
public function setData($data, $replace_data = true, $save = false)
|
||||
{
|
||||
$result = false;
|
||||
|
||||
// todo: photoUrl.
|
||||
if (isset($data['photo_url'])) {
|
||||
unset($data['photo_url']);
|
||||
}
|
||||
|
||||
if (!empty($data['background']) && empty($data['notes'])) {
|
||||
$data['notes'] = $data['background'];
|
||||
}
|
||||
|
||||
if ($replace_data) {
|
||||
// Replace data.
|
||||
$data_prepared = $data;
|
||||
foreach ($data_prepared as $i => $value) {
|
||||
if (is_array($value)) {
|
||||
unset($data_prepared[$i]);
|
||||
}
|
||||
}
|
||||
$this->fill($data_prepared);
|
||||
$result = true;
|
||||
} else {
|
||||
// Update empty fields.
|
||||
foreach ($data as $key => $value) {
|
||||
if (in_array($key, $this->fillable) && empty($this->$key)) {
|
||||
$this->$key = $value;
|
||||
$result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set JSON values.
|
||||
if (!empty($data['phone'])) {
|
||||
$this->addPhone($data['phone']);
|
||||
}
|
||||
foreach ($data as $key => $value) {
|
||||
if (!in_array($key, $this->json_fields) && $key != 'emails') {
|
||||
continue;
|
||||
}
|
||||
if ($key == 'phones') {
|
||||
if (isset($value['value'])) {
|
||||
$this->addPhone($value);
|
||||
} else {
|
||||
$this->setPhones($value);
|
||||
// foreach ($value as $phone_value) {
|
||||
// $this->addPhone($phone_value);
|
||||
// }
|
||||
}
|
||||
$result = true;
|
||||
}
|
||||
if ($key == 'websites') {
|
||||
if (is_array($value)) {
|
||||
$this->setWebsites($value);
|
||||
// foreach ($value as $website) {
|
||||
// $this->addWebsite($website);
|
||||
// }
|
||||
} else {
|
||||
$this->addWebsite($value);
|
||||
}
|
||||
$result = true;
|
||||
}
|
||||
if ($key == 'social_profiles') {
|
||||
$this->setSocialProfiles($value);
|
||||
$result = true;
|
||||
}
|
||||
if ($key == 'country') {
|
||||
if (array_search($this->country, Customer::$countries)) {
|
||||
$this->country = array_search($this->country, Customer::$countries);
|
||||
}
|
||||
$this->country = strtoupper(mb_substr($this->country, 0, 2));
|
||||
$result = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Emails must be processed the last as they need to save object.
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key == 'emails') {
|
||||
foreach ($value as $email_data) {
|
||||
if (is_string($email_data)) {
|
||||
if (!$this->id) {
|
||||
$this->save();
|
||||
}
|
||||
$email_created = Email::create($email_data, $this->id, Email::TYPE_WORK);
|
||||
|
||||
if ($email_created) {
|
||||
$result = true;
|
||||
}
|
||||
} elseif (!empty($email_data['value'])) {
|
||||
if (!$this->id) {
|
||||
$this->save();
|
||||
}
|
||||
$email_created = Email::create($email_data['value'], $this->id, $email_data['type']);
|
||||
|
||||
if ($email_created) {
|
||||
$result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Maybe Todo: check phone uniqueness.
|
||||
// Same phone can be written in many ways, so it's almost useless to chek uniqueness.
|
||||
|
||||
\Eventy::action('customer.set_data', $this, $data, $replace_data);
|
||||
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a customer, email is not required.
|
||||
* For phone conversations.
|
||||
*/
|
||||
public static function createWithoutEmail($data = [])
|
||||
{
|
||||
$customer = new self();
|
||||
$customer->setData($data);
|
||||
|
||||
$customer->save();
|
||||
|
||||
\Eventy::action('customer.created', $customer);
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
@@ -704,6 +1091,16 @@ class Customer extends Model
|
||||
return route('customers.update', ['id'=>$this->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get view customer URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function urlView()
|
||||
{
|
||||
return route('customers.conversations', ['id'=>$this->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date according to customer's timezone.
|
||||
*
|
||||
@@ -717,9 +1114,274 @@ class Customer extends Model
|
||||
return $date->format($format);
|
||||
}
|
||||
|
||||
public function getPhotoUrl()
|
||||
/**
|
||||
* Get full representation of customer.
|
||||
*/
|
||||
public function getEmailAndName()
|
||||
{
|
||||
// todo
|
||||
return '/img/default-avatar.png';
|
||||
// Email can be fetched using query.
|
||||
$text = $this->email;
|
||||
if (!$text) {
|
||||
$text = $this->getMainEmail();
|
||||
}
|
||||
if ($this->getFullName()) {
|
||||
if ($text) {
|
||||
$text .= ' ('.$this->getFullName().')';
|
||||
} else {
|
||||
$text .= $this->getFullName();
|
||||
}
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
public function getNameAndEmail()
|
||||
{
|
||||
// Email can be fetched using query.
|
||||
$text = $this->getFullName();
|
||||
$email = $this->email;
|
||||
if (!$email) {
|
||||
$email = $this->getMainEmail();
|
||||
}
|
||||
if ($email) {
|
||||
if ($text) {
|
||||
$text .= ' <'.$email.'>';
|
||||
} else {
|
||||
$text .= $email;
|
||||
}
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customers info for the list of emails.
|
||||
*/
|
||||
public static function emailsToCustomers($list)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
$data = Customer::select(['emails.email', 'customers.first_name', 'customers.last_name'])
|
||||
->join('emails', function ($join) {
|
||||
$join->on('emails.customer_id', '=', 'customers.id');
|
||||
})
|
||||
->whereIn('emails.email', $list)
|
||||
//->groupby('customers.id')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
foreach ($list as $email) {
|
||||
// Dummy customer.
|
||||
$customer = new Customer();
|
||||
$customer->email = $email;
|
||||
|
||||
foreach ($data as $values) {
|
||||
if (strtolower($values['email']) == strtolower($email)) {
|
||||
$customer->first_name = $values['first_name'];
|
||||
$customer->last_name = $values['last_name'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
$result[$email] = $customer->getNameAndEmail();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer by email.
|
||||
*/
|
||||
public static function getByEmail($email)
|
||||
{
|
||||
return Customer::select('customers.*')
|
||||
->where('emails.email', $email)
|
||||
->join('emails', function ($join) {
|
||||
$join->on('emails.customer_id', '=', 'customers.id');
|
||||
})->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email or phone if email is empty.
|
||||
*/
|
||||
public function getEmailOrPhone()
|
||||
{
|
||||
if (!empty($this->email)) {
|
||||
// Email can be selected with query.
|
||||
return $this->email;
|
||||
} elseif ($main_email = $this->getMainEmail()) {
|
||||
return $main_email;
|
||||
} elseif ($phones = $this->getPhones() && !empty($phones[0]['value'])) {
|
||||
return $phones[0]['value'];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getPhotoUrl($default_if_empty = true)
|
||||
{
|
||||
if (!empty($this->photo_url) || !$default_if_empty) {
|
||||
if (!empty($this->photo_url)) {
|
||||
return self::getPhotoUrlByFileName($this->photo_url);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
return asset('/img/default-avatar.png');
|
||||
}
|
||||
}
|
||||
|
||||
public static function getPhotoUrlByFileName($file_name)
|
||||
{
|
||||
return Storage::url(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$file_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize and save photo.
|
||||
*/
|
||||
public function savePhoto($real_path, $mime_type)
|
||||
{
|
||||
$resized_image = \App\Misc\Helper::resizeImage($real_path, $mime_type, self::PHOTO_SIZE, self::PHOTO_SIZE);
|
||||
|
||||
if (!$resized_image) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$file_name = md5(Hash::make($this->id)).'.jpg';
|
||||
$dest_path = Storage::path(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$file_name);
|
||||
|
||||
$dest_dir = pathinfo($dest_path, PATHINFO_DIRNAME);
|
||||
if (!file_exists($dest_dir)) {
|
||||
\File::makeDirectory($dest_dir, 0755);
|
||||
}
|
||||
|
||||
// Remove current photo
|
||||
if ($this->photo_url) {
|
||||
Storage::delete(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$this->photo_url);
|
||||
}
|
||||
|
||||
imagejpeg($resized_image, $dest_path, self::PHOTO_QUALITY);
|
||||
|
||||
return $file_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove user photo.
|
||||
*/
|
||||
public function removePhoto()
|
||||
{
|
||||
if ($this->photo_url) {
|
||||
Storage::delete(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$this->photo_url);
|
||||
}
|
||||
$this->photo_url = '';
|
||||
}
|
||||
|
||||
public function getCountryName()
|
||||
{
|
||||
if ($this->country && !empty(self::$countries[$this->country])) {
|
||||
return self::$countries[$this->country];
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get first and last name.
|
||||
*/
|
||||
public static function parseName($name)
|
||||
{
|
||||
$data = [];
|
||||
|
||||
if (!$name) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$name_parts = explode(' ', $name, 2);
|
||||
$data['first_name'] = $name_parts[0];
|
||||
if (!empty($name_parts[1])) {
|
||||
$data['last_name'] = $name_parts[1];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public static function formatSocialProfile($sp)
|
||||
{
|
||||
if (empty($sp['type']) || !isset(self::$social_type_names[$sp['type']])) {
|
||||
$sp['type'] = self::SOCIAL_TYPE_OTHER;
|
||||
}
|
||||
|
||||
$sp['type_name'] = self::$social_type_names[$sp['type']];
|
||||
|
||||
$sp['value_url'] = $sp['value'];
|
||||
|
||||
if (!preg_match("/^https?:\/\//i", $sp['value_url'])) {
|
||||
switch ($sp['type']) {
|
||||
case self::SOCIAL_TYPE_TELEGRAM:
|
||||
$sp['value_url'] = 'https://t.me/'.$sp['value'];
|
||||
break;
|
||||
|
||||
default:
|
||||
$sp['value_url'] = 'http://'.$sp['value_url'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (empty($sp['value_url'])) {
|
||||
$sp['value_url'] = '';
|
||||
}
|
||||
|
||||
return $sp;
|
||||
}
|
||||
|
||||
public function setPhotoFromRemoteFile($url)
|
||||
{
|
||||
$headers = get_headers($url);
|
||||
|
||||
if (!preg_match("/200/", $headers[0])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$image_data = file_get_contents($url);
|
||||
|
||||
if (!$image_data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$temp_file = tempnam(sys_get_temp_dir(), 'photo');
|
||||
|
||||
\File::put($temp_file, $image_data);
|
||||
|
||||
$photo_url = $this->savePhoto($temp_file, \File::mimeType($temp_file));
|
||||
|
||||
if ($photo_url) {
|
||||
$this->photo_url = $photo_url;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getChannelName()
|
||||
{
|
||||
return \Eventy::filter('channel.name', '', $this->channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get thread meta value.
|
||||
*/
|
||||
public function getMeta($key, $default = null)
|
||||
{
|
||||
if (isset($this->meta[$key])) {
|
||||
return $this->meta[$key];
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set thread meta value.
|
||||
*/
|
||||
public function setMeta($key, $value)
|
||||
{
|
||||
$meta = $this->meta;
|
||||
$meta[$key] = $value;
|
||||
$this->meta = $meta;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Watson\Rememberable\Rememberable;
|
||||
|
||||
class Email extends Model
|
||||
{
|
||||
use Rememberable;
|
||||
// This is obligatory.
|
||||
public $rememberCacheDriver = 'array';
|
||||
|
||||
/**
|
||||
* Email types.
|
||||
*/
|
||||
@@ -47,11 +52,15 @@ class Email extends Model
|
||||
// Email validation is not recommended:
|
||||
// http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address/201378#201378
|
||||
// So we just check for @
|
||||
if (!preg_match('/@/', $email)) {
|
||||
if (!preg_match('/@/', $email ?? '')) {
|
||||
return false;
|
||||
}
|
||||
$email = filter_var($email, FILTER_SANITIZE_EMAIL);
|
||||
$email = mb_strtolower($email, 'UTF-8');
|
||||
// Remove trailing dots.
|
||||
$email = preg_replace("/\.+$/", '', $email);
|
||||
// Remove dot before @
|
||||
$email = preg_replace("/\.+@/", '@', $email);
|
||||
|
||||
return $email;
|
||||
}
|
||||
@@ -60,4 +69,19 @@ class Email extends Model
|
||||
{
|
||||
return explode('@', $this->email)[0];
|
||||
}
|
||||
|
||||
public static function create($email, $customer_id, $type = self::TYPE_WORK)
|
||||
{
|
||||
try {
|
||||
$email_obj = new Email();
|
||||
$email_obj->email = $email;
|
||||
$email_obj->type = array_key_exists($type, self::$types) ? $type : self::TYPE_WORK;
|
||||
$email_obj->customer_id = $customer_id;
|
||||
$email_obj->save();
|
||||
|
||||
return $email_obj;
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,15 @@
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RealtimeBroadcastNotificationCreated implements ShouldBroadcastNow
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
/**
|
||||
* The notifiable entity who received the notification.
|
||||
*
|
||||
* @var mixed
|
||||
@@ -55,7 +53,7 @@ class RealtimeBroadcastNotificationCreated implements ShouldBroadcastNow
|
||||
{
|
||||
$channels = $this->notification->broadcastOn();
|
||||
|
||||
if (! empty($channels)) {
|
||||
if (!empty($channels)) {
|
||||
return $channels;
|
||||
}
|
||||
|
||||
@@ -72,7 +70,7 @@ class RealtimeBroadcastNotificationCreated implements ShouldBroadcastNow
|
||||
public function broadcastWith()
|
||||
{
|
||||
return array_merge($this->data, [
|
||||
'id' => $this->notification->id,
|
||||
'id' => $this->notification->id,
|
||||
'type' => get_class($this->notification),
|
||||
]);
|
||||
// return [
|
||||
@@ -95,4 +93,4 @@ class RealtimeBroadcastNotificationCreated implements ShouldBroadcastNow
|
||||
|
||||
return $class.'.'.$this->notifiable->getKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
118
app/Events/RealtimeConvNewThread.php
Normal file
118
app/Events/RealtimeConvNewThread.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
/**
|
||||
* New thread created in conversation.
|
||||
*/
|
||||
namespace App\Events;
|
||||
|
||||
use App\Conversation;
|
||||
use App\Mailbox;
|
||||
use App\Thread;
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RealtimeConvNewThread implements ShouldBroadcastNow
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* The notification data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $data = [];
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new \Illuminate\Broadcasting\Channel($this->channelName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data to broadcast.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function broadcastWith()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the broadcast channel name for the event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function channelName()
|
||||
{
|
||||
if (!empty($this->data['conversation_id'])) {
|
||||
return 'conv.'.$this->data['conversation_id'];
|
||||
} else {
|
||||
return 'conv.0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper funciton.
|
||||
*/
|
||||
public static function dispatchSelf($thread)
|
||||
{
|
||||
if ($thread->state != Thread::STATE_PUBLISHED) {
|
||||
return;
|
||||
}
|
||||
$notification_data = [
|
||||
'thread_id' => $thread->id,
|
||||
'conversation_id' => $thread->conversation_id,
|
||||
// conversation is prefetched in ThreadObserver.
|
||||
'mailbox_id' => $thread->conversation->mailbox_id,
|
||||
//'user_id' => $thread->created_by_user_id,
|
||||
];
|
||||
event(new \App\Events\RealtimeConvNewThread($notification_data));
|
||||
}
|
||||
|
||||
public static function processPayload($payload)
|
||||
{
|
||||
$user = auth()->user();
|
||||
$mailbox = Mailbox::rememberForever()->find($payload->mailbox_id);
|
||||
|
||||
// Check if user can listen to this event.
|
||||
if (!$user || !$mailbox || !$user->can('viewCached', $mailbox)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$thread = Thread::find($payload->thread_id);
|
||||
if (!$thread) {
|
||||
return $payload;
|
||||
}
|
||||
|
||||
// Add thread html to the payload.
|
||||
$template_data = [
|
||||
'conversation' => $thread->conversation,
|
||||
'mailbox' => $thread->conversation->mailbox,
|
||||
'threads' => [$thread],
|
||||
];
|
||||
|
||||
$payload->thread_html = \View::make('conversations/partials/threads')->with($template_data)->render();
|
||||
$payload->conversation_user_id = $thread->conversation->user_id;
|
||||
$payload->conversation_status = $thread->conversation->status;
|
||||
$payload->conversation_status_class = Conversation::$status_classes[$thread->conversation->status];
|
||||
$payload->conversation_status_icon = Conversation::$status_icons[$thread->conversation->status];
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
113
app/Events/RealtimeConvView.php
Normal file
113
app/Events/RealtimeConvView.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
//use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RealtimeConvView implements ShouldBroadcastNow
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* The notifiable entity who received the notification.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
//public $notifiable;
|
||||
|
||||
/**
|
||||
* The notification instance.
|
||||
*
|
||||
* @var \Illuminate\Notifications\Notification
|
||||
*/
|
||||
//public $notification;
|
||||
|
||||
/**
|
||||
* The notification data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $data = [];
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(/*$notifiable, $notification,*/ $data)
|
||||
{
|
||||
$this->data = $data;
|
||||
// $this->notifiable = $notifiable;
|
||||
// $this->notification = $notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
// $channels = $this->notification->broadcastOn();
|
||||
|
||||
// if (!empty($channels)) {
|
||||
// return $channels;
|
||||
// }
|
||||
|
||||
return new \Illuminate\Broadcasting\Channel($this->channelName());
|
||||
//return [new PrivateChannel()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data to broadcast.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function broadcastWith()
|
||||
{
|
||||
return $this->data;
|
||||
|
||||
/*return array_merge($this->data, [
|
||||
'id' => $this->notification->id,
|
||||
'type' => get_class($this->notification),
|
||||
]);*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the broadcast channel name for the event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function channelName()
|
||||
{
|
||||
// if (method_exists($this->notifiable, 'receivesBroadcastNotificationsOn')) {
|
||||
// return $this->notifiable->receivesBroadcastNotificationsOn($this->notification);
|
||||
// }
|
||||
|
||||
return 'conv';
|
||||
|
||||
// $class = str_replace('\\', '.', get_class($this->notifiable));
|
||||
|
||||
// return $class.'.'.$this->notifiable->getKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper funciton.
|
||||
*/
|
||||
public static function dispatchSelf($conversation_id, $user, $replying = false)
|
||||
{
|
||||
$notification_data = [
|
||||
'conversation_id' => $conversation_id,
|
||||
'user_id' => $user->id,
|
||||
'user_photo_url' => $user->getPhotoUrl(false),
|
||||
// These has to be encoded to avoid "Unable to JSON encode payload. Error code: 5"
|
||||
'user_initials' => htmlentities($user->getInitials()),
|
||||
'user_name' => htmlentities($user->getFullName()),
|
||||
'replying' => (int)$replying,
|
||||
];
|
||||
event(new \App\Events\RealtimeConvView($notification_data));
|
||||
}
|
||||
}
|
||||
62
app/Events/RealtimeConvViewFinish.php
Normal file
62
app/Events/RealtimeConvViewFinish.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/**
|
||||
* User finished viewing conversatin.
|
||||
*/
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RealtimeConvViewFinish implements ShouldBroadcastNow
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* The notification data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $data = [];
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new \Illuminate\Broadcasting\Channel($this->channelName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data to broadcast.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function broadcastWith()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the broadcast channel name for the event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function channelName()
|
||||
{
|
||||
return 'conv';
|
||||
}
|
||||
}
|
||||
110
app/Events/RealtimeMailboxNewThread.php
Normal file
110
app/Events/RealtimeMailboxNewThread.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
/**
|
||||
* New thread created in mailbox.
|
||||
*/
|
||||
namespace App\Events;
|
||||
|
||||
use App\Conversation;
|
||||
use App\Mailbox;
|
||||
use App\Folder;
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RealtimeMailboxNewThread implements ShouldBroadcastNow
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* The notification data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $data = [];
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new \Illuminate\Broadcasting\Channel($this->channelName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data to broadcast.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function broadcastWith()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the broadcast channel name for the event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function channelName()
|
||||
{
|
||||
if (!empty($this->data['mailbox_id'])) {
|
||||
return 'mailbox.'.$this->data['mailbox_id'];
|
||||
} else {
|
||||
return 'mailbox.0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper funciton.
|
||||
*/
|
||||
public static function dispatchSelf($mailbox_id)
|
||||
{
|
||||
$notification_data = [
|
||||
'mailbox_id' => $mailbox_id
|
||||
];
|
||||
event(new \App\Events\RealtimeMailboxNewThread($notification_data));
|
||||
}
|
||||
|
||||
public static function processPayload($payload)
|
||||
{
|
||||
$user = auth()->user();
|
||||
$mailbox = Mailbox::rememberForever()->find($payload->mailbox_id);
|
||||
|
||||
// Check if user can listen to this event.
|
||||
if (!$user || !$mailbox || !$user->can('viewCached', $mailbox)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$folder = null;
|
||||
$foler_id = Conversation::getFolderParam();
|
||||
if ($foler_id) {
|
||||
$folder = Folder::find($foler_id);
|
||||
}
|
||||
// Just in case.
|
||||
if (!$folder) {
|
||||
$folder = new Folder();
|
||||
}
|
||||
$template_data = [
|
||||
'folders' => $mailbox->getAssesibleFolders(),
|
||||
'folder' => $folder,
|
||||
'mailbox' => $mailbox,
|
||||
];
|
||||
|
||||
$payload->folders_html = \View::make('mailboxes/partials/folders')->with($template_data)->render();
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use App\User;
|
||||
|
||||
class UserDeleted
|
||||
{
|
||||
public $user;
|
||||
public $by_user;
|
||||
public $deleted_user;
|
||||
|
||||
/**
|
||||
@@ -14,9 +14,11 @@ class UserDeleted
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(User $user, User $deleted_user)
|
||||
public function __construct(User $deleted_user, User $by_user)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->by_user = $by_user;
|
||||
$this->deleted_user = $deleted_user;
|
||||
|
||||
\Eventy::action('user.deleted', $deleted_user, $by_user);
|
||||
}
|
||||
}
|
||||
|
||||
213
app/Folder.php
213
app/Folder.php
@@ -23,12 +23,12 @@ class Folder extends Model
|
||||
public static $types = [
|
||||
self::TYPE_UNASSIGNED => 'Unassigned',
|
||||
self::TYPE_MINE => 'Mine',
|
||||
self::TYPE_STARRED => 'Starred',
|
||||
self::TYPE_DRAFTS => 'Drafts',
|
||||
self::TYPE_ASSIGNED => 'Assigned',
|
||||
self::TYPE_CLOSED => 'Closed',
|
||||
self::TYPE_SPAM => 'Spam',
|
||||
self::TYPE_DELETED => 'Deleted',
|
||||
self::TYPE_STARRED => 'Starred',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -68,8 +68,16 @@ class Folder extends Model
|
||||
self::TYPE_STARRED,
|
||||
];
|
||||
|
||||
// Counter mode.
|
||||
const COUNTER_ACTIVE = 1;
|
||||
const COUNTER_TOTAL = 2;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $casts = [
|
||||
'meta' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the mailbox to which folder belongs.
|
||||
*/
|
||||
@@ -96,12 +104,32 @@ class Folder extends Model
|
||||
|
||||
public function getTypeName()
|
||||
{
|
||||
return __(self::$types[$this->type]);
|
||||
// To make name translatable.
|
||||
switch ($this->type) {
|
||||
case self::TYPE_UNASSIGNED:
|
||||
return __('Unassigned');
|
||||
case self::TYPE_MINE:
|
||||
return __('Mine');
|
||||
case self::TYPE_DRAFTS:
|
||||
return __('Drafts');
|
||||
case self::TYPE_ASSIGNED:
|
||||
return __('Assigned');
|
||||
case self::TYPE_CLOSED:
|
||||
return __('Closed');
|
||||
case self::TYPE_SPAM:
|
||||
return __('Spam');
|
||||
case self::TYPE_DELETED:
|
||||
return __('Deleted');
|
||||
case self::TYPE_STARRED:
|
||||
return __('Starred');
|
||||
default:
|
||||
return __(\Eventy::filter('folder.type_name', self::$types[$this->type] ?? '', $this));
|
||||
}
|
||||
}
|
||||
|
||||
public function getTypeIcon()
|
||||
{
|
||||
return self::$type_icons[$this->type];
|
||||
return \Eventy::filter('folder.type_icon', self::$type_icons[$this->type] ?? '', $this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,7 +152,6 @@ class Folder extends Model
|
||||
case self::TYPE_STARRED:
|
||||
$order_by[] = ['status' => 'asc'];
|
||||
$order_by[] = ['last_reply_at' => 'desc'];
|
||||
//$order_by = [['conversation_folder.id' => 'desc']];
|
||||
break;
|
||||
|
||||
case self::TYPE_DRAFTS:
|
||||
@@ -141,6 +168,30 @@ class Folder extends Model
|
||||
case self::TYPE_DELETED:
|
||||
$order_by = [['user_updated_at' => 'desc']];
|
||||
break;
|
||||
|
||||
default:
|
||||
$order_by = \Eventy::filter('folder.conversations_order_by', $order_by, $this->type);
|
||||
break;
|
||||
}
|
||||
|
||||
// Process columns sorting.
|
||||
$sorting = Conversation::getConvTableSorting();
|
||||
if ($sorting['sort_by'] == 'date') {
|
||||
if ($sorting['order'] != 'desc') {
|
||||
foreach ($order_by as $block_i => $block) {
|
||||
foreach ($block as $field => $order) {
|
||||
if ($field == 'status') {
|
||||
unset($order_by[$block_i][$field]);
|
||||
} else {
|
||||
if ($order == 'desc') {
|
||||
$order_by[$block_i][$field] = 'asc';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$order_by = [[$sorting['sort_by'] => $sorting['order']]];
|
||||
}
|
||||
|
||||
return $order_by;
|
||||
@@ -166,31 +217,38 @@ class Folder extends Model
|
||||
*/
|
||||
public function isIndirect()
|
||||
{
|
||||
return in_array($this->type, Folder::$indirect_types);
|
||||
return in_array($this->type, self::$indirect_types);
|
||||
}
|
||||
|
||||
public function updateCounters()
|
||||
{
|
||||
if ($this->type == Folder::TYPE_MINE && $this->user_id) {
|
||||
if (\Eventy::filter('folder.update_counters', false, $this)) {
|
||||
return;
|
||||
}
|
||||
if ($this->type == self::TYPE_MINE && $this->user_id) {
|
||||
$this->active_count = Conversation::where('user_id', $this->user_id)
|
||||
->where('mailbox_id', $this->mailbox_id)
|
||||
->where('status', Conversation::STATUS_ACTIVE)
|
||||
->where('state', Conversation::STATE_PUBLISHED)
|
||||
->where('status', Conversation::STATUS_ACTIVE)
|
||||
->count();
|
||||
$this->total_count = Conversation::where('user_id', $this->user_id)
|
||||
->where('mailbox_id', $this->mailbox_id)
|
||||
->where('state', Conversation::STATE_PUBLISHED)
|
||||
->count();
|
||||
} elseif ($this->type == Folder::TYPE_STARRED) {
|
||||
} elseif ($this->type == self::TYPE_STARRED) {
|
||||
$this->active_count = count(Conversation::getUserStarredConversationIds($this->mailbox_id, $this->user_id));
|
||||
$this->total_count = $this->active_count;
|
||||
} elseif ($this->type == Folder::TYPE_DELETED) {
|
||||
} elseif ($this->type == self::TYPE_DELETED) {
|
||||
$this->active_count = $this->conversations()->where('state', Conversation::STATE_DELETED)
|
||||
->count();
|
||||
$this->total_count = $this->active_count;
|
||||
} elseif ($this->isIndirect()) {
|
||||
// Conversation are connected to folder via conversation_folder table.
|
||||
$this->active_count = ConversationFolder::where('folder_id', $this->id)->count();
|
||||
// Drafts.
|
||||
$this->active_count = ConversationFolder::where('conversation_folder.folder_id', $this->id)
|
||||
->join('conversations', 'conversations.id', '=', 'conversation_folder.conversation_id')
|
||||
//->where('state', Conversation::STATE_PUBLISHED)
|
||||
->count();
|
||||
$this->total_count = $this->active_count;
|
||||
} else {
|
||||
$this->active_count = $this->conversations()
|
||||
@@ -204,19 +262,46 @@ class Folder extends Model
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count to display in folders list.
|
||||
*
|
||||
* @param array $folders [description]
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function getCount($folders = [])
|
||||
{
|
||||
$counter = \Eventy::filter('folder.counter', self::COUNTER_ACTIVE, $this, $folders);
|
||||
|
||||
$count = \Eventy::filter('folder.count', false, $this, $counter, $folders);
|
||||
if ($count !== false) {
|
||||
return $count;
|
||||
}
|
||||
|
||||
if ($counter == self::COUNTER_TOTAL || $this->type == self::TYPE_STARRED || $this->type == self::TYPE_DRAFTS) {
|
||||
return $this->total_count;
|
||||
} else {
|
||||
return $this->getActiveCount($folders);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calculated number of active conversations.
|
||||
*/
|
||||
public function getActiveCount($folders = [])
|
||||
{
|
||||
$active_count = $this->active_count;
|
||||
if ($this->type == Folder::TYPE_ASSIGNED) {
|
||||
if ($folders) {
|
||||
$mine_folder = $folders->firstWhere('type', Folder::TYPE_MINE);
|
||||
} else {
|
||||
$mine_folder = $this->mailbox->folders()->where('type', Folder::TYPE_MINE)->first();
|
||||
if ($this->type == self::TYPE_ASSIGNED) {
|
||||
$mine_folder = \Eventy::filter('folder.active_count_mine_folder', null, $this, $folders);
|
||||
|
||||
if (!$mine_folder) {
|
||||
if ($folders) {
|
||||
$mine_folder = $folders->firstWhere('type', self::TYPE_MINE);
|
||||
} elseif ($this->mailbox_id) {
|
||||
$mine_folder = $this->mailbox->folders()->where('type', self::TYPE_MINE)->first();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($mine_folder) {
|
||||
$active_count = $active_count - $mine_folder->active_count;
|
||||
if ($active_count < 0) {
|
||||
@@ -227,4 +312,100 @@ class Folder extends Model
|
||||
|
||||
return $active_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query for waiting since.
|
||||
*/
|
||||
public function getWaitingSinceQuery()
|
||||
{
|
||||
$query = null;
|
||||
|
||||
if ($this->type == self::TYPE_MINE) {
|
||||
// Assigned to user.
|
||||
$query = Conversation::where('user_id', $this->user_id)
|
||||
->where('mailbox_id', $this->mailbox_id);
|
||||
} elseif ($this->isIndirect()) {
|
||||
// Via intermediate table.
|
||||
$query = Conversation::join('conversation_folder', 'conversations.id', '=', 'conversation_folder.conversation_id')
|
||||
->where('conversation_folder.folder_id', $this->id);
|
||||
} else {
|
||||
// All other conversations.
|
||||
$query = $this->conversations();
|
||||
}
|
||||
|
||||
return \Eventy::filter('folder.waiting_since_query', $query, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Works for main folder only for now.
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function getWaitingSince()
|
||||
{
|
||||
// Get oldest active conversation.
|
||||
$conversation = $this->getWaitingSinceQuery()
|
||||
->where('state', Conversation::STATE_PUBLISHED)
|
||||
->where('status', Conversation::STATUS_ACTIVE)
|
||||
->orderBy($this->getWaitingSinceField(), 'asc')
|
||||
->first();
|
||||
if ($conversation) {
|
||||
return $conversation->getWaitingSince($this);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conversation field used to detect waiting since time.
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function getWaitingSinceField()
|
||||
{
|
||||
if ($this->type == \App\Folder::TYPE_CLOSED) {
|
||||
return 'closed_at';
|
||||
} elseif ($this->type == \App\Folder::TYPE_DRAFTS) {
|
||||
return 'updated_at';
|
||||
} elseif ($this->type == \App\Folder::TYPE_DELETED) {
|
||||
return 'user_updated_at';
|
||||
} else {
|
||||
return'last_reply_at';
|
||||
}
|
||||
}
|
||||
|
||||
public function url($mailbox_id)
|
||||
{
|
||||
return \Eventy::filter('folder.url', route('mailboxes.view.folder', ['id'=>$mailbox_id, 'folder_id'=>$this->id]), $mailbox_id, $this);
|
||||
}
|
||||
|
||||
public static function create($data, $unique_per_user = true, $save = true)
|
||||
{
|
||||
if (!isset($data['mailbox_id']) || !isset($data['type'])) {
|
||||
return null;
|
||||
}
|
||||
$folder = new Folder ();
|
||||
$folder->mailbox_id = $data['mailbox_id'];
|
||||
$folder->type = $data['type'];
|
||||
|
||||
if (!empty($data['user_id'])) {
|
||||
if ($unique_per_user) {
|
||||
$user_folder = Folder::where('mailbox_id', $data['mailbox_id'])
|
||||
->where('user_id', $data['user_id'])
|
||||
->where('type', $data['type'])
|
||||
->first();
|
||||
// User folder already exists.
|
||||
if ($user_folder) {
|
||||
return $user_folder;
|
||||
}
|
||||
}
|
||||
$folder->user_id = $data['user_id'];
|
||||
}
|
||||
|
||||
if ($save) {
|
||||
$folder->save();
|
||||
}
|
||||
|
||||
return $folder;
|
||||
}
|
||||
}
|
||||
|
||||
10
app/Follower.php
Normal file
10
app/Follower.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Follower extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ class LoginController extends Controller
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/';
|
||||
protected $redirectTo = '/home';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
|
||||
1725
app/Http/Controllers/ConversationsController.php
Normal file → Executable file
1725
app/Http/Controllers/ConversationsController.php
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,6 @@ use App\Conversation;
|
||||
use App\Customer;
|
||||
use App\Email;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Validator;
|
||||
|
||||
class CustomersController extends Controller
|
||||
@@ -68,12 +67,26 @@ class CustomersController extends Controller
|
||||
//'emails' => 'array|required_without:first_name',
|
||||
//'emails.1' => 'nullable|email|required_without:first_name',
|
||||
'emails.*' => 'nullable|email|distinct|required_without:first_name',
|
||||
'photo_url' => 'nullable|image|mimes:jpeg,png,jpg,gif',
|
||||
]);
|
||||
$validator->setAttributeNames([
|
||||
//'emails.1' => __('Email'),
|
||||
'photo_url' => __('Photo'),
|
||||
'emails.*' => __('Email'),
|
||||
]);
|
||||
|
||||
|
||||
// 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());
|
||||
|
||||
if ($path_url) {
|
||||
$customer->photo_url = $path_url;
|
||||
} else {
|
||||
$validator->errors()->add('photo_url', __('Error occured processing the image. Make sure that PHP GD extension is enabled.'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('customers.update', ['id' => $id])
|
||||
->withErrors($validator)
|
||||
@@ -96,12 +109,12 @@ class CustomersController extends Controller
|
||||
foreach ($new_emails as $new_email) {
|
||||
$email = Email::where('email', $new_email)->first();
|
||||
if ($email && $email->customer) {
|
||||
// If customer whose email is removed does not have first name and other emails
|
||||
// If customer whose email is removed does not have first name and other emails
|
||||
// we have to create first name for this customer
|
||||
if (!$email->customer->first_name && count($email->customer->emails) == 1) {
|
||||
if ($request->first_name) {
|
||||
$email->customer->first_name = $request->first_name;
|
||||
} else if ($customer->first_name) {
|
||||
} elseif ($customer->first_name) {
|
||||
$email->customer->first_name = $customer->first_name;
|
||||
} else {
|
||||
$email->customer->first_name = mb_ucfirst($email->getNameFromEmail());
|
||||
@@ -110,12 +123,12 @@ class CustomersController extends Controller
|
||||
}
|
||||
|
||||
$flash_message .= __('Email :tag_email_begin:email:tag_email_end has been moved from another customer: :a_begin:customer:a_end.', [
|
||||
'email' => $email->email,
|
||||
'email' => $email->email,
|
||||
'tag_email_begin' => '<strong>',
|
||||
'tag_email_end' => '</strong>',
|
||||
'customer' => $email->customer->getFullName(),
|
||||
'a_begin' => '<strong><a href="'.$email->customer->url().'" target="_blank">',
|
||||
'a_end' => '</a></strong>',
|
||||
'tag_email_end' => '</strong>',
|
||||
'customer' => $email->customer->getFullName(),
|
||||
'a_begin' => '<strong><a href="'.$email->customer->url().'" target="_blank">',
|
||||
'a_end' => '</a></strong>',
|
||||
]).' ';
|
||||
|
||||
$new_emails_change_customer[] = $email->email;
|
||||
@@ -129,11 +142,17 @@ class CustomersController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$customer->fill($request->all());
|
||||
// Websites
|
||||
if (!empty($request->websites)) {
|
||||
$customer->setWebsites($request->websites);
|
||||
$request_data = $request->all();
|
||||
|
||||
if (isset($request_data['photo_url'])) {
|
||||
unset($request_data['photo_url']);
|
||||
}
|
||||
|
||||
$customer->setData($request_data);
|
||||
// Websites
|
||||
// if (!empty($request->websites)) {
|
||||
// $customer->setWebsites($request->websites);
|
||||
// }
|
||||
$customer->save();
|
||||
|
||||
$customer->syncEmails($request->emails);
|
||||
@@ -159,8 +178,12 @@ class CustomersController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
\Eventy::action('customer.updated', $customer);
|
||||
|
||||
$flash_message = __('Customer saved successfully.').' '.$flash_message;
|
||||
\Session::flash('flash_success_unescaped', $flash_message);
|
||||
|
||||
\Session::flash('customer.updated', 1);
|
||||
|
||||
return redirect()->route('customers.update', ['id' => $id]);
|
||||
}
|
||||
@@ -207,12 +230,13 @@ class CustomersController extends Controller
|
||||
|
||||
$conversations = $customer->conversations()
|
||||
->where('customer_id', $customer->id)
|
||||
->whereIn('mailbox_id', auth()->user()->mailboxesIdsCanView())
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(Conversation::DEFAULT_LIST_SIZE);
|
||||
|
||||
|
||||
return view('customers/conversations', [
|
||||
'customer' => $customer,
|
||||
'conversations' => $conversations
|
||||
'conversations' => $conversations,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -222,33 +246,94 @@ class CustomersController extends Controller
|
||||
public function ajaxSearch(Request $request)
|
||||
{
|
||||
$response = [
|
||||
'results' => [],
|
||||
'pagination' => ['more' => false]
|
||||
'results' => [],
|
||||
'pagination' => ['more' => false],
|
||||
];
|
||||
|
||||
$q = $request->q;
|
||||
|
||||
$customers = Customer::select('customers.id', 'emails.email', 'first_name', 'last_name')
|
||||
->join('emails', 'customers.id', '=', 'emails.customer_id')
|
||||
->where('emails.email', 'like', '%'.$q.'%')
|
||||
->where('emails.email', '<>', $request->exclude_email)
|
||||
->orWhere('first_name', 'like', '%'.$q.'%')
|
||||
->orWhere('last_name', 'like', '%'.$q.'%')
|
||||
->paginate(20);
|
||||
$join_emails = false;
|
||||
if ($request->search_by == 'all' || $request->search_by == 'email' || $request->exclude_email) {
|
||||
$join_emails = true;
|
||||
}
|
||||
|
||||
$select_list = ['customers.id', 'first_name', 'last_name'];
|
||||
if ($join_emails) {
|
||||
$select_list[] = 'emails.email';
|
||||
}
|
||||
if ($request->show_fields == 'phone') {
|
||||
$select_list[] = 'phones';
|
||||
}
|
||||
$customers_query = Customer::select($select_list);
|
||||
|
||||
if ($join_emails) {
|
||||
if ($request->allow_non_emails) {
|
||||
$customers_query->leftJoin('emails', 'customers.id', '=', 'emails.customer_id');
|
||||
} else {
|
||||
$customers_query->join('emails', 'customers.id', '=', 'emails.customer_id');
|
||||
}
|
||||
}
|
||||
|
||||
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 = $customers_query->paginate(20);
|
||||
|
||||
foreach ($customers as $customer) {
|
||||
$text = $customer->email;
|
||||
if ($customer->getFullName()) {
|
||||
$text .= ' ('.$customer->getFullName().')';
|
||||
}
|
||||
if (!empty($request->use_id)) {
|
||||
$id = $customer->id;
|
||||
$id = '';
|
||||
$text = '';
|
||||
|
||||
if ($request->show_fields != 'all') {
|
||||
switch ($request->show_fields) {
|
||||
case 'email':
|
||||
$text = $customer->email;
|
||||
break;
|
||||
case 'name':
|
||||
$text = $customer->getFullName();
|
||||
break;
|
||||
case 'phone':
|
||||
// Get phone which matches
|
||||
$phones = $customer->getPhones();
|
||||
foreach ($phones as $phone) {
|
||||
if (strstr($phone['value'], $q)) {
|
||||
$text = $phone['value'];
|
||||
if ($customer->getFullName()) {
|
||||
$text .= ' — '.$customer->getFullName();
|
||||
}
|
||||
$id = $phone['value'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$text = $customer->getNameAndEmail();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$id = $customer->email;
|
||||
$text = $customer->getNameAndEmail();
|
||||
}
|
||||
|
||||
if (!$id) {
|
||||
if (!empty($request->use_id)) {
|
||||
$id = $customer->id;
|
||||
} else {
|
||||
$id = $customer->email;
|
||||
}
|
||||
}
|
||||
$response['results'][] = [
|
||||
'id' => $id,
|
||||
'text' => $text
|
||||
'id' => $id,
|
||||
'text' => $text,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -256,4 +341,69 @@ class CustomersController extends Controller
|
||||
|
||||
return \Response::json($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax controller.
|
||||
*/
|
||||
public function ajax(Request $request)
|
||||
{
|
||||
$response = [
|
||||
'status' => 'error',
|
||||
'msg' => '', // this is error message
|
||||
];
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
switch ($request->action) {
|
||||
|
||||
// Change conversation user
|
||||
case 'create':
|
||||
// First name or email must be specified
|
||||
$validator = Validator::make($request->all(), [
|
||||
'first_name' => 'required|string|max:255',
|
||||
'last_name' => 'nullable|string|max:255',
|
||||
'email' => 'required|email|unique:emails,email',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
foreach ($validator->errors()->getMessages()as $errors) {
|
||||
foreach ($errors as $field => $message) {
|
||||
$response['msg'] .= $message.' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
|
||||
$customer = Customer::create($request->email, $request->all());
|
||||
if ($customer) {
|
||||
$response['email'] = $request->email;
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// Conversations navigation
|
||||
case 'customers_pagination':
|
||||
|
||||
$customers = app('App\Http\Controllers\ConversationsController')->searchCustomers($request, $user);
|
||||
|
||||
$response['status'] = 'success';
|
||||
|
||||
$response['html'] = view('customers/partials/customers_table', [
|
||||
'customers' => $customers,
|
||||
])->render();
|
||||
break;
|
||||
|
||||
default:
|
||||
$response['msg'] = 'Unknown action';
|
||||
break;
|
||||
}
|
||||
|
||||
if ($response['status'] == 'error' && empty($response['msg'])) {
|
||||
$response['msg'] = 'Unknown error occured';
|
||||
}
|
||||
|
||||
return \Response::json($response);
|
||||
}
|
||||
}
|
||||
|
||||
540
app/Http/Controllers/MailboxesController.php
Normal file → Executable file
540
app/Http/Controllers/MailboxesController.php
Normal file → Executable file
@@ -29,9 +29,17 @@ class MailboxesController extends Controller
|
||||
*/
|
||||
public function mailboxes()
|
||||
{
|
||||
//$this->authorize('create', 'App\Mailbox');
|
||||
//$mailboxes = Mailbox::all();
|
||||
$mailboxes = auth()->user()->mailboxesCanView();
|
||||
$user = auth()->user();
|
||||
|
||||
$mailboxes = $user->mailboxesCanView();
|
||||
|
||||
if (!\Eventy::filter('user.can_view_mailbox_menu', false, $user)) {
|
||||
foreach ($mailboxes as $i => $mailbox) {
|
||||
if (!$user->canManageMailbox($mailbox->id)) {
|
||||
$mailboxes->forget($i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return view('mailboxes/mailboxes', ['mailboxes' => $mailboxes]);
|
||||
}
|
||||
@@ -43,7 +51,7 @@ class MailboxesController extends Controller
|
||||
{
|
||||
$this->authorize('create', 'App\Mailbox');
|
||||
|
||||
$users = User::where('role', '!=', User::ROLE_ADMIN)->get();
|
||||
$users = User::nonDeleted()->where('role', '!=', User::ROLE_ADMIN)->get();
|
||||
|
||||
return view('mailboxes/create', ['users' => $users]);
|
||||
}
|
||||
@@ -55,6 +63,8 @@ class MailboxesController extends Controller
|
||||
*/
|
||||
public function createSave(Request $request)
|
||||
{
|
||||
$invalid = false;
|
||||
|
||||
$this->authorize('create', 'App\Mailbox');
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
@@ -64,7 +74,12 @@ class MailboxesController extends Controller
|
||||
|
||||
// //event(new Registered($user = $this->create($request->all())));
|
||||
|
||||
if ($validator->fails()) {
|
||||
if (Mailbox::userEmailExists($request->email)) {
|
||||
$invalid = true;
|
||||
$validator->errors()->add('email', __('There is a user with such email. Users and mailboxes can not have the same email addresses.'));
|
||||
}
|
||||
|
||||
if ($invalid || $validator->fails()) {
|
||||
return redirect()->route('mailboxes.create')
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
@@ -88,9 +103,24 @@ class MailboxesController extends Controller
|
||||
public function update($id)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
//$this->authorize('update', $mailbox);
|
||||
if (!auth()->user()->can('update', $mailbox)) {
|
||||
$accessible_route = \Eventy::filter('mailbox.accessible_settings_route', '', auth()->user(), $mailbox);
|
||||
$user = auth()->user();
|
||||
if (!$user->can('updateSettings', $mailbox) && !$user->can('updateEmailSignature', $mailbox)) {
|
||||
$accessible_route = '';
|
||||
|
||||
$mailbox_settings = $user->mailboxSettings($mailbox->id);
|
||||
$access_permissions = json_decode($mailbox_settings->access ?? '');
|
||||
|
||||
if ($access_permissions && is_array($access_permissions)) {
|
||||
foreach ($access_permissions as $perm) {
|
||||
$accessible_route = Mailbox::getAccessPermissionRoute($perm);
|
||||
if ($accessible_route) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$accessible_route) {
|
||||
$accessible_route = \Eventy::filter('mailbox.accessible_settings_route', '', auth()->user(), $mailbox);
|
||||
}
|
||||
if ($accessible_route) {
|
||||
return redirect()->route($accessible_route, ['id' => $mailbox->id]);
|
||||
} else {
|
||||
@@ -98,9 +128,17 @@ class MailboxesController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$user = auth()->user();
|
||||
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||
if (!$mailbox_user && $user->isAdmin()) {
|
||||
// Admin may not be connected to the mailbox yet
|
||||
$user->mailboxes()->attach($id);
|
||||
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||
}
|
||||
|
||||
//$mailboxes = Mailbox::all()->except($id);
|
||||
|
||||
return view('mailboxes/update', ['mailbox' => $mailbox, 'flashes' => $this->mailboxActiveWarning($mailbox)]);
|
||||
return view('mailboxes/update', ['mailbox' => $mailbox, 'mailbox_user' => $mailbox_user, 'flashes' => $this->mailboxActiveWarning($mailbox)]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,30 +149,63 @@ class MailboxesController extends Controller
|
||||
*/
|
||||
public function updateSave($id, Request $request)
|
||||
{
|
||||
$invalid = false;
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
|
||||
$this->authorize('update', $mailbox);
|
||||
|
||||
$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',
|
||||
'from_name' => 'required|integer',
|
||||
'from_name_custom' => 'nullable|string|max:128',
|
||||
'ticket_status' => 'required|integer',
|
||||
'template' => 'required|integer',
|
||||
'ticket_assignee' => 'required|integer',
|
||||
'signature' => 'nullable|string',
|
||||
]);
|
||||
|
||||
//event(new Registered($user = $this->create($request->all())));
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('mailboxes.update', ['id' => $id])
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user->can('updateSettings', $mailbox) && !$user->can('updateEmailSignature', $mailbox)) {
|
||||
\Helper::denyAccess();
|
||||
}
|
||||
|
||||
if ($user->can('updateSettings', $mailbox)) {
|
||||
|
||||
// if not admin, the text only fields don't pass so spike them into the request.
|
||||
if (!auth()->user()->isAdmin()) {
|
||||
$request->merge([
|
||||
'name' => $mailbox->name,
|
||||
'email' => $mailbox->email
|
||||
]);
|
||||
}
|
||||
|
||||
$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',
|
||||
'from_name' => 'required|integer',
|
||||
'from_name_custom' => 'nullable|string|max:128',
|
||||
'ticket_status' => 'required|integer',
|
||||
'template' => 'required|integer',
|
||||
'ticket_assignee' => 'required|integer',
|
||||
]);
|
||||
|
||||
//event(new Registered($user = $this->create($request->all())));
|
||||
if (Mailbox::userEmailExists($request->email)) {
|
||||
$invalid = true;
|
||||
$validator->errors()->add('email', __('There is a user with such email. Users and mailboxes can not have the same email addresses.'));
|
||||
}
|
||||
|
||||
if ($invalid || $validator->fails()) {
|
||||
return redirect()->route('mailboxes.update', ['id' => $id])
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
if ($user->can('updateEmailSignature', $mailbox)) {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'signature' => 'nullable|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('mailboxes.email_signature', ['id' => $id])
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
\Eventy::action( 'mailboxes.settings_before_save', $id, $request );
|
||||
|
||||
$mailbox->fill($request->all());
|
||||
|
||||
$mailbox->save();
|
||||
@@ -151,11 +222,25 @@ class MailboxesController extends Controller
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('updatePermissions', $mailbox);
|
||||
|
||||
$users = User::where('role', '!=', User::ROLE_ADMIN)->get();
|
||||
$users = User::nonDeleted()->where('role', '!=', User::ROLE_ADMIN)->get();
|
||||
$users = User::sortUsers($users);
|
||||
|
||||
return view('mailboxes/permissions', ['mailbox' => $mailbox, 'users' => $users, 'mailbox_users' => $mailbox->users]);
|
||||
$managers = User::nonDeleted()
|
||||
->select(['users.*', 'mailbox_user.hide', 'mailbox_user.access'])
|
||||
->leftJoin('mailbox_user', function ($join) use ($mailbox) {
|
||||
$join->on('mailbox_user.user_id', '=', 'users.id');
|
||||
$join->where('mailbox_user.mailbox_id', $mailbox->id);
|
||||
})->get();
|
||||
$managers = User::sortUsers($managers);
|
||||
|
||||
return view('mailboxes/permissions', [
|
||||
'mailbox' => $mailbox,
|
||||
'users' => $users,
|
||||
'managers' => $managers,
|
||||
'mailbox_users' => $mailbox->users,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,11 +252,47 @@ class MailboxesController extends Controller
|
||||
public function permissionsSave($id, Request $request)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('updatePermissions', $mailbox);
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
$mailbox->users()->sync($request->users);
|
||||
$mailbox->syncPersonalFolders($request->users);
|
||||
|
||||
// Save admins settings.
|
||||
$admins = User::nonDeleted()->where('role', User::ROLE_ADMIN)->get();
|
||||
foreach ($admins as $admin) {
|
||||
$mailbox_user = $admin->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||
if (!$mailbox_user) {
|
||||
// Admin may not be connected to the mailbox yet
|
||||
$admin->mailboxes()->attach($id);
|
||||
$mailbox_user = $admin->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||
}
|
||||
$mailbox_user->settings->hide = (isset($request->managers[$admin->id]['hide']) ? (int)$request->managers[$admin->id]['hide'] : false);
|
||||
$mailbox_user->settings->save();
|
||||
}
|
||||
|
||||
// Sets the mailbox_user.access array
|
||||
$mailbox_users = $mailbox->users;
|
||||
foreach ($mailbox_users as $mailbox_user) {
|
||||
$access = [];
|
||||
$mailbox_with_settings = $mailbox_user->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||
|
||||
foreach (\App\Mailbox::$access_permissions as $perm) {
|
||||
if (!empty($request->managers[$mailbox_user->id]['access'][$perm])) {
|
||||
$access[] = $request->managers[$mailbox_user->id]['access'][$perm];
|
||||
}
|
||||
}
|
||||
|
||||
if ($user->id == $mailbox_user->id && !$user->isAdmin()) {
|
||||
// User with Permission priv's can't edit their own additional priv's.
|
||||
} else {
|
||||
$mailbox_with_settings->settings->access = json_encode($access);
|
||||
}
|
||||
$mailbox_with_settings->settings->hide = (isset($request->managers[$mailbox_user->id]['hide']) ? (int)$request->managers[$mailbox_user->id]['hide'] : false);
|
||||
$mailbox_with_settings->settings->save();
|
||||
}
|
||||
|
||||
\Session::flash('flash_success_floating', __('Mailbox permissions saved!'));
|
||||
|
||||
return redirect()->route('mailboxes.permissions', ['id' => $id]);
|
||||
@@ -183,7 +304,7 @@ class MailboxesController extends Controller
|
||||
public function connectionOutgoing($id)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
return view('mailboxes/connection', ['mailbox' => $mailbox, 'sendmail_path' => ini_get('sendmail_path'), 'flashes' => $this->mailboxActiveWarning($mailbox)]);
|
||||
}
|
||||
@@ -194,14 +315,14 @@ class MailboxesController extends Controller
|
||||
public function connectionOutgoingSave($id, Request $request)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
if ($request->out_method == Mailbox::OUT_METHOD_SMTP) {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'out_server' => 'required|string|max:255',
|
||||
'out_port' => 'required|integer',
|
||||
'out_username' => 'required|string|max:100',
|
||||
'out_password' => 'required|string|max:255',
|
||||
'out_username' => 'nullable|string|max:100',
|
||||
'out_password' => 'nullable|string|max:255',
|
||||
'out_encryption' => 'required|integer',
|
||||
]);
|
||||
|
||||
@@ -212,9 +333,22 @@ class MailboxesController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$mailbox->fill($request->all());
|
||||
// Do not save dummy password.
|
||||
if (preg_match("/^\*+$/", $request->out_password)) {
|
||||
$params = $request->except(['out_password']);
|
||||
} else {
|
||||
$params = $request->all();
|
||||
}
|
||||
$mailbox->fill($params);
|
||||
$mailbox->save();
|
||||
|
||||
if (!empty($request->send_test_to)) {
|
||||
\Option::set('send_test_to', $request->send_test_to);
|
||||
}
|
||||
|
||||
// Sometimes background job continues to use old connection settings.
|
||||
\Helper::queueWorkRestart();
|
||||
|
||||
\Session::flash('flash_success_floating', __('Connection settings saved!'));
|
||||
|
||||
return redirect()->route('mailboxes.connection', ['id' => $id]);
|
||||
@@ -223,12 +357,26 @@ class MailboxesController extends Controller
|
||||
/**
|
||||
* Mailbox incoming settings.
|
||||
*/
|
||||
public function connectionIncoming($id)
|
||||
public function connectionIncoming($id, Request $request)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
return view('mailboxes/connection_incoming', ['mailbox' => $mailbox, 'flashes' => $this->mailboxActiveWarning($mailbox)]);
|
||||
$fields = [
|
||||
'in_server' => $mailbox->in_server ?? '',
|
||||
'in_port' => $mailbox->in_port ?? '',
|
||||
'in_username' => $mailbox->in_username ?? '',
|
||||
'in_password' => $mailbox->in_password ?? '',
|
||||
];
|
||||
|
||||
$validator = Validator::make($fields, [
|
||||
'in_server' => 'required',
|
||||
'in_port' => 'required',
|
||||
'in_username' => 'required',
|
||||
'in_password' => 'required',
|
||||
]);
|
||||
|
||||
return view('mailboxes/connection_incoming', ['mailbox' => $mailbox, 'flashes' => $this->mailboxActiveWarning($mailbox)])->withErrors($validator);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -237,22 +385,45 @@ class MailboxesController extends Controller
|
||||
public function connectionIncomingSave($id, Request $request)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'in_server' => 'required|string|max:255',
|
||||
'in_port' => 'required|integer',
|
||||
'in_username' => 'required|string|max:100',
|
||||
'in_password' => 'required|string|max:255',
|
||||
// $validator = Validator::make($request->all(), [
|
||||
// 'in_server' => 'nullable|string|max:255',
|
||||
// 'in_port' => 'nullable|integer',
|
||||
// 'in_username' => 'nullable|string|max:100',
|
||||
// 'in_password' => 'nullable|string|max:255',
|
||||
// ]);
|
||||
|
||||
// if ($validator->fails()) {
|
||||
// return redirect()->route('mailboxes.connection.incoming', ['id' => $id])
|
||||
// ->withErrors($validator)
|
||||
// ->withInput();
|
||||
// }
|
||||
|
||||
// Checkboxes
|
||||
$request->merge([
|
||||
'in_validate_cert' => ($request->filled('in_validate_cert') ?? false),
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('mailboxes.connection.incoming', ['id' => $id])
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
// Do not save dummy password.
|
||||
if (preg_match("/^\*+$/", $request->in_password ?? '')) {
|
||||
$params = $request->except(['in_password']);
|
||||
} else {
|
||||
$params = $request->all();
|
||||
}
|
||||
|
||||
$mailbox->fill($request->all());
|
||||
$mailbox->fill($params);
|
||||
|
||||
// Save IMAP Folders.
|
||||
// Save all custom folders except INBOX.
|
||||
$in_imap_folders = [];
|
||||
if (is_array($request->in_imap_folders)) {
|
||||
foreach ($request->in_imap_folders as $imap_folder) {
|
||||
$in_imap_folders[] = $imap_folder;
|
||||
}
|
||||
}
|
||||
$mailbox->setInImapFolders($in_imap_folders);
|
||||
|
||||
$mailbox->save();
|
||||
|
||||
\Session::flash('flash_success_floating', __('Connection settings saved!'));
|
||||
@@ -265,10 +436,14 @@ class MailboxesController extends Controller
|
||||
*/
|
||||
public function view($id, $folder_id = null)
|
||||
{
|
||||
//auth()->guard()->attempt(['email' => '', 'password' => '']);
|
||||
$user = auth()->user();
|
||||
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('view', $mailbox);
|
||||
if ($user->isAdmin()) {
|
||||
$mailbox = Mailbox::findOrFailWithSettings($id, $user->id);
|
||||
} else {
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
}
|
||||
$this->authorize('viewCached', $mailbox);
|
||||
|
||||
$folders = $mailbox->getAssesibleFolders();
|
||||
|
||||
@@ -287,8 +462,6 @@ class MailboxesController extends Controller
|
||||
|
||||
$this->authorize('view', $folder);
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
$query_conversations = Conversation::getQueryByFolder($folder, $user->id);
|
||||
$conversations = $folder->queryAddOrderBy($query_conversations)->paginate(Conversation::DEFAULT_LIST_SIZE);
|
||||
|
||||
@@ -304,7 +477,7 @@ class MailboxesController extends Controller
|
||||
{
|
||||
$flashes = [];
|
||||
|
||||
if ($mailbox) {
|
||||
if ($mailbox && \Auth::user()->can('admin', $mailbox)) {
|
||||
if (Route::currentRouteName() != 'mailboxes.connection' && !$mailbox->isOutActive()) {
|
||||
$flashes[] = [
|
||||
'type' => 'warning',
|
||||
@@ -330,14 +503,14 @@ class MailboxesController extends Controller
|
||||
public function autoReply($id)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('updateAutoReply', $mailbox);
|
||||
|
||||
if (!$mailbox->auto_reply_subject) {
|
||||
$mailbox->auto_reply_subject = 'Re: {%subject%}';
|
||||
}
|
||||
|
||||
return view('mailboxes/auto_reply', [
|
||||
'mailbox' => $mailbox
|
||||
'mailbox' => $mailbox,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -348,10 +521,11 @@ class MailboxesController extends Controller
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
|
||||
$this->authorize('update', $mailbox);
|
||||
|
||||
// $this->authorize('update', $mailbox);
|
||||
$this->authorize('updateAutoReply', $mailbox);
|
||||
|
||||
$request->merge([
|
||||
'auto_reply_enabled' => ($request->filled('auto_reply_enabled') ?? false)
|
||||
'auto_reply_enabled' => ($request->filled('auto_reply_enabled') ?? false),
|
||||
]);
|
||||
|
||||
if ($request->auto_reply_enabled) {
|
||||
@@ -363,8 +537,8 @@ class MailboxesController extends Controller
|
||||
]);
|
||||
$validator->setAttributeNames([
|
||||
'auto_reply_subject' => __('Subject'),
|
||||
'auto_reply_message' => __('Message')
|
||||
]);
|
||||
'auto_reply_message' => __('Message'),
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('mailboxes.auto_reply', ['id' => $id])
|
||||
@@ -382,6 +556,24 @@ class MailboxesController extends Controller
|
||||
return redirect()->route('mailboxes.auto_reply', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto reply settings.
|
||||
*/
|
||||
public function emailSignature($id)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('updateAutoReply', $mailbox);
|
||||
|
||||
if (!$mailbox->auto_reply_subject) {
|
||||
$mailbox->auto_reply_subject = 'Re: {%subject%}';
|
||||
}
|
||||
|
||||
return view('mailboxes/email_signature', [
|
||||
'mailbox' => $mailbox,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Users ajax controller.
|
||||
*/
|
||||
@@ -402,12 +594,20 @@ class MailboxesController extends Controller
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
} elseif (!$user->can('update', $mailbox)) {
|
||||
} elseif (!$user->can('admin', $mailbox)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
} elseif (empty($request->to)) {
|
||||
$response['msg'] = __('Please specify recipient of the test email');
|
||||
}
|
||||
|
||||
// Check if outgoing port is open.
|
||||
if (!$response['msg'] && $mailbox->out_method == Mailbox::OUT_METHOD_SMTP) {
|
||||
$test_result = \Helper::checkPort($mailbox->out_server, $mailbox->out_port);
|
||||
if (!$test_result) {
|
||||
$response['msg'] = __(':host is not available on :port port. Make sure that :host address is correct and that outgoing port :port on YOUR server is open.', ['host' => '<strong>'.$mailbox->out_server.'</strong>', 'port' => '<strong>'.$mailbox->out_port.'</strong>']);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
$test_result = false;
|
||||
|
||||
@@ -418,7 +618,7 @@ class MailboxesController extends Controller
|
||||
}
|
||||
|
||||
if (!$test_result && !$response['msg']) {
|
||||
$response['msg'] = __('Error occurend sending email. Please check your mail server logs for more details.');
|
||||
$response['msg'] = __('Error occurred sending email. Please check your mail server logs for more details.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,22 +637,86 @@ class MailboxesController extends Controller
|
||||
$mailbox = Mailbox::find($request->mailbox_id);
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Conversation not found');
|
||||
} elseif (!$user->can('update', $mailbox)) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
} elseif (!$user->can('admin', $mailbox)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
$response = \Eventy::filter('mailbox.fetch_test', $response, $mailbox);
|
||||
|
||||
$tested = (isset($response['tested']) && $response['tested'] === true);
|
||||
|
||||
// Check if outgoing port is open.
|
||||
if (!$response['msg'] && !$tested) {
|
||||
$test_result = \Helper::checkPort($mailbox->in_server, $mailbox->in_port);
|
||||
if (!$test_result) {
|
||||
$response['msg'] = __(':host is not available on :port port. Make sure that :host address is correct and that outgoing port :port on YOUR server is open.', ['host' => '<strong>'.$mailbox->in_server.'</strong>', 'port' => '<strong>'.$mailbox->in_port.'</strong>']);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$response['msg'] && !$tested) {
|
||||
$test_result = false;
|
||||
|
||||
try {
|
||||
$test_result = \App\Misc\Mail::fetchTest($mailbox);
|
||||
$test_result = \MailHelper::fetchTest($mailbox);
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = $e->getMessage();
|
||||
}
|
||||
|
||||
if (!$test_result && !$response['msg']) {
|
||||
$response['msg'] = __('Error occurend connecting to the server');
|
||||
$response['msg'] = __('Error occurred connecting to the server');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$response['msg'] && !$tested) {
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
break;
|
||||
|
||||
// Retrieve a list of available IMAP folders from server.
|
||||
case 'imap_folders':
|
||||
$mailbox = Mailbox::find($request->mailbox_id);
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
} elseif (!$user->can('admin', $mailbox)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
$response['folders'] = [];
|
||||
|
||||
if (!$response['msg']) {
|
||||
|
||||
try {
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
$client->connect();
|
||||
|
||||
$imap_folders = $client->getFolders();
|
||||
|
||||
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) {
|
||||
if (!empty($child_imap_folder->fullName)) {
|
||||
$response['folders'][] = $child_imap_folder->fullName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($response['folders'])) {
|
||||
$response['msg_success'] = __('IMAP folders retrieved: '.implode(', ', $response['folders']));
|
||||
} else {
|
||||
$response['msg_success'] = __('Connected, but no IMAP folders found');
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,7 +731,7 @@ class MailboxesController extends Controller
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
} elseif (!$user->can('update', $mailbox)) {
|
||||
} elseif (!$user->can('admin', $mailbox)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
} elseif (!Hash::check($request->password, $user->password)) {
|
||||
$response['msg'] = __('Please double check your password, and try again');
|
||||
@@ -482,7 +746,7 @@ class MailboxesController extends Controller
|
||||
$mailbox->users()->sync([]);
|
||||
$mailbox->folders()->delete();
|
||||
// Maybe remove notifications on events in this mailbox?
|
||||
|
||||
|
||||
$mailbox->delete();
|
||||
|
||||
\Session::flash('flash_success_floating', __('Mailbox deleted'));
|
||||
@@ -491,6 +755,31 @@ class MailboxesController extends Controller
|
||||
}
|
||||
break;
|
||||
|
||||
// Mute notifications
|
||||
case 'mute':
|
||||
$mailbox = Mailbox::find($request->mailbox_id);
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
} elseif (!$user->isAdmin()) {
|
||||
$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
|
||||
$user->mailboxes()->attach($id);
|
||||
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $mailbox->id)->first();
|
||||
}
|
||||
$mailbox_user->settings->mute = (bool)$request->mute;
|
||||
$mailbox_user->settings->save();
|
||||
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$response['msg'] = 'Unknown action';
|
||||
break;
|
||||
@@ -502,4 +791,107 @@ class MailboxesController extends Controller
|
||||
|
||||
return \Response::json($response);
|
||||
}
|
||||
|
||||
public function oauth(Request $request)
|
||||
{
|
||||
$mailbox_id = $request->id ?? '';
|
||||
$provider = $request->provider ?? '';
|
||||
|
||||
$state_data = [];
|
||||
if (!empty($request->state)) {
|
||||
$state_data = json_decode($request->state, true);
|
||||
if (!empty($state_data['mailbox_id'])) {
|
||||
$mailbox_id = $state_data['mailbox_id'];
|
||||
}
|
||||
if (!empty($state_data['provider'])) {
|
||||
$provider = $state_data['provider'];
|
||||
}
|
||||
}
|
||||
|
||||
// MS Exchange.
|
||||
if (!empty($request->error) && $request->error == 'invalid_request' && !empty($request->error_description)) {
|
||||
return htmlspecialchars($request->error_description);
|
||||
}
|
||||
|
||||
if (empty($provider)) {
|
||||
return 'Invalid oAuth Provider';
|
||||
}
|
||||
|
||||
$mailbox = Mailbox::findOrFail($mailbox_id);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
if (empty($mailbox)) {
|
||||
return __('Mailbox not found').': '.$mailbox_id;
|
||||
}
|
||||
if (empty($mailbox->in_username)) {
|
||||
return 'Enter oAuth Client ID as Username and save mailbox settings';
|
||||
}
|
||||
if (empty($mailbox->in_password)) {
|
||||
return 'Enter oAuth Client Secret as Password and save mailbox settings';
|
||||
}
|
||||
|
||||
$session_data = [];
|
||||
if (\Session::get('mailbox_oauth_'.$provider.'_'.$mailbox_id)) {
|
||||
$session_data = \Session::get('mailbox_oauth_'.$provider.'_'.$mailbox_id);
|
||||
}
|
||||
|
||||
if (empty($request->code)) {
|
||||
$state = [
|
||||
'provider' => $provider,
|
||||
'mailbox_id' => $mailbox_id,
|
||||
'state' => crc32($mailbox->in_username.$mailbox->in_password),
|
||||
];
|
||||
$url = \MailHelper::oauthGetAuthorizationUrl(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
'state' => json_encode($state),
|
||||
'client_id' => $mailbox->in_username,
|
||||
]);
|
||||
if ($url) {
|
||||
\Session::put('mailbox_oauth_'.$provider.'_'.$mailbox_id, $state);
|
||||
// [
|
||||
// 'provider' => $request->provider,
|
||||
// 'mailbox_id' => $request->mailbox_id,
|
||||
// 'state' => $provider->getState(),
|
||||
// ]);
|
||||
return redirect()->away($url);
|
||||
} else {
|
||||
return 'Could not generate authorization URL: check Client ID (Username) and Client Secret (Password)';
|
||||
}
|
||||
|
||||
// Check given state against previously stored one to mitigate CSRF attack
|
||||
} elseif (empty($request->state) || ($state_data['state'] ?? '') !== ($session_data['state'] ?? '')) {
|
||||
|
||||
\Session::forget('mailbox_oauth_'.$provider.'_'.$mailbox_id);
|
||||
return 'Invalid oAuth state';
|
||||
|
||||
} else {
|
||||
|
||||
// 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,
|
||||
'code' => $request->code,
|
||||
]);
|
||||
|
||||
if (!empty($token_data['a_token'])) {
|
||||
$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]);
|
||||
}
|
||||
}
|
||||
|
||||
public function oauthDisconnect(Request $request)
|
||||
{
|
||||
$mailbox_id = $request->id ?? '';
|
||||
$provider = $request->provider ?? '';
|
||||
|
||||
$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]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,10 @@ class ModulesController extends Controller
|
||||
{
|
||||
$installed_modules = [];
|
||||
$modules_directory = [];
|
||||
|
||||
$all_modules = [];
|
||||
$flashes = [];
|
||||
$updates_available = false;
|
||||
|
||||
$flash = \Cache::get('modules_flash');
|
||||
if ($flash) {
|
||||
$flashes[] = $flash;
|
||||
@@ -52,21 +54,25 @@ class ModulesController extends Controller
|
||||
\Module::clearCache();
|
||||
$modules = \Module::all();
|
||||
foreach ($modules as $module) {
|
||||
$img = '';
|
||||
$installed_modules[] = [
|
||||
'alias' => $module->getAlias(),
|
||||
'name' => $module->getName(),
|
||||
'description' => $module->getDescription(),
|
||||
'version' => $module->get('version'),
|
||||
'detailsUrl' => $module->get('detailsUrl'),
|
||||
'author' => $module->get('author'),
|
||||
'authorUrl' => $module->get('authorUrl'),
|
||||
'requiredAppVersion' => $module->get('requiredAppVersion'),
|
||||
'img' => $img,
|
||||
'active' => $module->active(), //\App\Module::isActive($module->getAlias()),
|
||||
'installed' => true,
|
||||
'activated' => \App\Module::isLicenseActivated($module->getAlias(), $module->get('authorUrl')),
|
||||
'license' => \App\Module::getLicense($module->getAlias()),
|
||||
'alias' => $module->getAlias(),
|
||||
'name' => $module->getName(),
|
||||
'description' => $module->getDescription(),
|
||||
'version' => $module->get('version'),
|
||||
'detailsUrl' => $module->get('detailsUrl'),
|
||||
'author' => $module->get('author'),
|
||||
'authorUrl' => $module->get('authorUrl'),
|
||||
'requiredAppVersion' => $module->get('requiredAppVersion'),
|
||||
'requiredPhpExtensions' => $module->get('requiredPhpExtensions'),
|
||||
'requiredPhpExtensionsMissing' => \App\Module::getMissingExtensions($module->get('requiredPhpExtensions')),
|
||||
'requiredModulesMissing' => \App\Module::getMissingModules($module->get('requiredModules'), $modules),
|
||||
'img' => $module->get('img'),
|
||||
'active' => $module->active(), //\App\Module::isActive($module->getAlias()),
|
||||
'installed' => true,
|
||||
'activated' => \App\Module::isLicenseActivated($module->getAlias(), $module->get('authorUrl')),
|
||||
'license' => \App\Module::getLicense($module->getAlias()),
|
||||
// Determined later
|
||||
'new_version' => '',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -79,12 +85,24 @@ class ModulesController extends Controller
|
||||
// Prepare directory modules
|
||||
if (is_array($modules_directory)) {
|
||||
foreach ($modules_directory as $i_dir => $dir_module) {
|
||||
// Remove installed modules from modules directory
|
||||
// Remove modules without aliases
|
||||
if (empty($dir_module['alias'])) {
|
||||
unset($modules_directory[$i_dir]);
|
||||
}
|
||||
$all_modules[$dir_module['alias']] = $dir_module['name'];
|
||||
foreach ($installed_modules as $i_installed => $module) {
|
||||
if ($dir_module['alias'] == $module['alias']) {
|
||||
// Set image from director
|
||||
$installed_modules[$i_installed]['img'] = $dir_module['img'];
|
||||
// Remove installed modules from modules directory.
|
||||
unset($modules_directory[$i_dir]);
|
||||
|
||||
// Detect if new version is available
|
||||
if (!empty($dir_module['version']) && version_compare($dir_module['version'], $module['version'], '>')) {
|
||||
$installed_modules[$i_installed]['new_version'] = $dir_module['version'];
|
||||
$updates_available = true;
|
||||
}
|
||||
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
@@ -94,6 +112,9 @@ class ModulesController extends Controller
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($dir_module['requiredPhpExtensions'])) {
|
||||
$modules_directory[$i_dir]['requiredPhpExtensionsMissing'] = \App\Module::getMissingExtensions($dir_module['requiredPhpExtensions']);
|
||||
}
|
||||
$modules_directory[$i_dir]['active'] = \App\Module::isActive($dir_module['alias']);
|
||||
$modules_directory[$i_dir]['activated'] = false;
|
||||
}
|
||||
@@ -104,7 +125,9 @@ class ModulesController extends Controller
|
||||
return view('modules/modules', [
|
||||
'installed_modules' => $installed_modules,
|
||||
'modules_directory' => $modules_directory,
|
||||
'flashes' => $flashes
|
||||
'flashes' => $flashes,
|
||||
'updates_available' => $updates_available,
|
||||
'all_modules' => $all_modules,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -123,7 +146,7 @@ class ModulesController extends Controller
|
||||
case 'install':
|
||||
case 'activate_license':
|
||||
$license = $request->license;
|
||||
$alias = $request->alias;
|
||||
$alias = $request->alias;
|
||||
|
||||
if (!$license) {
|
||||
$response['msg'] = __('Empty license key');
|
||||
@@ -133,31 +156,31 @@ class ModulesController extends Controller
|
||||
$params = [
|
||||
'license' => $license,
|
||||
'module_alias' => $alias,
|
||||
'url' => $request->getHttpHost(),
|
||||
'url' => \App\Module::getAppUrl(),
|
||||
];
|
||||
$result = WpApi::activateLicense($params);
|
||||
|
||||
if (WpApi::$lastError) {
|
||||
$response['msg'] = WpApi::$lastError['message'];
|
||||
} else if (!empty($result['code']) && !empty($result['message'])) {
|
||||
} elseif (!empty($result['code']) && !empty($result['message'])) {
|
||||
$response['msg'] = $result['message'];
|
||||
} else {
|
||||
if (!empty($result['status']) && $result['status'] == 'valid') {
|
||||
|
||||
if ($request->action == 'install') {
|
||||
// Download and install module
|
||||
$license_details = WpApi::getVersion($params);
|
||||
|
||||
|
||||
if (WpApi::$lastError) {
|
||||
$response['msg'] = WpApi::$lastError['message'];
|
||||
} else if (!empty($license_details['code']) && !empty($license_details['message'])) {
|
||||
} elseif (!empty($license_details['code']) && !empty($license_details['message'])) {
|
||||
$response['msg'] = $license_details['message'];
|
||||
} 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) {
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = $e->getMessage();
|
||||
}
|
||||
|
||||
@@ -168,7 +191,7 @@ class ModulesController extends Controller
|
||||
// Extract
|
||||
try {
|
||||
\Helper::unzip($module_archive, \Module::getPath());
|
||||
} catch(\Exception $e) {
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = $e->getMessage();
|
||||
}
|
||||
// Check if extracted module exists
|
||||
@@ -190,9 +213,7 @@ class ModulesController extends Controller
|
||||
|
||||
\Session::flash('flash_success_floating', __('Module successfully installed!'));
|
||||
$response['status'] = 'success';
|
||||
|
||||
} elseif ($download_error) {
|
||||
|
||||
$response['reload'] = true;
|
||||
|
||||
if ($response['msg']) {
|
||||
@@ -202,7 +223,7 @@ class ModulesController extends Controller
|
||||
\Session::flash('flash_error_unescaped', __('Error occured 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>']));
|
||||
}
|
||||
} else {
|
||||
$response['msg'] = __('Error occured, please try again later.');
|
||||
$response['msg'] = __('Error occured. Please try again later.');
|
||||
}
|
||||
} else {
|
||||
// Just activate license
|
||||
@@ -211,36 +232,10 @@ class ModulesController extends Controller
|
||||
\Session::flash('flash_success_floating', __('License successfully activated!'));
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
|
||||
} elseif (!empty($result['error'])) {
|
||||
switch ($result['error']) {
|
||||
case 'missing':
|
||||
$response['msg'] = __('License key does not exist');
|
||||
break;
|
||||
case 'license_not_activable':
|
||||
$response['msg'] = __("You have to activate each bundle's module separately");
|
||||
break;
|
||||
case 'disabled':
|
||||
$response['msg'] = __("License key has been revoked");
|
||||
break;
|
||||
case 'no_activations_left':
|
||||
$response['msg'] = __("No activations left for this license key");
|
||||
break;
|
||||
case 'expired':
|
||||
$response['msg'] = __("License key has expired");
|
||||
break;
|
||||
case 'key_mismatch':
|
||||
$response['msg'] = __("License key belongs to another module");
|
||||
break;
|
||||
case 'invalid_item_id':
|
||||
$response['msg'] = __("Module not found in the modules directory");
|
||||
break;
|
||||
default:
|
||||
$response['msg'] = __('Error code:'.' '.$result['error']);
|
||||
break;
|
||||
}
|
||||
$response['msg'] = $this->getErrorMessage($result['error'], $result);
|
||||
} else {
|
||||
$response['msg'] = __('Error occured, please try again later.');
|
||||
$response['msg'] = __('Error occured. Please try again later.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,27 +243,126 @@ class ModulesController extends Controller
|
||||
|
||||
case 'activate':
|
||||
$alias = $request->alias;
|
||||
\App\Module::setActive($alias, true);
|
||||
$module = \Module::findByAlias($alias);
|
||||
|
||||
$outputLog = new BufferedOutput;
|
||||
\Artisan::call('freescout:module-install', ['module_alias' => $alias], $outputLog);
|
||||
if (!$module) {
|
||||
$response['msg'] = __('Module not found').': '.$alias;
|
||||
}
|
||||
|
||||
// Check license
|
||||
if (!$response['msg']) {
|
||||
if (!empty($module->get('authorUrl')) && $module->isOfficial()) {
|
||||
$params = [
|
||||
'license' => $module->getLicense(),
|
||||
'module_alias' => $alias,
|
||||
'url' => \App\Module::getAppUrl(),
|
||||
];
|
||||
$license_result = WpApi::checkLicense($params);
|
||||
|
||||
if (!empty($license_result['code']) && !empty($license_result['message'])) {
|
||||
// Remove remembered license key and deactivate license in DB
|
||||
\App\Module::deactivateLicense($alias, '');
|
||||
|
||||
$response['msg'] = $license_result['message'];
|
||||
} elseif (!empty($license_result['status']) && $license_result['status'] != 'valid' && $license_result['status'] != 'inactive') {
|
||||
// Remove remembered license key and deactivate license in DB
|
||||
\App\Module::deactivateLicense($alias, '');
|
||||
|
||||
switch ($license_result['status']) {
|
||||
case 'expired':
|
||||
$response['msg'] = __('License key has expired');
|
||||
break;
|
||||
case 'disabled':
|
||||
$response['msg'] = __('License key has been revoked');
|
||||
break;
|
||||
case 'inactive':
|
||||
$response['msg'] = __('License key has not been activated yet');
|
||||
case 'site_inactive':
|
||||
$response['msg'] = __('Your domain is deactivated');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
\App\Module::setActive($alias, true);
|
||||
|
||||
$outputLog = new BufferedOutput();
|
||||
\Artisan::call('freescout:module-install', ['module_alias' => $alias], $outputLog);
|
||||
$output = $outputLog->fetch();
|
||||
|
||||
// Get module name
|
||||
$name = '?';
|
||||
if ($module) {
|
||||
$name = $module->getName();
|
||||
}
|
||||
|
||||
$type = 'danger';
|
||||
$msg = __('Error occured activating ":name" module', ['name' => $name]);
|
||||
if (session('flashes_floating') && is_array(session('flashes_floating'))) {
|
||||
// If there was any error, module has been deactivated via modules.register_error filter
|
||||
$msg = '';
|
||||
foreach (session('flashes_floating') as $flash) {
|
||||
$msg .= $flash['text'].' ';
|
||||
}
|
||||
} elseif (strstr($output, 'Configuration cached successfully')) {
|
||||
$type = 'success';
|
||||
$msg = __('":name" module successfully activated!', ['name' => $name]);
|
||||
} else {
|
||||
// Deactivate the module.
|
||||
\App\Module::setActive($alias, false);
|
||||
\Artisan::call('freescout:clear-cache');
|
||||
}
|
||||
|
||||
// Check public folder.
|
||||
if ($module && file_exists($module->getPath().DIRECTORY_SEPARATOR.'Public')) {
|
||||
$public_folder = public_path().\Module::getPublicPath($alias);
|
||||
if (!file_exists($public_folder)) {
|
||||
$type = 'danger';
|
||||
$msg = 'Error occured creating a module symlink ('.$public_folder.'). Please check folder permissions.';
|
||||
\App\Module::setActive($alias, false);
|
||||
\Artisan::call('freescout:clear-cache');
|
||||
}
|
||||
}
|
||||
|
||||
if ($type == 'success') {
|
||||
// Migrate again, in case migration did not work in the moment the module was activated.
|
||||
\Artisan::call('migrate', ['--force' => true]);
|
||||
}
|
||||
|
||||
// \Session::flash does not work after BufferedOutput
|
||||
$flash = [
|
||||
'text' => '<strong>'.$msg.'</strong><pre class="margin-top">'.$output.'</pre>',
|
||||
'unescaped' => true,
|
||||
'type' => $type,
|
||||
];
|
||||
\Cache::forever('modules_flash', $flash);
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'deactivate':
|
||||
$alias = $request->alias;
|
||||
\App\Module::setActive($alias, false);
|
||||
|
||||
$outputLog = new BufferedOutput();
|
||||
\Artisan::call('freescout:clear-cache', [], $outputLog);
|
||||
$output = $outputLog->fetch();
|
||||
|
||||
// Get module name
|
||||
$module = \Module::findByAlias($alias);
|
||||
$name = '?';
|
||||
if ($module) {
|
||||
$name = $module->getName();
|
||||
}
|
||||
|
||||
$type = 'danger';
|
||||
$msg = __('Error occured activating module');
|
||||
if (session('flashes_floating') && is_array(session('flashes_floating'))) {
|
||||
// If there was any error, module has been deactivate via modules.register_error filter
|
||||
$msg = '';
|
||||
foreach (session('flashes_floating') as $flash) {
|
||||
$msg .= $flash['text'].' ';
|
||||
}
|
||||
} elseif (strstr($output, 'Configuration cached successfully')) {
|
||||
$msg = __('Error occured deactivating :name module', ['name' => $name]);
|
||||
if (strstr($output, 'Configuration cached successfully')) {
|
||||
$type = 'success';
|
||||
$msg = __('Module successfully activated!');
|
||||
} else {
|
||||
// Deactivate module
|
||||
\App\Module::setActive($alias, false);
|
||||
\Artisan::call('freescout:clear-cache');
|
||||
$msg = __('":name" module successfully DEactivated!', ['name' => $name]);
|
||||
}
|
||||
|
||||
// \Session::flash does not work after BufferedOutput
|
||||
@@ -281,29 +375,54 @@ class ModulesController extends Controller
|
||||
$response['status'] = 'success';
|
||||
break;
|
||||
|
||||
case 'deactivate':
|
||||
case 'deactivate_license':
|
||||
$license = $request->license;
|
||||
$alias = $request->alias;
|
||||
\App\Module::setActive($alias, false);
|
||||
|
||||
$outputLog = new BufferedOutput;
|
||||
\Artisan::call('freescout:clear-cache', [], $outputLog);
|
||||
$output = $outputLog->fetch();
|
||||
|
||||
$type = 'danger';
|
||||
$msg = __('Error occured deactivating module');
|
||||
if (strstr($output, 'Configuration cached successfully')) {
|
||||
$type = 'success';
|
||||
$msg = __('Module successfully deactivated!');
|
||||
if (!$license) {
|
||||
$response['msg'] = __('Empty license key');
|
||||
}
|
||||
|
||||
// \Session::flash does not work after BufferedOutput
|
||||
$flash = [
|
||||
'text' => '<strong>'.$msg.'</strong><pre class="margin-top">'.$output.'</pre>',
|
||||
'unescaped' => true,
|
||||
'type' => $type,
|
||||
];
|
||||
\Cache::forever('modules_flash', $flash);
|
||||
$response['status'] = 'success';
|
||||
if (!$response['msg']) {
|
||||
$params = [
|
||||
'license' => $license,
|
||||
'module_alias' => $alias,
|
||||
'url' => (!empty($request->any_url) ? '*' : \App\Module::getAppUrl()),
|
||||
];
|
||||
$result = WpApi::deactivateLicense($params);
|
||||
|
||||
if (WpApi::$lastError) {
|
||||
$response['msg'] = WpApi::$lastError['message'];
|
||||
} elseif (!empty($result['code']) && !empty($result['message'])) {
|
||||
$response['msg'] = $result['message'];
|
||||
} else {
|
||||
if (!empty($result['status']) && $result['status'] == 'success') {
|
||||
$db_module = \App\Module::getByAlias($alias);
|
||||
if ($db_module && trim($db_module->license) == trim($license)) {
|
||||
// Remove remembered license key and deactivate license in DB
|
||||
\App\Module::deactivateLicense($alias, '');
|
||||
|
||||
// Deactivate module
|
||||
\App\Module::setActive($alias, false);
|
||||
\Artisan::call('freescout:clear-cache', []);
|
||||
}
|
||||
|
||||
// Flash does not work here.
|
||||
$flash = [
|
||||
'text' => '<strong>'.__('License successfully DEactivated!').'</strong>',
|
||||
'unescaped' => true,
|
||||
'type' => 'success',
|
||||
];
|
||||
\Cache::forever('modules_flash', $flash);
|
||||
|
||||
$response['status'] = 'success';
|
||||
} elseif (!empty($result['error'])) {
|
||||
$response['msg'] = $this->getErrorMessage($result['error'], $result);
|
||||
} else {
|
||||
$response['msg'] = __('Error occured. Please try again later.');
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
@@ -318,12 +437,126 @@ class ModulesController extends Controller
|
||||
$module->delete();
|
||||
\Session::flash('flash_success_floating', __('Module deleted'));
|
||||
} else {
|
||||
$response['msg'] = __("Module not found").': '.$alias;
|
||||
$response['msg'] = __('Module not found').': '.$alias;
|
||||
}
|
||||
|
||||
$response['status'] = 'success';
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
$alias = $request->alias;
|
||||
$module = \Module::findByAlias($alias);
|
||||
|
||||
if (!$module) {
|
||||
$response['msg'] = __('Module not found').': '.$alias;
|
||||
}
|
||||
|
||||
// Download new version
|
||||
$download_error = false;
|
||||
if (!$response['msg']) {
|
||||
$params = [
|
||||
'license' => \App\Module::getLicense($alias),
|
||||
'module_alias' => $alias,
|
||||
'url' => \App\Module::getAppUrl(),
|
||||
];
|
||||
$license_details = WpApi::getVersion($params);
|
||||
|
||||
if (WpApi::$lastError) {
|
||||
$response['msg'] = WpApi::$lastError['message'];
|
||||
} elseif (!empty($license_details['code']) && !empty($license_details['message'])) {
|
||||
$response['msg'] = $license_details['message'];
|
||||
} elseif (!empty($license_details['required_app_version']) && !\Helper::checkAppVersion($license_details['required_app_version'])) {
|
||||
$response['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) {
|
||||
$response['msg'] = $e->getMessage();
|
||||
}
|
||||
|
||||
if (!file_exists($module_archive)) {
|
||||
$download_error = true;
|
||||
} else {
|
||||
// Extract
|
||||
try {
|
||||
\Helper::unzip($module_archive, \Module::getPath());
|
||||
} catch (\Exception $e) {
|
||||
// We will show this as floating message on page reload
|
||||
$response['msg'] = $e->getMessage();
|
||||
}
|
||||
// Check if extracted module exists
|
||||
\Module::clearCache();
|
||||
$module = \Module::findByAlias($alias);
|
||||
if (!$module) {
|
||||
$download_error = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove archive
|
||||
if (file_exists($module_archive)) {
|
||||
\File::delete($module_archive);
|
||||
}
|
||||
|
||||
if ($download_error) {
|
||||
$response['reload'] = true;
|
||||
|
||||
if ($response['msg']) {
|
||||
\Session::flash('flash_error_floating', $response['msg']);
|
||||
}
|
||||
|
||||
\Session::flash('flash_error_unescaped', __('Error occured 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'] && $response['msg'] = $this->getErrorMessage($license_details['status'])) {
|
||||
//$response['msg'] = ;
|
||||
} else {
|
||||
$response['msg'] = __('Error occured. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// Install updated module
|
||||
if (!$response['msg'] && !$download_error) {
|
||||
$outputLog = new BufferedOutput();
|
||||
\Artisan::call('freescout:module-install', ['module_alias' => $alias], $outputLog);
|
||||
$output = $outputLog->fetch();
|
||||
|
||||
// Get module name
|
||||
$name = '?';
|
||||
if ($module) {
|
||||
$name = $module->getName();
|
||||
}
|
||||
|
||||
$type = 'danger';
|
||||
$msg = __('Error occured activating ":name" module', ['name' => $name]);
|
||||
if (session('flashes_floating') && is_array(session('flashes_floating'))) {
|
||||
// If there was any error, module has been deactivated via modules.register_error filter
|
||||
$msg = '';
|
||||
foreach (session('flashes_floating') as $flash) {
|
||||
$msg .= $flash['text'].' ';
|
||||
}
|
||||
} elseif (strstr($output, 'Configuration cached successfully')) {
|
||||
$type = 'success';
|
||||
$msg = __('":name" module successfully updated!', ['name' => $name]);
|
||||
} else {
|
||||
// Deactivate module
|
||||
\App\Module::setActive($alias, false);
|
||||
\Artisan::call('freescout:clear-cache');
|
||||
}
|
||||
|
||||
// \Session::flash does not work after BufferedOutput
|
||||
$flash = [
|
||||
'text' => '<strong>'.$msg.'</strong><pre class="margin-top">'.$output.'</pre>',
|
||||
'unescaped' => true,
|
||||
'type' => $type,
|
||||
];
|
||||
\Cache::forever('modules_flash', $flash);
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
$response['msg'] = 'Unknown action';
|
||||
break;
|
||||
@@ -335,4 +568,40 @@ class ModulesController extends Controller
|
||||
|
||||
return \Response::json($response);
|
||||
}
|
||||
|
||||
public function getErrorMessage($code, $result = null)
|
||||
{
|
||||
$msg = '';
|
||||
|
||||
switch ($code) {
|
||||
case 'missing':
|
||||
$msg = __('License key does not exist');
|
||||
break;
|
||||
case 'license_not_activable':
|
||||
$msg = __("You have to activate each bundle's module separately");
|
||||
break;
|
||||
case 'disabled':
|
||||
$msg = __('License key has been revoked');
|
||||
break;
|
||||
case 'no_activations_left':
|
||||
$msg = __('No activations left for this license key').' ('.__('Use Deactivate License button to transfer license key from another domain').')';
|
||||
break;
|
||||
case 'expired':
|
||||
$msg = __('License key has expired');
|
||||
break;
|
||||
case 'key_mismatch':
|
||||
$msg = __('License key belongs to another module');
|
||||
break;
|
||||
case 'invalid_item_id':
|
||||
$msg = __('Module not found in the modules directory');
|
||||
break;
|
||||
default:
|
||||
if ($result && !empty($result['error'])) {
|
||||
$msg = __('Error code:'.' '.$result['error']);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $msg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Attachment;
|
||||
use App\Conversation;
|
||||
use App\Option;
|
||||
use App\Thread;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -17,7 +20,6 @@ class PublicController extends Controller
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,12 +34,15 @@ class PublicController extends Controller
|
||||
}
|
||||
$user = User::where('invite_hash', $hash)->first();
|
||||
|
||||
if ($user && $user->locale) {
|
||||
\Helper::setLocale($user->locale);
|
||||
}
|
||||
|
||||
return view('public/user_setup', ['user' => $user]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save user from invitation.
|
||||
*
|
||||
*/
|
||||
public function userSetupSave($hash, Request $request)
|
||||
{
|
||||
@@ -106,4 +111,95 @@ class PublicController extends Controller
|
||||
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
||||
/*
|
||||
* Set a thread as read by customer
|
||||
*/
|
||||
public function setThreadAsRead($conversation_id, $thread_id)
|
||||
{
|
||||
$conversation = Conversation::findOrFail($conversation_id);
|
||||
$thread = Thread::findOrFail($thread_id);
|
||||
|
||||
// We only track the first opening
|
||||
if (empty($thread->opened_at)) {
|
||||
$thread->opened_at = date('Y-m-d H:i:s');
|
||||
$thread->save();
|
||||
\Eventy::action('thread.opened', $thread, $conversation);
|
||||
}
|
||||
|
||||
// Create a 1x1 ttransparent pixel and return it
|
||||
$pixel = sprintf('%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c', 71, 73, 70, 56, 57, 97, 1, 0, 1, 0, 128, 255, 0, 192, 192, 192, 0, 0, 0, 33, 249, 4, 1, 0, 0, 0, 0, 44, 0, 0, 0, 0, 1, 0, 1, 0, 0, 2, 2, 68, 1, 0, 59);
|
||||
$response = \Response::make($pixel, 200);
|
||||
$response->header('Content-type', 'image/gif');
|
||||
$response->header('Content-Length', 42);
|
||||
$response->header('Cache-Control', 'private, no-cache, no-cache=Set-Cookie, proxy-revalidate');
|
||||
$response->header('Expires', 'Wed, 11 Jan 2000 12:59:00 GMT');
|
||||
$response->header('Last-Modified', 'Wed, 11 Jan 2006 12:59:00 GMT');
|
||||
$response->header('Pragma', 'no-cache');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download an attachment.
|
||||
*/
|
||||
public function downloadAttachment($dir_1, $dir_2, $dir_3, $file_name, Request $request)
|
||||
{
|
||||
$id = $request->query('id', '');
|
||||
$token = $request->query('token', '');
|
||||
$attachment = null;
|
||||
|
||||
// Old attachments can not be requested by id.
|
||||
if (!$token && $id) {
|
||||
return \Helper::denyAccess();
|
||||
}
|
||||
|
||||
// Get attachment by id.
|
||||
if ($id) {
|
||||
$attachment = Attachment::findOrFail($id);
|
||||
}
|
||||
|
||||
if (!$attachment) {
|
||||
$attachment = Attachment::where('file_dir', $dir_1.DIRECTORY_SEPARATOR.$dir_2.DIRECTORY_SEPARATOR.$dir_3.DIRECTORY_SEPARATOR)
|
||||
->where('file_name', $file_name)
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
// Only allow download if the attachment is public or if the token matches the hash of the contents
|
||||
if ($token != $attachment->getToken() && (bool)$attachment->public !== true) {
|
||||
return \Helper::denyAccess();
|
||||
}
|
||||
|
||||
$view_attachment = false;
|
||||
$file_ext = strtolower(pathinfo($attachment->file_name, PATHINFO_EXTENSION));
|
||||
|
||||
// Some file type should be viewed in the browser instead of downloading.
|
||||
if (in_array($file_ext, config('app.viewable_attachments'))) {
|
||||
$view_attachment = true;
|
||||
}
|
||||
|
||||
if (config('app.download_attachments_via') == 'apache') {
|
||||
// Send using Apache mod_xsendfile.
|
||||
$response = response(null)
|
||||
->header('Content-Type' , $attachment->mime_type)
|
||||
->header('X-Sendfile', $attachment->getLocalFilePath());
|
||||
|
||||
if (!$view_attachment) {
|
||||
$response->header('Content-Disposition', 'attachment; filename="'.$attachment->file_name.'"');
|
||||
}
|
||||
} elseif (config('app.download_attachments_via') == 'nginx') {
|
||||
// Send using Nginx.
|
||||
$response = response(null)
|
||||
->header('Content-Type' , $attachment->mime_type)
|
||||
->header('X-Accel-Redirect', $attachment->getLocalFilePath(false));
|
||||
|
||||
if (!$view_attachment) {
|
||||
$response->header('Content-Disposition', 'attachment; filename="'.$attachment->file_name.'"');
|
||||
}
|
||||
} else {
|
||||
$response = $attachment->download($view_attachment);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\ActivityLog;
|
||||
use App\Option;
|
||||
use App\Misc\Helper;
|
||||
use App\SendLog;
|
||||
use App\Thread;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SecureController extends Controller
|
||||
@@ -29,7 +28,15 @@ class SecureController extends Controller
|
||||
*/
|
||||
public function dashboard()
|
||||
{
|
||||
$mailboxes = auth()->user()->mailboxesCanView();
|
||||
$user = auth()->user();
|
||||
if (!$user->isAdmin()) {
|
||||
$mailboxes = $user->mailboxesCanView();
|
||||
} else {
|
||||
$mailboxes = $user->mailboxesCanViewWithSettings();
|
||||
}
|
||||
|
||||
// Sort by name.
|
||||
$mailboxes = \Eventy::filter('dashboard.mailboxes', $mailboxes->sortBy('name'));
|
||||
|
||||
return view('secure/dashboard', ['mailboxes' => $mailboxes]);
|
||||
}
|
||||
@@ -63,7 +70,7 @@ class SecureController extends Controller
|
||||
$activities = ActivityLog::inLog($request->name)->orderBy('created_at', 'desc')->paginate($page_size);
|
||||
$name = $request->name;
|
||||
} elseif (count($names)) {
|
||||
$name = ActivityLog::NAME_OUT_EMAILS;
|
||||
$name = ActivityLog::NAME_OUT_EMAILS;
|
||||
// $activities = ActivityLog::inLog($names[0])->orderBy('created_at', 'desc')->paginate($page_size);
|
||||
// $name = $names[0];
|
||||
}
|
||||
@@ -113,15 +120,17 @@ class SecureController extends Controller
|
||||
$activities = SendLog::orderBy('created_at', 'desc')->paginate($page_size);
|
||||
|
||||
foreach ($activities as $record) {
|
||||
|
||||
$conversation = '';
|
||||
if ($record->thread_id) {
|
||||
$conversation = Thread::find($record->thread_id);
|
||||
}
|
||||
|
||||
|
||||
$status = $record->getStatusName();
|
||||
if ($record->status_message) {
|
||||
$status .= '. '.$record->status_message;
|
||||
if ($record->status == SendLog::STATUS_SEND_ERROR) {
|
||||
$status .= '. Message-ID: '.$record->message_id;
|
||||
}
|
||||
}
|
||||
|
||||
$logs[] = [
|
||||
@@ -134,7 +143,6 @@ class SecureController extends Controller
|
||||
'customer' => $record->customer,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
array_unshift($names, ActivityLog::NAME_OUT_EMAILS);
|
||||
@@ -145,16 +153,16 @@ class SecureController extends Controller
|
||||
}
|
||||
|
||||
return view('secure/logs', [
|
||||
'logs' => $logs,
|
||||
'names' => $names,
|
||||
'logs' => $logs,
|
||||
'names' => $names,
|
||||
'current_name' => $name,
|
||||
'cols' => $cols,
|
||||
'activities' => $activities
|
||||
'cols' => $cols,
|
||||
'activities' => $activities,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs page submitted
|
||||
* Logs page submitted.
|
||||
*/
|
||||
public function logsSubmit(Request $request)
|
||||
{
|
||||
@@ -165,7 +173,7 @@ class SecureController extends Controller
|
||||
$activities = ActivityLog::inLog($request->name)->orderBy('created_at', 'desc')->get();
|
||||
$name = $request->name;
|
||||
} elseif (count($names = ActivityLog::select('log_name')->distinct()->get()->pluck('log_name'))) {
|
||||
$name = ActivityLog::NAME_OUT_EMAILS;
|
||||
$name = ActivityLog::NAME_OUT_EMAILS;
|
||||
// $activities = ActivityLog::inLog($names[0])->orderBy('created_at', 'desc')->get();
|
||||
// $name = $names[0];
|
||||
}
|
||||
@@ -181,4 +189,41 @@ class SecureController extends Controller
|
||||
|
||||
return redirect()->route('logs', ['name' => $name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload files and images.
|
||||
*/
|
||||
public function upload(Request $request, $allowed_exts = [])
|
||||
{
|
||||
// 'jpg','gif','png'
|
||||
$response = [
|
||||
'status' => 'error',
|
||||
'msg' => '', // this is error message
|
||||
];
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user) {
|
||||
$response['msg'] = __('Please login to upload file');
|
||||
}
|
||||
|
||||
if (!$request->hasFile('file') || !$request->file('file')->isValid() || !$request->file) {
|
||||
$response['msg'] = __('Error occured uploading file');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
|
||||
$upload = Helper::uploadFile($request->file, $allowed_exts);
|
||||
$filename = basename($upload);
|
||||
|
||||
if ($upload) {
|
||||
$response['status'] = 'success';
|
||||
$response['url'] = Helper::uploadedFileUrl($filename);
|
||||
} else {
|
||||
$response['msg'] = __('Error occured uploading file');
|
||||
}
|
||||
}
|
||||
|
||||
return \Response::json($response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Conversation;
|
||||
use App\Option;
|
||||
use App\Subscription;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Validator;
|
||||
|
||||
class SettingsController extends Controller
|
||||
@@ -21,12 +22,12 @@ class SettingsController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* General settings
|
||||
* General settings.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function view($section = 'general')
|
||||
{
|
||||
{
|
||||
$settings = $this->getSectionSettings($section);
|
||||
|
||||
if (!$settings) {
|
||||
@@ -36,10 +37,10 @@ class SettingsController extends Controller
|
||||
$sections = $this->getSections();
|
||||
|
||||
$template_vars = [
|
||||
'settings' => $settings,
|
||||
'section' => $section,
|
||||
'sections' => $this->getSections(),
|
||||
'section_name' => $sections[$section]['title']
|
||||
'settings' => $settings,
|
||||
'section' => $section,
|
||||
'sections' => $this->getSections(),
|
||||
'section_name' => $sections[$section]['title'],
|
||||
];
|
||||
$template_vars = $this->getTemplateVars($section, $template_vars);
|
||||
|
||||
@@ -48,78 +49,184 @@ class SettingsController extends Controller
|
||||
|
||||
public function getValidator($section)
|
||||
{
|
||||
switch ($section) {
|
||||
case 'emails':
|
||||
$rules = [
|
||||
'settings.mail_from' => 'required|email',
|
||||
];
|
||||
break;
|
||||
// default:
|
||||
// $rules = \Event::fire('filter.settings_validate_rules');
|
||||
// break;
|
||||
}
|
||||
$rules = $this->getSectionParams($section, 'validator_rules');
|
||||
|
||||
if (!empty($rules)) {
|
||||
return Validator::make(request()->all(), $rules);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getTemplateVars($section, $template_vars)
|
||||
{
|
||||
$section_vars = $this->getSectionParams($section, 'template_vars');
|
||||
|
||||
if ($section_vars && is_array($section_vars)) {
|
||||
return array_merge($template_vars, $section_vars);
|
||||
} else {
|
||||
return $template_vars;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters of the sections settings.
|
||||
*
|
||||
* If in settings parameter `env` is set, option will be saved into .env file
|
||||
* instead of DB.
|
||||
*
|
||||
* @param [type] $section [description]
|
||||
* @param string $param [description]
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function getSectionParams($section, $param = '')
|
||||
{
|
||||
$params = [];
|
||||
|
||||
switch ($section) {
|
||||
case 'emails':
|
||||
$template_vars['sendmail_path'] = ini_get('sendmail_path');
|
||||
$template_vars['mail_drivers'] = [
|
||||
'mail' => __("PHP's mail() function"),
|
||||
'sendmail' => __("Sendmail"),
|
||||
'smtp' => 'SMTP',
|
||||
$params = [
|
||||
'template_vars' => [
|
||||
'sendmail_path' => ini_get('sendmail_path'),
|
||||
'mail_drivers' => [
|
||||
'mail' => __("PHP's mail() function"),
|
||||
'sendmail' => __('Sendmail'),
|
||||
'smtp' => 'SMTP',
|
||||
],
|
||||
],
|
||||
'validator_rules' => [
|
||||
'settings.mail_from' => 'required|email',
|
||||
],
|
||||
'settings' => [
|
||||
'fetch_schedule' => [
|
||||
'env' => 'APP_FETCH_SCHEDULE',
|
||||
],
|
||||
'mail_password' => [
|
||||
'safe_password' => true,
|
||||
'encrypt' => true,
|
||||
]
|
||||
],
|
||||
];
|
||||
break;
|
||||
|
||||
// default:
|
||||
// $template_vars = \Event::fire('filter.settings_template_vars', [$template_vars]);
|
||||
// break;
|
||||
case 'general':
|
||||
$params = [
|
||||
'settings' => [
|
||||
'email_conv_history' => [
|
||||
'env' => 'APP_EMAIL_CONV_HISTORY',
|
||||
],
|
||||
'email_user_history' => [
|
||||
'env' => 'APP_EMAIL_USER_HISTORY',
|
||||
],
|
||||
'locale' => [
|
||||
'env' => 'APP_LOCALE',
|
||||
],
|
||||
'timezone' => [
|
||||
'env' => 'APP_TIMEZONE',
|
||||
],
|
||||
'user_permissions' => [
|
||||
'env' => 'APP_USER_PERMISSIONS',
|
||||
'env_encode' => true,
|
||||
],
|
||||
],
|
||||
];
|
||||
break;
|
||||
case 'alerts':
|
||||
$subscriptions_defaults = Subscription::getDefaultSubscriptions();
|
||||
$subscriptions = array();
|
||||
foreach ($subscriptions_defaults as $medium => $subscriptions_for_medium) {
|
||||
foreach ($subscriptions_defaults[$medium] as $subscription) {
|
||||
$subscriptions[] = (object) array("medium" => $medium, "event" => $subscription);
|
||||
}
|
||||
}
|
||||
$params = [
|
||||
'template_vars' => [
|
||||
'logs' => \App\ActivityLog::getAvailableLogs(),
|
||||
'person' => null,
|
||||
'subscriptions' => $subscriptions,
|
||||
'mobile_available' => \Eventy::filter('notifications.mobile_available', false),
|
||||
],
|
||||
'settings' => [
|
||||
'alert_logs' => [
|
||||
'env' => 'APP_ALERT_LOGS',
|
||||
],
|
||||
'alert_logs_period' => [
|
||||
'env' => 'APP_ALERT_LOGS_PERIOD',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// todo: monitor App Logs
|
||||
foreach ($params['template_vars']['logs'] as $i => $log) {
|
||||
if ($log == \App\ActivityLog::NAME_APP_LOGS || $log == \App\ActivityLog::NAME_OUT_EMAILS) {
|
||||
unset($params['template_vars']['logs'][$i]);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
$params = \Eventy::filter('settings.section_params', $params, $section);
|
||||
break;
|
||||
}
|
||||
|
||||
return $template_vars;
|
||||
if ($param) {
|
||||
if (isset($params[$param])) {
|
||||
return $params[$param];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSectionSettings($section)
|
||||
{
|
||||
$settings = [];
|
||||
|
||||
switch ($section) {
|
||||
case 'general':
|
||||
$settings = [
|
||||
'company_name' => Option::get('company_name', \Config::get('app.name')),
|
||||
'next_ticket' => Option::get('next_ticket'),
|
||||
'user_permissions' => Option::get('user_permissions', []),
|
||||
'next_ticket' => (Option::get('next_ticket') >= Conversation::max('number') + 1) ? Option::get('next_ticket') : Conversation::max('number') + 1,
|
||||
'user_permissions' => User::getGlobalUserPermissions(),
|
||||
'email_branding' => Option::get('email_branding'),
|
||||
'open_tracking' => Option::get('open_tracking'),
|
||||
'email_conv_history' => config('app.email_conv_history'),
|
||||
'email_user_history' => config('app.email_user_history'),
|
||||
'enrich_customer_data' => Option::get('enrich_customer_data'),
|
||||
'time_format' => Option::get('time_format', User::TIME_FORMAT_24),
|
||||
'locale' => \Helper::getRealAppLocale(),
|
||||
'timezone' => config('app.timezone'),
|
||||
];
|
||||
break;
|
||||
case 'emails':
|
||||
$settings = [
|
||||
'mail_from' => \App\Misc\Mail::getSystemMailFrom(),
|
||||
'mail_driver' => Option::get('mail_driver', \Config::get('mail.driver')),
|
||||
'mail_host' => Option::get('mail_host', \Config::get('mail.host')),
|
||||
'mail_port' => Option::get('mail_port', \Config::get('mail.port')),
|
||||
'mail_username' => Option::get('mail_username', \Config::get('mail.username')),
|
||||
'mail_password' => Option::get('mail_password', \Config::get('mail.password')),
|
||||
'mail_from' => \App\Misc\Mail::getSystemMailFrom(),
|
||||
'mail_driver' => Option::get('mail_driver', \Config::get('mail.driver')),
|
||||
'mail_host' => Option::get('mail_host', \Config::get('mail.host')),
|
||||
'mail_port' => Option::get('mail_port', \Config::get('mail.port')),
|
||||
'mail_username' => Option::get('mail_username', \Config::get('mail.username')),
|
||||
'mail_password' => \Helper::decrypt(Option::get('mail_password', \Config::get('mail.password'))),
|
||||
'mail_encryption' => Option::get('mail_encryption', \Config::get('mail.encryption')),
|
||||
'fetch_schedule' => config('app.fetch_schedule'),
|
||||
];
|
||||
break;
|
||||
case 'alerts':
|
||||
$settings = [
|
||||
'alert_recipients' => Option::get('alert_recipients'),
|
||||
'alert_fetch' => Option::get('alert_fetch'),
|
||||
'alert_fetch_period' => Option::get('alert_fetch_period'),
|
||||
'alert_send' => Option::get('alert_send'),
|
||||
];
|
||||
$settings = Option::getOptions([
|
||||
'alert_recipients',
|
||||
'alert_fetch',
|
||||
'alert_fetch_period',
|
||||
'alert_logs',
|
||||
'alert_logs_names',
|
||||
'alert_logs_period',
|
||||
'subscription_defaults',
|
||||
], [
|
||||
'alert_logs_names' => [],
|
||||
'alert_logs' => config('app.alert_logs'),
|
||||
'alert_logs_period' => config('app.alert_logs_period'),
|
||||
]);
|
||||
break;
|
||||
default:
|
||||
$settings = \Event::fire('filter.section_settings', [$section]);
|
||||
$settings = \Eventy::filter('settings.section_settings', $settings, $section);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -134,7 +241,8 @@ class SettingsController extends Controller
|
||||
'emails' => ['title' => __('Mail Settings'), 'icon' => 'transfer', 'order' => 200],
|
||||
'alerts' => ['title' => __('Alerts'), 'icon' => 'bell', 'order' => 300],
|
||||
];
|
||||
//return \Event::fire('filter.settings_sections', [$sections]);
|
||||
$sections = \Eventy::filter('settings.sections', $sections);
|
||||
|
||||
return $sections;
|
||||
}
|
||||
|
||||
@@ -167,25 +275,83 @@ class SettingsController extends Controller
|
||||
|
||||
$request = request();
|
||||
|
||||
$request->settings = array_merge($request->settings, [
|
||||
'email_branding' => !empty($request->settings['email_branding']) ? $request->settings['email_branding'] : 0,
|
||||
'open_tracking' => !empty($request->settings['open_tracking']) ? $request->settings['open_tracking'] : 0,
|
||||
'enrich_customer_data' => !empty($request->settings['enrich_customer_data']) ? $request->settings['enrich_customer_data'] : 0,
|
||||
]);
|
||||
$request = \Eventy::filter('settings.before_save', $request, $section, $settings);
|
||||
|
||||
$cc_required = false;
|
||||
$settings_params = $this->getSectionParams($section, 'settings');
|
||||
foreach ($settings as $i => $option_name) {
|
||||
// By some reason isset() does not work for empty elements
|
||||
// Do not save dummy passwords.
|
||||
if (!empty($settings_params[$option_name])
|
||||
&& !empty($settings_params[$option_name]['safe_password'])
|
||||
&& $request->settings[$option_name]
|
||||
&& preg_match("/^\*+$/", $request->settings[$option_name])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Option has to be saved to .env file.
|
||||
if (!empty($settings_params[$option_name]) && !empty($settings_params[$option_name]['env'])) {
|
||||
$env_value = $request->settings[$option_name] ?? '';
|
||||
|
||||
if (is_array($env_value)) {
|
||||
$env_value = json_encode($env_value);
|
||||
}
|
||||
|
||||
if (!empty($settings_params[$option_name]['encrypt'])) {
|
||||
$env_value = encrypt($env_value);
|
||||
}
|
||||
|
||||
if (!empty($settings_params[$option_name]['env_encode'])) {
|
||||
$env_value = base64_encode($env_value);
|
||||
}
|
||||
|
||||
\Helper::setEnvFileVar($settings_params[$option_name]['env'], $env_value);
|
||||
|
||||
config($option_name, $env_value);
|
||||
$cc_required = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// By some reason isset() does not work for empty elements.
|
||||
if (array_key_exists($option_name, $request->settings)) {
|
||||
$option_value = $request->settings[$option_name];
|
||||
|
||||
if (!empty($settings_params[$option_name]['encrypt'])) {
|
||||
$option_value = encrypt($option_value);
|
||||
}
|
||||
|
||||
Option::set($option_name, $option_value);
|
||||
} else {
|
||||
Option::remove($option_name);
|
||||
// If option does not exist, default will be used,
|
||||
// so we can not just remove bool settings.
|
||||
if (isset($settings_params[$option_name]['default'])) {
|
||||
$default = $settings_params[$option_name]['default'];
|
||||
} else {
|
||||
$default = \Option::getDefault($option_name, null);
|
||||
}
|
||||
if ($default === true) {
|
||||
Option::set($option_name, false);
|
||||
} elseif (is_array(\Option::getDefault($option_name, -1))) {
|
||||
Option::set($option_name, []);
|
||||
} else {
|
||||
Option::remove($option_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
\Session::flash('flash_success_floating', __('Settings updated'));
|
||||
// Clear cache if some options have been saved to .env file.
|
||||
if ($cc_required) {
|
||||
\Helper::clearCache(['--doNotGenerateVars' => true]);
|
||||
}
|
||||
|
||||
return redirect()->route('settings', ['section' => $section]);
|
||||
// \Helper::clearCache prevents \Session::flash() from displaying.
|
||||
$request->session()->flash('flash_success_floating', __('Settings updated'));
|
||||
|
||||
$response = redirect()->route('settings', ['section' => $section]);
|
||||
|
||||
$response = \Eventy::filter('settings.after_save', $response, $request, $section, $settings);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,7 +385,7 @@ class SettingsController extends Controller
|
||||
}
|
||||
|
||||
if (!$test_result && !$response['msg']) {
|
||||
$response['msg'] = __('Error occurend sending email. Please check your mail server logs for more details.');
|
||||
$response['msg'] = __('Error occurred sending email. Please check your mail server logs for more details.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ use Symfony\Component\Console\Output\BufferedOutput;
|
||||
|
||||
class SystemController extends Controller
|
||||
{
|
||||
public static $latest_version_error = '';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
@@ -17,7 +19,9 @@ class SystemController extends Controller
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->middleware('auth', ['except' => [
|
||||
'cron'
|
||||
]]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,13 +45,65 @@ class SystemController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
// Functions.
|
||||
$functions = [
|
||||
'shell_exec (PHP)' => function_exists('shell_exec'),
|
||||
'proc_open (PHP)' => function_exists('proc_open'),
|
||||
'ps (shell)' => function_exists('shell_exec') ? shell_exec('ps') : false,
|
||||
];
|
||||
|
||||
// Permissions.
|
||||
$permissions = [];
|
||||
foreach (config('installer.permissions') as $perm_path => $perm_value) {
|
||||
$path = base_path($perm_path);
|
||||
$value = '';
|
||||
if (file_exists($path)) {
|
||||
$value = substr(sprintf('%o', fileperms($path)), -4);
|
||||
}
|
||||
$permissions[$perm_path] = [
|
||||
'status' => \Helper::isFolderWritable($path),
|
||||
'value' => $value,
|
||||
];
|
||||
}
|
||||
|
||||
// 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 = trim($non_writable_cache_file ?? '');
|
||||
if (!strstr($non_writable_cache_file, base_path('storage/framework/cache/data/'))) {
|
||||
$non_writable_cache_file = '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check if public symlink exists, if not, try to create.
|
||||
$public_symlink_exists = true;
|
||||
$public_path = public_path('storage');
|
||||
$public_test = $public_path.DIRECTORY_SEPARATOR.'.gitignore';
|
||||
|
||||
if (!file_exists($public_test) || !file_get_contents($public_test)) {
|
||||
\File::delete($public_path);
|
||||
\Artisan::call('storage:link');
|
||||
if (!file_exists($public_test) || !file_get_contents($public_test)) {
|
||||
$public_symlink_exists = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if .env is writable.
|
||||
$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();
|
||||
$failed_queues = $failed_jobs->pluck('queue')->unique();
|
||||
|
||||
// Commands
|
||||
$commands_list = ['freescout:fetch-emails', 'queue:work'];
|
||||
foreach ($commands_list as $command_name) {
|
||||
$commands_list = [
|
||||
'freescout:fetch-emails' => 'freescout:fetch-emails',
|
||||
\Helper::getWorkerIdentifier() => 'queue:work'
|
||||
];
|
||||
foreach ($commands_list as $command_identifier => $command_name) {
|
||||
$status_texts = [];
|
||||
|
||||
// Check if command is running now
|
||||
@@ -55,7 +111,7 @@ class SystemController extends Controller
|
||||
$running_commands = 0;
|
||||
|
||||
try {
|
||||
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep '{$command_name}'"));
|
||||
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep '{$command_identifier}'"));
|
||||
$pids = [];
|
||||
foreach ($processes as $process) {
|
||||
preg_match("/^[\S]+\s+([\d]+)\s+/", $process, $m);
|
||||
@@ -77,7 +133,7 @@ class SystemController extends Controller
|
||||
} elseif ($running_commands > 1) {
|
||||
// queue:work command is stopped by settings a cache key
|
||||
if ($command_name == 'queue:work') {
|
||||
\Cache::forever('illuminate:queue:restart', Carbon::now()->getTimestamp());
|
||||
\Helper::queueWorkRestart();
|
||||
$commands[] = [
|
||||
'name' => $command_name,
|
||||
'status' => 'error',
|
||||
@@ -120,9 +176,10 @@ class SystemController extends Controller
|
||||
}
|
||||
|
||||
// If queue:work is not running, clear cache to let it start if something is wrong with the mutex
|
||||
if ($command_name == 'queue:work' && !$last_successful_run && function_exists('shell_exec')) {
|
||||
$status_texts[] = __('Cleared cache to force command to start.');
|
||||
\Artisan::call('cache:clear');
|
||||
if ($command_name == 'queue:work' && !$last_successful_run) {
|
||||
$status_texts[] = __('Try to :%a_start%clear cache:%a_end% to force command to start.', ['%a_start%' => '<a href="'.route('system.tools').'" target="_blank">', '%a_end%' => '</a>']);
|
||||
// This sometimes makes Status page open as non logged in user.
|
||||
//\Artisan::call('freescout:clear-cache', ['--doNotGenerateVars' => true]);
|
||||
}
|
||||
|
||||
$commands[] = [
|
||||
@@ -132,24 +189,67 @@ class SystemController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
// Check new version
|
||||
// Check new version if enabled
|
||||
$new_version_available = false;
|
||||
$latest_version = \Cache::remember('latest_version', 15, function() {
|
||||
return \Updater::getVersionAvailable();
|
||||
});
|
||||
if ($latest_version && $latest_version != \Config::get('app.version')) {
|
||||
$new_version_available = true;
|
||||
if (!\Config::get('app.disable_updating')) {
|
||||
$latest_version = \Cache::remember('latest_version', 15, function () {
|
||||
try {
|
||||
return \Updater::getVersionAvailable();
|
||||
} catch (\Exception $e) {
|
||||
SystemController::$latest_version_error = $e->getMessage();
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
if ($latest_version && version_compare($latest_version, \Config::get('app.version'), '>')) {
|
||||
$new_version_available = true;
|
||||
}
|
||||
} else {
|
||||
$latest_version = \Config::get('app.version');
|
||||
}
|
||||
|
||||
return view('system/status', [
|
||||
'commands' => $commands,
|
||||
'queued_jobs' => $queued_jobs,
|
||||
'failed_jobs' => $failed_jobs,
|
||||
'php_extensions' => $php_extensions,
|
||||
'commands' => $commands,
|
||||
'queued_jobs' => $queued_jobs,
|
||||
'failed_jobs' => $failed_jobs,
|
||||
'failed_queues' => $failed_queues,
|
||||
'php_extensions' => $php_extensions,
|
||||
'functions' => $functions,
|
||||
'permissions' => $permissions,
|
||||
'new_version_available' => $new_version_available,
|
||||
'latest_version' => $latest_version,
|
||||
'latest_version_error' => SystemController::$latest_version_error,
|
||||
'public_symlink_exists' => $public_symlink_exists,
|
||||
'env_is_writable' => $env_is_writable,
|
||||
'non_writable_cache_file' => $non_writable_cache_file,
|
||||
]);
|
||||
}
|
||||
|
||||
public function action(Request $request)
|
||||
{
|
||||
switch ($request->action) {
|
||||
case 'cancel_job':
|
||||
\App\Job::where('id', $request->job_id)->delete();
|
||||
\Session::flash('flash_success_floating', __('Done'));
|
||||
break;
|
||||
|
||||
case 'delete_failed_jobs':
|
||||
\App\FailedJob::where('queue', $request->failed_queue)->delete();
|
||||
\Session::flash('flash_success_floating', __('Failed jobs deleted'));
|
||||
break;
|
||||
|
||||
case 'retry_failed_jobs':
|
||||
$jobs = \App\FailedJob::where('queue', $request->failed_queue)->get();
|
||||
foreach ($jobs as $job) {
|
||||
\Artisan::call('queue:retry', ['id' => $job->id]);
|
||||
}
|
||||
\Session::flash('flash_success_floating', __('Failed jobs restarted'));
|
||||
break;
|
||||
}
|
||||
|
||||
return redirect()->route('system');
|
||||
}
|
||||
|
||||
/**
|
||||
* System tools.
|
||||
*/
|
||||
@@ -161,18 +261,20 @@ class SystemController extends Controller
|
||||
}
|
||||
|
||||
return view('system/tools', [
|
||||
'output' => $output
|
||||
'output' => $output,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute tools action.
|
||||
* @param Request $request [description]
|
||||
* @return [type] [description]
|
||||
*
|
||||
* @param Request $request [description]
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function toolsExecute(Request $request)
|
||||
{
|
||||
$outputLog = new BufferedOutput;
|
||||
$outputLog = new BufferedOutput();
|
||||
|
||||
switch ($request->action) {
|
||||
case 'clear_cache':
|
||||
@@ -180,7 +282,18 @@ class SystemController extends Controller
|
||||
break;
|
||||
|
||||
case 'fetch_emails':
|
||||
\Artisan::call('freescout:fetch-emails', [], $outputLog);
|
||||
$params = [];
|
||||
$params['--days'] = (int)$request->days;
|
||||
$params['--unseen'] = (int)$request->unseen;
|
||||
\Artisan::call('freescout:fetch-emails', $params, $outputLog);
|
||||
break;
|
||||
|
||||
case 'migrate_db':
|
||||
\Artisan::call('migrate', ['--force' => true], $outputLog);
|
||||
break;
|
||||
|
||||
case 'logout_users':
|
||||
\Artisan::call('freescout:logout-users', [], $outputLog);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -192,7 +305,7 @@ class SystemController extends Controller
|
||||
\Cache::forever('tools_execute_output', $output);
|
||||
}
|
||||
|
||||
return redirect()->route('system.tools');
|
||||
return redirect()->route('system.tools')->withInput($request->input());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,26 +323,34 @@ class SystemController extends Controller
|
||||
case 'update':
|
||||
try {
|
||||
$status = \Updater::update();
|
||||
|
||||
// Artisan::output()
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = __('Error occured').': '.$e->getMessage();
|
||||
$response['msg'] = __('Error occured. Please try again or try another :%a_start%update method:%a_end%', ['%a_start%' => '<a href="'.config('app.freescout_url').'/docs/update/" target="_blank">', '%a_end%' => '</a>']);
|
||||
$response['msg'] .= '<br/><br/>'.$e->getMessage();
|
||||
|
||||
\Helper::logException($e);
|
||||
}
|
||||
if (!$response['msg'] && $status) {
|
||||
// Adding session flash is useless as cache is cleated
|
||||
// Adding session flash is useless as cache is cleared
|
||||
$response['msg_success'] = __('Application successfully updated');
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'check_updates':
|
||||
try {
|
||||
$response['new_version_available'] = \Updater::isNewVersionAvailable(config('app.version'));
|
||||
$response['status'] = 'success';
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = __('Error occured').': '.$e->getMessage();
|
||||
}
|
||||
if (!$response['msg'] && !$response['new_version_available']) {
|
||||
// Adding session flash is useless as cache is cleated
|
||||
if (!\Config::get('app.disable_updating')) {
|
||||
try {
|
||||
$response['new_version_available'] = \Updater::isNewVersionAvailable(config('app.version'));
|
||||
$response['status'] = 'success';
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = __('Error occured').': '.$e->getMessage();
|
||||
}
|
||||
if (!$response['msg'] && !$response['new_version_available']) {
|
||||
// Adding session flash is useless as cache is cleated
|
||||
$response['msg_success'] = __('You have the latest version installed');
|
||||
}
|
||||
} else {
|
||||
$response['msg_success'] = __('You have the latest version installed');
|
||||
}
|
||||
break;
|
||||
@@ -245,4 +366,19 @@ class SystemController extends Controller
|
||||
|
||||
return \Response::json($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Cron.
|
||||
*/
|
||||
public function cron(Request $request)
|
||||
{
|
||||
if (empty($request->hash) || $request->hash != \Helper::getWebCronHash()) {
|
||||
abort(404);
|
||||
}
|
||||
$outputLog = new BufferedOutput();
|
||||
\Artisan::call('schedule:run', [], $outputLog);
|
||||
$output = $outputLog->fetch();
|
||||
|
||||
return response($output, 200)->header('Content-Type', 'text/plain');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,44 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use \Barryvdh\TranslationManager\Models\Translation;
|
||||
use Barryvdh\TranslationManager\Controller as BaseController;
|
||||
|
||||
class TranslateController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Send translations to FreeScout team.
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function postSend()
|
||||
{
|
||||
$result = false;
|
||||
|
||||
// Count changed translations.
|
||||
$changed_data = Translation::select(['locale', 'group', \DB::raw('count(*) as changed')])
|
||||
->where('status', Translation::STATUS_CHANGED)
|
||||
->groupBy(['locale', 'group'])
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
$this->manager->exportTranslations('*', false);
|
||||
|
||||
// Archive langs folder
|
||||
$archive_path = \Helper::createZipArchive(base_path().DIRECTORY_SEPARATOR.'resources/lang', 'lang.zip', 'lang');
|
||||
try {
|
||||
$archive_path = \Helper::createZipArchive(base_path().DIRECTORY_SEPARATOR.'resources/lang', 'lang.zip', 'lang');
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'error_msg' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($archive_path) {
|
||||
$attachments[] = $archive_path;
|
||||
|
||||
// Send archive to developers
|
||||
$result = \MailHelper::sendEmailToDevs('Translations', '', $attachments, auth()->user());
|
||||
$result = \MailHelper::sendEmailToDevs('Translations', json_encode($changed_data), $attachments, auth()->user());
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
@@ -28,4 +48,47 @@ class TranslateController extends BaseController
|
||||
abort(500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all translations which has not been published yet.
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function postRemoveUnpublished()
|
||||
{
|
||||
\Barryvdh\TranslationManager\Models\Translation::truncate();
|
||||
|
||||
return ['status' => 'ok'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Download as ZIP.
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function postDownload()
|
||||
{
|
||||
$this->manager->exportTranslations('*', false);
|
||||
$file_name = 'lang.zip';
|
||||
// Archive langs folder
|
||||
$archive_path = \Helper::createZipArchive(base_path().DIRECTORY_SEPARATOR.'resources/lang', $file_name, 'lang');
|
||||
$public_path = storage_path('app/public/'.$file_name);
|
||||
|
||||
\File::copy($archive_path, $public_path);
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/zip',
|
||||
];
|
||||
|
||||
return \Response::download($public_path, $file_name, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* List of strings to translate.
|
||||
*/
|
||||
public function stringsToTranslate()
|
||||
{
|
||||
__(':field is required');
|
||||
__('The following modules have to be installed and activated: :modules');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Events\UserDeleted;
|
||||
use App\Folder;
|
||||
use App\Mailbox;
|
||||
use App\Subscription;
|
||||
use App\Thread;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
@@ -32,7 +33,8 @@ class UsersController extends Controller
|
||||
{
|
||||
$this->authorize('create', 'App\User');
|
||||
|
||||
$users = User::all();
|
||||
$users = User::nonDeleted()->get();
|
||||
$users = User::sortUsers($users);
|
||||
|
||||
return view('users/users', ['users' => $users]);
|
||||
}
|
||||
@@ -55,21 +57,30 @@ class UsersController extends Controller
|
||||
*/
|
||||
public function createSave(Request $request)
|
||||
{
|
||||
$invalid = false;
|
||||
$this->authorize('create', 'App\User');
|
||||
$auth_user = auth()->user();
|
||||
|
||||
$rules = [
|
||||
'role' => 'integer',
|
||||
'first_name' => 'required|string|max:20',
|
||||
'last_name' => 'required|string|max:30',
|
||||
'email' => 'required|string|email|max:100|unique:users',
|
||||
'role' => ['required', Rule::in(array_keys(User::$roles))],
|
||||
//'role' => ['required', Rule::in(array_keys(User::$roles))],
|
||||
];
|
||||
if ($auth_user->isAdmin()) {
|
||||
$rules['role'] = ['required', Rule::in(array_keys(User::$roles))];
|
||||
}
|
||||
if (empty($request->send_invite)) {
|
||||
$rules['password'] = 'required|string|max:255';
|
||||
}
|
||||
$validator = Validator::make($request->all(), $rules);
|
||||
|
||||
if ($validator->fails()) {
|
||||
if (User::mailboxEmailExists($request->email)) {
|
||||
$invalid = true;
|
||||
$validator->errors()->add('email', __('There is a mailbox with such email. Users and mailboxes can not have the same email addresses.'));
|
||||
}
|
||||
|
||||
if ($invalid || $validator->fails()) {
|
||||
return redirect()->route('users.create')
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
@@ -77,6 +88,9 @@ class UsersController extends Controller
|
||||
|
||||
$user = new User();
|
||||
$user->fill($request->all());
|
||||
if (!$auth_user->can('changeRole', $user)) {
|
||||
$user->role = User::ROLE_USER;
|
||||
}
|
||||
if (empty($request->send_invite)) {
|
||||
// Set password from request
|
||||
$user->password = Hash::make($request->password);
|
||||
@@ -110,6 +124,9 @@ class UsersController extends Controller
|
||||
public function profile($id)
|
||||
{
|
||||
$user = User::findOrFail($id);
|
||||
if ($user->isDeleted()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$this->authorize('update', $user);
|
||||
|
||||
@@ -121,7 +138,7 @@ class UsersController extends Controller
|
||||
public function getUsersForSidebar($except_id)
|
||||
{
|
||||
if (auth()->user()->isAdmin()) {
|
||||
return User::all()->except($except_id);
|
||||
return User::sortUsers(User::nonDeleted()->get());/*->except($except_id)*/;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
@@ -136,6 +153,8 @@ class UsersController extends Controller
|
||||
*/
|
||||
public function profileSave($id, Request $request)
|
||||
{
|
||||
$invalid = false;
|
||||
|
||||
$user = User::findOrFail($id);
|
||||
$this->authorize('update', $user);
|
||||
|
||||
@@ -144,7 +163,7 @@ class UsersController extends Controller
|
||||
'first_name' => 'required|string|max:20',
|
||||
'last_name' => 'required|string|max:30',
|
||||
'email' => 'required|string|email|max:100|unique:users,email,'.$id,
|
||||
'emails' => 'max:100',
|
||||
//'emails' => 'max:100',
|
||||
'job_title' => 'max:100',
|
||||
'phone' => 'max:60',
|
||||
'timezone' => 'required|string|max:255',
|
||||
@@ -164,6 +183,7 @@ class UsersController extends Controller
|
||||
if ($path_url) {
|
||||
$user->photo_url = $path_url;
|
||||
} else {
|
||||
$invalid = true;
|
||||
$validator->errors()->add('photo_url', __('Error occured processing the image. Make sure that PHP GD extension is enabled.'));
|
||||
}
|
||||
}
|
||||
@@ -172,17 +192,28 @@ class UsersController extends Controller
|
||||
if ($user->isAdmin() && isset($request->role) && $request->role != User::ROLE_ADMIN) {
|
||||
$admins_count = User::where('role', User::ROLE_ADMIN)->count();
|
||||
if ($admins_count < 2) {
|
||||
$invalid = true;
|
||||
$validator->errors()->add('role', __('Role of the only one administrator can not be changed.'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($validator->fails()) {
|
||||
if (User::mailboxEmailExists($request->email)) {
|
||||
$invalid = true;
|
||||
$validator->errors()->add('email', __('There is a mailbox with such email. Users and mailboxes can not have the same email addresses.'));
|
||||
}
|
||||
|
||||
if ($invalid || $validator->fails()) {
|
||||
return redirect()->route('users.profile', ['id' => $id])
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
}
|
||||
|
||||
// Save language into session.
|
||||
if (auth()->user()->id == $id && $request->locale) {
|
||||
session()->put('user_locale', $request->locale);
|
||||
}
|
||||
|
||||
$request_data = $request->all();
|
||||
|
||||
if (isset($request_data['photo_url'])) {
|
||||
@@ -191,6 +222,13 @@ class UsersController extends Controller
|
||||
if (!auth()->user()->can('changeRole', $user)) {
|
||||
unset($request_data['role']);
|
||||
}
|
||||
if ($user->status != User::STATUS_DELETED) {
|
||||
if (!empty($request_data['disabled'])) {
|
||||
$request_data['status'] = User::STATUS_DISABLED;
|
||||
} else {
|
||||
$request_data['status'] = User::STATUS_ACTIVE;
|
||||
}
|
||||
}
|
||||
$user->fill($request_data);
|
||||
|
||||
if (empty($request->input('enable_kb_shortcuts'))) {
|
||||
@@ -221,8 +259,8 @@ class UsersController extends Controller
|
||||
$users = $this->getUsersForSidebar($id);
|
||||
|
||||
return view('users/permissions', [
|
||||
'user' => $user,
|
||||
'mailboxes' => $mailboxes,
|
||||
'user' => $user,
|
||||
'mailboxes' => $mailboxes,
|
||||
'user_mailboxes' => $user->mailboxes,
|
||||
'users' => $users,
|
||||
]);
|
||||
@@ -240,12 +278,27 @@ class UsersController extends Controller
|
||||
if (!$user->isAdmin()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
|
||||
$user = User::findOrFail($id);
|
||||
|
||||
$user->mailboxes()->sync($request->mailboxes);
|
||||
$user->syncPersonalFolders($request->mailboxes);
|
||||
|
||||
// Save permissions.
|
||||
$user_permissions = $request->user_permissions ?? [];
|
||||
$permissions = [];
|
||||
|
||||
foreach (User::getUserPermissionsList() as $permission_id) {
|
||||
$new_has_permission = in_array($permission_id, $user_permissions);
|
||||
|
||||
if ($user->hasPermission($permission_id, false) != $new_has_permission) {
|
||||
$permissions[$permission_id] = (int)(bool)$new_has_permission;
|
||||
$save_user = true;
|
||||
}
|
||||
}
|
||||
$user->permissions = $permissions;
|
||||
$user->save();
|
||||
|
||||
\Session::flash('flash_success_floating', __('Permissions saved successfully'));
|
||||
|
||||
return redirect()->route('users.permissions', ['id' => $id]);
|
||||
@@ -273,6 +326,7 @@ class UsersController extends Controller
|
||||
'subscriptions' => $subscriptions,
|
||||
'person' => $person,
|
||||
'users' => $users,
|
||||
'mobile_available' => \Eventy::filter('notifications.mobile_available', false),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -311,10 +365,10 @@ class UsersController extends Controller
|
||||
// Both send and resend
|
||||
case 'send_invite':
|
||||
if (!$auth_user->isAdmin()) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
if (empty($request->user_id)) {
|
||||
$response['msg'] = __('Incorrect user');
|
||||
$response['msg'] = __('Incorrect user');
|
||||
}
|
||||
if (!$response['msg']) {
|
||||
$user = User::find($request->user_id);
|
||||
@@ -340,10 +394,10 @@ class UsersController extends Controller
|
||||
// Reset password
|
||||
case 'reset_password':
|
||||
if (!auth()->user()->isAdmin()) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
if (empty($request->user_id)) {
|
||||
$response['msg'] = __('Incorrect user');
|
||||
$response['msg'] = __('Incorrect user');
|
||||
}
|
||||
if (!$response['msg']) {
|
||||
$user = User::find($request->user_id);
|
||||
@@ -368,7 +422,7 @@ class UsersController extends Controller
|
||||
// Load website notifications
|
||||
case 'web_notifications':
|
||||
if (!$auth_user) {
|
||||
$response['msg'] = __('You are not logged in');
|
||||
$response['msg'] = __('You are not logged in');
|
||||
}
|
||||
if (!$response['msg']) {
|
||||
$web_notifications_info = $auth_user->getWebsiteNotificationsInfo(false);
|
||||
@@ -376,8 +430,8 @@ class UsersController extends Controller
|
||||
'web_notifications_info_data' => $web_notifications_info['data'],
|
||||
])->render();
|
||||
|
||||
$response['has_more_pages'] = (int)$web_notifications_info['notifications']->hasMorePages();
|
||||
|
||||
$response['has_more_pages'] = (int) $web_notifications_info['notifications']->hasMorePages();
|
||||
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
break;
|
||||
@@ -385,7 +439,7 @@ class UsersController extends Controller
|
||||
// Mark all user website notifications as read
|
||||
case 'mark_notifications_as_read':
|
||||
if (!$auth_user) {
|
||||
$response['msg'] = __('You are not logged in');
|
||||
$response['msg'] = __('You are not logged in');
|
||||
}
|
||||
if (!$response['msg']) {
|
||||
$auth_user->unreadNotifications()->update(['read_at' => now()]);
|
||||
@@ -431,53 +485,85 @@ class UsersController extends Controller
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
|
||||
event(new UserDeleted($user, $auth_user));
|
||||
|
||||
// We have to process conversations one by one to set move them to Unassigned folder,
|
||||
// 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) {
|
||||
|
||||
$user->conversations->each(function ($conversation) use ($auth_user, $request) {
|
||||
// We don't fire ConversationUserChanged event to avoid sending notifications to users
|
||||
$conversation->user_id = null;
|
||||
|
||||
$folder_id = null;
|
||||
if (!empty($mailbox_unassigned_folders[$conversation->mailbox_id])) {
|
||||
$folder_id = $mailbox_unassigned_folders[$conversation->mailbox_id];
|
||||
if (!empty($request->assign_user) && !empty($request->assign_user[$conversation->mailbox_id]) && (int) $request->assign_user[$conversation->mailbox_id] != -1) {
|
||||
// Set assignee.
|
||||
$conversation->user_id = $request->assign_user[$conversation->mailbox_id];
|
||||
// In this case conversation stays assigned, just assignee changes.
|
||||
} else {
|
||||
$folder = $conversation->mailbox->folders()
|
||||
->where('type', Folder::TYPE_UNASSIGNED)
|
||||
->first();
|
||||
// Set assignee.
|
||||
$conversation->user_id = null;
|
||||
|
||||
if ($folder) {
|
||||
$folder_id = $folder->id;
|
||||
$mailbox_unassigned_folders[$conversation->mailbox_id] = $folder_id;
|
||||
// Change conversation folder to ANASSIGNED.
|
||||
$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;
|
||||
}
|
||||
}
|
||||
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();
|
||||
});
|
||||
}
|
||||
//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->delete();
|
||||
|
||||
\Session::flash('flash_success_floating', __('User deleted'));
|
||||
$user->status = \App\User::STATUS_DELETED;
|
||||
// Update email.
|
||||
$email_suffix = '_deleted'.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));
|
||||
|
||||
\Session::flash('flash_success_floating', __('User deleted').': '.$user->getFullName());
|
||||
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
@@ -533,7 +619,7 @@ class UsersController extends Controller
|
||||
// Check current password
|
||||
if (!Hash::check($request->password_current, $user->password)) {
|
||||
$validator->errors()->add('password_current', __('This password is incorrect.'));
|
||||
} else if (Hash::check($request->password, $user->password)) {
|
||||
} elseif (Hash::check($request->password, $user->password)) {
|
||||
// Check new password
|
||||
$validator->errors()->add('password', __('The new password is the same as the old password.'));
|
||||
}
|
||||
|
||||
@@ -33,17 +33,21 @@ class Kernel extends HttpKernel
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\App\Http\Middleware\TokenAuth::class,
|
||||
// \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\App\Http\Middleware\HttpsRedirect::class,
|
||||
\App\Http\Middleware\Localize::class,
|
||||
\App\Http\Middleware\LogoutIfDeleted::class,
|
||||
\App\Http\Middleware\CustomHandle::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'throttle:60,1',
|
||||
'bindings',
|
||||
],
|
||||
// 'api' => [
|
||||
// 'throttle:60,1',
|
||||
// 'bindings',
|
||||
// ],
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
24
app/Http/Middleware/CustomHandle.php
Normal file
24
app/Http/Middleware/CustomHandle.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
|
||||
class CustomHandle
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
// Hook.
|
||||
\Eventy::action('middleware.web.custom_handle', $request);
|
||||
|
||||
return \Eventy::filter('middleware.web.custom_handle.response', $next($request), $request, $next);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,6 @@ class EncryptCookies extends Middleware
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
'in_app'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -9,14 +9,32 @@ namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HttpsRedirect {
|
||||
|
||||
/**
|
||||
* The current proxy header mappings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $headers = [
|
||||
Request::HEADER_FORWARDED => 'FORWARDED',
|
||||
Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
|
||||
Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
|
||||
Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
|
||||
Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
|
||||
];
|
||||
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if (\Config::get('app.force_https') == 'true') {
|
||||
$request->setTrustedProxies( [ $request->getClientIp() ] );
|
||||
if (!$request->secure() /*&& App::environment() === 'production'*/) {
|
||||
$request->setTrustedProxies( [ $request->getClientIp() ], array_keys($this->headers));
|
||||
|
||||
if (!$request->secure() && strtolower($_SERVER['HTTPS'] ?? '') != 'on'
|
||||
&& ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') != 'https'
|
||||
&& ($_SERVER['HTTP_CF_VISITOR'] ?? '') != '{"scheme":"https"}'
|
||||
) {
|
||||
return redirect()->secure($request->getRequestUri());
|
||||
}
|
||||
}
|
||||
|
||||
29
app/Http/Middleware/Localize.php
Normal file
29
app/Http/Middleware/Localize.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
|
||||
class Localize
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
// Interface language is set automatically, as locale is stored in .env file.
|
||||
|
||||
// Set user language if user logged in.
|
||||
$user_locale = \Eventy::filter('locale', session('user_locale'));
|
||||
if ($user_locale) {
|
||||
\Helper::setLocale($user_locale);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
30
app/Http/Middleware/LogoutIfDeleted.php
Normal file
30
app/Http/Middleware/LogoutIfDeleted.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
|
||||
class LogoutIfDeleted
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @param string|null $guard
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next, $guard = null)
|
||||
{
|
||||
$user = Auth::user();
|
||||
if ($user && ($user->isDeleted() || $user->isDisabled())) {
|
||||
Auth::logout();
|
||||
return Redirect::route('login');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,9 @@ class ResponseHeaders
|
||||
|
||||
// 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');
|
||||
}
|
||||
$response->header('Pragma', 'no-cache');
|
||||
$response->header('Cache-Control', 'no-cache, max-age=0, must-revalidate, no-store');
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
33
app/Http/Middleware/TokenAuth.php
Normal file
33
app/Http/Middleware/TokenAuth.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\User;
|
||||
use Closure;
|
||||
|
||||
class TokenAuth
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if (!$request->user() && !empty($request->auth_token) && $request->cookie('in_app')) {
|
||||
try {
|
||||
$user = User::where(\DB::raw('md5(CONCAT(id, "'.config('app.key').'"))') , $request->auth_token)
|
||||
->first();
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e, '[TokenAuth]');
|
||||
}
|
||||
if (!empty($user)) {
|
||||
\Auth::login($user);
|
||||
}
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Job extends Model
|
||||
{
|
||||
const UPDATED_AT = null;
|
||||
|
||||
/**
|
||||
* Automatically converted into Carbon dates.
|
||||
*/
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
/**
|
||||
* Send alert to super admin.
|
||||
*/
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Mail\Alert;
|
||||
use App\SendLog;
|
||||
use App\User;
|
||||
use App\Mail\Alert;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
@@ -41,7 +42,7 @@ class SendAlert implements ShouldQueue
|
||||
{
|
||||
// Configure mail driver according to Mailbox settings
|
||||
\MailHelper::setSystemMailDriver();
|
||||
|
||||
|
||||
$recipients = [];
|
||||
|
||||
$super_admin = User::getSuperAdmin();
|
||||
|
||||
@@ -55,10 +55,15 @@ class SendAutoReply implements ShouldQueue
|
||||
$headers['Message-ID'] = $message_id;
|
||||
|
||||
$customer_email = $this->conversation->customer_email;
|
||||
|
||||
if (!$customer_email) {
|
||||
// When message is received via Chat, customer has no email adddress.
|
||||
return;
|
||||
}
|
||||
$recipients = [$customer_email];
|
||||
$failures = [];
|
||||
$exception = null;
|
||||
|
||||
$failures = [];
|
||||
$exception = null;
|
||||
|
||||
try {
|
||||
Mail::to([['name' => $this->customer->getFullName(), 'email' => $customer_email]])
|
||||
->send(new AutoReply($this->conversation, $this->mailbox, $this->customer, $headers));
|
||||
|
||||
@@ -7,7 +7,6 @@ namespace App\Jobs;
|
||||
|
||||
use App\Mail\UserEmailReplyError;
|
||||
use App\SendLog;
|
||||
use App\Thread;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
@@ -48,6 +47,7 @@ class SendEmailReplyError implements ShouldQueue
|
||||
\App\Misc\Mail::setMailDriver($this->mailbox);
|
||||
|
||||
$exception = null;
|
||||
|
||||
try {
|
||||
Mail::to([['name' => '', 'email' => $this->from]])
|
||||
->send(new UserEmailReplyError());
|
||||
|
||||
@@ -16,6 +16,9 @@ class SendNotificationToUsers implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
// Max retries + 1
|
||||
public $tries = 168; // One per hour
|
||||
|
||||
public $users;
|
||||
|
||||
public $conversation;
|
||||
@@ -48,17 +51,30 @@ class SendNotificationToUsers implements ShouldQueue
|
||||
|
||||
// Threads has to be sorted here, if sorted before, they come here in wrong order
|
||||
$this->threads = $this->threads->sortByDesc(function ($item, $key) {
|
||||
return $item->created_at;
|
||||
return $item->id;
|
||||
});
|
||||
|
||||
$headers = [];
|
||||
$last_thread = $this->threads->first();
|
||||
|
||||
if (!$last_thread) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If thread is draft, it means it has been undone
|
||||
if ($last_thread->isDraft()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Limit conversation history
|
||||
if (config('app.email_user_history') == 'last') {
|
||||
$this->threads = $this->threads->slice(0, 2);
|
||||
}
|
||||
|
||||
if (config('app.email_user_history') == 'none') {
|
||||
$this->threads = $this->threads->slice(0, 1);
|
||||
}
|
||||
|
||||
// All notification for the same conversation has same dummy Message-ID
|
||||
$prev_message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_NOTIFICATION_IN_REPLY.'-'.$this->conversation->id.'-'.md5($this->conversation->id).'@'.$mailbox->getEmailDomain();
|
||||
$headers['In-Reply-To'] = '<'.$prev_message_id.'>';
|
||||
@@ -68,22 +84,50 @@ class SendNotificationToUsers implements ShouldQueue
|
||||
$global_exception = null;
|
||||
|
||||
foreach ($this->users as $user) {
|
||||
|
||||
// User cam ne deleted.
|
||||
if (!isset($user->id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If for one user sending fails the job is marked as failed and retried after some time.
|
||||
// So we have to check if notification email has already been successfully sent to this user.
|
||||
if ($this->attempts() > 1) {
|
||||
// Maybe add indexes to the table.
|
||||
$already_sent = SendLog::where('thread_id', $last_thread->id)
|
||||
->where('mail_type', SendLog::MAIL_TYPE_USER_NOTIFICATION)
|
||||
->where('user_id', $user->id)
|
||||
->whereIn('status', SendLog::$sent_success)
|
||||
->exists();
|
||||
if ($already_sent) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_NOTIFICATION.'-'.$last_thread->id.'-'.$user->id.'-'.time().'@'.$mailbox->getEmailDomain();
|
||||
$headers['Message-ID'] = $message_id;
|
||||
|
||||
// If this is notification on message from customer set customer as sender name
|
||||
// If this is notification on message from customer, set customer as sender name
|
||||
$from_name = '';
|
||||
if ($last_thread->type == Thread::TYPE_CUSTOMER) {
|
||||
$from_name = $last_thread->customer->getFullName();
|
||||
$from_name = '';
|
||||
if ($last_thread->customer) {
|
||||
$from_name = $last_thread->customer->getFullName(true, true);
|
||||
}
|
||||
if ($from_name) {
|
||||
$from_name = $from_name.' '.__('via').' '.\Config::get('app.name');
|
||||
$from_name = $from_name.' '.__('via').' '.$mailbox->name;
|
||||
}
|
||||
}
|
||||
if (!$from_name) {
|
||||
$from_name = \Config::get('app.name');
|
||||
$from_name = $mailbox->name;
|
||||
}
|
||||
$from = ['address' => $mailbox->email, 'name' => $from_name];
|
||||
|
||||
// Set user language
|
||||
app()->setLocale($user->getLocale());
|
||||
|
||||
$headers['X-FreeScout-Mail-Type'] = 'user.notification';
|
||||
|
||||
$exception = null;
|
||||
|
||||
try {
|
||||
@@ -122,7 +166,23 @@ class SendNotificationToUsers implements ShouldQueue
|
||||
}
|
||||
|
||||
if ($global_exception) {
|
||||
throw $global_exception;
|
||||
// Retry job with delay.
|
||||
// https://stackoverflow.com/questions/35258175/how-can-i-create-delays-between-failed-queued-job-attempts-in-laravel
|
||||
if ($this->attempts() < $this->tries) {
|
||||
if ($this->attempts() == 1) {
|
||||
// Second attempt after 5 min.
|
||||
$this->release(300);
|
||||
} else {
|
||||
// Others - after 1 hour.
|
||||
$this->release(3600);
|
||||
}
|
||||
|
||||
throw $global_exception;
|
||||
} else {
|
||||
$this->fail($global_exception);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,15 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Mail;
|
||||
use Webklex\IMAP\Client;
|
||||
|
||||
class SendReplyToCustomer implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
// Number of retries + 1
|
||||
public $tries = 168; // one per hour
|
||||
|
||||
public $conversation;
|
||||
|
||||
public $threads;
|
||||
@@ -47,47 +51,131 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$send_previous_messages = false;
|
||||
$is_forward = false;
|
||||
|
||||
// When forwarding conversation is undone, new conversation is deleted.
|
||||
if (!$this->conversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mailbox = $this->conversation->mailbox;
|
||||
|
||||
// Mailbox may be deleted.
|
||||
if (!$mailbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add forwarded conversation replies.
|
||||
if ($this->conversation->threads_count == 1 && count($this->threads) == 1) {
|
||||
$forward_child_thread = $this->threads[0];
|
||||
if ($forward_child_thread->isForwarded() && $forward_child_thread->getForwardParentConversation()) {
|
||||
|
||||
// Add replies from original conversation.
|
||||
$forwarded_replies = $forward_child_thread->getForwardParentConversation()->getReplies();
|
||||
$forwarded_replies = $forwarded_replies->sortByDesc(function ($item, $key) {
|
||||
return $item->created_at;
|
||||
});
|
||||
$forward_parent_thread = Thread::find($forward_child_thread->getMeta('forward_parent_thread_id'));
|
||||
|
||||
if ($forward_parent_thread) {
|
||||
// Remove threads created after forwarding.
|
||||
foreach ($forwarded_replies as $i => $thread) {
|
||||
if ($thread->created_at > $forward_parent_thread->created_at) {
|
||||
$forwarded_replies->forget($i);
|
||||
}
|
||||
}
|
||||
$this->threads = $this->threads->merge($forwarded_replies);
|
||||
$is_forward = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Threads has to be sorted here, if sorted before, they come here in wrong order
|
||||
$this->threads = $this->threads->sortByDesc(function ($item, $key) {
|
||||
return $item->created_at;
|
||||
return $item->id;
|
||||
});
|
||||
|
||||
$new = false;
|
||||
$headers = [];
|
||||
$this->last_thread = $this->threads->first();
|
||||
$prev_thread = null;
|
||||
if ($this->last_thread === null) {
|
||||
return;
|
||||
}
|
||||
$last_customer_thread = null;
|
||||
|
||||
// If thread is draft, it means it has been undone
|
||||
if ($this->last_thread->isDraft()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure mail driver according to Mailbox settings
|
||||
\App\Misc\Mail::setMailDriver($mailbox, $this->last_thread->created_by_user);
|
||||
|
||||
if (count($this->threads) == 1) {
|
||||
$new = true;
|
||||
}
|
||||
$i = 0;
|
||||
foreach ($this->threads as $thread) {
|
||||
if ($i == 1) {
|
||||
$prev_thread = $thread;
|
||||
break;
|
||||
if (!$new) {
|
||||
$i = 0;
|
||||
foreach ($this->threads as $thread) {
|
||||
if ($i > 0 && $thread->type == Thread::TYPE_CUSTOMER) {
|
||||
$last_customer_thread = $thread;
|
||||
break;
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
|
||||
// Get penultimate email Message-Id if reply
|
||||
if (!$new && !empty($prev_thread) && $prev_thread->message_id) {
|
||||
$headers['In-Reply-To'] = '<'.$prev_thread->message_id.'>';
|
||||
$headers['References'] = '<'.$prev_thread->message_id.'>';
|
||||
// Conversation history.
|
||||
$email_conv_history = config('app.email_conv_history');
|
||||
|
||||
$meta_conv_history = $this->last_thread->getMeta(Thread::META_CONVERSATION_HISTORY);
|
||||
if (!empty($meta_conv_history)) {
|
||||
$email_conv_history = $meta_conv_history;
|
||||
}
|
||||
|
||||
if ($is_forward && $email_conv_history == 'global') {
|
||||
$email_conv_history = 'full';
|
||||
}
|
||||
|
||||
if ($is_forward && $email_conv_history == 'none') {
|
||||
$email_conv_history = 'full';
|
||||
}
|
||||
|
||||
if ($email_conv_history == 'full') {
|
||||
$send_previous_messages = true;
|
||||
}
|
||||
|
||||
if ($email_conv_history == 'last') {
|
||||
$send_previous_messages = true;
|
||||
$this->threads = $this->threads->slice(0, 2);
|
||||
}
|
||||
|
||||
if ($email_conv_history == 'none') {
|
||||
$send_previous_messages = false;
|
||||
}
|
||||
|
||||
if (!$is_forward) {
|
||||
$send_previous_messages = \Eventy::filter('jobs.send_reply_to_customer.send_previous_messages', $send_previous_messages, $this->last_thread, $this->threads, $this->conversation, $this->customer);
|
||||
}
|
||||
|
||||
// Remove previous messages.
|
||||
if (!$send_previous_messages) {
|
||||
$this->threads = $this->threads->slice(0, 1);
|
||||
}
|
||||
|
||||
// Configure mail driver according to Mailbox settings
|
||||
\App\Misc\Mail::setMailDriver($mailbox, $this->last_thread->created_by_user);
|
||||
|
||||
// Get penultimate email Message-Id if reply
|
||||
if (!$new && !empty($last_customer_thread) && $last_customer_thread->message_id) {
|
||||
|
||||
$headers['In-Reply-To'] = '<'.$last_customer_thread->message_id.'>';
|
||||
$headers['References'] = '<'.$last_customer_thread->message_id.'>';
|
||||
}
|
||||
|
||||
$this->message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_REPLY_TO_CUSTOMER.'-'.$this->last_thread->id.'-'.md5($this->last_thread->id).'@'.$mailbox->getEmailDomain();
|
||||
$headers['Message-ID'] = $this->message_id;
|
||||
|
||||
$this->customer_email = $this->conversation->customer_email;
|
||||
$to_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getToArray());
|
||||
$cc_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getCcArray());
|
||||
$bcc_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getBccArray());
|
||||
|
||||
@@ -95,35 +183,125 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
$cc_array = \App\Misc\Mail::removeEmailFromArray($cc_array, $this->customer_email);
|
||||
$bcc_array = \App\Misc\Mail::removeEmailFromArray($bcc_array, $this->customer_email);
|
||||
|
||||
// Auto Bcc.
|
||||
if ($mailbox->auto_bcc) {
|
||||
$auto_bcc = \MailHelper::sanitizeEmails($mailbox->auto_bcc);
|
||||
if ($auto_bcc) {
|
||||
$bcc_array = array_merge($bcc_array, $auto_bcc);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from BCC emails which are present in CC
|
||||
foreach ($cc_array as $cc_email) {
|
||||
$bcc_array = \App\Misc\Mail::removeEmailFromArray($bcc_array, $cc_email);
|
||||
}
|
||||
|
||||
$this->recipients = array_merge([$this->customer_email], $cc_array, $bcc_array);
|
||||
$this->recipients = array_merge($to_array, $cc_array, $bcc_array);
|
||||
|
||||
$to = [];
|
||||
if (count($to_array) > 1) {
|
||||
$to = $to_array;
|
||||
} else {
|
||||
$to = [['name' => $this->customer->getFullName(), 'email' => $this->customer_email]];
|
||||
}
|
||||
|
||||
// If sending fails, all recipiens fail.
|
||||
// if ($this->attempts() > 1) {
|
||||
// $cc_array = [];
|
||||
// $bcc_array = [];
|
||||
// }
|
||||
|
||||
$headers['X-FreeScout-Mail-Type'] = 'customer.message';
|
||||
|
||||
$reply_mail = new ReplyToCustomer($this->conversation, $this->threads, $headers, $mailbox);
|
||||
|
||||
try {
|
||||
Mail::to([['name' => $this->customer->getFullName(), 'email' => $this->customer_email]])
|
||||
Mail::to($to)
|
||||
->cc($cc_array)
|
||||
->bcc($bcc_array)
|
||||
->send(new ReplyToCustomer($this->conversation, $this->threads, $headers, $mailbox));
|
||||
->send($reply_mail);
|
||||
} catch (\Exception $e) {
|
||||
// We come here in case SMTP server unavailable for example
|
||||
activity()
|
||||
->causedBy($this->customer)
|
||||
->withProperties([
|
||||
'error' => $e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')',
|
||||
])
|
||||
->useLog(\App\ActivityLog::NAME_EMAILS_SENDING)
|
||||
->log(\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR_TO_CUSTOMER);
|
||||
if ($this->attempts() == 1) {
|
||||
activity()
|
||||
->causedBy($this->customer)
|
||||
->withProperties([
|
||||
'error' => $e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')',
|
||||
])
|
||||
->useLog(\App\ActivityLog::NAME_EMAILS_SENDING)
|
||||
->log(\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR_TO_CUSTOMER);
|
||||
}
|
||||
|
||||
// Failures will be saved to send log when retry attempts will finish
|
||||
// Mail::failures() is empty in case of connection error.
|
||||
$this->failures = $this->recipients;
|
||||
|
||||
// Save to send log
|
||||
$this->saveToSendLog($e->getMessage());
|
||||
// Save to send log (only first attempt).
|
||||
if ($this->attempts() == 1) {
|
||||
$this->saveToSendLog($e->getMessage());
|
||||
}
|
||||
|
||||
throw $e;
|
||||
$error_message = $e->getMessage();
|
||||
|
||||
// Retry job with delay.
|
||||
// https://stackoverflow.com/questions/35258175/how-can-i-create-delays-between-failed-queued-job-attempts-in-laravel
|
||||
if ($this->attempts() < $this->tries && !preg_match("/".config("app.no_retry_mail_errors")."/i", $error_message)) {
|
||||
if ($this->attempts() == 1) {
|
||||
// Second attempt after 5 min.
|
||||
$this->release(300);
|
||||
} else {
|
||||
// Others - after 1 hour.
|
||||
$this->release(3600);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
} else {
|
||||
$this->last_thread->send_status = SendLog::STATUS_SEND_ERROR;
|
||||
$this->last_thread->updateSendStatusData(['msg' => $error_message]);
|
||||
$this->last_thread->save();
|
||||
|
||||
// This executes $this->failed().
|
||||
$this->fail($e);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$imap_sent_folder = $mailbox->imap_sent_folder;
|
||||
if ($imap_sent_folder) {
|
||||
try {
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
$client->connect();
|
||||
|
||||
$envelope['from'] = $mailbox->getMailFrom()['address'];
|
||||
$envelope['to'] = $this->customer_email;
|
||||
$envelope['subject'] = 'Re: ' . $this->conversation->subject;
|
||||
|
||||
// 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: <'.$last_customer_thread->message_id.'>'
|
||||
];
|
||||
}
|
||||
|
||||
$part1['type'] = TYPETEXT;
|
||||
$part1['subtype'] = 'html';
|
||||
$part1['contents.data'] = $reply_mail->render();
|
||||
|
||||
try {
|
||||
$folder = $client->getFolder($imap_sent_folder);
|
||||
$folder->appendMessage(imap_mail_compose($envelope, [$part1]), '\Seen', now()->format('d-M-Y H:i:s O'));
|
||||
} 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.' - ');
|
||||
//$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.' - ');
|
||||
}
|
||||
}
|
||||
|
||||
// In message_id we are storing Message-ID of the incoming email which created the thread
|
||||
|
||||
44
app/Jobs/TriggerAction.php
Normal file
44
app/Jobs/TriggerAction.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* Used to tirgger Eventy actions with delay.
|
||||
*/
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class TriggerAction implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $action;
|
||||
public $params;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($action, $params)
|
||||
{
|
||||
$this->action = $action;
|
||||
$this->params = $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$args = $this->params;
|
||||
array_unshift($args, $this->action);
|
||||
|
||||
call_user_func_array("\Eventy::action", $args);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class LogUserDeletion
|
||||
public function handle(UserDeleted $event)
|
||||
{
|
||||
activity()
|
||||
->causedBy($event->user)
|
||||
->causedBy($event->by_user)
|
||||
->withProperties(['deleted_user' => $event->deleted_user->getFullName().' ['.$event->deleted_user->id.']'])
|
||||
->useLog(\App\ActivityLog::NAME_USER)
|
||||
->log(\App\ActivityLog::DESCRIPTION_USER_DELETED);
|
||||
|
||||
25
app/Listeners/ProcessSwiftMessage.php
Normal file
25
app/Listeners/ProcessSwiftMessage.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
class ProcessSwiftMessage
|
||||
{
|
||||
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*/
|
||||
public function handle($event)
|
||||
{
|
||||
return \Eventy::filter('mail.process_swift_message', true, $event->message);
|
||||
}
|
||||
}
|
||||
30
app/Listeners/RefreshConversations.php
Normal file
30
app/Listeners/RefreshConversations.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Conversation;
|
||||
|
||||
class RefreshConversations
|
||||
{
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param $event
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle($event)
|
||||
{
|
||||
Conversation::refreshConversations($event->conversation, $event->thread ?? $event->last_thread);
|
||||
}
|
||||
}
|
||||
31
app/Listeners/RememberUserLocale.php
Normal file
31
app/Listeners/RememberUserLocale.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use Illuminate\Auth\Events\Login;
|
||||
|
||||
class RememberUserLocale
|
||||
{
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param Login $event
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(Login $event)
|
||||
{
|
||||
// Save user locale to session to show user app in his chosen language.
|
||||
session()->put('user_locale', $event->user->getLocale());
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Conversation;
|
||||
use App\Mailbox;
|
||||
use App\SendLog;
|
||||
|
||||
class SendAutoReply
|
||||
{
|
||||
const CHECK_PERIOD = 180; // min
|
||||
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
@@ -23,7 +27,63 @@ class SendAutoReply
|
||||
{
|
||||
$conversation = $event->conversation;
|
||||
|
||||
if (/*!$conversation->imported &&*/ $conversation->mailbox->auto_reply_enabled) {
|
||||
if (!$conversation->imported && $conversation->mailbox->auto_reply_enabled) {
|
||||
$thread = $conversation->threads()->first();
|
||||
|
||||
// Do not send auto reply to auto responders.
|
||||
if ($thread->isAutoResponder()) {
|
||||
return;
|
||||
}
|
||||
// Do not send auto replies to bounces.
|
||||
if ($thread->isBounce()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not send auto replies to spam messages.
|
||||
if ($conversation->status == Conversation::STATUS_SPAM) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!\Eventy::filter('autoreply.should_send', true, $conversation)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We can not send auto reply to incoming bounce messages, as it will lead to the infinite loop:
|
||||
// application will be sending auto replies and mail server will be sending bounce messages to auto replies.
|
||||
// Bounce detection can not be 100% reliable.
|
||||
// So to prevent infinite loop, we are checking number of auto replies sent to the customer recently.
|
||||
$created_at = \Illuminate\Support\Carbon::now()->subMinutes(self::CHECK_PERIOD);
|
||||
|
||||
$auto_replies_sent = SendLog::where('customer_id', $conversation->customer_id)
|
||||
->where('mail_type', SendLog::MAIL_TYPE_AUTO_REPLY)
|
||||
->where('created_at', '>', $created_at)
|
||||
->count();
|
||||
|
||||
if ($auto_replies_sent >= 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($auto_replies_sent >= 2) {
|
||||
// Find conversations from this customer
|
||||
$prev_conversations = Conversation::select('subject', 'id')
|
||||
->where('customer_id', $conversation->customer_id)
|
||||
->where('created_at', '>', $created_at)
|
||||
->get();
|
||||
|
||||
foreach ($prev_conversations as $prev_conv) {
|
||||
if ($prev_conv->subject == $conversation->subject && $prev_conv->id != $conversation->id) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do not send autoreplies to own mailboxes.
|
||||
if ($conversation->customer_email) {
|
||||
$is_internal_email = Mailbox::where('email', $conversation->customer_email)->exists();
|
||||
if ($is_internal_email) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 24h limit has been disabled: https://github.com/freescout-helpdesk/freescout/pull/95
|
||||
// Send auto reply once in 24h
|
||||
@@ -37,7 +97,7 @@ class SendAutoReply
|
||||
return;
|
||||
}*/
|
||||
|
||||
\App\Jobs\SendAutoReply::dispatch($conversation, $conversation->threads()->first(), $conversation->mailbox, $conversation->customer)
|
||||
\App\Jobs\SendAutoReply::dispatch($conversation, $thread, $conversation->mailbox, $conversation->customer)
|
||||
->onQueue('emails');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Send notifications to users by email and in browser.
|
||||
*/
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Conversation;
|
||||
use App\Subscription;
|
||||
|
||||
class SendNotificationToUsers
|
||||
@@ -32,14 +35,21 @@ class SendNotificationToUsers
|
||||
break;
|
||||
case 'App\Events\UserAddedNote':
|
||||
$caused_by_user_id = $event->thread->created_by_user_id;
|
||||
$event_type = Subscription::EVENT_TYPE_USER_ADDED_NOTE;
|
||||
// When conversation is forwarded only notification
|
||||
// about child forward conversation is sent.
|
||||
if (!$event->thread->isForward()) {
|
||||
$event_type = Subscription::EVENT_TYPE_USER_ADDED_NOTE;
|
||||
}
|
||||
break;
|
||||
case 'App\Events\UserCreatedConversation':
|
||||
$caused_by_user_id = $event->conversation->created_by_user_id;
|
||||
$event_type = Subscription::EVENT_TYPE_NEW;
|
||||
break;
|
||||
case 'App\Events\CustomerCreatedConversation':
|
||||
$event_type = Subscription::EVENT_TYPE_NEW;
|
||||
// Do not send notification if conversation is spam.
|
||||
if ($event->conversation->status != Conversation::STATUS_SPAM) {
|
||||
$event_type = Subscription::EVENT_TYPE_NEW;
|
||||
}
|
||||
break;
|
||||
case 'App\Events\ConversationUserChanged':
|
||||
$caused_by_user_id = $event->user->id;
|
||||
@@ -52,12 +62,13 @@ class SendNotificationToUsers
|
||||
if (empty($event->conversation) || !$event_type) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore imported threads.
|
||||
if (!empty($event->thread) && $event->thread->imported) {
|
||||
return;
|
||||
}
|
||||
$conversation = $event->conversation;
|
||||
|
||||
// We can not check imported here, as after conversation has been imported via API
|
||||
// notifications has to be sent.
|
||||
//if (!$conversation->imported) {
|
||||
|
||||
// Using the last argument you can make event to be processed immediately
|
||||
Subscription::registerEvent($event_type, $conversation, $caused_by_user_id/*, true*/);
|
||||
}
|
||||
|
||||
@@ -23,11 +23,31 @@ class SendReplyToCustomer
|
||||
{
|
||||
$conversation = $event->conversation;
|
||||
|
||||
// Do not send email if this is a Phone conversation.
|
||||
if ($conversation->isPhone()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$replies = $conversation->getReplies();
|
||||
|
||||
// Ignore imported messages.
|
||||
if ($replies && $replies->first() && $replies->first()->imported) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat conversation.
|
||||
if ($conversation->isChat()) {
|
||||
\Helper::backgroundAction('chat_conversation.send_reply', [$conversation, $replies, $conversation->customer], now()->addSeconds(Conversation::UNDO_TIMOUT));
|
||||
return;
|
||||
}
|
||||
|
||||
// We can not check imported here, as after conversation has been imported via API
|
||||
// notifications has to be sent.
|
||||
//if (!$conversation->imported) {
|
||||
\App\Jobs\SendReplyToCustomer::dispatch($conversation, $conversation->getReplies(), $conversation->customer)
|
||||
->delay(now()->addSeconds(Conversation::UNDO_TIMOUT))
|
||||
$delay = \Eventy::filter('conversation.send_reply_to_customer_delay', now()->addSeconds(Conversation::UNDO_TIMOUT), $conversation, $replies);
|
||||
|
||||
\App\Jobs\SendReplyToCustomer::dispatch($conversation, $replies, $conversation->customer)
|
||||
->delay($delay)
|
||||
->onQueue('emails');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Mail\Mailer as MailerContract;
|
||||
use Illuminate\Mail\Mailable;
|
||||
|
||||
class Alert extends Mailable
|
||||
@@ -20,7 +18,6 @@ class Alert extends Mailable
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
*/
|
||||
public function __construct($text, $title = '')
|
||||
{
|
||||
|
||||
@@ -2,41 +2,32 @@
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Mail\Mailer as MailerContract;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class AutoReply extends Mailable
|
||||
{
|
||||
/**
|
||||
* Conversation created by customer.
|
||||
*
|
||||
*/
|
||||
public $conversation;
|
||||
|
||||
/**
|
||||
* Mailbox.
|
||||
*
|
||||
*/
|
||||
public $mailbox;
|
||||
|
||||
/**
|
||||
* Customer.
|
||||
*
|
||||
*/
|
||||
public $customer;
|
||||
|
||||
/**
|
||||
* Custom headers.
|
||||
*
|
||||
*/
|
||||
public $headers = [];
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
*/
|
||||
public function __construct($conversation, $mailbox, $customer, $headers)
|
||||
{
|
||||
@@ -65,8 +56,10 @@ class AutoReply extends Mailable
|
||||
];
|
||||
|
||||
// Set variables
|
||||
$subject = \App\Misc\Mail::replaceMailVars($this->mailbox->auto_reply_subject, $data);
|
||||
$view_params['auto_reply_message'] = \App\Misc\Mail::replaceMailVars($this->mailbox->auto_reply_message, $data);
|
||||
$subject = \MailHelper::replaceMailVars($this->mailbox->auto_reply_subject, $data);
|
||||
$view_params['auto_reply_message'] = \MailHelper::replaceMailVars($this->mailbox->auto_reply_message, $data);
|
||||
|
||||
$subject = \Eventy::filter('email.auto_reply.subject', $subject, $this->conversation);
|
||||
|
||||
$message = $this->subject($subject)
|
||||
->view('emails/customer/auto_reply', $view_params)
|
||||
|
||||
@@ -2,12 +2,7 @@
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Option;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Mail\Mailer as MailerContract;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class PasswordChanged extends Mailable
|
||||
{
|
||||
@@ -18,7 +13,6 @@ class PasswordChanged extends Mailable
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
*/
|
||||
public function __construct($user)
|
||||
{
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Mail\Mailer as MailerContract;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
// https://medium.com/@guysmilez/queuing-mailables-with-custom-headers-in-laravel-5-4-ab615f022f17
|
||||
//abstract class AbstractMessage extends Mailable
|
||||
@@ -83,6 +81,7 @@ class ReplyToCustomer extends Mailable
|
||||
if (count($this->threads) > 1) {
|
||||
$subject = 'Re: '.$subject;
|
||||
}
|
||||
$subject = \Eventy::filter('email.reply_to_customer.subject', $subject, $this->conversation);
|
||||
|
||||
// from($this->from) Sets only email, name stays empty.
|
||||
// So we set from in Mail::setMailDriver
|
||||
@@ -92,7 +91,7 @@ class ReplyToCustomer extends Mailable
|
||||
|
||||
if ($this->threads->first()->has_attachments) {
|
||||
foreach ($this->threads->first()->attachments as $attachment) {
|
||||
$message->attach($attachment->getLocalFilePath());
|
||||
$message->attach($attachment->getLocalFilePath());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Mail\Mailer as MailerContract;
|
||||
use Illuminate\Mail\Mailable;
|
||||
|
||||
class Test extends Mailable
|
||||
@@ -12,7 +10,6 @@ class Test extends Mailable
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
*/
|
||||
public function __construct($mailbox = null)
|
||||
{
|
||||
@@ -26,6 +23,13 @@ class Test extends Mailable
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
$this->withSwiftMessage(function ($swiftmessage) {
|
||||
$headers = $swiftmessage->getHeaders();
|
||||
$headers->addTextHeader('X-FreeScout-Mail-Type', 'test.mailbox');
|
||||
|
||||
return $swiftmessage;
|
||||
});
|
||||
|
||||
$message = $this->subject(__(':app_name Test Email', ['app_name' => \Config::get('app.name')]));
|
||||
if ($this->mailbox) {
|
||||
$message->view('emails/user/test', ['mailbox' => $this->mailbox]);
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
/**
|
||||
* User replied from wrong email address to the email notification.
|
||||
*/
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class UserEmailReplyError extends Mailable
|
||||
{
|
||||
@@ -17,7 +16,6 @@ class UserEmailReplyError extends Mailable
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,11 +3,7 @@
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Option;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Mail\Mailer as MailerContract;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class UserInvite extends Mailable
|
||||
{
|
||||
@@ -18,7 +14,6 @@ class UserInvite extends Mailable
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
*/
|
||||
public function __construct($user)
|
||||
{
|
||||
@@ -32,7 +27,7 @@ class UserInvite extends Mailable
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
$message = $this->subject(__('Welcome to :company_name', ['company_name' => Option::get('company_name')]))
|
||||
$message = $this->subject(__('Welcome to :company_name!', ['company_name' => Option::getCompanyName()]))
|
||||
->view('emails/user/user_invite')
|
||||
->text('emails/user/user_invite_text');
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class UserNotification extends Mailable
|
||||
{
|
||||
@@ -45,7 +43,7 @@ class UserNotification extends Mailable
|
||||
|
||||
/**
|
||||
* Mailbox.
|
||||
*
|
||||
*
|
||||
* @var [type]
|
||||
*/
|
||||
public $mailbox;
|
||||
|
||||
468
app/Mailbox.php
468
app/Mailbox.php
@@ -2,11 +2,18 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Email;
|
||||
use App\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Watson\Rememberable\Rememberable;
|
||||
|
||||
class Mailbox extends Model
|
||||
{
|
||||
use Rememberable;
|
||||
// This is obligatory.
|
||||
public $rememberCacheDriver = 'array';
|
||||
|
||||
/**
|
||||
* From Name: name that will appear in the From field when a customer views your email.
|
||||
*/
|
||||
@@ -86,11 +93,33 @@ class Mailbox extends Model
|
||||
const RATINGS_PLACEMENT_ABOVE = 1;
|
||||
const RATINGS_PLACEMENT_BELOW = 2;
|
||||
|
||||
/**
|
||||
* Access permissions.
|
||||
*/
|
||||
const ACCESS_PERM_EDIT = 'edit';
|
||||
const ACCESS_PERM_PERMISSIONS = 'perm';
|
||||
const ACCESS_PERM_AUTO_REPLIES = 'auto';
|
||||
const ACCESS_PERM_SIGNATURE = 'sig';
|
||||
|
||||
public static $access_permissions = [
|
||||
self::ACCESS_PERM_EDIT,
|
||||
self::ACCESS_PERM_PERMISSIONS,
|
||||
self::ACCESS_PERM_AUTO_REPLIES,
|
||||
self::ACCESS_PERM_SIGNATURE,
|
||||
];
|
||||
|
||||
public static $access_routes = [
|
||||
self::ACCESS_PERM_EDIT => 'mailboxes.update',
|
||||
self::ACCESS_PERM_PERMISSIONS => 'mailboxes.permissions',
|
||||
self::ACCESS_PERM_AUTO_REPLIES => 'mailboxes.auto_reply',
|
||||
self::ACCESS_PERM_SIGNATURE => 'mailboxes.update',
|
||||
];
|
||||
|
||||
/**
|
||||
* Default signature set when mailbox created.
|
||||
*/
|
||||
const DEFAULT_SIGNATURE = '<br><span style="color:#808080;">--<br>
|
||||
{%mailbox.name%}<br></span>';
|
||||
{%mailbox.name%}</span>';
|
||||
|
||||
/**
|
||||
* Default values.
|
||||
@@ -99,20 +128,82 @@ class Mailbox extends Model
|
||||
'signature' => self::DEFAULT_SIGNATURE,
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'meta' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Attributes fillable using fill() method.
|
||||
*
|
||||
* @var [type]
|
||||
*/
|
||||
protected $fillable = ['name', 'slug', 'email', 'aliases', 'from_name', 'from_name_custom', 'ticket_status', 'ticket_assignee', 'template', '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', 'auto_reply_enabled', 'auto_reply_subject', 'auto_reply_message', 'office_hours_enabled', 'ratings', 'ratings_placement', 'ratings_text'];
|
||||
protected $fillable = ['name', 'email', 'aliases', '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 static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
self::created(function (Mailbox $model) {
|
||||
$model->slug = strtolower(substr(md5(Hash::make($model->id)), 0, 16));
|
||||
});
|
||||
// self::created(function (Mailbox $model) {
|
||||
// $model->slug = strtolower(substr(md5(Hash::make($model->id)), 0, 16));
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically encrypt password on save.
|
||||
*/
|
||||
public function setInPasswordAttribute($value)
|
||||
{
|
||||
if ($value != '') {
|
||||
$this->attributes['in_password'] = encrypt($value);
|
||||
} else {
|
||||
$this->attributes['in_password'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically decrypt password on read.
|
||||
*/
|
||||
public function getInPasswordAttribute($value)
|
||||
{
|
||||
if (!$value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return decrypt($value);
|
||||
} catch (\Exception $e) {
|
||||
// do nothing if decrypt wasn't succefull
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically encrypt password on save.
|
||||
*/
|
||||
public function setOutPasswordAttribute($value)
|
||||
{
|
||||
if ($value != '') {
|
||||
$this->attributes['out_password'] = encrypt($value);
|
||||
} else {
|
||||
$this->attributes['out_password'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically decrypt password on read.
|
||||
*/
|
||||
public function getOutPasswordAttribute($value)
|
||||
{
|
||||
if (!$value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return decrypt($value);
|
||||
} catch (\Exception $e) {
|
||||
// do nothing if decrypt wasn't succefull
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,7 +211,24 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany('App\User')->as('settings')->withPivot('after_send');
|
||||
return $this->belongsToMany('App\User');
|
||||
}
|
||||
|
||||
public function usersWithSettings()
|
||||
{
|
||||
return $this->belongsToMany('App\User')->as('settings')
|
||||
->withPivot('after_send')
|
||||
->withPivot('hide')
|
||||
->withPivot('mute')
|
||||
->withPivot('access');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users having access to the mailbox.
|
||||
*/
|
||||
public function users_cached()
|
||||
{
|
||||
return $this->users()->rememberForever();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,11 +284,11 @@ class Mailbox extends Model
|
||||
continue;
|
||||
}
|
||||
foreach ($folder_types as $type) {
|
||||
$folder = new Folder();
|
||||
$folder->mailbox_id = $mailbox_id;
|
||||
$folder->user_id = $user_id;
|
||||
$folder->type = $type;
|
||||
$folder->save();
|
||||
Folder::create([
|
||||
'mailbox_id' => $mailbox_id,
|
||||
'user_id' => $user_id,
|
||||
'type' => $type,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,26 +325,45 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function getMainFolders()
|
||||
{
|
||||
$main_folders = \Eventy::filter('mailbox.main_folders', [], $this);
|
||||
if ($main_folders) {
|
||||
return $main_folders;
|
||||
}
|
||||
|
||||
return $this->folders()
|
||||
->where(function ($query) {
|
||||
$query->whereIn('type', [Folder::TYPE_UNASSIGNED, Folder::TYPE_ASSIGNED, Folder::TYPE_DRAFTS])
|
||||
->orWhere(function ($query2) {
|
||||
$query2->where(['type' => Folder::TYPE_MINE]);
|
||||
$query2->where(['user_id' => auth()->user()->id]);
|
||||
})
|
||||
->orWhere(function ($query3) {
|
||||
$query3->where(['type' => Folder::TYPE_STARRED]);
|
||||
$query3->where(['user_id' => auth()->user()->id]);
|
||||
});
|
||||
})
|
||||
->orderBy('type')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get folder by it's type.
|
||||
*/
|
||||
public function getFolderByType($type)
|
||||
{
|
||||
return $this->folders()
|
||||
->where('type', $type)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get folders available for the current user.
|
||||
*/
|
||||
public function getAssesibleFolders()
|
||||
{
|
||||
return $this->folders()
|
||||
$folders = $this->folders()
|
||||
->where(function ($query) {
|
||||
$query->whereIn('type', Folder::$public_types)
|
||||
$query->whereIn('type', \Eventy::filter('mailbox.folders.public_types', Folder::$public_types))
|
||||
->orWhere(function ($query2) {
|
||||
$query2->whereIn('type', Folder::$personal_types);
|
||||
$query2->where(['user_id' => auth()->user()->id]);
|
||||
@@ -244,6 +371,8 @@ class Mailbox extends Model
|
||||
})
|
||||
->orderBy('type')
|
||||
->get();
|
||||
|
||||
return \Eventy::filter('mailbox.folders', $folders, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,6 +408,12 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function isInActive()
|
||||
{
|
||||
$in_active = \Eventy::filter('mailbox.in_active', null, $this);
|
||||
|
||||
if (is_bool($in_active)) {
|
||||
return $in_active;
|
||||
}
|
||||
|
||||
if ($this->in_protocol && $this->in_server && $this->in_port && $this->in_username && $this->in_password) {
|
||||
return true;
|
||||
} else {
|
||||
@@ -294,7 +429,7 @@ class Mailbox extends Model
|
||||
public function isOutActive()
|
||||
{
|
||||
if ($this->out_method != self::OUT_METHOD_PHP_MAIL && $this->out_method != self::OUT_METHOD_SENDMAIL
|
||||
&& (!$this->out_server || !$this->out_username || !$this->out_password)
|
||||
&& (!$this->out_server /*|| !$this->out_username || !$this->out_password*/)
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
@@ -305,12 +440,59 @@ class Mailbox extends Model
|
||||
/**
|
||||
* Get users who have access to the mailbox.
|
||||
*/
|
||||
public function usersHavingAccess($cache = false)
|
||||
public function usersHavingAccess($cache = false, $fields = 'users.*', $sort = true)
|
||||
{
|
||||
$users = $this->users;
|
||||
$admins = User::where('role', User::ROLE_ADMIN)->remember(\App\Misc\Helper::cacheTime($cache))->get();
|
||||
$admins = User::where('role', User::ROLE_ADMIN)->select($fields)->remember(\Helper::cacheTime($cache))->get();
|
||||
|
||||
return $users->merge($admins)->unique();
|
||||
$users = $this->users()->select($fields)->remember(\Helper::cacheTime($cache))->get()->merge($admins)->unique();
|
||||
|
||||
// Exclude deleted users (better to do it in PHP).
|
||||
foreach ($users as $i => $user) {
|
||||
if (!$user->isActive()) {
|
||||
$users->forget($i);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by full name
|
||||
if ($sort) {
|
||||
$users = User::sortUsers($users);
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
public function usersAssignable($cache = true)
|
||||
{
|
||||
// Exclude hidden admins.
|
||||
$mailbox_id = $this->id;
|
||||
$admins = User::select(['users.*', 'mailbox_user.hide'])
|
||||
->leftJoin('mailbox_user', function ($join) use ($mailbox_id) {
|
||||
$join->on('mailbox_user.user_id', '=', 'users.id');
|
||||
$join->where('mailbox_user.mailbox_id', $mailbox_id);
|
||||
})
|
||||
->where('role', User::ROLE_ADMIN)
|
||||
->remember(\Helper::cacheTime($cache))
|
||||
->get();
|
||||
|
||||
$users = $this->users()->select('users.*')->remember(\Helper::cacheTime($cache))->get()->merge($admins)->unique();
|
||||
|
||||
foreach ($users as $i => $user) {
|
||||
if (!empty($user->hide)) {
|
||||
$users->forget($i);
|
||||
}
|
||||
}
|
||||
|
||||
// Exclude deleted users (better to do it in PHP).
|
||||
foreach ($users as $i => $user) {
|
||||
if (!$user->isActive()) {
|
||||
$users->forget($i);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by full name
|
||||
$users = User::sortUsers($users);
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -318,10 +500,12 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function userIdsHavingAccess()
|
||||
{
|
||||
$user_ids = $this->users()->pluck('users.id');
|
||||
return $this->usersHavingAccess(false, ['users.id', 'users.status'])->pluck('id')->toArray();
|
||||
|
||||
/*$user_ids = $this->users()->pluck('users.id');
|
||||
$admin_ids = User::where('role', User::ROLE_ADMIN)->pluck('id');
|
||||
|
||||
return $user_ids->merge($admin_ids)->unique()->toArray();
|
||||
return $user_ids->merge($admin_ids)->unique()->toArray();*/
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -329,9 +513,15 @@ class Mailbox extends Model
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function userHasAccess($user_id)
|
||||
public function userHasAccess($user_id, $user = null)
|
||||
{
|
||||
$user = User::find($user_id);
|
||||
if (!$user) {
|
||||
if ($user_id instanceof \App\User) {
|
||||
$user = $user_id;
|
||||
} else {
|
||||
$user = User::find($user_id);
|
||||
}
|
||||
}
|
||||
if ($user && $user->isAdmin()) {
|
||||
return true;
|
||||
} else {
|
||||
@@ -417,7 +607,32 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function getInProtocolName()
|
||||
{
|
||||
return self::$in_protocols[$this->in_protocol];
|
||||
return $this->getInProtocols()[$this->in_protocol] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get incoming protocol display name for the UI.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getInProtocolDisplayNames()
|
||||
{
|
||||
$display_names = self::$in_protocols;
|
||||
|
||||
$display_names[self::IN_PROTOCOL_IMAP] = 'IMAP';
|
||||
$display_names[self::IN_PROTOCOL_POP3] = 'POP3';
|
||||
|
||||
return \Eventy::filter('mailbox.in_protocols.display_names', $display_names);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available incoming protocols.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getInProtocols()
|
||||
{
|
||||
return \Eventy::filter('mailbox.in_protocols', self::$in_protocols);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -425,22 +640,43 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function getUserSettings($user_id)
|
||||
{
|
||||
$mailbox_user = $this->users()->where('users.id', $user_id)->first();
|
||||
$mailbox_user = $this->usersWithSettings()->where('users.id', $user_id)->first();
|
||||
if ($mailbox_user) {
|
||||
return $mailbox_user->settings;
|
||||
} else {
|
||||
// Admin may have no record in mailbox_user table
|
||||
// Create dummy object with default parameters
|
||||
$settings = new \StdClass();
|
||||
$settings->after_send = MailboxUser::AFTER_SEND_NEXT;
|
||||
|
||||
return $settings;
|
||||
return self::getDummySettings();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create dummy object with default parameters
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public static function getDummySettings()
|
||||
{
|
||||
$settings = new \StdClass();
|
||||
$settings->after_send = MailboxUser::AFTER_SEND_NEXT;
|
||||
$settings->hide = false;
|
||||
$settings->mute = false;
|
||||
$settings->access = [];
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public function fetchUserSettings($user_id)
|
||||
{
|
||||
$settings = $this->getUserSettings($user_id);
|
||||
|
||||
$this->after_send = $settings->after_send;
|
||||
$this->hide = $settings->hide;
|
||||
$this->mute = $settings->mute;
|
||||
$this->access = $settings->access;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get main email and aliases.
|
||||
*
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getEmails()
|
||||
@@ -456,13 +692,14 @@ class Mailbox extends Model
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $emails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove mailbox email and aliases from the list of emails.
|
||||
*
|
||||
* @param array $list
|
||||
* @param array $list
|
||||
* @param Mailbox $mailbox
|
||||
*
|
||||
* @return array
|
||||
@@ -481,4 +718,173 @@ class Mailbox extends Model
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active mailboxes.
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public static function getActiveMailboxes()
|
||||
{
|
||||
$active = [];
|
||||
|
||||
// It is more effective to retrive all mailboxes and filter them in PHP.
|
||||
$mailboxes = self::all();
|
||||
foreach ($mailboxes as $mailbox) {
|
||||
if ($mailbox->isActive()) {
|
||||
$active[] = $mailbox;
|
||||
}
|
||||
}
|
||||
|
||||
return $active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mailbox URL.
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function url()
|
||||
{
|
||||
return \Eventy::filter('mailbox.url', route('mailboxes.view', ['id' => $this->id]), $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the model with an array of attributes.
|
||||
*
|
||||
* @param array $attributes [description]
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function fill(array $attributes)
|
||||
{
|
||||
$this->fillable(array_merge($this->getFillable(), \Eventy::filter('mailbox.fillable_fields', [])));
|
||||
|
||||
return parent::fill($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set phones as JSON.
|
||||
*
|
||||
* @param array $phones_array
|
||||
*/
|
||||
public function setInImapFolders(array $in_imap_folders)
|
||||
{
|
||||
$this->in_imap_folders = json_encode($in_imap_folders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of imap folders.
|
||||
*/
|
||||
public function getInImapFolders()
|
||||
{
|
||||
$in_imap_folders = \Helper::jsonToArray($this->in_imap_folders);
|
||||
if (count($in_imap_folders)) {
|
||||
return $in_imap_folders;
|
||||
} else {
|
||||
return ["INBOX"];
|
||||
}
|
||||
}
|
||||
|
||||
public function outPasswordSafe()
|
||||
{
|
||||
return \Helper::safePassword($this->out_password);
|
||||
}
|
||||
|
||||
public function inPasswordSafe()
|
||||
{
|
||||
return \Helper::safePassword($this->in_password);
|
||||
}
|
||||
|
||||
public function getReplySeparator()
|
||||
{
|
||||
return $this->before_reply ?: \MailHelper::REPLY_SEPARATOR_TEXT;
|
||||
}
|
||||
|
||||
public static function findOrFailWithSettings($id, $user_id)
|
||||
{
|
||||
return Mailbox::select(['mailboxes.*', 'mailbox_user.hide', 'mailbox_user.mute', 'mailbox_user.access'])
|
||||
->where('mailboxes.id', $id)
|
||||
->leftJoin('mailbox_user', function ($join) use ($user_id) {
|
||||
$join->on('mailbox_user.mailbox_id', '=', 'mailboxes.id');
|
||||
$join->where('mailbox_user.user_id', $user_id);
|
||||
})->firstOrFail();
|
||||
}
|
||||
|
||||
/*public static function getUserSettings($mailbox_id, $user_id)
|
||||
{
|
||||
return MailboxUser::where('mailbox_id', $mailbox_id)
|
||||
->where('user_id', $user_id)
|
||||
->first();
|
||||
}*/
|
||||
|
||||
public static function getAccessPermissionName($perm)
|
||||
{
|
||||
$access_permissions = [
|
||||
self::ACCESS_PERM_EDIT => __('Edit Mailbox'),
|
||||
self::ACCESS_PERM_PERMISSIONS => __('Permissions'),
|
||||
self::ACCESS_PERM_AUTO_REPLIES => __('Auto Replies'),
|
||||
self::ACCESS_PERM_SIGNATURE => __('Email Signature'),
|
||||
];
|
||||
$access_permissions = \Eventy::filter('mailbox.access_permissions_list', $access_permissions);
|
||||
|
||||
return $access_permissions[$perm] ?? '';
|
||||
}
|
||||
|
||||
public static function getAccessPermissionRoute($perm)
|
||||
{
|
||||
$route = self::$access_routes[$perm] ?? '';
|
||||
$route = \Eventy::filter('mailbox.access_permissions_route', $route, $perm);
|
||||
|
||||
return $route;
|
||||
}
|
||||
|
||||
public function setMetaParam($param, $value, $save = false)
|
||||
{
|
||||
$meta = $this->meta;
|
||||
$meta[$param] = $value;
|
||||
$this->meta = $meta;
|
||||
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function removeMetaParam($param, $save = false)
|
||||
{
|
||||
$meta = $this->meta;
|
||||
if (isset($meta[$param])) {
|
||||
unset($meta[$param]);
|
||||
}
|
||||
$this->meta = $meta;
|
||||
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is a user with specified email.
|
||||
*/
|
||||
public static function userEmailExists($email)
|
||||
{
|
||||
$email = Email::sanitizeEmail($email);
|
||||
$user = User::where('email', $email)->first();
|
||||
|
||||
if ($user) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function oauthEnabled()
|
||||
{
|
||||
return !empty($this->meta['oauth']['provider']);
|
||||
}
|
||||
|
||||
public function oauthGetParam($param)
|
||||
{
|
||||
return $this->meta['oauth'][$param] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,25 @@
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Watson\Rememberable\Rememberable;
|
||||
|
||||
class MailboxUser extends Model
|
||||
{
|
||||
use Rememberable;
|
||||
// This is obligatory.
|
||||
public $rememberCacheDriver = 'array';
|
||||
|
||||
// Action after sending a message
|
||||
const AFTER_SEND_STAY = 1;
|
||||
const AFTER_SEND_NEXT = 2;
|
||||
const AFTER_SEND_FOLDER = 3;
|
||||
|
||||
protected $table = 'mailbox_user';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
// Does not work as we receive it via pivot
|
||||
// protected $casts = [
|
||||
// 'access' => 'array',
|
||||
// ];
|
||||
}
|
||||
|
||||
1476
app/Misc/Helper.php
1476
app/Misc/Helper.php
File diff suppressed because it is too large
Load Diff
@@ -35,22 +35,50 @@ class Mail
|
||||
* Encryptions.
|
||||
*/
|
||||
const MAIL_ENCRYPTION_NONE = '';
|
||||
const MAIL_ENCRYPTION_SSL = 'ssl';
|
||||
const MAIL_ENCRYPTION_TLS = 'tls';
|
||||
const MAIL_ENCRYPTION_SSL = 'ssl';
|
||||
const MAIL_ENCRYPTION_TLS = 'tls';
|
||||
|
||||
const FETCH_SCHEDULE_EVERY_MINUTE = 1;
|
||||
const FETCH_SCHEDULE_EVERY_FIVE_MINUTES = 5;
|
||||
const FETCH_SCHEDULE_EVERY_TEN_MINUTES = 10;
|
||||
const FETCH_SCHEDULE_EVERY_FIFTEEN_MINUTES = 15;
|
||||
const FETCH_SCHEDULE_EVERY_THIRTY_MINUTES = 30;
|
||||
const FETCH_SCHEDULE_HOURLY = 60;
|
||||
|
||||
const OAUTH_PROVIDER_MICROSOFT = 'ms';
|
||||
|
||||
/**
|
||||
* If reply is not extracted properly from the incoming email, add here new separator.
|
||||
* If reply is not extracted properly from the incoming email, add here a new separator.
|
||||
* Order is not important.
|
||||
* Idially separators must contain < or > to avoid false positives.
|
||||
* Regex separators has "regex:" in the beginning.
|
||||
*/
|
||||
public static $alternative_reply_separators = [
|
||||
self::REPLY_SEPARATOR_HTML,
|
||||
self::REPLY_SEPARATOR_TEXT,
|
||||
'<div class="gmail_quote">',
|
||||
'<blockquote',
|
||||
self::REPLY_SEPARATOR_HTML, // Our HTML separator
|
||||
self::REPLY_SEPARATOR_TEXT, // Our plain text separator
|
||||
|
||||
// Email service providers specific separators.
|
||||
'<div class="gmail_quote">', // Gmail
|
||||
'<div id="appendonsend"></div>', // Outlook / Live / Hotmail / Microsoft
|
||||
'<div name="quote" ',
|
||||
'yahoo_quoted_', // Yahoo, full: <div id=3D"ydp6h4f5c59yahoo_quoted_2937493705"
|
||||
'------------------ 原始邮件 ------------------', // QQ
|
||||
'------------------ 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
|
||||
|
||||
// General separators.
|
||||
'regex:/<blockquote((?!quote)[^>])*>/', // General sepator. Should skip Gmail's <blockquote class="gmail_quote">.
|
||||
'<!-- originalMessage -->',
|
||||
//'---Original---', // QQ separator, wait for emails from QQ and check
|
||||
'‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐',
|
||||
'--------------- Original Message ---------------',
|
||||
];
|
||||
|
||||
/**
|
||||
* md5 of the last applied mail config.
|
||||
*/
|
||||
public static $last_mail_config_hash = '';
|
||||
|
||||
/**
|
||||
* Configure mail sending parameters.
|
||||
*
|
||||
@@ -67,8 +95,13 @@ class Mail
|
||||
if ($mailbox->out_method == Mailbox::OUT_METHOD_SMTP) {
|
||||
\Config::set('mail.host', $mailbox->out_server);
|
||||
\Config::set('mail.port', $mailbox->out_port);
|
||||
\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());
|
||||
}
|
||||
} else {
|
||||
@@ -77,7 +110,33 @@ class Mail
|
||||
\Config::set('mail.from', ['address' => self::getSystemMailFrom(), 'name' => '']);
|
||||
}
|
||||
|
||||
self::reapplyMailConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reapply new mail config.
|
||||
*/
|
||||
public static function reapplyMailConfig()
|
||||
{
|
||||
// Check hash to avoid recreating MailServiceProvider.
|
||||
$mail_config_hash = md5(json_encode(\Config::get('mail')));
|
||||
|
||||
if (self::$last_mail_config_hash != $mail_config_hash) {
|
||||
self::$last_mail_config_hash = $mail_config_hash;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Without doing this, Swift mailer uses old config values
|
||||
// if there were emails sent with previous config.
|
||||
\App::forgetInstance('mailer');
|
||||
\App::forgetInstance('swift.mailer');
|
||||
\App::forgetInstance('swift.transport');
|
||||
|
||||
(new \Illuminate\Mail\MailServiceProvider(app()))->register();
|
||||
// We have to update Mailer facade manually, as it does not happen automatically
|
||||
// and previous instance of app('mailer') is used.
|
||||
\Mail::swap(app('mailer'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,40 +148,66 @@ class Mail
|
||||
{
|
||||
\Config::set('mail.driver', self::getSystemMailDriver());
|
||||
\Config::set('mail.from', [
|
||||
'address' => self::getSystemMailFrom(),
|
||||
'name' => Option::get('company_name', \Config::get('app.name'))
|
||||
'address' => self::getSystemMailFrom(),
|
||||
'name' => Option::get('company_name', \Config::get('app.name')),
|
||||
]);
|
||||
|
||||
// SMTP
|
||||
if (\Config::get('mail.driver') == self::MAIL_DRIVER_SMTP) {
|
||||
\Config::set('mail.host', Option::get('mail_host'));
|
||||
\Config::set('mail.port', Option::get('mail_port'));
|
||||
\Config::set('mail.username', Option::get('mail_username'));
|
||||
\Config::set('mail.password', Option::get('mail_password'));
|
||||
if (!Option::get('mail_username')) {
|
||||
\Config::set('mail.username', null);
|
||||
\Config::set('mail.password', null);
|
||||
} else {
|
||||
\Config::set('mail.username', Option::get('mail_username'));
|
||||
\Config::set('mail.password', \Helper::decrypt(Option::get('mail_password')));
|
||||
}
|
||||
\Config::set('mail.encryption', Option::get('mail_encryption'));
|
||||
}
|
||||
|
||||
(new \Illuminate\Mail\MailServiceProvider(app()))->register();
|
||||
self::reapplyMailConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace mail vars in the text.
|
||||
*/
|
||||
public static function replaceMailVars($text, $data = [])
|
||||
public static function replaceMailVars($text, $data = [], $escape = false)
|
||||
{
|
||||
// Available variables to insert into email in UI.
|
||||
$vars = [
|
||||
'{%subject%}' => $data['conversation']->subject,
|
||||
'{%mailbox.email%}' => $data['mailbox']->email,
|
||||
'{%mailbox.name%}' => $data['mailbox']->name,
|
||||
'{%conversation.number%}' => $data['conversation']->number,
|
||||
'{%customer.email%}' => $data['conversation']->customer_email
|
||||
];
|
||||
$vars = [];
|
||||
|
||||
if ($data['customer']) {
|
||||
$vars['{%customer.fullName%}'] = $data['customer']->getFullName(true);
|
||||
if (!empty($data['conversation'])) {
|
||||
$vars['{%subject%}'] = $data['conversation']->subject;
|
||||
$vars['{%conversation.number%}'] = $data['conversation']->number;
|
||||
$vars['{%customer.email%}'] = $data['conversation']->customer_email;
|
||||
}
|
||||
if (!empty($data['mailbox'])) {
|
||||
$vars['{%mailbox.email%}'] = $data['mailbox']->email;
|
||||
$vars['{%mailbox.name%}'] = $data['mailbox']->name;
|
||||
$vars['{%mailbox.fromName%}'] = $data['mailbox']->getMailFrom(!empty($data['user']) ? $data['user'] : null)['name'];
|
||||
}
|
||||
if (!empty($data['customer'])) {
|
||||
$vars['{%customer.fullName%}'] = $data['customer']->getFullName(true);
|
||||
$vars['{%customer.firstName%}'] = $data['customer']->getFirstName(true);
|
||||
$vars['{%customer.lastName%}'] = $data['customer']->last_name;
|
||||
$vars['{%customer.lastName%}'] = $data['customer']->last_name;
|
||||
}
|
||||
if (!empty($data['user'])) {
|
||||
$vars['{%user.fullName%}'] = $data['user']->getFullName();
|
||||
$vars['{%user.firstName%}'] = $data['user']->getFirstName();
|
||||
$vars['{%user.phone%}'] = $data['user']->phone;
|
||||
$vars['{%user.email%}'] = $data['user']->email;
|
||||
$vars['{%user.jobTitle%}'] = $data['user']->job_title;
|
||||
$vars['{%user.lastName%}'] = $data['user']->last_name;
|
||||
$vars['{%user.photoUrl%}'] = $data['user']->getPhotoUrl();
|
||||
}
|
||||
|
||||
$vars = \Eventy::filter('mail_vars.replace', $vars, $data);
|
||||
|
||||
if ($escape) {
|
||||
foreach ($vars as $i => $var) {
|
||||
$vars[$i] = htmlspecialchars($var ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
return strtr($text, $vars);
|
||||
@@ -133,7 +218,7 @@ class Mail
|
||||
*/
|
||||
public static function hasVars($text)
|
||||
{
|
||||
return preg_match("/({%|%})/", $text);
|
||||
return preg_match('/({%|%})/', $text);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,6 +238,7 @@ class Mail
|
||||
if (!$mail_from) {
|
||||
$mail_from = 'freescout@'.\Helper::getDomain();
|
||||
}
|
||||
|
||||
return $mail_from;
|
||||
}
|
||||
|
||||
@@ -174,6 +260,7 @@ class Mail
|
||||
\MailHelper::setMailDriver($mailbox);
|
||||
|
||||
$status_message = '';
|
||||
|
||||
try {
|
||||
\Mail::to([$to])->send(new \App\Mail\Test($mailbox));
|
||||
} catch (\Exception $e) {
|
||||
@@ -185,6 +272,7 @@ class Mail
|
||||
\MailHelper::setSystemMailDriver();
|
||||
|
||||
$status_message = '';
|
||||
|
||||
try {
|
||||
\Mail::to([['name' => '', 'email' => $to]])
|
||||
->send(new \App\Mail\Test());
|
||||
@@ -203,6 +291,7 @@ class Mail
|
||||
}
|
||||
} else {
|
||||
SendLog::log(null, null, $to, SendLog::MAIL_TYPE_TEST, SendLog::STATUS_ACCEPTED);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -212,15 +301,7 @@ class Mail
|
||||
*/
|
||||
public static function fetchTest($mailbox)
|
||||
{
|
||||
$client = new Client([
|
||||
'host' => $mailbox->in_server,
|
||||
'port' => $mailbox->in_port,
|
||||
'encryption' => $mailbox->getInEncryptionName(),
|
||||
'validate_cert' => true,
|
||||
'username' => $mailbox->in_username,
|
||||
'password' => $mailbox->in_password,
|
||||
'protocol' => $mailbox->getInProtocolName(),
|
||||
]);
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
|
||||
// Connect to the Server
|
||||
$client->connect();
|
||||
@@ -234,8 +315,24 @@ class Mail
|
||||
// Get unseen messages for a period
|
||||
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->get();
|
||||
|
||||
if ($client->getLastError()) {
|
||||
throw new \Exception($client->getLastError(), 1);
|
||||
$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);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
@@ -253,7 +350,7 @@ class Mail
|
||||
if (is_array($emails)) {
|
||||
$emails_array = $emails;
|
||||
} else {
|
||||
$emails_array = explode(',', $emails);
|
||||
$emails_array = explode(',', $emails ?? '');
|
||||
}
|
||||
|
||||
foreach ($emails_array as $i => $email) {
|
||||
@@ -266,6 +363,18 @@ class Mail
|
||||
return $emails_array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if email format is valid.
|
||||
*
|
||||
* @param [type] $email [description]
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public static function validateEmail($email)
|
||||
{
|
||||
return filter_var($email, FILTER_VALIDATE_EMAIL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send system alert to super admin.
|
||||
*/
|
||||
@@ -283,8 +392,9 @@ class Mail
|
||||
\MailHelper::setSystemMailDriver();
|
||||
|
||||
$status_message = '';
|
||||
|
||||
try {
|
||||
\Mail::raw($body, function($message) use ($subject, $attachments, $from_user) {
|
||||
\Mail::raw($body, function ($message) use ($subject, $attachments, $from_user) {
|
||||
$message
|
||||
->subject($subject)
|
||||
->to(\Config::get('app.freescout_email'));
|
||||
@@ -310,4 +420,400 @@ class Mail
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email marker for the outgoing email to track replies
|
||||
* in case Message-ID header is removed by mail service provider.
|
||||
*
|
||||
* @param [type] $message_id [description]
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public static function getMessageMarker($message_id)
|
||||
{
|
||||
// It has to be BASE64, as Gmail converts it into link.
|
||||
return '{#FS:'.base64_encode($message_id).'#}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Message-ID from incoming email body.
|
||||
*
|
||||
* @param [type] $message_id [description]
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public static function fetchMessageMarkerValue($body)
|
||||
{
|
||||
preg_match('/{#FS:([^#]+)#}/', $body, $matches);
|
||||
if (!empty($matches[1]) && base64_decode($matches[1])) {
|
||||
// Return first found marker.
|
||||
return base64_decode($matches[1]);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect autoresponder by headers.
|
||||
* https://github.com/jpmckinney/multi_mail/wiki/Detecting-autoresponders
|
||||
* https://www.jitbit.com/maxblog/18-detecting-outlook-autoreplyout-of-office-emails-and-x-auto-response-suppress-header/.
|
||||
*
|
||||
* @return bool [description]
|
||||
*/
|
||||
public static function isAutoResponder($headers_str)
|
||||
{
|
||||
$autoresponder_headers = [
|
||||
'x-autoreply' => '',
|
||||
'x-autorespond' => '',
|
||||
'auto-submitted' => 'auto-replied',
|
||||
'precedence' => ['auto_reply', 'bulk', 'junk'],
|
||||
'x-precedence' => ['auto_reply', 'bulk', 'junk'],
|
||||
];
|
||||
$headers = explode("\n", $headers_str ?? '');
|
||||
|
||||
foreach ($autoresponder_headers as $auto_header => $auto_header_value) {
|
||||
foreach ($headers as $header) {
|
||||
$parts = explode(':', $header, 2);
|
||||
if (count($parts) == 2) {
|
||||
$name = trim(strtolower($parts[0]));
|
||||
$value = trim($parts[1]);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if (strtolower($name) == $auto_header) {
|
||||
if (!$auto_header_value) {
|
||||
return true;
|
||||
} elseif (is_array($auto_header_value)) {
|
||||
foreach ($auto_header_value as $auto_header_value_item) {
|
||||
if ($value == $auto_header_value_item) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} elseif ($value == $auto_header_value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Content-Type header.
|
||||
* This is not 100% reliable, detects only standard DSN bounces.
|
||||
*
|
||||
* @param [type] $headers [description]
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public static function detectBounceByHeaders($headers)
|
||||
{
|
||||
if (preg_match("/Content-Type:((?:[^\n]|\n[\t ])+)(?:\n[^\t ]|$)/i", $headers, $match)
|
||||
&& preg_match("/multipart\/report/i", $match[1])
|
||||
&& preg_match("/report-type=[\"']?delivery-status[\"']?/i", $match[1])
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse email headers.
|
||||
*
|
||||
* @param [type] $headers_str [description]
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public static function parseHeaders($headers_str)
|
||||
{
|
||||
try {
|
||||
return imap_rfc822_parse_headers($headers_str);
|
||||
} catch (\Exception $e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getHeader($headers_str, $header)
|
||||
{
|
||||
$headers = self::parseHeaders($headers_str);
|
||||
if (!$headers) {
|
||||
return;
|
||||
}
|
||||
$value = null;
|
||||
if (property_exists($headers, $header)) {
|
||||
$value = $headers->$header;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
switch ($header) {
|
||||
case 'message_id':
|
||||
$value = str_replace(['<', '>'], '', $value);
|
||||
break;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client for fetching emails.
|
||||
*/
|
||||
public static function getMailboxClient($mailbox)
|
||||
{
|
||||
if (!$mailbox->oauthEnabled()) {
|
||||
return new \Webklex\IMAP\Client([
|
||||
'host' => $mailbox->in_server,
|
||||
'port' => $mailbox->in_port,
|
||||
'encryption' => $mailbox->getInEncryptionName(),
|
||||
'validate_cert' => $mailbox->in_validate_cert,
|
||||
'username' => $mailbox->in_username,
|
||||
'password' => $mailbox->in_password,
|
||||
'protocol' => $mailbox->getInProtocolName(),
|
||||
]);
|
||||
} 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',
|
||||
]);
|
||||
// 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 ((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,
|
||||
]);
|
||||
throw new \Exception($error_message, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// This makes it authenticate two times.
|
||||
//$cm->setTimeout(60);
|
||||
|
||||
return $cm->account('default');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate artificial Message-ID.
|
||||
*/
|
||||
public static function generateMessageId($email_address, $raw_body = '')
|
||||
{
|
||||
$hash = str_random(16);
|
||||
if ($raw_body) {
|
||||
$hash = md5(strval($raw_body));
|
||||
}
|
||||
|
||||
return 'fs-'.$hash.'@'.preg_replace("/.*@/", '', $email_address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch IMAP message by Message-ID.
|
||||
*/
|
||||
public static function fetchMessage($mailbox, $message_id)
|
||||
{
|
||||
$no_charset = false;
|
||||
|
||||
if (!$message_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
$client->connect();
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e, '('.$mailbox->name.') Could not fetch specific message by Message-ID via IMAP:');
|
||||
return null;
|
||||
}
|
||||
|
||||
$imap_folders = $mailbox->getInImapFolders();
|
||||
|
||||
foreach ($imap_folders as $folder_name) {
|
||||
try {
|
||||
$folder = $client->getFolder($folder_name);
|
||||
// Message-ID: <123@123.com>
|
||||
$query = $folder->query()->text('<'.$message_id.'>')->leaveUnread()->limit(1);
|
||||
|
||||
if ($no_charset) {
|
||||
$query->setCharset(null);
|
||||
}
|
||||
|
||||
$messages = $query->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()->text('<'.$message_id.'>')->leaveUnread()->limit(1)->setCharset(null)->get();
|
||||
$no_charset = true;
|
||||
}
|
||||
|
||||
if (count($messages)) {
|
||||
return $messages->first();
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e, '('.$mailbox->name.') Could not fetch specific message by Message-ID via IMAP:');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function oauthGetAuthorizationUrl($provider_code, $params)
|
||||
{
|
||||
$args = [];
|
||||
|
||||
switch ($provider_code) {
|
||||
case self::OAUTH_PROVIDER_MICROSOFT:
|
||||
// https://docs.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
|
||||
$args = [
|
||||
'scope' => 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send',
|
||||
'response_type' => 'code',
|
||||
'approval_prompt' => 'auto',
|
||||
'redirect_uri' => route('mailboxes.oauth_callback'),
|
||||
];
|
||||
$args = array_merge($args, $params);
|
||||
$url = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?'.http_build_query($args);
|
||||
break;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public static function oauthGetAccessToken($provider_code, $params)
|
||||
{
|
||||
$token_data = [];
|
||||
$post_params = [];
|
||||
|
||||
switch ($provider_code) {
|
||||
case self::OAUTH_PROVIDER_MICROSOFT:
|
||||
$post_params = [
|
||||
'scope' => 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send',
|
||||
"grant_type" => "authorization_code",
|
||||
'redirect_uri' => route('mailboxes.oauth_callback'),
|
||||
];
|
||||
|
||||
$post_params = array_merge($post_params, $params);
|
||||
|
||||
// Refreshing Access Token.
|
||||
if (!empty($post_params['refresh_token'])) {
|
||||
$post_params['grant_type'] = 'refresh_token';
|
||||
}
|
||||
|
||||
// $postUrl = "/common/oauth2/token";
|
||||
// $hostname = "login.microsoftonline.com";
|
||||
$full_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
|
||||
|
||||
// $headers = array(
|
||||
// // "POST " . $postUrl . " HTTP/1.1",
|
||||
// // "Host: login.windows.net",
|
||||
// "Content-type: application/x-www-form-urlencoded",
|
||||
// );
|
||||
|
||||
$curl = curl_init($full_url);
|
||||
|
||||
curl_setopt($curl, CURLOPT_POST, true);
|
||||
//curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $post_params);
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, array("application/x-www-form-urlencoded"));
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
|
||||
$response = curl_exec($curl);
|
||||
|
||||
if ($response) {
|
||||
$result = json_decode($response, true);
|
||||
|
||||
// [token_type] => Bearer
|
||||
// [scope] => IMAP.AccessAsUser.All offline_access SMTP.Send User.Read
|
||||
// [expires_in] => 4514
|
||||
// [ext_expires_in] => 4514
|
||||
// [expires_on] => 1646122657
|
||||
// [not_before] => 1646117842
|
||||
// [resource] => 00000002-0000-0000-c000-000000000000
|
||||
// [access_token] => dd
|
||||
// [refresh_token] => dd
|
||||
// [id_token] => dd
|
||||
if (!empty($result['access_token'])) {
|
||||
$token_data['provider'] = self::OAUTH_PROVIDER_MICROSOFT;
|
||||
$token_data['a_token'] = $result['access_token'];
|
||||
$token_data['r_token'] = $result['refresh_token'];
|
||||
//$token_data['id_token'] = $result['id_token'];
|
||||
$token_data['issued_on'] = now()->toDateTimeString();
|
||||
$token_data['expires_in'] = $result['expires_in'];
|
||||
} elseif ($response) {
|
||||
$token_data['error'] = $response;
|
||||
} else {
|
||||
$token_data['error'] = 'Response code: '.curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
}
|
||||
}
|
||||
curl_close($curl);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return $token_data;
|
||||
}
|
||||
|
||||
public static function oauthDisconnect($provider_code, $redirect_uri)
|
||||
{
|
||||
switch ($provider_code) {
|
||||
case self::OAUTH_PROVIDER_MICROSOFT:
|
||||
return redirect()->away('https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri='.urlencode($redirect_uri));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// public static function oauthGetProvider($provider_code, $params)
|
||||
// {
|
||||
// $provider = null;
|
||||
|
||||
// switch ($provider_code) {
|
||||
// case self::OAUTH_PROVIDER_MICROSOFT:
|
||||
// $provider = new \Stevenmaguire\OAuth2\Client\Provider\Microsoft([
|
||||
// // Required
|
||||
// 'clientId' => $params['client_id'],
|
||||
// 'clientSecret' => $params['client_secret'],
|
||||
// 'redirectUri' => route('mailboxes.oauth_callback'),
|
||||
// //https://login.microsoftonline.com/common/oauth2/authorize';
|
||||
// 'urlAuthorize' => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
||||
// 'urlAccessToken' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
// 'urlResourceOwnerDetails' => 'https://outlook.office.com/api/v1.0/me'
|
||||
// ]);
|
||||
// break;
|
||||
// }
|
||||
|
||||
// return $provider;
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -2,47 +2,75 @@
|
||||
|
||||
namespace App\Misc;
|
||||
|
||||
use Zttp\Zttp;
|
||||
|
||||
class WpApi
|
||||
{
|
||||
const ENDPOINT_MODULES = 'freescout/v1/modules';
|
||||
|
||||
const METHOD_GET = 'get';
|
||||
const METHOD_POST = 'post';
|
||||
const METHOD_GET = 'GET';
|
||||
const METHOD_POST = 'POST';
|
||||
|
||||
const ACTION_CHECK_LICENSE = 'check_license';
|
||||
const ACTION_CHECK_LICENSE = 'check_license';
|
||||
const ACTION_CHECK_LICENSES = 'check_licenses';
|
||||
const ACTION_ACTIVATE_LICENSE = 'activate_license';
|
||||
const ACTION_GET_VERSION = 'get_version';
|
||||
const ACTION_DEACTIVATE_LICENSE = 'deactivate_license';
|
||||
const ACTION_GET_VERSION = 'get_version';
|
||||
|
||||
public static $lastError;
|
||||
|
||||
public static function url($path)
|
||||
public static function url($path, $alternative = false)
|
||||
{
|
||||
return \Config::get('app.freescout_api').$path;
|
||||
if ($alternative) {
|
||||
return \Config::get('app.freescout_alt_api').$path;
|
||||
} else {
|
||||
return \Config::get('app.freescout_api').$path;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static function httpRequest($method, $url, $params)
|
||||
{
|
||||
$client = new \GuzzleHttp\Client();
|
||||
|
||||
if ($method == self::METHOD_POST) {
|
||||
return $client->request('POST', $url, [
|
||||
'connect_timeout' => 10,
|
||||
'form_params' => $params,
|
||||
]);
|
||||
} else {
|
||||
return $client->request('GET', $url, [
|
||||
'connect_timeout' => 10,
|
||||
'query' => $params,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API request.
|
||||
*/
|
||||
public static function request($method, $endpoint, $params = [])
|
||||
public static function request($method, $endpoint, $params = [], $alternative_api = false)
|
||||
{
|
||||
self::$lastError = null;
|
||||
|
||||
try {
|
||||
$response = Zttp::$method(self::url($endpoint), $params);
|
||||
$response = self::httpRequest($method, self::url($endpoint, $alternative_api), $params);
|
||||
} catch (\Exception $e) {
|
||||
self::$lastError = array(
|
||||
'code' => $e->getCode(),
|
||||
if (!$alternative_api) {
|
||||
return self::request($method, $endpoint, $params, true);
|
||||
}
|
||||
\Helper::logException($e, 'WpApi');
|
||||
self::$lastError = [
|
||||
'code' => $e->getCode(),
|
||||
'message' => $e->getMessage(),
|
||||
);
|
||||
];
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// https://guzzle3.readthedocs.io/http-client/response.html
|
||||
if ($response->status() < 500) {
|
||||
$json = $response->json();
|
||||
if (!empty($json['code']) && !empty($json['message']) &&
|
||||
if ($response->getStatusCode() < 500) {
|
||||
$json = \Helper::jsonToArray($response->getBody());
|
||||
|
||||
if (!empty($json['code']) && !empty($json['message']) &&
|
||||
!empty($json['data']) && !empty($json['data']['status']) && $json['data']['status'] != 200
|
||||
) {
|
||||
self::$lastError = $json;
|
||||
@@ -80,6 +108,18 @@ class WpApi
|
||||
return self::request(self::METHOD_POST, $endpoint, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check module license.
|
||||
*/
|
||||
public static function checkLicenses($params)
|
||||
{
|
||||
$params['action'] = self::ACTION_CHECK_LICENSES;
|
||||
|
||||
$endpoint = self::ENDPOINT_MODULES;
|
||||
|
||||
return self::request(self::METHOD_POST, $endpoint, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate module license.
|
||||
*/
|
||||
@@ -96,6 +136,22 @@ class WpApi
|
||||
return self::request(self::METHOD_POST, $endpoint, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate module license.
|
||||
*/
|
||||
public static function deactivateLicense($params)
|
||||
{
|
||||
$params['action'] = self::ACTION_DEACTIVATE_LICENSE;
|
||||
|
||||
$endpoint = self::ENDPOINT_MODULES;
|
||||
|
||||
if (!empty($params['module_alias'])) {
|
||||
$endpoint .= '/'.$params['module_alias'];
|
||||
}
|
||||
|
||||
return self::request(self::METHOD_POST, $endpoint, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license details.
|
||||
*/
|
||||
|
||||
110
app/Module.php
110
app/Module.php
@@ -3,6 +3,7 @@
|
||||
* 'active' parameter in module.json is not taken in account.
|
||||
* Module 'active' flag is taken from DB.
|
||||
*/
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -20,15 +21,16 @@ class Module extends Model
|
||||
|
||||
public static function getCached()
|
||||
{
|
||||
if (!self::$modules) {
|
||||
if (!self::$modules) {
|
||||
// At this stage modules table may not exist
|
||||
try {
|
||||
self::$modules = Module::all();
|
||||
self::$modules = self::all();
|
||||
} catch (\Exception $e) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
return self::$modules;
|
||||
}
|
||||
|
||||
return self::$modules;
|
||||
}
|
||||
|
||||
public static function isActive($alias)
|
||||
@@ -48,6 +50,7 @@ class Module extends Model
|
||||
if ($save) {
|
||||
$module->save();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -76,9 +79,11 @@ class Module extends Model
|
||||
|
||||
/**
|
||||
* Activate module license.
|
||||
* @param [type] $alias [description]
|
||||
* @param [type] $details_url [description]
|
||||
* @return boolean [description]
|
||||
*
|
||||
* @param [type] $alias [description]
|
||||
* @param [type] $details_url [description]
|
||||
*
|
||||
* @return bool [description]
|
||||
*/
|
||||
public static function activateLicense($alias, $license)
|
||||
{
|
||||
@@ -100,9 +105,10 @@ class Module extends Model
|
||||
{
|
||||
$module = self::getByAlias($alias);
|
||||
if (!$module) {
|
||||
$module = new Module();
|
||||
$module = new self();
|
||||
$module->alias = $alias;
|
||||
}
|
||||
|
||||
return $module;
|
||||
}
|
||||
|
||||
@@ -119,6 +125,13 @@ class Module extends Model
|
||||
}
|
||||
}
|
||||
|
||||
public static function setLicense($alias, $license)
|
||||
{
|
||||
$module = self::getByAliasOrCreate($alias);
|
||||
$module->license = $license;
|
||||
$module->save();
|
||||
}
|
||||
|
||||
public static function normalizeAlias($alias)
|
||||
{
|
||||
return trim(strtolower($alias));
|
||||
@@ -130,17 +143,94 @@ class Module extends Model
|
||||
if ($modules) {
|
||||
return self::getCached()->where('alias', $alias)->first();
|
||||
} else {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate module and update modules cache.
|
||||
*/
|
||||
public static function deactiveModule($alias)
|
||||
public static function deactiveModule($alias, $clear_app_cache = true)
|
||||
{
|
||||
self::setActive($alias, false);
|
||||
// Update modules cache
|
||||
\Module::clearCache();
|
||||
if ($clear_app_cache) {
|
||||
\Artisan::call('freescout:clear-cache');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL used to active and check license.
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public static function getAppUrl()
|
||||
{
|
||||
return parse_url(\Config::get('app.url'), PHP_URL_HOST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check missing extensions among required by module.
|
||||
*
|
||||
* @param [type] $required_extensions [description]
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public static function getMissingExtensions($required_extensions)
|
||||
{
|
||||
$missing = [];
|
||||
|
||||
$list = explode(',', $required_extensions ?? '');
|
||||
if (!is_array($list) || !count($list)) {
|
||||
return [];
|
||||
}
|
||||
foreach ($list as $ext) {
|
||||
$ext = trim($ext);
|
||||
if ($ext && !extension_loaded($ext)) {
|
||||
$missing[] = $ext;
|
||||
}
|
||||
}
|
||||
|
||||
return $missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check missing modules required by the module.
|
||||
*/
|
||||
public static function getMissingModules($required_modules, $modules = [])
|
||||
{
|
||||
$missing = [];
|
||||
|
||||
if (!$modules) {
|
||||
$modules = \Module::all();
|
||||
}
|
||||
|
||||
if (!is_array($required_modules) || !count($required_modules)) {
|
||||
return [];
|
||||
}
|
||||
foreach ($required_modules as $alias => $version) {
|
||||
$module = null;
|
||||
foreach ($modules as $module_item) {
|
||||
if ($module_item->alias == $alias) {
|
||||
$module = $module_item;
|
||||
}
|
||||
}
|
||||
if (!$module) {
|
||||
$missing[$alias] = $version;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!self::isActive($alias) || !version_compare($module->version, $version, '>=')) {
|
||||
$missing[$alias] = $version;
|
||||
}
|
||||
}
|
||||
|
||||
return $missing;
|
||||
}
|
||||
|
||||
public static function formatName($name)
|
||||
{
|
||||
return preg_replace("/ Module$/", '', $name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,15 @@ namespace App\Notifications;
|
||||
use App\Conversation;
|
||||
use App\Subscription;
|
||||
use App\Thread;
|
||||
use App\Channels\RealtimeBroadcastChannel;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\BroadcastMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class BroadcastNotification extends Notification
|
||||
class BroadcastNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $conversation;
|
||||
public $thread;
|
||||
public $mediums;
|
||||
@@ -34,7 +37,8 @@ class BroadcastNotification extends Notification
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $user
|
||||
* @param mixed $user
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function via($user)
|
||||
@@ -47,7 +51,8 @@ class BroadcastNotification extends Notification
|
||||
/**
|
||||
* Get the broadcastable representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return BroadcastMessage
|
||||
*/
|
||||
public function toBroadcast($user)
|
||||
@@ -67,6 +72,9 @@ class BroadcastNotification extends Notification
|
||||
return $data;
|
||||
}
|
||||
|
||||
// Try to convert to array.
|
||||
$mediums = (array)$payload->mediums;
|
||||
|
||||
$thread = Thread::find($payload->thread_id);
|
||||
|
||||
if (empty($thread)) {
|
||||
@@ -77,8 +85,7 @@ class BroadcastNotification extends Notification
|
||||
$db_notification = new \Illuminate\Notifications\DatabaseNotification();
|
||||
|
||||
// HTML for the menu notification (uses same medium as for email)
|
||||
if (in_array(Subscription::MEDIUM_EMAIL, $payload->mediums)) {
|
||||
|
||||
if (in_array(Subscription::MEDIUM_EMAIL, $mediums)) {
|
||||
$web_notifications_info = [];
|
||||
|
||||
// Get last reply or note of the conversation to display it's text
|
||||
@@ -110,7 +117,7 @@ class BroadcastNotification extends Notification
|
||||
}
|
||||
|
||||
// Text and url for the browser push notification
|
||||
if (in_array(Subscription::MEDIUM_BROWSER, $payload->mediums)) {
|
||||
if (in_array(Subscription::MEDIUM_BROWSER, $mediums)) {
|
||||
$data['browser']['text'] = strip_tags($thread->getActionDescription($thread->conversation->number));
|
||||
$data['browser']['url'] = $thread->conversation->url(null, $thread->id, ['mark_as_read' => $db_notification->id]);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
<?php
|
||||
/**
|
||||
* Website notification (DB notification).
|
||||
* Website notification (DB notification) to display notifications in the menu.
|
||||
*/
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Conversation;
|
||||
use App\Thread;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class WebsiteNotification extends Notification
|
||||
class WebsiteNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $conversation;
|
||||
|
||||
public $thread;
|
||||
@@ -28,7 +33,8 @@ class WebsiteNotification extends Notification
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $user
|
||||
* @param mixed $user
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function via($user)
|
||||
@@ -39,13 +45,14 @@ class WebsiteNotification extends Notification
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @param mixed $user
|
||||
* @param mixed $user
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($user)
|
||||
{
|
||||
return [
|
||||
'thread_id' => $this->thread->id,
|
||||
'thread_id' => $this->thread->id,
|
||||
'conversation_id' => $this->conversation->id,
|
||||
];
|
||||
}
|
||||
@@ -79,22 +86,21 @@ class WebsiteNotification extends Notification
|
||||
}
|
||||
|
||||
// Get last reply or note of the conversation to display it's text
|
||||
if ($threads) {
|
||||
$last_threads = Thread::whereIn('conversation_id', $threads->pluck('conversation_id')->unique()->toArray())
|
||||
// Select must contain all fields from orderBy() to avoid:
|
||||
// General error: 3065 Expression #1 of ORDER BY clause is not in SELECT
|
||||
->select(['id', 'conversation_id', 'body', 'created_at'])
|
||||
->whereIn('type', [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE, Thread::TYPE_NOTE])
|
||||
->distinct('conversation_id')
|
||||
->orderBy('created_at')
|
||||
// We can not use groupBy because of "isn't in GROUP BY"
|
||||
//->groupBy('conversation_id')
|
||||
->get();
|
||||
}
|
||||
// if ($threads) {
|
||||
// $last_threads = Thread::whereIn('conversation_id', $threads->pluck('conversation_id')->unique()->toArray())
|
||||
// // Select must contain all fields from orderBy() to avoid:
|
||||
// // General error: 3065 Expression #1 of ORDER BY clause is not in SELECT
|
||||
// ->select(['id', 'conversation_id', 'body', 'created_at'])
|
||||
// ->whereIn('type', [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE, Thread::TYPE_NOTE])
|
||||
// ->distinct('conversation_id')
|
||||
// ->orderBy('created_at', 'desc')
|
||||
// // We can not use groupBy because of "isn't in GROUP BY"
|
||||
// //->groupBy('conversation_id')
|
||||
// ->get();
|
||||
// }
|
||||
|
||||
// Populate all collected data into array
|
||||
foreach ($notifications as $notification) {
|
||||
|
||||
$conversation_number = '';
|
||||
if (!empty($notification->data['number'])) {
|
||||
$conversation_number = $notification->data['number'];
|
||||
@@ -123,14 +129,14 @@ class WebsiteNotification extends Notification
|
||||
continue;
|
||||
}
|
||||
|
||||
$last_thread_body = '';
|
||||
$conversation = null;
|
||||
// $last_thread_body = '';
|
||||
// $conversation = null;
|
||||
|
||||
$last_thread = $last_threads->firstWhere('conversation_id', $thread->conversation_id);
|
||||
if ($last_thread) {
|
||||
$last_thread_body = $last_thread->body;
|
||||
$conversation = $last_thread->conversation;
|
||||
}
|
||||
// $last_thread = $last_threads->firstWhere('conversation_id', $thread->conversation_id);
|
||||
// if ($last_thread) {
|
||||
$last_thread_body = $thread->body;
|
||||
$conversation = $thread->conversation;
|
||||
//}
|
||||
if (empty($conversation)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
18
app/Observers/EmailObserver.php
Normal file
18
app/Observers/EmailObserver.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Email;
|
||||
|
||||
class EmailObserver
|
||||
{
|
||||
/**
|
||||
* Email created.
|
||||
*
|
||||
* @param User $user
|
||||
*/
|
||||
public function created(Email $email)
|
||||
{
|
||||
\Eventy::action('email.created', $email);
|
||||
}
|
||||
}
|
||||
@@ -36,5 +36,7 @@ class MailboxObserver
|
||||
$mailbox->users()->delete();
|
||||
$mailbox->conversations()->delete();
|
||||
$mailbox->folders()->delete();
|
||||
|
||||
\Eventy::action('mailbox.before_delete', $mailbox);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class SendLogObserver
|
||||
public function created(SendLog $send_log)
|
||||
{
|
||||
// Update status for thread if any
|
||||
if ($send_log->thread_id && ($send_log->customer_id || ($send_log->user_id && $send_log->user_id == $send_log->thread->user_id)) ) {
|
||||
if ($send_log->thread_id && ($send_log->customer_id || ($send_log->user_id && $send_log->user_id == $send_log->thread->user_id))) {
|
||||
$send_log->thread->send_status = $send_log->status;
|
||||
$send_log->thread->save();
|
||||
}
|
||||
|
||||
@@ -24,7 +24,11 @@ class ThreadObserver
|
||||
if (!in_array($thread->type, [Thread::TYPE_CUSTOMER])) {
|
||||
$conversation->user_updated_at = $now;
|
||||
}
|
||||
if (in_array($thread->type, [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE]) && $thread->state == Thread::STATE_PUBLISHED) {
|
||||
|
||||
if ((in_array($thread->type, [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE])
|
||||
|| ($conversation->isPhone() && in_array($thread->type, [Thread::TYPE_NOTE])))
|
||||
&& $thread->state == Thread::STATE_PUBLISHED
|
||||
) {
|
||||
// $conversation->cc = $thread->cc;
|
||||
// $conversation->bcc = $thread->bcc;
|
||||
$conversation->last_reply_at = $now;
|
||||
@@ -33,6 +37,36 @@ class ThreadObserver
|
||||
if ($conversation->source_via == Conversation::PERSON_CUSTOMER) {
|
||||
$conversation->read_by_user = false;
|
||||
}
|
||||
|
||||
// Update preview.
|
||||
if (in_array($thread->type, [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE, Thread::TYPE_NOTE])
|
||||
&& $thread->state == Thread::STATE_PUBLISHED
|
||||
&& !$thread->isForward()
|
||||
&& ($conversation->threads_count > 1 || $thread->type == Thread::TYPE_NOTE)
|
||||
) {
|
||||
$conversation->setPreview($thread->body);
|
||||
}
|
||||
|
||||
$conversation->save();
|
||||
|
||||
// $is_new_conversation = false;
|
||||
// if ($conversation->threads_count == 0
|
||||
// && in_array($thread->type, [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE, Thread::TYPE_NOTE])
|
||||
// && $thread->state == Thread::STATE_PUBLISHED
|
||||
// ) {
|
||||
// $is_new_conversation = true;
|
||||
// }
|
||||
|
||||
// User threads are created as drafts first.
|
||||
// if ($thread->state == Thread::STATE_PUBLISHED) {
|
||||
// \Eventy::action('thread.created', $thread);
|
||||
// }
|
||||
|
||||
// Real time for user notifications is sent using events.
|
||||
if ($thread->type == Thread::TYPE_CUSTOMER
|
||||
|| ($thread->type == Thread::TYPE_MESSAGE && $thread->state == Thread::STATE_DRAFT)
|
||||
) {
|
||||
Conversation::refreshConversations($conversation, $thread);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
167
app/Option.php
167
app/Option.php
@@ -1,13 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* todo: implement caching by saving all options in one cache variable on register_shutdown_function
|
||||
* todo: implement caching by saving all options in one cache variable on register_shutdown_function.
|
||||
*/
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Option extends Model
|
||||
{
|
||||
// Value returned when cache contains default value.
|
||||
const CACHE_DEFAULT_VALUE = 'CACHE_DEFAULT_VALUE';
|
||||
|
||||
// todo: cache like in WordPress (fetch all options from DB each time)
|
||||
public static $cache = [];
|
||||
|
||||
@@ -23,9 +27,10 @@ class Option extends Model
|
||||
/**
|
||||
* Set an option.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @return boolean
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function set($name, $value)
|
||||
{
|
||||
@@ -45,7 +50,7 @@ class Option extends Model
|
||||
|
||||
$serialized_value = self::maybeSerialize($value);
|
||||
|
||||
$option = Option::firstOrCreate(
|
||||
$option = self::firstOrCreate(
|
||||
['name' => $name], ['value' => $serialized_value]
|
||||
);
|
||||
|
||||
@@ -62,60 +67,148 @@ class Option extends Model
|
||||
/**
|
||||
* Get option.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get($name, $default = false, $decode = true)
|
||||
{
|
||||
// If not passed, get default value from config
|
||||
// If not passed, get default value from config
|
||||
if (func_num_args() == 1) {
|
||||
$default = self::getDefault($name, $default);
|
||||
}
|
||||
|
||||
if (isset(self::$cache[$name])) {
|
||||
return self::$cache[$name];
|
||||
if (self::$cache[$name] == self::CACHE_DEFAULT_VALUE) {
|
||||
return $default;
|
||||
} else {
|
||||
return self::$cache[$name];
|
||||
}
|
||||
}
|
||||
|
||||
$option = Option::where('name', (string)$name)->first();
|
||||
$option = self::where('name', (string) $name)->first();
|
||||
if ($option) {
|
||||
if ($decode) {
|
||||
$value = self::maybeUnserialize($option->value);
|
||||
} else {
|
||||
$value = $option->value;
|
||||
}
|
||||
self::$cache[$name] = $value;
|
||||
} else {
|
||||
$value = $default;
|
||||
self::$cache[$name] = self::CACHE_DEFAULT_VALUE;
|
||||
}
|
||||
|
||||
self::$cache[$name] = $value;
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public static function getDefault($option_name, $default)
|
||||
public static function getDefault($option_name, $default = false)
|
||||
{
|
||||
|
||||
$options = \Config::get('app.options');
|
||||
|
||||
if (isset($options[$option_name]) && isset($options[$option_name]['default'])) {
|
||||
return $options[$option_name]['default'];
|
||||
} else {
|
||||
// Try to get default option value from module's config.
|
||||
preg_match("/^([a-z_]+)\.(.*)/", $option_name, $m);
|
||||
|
||||
if (!empty($m[1]) && !empty($m[2])) {
|
||||
$module_alias = $m[1];
|
||||
$modle_option_name = $m[2];
|
||||
$module_options = \Config::get($module_alias.'.options');
|
||||
if (isset($module_options[$modle_option_name]) && isset($module_options[$modle_option_name]['default'])) {
|
||||
return $module_options[$modle_option_name]['default'];
|
||||
}
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
public static function isDefaultSet($option_name)
|
||||
{
|
||||
$options = \Config::get('app.options');
|
||||
|
||||
return isset($options[$option_name]) && isset($options[$option_name]['default']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple options.
|
||||
*
|
||||
* @param [type] $name [description]
|
||||
* @param bool $default [description]
|
||||
* @param bool $decode [description]
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public static function getOptions($options, $defaults = [], $decode = [])
|
||||
{
|
||||
$values = [];
|
||||
|
||||
// Check in cache first
|
||||
// Return if we can get all options from cache
|
||||
foreach ($options as $name) {
|
||||
if (isset(self::$cache[$name])) {
|
||||
if (self::$cache[$name] == self::CACHE_DEFAULT_VALUE) {
|
||||
if (!isset($defaults[$name])) {
|
||||
$default = self::getDefault($name);
|
||||
} else {
|
||||
$default = $defaults[$name];
|
||||
}
|
||||
$values[$name] = $default;
|
||||
} else {
|
||||
$values[$name] = self::$cache[$name];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (count($values) == count($options)) {
|
||||
return $values;
|
||||
} else {
|
||||
$values = [];
|
||||
}
|
||||
|
||||
$db_options = self::whereIn('name', $options)->get();
|
||||
foreach ($options as $name) {
|
||||
// If not passed, get default value from config
|
||||
if (!isset($defaults[$name])) {
|
||||
$default = self::getDefault($name);
|
||||
} else {
|
||||
$default = $defaults[$name];
|
||||
}
|
||||
$db_option = $db_options->where('name', $name)->first();
|
||||
if ($db_option) {
|
||||
// todo: decode
|
||||
if (1 || $decode) {
|
||||
$value = self::maybeUnserialize($db_option->value);
|
||||
} else {
|
||||
$value = $db_option->value;
|
||||
}
|
||||
self::$cache[$name] = $value;
|
||||
} else {
|
||||
$value = $default;
|
||||
self::$cache[$name] = self::CACHE_DEFAULT_VALUE;
|
||||
}
|
||||
|
||||
$values[$name] = $value;
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
public static function remove($name)
|
||||
{
|
||||
Option::where('name', (string)$name)->delete();
|
||||
self::where('name', (string) $name)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize data, if needed.
|
||||
*/
|
||||
public static function maybeSerialize($data) {
|
||||
public static function maybeSerialize($data)
|
||||
{
|
||||
if (is_array($data) || is_object($data)) {
|
||||
return serialize( $data );
|
||||
return serialize($data);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -130,8 +223,10 @@ class Option extends Model
|
||||
} catch (\Exception $e) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
return $original;
|
||||
}
|
||||
|
||||
return $original;
|
||||
}
|
||||
|
||||
@@ -139,16 +234,17 @@ class Option extends Model
|
||||
* Check value to find if it was serialized.
|
||||
* Serialized data is always a string.
|
||||
*/
|
||||
public static function isSerialized($data, $strict = true) {
|
||||
public static function isSerialized($data, $strict = true)
|
||||
{
|
||||
// if it isn't a string, it isn't serialized.
|
||||
if (!is_string($data )) {
|
||||
if (!is_string($data)) {
|
||||
return false;
|
||||
}
|
||||
$data = trim($data);
|
||||
if ('N;' == $data) {
|
||||
return true;
|
||||
}
|
||||
if (strlen( $data ) < 4) {
|
||||
if (strlen($data) < 4) {
|
||||
return false;
|
||||
}
|
||||
if (':' !== $data[1]) {
|
||||
@@ -161,36 +257,41 @@ class Option extends Model
|
||||
}
|
||||
} else {
|
||||
$semicolon = strpos($data, ';');
|
||||
$brace = strpos($data, '}');
|
||||
$brace = strpos($data, '}');
|
||||
// Either ; or } must exist.
|
||||
if (false === $semicolon && false === $brace)
|
||||
if (false === $semicolon && false === $brace) {
|
||||
return false;
|
||||
}
|
||||
// But neither must be in the first X characters.
|
||||
if (false !== $semicolon && $semicolon < 3)
|
||||
if (false !== $semicolon && $semicolon < 3) {
|
||||
return false;
|
||||
if (false !== $brace && $brace < 4)
|
||||
}
|
||||
if (false !== $brace && $brace < 4) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$token = $data[0];
|
||||
switch ($token) {
|
||||
case 's' :
|
||||
case 's':
|
||||
if ($strict) {
|
||||
if ('"' !== substr( $data, -2, 1)) {
|
||||
if ('"' !== substr($data, -2, 1)) {
|
||||
return false;
|
||||
}
|
||||
} elseif (false === strpos( $data, '"')) {
|
||||
} elseif (false === strpos($data, '"')) {
|
||||
return false;
|
||||
}
|
||||
// or else fall through
|
||||
case 'a' :
|
||||
case 'O' :
|
||||
return (bool)preg_match("/^{$token}:[0-9]+:/s", $data);
|
||||
case 'b' :
|
||||
case 'i' :
|
||||
case 'd' :
|
||||
case 'a':
|
||||
case 'O':
|
||||
return (bool) preg_match("/^{$token}:[0-9]+:/s", $data);
|
||||
case 'b':
|
||||
case 'i':
|
||||
case 'd':
|
||||
$end = $strict ? '$' : '';
|
||||
return (bool)preg_match("/^{$token}:[0-9.E-]+;$end/", $data);
|
||||
|
||||
return (bool) preg_match("/^{$token}:[0-9.E-]+;$end/", $data);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Conversation;
|
||||
use App\Mailbox;
|
||||
use App\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
@@ -31,6 +32,26 @@ class ConversationPolicy
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached version.
|
||||
*
|
||||
* @param User $user [description]
|
||||
* @param Conversation $conversation [description]
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function viewCached(User $user, Conversation $conversation)
|
||||
{
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
} else {
|
||||
if ($conversation->mailbox->users_cached->contains($user)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the conversation.
|
||||
*
|
||||
@@ -63,4 +84,21 @@ class ConversationPolicy
|
||||
return $user->hasPermission(User::PERM_DELETE_CONVERSATIONS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether current user can move conversations
|
||||
*
|
||||
* @param \App\User $user
|
||||
* @param \App\Mailbox $mailbox
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function move(User $user)
|
||||
{
|
||||
// First check this, because it is cached in conversation page
|
||||
if (count($user->mailboxesCanView(true)) > 1) {
|
||||
return true;
|
||||
}
|
||||
return Mailbox::count() > 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class FolderPolicy
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
} else {
|
||||
if ($folder->user_id == $user->id || $user->mailboxes->contains($folder->mailbox)) {
|
||||
if ($folder->user_id == $user->id || $user->mailboxesSettings()->pluck('mailbox_id')->contains($folder->mailbox_id)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
||||
@@ -46,6 +46,40 @@ class MailboxPolicy
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view mailbox conversations.
|
||||
*
|
||||
* @param \App\User $user
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function viewCached(User $user, Mailbox $mailbox)
|
||||
{
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
} else {
|
||||
// Use cached users for Realtime events
|
||||
if ($mailbox->users_cached->contains($user)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can admin the mailbox.
|
||||
*
|
||||
* @param \App\User $user
|
||||
* @param \App\Mailbox $mailbox
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function admin(User $user, Mailbox $mailbox)
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the mailbox.
|
||||
*
|
||||
@@ -56,7 +90,75 @@ class MailboxPolicy
|
||||
*/
|
||||
public function update(User $user, Mailbox $mailbox)
|
||||
{
|
||||
if ($user->isAdmin()) {
|
||||
if ($user->isAdmin() || $user->canManageMailbox($mailbox->id)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the mailbox auto reply.
|
||||
*
|
||||
* @param \App\User $user
|
||||
* @param \App\Mailbox $mailbox
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function updateAutoReply(User $user, Mailbox $mailbox)
|
||||
{
|
||||
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, 'auto')) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the mailbox Permissions.
|
||||
*
|
||||
* @param \App\User $user
|
||||
* @param \App\Mailbox $mailbox
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function updatePermissions(User $user, Mailbox $mailbox)
|
||||
{
|
||||
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, 'perm')) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the mailbox Permissions.
|
||||
*
|
||||
* @param \App\User $user
|
||||
* @param \App\Mailbox $mailbox
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function updateSettings(User $user, Mailbox $mailbox)
|
||||
{
|
||||
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, 'edit')) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the mailbox Email Signature.
|
||||
*
|
||||
* @param \App\User $user
|
||||
* @param \App\Mailbox $mailbox
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function updateEmailSignature(User $user, Mailbox $mailbox)
|
||||
{
|
||||
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, 'sig')) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
||||
33
app/Policies/ThreadPolicy.php
Normal file
33
app/Policies/ThreadPolicy.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Thread;
|
||||
use App\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class ThreadPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Determine whether the user can edit the thread.
|
||||
*
|
||||
* @param \App\User $user
|
||||
* @param \App\Thread $thread
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function edit(User $user, Thread $thread)
|
||||
{
|
||||
if (($thread->created_by_user_id
|
||||
&& in_array($thread->type, [Thread::TYPE_MESSAGE, Thread::TYPE_NOTE])
|
||||
&& ($user->isAdmin() || ($user->hasPermission(User::PERM_EDIT_CONVERSATIONS) && $thread->created_by_user_id == $user->id)))
|
||||
|| ($thread->created_by_customer_id && in_array($thread->type, [Thread::TYPE_CUSTOMER]))
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user