Compare commits

...
Sign in to create a new pull request.

294 commits

Author SHA1 Message Date
2c20bd4074 banners: add 15 hours for posting banner 2025-09-01 22:58:27 +02:00
5171ddcabe Merge pull request 'Fix long reported post cuts dismiss buttons #129' (#150) from 129-long-reported-posts into config
Reviewed-on: leftypol/leftypol#150
2025-08-31 08:15:12 -05:00
e6d188799e pages.php: append dismiss buttons after the post, remove horrendous workaround 2025-08-31 15:12:33 +02:00
22acbbf113 style.css: port post body padding to div.report 2025-08-31 15:03:32 +02:00
60866725c4 Merge pull request 'Backport refactor' (#148) from group-drivers into config
Reviewed-on: leftypol/leftypol#148
2025-08-30 15:40:38 -05:00
1ec3f464e2 Use Model directory 2025-08-30 22:29:26 +02:00
80d7ad9322 Data: make more proper Model directory and namespace 2025-08-30 22:27:05 +02:00
1c78a7ad21 Use Log subdirectory 2025-08-30 22:26:37 +02:00
c5ce9a8030 LogDriver: move to subdirectory 2025-08-30 22:24:45 +02:00
3cb6fa3b5a Use cache subdirectory 2025-08-30 22:24:39 +02:00
311a5477f8 CacheDriver: moved to subdirectory 2025-08-30 22:13:45 +02:00
8ee471c868 maintenance.php: fix deleted count couting 2025-08-30 21:58:43 +02:00
02d181f7b8 ReportQueries.php: fix filterReports always returning valid reports 2025-08-30 21:54:49 +02:00
73b2bebe56 Merge pull request 'Trim ban parameters' (#147) from trim-ban-args into config
Reviewed-on: leftypol/leftypol#147
2025-08-22 18:13:51 -05:00
9d5493989e pages.php: trim ban POST parameters 2025-08-23 01:11:30 +02:00
dade469b91 faq: update the onion link 2025-08-21 22:10:12 +02:00
88bb079d45 functions.php: tweak compressed page creation 2025-07-29 17:36:18 +02:00
a6d4772db0 SearchService.php: adjust wrong array_values 2025-07-24 00:25:28 +02:00
00a4435a61 Merge pull request 'Fix #135: make the flag filter work' (#143) from fix-flag-filter into config
Reviewed-on: leftypol/leftypol#143
2025-07-23 16:49:04 -05:00
783f4d0a78 SearchService.php: fix matchStrings not matching flags 2025-07-23 23:47:46 +02:00
67d9a638db SearchService.php: fix flag filtering 2025-07-23 23:47:19 +02:00
f491fa0cf8 UserSearchQueries.php: fix query generation 2025-07-23 23:47:19 +02:00
f6b74cb282 pages.php: gzip buffering of json ban stream was causing excessive latency 2025-07-23 21:19:35 +02:00
cbbc267b65 Merge pull request 'Fix #139 Stream the ban list from the database' (#140) from bans-json into config
Reviewed-on: leftypol/leftypol#140
2025-07-23 14:06:40 -05:00
dc6c33d095 pages.php: remove obsolete parameter from json bans stream 2025-07-23 21:06:04 +02:00
29b81517a9 public_banlist: remove obsolete parameter from json stream 2025-07-23 21:06:04 +02:00
3a8588ab7f bans.php: load database rows in a streaming fashion 2025-07-23 21:06:04 +02:00
68c0b2ae69 public_banlist: format 2025-07-23 21:06:04 +02:00
938bcd2889 Revert "Display banned post contents if reason does not contain cp"
This reverts commit 9f00630b40.
2025-07-23 19:40:20 +02:00
0eb075705c Revert "Add porn to public banlist theme"
This reverts commit 8b5688e12a.
2025-07-23 19:40:10 +02:00
46a53aa483 Merge pull request 'Fix #136: remove filtered out keywords and filters' (#137) from board-search-too-broad into config
Reviewed-on: leftypol/leftypol#137
2025-07-12 19:00:17 -05:00
45603467bc SearchService.php: remove filtered out keywords and filters 2025-07-13 01:54:47 +02:00
cef13f0b31 config.php: increase the default search max weight 2025-07-12 00:07:58 +02:00
28ce41dedc SearchService.php: prettier weight logging 2025-07-12 00:07:22 +02:00
1767c7b316 UserPostQueries.php: fix post search ordering 2025-07-08 21:38:00 +02:00
360161b1fc SearchService.php: handle filters getting trimmed into oblivion 2025-07-08 21:31:39 +02:00
75ce687af5 SearchService.php: fix search detecting spurious flags 2025-07-08 21:17:18 +02:00
8028c1bd9a context.php: handle the search.flag_filter configuration option 2025-07-08 21:09:02 +02:00
d695ff2582 config.php: add flag_filter option, off by default 2025-07-08 21:08:02 +02:00
6e9d0a4e77 Merge pull request 'Refactor the search subsystem' (#127) from rework-search into config
Reviewed-on: leftypol/leftypol#127
2025-07-08 13:59:52 -05:00
26ad13bbea search_form.html: make the search for UI less terrible 2025-07-08 00:33:19 +02:00
746f36e9f2 search_form.html: update filters help 2025-07-08 00:33:19 +02:00
626a3fd683 search_form.html: format 2025-07-08 00:33:19 +02:00
3c5cca9265 search.php: supply flag filter enablement 2025-07-08 00:33:19 +02:00
c9610bb237 search.php: handle query too broad error 2025-07-08 00:33:19 +02:00
1723db32a6 style.css: remove fieldset label css (interfered with threads in the search page) 2025-07-08 00:33:19 +02:00
bbcfbf78ae docker: print PHP errors on docker log 2025-07-08 00:33:19 +02:00
29f476e3a9 search.php: refactor, use SearchService 2025-07-08 00:33:19 +02:00
b2308b1ffe context.php: add SearchQueries 2025-07-08 00:33:19 +02:00
519036e625 context.php: add SearchService 2025-07-08 00:33:19 +02:00
b542ee949a context.php: add UserPostQueries 2025-07-08 00:33:19 +02:00
48f29774c3 config.php: add search.max_length 2025-07-08 00:33:19 +02:00
701007ea95 config.php: better documentation for search.boards 2025-07-08 00:33:19 +02:00
330d6b7c01 config.php: add search.max_weight to the configuration options 2025-07-08 00:33:19 +02:00
78e31f653c SearchService.php: expose if the flag filter is enabled 2025-07-08 00:33:19 +02:00
a749cc829c SearchService.php: check for query too broad 2025-07-08 00:33:19 +02:00
b61cb8acf3 SearchService.php: add SearchQueries and checkFlood 2025-07-08 00:33:19 +02:00
63be2bca4e SearchService.php: limit and expose the searchable boards 2025-07-08 00:33:19 +02:00
d6ca80f7fd SearchService.php: add search service with fully linear regex parser 2025-07-08 00:33:19 +02:00
6b4542edb3 Flags.php: add the flags that come embedded with php 2025-07-08 00:33:19 +02:00
6e153daa1d FiltersParseResult.php: add 2025-07-08 00:33:19 +02:00
46d5dc0fd4 SearchFilters.php: add 2025-07-08 00:33:19 +02:00
c6cec16971 UserPostQueries.php: add searchPost method 2025-07-08 00:33:16 +02:00
57cf3abed4 maintenance.php: add SearchQueries cleanup 2025-07-07 22:07:14 +02:00
73df37918d SearchQueries.php: add 2025-07-07 22:07:11 +02:00
9878b0c4d2 static: use the alunya favicon 2025-07-03 22:22:40 +02:00
342d4f9608 youtube.js: use www.youtube-nocookie.com 2025-06-10 18:07:42 +02:00
76bcbccbf4 tsuki: fix post color 2025-06-10 18:00:48 +02:00
9b1f4debad README.md: trim 2025-06-02 00:51:17 +02:00
5bc1009dfb faq: remove broken link to readlist thread 2025-06-01 23:38:30 +02:00
b55c299842 Merge pull request 'Fix #124' (#126) from fix-catalog-hover into config
Reviewed-on: leftypol/leftypol#126
2025-06-01 16:26:13 -05:00
ca25c85984 tsuki.css: change hover color 2025-06-01 23:21:13 +02:00
ee84baf87d tsuki.css: remove duplicated css 2025-06-01 23:19:34 +02:00
d4bc625c05 szalet.css: change catalog hover color 2025-06-01 23:16:29 +02:00
34bf9b2261 dark_spook.css: change catalog hover color 2025-06-01 23:12:38 +02:00
f580100121 dark.css: change catalog hover color 2025-06-01 23:10:39 +02:00
46c5f17db8 dark_red.css: change catalog hover color 2025-06-01 23:08:19 +02:00
c9926802f7 database.php: force utf8mb4 for PDO 2025-06-01 16:05:43 +02:00
e76c4eeed4 static: add the logo (somehow it wasn't in the repo!) 2025-05-31 23:04:41 +02:00
3c9c86901a MemcacheCacheDriver.php: fix error check 2025-05-31 18:10:51 +02:00
7aae8be1ae MemcacheCacheDriver.php: add detailed errors 2025-05-31 18:08:54 +02:00
2b30929bc9 MemcacheCacheDriver.php: fix init again 2025-05-30 00:22:42 +02:00
eed2b2986a Merge pull request 'Fix memecached driver' (#123) from memcached-fix into config
Reviewed-on: leftypol/leftypol#123
2025-05-29 17:07:23 -05:00
de9d118390 cache.php: update MemcachedCacheDriver initialization 2025-05-30 00:06:23 +02:00
37e771f0be MemcacheCacheDriver.php: fix configuration 2025-05-30 00:01:55 +02:00
b9a29927f3 RedisCacheDriver.php: use CacheDriverTrait 2025-05-30 00:01:55 +02:00
84a22a788e CacheDriverTrait.php: add CacheDriverTrait to share code 2025-05-30 00:01:53 +02:00
126314846c banners: add new banner 2025-05-13 23:11:55 +02:00
e10cd5fab4 banners: add closeted dengoid flag 2025-05-12 22:43:53 +02:00
e5145cd98c banners: add thread on leftypol banner 2025-05-12 22:43:50 +02:00
dcb978e31a delete-stray-images.php: handle missing files 2025-05-11 22:47:19 +02:00
0c898abdfe flags: remove grace flag 2025-05-08 21:44:56 +02:00
6e5a56ff0e flags: remove incorrectly named rodina flags 2025-05-08 21:44:20 +02:00
a779f12144 flags: correct rodina naming 2025-05-03 22:21:06 +02:00
a586025a62 Merge pull request 'Set a minimum width for text in posts' (#122) from anon-css-fixes into config
Reviewed-on: leftypol/leftypol#122
2025-05-02 16:20:17 -05:00
18624963d5 style.css: set minimum width for text 2025-05-02 22:57:04 +02:00
2cc7a70e4c flags: fix typo in Stirner flag name 2025-04-25 23:31:33 +02:00
e3693f2d19 flags: add 420-tan flag 2025-04-25 14:23:52 +02:00
63228d04be pages.php: use Context provided config 2025-04-23 23:16:18 +02:00
4cf6fd3838 Merge pull request 'Better auth' (#121) from better-auth into config
Reviewed-on: leftypol/leftypol#121
2025-04-23 15:52:29 -05:00
f7dae74522 auth.php: format 2025-04-23 22:45:32 +02:00
3e3b71211a auth.php: use php 8.4 cost for bcrypt 2025-04-23 22:45:32 +02:00
2f69af8267 auth.php: remove unused global 2025-04-23 22:45:32 +02:00
fowr
4ef10e26fc auth.php: cleanup ununsed function 2025-04-23 22:45:32 +02:00
fowr
715005ec96 auth.php: no need to repass version anymore in test_password 2025-04-23 22:45:32 +02:00
f7bef11ac9 auth.php: use pre-hashing for BCRYPT 2025-04-23 22:45:32 +02:00
fowr
a8a947af65 config.php: bump password crypt version 2025-04-23 22:45:32 +02:00
fowr
3510f05fe8 auth.php: use password_hash with bcrypt and password_verify for login 2025-04-23 22:45:32 +02:00
2cac548b4d Merge pull request 'Remove all nntpchan support' (#120) from nntpchan-remove into config
Reviewed-on: leftypol/leftypol#120
2025-04-23 15:07:29 -05:00
24e43a5aa1 nntpchan: removed handler code 2025-04-23 22:03:32 +02:00
634f769592 config.php: remove nntpchan options 2025-04-23 22:02:57 +02:00
b9938d9513 post.php: remove nttpchan 2025-04-23 22:01:49 +02:00
55508e6210 Merge pull request 'Hande crossboard invalid links with strikethrough' (#119) from cross-strikethrough into config
Reviewed-on: leftypol/leftypol#119
2025-04-23 13:13:05 -05:00
a28d9a4246 functions.php: minor code semplification for crossbooard citations 2025-04-23 19:57:22 +02:00
439730f216 functions.php: add crossboard strikethrough for invalid cites 2025-04-23 19:52:10 +02:00
f2d0ac7341 functions.php: minor format 2025-04-23 19:34:49 +02:00
6763f7b416 Merge pull request 'Secured PDF and DJVU file handling and thumbnailing' (#118) from secure-file-handling into config
Reviewed-on: leftypol/leftypol#118
2025-04-21 09:45:48 -05:00
19c0868320 docker: add djvulibre and ghostscript tools 2025-04-21 16:42:20 +02:00
8cb6a76f0a post.php: add safe djvu thumbnail generation 2025-04-21 16:42:20 +02:00
8282d5cd63 post.php: implement safe PDF thumbnailing 2025-04-21 16:42:17 +02:00
28f75c8aed config.php: add missing djvu_file_thumbnail option 2025-04-21 16:42:17 +02:00
ff94e58f2e config.php: update pdf_file_thumbnail documentation 2025-04-21 16:42:17 +02:00
181f4ba49a config.php: add generic invalidfile error 2025-04-21 14:53:09 +02:00
bac5032b56 rules.html: make ordinance 2 into rule 15 2025-04-20 16:45:18 +02:00
8d189eb9c8 Merge pull request 'Better authentication token generation' (#116) from auth-sha256 into config
Reviewed-on: leftypol/leftypol#116
2025-04-16 07:47:29 -05:00
3c0779992a auth.php: use secure salt source, use a cryptographically secure hashing algorithm for login tokens 2025-04-16 14:44:21 +02:00
8cffb479fa functions.php: use secure_hash where appropriate 2025-04-16 14:44:21 +02:00
08c2d6f5d1 composer: add hide.php 2025-04-16 14:44:21 +02:00
acdf792daf hide.php: add hide.php to the functions 2025-04-16 14:44:21 +02:00
54dcf79a7f search.php: fix formatting 2025-04-15 22:31:18 +02:00
2e1cb7995f pages.php: better input validation in recent_posts page 2025-04-14 17:49:30 +02:00
8ee3f4c81d flags: add Tania's flag 2025-04-13 17:02:03 +02:00
68b2911dfd flags: add Rodinia's /leftypol/ flag 2025-04-13 16:50:49 +02:00
612d1cfc57 flags: add Rodinia's /get/ flag 2025-04-13 16:49:50 +02:00
5613baca05 flags: add grace flag 2025-04-13 16:49:13 +02:00
92fc2daa9c post.php: fix undefined exif_stripped 2025-03-29 08:55:15 +01:00
d224c0af23 post.php: skip resize only if already stripped 2025-03-29 00:34:37 +01:00
c1c20bdab2 post.php: fix typo 2025-03-29 00:32:56 +01:00
42e850091a config.php: remove minimum_copy_resize 2025-03-29 00:31:29 +01:00
87029580b6 post.php: default minimum_copy_resize to true by removing it 2025-03-29 00:30:59 +01:00
cf74272806 Merge pull request 'Remove functionality' (#114) from remove-tesseract into config
Reviewed-on: leftypol/leftypol#114
2025-03-28 09:06:52 -05:00
336c40b0f7 remove all tesseract traces 2025-03-28 15:05:01 +01:00
81c02be563 style.css: reduce margin between multiple files 2025-03-27 01:23:37 +01:00
fa56876c36 style.css: set minimum file container width and multifile margin 2025-03-27 00:17:17 +01:00
8da10af101 frames.html: break long words to prevent page getting cut 2025-03-26 13:55:04 +01:00
9431536112 frames.html: trim 2025-03-26 13:54:55 +01:00
025f1c4221 news.html: remove spurious stuff from css 2025-03-26 13:40:28 +01:00
501e696891 banners: remove just monika banner
This reverts commit 6bcf22aa7e.
2025-03-23 17:05:20 +01:00
557e43e38f Merge pull request 'Fix #66 Use equilateral triangle as the post menu button' (#113) from 10-post-menu-triangle into config
Reviewed-on: leftypol/leftypol#113
2025-03-17 15:24:36 -05:00
6ee8670401 post-menu.js: use unicode code with variant selector for equilateral triangle 2025-03-17 16:20:49 +01:00
c4d7bc39de Merge pull request 'WIP Port LogDriver from upstream' (#111) from log-driver-port into config
Reviewed-on: leftypol/leftypol#111
2025-03-16 12:37:21 -05:00
b2029d2533 context.php: log error if log_system is erroneous 2025-03-16 18:17:42 +01:00
5b4d1b7f4c pages.php: use LogDriver 2025-03-16 18:10:19 +01:00
665e3d339a post.php: use LogDriver 2025-03-16 18:07:58 +01:00
6be3f4bbff post.php: update LogDriver 2025-03-16 18:07:58 +01:00
8f7db3bdef FileLogDriver.php: flush writes to file 2025-03-16 18:07:58 +01:00
cca8d88d91 config.php: update LogDriver configuration 2025-03-16 18:07:58 +01:00
Zankaria
268bd84128 Refactor the logging system 2025-03-16 18:07:58 +01:00
2c0c003b2c context.php: report deprecation notice on syslog option 2025-03-16 18:07:58 +01:00
fe4813867b context.php: fix log init 2025-03-16 18:07:58 +01:00
6132084b4b context.php: update LogDriver 2025-03-16 18:07:57 +01:00
79523f8251 context.php: initial add LogDriver 2025-03-16 18:07:13 +01:00
707fb62c04 log-driver.php: split up log driver 2025-03-16 18:05:40 +01:00
6f9ea52212 faq: update git repo (again) 2025-02-24 16:14:53 +01:00
9ea7bf5938 Merge pull request 'Fix 108: fix backlink' (#110) from 108-buggy-op-backlink into config
Reviewed-on: leftypol/leftypol#110
2025-02-22 16:37:10 -06:00
c45fbd5ec9 show-backlinks.js: fix backlink 2025-02-22 23:33:48 +01:00
77ae0b101e Merge pull request 'Finish the work of the previous PR' (#107) from ip-note-queries-refactor-2 into config
Reviewed-on: leftypol/leftypol#107
2025-02-21 05:40:27 -06:00
94b5c82517 post.php: pass Context to filters 2025-02-21 12:35:28 +01:00
e753588aeb filters.php: use IpNoteQueries 2025-02-21 12:35:13 +01:00
c3a1960427 filters.php: trim 2025-02-21 12:33:28 +01:00
8754eda6c7 Merge pull request 'refactor IP notes backend' (#106) from ip-note-queries into config
Reviewed-on: leftypol/leftypol#106
2025-02-21 05:01:50 -06:00
b61653230d context.php: provide IpNoteQueries 2025-02-21 11:55:44 +01:00
041958e20f Merge pull request 'Fix #71 fix matrix report url fragment' (#105) from 71-incorrect-fragment into config
Reviewed-on: leftypol/leftypol#105
2025-02-20 15:32:06 -06:00
218dc42aca post.php: fix matrix report url fragment 2025-02-20 22:29:00 +01:00
5d52f6b32a pages.php: use IpNoteQueries 2025-02-18 12:17:06 +01:00
583b50af56 IpNoteQueries.php: add ip notes wrapper 2025-02-18 12:16:52 +01:00
e5ba5feb25 youtube.js: adjust referrer policy 2025-02-17 21:42:55 +01:00
9256c15c46 rules.html: extend rule 10 to extreme fetishes 2025-02-17 18:51:24 +01:00
0819c509fa dark_red.css: increase contrast 2025-02-17 18:01:46 +01:00
9a4ce56d86 dark_red.css: always use Verdana font if available 2025-02-17 17:21:32 +01:00
230dc4760b Merge branch '67-strikethrough-invalid-citations' into 'config'
Resolve "Strikethrough invalid citations"

Closes #67

See merge request leftypol/leftypol!33
2025-02-16 23:43:29 +00:00
c73a893e54 functions.php: strikethrough bad citations 2025-02-17 00:31:59 +01:00
2da6c95aa5 functions.php: remove unused parameter from markup 2025-02-16 23:54:01 +01:00
9a5b79452f pages.php: remove unused code 2025-02-16 23:52:56 +01:00
Zankaria
bf060ce174 UserPostQueries.php: fix paging boundaries 2025-02-14 12:50:15 +01:00
b1989a69b0 flags: add Alunya flag 2025-02-14 09:40:04 +01:00
9746293fed inline-expanding.js: fit expanded images into the screen's height (port of soyjak party feature) 2025-02-13 10:35:01 +01:00
3a44b68c41 Merge branch '68-embed-field-is-not-reset-after-submitting-post' into 'config'
Resolve "Embed field is not reset after submitting post"

Closes #68

See merge request leftypol/leftypol!31
2025-02-12 23:04:22 +00:00
aed49a6188 ajax.js: clear embed 2025-02-13 00:00:20 +01:00
e3252306a5 Merge branch '69-set-style-menu-in-lower-bar' into 'config'
Resolve "Set style menu in lower bar"

Closes #69

See merge request leftypol/leftypol!30
2025-02-12 22:42:11 +00:00
9bb7f0d2f2 general.js: do not move the style-select in the options 2025-02-12 23:30:00 +01:00
a73456c283 overboards.php: update exclude list 2025-02-12 21:57:17 +01:00
f7b33678a4 Merge branch 'embeds' into 'config'
Rework youtube.js for better youtube shorts support

See merge request leftypol/leftypol!29
2025-02-12 20:39:27 +00:00
89d79a5278 youtube.js: remove broken onion proxy 2025-02-12 21:36:18 +01:00
aa18595adf main.js: do not initialize captcha if not required by the mode 2025-02-12 21:32:14 +01:00
d8cafc8fd8 config.php: better youtube embedding regex 2025-02-12 21:17:49 +01:00
3c282852a3 config.php: adjust youtube embedding for youtube.js 2025-02-12 21:17:49 +01:00
4787e98c02 youtube.js: rework 2025-02-12 21:17:46 +01:00
6df32997bf config.php: adjust youtube short referer policy (breaks on ids with a - dash) 2025-02-08 23:02:47 +01:00
856d124c88 config.php: adjust youtube shorts sizes 2025-02-08 22:40:38 +01:00
da7266d5e1 config.php: adjust embedding margins 2025-02-08 22:29:14 +01:00
e677751010 config.php: add youtube shorts embedding 2025-02-08 22:22:23 +01:00
91f53552c9 ajax.js: trigger the captcha only if present and needed 2025-02-08 21:30:25 +01:00
262e8971bd main.js: store captcha mode 2025-02-08 21:30:08 +01:00
d48ba6113a ajax.js: fix previous commit 2025-02-08 18:52:20 +01:00
222c4c4d27 ajax.js: do not send request if captcha does not have a response 2025-02-08 18:48:16 +01:00
ae6b9edf45 Merge branch '11-block-posting-if-catpcha-isn-t-ok' into 'config'
Resolve "Block posting if catpcha isn't ok"

Closes #11

See merge request leftypol/leftypol!22
2025-02-08 17:21:22 +00:00
a70b6e6ec9 main.js: use new config style 2025-02-08 18:15:07 +01:00
4eb12479ec post_form.html: use new config style 2025-02-08 18:14:52 +01:00
d34b1e105e captcha_script.html: use new config style 2025-02-08 18:14:32 +01:00
a45b40f59c header.html: use new config style 2025-02-08 18:14:06 +01:00
01fbbfc61a main.js: add minimal jsdoc to captcha wrappers 2025-02-08 00:18:21 +01:00
3be6111fa4 main.js: require captcha before sending a post 2025-02-08 00:18:20 +01:00
869e2602ef dark_red.css: fix email/name color 2025-02-08 00:13:19 +01:00
6f181effac dark_red.css: make post no color override style.css setting 2025-02-07 23:08:10 +01:00
26e56eff06 dark_red.css: use button color for post numbers 2025-02-07 22:47:10 +01:00
efabee7006 Merge branch '16-missing-user-menu-filters' into 'config'
Resolve "Missing user menu filters"

Closes #16

See merge request leftypol/leftypol!27
2025-02-07 16:46:59 +00:00
95d403dbee post-filter.js: remove buggy forced anon support causing regression 2025-02-07 17:44:08 +01:00
7b691a2330 overboards: handle no included boards 2025-02-06 15:11:54 +01:00
deysu
2e84ebcbe2 Add banners as default functionality & display them on mod login / dashboard when enabled (#513)
* Add banner support as a default, integrated option.
Used lainchan's original banner script, authored by barrucadu

* Display banners on moderator login and dashboard

* Remove memes & better directory structure
2025-02-06 14:50:12 +01:00
fd0e3113b3 Merge branch '44-extract-the-default-theme-selection-buttons-to-js' into 'config'
Resolve "Extract the default theme selection buttons to JS"

Closes #44

See merge request leftypol/leftypol!26
2025-02-05 23:54:29 +00:00
12be74304a style-select.js: adapt js 2025-02-06 00:52:22 +01:00
b1b96ece81 style-select.js: format 2025-02-06 00:14:18 +01:00
cd2a99f543 style-select-simple.js: add simple style selector back in 2025-02-06 00:00:29 +01:00
95f8d2a159 main.js: remove init style chooser 2025-02-06 00:00:10 +01:00
a7f4864cd9 youtube.js: make video iframe spacing equal to image spacing 2025-02-04 21:43:38 +01:00
800fa6f734 UserPostQueries.php: fix broken query 2025-02-03 22:17:18 +01:00
55e9c4514e Merge branch 'image-margins-css' into 'config'
Image margins css

See merge request leftypol/leftypol!25
2025-02-03 19:11:07 +00:00
45959559cf style.css: fix full-image padding 2025-02-03 20:04:00 +01:00
a4e4074f81 style.css: add margin at bottom of single images 2025-02-03 20:01:49 +01:00
9ff71351a4 style.css: move margin from body to head 2025-02-03 20:01:37 +01:00
4c17b0af9e style.css: remove unused css 2025-02-03 19:53:12 +01:00
936669acb4 Merge branch '26-change-the-warning-color' into 'config'
Resolve "Change the warning color"

Closes #26

See merge request leftypol/leftypol!23
2025-01-24 22:45:43 +00:00
08674df6e7 style.css: make the warning orange 2025-01-24 23:43:55 +01:00
964ea43b84 config.php: add functional vocaroo embedding support 2025-01-22 00:52:44 +01:00
7cea9fb942 faq: remove duplicated header tag 2025-01-21 22:51:50 +01:00
7a50a603ae news.html: add missing meta tags 2025-01-21 22:51:29 +01:00
ceb6638b99 faq: fix wrong matrix link 2025-01-21 22:19:01 +01:00
216477177f faq: Text file thumbnail generation is not supported 2025-01-21 22:17:14 +01:00
d30e0a1a9b pages.php: add missing mod global 2025-01-21 21:59:25 +01:00
0af5185dd9 pages.php: use context to fetch config in mod_view_catalog 2025-01-21 21:58:52 +01:00
06e622fc99 faq: update faq matrix antichamber link 2025-01-20 19:59:49 +01:00
7de4ca2133 css: add missing media queries to css styles 2025-01-08 22:15:16 +01:00
55d21a4dd5 style.css: set the margin of omitted post indicator like margin on intro 2025-01-07 18:55:08 +01:00
22626b4a6f style.css: harmonize head margin 2025-01-07 18:48:12 +01:00
020d66ffdc style.css: harmonize post body margins 2025-01-07 18:39:31 +01:00
7e881e2ec3 style.css: remove extra padding from OP 2025-01-07 18:29:03 +01:00
66b1e7d569 show-backlinks.js: simplify 2024-12-29 15:33:10 +01:00
cd2a4b5fac Revert "pages.php: remove seemingly unused code"
This reverts commit e52465753e.
2024-12-29 15:13:07 +01:00
8b82318bd8 Merge branch '57-add-backlinks-to-op' into 'config'
Resolve "Add backlinks to OP"

Closes #57

See merge request leftypol/leftypol!20
2024-12-29 00:14:12 +00:00
87a30b8aab show-backlinks.js: add support for op post 2024-12-29 01:12:12 +01:00
9430b1b78e Merge branch 'fix-security-token' into 'config'
Fix security token endpoin in browse user posts by IP

See merge request leftypol/leftypol!19
2024-12-28 23:27:54 +00:00
34c521aab1 pages.php: remove seemingly unused code 2024-12-29 00:27:35 +01:00
7c982da304 view_ip.html: use ?/IP endpoint to remove bans and add notes 2024-12-29 00:19:48 +01:00
e52465753e pages.php: remove seemingly unused code 2024-12-29 00:19:17 +01:00
c718eb70b0 Revert "pages.php: QUICKFIX handle unban and notes in mod_user_posts_by_ip to workaround security token issue"
This reverts commit 4b49019282.
2024-12-29 00:01:41 +01:00
e87f50407c pages.php: change security token of mod_user_posts_by_ip 2024-12-29 00:01:09 +01:00
4b49019282 pages.php: QUICKFIX handle unban and notes in mod_user_posts_by_ip to workaround security token issue 2024-12-27 20:14:47 +01:00
a45106b65f pages.php: adjust redirect on mod_ip 2024-12-27 19:59:02 +01:00
5c176b95e7 Merge branch '22-browse-by-password' into 'config'
Resolve "Browse by password"

Closes #22

See merge request leftypol/leftypol!17
2024-12-27 18:47:32 +00:00
0c23445d72 pages.php: slightly better ip error 2024-12-27 19:43:22 +01:00
d4781d6f00 view_passwd.html: add view password template 2024-12-27 19:39:24 +01:00
10401bd094 pages.php: add mod_user_posts_by_passwd implementation 2024-12-27 19:39:21 +01:00
0609e36ca4 config.php: deprecate ip_recentposts for recent_user_posts 2024-12-27 19:35:25 +01:00
e1514784db mod.php: add mod_user_posts_by_passwd 2024-12-27 19:35:21 +01:00
00ef803950 ip.html: add password link 2024-12-27 19:33:25 +01:00
2ec23083a4 view_ip.html: use mod_user_posts_by_ip 2024-12-27 19:33:25 +01:00
39b6a60257 view_ip.html: use user_posts_list.html 2024-12-27 19:33:25 +01:00
d29de1a9ef user_posts_list.html: add template to list posts 2024-12-27 19:33:25 +01:00
720ca77875 mod.php: add mod_user_posts_by_ip 2024-12-27 19:33:18 +01:00
bf25002295 ip.html: use mod_user_posts_by_ip 2024-12-27 19:32:15 +01:00
11c5748888 pages.php: refactor mod_ip into mod_user_posts_by_ip 2024-12-27 19:32:15 +01:00
27b7f1c60d UserPostQueries.php: add post fetching by password 2024-12-27 19:32:15 +01:00
4ede43b192 UserPostQueries.php: refactor implementation 2024-12-27 19:32:15 +01:00
bebe786c5b mod.php: dots are not required in the cursors 2024-12-27 19:31:11 +01:00
4f7d872d0d Tinyboard.php: remove dead code 2024-12-27 19:05:34 +01:00
750ac11581 Tinyboard.php: trim 2024-12-27 19:05:10 +01:00
1249fd765e Merge branch 'user-passwd-hash-backport' into 'config'
User passwd hash backport

See merge request leftypol/leftypol!18
2024-12-27 15:31:22 +00:00
de75d12bc5 hash-passwords.php: minor refactor 2024-12-27 15:41:15 +01:00
57324f169d post.php: restore password length limit 2024-12-27 15:41:12 +01:00
fowr
8b2f002582 hash poster passwords 2024-12-27 15:27:46 +01:00
fowr
c7bb61f2ff posts.sql: update column password 2024-12-27 15:18:28 +01:00
6bcf22aa7e banners: add just monika banner 2024-12-18 15:10:44 +01:00
dbb44dfa91 banners: add deny defend depose banner 2024-12-18 15:10:21 +01:00
0205ea2da6 Track banners 2024-12-18 15:07:40 +01:00
49b0ade2d3 lib: remove vendored library 2024-12-17 17:55:57 +01:00
da1970d16f Merge branch 'user-browse-refactor' into 'config'
User browse wrapper class

See merge request leftypol/leftypol!16
2024-12-17 16:21:46 +00:00
0b4ac333c7 context.php: use UserPostQueries 2024-12-17 17:18:38 +01:00
3c2bc57245 pages.php: use UserPostQueries for mod_page_ip 2024-12-17 17:18:38 +01:00
71416afc75 UserPostQueries.php: add user post queries class 2024-12-17 17:18:38 +01:00
1d41ffbe4f anti-bot.php: include missing variable 2024-12-17 17:18:03 +01:00
b197c9ed43 docker: remove unused options in mysql 2024-12-17 17:17:44 +01:00
b03130fcb4 anti-bot.php: retry transaction upon deadlock 2024-12-17 17:13:49 +01:00
21155cbb06 functions.php: optimize listBoards a bit 2024-12-14 00:16:38 +01:00
129 changed files with 4028 additions and 3713 deletions

3
.gitignore vendored
View file

@ -70,9 +70,6 @@ tf/
/mod/
/random/
# Banners
static/banners/*
#Fonts
stylesheets/fonts

View file

@ -14,7 +14,7 @@ Requirements
PHP 8.0 is explicitly supported. PHP 7.x should be compatable.
2. MySQL/MariaDB server >= 5.5.3
3. [Composer](https://getcomposer.org/) (To install various packages)
4. [mbstring](http://www.php.net/manual/en/mbstring.installation.php)
4. [mbstring](http://www.php.net/manual/en/mbstring.installation.php)
5. [PHP GD](http://www.php.net/manual/en/intro.image.php)
6. [PHP PDO](http://www.php.net/manual/en/intro.pdo.php)
@ -44,7 +44,7 @@ Installation
development version with:
git clone git://git.leftypol.org/leftypol/leftypol.git
2. run ```composer install``` inside the directory
3. Navigate to ```install.php``` in your web browser and follow the
prompts.
@ -80,7 +80,7 @@ find support from a variety of sources:
* For support, reply to the sticky on our [/tech/](https://leftypol.org/tech/) board.
### Tinyboard support
vichan, and by extension lainchan and leftypol, is based on a Tinyboard, so both engines have very much in common. These links may be helpful for you as well:
vichan, and by extension lainchan and leftypol, is based on a Tinyboard, so both engines have very much in common. These links may be helpful for you as well:
* Tinyboard documentation can be found [here](https://web.archive.org/web/20121016074303/http://tinyboard.org/docs/?p=Main_Page).

View file

@ -28,8 +28,6 @@ services:
#MySQL Service
db:
image: mysql:8.0.35
restart: unless-stopped
tty: true
ports:
- "3306:3306"
environment:

View file

@ -25,6 +25,7 @@
"inc/polyfill.php",
"inc/error.php",
"inc/functions.php",
"inc/functions/hide.php",
"inc/functions/net.php"
]
},

View file

@ -18,6 +18,8 @@ RUN apk add --no-cache \
graphicsmagick \
gifsicle \
ffmpeg \
djvulibre \
ghostscript \
bind-tools \
gettext \
gettext-dev \

View file

@ -1,5 +1,7 @@
[www]
access.log = /proc/self/fd/2
php_admin_value[error_log] = /proc/self/fd/2
php_admin_flag[log_errors] = on
; Ensure worker stdout and stderr are sent to the main error log.
catch_workers_output = yes

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View file

@ -1,5 +1,5 @@
<?php
namespace Vichan\Data\Driver;
namespace Vichan\Data\Driver\Cache;
defined('TINYBOARD') or exit;

View file

@ -1,5 +1,5 @@
<?php
namespace Vichan\Data\Driver;
namespace Vichan\Data\Driver\Cache;
defined('TINYBOARD') or exit;

View file

@ -1,5 +1,5 @@
<?php
namespace Vichan\Data\Driver;
namespace Vichan\Data\Driver\Cache;
defined('TINYBOARD') or exit;

View file

@ -0,0 +1,20 @@
<?php
namespace Vichan\Data\Driver\Cache;
trait CacheDriverTrait {
/**
* Tries to interpret the uri as a path to a unix socket.
*
* @param string $uri
* @return ?string The path to the socket, null if it cannot be interpreted as such.
*/
private static function asUnixSocketPath(string $uri): ?string {
if (str_starts_with($uri, 'unix:')) {
return \substr($uri, 5);
} elseif (str_starts_with($uri, ':')) {
return \substr($uri, 1);
}
return null;
}
}

View file

@ -1,5 +1,5 @@
<?php
namespace Vichan\Data\Driver;
namespace Vichan\Data\Driver\Cache;
defined('TINYBOARD') or exit;

View file

@ -0,0 +1,74 @@
<?php
namespace Vichan\Data\Driver\Cache;
defined('TINYBOARD') or exit;
class MemcachedCacheDriver implements CacheDriver {
use CacheDriverTrait;
private \Memcached $inner;
public function __construct(string $prefix, string $server_uri, int $server_port, int $server_weight) {
$this->inner = new \Memcached();
if (!$this->inner->setOption(\Memcached::OPT_BINARY_PROTOCOL, true)) {
$err = $this->inner->getResultMessage();
throw new \RuntimeException("Unable to set the memcached protocol: '$err'");
}
if (!$this->inner->setOption(\Memcached::OPT_PREFIX_KEY, $prefix)) {
$err = $this->inner->getResultMessage();
throw new \RuntimeException("Unable to set the memcached prefix: '$err'");
}
$maybe_unix_path = self::asUnixSocketPath($server_uri);
$is_unix = $maybe_unix_path !== null;
if ($is_unix) {
$server_uri = $maybe_unix_path;
}
// Memcached keeps the server connections open across requests.
$current_servers = $this->inner->getServerList();
$found_in_curr = false;
foreach ($current_servers as $curr) {
// Ignore the port if the server is connected with a unix socket.
if ($curr['host'] === $server_uri && ($is_unix || $curr['port'] === $server_port)) {
$found_in_curr = true;
}
}
if (!$found_in_curr) {
if (!empty($current_servers)) {
if (!$this->inner->resetServerList()) {
$err = $this->inner->getResultMessage();
throw new \RuntimeException("Unable to reset the memcached server list: '$err'");
}
}
if (!$this->inner->addServer($server_uri, $server_port, $server_weight)) {
$err = $this->inner->getResultMessage();
throw new \RuntimeException("Unable to add memcached servers: '$err'");
}
}
}
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();
}
}

View file

@ -1,5 +1,5 @@
<?php
namespace Vichan\Data\Driver;
namespace Vichan\Data\Driver\Cache;
defined('TINYBOARD') or exit;

View file

@ -1,22 +1,21 @@
<?php
namespace Vichan\Data\Driver;
namespace Vichan\Data\Driver\Cache;
defined('TINYBOARD') or exit;
class RedisCacheDriver implements CacheDriver {
use CacheDriverTrait;
private string $prefix;
private \Redis $inner;
public function __construct(string $prefix, string $host, ?int $port, ?string $password, int $database) {
$this->inner = new \Redis();
if (str_starts_with($host, 'unix:') || str_starts_with($host, ':')) {
$ret = \explode(':', $host);
if (count($ret) < 2) {
throw new \RuntimeException("Invalid unix socket path $host");
}
// Unix socket.
$this->inner->connect($ret[1]);
$maybe_unix = self::asUnixSocketPath($host);
if ($maybe_unix !== null) {
$this->inner->connect($maybe_unix);
} elseif ($port === null) {
$this->inner->connect($host);
} else {

View file

@ -0,0 +1,28 @@
<?php
namespace Vichan\Data\Driver\Log;
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);
}
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Vichan\Data\Driver\Log;
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);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Vichan\Data\Driver\Log;
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;
}

View file

@ -0,0 +1,26 @@
<?php
namespace Vichan\Data\Driver\Log;
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');
}
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Vichan\Data\Driver\Log;
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");
}
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Vichan\Data\Driver\Log;
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);
}
}
}
}

View file

@ -1,43 +0,0 @@
<?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();
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace Vichan\Data;
use Vichan\Data\Driver\Cache\CacheDriver;
class IpNoteQueries {
private \PDO $pdo;
private CacheDriver $cache;
public function __construct(\PDO $pdo, CacheDriver $cache) {
$this->pdo = $pdo;
$this->cache = $cache;
}
/**
* Get all the notes relative to an IP.
*
* @param string $ip The IP of the notes. THE STRING IS NOT VALIDATED.
* @return array Returns an array of notes sorted by the most recent. Includes the username of the mods.
*/
public function getByIp(string $ip) {
$ret = $this->cache->get("ip_note_queries_$ip");
if ($ret !== null) {
return $ret;
}
$query = $this->pdo->prepare('SELECT `ip_notes`.*, `username` FROM `ip_notes` LEFT JOIN `mods` ON `mod` = `mods`.`id` WHERE `ip` = :ip ORDER BY `time` DESC');
$query->bindValue(':ip', $ip);
$query->execute();
$ret = $query->fetchAll(\PDO::FETCH_ASSOC);
$this->cache->set("ip_note_queries_$ip", $ret);
return $ret;
}
/**
* Creates a new note relative to the given ip.
*
* @param string $ip The IP of the note. THE STRING IS NOT VALIDATED.
* @param int $mod_id The id of the mod who created the note.
* @param string $body The text of the note.
* @return void
*/
public function add(string $ip, int $mod_id, string $body) {
$query = $this->pdo->prepare('INSERT INTO `ip_notes` (`ip`, `mod`, `time`, `body`) VALUES (:ip, :mod, :time, :body)');
$query->bindValue(':ip', $ip);
$query->bindValue(':mod', $mod_id);
$query->bindValue(':time', time());
$query->bindValue(':body', $body);
$query->execute();
$this->cache->delete("ip_note_queries_$ip");
}
/**
* Delete a note only if it's of a particular IP address.
*
* @param int $id The id of the note.
* @param int $ip The expected IP of the note. THE STRING IS NOT VALIDATED.
* @return bool True if any note was deleted.
*/
public function deleteWhereIp(int $id, string $ip): bool {
$query = $this->pdo->prepare('DELETE FROM `ip_notes` WHERE `ip` = :ip AND `id` = :id');
$query->bindValue(':ip', $ip);
$query->bindValue(':id', $id);
$query->execute();
$any = $query->rowCount() != 0;
if ($any) {
$this->cache->delete("ip_note_queries_$ip");
}
return $any;
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace Vichan\Data\Model;
class FiltersParseResult {
public array $body = [];
public ?string $subject = null;
public ?string $name = null;
public ?string $board = null;
public ?string $flag = null;
public ?int $id = null;
public ?int $thread = null;
}

285
inc/Data/Model/Flags.php Normal file
View file

@ -0,0 +1,285 @@
<?php
namespace Vichan\Data\Model;
class Flags {
/**
* Short names of the flags embedded with vichan.
*/
public const EMBEDDED_FLAGS = [
'a1',
'a2',
'ac',
'ad',
'ae',
'af',
'ag',
'ai',
'al',
'am',
'an',
'ao',
'ap',
'aq',
'ar',
'as',
'at',
'au',
'aw',
'ax',
'az',
'ba',
'bb',
'bd',
'be',
'bf',
'bg',
'bh',
'bi',
'bj',
'bl',
'bm',
'bn',
'bo',
'bq',
'br',
'bs',
'bt',
'bu',
'bv',
'bw',
'by',
'bz',
'ca',
'cat',
'cc',
'cd',
'cf',
'cg',
'ch',
'ci',
'ck',
'cl',
'cm',
'cn',
'co',
'cp',
'cr',
'cs',
'cu',
'cv',
'cw',
'cx',
'cy',
'cz',
'de',
'dg',
'dj',
'dk',
'dm',
'do',
'dz',
'ea',
'ec',
'ee',
'eg',
'eh',
'er',
'es',
'et',
'eu',
'fi',
'fj',
'fk',
'fm',
'fo',
'fr',
'fx',
'ga',
'gb',
'gd',
'ge',
'gf',
'gg',
'gh',
'gi',
'gl',
'gm',
'gn',
'gp',
'gq',
'gr',
'gs',
'gt',
'gu',
'gw',
'gy',
'hk',
'hm',
'hn',
'hr',
'ht',
'hu',
'ic',
'id',
'ie',
'il',
'im',
'in',
'io',
'iq',
'ir',
'is',
'it',
'je',
'jm',
'jo',
'jp',
'ke',
'kg',
'kh',
'ki',
'km',
'kn',
'kp',
'kr',
'kw',
'ky',
'kz',
'la',
'lb',
'lc',
'li',
'lk',
'lr',
'ls',
'lt',
'lu',
'lv',
'ly',
'ma',
'mc',
'md',
'me',
'mf',
'mg',
'mh',
'mk',
'ml',
'mm',
'mn',
'mo',
'mp',
'mq',
'mr',
'ms',
'mt',
'mu',
'mv',
'mw',
'mx',
'my',
'mz',
'na',
'nc',
'ne',
'nf',
'ng',
'ni',
'nl',
'no',
'np',
'nr',
'nt',
'nu',
'nz',
'o1',
'om',
'pa',
'pe',
'pf',
'pg',
'ph',
'pk',
'pl',
'pm',
'pn',
'pr',
'ps',
'pt',
'pw',
'py',
'qa',
're',
'ro',
'rs',
'ru',
'rw',
'sa',
'sb',
'sc',
'sd',
'se',
'sf',
'sg',
'sh',
'si',
'sj',
'sk',
'sl',
'sm',
'sn',
'so',
'sr',
'ss',
'st',
'su',
'sv',
'sx',
'sy',
'sz',
'ta',
'tc',
'td',
'tf',
'tg',
'th',
'ti',
'tj',
'tk',
'tl',
'tm',
'tn',
'to',
'tp',
'tr',
'tt',
'tv',
'tw',
'tz',
'ua',
'ug',
'uk',
'um',
'us',
'uy',
'uz',
'va',
'vc',
've',
'vg',
'vi',
'vn',
'vu',
'wf',
'ws',
'xx',
'ye',
'yt',
'yu',
'za',
'zm',
'zr',
'zw',
];
}

View file

@ -0,0 +1,15 @@
<?php
namespace Vichan\Data\Model;
/**
* A page of user posts.
*/
class PageFetchResult {
/**
* @var array[array] Posts grouped by board uri.
*/
public array $by_uri;
public ?string $cursor_prev;
public ?string $cursor_next;
}

View file

@ -0,0 +1,32 @@
<?php
namespace Vichan\Data\Model;
/**
* POD with the fragments of each filter.
*/
class SearchFilters {
/**
* @var array<array<string>>
*/
public array $body = [];
/**
* @var array<string>
*/
public array $subject = [];
/**
* @var array<string>
*/
public array $name = [];
/**
* @var ?string
*/
public ?string $board = null;
/**
* @var array<string>
*/
public array $flag = [];
public ?int $id = null;
public ?int $thread = null;
public float $weight = 0;
}

View file

@ -89,7 +89,7 @@ class ReportQueries {
// Get the reports without a post.
$invalid = [];
foreach ($raw_reports as $report) {
if (isset($report_posts[$report['board']][$report['post']])) {
if (!isset($report_posts[$report['board']][$report['post']])) {
$invalid[] = $report;
}
}
@ -129,7 +129,7 @@ class ReportQueries {
$query = $this->pdo->prepare('SELECT `board`, `post`, `id` FROM `reports`');
$query->execute();
$raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC);
$valid_reports = $this->filterReports($raw_reports, false, null);
$valid_reports = $this->filterReports($raw_reports, false);
$count = \count($valid_reports);
return $count;
@ -176,7 +176,7 @@ class ReportQueries {
$query = $this->pdo->prepare('SELECT `board`, `post`, `id` FROM `reports`');
$query->execute();
$raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC);
$invalid_reports = $this->filterReports($raw_reports, true, null);
$invalid_reports = $this->filterReports($raw_reports, true);
foreach ($invalid_reports as $report) {
$this->deleteReportImpl($report['board'], $report['post']);

View file

@ -0,0 +1,98 @@
<?php
namespace Vichan\Data;
/**
* Implements flood control for search queries.
*/
class SearchQueries {
private \PDO $pdo;
private int $queries_for_single;
private int $range_for_single;
private int $queries_for_all;
private int $range_for_all;
private bool $auto_gc;
private function checkFloodImpl(string $ip, string $phrase): bool {
$now = \time();
$query = $this->pdo->prepare("SELECT COUNT(2) FROM `search_queries` WHERE `ip` = :ip AND `time` > :time");
$query->bindValue(':ip', $ip);
$query->bindValue(':time', $now - $this->range_for_single, \PDO::PARAM_INT);
$query->execute();
if ($query->fetchColumn() > $this->queries_for_single) {
return true;
}
$query = $this->pdo->prepare("SELECT COUNT(2) FROM `search_queries` WHERE `time` > :time");
$query->bindValue(':time', $now - $this->range_for_all, \PDO::PARAM_INT);
$query->execute();
if ($query->fetchColumn() > $this->queries_for_all) {
return true;
}
$query = $this->pdo->prepare("INSERT INTO `search_queries` VALUES (:ip, :time, :query)");
$query->bindValue(':ip', $ip);
$query->bindValue(':time', $now, \PDO::PARAM_INT);
$query->bindValue(':query', $phrase);
$query->execute();
if ($this->auto_gc) {
$this->purgeExpired();
}
return false;
}
/**
* @param \PDO $pdo PDO to access the DB.
* @param int $queries_for_single Maximum number of queries for a single IP, in seconds.
* @param int $range_for_single Maximum age of the oldest query to consider from a single IP.
* @param int $queries_for_all Maximum number of queries for all IPs.
* @param int $range_for_all Maximum age of the oldest query to consider from all IPs, in seconds.
* @param bool $auto_gc If to run the cleanup at every check. Must be invoked from the outside otherwise.
*/
public function __construct(
\PDO $pdo,
int $queries_for_single,
int $range_for_single,
int $queries_for_all,
int $range_for_all,
bool $auto_gc
) {
$this->pdo = $pdo;
$this->queries_for_single = $queries_for_single;
$this->range_for_single = $range_for_single;
$this->queries_for_all = $queries_for_all;
$this->range_for_all = $range_for_all;
$this->auto_gc = $auto_gc;
}
/**
* Check if the IP-query pair overflows the limit.
*
* @param string $ip Source IP.
* @param string $phrase The search query.
* @return bool True if the request goes over the limit.
*/
public function checkFlood(string $ip, string $phrase): bool {
$this->pdo->beginTransaction();
try {
$ret = $this->checkFloodImpl($ip, $phrase);
$this->pdo->commit();
return $ret;
} catch (\Exception $e) {
$this->pdo->rollBack();
throw $e;
}
}
public function purgeExpired(): int {
// Cleanup search queries table.
$query = $this->pdo->prepare("DELETE FROM `search_queries` WHERE `time` <= :expiry_limit");
$query->bindValue(':expiry_limit', \time() - $this->range_for_all, \PDO::PARAM_INT);
$query->execute();
return $query->rowCount();
}
}

View file

@ -0,0 +1,275 @@
<?php
namespace Vichan\Data;
use Vichan\Functions\Net;
use Vichan\Data\Model\PageFetchResult;
/**
* Browse user posts
*/
class UserPostQueries {
private const CURSOR_TYPE_PREV = 'p';
private const CURSOR_TYPE_NEXT = 'n';
private \PDO $pdo;
/**
* Escapes wildcards from LIKE operators using the default escape character.
*/
private static function escapeLike(string $str): string {
// Escape any existing escape characters.
$str = \str_replace('\\', '\\\\', $str);
// Escape wildcard characters.
$str = \str_replace('%', '\\%', $str);
$str = \str_replace('_', '\\_', $str);
return $str;
}
/**
* Joins the fragments of filter into a list of bindable parameters for the CONCAT sql function.
* Given prefix = cat and fragments_count = 3, we get [ "'%'", ":cat0%", "'%', ":cat1", "'%'" ":cat2%", "'%'" ];
*
* @param string $prefix The prefix for the parameter binding
* @param int $fragments_count MUST BE >= 1.
* @return array
*/
private static function arrayOfFragments(string $prefix, int $fragments_count): array {
$args = [ "'%'" ];
for ($i = 0; $i < $fragments_count; $i++) {
$args[] = ":$prefix$i";
$args[] = "'%'";
}
return $args;
}
public function __construct(\PDO $pdo) {
$this->pdo = $pdo;
}
private function paginate(array $board_uris, int $page_size, ?string $cursor, callable $callback): PageFetchResult {
// Decode the cursor.
if ($cursor !== null) {
list($cursor_type, $uri_id_cursor_map) = Net\decode_cursor($cursor);
} else {
// Defaults if $cursor is an invalid string.
$cursor_type = null;
$uri_id_cursor_map = [];
}
$next_cursor_map = [];
$prev_cursor_map = [];
$rows = [];
foreach ($board_uris as $uri) {
// Extract the cursor relative to the board.
$start_id = null;
if ($cursor_type !== null && isset($uri_id_cursor_map[$uri])) {
$value = $uri_id_cursor_map[$uri];
if (\is_numeric($value)) {
$start_id = (int)$value;
}
}
$posts = $callback($uri, $cursor_type, $start_id, $page_size);
$posts_count = \count($posts);
// By fetching one extra post bellow and/or above the limit, we know if there are any posts beside the current page.
if ($posts_count === $page_size + 2) {
$has_extra_prev_post = true;
$has_extra_end_post = true;
} else {
/*
* If the id we start fetching from is also the first id fetched from the DB, then we exclude it from
* the results, noting that we fetched 1 more posts than we needed, and it was before the current page.
* Hence, we have no extra post at the end and no next page.
*/
$has_extra_prev_post = $start_id !== null && $start_id === (int)$posts[0]['id'];
$has_extra_end_post = !$has_extra_prev_post && $posts_count > $page_size;
}
// Get the previous cursor, if any.
if ($has_extra_prev_post) {
\array_shift($posts);
$posts_count--;
// Select the most recent post.
$prev_cursor_map[$uri] = $posts[0]['id'];
}
// Get the next cursor, if any.
if ($has_extra_end_post) {
\array_pop($posts);
// Select the oldest post.
$next_cursor_map[$uri] = $posts[$posts_count - 2]['id'];
}
$rows[$uri] = $posts;
}
$res = new PageFetchResult();
$res->by_uri = $rows;
$res->cursor_prev = !empty($prev_cursor_map) ? Net\encode_cursor(self::CURSOR_TYPE_PREV, $prev_cursor_map) : null;
$res->cursor_next = !empty($next_cursor_map) ? Net\encode_cursor(self::CURSOR_TYPE_NEXT, $next_cursor_map) : null;
return $res;
}
/**
* Fetch a page of user posts.
*
* @param array $board_uris The uris of the boards that should be included.
* @param string $ip The IP of the target user.
* @param integer $page_size The Number of posts that should be fetched.
* @param string|null $cursor The directional cursor to fetch the next or previous page. Null to start from the beginning.
* @return PageFetchResult
*/
public function fetchPaginatedByIp(array $board_uris, string $ip, int $page_size, ?string $cursor = null): PageFetchResult {
return $this->paginate($board_uris, $page_size, $cursor, function($uri, $cursor_type, $start_id, $page_size) use ($ip) {
if ($cursor_type === null) {
$query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri));
$query->bindValue(':ip', $ip);
$query->bindValue(':limit', $page_size + 1, \PDO::PARAM_INT); // Always fetch more.
$query->execute();
return $query->fetchAll(\PDO::FETCH_ASSOC);
} elseif ($cursor_type === self::CURSOR_TYPE_NEXT) {
$query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip AND `id` <= :start_id ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri));
$query->bindValue(':ip', $ip);
$query->bindValue(':start_id', $start_id, \PDO::PARAM_INT);
$query->bindValue(':limit', $page_size + 2, \PDO::PARAM_INT); // Always fetch more.
$query->execute();
return $query->fetchAll(\PDO::FETCH_ASSOC);
} elseif ($cursor_type === self::CURSOR_TYPE_PREV) {
$query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip AND `id` >= :start_id ORDER BY `sticky` ASC, `id` ASC LIMIT :limit', $uri));
$query->bindValue(':ip', $ip);
$query->bindValue(':start_id', $start_id, \PDO::PARAM_INT);
$query->bindValue(':limit', $page_size + 2, \PDO::PARAM_INT); // Always fetch more.
$query->execute();
return \array_reverse($query->fetchAll(\PDO::FETCH_ASSOC));
} else {
throw new \RuntimeException("Unknown cursor type '$cursor_type'");
}
});
}
/**
* Fetch a page of user posts.
*
* @param array $board_uris The uris of the boards that should be included.
* @param string $password The password of the target user.
* @param integer $page_size The Number of posts that should be fetched.
* @param string|null $cursor The directional cursor to fetch the next or previous page. Null to start from the beginning.
* @return PageFetchResult
*/
public function fetchPaginateByPassword(array $board_uris, string $password, int $page_size, ?string $cursor = null): PageFetchResult {
return $this->paginate($board_uris, $page_size, $cursor, function($uri, $cursor_type, $start_id, $page_size) use ($password) {
if ($cursor_type === null) {
$query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `password` = :password ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri));
$query->bindValue(':password', $password);
$query->bindValue(':limit', $page_size + 1, \PDO::PARAM_INT); // Always fetch more.
$query->execute();
return $query->fetchAll(\PDO::FETCH_ASSOC);
} elseif ($cursor_type === self::CURSOR_TYPE_NEXT) {
$query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `password` = :password AND `id` <= :start_id ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri));
$query->bindValue(':password', $password);
$query->bindValue(':start_id', $start_id, \PDO::PARAM_INT);
$query->bindValue(':limit', $page_size + 2, \PDO::PARAM_INT); // Always fetch more.
$query->execute();
return $query->fetchAll(\PDO::FETCH_ASSOC);
} elseif ($cursor_type === self::CURSOR_TYPE_PREV) {
$query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `password` = :password AND `id` >= :start_id ORDER BY `sticky` ASC, `id` ASC LIMIT :limit', $uri));
$query->bindValue(':password', $password);
$query->bindValue(':start_id', $start_id, \PDO::PARAM_INT);
$query->bindValue(':limit', $page_size + 2, \PDO::PARAM_INT); // Always fetch more.
$query->execute();
return \array_reverse($query->fetchAll(\PDO::FETCH_ASSOC));
} else {
throw new \RuntimeException("Unknown cursor type '$cursor_type'");
}
});
}
/**
* Search among the user posts with the given filters.
* The subject, name and elements of the bodies filters are fragments which are joined together with wildcards, to
* allow for more flexible filtering.
*
* @param string $board The board where to search in.
* @param array<string> $subject Fragments of the subject filter.
* @param array<string> $name Fragments of the name filter.
* @param array<string> $flags An array of the flag names to search among the HTML.
* @param ?int $id Post id filter.
* @param ?int $thread Thread id filter.
* @param array<array<string>> $bodies An array whose element are arrays containing the fragments of multiple body filters, each
* searched independently from the others
* @param integer $limit The maximum number of results.
* @throws PDOException On error.
* @return array<array>
*/
public function searchPosts(string $board, array $subject, array $name, array $flags, ?int $id, ?int $thread, array $bodies, int $limit): array {
$where_acc = [];
if (!empty($subject)) {
$like_arg = self::arrayOfFragments('subj', \count($subject));
$where_acc[] = 'subject LIKE CONCAT(' . \implode(', ', $like_arg) . ')';
}
if (!empty($name)) {
$like_arg = self::arrayOfFragments('name', \count($name));
$where_acc[] = 'name LIKE CONCAT(' . \implode(', ', $like_arg) . ')';
}
if (!empty($flags)) {
$flag_acc = [];
for ($i = 0; $i < \count($flags); $i++) {
// Yes, vichan stores the flag inside the generated HTML. Now you know why it's slow as shit.
// English lacks the words to express my feelings about it in a satisfying manner.
$flag_acc[] = "CONCAT('%<tinyboard flag alt>', :flag$i, '</tinyboard>%')";
}
$where_acc[] = 'body_nomarkup LIKE (' . \implode(' OR ', $flag_acc) . ')';
}
if ($id !== null) {
$where_acc[] = 'id = :id';
}
if ($thread !== null) {
$where_acc[] = 'thread = :thread';
}
for ($i = 0; $i < \count($bodies); $i++) {
$body = $bodies[$i];
$like_arg = self::arrayOfFragments("body_{$i}_", \count($body));
$where_acc[] = 'body_nomarkup LIKE CONCAT(' . \implode(', ', $like_arg) . ')';
}
if (empty($where_acc)) {
return [];
}
$sql = "SELECT * FROM `posts_$board` WHERE " . \implode(' AND ', $where_acc) . ' ORDER BY `time` DESC LIMIT :limit';
$query = $this->pdo->prepare($sql);
for ($i = 0; $i < \count($subject); $i++) {
$query->bindValue(":subj$i", self::escapeLike($subject[$i]));
}
for ($i = 0; $i < \count($name); $i++) {
$query->bindValue(":name$i", self::escapeLike($name[$i]));
}
for ($i = 0; $i < \count($flags); $i++) {
$query->bindValue(":flag$i", self::escapeLike($flags[$i]));
}
if ($id !== null) {
$query->bindValue(':id', $id, \PDO::PARAM_INT);
}
if ($thread !== null) {
$query->bindValue(':thread', $thread, \PDO::PARAM_INT);
}
for ($body_i = 0; $body_i < \count($bodies); $body_i++) {
$body = $bodies[$body_i];
for ($i = 0; $i < \count($body); $i++) {
$query->bindValue(":body_{$body_i}_{$i}", self::escapeLike($body[$i]));
}
}
$query->bindValue(':limit', $limit, \PDO::PARAM_INT);
$query->execute();
return $query->fetchAll(\PDO::FETCH_ASSOC);
}
}

View file

@ -0,0 +1,432 @@
<?php
namespace Vichan\Service;
use Vichan\Data\Driver\Log\LogDriver;
use Vichan\Data\{UserPostQueries, SearchQueries};
use Vichan\Data\Model\{FiltersParseResult, SearchFilters};
class SearchService {
private const COMMON_WORDS = [
'anon', 'thread', 'board', 'post', 'reply', 'image', 'topic', 'bump', 'sage', 'tripcode', 'groyper',
'mod', 'admin', 'ban', 'rules', 'sticky', 'archive', 'catalog', 'report', 'captcha', 'proxy', 'the',
'vpn', 'tor', 'doxx', 'spam', 'troll', 'bait', 'flame', 'greentext', 'copypasta', 'meme', 'this',
'shitpost', 'shitposting', 'edgy', 'kek', 'lulz', 'rekt', 'smug', 'lewd', 'nsfw', 'anonymous', 'glowie',
'cringe', 'normie', 'boomer', 'zoomer', 'incel', 'chad', 'stacy', 'simp', 'based', 'redpill', 'color',
'blackpill', 'whitepill', 'bluepill', 'clownworld', 'coomer', 'doomer', 'wojak', 'soyjak', 'pepe',
'style', 'weight', 'size', 'freedom', 'speech', 'censorship', 'moderation', 'community', 'anonymous',
'reply', 'search', 'group', 'merge', 'flatten', 'lock', 'unlock', 'hide', 'uyghur', 'soyshit', 'glow',
'also', 'only', 'just', 'even', 'very', 'than', 'then', 'that', 'this', 'with',
'from', 'into', 'onto', 'over', 'under', 'about', 'after', 'before', 'since', 'while',
'because', 'although', 'though', 'unless', 'until', 'where', 'which', 'whose', 'there', 'their',
'these', 'those', 'being', 'having', 'doing', 'going', 'would', 'could', 'should', 'shall', 'everything',
'might', 'must', 'will', 'have', 'been', 'were', 'wasn', 'aren', 'isn', 'does', 'isnt', 'mustnt',
'didn', 'hadn', 'hasn', 'dont', 'cant', 'wont', 'cannot', 'haven', 'weren', 'didnt', 'since',
'mustn', 'mightn', 'shouldn', 'wouldn', 'mightve', 'wouldve', 'shouldve', 'couldve', 'mustve',
'wasnt', 'werent', 'hasnt', 'hadnt', 'wont', 'wouldnt', 'shouldnt', 'couldnt', 'mightnt',
'each', 'such', 'some', 'most', 'many', 'more', 'much', 'less', 'few', 'none', 'although', 'because',
'both', 'either', 'neither', 'every', 'anyone', 'someone', 'everyone', 'nobody', 'nothing', 'so',
'above', 'below', 'along', 'across', 'among', 'until', 'and', 'but', 'or', 'nor', 'for', 'yet',
];
private const MAX_LENGTH_SUBJECT = 100; // posts.sql
private const MAX_LENGTH_NAME = 35; // posts.sql
private LogDriver $log;
private UserPostQueries $user_queries;
private SearchQueries $search_queries;
private ?array $flag_map;
private float $max_weight;
private int $max_query_length;
private int $post_limit;
private array $searchable_board_uris;
private static function truncateQuery(string $text, int $byteLimit): ?string {
if (\strlen($text) <= $byteLimit) {
return $text;
}
// Cut at byte length, trimming incomplete multibyte character at the end.
$cut = \mb_convert_encoding(\substr($text, 0, $byteLimit), 'UTF-8', 'UTF-8');
// Try the last space.
$spacePos = \strrpos($cut, ' ');
if ($spacePos !== false) {
return \substr($cut, 0, $spacePos);
}
// Fallback to the last word boundary.
if (\preg_match('/^(.+)\b/u', $cut, $m)) {
return $m[1];
}
// Too long but could not cut.
return null;
}
private static function trim(string $str): string {
return \trim($str, "* \n\r\t\v\0");
}
private static function unescape(string $str): string {
return \strtr($str, [
'\\\\' => '\\',
'\\*' => '*',
'\\"' => '"'
]);
}
/**
* Split the filter into fragments along the wildcards, handling escaping.
*
* @param string $str The full filter.
* @return array<string>
*/
private static function split(string $str): array {
// Split the fragments
return \preg_split('/(?:\\\\\\\\)*\\\\\*|(?:\\\\\\\\)*\*+/', $str);
}
private static function weightByContent(array $fragments): float {
$w = 0;
foreach ($fragments as $fragment) {
$short = \strlen($fragment) < 4;
if (\in_array($fragment, self::COMMON_WORDS)) {
$w += $short ? 16 : 6;
} elseif ($short) {
$w += 6;
}
}
return $w;
}
private static function filterAndWeight(string $filter): array {
$fragments = self::split($filter);
$acc = [];
$total_len = 0;
foreach ($fragments as $fragment) {
$fragment = self::trim(self::unescape($fragment));
if (!empty($fragment)) {
$total_len += \strlen($fragment);
$acc[] = $fragment;
}
}
$wildcard_weight = 0;
if (!empty($acc) && $total_len >= 0) {
// Interword wildcards
$interword = \min(\count($fragments) - 1, 0);
// Wildcards over the total length of the word. Ergo the number of fragments minus 1.
$perc = $interword / $total_len * 100;
$wildcard_weight = $perc + \count($fragments) * 2;
}
return [ $acc, $total_len, $wildcard_weight ];
}
/**
* Gets a subset of the given strings which match every filter.
*
* @param array<string> $fragments User provided fragments to search in the flags.
* @param array<string> $strings An array of strings.
* @return array<string> An array of strings, subset of $strings.
*/
private static function matchStrings(array $strings, array $fragments): array {
return \array_filter($strings, function ($str) use ($fragments) {
// Saves the last position. We use this to ensure the fragments are one after the other.
$last_ret = -1;
foreach ($fragments as $fragment) {
if ($last_ret + 1 > \strlen($fragment)) {
// Cannot possibly match.
return false;
}
$last_ret = \stripos($str, $fragment, $last_ret + 1);
if ($last_ret === false) {
// Exclude flags that don't match even a single fragment.
return false;
}
}
return true;
});
}
/**
* Parses a raw search query.
*
* @param string $raw_query Raw user query. Phrases are searched in the post bodies. The user can specify also
* additional filters in the <key>:<value> format.
* Available filters:
* - board: the board, value can be quoted
* - subject: post subject, value can be quoted, supports wildcards
* - name: post name, value can be quoted, supports wildcards
* - flag: post flag, value can be quoted, supports wildcards
* - id: post id, must be numeric
* - thread: thread id, must be numeric
* The remaining text is split into chunks and searched in the post body.
* @return FiltersParseResult
*/
public function parse(string $raw_query): FiltersParseResult{
$tres = self::truncateQuery($raw_query, $this->max_query_length);
if ($tres === null) {
throw new \RuntimeException('Could not truncate query');
}
$pres = \preg_match_all(
'/(?:
\b(board):
(?:
"([^"]+)" # [2] board: "quoted"
|
([^\s"]+) # [3] board: unquoted
)
|
\b(subject|name|flag):
(?:
"((?:\\\\\\\\|\\\\\"|\\\\\*|[^"\\\\])*)" # [5] quoted with wildcards
|
((?:\\\\\\\\|\\\\\*|[^\s\\\\])++) # [6] unquoted with wildcards
)
|
\b(id|thread):
(\d+) # [8] numeric only
|
"((?:\\\\\\\\|\\\\\"|\\\\\*|[^"\\\\])*)" # [9] quoted free text
|
([^"\s]++) # [10] unquoted free text block
)/iux',
$tres,
$matches,
\PREG_SET_ORDER
);
if ($pres === false) {
throw new \RuntimeException('Could not decode the query');
}
$filters = new FiltersParseResult();
foreach ($matches as $m) {
if (!empty($m[1])) {
// board (no wildcards).
$value = \trim(!empty($m[2]) ? $m[2] : $m[3], '/');
$filters->board = $value;
} elseif (!empty($m[4])) {
// subject, name, flag (with wildcards).
$key = \strtolower($m[4]);
$value = !empty($m[5]) ? $m[5] : $m[6];
if ($key === 'name') {
$filters->name = $value;
} elseif ($key === 'subject') {
$filters->subject = $value;
} else {
$filters->flag = $value;
}
} elseif (!empty($m[7])) {
$key = \strtolower($m[7]);
$value = (int)$m[8];
if ($key === 'id') {
$filters->id = $value;
} else {
$filters->thread = $value;
}
} elseif (!empty($m[9]) || !empty($m[10])) {
$value = !empty($m[9]) ? $m[9] : $m[10];
$filters->body[] = $value;
}
}
return $filters;
}
/**
* @param LogDriver $log Log river.
* @param UserPostQueries $user_queries User posts queries.
* @param SearchQueries $search_queries Search queries for flood detection.
* @param ?array $flag_map The key-value map of user flags, or null to disable flag search.
* @param float $max_weight The maximum weight of the parsed user query. Body filters that go beyond this limit are discarded.
* @param int $max_query_length Maximum length of the raw input query before it's truncated.
* @param int $post_limit Maximum number of results.
* @param ?array $searchable_board_uris The uris of the board that can be searched. Null to search all the boards.
*/
public function __construct(
LogDriver $log,
UserPostQueries $user_queries,
SearchQueries $search_queries,
?array $flag_map,
float $max_weight,
int $max_query_length,
int $post_limit,
?array $searchable_board_uris
) {
$this->log = $log;
$this->user_queries = $user_queries;
$this->search_queries = $search_queries;
$this->flag_map = $flag_map;
$this->max_weight = $max_weight;
$this->max_query_length = $max_query_length;
$this->post_limit = $post_limit;
$this->searchable_board_uris = $searchable_board_uris ?? listBoards(true);
}
/**
* Reduces the user provided filters and assigns them a total weight.
*
* @param FiltersParseResult $filters The filters to sanitize, reduce and weight.
* @return SearchFilters
*/
public function reduceAndWeight(FiltersParseResult $filters): SearchFilters {
$weighted = new SearchFilters();
if ($filters->subject !== null) {
list($fragments, $total_len, $wildcard_weight) = self::filterAndWeight($filters->subject);
if (!empty($fragments) && $total_len >= 0) {
if ($total_len <= self::MAX_LENGTH_SUBJECT) {
$weighted->subject = $fragments;
$weighted->weight += $wildcard_weight;
}
}
}
if ($filters->name !== null) {
list($fragments, $total_len, $wildcard_weight) = self::filterAndWeight($filters->name);
if (!empty($fragments) && $total_len >= 0) {
if ($total_len <= self::MAX_LENGTH_NAME) {
$weighted->name = $fragments;
$weighted->weight += $wildcard_weight;
}
}
}
// No wildcard support, and obligatory anyway so it weights 0.
$weighted->board = $filters->board;
if ($filters->flag !== null) {
$weighted->flag = [];
if (!empty($this->flag_map)) {
$max_flag_length = \array_reduce($this->flag_map, fn($max, $str) => \max($max, \strlen($str)), 0);
list($fragments, $total_len, $wildcard_weight) = self::filterAndWeight($filters->flag);
if (!empty($fragments) && $total_len >= 0) {
// Add 2 to account for possible wildcards on the ends.
if ($total_len <= $max_flag_length + 2) {
$weighted->flag = $fragments;
$weighted->weight += $wildcard_weight;
}
}
}
}
$weighted->id = $filters->id;
$weighted->thread = $filters->thread;
if (!empty($filters->body)) {
foreach ($filters->body as $keyword) {
list($fragments, $total_len, $wildcard_weight) = self::filterAndWeight($keyword);
if (!empty($fragments) && $total_len >= 0) {
$content_weight = self::weightByContent($fragments);
$str_weight = $content_weight + $wildcard_weight;
if ($str_weight + $weighted->weight <= $this->max_weight) {
$weighted->weight += $str_weight;
$weighted->body[] = $fragments;
}
}
}
}
return $weighted;
}
/**
* Run a search on user posts with the given filters.
*
* @param SearchFilters $filters An array of filters made by {@see self::parse()}.
* @param ?string $fallback_board Fallback board if there isn't a board filter.
* @return ?array Data array straight from the PDO, with all the fields in posts.sql, or null if the query was too broad.
*/
public function search(string $ip, string $raw_query, SearchFilters $filters, ?string $fallback_board): ?array {
$board = !empty($filters->board) ? $filters->board : $fallback_board;
if ($board === null) {
return [];
}
// Only board is specified.
if (empty($filters->subject) &&
empty($filters->name) &&
empty($filters->flag) &&
$filters->id === null &&
$filters->thread === null &&
empty($filters->body)
) {
return null;
}
if (!\in_array($board, $this->searchable_board_uris)) {
return [];
}
$weight_perc = ($filters->weight / $this->max_weight) * 100;
if ($weight_perc > 85) {
/// Over 85 of the weight.
$this->log->log(LogDriver::NOTICE, "$ip search: weight {$weight_perc}% ({$filters->weight}) query '$raw_query'");
} else {
$this->log->log(LogDriver::INFO, "$ip search: weight {$weight_perc}% ({$filters->weight}) query '$raw_query'");
}
$flags = [];
if (!empty($filters->flag) && !empty($this->flag_map)) {
// A double array_values is necessary in order to re-index the array, otherwise it's left with random indexes.
$reverse_flags = \array_values($this->flag_map);
$flags = \array_values($this->matchStrings($reverse_flags, $filters->flag));
if (empty($flags)) {
// The query doesn't match any flags so it will always fail anyway.
return [];
}
}
return $this->user_queries->searchPosts(
$board,
$filters->subject,
$filters->name,
$flags,
$filters->id,
$filters->thread,
$filters->body,
$this->post_limit
);
}
/**
* Check if the IP-query pair passes the limit.
*
* @param string $ip Source IP.
* @param string $phrase The search query.
* @return bool True if the request goes over the limit.
*/
public function checkFlood(string $ip, string $raw_query) {
return $this->search_queries->checkFlood($ip, $raw_query);
}
/**
* Returns the uris of the boards that may be searched.
*/
public function getSearchableBoards(): array {
return $this->searchable_board_uris;
}
/**
* @return bool True if the flag filter is enabled.
*/
public function isFlagFilterEnabled(): bool {
return !empty($this->flag_map);
}
}

View file

@ -196,49 +196,56 @@ function _create_antibot($pdo, $board, $thread) {
$antibot = new AntiBot(array($board, $thread));
try {
$pdo->beginTransaction();
retry_on_deadlock(3, function() use ($config, $pdo, $thread, $board, $antibot, $purged_old_antispam) {
try {
$pdo->beginTransaction();
// Delete old expired antispam, skipping those with NULL expiration timestamps (infinite lifetime).
if (!isset($purged_old_antispam) && $config['auto_maintenance']) {
$purged_old_antispam = true;
purge_old_antispam();
}
// Delete old expired antispam, skipping those with NULL expiration timestamps (infinite lifetime).
if (!isset($purged_old_antispam) && $config['auto_maintenance']) {
$purged_old_antispam = true;
purge_old_antispam();
}
// Keep the now invalid timestamps around for a bit to enable users to post if they're still on an old version of
// the HTML page.
// By virtue of existing, we know that we're making a new version of the page, and the user from now on may just reload.
if ($thread) {
$query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` = :thread AND `expires` IS NULL');
} else {
$query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` IS NULL AND `expires` IS NULL');
}
// Keep the now invalid timestamps around for a bit to enable users to post if they're still on an old version of
// the HTML page.
// By virtue of existing, we know that we're making a new version of the page, and the user from now on may just reload.
if ($thread) {
$query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` = :thread AND `expires` IS NULL');
} else {
$query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` IS NULL AND `expires` IS NULL');
}
$query->bindValue(':board', $board);
if ($thread) {
$query->bindValue(':thread', $thread);
}
$query->bindValue(':expires', $config['spam']['hidden_inputs_expire']);
// Throws on error.
$query->execute();
$query->bindValue(':board', $board);
if ($thread) {
$query->bindValue(':thread', $thread);
}
$query->bindValue(':expires', $config['spam']['hidden_inputs_expire']);
// Throws on error.
$query->execute();
$hash = $antibot->hash();
$hash = $antibot->hash();
// Insert an antispam with infinite life as the HTML page of a thread might last well beyond the expiry date.
$query = prepare('INSERT INTO ``antispam`` VALUES (:board, :thread, :hash, UNIX_TIMESTAMP(), NULL, 0)');
$query->bindValue(':board', $board);
$query->bindValue(':thread', $thread);
$query->bindValue(':hash', $hash);
// Throws on error.
$query->execute();
// Insert an antispam with infinite life as the HTML page of a thread might last well beyond the expiry date.
$query = prepare('INSERT INTO ``antispam`` VALUES (:board, :thread, :hash, UNIX_TIMESTAMP(), NULL, 0)');
$query->bindValue(':board', $board);
$query->bindValue(':thread', $thread);
$query->bindValue(':hash', $hash);
// Throws on error.
$query->execute();
$pdo->commit();
$pdo->commit();
} catch (\Exception $e) {
$pdo->rollBack();
throw $e;
}
});
} catch (\PDOException $e) {
$pdo->rollBack();
if ($e->errorInfo === null || $e->errorInfo[1] != MYSQL_ER_LOCK_DEADLOCK) {
throw $e;
} else {
error_log('Deadlock on _create_antibot while inserting, skipping');
\error_log('5 or more deadlocks on _create_antibot while inserting, skipping');
}
}

View file

@ -285,68 +285,68 @@ class Bans {
}
}
static public function stream_json($out = false, $filter_ips = false, $filter_staff = false, $board_access = false, $hide_regexes = []) {
$query = query("SELECT ``bans``.*, `username` FROM ``bans``
LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`
ORDER BY `created` DESC") or error(db_error());
$bans = $query->fetchAll(PDO::FETCH_ASSOC);
static public function stream_json($filter_ips = false, $filter_staff = false, $board_access = false, $hide_message = false) {
if ($board_access && $board_access[0] == '*') {
$board_access = false;
}
$out ? fputs($out, "[") : print("[");
$query = query("SELECT ``bans``.*, `username` FROM ``bans``
LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`
ORDER BY `created` DESC") or error(db_error());
$end = end($bans);
print('[');
foreach ($bans as &$ban) {
$ban['mask'] = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
$has_previous = false;
$hide_message = false;
foreach ($hide_regexes as $regex) {
if(preg_match($regex, $ban['reason'])) {
$hide_message = true;
break;
while (true) {
$ban = $query->fetch(PDO::FETCH_ASSOC);
if (\is_array($ban)) {
$ban['mask'] = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
if ($ban['post'] && !$hide_message) {
$post = \json_decode($ban['post']);
$ban['message'] = isset($post->body) ? $post->body : 0;
}
}
unset($ban['ipstart'], $ban['ipend'], $ban['post'], $ban['creator']);
if ($ban['post'] && !$hide_message) {
$post = json_decode($ban['post']);
$ban['message'] = isset($post->body) ? $post->body : 0;
}
unset($ban['ipstart'], $ban['ipend'], $ban['post'], $ban['creator']);
if ($board_access === false || in_array ($ban['board'], $board_access)) {
$ban['access'] = true;
}
if (filter_var($ban['mask'], FILTER_VALIDATE_IP) !== false) {
$ban['single_addr'] = true;
}
if ($filter_staff || ($board_access !== false && !in_array($ban['board'], $board_access))) {
$ban['username'] = '?';
}
if ($filter_ips || ($board_access !== false && !in_array($ban['board'], $board_access))) {
@list($ban['mask'], $subnet) = explode("/", $ban['mask']);
$ban['mask'] = preg_split("/[\.:]/", $ban['mask']);
$ban['mask'] = array_slice($ban['mask'], 0, 2);
$ban['mask'] = implode(".", $ban['mask']);
$ban['mask'] .= ".x.x";
if (isset ($subnet)) {
$ban['mask'] .= "/$subnet";
if ($board_access === false || in_array ($ban['board'], $board_access)) {
$ban['access'] = true;
}
$ban['masked'] = true;
}
$json = json_encode($ban);
$out ? fputs($out, $json) : print($json);
if (filter_var($ban['mask'], FILTER_VALIDATE_IP) !== false) {
$ban['single_addr'] = true;
}
if ($filter_staff || ($board_access !== false && !\in_array($ban['board'], $board_access))) {
$ban['username'] = '?';
}
if ($filter_ips || ($board_access !== false && !\in_array($ban['board'], $board_access))) {
@list($ban['mask'], $subnet) = explode("/", $ban['mask']);
$ban['mask'] = \preg_split("/[\.:]/", $ban['mask']);
$ban['mask'] = \array_slice($ban['mask'], 0, 2);
$ban['mask'] = \implode(".", $ban['mask']);
$ban['mask'] .= ".x.x";
if (isset($subnet)) {
$ban['mask'] .= "/$subnet";
}
$ban['masked'] = true;
}
if ($ban['id'] != $end['id']) {
$out ? fputs($out, ",") : print(",");
$json = \json_encode($ban);
// Add a comma if there's a previous row.
if ($has_previous) {
print(',');
}
$has_previous = true;
print($json);
} else {
break;
}
}
$out ? fputs($out, "]") : print("]");
print(']');
}
static public function seen($ban_id) {

View file

@ -4,7 +4,7 @@
* Copyright (c) 2010-2013 Tinyboard Development Group
*/
use Vichan\Data\Driver\{CacheDriver, ApcuCacheDriver, ArrayCacheDriver, FsCacheDriver, MemcachedCacheDriver, NoneCacheDriver, RedisCacheDriver};
use Vichan\Data\Driver\Cache\{CacheDriver, ApcuCacheDriver, ArrayCacheDriver, FsCacheDriver, MemcachedCacheDriver, NoneCacheDriver, RedisCacheDriver};
defined('TINYBOARD') or exit;
@ -15,10 +15,18 @@ class Cache {
switch ($config['cache']['enabled']) {
case 'memcached':
return new MemcachedCacheDriver(
$config['cache']['prefix'],
$config['cache']['memcached']
);
$prefix = $config['cache']['prefix'];
$uri = $config['cache']['memcached'][0];
$port = 0;
$weight = 0;
if (isset($config['cache']['memcached'][1]) && $config['cache']['memcached'][1] !== null) {
$port = \intval($config['cache']['memcached'][1]);
}
if (isset($config['cache']['memcached'][2]) && $config['cache']['memcached'][2] !== null) {
$weight = \intval($config['cache']['memcached'][2]);
}
return new MemcachedCacheDriver($prefix, $uri, $port, $weight);
case 'redis':
$port = $config['cache']['redis'][1];
$port = empty($port) ? null : intval($port);

View file

@ -63,9 +63,29 @@
// been generated. This keeps the script from querying the database and causing strain when not needed.
$config['has_installed'] = '.installed';
// Use syslog() for logging all error messages and unauthorized login attempts.
// Deprecated, use 'log_system'.
$config['syslog'] = false;
$config['log_system'] = [
/*
* Log all error messages and unauthorized login attempts.
* Can be "syslog", "error_log" (default), "file", or "stderr".
*/
'type' => 'error_log',
// The application name used by the logging system. Defaults to "tinyboard" for backwards compatibility.
'name' => 'tinyboard',
/*
* Only relevant if 'log_system' is set to "syslog". If true, double print the logs also in stderr. Defaults to
* false.
*/
'syslog_stderr' => false,
/*
* Only relevant if "log_system" is set to `file`. Sets the file that vichan will log to. Defaults to
* '/var/log/vichan.log'.
*/
'file_path' => '/var/log/vichan.log',
];
// Use `host` via shell_exec() to lookup hostnames, avoiding query timeouts. May not work on your system.
// Requires safe_mode to be disabled.
$config['dns_system'] = false;
@ -200,6 +220,9 @@
// Used to salt secure tripcodes ("##trip") and poster IDs (if enabled).
$config['secure_trip_salt'] = ')(*&^%$#@!98765432190zyxwvutsrqponmlkjihgfedcba';
// Used to salt poster passwords.
$config['secure_password_salt'] = 'wKJSb7M5SyzMcFWD2gPO3j2RYUSO9B789!@#$%^&*()';
/*
* ====================
* Flood/spam settings
@ -920,10 +943,6 @@
// Location of thumbnail to use for deleted images.
$config['image_deleted'] = 'static/deleted.png';
// When a thumbnailed image is going to be the same (in dimension), just copy the entire file and use
// that as a thumbnail instead of resizing/redrawing.
$config['minimum_copy_resize'] = false;
// Maximum image upload size in bytes.
$config['max_filesize'] = 10 * 1024 * 1024; // 10MB
// Maximum image dimensions.
@ -962,15 +981,6 @@
// Set this to true if you're using Linux and you can execute `md5sum` binary.
$config['gnu_md5'] = false;
// Use Tesseract OCR to retrieve text from images, so you can use it as a spamfilter.
$config['tesseract_ocr'] = false;
// Tesseract parameters
$config['tesseract_params'] = '';
// Tesseract preprocess command
$config['tesseract_preprocess_command'] = 'convert -monochrome %s -';
// Number of posts in a "View Last X Posts" page
$config['noko50_count'] = 50;
// Number of posts a thread needs before it gets a "View Last X Posts" page.
@ -1192,10 +1202,22 @@
// Custom embedding (YouTube, vimeo, etc.)
// It's very important that you match the entire input (with ^ and $) or things will not work correctly.
$config['embedding'] = array(
array(
'/^https?:\/\/(\w+\.)?youtube\.com\/watch\?v=([a-zA-Z0-9\-_]{10,11})(&.+)?$/i',
'<iframe style="float: left;margin: 10px 20px;" width="%%tb_width%%" height="%%tb_height%%" frameborder="0" id="ytplayer" src="http://www.youtube.com/embed/$2"></iframe>'
),
[
'/^(?:(?:https?:)?\/\/)?((?:www|m)\.)?(?:(?:youtube(?:-nocookie)?\.com|youtu\.be))(?:\/(?:[\w\-]+\?v=|embed\/|live\/|v\/)?)([\w\-]{11})((?:\?|\&)\S+)?$/i',
'<div class="video-container" data-video-id="$2" data-iframe-width="360" data-iframe-height="202">
<a href="https://youtu.be/$2" target="_blank" class="file">
<img style="width:360px;height:202px;object-fit:cover" src="https://img.youtube.com/vi/$2/0.jpg" class="post-image"/>
</a>
</div>'
],
[
'/^https?:\/\/(\w+\.)?youtube\.com\/shorts\/([a-zA-Z0-9\-_]{10,11})(\?.*)?$/i',
'<div class="video-container" data-video-id="$2" data-iframe-width="202" data-iframe-height="360">
<a href="https://youtu.be/$2" target="_blank" class="file">
<img style="width:202px;height:360px;object-fit:cover" src="https://img.youtube.com/vi/$2/0.jpg" class="post-image"/>
</a>
</div>'
],
array(
'/^https?:\/\/(\w+\.)?vimeo\.com\/(\d{2,10})(\?.+)?$/i',
'<iframe src="https://player.vimeo.com/video/$2" style="float: left;margin: 10px 20px;" width="%%tb_width%%" height="%%tb_height%%" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>'
@ -1212,10 +1234,18 @@
'/^https?:\/\/video\.google\.com\/videoplay\?docid=(\d+)([&#](.+)?)?$/i',
'<embed src="http://video.google.com/googleplayer.swf?docid=$1&hl=en&fs=true" style="width:%%tb_width%%px;height:%%tb_height%%px;float:left;margin:10px 20px" allowFullScreen="true" allowScriptAccess="always" type="application/x-shockwave-flash"></embed>'
),
array(
[
'/^https?:\/\/(\w+\.)?vocaroo\.com\/i\/([a-zA-Z0-9]{2,15})$/i',
'<object style="float: left;margin: 10px 20px;" width="148" height="44"><param name="movie" value="http://vocaroo.com/player.swf?playMediaID=$2&autoplay=0"><param name="wmode" value="transparent"><embed src="http://vocaroo.com/player.swf?playMediaID=$2&autoplay=0" width="148" height="44" wmode="transparent" type="application/x-shockwave-flash"></object>'
)
'<iframe style="float:left;margin:10px 1em 10px 0" width="300" height="60" src="https://vocaroo.com/embed/$2?autoplay=0" frameborder="0" allow="autoplay"></iframe>'
],
[
'/^https?:\/\/(\w+\.)?voca\.ro\/([a-zA-Z0-9]{2,15})$/i',
'<iframe style="float:left;margin:10px 1em 10px 0" width="300" height="60" src="https://vocaroo.com/embed/$2?autoplay=0" frameborder="0" allow="autoplay"></iframe>'
],
[
'/^https?:\/\/(\w+\.)?vocaroo\.com\/([a-zA-Z0-9]{2,15})#?$/i',
'<iframe style="float:left;margin:10px 1em 10px 0" width="300" height="60" src="https://vocaroo.com/embed/$2?autoplay=0" frameborder="0" allow="autoplay"></iframe>'
]
);
// Embedding width and height.
@ -1268,6 +1298,7 @@
$config['error']['pendingappeal'] = _('There is already a pending appeal for this ban.');
$config['error']['invalidpassword'] = _('Wrong password…');
$config['error']['invalidimg'] = _('Invalid image.');
$config['error']['invalidfile'] = _('Invalid file.');
$config['error']['unknownext'] = _('Unknown file extension.');
$config['error']['filesize'] = _('Maximum file size: %maxsz% bytes<br>Your file\'s size: %filesz% bytes');
$config['error']['maxsize'] = _('The file was too big.');
@ -1521,8 +1552,8 @@
// Do DNS lookups on IP addresses to get their hostname for the moderator IP pages (?/IP/x.x.x.x).
$config['mod']['dns_lookup'] = true;
// How many recent posts, per board, to show in each page of ?/IP/x.x.x.x.
$config['mod']['ip_recentposts'] = 5;
// How many recent posts, per board, to show in ?/user_posts/ip/x.x.x.x. and ?/user_posts/passwd/xxxxxxxx
$config['mod']['recent_user_posts'] = 5;
// Number of posts to display on the reports page.
$config['mod']['recent_reports'] = 10;
@ -1825,7 +1856,18 @@
// Limit of search results
$config['search']['search_limit'] = 100;
// Boards for searching
// Maximum weigth of the search query.
// Body search filters are discarded if they make the query heavier than this.
$config['search']['max_weight'] = 100;
// Maximum length of the user sent search query.
// Characters beyond the limit are truncated and ignored.
$config['search']['max_length'] = 768;
// Enable the flag search filter.
$config['search']['flag_filter'] = false;
// Uncomment to limit the search feature to the given boards by uri.
//$config['search']['boards'] = array('a', 'b', 'c', 'd', 'e');
// Enable public logs? 0: NO, 1: YES, 2: YES, but drop names
@ -1865,45 +1907,6 @@
// Example: Adding the pre-markup post body to the API as "com_nomarkup".
// $config['api']['extra_fields'] = array('body_nomarkup' => 'com_nomarkup');
/*
* ==================
* NNTPChan settings
* ==================
*/
/*
* Please keep in mind that NNTPChan support in vichan isn't finished yet / is in an experimental
* state. Please join #nntpchan on Rizon in order to peer with someone.
*/
$config['nntpchan'] = array();
// Enable NNTPChan integration
$config['nntpchan']['enabled'] = false;
// NNTP server
$config['nntpchan']['server'] = "localhost:1119";
// Global dispatch array. Add your boards to it to enable them. Please make
// sure that this setting is set in a global context.
$config['nntpchan']['dispatch'] = array(); // 'overchan.test' => 'test'
// Trusted peer - an IP address of your NNTPChan instance. This peer will have
// increased capabilities, eg.: will evade spamfilter.
$config['nntpchan']['trusted_peer'] = '127.0.0.1';
// Salt for message ID generation. Keep it long and secure.
$config['nntpchan']['salt'] = 'change_me+please';
// A local message ID domain. Make sure to change it.
$config['nntpchan']['domain'] = 'example.vichan.net';
// An NNTPChan group name.
// Please set this setting in your board/config.php, not globally.
$config['nntpchan']['group'] = false; // eg. 'overchan.test'
/*
* ====================
* Other/uncategorized
@ -2000,12 +2003,6 @@
// is the absolute maximum, because MySQL cannot handle table names greater than 64 characters.
$config['board_regex'] = '[0-9a-zA-Z$_\x{0080}-\x{FFFF}]{1,58}';
// Youtube.js embed HTML code
$config['youtube_js_html'] = '<div class="video-container" data-video="$2">'.
'<a href="https://youtu.be/$2" target="_blank" class="file">'.
'<img style="width:360px;height:270px;" src="//img.youtube.com/vi/$2/0.jpg" class="post-image"/>'.
'</a></div>';
// Slack Report Notification
$config['slack'] = false;
$config['slack_channel'] = "";
@ -2024,7 +2021,7 @@
// Password hashing method version
// If set to 0, it won't upgrade hashes using old password encryption schema, only create new.
// You can set it to a higher value, to further migrate to other password hashing function.
$config['password_crypt_version'] = 1;
$config['password_crypt_version'] = 2;
// Use CAPTCHA for reports?
$config['report_captcha'] = false;
@ -2044,9 +2041,16 @@
// Enable auto IP note generation of moderator deleted posts
$config['autotagging'] = false;
// Enable PDF file thumbnail generation
// Enable PDF thumbnail generation.
// Requires a working installation of ghostscript and imagemagick.
// Imagemagick support of PDF files is not required.
$config['pdf_file_thumbnail'] = false;
// Enable djvu thumbnail generation.
// Requires djvulibre's tools and imagemagick.
// Imagemagick support of djvu files is not required.
$config['djvu_file_thumbnail'] = false;
// Enable TXT file thumbnail
$config['txt_file_thumbnail'] = false;

View file

@ -1,8 +1,11 @@
<?php
namespace Vichan;
use Vichan\Data\Driver\CacheDriver;
use Vichan\Data\ReportQueries;
use Vichan\Data\{IpNoteQueries, ReportQueries, SearchQueries, UserPostQueries};
use Vichan\Data\Driver\Cache\CacheDriver;
use Vichan\Data\Driver\Log\{ErrorLogLogDriver, FileLogDriver, LogDriver, StderrLogDriver, SyslogLogDriver};
use Vichan\Data\Model\Flags;
use Vichan\Service\SearchService;
defined('TINYBOARD') or exit;
@ -31,6 +34,34 @@ class Context {
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'];
$legacy_syslog = isset($config['syslog']) && $config['syslog'];
// Check 'syslog' for backwards compatibility.
if ($legacy_syslog || $backend === 'syslog') {
$log_driver = new SyslogLogDriver($name, $level, $config['log_system']['syslog_stderr']);
if ($legacy_syslog) {
$log_driver->log(LogDriver::NOTICE, 'The configuration setting \'syslog\' is deprecated. Please use \'log_system\' instead');
}
return $log_driver;
} elseif ($backend === 'file') {
return new FileLogDriver($name, $level, $config['log_system']['file_path']);
} elseif ($backend === 'stderr') {
return new StderrLogDriver($name, $level);
} elseif ($backend === 'error_log') {
return new ErrorLogLogDriver($name, $level);
} else {
$log_driver = new ErrorLogLogDriver($name, $level);
$log_driver->log(LogDriver::ERROR, "Unknown 'log_system' value '$backend', using 'error_log' default");
return $log_driver;
}
},
CacheDriver::class => function($c) {
// Use the global for backwards compatibility.
return \cache::getCache();
@ -41,10 +72,53 @@ function build_context(array $config): Context {
sql_open();
return $pdo;
},
SearchService::class => function($c) {
$config = $c->get('config');
$flags = null;
if ($config['search']['flag_filter']) {
if ($config['user_flag']) {
$flags = $config['user_flags'];
} elseif ($config['country_flags']) {
$flags = Flags::EMBEDDED_FLAGS;
}
}
$board_uris = $config['search']['boards'] ?? null;
return new SearchService(
$c->get(LogDriver::class),
$c->get(UserPostQueries::class),
$c->get(SearchQueries::class),
$flags,
$config['search']['max_weight'],
$config['search']['max_length'],
$config['search']['search_limit'],
$board_uris
);
},
ReportQueries::class => function($c) {
$auto_maintenance = (bool)$c->get('config')['auto_maintenance'];
$pdo = $c->get(\PDO::class);
return new ReportQueries($pdo, $auto_maintenance);
},
UserPostQueries::class => function($c) {
return new UserPostQueries($c->get(\PDO::class));
},
IpNoteQueries::class => fn($c) => new IpNoteQueries($c->get(\PDO::class), $c->get(CacheDriver::class)),
SearchQueries::class => function($c) {
$config = $c->get('config');
list($queries_for_single, $range_for_single_min) = $config['search']['queries_per_minutes'];
list($queries_for_all, $range_for_all_min) = $config['search']['queries_per_minutes_all'];
return new SearchQueries(
$c->get(\PDO::class),
$queries_for_single,
$range_for_single_min * 60,
$queries_for_all,
$range_for_all_min * 60,
(bool)$config['auto_maintenance']
);
}
]);
}

View file

@ -66,7 +66,7 @@ function sql_open() {
$dsn = $config['db']['type'] . ':' .
($unix_socket ? 'unix_socket=' . $unix_socket : 'host=' . $config['db']['server']) .
';dbname=' . $config['db']['database'];
';charset=utf8mb4;dbname=' . $config['db']['database'];
if (!empty($config['db']['dsn']))
$dsn .= ';' . $config['db']['dsn'];
try {
@ -85,7 +85,7 @@ function sql_open() {
if ($config['debug']) {
$debug['time']['db_connect'] = '~' . round((microtime(true) - $start) * 1000, 2) . 'ms';
if ($config['db']['type'] == "mysql") {
query('SET NAMES utf8') or error(db_error());
query('SET NAMES utf8mb4') or error(db_error());
}
}
return $pdo;

View file

@ -4,23 +4,26 @@
* Copyright (c) 2010-2013 Tinyboard Development Group
*/
use Vichan\Context;
use Vichan\Data\IpNoteQueries;
defined('TINYBOARD') or exit;
class Filter {
public $flood_check;
private $condition;
private $post;
public function __construct(array $arr) {
foreach ($arr as $key => $value)
$this->$key = $value;
$this->$key = $value;
}
public function match($condition, $match) {
$condition = strtolower($condition);
$post = &$this->post;
switch($condition) {
case 'custom':
if (!is_callable($match))
@ -29,11 +32,11 @@ class Filter {
case 'flood-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) {
@ -69,10 +72,10 @@ class Filter {
}
$flood_check_matched[] = $flood_post;
}
// is there any reason for this assignment?
$this->flood_check = $flood_check_matched;
return !empty($this->flood_check);
case 'flood-time':
foreach ($this->flood_check as $flood_post) {
@ -135,46 +138,42 @@ class Filter {
error('Unknown filter condition: ' . $condition);
}
}
public function action() {
public function action(Context $ctx) {
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));
}
$note_queries = $ctx->get(IpNoteQueries::class);
$note_queries->add($_SERVER['REMOTE_ADDR'], -1, 'Autoban message: ' . $this->post['body']);
}
if (isset ($this->action)) switch($this->action) {
case 'reject':
error(isset($this->message) ? $this->message : 'Posting blocked 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);
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) {
@ -184,7 +183,7 @@ class Filter {
} else {
$NOT = false;
}
if ($this->match($condition, $value) == $NOT)
return false;
}
@ -194,11 +193,11 @@ 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 (isset($config['flood_cache'])) {
$max_time = &$config['flood_cache'];
} else {
@ -208,18 +207,18 @@ function purge_flood_table() {
$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) {
function do_filters(Context $ctx, array $post) {
global $config;
if (!isset($config['filters']) || empty($config['filters']))
return;
foreach ($config['filters'] as $filter) {
if (isset($filter['condition']['flood-match'])) {
$has_flood = true;
@ -232,15 +231,15 @@ 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)) {
$filter->action();
$filter->action($ctx);
}
}
purge_flood_table();
}

View file

@ -11,6 +11,7 @@ if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) {
$microtime_start = microtime(true);
use Vichan\Functions\Hide;
use Lifo\IP\IP; // for expanding IPv6 address in DNSBL()
// the user is not currently logged in as a moderator
@ -646,13 +647,14 @@ function file_write($path, $data, $simple = false, $skip_purge = false) {
if ($config['gzip_static']) {
$gzpath = "$path.gz";
if ($bytes & ~0x3ff) { // if ($bytes >= 1024)
if (file_put_contents($gzpath, gzencode($data), $simple ? 0 : LOCK_EX) === false)
error("Unable to write to file: $gzpath");
//if (!touch($gzpath, filemtime($path), fileatime($path)))
// error("Unable to touch file: $gzpath");
}
else {
// 12KBs (2 left for headers etc) to stay within the 14 KBs of the standard initial TCP packet.
if ($bytes >= 12288) {
if (\file_put_contents($gzpath, \gzencode($data), $simple ? 0 : LOCK_EX) === false) {
// Do not fail completely if the write fails.
\error_log("Unable to write to file: $gzpath");
@unlink($gzpath);
}
} else {
@unlink($gzpath);
}
}
@ -745,24 +747,23 @@ function hasPermission($action = null, $board = null, $_mod = null) {
function listBoards($just_uri = false) {
global $config;
$just_uri ? $cache_name = 'all_boards_uri' : $cache_name = 'all_boards';
$cache_name = $just_uri ? 'all_boards_uri' : 'all_boards';
if ($config['cache']['enabled'] && ($boards = cache::get($cache_name)))
if ($config['cache']['enabled'] && ($boards = cache::get($cache_name))) {
return $boards;
if (!$just_uri) {
$query = query("SELECT * FROM ``boards`` ORDER BY `uri`") or error(db_error());
$boards = $query->fetchAll();
} else {
$boards = array();
$query = query("SELECT `uri` FROM ``boards``") or error(db_error());
while ($board = $query->fetchColumn()) {
$boards[] = $board;
}
}
if ($config['cache']['enabled'])
if (!$just_uri) {
$query = query('SELECT * FROM ``boards`` ORDER BY `uri`');
$boards = $query->fetchAll();
} else {
$query = query('SELECT `uri` FROM ``boards``');
$boards = $query->fetchAll(\PDO::FETCH_COLUMN);
}
if ($config['cache']['enabled']) {
cache::set($cache_name, $boards);
}
return $boards;
}
@ -1692,7 +1693,7 @@ function checkSpam(array $extra_salt = array()) {
$_hash = sha1($_hash . $extra_salt);
if ($hash != $_hash) {
return true;
return true;
}
$query = prepare('SELECT `passed` FROM ``antispam`` WHERE `hash` = :hash');
@ -2070,7 +2071,7 @@ function remove_modifiers($body) {
return preg_replace('@<tinyboard ([\w\s]+)>(.+?)</tinyboard>@usm', '', $body);
}
function markup(&$body, $track_cites = false, $op = false) {
function markup(&$body, $track_cites = false) {
global $board, $config, $markup_urls;
$modifiers = extract_modifiers($body);
@ -2169,12 +2170,15 @@ function markup(&$body, $track_cites = false, $op = false) {
link_for(array('id' => $cite, 'thread' => $cited_posts[$cite])) . '#' . $cite . '">' .
'&gt;&gt;' . $cite .
'</a>';
} else {
$replacement = "<s>&gt;&gt;$cite</s>";
}
$body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[3][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0]));
$skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[3][0]) - mb_strlen($matches[0][0]);
$body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[3][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0]));
$skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[3][0]) - mb_strlen($matches[0][0]);
if ($track_cites && $config['track_cites'])
$tracked_cites[] = array($board['uri'], $cite);
if ($track_cites && $config['track_cites']) {
$tracked_cites[] = array($board['uri'], $cite);
}
}
}
@ -2225,20 +2229,15 @@ function markup(&$body, $track_cites = false, $op = false) {
$clauses = array_unique($clauses);
if ($board['uri'] != $_board) {
if (!openBoard($_board)){
if (in_array($_board,array_keys($config['boards_alias']))){
$_board = $config['boards_alias'][$_board];
if (openBoard($_board)){
}
else {
if (!openBoard($_board)) {
if (\in_array($_board, \array_keys($config['boards_alias']))) {
$_board = $config['boards_alias'][$_board];
if (!openBoard($_board)) {
continue; // Unknown board
}
}
else {
}
} else {
continue; // Unknown board
}
}
}
@ -2279,38 +2278,31 @@ function markup(&$body, $track_cites = false, $op = false) {
if ($cite) {
if (isset($cited_posts[$_board][$cite])) {
$link = $cited_posts[$_board][$cite];
if (isset($original_board)){
$replacement = '<a ' .
$replacement_board = $original_board ?? $_board;
$replacement = '<a ' .
($_board == $board['uri'] ?
'onclick="highlightReply(\''.$cite.'\', event);" '
: '') . 'href="' . $link . '">' .
'&gt;&gt;&gt;/' . $original_board . '/' . $cite .
'&gt;&gt;&gt;/' . $replacement_board . '/' . $cite .
'</a>';
if ($track_cites && $config['track_cites']) {
$tracked_cites[] = [ $_board, $cite ];
}
else {
$replacement = '<a ' .
($_board == $board['uri'] ?
'onclick="highlightReply(\''.$cite.'\', event);" '
: '') . 'href="' . $link . '">' .
'&gt;&gt;&gt;/' . $_board . '/' . $cite .
'</a>';
}
$body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[4][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0]));
$skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[4][0]) - mb_strlen($matches[0][0]);
if ($track_cites && $config['track_cites'])
$tracked_cites[] = array($_board, $cite);
} else {
$replacement = "<s>&gt;&gt;&gt;/$_board/$cite</s>";
}
} elseif(isset($crossboard_indexes[$_board])) {
} elseif (isset($crossboard_indexes[$_board])) {
$replacement = '<a href="' . $crossboard_indexes[$_board] . '">' .
'&gt;&gt;&gt;/' . $_board . '/' .
'</a>';
$body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[4][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0]));
$skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[4][0]) - mb_strlen($matches[0][0]);
} else {
$replacement = "<s>&gt;&gt;&gt;/$_board/$cite</s>";
}
$body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[4][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0]));
$skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[4][0]) - mb_strlen($matches[0][0]);
}
}
@ -2581,11 +2573,11 @@ function rrmdir($dir) {
function poster_id($ip, $thread) {
global $config;
if ($id = event('poster-id', $ip, $thread))
if ($id = event('poster-id', $ip, $thread)) {
return $id;
}
// Confusing, hard to brute-force, but simple algorithm
return substr(sha1(sha1($ip . $config['secure_trip_salt'] . $thread) . $config['secure_trip_salt']), 0, $config['poster_id_length']);
return \substr(Hide\secure_hash($ip . $config['secure_trip_salt'] . $thread . $config['secure_trip_salt'], false), 0, $config['poster_id_length']);
}
function generate_tripcode($name) {
@ -2613,7 +2605,7 @@ function generate_tripcode($name) {
if (isset($config['custom_tripcode']["##{$trip}"]))
$trip = $config['custom_tripcode']["##{$trip}"];
else
$trip = '!!' . substr(crypt($trip, str_replace('+', '.', '_..A.' . substr(base64_encode(sha1($trip . $config['secure_trip_salt'], true)), 0, 4))), -10);
$trip = '!!' . substr(crypt($trip, str_replace('+', '.', '_..A.' . substr(Hide\secure_hash($trip . $config['secure_trip_salt'], false), 0, 4))), -10);
} else {
if (isset($config['custom_tripcode']["#{$trip}"]))
$trip = $config['custom_tripcode']["#{$trip}"];
@ -3083,3 +3075,8 @@ function strategy_first($fun, $array) {
return array('defer');
}
}
function hashPassword($password) {
global $config;
return hash('sha3-256', $password . $config['secure_password_salt']);
}

6
inc/functions/hide.php Normal file
View file

@ -0,0 +1,6 @@
<?php
namespace Vichan\Functions\Hide;
function secure_hash(string $data, bool $binary): string {
return \hash('tiger160,3', $data, $binary);
}

View file

@ -1,20 +0,0 @@
Copyright (c) 2013 Jason Morriss
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -1,293 +0,0 @@
<?php
/**
* This file is part of the Lifo\IP PHP Library.
*
* (c) Jason Morriss <lifo2013@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Lifo\IP;
/**
* BCMath helper class.
*
* Provides a handful of BCMath routines that are not included in the native
* PHP library.
*
* Note: The Bitwise functions operate on fixed byte boundaries. For example,
* comparing the following numbers uses X number of bits:
* 0xFFFF and 0xFF will result in comparison of 16 bits.
* 0xFFFFFFFF and 0xF will result in comparison of 32 bits.
* etc...
*
*/
abstract class BC
{
// Some common (maybe useless) constants
const MAX_INT_32 = '2147483647'; // 7FFFFFFF
const MAX_UINT_32 = '4294967295'; // FFFFFFFF
const MAX_INT_64 = '9223372036854775807'; // 7FFFFFFFFFFFFFFF
const MAX_UINT_64 = '18446744073709551615'; // FFFFFFFFFFFFFFFF
const MAX_INT_96 = '39614081257132168796771975167'; // 7FFFFFFFFFFFFFFFFFFFFFFF
const MAX_UINT_96 = '79228162514264337593543950335'; // FFFFFFFFFFFFFFFFFFFFFFFF
const MAX_INT_128 = '170141183460469231731687303715884105727'; // 7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
const MAX_UINT_128 = '340282366920938463463374607431768211455'; // FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
/**
* BC Math function to convert a HEX string into a DECIMAL
*/
public static function bchexdec($hex)
{
if (strlen($hex) == 1) {
return hexdec($hex);
}
$remain = substr($hex, 0, -1);
$last = substr($hex, -1);
return bcadd(bcmul(16, self::bchexdec($remain), 0), hexdec($last), 0);
}
/**
* BC Math function to convert a DECIMAL string into a BINARY string
*/
public static function bcdecbin($dec, $pad = null)
{
$bin = '';
while ($dec) {
$m = bcmod($dec, 2);
$dec = bcdiv($dec, 2, 0);
$bin = abs($m) . $bin;
}
return $pad ? sprintf("%0{$pad}s", $bin) : $bin;
}
/**
* BC Math function to convert a BINARY string into a DECIMAL string
*/
public static function bcbindec($bin)
{
$dec = '0';
for ($i=0, $j=strlen($bin); $i<$j; $i++) {
$dec = bcmul($dec, '2', 0);
$dec = bcadd($dec, $bin[$i], 0);
}
return $dec;
}
/**
* BC Math function to convert a BINARY string into a HEX string
*/
public static function bcbinhex($bin, $pad = 0)
{
return self::bcdechex(self::bcbindec($bin));
}
/**
* BC Math function to convert a DECIMAL into a HEX string
*/
public static function bcdechex($dec)
{
$last = bcmod($dec, 16);
$remain = bcdiv(bcsub($dec, $last, 0), 16, 0);
return $remain == 0 ? dechex($last) : self::bcdechex($remain) . dechex($last);
}
/**
* Bitwise AND two arbitrarily large numbers together.
*/
public static function bcand($left, $right)
{
$len = self::_bitwise($left, $right);
$value = '';
for ($i=0; $i<$len; $i++) {
$value .= (($left[$i] + 0) & ($right[$i] + 0)) ? '1' : '0';
}
return self::bcbindec($value != '' ? $value : '0');
}
/**
* Bitwise OR two arbitrarily large numbers together.
*/
public static function bcor($left, $right)
{
$len = self::_bitwise($left, $right);
$value = '';
for ($i=0; $i<$len; $i++) {
$value .= (($left[$i] + 0) | ($right[$i] + 0)) ? '1' : '0';
}
return self::bcbindec($value != '' ? $value : '0');
}
/**
* Bitwise XOR two arbitrarily large numbers together.
*/
public static function bcxor($left, $right)
{
$len = self::_bitwise($left, $right);
$value = '';
for ($i=0; $i<$len; $i++) {
$value .= (($left[$i] + 0) ^ ($right[$i] + 0)) ? '1' : '0';
}
return self::bcbindec($value != '' ? $value : '0');
}
/**
* Bitwise NOT two arbitrarily large numbers together.
*/
public static function bcnot($left, $bits = null)
{
$right = 0;
$len = self::_bitwise($left, $right, $bits);
$value = '';
for ($i=0; $i<$len; $i++) {
$value .= $left[$i] == '1' ? '0' : '1';
}
return self::bcbindec($value);
}
/**
* Shift number to the left
*
* @param integer $bits Total bits to shift
*/
public static function bcleft($num, $bits) {
return bcmul($num, bcpow('2', $bits));
}
/**
* Shift number to the right
*
* @param integer $bits Total bits to shift
*/
public static function bcright($num, $bits) {
return bcdiv($num, bcpow('2', $bits));
}
/**
* Determine how many bits are needed to store the number rounded to the
* nearest bit boundary.
*/
public static function bits_needed($num, $boundary = 4)
{
$bits = 0;
while ($num > 0) {
$num = bcdiv($num, '2', 0);
$bits++;
}
// round to nearest boundrary
return $boundary ? ceil($bits / $boundary) * $boundary : $bits;
}
/**
* BC Math function to return an arbitrarily large random number.
*/
public static function bcrand($min, $max = null)
{
if ($max === null) {
$max = $min;
$min = 0;
}
// swap values if $min > $max
if (bccomp($min, $max) == 1) {
list($min,$max) = array($max,$min);
}
return bcadd(
bcmul(
bcdiv(
mt_rand(0, mt_getrandmax()),
mt_getrandmax(),
strlen($max)
),
bcsub(
bcadd($max, '1'),
$min
)
),
$min
);
}
/**
* Computes the natural logarithm using a series.
* @author Thomas Oldbury.
* @license Public domain.
*/
public static function bclog($num, $iter = 10, $scale = 100)
{
$log = "0.0";
for($i = 0; $i < $iter; $i++) {
$pow = 1 + (2 * $i);
$mul = bcdiv("1.0", $pow, $scale);
$fraction = bcmul($mul, bcpow(bcsub($num, "1.0", $scale) / bcadd($num, "1.0", $scale), $pow, $scale), $scale);
$log = bcadd($fraction, $log, $scale);
}
return bcmul("2.0", $log, $scale);
}
/**
* Computes the base2 log using baseN log.
*/
public static function bclog2($num, $iter = 10, $scale = 100)
{
return bcdiv(self::bclog($num, $iter, $scale), self::bclog("2", $iter, $scale), $scale);
}
public static function bcfloor($num)
{
if (substr($num, 0, 1) == '-') {
return bcsub($num, 1, 0);
}
return bcadd($num, 0, 0);
}
public static function bcceil($num)
{
if (substr($num, 0, 1) == '-') {
return bcsub($num, 0, 0);
}
return bcadd($num, 1, 0);
}
/**
* Compare two numbers and return -1, 0, 1 depending if the LEFT number is
* < = > the RIGHT.
*
* @param string|integer $left Left side operand
* @param string|integer $right Right side operand
* @return integer Return -1,0,1 for <=> comparison
*/
public static function cmp($left, $right)
{
// @todo could an optimization be done to determine if a normal 32bit
// comparison could be done instead of using bccomp? But would
// the number verification cause too much overhead to be useful?
return bccomp($left, $right, 0);
}
/**
* Internal function to prepare for bitwise operations
*/
private static function _bitwise(&$left, &$right, $bits = null)
{
if ($bits === null) {
$bits = max(self::bits_needed($left), self::bits_needed($right));
}
$left = self::bcdecbin($left);
$right = self::bcdecbin($right);
$len = max(strlen($left), strlen($right), (int)$bits);
$left = sprintf("%0{$len}s", $left);
$right = sprintf("%0{$len}s", $right);
return $len;
}
}

View file

@ -1,706 +0,0 @@
<?php
/**
* This file is part of the Lifo\IP PHP Library.
*
* (c) Jason Morriss <lifo2013@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Lifo\IP;
/**
* CIDR Block helper class.
*
* Most routines can be used statically or by instantiating an object and
* calling its methods.
*
* Provides routines to do various calculations on IP addresses and ranges.
* Convert to/from CIDR to ranges, etc.
*/
class CIDR
{
const INTERSECT_NO = 0;
const INTERSECT_YES = 1;
const INTERSECT_LOW = 2;
const INTERSECT_HIGH = 3;
protected $start;
protected $end;
protected $prefix;
protected $version;
protected $istart;
protected $iend;
private $cache;
/**
* Create a new CIDR object.
*
* The IP range can be arbitrary and does not have to fall on a valid CIDR
* range. Some methods will return different values depending if you ignore
* the prefix or not. By default all prefix sensitive methods will assume
* the prefix is used.
*
* @param string $cidr An IP address (1.2.3.4), CIDR block (1.2.3.4/24),
* or range "1.2.3.4-1.2.3.10"
* @param string $end Ending IP in range if no cidr/prefix is given
*/
public function __construct($cidr, $end = null)
{
if ($end !== null) {
$this->setRange($cidr, $end);
} else {
$this->setCidr($cidr);
}
}
/**
* Returns the string representation of the CIDR block.
*/
public function __toString()
{
// do not include the prefix if its a single IP
try {
if ($this->isTrueCidr() && (
($this->version == 4 and $this->prefix != 32) ||
($this->version == 6 and $this->prefix != 128)
)
) {
return $this->start . '/' . $this->prefix;
}
} catch (\Exception $e) {
// isTrueCidr() calls getRange which can throw an exception
}
if (strcmp($this->start, $this->end) == 0) {
return $this->start;
}
return $this->start . ' - ' . $this->end;
}
public function __clone()
{
// do not clone the cache. No real reason why. I just want to keep the
// memory foot print as low as possible, even though this is trivial.
$this->cache = array();
}
/**
* Set an arbitrary IP range.
* The closest matching prefix will be calculated but the actual range
* stored in the object can be arbitrary.
* @param string $start Starting IP or combination "start-end" string.
* @param string $end Ending IP or null.
*/
public function setRange($ip, $end = null)
{
if (strpos($ip, '-') !== false) {
list($ip, $end) = array_map('trim', explode('-', $ip, 2));
}
if (false === filter_var($ip, FILTER_VALIDATE_IP) ||
false === filter_var($end, FILTER_VALIDATE_IP)) {
throw new \InvalidArgumentException("Invalid IP range \"$ip-$end\"");
}
// determine version (4 or 6)
$this->version = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4;
$this->istart = IP::inet_ptod($ip);
$this->iend = IP::inet_ptod($end);
// fix order
if (bccomp($this->istart, $this->iend) == 1) {
list($this->istart, $this->iend) = array($this->iend, $this->istart);
list($ip, $end) = array($end, $ip);
}
$this->start = $ip;
$this->end = $end;
// calculate real prefix
$len = $this->version == 4 ? 32 : 128;
$this->prefix = $len - strlen(BC::bcdecbin(BC::bcxor($this->istart, $this->iend)));
}
/**
* Returns true if the current IP is a true cidr block
*/
public function isTrueCidr()
{
return $this->start == $this->getNetwork() && $this->end == $this->getBroadcast();
}
/**
* Set the CIDR block.
*
* The prefix length is optional and will default to 32 ot 128 depending on
* the version detected.
*
* @param string $cidr CIDR block string, eg: "192.168.0.0/24" or "2001::1/64"
* @throws \InvalidArgumentException If the CIDR block is invalid
*/
public function setCidr($cidr)
{
if (strpos($cidr, '-') !== false) {
return $this->setRange($cidr);
}
list($ip, $bits) = array_pad(array_map('trim', explode('/', $cidr, 2)), 2, null);
if (false === filter_var($ip, FILTER_VALIDATE_IP)) {
throw new \InvalidArgumentException("Invalid IP address \"$cidr\"");
}
// determine version (4 or 6)
$this->version = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4;
$this->start = $ip;
$this->istart = IP::inet_ptod($ip);
if ($bits !== null and $bits !== '') {
$this->prefix = $bits;
} else {
$this->prefix = $this->version == 4 ? 32 : 128;
}
if (($this->prefix < 0)
|| ($this->prefix > 32 and $this->version == 4)
|| ($this->prefix > 128 and $this->version == 6)) {
throw new \InvalidArgumentException("Invalid IP address \"$cidr\"");
}
$this->end = $this->getBroadcast();
$this->iend = IP::inet_ptod($this->end);
$this->cache = array();
}
/**
* Get the IP version. 4 or 6.
*
* @return integer
*/
public function getVersion()
{
return $this->version;
}
/**
* Get the prefix.
*
* Always returns the "proper" prefix, even if the IP range is arbitrary.
*
* @return integer
*/
public function getPrefix()
{
return $this->prefix;
}
/**
* Return the starting presentational IP or Decimal value.
*
* Ignores prefix
*/
public function getStart($decimal = false)
{
return $decimal ? $this->istart : $this->start;
}
/**
* Return the ending presentational IP or Decimal value.
*
* Ignores prefix
*/
public function getEnd($decimal = false)
{
return $decimal ? $this->iend : $this->end;
}
/**
* Return the next presentational IP or Decimal value (following the
* broadcast address of the current CIDR block).
*/
public function getNext($decimal = false)
{
$next = bcadd($this->getEnd(true), '1');
return $decimal ? $next : new self(IP::inet_dtop($next));
}
/**
* Returns true if the IP is an IPv4
*
* @return boolean
*/
public function isIPv4()
{
return $this->version == 4;
}
/**
* Returns true if the IP is an IPv6
*
* @return boolean
*/
public function isIPv6()
{
return $this->version == 6;
}
/**
* Get the cidr notation for the subnet block.
*
* This is useful for when you want a string representation of the IP/prefix
* and the starting IP is not on a valid network boundrary (eg: Displaying
* an IP from an interface).
*
* @return string IP in CIDR notation "ipaddr/prefix"
*/
public function getCidr()
{
return $this->start . '/' . $this->prefix;
}
/**
* Get the [low,high] range of the CIDR block
*
* Prefix sensitive.
*
* @param boolean $ignorePrefix If true the arbitrary start-end range is
* returned. default=false.
*/
public function getRange($ignorePrefix = false)
{
$range = $ignorePrefix
? array($this->start, $this->end)
: self::cidr_to_range($this->start, $this->prefix);
// watch out for IP '0' being converted to IPv6 '::'
if ($range[0] == '::' and strpos($range[1], ':') == false) {
$range[0] = '0.0.0.0';
}
return $range;
}
/**
* Return the IP in its fully expanded form.
*
* For example: 2001::1 == 2007:0000:0000:0000:0000:0000:0000:0001
*
* @see IP::inet_expand
*/
public function getExpanded()
{
return IP::inet_expand($this->start);
}
/**
* Get network IP of the CIDR block
*
* Prefix sensitive.
*
* @param boolean $ignorePrefix If true the arbitrary start-end range is
* returned. default=false.
*/
public function getNetwork($ignorePrefix = false)
{
// micro-optimization to prevent calling getRange repeatedly
$k = $ignorePrefix ? 1 : 0;
if (!isset($this->cache['range'][$k])) {
$this->cache['range'][$k] = $this->getRange($ignorePrefix);
}
return $this->cache['range'][$k][0];
}
/**
* Get broadcast IP of the CIDR block
*
* Prefix sensitive.
*
* @param boolean $ignorePrefix If true the arbitrary start-end range is
* returned. default=false.
*/
public function getBroadcast($ignorePrefix = false)
{
// micro-optimization to prevent calling getRange repeatedly
$k = $ignorePrefix ? 1 : 0;
if (!isset($this->cache['range'][$k])) {
$this->cache['range'][$k] = $this->getRange($ignorePrefix);
}
return $this->cache['range'][$k][1];
}
/**
* Get the network mask based on the prefix.
*
*/
public function getMask()
{
return self::prefix_to_mask($this->prefix, $this->version);
}
/**
* Get total hosts within CIDR range
*
* Prefix sensitive.
*
* @param boolean $ignorePrefix If true the arbitrary start-end range is
* returned. default=false.
*/
public function getTotal($ignorePrefix = false)
{
// micro-optimization to prevent calling getRange repeatedly
$k = $ignorePrefix ? 1 : 0;
if (!isset($this->cache['range'][$k])) {
$this->cache['range'][$k] = $this->getRange($ignorePrefix);
}
return bcadd(bcsub(IP::inet_ptod($this->cache['range'][$k][1]),
IP::inet_ptod($this->cache['range'][$k][0])), '1');
}
public function intersects($cidr)
{
return self::cidr_intersect((string)$this, $cidr);
}
/**
* Determines the intersection between an IP (with optional prefix) and a
* CIDR block.
*
* The IP will be checked against the CIDR block given and will either be
* inside or outside the CIDR completely, or partially.
*
* NOTE: The caller should explicitly check against the INTERSECT_*
* constants because this method will return a value > 1 even for partial
* matches.
*
* @param mixed $ip The IP/cidr to match
* @param mixed $cidr The CIDR block to match within
* @return integer Returns an INTERSECT_* constant
* @throws \InvalidArgumentException if either $ip or $cidr is invalid
*/
public static function cidr_intersect($ip, $cidr)
{
// use fixed length HEX strings so we can easily do STRING comparisons
// instead of using slower bccomp() math.
list($lo,$hi) = array_map(function($v){ return sprintf("%032s", IP::inet_ptoh($v)); }, CIDR::cidr_to_range($ip));
list($min,$max) = array_map(function($v){ return sprintf("%032s", IP::inet_ptoh($v)); }, CIDR::cidr_to_range($cidr));
/** visualization of logic used below
lo-hi = $ip to check
min-max = $cidr block being checked against
--- --- --- lo --- --- hi --- --- --- --- --- IP/prefix to check
--- min --- --- max --- --- --- --- --- --- --- Partial "LOW" match
--- --- --- --- --- min --- --- max --- --- --- Partial "HIGH" match
--- --- --- --- min max --- --- --- --- --- --- No match "NO"
--- --- --- --- --- --- --- --- min --- max --- No match "NO"
min --- max --- --- --- --- --- --- --- --- --- No match "NO"
--- --- min --- --- --- --- max --- --- --- --- Full match "YES"
*/
// IP is exact match or completely inside the CIDR block
if ($lo >= $min and $hi <= $max) {
return self::INTERSECT_YES;
}
// IP is completely outside the CIDR block
if ($max < $lo or $min > $hi) {
return self::INTERSECT_NO;
}
// @todo is it useful to return LOW/HIGH partial matches?
// IP matches the lower end
if ($max <= $hi and $min <= $lo) {
return self::INTERSECT_LOW;
}
// IP matches the higher end
if ($min >= $lo and $max >= $hi) {
return self::INTERSECT_HIGH;
}
return self::INTERSECT_NO;
}
/**
* Converts an IPv4 or IPv6 CIDR block into its range.
*
* @todo May not be the fastest way to do this.
*
* @static
* @param string $cidr CIDR block or IP address string.
* @param integer|null $bits If /bits is not specified on string they can be
* passed via this parameter instead.
* @return array A 2 element array with the low, high range
*/
public static function cidr_to_range($cidr, $bits = null)
{
if (strpos($cidr, '/') !== false) {
list($ip, $_bits) = array_pad(explode('/', $cidr, 2), 2, null);
} else {
$ip = $cidr;
$_bits = $bits;
}
if (false === filter_var($ip, FILTER_VALIDATE_IP)) {
throw new \InvalidArgumentException("IP address \"$cidr\" is invalid");
}
// force bit length to 32 or 128 depending on type of IP
$bitlen = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 128 : 32;
if ($bits === null) {
// if no prefix is given use the length of the binary string which
// will give us 32 or 128 and result in a single IP being returned.
$bits = $_bits !== null ? $_bits : $bitlen;
}
if ($bits > $bitlen) {
throw new \InvalidArgumentException("IP address \"$cidr\" is invalid");
}
$ipdec = IP::inet_ptod($ip);
$ipbin = BC::bcdecbin($ipdec, $bitlen);
// calculate network
$netmask = BC::bcbindec(str_pad(str_repeat('1',$bits), $bitlen, '0'));
$ip1 = BC::bcand($ipdec, $netmask);
// calculate "broadcast" (not technically a broadcast in IPv6)
$ip2 = BC::bcor($ip1, BC::bcnot($netmask));
return array(IP::inet_dtop($ip1), IP::inet_dtop($ip2));
}
/**
* Return the CIDR string from the range given
*/
public static function range_to_cidr($start, $end)
{
$cidr = new CIDR($start, $end);
return (string)$cidr;
}
/**
* Return the maximum prefix length that would fit the IP address given.
*
* This is useful to determine how my bit would be needed to store the IP
* address when you don't already have a prefix for the IP.
*
* @example 216.240.32.0 would return 27
*
* @param string $ip IP address without prefix
* @param integer $bits Maximum bits to check; defaults to 32 for IPv4 and 128 for IPv6
*/
public static function max_prefix($ip, $bits = null)
{
static $mask = array();
$ver = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4;
$max = $ver == 6 ? 128 : 32;
if ($bits === null) {
$bits = $max;
}
$int = IP::inet_ptod($ip);
while ($bits > 0) {
// micro-optimization; calculate mask once ...
if (!isset($mask[$ver][$bits-1])) {
// 2^$max - 2^($max - $bits);
if ($ver == 4) {
$mask[$ver][$bits-1] = pow(2, $max) - pow(2, $max - ($bits-1));
} else {
$mask[$ver][$bits-1] = bcsub(bcpow(2, $max), bcpow(2, $max - ($bits-1)));
}
}
$m = $mask[$ver][$bits-1];
//printf("%s/%d: %s & %s == %s\n", $ip, $bits-1, BC::bcdecbin($m, 32), BC::bcdecbin($int, 32), BC::bcdecbin(BC::bcand($int, $m)));
//echo "$ip/", $bits-1, ": ", IP::inet_dtop($m), " ($m) & $int == ", BC::bcand($int, $m), "\n";
if (bccomp(BC::bcand($int, $m), $int) != 0) {
return $bits;
}
$bits--;
}
return $bits;
}
/**
* Return a contiguous list of true CIDR blocks that span the range given.
*
* Note: It's not a good idea to call this with IPv6 addresses. While it may
* work for certain ranges this can be very slow. Also an IPv6 list won't be
* as accurate as an IPv4 list.
*
* @example
* range_to_cidrlist(192.168.0.0, 192.168.0.15) ==
* 192.168.0.0/28
* range_to_cidrlist(192.168.0.0, 192.168.0.20) ==
* 192.168.0.0/28
* 192.168.0.16/30
* 192.168.0.20/32
*/
public static function range_to_cidrlist($start, $end)
{
$ver = (false === filter_var($start, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4;
$start = IP::inet_ptod($start);
$end = IP::inet_ptod($end);
$len = $ver == 4 ? 32 : 128;
$log2 = $ver == 4 ? log(2) : BC::bclog(2);
$list = array();
while (BC::cmp($end, $start) >= 0) { // $end >= $start
$prefix = self::max_prefix(IP::inet_dtop($start), $len);
if ($ver == 4) {
$diff = $len - floor( log($end - $start + 1) / $log2 );
} else {
// this is not as accurate due to the bclog function
$diff = bcsub($len, BC::bcfloor(bcdiv(BC::bclog(bcadd(bcsub($end, $start), '1')), $log2)));
}
if ($prefix < $diff) {
$prefix = $diff;
}
$list[] = IP::inet_dtop($start) . "/" . $prefix;
if ($ver == 4) {
$start += pow(2, $len - $prefix);
} else {
$start = bcadd($start, bcpow(2, $len - $prefix));
}
}
return $list;
}
/**
* Return an list of optimized CIDR blocks by collapsing adjacent CIDR
* blocks into larger blocks.
*
* @param array $cidrs List of CIDR block strings or objects
* @param integer $maxPrefix Maximum prefix to allow
* @return array Optimized list of CIDR objects
*/
public static function optimize_cidrlist($cidrs, $maxPrefix = 32)
{
// all indexes must be a CIDR object
$cidrs = array_map(function($o){ return $o instanceof CIDR ? $o : new CIDR($o); }, $cidrs);
// sort CIDR blocks in proper order so we can easily loop over them
$cidrs = self::cidr_sort($cidrs);
$list = array();
while ($cidrs) {
$c = array_shift($cidrs);
$start = $c->getStart();
$max = bcadd($c->getStart(true), $c->getTotal());
// loop through each cidr block until its ending range is more than
// the current maximum.
while (!empty($cidrs) and $cidrs[0]->getStart(true) <= $max) {
$b = array_shift($cidrs);
$newmax = bcadd($b->getStart(true), $b->getTotal());
if ($newmax > $max) {
$max = $newmax;
}
}
// add the new cidr range to the optimized list
$list = array_merge($list, self::range_to_cidrlist($start, IP::inet_dtop(bcsub($max, '1'))));
}
return $list;
}
/**
* Sort the list of CIDR blocks, optionally with a custom callback function.
*
* @param array $cidrs A list of CIDR blocks (strings or objects)
* @param Closure $callback Optional callback to perform the sorting.
* See PHP usort documentation for more details.
*/
public static function cidr_sort($cidrs, $callback = null)
{
// all indexes must be a CIDR object
$cidrs = array_map(function($o){ return $o instanceof CIDR ? $o : new CIDR($o); }, $cidrs);
if ($callback === null) {
$callback = function($a, $b) {
if (0 != ($o = BC::cmp($a->getStart(true), $b->getStart(true)))) {
return $o; // < or >
}
if ($a->getPrefix() == $b->getPrefix()) {
return 0;
}
return $a->getPrefix() < $b->getPrefix() ? -1 : 1;
};
} elseif (!($callback instanceof \Closure) or !is_callable($callback)) {
throw new \InvalidArgumentException("Invalid callback in CIDR::cidr_sort, expected Closure, got " . gettype($callback));
}
usort($cidrs, $callback);
return $cidrs;
}
/**
* Return the Prefix bits from the IPv4 mask given.
*
* This is only valid for IPv4 addresses since IPv6 addressing does not
* have a concept of network masks.
*
* Example: 255.255.255.0 == 24
*
* @param string $mask IPv4 network mask.
*/
public static function mask_to_prefix($mask)
{
if (false === filter_var($mask, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
throw new \InvalidArgumentException("Invalid IP netmask \"$mask\"");
}
return strrpos(IP::inet_ptob($mask, 32), '1') + 1;
}
/**
* Return the network mask for the prefix given.
*
* Normally this is only useful for IPv4 addresses but you can generate a
* mask for IPv6 addresses as well, only because its mathematically
* possible.
*
* @param integer $prefix CIDR prefix bits (0-128)
* @param integer $version IP version. If null the version will be detected
* based on the prefix length given.
*/
public static function prefix_to_mask($prefix, $version = null)
{
if ($version === null) {
$version = $prefix > 32 ? 6 : 4;
}
if ($prefix < 0 or $prefix > 128) {
throw new \InvalidArgumentException("Invalid prefix length \"$prefix\"");
}
if ($version != 4 and $version != 6) {
throw new \InvalidArgumentException("Invalid version \"$version\". Must be 4 or 6");
}
if ($version == 4) {
return long2ip($prefix == 0 ? 0 : (0xFFFFFFFF >> (32 - $prefix)) << (32 - $prefix));
} else {
return IP::inet_dtop($prefix == 0 ? 0 : BC::bcleft(BC::bcright(BC::MAX_UINT_128, 128-$prefix), 128-$prefix));
}
}
/**
* Return true if the $ip given is a true CIDR block.
*
* A true CIDR block is one where the $ip given is the actual Network
* address and broadcast matches the prefix appropriately.
*/
public static function cidr_is_true($ip)
{
$ip = new CIDR($ip);
return $ip->isTrueCidr();
}
}

View file

@ -1,207 +0,0 @@
<?php
/**
* This file is part of the Lifo\IP PHP Library.
*
* (c) Jason Morriss <lifo2013@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Lifo\IP;
/**
* IP Address helper class.
*
* Provides routines to translate IPv4 and IPv6 addresses between human readable
* strings, decimal, hexidecimal and binary.
*
* Requires BCmath extension and IPv6 PHP support
*/
abstract class IP
{
/**
* Convert a human readable (presentational) IP address string into a decimal string.
*/
public static function inet_ptod($ip)
{
// shortcut for IPv4 addresses
if (strpos($ip, ':') === false && strpos($ip, '.') !== false) {
return sprintf('%u', ip2long($ip));
}
// remove any cidr block notation
if (($o = strpos($ip, '/')) !== false) {
$ip = substr($ip, 0, $o);
}
// unpack into 4 32bit integers
$parts = unpack('N*', inet_pton($ip));
foreach ($parts as &$part) {
if ($part < 0) {
// convert signed int into unsigned
$part = sprintf('%u', $part);
//$part = bcadd($part, '4294967296');
}
}
// add each 32bit integer to the proper bit location in our big decimal
$decimal = $parts[4]; // << 0
$decimal = bcadd($decimal, bcmul($parts[3], '4294967296')); // << 32
$decimal = bcadd($decimal, bcmul($parts[2], '18446744073709551616')); // << 64
$decimal = bcadd($decimal, bcmul($parts[1], '79228162514264337593543950336')); // << 96
return $decimal;
}
/**
* Convert a decimal string into a human readable IP address.
*/
public static function inet_dtop($decimal, $expand = false)
{
$parts = array();
$parts[1] = bcdiv($decimal, '79228162514264337593543950336', 0); // >> 96
$decimal = bcsub($decimal, bcmul($parts[1], '79228162514264337593543950336'));
$parts[2] = bcdiv($decimal, '18446744073709551616', 0); // >> 64
$decimal = bcsub($decimal, bcmul($parts[2], '18446744073709551616'));
$parts[3] = bcdiv($decimal, '4294967296', 0); // >> 32
$decimal = bcsub($decimal, bcmul($parts[3], '4294967296'));
$parts[4] = $decimal; // >> 0
foreach ($parts as &$part) {
if (bccomp($part, '2147483647') == 1) {
$part = bcsub($part, '4294967296');
}
$part = (int) $part;
}
// if the first 96bits is all zeros then we can safely assume we
// actually have an IPv4 address. Even though it's technically possible
// you're not really ever going to see an IPv6 address in the range:
// ::0 - ::ffff
// It's feasible to see an IPv6 address of "::", in which case the
// caller is going to have to account for that on their own.
if (($parts[1] | $parts[2] | $parts[3]) == 0) {
$ip = long2ip($parts[4]);
} else {
$packed = pack('N4', $parts[1], $parts[2], $parts[3], $parts[4]);
$ip = inet_ntop($packed);
}
// Turn IPv6 to IPv4 if it's IPv4
if (preg_match('/^::\d+\./', $ip)) {
return substr($ip, 2);
}
return $expand ? self::inet_expand($ip) : $ip;
}
/**
* Convert a human readable (presentational) IP address into a HEX string.
*/
public static function inet_ptoh($ip)
{
return bin2hex(inet_pton($ip));
//return BC::bcdechex(self::inet_ptod($ip));
}
/**
* Convert a human readable (presentational) IP address into a BINARY string.
*/
public static function inet_ptob($ip, $bits = 128)
{
return BC::bcdecbin(self::inet_ptod($ip), $bits);
}
/**
* Convert a binary string into an IP address (presentational) string.
*/
public static function inet_btop($bin)
{
return self::inet_dtop(BC::bcbindec($bin));
}
/**
* Convert a HEX string into a human readable (presentational) IP address
*/
public static function inet_htop($hex)
{
return self::inet_dtop(BC::bchexdec($hex));
}
/**
* Expand an IP address. IPv4 addresses are returned as-is.
*
* Example:
* 2001::1 expands to 2001:0000:0000:0000:0000:0000:0000:0001
* ::127.0.0.1 expands to 0000:0000:0000:0000:0000:0000:7f00:0001
* 127.0.0.1 expands to 127.0.0.1
*/
public static function inet_expand($ip)
{
// strip possible cidr notation off
if (($pos = strpos($ip, '/')) !== false) {
$ip = substr($ip, 0, $pos);
}
$bytes = unpack('n*', inet_pton($ip));
if (count($bytes) > 2) {
return implode(':', array_map(function ($b) {
return sprintf("%04x", $b);
}, $bytes));
}
return $ip;
}
/**
* Convert an IPv4 address into an IPv6 address.
*
* One use-case for this is IP 6to4 tunnels used in networking.
*
* @example
* to_ipv4("10.10.10.10") == a0a:a0a
*
* @param string $ip IPv4 address.
* @param boolean $mapped If true a Full IPv6 address is returned within the
* official ipv4to6 mapped space "0:0:0:0:0:ffff:x:x"
*/
public static function to_ipv6($ip, $mapped = false)
{
if (!self::isIPv4($ip)) {
throw new \InvalidArgumentException("Invalid IPv4 address \"$ip\"");
}
$num = IP::inet_ptod($ip);
$o1 = dechex($num >> 16);
$o2 = dechex($num & 0x0000FFFF);
return $mapped ? "0:0:0:0:0:ffff:$o1:$o2" : "$o1:$o2";
}
/**
* Returns true if the IP address is a valid IPv4 address
*/
public static function isIPv4($ip)
{
return $ip === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
}
/**
* Returns true if the IP address is a valid IPv6 address
*/
public static function isIPv6($ip)
{
return $ip === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
}
/**
* Compare two IP's (v4 or v6) and return -1, 0, 1 if the first is < = >
* the second.
*
* @param string $ip1 IP address
* @param string $ip2 IP address to compare against
* @return integer Return -1,0,1 depending if $ip1 is <=> $ip2
*/
public static function cmp($ip1, $ip2)
{
return bccomp(self::inet_ptod($ip1), self::inet_ptod($ip2), 0);
}
}

View file

@ -32,7 +32,7 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension
new Twig_SimpleFilter('addslashes', 'addslashes'),
);
}
/**
* Returns a list of functions to add to the existing list.
*
@ -52,7 +52,7 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension
new Twig_SimpleFunction('link_for', 'link_for')
);
}
/**
* Returns the name of the extension.
*
@ -88,7 +88,7 @@ function twig_hasPermission_filter($mod, $permission, $board = null) {
function twig_extension_filter($value, $case_insensitive = true) {
$ext = mb_substr($value, mb_strrpos($value, '.') + 1);
if($case_insensitive)
$ext = mb_strtolower($ext);
$ext = mb_strtolower($ext);
return $ext;
}
@ -113,7 +113,7 @@ function twig_filename_truncate_filter($value, $length = 30, $separator = '…')
$value = strrev($value);
$array = array_reverse(explode(".", $value, 2));
$array = array_map("strrev", $array);
$filename = &$array[0];
$extension = isset($array[1]) ? $array[1] : false;
@ -127,11 +127,11 @@ function twig_filename_truncate_filter($value, $length = 30, $separator = '…')
function twig_ratio_function($w, $h) {
return fraction($w, $h, ':');
}
function twig_secure_link_confirm($text, $title, $confirm_message, $href) {
global $config;
function twig_secure_link_confirm($text, $title, $confirm_message, $href) {
return '<a onclick="if (event.which==2) return true;if (confirm(\'' . htmlentities(addslashes($confirm_message)) . '\')) document.location=\'?/' . htmlspecialchars(addslashes($href . '/' . make_secure_link_token($href))) . '\';return false;" title="' . htmlentities($title) . '" href="?/' . $href . '">' . $text . '</a>';
}
function twig_secure_link($href) {
return $href . '/' . make_secure_link_token($href);
}

View file

@ -5,108 +5,106 @@
*/
use Vichan\Context;
use Vichan\Functions\Net;
use Vichan\Functions\{Hide, Net};
defined('TINYBOARD') or exit;
// create a hash/salt pair for validate logins
function mkhash($username, $password, $salt = false) {
function mkhash(string $username, ?string $password, mixed $salt = false): array|string {
global $config;
if (!$salt) {
// create some sort of salt for the hash
$salt = substr(base64_encode(sha1(rand() . time(), true) . $config['cookies']['salt']), 0, 15);
// Create some salt for the hash.
$salt = \bin2hex(\random_bytes(15)); // 20 characters.
$generated_salt = true;
} else {
$generated_salt = false;
}
// generate hash (method is not important as long as it's strong)
$hash = substr(
base64_encode(
md5(
$username . $config['cookies']['salt'] . sha1(
$username . $password . $salt . (
$config['mod']['lock_ip'] ? $_SERVER['REMOTE_ADDR'] : ''
), true
) . sha1($config['password_crypt_version']) // Log out users being logged in with older password encryption schema
, true
)
), 0, 20
$hash = \substr(
Hide\secure_hash(
$username . $config['cookies']['salt'] . Hide\secure_hash(
$username . $password . $salt . (
$config['mod']['lock_ip'] ? $_SERVER['REMOTE_ADDR'] : ''
), true
) . Hide\secure_hash($config['password_crypt_version'], true), // Log out users being logged in with older password encryption schema
false
),
0,
40
);
if (isset($generated_salt))
return array($hash, $salt);
else
if ($generated_salt) {
return [ $hash, $salt ];
} else {
return $hash;
}
}
function crypt_password($password) {
function crypt_password(string $password): array {
global $config;
// `salt` database field is reused as a version value. We don't want it to be 0.
$version = $config['password_crypt_version'] ? $config['password_crypt_version'] : 1;
$new_salt = generate_salt();
$password = crypt($password, $config['password_crypt'] . $new_salt . "$");
return array($version, $password);
}
function test_password($password, $salt, $test) {
global $config;
// Version = 0 denotes an old password hashing schema. In the same column, the
// password hash was kept previously
$version = (strlen($salt) <= 8) ? (int) $salt : 0;
if ($version == 0) {
$comp = hash('sha256', $salt . sha1($test));
$pre_hash = \hash('tiger160,3', $password, false); // Note that it's truncated to 72 in the next line.
$r = \password_hash($pre_hash, \PASSWORD_BCRYPT, [ 'cost' => 12 ]);
if ($r === false) {
throw new \RuntimeException("Could not hash password");
}
else {
$comp = crypt($test, $password);
return [ $version, $r ];
}
function test_password(string $db_hash, string|int $version, string $input_password): bool {
$version = (int)$version;
if ($version < 2) {
$ok = \hash_equals($db_hash, \crypt($input_password, $db_hash));
} else {
$pre_hash = \hash('tiger160,3', $input_password, false);
$ok = \password_verify($pre_hash, $db_hash);
}
return array($version, hash_equals($password, $comp));
return $ok;
}
function generate_salt() {
return strtr(base64_encode(random_bytes(16)), '+', '.');
}
function login($username, $password) {
global $mod, $config;
function login(string $username, string $password): array|false {
global $mod;
$query = prepare("SELECT `id`, `type`, `boards`, `password`, `version` FROM ``mods`` WHERE BINARY `username` = :username");
$query->bindValue(':username', $username);
$query->execute() or error(db_error($query));
$query->execute();
if ($user = $query->fetch(PDO::FETCH_ASSOC)) {
list($version, $ok) = test_password($user['password'], $user['version'], $password);
$ok = test_password($user['password'], $user['version'], $password);
if ($ok) {
if ($config['password_crypt_version'] > $version) {
if ((int)$user['version'] < 2) {
// It's time to upgrade the password hashing method!
list ($user['version'], $user['password']) = crypt_password($password);
$query = prepare("UPDATE ``mods`` SET `password` = :password, `version` = :version WHERE `id` = :id");
$query->bindValue(':password', $user['password']);
$query->bindValue(':version', $user['version']);
$query->bindValue(':id', $user['id']);
$query->execute() or error(db_error($query));
$query->execute();
}
return $mod = array(
return $mod = [
'id' => $user['id'],
'type' => $user['type'],
'username' => $username,
'hash' => mkhash($username, $user['password']),
'boards' => explode(',', $user['boards'])
);
];
}
}
return false;
}
function setCookies() {
function setCookies(): void {
global $mod, $config;
if (!$mod)
if (!$mod) {
error('setCookies() was called for a non-moderator!');
}
$is_https = Net\is_connection_https();
@ -119,14 +117,14 @@ function setCookies() {
time() + $config['cookies']['expire'], $config['cookies']['jail'] ? $config['cookies']['path'] : '/', null, $is_https, $config['cookies']['httponly']);
}
function destroyCookies() {
function destroyCookies(): void {
global $config;
$is_https = Net\is_connection_https();
// Delete the cookies
setcookie($config['cookies']['mod'], 'deleted', time() - $config['cookies']['expire'], $config['cookies']['jail']?$config['cookies']['path'] : '/', null, $is_https, true);
}
function modLog($action, $_board=null) {
function modLog(string $action, ?string $_board = null): void {
global $mod, $board, $config;
$query = prepare("INSERT INTO ``modlogs`` VALUES (:id, :ip, :board, :time, :text)");
$query->bindValue(':id', (isset($mod['id']) ? $mod['id'] : -1), PDO::PARAM_INT);
@ -141,16 +139,18 @@ function modLog($action, $_board=null) {
$query->bindValue(':board', null, PDO::PARAM_NULL);
$query->execute() or error(db_error($query));
if ($config['syslog'])
if ($config['syslog']) {
_syslog(LOG_INFO, '[mod/' . $mod['username'] . ']: ' . $action);
}
}
function create_pm_header() {
function create_pm_header(): mixed {
global $mod, $config;
if ($config['cache']['enabled'] && ($header = cache::get('pm_unread_' . $mod['id'])) != false) {
if ($header === true)
if ($header === true) {
return false;
}
return $header;
}
@ -159,26 +159,29 @@ function create_pm_header() {
$query->bindValue(':id', $mod['id'], PDO::PARAM_INT);
$query->execute() or error(db_error($query));
if ($pm = $query->fetch(PDO::FETCH_ASSOC))
$header = array('id' => $pm['id'], 'waiting' => $query->rowCount() - 1);
else
if ($pm = $query->fetch(PDO::FETCH_ASSOC)) {
$header = [ 'id' => $pm['id'], 'waiting' => $query->rowCount() - 1 ];
} else {
$header = true;
}
if ($config['cache']['enabled'])
if ($config['cache']['enabled']) {
cache::set('pm_unread_' . $mod['id'], $header);
}
if ($header === true)
if ($header === true) {
return false;
}
return $header;
}
function make_secure_link_token($uri) {
function make_secure_link_token(string $uri): string {
global $mod, $config;
return substr(sha1($config['cookies']['salt'] . '-' . $uri . '-' . $mod['id']), 0, 8);
}
function check_login(Context $ctx, $prompt = false) {
function check_login(Context $ctx, bool $prompt = false): void {
global $config, $mod;
// Validate session
@ -188,7 +191,9 @@ function check_login(Context $ctx, $prompt = false) {
if (count($cookie) != 3) {
// Malformed cookies
destroyCookies();
if ($prompt) mod_login($ctx);
if ($prompt) {
mod_login($ctx);
}
exit;
}
@ -201,7 +206,9 @@ function check_login(Context $ctx, $prompt = false) {
if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) {
// Malformed cookies
destroyCookies();
if ($prompt) mod_login($ctx);
if ($prompt) {
mod_login($ctx);
}
exit;
}

File diff suppressed because it is too large Load diff

View file

@ -1,152 +0,0 @@
<?php
/*
* Copyright (c) 2016 vichan-devel
*/
defined('TINYBOARD') or exit;
function gen_msgid($board, $id) {
global $config;
$b = preg_replace("/[^0-9a-zA-Z$]/", 'x', $board);
$salt = sha1($board . "|" . $id . "|" . $config['nntpchan']['salt']);
$salt = substr($salt, 0, 7);
$salt = base_convert($salt, 16, 36);
return "<$b.$id.$salt@".$config['nntpchan']['domain'].">";
}
function gen_nntp($headers, $files) {
if (count($files) == 0) {
}
else if (count($files) == 1 && $files[0]['type'] == 'text/plain') {
$content = $files[0]['text'] . "\r\n";
$headers['Content-Type'] = "text/plain; charset=UTF-8";
}
else {
$boundary = sha1($headers['Message-Id']);
$content = "";
$headers['Content-Type'] = "multipart/mixed; boundary=$boundary";
foreach ($files as $file) {
$content .= "--$boundary\r\n";
if (isset($file['name'])) {
$file['name'] = preg_replace('/[\r\n\0"]/', '', $file['name']);
$content .= "Content-Disposition: form-data; filename=\"$file[name]\"; name=\"attachment\"\r\n";
}
$type = explode('/', $file['type'])[0];
if ($type == 'text') {
$file['type'] .= '; charset=UTF-8';
}
$content .= "Content-Type: $file[type]\r\n";
if ($type != 'text' && $type != 'message') {
$file['text'] = base64_encode($file['text']);
$content .= "Content-Transfer-Encoding: base64\r\n";
}
$content .= "\r\n";
$content .= $file['text'];
$content .= "\r\n";
}
$content .= "--$boundary--\r\n";
$headers['Mime-Version'] = '1.0';
}
//$headers['Content-Length'] = strlen($content);
$headers['Date'] = date('r', $headers['Date']);
$out = "";
foreach ($headers as $id => $val) {
$val = str_replace("\n", "\n\t", $val);
$out .= "$id: $val\r\n";
}
$out .= "\r\n";
$out .= $content;
return $out;
}
function nntp_publish($msg, $id) {
global $config;
$server = $config["nntpchan"]["server"];
$s = fsockopen("tcp://$server");
fgets($s);
fputs($s, "MODE STREAM\r\n");
fgets($s);
fputs($s, "TAKETHIS $id\r\n");
fputs($s, $msg);
fputs($s, "\r\n.\r\n");
fgets($s);
fputs($s, "QUIT\r\n");
fclose($s);
}
function post2nntp($post, $msgid) {
global $config;
$headers = array();
$files = array();
$headers['Message-Id'] = $msgid;
$headers['Newsgroups'] = $config['nntpchan']['group'];
$headers['Date'] = time();
$headers['Subject'] = $post['subject'] ? $post['subject'] : "None";
$headers['From'] = $post['name'] . " <poster@" . $config['nntpchan']['domain'] . ">";
if ($post['email'] == 'sage') {
$headers['X-Sage'] = true;
}
if (!$post['op']) {
// Get muh parent
$query = prepare("SELECT `message_id` FROM ``nntp_references`` WHERE `board` = :board AND `id` = :id");
$query->bindValue(':board', $post['board']);
$query->bindValue(':id', $post['thread']);
$query->execute() or error(db_error($query));
if ($result = $query->fetch(PDO::FETCH_ASSOC)) {
$headers['References'] = $result['message_id'];
}
else {
return false; // We don't have OP. Discarding.
}
}
// Let's parse the body a bit.
$body = trim($post['body_nomarkup']);
$body = preg_replace('/\r?\n/', "\r\n", $body);
$body = preg_replace_callback('@>>(>/([a-zA-Z0-9_+-]+)/)?([0-9]+)@', function($o) use ($post) {
if ($o[1]) {
$board = $o[2];
}
else {
$board = $post['board'];
}
$id = $o[3];
$query = prepare("SELECT `message_id_digest` FROM ``nntp_references`` WHERE `board` = :board AND `id` = :id");
$query->bindValue(':board', $board);
$query->bindValue(':id', $id);
$query->execute() or error(db_error($query));
if ($result = $query->fetch(PDO::FETCH_ASSOC)) {
return ">>".substr($result['message_id_digest'], 0, 18);
}
else {
return $o[0]; // Should send URL imo
}
}, $body);
$body = preg_replace('/>>>>([0-9a-fA-F])+/', '>>\1', $body);
$files[] = array('type' => 'text/plain', 'text' => $body);
foreach ($post['files'] as $id => $file) {
$fc = array();
$fc['type'] = $file['type'];
$fc['text'] = file_get_contents($file['file_path']);
$fc['name'] = $file['name'];
$files[] = $fc;
}
return array($headers, $files);
}

View file

@ -1,30 +0,0 @@
<?php
define('TINYBOARD', 'fuck yeah');
require_once('nntpchan.php');
die();
$time = time();
echo "\n@@@@ Thread:\n";
echo $m0 = gennntp(["From" => "czaks <marcin@6irc.net>", "Message-Id" => "<1234.0000.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None"],
[['type' => 'text/plain', 'text' => "THIS IS A NEW TEST THREAD"]]);
echo "\n@@@@ Single msg:\n";
echo $m1 = gennntp(["From" => "czaks <marcin@6irc.net>", "Message-Id" => "<1234.1234.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"],
[['type' => 'text/plain', 'text' => "hello world, with no image :("]]);
echo "\n@@@@ Single msg and pseudoimage:\n";
echo $m2 = gennntp(["From" => "czaks <marcin@6irc.net>", "Message-Id" => "<1234.2137.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"],
[['type' => 'text/plain', 'text' => "hello world, now with an image!"],
['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif.gif"]]);
echo "\n@@@@ Single msg and two pseudoimages:\n";
echo $m3 = gennntp(["From" => "czaks <marcin@6irc.net>", "Message-Id" => "<1234.1488.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"],
[['type' => 'text/plain', 'text' => "hello world, now WITH TWO IMAGES!!!"],
['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif.gif"],
['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif2.gif"]]);
shoveitup($m0, "<1234.0000.".$time."@example.vichan.net>");
sleep(1);
shoveitup($m1, "<1234.1234.".$time."@example.vichan.net>");
sleep(1);
shoveitup($m2, "<1234.2137.".$time."@example.vichan.net>");
shoveitup($m3, "<1234.1488.".$time."@example.vichan.net>");

View file

@ -881,6 +881,7 @@ if ($step == 0) {
$config['cookies']['salt'] = substr(base64_encode(sha1(rand())), 0, 30);
$config['secure_trip_salt'] = substr(base64_encode(sha1(rand())), 0, 30);
$config['secure_password_salt'] = substr(base64_encode(sha1(rand())), 0, 30);
echo Element('page.html', array(
'body' => Element('installer/config.html', array(

View file

@ -18,16 +18,25 @@ $(window).ready(function() {
// Enable submit button if disabled (cache problem)
$('input[type="submit"]').removeAttr('disabled');
var setup_form = function($form) {
$form.submit(function() {
if (do_not_ajax)
return true;
// If the captcha is present, halt if it does not have a response.
if (captchaMode === 'static' || (captchaMode === 'dynamic' && isDynamicCaptchaEnabled())) {
if (captcha_renderer && postCaptchaId && !captcha_renderer.hasResponse(postCaptchaId)) {
captcha_renderer.execute(postCaptchaId);
return false;
}
}
var form = this;
var submit_txt = $(this).find('input[type="submit"]').val();
if (window.FormData === undefined)
return true;
var formData = new FormData(this);
formData.append('json_response', '1');
formData.append('post', submit_txt);
@ -94,15 +103,15 @@ $(window).ready(function() {
setTimeout(function() { $(window).trigger("scroll"); }, 100);
}
});
highlightReply(post_response.id);
window.location.hash = post_response.id;
$(window).scrollTop($(document).height());
$(form).find('input[type="submit"]').val(submit_txt);
$(form).find('input[type="submit"]').removeAttr('disabled');
$(form).find('input[name="subject"],input[name="file_url"],\
textarea[name="body"],input[type="file"]').val('').change();
textarea[name="body"],input[type="file"],input[name="embed"]').val('').change();
},
cache: false,
contentType: false,
@ -114,7 +123,7 @@ $(window).ready(function() {
$(form).find('input[type="submit"]').val(submit_txt);
$(form).find('input[type="submit"]').removeAttr('disabled');
$(form).find('input[name="subject"],input[name="file_url"],\
textarea[name="body"],input[type="file"]').val('').change();
textarea[name="body"],input[type="file"],input[name="embed"]').val('').change();
} else {
alert(_('An unknown error occured when posting!'));
$(form).find('input[type="submit"]').val(submit_txt);
@ -132,10 +141,10 @@ $(window).ready(function() {
contentType: false,
processData: false
}, 'json');
$(form).find('input[type="submit"]').val(_('Posting...'));
$(form).find('input[type="submit"]').attr('disabled', true);
return false;
});
};

View file

@ -17,6 +17,10 @@ $(document).ready(function() {
// Default maximum image loads.
const DEFAULT_MAX = 5;
if (localStorage.inline_expand_fit_height !== 'false') {
$('<style id="expand-fit-height-style">.full-image { max-height: ' + window.innerHeight + 'px; }</style>').appendTo($('head'));
}
let inline_expand_post = function() {
let link = this.getElementsByTagName('a');
@ -56,12 +60,12 @@ $(document).ready(function() {
},
add: function(ele) {
ele.deferred = $.Deferred();
ele.deferred.done(function () {
ele.deferred.done(function() {
let $loadstart = $.Deferred();
let thumb = ele.childNodes[0];
let img = ele.childNodes[1];
let onLoadStart = function (img) {
let onLoadStart = function(img) {
if (img.naturalWidth) {
$loadstart.resolve(img, thumb);
} else {
@ -69,15 +73,15 @@ $(document).ready(function() {
}
};
$(img).one('load', function () {
$.when($loadstart).done(function () {
// Once fully loaded, update the waiting queue.
$(img).one('load', function() {
$.when($loadstart).done(function() {
// once fully loaded, update the waiting queue
--loading;
$(ele).data('imageLoading', 'false');
update();
});
});
$loadstart.done(function (img, thumb) {
$loadstart.done(function(img, thumb) {
thumb.style.display = 'none';
img.style.display = '';
});
@ -202,6 +206,8 @@ $(document).ready(function() {
Options.extend_tab('general', '<span id="inline-expand-max">' +
_('Number of simultaneous image downloads (0 to disable): ') +
'<input type="number" step="1" min="0" size="4"></span>');
Options.extend_tab('general', '<label id="inline-expand-fit-height"><input type="checkbox">' + _('Fit expanded images into screen height') + '</label>');
$('#inline-expand-max input')
.css('width', '50px')
.val(localStorage.inline_expand_max || DEFAULT_MAX)
@ -212,6 +218,21 @@ $(document).ready(function() {
localStorage.inline_expand_max = val;
});
$('#inline-expand-fit-height input').on('change', function() {
if (localStorage.inline_expand_fit_height !== 'false') {
localStorage.inline_expand_fit_height = 'false';
$('#expand-fit-height-style').remove();
}
else {
localStorage.inline_expand_fit_height = 'true';
$('<style id="expand-fit-height-style">.full-image { max-height: ' + window.innerHeight + 'px; }</style>').appendTo($('head'));
}
});
if (localStorage.inline_expand_fit_height !== 'false') {
$('#inline-expand-fit-height input').prop('checked', true);
}
}
if (window.jQuery) {

View file

@ -43,9 +43,6 @@ $(function(){
document.location.reload();
}
});
$("#style-select").detach().css({float:"none","margin-bottom":0}).appendTo(tab.content);
});
}();

View file

@ -237,12 +237,8 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
var postUid = $ele.find('.poster_id').text();
}
let postName;
let postTrip = '';
if (!pageData.forcedAnon) {
postName = (typeof $ele.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($ele.find('.name')[0]);
postTrip = $ele.find('.trip').text();
}
let postName = (typeof $ele.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($ele.find('.name')[0]);
let postTrip = $ele.find('.trip').text();
/* display logic and bind click handlers
*/
@ -297,39 +293,34 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
}
// name
if (!pageData.forcedAnon && !$ele.data('hiddenByName')) {
if (!$ele.data('hiddenByName')) {
$buffer.find('#filter-add-name').click(function () {
addFilter('name', postName, false);
});
$buffer.find('#filter-remove-name').addClass('hidden');
} else if (!pageData.forcedAnon) {
} else {
$buffer.find('#filter-remove-name').click(function () {
removeFilter('name', postName, false);
});
$buffer.find('#filter-add-name').addClass('hidden');
} else {
// board has forced anon
$buffer.find('#filter-remove-name').addClass('hidden');
$buffer.find('#filter-add-name').addClass('hidden');
}
// tripcode
if (!pageData.forcedAnon && !$ele.data('hiddenByTrip') && postTrip !== '') {
if (!$ele.data('hiddenByTrip') && postTrip !== '') {
$buffer.find('#filter-add-trip').click(function () {
addFilter('trip', postTrip, false);
});
$buffer.find('#filter-remove-trip').addClass('hidden');
} else if (!pageData.forcedAnon && postTrip !== '') {
} else if (postTrip !== '') {
$buffer.find('#filter-remove-trip').click(function () {
removeFilter('trip', postTrip, false);
});
$buffer.find('#filter-add-trip').addClass('hidden');
} else {
// board has forced anon
$buffer.find('#filter-remove-trip').addClass('hidden');
$buffer.find('#filter-add-trip').addClass('hidden');
}
@ -391,7 +382,6 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
var localList = pageData.localList;
var noReplyList = pageData.noReplyList;
var hasUID = pageData.hasUID;
var forcedAnon = pageData.forcedAnon;
var hasTrip = ($post.find('.trip').length > 0);
var hasSub = ($post.find('.subject').length > 0);
@ -432,9 +422,8 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
}
// matches generalFilter
if (!forcedAnon)
name = (typeof $post.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($post.find('.name')[0]);
if (!forcedAnon && hasTrip)
name = (typeof $post.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($post.find('.name')[0]);
if (hasTrip)
trip = $post.find('.trip').text();
if (hasSub)
subject = $post.find('.subject').text();
@ -455,13 +444,13 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
pattern = new RegExp(rule.value);
switch (rule.type) {
case 'name':
if (!forcedAnon && pattern.test(name)) {
if (pattern.test(name)) {
$post.data('hiddenByName', true);
hide(post);
}
break;
case 'trip':
if (!forcedAnon && hasTrip && pattern.test(trip)) {
if (hasTrip && pattern.test(trip)) {
$post.data('hiddenByTrip', true);
hide(post);
}
@ -488,13 +477,13 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
} else {
switch (rule.type) {
case 'name':
if (!forcedAnon && rule.value == name) {
if (rule.value == name) {
$post.data('hiddenByName', true);
hide(post);
}
break;
case 'trip':
if (!forcedAnon && hasTrip && rule.value == trip) {
if (hasTrip && rule.value == trip) {
$post.data('hiddenByTrip', true);
hide(post);
}
@ -827,8 +816,7 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
boardId: board_name, // get the id from the global variable
localList: [], // all the blacklisted post IDs or UIDs that apply to the current page
noReplyList: [], // any posts that replies to the contents of this list shall be hidden
hasUID: (document.getElementsByClassName('poster_id').length > 0),
forcedAnon: ($('th:contains(Name)').length === 0) // tests by looking for the Name label on the reply form
hasUID: (document.getElementsByClassName('poster_id').length > 0)
};
initStyle();

View file

@ -104,8 +104,10 @@ function buildMenu(e) {
function addButton(post) {
var $ele = $(post);
// Use unicode code with ascii variant selector
// https://stackoverflow.com/questions/37906969/how-to-prevent-ios-from-converting-ascii-into-emoji
$ele.find('input.delete').after(
$('<a>', {href: '#', class: 'post-btn', title: 'Post menu'}).text('►')
$('<a>', {href: '#', class: 'post-btn', title: 'Post menu'}).text('\u{25B6}\u{fe0e}')
);
}

View file

@ -15,7 +15,7 @@
$(document).ready(function() {
let showBackLinks = function() {
let replyId = $(this).attr('id').replace(/^reply_/, '');
let replyId = $(this).attr('id').split('_')[1];
$(this).find('div.body a:not([rel="nofollow"])').each(function() {
let id = $(this).text().match(/^>>(\d+)$/);
@ -25,13 +25,15 @@ $(document).ready(function() {
return;
}
let post = $('#reply_' + id);
if(post.length == 0)
let post = $('#reply_' + id + ', #op_' + id);
if (post.length == 0) {
return;
}
let mentioned = post.find('.head div.mentioned');
if (mentioned.length === 0) {
mentioned = $('<div class="mentioned unimportant"></div>').prependTo(post.find('.head'));
// The op has two "head"s divs, use the second.
mentioned = $('<div class="mentioned unimportant"></div>').prependTo(post.find('.head').last());
}
if (mentioned.find('a.mentioned-' + replyId).length !== 0) {
@ -48,13 +50,13 @@ $(document).ready(function() {
});
};
$('div.post.reply').each(showBackLinks);
$('div.post').each(showBackLinks);
$(document).on('new_post', function(e, post) {
if ($(post).hasClass('reply')) {
if ($(post).hasClass('reply') || $(post).hasClass('op')) {
showBackLinks.call(post);
} else {
$(post).find('div.post.reply').each(showBackLinks);
$(post).find('div.post').each(showBackLinks);
}
});
});

36
js/style-select-simple.js Normal file
View file

@ -0,0 +1,36 @@
/*
* style-select-simple.js
*
* Changes the stylesheet chooser links to a <select>
*
* Released under the MIT license
* Copyright (c) 2025 Zankaria Auxa <zankaria.auxa@mailu.io>
*
* Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js';
* // $config['additional_javascript'][] = 'js/style-select.js'; // Conflicts with this file.
* $config['additional_javascript'][] = 'js/style-select-simple.js';
*/
$(document).ready(function() {
let newElement = document.createElement('div');
newElement.className = 'styles';
// styles is defined in main.js.
for (styleName in styles) {
if (styleName) {
let style = document.createElement('a');
style.innerHTML = '[' + styleName + ']';
style.onclick = function() {
changeStyle(this.innerHTML.substring(1, this.innerHTML.length - 1), this);
};
if (styleName == selectedstyle) {
style.className = 'selected';
}
style.href = 'javascript:void(0);';
newElement.appendChild(style);
}
}
document.getElementById('bottom-hud').before(newElement);
});

View file

@ -6,48 +6,53 @@
*
* Released under the MIT license
* Copyright (c) 2013 Michael Save <savetheinternet@tinyboard.org>
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
*
* Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/style-select.js';
*
*/
$(document).ready(function() {
var stylesDiv = $('div.styles');
var pages = $('div.pages');
var stylesSelect = $('<select></select>').css({float:"none"});
var options = [];
var i = 1;
stylesDiv.children().each(function() {
var name = this.innerHTML.replace(/(^\[|\]$)/g, '');
var opt = $('<option></option>')
.html(name)
.val(i);
if ($(this).hasClass('selected'))
opt.attr('selected', true);
options.push ([name.toUpperCase (), opt]);
$(this).attr('id', 'style-select-' + i);
i++;
});
let pages = $('div.pages');
let stylesSelect = $('<select></select>').css({float:"none"});
let options = [];
options.sort ((a, b) => {
let i = 1;
for (styleName in styles) {
if (styleName) {
let opt = $('<option></option>')
.html(styleName)
.val(i);
if (selectedstyle == styleName) {
opt.attr('selected', true);
}
opt.attr('id', 'style-select-' + i);
options.push([styleName.toUpperCase (), opt]);
i++;
}
}
options.sort((a, b) => {
const keya = a [0];
const keyb = b [0];
if (keya < keyb) { return -1; }
if (keya > keyb) { return 1; }
if (keya < keyb) {
return -1;
}
if (keya > keyb) {
return 1;
}
return 0;
}).forEach (([key, opt]) => {
}).forEach(([key, opt]) => {
stylesSelect.append(opt);
});
stylesSelect.change(function() {
$('#style-select-' + $(this).val()).click();
let sel = $(this).find(":selected")[0];
let styleName = sel.innerHTML;
changeStyle(styleName, sel);
});
stylesDiv.hide()
pages.after(
$('<div id="style-select"></div>')
.append(_('Select theme: '), stylesSelect)

View file

@ -1,41 +1,41 @@
/*
* youtube
* https://github.com/savetheinternet/Tinyboard/blob/master/js/youtube.js
*
* Don't load the YouTube player unless the video image is clicked.
* This increases performance issues when many videos are embedded on the same page.
* Currently only compatiable with YouTube.
*
* Proof of concept.
*
* Released under the MIT license
* Copyright (c) 2013 Michael Save <savetheinternet@tinyboard.org>
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
*
* Usage:
* $config['embedding'] = array();
* $config['embedding'][0] = array(
* '/^https?:\/\/(\w+\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9\-_]{10,11})(&.+)?$/i',
* $config['youtube_js_html']);
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/youtube.js';
*
*/
* Don't load the 3rd party embedded content player unless the image is clicked.
* This increases performance issues when many videos are embedded on the same page.
*
* Released under the MIT license
* Copyright (c) 2013 Michael Save <savetheinternet@tinyboard.org>
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
* Copyright (c) 2025 Zankaria Auxa <zankaria.auxa@mailu.io>
*
* Usage:
* $config['embedding'] = array();
* $config['embedding'][0] = array(
* '/^https?:\/\/(\w+\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9\-_]{10,11})(&.+)?$/i',
* $config['youtube_js_html']);
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/youtube.js';
*/
$(document).ready(function(){
// Adds Options panel item
$(document).ready(function() {
const ON = '[Remove]';
const YOUTUBE = 'www.youtube-nocookie.com';
function makeEmbedNode(embedHost, videoId, width, height) {
return $(`<iframe type="text/html" width="${width}" height="${height}" class="full-image"
src="https://${embedHost}/embed/${videoId}?autoplay=1" allow="fullscreen" frameborder="0" referrerpolicy="strict-origin"/>`);
}
// Adds Options panel item.
if (typeof localStorage.youtube_embed_proxy === 'undefined') {
if (location.hostname.includes(".onion")){
localStorage.youtube_embed_proxy = 'tuberyps2pn6dor6h47brof3w2asmauahhk4ei42krugybzzzo55klad.onion';
} else {
localStorage.youtube_embed_proxy = 'incogtube.com'; //default value
}
localStorage.youtube_embed_proxy = 'incogtube.com'; // Default value.
}
if (window.Options && Options.get_tab('general')) {
Options.extend_tab("general", "<fieldset id='media-proxy-fs'><legend>"+_("Media Proxy (requires refresh)")+"</legend>"
+ ('<label id="youtube-embed-proxy-url">' + _('YouTube embed proxy url&nbsp;&nbsp;')+'<input type="text" size=30></label>')
+ '</fieldset>');
Options.extend_tab("general",
"<fieldset id='media-proxy-fs'><legend>" + _("Media Proxy (requires refresh)") + "</legend>"
+ '<label id="youtube-embed-proxy-url">' + _('YouTube embed proxy url&nbsp;&nbsp;')
+ '<input type="text" size=30></label>'
+ '</fieldset>');
$('#youtube-embed-proxy-url>input').val(localStorage.youtube_embed_proxy);
$('#youtube-embed-proxy-url>input').on('input', function() {
@ -43,51 +43,65 @@ $(document).ready(function(){
});
}
const ON = "[Remove]";
const OFF = "[Embed]";
const YOUTUBE = 'www.youtube.com';
const PROXY = localStorage.youtube_embed_proxy;
function addEmbedButton(index, videoNode) {
videoNode = $(videoNode);
var contents = videoNode.contents();
var videoId = videoNode.data('video');
var span = $("<span>[Embed]</span>");
var spanProxy = $("<span>[Proxy]</span>");
const proxy = localStorage.youtube_embed_proxy;
var makeEmbedNode = function(embedHost) {
return $('<iframe style="float:left;margin: 10px 20px" type="text/html" '+
'width="360" height="270" src="//' + embedHost + '/embed/' + videoId +
'?autoplay=1&html5=1" allowfullscreen frameborder="0"/>');
}
var defaultEmbed = makeEmbedNode(location.hostname.includes(".onion") ? PROXY : YOUTUBE);
var proxyEmbed = makeEmbedNode(PROXY);
videoNode.click(function(e) {
function addEmbedButton(_i, node) {
node = $(node);
const contents = node.contents();
const embedUrl = node.data('video-id');
const embedWidth = node.data('iframe-width');
const embedHeight = node.data('iframe-height');
const span = $('<span>[Embed]</span>');
const spanProxy = $("<span>[Proxy]</span>");
let iframeDefault = null;
let iframeProxy = null;
node.click(function(e) {
e.preventDefault();
if (span.text() == ON){
videoNode.append(spanProxy);
videoNode.append(contents);
defaultEmbed.remove();
proxyEmbed.remove();
span.text(OFF);
if (span.text() == ON) {
contents.css('display', '');
spanProxy.css('display', '');
if (iframeDefault !== null) {
iframeDefault.remove();
}
if (iframeProxy !== null) {
iframeProxy.remove();
}
span.text('[Embed]');
} else {
contents.detach();
let useProxy = e.target == spanProxy[0];
// Lazily create the iframes.
if (useProxy) {
if (iframeProxy === null) {
iframeProxy = makeEmbedNode(proxy, embedUrl, embedWidth, embedHeight);
}
node.prepend(iframeProxy);
} else {
if (iframeDefault === null) {
iframeDefault = makeEmbedNode(YOUTUBE, embedUrl, embedWidth, embedHeight);
}
node.prepend(iframeDefault);
}
contents.css('display', 'none');
spanProxy.css('display', 'none');
span.text(ON);
spanProxy.remove();
videoNode.append(e.target == spanProxy[0] ? proxyEmbed : defaultEmbed);
}
});
videoNode.append(span);
videoNode.append(spanProxy);
node.append(span);
node.append(spanProxy);
}
$('div.video-container', document).each(addEmbedButton);
// allow to work with auto-reload.js, etc.
// Allow to work with auto-reload.js, etc.
$(document).on('new_post', function(e, post) {
$('div.video-container', post).each(addEmbedButton);
});
});

View file

@ -68,9 +68,15 @@ $pages = [
'/reports/(\d+)/dismiss(all)?' => 'secure report_dismiss', // dismiss a report
'/IP/([\w.:]+)' => 'secure_POST ip', // view ip address
'/IP/([\w.:]+)/cursor/([\w|-|_|.]+)' => 'secure_POST ip', // view ip address
'/IP/([\w.:]+)/cursor/([\w|-|_]+)' => 'secure_POST ip', // view ip address
'/IP/([\w.:]+)/remove_note/(\d+)' => 'secure ip_remove_note', // remove note from ip address
'/user_posts/ip/([\w.:]+)' => 'secure_POST user_posts_by_ip', // view user posts by ip address
'/user_posts/ip/([\w.:]+)/cursor/([\w|-|_]+)' => 'secure_POST user_posts_by_ip', // remove note from ip address
'/user_posts/passwd/(\w+)' => 'secure_POST user_posts_by_passwd', // view user posts by ip address
'/user_posts/passwd/(\w+)/cursor/([\w|-|_]+)' => 'secure_POST user_posts_by_passwd', // remove note from ip address
'/ban' => 'secure_POST ban', // new ban
'/bans' => 'secure_POST bans', // ban list
'/bans.json' => 'secure bans_json', // ban list JSON

674
post.php
View file

@ -5,6 +5,7 @@
use Vichan\Context;
use Vichan\Data\ReportQueries;
use Vichan\Data\Driver\Log\LogDriver;
require_once 'inc/bootstrap.php';
@ -221,7 +222,7 @@ function send_matrix_report(
$end = strlen($post['body_nomarkup']) > $max_msg_len ? ' [...]' : '';
$post_content = mb_substr($post['body_nomarkup'], 0, $max_msg_len) . $end;
$text_body = $reported_post_url . ($post['thread'] ? "#$post_id" : '') . " \nReason:\n" . $report_reason . " \nPost:\n" . $post_content;
$text_body = $reported_post_url . ($post['thread'] ? "#$id" : '') . " \nReason:\n" . $report_reason . " \nPost:\n" . $post_content;
$random_transaction_id = mt_rand();
$json_body = json_encode([
@ -352,166 +353,6 @@ function db_select_ban_appeals($ban_id)
* Method handling functions
*/
$dropped_post = false;
function handle_nntpchan()
{
global $config;
if ($_SERVER['REMOTE_ADDR'] != $config['nntpchan']['trusted_peer']) {
error("NNTPChan: Forbidden. $_SERVER[REMOTE_ADDR] is not a trusted peer");
}
$_POST = [];
$_POST['json_response'] = true;
$headers = json_encode($_GET);
if (!isset($_GET['Message-Id'])) {
if (!isset($_GET['Message-ID'])) {
error("NNTPChan: No message ID");
} else {
$msgid = $_GET['Message-ID'];
}
} else {
$msgid = $_GET['Message-Id'];
}
$groups = preg_split("/,\s*/", $_GET['Newsgroups']);
if (count($groups) != 1) {
error("NNTPChan: Messages can go to only one newsgroup");
}
$group = $groups[0];
if (!isset($config['nntpchan']['dispatch'][$group])) {
error("NNTPChan: We don't synchronize $group");
}
$xboard = $config['nntpchan']['dispatch'][$group];
$ref = null;
if (isset($_GET['References'])) {
$refs = preg_split("/,\s*/", $_GET['References']);
if (count($refs) > 1) {
error("NNTPChan: We don't support multiple references");
}
$ref = $refs[0];
$query = prepare("SELECT `board`,`id` FROM ``nntp_references`` WHERE `message_id` = :ref");
$query->bindValue(':ref', $ref);
$query->execute() or error(db_error($query));
$ary = $query->fetchAll(PDO::FETCH_ASSOC);
if (count($ary) == 0) {
error("NNTPChan: We don't have $ref that $msgid references");
}
$p_id = $ary[0]['id'];
$p_board = $ary[0]['board'];
if ($p_board != $xboard) {
error("NNTPChan: Cross board references not allowed. Tried to reference $p_board on $xboard");
}
$_POST['thread'] = $p_id;
}
$date = isset($_GET['Date']) ? strtotime($_GET['Date']) : time();
list($ct) = explode('; ', $_GET['Content-Type']);
$query = prepare("SELECT COUNT(*) AS `c` FROM ``nntp_references`` WHERE `message_id` = :msgid");
$query->bindValue(":msgid", $msgid);
$query->execute() or error(db_error($query));
$a = $query->fetch(PDO::FETCH_ASSOC);
if ($a['c'] > 0) {
error("NNTPChan: We already have this post. Post discarded.");
}
if ($ct == 'text/plain') {
$content = file_get_contents("php://input");
} elseif ($ct == 'multipart/mixed' || $ct == 'multipart/form-data') {
_syslog(LOG_INFO, "MM: Files: " . print_r($GLOBALS, true)); // Debug
$content = '';
$newfiles = [];
foreach ($_FILES['attachment']['error'] as $id => $error) {
if ($_FILES['attachment']['type'][$id] == 'text/plain') {
$content .= file_get_contents($_FILES['attachment']['tmp_name'][$id]);
} elseif ($_FILES['attachment']['type'][$id] == 'message/rfc822') {
// Signed message, ignore for now
} else {
// A real attachment :^)
$file = [];
$file['name'] = $_FILES['attachment']['name'][$id];
$file['type'] = $_FILES['attachment']['type'][$id];
$file['size'] = $_FILES['attachment']['size'][$id];
$file['tmp_name'] = $_FILES['attachment']['tmp_name'][$id];
$file['error'] = $_FILES['attachment']['error'][$id];
$newfiles["file$id"] = $file;
}
}
$_FILES = $newfiles;
} else {
error("NNTPChan: Wrong mime type: $ct");
}
$_POST['subject'] = isset($_GET['Subject']) ? ($_GET['Subject'] == 'None' ? '' : $_GET['Subject']) : '';
$_POST['board'] = $xboard;
if (isset($_GET['From'])) {
list($name, $mail) = explode(" <", $_GET['From'], 2);
$mail = preg_replace('/>\s+$/', '', $mail);
$_POST['name'] = $name;
//$_POST['email'] = $mail;
$_POST['email'] = '';
}
if (isset($_GET['X_Sage'])) {
$_POST['email'] = 'sage';
}
$content = preg_replace_callback('/>>([0-9a-fA-F]{6,})/', function ($id) use ($xboard) {
$id = $id[1];
$query = prepare("SELECT `board`,`id` FROM ``nntp_references`` WHERE `message_id_digest` LIKE :rule");
$idx = $id . "%";
$query->bindValue(':rule', $idx);
$query->execute() or error(db_error($query));
$ary = $query->fetchAll(PDO::FETCH_ASSOC);
if (count($ary) == 0) {
return ">>>>$id";
} else {
$ret = [];
foreach ($ary as $v) {
if ($v['board'] != $xboard) {
$ret[] = ">>>/" . $v['board'] . "/" . $v['id'];
} else {
$ret[] = ">>" . $v['id'];
}
}
return implode($ret, ", ");
}
}, $content);
$_POST['body'] = $content;
$dropped_post = array(
'date' => $date,
'board' => $xboard,
'msgid' => $msgid,
'headers' => $headers,
'from_nntp' => true,
);
}
function handle_delete(Context $ctx)
{
// Delete
@ -530,10 +371,12 @@ function handle_delete(Context $ctx)
$password = &$_POST['password'];
if ($password == '') {
if (empty($password)) {
error($config['error']['invalidpassword']);
}
$password = hashPassword($_POST['password']);
$delete = [];
foreach ($_POST as $post => $value) {
if (preg_match('/^delete_(\d+)$/', $post, $m)) {
@ -608,8 +451,8 @@ function handle_delete(Context $ctx)
modLog("User at $ip deleted his own post #$id");
}
_syslog(
LOG_INFO,
$ctx->get(LogDriver::class)->log(
LogDriver::INFO,
'Deleted post: ' .
'/' . $board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $post['thread'] ? $post['thread'] : $id) . ($post['thread'] ? '#' . $id : '')
);
@ -697,9 +540,7 @@ function handle_report(Context $ctx)
foreach ($report as $id) {
$post = db_select_post_minimal($board['uri'], $id);
if ($post === false) {
if ($config['syslog']) {
_syslog(LOG_INFO, "Failed to report non-existing post #{$id} in {$board['dir']}");
}
$ctx->get(LogDriver::class)->log(LogDriver::INFO, "Failed to report non-existing post #{$id} in {$board['dir']}");
error($config['error']['nopost']);
}
@ -714,13 +555,12 @@ function handle_report(Context $ctx)
error($error);
}
if ($config['syslog'])
_syslog(
LOG_INFO,
'Reported post: ' .
'/' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '') .
' for "' . $reason . '"'
);
$ctx->get(LogDriver::class)->log(
LogDriver::INFO,
'Reported post: ' .
'/' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '') .
' for "' . $reason . '"'
);
$report_queries->add($_SERVER['REMOTE_ADDR'], $board['uri'], $id, $reason);
@ -789,9 +629,9 @@ function handle_report(Context $ctx)
function handle_post(Context $ctx)
{
global $config, $dropped_post, $board, $mod, $pdo;
global $config, $board, $mod, $pdo;
if (!isset($_POST['body'], $_POST['board']) && !$dropped_post) {
if (!isset($_POST['body'], $_POST['board'])) {
error($config['error']['bot']);
}
@ -834,108 +674,104 @@ function handle_post(Context $ctx)
}
if (!$dropped_post) {
// Check for CAPTCHA right after opening the board so the "return" link is in there.
if ($config['captcha']['mode'] !== false) {
if (!isset($_POST['captcha-response'], $_POST['captcha-form-id'])) {
error($config['error']['bot']);
}
$expected_action = $post['op'] ? 'post-thread' : 'post-reply';
$ret = check_captcha($config['captcha'], $_POST['captcha-form-id'], $post['board'], $_POST['captcha-response'], $_SERVER['REMOTE_ADDR'], $expected_action);
if (!$ret) {
error($config['error']['captcha']);
}
}
if (isset($config['simple_spam']) && $post['op']) {
if (!isset($_POST['simple_spam']) || $config['simple_spam']['answer'] != $_POST['simple_spam']) {
error($config['error']['simple_spam']);
}
}
if (isset($config['securimage']) && $config['securimage']) {
if (!isset($_POST['captcha'])) {
error($config['error']['securimage']['missing']);
}
if (empty($_POST['captcha'])) {
error($config['error']['securimage']['empty']);
}
if (!db_delete_captcha($_SERVER['REMOTE_ADDR'], $_POST['captcha'])) {
error($config['error']['securimage']['bad']);
}
}
if (
!(($post['op'] && $_POST['post'] == $config['button_newtopic']) ||
(!$post['op'] && $_POST['post'] == $config['button_reply']))
) {
// Check for CAPTCHA right after opening the board so the "return" link is in there.
if ($config['captcha']['mode'] !== false) {
if (!isset($_POST['captcha-response'], $_POST['captcha-form-id'])) {
error($config['error']['bot']);
}
// Check the referrer
if (
$config['referer_match'] !== false &&
(!isset($_SERVER['HTTP_REFERER']) || !preg_match($config['referer_match'], rawurldecode($_SERVER['HTTP_REFERER'])))
) {
error($config['error']['referer']);
$expected_action = $post['op'] ? 'post-thread' : 'post-reply';
$ret = check_captcha($config['captcha'], $_POST['captcha-form-id'], $post['board'], $_POST['captcha-response'], $_SERVER['REMOTE_ADDR'], $expected_action);
if (!$ret) {
error($config['error']['captcha']);
}
}
if (isset($config['simple_spam']) && $post['op']) {
if (!isset($_POST['simple_spam']) || $config['simple_spam']['answer'] != $_POST['simple_spam']) {
error($config['error']['simple_spam']);
}
}
if (isset($config['securimage']) && $config['securimage']) {
if (!isset($_POST['captcha'])) {
error($config['error']['securimage']['missing']);
}
checkDNSBL();
// Check if banned
checkBan($board['uri']);
if ($config['op_require_history'] && $post['op'] && !isIPv6()) {
$has_any = has_any_history($_SERVER['REMOTE_ADDR'], $_POST['password']);
if (!$has_any) {
error($config['error']['opnohistory']);
}
if (empty($_POST['captcha'])) {
error($config['error']['securimage']['empty']);
}
if ($post['mod'] = isset($_POST['mod']) && $_POST['mod']) {
check_login($ctx, false);
if (!$mod) {
// Liar. You're not a mod >:-[
error($config['error']['notamod']);
}
if (!db_delete_captcha($_SERVER['REMOTE_ADDR'], $_POST['captcha'])) {
error($config['error']['securimage']['bad']);
}
}
$post['sticky'] = $post['op'] && isset($_POST['sticky']);
$post['locked'] = $post['op'] && isset($_POST['lock']);
$post['raw'] = isset($_POST['raw']);
if (
!(($post['op'] && $_POST['post'] == $config['button_newtopic']) ||
(!$post['op'] && $_POST['post'] == $config['button_reply']))
) {
error($config['error']['bot']);
}
if ($post['sticky'] && !hasPermission($config['mod']['sticky'], $board['uri'])) {
error($config['error']['noaccess']);
}
if ($post['locked'] && !hasPermission($config['mod']['lock'], $board['uri'])) {
error($config['error']['noaccess']);
}
if ($post['raw'] && !hasPermission($config['mod']['rawhtml'], $board['uri'])) {
error($config['error']['noaccess']);
}
// Check the referrer
if (
$config['referer_match'] !== false &&
(!isset($_SERVER['HTTP_REFERER']) || !preg_match($config['referer_match'], rawurldecode($_SERVER['HTTP_REFERER'])))
) {
error($config['error']['referer']);
}
checkDNSBL();
// Check if banned
checkBan($board['uri']);
if ($config['op_require_history'] && $post['op'] && !isIPv6()) {
$has_any = has_any_history($_SERVER['REMOTE_ADDR'], $_POST['password']);
if (!$has_any) {
error($config['error']['opnohistory']);
}
}
if ($post['mod'] = isset($_POST['mod']) && $_POST['mod']) {
check_login($ctx, false);
if (!$mod) {
// Liar. You're not a mod >:-[
error($config['error']['notamod']);
}
if (!$post['mod'] && $config['spam']['enabled'] == true) {
$post['antispam_hash'] = checkSpam(
array(
$board['uri'],
isset($post['thread']) ? $post['thread'] : ($config['try_smarter'] && isset($_POST['page']) ? 0 - (int) $_POST['page'] : null)
)
);
//$post['antispam_hash'] = checkSpam();
$post['sticky'] = $post['op'] && isset($_POST['sticky']);
$post['locked'] = $post['op'] && isset($_POST['lock']);
$post['raw'] = isset($_POST['raw']);
if ($post['antispam_hash'] === true) {
error($config['error']['spam']);
}
if ($post['sticky'] && !hasPermission($config['mod']['sticky'], $board['uri'])) {
error($config['error']['noaccess']);
}
if ($post['locked'] && !hasPermission($config['mod']['lock'], $board['uri'])) {
error($config['error']['noaccess']);
}
if ($post['raw'] && !hasPermission($config['mod']['rawhtml'], $board['uri'])) {
error($config['error']['noaccess']);
}
}
if ($config['robot_enable'] && $config['robot_mute']) {
checkMute();
if (!$post['mod'] && $config['spam']['enabled'] == true) {
$post['antispam_hash'] = checkSpam(
array(
$board['uri'],
isset($post['thread']) ? $post['thread'] : ($config['try_smarter'] && isset($_POST['page']) ? 0 - (int) $_POST['page'] : null)
)
);
//$post['antispam_hash'] = checkSpam();
if ($post['antispam_hash'] === true) {
error($config['error']['spam']);
}
} else {
$mod = $post['mod'] = false;
}
if ($config['robot_enable'] && $config['robot_mute']) {
checkMute();
}
// Check if thread exists.
@ -1009,47 +845,47 @@ function handle_post(Context $ctx)
}
}
// We must do this check now before the passowrd is hashed and overwritten.
if (\mb_strlen($_POST['password']) > 20) {
error(\sprintf($config['error']['toolong'], 'password'));
}
$post['name'] = $_POST['name'] != '' ? $_POST['name'] : $config['anonymous'];
$post['subject'] = $_POST['subject'];
$post['email'] = str_replace(' ', '%20', htmlspecialchars($_POST['email']));
$post['body'] = $_POST['body'];
$post['password'] = $_POST['password'];
$post['password'] = hashPassword($_POST['password']);
$post['has_file'] = (!isset($post['embed']) && (($post['op'] && !isset($post['no_longer_require_an_image_for_op']) && $config['force_image_op']) || count($_FILES) > 0));
if (!$dropped_post) {
if (!($post['has_file'] || isset($post['embed'])) || (($post['op'] && $config['force_body_op']) || (!$post['op'] && $config['force_body']))) {
$stripped_whitespace = preg_replace('/[\s]/u', '', $post['body']);
if ($stripped_whitespace == '') {
error($config['error']['tooshort_body']);
}
}
if (!$post['op']) {
// Check if thread is locked
// but allow mods to post
if ($thread['locked'] && !hasPermission($config['mod']['postinlocked'], $board['uri'])) {
error($config['error']['locked']);
}
$numposts = numPosts($post['thread']);
$replythreshold = isset($thread['cycle']) && $thread['cycle'] ? $numposts['replies'] - 1 : $numposts['replies'];
$imagethreshold = isset($thread['cycle']) && $thread['cycle'] ? $numposts['images'] - 1 : $numposts['images'];
if ($config['reply_hard_limit'] != 0 && $config['reply_hard_limit'] <= $replythreshold) {
error($config['error']['reply_hard_limit']);
}
if ($post['has_file'] && $config['image_hard_limit'] != 0 && $config['image_hard_limit'] <= $imagethreshold) {
error($config['error']['image_hard_limit']);
}
}
} else {
if (!$post['op']) {
$numposts = numPosts($post['thread']);
if (!($post['has_file'] || isset($post['embed'])) || (($post['op'] && $config['force_body_op']) || (!$post['op'] && $config['force_body']))) {
$stripped_whitespace = preg_replace('/[\s]/u', '', $post['body']);
if ($stripped_whitespace == '') {
error($config['error']['tooshort_body']);
}
}
if (!$post['op']) {
// Check if thread is locked
// but allow mods to post
if ($thread['locked'] && !hasPermission($config['mod']['postinlocked'], $board['uri'])) {
error($config['error']['locked']);
}
$numposts = numPosts($post['thread']);
$replythreshold = isset($thread['cycle']) && $thread['cycle'] ? $numposts['replies'] - 1 : $numposts['replies'];
$imagethreshold = isset($thread['cycle']) && $thread['cycle'] ? $numposts['images'] - 1 : $numposts['images'];
if ($config['reply_hard_limit'] != 0 && $config['reply_hard_limit'] <= $replythreshold) {
error($config['error']['reply_hard_limit']);
}
if ($post['has_file'] && $config['image_hard_limit'] != 0 && $config['image_hard_limit'] <= $imagethreshold) {
error($config['error']['image_hard_limit']);
}
}
if ($post['has_file']) {
// Determine size sanity
$size = 0;
@ -1155,18 +991,16 @@ function handle_post(Context $ctx)
$post['has_file'] = false;
}
if (!$dropped_post) {
// Check for a file
if ($post['op'] && !isset($post['no_longer_require_an_image_for_op'])) {
if (!$post['has_file'] && $config['force_image_op']) {
error($config['error']['noimage']);
}
// Check for a file
if ($post['op'] && !isset($post['no_longer_require_an_image_for_op'])) {
if (!$post['has_file'] && $config['force_image_op']) {
error($config['error']['noimage']);
}
}
// Check for too many files
if (sizeof($post['files']) > $config['max_images']) {
error($config['error']['toomanyimages']);
}
// Check for too many files
if (sizeof($post['files']) > $config['max_images']) {
error($config['error']['toomanyimages']);
}
if ($config['strip_combining_chars']) {
@ -1176,36 +1010,31 @@ function handle_post(Context $ctx)
$post['body'] = strip_combining_chars($post['body']);
}
if (!$dropped_post) {
// Check string lengths
if (mb_strlen($post['name']) > 35) {
error(sprintf($config['error']['toolong'], 'name'));
}
if (mb_strlen($post['email']) > 40) {
error(sprintf($config['error']['toolong'], 'email'));
}
if (mb_strlen($post['subject']) > 100) {
error(sprintf($config['error']['toolong'], 'subject'));
}
if (!$mod) {
$body_mb_len = mb_strlen($post['body']);
$is_op = $post['op'];
// Check string lengths
if (mb_strlen($post['name']) > 35) {
error(sprintf($config['error']['toolong'], 'name'));
}
if (mb_strlen($post['email']) > 40) {
error(sprintf($config['error']['toolong'], 'email'));
}
if (mb_strlen($post['subject']) > 100) {
error(sprintf($config['error']['toolong'], 'subject'));
}
if (!$mod) {
$body_mb_len = mb_strlen($post['body']);
$is_op = $post['op'];
if (($is_op && $config['force_body_op']) || (!$is_op && $config['force_body'])) {
$min_body = $is_op ? $config['min_body_op'] : $config['min_body'];
if (($is_op && $config['force_body_op']) || (!$is_op && $config['force_body'])) {
$min_body = $is_op ? $config['min_body_op'] : $config['min_body'];
if ($body_mb_len < $min_body) {
error($config['error']['tooshort_body']);
}
}
$max_body = $is_op ? $config['max_body_op'] : $config['max_body'];
if ($body_mb_len > $max_body) {
error($config['error']['toolong_body']);
if ($body_mb_len < $min_body) {
error($config['error']['tooshort_body']);
}
}
if (mb_strlen($post['password']) > 20) {
error(sprintf($config['error']['toolong'], 'password'));
$max_body = $is_op ? $config['max_body_op'] : $config['max_body'];
if ($body_mb_len > $max_body) {
error($config['error']['toolong_body']);
}
}
@ -1217,33 +1046,32 @@ function handle_post(Context $ctx)
$post['body'] .= "\n<tinyboard raw html>1</tinyboard>";
}
if (!$dropped_post)
if (($config['country_flags'] && !$config['allow_no_country']) || ($config['country_flags'] && $config['allow_no_country'] && !isset($_POST['no_country']))) {
$gi = geoip_open('inc/lib/geoip/GeoIPv6.dat', GEOIP_STANDARD);
if (($config['country_flags'] && !$config['allow_no_country']) || ($config['country_flags'] && $config['allow_no_country'] && !isset($_POST['no_country']))) {
$gi = geoip_open('inc/lib/geoip/GeoIPv6.dat', GEOIP_STANDARD);
function ipv4to6($ip)
{
if (strpos($ip, ':') !== false) {
if (strpos($ip, '.') > 0) {
$ip = substr($ip, strrpos($ip, ':') + 1);
} else {
// Native ipv6.
return $ip;
}
function ipv4to6($ip)
{
if (strpos($ip, ':') !== false) {
if (strpos($ip, '.') > 0) {
$ip = substr($ip, strrpos($ip, ':') + 1);
} else {
// Native ipv6.
return $ip;
}
$iparr = array_pad(explode('.', $ip), 4, 0);
$part7 = base_convert(($iparr[0] * 256) + $iparr[1], 10, 16);
$part8 = base_convert(($iparr[2] * 256) + $iparr[3], 10, 16);
return '::ffff:' . $part7 . ':' . $part8;
}
$iparr = array_pad(explode('.', $ip), 4, 0);
$part7 = base_convert(($iparr[0] * 256) + $iparr[1], 10, 16);
$part8 = base_convert(($iparr[2] * 256) + $iparr[3], 10, 16);
return '::ffff:' . $part7 . ':' . $part8;
}
if ($country_code = geoip_country_code_by_addr_v6($gi, ipv4to6($_SERVER['REMOTE_ADDR']))) {
if (!in_array(strtolower($country_code), array('eu', 'ap', 'o1', 'a1', 'a2'))) {
$post['body'] .= "\n<tinyboard flag>" . strtolower($country_code) . "</tinyboard>" .
"\n<tinyboard flag alt>" . geoip_country_name_by_addr_v6($gi, ipv4to6($_SERVER['REMOTE_ADDR'])) . "</tinyboard>";
}
if ($country_code = geoip_country_code_by_addr_v6($gi, ipv4to6($_SERVER['REMOTE_ADDR']))) {
if (!in_array(strtolower($country_code), array('eu', 'ap', 'o1', 'a1', 'a2'))) {
$post['body'] .= "\n<tinyboard flag>" . strtolower($country_code) . "</tinyboard>" .
"\n<tinyboard flag alt>" . geoip_country_name_by_addr_v6($gi, ipv4to6($_SERVER['REMOTE_ADDR'])) . "</tinyboard>";
}
}
}
if ($config['user_flag'] && isset($_POST['user_flag']))
if (!empty($_POST['user_flag'])) {
@ -1263,11 +1091,9 @@ function handle_post(Context $ctx)
$post['body'] .= "\n<tinyboard tag>" . $_POST['tag'] . "</tinyboard>";
}
if (!$dropped_post) {
if ($config['proxy_save'] && isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$proxy = preg_replace("/[^0-9a-fA-F.,: ]/", '', $_SERVER['HTTP_X_FORWARDED_FOR']);
$post['body'] .= "\n<tinyboard proxy>" . $proxy . "</tinyboard>";
}
if ($config['proxy_save'] && isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$proxy = preg_replace("/[^0-9a-fA-F.,: ]/", '', $_SERVER['HTTP_X_FORWARDED_FOR']);
$post['body'] .= "\n<tinyboard proxy>" . $proxy . "</tinyboard>";
}
$post['body_nomarkup'] = $post['body'];
@ -1309,9 +1135,9 @@ function handle_post(Context $ctx)
}
}
if (!hasPermission($config['mod']['bypass_filters'], $board['uri']) && !$dropped_post) {
if (!hasPermission($config['mod']['bypass_filters'], $board['uri'])) {
require_once 'inc/filters.php';
do_filters($post);
do_filters($ctx, $post);
}
if ($post['has_file']) {
@ -1400,13 +1226,13 @@ function handle_post(Context $ctx)
$file['thumbwidth'] = $size[0];
$file['thumbheight'] = $size[1];
} elseif (
$config['minimum_copy_resize'] &&
(($config['strip_exif'] && isset($file['exif_stripped']) && $file['exif_stripped']) || !$config['strip_exif']) &&
$image->size->width <= $config['thumb_width'] &&
$image->size->height <= $config['thumb_height'] &&
$file['extension'] == ($config['thumb_ext'] ? $config['thumb_ext'] : $file['extension'])
) {
// Copy, because there's nothing to resize
coopy($file['tmp_name'], $file['thumb']);
copy($file['tmp_name'], $file['thumb']);
$file['thumbwidth'] = $image->size->width;
$file['thumbheight'] = $image->size->height;
@ -1445,17 +1271,35 @@ function handle_post(Context $ctx)
}
$image->destroy();
} else {
if (
($file['extension'] == "pdf" && $config['pdf_file_thumbnail']) ||
($file['extension'] == "djvu" && $config['djvu_file_thumbnail'])
) {
$path = $file['thumb'];
$error = shell_exec_error('convert -size ' . $config['thumb_width'] . 'x' . $config['thumb_height'] . ' -thumbnail ' . $config['thumb_width'] . 'x' . $config['thumb_height'] . ' -background white -alpha remove ' .
escapeshellarg($file['tmp_name'] . '[0]') . ' ' .
escapeshellarg($file['thumb']));
$mime = \mime_content_type($file['tmp_name']);
$pdf = $file['extension'] === "pdf" && $config['pdf_file_thumbnail'];
$djvu = $file['extension'] === "djvu" && $config['djvu_file_thumbnail'];
if ($pdf || $djvu) {
$e_thumb_path = \escapeshellarg($file['thumb']);
$e_file_path = \escapeshellarg($file['tmp_name']);
$thumb_width = $config['thumb_width'];
$thumb_height = $config['thumb_height'];
// Generates a PPM image and pipes it directly into convert for resizing + type conversion.
if ($pdf && $mime === 'application/pdf') {
$error = shell_exec_error("gs -dSAFER -dBATCH -dNOPAUSE -dQUIET \
-sDEVICE=ppmraw -r100 -dFirstPage=1 -dLastPage=1 -sOutputFile=- $e_file_path \
| convert -thumbnail {$thumb_width}x{$thumb_height} ppm:- $e_thumb_path");
} elseif ($djvu && $mime === 'image/vnd.djvu') {
$error = shell_exec_error("ddjvu -format=ppm -page 1 $e_file_path \
| convert -thumbnail {$thumb_width}x{$thumb_height} ppm:- $e_thumb_path");
} else {
// Mime check failed.
error($config['error']['invalidfile']);
}
if ($error) {
$path = sprintf($config['file_thumb'], isset($config['file_icons'][$file['extension']]) ? $config['file_icons'][$file['extension']] : $config['file_icons']['default']);
$log = $ctx->get(LogDriver::class);
$log->log(LogDriver::ERROR, 'Could not render thumbnail for PDF/DJVU file, using static fallback.');
$path = \sprintf($config['file_thumb'], isset($config['file_icons'][$file['extension']]) ? $config['file_icons'][$file['extension']] : $config['file_icons']['default']);
} else {
$path = $file['thumb'];
}
$file['thumb'] = basename($file['thumb']);
@ -1549,35 +1393,6 @@ function handle_post(Context $ctx)
}
}
if ($config['tesseract_ocr'] && $file['thumb'] != 'file') {
// Let's OCR it!
$fname = $file['tmp_name'];
if ($file['height'] > 500 || $file['width'] > 500) {
$fname = $file['thumb'];
}
if ($fname == 'spoiler') {
// We don't have that much CPU time, do we?
} else {
$tmpname = __DIR__ . "/tmp/tesseract/" . rand(0, 10000000);
// Preprocess command is an ImageMagick b/w quantization
$error = shell_exec_error(sprintf($config['tesseract_preprocess_command'], escapeshellarg($fname)) . " | " .
'tesseract stdin ' . escapeshellarg($tmpname) . ' ' . $config['tesseract_params']);
$tmpname .= ".txt";
$value = @file_get_contents($tmpname);
@unlink($tmpname);
if ($value && trim($value)) {
// This one has an effect, that the body is appended to a post body. So you can write a correct
// spamfilter.
$post['body_nomarkup'] .= "<tinyboard ocr image $key>" . htmlspecialchars($value) . "</tinyboard>";
}
}
}
if (!isset($dont_copy_file) || !$dont_copy_file) {
if (isset($file['file_tmp'])) {
if (!@rename($file['tmp_name'], $file['file'])) {
@ -1625,12 +1440,7 @@ function handle_post(Context $ctx)
}
}
// Do filters again if OCRing
if ($config['tesseract_ocr'] && !hasPermission($config['mod']['bypass_filters'], $board['uri']) && !$dropped_post) {
do_filters($post);
}
if (!hasPermission($config['mod']['postunoriginal'], $board['uri']) && $config['robot_enable'] && checkRobot($post['body_nomarkup']) && !$dropped_post) {
if (!hasPermission($config['mod']['postunoriginal'], $board['uri']) && $config['robot_enable'] && checkRobot($post['body_nomarkup'])) {
undoImage($post);
if ($config['robot_mute']) {
error(sprintf($config['error']['muted'], mute()));
@ -1676,41 +1486,6 @@ function handle_post(Context $ctx)
$post['id'] = $id = post($post);
$post['slug'] = slugify($post);
if ($dropped_post && $dropped_post['from_nntp']) {
$query = prepare("INSERT INTO ``nntp_references`` (`board`, `id`, `message_id`, `message_id_digest`, `own`, `headers`) VALUES " .
"(:board , :id , :message_id , :message_id_digest , false, :headers)");
$query->bindValue(':board', $dropped_post['board']);
$query->bindValue(':id', $id);
$query->bindValue(':message_id', $dropped_post['msgid']);
$query->bindValue(':message_id_digest', sha1($dropped_post['msgid']));
$query->bindValue(':headers', $dropped_post['headers']);
$query->execute() or error(db_error($query));
} // ^^^^^ For inbound posts ^^^^^
elseif ($config['nntpchan']['enabled'] && $config['nntpchan']['group']) {
// vvvvv For outbound posts vvvvv
require_once('inc/nntpchan/nntpchan.php');
$msgid = gen_msgid($post['board'], $post['id']);
list($headers, $files) = post2nntp($post, $msgid);
$message = gen_nntp($headers, $files);
$query = prepare("INSERT INTO ``nntp_references`` (`board`, `id`, `message_id`, `message_id_digest`, `own`, `headers`) VALUES " .
"(:board , :id , :message_id , :message_id_digest , true , :headers)");
$query->bindValue(':board', $post['board']);
$query->bindValue(':id', $post['id']);
$query->bindValue(':message_id', $msgid);
$query->bindValue(':message_id_digest', sha1($msgid));
$query->bindValue(':headers', json_encode($headers));
$query->execute() or error(db_error($query));
// Let's broadcast it!
nntp_publish($message, $msgid);
}
insertFloodPost($post);
// Handle cyclical threads
@ -1779,10 +1554,10 @@ function handle_post(Context $ctx)
buildThread($post['op'] ? $id : $post['thread']);
if ($config['syslog']) {
_syslog(LOG_INFO, 'New post: /' . $board['dir'] . $config['dir']['res'] .
link_for($post) . (!$post['op'] ? '#' . $id : ''));
}
$ctx->get(LogDriver::class)->log(
LogDriver::INFO,
'New post: /' . $board['dir'] . $config['dir']['res'] . link_for($post) . (!$post['op'] ? '#' . $id : '')
);
if (!$post['mod']) {
header('X-Associated-Content: "' . $redirect . '"');
@ -1875,22 +1650,13 @@ function handle_appeal(Context $ctx)
displayBan($ban);
}
// Is it a post coming from NNTP? Let's extract it and pretend it's a normal post.
if (isset($_GET['Newsgroups'])) {
if ($config['nntpchan']['enabled']) {
handle_nntpchan();
} else {
error("NNTPChan: NNTPChan support is disabled");
}
}
$ctx = Vichan\build_context($config);
if (isset($_POST['delete'])) {
handle_delete($ctx);
} elseif (isset($_POST['report'])) {
handle_report($ctx);
} elseif (isset($_POST['post']) || $dropped_post) {
} elseif (isset($_POST['post'])) {
handle_post($ctx);
} elseif (isset($_POST['appeal'])) {
handle_appeal($ctx);

View file

@ -1,174 +1,77 @@
<?php
require 'inc/bootstrap.php';
if (!$config['search']['enable']) {
die(_("Post search is disabled"));
use Vichan\Service\SearchService;
require 'inc/bootstrap.php';
if (!$config['search']['enable']) {
die(_("Post search is disabled"));
}
$ctx = Vichan\build_context($config);
$search_service = $ctx->get(SearchService::class);
if (isset($_GET['search']) && !empty($_GET['search'])) {
$raw_search = $_GET['search'];
$ip = $_SERVER['REMOTE_ADDR'];
$fallback_board = (isset($_GET['board']) && !empty($_GET['board'])) ? $_GET['board'] : null;
if ($search_service->checkFlood($ip, $raw_search)) {
error(_('Wait a while before searching again, please.'));
}
$queries_per_minutes = $config['search']['queries_per_minutes'];
$queries_per_minutes_all = $config['search']['queries_per_minutes_all'];
$search_limit = $config['search']['search_limit'];
if (isset($config['search']['boards'])) {
$boards = $config['search']['boards'];
// Actually do the search.
$parse_res = $search_service->parse($raw_search);
$filters = $search_service->reduceAndWeight($parse_res);
$search_res = $search_service->search($ip, $raw_search, $filters, $fallback_board);
// Needed to set a global variable further down the stack, plus the template.
$actual_board = $filters->board ?? $fallback_board;
$body = Element('search_form.html', [
'boards' => $search_service->getSearchableBoards(),
'board' => $actual_board,
'search' => \str_replace('"', '&quot;', utf8tohtml($_GET['search'])),
'flags_enabled' => $search_service->isFlagFilterEnabled()
]);
if ($search_res === null) {
$body .= '<hr/><p style="text-align:center" class="unimportant">(' . _('Query too broad.') . ')</p>';
} elseif (empty($search_res)) {
$body .= '<hr/><p style="text-align:center" class="unimportant">(' . _('No results.') . ')</p>';
} else {
$boards = listBoards(TRUE);
}
$body = Element('search_form.html', Array('boards' => $boards, 'board' => isset($_GET['board']) ? $_GET['board'] : false, 'search' => isset($_GET['search']) ? str_replace('"', '&quot;', utf8tohtml($_GET['search'])) : false));
if(isset($_GET['search']) && !empty($_GET['search']) && isset($_GET['board']) && in_array($_GET['board'], $boards)) {
$phrase = $_GET['search'];
$_body = '';
$query = prepare("SELECT COUNT(*) FROM ``search_queries`` WHERE `ip` = :ip AND `time` > :time");
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
$query->bindValue(':time', time() - ($queries_per_minutes[1] * 60));
$query->execute() or error(db_error($query));
if($query->fetchColumn() > $queries_per_minutes[0])
error(_('Wait a while before searching again, please.'));
$query = prepare("SELECT COUNT(*) FROM ``search_queries`` WHERE `time` > :time");
$query->bindValue(':time', time() - ($queries_per_minutes_all[1] * 60));
$query->execute() or error(db_error($query));
if($query->fetchColumn() > $queries_per_minutes_all[0])
error(_('Wait a while before searching again, please.'));
$query = prepare("INSERT INTO ``search_queries`` VALUES (:ip, :time, :query)");
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
$query->bindValue(':time', time());
$query->bindValue(':query', $phrase);
$query->execute() or error(db_error($query));
_syslog(LOG_NOTICE, 'Searched /' . $_GET['board'] . '/ for "' . $phrase . '"');
$body .= '<hr/>';
// Cleanup search queries table
$query = prepare("DELETE FROM ``search_queries`` WHERE `time` <= :time");
$query->bindValue(':time', time() - ($queries_per_minutes_all[1] * 60));
$query->execute() or error(db_error($query));
openBoard($_GET['board']);
$filters = Array();
function search_filters($m) {
global $filters;
$name = $m[2];
$value = isset($m[4]) ? $m[4] : $m[3];
if(!in_array($name, array('id', 'thread', 'subject', 'name'))) {
// unknown filter
return $m[0];
}
$filters[$name] = $value;
return $m[1];
}
$phrase = trim(preg_replace_callback('/(^|\s)(\w+):("(.*)?"|[^\s]*)/', 'search_filters', $phrase));
if(!preg_match('/[^*^\s]/', $phrase) && empty($filters)) {
_syslog(LOG_WARNING, 'Query too broad.');
$body .= '<p class="unimportant" style="text-align:center">(Query too broad.)</p>';
echo Element('page.html', Array(
'config'=>$config,
'title'=>'Search',
'body'=>$body,
));
exit;
}
// Escape escape character
$phrase = str_replace('!', '!!', $phrase);
// Remove SQL wildcard
$phrase = str_replace('%', '!%', $phrase);
// Use asterisk as wildcard to suit convention
$phrase = str_replace('*', '%', $phrase);
// Remove `, it's used by table prefix magic
$phrase = str_replace('`', '!`', $phrase);
openBoard($actual_board);
$like = '';
$match = Array();
// Find exact phrases
if(preg_match_all('/"(.+?)"/', $phrase, $m)) {
foreach($m[1] as &$quote) {
$phrase = str_replace("\"{$quote}\"", '', $phrase);
$match[] = $pdo->quote($quote);
}
}
$words = explode(' ', $phrase);
foreach($words as &$word) {
if(empty($word))
continue;
$match[] = $pdo->quote($word);
}
$like = '';
foreach($match as &$phrase) {
if(!empty($like))
$like .= ' AND ';
$phrase = preg_replace('/^\'(.+)\'$/', '\'%$1%\'', $phrase);
$like .= '`body` LIKE ' . $phrase . ' ESCAPE \'!\'';
}
foreach($filters as $name => $value) {
if(!empty($like))
$like .= ' AND ';
$like .= '`' . $name . '` = '. $pdo->quote($value);
}
$like = str_replace('%', '%%', $like);
$query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE " . $like . " ORDER BY `time` DESC LIMIT :limit", $board['uri']));
$query->bindValue(':limit', $search_limit, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
if($query->rowCount() == $search_limit) {
_syslog(LOG_WARNING, 'Query too broad.');
$body .= '<p class="unimportant" style="text-align:center">('._('Query too broad.').')</p>';
echo Element('page.html', Array(
'config'=>$config,
'title'=>'Search',
'body'=>$body,
));
exit;
}
$temp = '';
while($post = $query->fetch()) {
if(!$post['thread']) {
$posts_html = '';
foreach ($search_res as $post) {
if (!$post['thread']) {
$po = new Thread($post);
} else {
$po = new Post($post);
}
$temp .= $po->build(true) . '<hr/>';
$posts_html .= $po->build(true) . '<hr/>';
}
if(!empty($temp))
$_body .= '<fieldset><legend>' .
sprintf(ngettext('%d result in', '%d results in', $query->rowCount()),
$query->rowCount()) . ' <a href="/' .
sprintf($config['board_path'], $board['uri']) . $config['file_index'] .
'">' .
$body .= '<fieldset><legend>' .
sprintf(ngettext('%d result in', '%d results in', \count($search_res)), \count($search_res)) . ' <a href="/' .
sprintf($config['board_path'], $board['uri']) . $config['file_index'] . '">' .
sprintf($config['board_abbreviation'], $board['uri']) . ' - ' . $board['title'] .
'</a></legend>' . $temp . '</fieldset>';
$body .= '<hr/>';
if(!empty($_body))
$body .= $_body;
else
$body .= '<p style="text-align:center" class="unimportant">('._('No results.').')</p>';
'</a></legend>' . $posts_html . '</fieldset>';
}
echo Element('page.html', Array(
'config'=>$config,
'title'=>_('Search'),
'body'=>'' . $body
));
} else {
$body = Element('search_form.html', [
'boards' => $search_service->getSearchableBoards(),
'board' => false,
'search' => false,
'flags_enabled' => $search_service->isFlagFilterEnabled()
]);
}
echo Element('page.html', Array(
'config'=>$config,
'title'=> _('Search'),
'body'=> $body
));

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
static/flags/420.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

BIN
static/flags/alunya.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

BIN
static/flags/rodina_get.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
static/flags/rodina_lp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

View file

Before

Width:  |  Height:  |  Size: 898 B

After

Width:  |  Height:  |  Size: 898 B

Before After
Before After

BIN
static/flags/tania.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

BIN
static/leftypol_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -152,6 +152,11 @@ div.post.reply.highlighted
box-shadow: 3px 5px #5c8c8e;
margin-left: auto;
margin-right: auto;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply div.body a:link, div.post.reply div.body a:visited
{

View file

@ -219,6 +219,11 @@ background-color: #2b2b2b;
background-color: #2b2b2b;
border: solid 1px #93e0e3;
box-shadow: 3px 5px #93e0e3;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
/*dont touch this*/

View file

@ -161,6 +161,11 @@ div.post.reply.highlighted
box-shadow: 3px 5px #5c8c8e;
margin-left: auto;
margin-right: auto;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply div.body a:link, div.post.reply div.body a:visited
{

View file

@ -120,6 +120,11 @@ div.post.reply.highlighted {
background: rgba(59, 22, 43, 0.4);
border: 1px solid #117743;
border-radius: 5px;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
/* POST CONTENT */

View file

@ -1,7 +1,7 @@
/*cyberpunk mod of ferus by kalyx
B332E6 = dark purp
33cccc = teal
33cccc = teal
00ff00 = green
FF46D2 = pink
dark blue = 00080C
@ -37,7 +37,7 @@ div.boardlist{
background-color: #0C0001;
}
@font-face
@font-face
{
font-family: 'lain';
src: url('./fonts/nrdyyh.woff') format('woff'),
@ -84,7 +84,7 @@ a:link, a:visited, p.intro a.email span.name
-ms-transition: 0.15s text-shadow, 0.15s color;
transition: 0.15s text-shadow, 0.15s color;
}
input[type="text"], textarea
input[type="text"], textarea
{
-moz-transition: 0.15s border-color;
-webkit-transition: 0.15s border-color;
@ -93,7 +93,7 @@ input[type="text"], textarea
-ms-transition: 0.15s border-color;
transition: 0.15s border-color;
}
input[type="submit"]
input[type="submit"]
{
-moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
-webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
@ -117,7 +117,7 @@ a.post_no {
color: #33cccc;
text-decoration: none;
}
span.quote
span.quote
{
color:#00ff00;
}
@ -136,6 +136,11 @@ div.post.reply {
div.post.reply.highlighted {
background: transparent;
border: #1A6666 2px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #646464;

View file

@ -1,7 +1,7 @@
/*cyberpunk mod of ferus by kalyx
B332E6 = dark purp
33cccc = teal
33cccc = teal
00ff00 = green
FF46D2 = pink
dark blue = 00080C
@ -14,7 +14,7 @@ body {
font-size: 11px;
/*text-shadow: 0px 0px 3px #FF46D2;*/
}
@font-face
@font-face
{
font-family: 'lain';
src: url('./fonts/nrdyyh.woff') format('woff'),
@ -61,7 +61,7 @@ a:link, a:visited, p.intro a.email span.name
-ms-transition: 0.15s text-shadow, 0.15s color;
transition: 0.15s text-shadow, 0.15s color;
}
input[type="text"], textarea
input[type="text"], textarea
{
-moz-transition: 0.15s border-color;
-webkit-transition: 0.15s border-color;
@ -70,7 +70,7 @@ input[type="text"], textarea
-ms-transition: 0.15s border-color;
transition: 0.15s border-color;
}
input[type="submit"]
input[type="submit"]
{
-moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
-webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
@ -94,7 +94,7 @@ a.post_no {
color: #33cccc;
text-decoration: none;
}
span.quote
span.quote
{
color:#00ff00;
}
@ -113,6 +113,11 @@ div.post.reply {
div.post.reply.highlighted {
background: transparent;
border: #33cccc 2px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #646464;

View file

@ -63,6 +63,11 @@ div.post.reply {
div.post.reply.highlighted {
background: #555;
border: transparent 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #CCCCCC;
@ -198,6 +203,10 @@ div.boardlist:not(.bottom) {
div.report {
color: #666666;
}
theme-catalog div.thread:hover {
background: #555;
border-color: transparent;
}
/* options.js */
#options_div, #alert_div {

View file

@ -5,7 +5,7 @@
/*dark.css has been prepended (2021-11-11) instead of @import'd for performance*/
body {
background: #1E1E1E;
color: #A7A7A7;
color: #C0C0C0;
font-family: Verdana, sans-serif;
font-size: 14px;
}
@ -31,26 +31,28 @@ div.title p {
font-size: 10px;
}
a, a:link, a:visited, .intro a.email span.name {
color: #CCCCCC;
color: #EEE;
text-decoration: none;
font-family: sans-serif;
font-family: Verdana, sans-serif;
}
a:link:hover, a:visited:hover {
color: #fff;
font-family: sans-serif;
font-family: Verdana, sans-serif;
text-decoration: none;
}
a.post_no {
color: #AAAAAA;
text-decoration: none;
}
a.post_no:hover {
color: #32DD72 !important;
text-decoration: underline overline;
}
.intro a.post_no {
color: #EEE;
}
div.post.reply {
background: #333333;
border: #555555 1px solid;
border: #4f4f4f 1px solid;
@media (max-width: 48em) {
border-left-style: none;
@ -58,21 +60,26 @@ div.post.reply {
}
}
div.post.reply.highlighted {
background: #555;
background: #4f4f4f;
border: transparent 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #CCCCCC;
color: #EEE;
}
div.post.reply div.body a:link:hover, div.post.reply div.body a:visited:hover {
color: #32DD72;
}
div.post.inline {
border: #555555 1px solid;
border: #4f4f4f 1px solid;
}
.intro span.subject {
font-size: 12px;
font-family: sans-serif;
font-family: Verdana, sans-serif;
color: #446655;
font-weight: 800;
}
@ -89,16 +96,16 @@ div.post.inline {
}
input[type="text"], textarea, select {
background: #333333;
color: #CCCCCC;
color: #EEE;
border: #666666 1px solid;
padding-left: 5px;
padding-right: -5px;
font-family: sans-serif;
font-family: Verdana, sans-serif;
font-size: 10pt;
}
input[type="password"] {
background: #333333;
color: #CCCCCC;
color: #EEE;
border: #666666 1px solid;
}
form table tr th {
@ -126,10 +133,10 @@ div.banner a {
input[type="submit"] {
background: #333333;
border: #888888 1px solid;
color: #CCCCCC;
color: #EEE;
}
input[type="submit"]:hover {
background: #555555;
background: #4f4f4f;
border: #888888 1px solid;
color: #32DD72;
}
@ -144,7 +151,7 @@ span.trip {
}
div.pages {
background: #1E1E1E;
font-family: sans-serif;
font-family: Verdana, sans-serif;
}
.bar.bottom {
bottom: 0px;
@ -152,7 +159,7 @@ div.pages {
background-color: #1E1E1E;
}
div.pages a.selected {
color: #CCCCCC;
color: #EEE;
}
hr {
height: 1px;
@ -160,7 +167,7 @@ hr {
}
div.boardlist {
text-align: center;
color: #A7A7A7;
color: #C0C0C0;
}
div.ban {
background-color: transparent;
@ -181,12 +188,16 @@ div.boardlist:not(.bottom) {
}
.desktop-style div.boardlist:not(.bottom) {
text-shadow: black 1px 1px 1px, black -1px -1px 1px, black -1px 1px 1px, black 1px -1px 1px;
color: #A7A7A7;
color: #C0C0C0;
background-color: #1E1E1E;
}
div.report {
color: #666666;
}
.theme-catalog div.thread:hover {
background: #4f4f4f;
border-color: transparent;
}
#options_div, #alert_div {
background: #333333;
}
@ -204,7 +215,7 @@ div.report {
}
.box {
background: #333333;
border-color: #555555;
border-color: #4f4f4f;
color: #C5C8C6;
border-radius: 10px;
}
@ -214,7 +225,7 @@ div.report {
}
table thead th {
background: #333333;
border-color: #555555;
border-color: #4f4f4f;
color: #C5C8C6;
border-radius: 4px;
}
@ -222,11 +233,11 @@ table tbody tr:nth-of-type( even ) {
background-color: #333333;
}
table.board-list-table .board-uri .board-sfw {
color: #CCCCCC;
color: #EEE;
}
tbody.board-list-omitted td {
background: #333333;
border-color: #555555;
border-color: #4f4f4f;
}
table.board-list-table .board-tags .board-cell:hover {
background: #1e1e1e;

View file

@ -61,6 +61,11 @@ div.post.reply {
div.post.reply.highlighted {
background: #555;
border: transparent 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply div.body a:link, div.post.reply div.body a:visited {
@ -204,6 +209,11 @@ div.boardlist:not(.bottom) {
background-color: #1E1E1E;
}
.theme-catalog div.thread:hover {
background: #555;
border-color: transparent;
}
div.report {
color: #666666;
}

View file

@ -1,7 +1,7 @@
/*cyberpunk mod of ferus by kalyx
B332E6 = dark purp
293728 = teal
293728 = teal
59B451 = green
FF46D2 = pink
dark blue = 293728
@ -19,13 +19,13 @@ div.boardlist{
}
@font-face
@font-face
{
font-family: 'lain';
src: url('./fonts/nrdyyh.woff') format('woff'),
url('./fonts/tojcxo.TTF') format('truetype');
}
@font-face
@font-face
{
font-family: 'DejaVuSansMono';
src: url('./fonts/DejaVuSansMono.ttf') format('truetype');
@ -79,7 +79,7 @@ a:link, a:visited, p.intro a.email span.name
-ms-transition: 0.15s text-shadow, 0.15s color;
transition: 0.15s text-shadow, 0.15s color;
}
input[type="text"], textarea
input[type="text"], textarea
{
-moz-transition: 0.15s border-color;
-webkit-transition: 0.15s border-color;
@ -88,7 +88,7 @@ input[type="text"], textarea
-ms-transition: 0.15s border-color;
transition: 0.15s border-color;
}
input[type="submit"]
input[type="submit"]
{
-moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
-webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
@ -112,7 +112,7 @@ a.post_no {
color: #293728;
text-decoration: none;
}
span.quote
span.quote
{
color:#4CADA7;
}
@ -131,6 +131,11 @@ div.post.reply {
div.post.reply.highlighted {
background: transparent;
border: #293728 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #646464;

Binary file not shown.

View file

@ -25,7 +25,7 @@ div.boardlist{
background-color: #000!important;
}
@font-face
@font-face
{
font-family: 'lain';
src: url('./fonts/nrdyyh.woff') format('woff'),
@ -74,7 +74,7 @@ a.post_no {
color: #d2738a;
text-decoration: none;
}
span.quote
span.quote
{
color:#d2738a;
}
@ -93,6 +93,11 @@ div.post.reply {
div.post.reply.highlighted {
background: transparent;
border: #EDC7D0 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #646464;
@ -216,11 +221,11 @@ table.modlog tr th {
#options_div {
background-color: #000000;
}
.options_tab_icon {
color: #c1b492;
}
.options_tab_icon.active {
color: #d2738a;
}

View file

@ -4,7 +4,7 @@ body {
font-family: monospace;
font-size: 11px;
}
@font-face
@font-face
{
font-family: 'lain';
src: url('./fonts/nrdyyh.woff') format('woff'),
@ -41,7 +41,7 @@ a.post_no {
color: #B332E6;
text-decoration: none;
}
span.quote
span.quote
{
color:#00ff00;
}
@ -59,6 +59,11 @@ div.post.reply {
div.post.reply.highlighted {
background: transparent;
border: #B332E6 2px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #646464;

View file

@ -1,7 +1,7 @@
/*cyberpunk mod of ferus by kalyx
B332E6 = dark purp
293728 = teal
293728 = teal
59B451 = green
FF46D2 = pink
dark blue = 293728
@ -19,13 +19,13 @@ div.boardlist{
}
@font-face
@font-face
{
font-family: 'lain';
src: url('./fonts/nrdyyh.woff') format('woff'),
url('./fonts/tojcxo.TTF') format('truetype');
}
@font-face
@font-face
{
font-family: 'DejaVuSansMono';
src: url('./fonts/DejaVuSansMono.ttf') format('truetype');
@ -79,7 +79,7 @@ a:link, a:visited, p.intro a.email span.name
-ms-transition: 0.15s text-shadow, 0.15s color;
transition: 0.15s text-shadow, 0.15s color;
}
input[type="text"], textarea
input[type="text"], textarea
{
-moz-transition: 0.15s border-color;
-webkit-transition: 0.15s border-color;
@ -88,7 +88,7 @@ input[type="text"], textarea
-ms-transition: 0.15s border-color;
transition: 0.15s border-color;
}
input[type="submit"]
input[type="submit"]
{
-moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
-webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
@ -112,7 +112,7 @@ a.post_no {
color: #293728;
text-decoration: none;
}
span.quote
span.quote
{
color:#4CADA7;
}
@ -131,6 +131,11 @@ div.post.reply {
div.post.reply.highlighted {
background: transparent;
border: #293728 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #646464;

View file

@ -34,6 +34,11 @@ div.post.reply {
border: 0px;
background: #FAE8D4;
border: 1px solid #E2C5B1;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply.highlighted {
background: #f0c0b0;
@ -62,7 +67,7 @@ div.pages {
padding: 7px 5px;
color: maroon;
font-size: 12pt;
background: none;
border-width: 1px;
border-style: inset;
@ -101,4 +106,3 @@ table.modlog tr th {
#options_div, #alert_div {
background: rgb(240, 224, 214);
}

View file

@ -23,6 +23,14 @@ div.post.reply, input, textarea, select {
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 2px;
}
@media (max-width: 48em) {
div.post.reply {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply.post-hover {
background: rgba(200, 200, 200, 0.85);
}
@ -72,4 +80,3 @@ table.modlog tr th {
#quick-reply table {
background: #0E0E0E url(data:image/gif;base64,R0lGODlhGAAMAKEEAOXl5ebm5vDw8PHx8SH+EUNyZWF0ZWQgd2l0aCBHSU1QACwAAAAAGAAMAAACRpQiY6cLa146MyY1EJQKjG81lNGRUPOIkgMJHtquBgIO7xwvpbrpduUSuXq8ntEC0bBEylYitdDAdM1ViaobkgKgZwyDLAAAOw==) repeat 0 0 !important;
}

View file

@ -56,6 +56,10 @@ div.post.reply {
border-top: none;
border-radius: 5px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35);
@media (max-width: 48em) {
border-left-style: none;
}
}
div.post.reply.highlighted {
@ -65,6 +69,10 @@ div.post.reply.highlighted {
border-top: none;
border-radius: 5px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35);
@media (max-width: 48em) {
border-right-style: none;
}
}
div.post.reply div.body a {

View file

@ -164,6 +164,11 @@ div.post.reply.highlighted
box-shadow: 3px 5px #5c8c8e;
margin-left: auto;
margin-right: auto;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply div.body a:link, div.post.reply div.body a:visited
{

View file

@ -53,6 +53,10 @@ div.post.reply {
border-top: none;
border-radius: 5px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35);
@media (max-width: 48em) {
border-right-style: none;
}
}
div.post.reply.highlighted {
@ -62,6 +66,10 @@ div.post.reply.highlighted {
border-top: none;
border-radius: 5px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35);
@media (max-width: 48em) {
border-right-style: none;
}
}
div.post.reply div.body a {

View file

@ -2,27 +2,27 @@
* dark.css
* Stolen from circlepuller who stole it from derpcat
*/
body
body
{
background: #1E1E1E;
color: #999999;
font-family: sans-serif;
font-size: 11px;
}
span.quote
span.quote
{
color:#B8D962;
}
@font-face
@font-face
{
font-family: 'lain';
src: url('./fonts/nrdyyh.woff') format('woff'),
url('./fonts/tojcxo.TTF') format('truetype');
}
h1
h1
{
font-family: 'lain', tahoma;
letter-spacing: -2px;
@ -30,20 +30,20 @@ h1
text-align: center;
color: #32DD72;
}
header div.subtitle
header div.subtitle
{
color: #32DD72;
}
div.title
div.title
{
color: #32DD72;
font-family: Arial, Helvetica, sans-serif;
}
div.title p
div.title p
{
font-size: 10px;
}
a:link, a:visited, p.intro a.email span.name
a:link, a:visited, p.intro a.email span.name
{
color: #CCCCCC;
text-decoration: none;
@ -58,7 +58,7 @@ a:link, a:visited, p.intro a.email span.name
-ms-transition: 0.15s text-shadow, 0.15s color;
transition: 0.15s text-shadow, 0.15s color;
}
input[type="text"], textarea
input[type="text"], textarea
{
-moz-transition: 0.15s border-color;
-webkit-transition: 0.15s border-color;
@ -67,7 +67,7 @@ input[type="text"], textarea
-ms-transition: 0.15s border-color;
transition: 0.15s border-color;
}
input[type="submit"]
input[type="submit"]
{
-moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
-webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
@ -76,23 +76,23 @@ input[type="submit"]
-ms-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
transition: 0.15s border-color, 0.15s background-color, 0.15s color;
}
a:link:hover, a:visited:hover
a:link:hover, a:visited:hover
{
color: #32DD72;
font-family: sans-serif;
text-decoration: none;
text-shadow: 0px 0px 5px #fff;
}
a.post_no
a.post_no
{
color: #AAA;
text-decoration: none;
}
p.intro a.post_no:hover
p.intro a.post_no:hover
{
color: #32DD72!important;
}
div.post.reply
div.post.reply
{
background: #181818;
border: #555555 0px solid;
@ -117,59 +117,64 @@ div.sidearrows
display:none;
}
div.post.reply.highlighted
div.post.reply.highlighted
{
background: #555;
border: transparent 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply div.body a:link, div.post.reply div.body a:visited
div.post.reply div.body a:link, div.post.reply div.body a:visited
{
color: #CCCCCC;
}
div.post.reply div.body a:link:hover, div.post.reply div.body a:visited:hover
div.post.reply div.body a:link:hover, div.post.reply div.body a:visited:hover
{
color: #32DD72;
}
p.intro span.subject
p.intro span.subject
{
font-size: 12px;
font-family: sans-serif;
color: #446655;
font-weight: 800;
}
p.intro span.name
p.intro span.name
{
color: #32DD72;
font-weight: 800;
}
p.intro a.capcode, p.intro a.nametag
p.intro a.capcode, p.intro a.nametag
{
color: magenta;
margin-left: 0;
}
p.intro a.email, p.intro a.email span.name, p.intro a.email:hover, p.intro a.email:hover span.name
p.intro a.email, p.intro a.email span.name, p.intro a.email:hover, p.intro a.email:hover span.name
{
color: #32ddaf;
}
input[type="text"], textarea, select
input[type="text"], textarea, select
{
background: #333333!important;
color: #CCCCCC!important;
border: #666666 1px solid!important;
}
input[type="password"]
input[type="password"]
{
background: #333333!important;
color: #CCCCCC!important;
border: #666666 1px solid!important;
}
form table tr th
form table tr th
{
background: #333333!important;
color: #AAAAAA!important;
border: #333333 1px solid!important;;
}
div.banner
div.banner
{
background: #E04000;
border: 1px solid hsl(17, 100%, 60%);
@ -180,31 +185,31 @@ div.banner
margin-left: auto;
margin-right: auto;
}
div.banner a
div.banner a
{
color:#000;
}
input[type="submit"]
input[type="submit"]
{
background: #333333;
border: #666 1px solid;
color: #CCCCCC;
}
input[type="submit"]:hover
input[type="submit"]:hover
{
background: #555;
border: #888 1px solid;
color: #32DD72;
}
input[type="text"]:focus, textarea:focus
input[type="text"]:focus, textarea:focus
{
border:#888 1px solid!important;
}
p.fileinfo a:hover
p.fileinfo a:hover
{
text-decoration: underline;
}
span.trip
span.trip
{
color: #AAA;
}
@ -214,7 +219,7 @@ span.trip
border-bottom: 0px solid #666;
widh:100%;
}
div.pages
div.pages
{
color: #AAA;
background: #333;
@ -222,21 +227,21 @@ div.pages
font-family: sans-serif;
font-size: 10pt;
}
div.pages a.selected
div.pages a.selected
{
color: #CCC;
}
hr
hr
{
height: 0px;
border: #333 1px solid;
}
div.boardlist
div.boardlist
{
color: #999;
}
div.ban
div.ban
{
background-color: #1e1e1e;
border: 1px solid #555;
@ -245,7 +250,7 @@ div.ban
border-radius: 2px;
text-align: left!important;
}
div.ban h2
div.ban h2
{
background: #333;
color: #32DD72;
@ -253,17 +258,17 @@ div.ban h2
font-size: 12pt;
border-bottom: 1px solid #555;
}
div.ban h2:not(:nth-child(1))
div.ban h2:not(:nth-child(1))
{
border-top: 1px solid #555;
}
table.modlog tr th
table.modlog tr th
{
background: #333;
color: #AAA;
}
div.report
div.report
{
color: #666;
}
@ -276,7 +281,7 @@ div.report
-ms-border-radius: 2px;
border-radius: 2px;
}
.blur
.blur
{
filter: blur(20px);
-webkit-filter: blur(23px);
@ -287,17 +292,15 @@ div.report
}
/* options.js */
#options_div
#options_div
{
background: #333333;
}
.options_tab_icon
.options_tab_icon
{
color: #AAAAAA;
}
.options_tab_icon.active
.options_tab_icon.active
{
color: #FFFFFF;
}

View file

@ -265,6 +265,11 @@ div.post.reply,
background: #220022;
border: #555555 1px solid;
border-radius: 10px;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
.post.highlighted {
@ -275,6 +280,11 @@ div.post.reply,
div.post.reply.highlighted {
background: #3A003A;
border: transparent 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
.post.highlighted {
@ -452,10 +462,10 @@ table.modlog tr th {
/* leftypol edits */
.bar,
.bar.top,
.bar.bottom,
div.boardlist,
.bar,
.bar.top,
.bar.bottom,
div.boardlist,
div.boardlist:not(.bottom) {
background-color: rgba(30%, 0%, 30%, 1.0);
}

View file

@ -215,6 +215,11 @@ margin-left: 10px;
margin-top: 20px;
border: double 3px #000;
background-color: rgb(194, 194, 194);
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
/*unfucks highlighting replies and gives border/shadow*/

View file

@ -32,7 +32,7 @@ a:hover,
header div.subtitle,
h1 {
color: #d93f42;
font-size: 20pt;
font-size: 20pt;
font-family: "Open Sans", sans-serif;
}
header div.subtitle {
@ -48,7 +48,7 @@ p.intro {
border-color: #cccccc;
border-style: solid;
border-width: 0.8px;
border-radius: 5px;
border-radius: 5px;
}
/* Replies */
/* Background color and border */
@ -58,6 +58,11 @@ div.post.reply {
border-style: solid;
border-width: 0.8px;
border-radius: 5px;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply.highlighted {
background: #d5dada;
@ -65,6 +70,11 @@ div.post.reply.highlighted {
border-style: solid;
border-width: 0.8px;
border-radius: 5px;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply div.body a {
color: #477085;
@ -96,7 +106,7 @@ orangeText {
background-color: #e9eced;
border: 1px solid #cccccc;
}
.thread.grid-li.grid-size-vsmall:hover,
.thread.grid-li.grid-size-vsmall:hover,
.thread.grid-li.grid-size-small:hover,
.thread.grid-li.grid-size-large:hover {
background: #d5dada;
@ -194,7 +204,7 @@ span.heading {
}
/* Fix OP file bleeding out of the border*/
.post-image {
margin: 37px
margin: 37px
}
/* Quick reply */
/* Quick reply banner */

View file

@ -28,14 +28,22 @@ div.post.reply {
background: #343439;
border-color: #3070A9;
border-top: 1px solid #3070A9;
border-left: 1px solid #3070A9;
border-radius: 3px;
padding: 0px;
@media (min-width: 48em) {
border-left: 1px solid #3070A9;
}
}
div.post.reply.highlighted {
background: #44444f;
border: 3px dashed #3070a9;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply div.body a, .mentioned {
@ -220,30 +228,30 @@ span.heading {
right: 1em !important;
position: absolute !important;
}
#expand-all-images{
margin-top: 4em !important;
}
#treeview{
margin-top: 5em !important;
}
#shrink-all-images{
margin-top: 6em !important;
}
#expand-all-images + hr,
#shrink-all-images + hr{
opacity: 0 !important;
margin: 0 !important;
}
#treeview + hr{
opacity: 0 !important;
clear: both !important;
}
#options_handler{
margin-top: 3em !important;
}

View file

@ -380,6 +380,7 @@ form table tr td div.center {
.file {
float: left;
min-width: 100px;
}
.file:not(.multifile) .post-image {
@ -390,6 +391,10 @@ form table tr td div.center {
float: none;
}
.file.multifile {
margin: 0 10px 0 0;
}
.file.multifile > p {
width: 0px;
min-width: 100%;
@ -429,19 +434,18 @@ img.banner,img.board_image {
.post-image {
display: block;
float: left;
margin: 5px 20px 10px 20px;
border: none;
}
.full-image {
float: left;
padding: 5px;
padding: 0.2em 0.2em 0.8em 0.2em;
margin: 0 20px 0 0;
max-width: 98%;
}
div.post .post-image {
padding: 0.2em;
padding: 0.2em 0.2em 0.8em 0.2em;
margin: 0 20px 0 0;
}
@ -538,8 +542,8 @@ div.post {
}
}
div.post > div.head {
margin: 0.1em 1em;
div.post div.head {
margin: 0.1em 1em 0.8em 1.4em;
clear: both;
line-height: 1.3em;
}
@ -564,17 +568,18 @@ div.post.op > p {
}
div.post div.body {
margin-top: 0.8em;
margin-left: 1.4em;
padding-right: 3em;
padding-bottom: 0.3em;
white-space: pre-wrap;
}
div.post.op div.body {
margin-left: 0.8em;
}
div.post.reply div.body {
margin-left: 1.8em;
div.post div.body:before {
content: "";
width: 18ch;
display: block;
overflow: hidden;
}
div.post.reply.highlighted {
@ -585,19 +590,9 @@ div.post.reply div.body a {
color: #D00;
}
div.post div.body {
white-space: pre-wrap;
}
div.post.op {
padding-top: 0px;
vertical-align: top;
/* Add back in the padding that is provided by body on large screens */
@media (max-width: 48em) {
padding-left: 4px;
padding-right: 4px;
}
}
div.post.reply {
@ -648,6 +643,7 @@ span.trip {
span.omitted {
display: block;
margin-top: 1em;
margin-left: 0.4em;
}
br.clear {
@ -757,10 +753,6 @@ table.test td img {
margin: 0;
}
fieldset label {
display: block;
}
div.pages {
/*! color: #89A; */
/*! background: #D6DAF0; */
@ -816,6 +808,10 @@ hr {
div.report {
color: #333;
margin-left: 1.4em;
padding-right: 3em;
padding-bottom: 0.3em;
white-space: pre-wrap;
}
div.top_notice {
@ -832,7 +828,7 @@ span.public_ban {
span.public_warning {
display: block;
color: steelblue;
color: orange;
font-weight: bold;
margin-top: 15px;
}

Some files were not shown because too many files have changed in this diff Show more