Compare commits

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

291 commits

Author SHA1 Message Date
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
d97c7f271e maintenance.php: use ReportQueries 2024-12-11 16:36:53 +01:00
5b231099b0 ReportQueries.php: fix filter for invalid reports 2024-12-11 16:33:49 +01:00
d3fdecfbb9 Add libsoc flag 2024-12-10 22:13:15 +01:00
d75f3fecf8 pages.php: use ReportQueries for the dashboard report counter 2024-12-08 23:16:25 +01:00
907656c0ff ReportQueries.php: remove obsolute code 2024-12-08 18:32:16 +01:00
efec014bd1 anti-bot.php: refactor antibot transaction handling 2024-12-08 18:19:35 +01:00
7fed118b1d ReportQueries.php: fix return type 2024-12-08 18:12:14 +01:00
b04cbdc4b7 ReportQueries.php: adjust db call and queries 2024-12-08 14:45:42 +01:00
222523e574 context.php: remove cache from ReportQueries 2024-12-08 13:30:12 +01:00
d3782562b8 ReportQueries.php: remove caching, since posts can be removed from the outside 2024-12-08 13:29:33 +01:00
88564ca12e log.php: pass context to mod_board_log 2024-12-07 16:37:21 +01:00
0a6d92d94b ReportQueries.php: fix comment 2024-12-06 22:48:56 +01:00
b5622e20c5 ReportQueries.php: delete then invalidate cache 2024-12-06 22:48:26 +01:00
f29f626eb1 catalog.js: add comment doc 2024-12-06 22:19:52 +01:00
912a105d91 Merge branch 'report-queries' into 'config'
Fix #51: by using ReportQueries

Closes #51

See merge request leftypol/leftypol!15
2024-12-06 20:55:25 +00:00
554bfdea44 cache.php: make it more forgiving to old redis cache config 2024-12-06 21:43:36 +01:00
399e33c97a docker: fix compose dependency 2024-12-06 21:33:17 +01:00
5ec59a9a0f Merge branch 'context-port' into 'config'
Context and cache port

See merge request leftypol/leftypol!14
2024-12-06 20:23:55 +00:00
0a2b1b5439 post.php: use ReportQueries 2024-12-06 21:14:13 +01:00
18f1aff2f6 pages.php: use ReportQueries 2024-12-06 21:14:13 +01:00
f6deafbc34 context.php: add ReportQueries class 2024-12-06 21:14:13 +01:00
8d8957cfeb context.php: add PDO and database to context 2024-12-06 21:14:11 +01:00
72e2d02cca ReportQueries.php: add implementation 2024-12-06 21:13:54 +01:00
5e45fc9d60 config.php: fix default config for redis cache 2024-12-06 21:09:08 +01:00
ac98a0459f RedisCacheDriver.php: fixup 2024-12-06 21:09:08 +01:00
cbcd743649 docker: add redis instance 2024-12-06 21:09:08 +01:00
5ee20431e2 pages.php: mod_merge cast post id 2024-12-06 21:09:08 +01:00
e80160f18c mod.php, pages.php: remove apc debug 2024-12-06 21:09:08 +01:00
ee9de3fe50 composer: add context.php 2024-12-06 21:09:08 +01:00
e58876a0ee mod.php: add missing context parameter 2024-12-06 21:09:08 +01:00
c0cce68f6b mod.php: $matches should always be an array 2024-12-06 21:09:08 +01:00
8cf497eb93 post.php, mod.php: pass the context to check_login 2024-12-06 21:09:08 +01:00
4197b5a376 auth.php: pass context to mod_login 2024-12-06 21:09:08 +01:00
Zankaria
7ef2d42bb0 mod.php, pages.php: pass the context to the mod pages 2024-12-06 21:09:08 +01:00
Zankaria
cb71e00b50 mod.php: remove last mod_page_* handler, use only mod_* for mod pages 2024-12-06 21:09:08 +01:00
Zankaria
98fb50e050 mod.php: use modern array syntax 2024-12-06 21:09:08 +01:00
82fb30ce9a polyfill.php: remove obsolete polyfills 2024-12-06 21:09:08 +01:00
5da430b0ba RedisCacheDriver.php: better support unix sockets 2024-12-06 21:09:08 +01:00
8ad5e4cebd polifill.php: add str_starts_with 2024-12-06 21:09:08 +01:00
8b586dc3bb RedisCacheDriver.php: add false value support 2024-12-06 21:09:08 +01:00
9b5906effe RedisCacheDriver.php: flush only the key-value pairs with matching prefix 2024-12-06 21:09:08 +01:00
39635cfa33 RedisCacheDriver.php: use extension provided serialization 2024-12-06 21:09:08 +01:00
39683db736 FsCacheDriver.php: collect expired cache items before operating on the cache 2024-12-06 21:09:08 +01:00
Zankaria
c31f5a4104 maintenance.php: delete expired filesystem cache 2024-12-06 21:09:07 +01:00
Zankaria
eca2ce0a8f context.php: use shared cache driver 2024-12-06 21:09:07 +01:00
Zankaria
842b4fdcee cache.php: wrap new cache drivers 2024-12-06 21:09:07 +01:00
33f83af1b1 driver: break up cache drivers 2024-12-06 21:09:07 +01:00
e85ccfab38 cache-driver.php: move to Data 2024-12-06 21:09:07 +01:00
Zankaria
8969b5816d cache-driver.php: filesystem handle expired values. 2024-12-06 21:09:07 +01:00
14eae7e9f4 config.php: update cache documentation 2024-12-06 21:09:07 +01:00
2892520438 cache: implement cache locking for filesystem cache and give it multiprocess support 2024-12-06 21:09:07 +01:00
Zankaria
7dbab7c26c context.php: add Dependency Injection container implementation 2024-12-06 21:08:47 +01:00
d3b94027c4 docker: rename compose file 2024-12-06 21:04:17 +01:00
a9e8fc0b8e docker: remove fixed container naiming from the database 2024-12-06 21:04:08 +01:00
22c73c2249 cli.php: remove double inclusion of anti-bot.php 2024-11-28 23:57:42 +01:00
367953f134 post.php: remove useless code 2024-11-28 16:28:33 +01:00
a095b0993c post.php: remove newline from report message 2024-11-28 16:21:26 +01:00
878cad6f06 functions.php: do not manually load anti-bot.php 2024-11-28 16:03:18 +01:00
708b4801dd composer: load anti-bot.php 2024-11-28 16:03:00 +01:00
120973a6b0 functions.php: supply pdo to _create_antibot 2024-11-28 16:01:50 +01:00
b459551ccb anti-bot.php: use transactions 2024-11-28 16:01:24 +01:00
73bc23a4c7 remove mysql 5.5 support 2024-11-28 01:06:59 +01:00
5bd89a9437 config.php: update min max op body length doc 2024-11-27 23:12:58 +01:00
f100d8fcda post.php: follow max_body_op and min_body_op 2024-11-27 23:10:26 +01:00
4c1ac32bda Merge branch 'min-body-op' into 'config'
Minimum body length specific for op

See merge request leftypol/leftypol!13
2024-11-27 20:39:42 +00:00
531e246e28 post.php: check different body length values for op 2024-11-27 21:28:25 +01:00
eb480975cd config.php: add op min and max body configuration options 2024-11-27 21:28:04 +01:00
b8af67a5e2 post.php: use modern array syntax 2024-11-27 17:39:57 +01:00
b88c876222 post.php: use additional password check 2024-11-14 15:27:42 +01:00
850958a45e functions.php: add optional password check for has_any_history 2024-11-14 15:27:42 +01:00
1f05efd2dd banners: add cockshott banner 2024-11-13 19:50:54 +01:00
7ad17bbee1 post.php: limit post history check to IPv4 as a workaround to #49 2024-11-13 19:44:57 +01:00
c8d84afa07 Merge branch '48-prev-reply-op' into 'config'
Resolve "Require previous replies to make a thread"

See merge request leftypol/leftypol!12
2024-11-12 14:19:41 +00:00
ee4ba0ed26 functions.php: add caching to has_any_history 2024-11-12 15:11:25 +01:00
4277cc9851 post.php: require post history before creating a thread 2024-11-12 15:07:49 +01:00
2904fb5f3e config.php: add no post history error 2024-11-12 15:07:49 +01:00
06b0cb8484 config.php: add op_require_history option 2024-11-12 15:07:38 +01:00
2981859ac1 functions.php: add has_any_history function 2024-11-12 15:07:15 +01:00
aaa725dd06 Merge branch '27-improve-reports' into 'config'
Resolve "Increase the number of reports displayed in the reports page"

Closes #27

See merge request leftypol/leftypol!5
2024-11-12 13:20:58 +00:00
09075b1465 pages.php: mod_reports rework query 2024-11-12 14:20:14 +01:00
c94a7cb403 pages.php: mod_reports signal if there are more reports than the limit 2024-11-12 14:20:12 +01:00
e7518dfe25 pages.php: format mod_reports 2024-11-12 14:19:52 +01:00
82391e0f83 maintenance.php: use tabs 2024-11-12 13:47:21 +01:00
d7db185129 Merge branch '37-report-maintenance' into 'config'
Resolve "Implement auto maintenance for the reports"

Closes #37

See merge request leftypol/leftypol!11
2024-11-12 09:55:50 +00:00
1df7c589cb maintenance.php: delete invalid reports 2024-11-12 10:48:02 +01:00
4c1a6d3c57 pages.php: skip reports deletion if auto maintenance is disabled 2024-11-04 23:13:14 +01:00
f7fd639d2d pages.php: use modern array syntax 2024-11-04 23:11:21 +01:00
5fa893655d Merge branch '7-matrix-reports' into 'config'
Resolve "Matrix report bot"

Closes #7

See merge request leftypol/leftypol!10
2024-11-04 21:59:28 +00:00
4d62dcd9a2 post.php: use modern matrix API 2024-11-04 22:57:59 +01:00
20a30f2661 post.php: split into a function matrix reporting 2024-11-04 22:57:59 +01:00
703637a948 config.php: restructure matrix configuration a bit 2024-11-04 22:57:57 +01:00
6473ff6ab6 jungle.css: add quote styling 2024-10-31 22:09:33 +01:00
9658db1666 jungle.css: format and remove obsolete properties 2024-10-31 21:30:22 +01:00
9e2ab87df6 ban_form.html: add autofocus on reason text form 2024-10-28 23:39:17 +01:00
aa8525aa86 Merge branch 'captcha-workaround' into 'config'
Captcha workaround

See merge request leftypol/leftypol!9
2024-10-28 12:36:37 +00:00
a99f8c5ef3 cache.php: fix redis value decoding 2024-10-28 13:35:20 +01:00
e451faae40 cache.php: fix typo 2024-10-28 13:35:16 +01:00
00d7073c25 cache.php: fix redis value decoding 2024-10-28 13:32:25 +01:00
777c3b628a cache.php: fix typo 2024-10-28 13:26:36 +01:00
a7e349d8cb post.php: workaround captcha and js/ajax.js bug 2024-10-28 13:26:34 +01:00
ebc0c54657 main.js: generate captcha form id 2024-10-28 12:33:36 +01:00
f792470af7 post_form.html: add captcha form id 2024-10-28 12:33:19 +01:00
2741b1ea05 config.php: accept captcha-form-id 2024-10-28 12:32:48 +01:00
04ef961440 config.php: refactor captcha configuration 2024-10-28 11:24:57 +01:00
6fdc16d2f3 config.php: accept unified captcha-response 2024-10-28 11:17:01 +01:00
93e37b0c42 main.js: use unified captcha-response 2024-10-28 11:16:07 +01:00
2319a7b74e post.php: use unified captcha-response 2024-10-28 11:16:05 +01:00
81ad1fff38 post_form: add unified captcha response hook 2024-10-28 00:44:26 +01:00
873b14d848 Merge branch '34-catalog-issues' into 'config'
Resolve "Catalog troubles"

Closes #34

See merge request leftypol/leftypol!7
2024-10-24 21:50:50 +00:00
7ea17c9f8e style.css: add spacing between catalog controls 2024-10-24 23:48:45 +02:00
56a3d9d6c6 catalog.html: add css classes for controls styling 2024-10-24 23:48:22 +02:00
0375e8238a Merge branch 'dark-spooks' into 'config'
Dark spooks

See merge request leftypol/leftypol!8
2024-10-24 21:24:59 +00:00
70c07dceca spooks.php: add spooks image redirector 2024-10-24 23:09:52 +02:00
d120cb0ed3 Add spook images 2024-10-24 23:09:44 +02:00
1a0054967b Merge branch '17-paginate-ips' into 'config'
Add IP pagination

Closes #17

See merge request leftypol/leftypol!3
2024-10-24 19:18:54 +00:00
894d5a4b8b net.php: remove base64 padding from encoded cursor 2024-10-24 21:13:05 +02:00
9d295ca82c dark_spook.css: remove some redaundant css 2024-10-23 23:39:06 +02:00
a1031d9370 dark_spook.css: fork it from dark.css 2024-10-23 23:22:51 +02:00
217f49f090 dark_spooks.css: add missing theme back in 2024-10-23 23:18:55 +02:00
8c3bc77992 gorby.css: clean up css 2024-10-23 23:17:11 +02:00
3ac173c914 stylesheets: optimize gorbachev's style images 2024-10-23 23:16:43 +02:00
007656c1d0 stylesheets: add back in gorbachev's theme 2024-10-23 23:00:28 +02:00
f0f784d113 style.css; fix missing semicolon 2024-10-23 21:35:46 +02:00
971191936a style.css: adjust catalog thread centering 2024-10-23 21:35:18 +02:00
a450342d0d catalog.html: adjust image size naming 2024-10-23 20:05:10 +02:00
e3b89dcc9d pages.php: fix redirection on mod_page_ip 2024-10-15 21:15:06 +02:00
4be1873538 pages.php: functional cursor pagination for mod_page_ip with a single query 2024-10-15 21:15:06 +02:00
6c3f24e2a1 database.php: PDO always throw even on PHP < 8.0 2024-10-15 21:15:06 +02:00
b9081be4ac view_ip.html: add links to previous and next cursors 2024-10-15 21:15:06 +02:00
82463bb720 mod.php: pass cursor to mod_page_ip 2024-10-15 21:15:06 +02:00
92fee878b5 net.php: add functions to encode and decode a typed cursor over an url 2024-10-15 21:15:06 +02:00
fa26f6e88f config.php: update configuration documentation for ip_recentposts 2024-10-15 21:15:06 +02:00
119 changed files with 4103 additions and 3307 deletions

3
.gitignore vendored
View file

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

View file

@ -7,7 +7,7 @@ services:
ports:
- "9091:80"
depends_on:
- leftypol-db
- db
volumes:
- ./local-instances/${INSTANCE:-0}/www:/var/www/html
- ./docker/nginx/leftypol.conf:/etc/nginx/conf.d/default.conf
@ -23,13 +23,11 @@ services:
volumes:
- ./local-instances/${INSTANCE:-0}/www:/var/www
- ./docker/php/www.conf:/usr/local/etc/php-fpm.d/www.conf
- redis-sock:/var/run/redis
#MySQL Service
leftypol-db:
db:
image: mysql:8.0.35
container_name: leftypol-db
restart: unless-stopped
tty: true
ports:
- "3306:3306"
environment:
@ -37,3 +35,13 @@ services:
MYSQL_ROOT_PASSWORD: password
volumes:
- ./local-instances/${INSTANCE:-0}/mysql:/var/lib/mysql
redis:
build:
context: ./
dockerfile: ./docker/redis/Dockerfile
volumes:
- redis-sock:/var/run/redis
volumes:
redis-sock:

View file

@ -11,7 +11,9 @@
"autoload": {
"classmap": ["inc/"],
"files": [
"inc/anti-bot.php",
"inc/bootstrap.php",
"inc/context.php",
"inc/display.php",
"inc/template.php",
"inc/database.php",

6
docker/redis/Dockerfile Normal file
View file

@ -0,0 +1,6 @@
FROM redis:7.4-alpine
RUN mkdir -p /var/run/redis && chmod 777 /var/run/redis
COPY ./docker/redis/redis.conf /etc/redis.conf
ENTRYPOINT [ "docker-entrypoint.sh", "/etc/redis.conf" ]

16
docker/redis/redis.conf Normal file
View file

@ -0,0 +1,16 @@
# Accept connections on the specified port, default is 6379 (IANA #815344).
# If port 0 is specified Redis will not listen on a TCP socket.
#port 6379
port 0
# Unix socket.
#
# Specify the path for the Unix socket that will be used to listen for
# incoming connections. There is no default, so Redis will not listen
# on a unix socket when not specified.
#
unixsocket /var/run/redis/redis-server.sock
# Executig a socket is a no-op, and we need to share acces to other programs.
# Shared the connection only with programs in the redis group for security.
#unixsocketperm 700
unixsocketperm 666

View file

@ -0,0 +1,28 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
class ApcuCacheDriver implements CacheDriver {
public function get(string $key): mixed {
$success = false;
$ret = \apcu_fetch($key, $success);
if ($success === false) {
return null;
}
return $ret;
}
public function set(string $key, mixed $value, mixed $expires = false): void {
\apcu_store($key, $value, (int)$expires);
}
public function delete(string $key): void {
\apcu_delete($key);
}
public function flush(): void {
\apcu_clear_cache();
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
/**
* A simple process-wide PHP array.
*/
class ArrayCacheDriver implements CacheDriver {
private static array $inner = [];
public function get(string $key): mixed {
return isset(self::$inner[$key]) ? self::$inner[$key] : null;
}
public function set(string $key, mixed $value, mixed $expires = false): void {
self::$inner[$key] = $value;
}
public function delete(string $key): void {
unset(self::$inner[$key]);
}
public function flush(): void {
self::$inner = [];
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
interface CacheDriver {
/**
* Get the value of associated with the key.
*
* @param string $key The key of the value.
* @return mixed|null The value associated with the key, or null if there is none.
*/
public function get(string $key): mixed;
/**
* Set a key-value pair.
*
* @param string $key The key.
* @param mixed $value The value.
* @param int|false $expires After how many seconds the pair will expire. Use false or ignore this parameter to keep
* the value until it gets evicted to make space for more items. Some drivers will always
* ignore this parameter and store the pair until it's removed.
*/
public function set(string $key, mixed $value, mixed $expires = false): void;
/**
* Delete a key-value pair.
*
* @param string $key The key.
*/
public function delete(string $key): void;
/**
* Delete all the key-value pairs.
*/
public function flush(): void;
}

View file

@ -0,0 +1,28 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
/**
* Log via the php function error_log.
*/
class ErrorLogLogDriver implements LogDriver {
use LogTrait;
private string $name;
private int $level;
public function __construct(string $name, int $level) {
$this->name = $name;
$this->level = $level;
}
public function log(int $level, string $message): void {
if ($level <= $this->level) {
$lv = $this->levelToString($level);
$line = "{$this->name} $lv: $message";
\error_log($line, 0, null, null);
}
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
/**
* Log to a file.
*/
class FileLogDriver implements LogDriver {
use LogTrait;
private string $name;
private int $level;
private mixed $fd;
public function __construct(string $name, int $level, string $file_path) {
/*
* error_log is slow as hell in it's 3rd mode, so use fopen + file locking instead.
* https://grobmeier.solutions/performance-ofnonblocking-write-to-files-via-php-21082009.html
*
* Whatever file appending is atomic is contentious:
* - There are no POSIX guarantees: https://stackoverflow.com/a/7237901
* - But linus suggested they are on linux, on some filesystems: https://web.archive.org/web/20151201111541/http://article.gmane.org/gmane.linux.kernel/43445
* - But it doesn't seem to be always the case: https://www.notthewizard.com/2014/06/17/are-files-appends-really-atomic/
*
* So we just use file locking to be sure.
*/
$this->fd = \fopen($file_path, 'a');
if ($this->fd === false) {
throw new \RuntimeException("Unable to open log file at $file_path");
}
$this->name = $name;
$this->level = $level;
// In some cases PHP does not run the destructor.
\register_shutdown_function([$this, 'close']);
}
public function __destruct() {
$this->close();
}
public function log(int $level, string $message): void {
if ($level <= $this->level) {
$lv = $this->levelToString($level);
$line = "{$this->name} $lv: $message\n";
\flock($this->fd, LOCK_EX);
\fwrite($this->fd, $line);
\fflush($this->fd);
\flock($this->fd, LOCK_UN);
}
}
public function close() {
\flock($this->fd, LOCK_UN);
\fclose($this->fd);
}
}

View file

@ -0,0 +1,155 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
class FsCacheDriver implements CacheDriver {
private string $prefix;
private string $base_path;
private mixed $lock_fd;
private int|false $collect_chance_den;
private function prepareKey(string $key): string {
$key = \str_replace('/', '::', $key);
$key = \str_replace("\0", '', $key);
return $this->prefix . $key;
}
private function sharedLockCache(): void {
\flock($this->lock_fd, LOCK_SH);
}
private function exclusiveLockCache(): void {
\flock($this->lock_fd, LOCK_EX);
}
private function unlockCache(): void {
\flock($this->lock_fd, LOCK_UN);
}
private function collectImpl(): int {
/*
* A read lock is ok, since it's alright if we delete expired items from under the feet of other processes, and
* no other process add new cache items or refresh existing ones.
*/
$files = \glob($this->base_path . $this->prefix . '*', \GLOB_NOSORT);
$count = 0;
foreach ($files as $file) {
$data = \file_get_contents($file);
$wrapped = \json_decode($data, true, 512, \JSON_THROW_ON_ERROR);
if ($wrapped['expires'] !== false && $wrapped['expires'] <= \time()) {
if (@\unlink($file)) {
$count++;
}
}
}
return $count;
}
private function maybeCollect(): void {
if ($this->collect_chance_den !== false && \mt_rand(0, $this->collect_chance_den - 1) === 0) {
$this->collect_chance_den = false; // Collect only once per instance (aka process).
$this->collectImpl();
}
}
public function __construct(string $prefix, string $base_path, string $lock_file, int|false $collect_chance_den) {
if ($base_path[\strlen($base_path) - 1] !== '/') {
$base_path = "$base_path/";
}
if (!\is_dir($base_path)) {
throw new \RuntimeException("$base_path is not a directory!");
}
if (!\is_writable($base_path)) {
throw new \RuntimeException("$base_path is not writable!");
}
$this->lock_fd = \fopen($base_path . $lock_file, 'w');
if ($this->lock_fd === false) {
throw new \RuntimeException('Unable to open the lock file!');
}
$this->prefix = $prefix;
$this->base_path = $base_path;
$this->collect_chance_den = $collect_chance_den;
}
public function __destruct() {
$this->close();
}
public function get(string $key): mixed {
$key = $this->prepareKey($key);
$this->sharedLockCache();
// Collect expired items first so if the target key is expired we shortcut to failure in the next lines.
$this->maybeCollect();
$fd = \fopen($this->base_path . $key, 'r');
if ($fd === false) {
$this->unlockCache();
return null;
}
$data = \stream_get_contents($fd);
\fclose($fd);
$this->unlockCache();
$wrapped = \json_decode($data, true, 512, \JSON_THROW_ON_ERROR);
if ($wrapped['expires'] !== false && $wrapped['expires'] <= \time()) {
// Already expired, leave it there since we already released the lock and pretend it doesn't exist.
return null;
} else {
return $wrapped['inner'];
}
}
public function set(string $key, mixed $value, mixed $expires = false): void {
$key = $this->prepareKey($key);
$wrapped = [
'expires' => $expires ? \time() + $expires : false,
'inner' => $value
];
$data = \json_encode($wrapped);
$this->exclusiveLockCache();
$this->maybeCollect();
\file_put_contents($this->base_path . $key, $data);
$this->unlockCache();
}
public function delete(string $key): void {
$key = $this->prepareKey($key);
$this->exclusiveLockCache();
@\unlink($this->base_path . $key);
$this->maybeCollect();
$this->unlockCache();
}
public function collect(): int {
$this->sharedLockCache();
$count = $this->collectImpl();
$this->unlockCache();
return $count;
}
public function flush(): void {
$this->exclusiveLockCache();
$files = \glob($this->base_path . $this->prefix . '*', \GLOB_NOSORT);
foreach ($files as $file) {
@\unlink($file);
}
$this->unlockCache();
}
public function close(): void {
\fclose($this->lock_fd);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
interface LogDriver {
public const EMERG = \LOG_EMERG;
public const ERROR = \LOG_ERR;
public const WARNING = \LOG_WARNING;
public const NOTICE = \LOG_NOTICE;
public const INFO = \LOG_INFO;
public const DEBUG = \LOG_DEBUG;
/**
* Log a message if the level of relevancy is at least the minimum.
*
* @param int $level Message level. Use Log interface constants.
* @param string $message The message to log.
*/
public function log(int $level, string $message): void;
}

View file

@ -0,0 +1,26 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
trait LogTrait {
public static function levelToString(int $level): string {
switch ($level) {
case LogDriver::EMERG:
return 'EMERG';
case LogDriver::ERROR:
return 'ERROR';
case LogDriver::WARNING:
return 'WARNING';
case LogDriver::NOTICE:
return 'NOTICE';
case LogDriver::INFO:
return 'INFO';
case LogDriver::DEBUG:
return 'DEBUG';
default:
throw new \InvalidArgumentException('Not a logging level');
}
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
class MemcachedCacheDriver implements CacheDriver {
private \Memcached $inner;
public function __construct(string $prefix, string $memcached_server) {
$this->inner = new \Memcached();
if (!$this->inner->setOption(\Memcached::OPT_BINARY_PROTOCOL, true)) {
throw new \RuntimeException('Unable to set the memcached protocol!');
}
if (!$this->inner->setOption(\Memcached::OPT_PREFIX_KEY, $prefix)) {
throw new \RuntimeException('Unable to set the memcached prefix!');
}
if (!$this->inner->addServers($memcached_server)) {
throw new \RuntimeException('Unable to add the memcached server!');
}
}
public function get(string $key): mixed {
$ret = $this->inner->get($key);
// If the returned value is false but the retrival was a success, then the value stored was a boolean false.
if ($ret === false && $this->inner->getResultCode() !== \Memcached::RES_SUCCESS) {
return null;
}
return $ret;
}
public function set(string $key, mixed $value, mixed $expires = false): void {
$this->inner->set($key, $value, (int)$expires);
}
public function delete(string $key): void {
$this->inner->delete($key);
}
public function flush(): void {
$this->inner->flush();
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
/**
* No-op cache. Useful for testing.
*/
class NoneCacheDriver implements CacheDriver {
public function get(string $key): mixed {
return null;
}
public function set(string $key, mixed $value, mixed $expires = false): void {
// No-op.
}
public function delete(string $key): void {
// No-op.
}
public function flush(): void {
// No-op.
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
class RedisCacheDriver implements CacheDriver {
private string $prefix;
private \Redis $inner;
public function __construct(string $prefix, string $host, ?int $port, ?string $password, 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]);
} elseif ($port === null) {
$this->inner->connect($host);
} else {
// IP + port.
$this->inner->connect($host, $port);
}
if ($password) {
$this->inner->auth($password);
}
if (!$this->inner->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_JSON)) {
throw new \RuntimeException('Unable to configure Redis serializer');
}
if (!$this->inner->select($database)) {
throw new \RuntimeException('Unable to connect to Redis database!');
}
$this->prefix = $prefix;
}
public function get(string $key): mixed {
$ret = $this->inner->get($this->prefix . $key);
if ($ret === false) {
return null;
}
if ($ret === null) {
return false;
}
return $ret;
}
public function set(string $key, mixed $value, mixed $expires = false): void {
$value = $value === false ? null : $value;
if ($expires === false) {
$this->inner->set($this->prefix . $key, $value);
} else {
$this->inner->setEx($this->prefix . $key, $expires, $value);
}
}
public function delete(string $key): void {
$this->inner->del($this->prefix . $key);
}
public function flush(): void {
if (empty($this->prefix)) {
$this->inner->flushDB();
} else {
$this->inner->unlink($this->inner->keys("{$this->prefix}*"));
}
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
/**
* Log to php's standard error file stream.
*/
class StderrLogDriver implements LogDriver {
use LogTrait;
private string $name;
private int $level;
public function __construct(string $name, int $level) {
$this->name = $name;
$this->level = $level;
}
public function log(int $level, string $message): void {
if ($level <= $this->level) {
$lv = $this->levelToString($level);
\fwrite(\STDERR, "{$this->name} $lv: $message\n");
}
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
/**
* Log to syslog.
*/
class SyslogLogDriver implements LogDriver {
private int $level;
public function __construct(string $name, int $level, bool $print_stderr) {
$flags = \LOG_ODELAY;
if ($print_stderr) {
$flags |= \LOG_PERROR;
}
if (!\openlog($name, $flags, \LOG_USER)) {
throw new \RuntimeException('Unable to open syslog');
}
$this->level = $level;
}
public function log(int $level, string $message): void {
if ($level <= $this->level) {
if (isset($_SERVER['REMOTE_ADDR'], $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'])) {
// CGI
\syslog($level, "$message - client: {$_SERVER['REMOTE_ADDR']}, request: \"{$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']}\"");
} else {
\syslog($level, $message);
}
}
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace Vichan\Data;
use Vichan\Data\Driver\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,15 @@
<?php
namespace Vichan\Data;
/**
* 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;
}

227
inc/Data/ReportQueries.php Normal file
View file

@ -0,0 +1,227 @@
<?php
namespace Vichan\Data;
class ReportQueries {
private \PDO $pdo;
private bool $auto_maintenance;
private function deleteReportImpl(string $board, int $post_id) {
$query = $this->pdo->prepare('DELETE FROM `reports` WHERE `post` = :id AND `board` = :board');
$query->bindValue(':id', $post_id, \PDO::PARAM_INT);
$query->bindValue(':board', $board);
$query->execute();
}
private function joinReportPosts(array $raw_reports, ?int $limit): array {
// Group the reports rows by board.
$reports_by_boards = [];
foreach ($raw_reports as $report) {
if (!isset($reports_by_boards[$report['board']])) {
$reports_by_boards[$report['board']] = [];
}
$reports_by_boards[$report['board']][] = $report['post'];
}
// Join the reports with the actual posts.
$report_posts = [];
foreach ($reports_by_boards as $board => $posts) {
$report_posts[$board] = [];
$query = $this->pdo->prepare(\sprintf('SELECT * FROM `posts_%s` WHERE `id` IN (' . \implode(',', $posts) . ')', $board));
$query->execute();
while ($post = $query->fetch(\PDO::FETCH_ASSOC)) {
$report_posts[$board][$post['id']] = $post;
}
}
// Filter out the reports without a valid post.
$valid = [];
foreach ($raw_reports as $report) {
if (isset($report_posts[$report['board']][$report['post']])) {
$report['post_data'] = $report_posts[$report['board']][$report['post']];
$valid[] = $report;
if ($limit !== null && \count($valid) >= $limit) {
return $valid;
}
} else {
// Invalid report (post has been deleted).
if ($this->auto_maintenance != false) {
$this->deleteReportImpl($report['board'], $report['post']);
}
}
}
return $valid;
}
/**
* Filters out the invalid reports.
*
* @param array $raw_reports Array with the raw fetched reports. Must include a `board`, `post` and `id` fields.
* @param bool $get_invalid True to reverse the filter and get the invalid reports instead.
* @return array An array of filtered reports.
*/
private function filterReports(array $raw_reports, bool $get_invalid): array {
// Group the reports rows by board.
$reports_by_boards = [];
foreach ($raw_reports as $report) {
if (!isset($reports_by_boards[$report['board']])) {
$reports_by_boards[$report['board']] = [];
}
$reports_by_boards[$report['board']][] = $report['post'];
}
// Join the reports with the actual posts.
$report_posts = [];
foreach ($reports_by_boards as $board => $posts) {
$report_posts[$board] = [];
$query = $this->pdo->prepare(\sprintf('SELECT `id` FROM `posts_%s` WHERE `id` IN (' . \implode(',', $posts) . ')', $board));
$query->execute();
while ($post = $query->fetch(\PDO::FETCH_ASSOC)) {
$report_posts[$board][$post['id']] = $post;
}
}
if ($get_invalid) {
// Get the reports without a post.
$invalid = [];
foreach ($raw_reports as $report) {
if (isset($report_posts[$report['board']][$report['post']])) {
$invalid[] = $report;
}
}
return $invalid;
} else {
// Filter out the reports without a valid post.
$valid = [];
foreach ($raw_reports as $report) {
if (isset($report_posts[$report['board']][$report['post']])) {
$valid[] = $report;
} else {
// Invalid report (post has been deleted).
if ($this->auto_maintenance != false) {
$this->deleteReportImpl($report['board'], $report['post']);
}
}
}
return $valid;
}
}
/**
* @param \PDO $pdo PDO connection.
* @param bool $auto_maintenance If the auto maintenance should be enabled.
*/
public function __construct(\PDO $pdo, bool $auto_maintenance) {
$this->pdo = $pdo;
$this->auto_maintenance = $auto_maintenance;
}
/**
* Get the number of reports.
*
* @return int The number of reports.
*/
public function getCount(): int {
$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);
$count = \count($valid_reports);
return $count;
}
/**
* Get the report with the given id. DOES NOT PERFORM VALIDITY CHECK.
*
* @param int $id The id of the report to fetch.
* @return ?array An array of the given report with the `board` and `ip` fields. Null if no such report exists.
*/
public function getReportById(int $id): ?array {
$query = prepare('SELECT `board`, `ip` FROM ``reports`` WHERE `id` = :id');
$query->bindValue(':id', $id);
$query->execute();
$ret = $query->fetch(\PDO::FETCH_ASSOC);
if ($ret !== false) {
return $ret;
} else {
return null;
}
}
/**
* Get the reports with the associated post data.
*
* @param int $count The maximum number of rows in the return array.
* @return array The reports with the associated post data.
*/
public function getReportsWithPosts(int $count): array {
$query = $this->pdo->prepare('SELECT * FROM `reports` ORDER BY `time`');
$query->execute();
$raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC);
return $this->joinReportPosts($raw_reports, $count);
}
/**
* Purge the invalid reports.
*
* @return int The number of reports deleted.
*/
public function purge(): int {
$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);
foreach ($invalid_reports as $report) {
$this->deleteReportImpl($report['board'], $report['post']);
}
return \count($invalid_reports);
}
/**
* Deletes the given report.
*
* @param int $id The report id.
*/
public function deleteById(int $id) {
$query = $this->pdo->prepare('DELETE FROM `reports` WHERE `id` = :id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
}
/**
* Deletes all reports from the given ip.
*
* @param string $ip The reporter ip.
*/
public function deleteByIp(string $ip) {
$query = $this->pdo->prepare('DELETE FROM `reports` WHERE `ip` = :ip');
$query->bindValue(':ip', $ip);
$query->execute();
}
/**
* Inserts a new report.
*
* @param string $ip Ip of the user sending the report.
* @param string $board_uri Board uri of the reported thread. MUST ALREADY BE SANITIZED.
* @param int $post_id Post reported.
* @param string $reason Reason of the report.
* @return void
*/
public function add(string $ip, string $board_uri, int $post_id, string $reason) {
$query = $this->pdo->prepare('INSERT INTO `reports` VALUES (NULL, :time, :ip, :board, :post, :reason)');
$query->bindValue(':time', time(), \PDO::PARAM_INT);
$query->bindValue(':ip', $ip);
$query->bindValue(':board', $board_uri);
$query->bindValue(':post', $post_id, \PDO::PARAM_INT);
$query->bindValue(':reason', $reason);
$query->execute();
}
}

View file

@ -0,0 +1,159 @@
<?php
namespace Vichan\Data;
use Vichan\Functions\Net;
/**
* Browse user posts
*/
class UserPostQueries {
private const CURSOR_TYPE_PREV = 'p';
private const CURSOR_TYPE_NEXT = 'n';
private \PDO $pdo;
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'");
}
});
}
}

View file

@ -190,53 +190,62 @@ class AntiBot {
}
}
function _create_antibot($board, $thread) {
function _create_antibot($pdo, $board, $thread) {
global $config, $purged_old_antispam;
$antibot = new AntiBot(array($board, $thread));
// 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();
}
retry_on_deadlock(4, function() use($thread, $board, $config) {
// 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();
});
try {
$hash = $antibot->hash();
retry_on_deadlock(3, function() use ($config, $pdo, $thread, $board, $antibot, $purged_old_antispam) {
try {
$pdo->beginTransaction();
retry_on_deadlock(2, function() use($board, $thread, $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();
// 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');
}
$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();
// 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();
} catch (\Exception $e) {
$pdo->rollBack();
throw $e;
}
});
} catch(\PDOException $e) {
} catch (\PDOException $e) {
$pdo->rollBack();
if ($e->errorInfo === null || $e->errorInfo[1] != MYSQL_ER_LOCK_DEADLOCK) {
throw $e;
} else {
error_log('Multiple deadlocks on _create_antibot while inserting, skipping');
\error_log('5 or more deadlocks on _create_antibot while inserting, skipping');
}
}

View file

@ -4,182 +4,91 @@
* Copyright (c) 2010-2013 Tinyboard Development Group
*/
use Vichan\Data\Driver\{CacheDriver, ApcuCacheDriver, ArrayCacheDriver, FsCacheDriver, MemcachedCacheDriver, NoneCacheDriver, RedisCacheDriver};
defined('TINYBOARD') or exit;
class Cache {
private static $cache;
public static function init() {
private static function buildCache(): CacheDriver {
global $config;
switch ($config['cache']['enabled']) {
case 'memcached':
self::$cache = new Memcached();
self::$cache->addServers($config['cache']['memcached']);
break;
return new MemcachedCacheDriver(
$config['cache']['prefix'],
$config['cache']['memcached']
);
case 'redis':
self::$cache = new Redis();
$ret = explode(':', $config['cache']['redis'][0]);
if (count($ret) > 0) {
// Unix socket.
self::$cache->connect($ret[1]);
} else {
// IP + port.
self::$cache->connect($ret[0], $config['cache']['redis'][1]);
}
if ($config['cache']['redis'][2]) {
self::$cache->auth($config['cache']['redis'][2]);
}
self::$cache->select($config['cache']['redis'][3]) or die('cache select failure');
break;
$port = $config['cache']['redis'][1];
$port = empty($port) ? null : intval($port);
return new RedisCacheDriver(
$config['cache']['prefix'],
$config['cache']['redis'][0],
$port,
$config['cache']['redis'][2],
$config['cache']['redis'][3]
);
case 'apcu':
return new ApcuCacheDriver;
case 'fs':
return new FsCacheDriver(
$config['cache']['prefix'],
"tmp/cache/{$config['cache']['prefix']}",
'.lock',
$config['auto_maintenance'] ? 1000 : false
);
case 'none':
return new NoneCacheDriver();
case 'php':
self::$cache = array();
break;
default:
return new ArrayCacheDriver();
}
}
public static function getCache(): CacheDriver {
static $cache;
return $cache ??= self::buildCache();
}
public static function get($key) {
global $config, $debug;
$key = $config['cache']['prefix'] . $key;
$data = false;
switch ($config['cache']['enabled']) {
case 'memcached':
if (!self::$cache)
self::init();
$data = self::$cache->get($key);
break;
case 'apc':
$data = apc_fetch($key);
break;
case 'xcache':
$data = xcache_get($key);
break;
case 'php':
$data = isset(self::$cache[$key]) ? self::$cache[$key] : false;
break;
case 'fs':
$key = str_replace('/', '::', $key);
$key = str_replace("\0", '', $key);
if (!file_exists('tmp/cache/'.$key)) {
$data = false;
}
else {
$data = file_get_contents('tmp/cache/'.$key);
$data = json_decode($data, true);
}
break;
case 'redis':
if (!self::$cache)
self::init();
$data = json_decode(self::$cache->get($key), true);
break;
$ret = self::getCache()->get($key);
if ($ret === null) {
$ret = false;
}
if ($config['debug'])
$debug['cached'][] = $key . ($data === false ? ' (miss)' : ' (hit)');
if ($config['debug']) {
$debug['cached'][] = $config['cache']['prefix'] . $key . ($ret === false ? ' (miss)' : ' (hit)');
}
return $data;
return $ret;
}
public static function set($key, $value, $expires = false) {
global $config, $debug;
$key = $config['cache']['prefix'] . $key;
if (!$expires)
if (!$expires) {
$expires = $config['cache']['timeout'];
switch ($config['cache']['enabled']) {
case 'memcached':
if (!self::$cache)
self::init();
self::$cache->set($key, $value, $expires);
break;
case 'redis':
if (!self::$cache)
self::init();
self::$cache->setex($key, $expires, json_encode($value));
break;
case 'apc':
apc_store($key, $value, $expires);
break;
case 'xcache':
xcache_set($key, $value, $expires);
break;
case 'fs':
$key = str_replace('/', '::', $key);
$key = str_replace("\0", '', $key);
file_put_contents('tmp/cache/'.$key, json_encode($value));
break;
case 'php':
self::$cache[$key] = $value;
break;
}
if ($config['debug'])
$debug['cached'][] = $key . ' (set)';
self::getCache()->set($key, $value, $expires);
if ($config['debug']) {
$debug['cached'][] = $config['cache']['prefix'] . $key . ' (set)';
}
}
public static function delete($key) {
global $config, $debug;
$key = $config['cache']['prefix'] . $key;
self::getCache()->delete($key);
switch ($config['cache']['enabled']) {
case 'memcached':
if (!self::$cache)
self::init();
self::$cache->delete($key);
break;
case 'redis':
if (!self::$cache)
self::init();
self::$cache->del($key);
break;
case 'apc':
apc_delete($key);
break;
case 'xcache':
xcache_unset($key);
break;
case 'fs':
$key = str_replace('/', '::', $key);
$key = str_replace("\0", '', $key);
@unlink('tmp/cache/'.$key);
break;
case 'php':
unset(self::$cache[$key]);
break;
if ($config['debug']) {
$debug['cached'][] = $config['cache']['prefix'] . $key . ' (deleted)';
}
if ($config['debug'])
$debug['cached'][] = $key . ' (deleted)';
}
public static function flush() {
global $config;
switch ($config['cache']['enabled']) {
case 'memcached':
if (!self::$cache)
self::init();
return self::$cache->flush();
case 'apc':
return apc_clear_cache('user');
case 'php':
self::$cache = array();
break;
case 'fs':
$files = glob('tmp/cache/*');
foreach ($files as $file) {
unlink($file);
}
break;
case 'redis':
if (!self::$cache)
self::init();
return self::$cache->flushDB();
}
self::getCache()->flush();
return false;
}
}

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;
@ -117,18 +137,26 @@
/*
* On top of the static file caching system, you can enable the additional caching system which is
* designed to minimize SQL queries and can significantly increase speed when posting or using the
* moderator interface. APC is the recommended method of caching.
* designed to minimize request processing can significantly increase speed when posting or using
* the moderator interface.
*
* http://tinyboard.org/docs/index.php?p=Config/Cache
* https://github.com/vichan-devel/vichan/wiki/cache
*/
// Uses a PHP array. MUST NOT be used in multiprocess environments.
$config['cache']['enabled'] = 'php';
// $config['cache']['enabled'] = 'xcache';
// $config['cache']['enabled'] = 'apc';
// The recommended in-memory method of caching. Requires the extension. Due to how APCu works, this should be
// disabled when you run tools from the cli.
// $config['cache']['enabled'] = 'apcu';
// The Memcache server. Requires the memcached extension, with a final D.
// $config['cache']['enabled'] = 'memcached';
// The Redis server. Requires the extension.
// $config['cache']['enabled'] = 'redis';
// Use the local cache folder. Slower than native but available out of the box and compatible with multiprocess
// environments. You can mount a ram-based filesystem in the cache directory to improve performance.
// $config['cache']['enabled'] = 'fs';
// Technically available, offers a no-op fake cache. Don't use this outside of testing or debugging.
// $config['cache']['enabled'] = 'none';
// Timeout for cached objects such as posts and HTML.
$config['cache']['timeout'] = 60 * 60 * 48; // 48 hours
@ -144,7 +172,7 @@
// Redis server to use. Location, port, password, database id.
// Note that Tinyboard may clear the database at times, so you may want to pick a database id just for
// Tinyboard to use.
$config['cache']['redis'] = array('localhost', 6379, '', 1);
$config['cache']['redis'] = [ 'localhost', 6379, null, 1 ];
// EXPERIMENTAL: Should we cache configs? Warning: this changes board behaviour, i'd say, a lot.
// If you have any lambdas/includes present in your config, you should move them to instance-functions.php
@ -192,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
@ -236,6 +267,9 @@
// To prevent bump atacks; returns the thread to last position after the last post is deleted.
$config['anti_bump_flood'] = false;
// Reject thread creation from IPs without any prior post history.
$config['op_require_history'] = false;
/*
* Introduction to Tinyboard's spam filter:
*
@ -301,9 +335,8 @@
'lock',
'raw',
'embed',
'g-recaptcha-response',
'h-captcha-response',
'cf-turnstile-response',
'captcha-response',
'captcha-form-id',
'spoiler',
'page',
'file_url',
@ -330,33 +363,40 @@
'answer' => '4'
);
*/
/**
* The captcha is dynamically injected on the client if the server replies with the `captcha-required` cookie set
* to 1.
* Use false to disable this configuration, otherwise, set the IP that vichan should check for captcha responses.
*/
$config['dynamic_captcha'] = false;
// Enable reCaptcha to make spam even harder. Rarely necessary.
$config['recaptcha'] = false;
// Public and private key pair from https://www.google.com/recaptcha/admin/create
$config['recaptcha_public'] = '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f';
$config['recaptcha_private'] = '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_';
// Enable hCaptcha.
$config['hcaptcha'] = false;
// Public and private key pair for using hCaptcha.
$config['hcaptcha_public'] = '7a4b21e0-dc53-46f2-a9f8-91d2e74b63a0';
$config['hcaptcha_private'] = '0x4e9A01bE637b51dC41a7Ea9865C3fDe4aB72Cf17';
// Enable Cloudflare's Turnstile captcha.
$config['turnstile'] = false;
// Public and private key pair for turnstile.
$config['turnstile_public'] = '';
$config['turnstile_private'] = '';
// Enable a captcha system to make spam even harder. Rarely necessary.
$config['captcha'] = [
/**
* Select the captcha backend, false to disable.
* Can be false, "recaptcha", "hcaptcha" or "turnstile".
*/
'mode' => false,
/**
* The captcha is dynamically injected on the client if the server replies with the `captcha-required` cookie set
* to 1.
* Use false to disable this configuration, otherwise, set the IP that vichan should check for captcha responses.
*/
'dynamic' => false,
// Require to be non-zero if you use js/ajax.js (preferably no more than a few seconds), otherwise weird errors might occur.
'passthrough_timeout' => 0,
// Configure Google reCAPTCHA.
'recaptcha' => [
// Public and private key pair from https://www.google.com/recaptcha/admin/create
'public' => '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f',
'private' => '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_',
],
// Configure hCaptcha.
'hcaptcha' => [
// Public and private key pair for using hCaptcha.
'public' => '7a4b21e0-dc53-46f2-a9f8-91d2e74b63a0',
'private' => '0x4e9A01bE637b51dC41a7Ea9865C3fDe4aB72Cf17',
],
// Configure Cloudflare Turnstile.
'turnstile' => [
// Public and private key pair for turnstile.
'public' => '',
'private' => '',
]
];
// Ability to lock a board for normal users and still allow mods to post. Could also be useful for making an archive board
$config['board_locked'] = false;
@ -547,6 +587,10 @@
// Requires $config['strip_combining_chars'] = true;
$config['max_combining_chars'] = 0;
// Maximum OP body length. Ignored if force_body_op is set to false.
$config['max_body_op'] = 1800;
// Minimum OP body length. Ignored if force_body_op is set to false.
$config['min_body_op'] = 0;
// Maximum post body length.
$config['max_body'] = 1800;
// Minimum post body length.
@ -709,18 +753,18 @@
// a link to an email address or IRC chat room to appeal the ban.
$config['ban_page_extra'] = '';
// Pre-configured ban reasons that pre-fill the ban form when clicked.
// To disable, set $config['ban_reasons'] = false;
$config['ban_reasons'] = array(
array( 'reason' => 'Low-quality posting',
'length' => '1d'),
array( 'reason' => 'Off-topic',
'length' => '1d'),
array( 'reason' => 'Ban evasion',
'length' => '30d'),
array( 'reason' => 'Illegal content',
'length' => ''),
);
// Pre-configured ban reasons that pre-fill the ban form when clicked.
// To disable, set $config['ban_reasons'] = false;
$config['ban_reasons'] = array(
array( 'reason' => 'Low-quality posting',
'length' => '1d'),
array( 'reason' => 'Off-topic',
'length' => '1d'),
array( 'reason' => 'Ban evasion',
'length' => '30d'),
array( 'reason' => 'Illegal content',
'length' => ''),
);
// How often (minimum) to purge the ban list of expired bans (which have been seen).
$config['purge_bans'] = 60 * 60 * 12; // 12 hours
@ -899,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.
@ -941,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.
@ -1171,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>'
@ -1191,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.
@ -1210,6 +1261,7 @@
// Error messages
$config['error']['bot'] = _('You look like a bot.');
$config['error']['referer'] = _('Your browser sent an invalid or no HTTP referer.');
$config['error']['opnohistory'] = _('You must post at least once before creating thread.');
$config['error']['toolong'] = _('The %s field was too long.');
$config['error']['toolong_body'] = _('The body was too long.');
$config['error']['tooshort_body'] = _('The body was too short or empty.');
@ -1499,8 +1551,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 ?/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;
@ -1889,22 +1941,24 @@
*/
// Matrix integration for reports
// $config['matrix'] = array(
// 'access_token' => 'ACCESS_TOKEN',
// 'room_id' => '%21askjdlkajsdlka:matrix.org',
// 'host' => 'https://matrix.org',
// 'max_message_length' => 240
// );
$config['matrix'] = [
'enabled' => false,
'access_token' => 'ACCESS_TOKEN',
// Note: must be already url-escaped.
'room_id' => '%21askjdlkajsdlka:matrix.org',
'host' => 'https://matrix.org',
'max_message_length' => 240
];
//Securimage captcha
//Note from lainchan PR: "TODO move a bunch of things here"
//Securimage captcha
//Note from lainchan PR: "TODO move a bunch of things here"
$config['spam']['valid_inputs'][]='captcha';
$config['error']['securimage']=array(
'missing'=>'The captcha field was missing. Please try again',
'empty'=>'Please fill out the captcha',
'bad'=>'Incorrect or expired captcha',
);
$config['spam']['valid_inputs'][]='captcha';
$config['error']['securimage']=array(
'missing'=>'The captcha field was missing. Please try again',
'empty'=>'Please fill out the captcha',
'bad'=>'Incorrect or expired captcha',
);
// Meta keywords. It's probably best to include these in per-board configurations.
// $config['meta_keywords'] = 'chan,anonymous discussion,imageboard,tinyboard';
@ -1976,12 +2030,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'] = "";

82
inc/context.php Normal file
View file

@ -0,0 +1,82 @@
<?php
namespace Vichan;
use Vichan\Data\{IpNoteQueries, ReportQueries, UserPostQueries};
use Vichan\Data\Driver\{CacheDriver, ErrorLogLogDriver, FileLogDriver, LogDriver, StderrLogDriver, SyslogLogDriver};
defined('TINYBOARD') or exit;
class Context {
private array $definitions;
public function __construct(array $definitions) {
$this->definitions = $definitions;
}
public function get(string $name): mixed {
if (!isset($this->definitions[$name])) {
throw new \RuntimeException("Could not find a dependency named $name");
}
$ret = $this->definitions[$name];
if (is_callable($ret) && !is_string($ret) && !is_array($ret)) {
$ret = $ret($this);
$this->definitions[$name] = $ret;
}
return $ret;
}
}
function build_context(array $config): Context {
return new Context([
'config' => $config,
LogDriver::class => function($c) {
$config = $c->get('config');
$name = $config['log_system']['name'];
$level = $config['debug'] ? LogDriver::DEBUG : LogDriver::NOTICE;
$backend = $config['log_system']['type'];
$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();
},
\PDO::class => function($c) {
global $pdo;
// Ensure the PDO is initialized.
sql_open();
return $pdo;
},
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)),
]);
}

View file

@ -72,6 +72,7 @@ function sql_open() {
try {
$options = [
PDO::ATTR_TIMEOUT => $config['db']['timeout'],
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // Set a consistent error mode between PHP versions.
];
if ($config['db']['type'] == "mysql")
@ -100,12 +101,6 @@ function sql_open() {
}
}
// 5.6.10 becomes 50610 HACK: hardcoded to be above critical value 50803 due to laziness
function mysql_version() {
// TODO delete all references of this function everywhere
return 80504;
}
function prepare($query) {
global $pdo, $debug, $config;

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

@ -355,9 +355,12 @@ function define_groups() {
}
function create_antibot($board, $thread = null) {
require_once dirname(__FILE__) . '/anti-bot.php';
global $pdo;
return _create_antibot($board, $thread);
// Ensure $pdo is initialized.
sql_open();
return _create_antibot($pdo, $board, $thread);
}
function rebuildThemes($action, $boardname = false) {
@ -742,24 +745,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;
}
@ -918,6 +920,48 @@ function checkBan($board = false) {
}
}
/**
* Checks if the given IP has any previous posts.
*
* @param string $ip The IP to check.
* @param ?string $passwd If not null, check also by password.
* @return bool True if the ip has already sent at least one post, false otherwise.
*/
function has_any_history(string $ip, ?string $passwd): bool {
global $config;
if ($config['cache']['enabled']) {
$ret = cache::get("post_history_$ip");
if ($ret !== false) {
return $ret !== 0x0;
}
}
foreach (listBoards(true) as $board_uri) {
if ($passwd === null) {
$query = prepare(sprintf('SELECT `id` FROM ``posts_%s`` WHERE `ip` = :ip LIMIT 1', $board_uri));
$query->bindValue(':ip', $ip);
} else {
$query = prepare(sprintf('SELECT `id` FROM ``posts_%s`` WHERE `ip` = :ip OR `password` = :passwd LIMIT 1', $board_uri));
$query->bindValue(':ip', $ip);
$query->bindValue(':passwd', $passwd);
}
$query->execute() or error(db_error());
if ($query->fetchColumn() !== false) {
// Found a post.
if ($config['cache']['enabled']) {
cache::set("post_history_$ip", 0xA);
}
return true;
}
}
if ($config['cache']['enabled']) {
cache::set("post_history_$ip", 0x0);
}
return false;
}
function threadLocked($id) {
global $board;
@ -2025,7 +2069,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);
@ -2040,9 +2084,6 @@ function markup(&$body, $track_cites = false, $op = false) {
$body = str_replace("\r", '', $body);
$body = utf8tohtml($body);
if (mysql_version() < 50503)
$body = mb_encode_numericentity($body, array(0x010000, 0xffffff, 0, 0xffffff), 'UTF-8');
if ($config['markup_code']) {
$code_markup = array();
$body = preg_replace_callback($config['markup_code'], function($matches) use (&$code_markup) {
@ -2127,12 +2168,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);
}
}
}
@ -3041,3 +3085,8 @@ function strategy_first($fun, $array) {
return array('defer');
}
}
function hashPassword($password) {
global $config;
return hash('sha3-256', $password . $config['secure_password_salt']);
}

View file

@ -15,3 +15,63 @@ function is_connection_https(): bool {
function is_connection_secure(): bool {
return is_connection_https() || (!empty($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] === '127.0.0.1');
}
/**
* Encodes a string into a base64 variant without characters illegal in urls.
*/
function base64_url_encode(string $input): string {
return str_replace([ '+', '/', '=' ], [ '-', '_', '' ], base64_encode($input));
}
/**
* Decodes a string from a base64 variant without characters illegal in urls.
*/
function base64_url_decode(string $input): string {
return base64_decode(strtr($input, '-_', '+/'));
}
/**
* Encodes a typed cursor.
*
* @param string $type The type for the cursor. Only the first character is considered.
* @param array $map A map of key-value pairs to encode.
* @return string An encoded string that can be sent through urls. Empty if either parameter is empty.
*/
function encode_cursor(string $type, array $map): string {
if (empty($type) || empty($map)) {
return '';
}
$acc = $type[0];
foreach ($map as $key => $value) {
$acc .= "|$key#$value";
}
return base64_url_encode($acc);
}
/**
* Decodes a typed cursor.
*
* @param string $cursor A string emitted by `encode_cursor`.
* @return array An array with the type of the cursor and an array of key-value pairs. The type is null and the map
* empty if either there are no key-value pairs or the encoding is incorrect.
*/
function decode_cursor(string $cursor): array {
$map = [];
$type = '';
$acc = base64_url_decode($cursor);
if ($acc === false || empty($acc)) {
return [ null, [] ];
}
$type = $acc[0];
foreach (explode('|', substr($acc, 2)) as $pair) {
$pair = explode('#', $pair);
if (count($pair) >= 2) {
$key = $pair[0];
$value = $pair[1];
$map[$key] = $value;
}
}
return [ $type, $map ];
}

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

@ -4,6 +4,7 @@
* Copyright (c) 2010-2013 Tinyboard Development Group
*/
use Vichan\Context;
use Vichan\Functions\Net;
defined('TINYBOARD') or exit;
@ -177,7 +178,7 @@ function make_secure_link_token($uri) {
return substr(sha1($config['cookies']['salt'] . '-' . $uri . '-' . $mod['id']), 0, 8);
}
function check_login($prompt = false) {
function check_login(Context $ctx, $prompt = false) {
global $config, $mod;
// Validate session
@ -187,7 +188,7 @@ function check_login($prompt = false) {
if (count($cookie) != 3) {
// Malformed cookies
destroyCookies();
if ($prompt) mod_login();
if ($prompt) mod_login($ctx);
exit;
}
@ -200,7 +201,7 @@ function check_login($prompt = false) {
if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) {
// Malformed cookies
destroyCookies();
if ($prompt) mod_login();
if ($prompt) mod_login($ctx);
exit;
}

File diff suppressed because it is too large Load diff

View file

@ -1,187 +1,10 @@
<?php
// PHP 5.4
// PHP 8.0
if (!function_exists('hex2bin')) {
function hex2bin($data) {
return pack("H*" , $hex_string);
}
}
// PHP 5.6
if (!function_exists('hash_equals')) {
function hash_equals($ours, $theirs) {
$ours = (string)$ours;
$theirs = (string)$theirs;
$tlen = strlen($theirs);
$olen = strlen($ours);
$answer = 0;
for ($i = 0; $i < $tlen; $i++) {
$answer |= ord($ours[$olen > $i ? $i : 0]) ^ ord($theirs[$i]);
}
return $answer === 0 && $olen === $tlen;
}
}
if (!function_exists('imagecreatefrombmp')) {
/*********************************************/
/* Fonction: imagecreatefrombmp */
/* Author: DHKold */
/* Contact: admin@dhkold.com */
/* Date: The 15th of June 2005 */
/* Version: 2.0B */
/*********************************************/
function imagecreatefrombmp($filename) {
if (! $f1 = fopen($filename,"rb")) return FALSE;
$FILE = unpack("vfile_type/Vfile_size/Vreserved/Vbitmap_offset", fread($f1,14));
if ($FILE['file_type'] != 19778) return FALSE;
$BMP = unpack('Vheader_size/Vwidth/Vheight/vplanes/vbits_per_pixel'.
'/Vcompression/Vsize_bitmap/Vhoriz_resolution'.
'/Vvert_resolution/Vcolors_used/Vcolors_important', fread($f1,40));
$BMP['colors'] = pow(2,$BMP['bits_per_pixel']);
if ($BMP['size_bitmap'] == 0) $BMP['size_bitmap'] = $FILE['file_size'] - $FILE['bitmap_offset'];
$BMP['bytes_per_pixel'] = $BMP['bits_per_pixel']/8;
$BMP['bytes_per_pixel2'] = ceil($BMP['bytes_per_pixel']);
$BMP['decal'] = ($BMP['width']*$BMP['bytes_per_pixel']/4);
$BMP['decal'] -= floor($BMP['width']*$BMP['bytes_per_pixel']/4);
$BMP['decal'] = 4-(4*$BMP['decal']);
if ($BMP['decal'] == 4) $BMP['decal'] = 0;
$PALETTE = array();
if ($BMP['colors'] < 16777216)
{
$PALETTE = unpack('V'.$BMP['colors'], fread($f1,$BMP['colors']*4));
}
$IMG = fread($f1,$BMP['size_bitmap']);
$VIDE = chr(0);
$res = imagecreatetruecolor($BMP['width'],$BMP['height']);
$P = 0;
$Y = $BMP['height']-1;
while ($Y >= 0)
{
$X=0;
while ($X < $BMP['width'])
{
if ($BMP['bits_per_pixel'] == 24)
$COLOR = unpack("V",substr($IMG,$P,3).$VIDE);
elseif ($BMP['bits_per_pixel'] == 16)
{
$COLOR = unpack("n",substr($IMG,$P,2));
$COLOR[1] = $PALETTE[$COLOR[1]+1];
}
elseif ($BMP['bits_per_pixel'] == 8)
{
$COLOR = unpack("n",$VIDE.substr($IMG,$P,1));
$COLOR[1] = $PALETTE[$COLOR[1]+1];
}
elseif ($BMP['bits_per_pixel'] == 4)
{
$COLOR = unpack("n",$VIDE.substr($IMG,floor($P),1));
if (($P*2)%2 == 0) $COLOR[1] = ($COLOR[1] >> 4) ; else $COLOR[1] = ($COLOR[1] & 0x0F);
$COLOR[1] = $PALETTE[$COLOR[1]+1];
}
elseif ($BMP['bits_per_pixel'] == 1)
{
$COLOR = unpack("n",$VIDE.substr($IMG,floor($P),1));
if (($P*8)%8 == 0) $COLOR[1] = $COLOR[1] >>7;
elseif (($P*8)%8 == 1) $COLOR[1] = ($COLOR[1] & 0x40)>>6;
elseif (($P*8)%8 == 2) $COLOR[1] = ($COLOR[1] & 0x20)>>5;
elseif (($P*8)%8 == 3) $COLOR[1] = ($COLOR[1] & 0x10)>>4;
elseif (($P*8)%8 == 4) $COLOR[1] = ($COLOR[1] & 0x8)>>3;
elseif (($P*8)%8 == 5) $COLOR[1] = ($COLOR[1] & 0x4)>>2;
elseif (($P*8)%8 == 6) $COLOR[1] = ($COLOR[1] & 0x2)>>1;
elseif (($P*8)%8 == 7) $COLOR[1] = ($COLOR[1] & 0x1);
$COLOR[1] = $PALETTE[$COLOR[1]+1];
}
else
return FALSE;
imagesetpixel($res,$X,$Y,$COLOR[1]);
$X++;
$P += $BMP['bytes_per_pixel'];
}
$Y--;
$P+=$BMP['decal'];
}
fclose($f1);
return $res;
}
}
if (!function_exists('imagebmp')) {
function imagebmp(&$img, $filename='') {
$widthOrig = imagesx($img);
$widthFloor = ((floor($widthOrig/16))*16);
$widthCeil = ((ceil($widthOrig/16))*16);
$height = imagesy($img);
$size = ($widthCeil*$height*3)+54;
// Bitmap File Header
$result = 'BM'; // header (2b)
$result .= int_to_dword($size); // size of file (4b)
$result .= int_to_dword(0); // reserved (4b)
$result .= int_to_dword(54); // byte location in the file which is first byte of IMAGE (4b)
// Bitmap Info Header
$result .= int_to_dword(40); // Size of BITMAPINFOHEADER (4b)
$result .= int_to_dword($widthCeil); // width of bitmap (4b)
$result .= int_to_dword($height); // height of bitmap (4b)
$result .= int_to_word(1); // biPlanes = 1 (2b)
$result .= int_to_word(24); // biBitCount = {1 (mono) or 4 (16 clr ) or 8 (256 clr) or 24 (16 Mil)} (2b
$result .= int_to_dword(0); // RLE COMPRESSION (4b)
$result .= int_to_dword(0); // width x height (4b)
$result .= int_to_dword(0); // biXPelsPerMeter (4b)
$result .= int_to_dword(0); // biYPelsPerMeter (4b)
$result .= int_to_dword(0); // Number of palettes used (4b)
$result .= int_to_dword(0); // Number of important colour (4b)
// is faster than chr()
$arrChr = array();
for ($i=0; $i<256; $i++){
$arrChr[$i] = chr($i);
}
// creates image data
$bgfillcolor = array('red'=>0, 'green'=>0, 'blue'=>0);
// bottom to top - left to right - attention blue green red !!!
$y=$height-1;
for ($y2=0; $y2<$height; $y2++) {
for ($x=0; $x<$widthFloor; ) {
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
}
for ($x=$widthFloor; $x<$widthCeil; $x++) {
$rgb = ($x<$widthOrig) ? imagecolorsforindex($img, imagecolorat($img, $x, $y)) : $bgfillcolor;
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
}
$y--;
}
// see imagegif
if ($filename == '') {
echo $result;
} else {
$file = fopen($filename, 'wb');
fwrite($file, $result);
fclose($file);
}
}
// imagebmp helpers
function int_to_dword($n) {
return chr($n & 255).chr(($n >> 8) & 255).chr(($n >> 16) & 255).chr(($n >> 24) & 255);
}
function int_to_word($n) {
return chr($n & 255).chr(($n >> 8) & 255);
if (!function_exists('str_starts_with')) {
function str_starts_with(string $haystack, string $needle): bool {
// https://wiki.php.net/rfc/add_str_starts_with_and_ends_with_functions#str_starts_with
return \strncmp($haystack, $needle, \strlen($needle)) === 0;
}
}

View file

@ -52,11 +52,7 @@ if (file_exists($config['has_installed'])) {
function __query($sql) {
sql_open();
if (mysql_version() >= 50503)
return query($sql);
else
return query(str_replace('utf8mb4', 'utf8', $sql));
return query($sql);
}
$boards = listBoards();
@ -885,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(
@ -939,7 +936,6 @@ if ($step == 0) {
$sql = @file_get_contents('install.sql') or error("Couldn't load install.sql.");
sql_open();
$mysql_version = mysql_version();
// This code is probably horrible, but what I'm trying
// to do is find all of the SQL queires and put them
@ -952,8 +948,6 @@ if ($step == 0) {
$sql_errors = '';
$sql_err_count = 0;
foreach ($queries as $query) {
if ($mysql_version < 50503)
$query = preg_replace('/(CHARSET=|CHARACTER SET )utf8mb4/', '$1utf8', $query);
$query = preg_replace('/^([\w\s]*)`([0-9a-zA-Z$_\x{0080}-\x{FFFF}]+)`/u', '$1``$2``', $query);
if (!query($query)) {
$sql_err_count++;

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

@ -1,3 +1,9 @@
/**
* Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/jquery.mixitup.min.js';
* $config['additional_javascript'][] = 'js/catalog.js';
*/
if (active_page == 'catalog') $(function(){
if (localStorage.catalog !== undefined) {
var catalog = JSON.parse(localStorage.catalog);

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.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

@ -21,4 +21,4 @@ if (!isset($_GET['page'])) {
$page = (int)$_GET['page'];
};
mod_board_log($board['uri'], $page, $hide_names, true);
mod_board_log(Vichan\build_context($config), $board['uri'], $page, $hide_names, true);

48
mod.php
View file

@ -1,18 +1,21 @@
<?php
/*
* Copyright (c) 2010-2014 Tinyboard Development Group
*/
require_once 'inc/bootstrap.php';
if ($config['debug'])
if ($config['debug']) {
$parse_start_time = microtime(true);
}
require_once 'inc/bans.php';
require_once 'inc/mod/pages.php';
check_login(true);
$ctx = Vichan\build_context($config);
check_login($ctx, true);
$query = isset($_SERVER['QUERY_STRING']) ? rawurldecode($_SERVER['QUERY_STRING']) : '';
@ -22,7 +25,7 @@ if(isset($_GET['thread'])) {
$query = explode("&thread=", $query)[0];
}
$pages = array(
$pages = [
'' => ':?/', // redirect to dashboard
'/' => 'dashboard', // dashboard
'/confirm/(.+)' => 'confirm', // confirm action (if javascript didn't work)
@ -65,8 +68,15 @@ $pages = array(
'/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.:]+)/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
@ -106,7 +116,6 @@ $pages = array(
// these pages aren't listed in the dashboard without $config['debug']
'/debug/antispam' => 'debug_antispam',
'/debug/recent' => 'debug_recent_posts',
'/debug/apc' => 'debug_apc',
'/debug/sql' => 'secure_POST debug_sql',
// This should always be at the end:
@ -120,14 +129,14 @@ $pages = array(
str_replace('%d', '(\d+)', preg_quote($config['file_page'], '!')) => 'view_thread',
'/(\%b)/' . preg_quote($config['dir']['res'], '!') .
str_replace(array('%d','%s'), array('(\d+)', '[a-z0-9-]+'), preg_quote($config['file_page50_slug'], '!')) => 'view_thread50',
str_replace([ '%d','%s' ], [ '(\d+)', '[a-z0-9-]+' ], preg_quote($config['file_page50_slug'], '!')) => 'view_thread50',
'/(\%b)/' . preg_quote($config['dir']['res'], '!') .
str_replace(array('%d','%s'), array('(\d+)', '[a-z0-9-]+'), preg_quote($config['file_page_slug'], '!')) => 'view_thread',
);
str_replace([ '%d','%s' ], [ '(\d+)', '[a-z0-9-]+' ], preg_quote($config['file_page_slug'], '!')) => 'view_thread',
];
if (!$mod) {
$pages = array('!^(.+)?$!' => 'login');
$pages = [ '!^(.+)?$!' => 'login' ];
} elseif (isset($_GET['status'], $_GET['r'])) {
header('Location: ' . $_GET['r'], true, (int)$_GET['status']);
exit;
@ -137,12 +146,11 @@ if (isset($config['mod']['custom_pages'])) {
$pages = array_merge($pages, $config['mod']['custom_pages']);
}
$new_pages = array();
$new_pages = [];
foreach ($pages as $key => $callback) {
if (is_string($callback) && preg_match('/^secure /', $callback)) {
$key .= '(/(?P<token>[a-f0-9]{8}))?';
}
}
$key = str_replace('\%b', '?P<board>' . sprintf(substr($config['board_path'], 0, -1), $config['board_regex']), $key);
$new_pages[@$key[0] == '!' ? $key : '!^' . $key . '(?:&[^&=]+=[^&]*)*$!u'] = $callback;
}
@ -150,7 +158,7 @@ $pages = $new_pages;
foreach ($pages as $uri => $handler) {
if (preg_match($uri, $query, $matches)) {
$matches = array_slice($matches, 1);
$matches[0] = $ctx; // Replace the text captured by the full pattern with a reference to the context.
if (isset($matches['board'])) {
$board_match = $matches['board'];
@ -170,7 +178,7 @@ foreach ($pages as $uri => $handler) {
if ($secure_post_only)
error($config['error']['csrf']);
else {
mod_confirm(substr($query, 1));
mod_confirm($ctx, substr($query, 1));
exit;
}
}
@ -185,24 +193,20 @@ foreach ($pages as $uri => $handler) {
}
if ($config['debug']) {
$debug['mod_page'] = array(
$debug['mod_page'] = [
'req' => $query,
'match' => $uri,
'handler' => $handler,
);
];
$debug['time']['parse_mod_req'] = '~' . round((microtime(true) - $parse_start_time) * 1000, 2) . 'ms';
}
if (is_array($matches)) {
// we don't want to call named parameters (PHP 8)
$matches = array_values($matches);
}
// We don't want to call named parameters (PHP 8).
$matches = array_values($matches);
if (is_string($handler)) {
if ($handler[0] == ':') {
header('Location: ' . substr($handler, 1), true, $config['redirect_http']);
} elseif (is_callable("mod_page_$handler")) {
call_user_func_array("mod_page_$handler", $matches);
} elseif (is_callable("mod_$handler")) {
call_user_func_array("mod_$handler", $matches);
} else {

428
post.php
View file

@ -3,6 +3,10 @@
* Copyright (c) 2010-2014 Tinyboard Development Group
*/
use Vichan\Context;
use Vichan\Data\ReportQueries;
use Vichan\Data\Driver\LogDriver;
require_once 'inc/bootstrap.php';
/**
@ -35,35 +39,6 @@ function md5_hash_of_file($config, $file)
}
}
/**
* Strip the markup from the given string
*
* @param string $post_body The body of the post.
* @return string
*/
function strip_markup($post_body)
{
if (mysql_version() >= 50503) {
// Assume we're using the utf8mb4 charset.
return $post_body;
} else {
// MySQL's `utf8` charset only supports up to 3-byte symbols.
// Remove anything >= 0x010000.
$chars = preg_split('//u', $post_body, -1, PREG_SPLIT_NO_EMPTY);
$res = '';
foreach ($chars as $char) {
$o = 0;
$ord = ordutf8($char, $o);
if ($ord >= 0x010000)
continue;
$res .= $char;
}
return $res;
}
}
/**
* Checks if the user at the remote ip passed the captcha.
*
@ -167,6 +142,126 @@ function check_turnstile($secret, $response, $remote_ip, $expected_action)
return $json_ret['success'] === true && $json_ret['action'] === $expected_action;
}
/**
* A "sophisticated" workaround to js/ajax.js calling post.php multiple times on error/ban.
*/
function check_captcha(array $captcha_config, string $form_id, string $board_uri, string $response, string $remote_ip, string $expected_action) {
$dynamic = $captcha_config['dynamic'];
if ($dynamic !== false && $remote_ip !== $dynamic) {
return true;
}
switch ($captcha_config['mode']) {
case 'recaptcha':
case 'hcaptcha':
case 'turnstile':
$mode = $captcha_config['mode'];
break;
case false:
return true;
default:
\error_log("Unknown captcha mode '{$captcha_config['mode']}'");
throw new \RuntimeException('Captcha configuration error');
}
$passthrough_timeout = $captcha_config['passthrough_timeout'];
if ($passthrough_timeout != 0) {
$pass = Cache::get("captcha_passthrough_{$remote_ip}_{$form_id}");
if ($pass !== false) {
$let_through = $pass['expires'] > time() && $pass['board_uri'] === $board_uri && $pass['captcha_response'] === $response;
if ($let_through) {
return true;
}
}
}
$remote_ip_send = $dynamic !== false ? null : $remote_ip;
$private_key = $captcha_config[$mode]['private'];
$public_key = $captcha_config[$mode]['public'];
switch ($mode) {
case 'recaptcha':
$ret = check_recaptcha($private_key, $response, $remote_ip_send);
break;
case 'hcaptcha':
$ret = check_hcaptcha($private_key, $response, $remote_ip_send, $public_key);
break;
case 'turnstile':
$ret = check_turnstile($private_key, $response, $remote_ip_send, $expected_action);
break;
}
if ($ret && $passthrough_timeout != 0) {
$pass = [
'expires' => time() + $passthrough_timeout,
'board_uri' => $board_uri,
'captcha_response' => $response
];
Cache::set("captcha_passthrough_{$remote_ip}_{$form_id}", $pass, $passthrough_timeout + 2);
}
return $ret;
}
function send_matrix_report(
string $matrix_host,
string $room_id,
string $access_token,
int $max_msg_len,
string $report_reason,
string $domain,
string $board_dir,
string $board_res_dir,
array $post,
int $id,
) {
$post_id = $post['thread'] ? $post['thread'] : $id;
$reported_post_url = "$domain/mod.php?/{$board_dir}{$board_res_dir}{$post_id}.html";
$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'] ? "#$id" : '') . " \nReason:\n" . $report_reason . " \nPost:\n" . $post_content;
$random_transaction_id = mt_rand();
$json_body = json_encode([
'msgtype' => 'm.text',
'body' => $text_body
]);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => "$matrix_host/_matrix/client/v3/rooms/$room_id/send/m.room.message/$random_transaction_id",
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"Authorization: Bearer $access_token"
],
CURLOPT_POSTFIELDS => $json_body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 3,
]);
$c_ret = curl_exec($ch);
if ($c_ret === false) {
$err_no = curl_errno($ch);
$err_str = curl_error($ch);
error_log("Failed to send report to matrix. Curl returned: $err_no ($err_str)");
return false;
}
curl_close($ch);
$json = json_decode($c_ret, true);
if ($json === null) {
error_log("Report forwarding failed, matrix returned a non-json value");
} elseif (!isset($json["event_id"])) {
$code = $json["errcode"] ?? '';
$desc = $json["error"] ?? '';
error_log("Report forwarding failed, matrix returned code '$code', with description '$desc'");
}
}
/**
* Deletes the (single) captcha associated with the ip and code.
*
@ -225,26 +320,6 @@ function db_select_post_minimal($board, $id)
return $post;
}
/**
* Inserts a new report.
*
* @param string $ip Ip of the user sending the report.
* @param string $board Board of the reported thread. MUST ALREADY BE SANITIZED.
* @param int $post_id Post reported.
* @param string $reason Reason of the report.
* @return void
*/
function db_insert_report($ip, $board, $post_id, $reason)
{
$query = prepare("INSERT INTO ``reports`` VALUES (NULL, :time, :ip, :board, :post, :reason)");
$query->bindValue(':time', time(), PDO::PARAM_INT);
$query->bindValue(':ip', $ip, PDO::PARAM_STR);
$query->bindValue(':board', $board, PDO::PARAM_STR);
$query->bindValue(':post', $post_id, PDO::PARAM_INT);
$query->bindValue(':reason', $reason, PDO::PARAM_STR);
$query->execute() or error(db_error($query));
}
/**
* Inserts a new ban appeal into the database.
*
@ -280,14 +355,14 @@ function db_select_ban_appeals($ban_id)
$dropped_post = false;
function handle_nntpchan()
function handle_nntpchan(Context $ctx)
{
global $config;
if ($_SERVER['REMOTE_ADDR'] != $config['nntpchan']['trusted_peer']) {
error("NNTPChan: Forbidden. $_SERVER[REMOTE_ADDR] is not a trusted peer");
}
$_POST = array();
$_POST = [];
$_POST['json_response'] = true;
$headers = json_encode($_GET);
@ -359,11 +434,11 @@ function handle_nntpchan()
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
$ctx->get(LogDriver::class)->log(LogDriver::DEBUG, 'MM: Files: ' . print_r($GLOBALS, true));
$content = '';
$newfiles = array();
$newfiles = [];
foreach ($_FILES['attachment']['error'] as $id => $error) {
if ($_FILES['attachment']['type'][$id] == 'text/plain') {
$content .= file_get_contents($_FILES['attachment']['tmp_name'][$id]);
@ -371,7 +446,7 @@ function handle_nntpchan()
// Signed message, ignore for now
} else {
// A real attachment :^)
$file = array();
$file = [];
$file['name'] = $_FILES['attachment']['name'][$id];
$file['type'] = $_FILES['attachment']['type'][$id];
$file['size'] = $_FILES['attachment']['size'][$id];
@ -415,7 +490,7 @@ function handle_nntpchan()
if (count($ary) == 0) {
return ">>>>$id";
} else {
$ret = array();
$ret = [];
foreach ($ary as $v) {
if ($v['board'] != $xboard) {
$ret[] = ">>>/" . $v['board'] . "/" . $v['id'];
@ -438,7 +513,7 @@ function handle_nntpchan()
);
}
function handle_delete()
function handle_delete(Context $ctx)
{
// Delete
global $config, $board, $mod;
@ -446,7 +521,7 @@ function handle_delete()
error($config['error']['bot']);
}
check_login(false);
check_login($ctx, false);
$is_mod = !!$mod;
if (isset($_POST['mod']) && $_POST['mod'] && !$mod) {
@ -456,11 +531,13 @@ function handle_delete()
$password = &$_POST['password'];
if ($password == '') {
if (empty($password)) {
error($config['error']['invalidpassword']);
}
$delete = array();
$password = hashPassword($_POST['password']);
$delete = [];
foreach ($_POST as $post => $value) {
if (preg_match('/^delete_(\d+)$/', $post, $m)) {
$delete[] = (int) $m[1];
@ -534,8 +611,8 @@ function handle_delete()
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 : '')
);
@ -562,13 +639,13 @@ function handle_delete()
rebuildThemes('post-delete', $board['uri']);
}
function handle_report()
function handle_report(Context $ctx)
{
global $config, $board;
if (!isset($_POST['board'], $_POST['reason']))
error($config['error']['bot']);
$report = array();
$report = [];
foreach ($_POST as $post => $value) {
if (preg_match('/^delete_(\d+)$/', $post, $m)) {
$report[] = (int) $m[1];
@ -618,12 +695,12 @@ function handle_report()
$reason = escape_markup_modifiers($_POST['reason']);
markup($reason);
$report_queries = $ctx->get(ReportQueries::class);
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']);
}
@ -638,15 +715,14 @@ function handle_report()
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 . '"'
);
db_insert_report($_SERVER['REMOTE_ADDR'], $board['uri'], $id, $reason);
$report_queries->add($_SERVER['REMOTE_ADDR'], $board['uri'], $id, $reason);
if ($config['slack']) {
function slack($message, $room = "reports", $icon = ":no_entry_sign:")
@ -680,25 +756,19 @@ function handle_report()
}
if (isset($config['matrix'])) {
$reported_post_url = $config['domain'] . "/mod.php?/" . $board['dir'] . $config['dir']['res'] . ($post['thread'] ? $post['thread'] : $id) . ".html";
$post_url = $config['matrix']['host'] . "/_matrix/client/r0/rooms/" . $config['matrix']['room_id'] . "/send/m.room.message?access_token=" . $config['matrix']['access_token'];
$trimmed_post = strlen($post['body_nomarkup']) > $config['matrix']['max_message_length'] ? ' [...]' : '';
$postcontent = mb_substr($post['body_nomarkup'], 0, $config['matrix']['max_message_length']) . $trimmed_post;
$matrix_message = $reported_post_url . ($post['thread'] ? '#' . $id : '') . " \nReason:\n" . $reason . " \nPost:\n" . $postcontent . " \n";
$post_data = json_encode(
array(
"msgtype" => "m.text",
"body" => $matrix_message
)
if ($config['matrix']['enabled']) {
send_matrix_report(
$config['matrix']['host'],
$config['matrix']['room_id'],
$config['matrix']['access_token'],
$config['matrix']['max_message_length'],
$reason,
$config['domain'],
$board['dir'],
$config['dir']['res'],
$post,
$id
);
$ch = curl_init($post_url);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$postResult = curl_exec($ch);
curl_close($ch);
}
}
@ -717,7 +787,7 @@ function handle_report()
}
}
function handle_post()
function handle_post(Context $ctx)
{
global $config, $dropped_post, $board, $mod, $pdo;
@ -725,7 +795,7 @@ function handle_post()
error($config['error']['bot']);
}
$post = array('board' => $_POST['board'], 'files' => array());
$post = array('board' => $_POST['board'], 'files' => []);
// Check if board exists
if (!openBoard($post['board'])) {
@ -765,56 +835,16 @@ function handle_post()
if (!$dropped_post) {
if ($config['dynamic_captcha'] !== false) {
if ($_SERVER['REMOTE_ADDR'] === $config['dynamic_captcha']) {
if ($config['recaptcha']) {
if (!isset($_POST['g-recaptcha-response'])) {
error($config['error']['bot']);
}
if (!check_recaptcha($config['recaptcha_private'], $_POST['g-recaptcha-response'], null)) {
error($config['error']['captcha']);
}
} elseif ($config['hcaptcha']) {
if (!isset($_POST['h-captcha-response'])) {
error($config['error']['bot']);
}
if (!check_hcaptcha($config['hcaptcha_private'], $_POST['h-captcha-response'], null, $config['hcaptcha_public'])) {
error($config['error']['captcha']);
}
} elseif ($config['turnstile']) {
if (!isset($_POST['cf-turnstile-response'])) {
error($config['error']['bot']);
}
$expected_action = $post['op'] ? 'post-thread' : 'post-reply';
if (!check_turnstile($config['turnstile_private'], $_POST['cf-turnstile-response'], null, $expected_action)) {
error($config['error']['captcha']);
}
}
// 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']);
}
} else {
// Check for CAPTCHA right after opening the board so the "return" link is in there.
if ($config['recaptcha']) {
if (!isset($_POST['g-recaptcha-response'])) {
error($config['error']['bot']);
}
if (!check_recaptcha($config['recaptcha_private'], $_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR'])) {
error($config['error']['captcha']);
}
} elseif ($config['hcaptcha']) {
if (!isset($_POST['h-captcha-response'])) {
error($config['error']['bot']);
}
if (!check_hcaptcha($config['hcaptcha_private'], $_POST['h-captcha-response'], $_SERVER['REMOTE_ADDR'], $config['hcaptcha_public'])) {
error($config['error']['captcha']);
}
} elseif ($config['turnstile']) {
if (!isset($_POST['cf-turnstile-response'])) {
error($config['error']['bot']);
}
$expected_action = $post['op'] ? 'post-thread' : 'post-reply';
if (!check_turnstile($config['turnstile_private'], $_POST['cf-turnstile-response'], null, $expected_action)) {
error($config['error']['captcha']);
}
$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']);
}
}
@ -858,8 +888,15 @@ function handle_post()
// 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(false);
check_login($ctx, false);
if (!$mod) {
// Liar. You're not a mod >:-[
error($config['error']['notamod']);
@ -972,11 +1009,16 @@ function handle_post()
}
}
// 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) {
@ -1150,14 +1192,22 @@ function handle_post()
if (mb_strlen($post['subject']) > 100) {
error(sprintf($config['error']['toolong'], 'subject'));
}
if (!$mod && mb_strlen($post['body']) > $config['max_body']) {
error($config['error']['toolong_body']);
}
if (!$mod && mb_strlen($post['body']) > 0 && (mb_strlen($post['body']) < $config['min_body'])) {
error($config['error']['tooshort_body']);
}
if (mb_strlen($post['password']) > 20) {
error(sprintf($config['error']['toolong'], 'password'));
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 ($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']);
}
}
}
@ -1222,7 +1272,7 @@ function handle_post()
}
}
$post['body_nomarkup'] = strip_markup($post['body']);
$post['body_nomarkup'] = $post['body'];
$post['tracked_cites'] = markup($post['body'], true);
if ($post['has_file']) {
@ -1263,7 +1313,7 @@ function handle_post()
if (!hasPermission($config['mod']['bypass_filters'], $board['uri']) && !$dropped_post) {
require_once 'inc/filters.php';
do_filters($post);
do_filters($ctx, $post);
}
if ($post['has_file']) {
@ -1352,7 +1402,7 @@ function handle_post()
$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'])
@ -1432,7 +1482,7 @@ function handle_post()
// getting all images and then choosing one.
for( $i = 0; $i < $zip->numFiles; $i++ ){
$stat = $zip->statIndex( $i );
$matches = array();
$matches = [];
if (preg_match('/.*cover.*\.(jpg|jpeg|png)/', $stat['name'], $matches)) {
$filename = $matches[0];
break;
@ -1501,35 +1551,6 @@ function handle_post()
}
}
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'])) {
@ -1577,11 +1598,6 @@ function handle_post()
}
}
// 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) {
undoImage($post);
if ($config['robot_mute']) {
@ -1623,10 +1639,6 @@ function handle_post()
$post = (array) $post;
if ($post['files']) {
$post['files'] = $post['files'];
}
$post['num_files'] = sizeof($post['files']);
$post['id'] = $id = post($post);
@ -1683,7 +1695,7 @@ function handle_post()
}
if (isset($post['tracked_cites']) && !empty($post['tracked_cites'])) {
$insert_rows = array();
$insert_rows = [];
foreach ($post['tracked_cites'] as $cite) {
$insert_rows[] = '(' .
$pdo->quote($board['uri']) . ', ' . (int) $id . ', ' .
@ -1701,7 +1713,7 @@ function handle_post()
if (isset($_COOKIE[$config['cookies']['js']])) {
$js = json_decode($_COOKIE[$config['cookies']['js']]);
} else {
$js = (object) array();
$js = (object) [];
}
// Tell it to delete the cached post for referer
$js->{$_SERVER['HTTP_REFERER']} = true;
@ -1735,10 +1747,10 @@ function handle_post()
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 . '"');
@ -1787,7 +1799,7 @@ function handle_post()
}
}
function handle_appeal()
function handle_appeal(Context $ctx)
{
global $config;
if (!isset($_POST['ban_id'])) {
@ -1831,23 +1843,25 @@ function handle_appeal()
displayBan($ban);
}
$ctx = Vichan\build_context($config);
// 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();
handle_nntpchan($ctx);
} else {
error("NNTPChan: NNTPChan support is disabled");
}
}
if (isset($_POST['delete'])) {
handle_delete();
handle_delete($ctx);
} elseif (isset($_POST['report'])) {
handle_report();
handle_report($ctx);
} elseif (isset($_POST['post']) || $dropped_post) {
handle_post();
handle_post($ctx);
} elseif (isset($_POST['appeal'])) {
handle_appeal();
handle_appeal($ctx);
} else {
if (!file_exists($config['has_installed'])) {
header('Location: install.php', true, $config['redirect_http']);

8
spooks.php Normal file
View file

@ -0,0 +1,8 @@
<?php
$files = scandir(__dir__ . '/static/spooks/', SCANDIR_SORT_NONE);
$files = array_diff($files, ['.', '..']);
$filename = $files[array_rand($files)];
header("Location: /static/spooks/$filename", true, 307);
header('Cache-Control: no-cache');

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
static/flags/alunya.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

BIN
static/flags/libsoc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

BIN
static/spooks/2987.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

BIN
static/spooks/3043.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
static/spooks/3605.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
static/spooks/3870.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
static/spooks/3951.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
static/spooks/4188.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

BIN
static/spooks/6899.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 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;

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,7 +188,7 @@ 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 {
@ -204,7 +211,7 @@ div.report {
}
.box {
background: #333333;
border-color: #555555;
border-color: #4f4f4f;
color: #C5C8C6;
border-radius: 10px;
}
@ -214,7 +221,7 @@ div.report {
}
table thead th {
background: #333333;
border-color: #555555;
border-color: #4f4f4f;
color: #C5C8C6;
border-radius: 4px;
}
@ -222,11 +229,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;

294
stylesheets/dark_spook.css Normal file
View file

@ -0,0 +1,294 @@
/**
* Based on dark.css, with a teal extension.
* Clumps all rules into three rules to determine the new accent color
*/
body {
background: #1E1E1E;
color: #999999;
font-family: Verdana, sans-serif;
font-size: 14px;
}
@font-face {
font-family: 'lain';
src: url('./fonts/nrdyyh.woff') format('woff'),
url('./fonts/tojcxo.TTF') format('truetype');
}
h1 {
letter-spacing: -2px;
font-size: 20pt;
text-align: center;
}
div.title p {
font-size: 10px;
}
a:link, a:visited, .intro a.email span.name {
color: #CCCCCC;
text-decoration: none;
font-family: sans-serif;
}
a:visited:hover {
color: #fff;
font-family: sans-serif;
text-decoration: none;
}
a.post_no {
color: #AAAAAA;
text-decoration: none;
}
a.post_no:hover {
color: #32DD72 !important;
text-decoration: underline overline;
}
div.post.reply {
background: #333333;
border: #555555 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
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;
}
.intro span.subject {
font-size: 12px;
font-family: sans-serif;
color: #446655;
font-weight: 800;
}
.intro span.name {
color: #32DD72;
font-weight: 800;
}
.intro a.capcode, p.intro a.nametag {
color: magenta;
margin-left: 0;
}
.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 {
background: #333333;
color: #CCCCCC;
border: #666666 1px solid;
padding-left: 5px;
padding-right: -5px;
font-family: sans-serif;
font-size: 10pt;
}
input[type="password"] {
background: #333333;
color: #CCCCCC;
border: #666666 1px solid;
}
form table tr th {
background: #333333;
color: #AAAAAA;
font-weight: 800;
text-align: left;
padding: 0;
}
div.banner {
background: #32DD72;
color: #000;
text-align: center;
width: 250px;
padding: 4px;
padding-left: 12px;
padding-right: 12px;
margin-left: auto;
margin-right: auto;
font-size: 12px;
}
div.banner a {
color:#000;
}
input[type="submit"] {
background: #333333;
border: #888888 1px solid;
color: #CCCCCC;
}
input[type="submit"]:hover {
background: #555555;
border: #888888 1px solid;
color: #32DD72;
}
input[type="text"]:focus {
border:#aaa 1px solid;
}
p.fileinfo a:hover {
text-decoration: underline;
}
span.trip {
color: #AAAAAA;
}
div.pages {
background: #1E1E1E;
font-family: sans-serif;
}
.bar.bottom {
bottom: 0px;
border-top: 1px solid #333333;
background-color: #1E1E1E;
}
div.pages a.selected {
color: #CCCCCC;
}
hr {
height: 1px;
border: #333333 1px solid;
}
div.boardlist {
text-align: center;
color: #999999;
}
div.ban {
background-color: transparent;
border: transparent 0px solid;
}
div.ban h2 {
background: transparent;
color: lime;
font-size: 12px;
}
table.modlog tr th {
background: #333333;
color: #AAAAAA;
}
div.boardlist:not(.bottom) {
background-color: #1E1E1E;
}
.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: #999999;
background-color: #1E1E1E;
}
div.report {
color: #666666;
}
/* options.js */
#options_div, #alert_div {
background: #333333;
}
.options_tab_icon {
color: #AAAAAA;
}
.options_tab_icon.active {
color: #FFFFFF;
}
#quick-reply table {
background: none repeat scroll 0% 0% #333 !important;
}
.modlog tr:nth-child(even), .modlog th {
background-color: #282A2E;
}
.box {
background: #333333;
border-color: #555555;
color: #C5C8C6;
border-radius: 10px;
}
.box-title {
background: transparent;
color: #32DD72;
}
table thead th {
background: #333333;
border-color: #555555;
color: #C5C8C6;
border-radius: 4px;
}
table tbody tr:nth-of-type( even ) {
background-color: #333333;
}
table.board-list-table .board-uri .board-sfw {
color: #CCCCCC;
}
tbody.board-list-omitted td {
background: #333333;
border-color: #555555;
}
table.board-list-table .board-tags .board-cell:hover {
background: #1e1e1e;
}
table.board-list-table tr:nth-of-type( even ) .board-tags .board-cell {
background: #333333;
}
.quote {
color:#3C827A;
}
div.blotter, h1, h2, header div.subtitle, div.title, a:link:hover, a:visited:hover p.intro a.post_no:hover,
div.post.reply div.body a:link:hover, div.post.reply div.body a:visited:hover, p.intro span.name,
p.intro a.email, p.intro a.email span.name, p.intro a.email:hover, p.intro a.email:hover span.name,
input[type="submit"]:hover, div.ban h2 {
color: #59938D;
}
p.intro span.subject, .intro span.capcode, p.intro a.capcode, p.intro a.nametag, span.heading {
color: #50C4B8;
}
#pagewrap{
background: url('/spooks.php') bottom 20px right 20px fixed no-repeat; background-size: 20%;
}

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;
}

162
stylesheets/gorby.css Normal file
View file

@ -0,0 +1,162 @@
/* based on jungle.css from brchan.org */
div.post.op {
margin-right: 20px;
margin-bottom: 5px;
background-image: url("img/pizza_pattern.png");
}
body {
background: #ffe;
background-image: url('img/pizza_pattern1.png'), url('img/pizza_pattern1.png');
background-repeat: repeat-x, repeat;
background-attachment: scroll, scroll;
color: #242B23;
font-family: serif;
font-size: 16px;
}
.bar {
border-color: #E5D959!important;
background-image: url('img/pizza_pattern.png');
box-shadow: none;
}
div.title h1 {
font-size: 24px;
}
div.title p {
font-size: 10px;
}
a:hover {
color: red !important;
}
a.post_no {
color: #800000;
}
desktop-style .bl-menu {
background-image: url('img/pizza_pattern1.png'), url('img/pizza_pattern1.png');
background-repeat: repeat-x, repeat;
background-attachment: scroll, scroll;
}
.boardlist .board a {
background: #65AB6B;
border: 1px solid #054500;
color: #054500;
font-weight: 600;
}
div.post.reply {
background-image: url('img/pizza_pattern.png');
border: 1px solid #E5D959;
border-left: none;
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 {
background-image: url('img/pizza_pattern1.png');
border: 1px solid #E5D959;
border-left: none;
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 {
color: #00E;
}
.intro span.subject {
color: #d00;
}
form table tr th {
background: #65AB6B;
border: 1px solid #054500;
font-weight: 600;
}
div.ban h2 {
background: #FCA;
color: inherit;
}
div.ban {
border-color: #800;
}
div.ban p {
color: black;
}
.topbar {
background-image: url('img/pizza_pattern.png');
}
div.pages {
padding: 7px 5px;
color: #054500;
font-size: 12pt;
background-image: url('img/pizza_pattern.png');
border-width: 1px;
border-style: inset;
}
div.pages a.selected {
color: #800;
}
hr {
border-width: 1px;
border-style: inset;
}
div.boardlist {
color: #52794F;
font-size: 11pt;
}
div.boardlist a {
color: #195319;
}
.post-hover {
border: solid 1px #265026 !important;
}
unimportant, .unimportant * {
font-size: 13px;
}
table.modlog tr th {
background: #EA8;
}
.intro span.name {
color: maroon;
font-weight: 600;
}
header div.subtitle, h1 {
color: #054500;
}
.desktop-style div.boardlist:nth-child(1) {
text-shadow: #fff 1px 1px 1px, #fff -1px -1px 1px;
}
.desktop-style div.boardlist:nth-child(1):hover, .desktop-style div.boardlist:nth-child(1).cb-menu {
background-color: rgba(90%, 90%, 90%, 0.55);
}

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
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View file

@ -5,143 +5,167 @@ body {
background-image: url('img/jungle_bg1.png'), url('img/jungle_bg.png');
background-repeat: repeat-x, repeat;
background-attachment: scroll, scroll;
background-attachment: scroll, scroll;
color: #242B23;
font-family: serif;
font-size: 16px;
}
.bar
{
.bar {
border-color: #E5D959!important;
background-image: url('img/jungle_td.png');
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
}
div.title h1 {
font-size: 24px;
}
div.title p {
font-size: 10px;
}
a:hover {
color: red !important;
color: red !important;
}
a.post_no {
color: #800000;
}
desktop-style .bl-menu{
desktop-style .bl-menu {
background-image: url('img/jungle_bg1.png'), url('img/jungle_bg.png');
background-repeat: repeat-x, repeat;
background-attachment: scroll, scroll;
}
.boardlist .board a {
background: #65AB6B;
border: 1px solid #054500;
color: #054500;
font-weight: 600;
}
.boardlist .board a {
background: #65AB6B;
border: 1px solid #054500;
color: #054500;
font-weight: 600;
}
div.post.reply {
background-image: url('img/jungle_td.png');
border: 1px solid #E5D959;
border-left: none;
border-top: none;
webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
-webkit-box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35);
-moz-box-shadow: 0px 2px 3px rgba(0,0,0,0.35);
-o-box-shadow: 0px 2px 3px rgba(0,0,0,0.35);
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35);
border-left: none;
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 {
background-image: url('img/jungle_td2.png');
border: 1px solid #E5D959;
border-left: none;
border-top: none;
webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
-webkit-box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35);
-moz-box-shadow: 0px 2px 3px rgba(0,0,0,0.35);
-o-box-shadow: 0px 2px 3px rgba(0,0,0,0.35);
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35);
border-left: none;
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 {
color: #00E;
}
.intro span.subject {
color: #d00;
}
span.orangeQuote {
color: #FF8C00;
text-shadow: 0.05em 0.05em orange;
}
.quote {
color: #789922;
text-shadow: 0.05em 0.05em green;
}
form table tr th {
background: #65AB6B;
border: 1px solid #054500;
font-weight: 600;
border: 1px solid #054500;
font-weight: 600;
}
div.ban h2 {
background: #FCA;
color: inherit;
}
div.ban {
border-color: #800;
}
div.ban p {
color: black;
}
.topbar {
background-image: url('img/jungle_td.png');
}
div.pages {
padding: 7px 5px;
color: #054500;
font-size: 12pt;
background-image: url('img/jungle_td.png');
border-width: 1px;
border-style: inset;
}
div.pages a.selected {
color: #800;
}
hr {
border-width: 1px;
border-style: inset;
}
div.boardlist {
color: #52794F;
font-size: 11pt;
}
div.boardlist a {
color: #195319;
}
.post-hover {
border: solid 1px #265026 !important;
border: solid 1px #265026 !important;
}
unimportant, .unimportant * {
font-size: 13px;
}
table.modlog tr th {
background: #EA8;
}
.intro span.name {
color: maroon;
font-weight: 600;
color: maroon;
font-weight: 600;
}
header div.subtitle, h1 {
color: #054500;
}
.desktop-style div.boardlist:nth-child(1) {
text-shadow: #fff 1px 1px 1px, #fff -1px -1px 1px;
color: #054500;
}
.desktop-style div.boardlist:nth-child(1) {
text-shadow: #fff 1px 1px 1px, #fff -1px -1px 1px;
}
.desktop-style div.boardlist:nth-child(1):hover, .desktop-style div.boardlist:nth-child(1).cb-menu {
background-color: rgba(90%, 90%, 90%, 0.55);
background-color: rgba(90%, 90%, 90%, 0.55);
}

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,11 @@ div.post.op > p {
}
div.post div.body {
margin-top: 0.8em;
margin-left: 1.4em;
padding-right: 3em;
padding-bottom: 0.3em;
}
div.post.op div.body {
margin-left: 0.8em;
}
div.post.reply div.body {
margin-left: 1.8em;
white-space: pre-wrap;
}
div.post.reply.highlighted {
@ -585,19 +583,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 +636,7 @@ span.trip {
span.omitted {
display: block;
margin-top: 1em;
margin-left: 0.4em;
}
br.clear {
@ -832,7 +821,7 @@ span.public_ban {
span.public_warning {
display: block;
color: steelblue;
color: orange;
font-weight: bold;
margin-top: 15px;
}
@ -923,10 +912,14 @@ form.ban-appeal textarea {
display:inline!important;
}
pre {
margin:0
margin: 0;
display: inline!important;
}
.theme-catalog .controls > span {
margin-right: 1em;
}
.theme-catalog div.thread img {
float: none!important;
margin: auto;
@ -936,13 +929,20 @@ pre {
border: 2px solid rgba(153,153,153,0);
}
/* Still for the catalog theme */
#Grid {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: center;
gap: 0.2em;
}
.theme-catalog div.thread {
display: inline-block;
vertical-align: top;
text-align: center;
font-weight: normal;
margin-top: 2px;
margin-bottom: 2px;
padding: 2px;
height: 300px;
width: 205px;
@ -961,7 +961,6 @@ pre {
.theme-catalog div.threads {
text-align: center;
margin-left: -20px;
}
.theme-catalog div.thread:hover {

View file

@ -48,6 +48,11 @@ div.post.reply.highlighted {
background: transparent;
border: transparent 1px dashed;
border-color:#00FF00;
@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: #00FF00;

View file

@ -69,6 +69,11 @@ div.post.reply {
div.post.reply.highlighted {
background: transparent;
border: transparent 1px dotted;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
p.intro span.subject {
font-size: 12px;

View file

@ -132,6 +132,11 @@ line-height: 1.4;
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;

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{% if config.hcaptcha %}
{% if config.captcha.mode == 'hcaptcha' %}
<script src="https://js.hcaptcha.com/1/api.js?recaptchacompat=off&render=explicit&onload=onCaptchaLoadHcaptcha" async defer></script>
{% endif %}
{% if config.turnstile %}
{% if config.captcha.mode == 'turnstile' %}
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=onCaptchaLoadTurnstile_{{ form_action_type }}" async defer></script>
{% endif %}

View file

@ -28,6 +28,6 @@
<script type="text/javascript" src="/js/mod/mod_snippets.js?v={{ config.resource_version }}"></script>
{% endif %}
{% endif %}
{% if config.recaptcha %}
{% if config.captcha.mode == 'recaptcha' %}
<script src="//www.google.com/recaptcha/api.js"></script>
{% endif %}

View file

@ -88,6 +88,9 @@
<label for="secure_trip_salt">Secure trip (##) salt:</label>
<input type="text" id="secure_trip_salt" name="secure_trip_salt" value="{{ config.secure_trip_salt }}" size="40">
<label for="secure_password_salt">Poster password salt:</label>
<input type="text" id="secure_password_salt" name="secure_password_salt" value="{{ config.secure_password_salt }}" size="40">
<label for="more">Additional configuration:</label>
<textarea id="more" name="more">{{ more }}</textarea>
</fieldset>

View file

@ -231,28 +231,6 @@ var resourceVersion = document.currentScript.getAttribute('data-resource-version
{% endif %}
{% raw %}
function initStyleChooser() {
var newElement = document.createElement('div');
newElement.className = 'styles';
for (styleName in styles) {
if (styleName) {
var 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);
}
function getCookie(cookie_name) {
let results = document.cookie.match('(^|;) ?' + cookie_name + '=([^;]*)(;|$)');
if (results) {
@ -265,26 +243,48 @@ function getCookie(cookie_name) {
{% endraw %}
/* BEGIN CAPTCHA REGION */
{% if config.hcaptcha or config.turnstile %} // If any captcha
{% if config.captcha.mode == 'hcaptcha' or config.captcha.mode == 'turnstile' %} // If any captcha
// Global captcha object. Assigned by `onCaptchaLoad()`.
var captcha_renderer = null;
// Captcha widget id of the post form.
var postCaptchaId = null;
{% if config.hcaptcha %} // If hcaptcha
{% if config.captcha.mode == 'hcaptcha' %} // If hcaptcha
function onCaptchaLoadHcaptcha() {
if (captcha_renderer === null && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) {
if ((captchaMode === 'static' || (captchaMode === 'dynamic' && isDynamicCaptchaEnabled()))
&& captcha_renderer === null
&& (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) {
let renderer = {
renderOn: (container) => hcaptcha.render(container, {
sitekey: "{{ config.hcaptcha_public }}",
/**
* @returns {object} Opaque widget id.
*/
applyOn: (container, params) => hcaptcha.render(container, {
sitekey: "{{ config.captcha.hcaptcha.public }}",
callback: params['on-success'],
}),
/**
* @returns {void}
*/
remove: (widgetId) => { /* Not supported */ },
reset: (widgetId) => hcaptcha.reset(widgetId)
/**
* @returns {void}
*/
reset: (widgetId) => hcaptcha.reset(widgetId),
/**
* @returns {bool}
*/
hasResponse: (widgetId) => !!hcaptcha.getResponse(widgetId),
/**
* @returns {void}
*/
execute: (widgetId) => hcaptcha.execute(widgetId)
};
onCaptchaLoad(renderer);
}
}
{% endif %} // End if hcaptcha
{% if config.turnstile %} // If turnstile
{% if config.captcha.mode == 'turnstile' %} // If turnstile
// Wrapper function to be called from thread.html
window.onCaptchaLoadTurnstile_post_reply = function() {
@ -298,20 +298,40 @@ window.onCaptchaLoadTurnstile_post_thread = function() {
// Should be called by the captcha API when it's ready. Ugly I know... D:
function onCaptchaLoadTurnstile(action) {
if (captcha_renderer === null && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) {
if ((captchaMode === 'static' || (captchaMode === 'dynamic' && isDynamicCaptchaEnabled()))
&& captcha_renderer === null
&& (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) {
let renderer = {
renderOn: function(container) {
/**
* @returns {object} Opaque widget id.
*/
applyOn: function(container, params) {
let widgetId = turnstile.render('#' + container, {
sitekey: "{{ config.turnstile_public }}",
sitekey: "{{ config.captcha.turnstile.public }}",
action: action,
callback: params['on-success'],
});
if (widgetId === undefined) {
return null;
}
return widgetId;
},
/**
* @returns {void}
*/
remove: (widgetId) => turnstile.remove(widgetId),
reset: (widgetId) => turnstile.reset(widgetId)
/**
* @returns {void}
*/
reset: (widgetId) => turnstile.reset(widgetId),
/**
* @returns {bool}
*/
hasResponse: (widgetId) => !!turnstile.getResponse(widgetId),
/**
* @returns {void}
*/
execute: (widgetId) => turnstile.execute(widgetId)
};
onCaptchaLoad(renderer);
@ -320,12 +340,20 @@ function onCaptchaLoadTurnstile(action) {
{% endif %} // End if turnstile
function onCaptchaLoad(renderer) {
// Initialize the form identifier with a random password.
document.getElementById('captcha-form-id').value = generatePassword();
captcha_renderer = renderer;
let widgetId = renderer.renderOn('captcha-container');
let widgetId = renderer.applyOn('captcha-container', {
'on-success': function(token) {
document.getElementById('captcha-response').value = token;
}
});
if (widgetId === null) {
console.error('Could not render captcha!');
}
postCaptchaId = widgetId;
document.addEventListener('afterdopost', function(e) {
// User posted! Reset the captcha.
renderer.reset(widgetId);
@ -333,6 +361,8 @@ function onCaptchaLoad(renderer) {
}
{% if config.dynamic_captcha %} // If dynamic captcha
var captchaMode = 'dynamic';
function isDynamicCaptchaEnabled() {
let cookie = getCookie('captcha-required');
return cookie === '1';
@ -346,8 +376,15 @@ function initCaptcha() {
}
}
}
{% else %}
var captchaMode = 'static';
{% endif %} // End if dynamic captcha
{% else %} // Else if any captcha
var captchaMode = 'none';
function isDynamicCaptchaEnabled() {
return false;
}
// No-op for `init()`.
function initCaptcha() {}
{% endif %} // End if any captcha
@ -403,6 +440,13 @@ function doPost(form) {
saved[document.location] = form.elements['body'].value;
sessionStorage.body = JSON.stringify(saved);
if (captchaMode === 'static' || (captchaMode === 'dynamic' && isDynamicCaptchaEnabled())) {
if (captcha_renderer && postCaptchaId && !captcha_renderer.hasResponse(postCaptchaId)) {
captcha_renderer.execute(postCaptchaId);
return false;
}
}
// Needs to be delayed by at least 1 frame, otherwise it may reset the form (read captcha) fields before they're sent.
setTimeout(() => document.dispatchEvent(new Event('afterdopost')));
return form.elements['body'].value != "" || (form.elements['file'] && form.elements['file'].value != "") || (form.elements.file_url && form.elements['file_url'].value != "");
@ -519,7 +563,6 @@ var script_settings = function(script_name) {
};
function init() {
initStyleChooser();
initCaptcha();
{% endraw %}

View file

@ -45,7 +45,7 @@ $(document).ready(function(){
<label for="reason">{% trans 'Reason' %}</label>
</th>
<td>
<textarea name="reason" id="reason" rows="5" cols="30">{{ reason|e }}</textarea>
<textarea name="reason" id="reason" rows="5" cols="30" autofocus>{{ reason|e }}</textarea>
</td>
</tr>
{% if post and board and not delete %}

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