mirror of
https://github.com/itflow-org/itflow
synced 2026-08-15 20:15:12 +00:00
Compare commits
1655 Commits
revert-117
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5ee0bff74 | ||
|
|
77319f8fc1 | ||
|
|
bdab434707 | ||
|
|
56d9e04eac | ||
|
|
72c037c581 | ||
|
|
2375e69f2d | ||
|
|
73e10e72f2 | ||
|
|
c44fa30792 | ||
|
|
8c0e5246b4 | ||
|
|
35239b563d | ||
|
|
e2512b0f86 | ||
|
|
1c5803dc12 | ||
|
|
6fc94d9733 | ||
|
|
bbbfff7413 | ||
|
|
2354dd1511 | ||
|
|
dc6b191eed | ||
|
|
0fe6d51bdb | ||
|
|
e6da3fd0e3 | ||
|
|
a084e37c39 | ||
|
|
691059aab2 | ||
|
|
eda32f7d57 | ||
|
|
c44ae74557 | ||
|
|
41900191af | ||
|
|
076f45abb0 | ||
|
|
f7548af234 | ||
|
|
de46184a5b | ||
|
|
f237993fd7 | ||
|
|
a96324628a | ||
|
|
b903ee02fe | ||
|
|
6ff454134b | ||
|
|
04991a0d79 | ||
|
|
46a63b191c | ||
|
|
6b65c8150f | ||
|
|
d484629ebc | ||
|
|
656aa95a4e | ||
|
|
db96766ab0 | ||
|
|
10f0c9de58 | ||
|
|
ac598f6379 | ||
|
|
605c7811e4 | ||
|
|
942fe41b18 | ||
|
|
64423cbefa | ||
|
|
3cae52067a | ||
|
|
abb724b40b | ||
|
|
f8899e592f | ||
|
|
108781db5b | ||
|
|
b3744e9ed5 | ||
|
|
b8c9d5b4cf | ||
|
|
77defcf7d8 | ||
|
|
9324d0822e | ||
|
|
eb2cc3d653 | ||
|
|
ddd9c2b56a | ||
|
|
83383599a1 | ||
|
|
49a3c2d266 | ||
|
|
cbf8922f5b | ||
|
|
b39f3c0b23 | ||
|
|
f9ae7598ec | ||
|
|
261bdab61a | ||
|
|
1b386567fa | ||
|
|
5eba64afad | ||
|
|
c30c12674a | ||
|
|
8aaf5b450f | ||
|
|
d74c9743be | ||
|
|
0311a3d056 | ||
|
|
c5288f9e30 | ||
|
|
e800064202 | ||
|
|
abd79c0aed | ||
|
|
a313278623 | ||
|
|
f87d43881e | ||
|
|
ea3d6a2fc6 | ||
|
|
7a44cdd438 | ||
|
|
ad234c5e46 | ||
|
|
f16e7893ac | ||
|
|
3b0d3839ae | ||
|
|
ffe32adbd3 | ||
|
|
5ee1c9580f | ||
|
|
315768d651 | ||
|
|
6b5be0f881 | ||
|
|
a27019511e | ||
|
|
ad15fabbb3 | ||
|
|
e793804203 | ||
|
|
89fba247c5 | ||
|
|
3e532fc792 | ||
|
|
cf4446f405 | ||
|
|
ab41543578 | ||
|
|
c8dab1a7e5 | ||
|
|
056db263a5 | ||
|
|
6107d9f833 | ||
|
|
23a09f54fc | ||
|
|
75028ba09a | ||
|
|
cdb4a43b89 | ||
|
|
4fec57709a | ||
|
|
abe02d7d8e | ||
|
|
53f45fa008 | ||
|
|
ae468d6cee | ||
|
|
6a5cf6704a | ||
|
|
16000efcf7 | ||
|
|
293ba534e2 | ||
|
|
7c5579677d | ||
|
|
09a74d6d26 | ||
|
|
8436cd6296 | ||
|
|
36c9c030c0 | ||
|
|
0631c45040 | ||
|
|
119334ab71 | ||
|
|
38e8edea19 | ||
|
|
1005b73416 | ||
|
|
f2d5ac4a29 | ||
|
|
3c8f812a16 | ||
|
|
729d22d19b | ||
|
|
2395a3ee16 | ||
|
|
7c8c93c01c | ||
|
|
e74307eaea | ||
|
|
74a5359b80 | ||
|
|
d01024931b | ||
|
|
8a092852cf | ||
|
|
3296e48ac6 | ||
|
|
eaf3f5abd1 | ||
|
|
75d8b4d37c | ||
|
|
7245ff0650 | ||
|
|
bc64c9e3d1 | ||
|
|
9c123c42d1 | ||
|
|
af9990ff13 | ||
|
|
a900b2cd74 | ||
|
|
fc5cdeea82 | ||
|
|
bb1f0d5489 | ||
|
|
566d74c15d | ||
|
|
750c2e58cc | ||
|
|
9533da4053 | ||
|
|
cc651c18be | ||
|
|
162a597da9 | ||
|
|
e76b384606 | ||
|
|
07c73a6a04 | ||
|
|
d5ed05017b | ||
|
|
dd45a0f4f1 | ||
|
|
d08ef6184a | ||
|
|
78ee38b39f | ||
|
|
1494d2cb5e | ||
|
|
31921ee99b | ||
|
|
b3f959ac55 | ||
|
|
0031438bce | ||
|
|
556ab22c79 | ||
|
|
78e7e1c49e | ||
|
|
c5ff3e2a3f | ||
|
|
7f7880a4a7 | ||
|
|
5f3a0bec46 | ||
|
|
c3896ba3d6 | ||
|
|
7327ebb37c | ||
|
|
2e855f62c9 | ||
|
|
4c65b8c561 | ||
|
|
c64c76c577 | ||
|
|
22e93589cb | ||
|
|
f129311b2e | ||
|
|
70d165e8bc | ||
|
|
b555a300bc | ||
|
|
829527244e | ||
|
|
50352fb779 | ||
|
|
8dcb45fc9d | ||
|
|
b18544c69b | ||
|
|
8825661360 | ||
|
|
a4fdced10f | ||
|
|
97490bef3b | ||
|
|
a844d7b428 | ||
|
|
686fca99e1 | ||
|
|
775e8f58f6 | ||
|
|
409d769eb7 | ||
|
|
cff416dd87 | ||
|
|
65de8c35e6 | ||
|
|
815802fb7c | ||
|
|
53842a50d2 | ||
|
|
5fc2f93b38 | ||
|
|
32be252261 | ||
|
|
04014da21f | ||
|
|
276e84ad0b | ||
|
|
cd34236716 | ||
|
|
a26ac2c4fc | ||
|
|
497ea3b669 | ||
|
|
887063394a | ||
|
|
04bf3c234f | ||
|
|
e6daf00528 | ||
|
|
c509b7f693 | ||
|
|
8d46e2a7db | ||
|
|
3756561458 | ||
|
|
ff4b69b511 | ||
|
|
761b2a7af4 | ||
|
|
7a793cbbcc | ||
|
|
113ef4b9a4 | ||
|
|
3524010d7a | ||
|
|
8993bce53f | ||
|
|
8813b2c066 | ||
|
|
5c4805b822 | ||
|
|
28f1a965b8 | ||
|
|
565aff35f6 | ||
|
|
3549c2ef2a | ||
|
|
8afc5aea89 | ||
|
|
af51c4316f | ||
|
|
dd358da65d | ||
|
|
91a1676e89 | ||
|
|
e7698f15bd | ||
|
|
c7b34fe107 | ||
|
|
5846119ec9 | ||
|
|
41b118ab87 | ||
|
|
9c65644adc | ||
|
|
3d94846a61 | ||
|
|
90bc8ed029 | ||
|
|
40be80981d | ||
|
|
b853c24c96 | ||
|
|
5f26d75e18 | ||
|
|
8f43b92496 | ||
|
|
7ccdc942fe | ||
|
|
2b756f6ea4 | ||
|
|
17e4c61067 | ||
|
|
fad62ca045 | ||
|
|
5402578ce6 | ||
|
|
8ee780566e | ||
|
|
08334b22c3 | ||
|
|
f4b1b6585b | ||
|
|
66b38b7f19 | ||
|
|
54e2005224 | ||
|
|
d5a1305192 | ||
|
|
4bb497ea9c | ||
|
|
97ab1df762 | ||
|
|
058e31c290 | ||
|
|
d173357b51 | ||
|
|
e134c89c3a | ||
|
|
a35ef025dc | ||
|
|
ce0a62b0c3 | ||
|
|
1a3d7a1e0d | ||
|
|
9cc7e5ff3c | ||
|
|
113d6220bb | ||
|
|
e40926c08d | ||
|
|
e11d8f3252 | ||
|
|
41d029937a | ||
|
|
1251944dcc | ||
|
|
2c09ce097c | ||
|
|
9e84ce4c07 | ||
|
|
a8ad16758c | ||
|
|
9f11af0d4d | ||
|
|
906f58473d | ||
|
|
e39ecb7ac9 | ||
|
|
86b0e11e95 | ||
|
|
2076bb2a93 | ||
|
|
7c9bd77603 | ||
|
|
b1c57fc5c7 | ||
|
|
89cb9074ab | ||
|
|
590bad3bcc | ||
|
|
60bae12024 | ||
|
|
e48ea57db9 | ||
|
|
3838d052f0 | ||
|
|
c8f616ebee | ||
|
|
33a2929ab6 | ||
|
|
df3a24eab7 | ||
|
|
6dbe84dee4 | ||
|
|
6e20800a98 | ||
|
|
dfedc31c04 | ||
|
|
dae7f09650 | ||
|
|
38a042d435 | ||
|
|
c3a438ee59 | ||
|
|
bcb244c245 | ||
|
|
cddcedbf1c | ||
|
|
eac913eb07 | ||
|
|
ee7ef71855 | ||
|
|
2181e92e44 | ||
|
|
746e919aff | ||
|
|
71f7299337 | ||
|
|
21a23e79b0 | ||
|
|
52b5063066 | ||
|
|
8f824a1868 | ||
|
|
9d540f4bfe | ||
|
|
b8873039a4 | ||
|
|
f1836c70cd | ||
|
|
29a0b3ead8 | ||
|
|
31cc3f3ac3 | ||
|
|
8de3cd300d | ||
|
|
b57ddc0e5c | ||
|
|
7bc47a58fe | ||
|
|
d62b6e2ae7 | ||
|
|
5eb9f6b6d5 | ||
|
|
bf0d799caf | ||
|
|
29bea9517d | ||
|
|
a2a96f9752 | ||
|
|
c1447f5bad | ||
|
|
79032de032 | ||
|
|
b81e57db28 | ||
|
|
95441dc3bb | ||
|
|
fe7e9b2398 | ||
|
|
91e9f6097b | ||
|
|
0f0aa89f75 | ||
|
|
8da3a107fb | ||
|
|
7ba1571400 | ||
|
|
d2026a417f | ||
|
|
fc59481476 | ||
|
|
21328c2dee | ||
|
|
317fe91359 | ||
|
|
68f705b135 | ||
|
|
c01645c4e2 | ||
|
|
ae5c9fe8ec | ||
|
|
de6d2b1595 | ||
|
|
47a5825d3e | ||
|
|
78c3dd0eed | ||
|
|
d1e1609b8a | ||
|
|
a32c4b5172 | ||
|
|
c34e4f5ff0 | ||
|
|
aab8b34970 | ||
|
|
1a1f6d80a1 | ||
|
|
fe9e3274fe | ||
|
|
9f14e97d58 | ||
|
|
fab5dbea04 | ||
|
|
1bcc535e99 | ||
|
|
6a448668ad | ||
|
|
166a606ba9 | ||
|
|
48c33ae1ab | ||
|
|
e7b53388a0 | ||
|
|
f14fa22222 | ||
|
|
c16a1753a5 | ||
|
|
a53d828a73 | ||
|
|
298569a2c2 | ||
|
|
8e9cd7a8a5 | ||
|
|
2010acaaa2 | ||
|
|
ceec38d3f1 | ||
|
|
bd310d1e5d | ||
|
|
3d858eb761 | ||
|
|
171a0d38f8 | ||
|
|
63ad3256ee | ||
|
|
6e7d2b2d6a | ||
|
|
a819dff19a | ||
|
|
6532c2dde4 | ||
|
|
b1a5b34725 | ||
|
|
d2d1a75448 | ||
|
|
d3a93652f3 | ||
|
|
2204bd52f4 | ||
|
|
300a1aff9f | ||
|
|
63d86917ae | ||
|
|
9b77bbdd0b | ||
|
|
a02d78bde5 | ||
|
|
bd15cbe375 | ||
|
|
c5d67cd4f9 | ||
|
|
7211426292 | ||
|
|
67382a4c4c | ||
|
|
351e81231f | ||
|
|
0ecd76329c | ||
|
|
f5da94a278 | ||
|
|
e0f2fc1e1b | ||
|
|
8094e6e761 | ||
|
|
a7c4136d60 | ||
|
|
67f9f9ec76 | ||
|
|
90232c82c0 | ||
|
|
0cdb780b88 | ||
|
|
a6d996b83f | ||
|
|
b6e3176ed0 | ||
|
|
e762ebb88b | ||
|
|
163581cbcd | ||
|
|
e9c3ee4661 | ||
|
|
90de4e4fe3 | ||
|
|
cb9ac33fbe | ||
|
|
319ccbad01 | ||
|
|
4e862053e8 | ||
|
|
9c0c8ec239 | ||
|
|
715aadb9d3 | ||
|
|
f94458690e | ||
|
|
134b5e6491 | ||
|
|
c4ba2bc326 | ||
|
|
1e02322382 | ||
|
|
81f87e3960 | ||
|
|
efaeac3c14 | ||
|
|
bb06ced05b | ||
|
|
ed564f64ff | ||
|
|
830f40edee | ||
|
|
ec9a148e97 | ||
|
|
5efdfc53ff | ||
|
|
c1bb1694a0 | ||
|
|
82b5613335 | ||
|
|
123a581583 | ||
|
|
346d7ed9f0 | ||
|
|
b79a6c390e | ||
|
|
fae74a8b1e | ||
|
|
c434b5e6f0 | ||
|
|
e7ed88e10e | ||
|
|
78971d1ccb | ||
|
|
8a4bb2e80d | ||
|
|
c25b5aac9d | ||
|
|
4856c5cb1f | ||
|
|
065e674a68 | ||
|
|
20be416fa6 | ||
|
|
4617f44cda | ||
|
|
0ae4c2fac9 | ||
|
|
9389f2cc91 | ||
|
|
52c2ba69cb | ||
|
|
8591758cd7 | ||
|
|
02d217402c | ||
|
|
52eb0b9c21 | ||
|
|
5698da9c5b | ||
|
|
aa19472512 | ||
|
|
11288ce782 | ||
|
|
383897ee27 | ||
|
|
7bed0231d2 | ||
|
|
d8359e1049 | ||
|
|
35fb8b1ee6 | ||
|
|
793b14885b | ||
|
|
bb3e311fb7 | ||
|
|
65b53dd7f4 | ||
|
|
0e75106e1a | ||
|
|
619dd0d472 | ||
|
|
22d33847c3 | ||
|
|
6d1b654d0c | ||
|
|
43dbc69358 | ||
|
|
5c24813d36 | ||
|
|
c8dd7f2970 | ||
|
|
d13104bb5a | ||
|
|
5d85810e58 | ||
|
|
b00014771d | ||
|
|
b133f63736 | ||
|
|
e45563ad66 | ||
|
|
cf639cbf98 | ||
|
|
b365b7c810 | ||
|
|
b394f484d3 | ||
|
|
ada336ceea | ||
|
|
26ebac450f | ||
|
|
6e33c071ef | ||
|
|
5bd21a2bd4 | ||
|
|
e2547122a6 | ||
|
|
edad83cc37 | ||
|
|
b0190f462c | ||
|
|
c936823912 | ||
|
|
7acaf55331 | ||
|
|
6122efc870 | ||
|
|
7f180eb6d9 | ||
|
|
72091670fd | ||
|
|
2b6ebca4c3 | ||
|
|
e44d571331 | ||
|
|
526fa1aff5 | ||
|
|
11ba077726 | ||
|
|
ffb97006ec | ||
|
|
8dbbef37bb | ||
|
|
92b98b95ee | ||
|
|
083262fedb | ||
|
|
fe9bc05937 | ||
|
|
726b1cd42b | ||
|
|
8de7b20ba1 | ||
|
|
6d2cb0aea3 | ||
|
|
c8273b2052 | ||
|
|
60b6c245ef | ||
|
|
fcf098f494 | ||
|
|
74ce8f4e98 | ||
|
|
6e3a36d8c4 | ||
|
|
6f0a2a5a73 | ||
|
|
111a30f13c | ||
|
|
202f55c2ff | ||
|
|
8ad8fd07b3 | ||
|
|
a1931f59f8 | ||
|
|
e7b70c7992 | ||
|
|
7563148182 | ||
|
|
a252ff717e | ||
|
|
7d41782013 | ||
|
|
91976cc415 | ||
|
|
8afa2dbf75 | ||
|
|
3983c45cac | ||
|
|
df5c350765 | ||
|
|
de8b9df4da | ||
|
|
3be815c749 | ||
|
|
71b19e125e | ||
|
|
8fc3dfed1f | ||
|
|
30357b9cf7 | ||
|
|
a81edc122d | ||
|
|
994526e2c8 | ||
|
|
203b161e82 | ||
|
|
47b8ec6f96 | ||
|
|
302914c789 | ||
|
|
f77bd4f0c6 | ||
|
|
af4327ff27 | ||
|
|
5cbed128e6 | ||
|
|
dd5fde4069 | ||
|
|
220e1d4e24 | ||
|
|
3481431eb5 | ||
|
|
b94604ae71 | ||
|
|
fe406487de | ||
|
|
99218d02ad | ||
|
|
8cd1668ff2 | ||
|
|
c0f26204c2 | ||
|
|
ef3a7d0490 | ||
|
|
f9558d4701 | ||
|
|
9d9ebe7b9e | ||
|
|
918b40afbe | ||
|
|
6da8821f2c | ||
|
|
90171064a8 | ||
|
|
6bbe887f8b | ||
|
|
b5fb14ec96 | ||
|
|
dd2ac00aa2 | ||
|
|
e7b5e7120a | ||
|
|
8bac4f9e53 | ||
|
|
023cb4ff11 | ||
|
|
1740599b61 | ||
|
|
24b244b612 | ||
|
|
c71d1f190e | ||
|
|
18e180eca5 | ||
|
|
d936339f07 | ||
|
|
ad16e92763 | ||
|
|
8a1335174d | ||
|
|
7b438e2889 | ||
|
|
550980719e | ||
|
|
3d80d1519e | ||
|
|
1d5fceeecd | ||
|
|
5b49908438 | ||
|
|
7e515afb79 | ||
|
|
dee5085f4a | ||
|
|
795144b288 | ||
|
|
5c3ff91f21 | ||
|
|
36ac006438 | ||
|
|
af1ebfea41 | ||
|
|
f653752026 | ||
|
|
4440581f14 | ||
|
|
2c47001b19 | ||
|
|
e1daa14087 | ||
|
|
308dc6e550 | ||
|
|
54638428e3 | ||
|
|
1508e6b055 | ||
|
|
9213249f7b | ||
|
|
63f6faf1e8 | ||
|
|
c2cbfc5558 | ||
|
|
ef1b65de09 | ||
|
|
ae81092112 | ||
|
|
f09be24188 | ||
|
|
10c37946b1 | ||
|
|
adb43703ed | ||
|
|
92fba49a91 | ||
|
|
ac4bb32081 | ||
|
|
7927312f4f | ||
|
|
04a74b5a34 | ||
|
|
87fd23f443 | ||
|
|
9cb1ff7330 | ||
|
|
1ba19cc249 | ||
|
|
702d9689d6 | ||
|
|
8efa354fdb | ||
|
|
129e95fd3a | ||
|
|
ef7794c411 | ||
|
|
12b3775041 | ||
|
|
52b509fdc5 | ||
|
|
f9d0a8bf43 | ||
|
|
2349ef33d1 | ||
|
|
ccaf15268c | ||
|
|
616635f951 | ||
|
|
ce7daaf1cf | ||
|
|
fb9f5d986d | ||
|
|
3148906cfe | ||
|
|
6c057f4fd0 | ||
|
|
3014bba66d | ||
|
|
97958d5e22 | ||
|
|
1a7452cc27 | ||
|
|
34ba5d0570 | ||
|
|
c0fe9813dc | ||
|
|
fd29eb7c15 | ||
|
|
1bee085b33 | ||
|
|
1d06e6d9c8 | ||
|
|
bdf8038148 | ||
|
|
62fb2c91a1 | ||
|
|
d1eeba67fc | ||
|
|
2ca8112daf | ||
|
|
546f10bc82 | ||
|
|
ec7f1d561d | ||
|
|
8d54bc3a2f | ||
|
|
1d8f77f799 | ||
|
|
1c6795dd55 | ||
|
|
4d895a56e4 | ||
|
|
bdd56c91f7 | ||
|
|
be172b5bd9 | ||
|
|
d224d71f59 | ||
|
|
065630b975 | ||
|
|
decea604ab | ||
|
|
004b3b2bdc | ||
|
|
dca0cc34e9 | ||
|
|
9afb165fc4 | ||
|
|
6a6eb4a714 | ||
|
|
629f4e0c81 | ||
|
|
7cbe9bf7fc | ||
|
|
fc33312e79 | ||
|
|
563c0ea9c4 | ||
|
|
10d1a902d9 | ||
|
|
4079257739 | ||
|
|
91aba13c0d | ||
|
|
2cca4f2f0e | ||
|
|
d2e8dc1439 | ||
|
|
f3f9d0dd71 | ||
|
|
f6845a046f | ||
|
|
a50a4f274f | ||
|
|
6b6d847756 | ||
|
|
65d1f59e9b | ||
|
|
f39e6ccbc9 | ||
|
|
747b62d78c | ||
|
|
af0f3ac25f | ||
|
|
6aefe99c2f | ||
|
|
61c5595a68 | ||
|
|
3d11611699 | ||
|
|
38b4ed4b96 | ||
|
|
34308a5f9a | ||
|
|
118cc10804 | ||
|
|
ae3386f2d5 | ||
|
|
bf1ccc62f5 | ||
|
|
1ce4dd932e | ||
|
|
ff0bb49926 | ||
|
|
018642cbb8 | ||
|
|
bf1d0655c4 | ||
|
|
874b07f4d7 | ||
|
|
19fa210743 | ||
|
|
c8a6513d1c | ||
|
|
01288c8452 | ||
|
|
a60d3bb3a0 | ||
|
|
0470159c55 | ||
|
|
e1a93035fd | ||
|
|
c0b7a26905 | ||
|
|
704d770ec2 | ||
|
|
d42d2d99b1 | ||
|
|
ebfcc15927 | ||
|
|
4693aa3c93 | ||
|
|
07df657848 | ||
|
|
ef9436a1fb | ||
|
|
9318f42ce0 | ||
|
|
85c37d78a8 | ||
|
|
ff02ab0cea | ||
|
|
bd8bf026f0 | ||
|
|
644dc95380 | ||
|
|
f4935efed3 | ||
|
|
1712d34846 | ||
|
|
f0f1134be6 | ||
|
|
e0f7460e08 | ||
|
|
ae1d71dcd7 | ||
|
|
3cfe38c948 | ||
|
|
c1f0b63101 | ||
|
|
0a658d7cab | ||
|
|
3ed2582a9b | ||
|
|
2ea68776f6 | ||
|
|
c1ff22298f | ||
|
|
e94d2f93ea | ||
|
|
e7f6f8a4c7 | ||
|
|
4ffe75683b | ||
|
|
687920743d | ||
|
|
1a93149643 | ||
|
|
11d6654763 | ||
|
|
512d65c17a | ||
|
|
5a274061f3 | ||
|
|
a398ac7a8d | ||
|
|
41df4c4b9f | ||
|
|
7e7909cec1 | ||
|
|
7322bd66df | ||
|
|
39affa5528 | ||
|
|
374111c88d | ||
|
|
72fc0015bf | ||
|
|
7ab406b3f5 | ||
|
|
df280cd574 | ||
|
|
0a30300bde | ||
|
|
cb8b99d6ae | ||
|
|
1d3f29d385 | ||
|
|
c154113474 | ||
|
|
e02a6fc5e6 | ||
|
|
78040573d1 | ||
|
|
981f9ace04 | ||
|
|
208f7ac8f0 | ||
|
|
661f8db10b | ||
|
|
b48168ffec | ||
|
|
908ebb46d9 | ||
|
|
58bcb38617 | ||
|
|
1de023f9df | ||
|
|
5815ef2f75 | ||
|
|
e5dab8b1ca | ||
|
|
56c4b7fbe6 | ||
|
|
365b65e5b2 | ||
|
|
2193cd8d3e | ||
|
|
8b221bc055 | ||
|
|
77e4d2b566 | ||
|
|
88a29b7599 | ||
|
|
64525750b6 | ||
|
|
30499123f1 | ||
|
|
79703042ff | ||
|
|
ccd5605d97 | ||
|
|
908277065b | ||
|
|
f2d4eb0486 | ||
|
|
f784b659e8 | ||
|
|
e60a7a59f9 | ||
|
|
cab81ca170 | ||
|
|
a82e2c7ea1 | ||
|
|
a277380441 | ||
|
|
ad5710b1d8 | ||
|
|
3e3531a6ce | ||
|
|
a79ce23ae5 | ||
|
|
32f996d034 | ||
|
|
312eb4dffc | ||
|
|
1916456c84 | ||
|
|
9b8d37b577 | ||
|
|
05018e5f17 | ||
|
|
72ef918452 | ||
|
|
27fde82aff | ||
|
|
b27ffe6635 | ||
|
|
84cc4a094a | ||
|
|
e75600ee05 | ||
|
|
871ad2ea7e | ||
|
|
8b5f2e0f3f | ||
|
|
58d6ab7342 | ||
|
|
ca5fb2e010 | ||
|
|
da561b296e | ||
|
|
523da0dea0 | ||
|
|
0e4cc76a84 | ||
|
|
7e39a7ed89 | ||
|
|
4a26ea7ed9 | ||
|
|
7c83ba15b9 | ||
|
|
10bfbed4bb | ||
|
|
81550bd7a8 | ||
|
|
a430bb917e | ||
|
|
e1a579387f | ||
|
|
fe8df66c67 | ||
|
|
5bb410f80c | ||
|
|
29b79b9d4e | ||
|
|
0f8a8d1464 | ||
|
|
cc92a4b7ee | ||
|
|
3ffef6df51 | ||
|
|
78e4787b99 | ||
|
|
540512a156 | ||
|
|
7737dbc65d | ||
|
|
faa94d888d | ||
|
|
99e2487d2b | ||
|
|
f09d8ffe05 | ||
|
|
c486e3fe62 | ||
|
|
ba2d6b6709 | ||
|
|
a388a279bc | ||
|
|
5a64b19a06 | ||
|
|
53178b8d20 | ||
|
|
0347382a34 | ||
|
|
840460afe7 | ||
|
|
c851e54e1d | ||
|
|
5ef53b569c | ||
|
|
698b4166e8 | ||
|
|
1a9a36829b | ||
|
|
155b8598ff | ||
|
|
4153c91f84 | ||
|
|
a99b19a1b5 | ||
|
|
18429fda2c | ||
|
|
435da991ec | ||
|
|
ebd9aae924 | ||
|
|
414a84d5ec | ||
|
|
a3b2517603 | ||
|
|
43535082f6 | ||
|
|
e73af9980e | ||
|
|
0bdd5784ee | ||
|
|
48719ce29c | ||
|
|
29839d3b23 | ||
|
|
185ea7d6ac | ||
|
|
ac7623d4f5 | ||
|
|
3d119261cc | ||
|
|
169619c9b9 | ||
|
|
b991f787a2 | ||
|
|
215fc6803e | ||
|
|
a79c1c8246 | ||
|
|
1aa6419b1b | ||
|
|
02694f6720 | ||
|
|
f50aabb570 | ||
|
|
19b8d09bfd | ||
|
|
66fb999a8c | ||
|
|
0c5883b61b | ||
|
|
ef66d5172c | ||
|
|
118f9a34d8 | ||
|
|
b61dfac569 | ||
|
|
79160f9b5c | ||
|
|
d2523cff4a | ||
|
|
1839599769 | ||
|
|
29e1b56e78 | ||
|
|
47e647c712 | ||
|
|
a87b0b0447 | ||
|
|
96b8fcad3a | ||
|
|
cf0fa0024c | ||
|
|
aba5ed9271 | ||
|
|
63141f3578 | ||
|
|
612041635d | ||
|
|
efcc0fd5cb | ||
|
|
b0724f5b66 | ||
|
|
66a2b4b6d2 | ||
|
|
77b4dfa50a | ||
|
|
1e6e7fd6d8 | ||
|
|
46a1b673ba | ||
|
|
7230325e62 | ||
|
|
af8e733cfb | ||
|
|
26ab43c57f | ||
|
|
15ed4ef1ce | ||
|
|
0ac76766bd | ||
|
|
abb97ad99f | ||
|
|
6cdc26b55b | ||
|
|
9f19fd3c75 | ||
|
|
61dedb7e7b | ||
|
|
65d2b8b2cb | ||
|
|
1d3f206660 | ||
|
|
ab46899e72 | ||
|
|
723a423b06 | ||
|
|
a837b97870 | ||
|
|
8be0789f25 | ||
|
|
99d017144d | ||
|
|
891f71006b | ||
|
|
d25017216a | ||
|
|
83b7c7b054 | ||
|
|
283c2a17df | ||
|
|
44de049f3b | ||
|
|
920d08f039 | ||
|
|
0cf1e338c2 | ||
|
|
293a2b800e | ||
|
|
650a099e19 | ||
|
|
46c2c8616e | ||
|
|
6295a5c878 | ||
|
|
39d8e19e16 | ||
|
|
9d3a44d110 | ||
|
|
54d46719c2 | ||
|
|
dbed2c17db | ||
|
|
f772ef2efd | ||
|
|
2f28f96f8d | ||
|
|
1f2bcf7c34 | ||
|
|
a9a5850fd4 | ||
|
|
09f3bfd8f4 | ||
|
|
3813fbf8f2 | ||
|
|
16001f8d4e | ||
|
|
49d3dbad9a | ||
|
|
56f32a4da2 | ||
|
|
a297b8d6d8 | ||
|
|
d365f48192 | ||
|
|
df6d955261 | ||
|
|
9fcaf9f5cc | ||
|
|
43a7b7faa5 | ||
|
|
69253385c5 | ||
|
|
cea7d61481 | ||
|
|
41f9a2e6e2 | ||
|
|
31d3659098 | ||
|
|
c12bfb157e | ||
|
|
a55dabb1cd | ||
|
|
06fec3c280 | ||
|
|
f733a27ad7 | ||
|
|
7ea39eb545 | ||
|
|
a85f898ef5 | ||
|
|
519975f3cf | ||
|
|
0e9a071e96 | ||
|
|
3917e66fd8 | ||
|
|
9f48e2d9f0 | ||
|
|
215eadcf2b | ||
|
|
b09e4938b7 | ||
|
|
d3d706ea68 | ||
|
|
8268761ef4 | ||
|
|
2850c35bdc | ||
|
|
24d8635dac | ||
|
|
8314a115bb | ||
|
|
b8e2423dbd | ||
|
|
52c67f4139 | ||
|
|
e895156d03 | ||
|
|
89abc18465 | ||
|
|
355dfbbb25 | ||
|
|
6d15640ae4 | ||
|
|
ad4ab5a54c | ||
|
|
3c5c86c4c5 | ||
|
|
09b91c8826 | ||
|
|
13ea48bff8 | ||
|
|
26bb430d6e | ||
|
|
82da54740f | ||
|
|
e02b10d12a | ||
|
|
1573045157 | ||
|
|
bf31c333a6 | ||
|
|
4229bca978 | ||
|
|
13bd929755 | ||
|
|
7f6c0346af | ||
|
|
0387e66066 | ||
|
|
04bae8dc37 | ||
|
|
559506fc90 | ||
|
|
f2b6d481a1 | ||
|
|
c66aa92365 | ||
|
|
e24ef68d8d | ||
|
|
0cacf83ae5 | ||
|
|
2dc66b329b | ||
|
|
10dc8ea2bf | ||
|
|
303f9174c9 | ||
|
|
c5dd5f2b6f | ||
|
|
ab77705ca2 | ||
|
|
10c89ebf73 | ||
|
|
ecce994921 | ||
|
|
5dd4f5ea62 | ||
|
|
93bb5db019 | ||
|
|
65ff008ccf | ||
|
|
f0c48d23fe | ||
|
|
975b52a43d | ||
|
|
079b0d5024 | ||
|
|
99ccb12b8c | ||
|
|
0bb7d24e07 | ||
|
|
b7a9f9ea38 | ||
|
|
21aee98f9f | ||
|
|
9a5a4be64a | ||
|
|
db7f8501d0 | ||
|
|
61d15cbf9e | ||
|
|
39c9c695f1 | ||
|
|
d97654581b | ||
|
|
2ee70fd3a8 | ||
|
|
b336ec4188 | ||
|
|
c77e1be1c3 | ||
|
|
986f688468 | ||
|
|
1d9429b762 | ||
|
|
d122d90a47 | ||
|
|
2c534d4d20 | ||
|
|
b7e0e5c5eb | ||
|
|
2915b12181 | ||
|
|
ed589ef65b | ||
|
|
0d5bfdafdf | ||
|
|
fbf3346052 | ||
|
|
3ff206f84d | ||
|
|
a3b0fce961 | ||
|
|
8130280b35 | ||
|
|
fea3020d9a | ||
|
|
1eb9d163fa | ||
|
|
e3e7c2e38b | ||
|
|
27e1d6a9cd | ||
|
|
2ec4cdc4fb | ||
|
|
35a7506c26 | ||
|
|
16242be74e | ||
|
|
3fcbe440d3 | ||
|
|
4ef0755039 | ||
|
|
a4ed906dd1 | ||
|
|
416a8d9a94 | ||
|
|
d8803aaac2 | ||
|
|
01f6615ca0 | ||
|
|
fd93ee3263 | ||
|
|
32bfd298a1 | ||
|
|
5de2e7a3bd | ||
|
|
6e8c133a99 | ||
|
|
956f18430b | ||
|
|
76c9933baf | ||
|
|
6c6a988c2b | ||
|
|
d829e39b66 | ||
|
|
0e401df3c0 | ||
|
|
9072c37e95 | ||
|
|
2eff11efbf | ||
|
|
6a7a02d220 | ||
|
|
d6349bbc5c | ||
|
|
9a2f887db1 | ||
|
|
71d30ff95f | ||
|
|
3135247936 | ||
|
|
0d629221fe | ||
|
|
181ea4b487 | ||
|
|
fa769665df | ||
|
|
00f5198bed | ||
|
|
785a291614 | ||
|
|
92209c7125 | ||
|
|
690007be5c | ||
|
|
e6bcf0e12f | ||
|
|
ca6a903b8f | ||
|
|
50f790dd6c | ||
|
|
ed6aa843b7 | ||
|
|
52a27699f1 | ||
|
|
dba08714bf | ||
|
|
edabc5c33f | ||
|
|
6b6c70f1df | ||
|
|
93061eb695 | ||
|
|
1f9133c188 | ||
|
|
e7dcc6df3c | ||
|
|
fbd58b4723 | ||
|
|
e992138456 | ||
|
|
058f79d0a1 | ||
|
|
5c448c05a9 | ||
|
|
e966cd3068 | ||
|
|
6d3351b2f7 | ||
|
|
61a1d61901 | ||
|
|
4ff3231451 | ||
|
|
ce832d2805 | ||
|
|
b11730303e | ||
|
|
565f9ab314 | ||
|
|
9435434cf9 | ||
|
|
a58ca6f66d | ||
|
|
c769bbc405 | ||
|
|
0379143829 | ||
|
|
ee235cf231 | ||
|
|
04b29d43df | ||
|
|
dc0715da57 | ||
|
|
902323a75b | ||
|
|
3a5b18f3dd | ||
|
|
ce7d84aa2f | ||
|
|
981fb9585d | ||
|
|
23b2dcba70 | ||
|
|
e4a437f54c | ||
|
|
d4167f9595 | ||
|
|
88475a2b76 | ||
|
|
c26ce4b7dc | ||
|
|
5960e7cbd9 | ||
|
|
68872ab9fb | ||
|
|
64f12b42b8 | ||
|
|
8c0d542d7d | ||
|
|
c016b67c3a | ||
|
|
49d127e957 | ||
|
|
e7353c4757 | ||
|
|
3106685972 | ||
|
|
2b7017fae2 | ||
|
|
da0b01e23f | ||
|
|
d450ea4beb | ||
|
|
9210734911 | ||
|
|
ebae80bb7e | ||
|
|
dcade3a5c7 | ||
|
|
f51c3e9e3f | ||
|
|
b34298e45b | ||
|
|
9fa78897bc | ||
|
|
9642babb7e | ||
|
|
d2d1aed393 | ||
|
|
58d43063ec | ||
|
|
6a5ce1dce3 | ||
|
|
f3c7c0761b | ||
|
|
27b3124f77 | ||
|
|
aa516529e7 | ||
|
|
2283ad0eb2 | ||
|
|
cece9ad46c | ||
|
|
d37da2e8ff | ||
|
|
9d9bfc65d5 | ||
|
|
d7d6e4ac6f | ||
|
|
46d93f986e | ||
|
|
2549a97a7e | ||
|
|
fc2cca5fdb | ||
|
|
ef1ec56270 | ||
|
|
cf63a2e7d1 | ||
|
|
ed9b99e2f0 | ||
|
|
33340b80f9 | ||
|
|
ec94bb3d2a | ||
|
|
bc2fe2bec8 | ||
|
|
a87a206c04 | ||
|
|
45083e19b0 | ||
|
|
de627c19c5 | ||
|
|
595e57dcdd | ||
|
|
dadcc69900 | ||
|
|
2c160d1ac9 | ||
|
|
292b074d8c | ||
|
|
2beff45811 | ||
|
|
469c36da46 | ||
|
|
8afe2d52a0 | ||
|
|
51273e9151 | ||
|
|
1517c96841 | ||
|
|
b7afbd3500 | ||
|
|
39d6c42c71 | ||
|
|
9f50c9355a | ||
|
|
fd73f132bc | ||
|
|
1f99e592b7 | ||
|
|
a90c4eded8 | ||
|
|
1638538fb4 | ||
|
|
da0892f9e3 | ||
|
|
66cab6c1ab | ||
|
|
535078b0ab | ||
|
|
d584c6b1e8 | ||
|
|
530a76a9a2 | ||
|
|
2a0181fd92 | ||
|
|
86c5b9a55d | ||
|
|
0a6b890250 | ||
|
|
af7f1f30ad | ||
|
|
f6c0f840fe | ||
|
|
0e54359db8 | ||
|
|
373f1574bd | ||
|
|
6f5ffef22f | ||
|
|
451206525e | ||
|
|
41f957ea3b | ||
|
|
91b0080428 | ||
|
|
170d6dd7e5 | ||
|
|
15ba385398 | ||
|
|
0b93d58a32 | ||
|
|
67078c4552 | ||
|
|
ab17de7efd | ||
|
|
1145260619 | ||
|
|
066952ed04 | ||
|
|
6ccf7a0fb8 | ||
|
|
7f49ecefb1 | ||
|
|
43324391ff | ||
|
|
89af8216b8 | ||
|
|
be9e30e6d8 | ||
|
|
85cfd6fb7c | ||
|
|
e694e20025 | ||
|
|
7618dd335a | ||
|
|
eca09253d3 | ||
|
|
96c32b3275 | ||
|
|
94a4fec3c6 | ||
|
|
a8386d3937 | ||
|
|
4b7aa4bece | ||
|
|
4c604f4009 | ||
|
|
98cf2f7a5a | ||
|
|
804467b1ba | ||
|
|
9fbf775299 | ||
|
|
5c93753b7e | ||
|
|
27016eaa28 | ||
|
|
859281784a | ||
|
|
7ae7b8904c | ||
|
|
559e0c6350 | ||
|
|
d942badec8 | ||
|
|
f443ae9203 | ||
|
|
d37dc37c2d | ||
|
|
236d895498 | ||
|
|
3bb0ef6f39 | ||
|
|
a0ced96d1d | ||
|
|
4ef1e91c78 | ||
|
|
2333fa2d9b | ||
|
|
37bef2d824 | ||
|
|
6427235aec | ||
|
|
535c8e9f7b | ||
|
|
890e166d1c | ||
|
|
337ceb7477 | ||
|
|
f82f3e9b8c | ||
|
|
3c709955e3 | ||
|
|
595e6090c7 | ||
|
|
1d98eb5160 | ||
|
|
7172c92f02 | ||
|
|
874d9ddbc3 | ||
|
|
73b84a9690 | ||
|
|
c78f50ab8c | ||
|
|
8407cd3ea5 | ||
|
|
9caa42975d | ||
|
|
44fdb6c24f | ||
|
|
c8984d1bc9 | ||
|
|
be66a1690f | ||
|
|
13838ec8d6 | ||
|
|
2d72a6fd12 | ||
|
|
e24ec04a75 | ||
|
|
9a0aa9ce97 | ||
|
|
643a4ce666 | ||
|
|
189098541e | ||
|
|
e8a1d4c66c | ||
|
|
08dd6147f2 | ||
|
|
7e55808a05 | ||
|
|
6b39c1a280 | ||
|
|
a38d0054df | ||
|
|
e39072d3bc | ||
|
|
b4e5e3fda7 | ||
|
|
e8af173b4f | ||
|
|
fef0d8761f | ||
|
|
76b48378ab | ||
|
|
a7e042b5f4 | ||
|
|
ab8f51622f | ||
|
|
fae7e76721 | ||
|
|
012b0f1a77 | ||
|
|
cc5b1c8040 | ||
|
|
2cf932600e | ||
|
|
d2dac1915d | ||
|
|
5d52b6c4ec | ||
|
|
7cfba7f2ef | ||
|
|
41a8e41463 | ||
|
|
07e2c41eb7 | ||
|
|
9fe0d8417b | ||
|
|
905908bcf1 | ||
|
|
8e87c25de1 | ||
|
|
5a76780cd9 | ||
|
|
10fd911dcc | ||
|
|
2333d81276 | ||
|
|
9ee76213e1 | ||
|
|
4ec1441189 | ||
|
|
38e2e50a29 | ||
|
|
95950700d8 | ||
|
|
0494bfc1cf | ||
|
|
4906e06bf1 | ||
|
|
699546f531 | ||
|
|
d5ec601d75 | ||
|
|
5f26f68a04 | ||
|
|
219fe59aa2 | ||
|
|
d115d57d1a | ||
|
|
6307099bc1 | ||
|
|
e0a5efdfaf | ||
|
|
e014e516e6 | ||
|
|
4d71eb5de0 | ||
|
|
0db6853fde | ||
|
|
197dd9f299 | ||
|
|
99aaeefe8e | ||
|
|
712804d2a8 | ||
|
|
a393284acd | ||
|
|
719a0e5efa | ||
|
|
6221fd67e1 | ||
|
|
56c7c76ac6 | ||
|
|
a5aae51b1a | ||
|
|
14691fa367 | ||
|
|
38b2a94231 | ||
|
|
4c031c8f83 | ||
|
|
b100498dd7 | ||
|
|
50a8e67917 | ||
|
|
dfe0a0b1c7 | ||
|
|
52dac2252a | ||
|
|
5aee84e84f | ||
|
|
3e7d7f3801 | ||
|
|
184aba4de4 | ||
|
|
dd482b246f | ||
|
|
717e6351e7 | ||
|
|
c36b1cf50f | ||
|
|
896da66caf | ||
|
|
f72b7763c1 | ||
|
|
46c4a62046 | ||
|
|
da3a1d2ce4 | ||
|
|
35d6b51770 | ||
|
|
35cbfdd736 | ||
|
|
a7b2522c87 | ||
|
|
fe8baf2ca4 | ||
|
|
300aef413c | ||
|
|
467c681117 | ||
|
|
31a89c0641 | ||
|
|
5f94543d9b | ||
|
|
10fd74b4c4 | ||
|
|
9bc705bfa1 | ||
|
|
307d9892b2 | ||
|
|
93f4ea51fd | ||
|
|
a6f83493f8 | ||
|
|
7e17fdf851 | ||
|
|
c76da10747 | ||
|
|
7c558ff842 | ||
|
|
a011dc4dea | ||
|
|
fa9acef279 | ||
|
|
ed0936059b | ||
|
|
77597fd7a9 | ||
|
|
8d1bf3d245 | ||
|
|
e7503e8f55 | ||
|
|
9b6be66623 | ||
|
|
6bc7862232 | ||
|
|
69a8a9246d | ||
|
|
b87b801b04 | ||
|
|
dba04a407a | ||
|
|
e281a39512 | ||
|
|
e0364183e2 | ||
|
|
6af61cbb3c | ||
|
|
67dc012caa | ||
|
|
8753655c9c | ||
|
|
873dc1f76d | ||
|
|
d781130b49 | ||
|
|
effbf241e6 | ||
|
|
396a67b198 | ||
|
|
1939e06a24 | ||
|
|
3f21e73b2e | ||
|
|
bc9529c488 | ||
|
|
19af05ebee | ||
|
|
eb11e5b2f8 | ||
|
|
b85137096d | ||
|
|
77e9d72f92 | ||
|
|
ec344cbaa7 | ||
|
|
4cb2f42d71 | ||
|
|
98723b76cf | ||
|
|
45ac7f084b | ||
|
|
3626777bd1 | ||
|
|
5fb1070567 | ||
|
|
f0bcf975b6 | ||
|
|
0ca28dc630 | ||
|
|
8196706538 | ||
|
|
411a6efeb5 | ||
|
|
ffad0cdec9 | ||
|
|
ca519fe4e1 | ||
|
|
6b0855cff8 | ||
|
|
e8a3309822 | ||
|
|
ce8c4dfc4c | ||
|
|
a1b99c46c7 | ||
|
|
1ee50fdfde | ||
|
|
05850f61d2 | ||
|
|
12d8c8e959 | ||
|
|
2b4933bd5c | ||
|
|
500e713764 | ||
|
|
2ca50776ec | ||
|
|
bd31513e5d | ||
|
|
6df04390bb | ||
|
|
49511276fd | ||
|
|
c4e91df1ed | ||
|
|
ed8a576ac3 | ||
|
|
5c6a7acb7d | ||
|
|
850db54a3b | ||
|
|
985593b7c2 | ||
|
|
857b5d1bfd | ||
|
|
498e86aa7a | ||
|
|
79d0fd28b0 | ||
|
|
426ca90515 | ||
|
|
e7328f1be5 | ||
|
|
baccc8051e | ||
|
|
f1c7690270 | ||
|
|
d700d1530b | ||
|
|
b60f44eede | ||
|
|
54ab788e93 | ||
|
|
ac2dd04959 | ||
|
|
9a449c35ac | ||
|
|
2ff5d2f87c | ||
|
|
8f704dade8 | ||
|
|
3b3e323ce5 | ||
|
|
43ec144bf0 | ||
|
|
d5f94819d0 | ||
|
|
98aa7e4993 | ||
|
|
7c61911dba | ||
|
|
2bb107a5d7 | ||
|
|
0c4a2aedb7 | ||
|
|
e7e7272002 | ||
|
|
f672991089 | ||
|
|
473fa2671d | ||
|
|
85e1515080 | ||
|
|
84437a2732 | ||
|
|
71911f418f | ||
|
|
2741f78bd8 | ||
|
|
012e54161a | ||
|
|
409084c499 | ||
|
|
27595b2c4a | ||
|
|
62b6535625 | ||
|
|
e593f438b3 | ||
|
|
44d46a2cc5 | ||
|
|
66e30dd1a8 | ||
|
|
4c74351d21 | ||
|
|
5d2b14259c | ||
|
|
878d5444e1 | ||
|
|
ec24ec60c6 | ||
|
|
9c096d1f65 | ||
|
|
dba3e895da | ||
|
|
07726322df | ||
|
|
bef18c0d72 | ||
|
|
7c3332570a | ||
|
|
a3d4a52188 | ||
|
|
5530e89f41 | ||
|
|
a3554b3dfd | ||
|
|
674da36cd1 | ||
|
|
18ce12f60d | ||
|
|
a462ab36f8 | ||
|
|
8aa41edc3e | ||
|
|
4e0252553a | ||
|
|
8745d09890 | ||
|
|
ac3a02baea | ||
|
|
f2bbc170da | ||
|
|
d79a17adb0 | ||
|
|
680dbb04ce | ||
|
|
6c8403fa09 | ||
|
|
fec8eaef70 | ||
|
|
5f007029b4 | ||
|
|
4e76ceaa0f | ||
|
|
08e467baa9 | ||
|
|
f78a2250f1 | ||
|
|
84e8a459c8 | ||
|
|
b777f80249 | ||
|
|
73da203dbb | ||
|
|
55a31865d0 | ||
|
|
350697869b | ||
|
|
939b07422d | ||
|
|
88369d480a | ||
|
|
cf083e94e6 | ||
|
|
11e8f5acfc | ||
|
|
77b3a89eb2 | ||
|
|
f572f4265a | ||
|
|
0b66c8e1be | ||
|
|
a8328a3f56 | ||
|
|
8b42b17121 | ||
|
|
db418ce662 | ||
|
|
96fe566e08 | ||
|
|
a00f26d8a4 | ||
|
|
eeef63d1c3 | ||
|
|
0b88ea85ae | ||
|
|
c564118156 | ||
|
|
01a7dc2068 | ||
|
|
bb44ecec3f | ||
|
|
b7b24d7de6 | ||
|
|
dcca93e34f | ||
|
|
4124188505 | ||
|
|
eb5d59623b | ||
|
|
8631c06731 | ||
|
|
40eb40fd86 | ||
|
|
c486682a0e | ||
|
|
336da073f1 | ||
|
|
d2e187a239 | ||
|
|
f69de29353 | ||
|
|
61de8bc792 | ||
|
|
d31f19707b | ||
|
|
811312466f | ||
|
|
5ecfb3e962 | ||
|
|
77be5af4e5 | ||
|
|
c512a716d2 | ||
|
|
14f5630caf | ||
|
|
8532bdc172 | ||
|
|
9d74bf8e19 | ||
|
|
f75445b4d0 | ||
|
|
e04fa1b696 | ||
|
|
e07dfb5f67 | ||
|
|
3d1af05fc2 | ||
|
|
0e38925d74 | ||
|
|
c0f3343412 | ||
|
|
6a368840fa | ||
|
|
5361391b3b | ||
|
|
b80662bb24 | ||
|
|
4c272b6b8d | ||
|
|
96abdef3ad | ||
|
|
0b04bc79e9 | ||
|
|
cefbbdc3a8 | ||
|
|
83ffe05a99 | ||
|
|
b6f73083ef | ||
|
|
693736023e | ||
|
|
fed87c93ab | ||
|
|
f53b77b556 | ||
|
|
b858d82b0b | ||
|
|
ccb2af6d17 | ||
|
|
8d937ac8f5 | ||
|
|
2786fb65ed | ||
|
|
025532f579 | ||
|
|
5bd03be1ad | ||
|
|
40086f1ce0 | ||
|
|
be66ad9a4c | ||
|
|
0df5c01bb7 | ||
|
|
b85fa38b67 | ||
|
|
546246d7c5 | ||
|
|
d5536e78f4 | ||
|
|
908738b7ca | ||
|
|
797e02bffa | ||
|
|
d856685782 | ||
|
|
1400983d8c | ||
|
|
2a43c5d868 | ||
|
|
a67675c649 | ||
|
|
fc344ef636 | ||
|
|
2ffb2be083 | ||
|
|
069772f27d | ||
|
|
241ec50802 | ||
|
|
b943c9cd89 | ||
|
|
80625f8c3f | ||
|
|
58435d3460 | ||
|
|
7a7ac4a47f | ||
|
|
429dfa5ca4 | ||
|
|
e1f212d30d | ||
|
|
670450bcfb | ||
|
|
83e15e9e4a | ||
|
|
b309081d75 | ||
|
|
f1a7b35aa6 | ||
|
|
469c5ef06d | ||
|
|
07cbe561bd | ||
|
|
b69a70cfc3 | ||
|
|
923001928c | ||
|
|
75ed461c67 | ||
|
|
691aebce91 | ||
|
|
846947ff49 | ||
|
|
65e107d154 | ||
|
|
19b809b699 | ||
|
|
60fe02bb47 | ||
|
|
3e708059c6 | ||
|
|
424104bb66 | ||
|
|
62696b9ebe | ||
|
|
87403e8c2d | ||
|
|
7a5a607ff6 | ||
|
|
a195774726 | ||
|
|
8d0da7b55b | ||
|
|
58c315cd09 | ||
|
|
dd6c4602db | ||
|
|
d413e0c8ff | ||
|
|
b356658635 | ||
|
|
d92f0fc49b | ||
|
|
f206a28cf7 | ||
|
|
70cb0ac635 | ||
|
|
a0ece18876 | ||
|
|
4a22b03952 | ||
|
|
5ebf797c90 | ||
|
|
a20759f1f2 | ||
|
|
c273cab36e | ||
|
|
8306a04eda | ||
|
|
f078203136 | ||
|
|
15e89c3c4e | ||
|
|
595c4f1440 | ||
|
|
91a523dc23 | ||
|
|
8567c97c09 | ||
|
|
3621e99c61 | ||
|
|
d99b9cbe68 | ||
|
|
e319768fd3 | ||
|
|
c30ffcf096 | ||
|
|
7286248fef | ||
|
|
dc49f80cc3 | ||
|
|
1ae2da8054 | ||
|
|
090f4cb560 | ||
|
|
0914716b8e | ||
|
|
ab463c1773 | ||
|
|
36af4d11fc | ||
|
|
14d8dc6fa6 | ||
|
|
2032b48ad3 | ||
|
|
2af795f548 | ||
|
|
7b4edb2948 | ||
|
|
17a906fd03 | ||
|
|
af46a1fd96 | ||
|
|
393c0b8c11 | ||
|
|
e92f2f714d | ||
|
|
42606067c0 | ||
|
|
98bb65509d | ||
|
|
a2599e5d43 | ||
|
|
0390b1bc2a | ||
|
|
531f3ec741 | ||
|
|
127afdca0d | ||
|
|
c4df5bf988 | ||
|
|
30234e044d | ||
|
|
1e98ee8916 | ||
|
|
d5665c2577 | ||
|
|
762ec51a19 | ||
|
|
309ad724ec | ||
|
|
34397fe468 | ||
|
|
2f82647f5e | ||
|
|
7d7854424c | ||
|
|
eaeadbe933 | ||
|
|
cf3f0cee6c | ||
|
|
a5f7b7fa9c | ||
|
|
9f7c289e94 | ||
|
|
c2bba7a919 | ||
|
|
4bb37a7198 | ||
|
|
231694aabe | ||
|
|
2fb75e6d67 | ||
|
|
810af638a3 | ||
|
|
9223b8cfb1 | ||
|
|
b7df21a663 | ||
|
|
7d47ed4dbd | ||
|
|
65eaf92862 | ||
|
|
8a01bc0d7d | ||
|
|
1a8a3781dc | ||
|
|
eadfdc41c4 | ||
|
|
421abd4c5b | ||
|
|
ae1a0dcc73 | ||
|
|
63b8804e2d | ||
|
|
634afcc089 | ||
|
|
90f5c8ad57 | ||
|
|
12fd45c144 | ||
|
|
382258a27c | ||
|
|
b5fa8ab4de | ||
|
|
bbb0db2f4f | ||
|
|
5b89e3dbee | ||
|
|
07b29a7bdc | ||
|
|
3286343026 | ||
|
|
6a26b611fa | ||
|
|
218fd2dcdc | ||
|
|
4c85db5e49 | ||
|
|
85ae42190a | ||
|
|
df8a755462 | ||
|
|
53713a0318 | ||
|
|
61aa477cbf | ||
|
|
c2adb92d28 | ||
|
|
19b2b08eac | ||
|
|
504346256f | ||
|
|
34e92d2223 | ||
|
|
573953704c | ||
|
|
0b9f10985d | ||
|
|
8e3dd42a32 | ||
|
|
0647933df7 | ||
|
|
93f4da3962 | ||
|
|
f72351ea88 | ||
|
|
d3c4c8c846 | ||
|
|
cf047024a1 | ||
|
|
ad3ed68932 | ||
|
|
62b8ee9d30 | ||
|
|
eedd92c894 | ||
|
|
622d5e5a44 | ||
|
|
475d653979 | ||
|
|
19a6f8f422 | ||
|
|
e69d69760d | ||
|
|
06de349fac | ||
|
|
c3ec83f640 | ||
|
|
37c20e4e0d | ||
|
|
400ba5bb20 | ||
|
|
a1ea5214a9 | ||
|
|
aae633c4ac | ||
|
|
ec8d7a36a8 | ||
|
|
21dc26b06f | ||
|
|
6a8d2cf1d4 | ||
|
|
b803ba4c55 | ||
|
|
4378fc2719 | ||
|
|
244e1290b4 | ||
|
|
5a64bd3a32 | ||
|
|
cc0b2126ba | ||
|
|
97f92e508e | ||
|
|
09fbe4e4ad | ||
|
|
508af6a80f | ||
|
|
073f816dbd | ||
|
|
bf327afd19 | ||
|
|
8fb8ce319e | ||
|
|
eabfef22be | ||
|
|
06c31e0808 | ||
|
|
509fb5cfed | ||
|
|
f1aa66119a | ||
|
|
7150b1545a | ||
|
|
59bbbe4a8d | ||
|
|
f64641cfdd | ||
|
|
f1783a6110 | ||
|
|
c8c1155c9c | ||
|
|
338a08da6c | ||
|
|
15aed891f4 | ||
|
|
c1c54780cb | ||
|
|
e93704bbdb | ||
|
|
c0db914213 | ||
|
|
b78e31e518 | ||
|
|
2533f0ced1 | ||
|
|
c7f4e48a27 | ||
|
|
87c1a95b06 | ||
|
|
4efb39358a | ||
|
|
619b93a545 | ||
|
|
a0598997d7 | ||
|
|
d0b8095bba | ||
|
|
daab1bca3d | ||
|
|
a0a2fddd90 | ||
|
|
f61c30bd5a | ||
|
|
4921d1eb19 | ||
|
|
28c8b1a6da | ||
|
|
3f0a6df717 | ||
|
|
07c8398e0d | ||
|
|
82c7a64a25 | ||
|
|
164a6a5604 | ||
|
|
9b6ea851e7 | ||
|
|
3804e18e53 | ||
|
|
244a47efad | ||
|
|
89e59b2448 | ||
|
|
bf9c2f6de7 | ||
|
|
ff80a3db3f | ||
|
|
c7d00d7b0d | ||
|
|
1c6e74b08e | ||
|
|
f8d054f8aa | ||
|
|
e5dc50ec35 | ||
|
|
e0dfaf2d22 | ||
|
|
757a62c35b | ||
|
|
52a62fc23c | ||
|
|
44b70d7161 | ||
|
|
ad9e4b4fb4 | ||
|
|
4fdd5ae769 | ||
|
|
9f2b9e3b3e | ||
|
|
2c074e9dc4 | ||
|
|
0fad31d683 | ||
|
|
b154930a4c | ||
|
|
359b04e7d1 | ||
|
|
cc00e3bf75 | ||
|
|
0454685039 | ||
|
|
b5eb325c5e | ||
|
|
ed6276a3e4 | ||
|
|
5da1310e34 | ||
|
|
a69b09c9e6 | ||
|
|
8da3bb15e9 | ||
|
|
8488445bf4 | ||
|
|
546d21adac | ||
|
|
580f50b187 | ||
|
|
4744276f2a | ||
|
|
6106b8aebb | ||
|
|
dd2b203321 | ||
|
|
7994c9c7a8 | ||
|
|
ae59aa3326 | ||
|
|
0ab9a1c97d | ||
|
|
2908568e2a | ||
|
|
2b673a1b6c | ||
|
|
bece8abfe2 | ||
|
|
ac2b355399 | ||
|
|
7e658ee1f2 | ||
|
|
45b61ba335 | ||
|
|
32e6345cbc | ||
|
|
d197995226 | ||
|
|
09fa23519d | ||
|
|
9da736daee | ||
|
|
410d1e0f86 | ||
|
|
12ad7962c0 | ||
|
|
397ebc5112 | ||
|
|
d48823925a | ||
|
|
6f49f16f6b | ||
|
|
c2664a2888 | ||
|
|
4529a56d7b | ||
|
|
fded8177c5 | ||
|
|
e670a9847a | ||
|
|
8aada99f06 | ||
|
|
04e624cc14 | ||
|
|
55ebe70808 | ||
|
|
f23afdd85c | ||
|
|
bac76871c1 | ||
|
|
56cbcf2921 | ||
|
|
f2dc25aedb | ||
|
|
2367ca2255 | ||
|
|
f28c1ce398 | ||
|
|
6cc6e80f92 | ||
|
|
9aeda2ce53 | ||
|
|
a08393b4d4 | ||
|
|
02c1446cb8 | ||
|
|
8e66434ec4 | ||
|
|
66d43d8a95 | ||
|
|
6c7052ea0d | ||
|
|
a994bb7e8c | ||
|
|
9b109c7abd | ||
|
|
95855fc22e | ||
|
|
3218ea85b2 |
20
.editorconfig
Normal file
20
.editorconfig
Normal file
@@ -0,0 +1,20 @@
|
||||
# Editor defaults for ITFlow - see CONTRIBUTING.md ("Style")
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
# Vendored - do not reformat
|
||||
[libs/**]
|
||||
indent_style = unset
|
||||
indent_size = unset
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
44
.gitattributes
vendored
Normal file
44
.gitattributes
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# ITFlow line-ending policy
|
||||
#
|
||||
# Everything ITFlow ships is LF in the repository and LF in the working tree.
|
||||
# Contributors on Windows get LF too - this is deliberate. ITFlow is deployed
|
||||
# to Linux/Apache and edited over sftp/ssh as often as it is cloned, so a
|
||||
# checkout must be byte-identical everywhere.
|
||||
|
||||
* text=auto eol=lf
|
||||
|
||||
# Explicit for the file types we author, so nothing depends on git's guess.
|
||||
*.php text eol=lf
|
||||
*.js text eol=lf
|
||||
*.css text eol=lf
|
||||
*.html text eol=lf
|
||||
*.sql text eol=lf
|
||||
*.md text eol=lf
|
||||
*.json text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.xsd text eol=lf
|
||||
*.svg text eol=lf
|
||||
*.txt text eol=lf
|
||||
*.ini text eol=lf
|
||||
.htaccess text eol=lf
|
||||
|
||||
# Binary assets: never touched, never diffed as text.
|
||||
*.png binary
|
||||
*.gif binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.webp binary
|
||||
*.ico binary
|
||||
*.icc binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.ttf binary
|
||||
*.eot binary
|
||||
*.crt binary
|
||||
*.ser binary
|
||||
*.z binary
|
||||
|
||||
# Vendored third-party code is preserved byte-for-byte as shipped upstream.
|
||||
# Per CONTRIBUTING.md libs/ is never edited in place - it is replaced wholesale -
|
||||
# so normalizing it here would create spurious diffs on the next library update.
|
||||
libs/** -text
|
||||
37
.gitignore
vendored
37
.gitignore
vendored
@@ -4,16 +4,30 @@ config.php
|
||||
uploads/favicon.ico
|
||||
uploads/clients/*
|
||||
!uploads/clients/index.php
|
||||
uploads/custom/*
|
||||
!uploads/custom/index.php
|
||||
uploads/documents/*
|
||||
!uploads/documents/index.php
|
||||
uploads/document_templates/*
|
||||
!uploads/document_templates/index.php
|
||||
uploads/expenses/*
|
||||
!uploads/expenses/index.php
|
||||
uploads/recurring_tickets/*
|
||||
!uploads/recurring_tickets/index.php
|
||||
uploads/settings/*
|
||||
!uploads/settings/index.php
|
||||
uploads/users/*
|
||||
!uploads/users/index.php
|
||||
uploads/tmp/*
|
||||
!uploads/tmp/index.php
|
||||
!uploads/tmp/.htaccess
|
||||
uploads/backups/*
|
||||
!uploads/backups/index.php
|
||||
!uploads/backups/.htaccess
|
||||
uploads/tickets/*
|
||||
!uploads/tickets/index.php
|
||||
uploads/ticket_templates/*
|
||||
!uploads/ticket_templates/index.php
|
||||
.idea/*
|
||||
plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/HTML/*
|
||||
!plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/HTML/.gitkeep
|
||||
@@ -22,7 +36,22 @@ plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/URI/*
|
||||
plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/CSS/*
|
||||
!plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/CSS/.gitkeep
|
||||
.vscode/settings.json
|
||||
xcustom/*
|
||||
!xcustom/readme.php
|
||||
post/xcustom
|
||||
!post/xcustom/readme.php
|
||||
admin/custom/*
|
||||
!admin/custom/readme.php
|
||||
agent/custom/*
|
||||
!agent/custom/readme.php
|
||||
client/custom/*
|
||||
!client/custom/readme.php
|
||||
guest/custom/*
|
||||
!guest/custom/readme.php
|
||||
cron/custom/*
|
||||
!cron/custom/readme.php
|
||||
scripts/custom/*
|
||||
!scripts/custom/readme.php
|
||||
setup/custom/*
|
||||
!setup/custom/readme.php
|
||||
api/v1/custom/*
|
||||
!api/v1/custom/readme.php
|
||||
.zed
|
||||
|
||||
*.patch
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# Prevent access to .git, .github, and config.php
|
||||
RedirectMatch 404 ^/(\.git|\.github|config\.php)
|
||||
RedirectMatch 401 ^/(\.git|\.github|config\.php)
|
||||
|
||||
1038
CHANGELOG.md
1038
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
296
CONTRIBUTING.md
Normal file
296
CONTRIBUTING.md
Normal file
@@ -0,0 +1,296 @@
|
||||
# Contributing to ITFlow
|
||||
|
||||
Thanks for your interest in contributing! ITFlow is intentionally simple: plain procedural PHP, MySQL via `mysqli`, and vanilla Bootstrap/AdminLTE. There is no framework, no ORM, no template engine, and no build step. If you can read a PHP file top to bottom, you can read ITFlow.
|
||||
|
||||
That simplicity comes with a trade-off: **safety and correctness depend on following conventions at every call site.** This document is the list of those conventions. Read it once, fully, before opening a PR — most review feedback we give is a restatement of something on this page.
|
||||
|
||||
---
|
||||
|
||||
## Quick start (development)
|
||||
|
||||
1. Clone the repo into a webroot served by Apache/PHP 8.x with the `mysqli`.
|
||||
2. Create a MySQL/MariaDB database and browse to `/setup/` — or import `db.sql` directly.
|
||||
3. Rename/skip setup as prompted; `config.php` is generated at the root (and is gitignored).
|
||||
There is no `composer install` or `npm install` step. All third-party libraries are vendored in `/libs/`. This is deliberate — ITFlow is distributed as "unzip and go" — so **never add a runtime Composer/npm dependency**. If a new library is truly needed, discuss it in an issue first; if accepted, it gets vendored into `/libs/`.
|
||||
|
||||
---
|
||||
|
||||
## Architecture map
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `agent/` | The main technician-facing app. Most feature work happens here. |
|
||||
| `admin/` | Settings, configuration, roles, mail, migrations. Admin-only. |
|
||||
| `client/` | The logged-in client portal (contacts of a client). |
|
||||
| `guest/` | Unauthenticated flows via URL keys (view/pay invoice, view quote/ticket, view shared credentials/files/documents). |
|
||||
| `api/v1/` | Key-authenticated JSON CRUD API, one directory per module. |
|
||||
| `cron/` | Scheduled jobs. `cron.php` is the dispatcher and the only entry in the crontab; everything else in the directory is a job it runs, with `cron/includes/` for the parts only cron uses. See [Cron](#cron). |
|
||||
| `functions.php` + `functions/` | Shared helper functions, split into topical files (`sanitize.php`, `auth.php`, `logging.php`, …) loaded by `functions.php`. New helpers go in the topical file that matches their concern. |
|
||||
| `includes/` (root) | **Shared** across portals: session/auth bootstrap, DB, layout partials. |
|
||||
| `post/` (root) | **Shared** POST handlers (logout, misc). |
|
||||
| `modals/` (root) | **Shared** modals used by both agent and admin. |
|
||||
| `js/`, `css/` (root) | Shared front-end assets (portals also have their own). |
|
||||
| `libs/` | Vendored third-party libraries. Never edit these; update them wholesale. |
|
||||
| `setup/` | First-run installer. |
|
||||
| `scripts/` | Helper/utility scripts — `setup_cli.php`, `update_cli.php`, `restore_cli.php`. CLI only; the directory denies web access. |
|
||||
|
||||
Rule of thumb: **root-level `includes/`, `post/`, `modals/`, `js/`, `css/` are shared code; everything inside a portal directory is scoped to that portal.**
|
||||
|
||||
### `custom/` directories
|
||||
|
||||
`agent/`, `admin/`, `client/`, `guest/`, and `cron/` each contain a `custom/` directory. These are hook points for site-specific code that survives updates. `triggerCustomAction($trigger, $entity_id)` fires named triggers (e.g. `ticket_resolve`) into `custom/custom_action_handler.php` if one exists. Core code should **call** `triggerCustomAction()` at meaningful events but never depend on anything inside `custom/`.
|
||||
|
||||
---
|
||||
|
||||
## Request lifecycle (how a page works)
|
||||
|
||||
**Read pages** (`agent/tickets.php`, etc.) start by requiring an `inc_all*.php` from the portal's `includes/`. That chain loads, in order: `config.php` → `functions.php` (a loader that pulls in the topical helper files under `functions/` — security, sanitize, auth, logging, etc.) → `check_login.php` (auth) → header/nav/layout partials. It also establishes the implicit globals every page relies on: `$mysqli`, `$session_user_id`, `$session_name`, and — on client-scoped pages via `inc_all_client.php` — `$client_id` (already `intval()`'d).
|
||||
|
||||
If your code "can't find" a variable, check which include chain the page uses before adding a query. The variable probably already exists.
|
||||
|
||||
**Write actions** go through the portal's `post.php` dispatcher, which:
|
||||
|
||||
1. Requires config, functions, and the login check.
|
||||
2. Defines the constant `FROM_POST_HANDLER`.
|
||||
3. Loads the handler files in `post/` (excluding `*_model.php`).
|
||||
Every handler file must start with:
|
||||
|
||||
```php
|
||||
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
|
||||
```
|
||||
|
||||
Handlers are a series of independent blocks, one per action:
|
||||
|
||||
```php
|
||||
if (isset($_POST['edit_ticket_priority'])) {
|
||||
validateCSRFToken($_POST['csrf_token']);
|
||||
enforceUserPermission('module_support', 2);
|
||||
// ... fetch, check client access, act, log, notify, redirect
|
||||
}
|
||||
```
|
||||
|
||||
**Copy the nearest existing block as your starting point** — but understand every line you copy. The next section explains why each one is there.
|
||||
|
||||
### The `_model.php` pattern
|
||||
|
||||
Files named `agent/post/*_model.php` hold shared field collection/sanitization logic used by both the create and edit blocks of a module (e.g. `asset_model.php` is included by both `add_asset` and `edit_asset`). If create and edit share more than a couple of fields, use this pattern rather than duplicating. Model files carry the same `FROM_POST_HANDLER` guard and are excluded from the dispatcher's auto-load.
|
||||
|
||||
**`_model.php` is a reserved suffix.** The exclusion is a filename match, so a *handler* named `*_model.php` is silently never loaded — its form posts, nothing claims the request, and the user gets a blank page with no error anywhere. This is what happened to `admin/post/ai_model.php`, which is why the AI Models handler is now `admin/post/ai_models.php`. Name entity handlers around the suffix (`ai_models.php`, `users.php`, `api_keys.php`).
|
||||
|
||||
A POST that reaches the end of `admin/post.php` or `agent/post.php` without a handler claiming it is logged to App Logs as a `Request` warning, which is the fastest way to spot this class of mistake.
|
||||
|
||||
---
|
||||
|
||||
## Cron
|
||||
|
||||
One crontab entry runs everything:
|
||||
|
||||
```
|
||||
* * * * * php /path/to/itflow/cron/cron.php >/dev/null
|
||||
```
|
||||
|
||||
`cron/cron.php` is a dispatcher. It wakes every minute, works out which scripts in `cron/` are due, and requires them into its own process. Adding a job is a new script in `cron/` plus an entry in `includes/cron_jobs.php`. The crontab never changes again.
|
||||
|
||||
That registry is the only thing that decides **which** scripts can run, and the schedule in it is only a default: it seeds the job's `cron_jobs` row the first time the dispatcher meets the job, and from then on the row is what runs, because Maintenance > Cron writes to it. The database therefore holds **when and whether**, never **what** — a row naming a script that is not in the registry is ignored, so nothing that reaches the database can point the dispatcher at an arbitrary file. Keep it that way.
|
||||
|
||||
Run Now in the admin UI does not execute anything in the web request: these scripts are CLI-only and some take minutes, so the button sets `cron_job_run_now` and the next dispatch picks it up, through the same lock and claim as a scheduled run.
|
||||
|
||||
Due-ness is recorded in the `cron_jobs` table rather than matched against the clock, so a job whose minute was missed — machine down, previous run still going — runs at the next opportunity instead of being skipped for the day. A job is claimed *before* it runs, not after: a run that dies half way through is not repeated, which matters because `nightly_tasks.php` generates invoices and charges cards. Each job is also locked individually for the length of its own run (`cron/includes/cron_lock.php`), so a long or hung job holds up only itself — the next minute's dispatch picks up everything else in a second process.
|
||||
|
||||
Because the jobs share one PHP process, job code has three rules:
|
||||
|
||||
1. **Never `exit()` or `die()`.** It ends the whole cycle and every job after it. Use `cronJobStop($message, $exit_code)` instead: it exits when the script was run directly and unwinds back to the dispatcher when it wasn't, so both paths behave as they always have.
|
||||
2. **Never declare a function or class another job might declare.** Two jobs each declaring the same helper is a fatal `Cannot redeclare` the moment they share a process. Shared helpers belong in `functions/`.
|
||||
3. **Be safe to run twice in one day.** The dispatcher's lock stops overlap, but nothing stops a repeat: an admin presses Run Now after the scheduled pass, or a schedule is misconfigured. Work selected by a date match (`... = CURDATE()`) fires again on every run of that day unless something records that it happened — nightly's late fees and overdue reminders guard on the history rows they write. A job whose work cannot be made repeat-safe declares `'interval_safe' => false` in `includes/cron_jobs.php`, which locks it to the daily schedule in Maintenance > Cron and in the dispatcher.
|
||||
4. **Set what you read.** One global scope and one set of `require_once` includes are shared across the cycle — a job's own `require_once "../config.php"` is a no-op if an earlier job already loaded it, and any variable an earlier job left behind is still there. Do not rely on the state a fresh process would have given you.
|
||||
|
||||
A job can also ship switched off with `'enabled' => 0` in the registry. The row is seeded disabled and stays that way until somebody turns it on in Maintenance > Cron. Use it for work an install should opt into rather than inherit silently from an upgrade — `backup` ships this way, because a full backup can be gigabytes a night.
|
||||
|
||||
### The master switch
|
||||
|
||||
`config_enable_cron` is a second, coarser switch that sits above the per-job ones. It is **not** enforced by the dispatcher — every job checks it in its own header and stops itself with `cronJobStop()`. A new job has to make that check too; one that skips it keeps running on an install that believes cron is off, which is exactly the trap `ticket_email_parser` and `ticket_sla` sat in until 26.08.
|
||||
|
||||
Be precise about what it does, because it is easy to oversell. It is **not** a guard on restored data: a full backup dumps the `settings` table, so a restored copy comes back with `config_enable_cron = 1` alongside every enabled `cron_jobs` row, exactly as production had them. What it gives you is one reversible bit — the fastest way to stop an install acting on live data once you have noticed, and the only way to stop everything without editing seven rows.
|
||||
|
||||
That last part is the reason it is not redundant with `cron_job_enabled`. Turning the switch off and back on returns you to exactly the configuration you had. Sweeping all seven rows off and back on does not: `backup` ships `'enabled' => 0`, so the sweep quietly turns on nightly backups nobody asked for, along with anything else that was deliberately disabled.
|
||||
|
||||
It defaults to `0`. That is a weaker guard than it looks — an install with no crontab entry runs nothing whatever the switch says, so the entry is the real gate — but it does mean adding the crontab line to a half-configured install is not enough on its own to start emailing. Both setup paths name the step on the way out.
|
||||
|
||||
## Backups
|
||||
|
||||
`functions/backup.php` is the whole engine, and all three entry points go through it: Maintenance > Backup, `cron/backup.php`, and `scripts/restore_cli.php`. Nothing else should dump, zip, or import a database.
|
||||
|
||||
Archives are AES-256 encrypted zips. The key is one value per install, generated on first use and appended to `config.php` — **never** the database and **never** the file name. That is the point: a backup that leaks cannot be opened with anything the backup itself contains, and a URL or an access log never carries the key. The 32 random characters in the file name are an unguessable path component, nothing more. Note that `unzip`, Windows Explorer and the macOS Archive Utility cannot read AES zips; 7-Zip, WinZip, PeaZip and Keka can.
|
||||
|
||||
The web tier never builds an archive in the request. It writes a `Pending` row and `cron/backup.php` does the work, because a dump of a real install outlives `request_terminate_timeout` and `set_time_limit()` does not help. Same reasoning as Run Now.
|
||||
|
||||
Two rules for anything touching restore:
|
||||
|
||||
1. **Validate before you destroy.** The key is checked and the archive unpacked before a single table is dropped, and the live database is dumped to a rollback file first. If the import fails the rollback goes back in. `mysqli` throws rather than returning false under PHP 8.1's default report mode, so every statement in the import path is wrapped — an uncaught throw there leaves an install with no database at all.
|
||||
2. **The archive does not get to decide what our guards say.** A restore wipes `uploads/`, and an archive is allowed to contain a `.htaccess`. `backupAssertUploadsGuards()` rewrites ours afterwards unconditionally, and the backup storage directory is preserved through the wipe so a restore cannot destroy every other archive on the box.
|
||||
|
||||
Retention lives in `nightly_tasks.php`, never in the backup job, so a failed backup cannot delete the archive it was supposed to replace. It never removes the newest complete backup, and an archive on disk with no row is **adopted** rather than deleted — after a restore the `backups` table is the old one, so everything made since looks unknown.
|
||||
|
||||
Setup's restore step closes itself once the `users` table has rows, whatever `config.php` says. It used to default `$config_enable_setup` to `1` when the flag was absent, which fails the wrong way: the flag is only appended at the end of a successful install, so an install abandoned in between left an unauthenticated endpoint that dropped every table, imported an arbitrary archive, and rewrote `uploads/` including the `.htaccess` that stops PHP running there.
|
||||
|
||||
Every script in `cron/` still runs standalone (`php cron/mail_queue.php`) and still takes its own lock when it does, so anything can be run by hand for testing.
|
||||
|
||||
---
|
||||
|
||||
## Security rules (non-negotiable)
|
||||
|
||||
ITFlow does not use prepared statements or an ORM; queries are built as strings. That works **only** if every value is neutralized before interpolation. The rules:
|
||||
|
||||
### 1. Every value interpolated into SQL is cast or sanitized. No exceptions.
|
||||
|
||||
- **Integers** (IDs, flags, counts): `intval($_POST['ticket_id'])`. Interpolate unquoted.
|
||||
- **Strings**: `escapeSql($_POST['subject'])`. This normalizes encoding to UTF-8, then runs `strip_tags()`, `trim()`, and `mysqli_real_escape_string()`. Because it relies on SQL escaping, the value **must be placed inside quotes in the query** (`'$subject'`). An escaped string interpolated without quotes is still injectable.
|
||||
- **Values read back from the database** get the same treatment before reuse in another query (you will see `escapeSql($row['ticket_prefix'])` throughout — this is why).
|
||||
If you write a query and even one variable in it skipped these, that is a SQL injection. This is the single most common review rejection.
|
||||
|
||||
**Fetch helpers return raw values — you escape them.** `getFieldById()` and `getTicketStatusName()` hand back exactly what is in the column. Escaping is the call site's job, the same as any other row you read:
|
||||
|
||||
```php
|
||||
$client_name = escapeSql(getFieldById('clients', $client_id, 'client_name')); // into a query
|
||||
$client_name = escapeHtml(getFieldById('clients', $client_id, 'client_name')); // into a page
|
||||
$client_id = intval(getFieldById('tickets', $ticket_id, 'ticket_client_id')); // an id
|
||||
```
|
||||
|
||||
Both helpers used to escape internally, via an `$escape_method` argument. It went badly: most call sites wrapped them in `escapeSql()` anyway and got a double-escaped value, so a client named `O'Brien` came back as `O\'Brien` and the backslash reached export filenames, PDF headings, flash messages and — on the user restore path — the database itself. A value that is already safe cannot be made safer, only wrong.
|
||||
|
||||
### 2. Every state-changing action validates CSRF.
|
||||
|
||||
`validateCSRFToken()` is the first line of every action block. It takes no argument — it reads `csrf_token` from `$_POST`, then `$_GET`, itself, so the same call covers form posts and link-style actions. (The signature still accepts an explicit token for callers that need one, but no call site in the tree passes one; use the bare form.) Forms and action links must include the token; copy how existing modals do it.
|
||||
|
||||
### 3. Every action enforces permissions.
|
||||
|
||||
`enforceUserPermission('module_x', level)` where level is `1` = read, `2` = write, `3` = full/delete. Current modules: `module_client`, `module_support`, `module_sales`, `module_financial`, `module_credential`, `module_reporting`. Read pages enforce level 1; create/edit enforce 2; destructive actions enforce 3. CSV/PDF exports are reads — gate them with the bare one-argument form, e.g. `enforceUserPermission('module_sales')`.
|
||||
|
||||
Two portals are gated differently, which is why their handlers look like they are missing the call:
|
||||
|
||||
- **Admin.** `admin/post.php` only loads anything in `admin/post/` when `$session_is_admin` is set, so admin handlers inherit the gate from the dispatcher and do not call `enforceUserPermission()` themselves.
|
||||
- **Client portal.** `client/post.php` is a single file of action blocks rather than a dispatcher, and gates on the contact's own capabilities with `enforceContactCan('accounting'|'contacts'|'itdoc')`.
|
||||
|
||||
Everywhere else — anything under `agent/post/` — the call belongs in the block.
|
||||
|
||||
### 4. Client scoping is enforced, not assumed.
|
||||
|
||||
A user can be restricted to a subset of clients through `user_client_permissions`. Enforcing that has two halves, and a page usually needs both.
|
||||
|
||||
**One record — `enforceClientAccess()`.** After loading a record, call it (optionally with the record's client ID) so technicians restricted to specific clients cannot touch other clients' data by editing an ID in the URL. Look at how `resolve_ticket` does it.
|
||||
|
||||
**A list — `clientScopeSql()`.** Any query returning more than one row appends the fragment for that resource's own client column:
|
||||
|
||||
```php
|
||||
$sql = mysqli_query($mysqli, "SELECT expense_id, expense_date, expense_amount, expense_description
|
||||
FROM expenses
|
||||
WHERE expense_archived_at IS NULL
|
||||
" . clientScopeSql('expense_client_id') . "
|
||||
ORDER BY expense_date DESC");
|
||||
```
|
||||
|
||||
It returns `" AND ..."` or `""`, so it needs a `WHERE` to hang off — add `WHERE 1=1` if the query has no other condition. It is column-aware and takes an alias fine (`clientScopeSql('t.ticket_client_id')`). The API calls the same helper through the `apiClientScopeSql()` wrapper.
|
||||
|
||||
Scope on the resource's **own** column, not on a joined `clients.client_id`. Joining `clients` just to scope makes the filter depend on the join: with a `LEFT JOIN`, a row whose client column is `0` produces `NULL`, and `NULL IN (...)` is neither true nor false, so the row silently vanishes. If the query joins `clients` for `client_name`, keep the join for that — but still scope on the owning column.
|
||||
|
||||
**Records with no client (`0`) stay visible to restricted users.** `clientScopeSql()` emits `IN (0,...)` deliberately. Client restrictions partition *client* data, and a record belonging to no client is not any client's data to withhold. Do not hand-roll a variant that drops the `0` — the tree had accumulated several before this helper existed, disagreeing with each other, and reconciling them is what surfaced the inconsistency.
|
||||
|
||||
### 5. Escape on output.
|
||||
|
||||
Anything rendered into HTML goes through `escapeHtml()`. `escapeSql()` on the way in is **not** output escaping — data can enter the DB through other paths (API, email parser, older versions).
|
||||
|
||||
In practice the escaping happens **where the row is read, not where it is echoed**. A page or modal fetches its row and assigns each field through `escapeHtml()` once, then echoes the resulting variable raw:
|
||||
|
||||
```php
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$asset_id = intval($row['asset_id']); // ints: intval, not escapeHtml
|
||||
$asset_name = escapeHtml($row['asset_name']);
|
||||
...
|
||||
<strong><?php echo $asset_name; ?></strong>
|
||||
```
|
||||
|
||||
Follow that pattern. Escaping at the echo instead would double-escape a value that is already safe, and mixing the two is how fields get missed. If you introduce a view variable that does not come from a row, escape it at assignment so the rule still holds at the top of the file.
|
||||
|
||||
A value that arrives through a fetch helper rather than a row read follows the same rule — `escapeHtml(getFieldById(...))` at the assignment, not at the echo. See rule 1.
|
||||
|
||||
Rich-text fields (TinyMCE content) are the exception and have their own handling; follow the existing pattern for the specific field rather than inventing one.
|
||||
|
||||
### 6. No shell-outs. No `eval`.
|
||||
|
||||
The project has deliberately moved off `shell_exec`/`exec` in favor of native PHP — `dns_get_record()` instead of `dig`, RDAP instead of `whois`, and so on. **Do not add new shell execution or `eval`.** PRs introducing either will be declined.
|
||||
|
||||
A handful of legacy call sites survive, all of them wrapping `git` or `which` in the self-update and diagnostics paths: `admin/debug.php`, `admin/update.php`, `admin/post/update.php`, `admin/post/backup.php`, `cron/cron.php`, `functions/app.php`, `scripts/update_cli.php`, `setup/index.php`. They are on the list to be replaced with direct `.git` file reads; treat them as debt, not as precedent.
|
||||
|
||||
### 7. Report vulnerabilities privately.
|
||||
|
||||
Per [SECURITY.md](SECURITY.md) — never in a public issue.
|
||||
|
||||
---
|
||||
|
||||
## Conventions
|
||||
|
||||
**Only technician-entered time is time worked.** `ticket_replies.ticket_reply_time_worked` is billable labour and feeds ticket totals, the technician and client time reports, project totals, invoicing and the API. A reply the *system* writes — assignment, priority change, merge, close, invoice/quote created, schedule edited, task completed or reopened — is an audit trail, not work, and records `'00:00:00'`. Only a value the technician actually typed goes in that column. Task completion estimates are planning information and stay on the task; they are never converted into time worked. `agent/ticket.php` hides the clock badge on a reply whose time is exactly `00:00:00`, so a zero renders as no time rather than as "0m".
|
||||
|
||||
**Database naming.** Every column is prefixed with the singular name of the entity it belongs to: `tickets.ticket_id`, `tickets.ticket_subject`, `clients.client_name`. This makes JOIN results unambiguous, so a `SELECT *` across joins is never *wrong*. New tables must follow it.
|
||||
|
||||
**Select the columns you use, not `*`.** Unambiguous is not the same as cheap. `SELECT *` across three joined tables fetches every column of all three, including the `*_notes` and `*_details` TEXT columns, and throws away whatever the page never renders. A search result list that shows five fields was pulling sixty. List the columns instead:
|
||||
|
||||
```php
|
||||
$sql = mysqli_query($mysqli, "SELECT ticket_id, ticket_prefix, ticket_number, ticket_subject, client_name
|
||||
FROM tickets
|
||||
LEFT JOIN clients ON ticket_client_id = client_id
|
||||
WHERE ticket_archived_at IS NULL
|
||||
" . clientScopeSql('ticket_client_id') . "");
|
||||
```
|
||||
|
||||
Two things follow from that:
|
||||
|
||||
- A query whose result only feeds `mysqli_num_rows()` needs no columns at all — write `SELECT 1`. Do not select a primary key "just in case": if the query joins two tables that both carry that column name, an unqualified `SELECT ticket_template_id` is an ambiguous-column error.
|
||||
- Keep the join even when no column of the joined table survives into the `SELECT`, if the join is doing work — supplying a `WHERE` term, an `ORDER BY`, or the client column you scope on. Dropping a join is a separate decision from trimming the column list.
|
||||
|
||||
The trade is real and worth stating: `SELECT *` picks up new columns for free, an explicit list does not. Add a column to a table and every query that needs it must be updated by hand, and the failure mode is a blank field or a PHP 8 undefined-key warning rather than an error. That is the price of not fetching data nobody reads, and the project has decided to pay it on anything that loops or touches a TEXT column.
|
||||
|
||||
The exception is `api/v1/*/read.php`. Those endpoints hand the whole row to `read_output.php`, which serialises it straight into the JSON response — there the row *is* the output contract, so `SELECT *` is correct and trimming it would silently drop fields from every consumer.
|
||||
|
||||
The prefix is the entity name, which is usually but not always the singular of the table name. Where a table is named for its container rather than its row, the prefix follows the row: `calendar_events` → `event_*`, `asset_interfaces` → `interface_*`, `invoice_items` / `quote_items` → `item_*`, `rack_units` → `unit_*`, `user_roles` → `role_*`, `product_stock` → `stock_*`. Pick the prefix your columns will read best as and use it for every column in the table.
|
||||
|
||||
Two standing exceptions: junction tables (`client_tags`, `service_assets`, …) carry the two parent FK names unprefixed, and `settings` / `user_settings` use `config_*` / `user_config_*`.
|
||||
|
||||
**Schema changes require two edits in one PR:**
|
||||
|
||||
1. `db.sql` — so fresh installs get the new schema.
|
||||
2. `admin/database_updates/<x.y.z>.php` — a new file named for the version it upgrades **to**, containing only the queries that apply the change. Migrations are sequential and rolling-release; never edit a historical file.
|
||||
|
||||
That is the whole job. `LATEST_DATABASE_VERSION` is derived from the highest-numbered filename in `admin/database_updates/`, and the runner (`admin/database_updates.php`) steps `config_current_database_version` after each file succeeds — so there is no constant to bump and no version-bump query to write. Each migration file needs the standard `defined('FROM_DB_UPDATER') || die(...)` guard at the top; copy an existing file's header.
|
||||
|
||||
A single update run applies every pending migration in order, stopping at the first failure with the version left at the last file that completed, so a re-run resumes at the one that broke.
|
||||
**After acting, log and notify.** State changes call `logAudit($type, $action, $description, $client_id, $entity_id)` for the audit trail. User-facing events may also call `appNotify()`. Fire `triggerCustomAction()` where a site might reasonably want a hook. Then call `flashAlert($message, $type)` and `redirect()` (defaults to the referer) rather than setting session keys or `header()` manually.
|
||||
|
||||
**Function names (post-rename).** Helpers were renamed for clarity in 2026; the old names **no longer exist** — code calling them fatals. If you're rebasing an old PR or following an old tutorial, translate: `sanitizeInput` → `escapeSql`, `nullable_htmlentities` → `escapeHtml`, `logAction` → `logAudit`, `flash_alert` → `flashAlert`, `customAction` → `triggerCustomAction`, `encryptLoginEntry`/`decryptLoginEntry` → `encryptCredentialEntry`/`decryptCredentialEntry`, `strtoAZaz09` → `toAlphanumeric`, `fetchUpdates` → `checkForUpdates`, `sanitize_url` → `escapeUrl`.
|
||||
|
||||
One removed **variable** deserves its own warning: the old `$access_permission_query` global is gone, replaced by `clientScopeSql()` (security rule 4). Unlike a removed function, it does not fatal — an undefined variable interpolates as an empty string, so a rebased query keeps running with **no client scoping at all**. Grep for it before rebasing anything that touches a list query.
|
||||
|
||||
**Helpers that fetch data return it raw.** If you add a `getXById()`-style helper, return the column value untouched and let callers escape it (security rule 1). Validating what the helper interpolates into its *own* query — table and column names, the id — is still the helper's job; that is query construction, not output escaping, and the two are not the same thing.
|
||||
|
||||
**Bulk vs. single actions.** If you change the behavior of a single action (e.g. resolving a ticket), check whether a `bulk_*` counterpart exists and update it too. They are currently parallel implementations and drift between them is a known bug source.
|
||||
|
||||
**UI.** Bootstrap 4 / AdminLTE, modals per-module under `<portal>/modals/<module>/`, DataTables for lists, monospace styling for technical data (IPs, serials, keys) and proportional for human text. Match the page you're standing in.
|
||||
|
||||
**Modals post to the portal you are standing in, not the one they live in.** Modal forms use `action="post.php"`, which the browser resolves against the *page* URL, not the modal's own path. A modal under `admin/modals/` that an agent page opens by relative path therefore submits to `agent/post.php` and is handled by `agent/post/`, not `admin/post/`. If you reuse a modal across portals, every portal that can open it needs a handler that accepts the same field set — otherwise fields are silently dropped on one side.
|
||||
|
||||
**Style.** Procedural PHP, 4-space indentation, LF line endings, code and comments in English. Match the surrounding code rather than importing a personal style. Don't reformat code you aren't changing — it buries the real diff.
|
||||
|
||||
Line endings and indentation are enforced by `.gitattributes` and `.editorconfig` at the repo root, so an editor that respects EditorConfig needs no configuration. `.gitattributes` marks `libs/` as `-text`: vendored code is preserved byte-for-byte as shipped upstream and must never be normalized, or the next wholesale library update turns into an unreviewable diff.
|
||||
|
||||
---
|
||||
|
||||
## Pull requests
|
||||
|
||||
- **Small, focused diffs.** One feature or one fix per PR. Never mix relocation/reformatting with logic changes — split them into separate commits or PRs so each is reviewable on its own.
|
||||
- Describe **what** and **why**, and note any schema changes prominently.
|
||||
- CI runs PHP lint and db.sql lint; SonarCloud scans for security issues. Green checks are required but not sufficient — the conventions above are checked by human review.
|
||||
- Test your change against a real install: fresh setup from `db.sql` **and** an upgrade via `database_updates.php` if you touched schema.
|
||||
- For anything larger than a bug fix, **open an issue first** and discuss the approach. ITFlow's roadmap favors incremental modernization of the existing PHP codebase; large rewrites, framework introductions, and new runtime dependencies are out of scope.
|
||||
## Getting help
|
||||
|
||||
Open a GitHub issue using the templates, or ask in the community forum linked from the README. When in doubt about a convention, find the closest existing example in the codebase and follow it — consistency beats novelty here.
|
||||
35
README.md
35
README.md
@@ -16,7 +16,7 @@
|
||||
<br />
|
||||
<a href="https://demo.itflow.org"><strong>View demo</strong></a>
|
||||
<br />
|
||||
Username: <b>demo@demo</b> | Password: <b>demo</b>
|
||||
Username: <b>demo@demo.com</b> | Password: <b>demo</b>
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://itflow.org/#about">About</a>
|
||||
@@ -27,7 +27,7 @@
|
||||
·
|
||||
<a href="https://forum.itflow.org/t/bug">Report Bug</a>
|
||||
·
|
||||
<a href="https://forum.itflow.org/t/features">Request Feature</a>
|
||||
<a href="https://forum.itflow.org/t/features">Suggest Feature</a>
|
||||
·
|
||||
<a href="https://github.com/itflow-org/itflow/security/policy">Security</a>
|
||||
</p>
|
||||
@@ -44,16 +44,16 @@
|
||||
### The Problem
|
||||
- You're a small but busy managed service provider with 101 things to do. Information about your clients is unorganised, unstructured and outdated.
|
||||
- For some work, you seem to spend longer looking for the relevant documentation than actually working on the issue/project.
|
||||
- On top of the technical day to day, you also have to take care of the financial side of the business - consistent pricing, quotes/invoicing, and accounting.
|
||||
- MSP tools can be expensive, with recurring licensing costs adding up quickly.
|
||||
- On top of the technical day-to-day, you also have to take care of the financial side of the business - consistent pricing, quotes/invoicing, and accounting.
|
||||
|
||||
### The Solution: ITFlow
|
||||
- ITFlow consolidates common MSP needs (documentation, ticketing and billing) into one unified system.
|
||||
- ITFlow consolidates common MSP needs (documentation, ticketing and billing) into one unified, free platform that you control.
|
||||
|
||||
<!-- GETTING STARTED -->
|
||||
## Getting Started
|
||||
|
||||
### Self Hosting
|
||||
- The best installation method is to use the [install script](https://docs.itflow.org/installation_script) on Ubuntu/Debian. A video walk through is available [here](https://www.youtube.com/watch?v=kKz9NOU_1XE).
|
||||
- The best installation method is to use the [install script](https://docs.itflow.org/installation_script) on Ubuntu/Debian. See the [video](https://www.youtube.com/watch?v=kKz9NOU_1XE) walk through.
|
||||
```
|
||||
wget -O itflow_install.sh https://github.com/itflow-org/itflow-install-script/raw/main/itflow_install.sh
|
||||
bash itflow_install.sh
|
||||
@@ -61,28 +61,28 @@
|
||||
- Other manual installation methods are available in the [docs](https://docs.itflow.org/installation).
|
||||
|
||||
### Managed Hosting
|
||||
- If you'd prefer, we can [host ITFlow for you](https://services.itflow.org/hosting.php).
|
||||
- If you'd prefer, we can [host ITFlow for you](https://services.itflow.org/hosting.php) (coming soon).
|
||||
|
||||
<!-- FEATURES -->
|
||||
## Key Features
|
||||
* Client documentation - assets, contacts, domains, docs, files, passwords, and more
|
||||
* Accounting / Billing - finance dashboard, quotes, invoices, accounting, expenses, etc
|
||||
* Client Portal - self service quote/invoice/ticket management for clients
|
||||
* Alerting - account balance, invoices, domain/SSL renewals
|
||||
* Completely free & open-source alternative to ITGlue and Hudu
|
||||
* Client documentation - assets, contacts, domains, docs/files, passwords, tickets, and more
|
||||
* Billing - quotes, invoices and expenses, with Stripe integration for online payments
|
||||
* Alerting - domain and SSL expiry/renewals
|
||||
* Client Portal - self service quote, invoice, and ticket management for clients
|
||||
|
||||
|
||||
<!-- ROADMAP -->
|
||||
## Roadmap / Future to-do
|
||||
We track the implementation of confirmed features and bugs via [TaskFlow](https://tasks.dev.itflow.org/tasks.php). Use the [forum](https://forum.itflow.org) to request features or raise bug reports.
|
||||
We track the implementation of confirmed features and bugs via [TaskFlow](https://tasks.dev.itflow.org/tasks.php). Use the [forum](https://forum.itflow.org) to suggest new features or report bugs.
|
||||
|
||||
<!-- CONTRIBUTING -->
|
||||
## Support & Contributions
|
||||
|
||||
### Forum
|
||||
For help using ITFlow, bugs, feature requests, and general ideas / discussions please use the community [forum](https://forum.itflow.org).
|
||||
For help using ITFlow, bugs, feature suggestions, and other general ideas or discussions, please use the community [forum](https://forum.itflow.org).
|
||||
|
||||
### Contributing
|
||||
If you want to improve ITFlow, feel free to fork the repo and create a pull reques. Make sure to discuss significant changes or new features with fellow contributors on the forum first. This helps ensure that your contributions are aligned with project goals, and saves time for everyone. All contributions should follow our [code standards](https://docs.itflow.org/code_standards). See the [contributing guide](https://docs.itflow.org/contribute).
|
||||
We have temporarily paused PRs from the community.
|
||||
|
||||
#### Contributors
|
||||
<a href="https://github.com/itflow-org/itflow/graphs/contributors">
|
||||
@@ -90,15 +90,16 @@ If you want to improve ITFlow, feel free to fork the repo and create a pull requ
|
||||
</a>
|
||||
|
||||
### Supporters
|
||||
We’re incredibly grateful to the organizations and individuals who support the project - a big thank you to:
|
||||
We’re incredibly grateful to the organizations and individuals who [sponsor](https://services.itflow.org) the project - a big thank you to:
|
||||
- CompuMatter
|
||||
- F1 for HELP
|
||||
- digiBandit
|
||||
|
||||
## License
|
||||
ITFlow is distributed "as is" under the GPL License, WITHOUT WARRANTY OF ANY KIND. See [`LICENSE`](https://github.com/itflow-org/itflow/blob/master/LICENSE) for details.
|
||||
|
||||
## Security
|
||||
* As of 2025, we now have a stable release of the project.
|
||||
* We've had a stable release of the project since Jan 2025.
|
||||
* Whilst we are confident in the safety of the code, no system is risk-free. Nearly all software has bugs. Use your best judgement before storing highly confidential information in ITFlow.
|
||||
* If you have a security concern, privately report it [here](https://github.com/itflow-org/itflow/security/policy).
|
||||
|
||||
|
||||
22
SECURITY.md
22
SECURITY.md
@@ -1,26 +1,26 @@
|
||||
# Security Policy
|
||||
|
||||
## **Please do NOT report security concerns/vulnerabilities publicly (Issues/forum)**
|
||||
## **Please do NOT report security concerns/vulnerabilities publicly**
|
||||
|
||||
**We take security seriously**
|
||||
**ITFlow stores important information, so we take security seriously**
|
||||
|
||||
- Whilst we are confident in the safety of the code, no system is risk-free. Nearly all software has bugs. Use your best judgement before storing highly confidential information in ITFlow.
|
||||
- We attempt to follow security best practices where possible, including [automated code scanning](https://sonarcloud.io/component_measures?id=itflow-org_itflow&metric=security_rating&view=list).
|
||||
- [](https://sonarcloud.io/summary/new_code?id=itflow-org_itflow)
|
||||
- Whilst we are confident in the safety of the code, no system is risk-free. Nearly all software has bugs.
|
||||
- Use your best judgement before storing highly confidential information in ITFlow.
|
||||
- We attempt to follow security best practices where possible, including automated code scanning.
|
||||
|
||||
## Supported Versions
|
||||
We operate a rolling release model. Any bug fixes will be released into latest version of ITFlow, so you must stay up-to-date.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| Beta | :x: |
|
||||
| 24.12 | :white_check_mark: |
|
||||
| 25.1 | :white_check_mark: (When released) |
|
||||
|---------| ------------------ |
|
||||
| 26.08 | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability via GitHub Security Advisories
|
||||
|
||||
**Security contact: [GitHub Security Advisories](https://github.com/itflow-org/itflow/security/advisories/new)**
|
||||
|
||||
If you have discovered a security issue, please **[report it](https://github.com/itflow-org/itflow/security/advisories/new)** to us in as much detail as possible, so we can fix it.
|
||||
If you have discovered a security issue, please **[report it](https://github.com/itflow-org/itflow/security/advisories/new)** to us so we can fix it.
|
||||
|
||||
You should expect to receive an initial acknowledgement within 72 hours. If you don't receive any feedback, we may have missed the initial email from GitHub (we're human!). Please raise a forum discussion quoting ONLY the assigned GHSA ref.
|
||||
You should expect to receive an initial acknowledgement within 7 days. If you don't receive any feedback, we may have missed the initial email from GitHub (we're human!). Please raise a forum discussion quoting ONLY the assigned GHSA ref.
|
||||
|
||||
Note: Please don't submit AI slop. AI can be useful for drafting and research, but you are responsible for reviewing and validating anything you submit. We reserve the right to restrict repository access if you repeatedly ignore this requirement.
|
||||
137
accounts.php
137
accounts.php
@@ -1,137 +0,0 @@
|
||||
<?php
|
||||
|
||||
// Default Column Sortby Filter
|
||||
$sort = "account_name";
|
||||
$order = "ASC";
|
||||
|
||||
require_once "includes/inc_all.php";
|
||||
|
||||
// Perms
|
||||
enforceUserPermission('module_financial');
|
||||
|
||||
//Rebuild URL
|
||||
$url_query_strings_sort = http_build_query($get_copy);
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS * FROM accounts
|
||||
WHERE (account_name LIKE '%$q%')
|
||||
AND account_archived_at IS NULL
|
||||
ORDER BY $sort $order LIMIT $record_from, $record_to"
|
||||
);
|
||||
|
||||
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
|
||||
?>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-2">
|
||||
<h3 class="card-title mt-2"><i class="fa fa-fw fa-piggy-bank mr-2"></i>Accounts</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addAccountModal"><i class="fas fa-plus mr-2"></i>New Account</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form autocomplete="off">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control col-md-4" name="q" value="<?php if (isset($q)) { echo stripslashes(nullable_htmlentities($q)); } ?>" placeholder="Search Accounts">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-striped table-borderless table-hover">
|
||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=account_name&order=<?php echo $disp; ?>">
|
||||
Name <?php if ($sort == 'account_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=account_currency_code&order=<?php echo $disp; ?>">
|
||||
Currency <?php if ($sort == 'account_currency_code') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th class="text-right">Balance</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
|
||||
while ($row = mysqli_fetch_array($sql)) {
|
||||
$account_id = intval($row['account_id']);
|
||||
$account_name = nullable_htmlentities($row['account_name']);
|
||||
$opening_balance = floatval($row['opening_balance']);
|
||||
$account_currency_code = nullable_htmlentities($row['account_currency_code']);
|
||||
$account_notes = nullable_htmlentities($row['account_notes']);
|
||||
|
||||
$sql_payments = mysqli_query($mysqli, "SELECT SUM(payment_amount) AS total_payments FROM payments WHERE payment_account_id = $account_id");
|
||||
$row = mysqli_fetch_array($sql_payments);
|
||||
$total_payments = floatval($row['total_payments']);
|
||||
|
||||
$sql_revenues = mysqli_query($mysqli, "SELECT SUM(revenue_amount) AS total_revenues FROM revenues WHERE revenue_account_id = $account_id");
|
||||
$row = mysqli_fetch_array($sql_revenues);
|
||||
$total_revenues = floatval($row['total_revenues']);
|
||||
|
||||
$sql_expenses = mysqli_query($mysqli, "SELECT SUM(expense_amount) AS total_expenses FROM expenses WHERE expense_account_id = $account_id");
|
||||
$row = mysqli_fetch_array($sql_expenses);
|
||||
$total_expenses = floatval($row['total_expenses']);
|
||||
|
||||
$balance = $opening_balance + $total_payments + $total_revenues - $total_expenses;
|
||||
?>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<a class="text-dark" href="#"
|
||||
data-toggle="ajax-modal"
|
||||
data-ajax-url="ajax/ajax_account_edit.php"
|
||||
data-ajax-id="<?php echo $account_id; ?>"
|
||||
>
|
||||
<?php echo $account_name; ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?php echo $account_currency_code; ?></td>
|
||||
<td class="text-right"><?php echo numfmt_format_currency($currency_format, $balance, $account_currency_code); ?></td>
|
||||
<td>
|
||||
<div class="dropdown dropleft text-center">
|
||||
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#"
|
||||
data-toggle="ajax-modal"
|
||||
data-ajax-url="ajax/ajax_account_edit.php"
|
||||
data-ajax-id="<?php echo $account_id; ?>"
|
||||
>
|
||||
<i class="fas fa-fw fa-edit mr-2"></i>Edit
|
||||
</a>
|
||||
<?php if ($balance == 0 && $account_id != $config_stripe_account) { //Cannot Archive an Account until it reaches 0 Balance and cant be selected as an online account ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger" href="post.php?archive_account=<?php echo $account_id; ?>&csrf_token=<?php echo $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-archive mr-2"></i>Archive
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php require_once "includes/filter_footer.php"; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
|
||||
require_once "modals/account_add_modal.php";
|
||||
require_once "includes/footer.php";
|
||||
118
admin/ai_models.php
Normal file
118
admin/ai_models.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
// Default Column Sortby Filter
|
||||
$sort = "ai_model_name";
|
||||
$order = "ASC";
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT ai_model_id, ai_model_name, ai_model_prompt, ai_model_use_case, ai_provider_id,
|
||||
ai_provider_name FROM ai_models LEFT JOIN ai_providers ON ai_model_ai_provider_id = ai_provider_id ORDER BY $sort $order");
|
||||
|
||||
$num_rows = mysqli_num_rows($sql);
|
||||
|
||||
?>
|
||||
|
||||
<ol class="breadcrumb d-print-none">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="/admin">Admin</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="ai_providers.php">AI Providers</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active">AI Models</li>
|
||||
</ol>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-2">
|
||||
<h3 class="card-title mt-2"><i class="fas fa-fw fa-robot me-2"></i>AI Models</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/ai/ai_model_add.php"><i class="fas fa-plus me-2"></i>Add Model</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-striped table-borderless table-hover">
|
||||
<thead class="text-dark <?php if ($num_rows == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ai_model_name&order=<?= $disp ?>">
|
||||
Model <?php if ($sort == 'ai_model_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ai_provider_name&order=<?= $disp ?>">
|
||||
Provider <?php if ($sort == 'ai_provider_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ai_model_use_case&order=<?= $disp ?>">
|
||||
Use Case<?php if ($sort == 'ai_model_use_case') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark">Prompt</a>
|
||||
</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$provider_id = intval($row['ai_provider_id']);
|
||||
$provider_name = escapeHtml($row['ai_provider_name']);
|
||||
$model_id = intval($row['ai_model_id']);
|
||||
$model_name = escapeHtml($row['ai_model_name']);
|
||||
$use_case = escapeHtml($row['ai_model_use_case']);
|
||||
$prompt = nl2br(escapeHtml($row['ai_model_prompt']));
|
||||
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<a class="text-dark text-bold ajax-modal" href="#"
|
||||
data-modal-url="modals/ai/ai_model_edit.php?id=<?= $model_id ?>">
|
||||
<?= $model_name ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?= $provider_name ?></td>
|
||||
<td><?= $use_case ?></td>
|
||||
<td><?= $prompt ?></td>
|
||||
<td>
|
||||
<div class="dropdown dropstart text-center">
|
||||
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="modals/ai/ai_model_edit.php?id=<?= $model_id ?>">
|
||||
<i class="fas fa-fw fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_ai_model=<?= $model_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php
|
||||
|
||||
}
|
||||
|
||||
if ($num_rows == 0) {
|
||||
echo "<h3 class='text-secondary mt-3' style='text-align: center'>No Records Here</h3>";
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
109
admin/ai_providers.php
Normal file
109
admin/ai_providers.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
// Default Column Sortby Filter
|
||||
$sort = "ai_provider_name";
|
||||
$order = "ASC";
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT ai_provider_api_key, ai_provider_api_url, ai_provider_id, ai_provider_name FROM ai_providers ORDER BY $sort $order");
|
||||
|
||||
$num_rows = mysqli_num_rows($sql);
|
||||
|
||||
?>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-2">
|
||||
<h3 class="card-title mt-2"><i class="fas fa-fw fa-robot me-2"></i>AI Providers</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/ai/ai_provider_add.php"><i class="fas fa-plus me-2"></i>Add Provider</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-striped table-borderless table-hover">
|
||||
<thead class="text-dark <?php if ($num_rows == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ai_provider_name&order=<?= $disp ?>">
|
||||
Provider <?php if ($sort == 'ai_provider_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ai_provider_api_url&order=<?= $disp ?>">
|
||||
URL <?php if ($sort == 'ai_provider_api_url') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ai_provider_api_key&order=<?= $disp ?>">
|
||||
Key <?php if ($sort == 'ai_provider_api_key') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<a class="text-dark">Models</a>
|
||||
</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$provider_id = intval($row['ai_provider_id']);
|
||||
$provider_name = escapeHtml($row['ai_provider_name']);
|
||||
$url = escapeHtml($row['ai_provider_api_url']);
|
||||
$key = escapeHtml($row['ai_provider_api_key']);
|
||||
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('ai_model_id') AS ai_model_count FROM ai_models WHERE ai_model_ai_provider_id = $provider_id"));
|
||||
$ai_model_count = intval($row['ai_model_count']);
|
||||
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<a class="text-dark text-bold ajax-modal" href="#"
|
||||
data-modal-url="modals/ai/ai_provider_edit.php?id=<?= $provider_id ?>">
|
||||
<?= $provider_name ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?= $url ?></td>
|
||||
<td><?= $key ?></td>
|
||||
<td class="text-center">
|
||||
<a class="badge bg-dark rounded-pill p-2" href="ai_models.php"><?= $ai_model_count ?></a>
|
||||
<td>
|
||||
<div class="dropdown dropstart text-center">
|
||||
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="modals/ai/ai_provider_edit.php?id=<?= $provider_id ?>">
|
||||
<i class="fas fa-fw fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_ai_provider=<?= $provider_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php
|
||||
|
||||
}
|
||||
|
||||
if ($num_rows == 0) {
|
||||
echo "<h3 class='text-secondary mt-3' style='text-align: center'>No Records Here</h3>";
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
170
admin/api_keys.php
Normal file
170
admin/api_keys.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
// Default Column Sortby Filter
|
||||
$sort = "api_key_name";
|
||||
$order = "ASC";
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS api_key_created_at, api_key_expire, api_key_id, api_key_name, api_key_secret, user_name FROM api_keys
|
||||
LEFT JOIN users on api_key_user_id = user_id
|
||||
WHERE (api_key_name LIKE '%$q%')
|
||||
ORDER BY $sort $order LIMIT $record_from, $record_to"
|
||||
);
|
||||
|
||||
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
|
||||
?>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-2">
|
||||
<h3 class="card-title mt-2"><i class="fas fa-fw fa-key me-2"></i>API Keys</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/api/api_key_add.php"><i class="fas fa-plus me-2"></i>New API Key</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<form autocomplete="off">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="input-group mb-3 mb-md-0">
|
||||
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search keys">
|
||||
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<div class="btn-group float-end">
|
||||
<div class="dropdown ms-2" id="bulkActionButton" hidden>
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-fw fa-layer-group me-2"></i>Bulk Action (<span id="selectedCount">0</span>)
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<button class="dropdown-item text-danger text-bold"
|
||||
type="submit" form="bulkActions" name="bulk_delete_api_keys">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<hr>
|
||||
|
||||
<div class="table-responsive-sm">
|
||||
|
||||
<form id="bulkActions" action="post.php" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<table class="table table-striped table-borderless table-hover">
|
||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<td class="pe-0">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" onclick="checkAll(this)">
|
||||
</div>
|
||||
</td>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=api_key_name&order=<?= $disp ?>">
|
||||
Name <?php if ($sort == 'api_key_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=api_key_user_id&order=<?= $disp ?>">
|
||||
User <?php if ($sort == 'api_key_user_id') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=api_key_secret&order=<?= $disp ?>">
|
||||
Secret <?php if ($sort == 'api_key_secret') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=api_key_created_at&order=<?= $disp ?>">
|
||||
Created <?php if ($sort == 'api_key_created_at') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=api_key_expire&order=<?= $disp ?>">
|
||||
Expires <?php if ($sort == 'api_key_expire') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$api_key_id = intval($row['api_key_id']);
|
||||
$api_key_name = escapeHtml($row['api_key_name']);
|
||||
$api_key_secret = escapeHtml("************" . substr($row['api_key_secret'], -4));
|
||||
$api_key_created_at = escapeHtml($row['api_key_created_at']);
|
||||
$api_key_expire = escapeHtml($row['api_key_expire']);
|
||||
if ($api_key_expire < date("Y-m-d H:i:s")) {
|
||||
$api_key_expire = $api_key_expire . " (Expired)";
|
||||
}
|
||||
|
||||
$api_key_user = !empty($row['user_name']) ? escapeHtml($row['user_name']) : "<i>None</i>";
|
||||
|
||||
?>
|
||||
<tr>
|
||||
<td class="pe-0">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input bulk-select" type="checkbox" name="api_key_ids[]" value="<?= $api_key_id ?>">
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-bold"><?= $api_key_name ?></td>
|
||||
<td><?= $api_key_user ?></td>
|
||||
<td><?= $api_key_secret ?></td>
|
||||
<td><?= $api_key_created_at ?></td>
|
||||
<td><?= $api_key_expire ?></td>
|
||||
<td>
|
||||
<div class="dropdown dropstart text-center">
|
||||
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/api/api_key_edit.php?id=<?= $api_key_id ?>">
|
||||
<i class="fas fa-fw fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
<?php if ($api_key_expire > date("Y-m-d H:i:s")) { ?>
|
||||
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?revoke_api_key=<?= $api_key_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-times me-2"></i>Revoke
|
||||
</a>
|
||||
<?php } ?>
|
||||
<?php if ($api_key_expire < date("Y-m-d H:i:s")) { ?>
|
||||
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_api_key=<?= $api_key_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-times me-2"></i>Delete
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<?php require_once "../includes/filter_footer.php"; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../js/bulk_actions.js"></script>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
171
admin/app_logs.php
Normal file
171
admin/app_logs.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
// Default Column Sortby Filter
|
||||
$sort = "app_log_id";
|
||||
$order = "DESC";
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
// Log Type Filter
|
||||
if (isset($_GET['type']) & !empty($_GET['type'])) {
|
||||
$log_type_query = "AND (app_log_type = '" . escapeSql($_GET['type']) . "')";
|
||||
$type_filter = escapeHtml($_GET['type']);
|
||||
} else {
|
||||
// Default - any
|
||||
$log_type_query = '';
|
||||
$type_filter = '';
|
||||
}
|
||||
|
||||
// Log Category Filter
|
||||
if (isset($_GET['category']) & !empty($_GET['catergory'])) {
|
||||
$log_category_query = "AND (app_log_category = '" . escapeSql($_GET['category']) . "')";
|
||||
$category_filter = escapeHtml($_GET['category']);
|
||||
} else {
|
||||
// Default - any
|
||||
$log_category_query = '';
|
||||
$category_filter = '';
|
||||
}
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS app_log_category, app_log_created_at, app_log_details, app_log_id, app_log_type FROM app_logs
|
||||
WHERE (app_log_type LIKE '%$q%' OR app_log_category LIKE '%$q%' OR app_log_details LIKE '%$q%')
|
||||
AND DATE(app_log_created_at) BETWEEN '$dtf' AND '$dtt'
|
||||
$log_type_query
|
||||
$log_category_query
|
||||
ORDER BY $sort $order LIMIT $record_from, $record_to"
|
||||
);
|
||||
|
||||
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
|
||||
?>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-3">
|
||||
<h3 class="card-title"><i class="fas fa-fw fa-history me-2"></i>App Logs</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search app logs">
|
||||
<button class="btn btn-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#advancedFilter"><i class="fas fa-filter"></i></button>
|
||||
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-2">
|
||||
<div class="mb-3">
|
||||
<select class="form-select select2" name="type" onchange="this.form.submit()">
|
||||
<option value="">- All Types -</option>
|
||||
|
||||
<?php
|
||||
$sql_types_filter = mysqli_query($mysqli, "SELECT DISTINCT app_log_type FROM app_logs ORDER BY app_log_type ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_types_filter)) {
|
||||
$log_type = escapeHtml($row['app_log_type']);
|
||||
?>
|
||||
<option <?php if ($type_filter == $log_type) { echo "selected"; } ?>><?= $log_type ?></option>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-2">
|
||||
<div class="mb-3">
|
||||
<select class="form-select select2" name="category" onchange="this.form.submit()">
|
||||
<option value="">- All Categories -</option>
|
||||
|
||||
<?php
|
||||
$sql_categories_filter = mysqli_query($mysqli, "SELECT DISTINCT app_log_category FROM app_logs ORDER BY app_log_category ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_categories_filter)) {
|
||||
$log_category = escapeHtml($row['app_log_category']);
|
||||
?>
|
||||
<option <?php if ($category_filter == $log_category) { echo "selected"; } ?>><?= $log_category ?></option>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse mt-3 <?php if (isset($_GET['dtf']) && $_GET['dtf'] !== '1970-01-01') { echo "show"; } ?>" id="advancedFilter">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label>Date range</label>
|
||||
<input type="text" id="dateFilter" class="form-control" autocomplete="off">
|
||||
<input type="hidden" name="canned_date" id="canned_date" value="<?= escapeHtml($_GET['canned_date']) ?? '' ?>">
|
||||
<input type="hidden" name="dtf" id="dtf" value="<?= escapeHtml($dtf ?? '') ?>">
|
||||
<input type="hidden" name="dtt" id="dtt" value="<?= escapeHtml($dtt ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-sm table-striped table-borderless table-hover">
|
||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=app_log_created_at&order=<?= $disp ?>">
|
||||
Timestamp <?php if ($sort == 'app_log_created_at') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=app_log_type&order=<?= $disp ?>">
|
||||
Type <?php if ($sort == 'app_log_type') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=app_log_category&order=<?= $disp ?>">
|
||||
Category <?php if ($sort == 'app_log_category') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=app_log_details&order=<?= $disp ?>">
|
||||
Details <?php if ($sort == 'app_log_details') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$log_id = intval($row['app_log_id']);
|
||||
$log_type = escapeHtml($row['app_log_type']);
|
||||
$log_category = escapeHtml($row['app_log_category']);
|
||||
$log_details = escapeHtml($row['app_log_details']);
|
||||
$log_created_at = escapeHtml($row['app_log_created_at']);
|
||||
|
||||
?>
|
||||
|
||||
<tr>
|
||||
<td class="font-monospace"><?= $log_created_at ?></td>
|
||||
<td><?= $log_type ?></td>
|
||||
<td><?= $log_category ?></td>
|
||||
<td><?= $log_details ?></td>
|
||||
</tr>
|
||||
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php require_once "../includes/filter_footer.php";
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
281
admin/audit_logs.php
Normal file
281
admin/audit_logs.php
Normal file
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
|
||||
// Default Column Sortby Filter
|
||||
$sort = "log_id";
|
||||
$order = "DESC";
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
// User Filter
|
||||
if (isset($_GET['user']) & !empty($_GET['user'])) {
|
||||
$user_query = 'AND (log_user_id = ' . intval($_GET['user']) . ')';
|
||||
$user_filter = intval($_GET['user']);
|
||||
} else {
|
||||
// Default - any
|
||||
$user_query = '';
|
||||
$user_filter = '';
|
||||
}
|
||||
|
||||
// Client Filter
|
||||
if (isset($_GET['client']) & !empty($_GET['client'])) {
|
||||
$client_query = 'AND (log_client_id = ' . intval($_GET['client']) . ')';
|
||||
$client_filter = intval($_GET['client']);
|
||||
} else {
|
||||
// Default - any
|
||||
$client_query = '';
|
||||
$client_filter = '';
|
||||
}
|
||||
|
||||
// Log Type Filter
|
||||
if (isset($_GET['type']) & !empty($_GET['type'])) {
|
||||
$log_type_query = "AND (log_type = '" . escapeSql($_GET['type']) . "')";
|
||||
$type_filter = escapeHtml($_GET['type']);
|
||||
} else {
|
||||
// Default - any
|
||||
$log_type_query = '';
|
||||
$type_filter = '';
|
||||
}
|
||||
|
||||
// Log Action Filter
|
||||
if (isset($_GET['action']) & !empty($_GET['action'])) {
|
||||
$log_action_query = "AND (log_action = '" . escapeSql($_GET['action']) . "')";
|
||||
$action_filter = escapeHtml($_GET['action']);
|
||||
} else {
|
||||
// Default - any
|
||||
$log_action_query = '';
|
||||
$action_filter = '';
|
||||
}
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS client_id, client_name, log_action, log_created_at, log_description, log_entity_id, log_id,
|
||||
log_ip, log_type, log_user_agent, user_id, user_name FROM logs
|
||||
LEFT JOIN users ON log_user_id = user_id
|
||||
LEFT JOIN clients ON log_client_id = client_id
|
||||
WHERE (log_type LIKE '%$q%' OR log_action LIKE '%$q%' OR log_description LIKE '%$q%' OR log_ip LIKE '%$q%' OR log_user_agent LIKE '%$q%' OR user_name LIKE '%$q%' OR client_name LIKE '%$q%')
|
||||
AND DATE(log_created_at) BETWEEN '$dtf' AND '$dtt'
|
||||
$user_query
|
||||
$client_query
|
||||
$log_type_query
|
||||
$log_action_query
|
||||
ORDER BY $sort $order LIMIT $record_from, $record_to"
|
||||
);
|
||||
|
||||
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
|
||||
?>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-3">
|
||||
<h3 class="card-title"><i class="fas fa-fw fa-history me-2"></i>Audit Logs</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="mb-4" autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<div class="input-group mb-3 mb-md-0">
|
||||
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search audit logs">
|
||||
<button class="btn btn-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#advancedFilter"><i class="fas fa-filter"></i></button>
|
||||
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-2">
|
||||
<div class="input-group mb-3 mb-md-0">
|
||||
<select class="form-select select2" name="client" onchange="this.form.submit()">
|
||||
<option value="">- All Clients -</option>
|
||||
|
||||
<?php
|
||||
$sql_clients_filter = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients ORDER BY client_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_clients_filter)) {
|
||||
$client_id = intval($row['client_id']);
|
||||
$client_name = escapeHtml($row['client_name']);
|
||||
?>
|
||||
<option <?php if ($client_filter == $client_id) { echo "selected"; } ?> value="<?= $client_id ?>"><?= $client_name ?></option>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-2">
|
||||
<div class="input-group mb-3 mb-md-0">
|
||||
<select class="form-select select2" name="user" onchange="this.form.submit()">
|
||||
<option value="">- All Users -</option>
|
||||
|
||||
<?php
|
||||
$sql_users_filter = mysqli_query($mysqli, "SELECT user_id, user_name FROM users ORDER BY user_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_users_filter)) {
|
||||
$user_id = intval($row['user_id']);
|
||||
$user_name = escapeHtml($row['user_name']);
|
||||
?>
|
||||
<option <?php if ($user_filter == $user_id) { echo "selected"; } ?> value="<?= $user_id ?>"><?= $user_name ?></option>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-2">
|
||||
<div class="input-group mb-3 mb-md-0">
|
||||
<select class="form-select select2" name="type" onchange="this.form.submit()">
|
||||
<option value="">- All Types -</option>
|
||||
|
||||
<?php
|
||||
$sql_types_filter = mysqli_query($mysqli, "SELECT DISTINCT log_type FROM logs ORDER BY log_type ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_types_filter)) {
|
||||
$log_type = escapeHtml($row['log_type']);
|
||||
?>
|
||||
<option <?php if ($type_filter == $log_type) { echo "selected"; } ?>><?= $log_type ?></option>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-2">
|
||||
<div class="input-group mb-3 mb-md-0">
|
||||
<select class="form-select select2" name="action" onchange="this.form.submit()">
|
||||
<option value="">- All Actions -</option>
|
||||
|
||||
<?php
|
||||
$sql_actions_filter = mysqli_query($mysqli, "SELECT DISTINCT log_action FROM logs ORDER BY log_action ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_actions_filter)) {
|
||||
$log_action = escapeHtml($row['log_action']);
|
||||
?>
|
||||
<option <?php if ($action_filter == $log_action) { echo "selected"; } ?>><?= $log_action ?></option>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse mt-3 <?php if (isset($_GET['dtf']) && $_GET['dtf'] !== '1970-01-01') { echo "show"; } ?>" id="advancedFilter">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label>Date range</label>
|
||||
<input type="text" id="dateFilter" class="form-control" autocomplete="off">
|
||||
<input type="hidden" name="canned_date" id="canned_date" value="<?= escapeHtml($_GET['canned_date']) ?? '' ?>">
|
||||
<input type="hidden" name="dtf" id="dtf" value="<?= escapeHtml($dtf ?? '') ?>">
|
||||
<input type="hidden" name="dtt" id="dtt" value="<?= escapeHtml($dtt ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-sm table-striped table-borderless table-hover">
|
||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
|
||||
<tr>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=log_created_at&order=<?= $disp ?>">
|
||||
Timestamp <?php if ($sort == 'log_created_at') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=user_name&order=<?= $disp ?>">
|
||||
User <?php if ($sort == 'user_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<?php if (empty($client)) { ?>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=client_name&order=<?= $disp ?>">
|
||||
Client <?php if ($sort == 'client_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<?php } ?>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=log_type&order=<?= $disp ?>">
|
||||
Type <?php if ($sort == 'log_type') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=log_action&order=<?= $disp ?>">
|
||||
Action <?php if ($sort == 'log_action') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=log_description&order=<?= $disp ?>">
|
||||
Description <?php if ($sort == 'log_description') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=log_ip&order=<?= $disp ?>">
|
||||
IP Address <?php if ($sort == 'log_ip') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=log_user_agent&order=<?= $disp ?>">
|
||||
User Agent <?php if ($sort == 'log_user_agent') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$log_id = intval($row['log_id']);
|
||||
$log_type = escapeHtml($row['log_type']);
|
||||
$log_action = escapeHtml($row['log_action']);
|
||||
$log_description = escapeHtml($row['log_description']);
|
||||
$log_ip = escapeHtml($row['log_ip']);
|
||||
$log_user_agent = escapeHtml($row['log_user_agent']);
|
||||
$log_user_os = getOS($log_user_agent);
|
||||
$log_user_browser = getWebBrowser($log_user_agent);
|
||||
$log_created_at = escapeHtml($row['log_created_at']);
|
||||
$user_id = intval($row['user_id']);
|
||||
$user_name = escapeHtml($row['user_name']);
|
||||
if (empty($user_name)) {
|
||||
$user_name_display = "-";
|
||||
} else {
|
||||
$user_name_display = $user_name;
|
||||
}
|
||||
$client_name = escapeHtml($row['client_name']);
|
||||
$client_id = intval($row['client_id']);
|
||||
if (empty($client_name)) {
|
||||
$client_name_display = "-";
|
||||
} else {
|
||||
$client_name_display = "<a href='../agent/client_overview.php?client_id=$client_id'>$client_name</a>";
|
||||
}
|
||||
$log_entity_id = intval($row['log_entity_id']);
|
||||
|
||||
?>
|
||||
|
||||
<tr>
|
||||
<td class="font-monospace"><?= $log_created_at ?></td>
|
||||
<td><?= $user_name_display ?></td>
|
||||
<?php if(empty($client)) { ?>
|
||||
<td><?= $client_name_display ?></td>
|
||||
<?php } ?>
|
||||
<td><?= $log_type ?></td>
|
||||
<td><?= $log_action ?></td>
|
||||
<td><?= $log_description ?></td>
|
||||
<td class="font-monospace"><?= $log_ip ?></td>
|
||||
<td><?= "$log_user_os<div class='text-secondary'>$log_user_browser</div>" ?></td>
|
||||
</tr>
|
||||
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php require_once "../includes/filter_footer.php";
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
291
admin/backup.php
Normal file
291
admin/backup.php
Normal file
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
$backup_key = backupEncryptionKey();
|
||||
$backup_dir = backupStorageDir();
|
||||
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_enable_cron, config_cron_last_dispatch_at, config_backup_retention_days, config_backup_retention_count, config_backup_cron_type FROM settings WHERE company_id = 1"));
|
||||
|
||||
$config_enable_cron = intval($row['config_enable_cron']);
|
||||
$cron_last_dispatch_at = $row['config_cron_last_dispatch_at'];
|
||||
$config_backup_retention_days = intval($row['config_backup_retention_days']);
|
||||
$config_backup_retention_count = intval($row['config_backup_retention_count']);
|
||||
$config_backup_cron_type = $row['config_backup_cron_type'];
|
||||
|
||||
// Same heartbeat rule as Maintenance > Cron - archives are built by the dispatcher, so a dead
|
||||
// crontab means the buttons below queue work that never runs
|
||||
$cron_is_running = $cron_last_dispatch_at !== null && (time() - strtotime($cron_last_dispatch_at)) < 300;
|
||||
|
||||
$backup_job = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT cron_job_enabled, cron_job_daily_at FROM cron_jobs WHERE cron_job_name = 'backup'"));
|
||||
|
||||
$backups = mysqli_query($mysqli, "SELECT backup_created_at, backup_error, backup_id, backup_size, backup_source, backup_status,
|
||||
backup_type FROM backups ORDER BY backup_created_at DESC LIMIT 100");
|
||||
|
||||
$pending_count = intval(mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(*) AS c FROM backups WHERE backup_status IN ('Pending','Running')"))['c']);
|
||||
|
||||
?>
|
||||
|
||||
<?php
|
||||
// Shown once, immediately after an export, then dropped - it must not survive a refresh
|
||||
if (!empty($_SESSION['backup_master_key_reveal'])) {
|
||||
$master_key_reveal = $_SESSION['backup_master_key_reveal'];
|
||||
unset($_SESSION['backup_master_key_reveal']);
|
||||
?>
|
||||
<div class="alert alert-warning">
|
||||
<h5><i class="fas fa-fw fa-key me-2"></i>Master encryption key</h5>
|
||||
<p class="mb-2">Shown once. Refreshing this page will not show it again.</p>
|
||||
<input type="text" class="form-control font-monospace" value="<?= escapeHtml($master_key_reveal) ?>" readonly onclick="this.select();">
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($backup_key === '') { ?>
|
||||
<div class="alert alert-danger">
|
||||
<h5><i class="fas fa-fw fa-exclamation-triangle me-2"></i>No backup encryption key</h5>
|
||||
ITFlow could not write a backup encryption key to <strong>config.php</strong>, so it cannot produce an encrypted backup.
|
||||
Make config.php writable by the web server user and reload this page, or add a line like
|
||||
<code>$config_backup_key = '<32 random characters>';</code> to it yourself.
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<?php if (!$cron_is_running) { ?>
|
||||
<div class="alert alert-danger">
|
||||
<h5><i class="fas fa-fw fa-exclamation-triangle me-2"></i>Cron is not running</h5>
|
||||
Backups are built by the cron dispatcher, not by your browser. Until cron is running, anything you
|
||||
start here will sit in the queue. See <a href="cron.php">Maintenance > Cron</a>.
|
||||
</div>
|
||||
<?php } elseif ($config_enable_cron == 0) { ?>
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-fw fa-exclamation-circle me-2"></i>Cron is switched off in
|
||||
<a href="cron.php">Maintenance > Cron</a>.
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="card card-dark mb-3">
|
||||
<div class="card-header py-3">
|
||||
<h3 class="card-title"><i class="fas fa-fw fa-cloud-upload-alt me-2"></i>Create a Backup</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<?php if ($pending_count > 0) { ?>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-fw fa-spinner me-2"></i><strong><?= $pending_count ?></strong> backup<?= $pending_count == 1 ? ' is' : 's are' ?>
|
||||
queued or building. You will get a notification when ready - this page does not refresh itself.
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="border rounded p-3 h-100 text-center">
|
||||
<i class="fas fa-fw fa-3x fa-box-open text-dark mb-3"></i>
|
||||
<h5>Full Backup</h5>
|
||||
<p class="text-muted small">The database and everything in the uploads folder. This is the one to keep.</p>
|
||||
<a class="btn btn-primary <?= $backup_key === '' ? 'disabled' : '' ?>" href="post.php?queue_backup=full&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-play me-2"></i>Start
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="border rounded p-3 h-100 text-center">
|
||||
<i class="fas fa-fw fa-3x fa-database text-dark mb-3"></i>
|
||||
<h5>Database Only</h5>
|
||||
<p class="text-muted small">Just the SQL dump. Much smaller and much quicker, but no attachments or documents.</p>
|
||||
<a class="btn btn-primary <?= $backup_key === '' ? 'disabled' : '' ?>" href="post.php?queue_backup=database&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-play me-2"></i>Start
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="border rounded p-3 h-100 text-center">
|
||||
<i class="fas fa-fw fa-3x fa-key text-dark mb-3"></i>
|
||||
<h5>Master Key</h5>
|
||||
<p class="text-muted small">The credential vault key. Only needed if every user password is lost - a normal restore recovers the vault on its own.</p>
|
||||
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#masterKeyModal">
|
||||
<i class="fas fa-fw fa-key me-2"></i>Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-dark mb-3">
|
||||
<div class="card-header py-3">
|
||||
<h3 class="card-title"><i class="fas fa-fw fa-archive me-2"></i>Backups</h3>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-borderless mb-0">
|
||||
<thead class="text-dark">
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Created</th>
|
||||
<th>Size</th>
|
||||
<th>Source</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (mysqli_num_rows($backups) === 0) { ?>
|
||||
<tr><td colspan="6" class="text-center text-muted py-4">No backups yet.</td></tr>
|
||||
<?php } ?>
|
||||
<?php while ($backup = mysqli_fetch_assoc($backups)) {
|
||||
|
||||
$backup_id = intval($backup['backup_id']);
|
||||
$status = $backup['backup_status'];
|
||||
|
||||
$badge = 'secondary';
|
||||
if ($status === 'Complete') { $badge = 'success'; }
|
||||
if ($status === 'Failed') { $badge = 'danger'; }
|
||||
if ($status === 'Missing') { $badge = 'warning'; }
|
||||
if ($status === 'Running' || $status === 'Pending') { $badge = 'info'; }
|
||||
?>
|
||||
<tr>
|
||||
<td><?= escapeHtml(backupTypeLabel($backup['backup_type'])) ?></td>
|
||||
<td><?= escapeHtml($backup['backup_created_at']) ?></td>
|
||||
<td><?= $backup['backup_size'] > 0 ? escapeHtml(backupFormatBytes($backup['backup_size'])) : '-' ?></td>
|
||||
<td><?= escapeHtml($backup['backup_source']) ?></td>
|
||||
<td>
|
||||
<span class="badge text-bg-<?= $badge ?>"><?= escapeHtml($status) ?></span>
|
||||
<?php if (!empty($backup['backup_error'])) { ?>
|
||||
<br><small class="text-danger"><?= escapeHtml($backup['backup_error']) ?></small>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<?php if ($status === 'Complete') { ?>
|
||||
<a class="btn btn-sm btn-primary" href="backup_download.php?backup_id=<?= $backup_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-download"></i>
|
||||
</a>
|
||||
<?php } ?>
|
||||
<a class="btn btn-sm btn-danger confirm-link" href="post.php?delete_backup=<?= $backup_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-dark mb-3">
|
||||
<div class="card-header py-3">
|
||||
<h3 class="card-title"><i class="fas fa-fw fa-lock me-2"></i>Encryption Key</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning mb-3">
|
||||
<i class="fas fa-fw fa-exclamation-triangle me-2"></i>
|
||||
<strong>Write this down and keep it somewhere other than this server.</strong>
|
||||
Every backup is encrypted with it, and without it a backup cannot be restored - not by you,
|
||||
not by anyone. It is stored in config.php and never in the database, which is what stops a
|
||||
stolen backup from carrying its own key.
|
||||
</div>
|
||||
|
||||
<?php if ($backup_key !== '') { ?>
|
||||
<div class="input-group col-md-6 px-0">
|
||||
<input type="text" class="form-control font-monospace" value="<?= escapeHtml($backup_key) ?>" readonly onclick="this.select();">
|
||||
<button class="btn btn-secondary" type="button" onclick="navigator.clipboard.writeText('<?= escapeHtml($backup_key) ?>');">
|
||||
<i class="fas fa-fw fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<p class="text-muted small mt-3 mb-0">
|
||||
Archives are AES-256 encrypted zips. <strong>7-Zip, WinZip, PeaZip and Keka</strong> can open them with this key.
|
||||
The <code>unzip</code> command, Windows Explorer and the macOS Archive Utility cannot - they do not support AES.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-dark mb-3">
|
||||
<div class="card-header py-3">
|
||||
<h3 class="card-title"><i class="fas fa-fw fa-clock me-2"></i>Scheduled Backups & Retention</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="post.php" method="POST" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="mb-3 col-md-4">
|
||||
<label>Scheduled backup type</label>
|
||||
<select class="form-select" name="config_backup_cron_type">
|
||||
<option <?= $config_backup_cron_type === 'full' ? 'selected' : '' ?> value="full">Full Backup</option>
|
||||
<option <?= $config_backup_cron_type === 'database' ? 'selected' : '' ?> value="database">Database Only</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3 col-md-4">
|
||||
<label>Keep backups for (days)</label>
|
||||
<input type="number" class="form-control" name="config_backup_retention_days" min="0" value="<?= intval($config_backup_retention_days) ?>">
|
||||
<small class="text-muted">0 disables age-based deletion.</small>
|
||||
</div>
|
||||
<div class="mb-3 col-md-4">
|
||||
<label>Keep at most (per type)</label>
|
||||
<input type="number" class="form-control" name="config_backup_retention_count" min="1" value="<?= intval($config_backup_retention_count) ?>">
|
||||
<small class="text-muted">Counted separately for each type. The newest of each is never deleted.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" name="edit_backup_settings" class="btn btn-primary"><i class="fas fa-fw fa-check me-2"></i>Save</button>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<p class="mb-0">
|
||||
<?php if (!empty($backup_job) && intval($backup_job['cron_job_enabled']) === 1) { ?>
|
||||
<i class="fas fa-fw fa-check text-success me-2"></i>Scheduled backups run daily at
|
||||
<strong><?= escapeHtml(substr((string)$backup_job['cron_job_daily_at'], 0, 5)) ?></strong>.
|
||||
<?php } else { ?>
|
||||
<i class="fas fa-fw fa-times text-danger me-2"></i>Scheduled backups are switched off.
|
||||
<?php } ?>
|
||||
Turn them on or change the time in <a href="cron.php">Maintenance > Cron</a>.
|
||||
</p>
|
||||
<p class="text-muted small mt-2 mb-0">
|
||||
Old backups are removed by the nightly job, never by the backup itself, so a failed nightly
|
||||
cannot delete an archive that was never replaced.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-3">
|
||||
<h3 class="card-title"><i class="fas fa-fw fa-undo me-2"></i>Restoring</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Restoring replaces the database and the uploads folder with what is in the archive. It cannot be done from here, on purpose - a running install is the wrong place to be dropping its own tables from a browser.</p>
|
||||
<p class="mb-2"><strong>From the command line</strong> - the only option that works for large backups:</p>
|
||||
<pre class="bg-dark text-white p-2"><?= escapeHtml("php " . dirname(__DIR__) . "/scripts/restore_cli.php --file=/path/to/backup.zip") ?></pre>
|
||||
<p class="mb-0"><strong>From a browser</strong>, on a fresh install only, the setup wizard has a restore step at <code>/setup</code>. Once an install has users, that step closes itself.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="masterKeyModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-key me-2"></i>Export Master Key</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="POST" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning">
|
||||
This key decrypts every credential in this install. It is shown on screen and is not written anywhere.
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label>Confirm your account password</label>
|
||||
<input type="password" class="form-control" name="password" autocomplete="new-password" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="backup_master_key" class="btn btn-primary"><i class="fas fa-fw fa-key me-2"></i>Show Master Key</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
61
admin/backup_download.php
Normal file
61
admin/backup_download.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - GET /admin/backup_download.php
|
||||
* Streams a backup archive to an administrator
|
||||
*
|
||||
* Deliberately NOT agent/file.php: that gates on module_client and resolves paths under
|
||||
* uploads/clients/<id>/, which would hand a full database dump to any agent with client
|
||||
* read access. A backup is an admin artifact and gets an admin-only path of its own.
|
||||
*/
|
||||
|
||||
require_once "../config.php";
|
||||
require_once "../functions.php";
|
||||
require_once "../includes/check_login.php";
|
||||
|
||||
enforceAdminPermission();
|
||||
validateCSRFToken();
|
||||
|
||||
if (!isset($_GET['backup_id'])) {
|
||||
http_response_code(400);
|
||||
exit("Backup ID required");
|
||||
}
|
||||
|
||||
$backup_id = intval($_GET['backup_id']);
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT backup_file_name FROM backups WHERE backup_id = $backup_id AND backup_status = 'Complete' LIMIT 1");
|
||||
|
||||
if (mysqli_num_rows($sql) !== 1) {
|
||||
http_response_code(404);
|
||||
exit("Backup not found");
|
||||
}
|
||||
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
|
||||
$file_path = backupResolvePath($row['backup_file_name']);
|
||||
|
||||
if ($file_path === false || !is_file($file_path)) {
|
||||
mysqli_query($mysqli, "UPDATE backups SET backup_status = 'Missing' WHERE backup_id = $backup_id");
|
||||
http_response_code(404);
|
||||
exit("Backup file is no longer on disk");
|
||||
}
|
||||
|
||||
$file_name = basename($file_path);
|
||||
|
||||
logAudit("Backup", "Download", ($session_name ?? 'Unknown User') . " downloaded backup " . escapeSql($file_name));
|
||||
mysqli_query($mysqli, "UPDATE backups SET backup_downloaded_at = NOW() WHERE backup_id = $backup_id");
|
||||
|
||||
header("Content-Type: application/zip");
|
||||
header("Content-Disposition: attachment; filename=\"$file_name\"");
|
||||
header("Content-Length: " . filesize($file_path));
|
||||
header("X-Content-Type-Options: nosniff");
|
||||
header("Cache-Control: private, no-store");
|
||||
header("Pragma: no-cache");
|
||||
|
||||
// Clear output buffers so a multi-gigabyte archive streams instead of loading into memory
|
||||
while (ob_get_level()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
readfile($file_path);
|
||||
exit;
|
||||
217
admin/categories.php
Normal file
217
admin/categories.php
Normal file
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
// Default Column Sortby Filter
|
||||
$sort = "category_name";
|
||||
$order = "ASC";
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
|
||||
if (isset($_GET['category'])) {
|
||||
$category = escapeSql($_GET['category']);
|
||||
} else {
|
||||
$category = "Expense";
|
||||
}
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS category_color, category_description, category_id, category_name FROM categories
|
||||
WHERE category_name LIKE '%$q%'
|
||||
AND category_type = '$category'
|
||||
AND category_$archive_query
|
||||
ORDER BY $sort $order LIMIT $record_from, $record_to"
|
||||
);
|
||||
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
|
||||
// Category types shown in the left nav
|
||||
$category_types = [
|
||||
'Expense' => ['label' => 'Expense', 'icon' => 'fa-shopping-cart'],
|
||||
'Income' => ['label' => 'Income', 'icon' => 'fa-hand-holding-usd'],
|
||||
'Referral' => ['label' => 'Referral', 'icon' => 'fa-share-alt'],
|
||||
'Ticket' => ['label' => 'Ticket', 'icon' => 'fa-life-ring'],
|
||||
'network_interface' => ['label' => 'Network Interface', 'icon' => 'fa-ethernet'],
|
||||
'asset_status' => ['label' => 'Asset Status', 'icon' => 'fa-heartbeat'],
|
||||
'software_type' => ['label' => 'Software Type', 'icon' => 'fa-cube'],
|
||||
'rack_type' => ['label' => 'Rack Type', 'icon' => 'fa-server'],
|
||||
'contact_note_type' => ['label' => 'Contact Note Type', 'icon' => 'fa-address-book'],
|
||||
'asset_note_type' => ['label' => 'Asset Note Type', 'icon' => 'fa-desktop'],
|
||||
];
|
||||
|
||||
// Label for the selected type, falling back for anything not in the map
|
||||
$category_label = $category_types[$category]['label'] ?? ucwords(str_replace('_', ' ', $category));
|
||||
|
||||
// Row count per type for the nav badges, respecting the archived view
|
||||
$category_type_counts = [];
|
||||
$sql_category_type_counts = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT category_type, COUNT(category_id) AS category_type_count FROM categories
|
||||
WHERE category_$archive_query
|
||||
GROUP BY category_type"
|
||||
);
|
||||
while ($row = mysqli_fetch_assoc($sql_category_type_counts)) {
|
||||
$category_type_counts[$row['category_type']] = intval($row['category_type_count']);
|
||||
}
|
||||
|
||||
// Archived nav item toggles the view while holding the selected type/search
|
||||
$archive_toggle_query = $_GET;
|
||||
unset($archive_toggle_query['page']);
|
||||
$archive_toggle_query['category'] = $category;
|
||||
if ($archived) {
|
||||
unset($archive_toggle_query['archived']);
|
||||
} else {
|
||||
$archive_toggle_query['archived'] = 1;
|
||||
}
|
||||
$archive_toggle_url = '?' . http_build_query($archive_toggle_query);
|
||||
|
||||
?>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-2">
|
||||
<h3 class="card-title mt-2"><i class="fa fa-fw fa-list-ul me-2"></i>
|
||||
<?= escapeHtml($category_label) ?> Categories
|
||||
</h3>
|
||||
<?php
|
||||
if (!isset($_GET['archived'])) {
|
||||
?>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/category/category_add.php?category=<?= escapeHtml($category) ?>"><i
|
||||
class="fas fa-plus me-2"></i>New <?= escapeHtml($category_label) ?> Category</button>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
|
||||
<!-- Category types -->
|
||||
<div class="col-md-3 border-end mb-3">
|
||||
<ul class="nav nav-pills flex-column bg-light">
|
||||
<?php foreach ($category_types as $category_type => $category_type_details) { ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link<?php if ($category == $category_type) {
|
||||
echo ' active';
|
||||
} ?>" href="?category=<?= urlencode($category_type) ?>">
|
||||
<i class="fa fa-fw <?= $category_type_details['icon'] ?> me-2"></i><?= escapeHtml($category_type_details['label']) ?>
|
||||
<span class="badge rounded-pill bg-dark float-end mt-1"><?= $category_type_counts[$category_type] ?? 0 ?></span>
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Categories -->
|
||||
<div class="col-md-9">
|
||||
<form autocomplete="off">
|
||||
<input type="hidden" name="category" value="<?= escapeHtml($category) ?>">
|
||||
<?php if ($archived) { ?>
|
||||
<input type="hidden" name="archived" value="1">
|
||||
<?php } ?>
|
||||
<div class="row">
|
||||
<div class="col-sm-6 mb-2">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control" name="q"
|
||||
value="<?php if (isset($q)) {
|
||||
echo stripslashes(escapeHtml($q));
|
||||
} ?>"
|
||||
placeholder="Search <?= escapeHtml($category_label) ?> Categories ">
|
||||
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 mb-2">
|
||||
<a href="<?= $archive_toggle_url ?>"
|
||||
class="btn float-end <?php if ($archived) {
|
||||
echo 'btn-primary';
|
||||
} else {
|
||||
echo 'btn-default';
|
||||
} ?>"><i
|
||||
class="fas fa-fw fa-archive me-2"></i>Archived</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-striped table-borderless table-hover">
|
||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=category_name&order=<?= $disp ?>">
|
||||
Name <?php if ($sort == 'category_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>Color</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$category_id = intval($row['category_id']);
|
||||
$category_name = escapeHtml($row['category_name']);
|
||||
$category_description = escapeHtml($row['category_description']);
|
||||
$category_color = escapeHtml($row['category_color']);
|
||||
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<a class="text-dark ajax-modal" href="#"
|
||||
data-modal-url="modals/category/category_edit.php?id=<?= $category_id ?>">
|
||||
<?= $category_name ?>
|
||||
<div><small class="text-secondary"><?= $category_description ?></small></div>
|
||||
</a>
|
||||
</td>
|
||||
<td><i class="fa fa-3x fa-circle" style="color:<?= $category_color ?>;"></i></td>
|
||||
<td>
|
||||
<div class="dropdown dropstart text-center">
|
||||
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<?php
|
||||
if ($archived) {
|
||||
?>
|
||||
<a class="dropdown-item text-info confirm-link"
|
||||
href="post.php?restore_category=<?= $category_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-redo me-2"></i>Restore
|
||||
</a>
|
||||
<a class="dropdown-item text-danger confirm-link"
|
||||
href="post.php?delete_category=<?= $category_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
</a>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="modals/category/category_edit.php?id=<?= $category_id ?>">
|
||||
<i class="fas fa-fw fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
<a class="dropdown-item text-danger confirm-link"
|
||||
href="post.php?archive_category=<?= $category_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-archive me-2"></i>Archive
|
||||
</a>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php require_once "../includes/filter_footer.php"; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
123
admin/contract_templates.php
Normal file
123
admin/contract_templates.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
// Default Column Sort by Filter
|
||||
$sort = "contract_template_name";
|
||||
$order = "ASC";
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
// Search query
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS * FROM contract_templates
|
||||
WHERE contract_template_name LIKE '%$q%' OR contract_template_type LIKE '%$q%' OR contract_template_name LIKE '%$q%'
|
||||
ORDER BY $sort $order LIMIT $record_from, $record_to"
|
||||
);
|
||||
|
||||
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
|
||||
?>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-2">
|
||||
<h3 class="card-title mt-2"><i class="fa fa-fw fa-file-contract me-2"></i>Contract Templates</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/contract_template/contract_template_add.php" data-modal-size="lg">
|
||||
<i class="fas fa-plus me-2"></i>New Template
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<form autocomplete="off">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search templates">
|
||||
<button class="btn btn-secondary"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-striped table-borderless table-hover">
|
||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<th>Template Name</th>
|
||||
<th>Type</th>
|
||||
<th>Update Frequency</th>
|
||||
<th>SLA (L/M/H Response)</th>
|
||||
<th>SLA (L/M/H Resolution)</th>
|
||||
<th>Hourly Rate</th>
|
||||
<th>After Hours Rate</th>
|
||||
<th>Support Hours</th>
|
||||
<th>Net Terms</th>
|
||||
<th>Created</th>
|
||||
<th>Updated</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$id = intval($row['contract_template_id']);
|
||||
$name = escapeHtml($row['contract_template_name']);
|
||||
$type = escapeHtml($row['contract_template_type']);
|
||||
$freq = escapeHtml($row['contract_template_renewal_frequency']);
|
||||
$sla_low_resp = escapeHtml($row['contract_template_sla_low_response_time']);
|
||||
$sla_med_resp = escapeHtml($row['contract_template_sla_medium_response_time']);
|
||||
$sla_high_resp = escapeHtml($row['contract_template_sla_high_response_time']);
|
||||
$sla_low_res = escapeHtml($row['contract_template_sla_low_resolution_time']);
|
||||
$sla_med_res = escapeHtml($row['contract_template_sla_medium_resolution_time']);
|
||||
$sla_high_res = escapeHtml($row['contract_template_sla_high_resolution_time']);
|
||||
$hourly_rate = escapeHtml($row['contract_template_rate_standard']);
|
||||
$after_hours = escapeHtml($row['contract_template_rate_after_hours']);
|
||||
$support_hours = escapeHtml($row['contract_template_support_hours']);
|
||||
$net_terms = escapeHtml($row['contract_template_net_terms']);
|
||||
$created = escapeHtml($row['contract_template_created_at']);
|
||||
$updated = escapeHtml($row['contract_template_updated_at']);
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<a class="text-bold" href="contract_template_details.php?contract_template_id=<?= $id ?>">
|
||||
<i class="fas fa-fw fa-file-alt text-dark"></i> <?= $name ?>
|
||||
</a>
|
||||
<div class="mt-1 text-secondary"><?= escapeHtml($row['contract_template_description']) ?></div>
|
||||
</td>
|
||||
<td><?= $type ?></td>
|
||||
<td><?= $freq ?></td>
|
||||
<td><?= "$sla_low_resp / $sla_med_resp / $sla_high_resp" ?></td>
|
||||
<td><?= "$sla_low_res / $sla_med_res / $sla_high_res" ?></td>
|
||||
<td><?= $hourly_rate ?></td>
|
||||
<td><?= $after_hours ?></td>
|
||||
<td><?= $support_hours ?></td>
|
||||
<td><?= $net_terms ?></td>
|
||||
<td><?= $created ?></td>
|
||||
<td><?= $updated ?></td>
|
||||
<td>
|
||||
<div class="dropdown dropstart text-center">
|
||||
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-size="xl"
|
||||
data-modal-url="modals/contract_template/contract_template_edit.php?id=<?= $id ?>">
|
||||
<i class="fas fa-fw fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger text-bold" href="post.php?delete_contract_template=<?= $id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
</div>
|
||||
<?php require_once "../includes/filter_footer.php"; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once "../includes/footer.php"; ?>
|
||||
208
admin/cron.php
Normal file
208
admin/cron.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/cron_jobs.php';
|
||||
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_enable_cron, config_cron_last_dispatch_at FROM settings WHERE company_id = 1"));
|
||||
|
||||
$config_enable_cron = intval($row['config_enable_cron']);
|
||||
$cron_last_dispatch_at = $row['config_cron_last_dispatch_at'];
|
||||
|
||||
// The dispatcher writes its heartbeat before it runs anything, so anything older than a few
|
||||
// minutes means the crontab entry itself is missing or failing - a different problem from a
|
||||
// job that is disabled or erroring, and the one people spend the longest not finding.
|
||||
$cron_is_running = $cron_last_dispatch_at !== null && (time() - strtotime($cron_last_dispatch_at)) < 300;
|
||||
|
||||
$cron_command = "* * * * * php " . dirname(__DIR__) . "/cron/cron.php >/dev/null";
|
||||
|
||||
// Registry order is dispatch order, so the table reads the way the cycle runs
|
||||
$cron_jobs = [];
|
||||
foreach (cronJobRegistry() as $job) {
|
||||
$cron_jobs[$job['name']] = $job;
|
||||
$cron_jobs[$job['name']]['row'] = null;
|
||||
}
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT cron_job_daily_at, cron_job_enabled, cron_job_id, cron_job_interval_minutes,
|
||||
cron_job_last_duration, cron_job_last_error, cron_job_last_error_at, cron_job_last_run_at, cron_job_last_status,
|
||||
cron_job_name, cron_job_run_now, cron_job_schedule FROM cron_jobs");
|
||||
while ($job_row = mysqli_fetch_assoc($sql)) {
|
||||
if (isset($cron_jobs[$job_row['cron_job_name']])) {
|
||||
$cron_jobs[$job_row['cron_job_name']]['row'] = $job_row;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-3">
|
||||
<h3 class="card-title"><i class="fas fa-fw fa-clock me-2"></i>Cron</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<?php if (!$cron_is_running) { ?>
|
||||
<div class="alert alert-danger">
|
||||
<h5><i class="fas fa-fw fa-exclamation-triangle me-2"></i>Cron is not running</h5>
|
||||
ITFlow last heard from cron <strong><?= escapeHtml(strtolower(cronJobTimeAgo($cron_last_dispatch_at))) ?></strong>.
|
||||
Nothing below will run - no mail is being sent, no email is being turned into tickets, and invoices are not being generated.
|
||||
Add this line to the crontab of the user that owns the ITFlow files:
|
||||
<pre class="bg-dark text-white p-2 mt-2 mb-0"><?= escapeHtml($cron_command) ?></pre>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-fw fa-check me-2"></i>Cron last checked in <strong><?= escapeHtml(strtolower(cronJobTimeAgo($cron_last_dispatch_at))) ?></strong>.
|
||||
<span class="text-muted ms-2"><?= escapeHtml($cron_command) ?></span>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($config_enable_cron == 0) { ?>
|
||||
<div class="alert alert-warning">
|
||||
<div class="float-end">
|
||||
<a class="btn btn-sm btn-warning" href="post.php?enable_cron=1&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-power-off me-2"></i>Turn cron on
|
||||
</a>
|
||||
</div>
|
||||
<h5><i class="fas fa-fw fa-exclamation-circle me-2"></i>Cron is switched off</h5>
|
||||
The dispatcher is running, but every job below stops itself immediately while this is off -
|
||||
no mail is sent, no email becomes a ticket, and nothing is invoiced.
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<p class="text-muted">
|
||||
<small>
|
||||
<i class="fas fa-fw fa-power-off me-1"></i>The master switch is <strong>on</strong>. Turning it off
|
||||
stops every job at once without touching their schedules, which is what you want on a restored
|
||||
backup or a staging clone - those come up with every job enabled and will otherwise email clients
|
||||
and charge cards. Switching back on returns you to exactly this configuration.
|
||||
<a href="post.php?disable_cron=1&csrf_token=<?= $_SESSION['csrf_token'] ?>">Turn cron off</a>.
|
||||
</small>
|
||||
</p>
|
||||
<?php } ?>
|
||||
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-borderless table-hover">
|
||||
<thead class="text-secondary">
|
||||
<tr>
|
||||
<th>Job</th>
|
||||
<th>Schedule</th>
|
||||
<th>Last Run</th>
|
||||
<th>Duration</th>
|
||||
<th>Status</th>
|
||||
<th>Next Run</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($cron_jobs as $job) {
|
||||
|
||||
$job_row = $job['row'];
|
||||
|
||||
// A job the dispatcher has not met yet has no row. It gets one on the next
|
||||
// pass, seeded from the schedule in the registry, so show that meanwhile.
|
||||
$cron_job_id = $job_row ? intval($job_row['cron_job_id']) : 0;
|
||||
$enabled = $job_row ? intval($job_row['cron_job_enabled']) : 1;
|
||||
$schedule = $job_row ? $job_row['cron_job_schedule'] : $job['schedule'];
|
||||
$interval_minutes = $job_row ? intval($job_row['cron_job_interval_minutes']) : intval($job['interval_minutes'] ?? 1);
|
||||
$daily_at = $job_row ? $job_row['cron_job_daily_at'] : ($job['daily_at'] ?? null);
|
||||
$run_now = $job_row ? intval($job_row['cron_job_run_now']) : 0;
|
||||
$last_run_at = $job_row ? $job_row['cron_job_last_run_at'] : null;
|
||||
$last_duration = $job_row ? $job_row['cron_job_last_duration'] : null;
|
||||
$last_status = $job_row ? $job_row['cron_job_last_status'] : null;
|
||||
$last_error = $job_row ? $job_row['cron_job_last_error'] : null;
|
||||
$last_error_at = $job_row ? $job_row['cron_job_last_error_at'] : null;
|
||||
|
||||
$next_run = $job_row ? cronJobNextRun($job_row) : null;
|
||||
|
||||
if ($run_now) {
|
||||
$status_badge = '<span class="badge bg-warning text-dark">Queued</span>';
|
||||
} elseif ($last_status === 'Running') {
|
||||
$status_badge = '<span class="badge bg-info text-dark">Running</span>';
|
||||
} elseif ($last_status === 'Completed') {
|
||||
$status_badge = '<span class="badge bg-success">Completed</span>';
|
||||
} elseif ($last_status === 'Failed') {
|
||||
$status_badge = '<span class="badge bg-danger">Failed</span>';
|
||||
} elseif ($last_status !== null) {
|
||||
$status_badge = '<span class="badge bg-secondary">Stopped</span>';
|
||||
} else {
|
||||
$status_badge = '<span class="badge bg-light text-dark">Never run</span>';
|
||||
}
|
||||
|
||||
?>
|
||||
<tr class="<?= $enabled ? '' : 'text-muted' ?>">
|
||||
<td>
|
||||
<strong><?= escapeHtml($job['label']) ?></strong>
|
||||
<?php if (!$enabled) { ?><span class="badge bg-secondary ms-1">Disabled</span><?php } ?>
|
||||
<br><small class="text-secondary"><?= escapeHtml($job['description']) ?></small>
|
||||
<br><small class="text-muted"><code>cron/<?= escapeHtml($job['script']) ?></code></small>
|
||||
</td>
|
||||
<td><?= escapeHtml(cronJobScheduleDescription($schedule, $interval_minutes, $daily_at)) ?></td>
|
||||
<td>
|
||||
<?= escapeHtml(cronJobTimeAgo($last_run_at)) ?>
|
||||
<?php if ($last_run_at) { ?><br><small class="text-muted"><?= escapeHtml(date('M j, g:i A', strtotime($last_run_at))) ?></small><?php } ?>
|
||||
</td>
|
||||
<td><?= $last_duration === null ? '-' : escapeHtml($last_duration) . 's' ?></td>
|
||||
<td>
|
||||
<?= $status_badge ?>
|
||||
<?php if ($last_status !== null && $last_status !== 'Running' && strlen($last_status) > 9) { ?>
|
||||
<br><small class="text-muted"><?= escapeHtml($last_status) ?></small>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td><?= $next_run === null ? '-' : escapeHtml(cronJobTimeAgo($next_run)) ?></td>
|
||||
<td class="text-center">
|
||||
<div class="dropdown dropstart text-center">
|
||||
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item <?= $cron_job_id === 0 ? 'disabled' : '' ?>" href="post.php?run_cron_job=<?= $cron_job_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-play me-2"></i>Run Now
|
||||
</a>
|
||||
<button class="dropdown-item ajax-modal <?= $cron_job_id === 0 ? 'disabled' : '' ?>" type="button" data-toggle="ajax-modal"
|
||||
data-modal-url="modals/cron/cron_edit.php?id=<?= $cron_job_id ?>">
|
||||
<i class="fas fa-fw fa-edit me-2"></i>Edit Schedule
|
||||
</button>
|
||||
<?php if ($enabled) { ?>
|
||||
<a class="dropdown-item text-danger" href="post.php?disable_cron_job=<?= $cron_job_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-pause me-2"></i>Disable
|
||||
</a>
|
||||
<?php } else { ?>
|
||||
<a class="dropdown-item text-success" href="post.php?enable_cron_job=<?= $cron_job_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-play-circle me-2"></i>Enable
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php if (!empty($last_error)) { ?>
|
||||
<tr>
|
||||
<td colspan="7" class="pt-0">
|
||||
<div class="alert alert-danger mb-0 py-2">
|
||||
<div class="float-end">
|
||||
<a class="text-danger" href="post.php?clear_cron_error=<?= $cron_job_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>" title="Dismiss">
|
||||
<i class="fas fa-fw fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
<strong><i class="fas fa-fw fa-exclamation-triangle me-2"></i>Last error</strong>
|
||||
<small class="text-muted ms-2"><?= escapeHtml(cronJobTimeAgo($last_error_at)) ?></small>
|
||||
<div class="mt-1"><small><?= escapeHtml($last_error) ?></small></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p class="text-muted mb-0">
|
||||
<small>
|
||||
<i class="fas fa-fw fa-info-circle me-1"></i>Run Now does not start the job in your browser - it asks the
|
||||
dispatcher to pick it up on its next pass, so a job starts within a minute and still runs on the command
|
||||
line with the same locking as a scheduled run. Detailed per-job output is in
|
||||
<a href="app_logs.php">App Logs</a>.
|
||||
</small>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once "../includes/footer.php"; ?>
|
||||
8
admin/custom/readme.php
Normal file
8
admin/custom/readme.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|
||||
- Custom Pages -
|
||||
If you wish to add custom pages to ITFlow, add them to this directory"
|
||||
Link to Documentation for File Directory Structure and examples
|
||||
*/
|
||||
147
admin/custom_links.php
Normal file
147
admin/custom_links.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
// Default Column Sortby Filter
|
||||
$sort = "custom_link_name";
|
||||
$order = "ASC";
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS custom_link_icon, custom_link_id, custom_link_location, custom_link_name,
|
||||
custom_link_new_tab, custom_link_order, custom_link_uri FROM custom_links
|
||||
WHERE custom_link_name LIKE '%$q%'
|
||||
ORDER BY $sort $order LIMIT $record_from, $record_to"
|
||||
);
|
||||
|
||||
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
|
||||
?>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-2">
|
||||
<h3 class="card-title mt-2"><i class="fas fa-fw fa-external-link-alt me-2"></i>Custom Links</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/custom_link/custom_link_add.php"><i class="fas fa-plus me-2"></i>New Link</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-4 mb-2">
|
||||
<form autocomplete="off">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search Links">
|
||||
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-striped table-borderless table-hover">
|
||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=custom_link_name&order=<?= $disp ?>">
|
||||
Name <?php if ($sort == 'custom_link_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=custom_link_order&order=<?= $disp ?>">
|
||||
Order <?php if ($sort == 'custom_link_order') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=custom_link_uri&order=<?= $disp ?>">
|
||||
URI / <span class="text-secondary">New Tab</span> <?php if ($sort == 'custom_link_uri') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=custom_link_location&order=<?= $disp ?>">
|
||||
Location <?php if ($sort == 'custom_link_location') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$custom_link_id = intval($row['custom_link_id']);
|
||||
$custom_link_name = escapeHtml($row['custom_link_name']);
|
||||
$custom_link_uri = escapeHtml($row['custom_link_uri']);
|
||||
$custom_link_icon = escapeHtml($row['custom_link_icon']);
|
||||
$custom_link_new_tab = intval($row['custom_link_new_tab']);
|
||||
if ($custom_link_new_tab == 1 ) {
|
||||
$custom_link_new_tab_display = "<i class='fas fa-fw fa-checkmark'></i>";
|
||||
} else {
|
||||
$custom_link_new_tab_display = "";
|
||||
}
|
||||
$custom_link_order = intval($row['custom_link_order']);
|
||||
if ($custom_link_order == 0 ) {
|
||||
$custom_link_order_display = "-";
|
||||
} else {
|
||||
$custom_link_order_display = $custom_link_order;
|
||||
}
|
||||
$custom_link_location = intval($row['custom_link_location']);
|
||||
if ($custom_link_location == 1) {
|
||||
$custom_link_location_display = "Main Side Nav";
|
||||
} elseif ($custom_link_location == 2) {
|
||||
$custom_link_location_display = "Top Nav";
|
||||
} elseif ($custom_link_location == 3) {
|
||||
$custom_link_location_display = "Client Portal Nav";
|
||||
} elseif ($custom_link_location == 4) {
|
||||
$custom_link_location_display = "Admin Nav";
|
||||
} elseif ($custom_link_location == 5) {
|
||||
$custom_link_location_display = "Reports Nav";
|
||||
}
|
||||
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<a class="ajax-modal" href="#"
|
||||
data-modal-url="modals/custom_link/custom_link_edit.php?id=<?= $custom_link_id ?>">
|
||||
<i class="fa fa-fw fa-<?= $custom_link_icon ?> me-2"></i><?= $custom_link_name ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?= $custom_link_order_display ?></td>
|
||||
<td><?= "$custom_link_uri $custom_link_new_tab_display" ?></td>
|
||||
<td><?= $custom_link_location_display ?></td>
|
||||
<td>
|
||||
<div class="dropdown dropstart text-center">
|
||||
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/custom_link/custom_link_edit.php?id=<?= $custom_link_id ?>">
|
||||
<i class="fas fa-fw fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_custom_link=<?= $custom_link_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php require_once "../includes/filter_footer.php";
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
69
admin/database_updates.php
Normal file
69
admin/database_updates.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow
|
||||
* Applies pending database migrations from admin/database_updates/
|
||||
* Used in conjunction with database_version.php
|
||||
*
|
||||
* Each file in admin/database_updates/ is named for the database version it
|
||||
* upgrades TO (e.g. 2.4.5.php contains the queries that bring 2.4.4 to 2.4.5).
|
||||
* This runner applies every file newer than the current database version, in
|
||||
* order, and bumps config_current_database_version after each one - so a
|
||||
* single run brings the database all the way to the latest version.
|
||||
*
|
||||
* To add a migration: create admin/database_updates/<new version>.php - that
|
||||
* is the whole job. The latest version is derived from the directory listing
|
||||
* (see includes/database_version.php) and this runner handles the version
|
||||
* bump, so there is no constant to update and no bump query to remember.
|
||||
*/
|
||||
|
||||
// Check if our database versions are defined
|
||||
// If undefined, the file is probably being accessed directly rather than called via post.php?update_db or update_cli.php
|
||||
if (!defined("LATEST_DATABASE_VERSION") || !defined("CURRENT_DATABASE_VERSION") || !isset($mysqli)) {
|
||||
echo "Cannot access this file directly.";
|
||||
exit();
|
||||
}
|
||||
|
||||
// Migration files include-guard against this constant
|
||||
define("FROM_DB_UPDATER", true);
|
||||
|
||||
// Outputs for the caller (post/update.php, update_cli.php)
|
||||
$database_updates_applied = []; // Versions successfully applied this run
|
||||
$database_updates_error = null; // "version: error message" if a migration failed
|
||||
|
||||
// Collect and order the migration files by version (glob sorts alphabetically,
|
||||
// which would put 2.4.10 before 2.4.9 - version_compare gets it right)
|
||||
$database_update_files = [];
|
||||
foreach (glob(__DIR__ . "/database_updates/*.php") as $file) {
|
||||
$version = basename($file, ".php");
|
||||
if (preg_match('/^\d+(\.\d+)+$/', $version)) {
|
||||
$database_update_files[$version] = $file;
|
||||
}
|
||||
}
|
||||
uksort($database_update_files, "version_compare");
|
||||
|
||||
// Apply everything newer than the current database version, in order
|
||||
// (CURRENT_DATABASE_VERSION is a constant frozen at page load, so track progress in a variable)
|
||||
$database_current_version = CURRENT_DATABASE_VERSION;
|
||||
|
||||
foreach ($database_update_files as $version => $file) {
|
||||
|
||||
if (version_compare($version, $database_current_version, "<=")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
require $file;
|
||||
} catch (Throwable $e) {
|
||||
// Stop here - config_current_database_version still points at the last
|
||||
// completed migration, so a re-run resumes at this file
|
||||
$database_updates_error = "$version: " . $e->getMessage();
|
||||
error_log("ITFlow database update to version $version failed: " . $e->getMessage());
|
||||
break;
|
||||
}
|
||||
|
||||
// Migration succeeded - bump the database version
|
||||
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '" . escapeSql($version) . "'");
|
||||
$database_current_version = $version;
|
||||
$database_updates_applied[] = $version;
|
||||
}
|
||||
184
admin/database_updates/2.0.0.php
Normal file
184
admin/database_updates/2.0.0.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.0.0 (from 1.9.9)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "RENAME TABLE `logins` TO `credentials`");
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `credentials`
|
||||
CHANGE COLUMN `login_id` `credential_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
CHANGE COLUMN `login_name` `credential_name` VARCHAR(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
CHANGE COLUMN `login_description` `credential_description` VARCHAR(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
|
||||
CHANGE COLUMN `login_category` `credential_category` VARCHAR(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
|
||||
CHANGE COLUMN `login_uri` `credential_uri` VARCHAR(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
|
||||
CHANGE COLUMN `login_uri_2` `credential_uri_2` VARCHAR(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
|
||||
CHANGE COLUMN `login_username` `credential_username` VARCHAR(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
|
||||
CHANGE COLUMN `login_password` `credential_password` VARBINARY(200) NULL DEFAULT NULL,
|
||||
CHANGE COLUMN `login_otp_secret` `credential_otp_secret` VARCHAR(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
|
||||
CHANGE COLUMN `login_note` `credential_note` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
|
||||
CHANGE COLUMN `login_important` `credential_important` TINYINT(1) NOT NULL DEFAULT '0',
|
||||
CHANGE COLUMN `login_created_at` `credential_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP(),
|
||||
CHANGE COLUMN `login_updated_at` `credential_updated_at` DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(),
|
||||
CHANGE COLUMN `login_archived_at` `credential_archived_at` DATETIME NULL DEFAULT NULL,
|
||||
CHANGE COLUMN `login_accessed_at` `credential_accessed_at` DATETIME NULL DEFAULT NULL,
|
||||
CHANGE COLUMN `login_password_changed_at` `credential_password_changed_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP(),
|
||||
CHANGE COLUMN `login_folder_id` `credential_folder_id` INT(11) NOT NULL DEFAULT '0',
|
||||
CHANGE COLUMN `login_contact_id` `credential_contact_id` INT(11) NOT NULL DEFAULT '0',
|
||||
CHANGE COLUMN `login_asset_id` `credential_asset_id` INT(11) NOT NULL DEFAULT '0',
|
||||
CHANGE COLUMN `login_client_id` `credential_client_id` INT(11) NOT NULL DEFAULT '0'
|
||||
");
|
||||
|
||||
// Rename table contact_logins to contact_credentials
|
||||
mysqli_query($mysqli, "RENAME TABLE `contact_logins` TO `contact_credentials`");
|
||||
|
||||
// Alter contact_credentials table and change login_id to credential_id
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `contact_credentials`
|
||||
CHANGE COLUMN `login_id` `credential_id` INT(11) NOT NULL
|
||||
");
|
||||
|
||||
// Clean up orphaned contact_id rows in contact_credentials
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `contact_credentials`
|
||||
WHERE `contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
|
||||
");
|
||||
|
||||
// Clean up orphaned credential_id rows in contact_credentials
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `contact_credentials`
|
||||
WHERE `credential_id` NOT IN (SELECT `credential_id` FROM `credentials`);
|
||||
");
|
||||
|
||||
// Add foreign keys to contact_credentials
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `contact_credentials`
|
||||
ADD FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`credential_id`) REFERENCES `credentials`(`credential_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// Rename table service_logins to service_credentials
|
||||
mysqli_query($mysqli, "RENAME TABLE `service_logins` TO `service_credentials`");
|
||||
|
||||
// Alter service_credentials table and change login_id to credential_id
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `service_credentials`
|
||||
CHANGE COLUMN `login_id` `credential_id` INT(11) NOT NULL
|
||||
");
|
||||
|
||||
// Clean up orphaned service_id rows in service_credentials
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `service_credentials`
|
||||
WHERE `service_id` NOT IN (SELECT `service_id` FROM `services`);
|
||||
");
|
||||
|
||||
// Clean up orphaned credential_id rows in service_credentials
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `service_credentials`
|
||||
WHERE `credential_id` NOT IN (SELECT `credential_id` FROM `credentials`);
|
||||
");
|
||||
|
||||
// Add foreign keys to service_credentials
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `service_credentials`
|
||||
ADD FOREIGN KEY (`service_id`) REFERENCES `services`(`service_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`credential_id`) REFERENCES `credentials`(`credential_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// Rename table software_logins to software_credentials
|
||||
mysqli_query($mysqli, "RENAME TABLE `software_logins` TO `software_credentials`");
|
||||
|
||||
// Alter software_credentials table and change login_id to credential_id
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `software_credentials`
|
||||
CHANGE COLUMN `login_id` `credential_id` INT(11) NOT NULL
|
||||
");
|
||||
|
||||
// Clean up orphaned software_id rows in software_credentials
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `software_credentials`
|
||||
WHERE `software_id` NOT IN (SELECT `software_id` FROM `software`);
|
||||
");
|
||||
|
||||
// Clean up orphaned credential_id rows in software_credentials
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `software_credentials`
|
||||
WHERE `credential_id` NOT IN (SELECT `credential_id` FROM `credentials`);
|
||||
");
|
||||
|
||||
// Add foreign keys to software_credentials
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `software_credentials`
|
||||
ADD FOREIGN KEY (`software_id`) REFERENCES `software`(`software_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`credential_id`) REFERENCES `credentials`(`credential_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// Rename table vendor_logins to vendor_credentials
|
||||
mysqli_query($mysqli, "RENAME TABLE `vendor_logins` TO `vendor_credentials`");
|
||||
|
||||
// Alter vendor_credentials table and change login_id to credential_id
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `vendor_credentials`
|
||||
CHANGE COLUMN `login_id` `credential_id` INT(11) NOT NULL
|
||||
");
|
||||
|
||||
// Clean up orphaned vendor_id rows in vendor_credentials
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `vendor_credentials`
|
||||
WHERE `vendor_id` NOT IN (SELECT `vendor_id` FROM `vendors`);
|
||||
");
|
||||
|
||||
// Clean up orphaned credential_id rows in vendor_credentials
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `vendor_credentials`
|
||||
WHERE `credential_id` NOT IN (SELECT `credential_id` FROM `credentials`);
|
||||
");
|
||||
|
||||
// Add foreign keys to vendor_credentials
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `vendor_credentials`
|
||||
ADD FOREIGN KEY (`vendor_id`) REFERENCES `vendors`(`vendor_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`credential_id`) REFERENCES `credentials`(`credential_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// Rename table login_tags to credential_tags
|
||||
mysqli_query($mysqli, "RENAME TABLE `login_tags` TO `credential_tags`");
|
||||
|
||||
// Alter credential_tags table and change login_id to credential_id
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `credential_tags`
|
||||
CHANGE COLUMN `login_id` `credential_id` INT(11) NOT NULL
|
||||
");
|
||||
|
||||
// Clean up orphaned tag_id rows in credential_tags
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `credential_tags`
|
||||
WHERE `tag_id` NOT IN (SELECT `tag_id` FROM `tags`);
|
||||
");
|
||||
|
||||
// Clean up orphaned credential_id rows in credential_tags
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `credential_tags`
|
||||
WHERE `credential_id` NOT IN (SELECT `credential_id` FROM `credentials`);
|
||||
");
|
||||
|
||||
// Add foreign keys to credential_tags
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `credential_tags`
|
||||
ADD FOREIGN KEY (`tag_id`) REFERENCES `tags`(`tag_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`credential_id`) REFERENCES `credentials`(`credential_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// Create asset_credentials table with foreign keys
|
||||
mysqli_query($mysqli, "
|
||||
CREATE TABLE `asset_credentials` (
|
||||
`credential_id` INT(11) NOT NULL,
|
||||
`asset_id` INT(11) NOT NULL,
|
||||
PRIMARY KEY (`credential_id`, `asset_id`),
|
||||
FOREIGN KEY (`credential_id`) REFERENCES `credentials`(`credential_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
|
||||
)
|
||||
");
|
||||
15
admin/database_updates/2.0.1.php
Normal file
15
admin/database_updates/2.0.1.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.0.1 (from 2.0.0)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
//Dropping patch panel as a patch panel can be documented as an asset with interfaces.
|
||||
mysqli_query($mysqli, "DROP TABLE `patch_panel_ports`");
|
||||
mysqli_query($mysqli, "DROP TABLE `patch_panels`");
|
||||
|
||||
mysqli_query($mysqli, "RENAME TABLE `events` TO `calendar_events`");
|
||||
mysqli_query($mysqli, "RENAME TABLE `event_attendees` TO `calendar_event_attendees`");
|
||||
153
admin/database_updates/2.0.2.php
Normal file
153
admin/database_updates/2.0.2.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.0.2 (from 2.0.1)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Clean up orphaned data before adding foreign keys
|
||||
|
||||
// Clean up orphaned asset_custom_asset_id rows in asset_custom
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `asset_custom`
|
||||
WHERE `asset_custom_asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
|
||||
");
|
||||
|
||||
// Add foreign key to asset_custom
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `asset_custom`
|
||||
ADD FOREIGN KEY (`asset_custom_asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// Clean up orphaned asset_id rows in asset_documents
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `asset_documents`
|
||||
WHERE `asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
|
||||
");
|
||||
|
||||
// Clean up orphaned document_id rows in asset_documents
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `asset_documents`
|
||||
WHERE `document_id` NOT IN (SELECT `document_id` FROM `documents`);
|
||||
");
|
||||
|
||||
// Add foreign keys to asset_documents
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `asset_documents`
|
||||
ADD FOREIGN KEY (`asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`document_id`) REFERENCES `documents`(`document_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// Clean up orphaned asset_id rows in asset_files
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `asset_files`
|
||||
WHERE `asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
|
||||
");
|
||||
|
||||
// Clean up orphaned file_id rows in asset_files
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `asset_files`
|
||||
WHERE `file_id` NOT IN (SELECT `file_id` FROM `files`);
|
||||
");
|
||||
|
||||
// Add foreign keys to asset_files
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `asset_files`
|
||||
ADD FOREIGN KEY (`asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`file_id`) REFERENCES `files`(`file_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// Clean up orphaned asset_history_asset_id rows in asset_history
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `asset_history`
|
||||
WHERE `asset_history_asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
|
||||
");
|
||||
|
||||
// Add foreign key to asset_history
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `asset_history`
|
||||
ADD FOREIGN KEY (`asset_history_asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// Clean up orphaned interface_asset_id rows in asset_interfaces
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `asset_interfaces`
|
||||
WHERE `interface_asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
|
||||
");
|
||||
|
||||
// Add foreign key to asset_interfaces
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `asset_interfaces`
|
||||
ADD FOREIGN KEY (`interface_asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// Clean up orphaned asset_note_asset_id rows in asset_notes
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `asset_notes`
|
||||
WHERE `asset_note_asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
|
||||
");
|
||||
|
||||
// Add foreign key to asset_notes
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `asset_notes`
|
||||
ADD FOREIGN KEY (`asset_note_asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// Clean up orphaned contact_id rows in contact_assets
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `contact_assets`
|
||||
WHERE `contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
|
||||
");
|
||||
|
||||
// Clean up orphaned asset_id rows in contact_assets
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `contact_assets`
|
||||
WHERE `asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
|
||||
");
|
||||
|
||||
// Add foreign keys to contact_assets
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `contact_assets`
|
||||
ADD FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// Clean up orphaned service_id rows in service_assets
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `service_assets`
|
||||
WHERE `service_id` NOT IN (SELECT `service_id` FROM `services`);
|
||||
");
|
||||
|
||||
// Clean up orphaned asset_id rows in service_assets
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `service_assets`
|
||||
WHERE `asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
|
||||
");
|
||||
|
||||
// Add foreign keys to service_assets
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `service_assets`
|
||||
ADD FOREIGN KEY (`service_id`) REFERENCES `services`(`service_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// Clean up orphaned software_id rows in software_assets
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `software_assets`
|
||||
WHERE `software_id` NOT IN (SELECT `software_id` FROM `software`);
|
||||
");
|
||||
|
||||
// Clean up orphaned asset_id rows in software_assets
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `software_assets`
|
||||
WHERE `asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
|
||||
");
|
||||
|
||||
// Add foreign keys to software_assets
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `software_assets`
|
||||
ADD FOREIGN KEY (`software_id`) REFERENCES `software`(`software_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
|
||||
");
|
||||
31
admin/database_updates/2.0.3.php
Normal file
31
admin/database_updates/2.0.3.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.0.3 (from 2.0.2)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Clean up orphans
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `calendar_event_attendees`
|
||||
WHERE `attendee_event_id` NOT IN (SELECT `event_id` FROM `calendar_events`);
|
||||
");
|
||||
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `calendar_events`
|
||||
WHERE `event_calendar_id` NOT IN (SELECT `calendar_id` FROM `calendars`);
|
||||
");
|
||||
|
||||
// Add foreign key to calendar_event_attendees
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `calendar_event_attendees`
|
||||
ADD FOREIGN KEY (`attendee_event_id`) REFERENCES `calendar_events`(`event_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// Add foreign key to calendar_events
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `calendar_events`
|
||||
ADD FOREIGN KEY (`event_calendar_id`) REFERENCES `calendars`(`calendar_id`) ON DELETE CASCADE
|
||||
");
|
||||
20
admin/database_updates/2.0.4.php
Normal file
20
admin/database_updates/2.0.4.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.0.4 (from 2.0.3)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Clean up orphaned history
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `certificate_history`
|
||||
WHERE `certificate_history_certificate_id` NOT IN (SELECT `certificate_id` FROM `certificates`);
|
||||
");
|
||||
|
||||
// Add foreign key certificate history
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `certificate_history`
|
||||
ADD FOREIGN KEY (`certificate_history_certificate_id`) REFERENCES `certificates`(`certificate_id`) ON DELETE CASCADE
|
||||
");
|
||||
364
admin/database_updates/2.0.5.php
Normal file
364
admin/database_updates/2.0.5.php
Normal file
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.0.5 (from 2.0.4)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Clean up orphaned history
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `client_notes`
|
||||
WHERE `client_note_client_id` NOT IN (SELECT `client_id` FROM `clients`);
|
||||
");
|
||||
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `client_notes`
|
||||
ADD FOREIGN KEY (`client_note_client_id`) REFERENCES `clients`(`client_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// Clean up orphaned history
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `client_tags`
|
||||
WHERE `client_id` NOT IN (SELECT `client_id` FROM `clients`);
|
||||
");
|
||||
|
||||
// Clean up orphaned history
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `client_tags`
|
||||
WHERE `tag_id` NOT IN (SELECT `tag_id` FROM `tags`);
|
||||
");
|
||||
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `client_tags`
|
||||
ADD FOREIGN KEY (`client_id`) REFERENCES `clients`(`client_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`tag_id`) REFERENCES `tags`(`tag_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
//Contact Assets
|
||||
// Clean up orphaned history
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `contact_assets`
|
||||
WHERE `contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
|
||||
");
|
||||
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `contact_assets`
|
||||
WHERE `asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
|
||||
");
|
||||
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `contact_assets`
|
||||
ADD FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// Contact Documents
|
||||
// Clean up orphaned history
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `contact_documents`
|
||||
WHERE `contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
|
||||
");
|
||||
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `contact_documents`
|
||||
WHERE `document_id` NOT IN (SELECT `document_id` FROM `documents`);
|
||||
");
|
||||
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `contact_documents`
|
||||
ADD FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`document_id`) REFERENCES `documents`(`document_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// contact_files
|
||||
// Clean up orphaned rows
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `contact_files`
|
||||
WHERE `contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
|
||||
");
|
||||
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `contact_files`
|
||||
WHERE `file_id` NOT IN (SELECT `file_id` FROM `files`);
|
||||
");
|
||||
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `contact_files`
|
||||
ADD FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`file_id`) REFERENCES `files`(`file_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// contact_notes
|
||||
// Clean up orphaned rows
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `contact_notes`
|
||||
WHERE `contact_note_contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
|
||||
");
|
||||
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `contact_notes`
|
||||
ADD FOREIGN KEY (`contact_note_contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// contact_tags
|
||||
// Clean up orphaned rows
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `contact_tags`
|
||||
WHERE `contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
|
||||
");
|
||||
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `contact_tags`
|
||||
WHERE `tag_id` NOT IN (SELECT `tag_id` FROM `tags`);
|
||||
");
|
||||
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `contact_tags`
|
||||
ADD FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`tag_id`) REFERENCES `tags`(`tag_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// document_files
|
||||
// Clean up orphaned rows
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `document_files`
|
||||
WHERE `document_id` NOT IN (SELECT `document_id` FROM `documents`);
|
||||
");
|
||||
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `document_files`
|
||||
WHERE `file_id` NOT IN (SELECT `file_id` FROM `files`);
|
||||
");
|
||||
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `document_files`
|
||||
ADD FOREIGN KEY (`document_id`) REFERENCES `documents`(`document_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`file_id`) REFERENCES `files`(`file_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// domain_history
|
||||
// Clean up orphaned rows
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `domain_history`
|
||||
WHERE `domain_history_domain_id` NOT IN (SELECT `domain_id` FROM `domains`);
|
||||
");
|
||||
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `domain_history`
|
||||
ADD FOREIGN KEY (`domain_history_domain_id`) REFERENCES `domains`(`domain_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// location_tags
|
||||
// Clean up orphaned rows
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `location_tags`
|
||||
WHERE `location_id` NOT IN (SELECT `location_id` FROM `locations`);
|
||||
");
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `location_tags`
|
||||
WHERE `tag_id` NOT IN (SELECT `tag_id` FROM `tags`);
|
||||
");
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `location_tags`
|
||||
ADD FOREIGN KEY (`location_id`) REFERENCES `locations`(`location_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`tag_id`) REFERENCES `tags`(`tag_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// quote_files
|
||||
// Clean up orphaned rows
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `quote_files`
|
||||
WHERE `quote_id` NOT IN (SELECT `quote_id` FROM `quotes`);
|
||||
");
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `quote_files`
|
||||
WHERE `file_id` NOT IN (SELECT `file_id` FROM `files`);
|
||||
");
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `quote_files`
|
||||
ADD FOREIGN KEY (`quote_id`) REFERENCES `quotes`(`quote_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`file_id`) REFERENCES `files`(`file_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// service_certificates
|
||||
// Clean up orphaned rows
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `service_certificates`
|
||||
WHERE `service_id` NOT IN (SELECT `service_id` FROM `services`);
|
||||
");
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `service_certificates`
|
||||
WHERE `certificate_id` NOT IN (SELECT `certificate_id` FROM `certificates`);
|
||||
");
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `service_certificates`
|
||||
ADD FOREIGN KEY (`service_id`) REFERENCES `services`(`service_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`certificate_id`) REFERENCES `certificates`(`certificate_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// service_contacts
|
||||
// Clean up orphaned rows
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `service_contacts`
|
||||
WHERE `service_id` NOT IN (SELECT `service_id` FROM `services`);
|
||||
");
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `service_contacts`
|
||||
WHERE `contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
|
||||
");
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `service_contacts`
|
||||
ADD FOREIGN KEY (`service_id`) REFERENCES `services`(`service_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// service_documents
|
||||
// Clean up orphaned rows
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `service_documents`
|
||||
WHERE `service_id` NOT IN (SELECT `service_id` FROM `services`);
|
||||
");
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `service_documents`
|
||||
WHERE `document_id` NOT IN (SELECT `document_id` FROM `documents`);
|
||||
");
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `service_documents`
|
||||
ADD FOREIGN KEY (`service_id`) REFERENCES `services`(`service_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`document_id`) REFERENCES `documents`(`document_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// service_domains
|
||||
// Clean up orphaned rows
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `service_domains`
|
||||
WHERE `service_id` NOT IN (SELECT `service_id` FROM `services`);
|
||||
");
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `service_domains`
|
||||
WHERE `domain_id` NOT IN (SELECT `domain_id` FROM `domains`);
|
||||
");
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `service_domains`
|
||||
ADD FOREIGN KEY (`service_id`) REFERENCES `services`(`service_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`domain_id`) REFERENCES `domains`(`domain_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// service_vendors
|
||||
// Clean up orphaned rows
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `service_vendors`
|
||||
WHERE `service_id` NOT IN (SELECT `service_id` FROM `services`);
|
||||
");
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `service_vendors`
|
||||
WHERE `vendor_id` NOT IN (SELECT `vendor_id` FROM `vendors`);
|
||||
");
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `service_vendors`
|
||||
ADD FOREIGN KEY (`service_id`) REFERENCES `services`(`service_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`vendor_id`) REFERENCES `vendors`(`vendor_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// software_contacts
|
||||
// Clean up orphaned rows
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `software_contacts`
|
||||
WHERE `software_id` NOT IN (SELECT `software_id` FROM `software`);
|
||||
");
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `software_contacts`
|
||||
WHERE `contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
|
||||
");
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `software_contacts`
|
||||
ADD FOREIGN KEY (`software_id`) REFERENCES `software`(`software_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// software_documents
|
||||
// Clean up orphaned rows
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `software_documents`
|
||||
WHERE `software_id` NOT IN (SELECT `software_id` FROM `software`);
|
||||
");
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `software_documents`
|
||||
WHERE `document_id` NOT IN (SELECT `document_id` FROM `documents`);
|
||||
");
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `software_documents`
|
||||
ADD FOREIGN KEY (`software_id`) REFERENCES `software`(`software_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`document_id`) REFERENCES `documents`(`document_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// software_files
|
||||
// Clean up orphaned rows
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `software_files`
|
||||
WHERE `software_id` NOT IN (SELECT `software_id` FROM `software`);
|
||||
");
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `software_files`
|
||||
WHERE `file_id` NOT IN (SELECT `file_id` FROM `files`);
|
||||
");
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `software_files`
|
||||
ADD FOREIGN KEY (`software_id`) REFERENCES `software`(`software_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`file_id`) REFERENCES `files`(`file_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// vendor_documents
|
||||
// Clean up orphaned rows
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `vendor_documents`
|
||||
WHERE `vendor_id` NOT IN (SELECT `vendor_id` FROM `vendors`);
|
||||
");
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `vendor_documents`
|
||||
WHERE `document_id` NOT IN (SELECT `document_id` FROM `documents`);
|
||||
");
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `vendor_documents`
|
||||
ADD FOREIGN KEY (`vendor_id`) REFERENCES `vendors`(`vendor_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`document_id`) REFERENCES `documents`(`document_id`) ON DELETE CASCADE
|
||||
");
|
||||
|
||||
// vendor_files
|
||||
// Clean up orphaned rows
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `vendor_files`
|
||||
WHERE `vendor_id` NOT IN (SELECT `vendor_id` FROM `vendors`);
|
||||
");
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `vendor_files`
|
||||
WHERE `file_id` NOT IN (SELECT `file_id` FROM `files`);
|
||||
");
|
||||
// Add foreign key
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `vendor_files`
|
||||
ADD FOREIGN KEY (`vendor_id`) REFERENCES `vendors`(`vendor_id`) ON DELETE CASCADE,
|
||||
ADD FOREIGN KEY (`file_id`) REFERENCES `files`(`file_id`) ON DELETE CASCADE
|
||||
");
|
||||
36
admin/database_updates/2.0.6.php
Normal file
36
admin/database_updates/2.0.6.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.0.6 (from 2.0.5)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// CONVERT All tables TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci
|
||||
|
||||
$tables = [
|
||||
'accounts', 'api_keys', 'app_logs', 'asset_credentials', 'asset_custom', 'asset_documents',
|
||||
'asset_files', 'asset_history', 'asset_interface_links', 'asset_interfaces', 'asset_notes', 'assets',
|
||||
'auth_logs', 'budget', 'calendar_event_attendees', 'calendar_events', 'calendars', 'categories',
|
||||
'certificate_history', 'certificates', 'client_notes', 'client_stripe', 'client_tags', 'clients',
|
||||
'companies', 'contact_assets', 'contact_credentials', 'contact_documents', 'contact_files', 'contact_notes',
|
||||
'contact_tags', 'contacts', 'credential_tags', 'credentials', 'custom_fields', 'custom_links',
|
||||
'custom_values', 'document_files', 'documents', 'domain_history', 'domains', 'email_queue', 'expenses',
|
||||
'files', 'folders', 'history', 'invoice_items', 'invoices', 'location_tags', 'locations', 'logs',
|
||||
'modules', 'networks', 'notifications', 'payments', 'products', 'project_template_ticket_templates',
|
||||
'project_templates', 'projects', 'quote_files', 'quotes', 'rack_units', 'racks', 'records',
|
||||
'recurring_expenses', 'recurring_invoices', 'recurring_payments', 'recurring_ticket_assets', 'recurring_tickets',
|
||||
'remember_tokens', 'revenues', 'service_assets', 'service_certificates', 'service_contacts', 'service_credentials',
|
||||
'service_documents', 'service_domains', 'service_vendors', 'services', 'settings', 'shared_items',
|
||||
'software', 'software_assets', 'software_contacts', 'software_credentials', 'software_documents', 'software_files',
|
||||
'tags', 'task_templates', 'tasks', 'taxes', 'ticket_assets', 'ticket_attachments', 'ticket_history', 'ticket_replies',
|
||||
'ticket_statuses', 'ticket_templates', 'ticket_views', 'ticket_watchers', 'tickets', 'transfers', 'trips',
|
||||
'user_client_permissions', 'user_role_permissions', 'user_roles', 'user_settings', 'users', 'vendor_credentials',
|
||||
'vendor_documents', 'vendor_files', 'vendors'
|
||||
];
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$sql = "ALTER TABLE `$table` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;";
|
||||
mysqli_query($mysqli, $sql);
|
||||
}
|
||||
11
admin/database_updates/2.0.7.php
Normal file
11
admin/database_updates/2.0.7.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.0.7 (from 2.0.6)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Fix service_domains to yse InnoDB instead of MyISAM
|
||||
mysqli_query($mysqli, "ALTER TABLE service_domains ENGINE = InnoDB;");
|
||||
10
admin/database_updates/2.0.8.php
Normal file
10
admin/database_updates/2.0.8.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.0.8 (from 2.0.7)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `files` DROP `file_hash`");
|
||||
12
admin/database_updates/2.0.9.php
Normal file
12
admin/database_updates/2.0.9.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.0.9 (from 2.0.8)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `files` DROP `file_has_thumbnail`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `files` DROP `file_has_preview`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `files` DROP `file_asset_id`");
|
||||
19
admin/database_updates/2.1.0.php
Normal file
19
admin/database_updates/2.1.0.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.1.0 (from 2.0.9)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `contacts` ADD `contact_phone_country_code` VARCHAR(10) DEFAULT 1 AFTER `contact_email`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `contacts` ADD `contact_mobile_country_code` VARCHAR(10) DEFAULT 1 AFTER `contact_extension`");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `locations` ADD `location_phone_country_code` VARCHAR(10) DEFAULT 1 AFTER `location_zip`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `locations` ADD `location_phone_extension` VARCHAR(10) DEFAULT NULL AFTER `location_phone`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `locations` ADD `location_fax_country_code` VARCHAR(10) DEFAULT 1 AFTER `location_phone_extension`");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `vendors` ADD `vendor_phone_country_code` VARCHAR(10) DEFAULT 1 AFTER `vendor_contact_name`");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `companies` ADD `company_phone_country_code` VARCHAR(10) DEFAULT 1 AFTER `company_country`");
|
||||
10
admin/database_updates/2.1.1.php
Normal file
10
admin/database_updates/2.1.1.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.1.1 (from 2.1.0)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `user_settings` ADD `user_config_signature` TEXT DEFAULT NULL AFTER `user_config_calendar_first_day`");
|
||||
10
admin/database_updates/2.1.2.php
Normal file
10
admin/database_updates/2.1.2.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.1.2 (from 2.1.1)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `settings` DROP `config_phone_mask`");
|
||||
36
admin/database_updates/2.1.3.php
Normal file
36
admin/database_updates/2.1.3.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.1.3 (from 2.1.2)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Update country_code to NULL for `contacts` table
|
||||
mysqli_query($mysqli, "ALTER TABLE `contacts` MODIFY `contact_phone_country_code` VARCHAR(10) DEFAULT NULL");
|
||||
mysqli_query($mysqli, "ALTER TABLE `contacts` MODIFY `contact_mobile_country_code` VARCHAR(10) DEFAULT NULL");
|
||||
|
||||
// Update country_code to NULL for `locations` table
|
||||
mysqli_query($mysqli, "ALTER TABLE `locations` MODIFY `location_phone_country_code` VARCHAR(10) DEFAULT NULL");
|
||||
mysqli_query($mysqli, "ALTER TABLE `locations` MODIFY `location_fax_country_code` VARCHAR(10) DEFAULT NULL");
|
||||
|
||||
// Update country_code to NULL for `vendors` table
|
||||
mysqli_query($mysqli, "ALTER TABLE `vendors` MODIFY `vendor_phone_country_code` VARCHAR(10) DEFAULT NULL");
|
||||
|
||||
// Update country_code to NULL for `companies` table
|
||||
mysqli_query($mysqli, "ALTER TABLE `companies` MODIFY `company_phone_country_code` VARCHAR(10) DEFAULT NULL");
|
||||
|
||||
// Set country_code to NULL for `contacts` table
|
||||
mysqli_query($mysqli, "UPDATE `contacts` SET `contact_phone_country_code` = NULL");
|
||||
mysqli_query($mysqli, "UPDATE `contacts` SET `contact_mobile_country_code` = NULL");
|
||||
|
||||
// Set country_code to NULL for `locations` table
|
||||
mysqli_query($mysqli, "UPDATE `locations` SET `location_phone_country_code` = NULL");
|
||||
mysqli_query($mysqli, "UPDATE `locations` SET `location_fax_country_code` = NULL");
|
||||
|
||||
// Set country_code to NULL for `vendors` table
|
||||
mysqli_query($mysqli, "UPDATE `vendors` SET `vendor_phone_country_code` = NULL");
|
||||
|
||||
// Set country_code to NULL for `companies` table
|
||||
mysqli_query($mysqli, "UPDATE `companies` SET `company_phone_country_code` = NULL");
|
||||
11
admin/database_updates/2.1.4.php
Normal file
11
admin/database_updates/2.1.4.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.1.4 (from 2.1.3)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `client_stripe` ADD `stripe_pm_details` VARCHAR(200) DEFAULT NULL AFTER `stripe_pm`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `client_stripe` ADD `stripe_pm_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `stripe_pm_details`");
|
||||
13
admin/database_updates/2.1.5.php
Normal file
13
admin/database_updates/2.1.5.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.1.5 (from 2.1.4)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `settings` ADD `config_ticket_timer_autostart` TINYINT(1) NOT NULL DEFAULT '0' AFTER `config_ticket_default_billable`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `tickets` ADD `ticket_due_at` DATETIME DEFAULT NULL AFTER `ticket_updated_at`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `companies` ADD `company_tax_id` VARCHAR(200) DEFAULT NULL AFTER `company_currency`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `settings` ADD `config_invoice_show_tax_id` TINYINT(1) NOT NULL DEFAULT '0' AFTER `config_invoice_paid_notification_email`");
|
||||
27
admin/database_updates/2.1.6.php
Normal file
27
admin/database_updates/2.1.6.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.1.6 (from 2.1.5)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "CREATE TABLE `document_versions` (
|
||||
`document_version_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`document_version_name` VARCHAR(200) NOT NULL,
|
||||
`document_version_description` TEXT DEFAULT NULL,
|
||||
`document_version_content` LONGTEXT NOT NULL,
|
||||
`document_version_created_by` INT(11) DEFAULT 0,
|
||||
`document_version_created_at` DATETIME NOT NULL,
|
||||
`document_version_document_id` INT(11) NOT NULL,
|
||||
PRIMARY KEY (`document_version_id`)
|
||||
)");
|
||||
|
||||
// Delete all Current Document Versions
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `documents`
|
||||
WHERE `document_parent` > 0 AND `document_parent` != `document_id`
|
||||
");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `documents` DROP `document_parent`");
|
||||
52
admin/database_updates/2.1.7.php
Normal file
52
admin/database_updates/2.1.7.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.1.7 (from 2.1.6)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "CREATE TABLE `document_templates` (
|
||||
`document_template_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`document_template_name` VARCHAR(200) NOT NULL,
|
||||
`document_template_description` TEXT DEFAULT NULL,
|
||||
`document_template_content` LONGTEXT NOT NULL,
|
||||
`document_template_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`document_template_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`document_template_archived_at` DATETIME NULL DEFAULT NULL,
|
||||
`document_template_created_by` INT(11) NOT NULL DEFAULT 0,
|
||||
`document_template_updated_by` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`document_template_id`)
|
||||
)");
|
||||
|
||||
// Copy Document Templates over to new document templates table
|
||||
mysqli_query($mysqli, "
|
||||
INSERT INTO document_templates (
|
||||
document_template_name,
|
||||
document_template_description,
|
||||
document_template_content,
|
||||
document_template_created_at,
|
||||
document_template_updated_at,
|
||||
document_template_archived_at,
|
||||
document_template_created_by,
|
||||
document_template_updated_by
|
||||
)
|
||||
SELECT
|
||||
document_name,
|
||||
document_description,
|
||||
document_content,
|
||||
document_created_at,
|
||||
document_updated_at,
|
||||
document_archived_at,
|
||||
document_created_by,
|
||||
document_updated_by
|
||||
FROM
|
||||
documents
|
||||
WHERE
|
||||
document_template = 1
|
||||
");
|
||||
|
||||
mysqli_query($mysqli, "DELETE FROM documents WHERE document_template = 1");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `documents` DROP `document_template`");
|
||||
57
admin/database_updates/2.1.8.php
Normal file
57
admin/database_updates/2.1.8.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.1.8 (from 2.1.7)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "CREATE TABLE `software_templates` (
|
||||
`software_template_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`software_template_name` VARCHAR(200) NOT NULL,
|
||||
`software_template_description` TEXT DEFAULT NULL,
|
||||
`software_template_version` VARCHAR(200) DEFAULT NULL,
|
||||
`software_template_type` VARCHAR(200) NOT NULL,
|
||||
`software_template_license_type` VARCHAR(200) DEFAULT NULL,
|
||||
`software_template_notes` TEXT DEFAULT NULL,
|
||||
`software_template_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`software_template_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`software_template_archived_at` DATETIME NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`software_template_id`)
|
||||
)");
|
||||
|
||||
// Copy software Templates over to new software templates table
|
||||
mysqli_query($mysqli, "
|
||||
INSERT INTO software_templates (
|
||||
software_template_name,
|
||||
software_template_description,
|
||||
software_template_version,
|
||||
software_template_type,
|
||||
software_template_license_type,
|
||||
software_template_notes,
|
||||
software_template_created_at,
|
||||
software_template_updated_at,
|
||||
software_template_archived_at
|
||||
)
|
||||
SELECT
|
||||
software_name,
|
||||
software_description,
|
||||
software_version,
|
||||
software_type,
|
||||
software_license_type,
|
||||
software_notes,
|
||||
software_created_at,
|
||||
software_updated_at,
|
||||
software_archived_at
|
||||
FROM
|
||||
software
|
||||
WHERE
|
||||
software_template = 1
|
||||
");
|
||||
|
||||
mysqli_query($mysqli, "DELETE FROM software WHERE software_template = 1");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `software` DROP `software_template`");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `software` DROP `software_template_id`");
|
||||
76
admin/database_updates/2.1.9.php
Normal file
76
admin/database_updates/2.1.9.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.1.9 (from 2.1.8)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "CREATE TABLE `vendor_templates` (
|
||||
`vendor_template_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`vendor_template_name` VARCHAR(200) NOT NULL,
|
||||
`vendor_template_description` VARCHAR(200) DEFAULT NULL,
|
||||
`vendor_template_contact_name` VARCHAR(200) DEFAULT NULL,
|
||||
`vendor_template_phone_country_code` VARCHAR(10) DEFAULT NULL,
|
||||
`vendor_template_phone` VARCHAR(200) DEFAULT NULL,
|
||||
`vendor_template_extension` VARCHAR(200) DEFAULT NULL,
|
||||
`vendor_template_email` VARCHAR(200) DEFAULT NULL,
|
||||
`vendor_template_website` VARCHAR(200) DEFAULT NULL,
|
||||
`vendor_template_hours` VARCHAR(200) DEFAULT NULL,
|
||||
`vendor_template_sla` VARCHAR(200) DEFAULT NULL,
|
||||
`vendor_template_code` VARCHAR(200) DEFAULT NULL,
|
||||
`vendor_template_account_number` VARCHAR(200) DEFAULT NULL,
|
||||
`vendor_template_notes` TEXT DEFAULT NULL,
|
||||
`vendor_template_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`vendor_template_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`vendor_template_archived_at` DATETIME NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`vendor_template_id`)
|
||||
)");
|
||||
|
||||
// Copy Vendor Templates over to new vendor templates table
|
||||
mysqli_query($mysqli, "
|
||||
INSERT INTO vendor_templates (
|
||||
vendor_template_name,
|
||||
vendor_template_description,
|
||||
vendor_template_contact_name,
|
||||
vendor_template_phone_country_code,
|
||||
vendor_template_phone,
|
||||
vendor_template_extension,
|
||||
vendor_template_email,
|
||||
vendor_template_website,
|
||||
vendor_template_hours,
|
||||
vendor_template_sla,
|
||||
vendor_template_code,
|
||||
vendor_template_account_number,
|
||||
vendor_template_notes,
|
||||
vendor_template_created_at,
|
||||
vendor_template_updated_at,
|
||||
vendor_template_archived_at
|
||||
)
|
||||
SELECT
|
||||
vendor_name,
|
||||
vendor_description,
|
||||
vendor_contact_name,
|
||||
vendor_phone_country_code,
|
||||
vendor_phone,
|
||||
vendor_extension,
|
||||
vendor_email,
|
||||
vendor_website,
|
||||
vendor_hours,
|
||||
vendor_sla,
|
||||
vendor_code,
|
||||
vendor_account_number,
|
||||
vendor_notes,
|
||||
vendor_created_at,
|
||||
vendor_updated_at,
|
||||
vendor_archived_at
|
||||
FROM
|
||||
vendors
|
||||
WHERE
|
||||
vendor_template = 1
|
||||
");
|
||||
|
||||
mysqli_query($mysqli, "DELETE FROM vendors WHERE vendor_template = 1");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `vendors` DROP `vendor_template`");
|
||||
10
admin/database_updates/2.2.0.php
Normal file
10
admin/database_updates/2.2.0.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.2.0 (from 2.1.9)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `companies` MODIFY `company_currency` VARCHAR(200) DEFAULT 'USD'");
|
||||
10
admin/database_updates/2.2.1.php
Normal file
10
admin/database_updates/2.2.1.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.2.1 (from 2.2.0)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `tickets` ADD `ticket_quote_id` INT(11) NOT NULL DEFAULT 0 AFTER `ticket_asset_id`");
|
||||
34
admin/database_updates/2.2.2.php
Normal file
34
admin/database_updates/2.2.2.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.2.2 (from 2.2.1)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "CREATE TABLE `ai_providers` (
|
||||
`ai_provider_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`ai_provider_name` VARCHAR(200) NOT NULL,
|
||||
`ai_provider_api_url` VARCHAR(200) NOT NULL,
|
||||
`ai_provider_api_key` VARCHAR(200) DEFAULT NULL,
|
||||
`ai_provider_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`ai_provider_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`ai_provider_id`)
|
||||
)");
|
||||
|
||||
mysqli_query($mysqli, "
|
||||
CREATE TABLE `ai_models` (
|
||||
`ai_model_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`ai_model_name` VARCHAR(200) NOT NULL,
|
||||
`ai_model_prompt` TEXT DEFAULT NULL,
|
||||
`ai_model_use_case` VARCHAR(200) DEFAULT NULL,
|
||||
`ai_model_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`ai_model_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`ai_model_ai_provider_id` INT(11) NOT NULL,
|
||||
PRIMARY KEY (`ai_model_id`),
|
||||
FOREIGN KEY (`ai_model_ai_provider_id`)
|
||||
REFERENCES `ai_providers`(`ai_provider_id`)
|
||||
ON DELETE CASCADE
|
||||
)
|
||||
");
|
||||
62
admin/database_updates/2.2.3.php
Normal file
62
admin/database_updates/2.2.3.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.2.3 (from 2.2.2)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "CREATE TABLE `payment_methods` (
|
||||
`payment_method_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`payment_method_name` VARCHAR(200) NOT NULL,
|
||||
`payment_method_description` VARCHAR(250) DEFAULT NULL,
|
||||
`payment_method_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`payment_method_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`payment_method_id`)
|
||||
)");
|
||||
|
||||
mysqli_query($mysqli, "CREATE TABLE `payment_providers` (
|
||||
`payment_provider_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`payment_provider_name` VARCHAR(200) NOT NULL,
|
||||
`payment_provider_description` VARCHAR(250) DEFAULT NULL,
|
||||
`payment_provider_public_key` VARCHAR(250) DEFAULT NULL,
|
||||
`payment_provider_private_key` VARCHAR(250) DEFAULT NULL,
|
||||
`payment_provider_threshold` DECIMAL(15,2) DEFAULT NULL,
|
||||
`payment_provider_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`payment_provider_account` INT(11) NOT NULL,
|
||||
`payment_provider_expense_vendor` INT(11) NOT NULL DEFAULT 0,
|
||||
`payment_provider_expense_category` INT(11) NOT NULL DEFAULT 0,
|
||||
`payment_provider_expense_percentage_fee` DECIMAL(4,4) DEFAULT NULL,
|
||||
`payment_provider_expense_flat_fee` DECIMAL(15,2) DEFAULT NULL,
|
||||
`payment_provider_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`payment_provider_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`payment_provider_id`)
|
||||
)");
|
||||
|
||||
mysqli_query($mysqli, "CREATE TABLE `client_saved_payment_methods` (
|
||||
`saved_payment_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`saved_payment_provider_method` VARCHAR(200) NOT NULL,
|
||||
`saved_payment_description` VARCHAR(200) DEFAULT NULL,
|
||||
`saved_payment_client_id` INT(11) NOT NULL,
|
||||
`saved_payment_provider_id` INT(11) NOT NULL,
|
||||
`saved_payment_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`saved_payment_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`saved_payment_id`),
|
||||
FOREIGN KEY (`saved_payment_client_id`) REFERENCES clients(`client_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`saved_payment_provider_id`) REFERENCES payment_providers(`payment_provider_id`) ON DELETE CASCADE
|
||||
)");
|
||||
|
||||
mysqli_query($mysqli, "CREATE TABLE `client_payment_provider` (
|
||||
`client_id` INT(11) NOT NULL,
|
||||
`payment_provider_id` INT(11) NOT NULL,
|
||||
`payment_provider_client` VARCHAR(200) NOT NULL,
|
||||
`client_payment_provider_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`client_id`, `payment_provider_id`),
|
||||
FOREIGN KEY (`client_id`) REFERENCES clients(`client_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`payment_provider_id`) REFERENCES payment_providers(`payment_provider_id`) ON DELETE CASCADE
|
||||
)");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `recurring_payments` ADD `recurring_payment_saved_payment_id` INT(11) DEFAULT NULL AFTER `recurring_payment_recurring_invoice_id`");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `recurring_payments` ADD CONSTRAINT `fk_recurring_saved_payment` FOREIGN KEY (`recurring_payment_saved_payment_id`) REFERENCES `client_saved_payment_methods`(`saved_payment_id`) ON DELETE CASCADE");
|
||||
34
admin/database_updates/2.2.4.php
Normal file
34
admin/database_updates/2.2.4.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.2.4 (from 2.2.3)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "CREATE TABLE `credits` (
|
||||
`credit_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`credit_amount` DECIMAL(15,2) NOT NULL,
|
||||
`credit_reference` VARCHAR(250) DEFAULT NULL,
|
||||
`credit_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP(),
|
||||
`credit_created_by` INT(11) NOT NULL,
|
||||
`credit_expire_at` DATE DEFAULT NULL,
|
||||
`credit_client_id` INT(11) NOT NULL,
|
||||
PRIMARY KEY (`credit_id`)
|
||||
)");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `invoices` ADD `invoice_credit_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00 AFTER `invoice_discount_amount`");
|
||||
|
||||
mysqli_query($mysqli, "CREATE TABLE `discount_codes` (
|
||||
`discount_code_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`discount_code_description` VARCHAR(250) DEFAULT NULL,
|
||||
`discount_code_amount` DECIMAL(15,2) NOT NULL,
|
||||
`discount_code` VARCHAR(200) NOT NULL,
|
||||
`discount_code_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP(),
|
||||
`discount_code_created_by` INT(11) NOT NULL,
|
||||
`discount_code_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`discount_code_archived_at` DATETIME NULL DEFAULT NULL,
|
||||
`discount_code_expire_at` DATE DEFAULT NULL,
|
||||
PRIMARY KEY (`discount_code_id`)
|
||||
)");
|
||||
10
admin/database_updates/2.2.5.php
Normal file
10
admin/database_updates/2.2.5.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.2.5 (from 2.2.4)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `settings` ADD `config_theme_dark` TINYINT(1) NOT NULL DEFAULT 0 AFTER `config_theme`");
|
||||
10
admin/database_updates/2.2.6.php
Normal file
10
admin/database_updates/2.2.6.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.2.6 (from 2.2.5)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `assets` ADD `asset_uri_client` VARCHAR(500) NULL DEFAULT NULL AFTER `asset_uri_2`");
|
||||
16
admin/database_updates/2.2.7.php
Normal file
16
admin/database_updates/2.2.7.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.2.7 (from 2.2.6)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `credits` DROP `credit_reference`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `credits` ADD `credit_type` ENUM('prepaid', 'manual', 'refund', 'promotion', 'usage') NOT NULL DEFAULT 'manual' AFTER `credit_amount`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `credits` ADD `credit_note` TEXT NULL DEFAULT NULL AFTER `credit_type`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `credits` ADD `credit_invoice_id` INT(11) NULL DEFAULT NULL AFTER `credit_expire_at`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `credits` ADD INDEX (`credit_client_id`)");
|
||||
mysqli_query($mysqli, "ALTER TABLE `credits` ADD INDEX (`credit_invoice_id`)");
|
||||
mysqli_query($mysqli, "ALTER TABLE `credits` ADD INDEX (`credit_created_at`)");
|
||||
11
admin/database_updates/2.2.8.php
Normal file
11
admin/database_updates/2.2.8.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.2.8 (from 2.2.7)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `user_settings` ADD `user_config_theme_dark` TINYINT(1) NOT NULL DEFAULT 0 AFTER `user_config_signature`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `settings` DROP `config_theme_dark`");
|
||||
23
admin/database_updates/2.2.9.php
Normal file
23
admin/database_updates/2.2.9.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.2.9 (from 2.2.8)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `products` ADD `product_type` ENUM('service', 'product') NOT NULL DEFAULT 'service' AFTER `product_name`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `products` ADD `product_code` VARCHAR(200) DEFAULT NULL AFTER `product_description`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `products` ADD `product_location` VARCHAR(250) DEFAULT NULL AFTER `product_code`");
|
||||
|
||||
mysqli_query($mysqli, "CREATE TABLE `product_stock` (
|
||||
`stock_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`stock_qty` INT(11) NOT NULL,
|
||||
`stock_note` TEXT DEFAULT NULL,
|
||||
`stock_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP(),
|
||||
`stock_expense_id` INT(11) DEFAULT NULL,
|
||||
`stock_item_id` INT(11) DEFAULT NULL,
|
||||
`stock_product_id` INT(11) NOT NULL,
|
||||
PRIMARY KEY (`stock_id`)
|
||||
)");
|
||||
84
admin/database_updates/2.3.0.php
Normal file
84
admin/database_updates/2.3.0.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.3.0 (from 2.2.9)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Migrate Stripe Settings over to new Tables
|
||||
|
||||
// Get Current Stripe Settings
|
||||
$sql_stripe_settings = mysqli_query($mysqli, "SELECT * FROM settings WHERE company_id = 1");
|
||||
$row = mysqli_fetch_assoc($sql_stripe_settings);
|
||||
$config_stripe_enable = intval($row['config_stripe_enable']);
|
||||
if ($config_stripe_enable === 1) {
|
||||
$config_stripe_publishable = mysqli_real_escape_string($mysqli, $row['config_stripe_publishable']);
|
||||
$config_stripe_secret = mysqli_real_escape_string($mysqli, $row['config_stripe_secret']);
|
||||
$config_stripe_account = intval($row['config_stripe_account']);
|
||||
$config_stripe_expense_vendor = intval($row['config_stripe_expense_vendor']);
|
||||
$config_stripe_expense_category = intval($row['config_stripe_expense_category']);
|
||||
$config_stripe_percentage_fee = floatval($row['config_stripe_percentage_fee']);
|
||||
$config_stripe_flat_fee = floatval($row['config_stripe_flat_fee']);
|
||||
|
||||
mysqli_query($mysqli,"INSERT INTO payment_providers SET
|
||||
payment_provider_name = 'Stripe',
|
||||
payment_provider_public_key = '$config_stripe_publishable',
|
||||
payment_provider_private_key = '$config_stripe_secret',
|
||||
payment_provider_account = $config_stripe_account,
|
||||
payment_provider_expense_vendor = $config_stripe_expense_vendor,
|
||||
payment_provider_expense_category = $config_stripe_expense_category,
|
||||
payment_provider_expense_percentage_fee = $config_stripe_percentage_fee,
|
||||
payment_provider_expense_flat_fee = $config_stripe_flat_fee"
|
||||
);
|
||||
|
||||
$provider_id = mysqli_insert_id($mysqli);
|
||||
|
||||
// Migrate Clients and Payment Method over
|
||||
$sql_stripe_clients = mysqli_query($mysqli, "SELECT * FROM client_stripe WHERE stripe_pm IS NOT NULL AND stripe_pm != ''");
|
||||
while ($row = mysqli_fetch_assoc($sql_stripe_clients)) {
|
||||
$client_id = intval($row['client_id']);
|
||||
$stripe_id = mysqli_real_escape_string($mysqli, $row['stripe_id']);
|
||||
$stripe_pm = mysqli_real_escape_string($mysqli, $row['stripe_pm']);
|
||||
$stripe_pm_details = mysqli_real_escape_string($mysqli, $row['stripe_pm_details'] ?? 'Saved Card');
|
||||
|
||||
mysqli_query($mysqli,"INSERT INTO client_payment_provider SET
|
||||
client_id = $client_id,
|
||||
payment_provider_id = $provider_id,
|
||||
payment_provider_client = '$stripe_id'"
|
||||
);
|
||||
|
||||
mysqli_query($mysqli,"INSERT INTO client_saved_payment_methods SET
|
||||
saved_payment_provider_method = '$stripe_pm',
|
||||
saved_payment_description = '$stripe_pm_details',
|
||||
saved_payment_client_id = $client_id,
|
||||
saved_payment_provider_id = $provider_id"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Get Stripe provider id
|
||||
$res = mysqli_query($mysqli, "
|
||||
SELECT payment_provider_id
|
||||
FROM payment_providers
|
||||
WHERE payment_provider_name = 'Stripe'
|
||||
ORDER BY payment_provider_id DESC
|
||||
LIMIT 1
|
||||
");
|
||||
$stripe = mysqli_fetch_assoc($res);
|
||||
$stripe_provider_id = intval($stripe['payment_provider_id']);
|
||||
|
||||
// Correct mapping: RP -> Recurring Invoice -> Client -> Client's Stripe saved method
|
||||
mysqli_query($mysqli, "
|
||||
UPDATE recurring_payments rp
|
||||
INNER JOIN recurring_invoices ri
|
||||
ON ri.recurring_invoice_id = rp.recurring_payment_recurring_invoice_id
|
||||
INNER JOIN client_saved_payment_methods spm
|
||||
ON spm.saved_payment_client_id = ri.recurring_invoice_client_id
|
||||
AND spm.saved_payment_provider_id = $stripe_provider_id
|
||||
SET
|
||||
rp.recurring_payment_method = 'Credit Card',
|
||||
rp.recurring_payment_saved_payment_id = spm.saved_payment_id
|
||||
WHERE rp.recurring_payment_method = 'Stripe'
|
||||
");
|
||||
17
admin/database_updates/2.3.1.php
Normal file
17
admin/database_updates/2.3.1.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.3.1 (from 2.3.0)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Migrate Payment Methods from Categories Table to new payment_methods table
|
||||
$sql_categories = mysqli_query($mysqli, "SELECT category_name FROM categories WHERE category_type = 'Payment Method' AND category_name != 'Stripe' AND category_archived_at IS NULL");
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql_categories)) {
|
||||
$category_name = escapeSql($row['category_name']);
|
||||
|
||||
mysqli_query($mysqli,"INSERT INTO payment_methods SET payment_method_name = '$category_name'");
|
||||
}
|
||||
31
admin/database_updates/2.3.2.php
Normal file
31
admin/database_updates/2.3.2.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.3.2 (from 2.3.1)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Delete all Recurring Payments that are Stripe
|
||||
mysqli_query($mysqli, "DELETE FROM recurring_payments WHERE recurring_payment_method = 'Stripe'");
|
||||
|
||||
// Delete Stripe Specific ITFlow Client Stripe Client Relationship Table
|
||||
mysqli_query($mysqli, "DROP TABLE client_stripe");
|
||||
|
||||
// Delete Unused Stripe and AI Settings now in their own tables
|
||||
mysqli_query($mysqli, "ALTER TABLE `settings`
|
||||
DROP `config_stripe_enable`,
|
||||
DROP `config_stripe_publishable`,
|
||||
DROP `config_stripe_secret`,
|
||||
DROP `config_stripe_account`,
|
||||
DROP `config_stripe_expense_vendor`,
|
||||
DROP `config_stripe_expense_category`,
|
||||
DROP `config_stripe_percentage_fee`,
|
||||
DROP `config_stripe_flat_fee`,
|
||||
DROP `config_ai_enable`,
|
||||
DROP `config_ai_provider`,
|
||||
DROP `config_ai_model`,
|
||||
DROP `config_ai_url`,
|
||||
DROP `config_ai_api_key`
|
||||
");
|
||||
18
admin/database_updates/2.3.3.php
Normal file
18
admin/database_updates/2.3.3.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.3.3 (from 2.3.2)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE settings
|
||||
ADD `config_imap_provider` ENUM('standard_imap','google_oauth','microsoft_oauth') NULL DEFAULT NULL AFTER `config_mail_from_name`,
|
||||
ADD `config_mail_oauth_client_id` VARCHAR(255) NULL AFTER `config_imap_provider`,
|
||||
ADD `config_mail_oauth_client_secret` VARCHAR(255) NULL AFTER `config_mail_oauth_client_id`,
|
||||
ADD `config_mail_oauth_tenant_id` VARCHAR(255) NULL AFTER `config_mail_oauth_client_secret`,
|
||||
ADD `config_mail_oauth_refresh_token` TEXT NULL AFTER `config_mail_oauth_tenant_id`,
|
||||
ADD `config_mail_oauth_access_token` TEXT NULL AFTER `config_mail_oauth_refresh_token`,
|
||||
ADD `config_mail_oauth_access_token_expires_at` DATETIME NULL AFTER `config_mail_oauth_access_token`
|
||||
");
|
||||
12
admin/database_updates/2.3.4.php
Normal file
12
admin/database_updates/2.3.4.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.3.4 (from 2.3.3)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE settings
|
||||
ADD `config_smtp_provider` ENUM('standard_smtp','google_oauth','microsoft_oauth') NULL DEFAULT NULL AFTER `config_start_page`
|
||||
");
|
||||
37
admin/database_updates/2.3.5.php
Normal file
37
admin/database_updates/2.3.5.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.3.5 (from 2.3.4)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Add Software Keys
|
||||
mysqli_query($mysqli, "CREATE TABLE `software_keys` (
|
||||
`software_key_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`software_key` VARCHAR(400) NOT NULL,
|
||||
`software_key_software_id` INT(11) NOT NULL,
|
||||
PRIMARY KEY (`software_key_id`),
|
||||
FOREIGN KEY (`software_key_software_id`) REFERENCES `software`(`software_id`) ON DELETE CASCADE
|
||||
)");
|
||||
|
||||
// Software Key Assignments to Contacts
|
||||
mysqli_query($mysqli, "CREATE TABLE `software_key_contact_assignments` (
|
||||
`software_key_id` INT(11) NOT NULL,
|
||||
`contact_id` INT(11) NOT NULL,
|
||||
`software_key_assigned_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`software_key_id`, `contact_id`),
|
||||
FOREIGN KEY (`software_key_id`) REFERENCES `software_keys`(`software_key_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE
|
||||
)");
|
||||
|
||||
// Software Key Assignments to Assets
|
||||
mysqli_query($mysqli, "CREATE TABLE `software_key_asset_assignments` (
|
||||
`software_key_id` INT(11) NOT NULL,
|
||||
`asset_id` INT(11) NOT NULL,
|
||||
`software_key_assigned_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`software_key_id`, `asset_id`),
|
||||
FOREIGN KEY (`software_key_id`) REFERENCES `software_keys`(`software_key_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
|
||||
)");
|
||||
11
admin/database_updates/2.3.6.php
Normal file
11
admin/database_updates/2.3.6.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.3.6 (from 2.3.5)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `settings` CHANGE `config_smtp_provider` `config_smtp_provider` VARCHAR(200) DEFAULT NULL");
|
||||
mysqli_query($mysqli, "ALTER TABLE `settings` CHANGE `config_imap_provider` `config_imap_provider` VARCHAR(200) DEFAULT NULL");
|
||||
84
admin/database_updates/2.3.7.php
Normal file
84
admin/database_updates/2.3.7.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.3.7 (from 2.3.6)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Create New Contract Templates Table
|
||||
mysqli_query($mysqli, "CREATE TABLE `contract_templates` (
|
||||
`contract_template_id` INT(11) AUTO_INCREMENT PRIMARY KEY,
|
||||
`contract_template_name` VARCHAR(255) NOT NULL,
|
||||
`contract_template_description` TEXT NULL DEFAULT NULL,
|
||||
`contract_template_type` VARCHAR(50) NULL DEFAULT NULL,
|
||||
|
||||
`contract_template_sla_low_response_time` INT(11) NULL DEFAULT NULL,
|
||||
`contract_template_sla_low_resolution_time` INT(11) NULL DEFAULT NULL,
|
||||
`contract_template_sla_medium_response_time` INT(11) NULL DEFAULT NULL,
|
||||
`contract_template_sla_medium_resolution_time` INT(11) NULL DEFAULT NULL,
|
||||
`contract_template_sla_high_response_time` INT(11) NULL DEFAULT NULL,
|
||||
`contract_template_sla_high_resolution_time` INT(11) NULL DEFAULT NULL,
|
||||
|
||||
`contract_template_rate_standard` DECIMAL(10,2) NULL DEFAULT NULL,
|
||||
`contract_template_rate_after_hours` DECIMAL(10,2) NULL DEFAULT NULL,
|
||||
|
||||
`contract_template_net_terms` VARCHAR(50) NULL DEFAULT NULL,
|
||||
`contract_template_support_hours` VARCHAR(100) NULL DEFAULT NULL,
|
||||
`contract_template_renewal_frequency` VARCHAR(50) NULL DEFAULT NULL,
|
||||
|
||||
`contract_template_details` TEXT NULL DEFAULT NULL,
|
||||
|
||||
`contract_template_created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`contract_template_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`contract_template_archived_at` DATETIME NULL DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
|
||||
|
||||
// Create New Contracts Table
|
||||
mysqli_query($mysqli, "CREATE TABLE `contracts` (
|
||||
`contract_id` INT(11) AUTO_INCREMENT PRIMARY KEY,
|
||||
`contract_name` VARCHAR(255) NOT NULL,
|
||||
`contract_status` VARCHAR(50) NOT NULL,
|
||||
`contract_type` VARCHAR(50) NOT NULL,
|
||||
|
||||
`contract_sla_low_response_time` INT(11) NULL DEFAULT NULL,
|
||||
`contract_sla_low_resolution_time` INT(11) NULL DEFAULT NULL,
|
||||
`contract_sla_medium_response_time` INT(11) NULL DEFAULT NULL,
|
||||
`contract_sla_medium_resolution_time` INT(11) NULL DEFAULT NULL,
|
||||
`contract_sla_high_response_time` INT(11) NULL DEFAULT NULL,
|
||||
`contract_sla_high_resolution_time` INT(11) NULL DEFAULT NULL,
|
||||
|
||||
`contract_details` TEXT NULL DEFAULT NULL,
|
||||
|
||||
`contract_client_id` INT(11) NULL DEFAULT NULL,
|
||||
`contract_client_name` VARCHAR(255) NULL DEFAULT NULL,
|
||||
`contract_client_address` TEXT NULL DEFAULT NULL,
|
||||
`contract_client_email` VARCHAR(255) NULL DEFAULT NULL,
|
||||
`contract_client_phone` VARCHAR(100) NULL DEFAULT NULL,
|
||||
|
||||
`contract_contact_name` VARCHAR(255) NULL DEFAULT NULL,
|
||||
`contract_contact_signature` TEXT NULL DEFAULT NULL,
|
||||
`contract_contact_signature_date` DATETIME NULL DEFAULT NULL,
|
||||
|
||||
`contract_agent_name` VARCHAR(255) NULL DEFAULT NULL,
|
||||
`contract_agent_signature` TEXT NULL DEFAULT NULL,
|
||||
`contract_agent_signature_date` DATETIME NULL DEFAULT NULL,
|
||||
|
||||
`contract_rate_standard` DECIMAL(10,2) NULL DEFAULT NULL,
|
||||
`contract_rate_after_hours` DECIMAL(10,2) NULL DEFAULT NULL,
|
||||
|
||||
`contract_net_terms` VARCHAR(50) NULL DEFAULT NULL,
|
||||
`contract_support_hours` VARCHAR(100) NULL DEFAULT NULL,
|
||||
|
||||
`contract_start_date` DATE NULL DEFAULT NULL,
|
||||
`contract_end_date` DATE NULL DEFAULT NULL,
|
||||
`contract_renewal_frequency` VARCHAR(50) NULL DEFAULT NULL,
|
||||
|
||||
`contract_created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`contract_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`contract_archived_at` DATETIME NULL DEFAULT NULL,
|
||||
|
||||
FOREIGN KEY (`contract_client_id`) REFERENCES `clients`(`client_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
24
admin/database_updates/2.3.8.php
Normal file
24
admin/database_updates/2.3.8.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.3.8 (from 2.3.7)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "
|
||||
CREATE TABLE `asset_tags` (
|
||||
`asset_tag_asset_id` INT(11) NOT NULL,
|
||||
`asset_tag_tag_id` INT(11) NOT NULL,
|
||||
PRIMARY KEY (`asset_tag_asset_id`, `asset_tag_tag_id`),
|
||||
CONSTRAINT `fk_asset`
|
||||
FOREIGN KEY (`asset_tag_asset_id`)
|
||||
REFERENCES `assets`(`asset_id`)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_tag`
|
||||
FOREIGN KEY (`asset_tag_tag_id`)
|
||||
REFERENCES `tags`(`tag_id`)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
");
|
||||
23
admin/database_updates/2.3.9.php
Normal file
23
admin/database_updates/2.3.9.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.3.9 (from 2.3.8)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "
|
||||
CREATE TABLE `task_approvals` (
|
||||
`approval_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`approval_scope` enum('client','internal') NOT NULL,
|
||||
`approval_type` enum('any','technical','billing','specific') NOT NULL,
|
||||
`approval_required_user_id` int(11) DEFAULT NULL,
|
||||
`approval_status` enum('pending','approved','declined') NOT NULL,
|
||||
`approval_created_by` int(11) NOT NULL,
|
||||
`approval_approved_by` varchar(255) DEFAULT NULL,
|
||||
`approval_url_key` varchar(200) NOT NULL,
|
||||
`approval_task_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`approval_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
");
|
||||
43
admin/database_updates/2.4.0.php
Normal file
43
admin/database_updates/2.4.0.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.4.0 (from 2.3.9)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `clients` ADD `client_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `client_notes`");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `locations` ADD `location_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `location_notes`");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `vendors` ADD `vendor_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `vendor_notes`");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `software` ADD `software_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `software_notes`");
|
||||
|
||||
mysqli_query(
|
||||
$mysqli,
|
||||
"ALTER TABLE `credentials`
|
||||
CHANGE `credential_important` `credential_favorite`
|
||||
TINYINT(1) NOT NULL DEFAULT 0
|
||||
AFTER `credential_note`"
|
||||
);
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `assets` DROP `asset_important`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `assets` ADD `asset_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `asset_notes`");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `documents` DROP `document_important`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `documents` ADD `document_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `document_client_visible`");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `racks` ADD `rack_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `rack_notes`");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `files` DROP `file_important`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `files` ADD `file_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `file_mime_type`");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `networks` ADD `network_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `network_notes`");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `domains` ADD `domain_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `domain_notes`");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `certificates` ADD `certificate_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `certificate_notes`");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `services` ADD `service_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `service_notes`");
|
||||
50
admin/database_updates/2.4.1.php
Normal file
50
admin/database_updates/2.4.1.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.4.1 (from 2.4.0)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "
|
||||
CREATE TABLE `quote_items` (
|
||||
`item_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`item_name` varchar(200) NOT NULL,
|
||||
`item_description` text DEFAULT NULL,
|
||||
`item_quantity` decimal(15,2) NOT NULL DEFAULT 0.00,
|
||||
`item_price` decimal(15,2) NOT NULL DEFAULT 0.00,
|
||||
`item_subtotal` decimal(15,2) NOT NULL DEFAULT 0.00,
|
||||
`item_tax` decimal(15,2) NOT NULL DEFAULT 0.00,
|
||||
`item_total` decimal(15,2) NOT NULL DEFAULT 0.00,
|
||||
`item_order` int(11) NOT NULL DEFAULT 0,
|
||||
`item_created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||
`item_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
|
||||
`item_archived_at` datetime DEFAULT NULL,
|
||||
`item_tax_id` int(11) NOT NULL DEFAULT 0,
|
||||
`item_product_id` int(11) NOT NULL DEFAULT 0,
|
||||
`item_quote_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`item_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
");
|
||||
|
||||
mysqli_query($mysqli, "
|
||||
CREATE TABLE `recurring_invoice_items` (
|
||||
`item_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`item_name` varchar(200) NOT NULL,
|
||||
`item_description` text DEFAULT NULL,
|
||||
`item_quantity` decimal(15,2) NOT NULL DEFAULT 0.00,
|
||||
`item_price` decimal(15,2) NOT NULL DEFAULT 0.00,
|
||||
`item_subtotal` decimal(15,2) NOT NULL DEFAULT 0.00,
|
||||
`item_tax` decimal(15,2) NOT NULL DEFAULT 0.00,
|
||||
`item_total` decimal(15,2) NOT NULL DEFAULT 0.00,
|
||||
`item_order` int(11) NOT NULL DEFAULT 0,
|
||||
`item_created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||
`item_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
|
||||
`item_archived_at` datetime DEFAULT NULL,
|
||||
`item_tax_id` int(11) NOT NULL DEFAULT 0,
|
||||
`item_product_id` int(11) NOT NULL DEFAULT 0,
|
||||
`item_recurring_invoice_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`item_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
");
|
||||
97
admin/database_updates/2.4.2.php
Normal file
97
admin/database_updates/2.4.2.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.4.2 (from 2.4.1)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Migrate Items
|
||||
mysqli_query($mysqli, "
|
||||
INSERT INTO `recurring_invoice_items` (
|
||||
`item_name`,
|
||||
`item_description`,
|
||||
`item_quantity`,
|
||||
`item_price`,
|
||||
`item_subtotal`,
|
||||
`item_tax`,
|
||||
`item_total`,
|
||||
`item_order`,
|
||||
`item_created_at`,
|
||||
`item_updated_at`,
|
||||
`item_archived_at`,
|
||||
`item_tax_id`,
|
||||
`item_product_id`,
|
||||
`item_recurring_invoice_id`
|
||||
)
|
||||
SELECT
|
||||
`item_name`,
|
||||
`item_description`,
|
||||
`item_quantity`,
|
||||
`item_price`,
|
||||
`item_subtotal`,
|
||||
`item_tax`,
|
||||
`item_total`,
|
||||
`item_order`,
|
||||
`item_created_at`,
|
||||
`item_updated_at`,
|
||||
`item_archived_at`,
|
||||
`item_tax_id`,
|
||||
`item_product_id`,
|
||||
`item_recurring_invoice_id`
|
||||
FROM `invoice_items`
|
||||
WHERE `item_recurring_invoice_id` != 0
|
||||
");
|
||||
|
||||
mysqli_query($mysqli, "
|
||||
INSERT INTO `quote_items` (
|
||||
`item_name`,
|
||||
`item_description`,
|
||||
`item_quantity`,
|
||||
`item_price`,
|
||||
`item_subtotal`,
|
||||
`item_tax`,
|
||||
`item_total`,
|
||||
`item_order`,
|
||||
`item_created_at`,
|
||||
`item_updated_at`,
|
||||
`item_archived_at`,
|
||||
`item_tax_id`,
|
||||
`item_product_id`,
|
||||
`item_quote_id`
|
||||
)
|
||||
SELECT
|
||||
`item_name`,
|
||||
`item_description`,
|
||||
`item_quantity`,
|
||||
`item_price`,
|
||||
`item_subtotal`,
|
||||
`item_tax`,
|
||||
`item_total`,
|
||||
`item_order`,
|
||||
`item_created_at`,
|
||||
`item_updated_at`,
|
||||
`item_archived_at`,
|
||||
`item_tax_id`,
|
||||
`item_product_id`,
|
||||
`item_quote_id`
|
||||
FROM `invoice_items`
|
||||
WHERE `item_quote_id` != 0
|
||||
");
|
||||
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `invoice_items`
|
||||
WHERE `item_recurring_invoice_id` != 0
|
||||
");
|
||||
|
||||
mysqli_query($mysqli, "
|
||||
DELETE FROM `invoice_items`
|
||||
WHERE `item_quote_id` != 0
|
||||
");
|
||||
|
||||
mysqli_query($mysqli, "
|
||||
ALTER TABLE `invoice_items`
|
||||
DROP COLUMN `item_quote_id`,
|
||||
DROP COLUMN `item_recurring_invoice_id`
|
||||
");
|
||||
21
admin/database_updates/2.4.3.php
Normal file
21
admin/database_updates/2.4.3.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.4.3 (from 2.4.2)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `categories` ADD `category_description` VARCHAR(255) DEFAULT NULL AFTER `category_name`");
|
||||
mysqli_query($mysqli, "ALTER TABLE `categories` ADD `category_order` INT(11) NOT NULL DEFAULT 0 AFTER `category_icon`");
|
||||
|
||||
// Create network_interfaces
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Ethernet', category_type = 'network_interface', category_order = 1"); // 1
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'SFP', category_type = 'network_interface', category_order = 2"); // 2
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'SFP+', category_type = 'network_interface', category_order = 3"); // 3
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'QSFP28', category_type = 'network_interface', category_order = 4"); // 4
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'QSFP-DD', category_type = 'network_interface', category_order = 5"); // 5
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Coaxial', category_type = 'network_interface', category_order = 6"); // 6
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Fiber', category_type = 'network_interface', category_order = 7"); // 7
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'WiFi', category_type = 'network_interface', category_order = 8"); // 8
|
||||
42
admin/database_updates/2.4.4.php
Normal file
42
admin/database_updates/2.4.4.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.4.4 (from 2.4.3)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Asset Status
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Ready to Deploy', category_description = 'Asset is configured and ready to be assigned', category_type = 'asset_status', category_order = 1"); // 1
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Deployed', category_description = 'Asset is actively in use and assigned to a client or location', category_type = 'asset_status', category_order = 2"); // 2
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Out for Repair', category_description = 'Asset has been sent out for servicing or repair', category_type = 'asset_status', category_order = 3"); // 3
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Lost', category_description = 'Asset location is unknown and cannot be accounted for', category_type = 'asset_status', category_order = 4"); // 4
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Stolen', category_description = 'Asset has been reported stolen', category_type = 'asset_status', category_order = 5"); // 5
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Retired', category_description = 'Asset has been decommissioned and is no longer in service', category_type = 'asset_status', category_order = 6"); // 6
|
||||
|
||||
// Contact note types
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Call', category_description = 'Phone call with a client or contact', category_icon = 'fa-phone-alt', category_type = 'contact_note_type', category_order = 1"); // 1
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Email', category_description = 'Email correspondence with a client or contact', category_icon = 'fa-envelope', category_type = 'contact_note_type', category_order = 2"); // 2
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Meeting', category_description = 'Scheduled meeting with a client or contact', category_icon = 'fa-handshake', category_type = 'contact_note_type', category_order = 3"); // 3
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'In Person', category_description = 'In person visit or on-site interaction', category_icon = 'fa-people-arrows', category_type = 'contact_note_type', category_order = 4"); // 4
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Note', category_description = 'General note or internal comment', category_icon = 'fa-sticky-note', category_type = 'contact_note_type', category_order = 5"); // 5
|
||||
|
||||
// Rack Types
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = '2-Post Open Frame', category_description = 'Two-post open frame rack for patch panels and lightweight equipment', category_type = 'rack_type', category_order = 1"); // 1
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = '4-Post Open Frame', category_description = 'Four-post open frame rack for servers and heavier equipment', category_type = 'rack_type', category_order = 2"); // 2
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = '4-Post Enclosed Cabinet', category_description = 'Four-post enclosed cabinet with doors and sides for secure equipment housing', category_type = 'rack_type', category_order = 3"); // 3
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Wall-Mount Open', category_description = 'Open frame rack mounted directly to a wall for small deployments', category_type = 'rack_type', category_order = 4"); // 4
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Wall-Mount Enclosed', category_description = 'Enclosed cabinet rack mounted to a wall with a locking door', category_type = 'rack_type', category_order = 5"); // 5
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Other', category_description = 'Rack type does not fit any standard category', category_type = 'rack_type', category_order = 6"); // 6
|
||||
|
||||
// Software Types
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Software as a Service (SaaS)', category_description = 'Cloud-hosted software accessed via a web browser or API', category_type = 'software_type', category_order = 1"); // 1
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Productivity Suite', category_description = 'Bundled office and collaboration tools such as Microsoft 365 or Google Workspace', category_type = 'software_type', category_order = 2"); // 2
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Web Application', category_description = 'Application hosted on a web server and accessed through a browser', category_type = 'software_type', category_order = 3"); // 3
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Desktop Application', category_description = 'Application installed and run locally on a workstation or laptop', category_type = 'software_type', category_order = 4"); // 4
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Mobile Application', category_description = 'Application installed and run on a mobile device or tablet', category_type = 'software_type', category_order = 5"); // 5
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Security Software', category_description = 'Software providing antivirus, endpoint protection, or security monitoring', category_type = 'software_type', category_order = 6"); // 6
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'System Software', category_description = 'Low-level software managing hardware resources and system operations', category_type = 'software_type', category_order = 7"); // 7
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Operating System', category_description = 'Core software managing hardware and providing a platform for applications', category_type = 'software_type', category_order = 8"); // 8
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Other', category_description = 'Software type does not fit any standard category', category_type = 'software_type', category_order = 9"); // 9
|
||||
11
admin/database_updates/2.4.5.php
Normal file
11
admin/database_updates/2.4.5.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.4.5 (from 2.4.4)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Gateway fee expense now uses the actual fee from Stripe's balance transaction
|
||||
mysqli_query($mysqli, "ALTER TABLE `payment_providers` DROP `payment_provider_expense_percentage_fee`, DROP `payment_provider_expense_flat_fee`");
|
||||
11
admin/database_updates/2.4.6.php
Normal file
11
admin/database_updates/2.4.6.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.4.6 (from 2.4.5)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Add Allow Deny Client Access to the Enforce Client Permissions
|
||||
mysqli_query($mysqli, "ALTER TABLE `user_client_permissions` ADD COLUMN `permission_type` ENUM('allow','deny') NOT NULL DEFAULT 'allow'");
|
||||
18
admin/database_updates/2.4.7.php
Normal file
18
admin/database_updates/2.4.7.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.4.7 (from 2.4.6)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// API keys now run as a user and inherit that user's RBAC (module / operation / client).
|
||||
// Existing keys predate this and can't be safely mapped to a user, so they are removed
|
||||
// and must be recreated with an owning user.
|
||||
mysqli_query($mysqli, "DELETE FROM api_keys");
|
||||
|
||||
// Tie keys to a user; client access now derives from that user, so the old per-key
|
||||
// client scope is removed.
|
||||
mysqli_query($mysqli, "ALTER TABLE `api_keys` ADD COLUMN `api_key_user_id` INT(11) NOT NULL DEFAULT 0");
|
||||
mysqli_query($mysqli, "ALTER TABLE `api_keys` DROP COLUMN `api_key_client_id`");
|
||||
16
admin/database_updates/2.4.8.php
Normal file
16
admin/database_updates/2.4.8.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.4.8 (from 2.4.7)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// The login lockout queries run on every login POST and filter the logs table by
|
||||
// a 10 minute window. With only PRIMARY KEY (log_id) that is a full scan of a
|
||||
// table that grows with every action in the app, which an attacker can trigger
|
||||
// once per request. Indexing log_created_at bounds both lockout queries to the
|
||||
// recent rows, and also speeds up the nightly log purge in cron.php, which had
|
||||
// the same problem.
|
||||
mysqli_query($mysqli, "ALTER TABLE `logs` ADD INDEX `log_created_at` (`log_created_at`)");
|
||||
59
admin/database_updates/2.5.0.php
Normal file
59
admin/database_updates/2.5.0.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.5.0 (from 2.4.8)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*
|
||||
* (2.4.9 was briefly used by the reverted refunds change - skipping that
|
||||
* number so instances that applied it before the revert still pick this up)
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Ticket SLAs. slas holds the response/resolution targets and
|
||||
// sla_assignments maps a client + priority to an SLA (client 0 rows are
|
||||
// the global default, an assignment pointing at SLA 0 is an explicit "no
|
||||
// SLA" override). SLAs are optional - with no assignments defined, nothing
|
||||
// in the app changes behaviour.
|
||||
mysqli_query($mysqli, "CREATE TABLE `sla_assignments` (
|
||||
`sla_assignment_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`sla_assignment_client_id` int(11) NOT NULL DEFAULT 0,
|
||||
`sla_assignment_priority` varchar(200) NOT NULL,
|
||||
`sla_assignment_sla_id` int(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`sla_assignment_id`),
|
||||
UNIQUE KEY `sla_assignment_client_priority` (`sla_assignment_client_id`,`sla_assignment_priority`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
|
||||
|
||||
mysqli_query($mysqli, "CREATE TABLE `slas` (
|
||||
`sla_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`sla_name` varchar(200) NOT NULL,
|
||||
`sla_description` varchar(500) DEFAULT NULL,
|
||||
`sla_response_minutes` int(11) NOT NULL,
|
||||
`sla_resolution_minutes` int(11) DEFAULT NULL,
|
||||
`sla_created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||
`sla_archived_at` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`sla_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
|
||||
|
||||
// SLA targets are computed once at write time and stored on the ticket, so
|
||||
// the ticket list and cron/ticket_sla.php only ever compare datetimes. The
|
||||
// due date indexes bound the cron's every-minute scans the same way the
|
||||
// 2.4.8 logs index bounded the login lockout queries.
|
||||
mysqli_query($mysqli, "ALTER TABLE `tickets`
|
||||
ADD COLUMN `ticket_sla_id` int(11) NOT NULL DEFAULT 0 AFTER `ticket_status`,
|
||||
ADD COLUMN `ticket_response_due_at` datetime DEFAULT NULL AFTER `ticket_first_response_at`,
|
||||
ADD COLUMN `ticket_resolution_due_at` datetime DEFAULT NULL AFTER `ticket_response_due_at`,
|
||||
ADD COLUMN `ticket_response_sla_met` tinyint(1) DEFAULT NULL AFTER `ticket_resolution_due_at`,
|
||||
ADD COLUMN `ticket_resolution_sla_met` tinyint(1) DEFAULT NULL AFTER `ticket_response_sla_met`,
|
||||
ADD COLUMN `ticket_response_sla_alert_stage` tinyint(1) NOT NULL DEFAULT 0 AFTER `ticket_resolution_sla_met`,
|
||||
ADD COLUMN `ticket_resolution_sla_alert_stage` tinyint(1) NOT NULL DEFAULT 0 AFTER `ticket_response_sla_alert_stage`,
|
||||
ADD INDEX `ticket_response_due_at` (`ticket_response_due_at`),
|
||||
ADD INDEX `ticket_resolution_due_at` (`ticket_resolution_due_at`)");
|
||||
|
||||
// Business hours + SLA notification settings
|
||||
mysqli_query($mysqli, "ALTER TABLE `settings`
|
||||
ADD COLUMN `config_business_days` varchar(20) NOT NULL DEFAULT '1,2,3,4,5' AFTER `config_timezone`,
|
||||
ADD COLUMN `config_business_hours_start` time NOT NULL DEFAULT '09:00:00' AFTER `config_business_days`,
|
||||
ADD COLUMN `config_business_hours_end` time NOT NULL DEFAULT '17:00:00' AFTER `config_business_hours_start`,
|
||||
ADD COLUMN `config_sla_warning_percent` tinyint(3) NOT NULL DEFAULT 75 AFTER `config_business_hours_end`,
|
||||
ADD COLUMN `config_sla_notification_email` varchar(200) DEFAULT NULL AFTER `config_sla_warning_percent`");
|
||||
41
admin/database_updates/2.5.1.php
Normal file
41
admin/database_updates/2.5.1.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.5.1 (from 2.5.0)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// SLA pausing. sla_history records the intervals during which a ticket's
|
||||
// resolution clock was actually running - one open row (ended_at NULL) per
|
||||
// running ticket. Statuses flagged below stop the clock, and the ticket's
|
||||
// resolution due date is recomputed from the remaining budget on resume.
|
||||
mysqli_query($mysqli, "CREATE TABLE `sla_history` (
|
||||
`sla_history_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`sla_history_started_at` datetime NOT NULL,
|
||||
`sla_history_ended_at` datetime DEFAULT NULL,
|
||||
`sla_history_minutes` int(11) DEFAULT NULL,
|
||||
`sla_history_ticket_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`sla_history_id`),
|
||||
KEY `sla_history_ticket_id` (`sla_history_ticket_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
|
||||
|
||||
// Which statuses pause the resolution clock. Nothing pauses by default, so
|
||||
// SLA behaviour is unchanged until an admin opts a status in.
|
||||
mysqli_query($mysqli, "ALTER TABLE `ticket_statuses`
|
||||
ADD COLUMN `ticket_status_pauses_sla` tinyint(1) NOT NULL DEFAULT 0 AFTER `ticket_status_active`");
|
||||
|
||||
// Backfill an open interval for every ticket already running a resolution
|
||||
// clock, anchored at creation. Without this their consumed time would read
|
||||
// as zero and the first pause/resume would hand back the full budget.
|
||||
mysqli_query($mysqli, "INSERT INTO sla_history (sla_history_started_at, sla_history_ticket_id)
|
||||
SELECT ticket_created_at, ticket_id
|
||||
FROM tickets
|
||||
LEFT JOIN slas ON ticket_sla_id = sla_id
|
||||
WHERE ticket_sla_id > 0
|
||||
AND sla_resolution_minutes > 0
|
||||
AND ticket_resolution_due_at IS NOT NULL
|
||||
AND ticket_resolved_at IS NULL
|
||||
AND ticket_closed_at IS NULL
|
||||
AND ticket_archived_at IS NULL");
|
||||
32
admin/database_updates/2.5.2.php
Normal file
32
admin/database_updates/2.5.2.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.5.2 (from 2.5.1)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Resolving a ticket from the kanban board or the client portal recorded
|
||||
// the resolution timestamp but never judged the resolution SLA, so those
|
||||
// tickets carry a target, a resolution time and no verdict. They read as
|
||||
// still-in-flight and are left out of SLA reporting entirely.
|
||||
//
|
||||
// Both timestamps were stored, so the verdict is simply recomputed from
|
||||
// them. Only rows with no verdict at all are touched - anything already
|
||||
// judged keeps the answer it was given at the time.
|
||||
mysqli_query($mysqli, "UPDATE tickets
|
||||
SET ticket_resolution_sla_met = (ticket_resolved_at <= ticket_resolution_due_at)
|
||||
WHERE ticket_sla_id > 0
|
||||
AND ticket_resolution_due_at IS NOT NULL
|
||||
AND ticket_resolved_at IS NOT NULL
|
||||
AND ticket_resolution_sla_met IS NULL");
|
||||
|
||||
// Same repair on the response track, for any ticket whose first response
|
||||
// was recorded without the verdict being written alongside it
|
||||
mysqli_query($mysqli, "UPDATE tickets
|
||||
SET ticket_response_sla_met = (ticket_first_response_at <= ticket_response_due_at)
|
||||
WHERE ticket_sla_id > 0
|
||||
AND ticket_response_due_at IS NOT NULL
|
||||
AND ticket_first_response_at IS NOT NULL
|
||||
AND ticket_response_sla_met IS NULL");
|
||||
19
admin/database_updates/2.5.3.php
Normal file
19
admin/database_updates/2.5.3.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.5.3 (from 2.5.2)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Asset note types
|
||||
// The asset_notes table itself already ships in db.sql (and picked up its
|
||||
// foreign key back in 2.0.2) - it was simply never wired to a UI, so the
|
||||
// only thing missing for multiple notes per asset is the type list
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Maintenance', category_description = 'Routine or scheduled maintenance performed on the asset', category_icon = 'fa-tools', category_type = 'asset_note_type', category_order = 1"); // 1
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Repair', category_description = 'Repair work or hardware replacement', category_icon = 'fa-wrench', category_type = 'asset_note_type', category_order = 2"); // 2
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Configuration', category_description = 'Configuration or settings change made to the asset', category_icon = 'fa-sliders-h', category_type = 'asset_note_type', category_order = 3"); // 3
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Upgrade', category_description = 'Hardware or software upgrade', category_icon = 'fa-arrow-circle-up', category_type = 'asset_note_type', category_order = 4"); // 4
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Inspection', category_description = 'Physical inspection or audit of the asset', category_icon = 'fa-clipboard-check', category_type = 'asset_note_type', category_order = 5"); // 5
|
||||
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Note', category_description = 'General note or internal comment', category_icon = 'fa-sticky-note', category_type = 'asset_note_type', category_order = 6"); // 6
|
||||
16
admin/database_updates/2.5.4.php
Normal file
16
admin/database_updates/2.5.4.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.5.4 (from 2.5.3)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Recurring tickets can now carry a ticket template. The template's subject
|
||||
// and details are copied into the recurring ticket when it is picked, so the
|
||||
// link exists for one reason only: to stamp the template's task list onto
|
||||
// every ticket the schedule raises. 0 means no template, matching the other
|
||||
// optional relations on this table.
|
||||
mysqli_query($mysqli, "ALTER TABLE `recurring_tickets`
|
||||
ADD COLUMN `recurring_ticket_ticket_template_id` int(11) NOT NULL DEFAULT 0 AFTER `recurring_ticket_asset_id`");
|
||||
24
admin/database_updates/2.5.5.php
Normal file
24
admin/database_updates/2.5.5.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.5.5 (from 2.5.4)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Backfill guest URL keys. Every path that raises a ticket generates one
|
||||
// except the recurring block in cron.php, which omitted the column - so
|
||||
// every ticket the nightly schedule has ever created carries no key. The
|
||||
// guest view matches on ticket_url_key, so those tickets cannot be opened
|
||||
// from the "View ticket" link in reply and task-approval emails. It fails
|
||||
// closed rather than open (NULL never matches), so this is a broken link
|
||||
// rather than an exposure, but the links stay broken until a key exists.
|
||||
$sql_tickets_without_url_key = mysqli_query($mysqli, "SELECT ticket_id FROM tickets WHERE ticket_url_key IS NULL OR ticket_url_key = ''");
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql_tickets_without_url_key)) {
|
||||
$ticket_id = intval($row['ticket_id']);
|
||||
$url_key = randomString(32);
|
||||
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_url_key = '$url_key' WHERE ticket_id = $ticket_id");
|
||||
}
|
||||
34
admin/database_updates/2.5.6.php
Normal file
34
admin/database_updates/2.5.6.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.5.6 (from 2.5.5)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Recurring tickets now own their task list rather than reading the linked
|
||||
// ticket template at every run. The template still fills the list in when it
|
||||
// is picked, but the list can then be edited per schedule - which is only
|
||||
// meaningful if the edits are what the run actually reads.
|
||||
mysqli_query($mysqli, "CREATE TABLE `recurring_ticket_tasks` (
|
||||
`recurring_ticket_task_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`recurring_ticket_task_name` varchar(255) NOT NULL,
|
||||
`recurring_ticket_task_order` int(11) NOT NULL DEFAULT 0,
|
||||
`recurring_ticket_task_completion_estimate` int(11) NOT NULL DEFAULT 0,
|
||||
`recurring_ticket_task_recurring_ticket_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`recurring_ticket_task_id`),
|
||||
KEY `recurring_ticket_task_recurring_ticket_id` (`recurring_ticket_task_recurring_ticket_id`),
|
||||
CONSTRAINT `recurring_ticket_tasks_ibfk_1` FOREIGN KEY (`recurring_ticket_task_recurring_ticket_id`) REFERENCES `recurring_tickets` (`recurring_ticket_id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
|
||||
|
||||
// Copy each linked template's current tasks onto the schedule that links it, so
|
||||
// every existing recurring ticket keeps raising exactly the tasks it raises
|
||||
// today. Without this the switch to an owned list would silently produce
|
||||
// taskless tickets on the next run.
|
||||
mysqli_query($mysqli, "INSERT INTO recurring_ticket_tasks
|
||||
(recurring_ticket_task_name, recurring_ticket_task_order, recurring_ticket_task_completion_estimate, recurring_ticket_task_recurring_ticket_id)
|
||||
SELECT task_template_name, task_template_order, task_template_completion_estimate, recurring_ticket_id
|
||||
FROM recurring_tickets
|
||||
INNER JOIN task_templates ON task_template_ticket_template_id = recurring_ticket_ticket_template_id
|
||||
WHERE recurring_ticket_ticket_template_id > 0");
|
||||
16
admin/database_updates/2.5.7.php
Normal file
16
admin/database_updates/2.5.7.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.5.7 (from 2.5.6)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Queued mail can now carry file attachments. The column holds a JSON manifest
|
||||
// of app-root-relative paths and display names rather than file contents, so
|
||||
// the queue table stays small and the files stay in uploads/ where the ticket
|
||||
// attachment endpoints already serve them from. It is scrubbed on delivery
|
||||
// alongside the body, like email_cal_str.
|
||||
mysqli_query($mysqli, "ALTER TABLE `email_queue`
|
||||
ADD COLUMN `email_attachments` text DEFAULT NULL AFTER `email_cal_str`");
|
||||
27
admin/database_updates/2.5.8.php
Normal file
27
admin/database_updates/2.5.8.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.5.8 (from 2.5.7)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Calendars can now be published as a read-only ICS feed for Google Calendar,
|
||||
// Nextcloud and any other subscription client. The key is stored in cleartext,
|
||||
// matching invoice_url_key and shared_items.item_key - hashing it would buy
|
||||
// nothing here (anyone holding the database already holds the events the key
|
||||
// grants access to) and would make the URL impossible to re-copy for a second
|
||||
// device without breaking every existing subscriber.
|
||||
//
|
||||
// The UNIQUE key is on a nullable column, so unshared calendars all keep NULL.
|
||||
// The column is explicitly utf8mb4_bin: the table default is utf8mb4_general_ci,
|
||||
// which compares case-insensitively, and the key alphabet is mixed-case
|
||||
// base64url - a _ci column would throw away entropy on lookup and make the
|
||||
// UNIQUE index blind to case.
|
||||
mysqli_query($mysqli, "ALTER TABLE `calendars`
|
||||
ADD COLUMN `calendar_feed_key` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL AFTER `calendar_color`,
|
||||
ADD COLUMN `calendar_feed_busy_only` tinyint(1) NOT NULL DEFAULT 0 AFTER `calendar_feed_key`,
|
||||
ADD COLUMN `calendar_feed_created_at` datetime DEFAULT NULL AFTER `calendar_feed_busy_only`,
|
||||
ADD COLUMN `calendar_feed_accessed_at` datetime DEFAULT NULL AFTER `calendar_feed_created_at`,
|
||||
ADD UNIQUE KEY `calendar_feed_key` (`calendar_feed_key`)");
|
||||
28
admin/database_updates/2.5.9.php
Normal file
28
admin/database_updates/2.5.9.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.5.9 (from 2.5.8)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Events can now be marked all-day explicitly. Until now calendar_events had
|
||||
// no all-day column at all - FullCalendar inferred it from a start value with
|
||||
// no time component, which meant a genuine midnight appointment was
|
||||
// indistinguishable from an all-day event.
|
||||
mysqli_query($mysqli, "ALTER TABLE `calendar_events`
|
||||
ADD COLUMN `event_all_day` tinyint(1) NOT NULL DEFAULT 0 AFTER `event_end`");
|
||||
|
||||
// Backfill using the same rule the calendar already rendered by, so existing
|
||||
// events keep displaying exactly as they do today: a midnight start, and
|
||||
// either no end or a midnight end.
|
||||
mysqli_query($mysqli, "UPDATE `calendar_events`
|
||||
SET `event_all_day` = 1
|
||||
WHERE TIME(`event_start`) = '00:00:00'
|
||||
AND (`event_end` IS NULL OR TIME(`event_end`) = '00:00:00')");
|
||||
|
||||
// Corrects the collation on installs that already applied 2.5.8 before the
|
||||
// column was pinned to utf8mb4_bin. Harmless to re-run.
|
||||
mysqli_query($mysqli, "ALTER TABLE `calendars`
|
||||
MODIFY COLUMN `calendar_feed_key` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL");
|
||||
26
admin/database_updates/2.6.0.php
Normal file
26
admin/database_updates/2.6.0.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.6.0 (from 2.5.9)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// cron/cron.php is now a dispatcher: it runs every minute and decides which of the
|
||||
// scripts in cron/ are due, so the crontab only needs one line. That decision needs
|
||||
// somewhere durable to record when each job last ran - a job whose minute was missed
|
||||
// has to be picked up at the next opportunity rather than skipped, and one that is
|
||||
// half way through must not be started again.
|
||||
//
|
||||
// Rows are created by the dispatcher the first time it sees a job, so adding a job
|
||||
// later needs a line in cron.php and nothing here.
|
||||
mysqli_query($mysqli, "CREATE TABLE IF NOT EXISTS `cron_jobs` (
|
||||
`cron_job_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`cron_job_name` varchar(200) NOT NULL,
|
||||
`cron_job_last_run_at` datetime DEFAULT NULL,
|
||||
`cron_job_last_finished_at` datetime DEFAULT NULL,
|
||||
`cron_job_last_status` varchar(200) DEFAULT NULL,
|
||||
PRIMARY KEY (`cron_job_id`),
|
||||
UNIQUE KEY `cron_job_name` (`cron_job_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
|
||||
32
admin/database_updates/2.6.1.php
Normal file
32
admin/database_updates/2.6.1.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.6.1 (from 2.6.0)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// The cron dispatcher's schedule moves out of code and into the database so it can be
|
||||
// managed from Maintenance > Cron. The registry in includes/cron_jobs.php still decides
|
||||
// which scripts exist and seeds these columns the first time it meets a job; from then
|
||||
// on the row is what runs. Nothing here can name a script - a row whose job is not in
|
||||
// the registry is ignored.
|
||||
mysqli_query($mysqli, "ALTER TABLE `cron_jobs`
|
||||
ADD COLUMN `cron_job_enabled` tinyint(1) NOT NULL DEFAULT 1 AFTER `cron_job_name`,
|
||||
ADD COLUMN `cron_job_schedule` varchar(200) NOT NULL DEFAULT 'Interval' AFTER `cron_job_enabled`,
|
||||
ADD COLUMN `cron_job_interval_minutes` int(11) NOT NULL DEFAULT 1 AFTER `cron_job_schedule`,
|
||||
ADD COLUMN `cron_job_daily_at` time DEFAULT NULL AFTER `cron_job_interval_minutes`,
|
||||
ADD COLUMN `cron_job_run_now` tinyint(1) NOT NULL DEFAULT 0 AFTER `cron_job_daily_at`");
|
||||
|
||||
// Duration is here to make a job that is quietly getting slower visible before it starts
|
||||
// overrunning its own interval.
|
||||
mysqli_query($mysqli, "ALTER TABLE `cron_jobs`
|
||||
ADD COLUMN `cron_job_last_duration` decimal(10,2) DEFAULT NULL AFTER `cron_job_last_finished_at`,
|
||||
ADD COLUMN `cron_job_last_error` text DEFAULT NULL AFTER `cron_job_last_status`,
|
||||
ADD COLUMN `cron_job_last_error_at` datetime DEFAULT NULL AFTER `cron_job_last_error`");
|
||||
|
||||
// Written by the dispatcher every minute before it runs anything, so the admin page can
|
||||
// tell "no job happened to be due" apart from "the crontab entry is missing".
|
||||
mysqli_query($mysqli, "ALTER TABLE `settings`
|
||||
ADD COLUMN `config_cron_last_dispatch_at` datetime DEFAULT NULL");
|
||||
38
admin/database_updates/2.6.2.php
Normal file
38
admin/database_updates/2.6.2.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.6.2 (from 2.6.1)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Two things, both about rows that already existed before 2.6.1 ran.
|
||||
//
|
||||
// 2.6.1 added the schedule columns but could not set them, so those rows took the column
|
||||
// defaults - every minute - which is wrong for three jobs and harmful for the nightly run,
|
||||
// whose overdue invoice reminders re-send on every pass. The registry in
|
||||
// includes/cron_jobs.php only ever seeds a row it is creating, so it cannot fix them.
|
||||
//
|
||||
// The domain refresher also moves onto the nightly schedule here, which is why it is
|
||||
// matched at either of the intervals it may be sitting on: 1 from the column default, or 5
|
||||
// if it was already put back by hand.
|
||||
//
|
||||
// Only rows still carrying one of those shipped values are touched, so a schedule someone
|
||||
// has deliberately changed is left alone. Deliberately hardcoded rather than read from the
|
||||
// registry: a migration has to keep meaning the same thing years from now, whatever that
|
||||
// file says by then.
|
||||
mysqli_query($mysqli, "UPDATE `cron_jobs`
|
||||
SET `cron_job_schedule` = 'Daily', `cron_job_daily_at` = '03:00:00'
|
||||
WHERE `cron_job_name` = 'nightly_tasks'
|
||||
AND `cron_job_schedule` = 'Interval' AND `cron_job_interval_minutes` = 1");
|
||||
|
||||
mysqli_query($mysqli, "UPDATE `cron_jobs`
|
||||
SET `cron_job_schedule` = 'Daily', `cron_job_daily_at` = '03:30:00'
|
||||
WHERE `cron_job_name` = 'certificate_refresher'
|
||||
AND `cron_job_schedule` = 'Interval' AND `cron_job_interval_minutes` = 1");
|
||||
|
||||
mysqli_query($mysqli, "UPDATE `cron_jobs`
|
||||
SET `cron_job_schedule` = 'Daily', `cron_job_daily_at` = '04:00:00'
|
||||
WHERE `cron_job_name` = 'domain_refresher'
|
||||
AND `cron_job_schedule` = 'Interval' AND `cron_job_interval_minutes` IN (1, 5)");
|
||||
13
admin/database_updates/2.6.3.php
Normal file
13
admin/database_updates/2.6.3.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.6.3 (from 2.6.2)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Add Indexes to Ticket Replies foreign key to ticket_id and same with Attachment Fixes a slow down issue with many Replies with large bodies.
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE ticket_replies ADD INDEX (ticket_reply_ticket_id, ticket_reply_archived_at)");
|
||||
mysqli_query($mysqli, "ALTER TABLE ticket_attachments ADD INDEX (ticket_attachment_reply_id)");
|
||||
38
admin/database_updates/2.6.4.php
Normal file
38
admin/database_updates/2.6.4.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.6.4 (from 2.6.3)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Backup catalogue - one row per archive produced, so the app knows what exists
|
||||
// without trusting a directory listing
|
||||
|
||||
mysqli_query($mysqli, "CREATE TABLE IF NOT EXISTS `backups` (
|
||||
`backup_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`backup_type` varchar(20) NOT NULL DEFAULT 'full',
|
||||
`backup_file_name` varchar(255) NOT NULL,
|
||||
`backup_size` bigint(20) NOT NULL DEFAULT 0,
|
||||
`backup_sha256` varchar(64) DEFAULT NULL,
|
||||
`backup_status` varchar(20) NOT NULL DEFAULT 'Pending',
|
||||
`backup_error` text DEFAULT NULL,
|
||||
`backup_source` varchar(20) NOT NULL DEFAULT 'Manual',
|
||||
`backup_created_by` varchar(200) DEFAULT NULL,
|
||||
`backup_created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||
`backup_completed_at` datetime DEFAULT NULL,
|
||||
`backup_downloaded_at` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`backup_id`),
|
||||
KEY `backup_status_created` (`backup_status`, `backup_created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
|
||||
|
||||
// Retention and what the scheduled backup produces
|
||||
mysqli_query($mysqli, "ALTER TABLE settings ADD COLUMN IF NOT EXISTS `config_backup_retention_days` int(11) NOT NULL DEFAULT 30");
|
||||
mysqli_query($mysqli, "ALTER TABLE settings ADD COLUMN IF NOT EXISTS `config_backup_retention_count` int(11) NOT NULL DEFAULT 5");
|
||||
mysqli_query($mysqli, "ALTER TABLE settings ADD COLUMN IF NOT EXISTS `config_backup_cron_type` varchar(20) NOT NULL DEFAULT 'full'");
|
||||
|
||||
// Seed the scheduled backup job. The dispatcher would create this row itself the first
|
||||
// time it sees the job, but seeding it here means the schedule is right on an install
|
||||
// that already has cron_jobs rows - the every-minute default bit us once already.
|
||||
mysqli_query($mysqli, "INSERT IGNORE INTO cron_jobs SET cron_job_name = 'backup', cron_job_enabled = 0, cron_job_schedule = 'Daily', cron_job_daily_at = '02:00'");
|
||||
26
admin/database_updates/2.6.5.php
Normal file
26
admin/database_updates/2.6.5.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.6.5 (from 2.6.4)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// credential_password was inherited as VARBINARY(200) when 2.0.0 renamed login_password.
|
||||
// What it actually stores is a 16-char IV followed by base64 AES-128-CBC ciphertext -
|
||||
// pure ASCII, the same shape as credential_username (varchar(500)) and
|
||||
// users.user_specific_encryption_ciphertext (varchar(200)). Nothing compares, indexes,
|
||||
// sorts or searches on the column, so binary semantics were never buying anything.
|
||||
//
|
||||
// The width was the real problem: base64 expands ~1.37x, so 200 bytes capped the
|
||||
// cleartext at 127 chars while the credential form offered 350. Anything longer
|
||||
// overflowed and errored the save. varchar(500) matches credential_username and makes
|
||||
// 350 the correct form limit for both fields.
|
||||
|
||||
// Widen while still binary first. If the charset conversion below fails on an install
|
||||
// with unexpected bytes, the column is at least already wide enough and the app keeps
|
||||
// working - varbinary(500) holds the same values just fine.
|
||||
mysqli_query($mysqli, "ALTER TABLE `credentials` MODIFY `credential_password` varbinary(500) DEFAULT NULL");
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `credentials` MODIFY `credential_password` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL");
|
||||
19
admin/database_updates/2.6.6.php
Normal file
19
admin/database_updates/2.6.6.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.6.6 (from 2.6.5)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// config_invoice_overdue_reminders never did anything. Both setup paths seeded it with
|
||||
// '1,3,7' and two files read it, but the only line that would have used it was commented
|
||||
// out in the nightly job - the reminder schedule is a hardcoded array. The read in
|
||||
// includes/load_global_settings.php also ran intval() over a comma-separated string, so
|
||||
// the global was 1 whatever the column said.
|
||||
//
|
||||
// Nothing in the app has ever written to it either, so no install has a value worth
|
||||
// keeping and there is nothing to migrate anywhere.
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `settings` DROP COLUMN IF EXISTS `config_invoice_overdue_reminders`");
|
||||
18
admin/database_updates/2.6.7.php
Normal file
18
admin/database_updates/2.6.7.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.6.7 (from 2.6.6)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// The AI endpoints used to send a hardcoded temperature (0.5, or 0.3 for ticket
|
||||
// summaries). Newer OpenAI models accept nothing but their own default and reject
|
||||
// the request outright, which surfaced as "Failed to get a response from the AI API".
|
||||
//
|
||||
// Temperature is now per-model and optional: NULL means don't send the parameter
|
||||
// at all, which is the setting that works on every provider. Existing rows get
|
||||
// NULL so they stop sending it.
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `ai_models` ADD COLUMN IF NOT EXISTS `ai_model_temperature` decimal(3,2) DEFAULT NULL AFTER `ai_model_use_case`");
|
||||
768
admin/debug.php
Normal file
768
admin/debug.php
Normal file
@@ -0,0 +1,768 @@
|
||||
<?php
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
require_once "../includes/database_version.php";
|
||||
require_once "../config.php";
|
||||
|
||||
$checks = [];
|
||||
|
||||
// Execute the git command to get the latest commit hash
|
||||
$commitHash = shell_exec('git log -1 --format=%H');
|
||||
|
||||
// Get branch info
|
||||
$gitBranch = shell_exec('git rev-parse --abbrev-ref HEAD');
|
||||
|
||||
// Section: System Information
|
||||
$systemInfo = [];
|
||||
|
||||
// Operating System and Version
|
||||
$os = php_uname();
|
||||
$systemInfo[] = [
|
||||
'name' => 'Operating System',
|
||||
'value' => $os,
|
||||
];
|
||||
|
||||
// Web Server and Version
|
||||
$webServer = $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown';
|
||||
$systemInfo[] = [
|
||||
'name' => 'Web Server',
|
||||
'value' => $webServer,
|
||||
];
|
||||
|
||||
// Kernel and Version
|
||||
$kernelVersion = php_uname('r');
|
||||
$systemInfo[] = [
|
||||
'name' => 'Kernel Version',
|
||||
'value' => $kernelVersion,
|
||||
];
|
||||
|
||||
// Database and Version
|
||||
$dbVersion = $mysqli->server_info;
|
||||
$systemInfo[] = [
|
||||
'name' => 'Database Version',
|
||||
'value' => $dbVersion,
|
||||
];
|
||||
|
||||
// Section: PHP Extensions
|
||||
$phpExtensions = [];
|
||||
$extensions = [
|
||||
'php-mysqli' => 'mysqli',
|
||||
'php-intl' => 'intl',
|
||||
'php-curl' => 'curl',
|
||||
'php-mbstring' => 'mbstring',
|
||||
'php-gd' => 'gd',
|
||||
'php-zip' => 'zip',
|
||||
'php-xml' => 'xml',
|
||||
];
|
||||
|
||||
foreach ($extensions as $name => $ext) {
|
||||
$loaded = extension_loaded($ext);
|
||||
$phpExtensions[] = [
|
||||
'name' => "$name installed",
|
||||
'passed' => $loaded,
|
||||
'value' => $loaded ? 'Installed' : 'Not Installed',
|
||||
];
|
||||
}
|
||||
|
||||
// Section: PHP Configuration
|
||||
$phpConfig = [];
|
||||
|
||||
// Check if shell_exec is enabled
|
||||
$disabled_functions = explode(',', ini_get('disable_functions'));
|
||||
$disabled_functions = array_map('trim', $disabled_functions);
|
||||
$shell_exec_enabled = !in_array('shell_exec', $disabled_functions);
|
||||
|
||||
$phpConfig[] = [
|
||||
'name' => 'shell_exec is enabled',
|
||||
'passed' => $shell_exec_enabled,
|
||||
'value' => $shell_exec_enabled ? 'Enabled' : 'Disabled',
|
||||
];
|
||||
|
||||
// Check upload_max_filesize and post_max_size >= 500M
|
||||
function toBytes($val) {
|
||||
$val = trim($val);
|
||||
$unit = strtolower(substr($val, -1));
|
||||
$num = (float)$val;
|
||||
switch ($unit) {
|
||||
case 'g':
|
||||
$num *= 1024;
|
||||
case 'm':
|
||||
$num *= 1024;
|
||||
case 'k':
|
||||
$num *= 1024;
|
||||
}
|
||||
return $num;
|
||||
}
|
||||
|
||||
$required_bytes = 500 * 1024 * 1024; // 500M in bytes
|
||||
|
||||
$upload_max_filesize = ini_get('upload_max_filesize');
|
||||
$post_max_size = ini_get('post_max_size');
|
||||
|
||||
$upload_passed = toBytes($upload_max_filesize) >= $required_bytes;
|
||||
$post_passed = toBytes($post_max_size) >= $required_bytes;
|
||||
|
||||
$phpConfig[] = [
|
||||
'name' => 'upload_max_filesize >= 500M',
|
||||
'passed' => $upload_passed,
|
||||
'value' => $upload_max_filesize,
|
||||
];
|
||||
|
||||
$phpConfig[] = [
|
||||
'name' => 'post_max_size >= 500M',
|
||||
'passed' => $post_passed,
|
||||
'value' => $post_max_size,
|
||||
];
|
||||
|
||||
// PHP Memory Limit >= 512M
|
||||
$memoryLimit = ini_get('memory_limit');
|
||||
$memoryLimitBytes = toBytes($memoryLimit);
|
||||
$memoryLimitPassed = $memoryLimitBytes >= (512 * 1024 * 1024);
|
||||
$phpConfig[] = [
|
||||
'name' => 'PHP Memory Limit >= 512M',
|
||||
'passed' => $memoryLimitPassed,
|
||||
'value' => $memoryLimit,
|
||||
];
|
||||
|
||||
// Max Execution Time >= 300 seconds
|
||||
$maxExecutionTime = ini_get('max_execution_time');
|
||||
$maxExecutionTimePassed = $maxExecutionTime >= 300;
|
||||
$phpConfig[] = [
|
||||
'name' => 'Max Execution Time >= 300 seconds',
|
||||
'passed' => $maxExecutionTimePassed,
|
||||
'value' => $maxExecutionTime . ' seconds',
|
||||
];
|
||||
|
||||
// Check PHP version >= 8.2.0
|
||||
$php_version = PHP_VERSION;
|
||||
$php_passed = version_compare($php_version, '8.2.0', '>=');
|
||||
|
||||
$phpConfig[] = [
|
||||
'name' => 'PHP version >= 8.2.0',
|
||||
'passed' => $php_passed,
|
||||
'value' => $php_version,
|
||||
];
|
||||
|
||||
// Section: Shell Commands
|
||||
$shellCommands = [];
|
||||
|
||||
if ($shell_exec_enabled) {
|
||||
$commands = ['git'];
|
||||
|
||||
foreach ($commands as $command) {
|
||||
$which = trim(shell_exec("which $command 2>/dev/null"));
|
||||
$exists = !empty($which);
|
||||
$shellCommands[] = [
|
||||
'name' => "Command '$command' available",
|
||||
'passed' => $exists,
|
||||
'value' => $exists ? $which : 'Not Found',
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// If shell_exec is disabled, mark commands as unavailable
|
||||
foreach (['git'] as $command) {
|
||||
$shellCommands[] = [
|
||||
'name' => "Command '$command' available",
|
||||
'passed' => false,
|
||||
'value' => 'shell_exec Disabled',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Section: SSL Checks
|
||||
$sslChecks = [];
|
||||
|
||||
// Check if accessing via HTTPS
|
||||
$https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443;
|
||||
$sslChecks[] = [
|
||||
'name' => 'Accessing via HTTPS',
|
||||
'passed' => $https,
|
||||
'value' => $https ? 'Yes' : 'No',
|
||||
];
|
||||
|
||||
// SSL Certificate Validity Check
|
||||
if ($https) {
|
||||
$streamContext = stream_context_create(["ssl" => ["capture_peer_cert" => true]]);
|
||||
$socket = @stream_socket_client("ssl://{$_SERVER['HTTP_HOST']}:443", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $streamContext);
|
||||
|
||||
if ($socket) {
|
||||
$params = stream_context_get_params($socket);
|
||||
$cert = $params['options']['ssl']['peer_certificate'];
|
||||
$certInfo = openssl_x509_parse($cert);
|
||||
|
||||
$validFrom = $certInfo['validFrom_time_t'];
|
||||
$validTo = $certInfo['validTo_time_t'];
|
||||
$currentTime = time();
|
||||
|
||||
$certValid = ($currentTime >= $validFrom && $currentTime <= $validTo);
|
||||
|
||||
$sslChecks[] = [
|
||||
'name' => 'SSL Certificate is valid',
|
||||
'passed' => $certValid,
|
||||
'value' => $certValid ? 'Valid' : 'Invalid or Expired',
|
||||
];
|
||||
} else {
|
||||
$sslChecks[] = [
|
||||
'name' => 'SSL Certificate is valid',
|
||||
'passed' => false,
|
||||
'value' => 'Unable to retrieve certificate',
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$sslChecks[] = [
|
||||
'name' => 'SSL Certificate is valid',
|
||||
'passed' => false,
|
||||
'value' => 'Not using HTTPS',
|
||||
];
|
||||
}
|
||||
|
||||
// Section: Domain Checks
|
||||
$domainChecks = [];
|
||||
|
||||
// Check if the site has a valid FQDN
|
||||
$fqdn = $_SERVER['HTTP_HOST'];
|
||||
$isValidFqdn = (bool) filter_var('http://' . $fqdn, FILTER_VALIDATE_URL) && preg_match('/^[a-z0-9.-]+\.[a-z]{2,}$/i', $fqdn);
|
||||
|
||||
$domainChecks[] = [
|
||||
'name' => 'Site has a valid FQDN',
|
||||
'passed' => $isValidFqdn,
|
||||
'value' => $fqdn,
|
||||
];
|
||||
|
||||
// Section: File Permissions
|
||||
$filePermissions = [];
|
||||
|
||||
// Check if web user has write access to webroot directory
|
||||
$webroot = $_SERVER['DOCUMENT_ROOT'];
|
||||
$writable = is_writable($webroot);
|
||||
$filePermissions[] = [
|
||||
'name' => 'Web user has write access to webroot directory',
|
||||
'passed' => $writable,
|
||||
'value' => $webroot,
|
||||
];
|
||||
|
||||
// Section: Uploads Directory Stats
|
||||
$uploadsStats = [];
|
||||
|
||||
// Define the uploads directory path
|
||||
$uploadsDir = __DIR__ . '/../uploads'; // Adjust the path if needed
|
||||
|
||||
if (is_dir($uploadsDir)) {
|
||||
// Function to recursively count files and calculate total size
|
||||
function getDirStats($dir) {
|
||||
$files = 0;
|
||||
$size = 0;
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()) {
|
||||
$files++;
|
||||
$size += $file->getSize();
|
||||
}
|
||||
}
|
||||
return ['files' => $files, 'size' => $size];
|
||||
}
|
||||
|
||||
$stats = getDirStats($uploadsDir);
|
||||
$sizeInMB = round($stats['size'] / (1024 * 1024), 2);
|
||||
|
||||
$uploadsStats[] = [
|
||||
'name' => 'Number of files in uploads directory',
|
||||
'value' => $stats['files'],
|
||||
];
|
||||
|
||||
$uploadsStats[] = [
|
||||
'name' => 'Total size of uploads directory (MB)',
|
||||
'value' => $sizeInMB . ' MB',
|
||||
];
|
||||
} else {
|
||||
$uploadsStats[] = [
|
||||
'name' => 'Uploads directory exists',
|
||||
'value' => 'Directory not found',
|
||||
];
|
||||
}
|
||||
|
||||
// Section: Database Stats
|
||||
$databaseStats = [];
|
||||
|
||||
// Get list of tables
|
||||
$tablesResult = $mysqli->query("SHOW TABLE STATUS");
|
||||
if ($tablesResult) {
|
||||
$totalTables = 0;
|
||||
$totalFields = 0;
|
||||
$totalRows = 0;
|
||||
$totalSize = 0;
|
||||
$tableDetails = [];
|
||||
|
||||
while ($table = $tablesResult->fetch_assoc()) {
|
||||
$tableName = $table['Name'];
|
||||
|
||||
// Accurate row count
|
||||
$countResult = $mysqli->query("SELECT COUNT(*) AS cnt FROM `$tableName`");
|
||||
$countRow = $countResult->fetch_assoc();
|
||||
$tableRows = $countRow['cnt'];
|
||||
$countResult->free();
|
||||
|
||||
$dataLength = $table['Data_length'];
|
||||
$indexLength = $table['Index_length'];
|
||||
$tableSize = ($dataLength + $indexLength) / (1024 * 1024); // Size in MB
|
||||
|
||||
// Get number of fields
|
||||
$fieldsResult = $mysqli->query("SHOW COLUMNS FROM `$tableName`");
|
||||
$numFields = $fieldsResult->num_rows;
|
||||
$fieldsResult->free();
|
||||
|
||||
$totalTables++;
|
||||
$totalFields += $numFields;
|
||||
$totalRows += $tableRows;
|
||||
$totalSize += $tableSize;
|
||||
|
||||
$tableDetails[] = [
|
||||
'name' => $tableName,
|
||||
'fields' => $numFields,
|
||||
'rows' => $tableRows,
|
||||
'size' => round($tableSize, 2),
|
||||
];
|
||||
}
|
||||
$tablesResult->free();
|
||||
|
||||
$databaseStats[] = [
|
||||
'name' => 'Total number of tables',
|
||||
'value' => $totalTables,
|
||||
];
|
||||
$databaseStats[] = [
|
||||
'name' => 'Total number of fields',
|
||||
'value' => $totalFields,
|
||||
];
|
||||
$databaseStats[] = [
|
||||
'name' => 'Total number of rows',
|
||||
'value' => $totalRows,
|
||||
];
|
||||
$databaseStats[] = [
|
||||
'name' => 'Total database size (MB)',
|
||||
'value' => round($totalSize, 2) . ' MB',
|
||||
];
|
||||
}
|
||||
|
||||
// Section: Database Structure Comparison
|
||||
$dbComparison = [];
|
||||
|
||||
// Path to the db.sql file
|
||||
$dbSqlFile = __DIR__ . '/../db.sql';
|
||||
|
||||
if (file_exists($dbSqlFile)) {
|
||||
// Read the db.sql file
|
||||
$sqlContent = file_get_contents($dbSqlFile);
|
||||
|
||||
// Remove comments and empty lines
|
||||
$lines = explode("\n", $sqlContent);
|
||||
$sqlStatements = [];
|
||||
$statement = '';
|
||||
|
||||
foreach ($lines as $line) {
|
||||
// Remove single-line comments
|
||||
$line = preg_replace('/--.*$/', '', $line);
|
||||
$line = preg_replace('/\/\*.*?\*\//', '', $line);
|
||||
|
||||
// Skip empty lines
|
||||
if (trim($line) == '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Append line to the current statement
|
||||
$statement .= $line . "\n";
|
||||
|
||||
// Check if the statement ends with a semicolon
|
||||
if (preg_match('/;\s*$/', $line)) {
|
||||
$sqlStatements[] = $statement;
|
||||
$statement = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the CREATE TABLE statements
|
||||
$sqlTables = [];
|
||||
|
||||
foreach ($sqlStatements as $sql) {
|
||||
if (preg_match('/CREATE TABLE\s+`?([^` ]+)`?\s*\((.*)\)(.*?);/msi', $sql, $match)) {
|
||||
$tableName = $match[1];
|
||||
$columnsDefinition = $match[2];
|
||||
|
||||
// Extract column names and data types
|
||||
$columns = [];
|
||||
$columnLines = explode("\n", $columnsDefinition);
|
||||
foreach ($columnLines as $line) {
|
||||
$line = trim($line);
|
||||
// Skip empty lines and lines that do not define columns
|
||||
if ($line == '' || strpos($line, 'PRIMARY KEY') !== false || strpos($line, 'UNIQUE KEY') !== false || strpos($line, 'KEY') === 0 || strpos($line, 'CONSTRAINT') === 0 || strpos($line, ')') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove trailing comma if present
|
||||
$line = rtrim($line, ',');
|
||||
|
||||
// Match column definition
|
||||
if (preg_match('/^`([^`]+)`\s+(.+)/', $line, $colMatch)) {
|
||||
$colName = $colMatch[1];
|
||||
$colDefinition = $colMatch[2];
|
||||
|
||||
// Extract the data type from the column definition
|
||||
$tokens = preg_split('/\s+/', $colDefinition);
|
||||
$colType = $tokens[0];
|
||||
|
||||
// Handle data types with parentheses (e.g., varchar(255), decimal(15,2))
|
||||
if (preg_match('/^([a-zA-Z]+)\(([^)]+)\)/', $colType, $typeMatch)) {
|
||||
$colType = $typeMatch[1] . '(' . $typeMatch[2] . ')';
|
||||
}
|
||||
|
||||
$columns[$colName] = $colType;
|
||||
}
|
||||
}
|
||||
$sqlTables[$tableName] = $columns;
|
||||
}
|
||||
}
|
||||
|
||||
// Get current database table structures
|
||||
$dbTables = [];
|
||||
$tablesResult = $mysqli->query("SHOW TABLES");
|
||||
while ($row = $tablesResult->fetch_row()) {
|
||||
$tableName = $row[0];
|
||||
$columnsResult = $mysqli->query("SHOW COLUMNS FROM `$tableName`");
|
||||
$columns = [];
|
||||
while ($col = $columnsResult->fetch_assoc()) {
|
||||
$columns[$col['Field']] = $col['Type'];
|
||||
}
|
||||
$columnsResult->free();
|
||||
$dbTables[$tableName] = $columns;
|
||||
}
|
||||
$tablesResult->free();
|
||||
|
||||
// Compare the structures
|
||||
foreach ($sqlTables as $tableName => $sqlColumns) {
|
||||
if (!isset($dbTables[$tableName])) {
|
||||
$dbComparison[] = [
|
||||
'name' => "Table `$tableName` missing in database",
|
||||
'status' => 'Missing Table',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compare columns
|
||||
$dbColumns = $dbTables[$tableName];
|
||||
foreach ($sqlColumns as $colName => $colType) {
|
||||
if (!isset($dbColumns[$colName])) {
|
||||
$dbComparison[] = [
|
||||
'name' => "Column `$colName` missing in table `$tableName`",
|
||||
'status' => 'Missing Column',
|
||||
];
|
||||
} else {
|
||||
// Normalize data types for comparison
|
||||
$sqlColType = strtolower($colType);
|
||||
$dbColType = strtolower($dbColumns[$colName]);
|
||||
|
||||
// Remove attributes and constraints
|
||||
$sqlColType = preg_replace('/\s+.*$/', '', $sqlColType);
|
||||
$dbColType = preg_replace('/\s+.*$/', '', $dbColType);
|
||||
|
||||
// Remove additional attributes like unsigned, zerofill, etc.
|
||||
$sqlColType = preg_replace('/\s+unsigned|\s+zerofill|\s+binary/', '', $sqlColType);
|
||||
$dbColType = preg_replace('/\s+unsigned|\s+zerofill|\s+binary/', '', $dbColType);
|
||||
|
||||
if ($sqlColType != $dbColType) {
|
||||
$dbComparison[] = [
|
||||
'name' => "Data type mismatch for `$colName` in table `$tableName`",
|
||||
'status' => "Expected: $colType, Found: {$dbColumns[$colName]}",
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for extra columns in the database that are not in the SQL file
|
||||
foreach ($dbColumns as $colName => $colType) {
|
||||
if (!isset($sqlColumns[$colName])) {
|
||||
$dbComparison[] = [
|
||||
'name' => "Extra column `$colName` in table `$tableName` not present in db.sql",
|
||||
'status' => 'Extra Column',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for tables in the database not present in the db.sql file
|
||||
foreach ($dbTables as $tableName => $dbColumns) {
|
||||
if (!isset($sqlTables[$tableName])) {
|
||||
$dbComparison[] = [
|
||||
'name' => "Extra table `$tableName` in database not present in db.sql",
|
||||
'status' => 'Extra Table',
|
||||
];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$dbComparison[] = [
|
||||
'name' => 'db.sql file not found',
|
||||
'status' => 'File Missing',
|
||||
];
|
||||
}
|
||||
|
||||
$mysqli->close();
|
||||
?>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-3">
|
||||
<h3 class="card-title"><i class="fas fa-fw fa-bug me-2"></i>Debug</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<h2>Debugging</h2>
|
||||
<ul>
|
||||
<li>If you are experiencing a problem with ITFlow, this page should help you identify any configuration issues.</li>
|
||||
<li>Note: You might also need to gather <a href="https://docs.itflow.org/gathering_logs#error_logs">error logs</a></li>
|
||||
</ul>
|
||||
<hr>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered mb-3">
|
||||
<tr>
|
||||
<th>ITFlow release version</th>
|
||||
<th><?= APP_VERSION ?></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Current DB Version</td>
|
||||
<td><?= CURRENT_DATABASE_VERSION ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Current Code Commit</td>
|
||||
<td><?= $commitHash ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Current Branch</td>
|
||||
<td><?= $gitBranch ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- System Information Table -->
|
||||
<h3>System Information</h3>
|
||||
<table class="table table-sm table-bordered">
|
||||
<tbody>
|
||||
<?php foreach ($systemInfo as $info): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($info['name']); ?></td>
|
||||
<td><?= htmlspecialchars($info['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- PHP Extensions and Configuration Table -->
|
||||
<h3 class="mt-3">PHP Extensions and Configuration</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered">
|
||||
<!-- PHP Extensions Section -->
|
||||
<thead>
|
||||
<tr class="table-secondary">
|
||||
<th colspan="3">PHP Extensions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($phpExtensions as $check): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($check['name']); ?></td>
|
||||
<td class="text-center">
|
||||
<?php if ($check['passed']): ?>
|
||||
<i class="fas fa-check" style="color:green"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-times" style="color:red"></i>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($check['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<!-- PHP Configuration Section -->
|
||||
<thead>
|
||||
<tr class="table-secondary">
|
||||
<th colspan="3">PHP Configuration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($phpConfig as $check): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($check['name']); ?></td>
|
||||
<td class="text-center">
|
||||
<?php if ($check['passed']): ?>
|
||||
<i class="fas fa-check" style="color:green"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-times" style="color:red"></i>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($check['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<thead>
|
||||
<tr class="table-secondary">
|
||||
<th colspan="3">Shell Commands</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($shellCommands as $check): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($check['name']); ?></td>
|
||||
<td class="text-center">
|
||||
<?php if ($check['passed']): ?>
|
||||
<i class="fas fa-check" style="color:green"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-times" style="color:red"></i>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($check['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<thead>
|
||||
<tr class="table-secondary">
|
||||
<th colspan="3">SSL Checks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($sslChecks as $check): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($check['name']); ?></td>
|
||||
<td class="text-center">
|
||||
<?php if ($check['passed']): ?>
|
||||
<i class="fas fa-check" style="color:green"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-times" style="color:red"></i>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($check['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<thead>
|
||||
<tr class="table-secondary">
|
||||
<th colspan="3">Domain Checks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($domainChecks as $check): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($check['name']); ?></td>
|
||||
<td class="text-center">
|
||||
<?php if ($check['passed']): ?>
|
||||
<i class="fas fa-check" style="color:green"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-times" style="color:red"></i>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($check['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
<!-- File Permissions Table -->
|
||||
<thead>
|
||||
<tr class="table-secondary">
|
||||
<th colspan="3">File Permissions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($filePermissions as $check): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($check['name']); ?></td>
|
||||
<td class="text-center">
|
||||
<?php if ($check['passed']): ?>
|
||||
<i class="fas fa-check" style="color:green"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-times" style="color:red"></i>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($check['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Database Structure Comparison Table -->
|
||||
<h3 class="mt-3">Database Structure Comparison</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered">
|
||||
<tbody>
|
||||
<?php if (!empty($dbComparison)): ?>
|
||||
<?php foreach ($dbComparison as $issue): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($issue['name']); ?></td>
|
||||
<td colspan="2"><?= htmlspecialchars($issue['status']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="3">No discrepancies found between the database and db.sql file.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Uploads Directory Stats Table -->
|
||||
<h3 class="mt-3">Uploads Directory Stats</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered">
|
||||
<tbody>
|
||||
<?php foreach ($uploadsStats as $stat): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($stat['name']); ?></td>
|
||||
<td colspan="2"><?= htmlspecialchars($stat['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Database Stats Table -->
|
||||
<h3 class="mt-3">Database Stats</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered">
|
||||
<tbody>
|
||||
<?php foreach ($databaseStats as $stat): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($stat['name']); ?></td>
|
||||
<td colspan="2"><?= htmlspecialchars($stat['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Table Stats Table -->
|
||||
<h3 class="mt-3">Table Stats</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Table Name</th>
|
||||
<th>Fields / Rows</th>
|
||||
<th>Size (MB)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($tableDetails as $table): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($table['name']); ?></td>
|
||||
<td><?= htmlspecialchars("Fields: {$table['fields']}, Rows: {$table['rows']}"); ?></td>
|
||||
<td><?= htmlspecialchars($table['size'] . ' MB'); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php
|
||||
|
||||
require_once "../includes/footer.php";
|
||||
71
admin/document_template.php
Normal file
71
admin/document_template.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
|
||||
//Initialize the HTML Purifier to prevent XSS
|
||||
require "../libs/htmlpurifier/HTMLPurifier.standalone.php";
|
||||
|
||||
$purifier_config = HTMLPurifier_Config::createDefault();
|
||||
$purifier_config->set('Cache.DefinitionImpl', null); // Disable cache by setting a non-existent directory or an invalid one
|
||||
$purifier_config->set('URI.AllowedSchemes', ['data' => true, 'src' => true, 'http' => true, 'https' => true]);
|
||||
$purifier = new HTMLPurifier($purifier_config);
|
||||
|
||||
if (isset($_GET['document_template_id'])) {
|
||||
$document_template_id = intval($_GET['document_template_id']);
|
||||
}
|
||||
|
||||
$sql_document = mysqli_query($mysqli, "SELECT document_template_content, document_template_created_at, document_template_description,
|
||||
document_template_name, document_template_updated_at FROM document_templates WHERE document_template_id = $document_template_id LIMIT 1");
|
||||
|
||||
if (mysqli_num_rows($sql_document) == 0) {
|
||||
echo "<center><h1 class='text-secondary mt-5'>Nothing to see here</h1><a class='btn btn-lg btn-secondary mt-3' href='javascript:history.back()'><i class='fa fa-fw fa-arrow-left'></i> Go Back</a></center>";
|
||||
require_once "../includes/footer.php";
|
||||
exit();
|
||||
}
|
||||
|
||||
$row = mysqli_fetch_assoc($sql_document);
|
||||
|
||||
$document_template_name = escapeHtml($row['document_template_name']);
|
||||
$document_template_description = escapeHtml($row['document_template_description']);
|
||||
$document_template_content = $purifier->purify($row['document_template_content']);
|
||||
$document_template_created_at = escapeHtml($row['document_template_created_at']);
|
||||
$document_template_updated_at = escapeHtml($row['document_template_updated_at']);
|
||||
|
||||
?>
|
||||
|
||||
<ol class="breadcrumb d-print-none">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="../">Home</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="users.php">Admin</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="document_templates.php">Document Templates</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active"><i class="fas fa-file-alt me-2"></i><?= $document_template_name ?></li>
|
||||
</ol>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header">
|
||||
|
||||
<h3 class="card-title mt-1"><i class="fa fa-fw fa-file-alt me-2"></i><?= $document_template_name ?></h3>
|
||||
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-tool ajax-modal"
|
||||
data-modal-size="xl"
|
||||
data-modal-url="modals/document_template/document_template_edit.php?id=<?= $document_template_id ?>">
|
||||
<i class="fas fa-edit me-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body prettyContent">
|
||||
<?= $document_template_content ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../js/pretty_content.js"></script>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
136
admin/document_templates.php
Normal file
136
admin/document_templates.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
// Default Column Sort by Filter
|
||||
$sort = "document_template_name";
|
||||
$order = "ASC";
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS document_template_content, document_template_created_at, document_template_description,
|
||||
document_template_id, document_template_name, document_template_updated_at, user_name FROM document_templates
|
||||
LEFT JOIN users ON document_template_created_by = user_id
|
||||
WHERE user_name LIKE '%$q%' OR document_template_name LIKE '%$q%'
|
||||
ORDER BY $sort $order LIMIT $record_from, $record_to"
|
||||
);
|
||||
|
||||
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
|
||||
?>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-2">
|
||||
<h3 class="card-title mt-2"><i class="fa fa-fw fa-file-alt me-2"></i>Document Templates</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/document_template/document_template_add.php" data-modal-size="xl">
|
||||
<i class="fas fa-plus me-2"></i>New Template
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<form autocomplete="off">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control " name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search templates">
|
||||
<button class="btn btn-secondary"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-striped table-borderless table-hover">
|
||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<th>
|
||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=document_template_name&order=<?= $disp ?>">
|
||||
Template Name <?php if ($sort == 'document_template_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=document_template_created_at&order=<?= $disp ?>">
|
||||
Created <?php if ($sort == 'document_template_created_at') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=document_template_updated_at&order=<?= $disp ?>">
|
||||
Updated <?php if ($sort == 'document_template_updated_at') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$document_template_id = intval($row['document_template_id']);
|
||||
$document_template_name = escapeHtml($row['document_template_name']);
|
||||
$document_template_description = escapeHtml($row['document_template_description']);
|
||||
$document_template_content = escapeHtml($row['document_template_content']);
|
||||
$document_template_created_by_name = escapeHtml($row['user_name']);
|
||||
$document_template_created_at = escapeHtml($row['document_template_created_at']);
|
||||
$document_template_updated_at = escapeHtml($row['document_template_updated_at']) ?: '-';
|
||||
|
||||
?>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<a class="text-dark ajax-modal" href="#"
|
||||
data-modal-size="xl"
|
||||
data-modal-url="modals/document_template/document_template_edit.php?id=<?= $document_template_id ?>">
|
||||
<div class="d-flex">
|
||||
<i class="fas fa-fw fa-2x fa-file-alt me-2"></i>
|
||||
<div class="flex-grow-1">
|
||||
<div><?= $document_template_name ?></div>
|
||||
<div><small class="text-secondary"><?= $document_template_description ?></small></div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<?= $document_template_created_at ?>
|
||||
<div class="text-secondary"><?= $document_template_created_by_name ?></div>
|
||||
</td>
|
||||
<td><?= $document_template_updated_at ?></td>
|
||||
<td>
|
||||
<div class="dropdown dropstart text-center">
|
||||
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="document_template.php?document_template_id=<?= $document_template_id ?>">
|
||||
<i class="fas fa-fw fa-eye me-2"></i>View
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-size="xl"
|
||||
data-modal-url="modals/document_template/document_template_edit.php?id=<?= $document_template_id ?>">
|
||||
<i class="fas fa-fw fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger text-bold" href="post.php?delete_document_template=<?= $document_template_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
</div>
|
||||
<?php require_once "../includes/filter_footer.php"; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once "../includes/footer.php";
|
||||
52
admin/identity_providers.php
Normal file
52
admin/identity_providers.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
require_once "includes/inc_all_admin.php";
|
||||
?>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-3">
|
||||
<h3 class="card-title"><i class="fas fa-fw fa-fingerprint me-2"></i>Identity Providers</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<h4>Client Portal SSO via Microsoft Entra</h4>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Identity Provider <small class='text-secondary'>(Currently only works with Microsoft Entra ID/AAD)</small></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-fingerprint"></i></span>
|
||||
<select class="form-select select2" readonly>
|
||||
<option <?php if (empty($config_azure_client_id)) { echo "selected"; } ?>>Disabled</option>
|
||||
<option <?php if ($config_azure_client_id) { echo "selected"; } ?>>Microsoft Entra</option>
|
||||
<option>Google (WIP)</option>
|
||||
<option>Custom SSO (WIP)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>MS Entra OAuth App (Client) ID</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
||||
<input type="text" class="form-control" name="azure_client_id" placeholder="e721e3b6-01d6-50e8-7f22-c84d951a52e7" maxlength="200" value="<?= escapeHtml($config_azure_client_id) ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>MS Entra OAuth Secret</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
|
||||
<input type="password" class="form-control" name="azure_client_secret" placeholder="Auto-generated from App Registration" maxlength="200" value="<?= escapeHtml($config_azure_client_secret) ?>" autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<button type="submit" name="edit_identity_provider" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Save</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once "../includes/footer.php";
|
||||
14
admin/includes/inc_all_admin.php
Normal file
14
admin/includes/inc_all_admin.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/config.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/functions.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/check_login.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/page_title.php';
|
||||
enforceAdminPermission();
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/top_nav.php';
|
||||
require_once 'includes/side_nav.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/inc_wrapper.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/inc_alert_feedback.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/filter_header.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/app_version.php';
|
||||
4
admin/includes/modal_header.php
Normal file
4
admin/includes/modal_header.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/modal_header.php';
|
||||
enforceAdminPermission();
|
||||
349
admin/includes/side_nav.php
Normal file
349
admin/includes/side_nav.php
Normal file
@@ -0,0 +1,349 @@
|
||||
<!-- Main Sidebar Container -->
|
||||
<aside class="app-sidebar shadow d-print-none" data-bs-theme="dark">
|
||||
<div class="sidebar-brand">
|
||||
<a class="brand-link" href="/agent/<?= $config_start_page ?>">
|
||||
<i class="fas fa-arrow-left me-2"></i>
|
||||
<span class="brand-text h6 mb-0">Back | <strong>Administration</strong></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar-wrapper">
|
||||
<!-- Sidebar Menu -->
|
||||
<nav>
|
||||
<ul class="nav nav-pills sidebar-menu flex-column mt-2" data-lte-toggle="treeview" data-accordion="false">
|
||||
<li class="nav-header">ACCESS</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/users.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "users.php") {echo "active";} ?>">
|
||||
<i class="nav-icon fas fa-users"></i>
|
||||
<p>Users</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/roles.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "roles.php") {echo "active";} ?>">
|
||||
<i class="nav-icon fas fa-user-shield"></i>
|
||||
<p>Roles</p>
|
||||
</a>
|
||||
</li>
|
||||
<!-- 2025-12-05 JQ - Hide Permission Modules currently just shows modules
|
||||
<li class="nav-item">
|
||||
<a href="/admin/modules.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "modules.php") {echo "active";} ?>">
|
||||
<i class="nav-icon fas fa-puzzle-piece"></i>
|
||||
<p>Modules</p>
|
||||
</a>
|
||||
</li>
|
||||
-->
|
||||
<li class="nav-item">
|
||||
<a href="/admin/api_keys.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "api_keys.php") {echo "active";} ?>">
|
||||
<i class="nav-icon fas fa-key"></i>
|
||||
<p>API Keys</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-header">LISTS</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/tags.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'tags.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-tags"></i>
|
||||
<p>Tags</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/categories.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'categories.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-list-ul"></i>
|
||||
<p>Categories</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php if ($config_module_enable_accounting) { ?>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/tax_rates.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'tax_rates.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-balance-scale"></i>
|
||||
<p>Tax Rates</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/payment_methods.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'payment_methods.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-hand-holding-usd"></i>
|
||||
<p>Payment Methods</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<?php if ($config_module_enable_ticketing) { ?>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/ticket_statuses.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'ticket_statuses.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-info-circle"></i>
|
||||
<p>Ticket Statuses</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<?php if ($config_module_enable_ticketing) { ?>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/sla.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'sla.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-stopwatch"></i>
|
||||
<p>SLAs</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
|
||||
<li class="nav-header">INTEGRATIONS</li>
|
||||
<?php if ($config_module_enable_accounting) { ?>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/payment_providers.php"
|
||||
class="nav-link <?= (in_array(basename($_SERVER['PHP_SELF']), ['payment_providers.php', 'saved_payment_methods.php']) ? 'active' : '') ?>">
|
||||
<i class="nav-icon far fa-credit-card"></i>
|
||||
<p>Payment Providers</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/ai_providers.php"
|
||||
class="nav-link <?= (in_array(basename($_SERVER['PHP_SELF']), ['ai_providers.php', 'ai_models.php']) ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-robot"></i>
|
||||
<p>AI Providers</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/custom_links.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'custom_links.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-external-link-alt"></i>
|
||||
<p>Custom Links</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<?php if ($config_module_enable_itdoc || $config_module_enable_ticketing) { ?>
|
||||
<li class="nav-header">TEMPLATES</li>
|
||||
<?php } ?>
|
||||
<?php if ($config_module_enable_ticketing) { ?>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/project_templates.php" class="nav-link <?= (in_array(basename($_SERVER['PHP_SELF']), ['project_templates.php', 'project_template.php']) ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-project-diagram"></i>
|
||||
<p>Project Templates</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/ticket_templates.php" class="nav-link <?= (in_array(basename($_SERVER['PHP_SELF']), ['ticket_templates.php', 'ticket_template.php']) ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-life-ring"></i>
|
||||
<p>Ticket Templates</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<?php if ($config_module_enable_itdoc) { ?>
|
||||
<!-- 2025-11-16 JQ - Hide Contracts not yet ready
|
||||
<li class="nav-item">
|
||||
<a href="/admin/contract_templates.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'contract_templates.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-file-contract"></i>
|
||||
<p>
|
||||
<span href="#" class="fas fa-plus-circle right ajax-modal" data-modal-url="/admin/modals/contract_template/contract_template_add.php" data-modal-size="lg"></span>
|
||||
Contract Templates
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
-->
|
||||
<li class="nav-item">
|
||||
<a href="/admin/vendor_templates.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'vendor_templates.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-building"></i>
|
||||
<p>Vendor Templates</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/software_templates.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'software_templates.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-box-open"></i>
|
||||
<p>License Templates</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/document_templates.php" class="nav-link <?= (in_array(basename($_SERVER['PHP_SELF']), ['document_templates.php', 'document_template.php']) ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-file-alt"></i>
|
||||
<p>Document Templates</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
|
||||
<li class="nav-header">MAINTENANCE</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a href="/admin/cron.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'cron.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-clock"></i>
|
||||
<p>Cron</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/mail_queue.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'mail_queue.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-inbox"></i>
|
||||
<p>Mail Queue</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/audit_logs.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'audit_logs.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-history"></i>
|
||||
<p>Audit Logs</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/app_logs.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'app_logs.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-clipboard-list"></i>
|
||||
<p>App Logs</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/backup.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'backup.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-cloud-upload-alt"></i>
|
||||
<p>Backup</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/debug.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'debug.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-bug"></i>
|
||||
<p>Debug</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/update.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'update.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-download"></i>
|
||||
<p>Update</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/starter_content.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'starter_content.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-seedling"></i>
|
||||
<p>Starter Content</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- SETTINGS Section -->
|
||||
<li class="nav-item mt-2 <?= (in_array(basename($_SERVER['PHP_SELF']), ['settings_company.php', 'settings_localization.php', 'settings_theme.php', 'settings_security.php', 'settings_mail.php', 'settings_notification.php', 'settings_default.php', 'settings_invoice.php', 'settings_quote.php', 'settings_online_payment.php', 'settings_online_payment_clients.php', 'settings_project.php', 'settings_ticket.php', 'settings_ai.php', 'identity_providers.php', 'settings_telemetry.php', 'settings_module.php']) ? 'menu-open' : '') ?>">
|
||||
<a href="#" class="nav-link">
|
||||
<p>
|
||||
SETTINGS
|
||||
<i class="right fas fa-angle-left"></i>
|
||||
</p>
|
||||
</a>
|
||||
<ul class="nav nav-treeview">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings_company.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'settings_company.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fa fa-briefcase"></i>
|
||||
<p>Company Details</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings_localization.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'settings_localization.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fa fa-globe"></i>
|
||||
<p>Localization</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings_theme.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'settings_theme.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fa fa-paint-brush"></i>
|
||||
<p>Theme</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings_security.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'settings_security.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-shield-alt"></i>
|
||||
<p>Security</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings_mail.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'settings_mail.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon far fa-envelope"></i>
|
||||
<p>Mail</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings_notification.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'settings_notification.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon far fa-bell"></i>
|
||||
<p>Notifications</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings_default.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'settings_default.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-cogs"></i>
|
||||
<p>Defaults</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php if ($config_module_enable_accounting) { ?>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings_quote.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'settings_quote.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-comment-dollar"></i>
|
||||
<p>Quote</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings_invoice.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'settings_invoice.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-file-invoice"></i>
|
||||
<p>Invoice</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<?php if ($config_module_enable_ticketing) { ?>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings_project.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'settings_project.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-project-diagram"></i>
|
||||
<p>Project</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings_ticket.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'settings_ticket.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-life-ring"></i>
|
||||
<p>Ticket</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<!-- Currently the only integration is the client portal SSO -->
|
||||
<?php if ($config_client_portal_enable) { ?>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/identity_providers.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'identity_providers.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-fingerprint"></i>
|
||||
<p>Identity Provider</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings_telemetry.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'settings_telemetry.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-satellite-dish"></i>
|
||||
<p>Telemetry</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings_module.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'settings_module.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-cube"></i>
|
||||
<p>Modules</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<?php
|
||||
$sql_custom_links = mysqli_query($mysqli, "SELECT custom_link_icon, custom_link_name, custom_link_new_tab, custom_link_uri FROM custom_links
|
||||
WHERE custom_link_location = 4 AND custom_link_archived_at IS NULL
|
||||
ORDER BY custom_link_order ASC, custom_link_name ASC"
|
||||
);
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql_custom_links)) {
|
||||
$custom_link_name = escapeHtml($row['custom_link_name']);
|
||||
$custom_link_uri = escapeUrl($row['custom_link_uri']);
|
||||
$custom_link_icon = escapeHtml($row['custom_link_icon']);
|
||||
$custom_link_new_tab = intval($row['custom_link_new_tab']);
|
||||
if ($custom_link_new_tab == 1) {
|
||||
$target = "target='_blank' rel='noopener noreferrer'";
|
||||
} else {
|
||||
$target = "";
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<li class="nav-item">
|
||||
<a href="<?= $custom_link_uri ?>" <?= $target ?> class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == basename($custom_link_uri)) { echo "active"; } ?>">
|
||||
<i class="fas fa-<?= $custom_link_icon ?> nav-icon"></i>
|
||||
<p><?= $custom_link_name ?></p>
|
||||
<i class="fas fa-angle-right nav-icon float-end"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
<!-- /.sidebar-menu -->
|
||||
<div class="mb-3"></div>
|
||||
</div>
|
||||
<!-- /.sidebar -->
|
||||
</aside>
|
||||
4
admin/index.php
Normal file
4
admin/index.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
|
||||
header('Location: users.php');
|
||||
|
||||
192
admin/mail_queue.php
Normal file
192
admin/mail_queue.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
// Default Column Sortby Filter
|
||||
$sort = "email_id";
|
||||
$order = "DESC";
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS email_attempts, email_failed_at, email_from, email_from_name, email_id, email_queued_at,
|
||||
email_recipient, email_recipient_name, email_sent_at, email_status, email_subject FROM email_queue
|
||||
WHERE (email_id LIKE '%$q%' OR email_from LIKE '%$q%' OR email_from_name LIKE '%$q%' OR email_recipient LIKE '%$q%' OR email_recipient_name LIKE '%$q%' OR email_subject LIKE '%$q%')
|
||||
AND DATE(email_queued_at) BETWEEN '$dtf' AND '$dtt'
|
||||
ORDER BY $sort $order LIMIT $record_from, $record_to"
|
||||
);
|
||||
|
||||
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
|
||||
?>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-3">
|
||||
<h3 class="card-title"><i class="fas fa-fw fa-inbox me-2"></i>Email Queue</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="mb-4" autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search mail queue">
|
||||
<button class="btn btn-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#advancedFilter"><i class="fas fa-filter"></i></button>
|
||||
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<div class="dropdown float-end" id="bulkActionButton" hidden>
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-fw fa-layer-group me-2"></i>Bulk Action (<span id="selectedCount">0</span>)
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<button class="dropdown-item"
|
||||
type="submit" form="bulkActions" name="bulk_cancel_emails">
|
||||
<i class="fas fa-fw fa-ban me-2"></i>Cancel
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item text-danger text-bold"
|
||||
type="submit" form="bulkActions" name="bulk_delete_emails">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse mt-3 <?php if (isset($_GET['dtf']) && $_GET['dtf'] !== '1970-01-01') { echo "show"; } ?>" id="advancedFilter">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label>Date range</label>
|
||||
<input type="text" id="dateFilter" class="form-control" autocomplete="off">
|
||||
<input type="hidden" name="canned_date" id="canned_date" value="<?= escapeHtml($_GET['canned_date']) ?? '' ?>">
|
||||
<input type="hidden" name="dtf" id="dtf" value="<?= escapeHtml($dtf ?? '') ?>">
|
||||
<input type="hidden" name="dtt" id="dtt" value="<?= escapeHtml($dtt ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
<form id="bulkActions" action="post.php" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-sm table-striped table-borderless table-hover">
|
||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<td class="bg-light pe-0">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" id="selectAllCheckbox" type="checkbox" onclick="checkAll(this)">
|
||||
</div>
|
||||
</td>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=email_queued_at&order=<?= $disp ?>">
|
||||
Queued <?php if ($sort == 'email_queued_at') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=email_from&order=<?= $disp ?>">
|
||||
From <?php if ($sort == 'email_from') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=email_recipient&order=<?= $disp ?>">
|
||||
To <?php if ($sort == 'email_recipient') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=email_subject&order=<?= $disp ?>">
|
||||
Subject <?php if ($sort == 'email_subject') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=email_status&order=<?= $disp ?>">
|
||||
Status <?php if ($sort == 'email_status') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=email_attempts&order=<?= $disp ?>">
|
||||
Attempts <?php if ($sort == 'email_attempts') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$email_id = intval($row['email_id']);
|
||||
$email_from = escapeHtml($row['email_from']);
|
||||
$email_from_name = escapeHtml($row['email_from_name']);
|
||||
$email_recipient = escapeHtml($row['email_recipient']);
|
||||
$email_recipient_name = escapeHtml($row['email_recipient_name']);
|
||||
$email_subject = escapeHtml($row['email_subject']);
|
||||
$email_attempts = intval($row['email_attempts']);
|
||||
$email_queued_at = escapeHtml($row['email_queued_at']);
|
||||
$email_failed_at = escapeHtml($row['email_failed_at']);
|
||||
$email_sent_at = escapeHtml($row['email_sent_at']);
|
||||
$email_status = intval($row['email_status']);
|
||||
if ($email_status == 0) {
|
||||
$email_status_display = "<div class='text-primary'>Queued</div>";
|
||||
} elseif($email_status == 1) {
|
||||
$email_status_display = "<div class='text-warning'>Sending</div>";
|
||||
} elseif($email_status == 2) {
|
||||
$email_status_display = "<div class='text-danger'>Failed</div><small class='text-secondary font-monospace'>$email_failed_at</small>";
|
||||
} else {
|
||||
$email_status_display = "<div class='text-success'>Sent</div><small class='text-secondary font-monospace'>$email_sent_at</small>";
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<tr>
|
||||
<td class="pe-0 bg-light">
|
||||
<?php if ($email_status !== 3) { ?>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input bulk-select" type="checkbox" name="email_ids[]" value="<?= $email_id ?>">
|
||||
</div>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="font-monospace"><?= $email_queued_at ?></td>
|
||||
<td><?= "$email_from<br><small class='text-secondary'>$email_from_name</small>" ?></td>
|
||||
<td><?= "$email_recipient<br><small class='text-secondary'>$email_recipient_name</small>" ?></td>
|
||||
<td><?= $email_subject ?></td>
|
||||
<td><?= $email_status_display ?></td>
|
||||
<td><?= $email_attempts ?></td>
|
||||
<td class="text-center">
|
||||
<a class="btn btn-sm btn-secondary ajax-modal" href="#"
|
||||
data-modal-size="lg"
|
||||
data-modal-url="modals/mail_queue/mail_queue_message_view.php?id=<?= $email_id ?>">
|
||||
<i class="fas fa-fw fa-eye"></i>
|
||||
</a>
|
||||
|
||||
<!-- Show force resend if all retries have failed -->
|
||||
<?php if ($email_status == 2 && $email_attempts > 3) { ?>
|
||||
<a class="btn btn-sm btn-success" href="post.php?send_failed_mail=<?= $email_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"><i class="fas fa-fw fa-paper-plane"></i></a>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Allow cancelling a message if it hasn't yet been picked up (e.g. stuck/bugged) -->
|
||||
<?php if ($email_status !== 3) { ?>
|
||||
<a class="btn btn-sm btn-danger confirm-link" href="post.php?cancel_mail=<?= $email_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"><i class="fas fa-fw fa-trash"></i></a>
|
||||
<?php } ?>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php require_once "../includes/filter_footer.php"; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../js/bulk_actions.js"></script>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
78
admin/modals/ai/ai_model_add.php
Normal file
78
admin/modals/ai/ai_model_add.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
require_once '../../includes/modal_header.php';
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-robot me-2"></i>Add AI Model</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Provider <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-robot"></i></span>
|
||||
<select class="form-select select2" name="provider" required>
|
||||
<option value="">- Select an AI Provider -</option>
|
||||
<?php
|
||||
$sql_ai_providers = mysqli_query($mysqli, "SELECT ai_provider_id, ai_provider_name FROM ai_providers");
|
||||
while ($row = mysqli_fetch_assoc($sql_ai_providers)) {
|
||||
$ai_provider_id = intval($row['ai_provider_id']);
|
||||
$ai_provider_name = escapeHtml($row['ai_provider_name']);
|
||||
|
||||
?>
|
||||
<option value="<?= $ai_provider_id ?>"><?= $ai_provider_name ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Model Name <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-robot"></i></span>
|
||||
<input type="text" class="form-control" name="model" placeholder="ex gpt-4" maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Use Case <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-th-list"></i></span>
|
||||
<select class="form-select select2" name="use_case">
|
||||
<option>General</option>
|
||||
<option>Tickets</option>
|
||||
<option>Documentation</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Temperature</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-thermometer-half"></i></span>
|
||||
<input type="number" class="form-control" name="temperature" step="0.1" min="0" max="2" value="" placeholder="Provider default">
|
||||
</div>
|
||||
<small class="form-text text-muted">Optional. Leave blank to let the provider use its default - some newer models reject every other value.</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control" rows="8" name="prompt" placeholder="Enter a model prompt:"></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="add_ai_model" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Create</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
91
admin/modals/ai/ai_model_edit.php
Normal file
91
admin/modals/ai/ai_model_edit.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
require_once '../../includes/modal_header.php';
|
||||
|
||||
$model_id = intval($_GET['id']);
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT ai_model_ai_provider_id, ai_model_id, ai_model_name, ai_model_prompt, ai_model_use_case, ai_model_temperature FROM ai_models WHERE ai_model_id = $model_id LIMIT 1");
|
||||
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$ai_model_ai_provider_id = intval($row['ai_model_ai_provider_id']);
|
||||
$model_id = intval($row['ai_model_id']);
|
||||
$model_name = escapeHtml($row['ai_model_name']);
|
||||
$use_case = escapeHtml($row['ai_model_use_case']);
|
||||
$temperature = escapeHtml($row['ai_model_temperature']);
|
||||
$prompt = escapeHtml($row['ai_model_prompt']);
|
||||
|
||||
// Generate the HTML form content using output buffering.
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-robot me-2"></i>Editing: <strong><?= $model_name ?></strong></h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="model_id" value="<?= $model_id ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Provider <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-robot"></i></span>
|
||||
<select class="form-select select2" name="provider" required>
|
||||
<option value="">- Select an AI Provider -</option>
|
||||
<?php
|
||||
$sql_ai_providers = mysqli_query($mysqli, "SELECT ai_provider_id, ai_provider_name FROM ai_providers");
|
||||
while ($row = mysqli_fetch_assoc($sql_ai_providers)) {
|
||||
$ai_provider_id = intval($row['ai_provider_id']);
|
||||
$ai_provider_name = escapeHtml($row['ai_provider_name']);
|
||||
|
||||
?>
|
||||
<option <?php if ($ai_provider_id = $ai_model_ai_provider_id) { echo "selected"; } ?> value="<?= $ai_provider_id ?>"><?= $ai_provider_name ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Model Name <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-robot"></i></span>
|
||||
<input type="text" class="form-control" name="model" value="<?= $model_name ?>" placeholder="ex gpt-4" maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Use Case <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-th-list"></i></span>
|
||||
<select class="form-select select2" name="use_case">
|
||||
<option <?php if ($use_case == 'General') { echo "selected"; } ?>>General</option>
|
||||
<option <?php if ($use_case == 'Tickets') { echo "selected"; } ?>>Tickets</option>
|
||||
<option <?php if ($use_case == 'Documentation') { echo "selected"; } ?>>Documentation</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Temperature</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-thermometer-half"></i></span>
|
||||
<input type="number" class="form-control" name="temperature" step="0.1" min="0" max="2" value="<?= $temperature ?>" placeholder="Provider default">
|
||||
</div>
|
||||
<small class="form-text text-muted">Optional. Leave blank to let the provider use its default - some newer models reject every other value.</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control" rows="8" name="prompt" placeholder="Enter a model prompt:"><?= $prompt ?></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="edit_ai_model" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Save</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user