forked from GithubBackups/vichan
Compare commits
708 Commits
eolann2023
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
83b644f0f5 | ||
|
953635f807 | ||
|
5968118231 | ||
|
03da3b8db0 | ||
|
f27e5a6989 | ||
|
0866adc89f | ||
|
5259f88262 | ||
|
87420966b2 | ||
|
8350645a70 | ||
|
16a6112b5b | ||
|
9468c9d372 | ||
|
8aa965b77f | ||
|
b08dbcff14 | ||
|
f90291faf0 | ||
|
e67f3f94bf | ||
|
4244b0a86c | ||
|
ac3f69a7eb | ||
|
7bc1f67b4e | ||
|
54f32bc75d | ||
|
5f1598367c | ||
|
3987b2a25d | ||
|
6ec0c17ac9 | ||
|
2c2d476cea | ||
|
717795ddff | ||
|
d1e57290e0 | ||
|
0c92315837 | ||
|
c026e85b3c | ||
|
8d76087710 | ||
|
50d57a4464 | ||
|
96099945b9 | ||
|
8c6cf17a97 | ||
|
b3f0658594 | ||
|
33efeda647 | ||
|
92096b43b8 | ||
|
f3cb2552ce | ||
|
440e3126c9 | ||
|
47cbbbc972 | ||
|
74959aaed9 | ||
|
2d9e5009f8 | ||
|
372784ecd2 | ||
|
0f8a5fa926 | ||
|
0a870ebdb3 | ||
|
3b9b23035e | ||
|
40571f2001 | ||
|
c4cd4d3c12 | ||
|
00fc21322c | ||
|
6606c182b5 | ||
|
58c2f095dc | ||
|
8cd4cae56e | ||
|
762a0edefd | ||
|
c94ab113de | ||
|
32f0cb3a5f | ||
|
2466a3d859 | ||
|
f3f7c0c75c | ||
|
29ee5aeb1d | ||
|
6a22a51a72 | ||
|
a9059fab66 | ||
|
9d590eed2a | ||
|
e7aa695775 | ||
|
f419ae046c | ||
|
0ed3513e66 | ||
|
a376a5a2e3 | ||
|
b2ca26dba5 | ||
|
6f5b0ae6d5 | ||
|
5f2f653993 | ||
|
0ff0e707d6 | ||
|
26ffd1aa72 | ||
|
2e91c1ed3d | ||
|
efd4810e83 | ||
|
9dacdf59b1 | ||
|
b889b10626 | ||
|
b5a9dc4d1a | ||
|
d6677bb90c | ||
|
c88acfc4f0 | ||
|
58f7302936 | ||
|
76f6c721e9 | ||
|
c1307feeb5 | ||
|
e65bfa87c4 | ||
|
b67ff982e2 | ||
|
fba88643ec | ||
|
ad1d56d092 | ||
|
ceccbfc5b7 | ||
|
b8c53fbbcd | ||
|
27e4bd833a | ||
|
fe7a667441 | ||
|
115f28807a | ||
|
003e8f6d3b | ||
|
243e4894fa | ||
|
589435b667 | ||
|
f138b4b887 | ||
|
ace2f2e83b | ||
|
3d406aeab2 | ||
|
66d2f90171 | ||
|
5ea42fa0e2 | ||
|
b57d9bfbb3 | ||
|
82ea1815fd | ||
|
7c305f58bf | ||
|
59d0dd9083 | ||
|
f60b4d190f | ||
|
88a81a6d74 | ||
|
3fe44653f2 | ||
|
0e8909ac4e | ||
|
767b8fd8c3 | ||
|
f9127dd478 | ||
|
6c6ec65b02 | ||
|
23f3d15a52 | ||
|
d88d6c814a | ||
|
65a668d3a8 | ||
|
b501852ea4 | ||
|
927a837216 | ||
|
79af4b34dd | ||
|
36d48951c1 | ||
|
5dab17e5f4 | ||
|
c97c61aeca | ||
|
59551a2042 | ||
|
1682272f75 | ||
|
3eed312b6b | ||
|
cae85a6a0c | ||
|
25b2b6bc6e | ||
|
7377885de9 | ||
|
f421e25e63 | ||
|
3e9ad58e97 | ||
|
f4ff39c876 | ||
|
e6133ef00f | ||
|
13ca053e06 | ||
|
44b31eff0b | ||
|
785643a3bd | ||
|
e70c087f5f | ||
|
428a686f47 | ||
|
c95877bdcb | ||
|
380ae8c675 | ||
|
2f37b3ce51 | ||
|
65008dec98 | ||
|
0e8aeca4af | ||
|
bdd7090e75 | ||
|
fd309443ea | ||
|
4332b70363 | ||
|
cb6d6f13dd | ||
|
6b60f841d4 | ||
|
3c5484a7c2 | ||
|
fcf5c3d73a | ||
|
9fbc816205 | ||
|
bd5c2c61b9 | ||
|
c1788d0792 | ||
|
ccfcd03c95 | ||
|
e1a4ae5336 | ||
|
1effe1648b | ||
|
36737b77a8 | ||
|
a457b905bf | ||
|
3291dc27f9 | ||
|
8c27b5261c | ||
|
16bb704154 | ||
|
1db5c788dd | ||
|
2b3eae89f1 | ||
|
ce844b9270 | ||
|
43b926c41b | ||
|
eeb55133eb | ||
|
cd5c57f717 | ||
|
1672646213 | ||
|
6ea8fd5bf3 | ||
|
bf32a24b96 | ||
|
187f16693c | ||
|
13b587cf62 | ||
|
1191dfb193 | ||
|
5ee48c5865 | ||
|
60135bbb89 | ||
|
39876f3cc7 | ||
|
84a3bedd18 | ||
|
90dead0394 | ||
|
4f68166870 | ||
|
d408ed0413 | ||
|
453ae795f5 | ||
|
b2df2ab2a5 | ||
|
81aebef2f4 | ||
|
b64bac5eb8 | ||
|
524ae94624 | ||
|
19082aec56 | ||
|
e640217a8f | ||
|
165ea5308a | ||
|
f7073d5d7e | ||
|
cb5fb68c5e | ||
|
fb92e5fb68 | ||
|
a275d04efa | ||
|
933594194c | ||
|
e825e7aac5 | ||
|
d4b4cf5825 | ||
|
3e72171889 | ||
|
d8391eb34a | ||
|
e16dc142b7 | ||
|
420ec4a852 | ||
|
4d8a4db338 | ||
|
d1b06acbe9 | ||
|
80be41f47a | ||
|
f3e81c80d9 | ||
|
b3ae38da57 | ||
|
51e0616eb8 | ||
|
7e4acbb6d2 | ||
|
ffe855222e | ||
|
1e0a95ce83 | ||
|
c4e3541b15 | ||
|
ede7591702 | ||
|
c057c6df29 | ||
|
4d97e69620 | ||
|
e5bbdb9d28 | ||
|
cc5e96eb9d | ||
|
82b8eb1e74 | ||
|
2298d4433f | ||
|
accca93084 | ||
|
cbaf19cb7a | ||
|
75714505a0 | ||
|
36476f6341 | ||
|
980b2ef7bf | ||
|
e4707ee2a8 | ||
|
00cc1f434d | ||
|
ee20bf574a | ||
|
609da43548 | ||
|
0c074016e7 | ||
|
562ad74a12 | ||
|
73fce5e571 | ||
|
e5d423e595 | ||
|
230cc252c5 | ||
|
0fbf2f6f77 | ||
|
db20a350a1 | ||
|
7f45f31aa8 | ||
|
4445254b00 | ||
|
85b03c0fb0 | ||
|
d9a333a69f | ||
|
e92e9469a8 | ||
|
41f9aed606 | ||
|
5306f1d1f9 | ||
|
2749567c3f | ||
|
44e9a5aa57 | ||
|
5550bc4212 | ||
|
d9d05ddbf5 | ||
|
d9feb5cfa7 | ||
|
b7f46a239d | ||
|
2728966c1c | ||
|
ed46907a6c | ||
|
31444d654a | ||
|
6bea01b00b | ||
|
023e59d88f | ||
|
b822a76b23 | ||
|
902c558237 | ||
|
a018772267 | ||
|
fa341b29d0 | ||
|
6daae3ec92 | ||
|
da0f26485a | ||
|
8b773b124e | ||
|
c327a0439e | ||
|
4b5e40f575 | ||
|
eb67076e60 | ||
|
990f27e80b | ||
|
8d3bfedc72 | ||
|
d78f865645 | ||
|
283973c141 | ||
|
098edb9cd7 | ||
|
4bc69be4bc | ||
|
a20f618d80 | ||
|
d7468bb93b | ||
|
541f31f4d1 | ||
|
c223b1c55d | ||
|
409f571955 | ||
|
d23d1526e8 | ||
|
9a80ae2434 | ||
|
dba38b10d4 | ||
|
89a31794d9 | ||
|
1a780ce9cb | ||
|
fe8fa0da8a | ||
|
e12cbf6d80 | ||
|
0df33b4956 | ||
|
ef1939500c | ||
|
555d14b7ae | ||
|
281f391205 | ||
|
b21865853b | ||
|
5b0a7fb975 | ||
|
e9f1d59209 | ||
|
fee67b6719 | ||
|
fff9b88c6d | ||
|
8d37f1dd2c | ||
|
991ed657fb | ||
|
ad532d1d2d | ||
|
d2f1b7e0e0 | ||
|
2d6b599b26 | ||
|
bd7eb130ea | ||
|
f852172e9b | ||
|
809ab99c9b | ||
|
5c99c8395e | ||
|
878a67389a | ||
|
d990c344d4 | ||
|
88a48befd4 | ||
|
7e4dd5567b | ||
|
8963ebfce9 | ||
|
9236e10f37 | ||
|
9aaed32c57 | ||
|
055d31d2db | ||
|
de39780194 | ||
|
37658f1817 | ||
|
00be5e6ced | ||
|
605f198d8c | ||
|
3d797c95ca | ||
|
566b04f94e | ||
|
7899476323 | ||
|
d700aa0522 | ||
|
feb2860d9e | ||
|
9f46f0fdd4 | ||
|
ba2daa88e6 | ||
|
519dd27221 | ||
|
cc55563411 | ||
|
de91423a9f | ||
|
f9c54dbbbe | ||
|
273722dc7e | ||
|
091c0442e8 | ||
|
b2f246abb0 | ||
|
96bebe8c79 | ||
|
827373819f | ||
|
ffa5c018e7 | ||
|
1a59e663c6 | ||
|
010ab2bf62 | ||
|
faa43cb8a6 | ||
|
dd224cea58 | ||
|
4c731ba241 | ||
|
ed05049777 | ||
|
49d815c2ee | ||
|
e5dc2cd5ce | ||
|
44fd0dfb82 | ||
|
b90d6f5680 | ||
|
8170e226ec | ||
|
a7cb62349c | ||
|
21cbdfef04 | ||
|
ad653af082 | ||
|
2fa3b3c93e | ||
|
021e20f373 | ||
|
34a3dcac24 | ||
|
b1bdb1222a | ||
|
b5182a1864 | ||
|
82881fe512 | ||
|
b0e6580845 | ||
|
0dd064b2ea | ||
|
a7658fe3c2 | ||
|
ae72ed42ad | ||
|
12bcc63753 | ||
|
67fbd90f0d | ||
|
1b6d6f38f1 | ||
|
da4842eb7b | ||
|
0c51d46cdf | ||
|
9db8444c3c | ||
|
abdf82e1c8 | ||
|
5a378dd605 | ||
|
5bb1202def | ||
|
f6960b8b3a | ||
|
7041cd13df | ||
|
687e8e078a | ||
|
b7eed34b83 | ||
|
39ce0e7dfc | ||
|
6cd11d546f | ||
|
4674f44327 | ||
|
4cfb1ec576 | ||
|
1df9d2d84f | ||
|
51aa6e3813 | ||
|
af06d84094 | ||
|
ebbb5fce53 | ||
|
735180cf54 | ||
|
4bb0f40cd1 | ||
|
7025035376 | ||
|
85471e007e | ||
|
19fa804116 | ||
|
2836ace551 | ||
|
9d7ddd46c5 | ||
|
cbb1571063 | ||
|
0278ce16be | ||
|
004481b920 | ||
|
73a83e5dc9 | ||
|
a19e4cc755 | ||
|
3175f3627d | ||
|
df07515397 | ||
|
0428c35684 | ||
|
b86b9e375d | ||
|
c3619c49fb | ||
|
024f955338 | ||
|
bf4f388a04 | ||
|
f161de3d57 | ||
|
107592f70c | ||
|
fbbdb5afd6 | ||
|
5c99b0f4f3 | ||
|
fb191a0ffd | ||
|
575b265c74 | ||
|
050bc59588 | ||
|
f8ea32376e | ||
|
5a5d315330 | ||
|
729219a3c4 | ||
|
d117619ce6 | ||
|
5bdbe49f38 | ||
|
711e824153 | ||
|
9d9804db13 | ||
|
79183ae8e6 | ||
|
eb01768191 | ||
|
c058ec12f9 | ||
|
1fed05c5ee | ||
|
2af07d006b | ||
|
8799c142b0 | ||
|
3de9fa24dd | ||
|
efdf93e3dd | ||
|
7aca69125f | ||
|
b1b28dcb90 | ||
|
bd3bf7e4f8 | ||
|
7979404c1b | ||
|
d55961995c | ||
|
482962844a | ||
|
cc8e458906 | ||
|
1d385ad6aa | ||
|
62f0e9894c | ||
|
7ac0460b0a | ||
|
460aee0dad | ||
|
03228eb87d | ||
|
f3c7bad9cd | ||
|
fc63de4ed4 | ||
|
5ca2d194b4 | ||
|
90235b2bab | ||
|
3a769b437f | ||
|
8389c399bd | ||
|
73f2e3222b | ||
|
a83bcf14a4 | ||
|
f93dd1fae5 | ||
|
fc716fd0a8 | ||
|
3c49645af0 | ||
|
1109ef3ba6 | ||
|
710f6aa6c2 | ||
|
55cb6bc400 | ||
|
f05c290b67 | ||
|
28395d55e5 | ||
|
4d9d68b550 | ||
|
5680d73f08 | ||
|
eaacf27199 | ||
|
6ceee8261e | ||
|
2bb1b0b9d4 | ||
|
9072ce5992 | ||
|
98650ec2e7 | ||
|
99d55ca2b5 | ||
|
59b763fd53 | ||
|
429ae8e352 | ||
|
e811a9deeb | ||
|
c91c58ed07 | ||
|
99133c90fa | ||
|
4ffe91e384 | ||
|
1fa60e7386 | ||
|
7479360aad | ||
|
9a014e8557 | ||
|
542c9f3342 | ||
|
f47332cdff | ||
|
09dc44ec40 | ||
|
e61ed35aa0 | ||
|
55034762b0 | ||
|
760431606d | ||
|
00b05099f3 | ||
|
8120c42440 | ||
|
650ef8bcc2 | ||
|
3016d69428 | ||
|
c3de90075e | ||
|
03a875f663 | ||
|
74c2aec338 | ||
|
95153233eb | ||
|
be3437e452 | ||
|
67475c2c2f | ||
|
4094437464 | ||
|
d0c0d272af | ||
|
2fa28510b9 | ||
|
24708dec7e | ||
|
06ea4898d4 | ||
|
f5328a436c | ||
|
3d2f0b6a1b | ||
|
45f476084e | ||
|
38b47a0844 | ||
|
afe7c73195 | ||
|
55984b4260 | ||
|
b10ca57da1 | ||
|
c5ca05185f | ||
|
d4bcc34d6d | ||
|
b739a8e846 | ||
|
018da09ac5 | ||
|
3e3fa057d5 | ||
|
67b88ec78f | ||
|
a2eb3d9053 | ||
|
27ddd5158d | ||
|
e455080d42 | ||
|
0cefa9353b | ||
|
af8240e166 | ||
|
dc903017ca | ||
|
819ce71c55 | ||
|
2cad97775a | ||
|
c20b0ba6f0 | ||
|
3dc239e204 | ||
|
65cebe9bd5 | ||
|
2a228e8ee4 | ||
|
0c3159673d | ||
|
70410e5e61 | ||
|
32d3573e56 | ||
|
3c23e01dab | ||
|
44684496c7 | ||
|
2a8b69fa77 | ||
|
61cdca7207 | ||
|
d5109426d9 | ||
|
e80549a881 | ||
|
caa05e41a2 | ||
|
43973862dd | ||
|
3ac86a074a | ||
|
9841102494 | ||
|
f48cf068d6 | ||
|
954bb08762 | ||
|
28df28de26 | ||
|
33534b5856 | ||
|
e3ae4aee80 | ||
|
032a6f42e3 | ||
|
e2d771239c | ||
|
6dabfa08bc | ||
|
fff3e11c48 | ||
|
b25cffe12e | ||
|
0296301fff | ||
|
e855d5213b | ||
|
ab06e62266 | ||
|
9aba8f35b6 | ||
|
4e707ab9ae | ||
|
2f88d0809f | ||
|
0154e54f85 | ||
|
8cebc7f80c | ||
|
575cb856a5 | ||
|
6715991cdd | ||
|
58134f8cde | ||
|
f683c72c39 | ||
|
557b183d36 | ||
|
9ae988dd6f | ||
|
8978bb5442 | ||
|
4439f1736a | ||
|
59c9e1771a | ||
|
1ed75a58c2 | ||
|
f7a8cb258f | ||
|
f317691dcd | ||
|
350060e7a6 | ||
|
7e19026be0 | ||
|
1b539ae40e | ||
|
3f529db5e4 | ||
|
0c6f7c8237 | ||
|
492da02b53 | ||
|
3d05ba0247 | ||
|
543d5e1757 | ||
|
4bda535c66 | ||
|
cb7a6ecf45 | ||
|
db7d51670e | ||
|
24ebabb976 | ||
|
dd5c25f99c | ||
|
e52c996a56 | ||
|
421e7ff283 | ||
|
2a0c495b59 | ||
|
acc284004f | ||
|
23670bae4f | ||
|
6cf8c393b5 | ||
|
fde425111c | ||
|
77bec421d9 | ||
|
0e2795d355 | ||
|
b35990e1cd | ||
|
fb3843efd5 | ||
|
5867963747 | ||
|
ec515621eb | ||
|
4bd3fdb8a9 | ||
|
1b33080214 | ||
|
3baa68c7b6 | ||
|
d3cd289511 | ||
|
c43229af8e | ||
|
d02c4eb31d | ||
|
15cce3a31f | ||
|
f45bc768fe | ||
|
2ab6a3919b | ||
|
8dec1f4798 | ||
|
6af53795f5 | ||
|
e80cc95858 | ||
|
d057c6d660 | ||
|
fe9573a385 | ||
|
f3c6093bf6 | ||
|
fa6e240c1c | ||
|
bc05223362 | ||
|
4545d33b74 | ||
|
e9ed9acd30 | ||
|
c9b05e5bfc | ||
|
2ade0b0c8a | ||
|
e5d6960e4a | ||
|
5e26d5f09e | ||
|
f396204e44 | ||
|
ad3731ec67 | ||
|
cca659bd1f | ||
|
9c34ee778a | ||
|
eda383cbb3 | ||
|
c3c7bc4fc3 | ||
|
fbc2464c96 | ||
|
1b14830dfd | ||
|
2094155b6c | ||
|
997babeee0 | ||
|
503e4e39d0 | ||
|
42a473ae8b | ||
|
063275c4c8 | ||
|
237c5c4d4b | ||
|
dd5d8830d7 | ||
|
b8190dfd06 | ||
|
4d3ca11b91 | ||
|
ea08c250fa | ||
|
4a151acb18 | ||
|
285bb035da | ||
|
29e02b705a | ||
|
b79731c548 | ||
|
fb8f1d04d0 | ||
|
bee2d07499 | ||
|
aa0f6607cf | ||
|
14ae2d7d06 | ||
|
3413e74f24 | ||
|
9f7205a27e | ||
|
3b659732d6 | ||
|
b22b56d58f | ||
|
a0da06b46b | ||
|
dfbf5df0a3 | ||
|
2ef0d2796c | ||
|
70b5c80cea | ||
|
1a5b3c8480 | ||
|
ab2d29c8d1 | ||
|
af6bb09031 | ||
|
7feb101bbf | ||
|
5ef004c7ad | ||
|
715e73ae90 | ||
|
972bc855b8 | ||
|
899cfe1aaa | ||
|
485862acaa | ||
|
f48872d5a4 | ||
|
694351baa8 | ||
|
e2a42c67f6 | ||
|
ed627325ad | ||
|
33397fa1cf | ||
|
3c2f1800ec | ||
|
eaf7c83e15 | ||
|
e912ee24ec | ||
|
5df3ae6606 | ||
|
728476ce99 | ||
|
0dd904aec5 | ||
|
744b025137 | ||
|
691ce02cbd | ||
|
222de51247 | ||
|
0470064ea6 | ||
|
07f998f9eb | ||
|
d8732d3d30 | ||
|
1a39025162 | ||
|
fbee2dfbe0 | ||
|
81d56bb784 | ||
|
2cd1ea4095 | ||
|
39f4f5cd0b | ||
|
6a13e1053d | ||
|
1504a30e45 | ||
|
7d95959735 | ||
|
b112201d2b | ||
|
0799b5399b | ||
|
ec8bb62764 | ||
|
5d1d52295a | ||
|
77f4684a6b | ||
|
bb59e907b1 | ||
|
727e8723f0 | ||
|
d04e0d2caa | ||
|
1db892fa6d | ||
|
bdfd967554 | ||
|
ef32749986 | ||
|
d636076597 | ||
|
380c3ba000 | ||
|
a2d9493602 | ||
|
639785cd43 | ||
|
d1d324fc81 | ||
|
b0445d8b61 | ||
|
144f0e1a3d | ||
|
d1724d0da6 | ||
|
66be6de2fd | ||
|
8012cc5f05 | ||
|
417406d970 | ||
|
f926d6c399 | ||
|
aee8288942 | ||
|
b3c6896e1b | ||
|
d825c36b8d | ||
|
f8c8d57d64 | ||
|
87f99715c9 | ||
|
c2eaa69013 | ||
|
b1e428bed7 | ||
|
b6c8b05cf7 | ||
|
05851f950e | ||
|
76c5f77932 | ||
|
ddae472dd1 | ||
|
db68297f58 | ||
|
4e383426bf | ||
|
fc00b08c80 | ||
|
427f3f8a0a | ||
|
d2bb450b1a | ||
|
2c847a6413 | ||
|
90bf50fa30 | ||
|
08c9929eb0 | ||
|
19c15325b7 | ||
|
448d506135 | ||
|
a93a2dc869 | ||
|
573dfa84e3 | ||
|
f9db8e05de | ||
|
dc7668df64 | ||
|
686dc80644 | ||
|
95d73ae6f9 | ||
|
324e2f6af4 | ||
|
e0e8ea50a9 | ||
|
f3f6d2aa2d | ||
|
9d460d5b0a | ||
|
5d31f3bab7 |
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
||||
**/.git
|
||||
**/.gitignore
|
||||
/local-instances
|
||||
**/.gitkeep
|
69
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
69
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
name: Bug Report
|
||||
description: File a bug report for Vichan
|
||||
title: "[BUG] "
|
||||
labels: ["bug"]
|
||||
assignees: []
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Thank you for reporting a bug! Please provide as much detail as possible.**
|
||||
|
||||
Before submitting, check the [Vichan Wiki](https://vichan.info) to see if there's already a solution to your problem.
|
||||
|
||||
- type: textarea
|
||||
id: bug_description
|
||||
attributes:
|
||||
label: "Describe the bug"
|
||||
description: "A clear and concise description of what the bug is."
|
||||
placeholder: "Posting doesn't go through and displays a collation error. The exact error message given is the text below and I've attached a screenshot..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps_to_reproduce
|
||||
attributes:
|
||||
label: "Steps to Reproduce"
|
||||
description: "Provide step-by-step instructions to reproduce the issue. If you're unsure on how, that is alright, just try and explain as well as you can."
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected_behavior
|
||||
attributes:
|
||||
label: "Expected Behavior"
|
||||
description: "What did you expect to happen?"
|
||||
placeholder: "Expected behavior here..."
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: server_specs
|
||||
attributes:
|
||||
label: "Server Specifications"
|
||||
description: "Provide details about your server environment. If you're unsure about any of this, you might be using shared hosting (Hostinger, HostGator, Serv00, etc). If so, put the name of your hosting provider here."
|
||||
placeholder: |
|
||||
- OS: (Ubuntu, CentOS, Windows Server 2025, etc.)
|
||||
- PHP Version: (e.g., 7.4, 8.0, 8.4)
|
||||
- Web Server: (Apache, NGINX, etc.)
|
||||
- Database: (MySQL, MariaDB, etc.)
|
||||
- Vichan Version: (5.2.0, 5.3.0 (dev branch), etc)
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional_context
|
||||
attributes:
|
||||
label: "Additional Context"
|
||||
description: "Any other details we should know?"
|
||||
placeholder: "Add any additional context here..."
|
||||
render: markdown
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
1
vichan/.gitignore → .gitignore
vendored
1
vichan/.gitignore → .gitignore
vendored
@ -44,5 +44,6 @@ Thumbs.db
|
||||
#vichan custom
|
||||
favicon.ico
|
||||
/static/spoiler.png
|
||||
/local-instances
|
||||
|
||||
/vendor/
|
5
vichan/.gitmodules → .gitmodules
vendored
5
vichan/.gitmodules → .gitmodules
vendored
@ -1,5 +1,5 @@
|
||||
[submodule "js/wPaint"]
|
||||
path = vichan/js/wPaint
|
||||
path = js/wPaint
|
||||
url = https://github.com/vichan-devel/wPaint.git
|
||||
branch = master
|
||||
|
||||
@ -7,3 +7,6 @@
|
||||
path = inc/lib/parsedown
|
||||
url = https://github.com/vichan-devel/parsedown.git
|
||||
branch = master
|
||||
[submodule "js/twemoji"]
|
||||
path = js/twemoji
|
||||
url = https://github.com/basedgentoo/twemoji
|
135
README.md
135
README.md
@ -1,7 +1,134 @@
|
||||
**Vichan has reached END OF LIFE (EOL). See [#543](https://github.com/vichan-devel/vichan/issues/543). It will receive NO FURTHER PATCHES, neither for security or anything else, unless the situation therein described substantially changes. Thank you for your years of interest, but this horse is dead. ~ Fredrick R. Brennan <<copypaste@kittens.ph>>**
|
||||
vichan - A lightweight and full featured PHP imageboard.
|
||||
========================================================
|
||||
|
||||
To see this repository as it appeared before Vichan reached EOL, please see commit `d4e25308ad784c0bdc7acfba879b094f46de9807`. It is tagged `final-pre-eol`.
|
||||
**Please do not contact Fredrick Brennan in regards to vichan issues.**
|
||||
|
||||
# oops i eated it
|
||||
As of 29 August 2022 it supports PHP8.1.
|
||||
|
||||

|
||||
About
|
||||
------------
|
||||
vichan is a free light-weight, fast, highly configurable and user-friendly
|
||||
imageboard software package. It is written in PHP and has few dependencies.
|
||||
|
||||
Some documentation may be found on our [wiki](https://github.com/vichan-devel/vichan/wiki). (feel free to contribute)
|
||||
|
||||
History
|
||||
------------
|
||||
vichan is a fork of (now defunc'd) [Tinyboard](http://github.com/savetheinternet/Tinyboard),
|
||||
a great imageboard package, actively building on it and adding a lot of features and other
|
||||
improvements.
|
||||
|
||||

|
||||
|
||||
### Maintainer timeline
|
||||
1. [@perdedora](https://github.com/perdedora) and [@RealAngeleno](https://github.com/RealAngeleno) - 2023-Present.
|
||||
2. Development Commission lead by [@basedgentoo](https://github.com/basedgentoo), [@kuz-sysadmin](https://github.com/kuz-sysadmin), and [@RealAngeleno](https://github.com/RealAngeleno). (2023 - 2023)
|
||||
3. [@h00j](https://github.com/h00j) (2021 - ???)
|
||||
4. [@ctrlcctrlv](https://github.com/ctrlcctrlv) (2017 - 2021)
|
||||
5. [@czaks](https://github.com/czaks) (2014 - 2017) (The author of vichan fork)
|
||||
6. [@savetheinternet](https://github.com/savetheinternet) (2010 - 2014) (The creator of Tinyboard)
|
||||
|
||||
Requirements
|
||||
------------
|
||||
1. PHP >= 7.4
|
||||
2. MySQL/MariaDB server
|
||||
3. [mbstring](http://www.php.net/manual/en/mbstring.installation.php)
|
||||
4. [PHP GD](http://www.php.net/manual/en/intro.image.php)
|
||||
5. [PHP PDO](http://www.php.net/manual/en/intro.pdo.php)
|
||||
6. A Unix-like OS, preferrably FreeBSD or GNU/Linux
|
||||
|
||||
We try to make sure vichan is compatible with all major web servers. vichan does not include an Apache `.htaccess` file nor does it need one.
|
||||
|
||||
### Recommended
|
||||
1. MySQL/MariaDB server >= 5.5.3
|
||||
2. ImageMagick (command-line ImageMagick or GraphicsMagick preferred).
|
||||
3. [APCu (Alternative PHP Cache)](http://php.net/manual/en/book.apcu.php),
|
||||
[Memcached](http://www.php.net/manual/en/intro.memcached.php) or
|
||||
[Redis](https://redis.io/docs/about/)
|
||||
|
||||
Contributing
|
||||
------------
|
||||
You can contribute to vichan by:
|
||||
* Developing patches/improvements/translations and using GitHub to submit pull requests
|
||||
* Providing feedback and suggestions
|
||||
* Writing/editing documentation
|
||||
|
||||
Installation
|
||||
-------------
|
||||
1. Get the latest development version with:
|
||||
|
||||
git clone git://github.com/vichan-devel/vichan.git
|
||||
|
||||
2. run ```composer install``` inside the directory
|
||||
3. Navigate to ```install.php``` in your web browser and follow the
|
||||
prompts.
|
||||
4. vichan should now be installed. Log in to ```mod.php``` with the
|
||||
default username and password combination: **admin / password**.
|
||||
|
||||
Please remember to change the administrator account password.
|
||||
|
||||
See also: [Configuration Basics](https://github.com/vichan-devel/vichan/wiki/config).
|
||||
|
||||
Upgrade
|
||||
-------
|
||||
To upgrade from any version of Tinyboard or vichan:
|
||||
|
||||
Either run ```git pull``` to update your files, if you used git, or
|
||||
backup your ```inc/instance-config.php```, replace all your files in place
|
||||
(don't remove boards etc.), then put ```inc/instance-config.php``` back and
|
||||
finally run ```install.php```.
|
||||
|
||||
To migrate from a Kusaba X board, use http://github.com/vichan-devel/Tinyboard-Migration
|
||||
|
||||
Demo
|
||||
--------
|
||||
Demo with the most updated version of [Vichan](https://vichan.27chan.org).
|
||||
|
||||
1. PHP 8.1
|
||||
2. MySQL 5.7
|
||||
3. KeyDB 6.2.1 (Redis)
|
||||
4. NGINX 1.14.0
|
||||
|
||||
Support
|
||||
--------
|
||||
vichan is still beta software -- there are bound to be bugs. If you find a
|
||||
bug, please report it.
|
||||
|
||||
CLI tools
|
||||
-----------------
|
||||
There are a few command line interface tools, based on Tinyboard-Tools. These need
|
||||
to be launched from a Unix shell account (SSH, or something). They are located in a ```tools/```
|
||||
directory.
|
||||
|
||||
You actually don't need these tools for your imageboard functioning, they are aimed
|
||||
at the power users. You won't be able to run these from shared hosting accounts
|
||||
(i.e. all free web servers).
|
||||
|
||||
Oekaki
|
||||
------
|
||||
vichan makes use of [wPaint](https://github.com/websanova/wPaint) for oekaki. After you pull the repository, however, you will need to download wPaint separately using git's `submodule` feature. Use the following commands:
|
||||
|
||||
```
|
||||
git submodule init
|
||||
git submodule update
|
||||
```
|
||||
|
||||
To enable oekaki, add all the scripts listed in `js/wpaint.js` to your `instance-config.php`.
|
||||
|
||||
WebM support
|
||||
------------
|
||||
Read `inc/lib/webm/README.md` for information about enabling webm.
|
||||
|
||||
Docker
|
||||
------------
|
||||
Vichan comes with a Dockerfile and docker-compose configuration, the latter aimed primarily at development and testing.
|
||||
See the `docker/doc.md` file for more information.
|
||||
|
||||
vichan API
|
||||
----------
|
||||
vichan provides by default a 4chan-compatible JSON API. For documentation on this, see:
|
||||
https://github.com/vichan-devel/vichan-API/ .
|
||||
|
||||
License
|
||||
--------
|
||||
See [LICENSE.md](http://github.com/vichan-devel/vichan/blob/master/LICENSE.md).
|
||||
|
8
b.php
Normal file
8
b.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
$files = scandir('static/banners/', SCANDIR_SORT_NONE);
|
||||
$files = array_diff($files, ['.', '..']);
|
||||
|
||||
$name = $files[array_rand($files)];
|
||||
header("Location: /static/banners/$name", true, 307);
|
||||
header('Cache-Control: no-cache');
|
40
compose.yml
Normal file
40
compose.yml
Normal file
@ -0,0 +1,40 @@
|
||||
services:
|
||||
#nginx webserver + php 8.x
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./docker/nginx/Dockerfile
|
||||
ports:
|
||||
- "9090:80"
|
||||
depends_on:
|
||||
- db
|
||||
volumes:
|
||||
- ./local-instances/${INSTANCE:-0}/www:/var/www/html
|
||||
- ./docker/nginx/vichan.conf:/etc/nginx/conf.d/default.conf
|
||||
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./docker/nginx/proxy.conf:/etc/nginx/conf.d/proxy.conf
|
||||
links:
|
||||
- php
|
||||
|
||||
php:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./docker/php/Dockerfile
|
||||
volumes:
|
||||
- ./local-instances/${INSTANCE:-0}/www:/var/www
|
||||
- ./docker/php/www.conf:/usr/local/etc/php-fpm.d/www.conf
|
||||
- ./docker/php/jit.ini:/usr/local/etc/php/conf.d/jit.ini
|
||||
|
||||
#MySQL Service
|
||||
db:
|
||||
image: mysql:8.0.35
|
||||
container_name: db
|
||||
restart: unless-stopped
|
||||
tty: true
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
MYSQL_DATABASE: vichan
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
volumes:
|
||||
- ./local-instances/${INSTANCE:-0}/mysql:/var/lib/mysql
|
@ -2,16 +2,24 @@
|
||||
"name": "vichan-devel/vichan",
|
||||
"description": "vichan imageboard",
|
||||
"type": "project",
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.4"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": ">=5.4",
|
||||
"ext-gd": ">=5.4",
|
||||
"ext-pdo": ">=5.4",
|
||||
"twig/twig": "^1.44.2",
|
||||
"php": ">=7.4",
|
||||
"ext-mbstring": ">=7.4",
|
||||
"ext-gd": ">=7.4",
|
||||
"ext-pdo": ">=7.4",
|
||||
"twig/twig": "^2.9",
|
||||
"phpmyadmin/twig-i18n-extension": "^4.0",
|
||||
"lifo/ip": "^1.0",
|
||||
"gettext/gettext": "^1.0",
|
||||
"gettext/gettext": "^5.5",
|
||||
"mrclay/minify": "^2.1.6",
|
||||
"geoip/geoip": "^1.17",
|
||||
"dapphp/securimage": "^4.0"
|
||||
"dapphp/securimage": "^4.0",
|
||||
"erusev/parsedown": "^1.7.4"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": ["inc/"],
|
||||
@ -25,8 +33,14 @@
|
||||
"inc/mod/auth.php",
|
||||
"inc/lock.php",
|
||||
"inc/queue.php",
|
||||
"inc/polyfill.php",
|
||||
"inc/functions.php"
|
||||
"inc/functions.php",
|
||||
"inc/functions/dice.php",
|
||||
"inc/functions/format.php",
|
||||
"inc/functions/net.php",
|
||||
"inc/functions/num.php",
|
||||
"inc/functions/theme.php",
|
||||
"inc/service/captcha-queries.php",
|
||||
"inc/context.php"
|
||||
]
|
||||
},
|
||||
"license": "Tinyboard + vichan",
|
432
vichan/composer.lock → composer.lock
generated
432
vichan/composer.lock → composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "1e3723687369c82eea457d2dded76b74",
|
||||
"content-hash": "72e79f203581eea6e6b0455147b25878",
|
||||
"packages": [
|
||||
{
|
||||
"name": "dapphp/securimage",
|
||||
@ -31,12 +31,12 @@
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"securimage.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Securimage\\": "./"
|
||||
}
|
||||
},
|
||||
"classmap": [
|
||||
"securimage.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
@ -114,37 +114,43 @@
|
||||
"issues": "https://github.com/maxmind/geoip-api-php/issues",
|
||||
"source": "https://github.com/maxmind/geoip-api-php/tree/master"
|
||||
},
|
||||
"abandoned": "geoip2/geoip2",
|
||||
"time": "2016-05-16T19:06:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "gettext/gettext",
|
||||
"version": "v1.1.5",
|
||||
"version": "v5.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-gettext/Gettext.git",
|
||||
"reference": "1bdf755a1b49f0614d6fc29f446df567eb62cd5c"
|
||||
"reference": "8657e580747bb3baacccdcebe69cac094661e404"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-gettext/Gettext/zipball/1bdf755a1b49f0614d6fc29f446df567eb62cd5c",
|
||||
"reference": "1bdf755a1b49f0614d6fc29f446df567eb62cd5c",
|
||||
"url": "https://api.github.com/repos/php-gettext/Gettext/zipball/8657e580747bb3baacccdcebe69cac094661e404",
|
||||
"reference": "8657e580747bb3baacccdcebe69cac094661e404",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
"gettext/languages": "^2.3",
|
||||
"php": "^7.2|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"brick/varexporter": "^0.3.5",
|
||||
"friendsofphp/php-cs-fixer": "^3.2",
|
||||
"oscarotero/php-cs-fixer-config": "^2.0",
|
||||
"phpunit/phpunit": "^8.0|^9.0",
|
||||
"squizlabs/php_codesniffer": "^3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Gettext": ""
|
||||
},
|
||||
"files": [
|
||||
"Gettext/translator_functions.php"
|
||||
]
|
||||
"psr-4": {
|
||||
"Gettext\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"AGPL-3.0"
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
@ -154,33 +160,123 @@
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "PHP - JS gettext conversor",
|
||||
"homepage": "https://github.com/oscarotero/Gettext",
|
||||
"description": "PHP gettext manager",
|
||||
"homepage": "https://github.com/php-gettext/Gettext",
|
||||
"keywords": [
|
||||
"JS",
|
||||
"gettext",
|
||||
"i18n",
|
||||
"mo",
|
||||
"po",
|
||||
"translation"
|
||||
],
|
||||
"support": {
|
||||
"email": "oom@oscarotero.com",
|
||||
"issues": "https://github.com/oscarotero/Gettext/issues",
|
||||
"source": "https://github.com/php-gettext/Gettext/tree/v1.1.5"
|
||||
"issues": "https://github.com/php-gettext/Gettext/issues",
|
||||
"source": "https://github.com/php-gettext/Gettext/tree/v5.7.0"
|
||||
},
|
||||
"time": "2014-10-22T15:53:45+00:00"
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://paypal.me/oscarotero",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/oscarotero",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://www.patreon.com/misteroom",
|
||||
"type": "patreon"
|
||||
}
|
||||
],
|
||||
"time": "2022-07-27T19:54:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "lifo/ip",
|
||||
"version": "v1.1",
|
||||
"name": "gettext/languages",
|
||||
"version": "2.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lifo101/ip.git",
|
||||
"reference": "b6a36dab288d7aea155698808bfc6649799fe413"
|
||||
"url": "https://github.com/php-gettext/Languages.git",
|
||||
"reference": "4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/lifo101/ip/zipball/b6a36dab288d7aea155698808bfc6649799fe413",
|
||||
"reference": "b6a36dab288d7aea155698808bfc6649799fe413",
|
||||
"url": "https://api.github.com/repos/php-gettext/Languages/zipball/4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab",
|
||||
"reference": "4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.4"
|
||||
},
|
||||
"bin": [
|
||||
"bin/export-plural-rules"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Gettext\\Languages\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michele Locati",
|
||||
"email": "mlocati@gmail.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "gettext languages with plural rules",
|
||||
"homepage": "https://github.com/php-gettext/Languages",
|
||||
"keywords": [
|
||||
"cldr",
|
||||
"i18n",
|
||||
"internationalization",
|
||||
"l10n",
|
||||
"language",
|
||||
"languages",
|
||||
"localization",
|
||||
"php",
|
||||
"plural",
|
||||
"plural rules",
|
||||
"plurals",
|
||||
"translate",
|
||||
"translations",
|
||||
"unicode"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-gettext/Languages/issues",
|
||||
"source": "https://github.com/php-gettext/Languages/tree/2.10.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://paypal.me/mlocati",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/mlocati",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2022-10-18T15:00:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "lifo/ip",
|
||||
"version": "v1.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lifo101/ip.git",
|
||||
"reference": "4c4cf5b554884be93f1d0422eaec8d6426993229"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/lifo101/ip/zipball/4c4cf5b554884be93f1d0422eaec8d6426993229",
|
||||
"reference": "4c4cf5b554884be93f1d0422eaec8d6426993229",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -212,9 +308,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/lifo101/ip/issues",
|
||||
"source": "https://github.com/lifo101/ip/tree/master"
|
||||
"source": "https://github.com/lifo101/ip/tree/v1.1.1"
|
||||
},
|
||||
"time": "2020-04-02T11:09:10+00:00"
|
||||
"time": "2022-07-12T15:45:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mrclay/minify",
|
||||
@ -268,29 +364,88 @@
|
||||
"time": "2017-11-03T21:04:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.23.0",
|
||||
"name": "phpmyadmin/twig-i18n-extension",
|
||||
"version": "v4.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce"
|
||||
"url": "https://github.com/phpmyadmin/twig-i18n-extension.git",
|
||||
"reference": "c0d0dd171cd1c7733bf152fd44b61055843df052"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce",
|
||||
"reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce",
|
||||
"url": "https://api.github.com/repos/phpmyadmin/twig-i18n-extension/zipball/c0d0dd171cd1c7733bf152fd44b61055843df052",
|
||||
"reference": "c0d0dd171cd1c7733bf152fd44b61055843df052",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0",
|
||||
"twig/twig": "^1.42.3|^2.0|^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpmyadmin/coding-standard": "^3.0.0",
|
||||
"phpmyadmin/motranslator": "^5.2",
|
||||
"phpstan/phpstan": "^0.12.66",
|
||||
"phpunit/phpunit": "^7 || ^8 || ^9"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PhpMyAdmin\\Twig\\Extensions\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "The phpMyAdmin Team",
|
||||
"email": "developers@phpmyadmin.net",
|
||||
"homepage": "https://www.phpmyadmin.net/team/"
|
||||
}
|
||||
],
|
||||
"description": "Internationalization support for Twig via the gettext library",
|
||||
"keywords": [
|
||||
"gettext",
|
||||
"i18n"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpmyadmin/twig-i18n-extension/issues",
|
||||
"source": "https://github.com/phpmyadmin/twig-i18n-extension"
|
||||
},
|
||||
"time": "2021-06-10T15:53:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.27.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a",
|
||||
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"provide": {
|
||||
"ext-ctype": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-ctype": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.23-dev"
|
||||
"dev-main": "1.27-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
@ -298,12 +453,12 @@
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Ctype\\": ""
|
||||
},
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
]
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Ctype\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
@ -328,7 +483,7 @@
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.0"
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -344,34 +499,195 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2021-02-19T12:13:01+00:00"
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
"version": "v1.44.5",
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.27.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/Twig.git",
|
||||
"reference": "dd4353357c5a116322e92a00d16043a31881a81e"
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/dd4353357c5a116322e92a00d16043a31881a81e",
|
||||
"reference": "dd4353357c5a116322e92a00d16043a31881a81e",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
|
||||
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/polyfill-ctype": "^1.8"
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"psr/container": "^1.0",
|
||||
"symfony/phpunit-bridge": "^4.4.9|^5.0.9"
|
||||
"provide": {
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.44-dev"
|
||||
"dev-main": "1.27-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Mbstring\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for the Mbstring extension",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"mbstring",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php72",
|
||||
"version": "v1.27.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php72.git",
|
||||
"reference": "869329b1e9894268a8a61dabb69153029b7a8c97"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/869329b1e9894268a8a61dabb69153029b7a8c97",
|
||||
"reference": "869329b1e9894268a8a61dabb69153029b7a8c97",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Php72\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php72/tree/v1.27.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
"version": "v2.15.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/Twig.git",
|
||||
"reference": "3e059001d6d597dd50ea7c74dd2464b4adea48d3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/3e059001d6d597dd50ea7c74dd2464b4adea48d3",
|
||||
"reference": "3e059001d6d597dd50ea7c74dd2464b4adea48d3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1.3",
|
||||
"symfony/polyfill-ctype": "^1.8",
|
||||
"symfony/polyfill-mbstring": "^1.3",
|
||||
"symfony/polyfill-php72": "^1.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"psr/container": "^1.0",
|
||||
"symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.15-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@ -410,7 +726,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/twigphp/Twig/issues",
|
||||
"source": "https://github.com/twigphp/Twig/tree/v1.44.5"
|
||||
"source": "https://github.com/twigphp/Twig/tree/v2.15.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -422,7 +738,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2021-09-17T08:35:19+00:00"
|
||||
"time": "2022-12-27T12:26:20+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
@ -437,5 +753,5 @@
|
||||
"ext-pdo": ">=5.4"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.0.0"
|
||||
"plugin-api-version": "2.3.0"
|
||||
}
|
20
docker/doc.md
Normal file
20
docker/doc.md
Normal file
@ -0,0 +1,20 @@
|
||||
The `php-fpm` process runs containerized.
|
||||
The php application always uses `/var/www` as it's work directory and home folder, and if `/var/www` is bind mounted it
|
||||
is necessary to adjust the path passed via FastCGI to `php-fpm` by changing the root directory to `/var/www`.
|
||||
This can achieved in nginx by setting the `fastcgi_param SCRIPT_FILENAME` to `/var/www/$fastcgi_script_name;`
|
||||
|
||||
The default docker compose settings are intended for development and testing purposes.
|
||||
The folder structure expected by compose is as follows
|
||||
|
||||
```
|
||||
<vichan-project>
|
||||
└── local-instances
|
||||
└── 1
|
||||
├── mysql
|
||||
└── www
|
||||
```
|
||||
The vichan container is by itself much less rigid.
|
||||
|
||||
|
||||
Use `docker compose up --build` to start the docker compose.
|
||||
Use `docker compose up --build -d php` to rebuild just the vichan container while the compose is running. Useful for development.
|
8
docker/nginx/Dockerfile
Normal file
8
docker/nginx/Dockerfile
Normal file
@ -0,0 +1,8 @@
|
||||
FROM nginx:1.25.3-alpine
|
||||
|
||||
COPY . /code
|
||||
RUN adduser --system www-data \
|
||||
&& adduser www-data www-data
|
||||
|
||||
CMD [ "nginx", "-g", "daemon off;" ]
|
||||
EXPOSE 80
|
34
docker/nginx/nginx.conf
Normal file
34
docker/nginx/nginx.conf
Normal file
@ -0,0 +1,34 @@
|
||||
# This and proxy.conf are based on
|
||||
# https://github.com/dead-guru/devichan/blob/master/nginx/nginx.conf
|
||||
|
||||
user www-data;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /dev/stdout warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Switch logging to console out to view via Docker
|
||||
access_log /dev/stdout;
|
||||
error_log /dev/stdout warn;
|
||||
sendfile on;
|
||||
keepalive_timeout 5;
|
||||
|
||||
gzip on;
|
||||
gzip_http_version 1.0;
|
||||
gzip_vary on;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/xml text/plain text/css application/xhtml+xml application/xml application/rss+xml application/atom_xml application/x-javascript application/x-httpd-php;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/sites-available/*.conf;
|
||||
}
|
40
docker/nginx/proxy.conf
Normal file
40
docker/nginx/proxy.conf
Normal file
@ -0,0 +1,40 @@
|
||||
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=czone:4m max_size=50m inactive=120m;
|
||||
proxy_temp_path /var/tmp/nginx;
|
||||
proxy_cache_key "$scheme://$host$request_uri";
|
||||
|
||||
|
||||
map $http_forwarded_request_id $x_request_id {
|
||||
"" $request_id;
|
||||
default $http_forwarded_request_id;
|
||||
}
|
||||
|
||||
map $http_forwarded_forwarded_host $forwardedhost {
|
||||
"" $host;
|
||||
default $http_forwarded_forwarded_host;
|
||||
}
|
||||
|
||||
|
||||
map $http_x_forwarded_proto $fcgi_https {
|
||||
default "";
|
||||
https on;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_proto $real_scheme {
|
||||
default $scheme;
|
||||
https https;
|
||||
}
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Server $host;
|
||||
|
||||
real_ip_header X-Forwarded-For;
|
||||
|
||||
set_real_ip_from 10.0.0.0/8;
|
||||
set_real_ip_from 172.16.0.0/12;
|
||||
set_real_ip_from 172.18.0.0;
|
||||
set_real_ip_from 192.168.0.0/24;
|
||||
set_real_ip_from 127.0.0.0/8;
|
||||
|
||||
real_ip_recursive on;
|
66
docker/nginx/vichan.conf
Normal file
66
docker/nginx/vichan.conf
Normal file
@ -0,0 +1,66 @@
|
||||
upstream php-upstream {
|
||||
server php:9000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server ipv6only=on;
|
||||
server_name vichan;
|
||||
root /var/www/html;
|
||||
add_header X-Frame-Options "SAMEORIGIN";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
|
||||
index index.html index.php;
|
||||
|
||||
charset utf-8;
|
||||
|
||||
location ~ ^([^.\?]*[^\/])$ {
|
||||
try_files $uri @addslash;
|
||||
}
|
||||
|
||||
# Expire rules for static content
|
||||
# Media: images, icons, video, audio, HTC
|
||||
location ~* \.(?:jpg|jpeg|gif|png|webp|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
|
||||
expires 1M;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
# CSS and Javascript
|
||||
location ~* \.(?:css|js)$ {
|
||||
expires 1y;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
|
||||
location ~* \.(html)$ {
|
||||
expires -1;
|
||||
}
|
||||
|
||||
location @addslash {
|
||||
return 301 $uri/;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php$is_args$args;
|
||||
}
|
||||
|
||||
client_max_body_size 2G;
|
||||
|
||||
location ~ \.php$ {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Request-Id $x_request_id;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header Forwarded-Request-Id $x_request_id;
|
||||
fastcgi_pass php-upstream;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME /var/www/$fastcgi_script_name;
|
||||
fastcgi_read_timeout 600;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
location = /favicon.ico { access_log off; log_not_found off; }
|
||||
location = /robots.txt { access_log off; log_not_found off; }
|
||||
}
|
88
docker/php/Dockerfile
Normal file
88
docker/php/Dockerfile
Normal file
@ -0,0 +1,88 @@
|
||||
# Based on https://github.com/dead-guru/devichan/blob/master/php-fpm/Dockerfile
|
||||
|
||||
FROM composer:lts AS composer
|
||||
FROM php:8.1-fpm-alpine
|
||||
|
||||
RUN apk add --no-cache \
|
||||
zlib \
|
||||
zlib-dev \
|
||||
libpng \
|
||||
libpng-dev \
|
||||
libjpeg-turbo \
|
||||
libjpeg-turbo-dev \
|
||||
libwebp \
|
||||
libwebp-dev \
|
||||
libcurl \
|
||||
curl-dev \
|
||||
imagemagick \
|
||||
graphicsmagick \
|
||||
gifsicle \
|
||||
ffmpeg \
|
||||
bind-tools \
|
||||
gettext \
|
||||
gettext-dev \
|
||||
icu-dev \
|
||||
oniguruma \
|
||||
oniguruma-dev \
|
||||
libmcrypt \
|
||||
libmcrypt-dev \
|
||||
lz4-libs \
|
||||
lz4-dev \
|
||||
imagemagick-dev \
|
||||
pcre-dev \
|
||||
$PHPIZE_DEPS \
|
||||
&& docker-php-ext-configure gd \
|
||||
--with-webp=/usr/include/webp \
|
||||
--with-jpeg=/usr/include \
|
||||
&& docker-php-ext-install -j$(nproc) \
|
||||
gd \
|
||||
curl \
|
||||
bcmath \
|
||||
opcache \
|
||||
pdo_mysql \
|
||||
gettext \
|
||||
intl \
|
||||
mbstring \
|
||||
&& pecl update-channels \
|
||||
&& pecl install -o -f igbinary \
|
||||
&& pecl install redis \
|
||||
&& pecl install imagick \
|
||||
$$ docker-php-ext-enable \
|
||||
igbinary \
|
||||
redis \
|
||||
imagick \
|
||||
&& apk del \
|
||||
zlib-dev \
|
||||
libpng-dev \
|
||||
libjpeg-turbo-dev \
|
||||
libwebp-dev \
|
||||
curl-dev \
|
||||
gettext-dev \
|
||||
oniguruma-dev \
|
||||
libmcrypt-dev \
|
||||
lz4-dev \
|
||||
imagemagick-dev \
|
||||
pcre-dev \
|
||||
$PHPIZE_DEPS \
|
||||
&& rm -rf /var/cache/* \
|
||||
&& rm -rf /tmp/pear
|
||||
RUN rmdir /var/www/html \
|
||||
&& install -d -m 744 -o www-data -g www-data /var/www \
|
||||
&& install -d -m 700 -o www-data -g www-data /var/tmp/vichan \
|
||||
&& install -d -m 700 -o www-data -g www-data /var/cache/gen-cache \
|
||||
&& install -d -m 700 -o www-data -g www-data /var/cache/template-cache
|
||||
|
||||
# Copy the bootstrap script.
|
||||
COPY ./docker/php/bootstrap.sh /usr/local/bin/bootstrap.sh
|
||||
|
||||
COPY --from=composer /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
# Copy the actual project (use .dockerignore to exclude stuff).
|
||||
COPY . /code
|
||||
|
||||
# Install the compose depedencies.
|
||||
RUN cd /code && composer install
|
||||
|
||||
WORKDIR "/var/www"
|
||||
CMD [ "bootstrap.sh" ]
|
||||
EXPOSE 9000
|
16
docker/php/Dockerfile.profile
Normal file
16
docker/php/Dockerfile.profile
Normal file
@ -0,0 +1,16 @@
|
||||
# syntax = devthefuture/dockerfile-x
|
||||
INCLUDE ./docker/php/Dockerfile
|
||||
|
||||
RUN apk add --no-cache \
|
||||
linux-headers \
|
||||
$PHPIZE_DEPS \
|
||||
&& pecl update-channels \
|
||||
&& pecl install xdebug \
|
||||
&& docker-php-ext-enable xdebug \
|
||||
&& apk del \
|
||||
linux-headers \
|
||||
$PHPIZE_DEPS \
|
||||
&& rm -rf /var/cache/*
|
||||
|
||||
ENV XDEBUG_OUT_DIR=/var/www/xdebug_out
|
||||
CMD [ "bootstrap.sh" ]
|
87
docker/php/bootstrap.sh
Executable file
87
docker/php/bootstrap.sh
Executable file
@ -0,0 +1,87 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
function set_cfg() {
|
||||
if [ -L "/var/www/inc/$1" ]; then
|
||||
echo "INFO: Resetting $1"
|
||||
rm "/var/www/inc/$1"
|
||||
cp "/code/inc/$1" "/var/www/inc/$1"
|
||||
chown www-data "/var/www/inc/$1"
|
||||
chgrp www-data "/var/www/inc/$1"
|
||||
chmod 600 "/var/www/inc/$1"
|
||||
else
|
||||
echo "INFO: Using existing $1"
|
||||
fi
|
||||
}
|
||||
|
||||
if ! mountpoint -q /var/www; then
|
||||
echo "WARNING: '/var/www' is not a mountpoint. All the data will remain inside the container!"
|
||||
fi
|
||||
|
||||
if [ ! -w /var/www ] ; then
|
||||
echo "ERROR: '/var/www' is not writable. Closing."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${XDEBUG_OUT_DIR:-''}" ] ; then
|
||||
echo "INFO: Initializing xdebug out directory at $XDEBUG_OUT_DIR"
|
||||
mkdir -p "$XDEBUG_OUT_DIR"
|
||||
chown www-data "$XDEBUG_OUT_DIR"
|
||||
chgrp www-data "$XDEBUG_OUT_DIR"
|
||||
chmod 755 "$XDEBUG_OUT_DIR"
|
||||
fi
|
||||
|
||||
# Link the entrypoints from the exposed directory.
|
||||
ln -nfs \
|
||||
/code/tools/ \
|
||||
/code/*.php \
|
||||
/code/LICENSE.* \
|
||||
/code/install.sql \
|
||||
/var/www/
|
||||
# Static files accessible from the webserver must be copied.
|
||||
cp -ur /code/static /var/www/
|
||||
cp -ur /code/stylesheets /var/www/
|
||||
|
||||
# Ensure correct permissions are set, since this might be bind mount.
|
||||
chown www-data /var/www
|
||||
chgrp www-data /var/www
|
||||
|
||||
# Initialize an empty robots.txt with the default if it doesn't exist.
|
||||
touch /var/www/robots.txt
|
||||
|
||||
# Link the cache and tmp files directory.
|
||||
ln -nfs /var/tmp/vichan /var/www/tmp
|
||||
|
||||
# Link the javascript directory.
|
||||
ln -nfs /code/js /var/www/
|
||||
|
||||
# Link the html templates directory and it's cache.
|
||||
ln -nfs /code/templates /var/www/
|
||||
ln -nfs -T /var/cache/template-cache /var/www/templates/cache
|
||||
chown -h www-data /var/www/templates/cache
|
||||
chgrp -h www-data /var/www/templates/cache
|
||||
|
||||
# Link the generic cache.
|
||||
ln -nfs -T /var/cache/gen-cache /var/www/tmp/cache
|
||||
chown -h www-data /var/www/tmp/cache
|
||||
chgrp -h www-data /var/www/tmp/cache
|
||||
|
||||
# Create the included files directory and link them
|
||||
install -d -m 700 -o www-data -g www-data /var/www/inc
|
||||
for file in /code/inc/*; do
|
||||
file="${file##*/}"
|
||||
if [ ! -e /var/www/inc/$file ]; then
|
||||
ln -s /code/inc/$file /var/www/inc/
|
||||
fi
|
||||
done
|
||||
|
||||
# Copy an empty instance configuration if the file is a link (it was linked because it did not exist before).
|
||||
set_cfg 'instance-config.php'
|
||||
set_cfg 'secrets.php'
|
||||
|
||||
# Link the composer dependencies.
|
||||
ln -nfs /code/vendor /var/www/
|
||||
|
||||
# Start the php-fpm server.
|
||||
exec php-fpm
|
2
docker/php/jit.ini
Normal file
2
docker/php/jit.ini
Normal file
@ -0,0 +1,2 @@
|
||||
opcache.jit_buffer_size=192M
|
||||
opcache.jit=tracing
|
13
docker/php/www.conf
Normal file
13
docker/php/www.conf
Normal file
@ -0,0 +1,13 @@
|
||||
[www]
|
||||
access.log = /proc/self/fd/2
|
||||
|
||||
; Ensure worker stdout and stderr are sent to the main error log.
|
||||
catch_workers_output = yes
|
||||
decorate_workers_output = no
|
||||
|
||||
user = www-data
|
||||
group = www-data
|
||||
|
||||
listen = 127.0.0.1:9000
|
||||
pm = static
|
||||
pm.max_children = 16
|
7
docker/php/xdebug-prof.ini
Normal file
7
docker/php/xdebug-prof.ini
Normal file
@ -0,0 +1,7 @@
|
||||
zend_extension=xdebug
|
||||
|
||||
[xdebug]
|
||||
xdebug.mode = profile
|
||||
xdebug.start_with_request = start
|
||||
error_reporting = E_ALL
|
||||
xdebug.output_dir = /var/www/xdebug_out
|
28
inc/Data/Driver/ApcuCacheDriver.php
Normal file
28
inc/Data/Driver/ApcuCacheDriver.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
class ApcuCacheDriver implements CacheDriver {
|
||||
public function get(string $key): mixed {
|
||||
$success = false;
|
||||
$ret = \apcu_fetch($key, $success);
|
||||
if ($success === false) {
|
||||
return null;
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||
\apcu_store($key, $value, (int)$expires);
|
||||
}
|
||||
|
||||
public function delete(string $key): void {
|
||||
\apcu_delete($key);
|
||||
}
|
||||
|
||||
public function flush(): void {
|
||||
\apcu_clear_cache();
|
||||
}
|
||||
}
|
28
inc/Data/Driver/ArrayCacheDriver.php
Normal file
28
inc/Data/Driver/ArrayCacheDriver.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
/**
|
||||
* A simple process-wide PHP array.
|
||||
*/
|
||||
class ArrayCacheDriver implements CacheDriver {
|
||||
private static $inner = [];
|
||||
|
||||
public function get(string $key) {
|
||||
return isset(self::$inner[$key]) ? self::$inner[$key] : null;
|
||||
}
|
||||
|
||||
public function set(string $key, $value, $expires = false): void {
|
||||
self::$inner[$key] = $value;
|
||||
}
|
||||
|
||||
public function delete(string $key): void {
|
||||
unset(self::$inner[$key]);
|
||||
}
|
||||
|
||||
public function flush(): void {
|
||||
self::$inner = [];
|
||||
}
|
||||
}
|
38
inc/Data/Driver/CacheDriver.php
Normal file
38
inc/Data/Driver/CacheDriver.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
interface CacheDriver {
|
||||
/**
|
||||
* Get the value of associated with the key.
|
||||
*
|
||||
* @param string $key The key of the value.
|
||||
* @return mixed|null The value associated with the key, or null if there is none.
|
||||
*/
|
||||
public function get(string $key);
|
||||
|
||||
/**
|
||||
* Set a key-value pair.
|
||||
*
|
||||
* @param string $key The key.
|
||||
* @param mixed $value The value.
|
||||
* @param int|false $expires After how many seconds the pair will expire. Use false or ignore this parameter to keep
|
||||
* the value until it gets evicted to make space for more items. Some drivers will always
|
||||
* ignore this parameter and store the pair until it's removed.
|
||||
*/
|
||||
public function set(string $key, $value, $expires = false);
|
||||
|
||||
/**
|
||||
* Delete a key-value pair.
|
||||
*
|
||||
* @param string $key The key.
|
||||
*/
|
||||
public function delete(string $key);
|
||||
|
||||
/**
|
||||
* Delete all the key-value pairs.
|
||||
*/
|
||||
public function flush();
|
||||
}
|
28
inc/Data/Driver/ErrorLogLogDriver.php
Normal file
28
inc/Data/Driver/ErrorLogLogDriver.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
/**
|
||||
* Log via the php function error_log.
|
||||
*/
|
||||
class ErrorLogLogDriver implements LogDriver {
|
||||
use LogTrait;
|
||||
|
||||
private string $name;
|
||||
private int $level;
|
||||
|
||||
public function __construct(string $name, int $level) {
|
||||
$this->name = $name;
|
||||
$this->level = $level;
|
||||
}
|
||||
|
||||
public function log(int $level, string $message): void {
|
||||
if ($level <= $this->level) {
|
||||
$lv = $this->levelToString($level);
|
||||
$line = "{$this->name} $lv: $message";
|
||||
\error_log($line, 0, null, null);
|
||||
}
|
||||
}
|
||||
}
|
61
inc/Data/Driver/FileLogDriver.php
Normal file
61
inc/Data/Driver/FileLogDriver.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
/**
|
||||
* Log to a file.
|
||||
*/
|
||||
class FileLogDriver implements LogDriver {
|
||||
use LogTrait;
|
||||
|
||||
private string $name;
|
||||
private int $level;
|
||||
private mixed $fd;
|
||||
|
||||
public function __construct(string $name, int $level, string $file_path) {
|
||||
/*
|
||||
* error_log is slow as hell in it's 3rd mode, so use fopen + file locking instead.
|
||||
* https://grobmeier.solutions/performance-ofnonblocking-write-to-files-via-php-21082009.html
|
||||
*
|
||||
* Whatever file appending is atomic is contentious:
|
||||
* - There are no POSIX guarantees: https://stackoverflow.com/a/7237901
|
||||
* - But linus suggested they are on linux, on some filesystems: https://web.archive.org/web/20151201111541/http://article.gmane.org/gmane.linux.kernel/43445
|
||||
* - But it doesn't seem to be always the case: https://www.notthewizard.com/2014/06/17/are-files-appends-really-atomic/
|
||||
*
|
||||
* So we just use file locking to be sure.
|
||||
*/
|
||||
|
||||
$this->fd = \fopen($file_path, 'a');
|
||||
if ($this->fd === false) {
|
||||
throw new \RuntimeException("Unable to open log file at $file_path");
|
||||
}
|
||||
|
||||
$this->name = $name;
|
||||
$this->level = $level;
|
||||
|
||||
// In some cases PHP does not run the destructor.
|
||||
\register_shutdown_function([$this, 'close']);
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
$this->close();
|
||||
}
|
||||
|
||||
public function log(int $level, string $message): void {
|
||||
if ($level <= $this->level) {
|
||||
$lv = $this->levelToString($level);
|
||||
$line = "{$this->name} $lv: $message\n";
|
||||
\flock($this->fd, LOCK_EX);
|
||||
\fwrite($this->fd, $line);
|
||||
\fflush($this->fd);
|
||||
\flock($this->fd, LOCK_UN);
|
||||
}
|
||||
}
|
||||
|
||||
public function close() {
|
||||
\flock($this->fd, LOCK_UN);
|
||||
\fclose($this->fd);
|
||||
}
|
||||
}
|
155
inc/Data/Driver/FsCachedriver.php
Normal file
155
inc/Data/Driver/FsCachedriver.php
Normal file
@ -0,0 +1,155 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
class FsCacheDriver implements CacheDriver {
|
||||
private string $prefix;
|
||||
private string $base_path;
|
||||
private mixed $lock_fd;
|
||||
private int|false $collect_chance_den;
|
||||
|
||||
|
||||
private function prepareKey(string $key): string {
|
||||
$key = \str_replace('/', '::', $key);
|
||||
$key = \str_replace("\0", '', $key);
|
||||
return $this->prefix . $key;
|
||||
}
|
||||
|
||||
private function sharedLockCache(): void {
|
||||
\flock($this->lock_fd, LOCK_SH);
|
||||
}
|
||||
|
||||
private function exclusiveLockCache(): void {
|
||||
\flock($this->lock_fd, LOCK_EX);
|
||||
}
|
||||
|
||||
private function unlockCache(): void {
|
||||
\flock($this->lock_fd, LOCK_UN);
|
||||
}
|
||||
|
||||
private function collectImpl(): int {
|
||||
/*
|
||||
* A read lock is ok, since it's alright if we delete expired items from under the feet of other processes, and
|
||||
* no other process add new cache items or refresh existing ones.
|
||||
*/
|
||||
$files = \glob($this->base_path . $this->prefix . '*', \GLOB_NOSORT);
|
||||
$count = 0;
|
||||
foreach ($files as $file) {
|
||||
$data = \file_get_contents($file);
|
||||
$wrapped = \json_decode($data, true, 512, \JSON_THROW_ON_ERROR);
|
||||
if ($wrapped['expires'] !== false && $wrapped['expires'] <= \time()) {
|
||||
if (@\unlink($file)) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function maybeCollect(): void {
|
||||
if ($this->collect_chance_den !== false && \mt_rand(0, $this->collect_chance_den - 1) === 0) {
|
||||
$this->collect_chance_den = false; // Collect only once per instance (aka process).
|
||||
$this->collectImpl();
|
||||
}
|
||||
}
|
||||
|
||||
public function __construct(string $prefix, string $base_path, string $lock_file, int|false $collect_chance_den) {
|
||||
if ($base_path[\strlen($base_path) - 1] !== '/') {
|
||||
$base_path = "$base_path/";
|
||||
}
|
||||
|
||||
if (!\is_dir($base_path)) {
|
||||
throw new \RuntimeException("$base_path is not a directory!");
|
||||
}
|
||||
|
||||
if (!\is_writable($base_path)) {
|
||||
throw new \RuntimeException("$base_path is not writable!");
|
||||
}
|
||||
|
||||
$this->lock_fd = \fopen($base_path . $lock_file, 'w');
|
||||
if ($this->lock_fd === false) {
|
||||
throw new \RuntimeException('Unable to open the lock file!');
|
||||
}
|
||||
|
||||
$this->prefix = $prefix;
|
||||
$this->base_path = $base_path;
|
||||
$this->collect_chance_den = $collect_chance_den;
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
$this->close();
|
||||
}
|
||||
|
||||
public function get(string $key): mixed {
|
||||
$key = $this->prepareKey($key);
|
||||
|
||||
$this->sharedLockCache();
|
||||
|
||||
// Collect expired items first so if the target key is expired we shortcut to failure in the next lines.
|
||||
$this->maybeCollect();
|
||||
|
||||
$fd = \fopen($this->base_path . $key, 'r');
|
||||
if ($fd === false) {
|
||||
$this->unlockCache();
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = \stream_get_contents($fd);
|
||||
\fclose($fd);
|
||||
$this->unlockCache();
|
||||
$wrapped = \json_decode($data, true, 512, \JSON_THROW_ON_ERROR);
|
||||
|
||||
if ($wrapped['expires'] !== false && $wrapped['expires'] <= \time()) {
|
||||
// Already expired, leave it there since we already released the lock and pretend it doesn't exist.
|
||||
return null;
|
||||
} else {
|
||||
return $wrapped['inner'];
|
||||
}
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||
$key = $this->prepareKey($key);
|
||||
|
||||
$wrapped = [
|
||||
'expires' => $expires ? \time() + $expires : false,
|
||||
'inner' => $value
|
||||
];
|
||||
|
||||
$data = \json_encode($wrapped);
|
||||
$this->exclusiveLockCache();
|
||||
$this->maybeCollect();
|
||||
\file_put_contents($this->base_path . $key, $data);
|
||||
$this->unlockCache();
|
||||
}
|
||||
|
||||
public function delete(string $key): void {
|
||||
$key = $this->prepareKey($key);
|
||||
|
||||
$this->exclusiveLockCache();
|
||||
@\unlink($this->base_path . $key);
|
||||
$this->maybeCollect();
|
||||
$this->unlockCache();
|
||||
}
|
||||
|
||||
public function collect(): int {
|
||||
$this->sharedLockCache();
|
||||
$count = $this->collectImpl();
|
||||
$this->unlockCache();
|
||||
return $count;
|
||||
}
|
||||
|
||||
public function flush(): void {
|
||||
$this->exclusiveLockCache();
|
||||
$files = \glob($this->base_path . $this->prefix . '*', \GLOB_NOSORT);
|
||||
foreach ($files as $file) {
|
||||
@\unlink($file);
|
||||
}
|
||||
$this->unlockCache();
|
||||
}
|
||||
|
||||
public function close(): void {
|
||||
\fclose($this->lock_fd);
|
||||
}
|
||||
}
|
131
inc/Data/Driver/HttpDriver.php
Normal file
131
inc/Data/Driver/HttpDriver.php
Normal file
@ -0,0 +1,131 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
/**
|
||||
* Honestly this is just a wrapper for cURL. Still useful to mock it and have an OOP API on PHP 7.
|
||||
*/
|
||||
class HttpDriver {
|
||||
private $inner;
|
||||
private int $timeout;
|
||||
private int $max_file_size;
|
||||
|
||||
|
||||
private function resetTowards(string $url, int $timeout): void {
|
||||
\curl_reset($this->inner);
|
||||
\curl_setopt_array($this->inner, [
|
||||
\CURLOPT_URL => $url,
|
||||
\CURLOPT_TIMEOUT => $timeout,
|
||||
\CURLOPT_USERAGENT => 'Tinyboard',
|
||||
\CURLOPT_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS,
|
||||
]);
|
||||
}
|
||||
|
||||
public function __construct(int $timeout, int $max_file_size) {
|
||||
$this->inner = \curl_init();
|
||||
$this->timeout = $timeout;
|
||||
$this->max_file_size = $max_file_size;
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
\curl_close($this->inner);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a GET request.
|
||||
*
|
||||
* @param string $endpoint Uri endpoint.
|
||||
* @param ?array $data Optional GET parameters.
|
||||
* @param int $timeout Optional request timeout in seconds. Use the default timeout if 0.
|
||||
* @return string Returns the body of the response.
|
||||
* @throws RuntimeException Throws on IO error.
|
||||
*/
|
||||
public function requestGet(string $endpoint, ?array $data, int $timeout = 0): string {
|
||||
if (!empty($data)) {
|
||||
$endpoint .= '?' . \http_build_query($data);
|
||||
}
|
||||
if ($timeout == 0) {
|
||||
$timeout = $this->timeout;
|
||||
}
|
||||
|
||||
$this->resetTowards($endpoint, $timeout);
|
||||
\curl_setopt($this->inner, \CURLOPT_RETURNTRANSFER, true);
|
||||
$ret = \curl_exec($this->inner);
|
||||
|
||||
if ($ret === false) {
|
||||
throw new \RuntimeException(\curl_error($this->inner));
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a POST request.
|
||||
*
|
||||
* @param string $endpoint Uri endpoint.
|
||||
* @param ?array $data Optional POST parameters.
|
||||
* @param int $timeout Optional request timeout in seconds. Use the default timeout if 0.
|
||||
* @return string Returns the body of the response.
|
||||
* @throws RuntimeException Throws on IO error.
|
||||
*/
|
||||
public function requestPost(string $endpoint, ?array $data, int $timeout = 0): string {
|
||||
if ($timeout == 0) {
|
||||
$timeout = $this->timeout;
|
||||
}
|
||||
|
||||
$this->resetTowards($endpoint, $timeout);
|
||||
\curl_setopt($this->inner, \CURLOPT_POST, true);
|
||||
if (!empty($data)) {
|
||||
\curl_setopt($this->inner, \CURLOPT_POSTFIELDS, \http_build_query($data));
|
||||
}
|
||||
\curl_setopt($this->inner, \CURLOPT_RETURNTRANSFER, true);
|
||||
$ret = \curl_exec($this->inner);
|
||||
|
||||
if ($ret === false) {
|
||||
throw new \RuntimeException(\curl_error($this->inner));
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the url's target with curl.
|
||||
*
|
||||
* @param string $url Url to the file to download.
|
||||
* @param ?array $data Optional GET parameters.
|
||||
* @param resource $fd File descriptor to save the content to.
|
||||
* @param int $timeout Optional request timeout in seconds. Use the default timeout if 0.
|
||||
* @return bool Returns true on success, false if the file was too large.
|
||||
* @throws RuntimeException Throws on IO error.
|
||||
*/
|
||||
public function requestGetInto(string $endpoint, ?array $data, $fd, int $timeout = 0): bool {
|
||||
if (!empty($data)) {
|
||||
$endpoint .= '?' . \http_build_query($data);
|
||||
}
|
||||
if ($timeout == 0) {
|
||||
$timeout = $this->timeout;
|
||||
}
|
||||
|
||||
$this->resetTowards($endpoint, $timeout);
|
||||
// Adapted from: https://stackoverflow.com/a/17642638
|
||||
$opt = (\PHP_MAJOR_VERSION >= 8 && \PHP_MINOR_VERSION >= 2) ? \CURLOPT_XFERINFOFUNCTION : \CURLOPT_PROGRESSFUNCTION;
|
||||
\curl_setopt_array($this->inner, [
|
||||
\CURLOPT_NOPROGRESS => false,
|
||||
$opt => fn($res, $next_dl, $dl, $next_up, $up) => (int)($dl <= $this->max_file_size),
|
||||
\CURLOPT_FAILONERROR => true,
|
||||
\CURLOPT_FOLLOWLOCATION => false,
|
||||
\CURLOPT_FILE => $fd,
|
||||
\CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
|
||||
]);
|
||||
$ret = \curl_exec($this->inner);
|
||||
|
||||
if ($ret === false) {
|
||||
if (\curl_errno($this->inner) === CURLE_ABORTED_BY_CALLBACK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new \RuntimeException(\curl_error($this->inner));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
22
inc/Data/Driver/LogDriver.php
Normal file
22
inc/Data/Driver/LogDriver.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
interface LogDriver {
|
||||
public const EMERG = \LOG_EMERG;
|
||||
public const ERROR = \LOG_ERR;
|
||||
public const WARNING = \LOG_WARNING;
|
||||
public const NOTICE = \LOG_NOTICE;
|
||||
public const INFO = \LOG_INFO;
|
||||
public const DEBUG = \LOG_DEBUG;
|
||||
|
||||
/**
|
||||
* Log a message if the level of relevancy is at least the minimum.
|
||||
*
|
||||
* @param int $level Message level. Use Log interface constants.
|
||||
* @param string $message The message to log.
|
||||
*/
|
||||
public function log(int $level, string $message): void;
|
||||
}
|
26
inc/Data/Driver/LogTrait.php
Normal file
26
inc/Data/Driver/LogTrait.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
trait LogTrait {
|
||||
public static function levelToString(int $level): string {
|
||||
switch ($level) {
|
||||
case LogDriver::EMERG:
|
||||
return 'EMERG';
|
||||
case LogDriver::ERROR:
|
||||
return 'ERROR';
|
||||
case LogDriver::WARNING:
|
||||
return 'WARNING';
|
||||
case LogDriver::NOTICE:
|
||||
return 'NOTICE';
|
||||
case LogDriver::INFO:
|
||||
return 'INFO';
|
||||
case LogDriver::DEBUG:
|
||||
return 'DEBUG';
|
||||
default:
|
||||
throw new \InvalidArgumentException('Not a logging level');
|
||||
}
|
||||
}
|
||||
}
|
43
inc/Data/Driver/MemcacheCacheDriver.php
Normal file
43
inc/Data/Driver/MemcacheCacheDriver.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
class MemcachedCacheDriver implements CacheDriver {
|
||||
private \Memcached $inner;
|
||||
|
||||
public function __construct(string $prefix, string $memcached_server) {
|
||||
$this->inner = new \Memcached();
|
||||
if (!$this->inner->setOption(\Memcached::OPT_BINARY_PROTOCOL, true)) {
|
||||
throw new \RuntimeException('Unable to set the memcached protocol!');
|
||||
}
|
||||
if (!$this->inner->setOption(\Memcached::OPT_PREFIX_KEY, $prefix)) {
|
||||
throw new \RuntimeException('Unable to set the memcached prefix!');
|
||||
}
|
||||
if (!$this->inner->addServers($memcached_server)) {
|
||||
throw new \RuntimeException('Unable to add the memcached server!');
|
||||
}
|
||||
}
|
||||
|
||||
public function get(string $key): mixed {
|
||||
$ret = $this->inner->get($key);
|
||||
// If the returned value is false but the retrival was a success, then the value stored was a boolean false.
|
||||
if ($ret === false && $this->inner->getResultCode() !== \Memcached::RES_SUCCESS) {
|
||||
return null;
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||
$this->inner->set($key, $value, (int)$expires);
|
||||
}
|
||||
|
||||
public function delete(string $key): void {
|
||||
$this->inner->delete($key);
|
||||
}
|
||||
|
||||
public function flush(): void {
|
||||
$this->inner->flush();
|
||||
}
|
||||
}
|
26
inc/Data/Driver/NoneCacheDriver.php
Normal file
26
inc/Data/Driver/NoneCacheDriver.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
/**
|
||||
* No-op cache. Useful for testing.
|
||||
*/
|
||||
class NoneCacheDriver implements CacheDriver {
|
||||
public function get(string $key): mixed {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
public function delete(string $key): void {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
public function flush(): void {
|
||||
// No-op.
|
||||
}
|
||||
}
|
48
inc/Data/Driver/RedisCacheDriver.php
Normal file
48
inc/Data/Driver/RedisCacheDriver.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
class RedisCacheDriver implements CacheDriver {
|
||||
private string $prefix;
|
||||
private \Redis $inner;
|
||||
|
||||
public function __construct(string $prefix, string $host, int $port, ?string $password, string $database) {
|
||||
$this->inner = new \Redis();
|
||||
$this->inner->connect($host, $port);
|
||||
if ($password) {
|
||||
$this->inner->auth($password);
|
||||
}
|
||||
if (!$this->inner->select($database)) {
|
||||
throw new \RuntimeException('Unable to connect to Redis!');
|
||||
}
|
||||
|
||||
$$this->prefix = $prefix;
|
||||
}
|
||||
|
||||
public function get(string $key): mixed {
|
||||
$ret = $this->inner->get($this->prefix . $key);
|
||||
if ($ret === false) {
|
||||
return null;
|
||||
}
|
||||
return \json_decode($ret, true);
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||
if ($expires === false) {
|
||||
$this->inner->set($this->prefix . $key, \json_encode($value));
|
||||
} else {
|
||||
$expires = $expires * 1000; // Seconds to milliseconds.
|
||||
$this->inner->setex($this->prefix . $key, $expires, \json_encode($value));
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(string $key): void {
|
||||
$this->inner->del($this->prefix . $key);
|
||||
}
|
||||
|
||||
public function flush(): void {
|
||||
$this->inner->flushDB();
|
||||
}
|
||||
}
|
27
inc/Data/Driver/StderrLogDriver.php
Normal file
27
inc/Data/Driver/StderrLogDriver.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
/**
|
||||
* Log to php's standard error file stream.
|
||||
*/
|
||||
class StderrLogDriver implements LogDriver {
|
||||
use LogTrait;
|
||||
|
||||
private string $name;
|
||||
private int $level;
|
||||
|
||||
public function __construct(string $name, int $level) {
|
||||
$this->name = $name;
|
||||
$this->level = $level;
|
||||
}
|
||||
|
||||
public function log(int $level, string $message): void {
|
||||
if ($level <= $this->level) {
|
||||
$lv = $this->levelToString($level);
|
||||
\fwrite(\STDERR, "{$this->name} $lv: $message\n");
|
||||
}
|
||||
}
|
||||
}
|
35
inc/Data/Driver/SyslogLogDriver.php
Normal file
35
inc/Data/Driver/SyslogLogDriver.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
/**
|
||||
* Log to syslog.
|
||||
*/
|
||||
class SyslogLogDriver implements LogDriver {
|
||||
private int $level;
|
||||
|
||||
public function __construct(string $name, int $level, bool $print_stderr) {
|
||||
$flags = \LOG_ODELAY;
|
||||
if ($print_stderr) {
|
||||
$flags |= \LOG_PERROR;
|
||||
}
|
||||
|
||||
if (!\openlog($name, $flags, \LOG_USER)) {
|
||||
throw new \RuntimeException('Unable to open syslog');
|
||||
}
|
||||
|
||||
$this->level = $level;
|
||||
}
|
||||
|
||||
public function log(int $level, string $message): void {
|
||||
if ($level <= $this->level) {
|
||||
if (isset($_SERVER['REMOTE_ADDR'], $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'])) {
|
||||
// CGI
|
||||
\syslog($level, "$message - client: {$_SERVER['REMOTE_ADDR']}, request: \"{$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']}\"");
|
||||
} else {
|
||||
\syslog($level, $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
inc/anti-bot.php
Normal file
5
inc/anti-bot.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Anti-bot.php has been deprecated and removed due to its functions not being necessary and being easily bypassable, by both customized and uncustomized spambots.
|
||||
*/
|
@ -9,14 +9,49 @@ defined('TINYBOARD') or exit;
|
||||
* Class for generating json API compatible with 4chan API
|
||||
*/
|
||||
class Api {
|
||||
function __construct(){
|
||||
global $config;
|
||||
/**
|
||||
* Translation from local fields to fields in 4chan-style API
|
||||
*/
|
||||
$this->config = $config;
|
||||
private bool $show_filename;
|
||||
private bool $hide_email;
|
||||
private bool $country_flags;
|
||||
private array $postFields;
|
||||
|
||||
$this->postFields = array(
|
||||
private const INTS = [
|
||||
'no' => 1,
|
||||
'resto' => 1,
|
||||
'time' => 1,
|
||||
'tn_w' => 1,
|
||||
'tn_h' => 1,
|
||||
'w' => 1,
|
||||
'h' => 1,
|
||||
'fsize' => 1,
|
||||
'omitted_posts' => 1,
|
||||
'omitted_images' => 1,
|
||||
'replies' => 1,
|
||||
'images' => 1,
|
||||
'sticky' => 1,
|
||||
'locked' => 1,
|
||||
'last_modified' => 1
|
||||
];
|
||||
|
||||
private const THREADS_PAGE_FIELDS = [
|
||||
'id' => 'no',
|
||||
'bump' => 'last_modified'
|
||||
];
|
||||
|
||||
private const FILE_FIELDS = [
|
||||
'thumbheight' => 'tn_h',
|
||||
'thumbwidth' => 'tn_w',
|
||||
'height' => 'h',
|
||||
'width' => 'w',
|
||||
'size' => 'fsize'
|
||||
];
|
||||
|
||||
public function __construct(bool $show_filename, bool $hide_email, bool $country_flags) {
|
||||
// Translation from local fields to fields in 4chan-style API
|
||||
$this->show_filename = $show_filename;
|
||||
$this->hide_email = $hide_email;
|
||||
$this->country_flags = $country_flags;
|
||||
|
||||
$this->postFields = [
|
||||
'id' => 'no',
|
||||
'thread' => 'resto',
|
||||
'subject' => 'sub',
|
||||
@ -35,83 +70,65 @@ class Api {
|
||||
'cycle' => 'cyclical',
|
||||
'bump' => 'last_modified',
|
||||
'embed' => 'embed',
|
||||
);
|
||||
|
||||
$this->threadsPageFields = array(
|
||||
'id' => 'no',
|
||||
'bump' => 'last_modified'
|
||||
);
|
||||
|
||||
$this->fileFields = array(
|
||||
'thumbheight' => 'tn_h',
|
||||
'thumbwidth' => 'tn_w',
|
||||
'height' => 'h',
|
||||
'width' => 'w',
|
||||
'size' => 'fsize',
|
||||
);
|
||||
];
|
||||
|
||||
if (isset($config['api']['extra_fields']) && gettype($config['api']['extra_fields']) == 'array'){
|
||||
$this->postFields = array_merge($this->postFields, $config['api']['extra_fields']);
|
||||
}
|
||||
}
|
||||
|
||||
private static $ints = array(
|
||||
'no' => 1,
|
||||
'resto' => 1,
|
||||
'time' => 1,
|
||||
'tn_w' => 1,
|
||||
'tn_h' => 1,
|
||||
'w' => 1,
|
||||
'h' => 1,
|
||||
'fsize' => 1,
|
||||
'omitted_posts' => 1,
|
||||
'omitted_images' => 1,
|
||||
'replies' => 1,
|
||||
'images' => 1,
|
||||
'sticky' => 1,
|
||||
'locked' => 1,
|
||||
'last_modified' => 1
|
||||
);
|
||||
|
||||
private function translateFields($fields, $object, &$apiPost) {
|
||||
foreach ($fields as $local => $translated) {
|
||||
if (!isset($object->$local))
|
||||
if (!isset($object->$local)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$toInt = isset(self::$ints[$translated]);
|
||||
$toInt = isset(self::INTS[$translated]);
|
||||
$val = $object->$local;
|
||||
if ($this->hide_email && $local === 'email') {
|
||||
$val = '';
|
||||
}
|
||||
if ($val !== null && $val !== '') {
|
||||
$apiPost[$translated] = $toInt ? (int) $val : $val;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private function translateFile($file, $post, &$apiPost) {
|
||||
$this->translateFields($this->fileFields, $file, $apiPost);
|
||||
$apiPost['filename'] = @substr($file->name, 0, strrpos($file->name, '.'));
|
||||
$this->translateFields(self::FILE_FIELDS, $file, $apiPost);
|
||||
$dotPos = strrpos($file->file, '.');
|
||||
$apiPost['ext'] = substr($file->file, $dotPos);
|
||||
$apiPost['tim'] = substr($file->file, 0, $dotPos);
|
||||
|
||||
if ($this->show_filename) {
|
||||
$apiPost['filename'] = @substr($file->name, 0, strrpos($file->name, '.'));
|
||||
} else {
|
||||
$apiPost['filename'] = substr($file->file, 0, $dotPos);
|
||||
}
|
||||
if (isset ($file->hash) && $file->hash) {
|
||||
$apiPost['md5'] = base64_encode(hex2bin($file->hash));
|
||||
}
|
||||
else if (isset ($post->filehash) && $post->filehash) {
|
||||
} elseif (isset ($post->filehash) && $post->filehash) {
|
||||
$apiPost['md5'] = base64_encode(hex2bin($post->filehash));
|
||||
}
|
||||
}
|
||||
|
||||
private function translatePost($post, $threadsPage = false) {
|
||||
private function translatePost($post, bool $threadsPage = false) {
|
||||
global $config, $board;
|
||||
$apiPost = array();
|
||||
$fields = $threadsPage ? $this->threadsPageFields : $this->postFields;
|
||||
|
||||
$apiPost = [];
|
||||
$fields = $threadsPage ? self::THREADS_PAGE_FIELDS : $this->postFields;
|
||||
$this->translateFields($fields, $post, $apiPost);
|
||||
|
||||
if (isset($config['poster_ids']) && $config['poster_ids']) $apiPost['id'] = poster_id($post->ip, $post->thread, $board['uri']);
|
||||
if ($threadsPage) return $apiPost;
|
||||
|
||||
if (isset($config['poster_ids']) && $config['poster_ids']) {
|
||||
$apiPost['id'] = poster_id($post->ip, $post->thread ?? $post->id);
|
||||
}
|
||||
if ($threadsPage) {
|
||||
return $apiPost;
|
||||
}
|
||||
|
||||
// Handle country field
|
||||
if (isset($post->body_nomarkup) && $this->config['country_flags']) {
|
||||
if (isset($post->body_nomarkup) && $this->country_flags) {
|
||||
$modifiers = extract_modifiers($post->body_nomarkup);
|
||||
if (isset($modifiers['flag']) && isset($modifiers['flag alt']) && preg_match('/^[a-z]{2}$/', $modifiers['flag'])) {
|
||||
$country = strtoupper($modifiers['flag']);
|
||||
@ -131,12 +148,15 @@ class Api {
|
||||
if (isset($post->files) && $post->files && !$threadsPage) {
|
||||
$file = $post->files[0];
|
||||
$this->translateFile($file, $post, $apiPost);
|
||||
|
||||
if (sizeof($post->files) > 1) {
|
||||
$extra_files = array();
|
||||
$extra_files = [];
|
||||
foreach ($post->files as $i => $f) {
|
||||
if ($i == 0) continue;
|
||||
|
||||
$extra_file = array();
|
||||
if ($i == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$extra_file = [];
|
||||
$this->translateFile($f, $post, $extra_file);
|
||||
|
||||
$extra_files[] = $extra_file;
|
||||
@ -148,8 +168,8 @@ class Api {
|
||||
return $apiPost;
|
||||
}
|
||||
|
||||
function translateThread(Thread $thread, $threadsPage = false) {
|
||||
$apiPosts = array();
|
||||
public function translateThread(Thread $thread, bool $threadsPage = false) {
|
||||
$apiPosts = [];
|
||||
$op = $this->translatePost($thread, $threadsPage);
|
||||
if (!$threadsPage) $op['resto'] = 0;
|
||||
$apiPosts['posts'][] = $op;
|
||||
@ -161,16 +181,16 @@ class Api {
|
||||
return $apiPosts;
|
||||
}
|
||||
|
||||
function translatePage(array $threads) {
|
||||
$apiPage = array();
|
||||
public function translatePage(array $threads) {
|
||||
$apiPage = [];
|
||||
foreach ($threads as $thread) {
|
||||
$apiPage['threads'][] = $this->translateThread($thread);
|
||||
}
|
||||
return $apiPage;
|
||||
}
|
||||
|
||||
function translateCatalogPage(array $threads, $threadsPage = false) {
|
||||
$apiPage = array();
|
||||
public function translateCatalogPage(array $threads, bool $threadsPage = false) {
|
||||
$apiPage = [];
|
||||
foreach ($threads as $thread) {
|
||||
$ts = $this->translateThread($thread, $threadsPage);
|
||||
$apiPage['threads'][] = current($ts['posts']);
|
||||
@ -178,8 +198,8 @@ class Api {
|
||||
return $apiPage;
|
||||
}
|
||||
|
||||
function translateCatalog($catalog, $threadsPage = false) {
|
||||
$apiCatalog = array();
|
||||
public function translateCatalog($catalog, bool $threadsPage = false) {
|
||||
$apiCatalog = [];
|
||||
foreach ($catalog as $page => $threads) {
|
||||
$apiPage = $this->translateCatalogPage($threads, $threadsPage);
|
||||
$apiPage['page'] = $page;
|
@ -1,46 +1,201 @@
|
||||
<?php
|
||||
|
||||
use Vichan\Functions\Format;
|
||||
use Lifo\IP\CIDR;
|
||||
|
||||
class Bans {
|
||||
static private function shouldDelete(array $ban, bool $require_ban_view) {
|
||||
return $ban['expires'] && ($ban['seen'] || !$require_ban_view) && $ban['expires'] < time();
|
||||
}
|
||||
|
||||
static private function deleteBans(array $ban_ids) {
|
||||
$len = count($ban_ids);
|
||||
if ($len === 1) {
|
||||
$query = prepare('DELETE FROM ``bans`` WHERE `id` = :id');
|
||||
$query->bindValue(':id', $ban_ids[0], PDO::PARAM_INT);
|
||||
$query->execute() or error(db_error());
|
||||
|
||||
Vichan\Functions\Theme\rebuild_themes('bans');
|
||||
} elseif ($len >= 1) {
|
||||
// Build the query.
|
||||
$query = 'DELETE FROM ``bans`` WHERE `id` IN (';
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$query .= ":id{$i},";
|
||||
}
|
||||
// Substitute the last comma with a parenthesis.
|
||||
substr_replace($query, ')', strlen($query) - 1);
|
||||
|
||||
// Bind the params
|
||||
$query = prepare($query);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$query->bindValue(":id{$i}", (int)$ban_ids[$i], PDO::PARAM_INT);
|
||||
}
|
||||
|
||||
$query->execute() or error(db_error());
|
||||
|
||||
Vichan\Functions\Theme\rebuild_themes('bans');
|
||||
}
|
||||
}
|
||||
|
||||
static private function findSingleAutoGc(string $ip, int $ban_id, bool $require_ban_view) {
|
||||
// Use OR in the query to also garbage collect bans.
|
||||
$query = prepare(
|
||||
'SELECT ``bans``.* FROM ``bans``
|
||||
WHERE ((`ipstart` = :ip OR (:ip >= `ipstart` AND :ip <= `ipend`)) OR (``bans``.id = :id))
|
||||
ORDER BY `expires` IS NULL, `expires` DESC'
|
||||
);
|
||||
|
||||
$query->bindValue(':id', $ban_id);
|
||||
$query->bindValue(':ip', inet_pton($ip));
|
||||
|
||||
$query->execute() or error(db_error($query));
|
||||
|
||||
$found_ban = null;
|
||||
$to_delete_list = [];
|
||||
|
||||
while ($ban = $query->fetch(PDO::FETCH_ASSOC)) {
|
||||
if (self::shouldDelete($ban, $require_ban_view)) {
|
||||
$to_delete_list[] = $ban['id'];
|
||||
} elseif ($ban['id'] === $ban_id) {
|
||||
if ($ban['post']) {
|
||||
$ban['post'] = json_decode($ban['post'], true);
|
||||
}
|
||||
$ban['mask'] = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
|
||||
$found_ban = $ban;
|
||||
}
|
||||
}
|
||||
|
||||
self::deleteBans($to_delete_list);
|
||||
|
||||
return $found_ban;
|
||||
}
|
||||
|
||||
static private function findSingleNoGc(int $ban_id) {
|
||||
$query = prepare(
|
||||
'SELECT ``bans``.* FROM ``bans``
|
||||
WHERE ``bans``.id = :id
|
||||
ORDER BY `expires` IS NULL, `expires` DESC
|
||||
LIMIT 1'
|
||||
);
|
||||
|
||||
$query->bindValue(':id', $ban_id);
|
||||
|
||||
$query->execute() or error(db_error($query));
|
||||
$ret = $query->fetch(PDO::FETCH_ASSOC);
|
||||
if ($query->rowCount() == 0) {
|
||||
return null;
|
||||
} else {
|
||||
if ($ret['post']) {
|
||||
$ret['post'] = json_decode($ret['post'], true);
|
||||
}
|
||||
$ret['mask'] = self::range_to_string([$ret['ipstart'], $ret['ipend']]);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
|
||||
static private function findAutoGc(?string $ip, $board, bool $get_mod_info, bool $require_ban_view, ?int $ban_id): array {
|
||||
$query = prepare('SELECT ``bans``.*' . ($get_mod_info ? ', `username`' : '') . ' FROM ``bans``
|
||||
' . ($get_mod_info ? 'LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`' : '') . '
|
||||
WHERE
|
||||
(' . ($board !== false ? '(`board` IS NULL OR `board` = :board) AND' : '') . '
|
||||
(`ipstart` = :ip OR (:ip >= `ipstart` AND :ip <= `ipend`)) OR (``bans``.id = :id))
|
||||
ORDER BY `expires` IS NULL, `expires` DESC');
|
||||
|
||||
if ($board !== false) {
|
||||
$query->bindValue(':board', $board, PDO::PARAM_STR);
|
||||
}
|
||||
|
||||
$query->bindValue(':id', $ban_id);
|
||||
$query->bindValue(':ip', inet_pton($ip));
|
||||
$query->execute() or error(db_error($query));
|
||||
|
||||
$ban_list = [];
|
||||
$to_delete_list = [];
|
||||
|
||||
while ($ban = $query->fetch(PDO::FETCH_ASSOC)) {
|
||||
if (self::shouldDelete($ban, $require_ban_view)) {
|
||||
$to_delete_list[] = $ban['id'];
|
||||
} else {
|
||||
if ($ban['post']) {
|
||||
$ban['post'] = json_decode($ban['post'], true);
|
||||
}
|
||||
$ban['mask'] = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
|
||||
$ban_list[] = $ban;
|
||||
}
|
||||
}
|
||||
|
||||
self::deleteBans($to_delete_list);
|
||||
|
||||
return $ban_list;
|
||||
}
|
||||
|
||||
static private function findNoGc(?string $ip, string $board, bool $get_mod_info, ?int $ban_id): array {
|
||||
$query = prepare('SELECT ``bans``.*' . ($get_mod_info ? ', `username`' : '') . ' FROM ``bans``
|
||||
' . ($get_mod_info ? 'LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`' : '') . '
|
||||
WHERE
|
||||
(' . ($board !== false ? '(`board` IS NULL OR `board` = :board) AND' : '') . '
|
||||
(`ipstart` = :ip OR (:ip >= `ipstart` AND :ip <= `ipend`)) OR (``bans``.id = :id))
|
||||
AND (`expires` IS NULL OR `expires` >= :curr_time)
|
||||
ORDER BY `expires` IS NULL, `expires` DESC');
|
||||
|
||||
if ($board !== false) {
|
||||
$query->bindValue(':board', $board, PDO::PARAM_STR);
|
||||
}
|
||||
|
||||
$query->bindValue(':id', $ban_id);
|
||||
$query->bindValue(':ip', inet_pton($ip));
|
||||
$query->bindValue(':curr_time', time());
|
||||
$query->execute() or error(db_error($query));
|
||||
|
||||
$ban_list = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||
array_walk($ban_list, function (&$ban, $_index) {
|
||||
if ($ban['post']) {
|
||||
$ban['post'] = json_decode($ban['post'], true);
|
||||
}
|
||||
$ban['mask'] = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
|
||||
});
|
||||
return $ban_list;
|
||||
}
|
||||
|
||||
static public function range_to_string($mask) {
|
||||
list($ipstart, $ipend) = $mask;
|
||||
|
||||
|
||||
if (!isset($ipend) || $ipend === false) {
|
||||
// Not a range. Single IP address.
|
||||
$ipstr = inet_ntop($ipstart);
|
||||
return $ipstr;
|
||||
}
|
||||
|
||||
|
||||
if (strlen($ipstart) != strlen($ipend))
|
||||
return '???'; // What the fuck are you doing, son?
|
||||
|
||||
|
||||
$range = CIDR::range_to_cidr(inet_ntop($ipstart), inet_ntop($ipend));
|
||||
if ($range !== false)
|
||||
return $range;
|
||||
|
||||
|
||||
return '???';
|
||||
}
|
||||
|
||||
|
||||
private static function calc_cidr($mask) {
|
||||
$cidr = new CIDR($mask);
|
||||
$range = $cidr->getRange();
|
||||
|
||||
return array(inet_pton($range[0]), inet_pton($range[1]));
|
||||
|
||||
return [ inet_pton($range[0]), inet_pton($range[1]) ];
|
||||
}
|
||||
|
||||
|
||||
public static function parse_time($str) {
|
||||
if (empty($str))
|
||||
return false;
|
||||
|
||||
|
||||
if (($time = @strtotime($str)) !== false)
|
||||
return $time;
|
||||
|
||||
|
||||
if (!preg_match('/^((\d+)\s?ye?a?r?s?)?\s?+((\d+)\s?mon?t?h?s?)?\s?+((\d+)\s?we?e?k?s?)?\s?+((\d+)\s?da?y?s?)?((\d+)\s?ho?u?r?s?)?\s?+((\d+)\s?mi?n?u?t?e?s?)?\s?+((\d+)\s?se?c?o?n?d?s?)?$/', $str, $matches))
|
||||
return false;
|
||||
|
||||
|
||||
$expire = 0;
|
||||
|
||||
|
||||
if (isset($matches[2])) {
|
||||
// Years
|
||||
$expire += (int)$matches[2]*60*60*24*365;
|
||||
@ -69,14 +224,14 @@ class Bans {
|
||||
// Seconds
|
||||
$expire += (int)$matches[14];
|
||||
}
|
||||
|
||||
|
||||
return time() + $expire;
|
||||
}
|
||||
|
||||
|
||||
static public function parse_range($mask) {
|
||||
$ipstart = false;
|
||||
$ipend = false;
|
||||
|
||||
|
||||
if (preg_match('@^(\d{1,3}\.){1,3}([\d*]{1,3})?$@', $mask) && substr_count($mask, '*') == 1) {
|
||||
// IPv4 wildcard mask
|
||||
$parts = explode('.', $mask);
|
||||
@ -97,55 +252,38 @@ class Bans {
|
||||
list($ipv4, $bits) = explode('/', $mask);
|
||||
if ($bits > 32)
|
||||
return false;
|
||||
|
||||
|
||||
list($ipstart, $ipend) = self::calc_cidr($mask);
|
||||
} elseif (preg_match('@^[:a-z\d]+/\d+$@i', $mask)) {
|
||||
list($ipv6, $bits) = explode('/', $mask);
|
||||
if ($bits > 128)
|
||||
return false;
|
||||
|
||||
|
||||
list($ipstart, $ipend) = self::calc_cidr($mask);
|
||||
} else {
|
||||
if (($ipstart = @inet_pton($mask)) === false)
|
||||
return false;
|
||||
}
|
||||
|
||||
return array($ipstart, $ipend);
|
||||
|
||||
return [$ipstart, $ipend];
|
||||
}
|
||||
|
||||
static public function find($ip, $board = false, $get_mod_info = false, $banid = null) {
|
||||
|
||||
static public function findSingle(string $ip, int $ban_id, bool $require_ban_view, bool $auto_gc) {
|
||||
if ($auto_gc) {
|
||||
return self::findSingleAutoGc($ip, $ban_id, $require_ban_view);
|
||||
} else {
|
||||
return self::findSingleNoGc($ban_id);
|
||||
}
|
||||
}
|
||||
|
||||
static public function find(?string $ip, $board = false, bool $get_mod_info = false, ?int $ban_id = null, bool $auto_gc = true) {
|
||||
global $config;
|
||||
|
||||
$query = prepare('SELECT ``bans``.*' . ($get_mod_info ? ', `username`' : '') . ' FROM ``bans``
|
||||
' . ($get_mod_info ? 'LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`' : '') . '
|
||||
WHERE
|
||||
(' . ($board !== false ? '(`board` IS NULL OR `board` = :board) AND' : '') . '
|
||||
(`ipstart` = :ip OR (:ip >= `ipstart` AND :ip <= `ipend`)) OR (``bans``.id = :id))
|
||||
ORDER BY `expires` IS NULL, `expires` DESC');
|
||||
|
||||
if ($board !== false)
|
||||
$query->bindValue(':board', $board, PDO::PARAM_STR);
|
||||
|
||||
$query->bindValue(':id', $banid);
|
||||
$query->bindValue(':ip', inet_pton($ip));
|
||||
|
||||
$query->execute() or error(db_error($query));
|
||||
|
||||
$ban_list = array();
|
||||
|
||||
while ($ban = $query->fetch(PDO::FETCH_ASSOC)) {
|
||||
if ($ban['expires'] && ($ban['seen'] || !$config['require_ban_view']) && $ban['expires'] < time()) {
|
||||
self::delete($ban['id']);
|
||||
} else {
|
||||
if ($ban['post'])
|
||||
$ban['post'] = json_decode($ban['post'], true);
|
||||
$ban['mask'] = self::range_to_string(array($ban['ipstart'], $ban['ipend']));
|
||||
$ban['cmask'] = cloak_mask($ban['mask']);
|
||||
$ban_list[] = $ban;
|
||||
}
|
||||
if ($auto_gc) {
|
||||
return self::findAutoGc($ip, $board, $get_mod_info, $config['require_ban_view'], $ban_id);
|
||||
} else {
|
||||
return self::findNoGc($ip, $board, $get_mod_info, $ban_id);
|
||||
}
|
||||
|
||||
return $ban_list;
|
||||
}
|
||||
|
||||
static public function stream_json($out = false, $filter_ips = false, $filter_staff = false, $board_access = false) {
|
||||
@ -161,8 +299,7 @@ class Bans {
|
||||
$end = end($bans);
|
||||
|
||||
foreach ($bans as &$ban) {
|
||||
$uncloaked_mask = self::range_to_string(array($ban['ipstart'], $ban['ipend']));
|
||||
$ban['mask'] = cloak_mask($uncloaked_mask);
|
||||
$ban['mask'] = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
|
||||
|
||||
if ($ban['post']) {
|
||||
$post = json_decode($ban['post']);
|
||||
@ -203,17 +340,29 @@ class Bans {
|
||||
$out ? fputs($out, "]") : print("]");
|
||||
|
||||
}
|
||||
|
||||
|
||||
static public function seen($ban_id) {
|
||||
$query = query("UPDATE ``bans`` SET `seen` = 1 WHERE `id` = " . (int)$ban_id) or error(db_error());
|
||||
rebuildThemes('bans');
|
||||
Vichan\Functions\Theme\rebuild_themes('bans');
|
||||
}
|
||||
|
||||
static public function purge() {
|
||||
$query = query("DELETE FROM ``bans`` WHERE `expires` IS NOT NULL AND `expires` < " . time() . " AND `seen` = 1") or error(db_error());
|
||||
rebuildThemes('bans');
|
||||
|
||||
static public function purge($require_seen, $moratorium) {
|
||||
if ($require_seen) {
|
||||
$query = prepare("DELETE FROM ``bans`` WHERE `expires` IS NOT NULL AND `expires` + :moratorium < :curr_time AND `seen` = 1");
|
||||
} else {
|
||||
$query = prepare("DELETE FROM ``bans`` WHERE `expires` IS NOT NULL AND `expires` + :moratorium < :curr_time");
|
||||
}
|
||||
$query->bindValue(':moratorium', $moratorium);
|
||||
$query->bindValue(':curr_time', time());
|
||||
$query->execute() or error(db_error($query));
|
||||
|
||||
$affected = $query->rowCount();
|
||||
if ($affected > 0) {
|
||||
Vichan\Functions\Theme\rebuild_themes('bans');
|
||||
}
|
||||
return $affected;
|
||||
}
|
||||
|
||||
|
||||
static public function delete($ban_id, $modlog = false, $boards = false, $dont_rebuild = false) {
|
||||
global $config;
|
||||
|
||||
@ -228,52 +377,51 @@ class Bans {
|
||||
|
||||
if ($boards !== false && !in_array($ban['board'], $boards))
|
||||
error($config['error']['noaccess']);
|
||||
|
||||
$mask = self::range_to_string(array($ban['ipstart'], $ban['ipend']));
|
||||
$cloaked_mask = cloak_mask($mask);
|
||||
|
||||
|
||||
$mask = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
|
||||
|
||||
modLog("Removed ban #{$ban_id} for " .
|
||||
(filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$cloaked_mask\">$cloaked_mask</a>" : $cloaked_mask));
|
||||
}
|
||||
|
||||
|
||||
query("DELETE FROM ``bans`` WHERE `id` = " . (int)$ban_id) or error(db_error());
|
||||
|
||||
if (!$dont_rebuild) rebuildThemes('bans');
|
||||
|
||||
if (!$dont_rebuild) Vichan\Functions\Theme\rebuild_themes('bans');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static public function new_ban($cloaked_mask, $reason, $length = false, $ban_board = false, $mod_id = false, $post = false) {
|
||||
$mask = uncloak_mask($cloaked_mask);
|
||||
|
||||
global $mod, $pdo, $board;
|
||||
|
||||
|
||||
if ($mod_id === false) {
|
||||
$mod_id = isset($mod['id']) ? $mod['id'] : -1;
|
||||
}
|
||||
|
||||
|
||||
$range = self::parse_range($mask);
|
||||
$mask = self::range_to_string($range);
|
||||
$cloaked_mask = cloak_mask($mask);
|
||||
|
||||
|
||||
$query = prepare("INSERT INTO ``bans`` VALUES (NULL, :ipstart, :ipend, :time, :expires, :board, :mod, :reason, 0, :post)");
|
||||
|
||||
|
||||
$query->bindValue(':ipstart', $range[0]);
|
||||
if ($range[1] !== false && $range[1] != $range[0])
|
||||
$query->bindValue(':ipend', $range[1]);
|
||||
else
|
||||
$query->bindValue(':ipend', null, PDO::PARAM_NULL);
|
||||
|
||||
|
||||
$query->bindValue(':mod', $mod_id);
|
||||
$query->bindValue(':time', time());
|
||||
|
||||
|
||||
if ($reason !== '') {
|
||||
$reason = escape_markup_modifiers($reason);
|
||||
markup($reason);
|
||||
$query->bindValue(':reason', $reason);
|
||||
} else
|
||||
$query->bindValue(':reason', null, PDO::PARAM_NULL);
|
||||
|
||||
|
||||
if ($length) {
|
||||
if (is_int($length) || ctype_digit($length)) {
|
||||
$length = time() + $length;
|
||||
@ -284,34 +432,40 @@ class Bans {
|
||||
} else {
|
||||
$query->bindValue(':expires', null, PDO::PARAM_NULL);
|
||||
}
|
||||
|
||||
|
||||
if ($ban_board)
|
||||
$query->bindValue(':board', $ban_board);
|
||||
else
|
||||
$query->bindValue(':board', null, PDO::PARAM_NULL);
|
||||
|
||||
|
||||
if ($post) {
|
||||
if (!isset($board['uri']))
|
||||
openBoard($post['board']);
|
||||
|
||||
$post['board'] = $board['uri'];
|
||||
/*
|
||||
* The body can be so long to make the json longer than 64KBs, causing the query to fail.
|
||||
* Truncate it to a safe length (32KBs). It could probably be longer, but if the deleted body is THAT big
|
||||
* already, the likelihood of it being just assorted spam/garbage is about 101%.
|
||||
*/
|
||||
// We're on UTF-8 only, right...?
|
||||
$post['body'] = mb_strcut($post['body'], 0, 32768);
|
||||
|
||||
$query->bindValue(':post', json_encode($post));
|
||||
} else
|
||||
$query->bindValue(':post', null, PDO::PARAM_NULL);
|
||||
|
||||
$query->execute() or error(db_error($query));
|
||||
if (isset($mod['id']) && $mod['id'] == $mod_id) {
|
||||
modLog('Created a new ' .
|
||||
($length > 0 ? preg_replace('/^(\d+) (\w+?)s?$/', '$1-$2', until($length)) : 'permanent') .
|
||||
' ban on ' .
|
||||
($ban_board ? '/' . $ban_board . '/' : 'all boards') .
|
||||
' for ' .
|
||||
(filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$cloaked_mask\">$cloaked_mask</a>" : $cloaked_mask) .
|
||||
' (<small>#' . $pdo->lastInsertId() . '</small>)' .
|
||||
' with ' . ($reason ? 'reason: ' . utf8tohtml($reason) . '' : 'no reason'));
|
||||
}
|
||||
|
||||
rebuildThemes('bans');
|
||||
$query->execute() or error(db_error($query));
|
||||
|
||||
$ban_len = $length > 0 ? preg_replace('/^(\d+) (\w+?)s?$/', '$1-$2', Format\until($length)) : 'permanent';
|
||||
$ban_board = $ban_board ? "/$ban_board/" : 'all boards';
|
||||
$ban_ip = filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$cloaked_mask\">$cloaked_mask</a>" : $cloaked_mask;
|
||||
$ban_id = $pdo->lastInsertId();
|
||||
$ban_reason = $reason ? 'reason: ' . utf8tohtml($reason) : 'no reason';
|
||||
|
||||
modLog("Created a new $ban_len ban on $ban_board for $ban_ip (<small># $ban_id </small>) with $ban_reason");
|
||||
|
||||
Vichan\Functions\Theme\rebuild_themes('bans');
|
||||
|
||||
return $pdo->lastInsertId();
|
||||
}
|
92
inc/cache.php
Normal file
92
inc/cache.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (c) 2010-2013 Tinyboard Development Group
|
||||
*/
|
||||
|
||||
use Vichan\Data\Driver\{CacheDriver, ApcuCacheDriver, ArrayCacheDriver, FsCacheDriver, MemcachedCacheDriver, NoneCacheDriver, RedisCacheDriver};
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
class Cache {
|
||||
private static function buildCache(): CacheDriver {
|
||||
global $config;
|
||||
|
||||
switch ($config['cache']['enabled']) {
|
||||
case 'memcached':
|
||||
return new MemcachedCacheDriver(
|
||||
$config['cache']['prefix'],
|
||||
$config['cache']['memcached']
|
||||
);
|
||||
case 'redis':
|
||||
return new RedisCacheDriver(
|
||||
$config['cache']['prefix'],
|
||||
$config['cache']['redis'][0],
|
||||
$config['cache']['redis'][1],
|
||||
$config['cache']['redis'][2],
|
||||
$config['cache']['redis'][3]
|
||||
);
|
||||
case 'apcu':
|
||||
return new ApcuCacheDriver;
|
||||
case 'fs':
|
||||
return new FsCacheDriver(
|
||||
$config['cache']['prefix'],
|
||||
"tmp/cache/{$config['cache']['prefix']}",
|
||||
'.lock',
|
||||
$config['auto_maintenance'] ? 1000 : false
|
||||
);
|
||||
case 'none':
|
||||
return new NoneCacheDriver();
|
||||
case 'php':
|
||||
default:
|
||||
return new ArrayCacheDriver();
|
||||
}
|
||||
}
|
||||
|
||||
public static function getCache(): CacheDriver {
|
||||
static $cache;
|
||||
return $cache ??= self::buildCache();
|
||||
}
|
||||
|
||||
public static function get($key) {
|
||||
global $config, $debug;
|
||||
|
||||
$ret = self::getCache()->get($key);
|
||||
if ($ret === null) {
|
||||
$ret = false;
|
||||
}
|
||||
|
||||
if ($config['debug']) {
|
||||
$debug['cached'][] = $config['cache']['prefix'] . $key . ($ret === false ? ' (miss)' : ' (hit)');
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
public static function set($key, $value, $expires = false) {
|
||||
global $config, $debug;
|
||||
|
||||
if (!$expires) {
|
||||
$expires = $config['cache']['timeout'];
|
||||
}
|
||||
|
||||
self::getCache()->set($key, $value, $expires);
|
||||
|
||||
if ($config['debug']) {
|
||||
$debug['cached'][] = $config['cache']['prefix'] . $key . ' (set)';
|
||||
}
|
||||
}
|
||||
public static function delete($key) {
|
||||
global $config, $debug;
|
||||
|
||||
self::getCache()->delete($key);
|
||||
|
||||
if ($config['debug']) {
|
||||
$debug['cached'][] = $config['cache']['prefix'] . $key . ' (deleted)';
|
||||
}
|
||||
}
|
||||
public static function flush() {
|
||||
self::getCache()->flush();
|
||||
return false;
|
||||
}
|
||||
}
|
@ -41,7 +41,7 @@ class CzaksCaptcha {
|
||||
function mutate_sizes() {
|
||||
foreach ($this->content as &$v) {
|
||||
if (!isset ($v['font-size']))
|
||||
$v['font-size'] = rand($this->height/3 - 4, $this->height/3 + 8);
|
||||
$v['font-size'] = rand(intval($this->height/3) - 4, intval($this->height/3) + 8);
|
||||
}
|
||||
}
|
||||
function mutate_positions() {
|
@ -1,10 +1,8 @@
|
||||
I integrated this from: https://github.com/ctrlcctrlv/infinity/commit/62a6dac022cb338f7b719d0c35a64ab3efc64658
|
||||
|
||||
<strike>First import the captcha/dbschema.sql in your database</strike> it is no longer required.
|
||||
|
||||
In inc/captcha/config.php change the database_name database_user database_password to your own settings.
|
||||
|
||||
Add js/captcha.js in your secrets.php or config.php
|
||||
Add js/captcha.js in your secrets.php or config.php
|
||||
|
||||
Go to Line 305 in the /inc/config file and copy the settings in instance config, while changing the url to your website.
|
||||
Go to the line beneath it if you only want to enable it when posting a new thread.
|
File diff suppressed because it is too large
Load Diff
91
inc/context.php
Normal file
91
inc/context.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
namespace Vichan;
|
||||
|
||||
use Vichan\Data\Driver\{CacheDriver, HttpDriver, ErrorLogLogDriver, FileLogDriver, LogDriver, StderrLogDriver, SyslogLogDriver};
|
||||
use Vichan\Service\HCaptchaQuery;
|
||||
use Vichan\Service\NativeCaptchaQuery;
|
||||
use Vichan\Service\ReCaptchaQuery;
|
||||
use Vichan\Service\RemoteCaptchaQuery;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
class Context {
|
||||
private array $definitions;
|
||||
|
||||
public function __construct(array $definitions) {
|
||||
$this->definitions = $definitions;
|
||||
}
|
||||
|
||||
public function get(string $name){
|
||||
if (!isset($this->definitions[$name])) {
|
||||
throw new \RuntimeException("Could not find a dependency named $name");
|
||||
}
|
||||
|
||||
$ret = $this->definitions[$name];
|
||||
if (is_callable($ret) && !is_string($ret) && !is_array($ret)) {
|
||||
$ret = $ret($this);
|
||||
$this->definitions[$name] = $ret;
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
|
||||
function build_context(array $config): Context {
|
||||
return new Context([
|
||||
'config' => $config,
|
||||
LogDriver::class => function($c) {
|
||||
$config = $c->get('config');
|
||||
|
||||
$name = $config['log_system']['name'];
|
||||
$level = $config['debug'] ? LogDriver::DEBUG : LogDriver::NOTICE;
|
||||
$backend = $config['log_system']['type'];
|
||||
|
||||
// Check 'syslog' for backwards compatibility.
|
||||
if ((isset($config['syslog']) && $config['syslog']) || $backend === 'syslog') {
|
||||
return new SyslogLogDriver($name, $level, $this->config['log_system']['syslog_stderr']);
|
||||
} elseif ($backend === 'file') {
|
||||
return new FileLogDriver($name, $level, $this->config['log_system']['file_path']);
|
||||
} elseif ($backend === 'stderr') {
|
||||
return new StderrLogDriver($name, $level);
|
||||
} else {
|
||||
return new ErrorLogLogDriver($name, $level);
|
||||
}
|
||||
},
|
||||
HttpDriver::class => function($c) {
|
||||
$config = $c->get('config');
|
||||
return new HttpDriver($config['upload_by_url_timeout'], $config['max_filesize']);
|
||||
},
|
||||
RemoteCaptchaQuery::class => function($c) {
|
||||
$config = $c->get('config');
|
||||
$http = $c->get(HttpDriver::class);
|
||||
switch ($config['captcha']['provider']) {
|
||||
case 'recaptcha':
|
||||
return new ReCaptchaQuery($http, $config['captcha']['recaptcha']['secret']);
|
||||
case 'hcaptcha':
|
||||
return new HCaptchaQuery(
|
||||
$http,
|
||||
$config['captcha']['hcaptcha']['secret'],
|
||||
$config['captcha']['hcaptcha']['sitekey']
|
||||
);
|
||||
default:
|
||||
throw new \RuntimeException('No remote captcha service available');
|
||||
}
|
||||
},
|
||||
NativeCaptchaQuery::class => function($c) {
|
||||
$config = $c->get('config');
|
||||
if ($config['captcha']['provider'] !== 'native') {
|
||||
throw new \RuntimeException('No native captcha service available');
|
||||
}
|
||||
return new NativeCaptchaQuery(
|
||||
$c->get(HttpDriver::class),
|
||||
$config['domain'],
|
||||
$config['captcha']['native']['provider_check'],
|
||||
$config['captcha']['native']['extra']
|
||||
);
|
||||
},
|
||||
CacheDriver::class => function($c) {
|
||||
// Use the global for backwards compatibility.
|
||||
return \cache::getCache();
|
||||
}
|
||||
]);
|
||||
}
|
@ -85,24 +85,24 @@ function sb_api($b) { global $config, $build_pages;
|
||||
}
|
||||
|
||||
function sb_ukko() {
|
||||
rebuildTheme("ukko", "post-thread");
|
||||
Vichan\Functions\Theme\rebuild_theme("ukko", "post-thread");
|
||||
return true;
|
||||
}
|
||||
|
||||
function sb_catalog($b) {
|
||||
if (!openBoard($b)) return false;
|
||||
|
||||
rebuildTheme("catalog", "post-thread", $b);
|
||||
Vichan\Functions\Theme\rebuild_theme("catalog", "post-thread", $b);
|
||||
return true;
|
||||
}
|
||||
|
||||
function sb_recent() {
|
||||
rebuildTheme("recent", "post-thread");
|
||||
Vichan\Functions\Theme\rebuild_theme("recent", "post-thread");
|
||||
return true;
|
||||
}
|
||||
|
||||
function sb_sitemap() {
|
||||
rebuildTheme("sitemap", "all");
|
||||
Vichan\Functions\Theme\rebuild_theme("sitemap", "all");
|
||||
return true;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
joaoptm78@gmail.com
|
||||
http://www.php.net/manual/en/function.filesize.php#100097
|
||||
*/
|
||||
@ -21,7 +21,7 @@ function format_bytes($size) {
|
||||
|
||||
function doBoardListPart($list, $root, &$boards) {
|
||||
global $config;
|
||||
|
||||
|
||||
$body = '';
|
||||
foreach ($list as $key => $board) {
|
||||
if (is_array($board))
|
||||
@ -34,21 +34,21 @@ function doBoardListPart($list, $root, &$boards) {
|
||||
if (isset ($boards[$board])) {
|
||||
$title = ' title="'.$boards[$board].'"';
|
||||
}
|
||||
|
||||
|
||||
$body .= ' <a href="' . $root . $board . '/' . $config['file_index'] . '"'.$title.'>' . $board . '</a> /';
|
||||
}
|
||||
}
|
||||
}
|
||||
$body = preg_replace('/\/$/', '', $body);
|
||||
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
function createBoardlist($mod=false) {
|
||||
global $config;
|
||||
|
||||
|
||||
if (!isset($config['boards'])) return array('top'=>'','bottom'=>'');
|
||||
|
||||
|
||||
$xboards = listBoards();
|
||||
$boards = array();
|
||||
foreach ($xboards as $val) {
|
||||
@ -59,26 +59,26 @@ function createBoardlist($mod=false) {
|
||||
|
||||
if ($config['boardlist_wrap_bracket'] && !preg_match('/\] $/', $body))
|
||||
$body = '[' . $body . ']';
|
||||
|
||||
|
||||
$body = trim($body);
|
||||
|
||||
// Message compact-boardlist.js faster, so that page looks less ugly during loading
|
||||
$top = "<script type='text/javascript'>if (typeof do_boardlist != 'undefined') do_boardlist();</script>";
|
||||
|
||||
|
||||
return array(
|
||||
'top' => '<div class="boardlist">' . $body . '</div>' . $top,
|
||||
'bottom' => '<div class="boardlist bottom">' . $body . '</div>'
|
||||
);
|
||||
}
|
||||
|
||||
function error($message, $priority = true, $debug_stuff = false) {
|
||||
function error($message, $priority = true, $debug_stuff = []) {
|
||||
global $board, $mod, $config, $db_error;
|
||||
|
||||
|
||||
if ($config['syslog'] && $priority !== false) {
|
||||
// Use LOG_NOTICE instead of LOG_ERR or LOG_WARNING because most error message are not significant.
|
||||
_syslog($priority !== true ? $priority : LOG_NOTICE, $message);
|
||||
}
|
||||
|
||||
|
||||
if (defined('STDIN')) {
|
||||
// Running from CLI
|
||||
echo('Error: ' . $message . "\n");
|
||||
@ -113,7 +113,7 @@ function error($message, $priority = true, $debug_stuff = false) {
|
||||
};
|
||||
|
||||
|
||||
if ($debug_stuff)
|
||||
if ($debug_stuff)
|
||||
$debug_stuff = array_filter($debug_stuff, $debug_callback);
|
||||
|
||||
die(Element($config['file_page_template'], array(
|
||||
@ -132,7 +132,7 @@ function error($message, $priority = true, $debug_stuff = false) {
|
||||
|
||||
function loginForm($error=false, $username=false, $redirect=false) {
|
||||
global $config;
|
||||
|
||||
|
||||
die(Element($config['file_page_template'], array(
|
||||
'index' => $config['root'],
|
||||
'title' => _('Login'),
|
||||
@ -149,34 +149,34 @@ function loginForm($error=false, $username=false, $redirect=false) {
|
||||
|
||||
function pm_snippet($body, $len=null) {
|
||||
global $config;
|
||||
|
||||
|
||||
if (!isset($len))
|
||||
$len = &$config['mod']['snippet_length'];
|
||||
|
||||
|
||||
// Replace line breaks with some whitespace
|
||||
$body = preg_replace('@<br/?>@i', ' ', $body);
|
||||
|
||||
|
||||
// Strip tags
|
||||
$body = strip_tags($body);
|
||||
|
||||
|
||||
// Unescape HTML characters, to avoid splitting them in half
|
||||
$body = html_entity_decode($body, ENT_COMPAT, 'UTF-8');
|
||||
|
||||
|
||||
// calculate strlen() so we can add "..." after if needed
|
||||
$strlen = mb_strlen($body);
|
||||
|
||||
|
||||
$body = mb_substr($body, 0, $len);
|
||||
|
||||
|
||||
// Re-escape the characters.
|
||||
return '<em>' . utf8tohtml($body) . ($strlen > $len ? '…' : '') . '</em>';
|
||||
}
|
||||
|
||||
function capcode($cap) {
|
||||
global $config;
|
||||
|
||||
|
||||
if (!$cap)
|
||||
return false;
|
||||
|
||||
|
||||
$capcode = array();
|
||||
if (isset($config['custom_capcode'][$cap])) {
|
||||
if (is_array($config['custom_capcode'][$cap])) {
|
||||
@ -191,59 +191,59 @@ function capcode($cap) {
|
||||
} else {
|
||||
$capcode['cap'] = sprintf($config['capcode'], $cap);
|
||||
}
|
||||
|
||||
|
||||
return $capcode;
|
||||
}
|
||||
|
||||
function truncate($body, $url, $max_lines = false, $max_chars = false) {
|
||||
global $config;
|
||||
|
||||
|
||||
if ($max_lines === false)
|
||||
$max_lines = $config['body_truncate'];
|
||||
if ($max_chars === false)
|
||||
$max_chars = $config['body_truncate_char'];
|
||||
|
||||
|
||||
// We don't want to risk truncating in the middle of an HTML comment.
|
||||
// It's easiest just to remove them all first.
|
||||
$body = preg_replace('/<!--.*?-->/s', '', $body);
|
||||
|
||||
|
||||
$original_body = $body;
|
||||
|
||||
|
||||
$lines = substr_count($body, '<br/>');
|
||||
|
||||
|
||||
// Limit line count
|
||||
if ($lines > $max_lines) {
|
||||
if (preg_match('/(((.*?)<br\/>){' . $max_lines . '})/', $body, $m))
|
||||
$body = $m[0];
|
||||
}
|
||||
|
||||
|
||||
$body = mb_substr($body, 0, $max_chars);
|
||||
|
||||
|
||||
if ($body != $original_body) {
|
||||
// Remove any corrupt tags at the end
|
||||
$body = preg_replace('/<([\w]+)?([^>]*)?$/', '', $body);
|
||||
|
||||
|
||||
// Open tags
|
||||
if (preg_match_all('/<([\w]+)[^>]*>/', $body, $open_tags)) {
|
||||
|
||||
|
||||
$tags = array();
|
||||
for ($x=0;$x<count($open_tags[0]);$x++) {
|
||||
if (!preg_match('/\/(\s+)?>$/', $open_tags[0][$x]))
|
||||
$tags[] = $open_tags[1][$x];
|
||||
}
|
||||
|
||||
|
||||
// List successfully closed tags
|
||||
if (preg_match_all('/(<\/([\w]+))>/', $body, $closed_tags)) {
|
||||
for ($x=0;$x<count($closed_tags[0]);$x++) {
|
||||
unset($tags[array_search($closed_tags[2][$x], $tags)]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// remove broken HTML entity at the end (if existent)
|
||||
$body = preg_replace('/&[^;]+$/', '', $body);
|
||||
|
||||
|
||||
$tags_no_close_needed = array("colgroup", "dd", "dt", "li", "optgroup", "option", "p", "tbody", "td", "tfoot", "th", "thead", "tr", "br", "img");
|
||||
|
||||
|
||||
// Close any open tags
|
||||
foreach ($tags as &$tag) {
|
||||
if (!in_array($tag, $tags_no_close_needed))
|
||||
@ -253,10 +253,10 @@ function truncate($body, $url, $max_lines = false, $max_chars = false) {
|
||||
// remove broken HTML entity at the end (if existent)
|
||||
$body = preg_replace('/&[^;]*$/', '', $body);
|
||||
}
|
||||
|
||||
|
||||
$body .= '<span class="toolong">'.sprintf(_('Post too long. Click <a href="%s">here</a> to view the full text.'), $url).'</span>';
|
||||
}
|
||||
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
@ -266,21 +266,21 @@ function bidi_cleanup($data) {
|
||||
|
||||
$explicits = '\xE2\x80\xAA|\xE2\x80\xAB|\xE2\x80\xAD|\xE2\x80\xAE';
|
||||
$pdf = '\xE2\x80\xAC';
|
||||
|
||||
|
||||
preg_match_all("!$explicits!", $data, $m1, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
|
||||
preg_match_all("!$pdf!", $data, $m2, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
|
||||
|
||||
|
||||
if (count($m1) || count($m2)){
|
||||
|
||||
|
||||
$p = array();
|
||||
foreach ($m1 as $m){ $p[$m[0][1]] = 'push'; }
|
||||
foreach ($m2 as $m){ $p[$m[0][1]] = 'pop'; }
|
||||
ksort($p);
|
||||
|
||||
|
||||
$offset = 0;
|
||||
$stack = 0;
|
||||
foreach ($p as $pos => $type){
|
||||
|
||||
|
||||
if ($type == 'push'){
|
||||
$stack++;
|
||||
}else{
|
||||
@ -294,15 +294,15 @@ function bidi_cleanup($data) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# now add some pops if your stack is bigger than 0
|
||||
for ($i=0; $i<$stack; $i++){
|
||||
$data .= "\xE2\x80\xAC";
|
||||
}
|
||||
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@ -317,24 +317,24 @@ function secure_link($href) {
|
||||
|
||||
function embed_html($link) {
|
||||
global $config;
|
||||
|
||||
|
||||
foreach ($config['embedding'] as $embed) {
|
||||
if ($html = preg_replace($embed[0], $embed[1], $link)) {
|
||||
if ($html == $link)
|
||||
continue; // Nope
|
||||
|
||||
|
||||
$html = str_replace('%%tb_width%%', $config['embed_width'], $html);
|
||||
$html = str_replace('%%tb_height%%', $config['embed_height'], $html);
|
||||
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($link[0] == '<') {
|
||||
// Prior to v0.9.6-dev-8, HTML code for embedding was stored in the database instead of the link.
|
||||
return $link;
|
||||
}
|
||||
|
||||
|
||||
return 'Embedding error.';
|
||||
}
|
||||
|
||||
@ -343,7 +343,7 @@ class Post {
|
||||
global $config;
|
||||
if (!isset($root))
|
||||
$root = &$config['root'];
|
||||
|
||||
|
||||
foreach ($post as $key => $value) {
|
||||
$this->{$key} = $value;
|
||||
}
|
||||
@ -351,31 +351,38 @@ class Post {
|
||||
if (isset($this->files) && $this->files) {
|
||||
$this->files = is_string($this->files) ? json_decode($this->files) : $this->files;
|
||||
// Compatibility for posts before individual file hashing
|
||||
foreach ($this->files as $i => &$file) {
|
||||
foreach ($this->files as $i => &$file) {
|
||||
if (empty($file)) {
|
||||
unset($this->files[$i]);
|
||||
continue;
|
||||
}
|
||||
if (!isset($file->hash))
|
||||
$file->hash = $this->filehash;
|
||||
if (is_array($file)) {
|
||||
if (!isset($file['hash'])) {
|
||||
$file['hash'] = $this->filehash;
|
||||
}
|
||||
} else if (is_object($file)) {
|
||||
if (!isset($file->hash)) {
|
||||
$file->hash = $this->filehash;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$this->subject = utf8tohtml($this->subject);
|
||||
$this->name = utf8tohtml($this->name);
|
||||
$this->mod = $mod;
|
||||
$this->root = $root;
|
||||
|
||||
|
||||
if ($this->embed)
|
||||
$this->embed = embed_html($this->embed);
|
||||
|
||||
|
||||
$this->modifiers = extract_modifiers($this->body_nomarkup);
|
||||
|
||||
|
||||
if ($config['always_regenerate_markup']) {
|
||||
$this->body = $this->body_nomarkup;
|
||||
markup($this->body);
|
||||
}
|
||||
|
||||
|
||||
if ($this->mod)
|
||||
// Fix internal links
|
||||
// Very complicated regex
|
||||
@ -387,14 +394,25 @@ class Post {
|
||||
}
|
||||
public function link($pre = '', $page = false) {
|
||||
global $config, $board;
|
||||
|
||||
|
||||
return $this->root . $board['dir'] . $config['dir']['res'] . link_for((array)$this, $page == '50') . '#' . $pre . $this->id;
|
||||
}
|
||||
|
||||
|
||||
public function build($index=false) {
|
||||
global $board, $config;
|
||||
|
||||
return Element($config['file_post_reply'], array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index, 'mod' => $this->mod));
|
||||
|
||||
$options = [
|
||||
'config' => $config,
|
||||
'board' => $board,
|
||||
'post' => &$this,
|
||||
'index' => $index,
|
||||
'mod' => $this->mod
|
||||
];
|
||||
if ($this->mod) {
|
||||
$options['pm'] = create_pm_header();
|
||||
}
|
||||
|
||||
return Element($config['file_post_reply'], $options);
|
||||
}
|
||||
};
|
||||
|
||||
@ -403,14 +421,14 @@ class Thread {
|
||||
global $config;
|
||||
if (!isset($root))
|
||||
$root = &$config['root'];
|
||||
|
||||
|
||||
foreach ($post as $key => $value) {
|
||||
$this->{$key} = $value;
|
||||
}
|
||||
|
||||
|
||||
if (isset($this->files))
|
||||
$this->files = is_string($this->files) ? json_decode($this->files) : $this->files;
|
||||
|
||||
|
||||
$this->subject = utf8tohtml($this->subject);
|
||||
$this->name = utf8tohtml($this->name);
|
||||
$this->mod = $mod;
|
||||
@ -420,17 +438,17 @@ class Thread {
|
||||
$this->posts = array();
|
||||
$this->omitted = 0;
|
||||
$this->omitted_images = 0;
|
||||
|
||||
|
||||
if ($this->embed)
|
||||
$this->embed = embed_html($this->embed);
|
||||
|
||||
|
||||
$this->modifiers = extract_modifiers($this->body_nomarkup);
|
||||
|
||||
|
||||
if ($config['always_regenerate_markup']) {
|
||||
$this->body = $this->body_nomarkup;
|
||||
markup($this->body);
|
||||
}
|
||||
|
||||
|
||||
if ($this->mod)
|
||||
// Fix internal links
|
||||
// Very complicated regex
|
||||
@ -442,7 +460,7 @@ class Thread {
|
||||
}
|
||||
public function link($pre = '', $page = false) {
|
||||
global $config, $board;
|
||||
|
||||
|
||||
return $this->root . $board['dir'] . $config['dir']['res'] . link_for((array)$this, $page == '50') . '#' . $pre . $this->id;
|
||||
}
|
||||
public function add(Post $post) {
|
||||
@ -453,15 +471,27 @@ class Thread {
|
||||
}
|
||||
public function build($index=false, $isnoko50=false) {
|
||||
global $board, $config, $debug;
|
||||
|
||||
|
||||
$hasnoko50 = $this->postCount() >= $config['noko50_min'];
|
||||
|
||||
|
||||
event('show-thread', $this);
|
||||
|
||||
$options = [
|
||||
'config' => $config,
|
||||
'board' => $board,
|
||||
'post' => &$this,
|
||||
'index' => $index,
|
||||
'hasnoko50' => $hasnoko50,
|
||||
'isnoko50' => $isnoko50,
|
||||
'mod' => $this->mod
|
||||
];
|
||||
if ($this->mod) {
|
||||
$options['pm'] = create_pm_header();
|
||||
}
|
||||
|
||||
$file = ($index && $config['file_board']) ? $config['file_post_thread_fileboard'] : $config['file_post_thread'];
|
||||
$built = Element($file, array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index, 'hasnoko50' => $hasnoko50, 'isnoko50' => $isnoko50, 'mod' => $this->mod));
|
||||
|
||||
$built = Element($file, $options);
|
||||
|
||||
return $built;
|
||||
}
|
||||
};
|
||||
|
@ -10,54 +10,63 @@ class Filter {
|
||||
public $flood_check;
|
||||
private $condition;
|
||||
private $post;
|
||||
|
||||
|
||||
|
||||
public function __construct(array $arr) {
|
||||
foreach ($arr as $key => $value)
|
||||
$this->$key = $value;
|
||||
foreach ($arr as $key => $value) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function match($condition, $match) {
|
||||
$condition = strtolower($condition);
|
||||
|
||||
$post = &$this->post;
|
||||
|
||||
|
||||
switch($condition) {
|
||||
case 'custom':
|
||||
if (!is_callable($match))
|
||||
if (!is_callable($match)) {
|
||||
error('Custom condition for filter is not callable!');
|
||||
}
|
||||
return $match($post);
|
||||
case 'flood-match':
|
||||
if (!is_array($match))
|
||||
if (!is_array($match)) {
|
||||
error('Filter condition "flood-match" must be an array.');
|
||||
|
||||
}
|
||||
|
||||
// Filter out "flood" table entries which do not match this filter.
|
||||
|
||||
|
||||
$flood_check_matched = array();
|
||||
|
||||
|
||||
foreach ($this->flood_check as $flood_post) {
|
||||
foreach ($match as $flood_match_arg) {
|
||||
switch ($flood_match_arg) {
|
||||
case 'ip':
|
||||
if ($flood_post['ip'] != $_SERVER['REMOTE_ADDR'])
|
||||
if ($flood_post['ip'] != $_SERVER['REMOTE_ADDR']) {
|
||||
continue 3;
|
||||
}
|
||||
break;
|
||||
case 'body':
|
||||
if ($flood_post['posthash'] != make_comment_hex($post['body_nomarkup']))
|
||||
if ($flood_post['posthash'] != make_comment_hex($post['body_nomarkup'])) {
|
||||
continue 3;
|
||||
}
|
||||
break;
|
||||
case 'file':
|
||||
if (!isset($post['filehash']))
|
||||
if (!isset($post['filehash'])) {
|
||||
return false;
|
||||
if ($flood_post['filehash'] != $post['filehash'])
|
||||
}
|
||||
if ($flood_post['filehash'] != $post['filehash']) {
|
||||
continue 3;
|
||||
}
|
||||
break;
|
||||
case 'board':
|
||||
if ($flood_post['board'] != $post['board'])
|
||||
if ($flood_post['board'] != $post['board']) {
|
||||
continue 3;
|
||||
}
|
||||
break;
|
||||
case 'isreply':
|
||||
if ($flood_post['isreply'] == $post['op'])
|
||||
if ($flood_post['isreply'] == $post['op']) {
|
||||
continue 3;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
error('Invalid filter flood condition: ' . $flood_match_arg);
|
||||
@ -65,9 +74,8 @@ class Filter {
|
||||
}
|
||||
$flood_check_matched[] = $flood_post;
|
||||
}
|
||||
|
||||
|
||||
$this->flood_check = $flood_check_matched;
|
||||
|
||||
return !empty($this->flood_check);
|
||||
case 'flood-time':
|
||||
foreach ($this->flood_check as $flood_post) {
|
||||
@ -97,8 +105,9 @@ class Filter {
|
||||
case 'filehash':
|
||||
return $match === $post['filehash'];
|
||||
case 'filename':
|
||||
if (!$post['files'])
|
||||
if (!$post['files']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($post['files'] as $file) {
|
||||
if (preg_match($match, $file['filename'])) {
|
||||
@ -107,8 +116,9 @@ class Filter {
|
||||
}
|
||||
return false;
|
||||
case 'extension':
|
||||
if (!$post['files'])
|
||||
if (!$post['files']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($post['files'] as $file) {
|
||||
if (preg_match($match, $file['extension'])) {
|
||||
@ -126,60 +136,75 @@ class Filter {
|
||||
return $post['board'] == $match;
|
||||
case 'password':
|
||||
return $post['password'] == $match;
|
||||
case 'unshorten':
|
||||
$extracted_urls = get_urls($post['body_nomarkup']);
|
||||
foreach ($extracted_urls as $url) {
|
||||
if (preg_match($match, trace_url($url))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
error('Unknown filter condition: ' . $condition);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function action() {
|
||||
global $board;
|
||||
|
||||
$this->add_note = isset($this->add_note) ? $this->add_note : false;
|
||||
if ($this->add_note) {
|
||||
$query = prepare('INSERT INTO ``ip_notes`` VALUES (NULL, :ip, :mod, :time, :body)');
|
||||
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
|
||||
$query->bindValue(':mod', -1);
|
||||
$query->bindValue(':time', time());
|
||||
$query->bindValue(':body', "Autoban message: ".$this->post['body']);
|
||||
$query->execute() or error(db_error($query));
|
||||
}
|
||||
if (isset ($this->action)) switch($this->action) {
|
||||
case 'reject':
|
||||
error(isset($this->message) ? $this->message : 'Posting throttled by filter.');
|
||||
case 'ban':
|
||||
if (!isset($this->reason))
|
||||
error('The ban action requires a reason.');
|
||||
|
||||
$this->expires = isset($this->expires) ? $this->expires : false;
|
||||
$this->reject = isset($this->reject) ? $this->reject : true;
|
||||
$this->all_boards = isset($this->all_boards) ? $this->all_boards : false;
|
||||
|
||||
Bans::new_ban($_SERVER['REMOTE_ADDR'], $this->reason, $this->expires, $this->all_boards ? false : $board['uri'], -1);
|
||||
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
|
||||
$query->bindValue(':mod', -1);
|
||||
$query->bindValue(':time', time());
|
||||
$query->bindValue(':body', "Autoban message: ".$this->post['body']);
|
||||
$query->execute() or error(db_error($query));
|
||||
}
|
||||
if (isset($this->action)) {
|
||||
switch($this->action) {
|
||||
case 'reject':
|
||||
error(isset($this->message) ? $this->message : 'Posting throttled by filter.');
|
||||
case 'ban':
|
||||
if (!isset($this->reason)) {
|
||||
error('The ban action requires a reason.');
|
||||
}
|
||||
|
||||
if ($this->reject) {
|
||||
if (isset($this->message))
|
||||
error($message);
|
||||
|
||||
checkBan($board['uri']);
|
||||
exit;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
error('Unknown filter action: ' . $this->action);
|
||||
$this->expires = isset($this->expires) ? $this->expires : false;
|
||||
$this->reject = isset($this->reject) ? $this->reject : true;
|
||||
$this->all_boards = isset($this->all_boards) ? $this->all_boards : false;
|
||||
|
||||
Bans::new_ban($_SERVER['REMOTE_ADDR'], $this->reason, $this->expires, $this->all_boards ? false : $board['uri'], -1);
|
||||
|
||||
if ($this->reject) {
|
||||
if (isset($this->message)) {
|
||||
error($message);
|
||||
}
|
||||
|
||||
checkBan($board['uri']);
|
||||
exit;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
error('Unknown filter action: ' . $this->action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function check(array $post) {
|
||||
$this->post = $post;
|
||||
foreach ($this->condition as $condition => $value) {
|
||||
if ($condition[0] == '!') {
|
||||
$NOT = true;
|
||||
$condition = substr($condition, 1);
|
||||
} else $NOT = false;
|
||||
|
||||
if ($this->match($condition, $value) == $NOT)
|
||||
} else {
|
||||
$NOT = false;
|
||||
}
|
||||
|
||||
if ($this->match($condition, $value) == $NOT) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -187,39 +212,41 @@ class Filter {
|
||||
|
||||
function purge_flood_table() {
|
||||
global $config;
|
||||
|
||||
|
||||
// Determine how long we need to keep a cache of posts for flood prevention. Unfortunately, it is not
|
||||
// aware of flood filters in other board configurations. You can solve this problem by settings the
|
||||
// config variable $config['flood_cache'] (seconds).
|
||||
|
||||
|
||||
if ($config['flood_cache'] != -1) {
|
||||
$max_time = &$config['flood_cache'];
|
||||
} else {
|
||||
$max_time = 0;
|
||||
foreach ($config['filters'] as $filter) {
|
||||
if (isset($filter['condition']['flood-time']))
|
||||
if (isset($filter['condition']['flood-time'])) {
|
||||
$max_time = max($max_time, $filter['condition']['flood-time']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$time = time() - $max_time;
|
||||
|
||||
|
||||
query("DELETE FROM ``flood`` WHERE `time` < $time") or error(db_error());
|
||||
}
|
||||
|
||||
function do_filters(array $post) {
|
||||
global $config;
|
||||
|
||||
if (!isset($config['filters']) || empty($config['filters']))
|
||||
|
||||
if (!isset($config['filters']) || empty($config['filters'])) {
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
foreach ($config['filters'] as $filter) {
|
||||
if (isset($filter['condition']['flood-match'])) {
|
||||
$has_flood = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (isset($has_flood)) {
|
||||
if ($post['has_file']) {
|
||||
$query = prepare("SELECT * FROM ``flood`` WHERE `ip` = :ip OR `posthash` = :posthash OR `filehash` = :filehash");
|
||||
@ -236,14 +263,14 @@ function do_filters(array $post) {
|
||||
} else {
|
||||
$flood_check = false;
|
||||
}
|
||||
|
||||
|
||||
foreach ($config['filters'] as $filter_array) {
|
||||
$filter = new Filter($filter_array);
|
||||
$filter->flood_check = $flood_check;
|
||||
if ($filter->check($post))
|
||||
if ($filter->check($post)) {
|
||||
$filter->action();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
purge_flood_table();
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
114
inc/functions/dice.php
Normal file
114
inc/functions/dice.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
namespace Vichan\Functions\Dice;
|
||||
|
||||
function _get_or_default_int(array $arr, int $index, int $default) {
|
||||
return (isset($arr[$index]) && is_numeric($arr[$index])) ? (int)$arr[$index] : $default;
|
||||
}
|
||||
|
||||
|
||||
/* Die rolling:
|
||||
* If "dice XdY+/-Z" is in the email field (where X or +/-Z may be
|
||||
* missing), X Y-sided dice are rolled and summed, with the modifier Z
|
||||
* added on. The result is displayed at the top of the post.
|
||||
*/
|
||||
function email_dice_roll($post) {
|
||||
global $config;
|
||||
if(strpos(strtolower($post->email), 'dice%20') === 0) {
|
||||
$dicestr = str_split(substr($post->email, strlen('dice%20')));
|
||||
|
||||
// Get params
|
||||
$diceX = '';
|
||||
$diceY = '';
|
||||
$diceZ = '';
|
||||
|
||||
$curd = 'diceX';
|
||||
for($i = 0; $i < count($dicestr); $i ++) {
|
||||
if(is_numeric($dicestr[$i])) {
|
||||
$$curd .= $dicestr[$i];
|
||||
} else if($dicestr[$i] == 'd') {
|
||||
$curd = 'diceY';
|
||||
} else if($dicestr[$i] == '-' || $dicestr[$i] == '+') {
|
||||
$curd = 'diceZ';
|
||||
$$curd = $dicestr[$i];
|
||||
}
|
||||
}
|
||||
|
||||
// Default values for X and Z
|
||||
if($diceX == '') {
|
||||
$diceX = '1';
|
||||
}
|
||||
|
||||
if($diceZ == '') {
|
||||
$diceZ = '+0';
|
||||
}
|
||||
|
||||
// Intify them
|
||||
$diceX = intval($diceX);
|
||||
$diceY = intval($diceY);
|
||||
$diceZ = intval($diceZ);
|
||||
|
||||
// Continue only if we have valid values
|
||||
if($diceX > 0 && $diceY > 0) {
|
||||
$dicerolls = array();
|
||||
$dicesum = $diceZ;
|
||||
for($i = 0; $i < $diceX; $i++) {
|
||||
$roll = rand(1, $diceY);
|
||||
$dicerolls[] = $roll;
|
||||
$dicesum += $roll;
|
||||
}
|
||||
|
||||
// Prepend the result to the post body
|
||||
$modifier = ($diceZ != 0) ? ((($diceZ < 0) ? ' - ' : ' + ') . abs($diceZ)) : '';
|
||||
$dicesum = ($diceX > 1) ? ' = ' . $dicesum : '';
|
||||
$post->body = '<table class="diceroll"><tr><td><img src="'.$config['dir']['static'].'d10.svg" alt="Dice roll" width="24"></td><td>Rolled ' . implode(', ', $dicerolls) . $modifier . $dicesum . '</td></tr></table><br/>' . $post->body;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rolls a dice and generates the appropriate html from the markup.
|
||||
* @param array $matches The array of the matches according to the default configuration.
|
||||
* 1 -> The number of dices to roll.
|
||||
* 3 -> The number faces of the dices.
|
||||
* 4 -> The offset to apply to the dice.
|
||||
* @param string $img_path Path to the image to use relative to the root. Null if none.
|
||||
* @return string The html to replace the original markup with.
|
||||
*/
|
||||
function inline_dice_roll_markup(array $matches, ?string $img_path): string {
|
||||
global $config;
|
||||
|
||||
$dice_count = _get_or_default_int($matches, 1, 1);
|
||||
$dice_faces = _get_or_default_int($matches, 3, 6);
|
||||
$dice_offset = _get_or_default_int($matches, 4, 0);
|
||||
|
||||
// Clamp between 1 and max_roll_count.
|
||||
$dice_count = max(min($dice_count, $config['max_roll_count']), 1);
|
||||
// Must be at least 2.
|
||||
if ($dice_faces < 2) {
|
||||
$dice_faces = 6;
|
||||
}
|
||||
|
||||
$tot = 0;
|
||||
for ($i = 0; $i < $dice_count; $i++) {
|
||||
$tot += mt_rand(1, $dice_faces);
|
||||
}
|
||||
// Ensure that final result is at least an integer.
|
||||
$tot = abs((int)($dice_offset + $tot));
|
||||
|
||||
|
||||
if ($img_path !== null) {
|
||||
$img_text = "<img src='{$config['root']}{$img_path}' alt='dice' title='dice' class=\"inline-dice\"/>";
|
||||
} else {
|
||||
$img_text = '';
|
||||
}
|
||||
|
||||
if ($dice_offset === 0) {
|
||||
$dice_offset_text = '';
|
||||
} elseif ($dice_offset > 0) {
|
||||
$dice_offset_text = "+{$dice_offset}";
|
||||
} else {
|
||||
$dice_offset_text = (string)$dice_offset;
|
||||
}
|
||||
|
||||
return "<span>$img_text {$dice_count}d{$dice_faces}{$dice_offset_text} = <b>$tot</b></span>";
|
||||
}
|
28
inc/functions/format.php
Normal file
28
inc/functions/format.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace Vichan\Functions\Format;
|
||||
|
||||
|
||||
function format_timestamp(int $delta): string {
|
||||
switch (true) {
|
||||
case $delta < 60:
|
||||
return $delta . ' ' . ngettext('second', 'seconds', $delta);
|
||||
case $delta < 3600: //60*60 = 3600
|
||||
return ($num = round($delta/ 60)) . ' ' . ngettext('minute', 'minutes', $num);
|
||||
case $delta < 86400: //60*60*24 = 86400
|
||||
return ($num = round($delta / 3600)) . ' ' . ngettext('hour', 'hours', $num);
|
||||
case $delta < 604800: //60*60*24*7 = 604800
|
||||
return ($num = round($delta / 86400)) . ' ' . ngettext('day', 'days', $num);
|
||||
case $delta < 31536000: //60*60*24*365 = 31536000
|
||||
return ($num = round($delta / 604800)) . ' ' . ngettext('week', 'weeks', $num);
|
||||
default:
|
||||
return ($num = round($delta / 31536000)) . ' ' . ngettext('year', 'years', $num);
|
||||
}
|
||||
}
|
||||
|
||||
function until(int $timestamp): string {
|
||||
return format_timestamp($timestamp - time());
|
||||
}
|
||||
|
||||
function ago(int $timestamp): string {
|
||||
return format_timestamp(time() - $timestamp);
|
||||
}
|
16
inc/functions/net.php
Normal file
16
inc/functions/net.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
namespace Vichan\Functions\Net;
|
||||
|
||||
|
||||
/**
|
||||
* @param bool $trust_headers. If true, trust the `HTTP_X_FORWARDED_PROTO` header to check if the connection is HTTPS.
|
||||
* @return bool Returns if the client-server connection is an encrypted one (HTTPS).
|
||||
*/
|
||||
function is_connection_secure(bool $trust_headers): bool {
|
||||
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
||||
return true;
|
||||
} elseif ($trust_headers && isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
33
inc/functions/num.php
Normal file
33
inc/functions/num.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace Vichan\Functions\Num;
|
||||
|
||||
// Highest common factor
|
||||
function hcf($a, $b){
|
||||
$gcd = 1;
|
||||
|
||||
if ($a > $b) {
|
||||
$a = $a+$b;
|
||||
$b = $a-$b;
|
||||
$a = $a-$b;
|
||||
}
|
||||
if ($b == (round($b / $a)) * $a) {
|
||||
$gcd = $a;
|
||||
} else {
|
||||
for ($i = round($a / 2); $i; $i--) {
|
||||
if ($a == round($a / $i) * $i && $b == round($b / $i) * $i) {
|
||||
$gcd = $i;
|
||||
$i = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $gcd;
|
||||
}
|
||||
|
||||
function fraction($numerator, $denominator, $sep) {
|
||||
$gcf = hcf($numerator, $denominator);
|
||||
$numerator = $numerator / $gcf;
|
||||
$denominator = $denominator / $gcf;
|
||||
|
||||
return "{$numerator}{$sep}{$denominator}";
|
||||
}
|
98
inc/functions/theme.php
Normal file
98
inc/functions/theme.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
namespace Vichan\Functions\Theme;
|
||||
|
||||
|
||||
function rebuild_themes(string $action, $boardname = false): void {
|
||||
global $config, $board, $current_locale;
|
||||
|
||||
// Save the global variables
|
||||
$_config = $config;
|
||||
$_board = $board;
|
||||
|
||||
// List themes
|
||||
if ($themes = \Cache::get("themes")) {
|
||||
// OK, we already have themes loaded
|
||||
} else {
|
||||
$query = query("SELECT `theme` FROM ``theme_settings`` WHERE `name` IS NULL AND `value` IS NULL") or error(db_error());
|
||||
$themes = $query->fetchAll(\PDO::FETCH_NUM);
|
||||
|
||||
\Cache::set("themes", $themes);
|
||||
}
|
||||
|
||||
foreach ($themes as $theme) {
|
||||
// Restore them
|
||||
$config = $_config;
|
||||
$board = $_board;
|
||||
|
||||
// Reload the locale
|
||||
if ($config['locale'] != $current_locale) {
|
||||
$current_locale = $config['locale'];
|
||||
init_locale($config['locale']);
|
||||
}
|
||||
|
||||
if (PHP_SAPI === 'cli') {
|
||||
echo "Rebuilding theme ".$theme[0]."... ";
|
||||
}
|
||||
|
||||
rebuild_theme($theme[0], $action, $boardname);
|
||||
|
||||
if (PHP_SAPI === 'cli') {
|
||||
echo "done\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Restore them again
|
||||
$config = $_config;
|
||||
$board = $_board;
|
||||
|
||||
// Reload the locale
|
||||
if ($config['locale'] != $current_locale) {
|
||||
$current_locale = $config['locale'];
|
||||
init_locale($config['locale']);
|
||||
}
|
||||
}
|
||||
|
||||
function load_theme_config($_theme) {
|
||||
global $config;
|
||||
|
||||
if (!file_exists($config['dir']['themes'] . '/' . $_theme . '/info.php')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load theme information into $theme
|
||||
include $config['dir']['themes'] . '/' . $_theme . '/info.php';
|
||||
|
||||
return $theme;
|
||||
}
|
||||
|
||||
function rebuild_theme($theme, string $action, $board = false) {
|
||||
global $config, $_theme;
|
||||
$_theme = $theme;
|
||||
|
||||
$theme = load_theme_config($_theme);
|
||||
|
||||
if (file_exists($config['dir']['themes'] . '/' . $_theme . '/theme.php')) {
|
||||
require_once $config['dir']['themes'] . '/' . $_theme . '/theme.php';
|
||||
|
||||
$theme['build_function']($action, theme_settings($_theme), $board);
|
||||
}
|
||||
}
|
||||
|
||||
function theme_settings($theme): array {
|
||||
if ($settings = \Cache::get("theme_settings_" . $theme)) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
$query = prepare("SELECT `name`, `value` FROM ``theme_settings`` WHERE `theme` = :theme AND `name` IS NOT NULL");
|
||||
$query->bindValue(':theme', $theme);
|
||||
$query->execute() or error(db_error($query));
|
||||
|
||||
$settings = [];
|
||||
while ($s = $query->fetch(\PDO::FETCH_ASSOC)) {
|
||||
$settings[$s['name']] = $s['value'];
|
||||
}
|
||||
|
||||
\Cache::set("theme_settings_".$theme, $settings);
|
||||
|
||||
return $settings;
|
||||
}
|
@ -10,7 +10,7 @@ class Image {
|
||||
public $src, $format, $image, $size;
|
||||
public function __construct($src, $format = false, $size = false) {
|
||||
global $config;
|
||||
|
||||
|
||||
$this->src = $src;
|
||||
$this->format = $format;
|
||||
|
||||
@ -24,21 +24,21 @@ class Image {
|
||||
error(_('Unsupported file format: ') . $this->format);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$this->image = new $classname($this, $size);
|
||||
|
||||
if (!$this->image->valid()) {
|
||||
$this->delete();
|
||||
error($config['error']['invalidimg']);
|
||||
}
|
||||
|
||||
|
||||
$this->size = (object)array('width' => $this->image->_width(), 'height' => $this->image->_height());
|
||||
if ($this->size->width < 1 || $this->size->height < 1) {
|
||||
$this->delete();
|
||||
error($config['error']['invalidimg']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function resize($extension, $max_width, $max_height) {
|
||||
global $config;
|
||||
|
||||
@ -62,16 +62,16 @@ class Image {
|
||||
error(_('Unsupported file format: ') . $extension);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$thumb = new $classname(false);
|
||||
$thumb->src = $this->src;
|
||||
$thumb->format = $this->format;
|
||||
$thumb->original_width = $this->size->width;
|
||||
$thumb->original_height = $this->size->height;
|
||||
|
||||
|
||||
$x_ratio = $max_width / $this->size->width;
|
||||
$y_ratio = $max_height / $this->size->height;
|
||||
|
||||
|
||||
if (($this->size->width <= $max_width) && ($this->size->height <= $max_height)) {
|
||||
$width = $this->size->width;
|
||||
$height = $this->size->height;
|
||||
@ -82,16 +82,16 @@ class Image {
|
||||
$width = ceil($y_ratio * $this->size->width);
|
||||
$height = $max_height;
|
||||
}
|
||||
|
||||
|
||||
$thumb->_resize($this->image->image, $width, $height);
|
||||
|
||||
|
||||
return $thumb;
|
||||
}
|
||||
|
||||
|
||||
public function to($dst) {
|
||||
$this->image->to($dst);
|
||||
}
|
||||
|
||||
|
||||
public function delete() {
|
||||
file_unlink($this->src);
|
||||
}
|
||||
@ -114,26 +114,26 @@ class ImageGD {
|
||||
}
|
||||
|
||||
class ImageBase extends ImageGD {
|
||||
public $image, $src, $original, $original_width, $original_height, $width, $height;
|
||||
public $image, $src, $original, $original_width, $original_height, $width, $height;
|
||||
public function valid() {
|
||||
return (bool)$this->image;
|
||||
}
|
||||
|
||||
|
||||
public function __construct($img, $size = false) {
|
||||
if (method_exists($this, 'init'))
|
||||
$this->init();
|
||||
|
||||
|
||||
if ($size && $size[0] > 0 && $size[1] > 0) {
|
||||
$this->width = $size[0];
|
||||
$this->height = $size[1];
|
||||
}
|
||||
|
||||
|
||||
if ($img !== false) {
|
||||
$this->src = $img->src;
|
||||
$this->from();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function _width() {
|
||||
if (method_exists($this, 'width'))
|
||||
return $this->width();
|
||||
@ -156,7 +156,7 @@ class ImageBase extends ImageGD {
|
||||
$this->original = &$original;
|
||||
$this->width = $width;
|
||||
$this->height = $height;
|
||||
|
||||
|
||||
if (method_exists($this, 'resize'))
|
||||
$this->resize();
|
||||
else
|
||||
@ -199,31 +199,31 @@ class ImageImagick extends ImageBase {
|
||||
}
|
||||
public function resize() {
|
||||
global $config;
|
||||
|
||||
|
||||
if ($this->format == 'gif' && ($config['thumb_ext'] == 'gif' || $config['thumb_ext'] == '')) {
|
||||
$this->image = new Imagick();
|
||||
$this->image->setFormat('gif');
|
||||
|
||||
|
||||
$keep_frames = array();
|
||||
for ($i = 0; $i < $this->original->getNumberImages(); $i += floor($this->original->getNumberImages() / $config['thumb_keep_animation_frames']))
|
||||
$keep_frames[] = $i;
|
||||
|
||||
|
||||
$i = 0;
|
||||
$delay = 0;
|
||||
foreach ($this->original as $frame) {
|
||||
$delay += $frame->getImageDelay();
|
||||
|
||||
|
||||
if (in_array($i, $keep_frames)) {
|
||||
// $frame->scaleImage($this->width, $this->height, false);
|
||||
$frame->sampleImage($this->width, $this->height);
|
||||
$frame->setImagePage($this->width, $this->height, 0, 0);
|
||||
$frame->setImageDelay($delay);
|
||||
$delay = 0;
|
||||
|
||||
|
||||
$this->image->addImage($frame->getImage());
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->image = clone $this->original;
|
||||
$this->image->scaleImage($this->width, $this->height, false);
|
||||
@ -234,15 +234,15 @@ class ImageImagick extends ImageBase {
|
||||
|
||||
class ImageConvert extends ImageBase {
|
||||
public $width, $height, $temp, $gm = false, $gifsicle = false;
|
||||
|
||||
|
||||
public function init() {
|
||||
global $config;
|
||||
|
||||
|
||||
if ($config['thumb_method'] == 'gm' || $config['thumb_method'] == 'gm+gifsicle')
|
||||
$this->gm = true;
|
||||
if ($config['thumb_method'] == 'convert+gifsicle' || $config['thumb_method'] == 'gm+gifsicle')
|
||||
$this->gifsicle = true;
|
||||
|
||||
|
||||
$this->temp = false;
|
||||
}
|
||||
public function get_size($src, $try_gd_first = true) {
|
||||
@ -264,7 +264,7 @@ class ImageConvert extends ImageBase {
|
||||
if ($size) {
|
||||
$this->width = $size[0];
|
||||
$this->height = $size[1];
|
||||
|
||||
|
||||
$this->image = true;
|
||||
} else {
|
||||
// mark as invalid
|
||||
@ -273,7 +273,7 @@ class ImageConvert extends ImageBase {
|
||||
}
|
||||
public function to($src) {
|
||||
global $config;
|
||||
|
||||
|
||||
if (!$this->temp) {
|
||||
if ($config['strip_exif']) {
|
||||
if($error = shell_exec_error(($this->gm ? 'gm ' : '') . 'convert ' .
|
||||
@ -291,6 +291,7 @@ class ImageConvert extends ImageBase {
|
||||
} else {
|
||||
rename($this->temp, $src);
|
||||
chmod($src, 0664);
|
||||
$this->temp = false;
|
||||
}
|
||||
}
|
||||
public function width() {
|
||||
@ -300,21 +301,23 @@ class ImageConvert extends ImageBase {
|
||||
return $this->height;
|
||||
}
|
||||
public function destroy() {
|
||||
@unlink($this->temp);
|
||||
$this->temp = false;
|
||||
if ($this->temp !== false) {
|
||||
@unlink($this->temp);
|
||||
$this->temp = false;
|
||||
}
|
||||
}
|
||||
public function resize() {
|
||||
global $config;
|
||||
|
||||
|
||||
if ($this->temp) {
|
||||
// remove old
|
||||
$this->destroy();
|
||||
}
|
||||
|
||||
|
||||
$this->temp = tempnam($config['tmp'], 'convert') . ($config['thumb_ext'] == '' ? '' : '.' . $config['thumb_ext']);
|
||||
|
||||
|
||||
$config['thumb_keep_animation_frames'] = (int)$config['thumb_keep_animation_frames'];
|
||||
|
||||
|
||||
if ($this->format == 'gif' && ($config['thumb_ext'] == 'gif' || $config['thumb_ext'] == '') && $config['thumb_keep_animation_frames'] > 1) {
|
||||
if ($this->gifsicle) {
|
||||
if (($error = shell_exec("gifsicle -w --unoptimize -O2 --resize {$this->width}x{$this->height} < " .
|
||||
@ -324,12 +327,7 @@ class ImageConvert extends ImageBase {
|
||||
error(_('Failed to resize image!'), null, $error);
|
||||
}
|
||||
} else {
|
||||
if ($config['convert_manual_orient'] && ($this->format == 'jpg' || $this->format == 'jpeg'))
|
||||
$convert_args = str_replace('-auto-orient', ImageConvert::jpeg_exif_orientation($this->src), $config['convert_args']);
|
||||
elseif ($config['convert_manual_orient'])
|
||||
$convert_args = str_replace('-auto-orient', '', $config['convert_args']);
|
||||
else
|
||||
$convert_args = &$config['convert_args'];
|
||||
$convert_args = &$config['convert_args'];
|
||||
|
||||
if (($error = shell_exec_error(($this->gm ? 'gm ' : '') . 'convert ' .
|
||||
sprintf($convert_args,
|
||||
@ -348,12 +346,8 @@ class ImageConvert extends ImageBase {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($config['convert_manual_orient'] && ($this->format == 'jpg' || $this->format == 'jpeg'))
|
||||
$convert_args = str_replace('-auto-orient', ImageConvert::jpeg_exif_orientation($this->src), $config['convert_args']);
|
||||
elseif ($config['convert_manual_orient'])
|
||||
$convert_args = str_replace('-auto-orient', '', $config['convert_args']);
|
||||
else
|
||||
$convert_args = &$config['convert_args'];
|
||||
$convert_args = &$config['convert_args'];
|
||||
|
||||
if (($error = shell_exec_error(($this->gm ? 'gm ' : '') . 'convert ' .
|
||||
sprintf($convert_args,
|
||||
$this->width,
|
||||
@ -380,69 +374,6 @@ class ImageConvert extends ImageBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For when -auto-orient doesn't exist (older versions)
|
||||
static public function jpeg_exif_orientation($src, $exif = false) {
|
||||
if (!$exif) {
|
||||
$exif = @exif_read_data($src);
|
||||
if (!isset($exif['Orientation']))
|
||||
return false;
|
||||
}
|
||||
switch($exif['Orientation']) {
|
||||
case 1:
|
||||
// Normal
|
||||
return false;
|
||||
case 2:
|
||||
// 888888
|
||||
// 88
|
||||
// 8888
|
||||
// 88
|
||||
// 88
|
||||
|
||||
return '-flop';
|
||||
case 3:
|
||||
|
||||
// 88
|
||||
// 88
|
||||
// 8888
|
||||
// 88
|
||||
// 888888
|
||||
|
||||
return '-flip -flop';
|
||||
case 4:
|
||||
// 88
|
||||
// 88
|
||||
// 8888
|
||||
// 88
|
||||
// 888888
|
||||
|
||||
return '-flip';
|
||||
case 5:
|
||||
// 8888888888
|
||||
// 88 88
|
||||
// 88
|
||||
|
||||
return '-rotate 90 -flop';
|
||||
case 6:
|
||||
// 88
|
||||
// 88 88
|
||||
// 8888888888
|
||||
|
||||
return '-rotate 90';
|
||||
case 7:
|
||||
// 88
|
||||
// 88 88
|
||||
// 8888888888
|
||||
|
||||
return '-rotate "-90" -flop';
|
||||
case 8:
|
||||
// 8888888888
|
||||
// 88 88
|
||||
// 88
|
||||
|
||||
return '-rotate "-90"';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ImagePNG extends ImageBase {
|
||||
@ -497,7 +428,11 @@ class ImageBMP extends ImageBase {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (PHP_MAJOR_VERSION <= 7 && PHP_MINOR_VERSION < 2) {
|
||||
include 'inc/image/bmp.php';
|
||||
class ImageWEBP extends ImageBase {
|
||||
public function from() {
|
||||
$this->image = @imagecreatefromwebp($this->src);
|
||||
}
|
||||
public function to($src) {
|
||||
imagewebp($this->image, $src);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user