Compare commits

...

303 Commits

Author SHA1 Message Date
Lorenzo Yario
83b644f0f5
Merge pull request #920 from vichan-devel/5.2.1
5.2.1 Release
2025-04-02 04:45:22 -05:00
Lorenzo Yario
953635f807
Delete static/banners/.gitkeep 2025-04-02 04:44:51 -05:00
Lorenzo Yario
5968118231
bugfix 2025-04-02 04:39:20 -05:00
Lorenzo Yario
03da3b8db0
bug fix 2025-04-02 04:27:38 -05:00
Lorenzo Yario
f27e5a6989
ensure premade ban reasons are passed 2025-04-02 04:04:08 -05:00
Lorenzo Yario
0866adc89f
styling for predefined ban reasons 2025-04-02 03:56:03 -05:00
Lorenzo Yario
5259f88262
Update config.php 2025-04-02 03:46:07 -05:00
Lorenzo Yario
87420966b2
kick doesn't seem all too bad anymore. Removed X/Bluesky test 2025-04-02 03:03:58 -05:00
Lorenzo Yario
8350645a70
we don't need kick it doesn't embed nicely 2025-04-02 02:52:44 -05:00
Lorenzo Yario
16a6112b5b
new embed stuff 2025-04-02 02:45:04 -05:00
Lorenzo Yario
9468c9d372
Add files via upload 2025-04-02 02:22:44 -05:00
Lorenzo Yario
8aa965b77f
Create .gitkeep 2025-04-02 02:22:26 -05:00
Lorenzo Yario
b08dbcff14
Delete static/banners 2025-04-02 02:22:15 -05:00
Lorenzo Yario
f90291faf0
Create banners 2025-04-02 02:21:31 -05:00
Lorenzo Yario
e67f3f94bf
Delete static/banners/defaultbanner.png 2025-04-02 02:20:16 -05:00
Lorenzo Yario
4244b0a86c
tidied slightly 2025-04-02 02:14:46 -05:00
Lorenzo Yario
ac3f69a7eb
jank? maybe but it fixes our current config.php 2025-04-02 02:12:50 -05:00
Lorenzo Yario
7bc1f67b4e
X 2025-04-02 00:34:45 -05:00
Lorenzo Yario
54f32bc75d
X 2025-03-30 21:54:28 -05:00
Lorenzo Yario
5f1598367c
Merge pull request #783 from perdedora/fix-poster-id
fix: wrong poster id for op in api
2025-03-16 02:59:08 -05:00
Weav
3987b2a25d
Merge branch 'master' into fix-poster-id 2025-03-15 17:18:22 +00:00
Lorenzo Yario
6ec0c17ac9
Update login.html #890 2025-02-08 22:28:53 -06:00
Lorenzo Yario
2c2d476cea
5.2.1 2025-02-08 22:27:21 -06:00
Lorenzo Yario
717795ddff
Update fileinfo.html #891 2025-02-08 22:26:17 -06:00
Lorenzo Yario
d1e57290e0
Update bug_report.yml 2025-02-02 23:07:01 -06:00
Lorenzo Yario
0c92315837
Create bug_report.yml 2025-02-02 23:03:47 -06:00
Lorenzo Yario
c026e85b3c
Delete .github/ISSUE_TEMPLATE/bug_report.md
yml
2025-02-02 23:03:35 -06:00
Lorenzo Yario
8d76087710
what did I break? 2025-02-02 23:02:41 -06:00
Lorenzo Yario
50d57a4464
Update bug_report.md 2025-02-02 23:01:48 -06:00
Lorenzo Yario
96099945b9
Update issue templates 2025-02-02 22:51:55 -06:00
Lorenzo Yario
8c6cf17a97
Delete .github/ISSUE_TEMPLATE/bug_report.md
failed
2025-02-02 22:43:03 -06:00
Lorenzo Yario
b3f0658594
Update issue templates 2025-02-02 22:42:08 -06:00
Lorenzo Yario
33efeda647
Merge pull request #886 from vichan-devel/RealAngeleno-patch-18
adding in perdedora's PR to the master branch
2025-02-02 22:24:36 -06:00
Lorenzo Yario
92096b43b8
adding in perdedora's PR to the master branch 2025-02-02 22:24:06 -06:00
Lorenzo Yario
f3cb2552ce
modify default captcha config to work out of the box 2025-01-03 00:25:31 -06:00
Lorenzo Yario
440e3126c9
Merge pull request #872 from vichan-devel/RealAngeleno-patch-17
modify themes to use numeric array #871
2024-12-30 17:20:08 -06:00
Lorenzo Yario
47cbbbc972
modify themes to use numeric array #871 2024-12-30 17:19:42 -06:00
Lorenzo Yario
74959aaed9
Merge pull request #868 from vichan-devel/RealAngeleno-captchafix
fix native captcha implementation
2024-12-26 22:30:48 -06:00
Lorenzo Yario
2d9e5009f8
fix typo 2024-12-26 22:28:21 -06:00
Lorenzo Yario
372784ecd2
Merge pull request #866 from vichan-devel/7.4-compatibility3
7.4 compatibility3
2024-12-23 12:18:35 -06:00
Lorenzo Yario
0f8a5fa926
continued 7.4 compatibility 2024-12-22 15:17:22 -06:00
Lorenzo Yario
0a870ebdb3
7.4 2024-12-22 15:05:16 -06:00
Lorenzo Yario
3b9b23035e
7.4 compatibility (updated) 2024-12-22 15:02:58 -06:00
Lorenzo Yario
40571f2001
7.4 compatibility 2024-12-22 14:59:12 -06:00
Lorenzo Yario
c4cd4d3c12
Update HttpDriver.php 2024-12-22 14:55:57 -06:00
Lorenzo Yario
00fc21322c
7.4 compatibility 2024-12-22 14:54:48 -06:00
Lorenzo Yario
6606c182b5
7.4 compatibility 2024-12-22 14:51:07 -06:00
Lorenzo Yario
58c2f095dc
7.4 compatibility 2024-12-22 14:49:27 -06:00
Lorenzo Yario
8cd4cae56e
Merge pull request #863 from vichan-devel/RealAngeleno-patch-16
continued php 7.4 compatibility
2024-12-22 14:44:20 -06:00
Lorenzo Yario
762a0edefd
continued 7.4 compatibility 2024-12-22 14:42:16 -06:00
Lorenzo Yario
c94ab113de
continued 7.4 backwards compatibility 2024-12-22 14:37:22 -06:00
Lorenzo Yario
32f0cb3a5f
Merge pull request #862 from vichan-devel/5.2.0-74compatibility
5.2.0 PHP 7.4 compatibility
2024-12-22 14:23:08 -06:00
Lorenzo Yario
2466a3d859
7.4 compatibility 2024-12-22 14:15:40 -06:00
Lorenzo Yario
f3f7c0c75c
7.4 compatibility 2024-12-22 14:13:37 -06:00
Lorenzo Yario
29ee5aeb1d
For 7.4 compatibility 2024-12-22 14:05:42 -06:00
Lorenzo Yario
6a22a51a72
Merge pull request #860 from vichan-devel/5.2.0-release
5.2.0 release
2024-12-22 11:11:31 -06:00
Lorenzo Yario
a9059fab66
Merge branch 'master' into 5.2.0-release 2024-12-20 17:40:59 -06:00
Lorenzo Yario
9d590eed2a
typo? 2024-12-20 15:08:24 -06:00
Lorenzo Yario
e7aa695775
2025 2024-12-20 15:04:30 -06:00
Lorenzo Yario
f419ae046c
replace captcha click me image 2024-12-20 14:59:46 -06:00
Lorenzo Yario
0ed3513e66
add youtu.be and voca.ro 2024-12-20 14:43:13 -06:00
Lorenzo Yario
a376a5a2e3
finish stripping current antispam system 2024-12-20 02:31:26 -06:00
Lorenzo Yario
b2ca26dba5
Merge pull request #858 from vichan-devel/removecurrentantibot
Remove current antibot system
2024-12-20 02:27:40 -06:00
Lorenzo Yario
6f5b0ae6d5
strip old antibot system 2024-12-20 02:24:18 -06:00
Lorenzo Yario
5f2f653993
Update functions.php 2024-12-20 02:16:59 -06:00
Lorenzo Yario
0ff0e707d6
remove vichan's current antibot system 2024-12-20 01:56:57 -06:00
Lorenzo Yario
26ffd1aa72
remove anti-bot functions 2024-12-20 01:08:41 -06:00
Lorenzo Yario
2e91c1ed3d
Update FileLogDriver.php 2024-12-20 00:24:46 -06:00
Lorenzo Yario
efd4810e83
Merge pull request #856 from vichan-devel/RealAngeleno-patch-15
skip captcha or log settings in mod.php config
2024-12-16 23:16:18 -06:00
Lorenzo Yario
9dacdf59b1
skip captcha or log settings in mod.php config
relates to multidimensional arrays
2024-12-16 23:15:23 -06:00
Lorenzo Yario
b889b10626
Merge pull request #834 from Zankaria/patch-2
Fix #830 remove multiple return types for PHP 7.4 support.
2024-10-31 13:52:09 -07:00
Zankaria
b5a9dc4d1a
bans.php: remove multiple return types for PHP 7.4 support 2024-10-31 14:47:11 +01:00
Lorenzo Yario
d6677bb90c
Merge pull request #825 from Zankaria/context-fix
context.php: fix missing include file
2024-10-20 12:03:51 -07:00
Lorenzo Yario
c88acfc4f0
Merge pull request #826 from vichan-devel/RealAngeleno-patch-13
minor bugfix relating to auth when changing your own username
2024-10-20 12:03:42 -07:00
Lorenzo Yario
58f7302936
minor bugfix relating to auth when changing your own username 2024-10-20 12:03:15 -07:00
Zankaria
76f6c721e9 context.php: fix missing include file 2024-10-16 22:49:19 +02:00
Lorenzo Yario
c1307feeb5
Merge pull request #818 from Zankaria/dep-inj-cache-wrap
Dependency injected cache
2024-10-15 20:40:34 -07:00
Lorenzo Yario
e65bfa87c4
Merge pull request #824 from Zankaria/inline-dice
Inline dices via markup
2024-10-15 20:40:21 -07:00
Zankaria
b67ff982e2 composer: add dice.php to autoload 2024-10-15 21:14:47 +02:00
Zankaria
fba88643ec config.php: add inline dice rolling markup support to the default configuration 2024-10-15 21:14:47 +02:00
Zankaria
ad1d56d092 dice.php: handle inline dice rolling markup 2024-10-15 21:14:44 +02:00
Zankaria
ceccbfc5b7 config.php: limit the number of dicerolls 2024-10-14 17:46:29 +02:00
Zankaria
b8c53fbbcd dice.php: extract email dice function from functions.php 2024-10-14 12:31:36 +02:00
Zankaria
27e4bd833a config.php: use op-cache friend array syntax for markup config 2024-10-14 12:18:04 +02:00
Zankaria
fe7a667441 style.css: add diceroll styling 2024-10-14 12:13:21 +02:00
Zankaria
115f28807a FsCacheDriver.php: collect expired cache items before operating on the cache 2024-10-10 23:10:05 +02:00
Zankaria
003e8f6d3b maintenance.php: delete expired filesystem cache 2024-10-10 23:10:05 +02:00
Zankaria
243e4894fa Use CacheDriver and Context for mod.php and mod pages 2024-10-10 23:10:05 +02:00
Zankaria
589435b667 context.php: use shared cache driver 2024-10-10 23:10:05 +02:00
Zankaria
f138b4b887 cache.php: wrap new cache drivers 2024-10-10 23:10:05 +02:00
Zankaria
ace2f2e83b driver: break up cache drivers 2024-10-10 23:10:05 +02:00
Zankaria
3d406aeab2 cache-driver.php: move to Data 2024-10-10 23:10:05 +02:00
Zankaria
66d2f90171 cache-driver.php: filesystem handle expired values. 2024-10-10 23:09:55 +02:00
Zankaria
5ea42fa0e2 config.php: update cache documentation 2024-10-10 22:56:56 +02:00
Zankaria
b57d9bfbb3 cache: implement cache locking for filesystem cache and give it multiprocess support 2024-10-10 22:56:56 +02:00
Zankaria
82ea1815fd Refactor cache driver 2024-10-10 22:56:56 +02:00
Lorenzo Yario
7c305f58bf
Merge pull request #822 from Zankaria/fix-captcha-call
post.php: fix invocation of native captcha
2024-10-08 20:12:25 -07:00
Lorenzo Yario
59d0dd9083
Merge pull request #821 from Zankaria/refactor-log
Break up the log subsystem and move it into Data
2024-10-08 20:12:13 -07:00
Lorenzo Yario
f60b4d190f
Merge pull request #789 from perdedora/expand-filename
Original filename changes
2024-10-08 20:11:49 -07:00
Lorenzo Yario
88a81a6d74
Merge pull request #819 from Zankaria/minor-format
Assorted PHP refactoring
2024-10-08 20:11:41 -07:00
Zankaria
3fe44653f2 post.php: fix invocation of native captcha 2024-10-06 13:08:29 +02:00
Zankaria
0e8909ac4e pages.php: fix whitespace indentation 2024-10-06 11:33:20 +02:00
Zankaria
767b8fd8c3 mod.php: add missing context parameter 2024-10-06 11:25:57 +02:00
Zankaria
f9127dd478 mod.php: $matches should always be an array 2024-10-06 11:25:11 +02:00
Zankaria
6c6ec65b02 pages.php: extract $mod array 2024-10-06 11:23:27 +02:00
Zankaria
23f3d15a52 extract create_pm_header 2024-10-06 10:41:32 +02:00
Zankaria
d88d6c814a docker: use shorter name for instance 2024-10-04 20:16:06 +02:00
Zankaria
65a668d3a8 composer.json: use classmap for LogDriver 2024-10-04 20:16:06 +02:00
Zankaria
b501852ea4 config.php: update LogDriver configuration 2024-10-04 20:16:06 +02:00
Zankaria
927a837216 post.php: update LogDriver 2024-10-04 20:16:06 +02:00
Zankaria
79af4b34dd context.php: update LogDriver 2024-10-04 20:16:06 +02:00
Zankaria
36d48951c1 log-driver.php: split up log driver 2024-10-04 20:16:04 +02:00
Zankaria
5dab17e5f4 log-driver.php: move to Data 2024-10-04 13:05:06 +02:00
Lorenzo Yario
c97c61aeca
Merge pull request #820 from Zankaria/http-refactor
Move HttpDriver to Data
2024-10-04 00:00:06 -07:00
Zankaria
59551a2042 composer.json: rely on classmap for HttpDriver 2024-10-04 01:21:21 +02:00
Zankaria
1682272f75 http-driver.php: rename to HttpDriver 2024-10-04 01:13:15 +02:00
Zankaria
3eed312b6b http-driver.php: minor refactor 2024-10-04 01:13:15 +02:00
Zankaria
cae85a6a0c http-driver.php: move to Data 2024-10-04 01:13:12 +02:00
Zankaria
25b2b6bc6e display.php: trim 2024-10-02 22:19:44 +02:00
Lorenzo Yario
7377885de9
Merge pull request #815 from Zankaria/remove-extensions
style.css: remove deprecated extensions
2024-10-02 08:33:02 -07:00
Lorenzo Yario
f421e25e63
Merge pull request #816 from Zankaria/fix-dir-config
Assorted php fixes
2024-10-02 08:32:52 -07:00
Zankaria
3e9ad58e97 config.php: fix mod config array 2024-10-01 23:54:16 +02:00
Zankaria
f4ff39c876 post.php, mod.php: pass the context to check_login 2024-10-01 22:16:54 +02:00
Zankaria
e6133ef00f auth.php: passthrough the context 2024-10-01 22:16:54 +02:00
Zankaria
13ca053e06 config.php: fix default 'dir' configuration 2024-10-01 22:16:54 +02:00
Zankaria
44b31eff0b style.css: remove deprecated extensions 2024-10-01 21:13:36 +02:00
Lorenzo Yario
785643a3bd
Merge pull request #814 from Zankaria/patch-1
docker: use composer LTS
2024-10-01 09:38:02 -07:00
Zankaria
e70c087f5f
docker: use composer LTS 2024-10-01 15:01:03 +02:00
Lorenzo Yario
428a686f47
fix minor exploit with banning 2024-09-21 14:39:13 -07:00
Lorenzo Yario
c95877bdcb
Merge pull request #803 from Zankaria/more-busting
More cache busting with resource_version
2024-09-20 18:45:14 -07:00
Lorenzo Yario
380ae8c675
Merge pull request #811 from Zankaria/main-js-let
Minor JS update
2024-09-20 18:42:01 -07:00
Lorenzo Yario
2f37b3ce51
Merge pull request #812 from Zankaria/anti-bot-format
Format anti-bot.php
2024-09-20 18:41:48 -07:00
Zankaria
65008dec98 anti-bot.php: remove unused variable 2024-09-19 23:13:42 +02:00
Zankaria
0e8aeca4af anti-bot.php: format 2024-09-19 23:09:02 +02:00
Zankaria
bdd7090e75 quote-selection.js: format 2024-09-19 22:58:23 +02:00
Zankaria
fd309443ea js: drop IE support 2024-09-19 22:54:04 +02:00
Zankaria
4332b70363 quote-selection.js: trim 2024-09-19 22:51:31 +02:00
Zankaria
cb6d6f13dd main.js: use let 2024-09-19 22:51:01 +02:00
Zankaria
6b60f841d4 template: supply data-resource-version to main.js 2024-09-19 22:28:55 +02:00
Zankaria
3c5484a7c2 main.js: load styles with dyanamically provided resource version.
This is done because:
 - If the version is updated before the rebuild, someone might cache the old version in the meanwhile.
 - One could not rebuild javascript before updating the version. Now it's possible.
2024-09-19 22:28:52 +02:00
Zankaria
fcf5c3d73a templates: bust all css caches with resource_version 2024-09-19 22:01:51 +02:00
Zankaria
9fbc816205 templates: bust all js caches with resource_version 2024-09-19 22:01:51 +02:00
Lorenzo Yario
bd5c2c61b9
Merge pull request #810 from vichan-devel/revert-809-howwwwww
i jinxed the previous commit
2024-09-19 01:33:18 -07:00
Lorenzo Yario
c1788d0792
Revert "revert trimming and this somehow fixes the antibot issues??????" 2024-09-19 01:32:49 -07:00
Lorenzo Yario
ccfcd03c95
Merge pull request #809 from vichan-devel/howwwwww
revert trimming and this somehow fixes the antibot issues??????
2024-09-19 01:28:18 -07:00
Lorenzo Yario
e1a4ae5336
revert trimming and this somehow fixes the antibot issues??????
idk but it works somehow??????
2024-09-19 01:26:50 -07:00
Lorenzo Yario
1effe1648b
add some spaces 2024-09-17 23:28:21 -07:00
Lorenzo Yario
36737b77a8
made https flag make sense 2024-09-17 22:20:36 -07:00
Lorenzo Yario
a457b905bf
from master branch 2024-09-17 21:44:06 -07:00
Lorenzo Yario
3291dc27f9
Merge pull request #788 from perdedora/patch-747
fixes #747
2024-09-17 21:40:04 -07:00
Lorenzo Yario
8c27b5261c
lazy loading commit 2024-09-17 21:18:09 -07:00
Lorenzo Yario
16bb704154
lazy loading commit 2024-09-17 21:15:43 -07:00
Lorenzo Yario
1db5c788dd
lazy loading 2024-09-17 21:03:05 -07:00
Lorenzo Yario
2b3eae89f1
Merge pull request #805 from perdedora/fix-oversights
Fix oversights
2024-09-17 21:01:53 -07:00
Lorenzo Yario
ce844b9270
Merge pull request #807 from seisatsu/dev
Fix typo in post.php that breaks Tesseract OCR
2024-09-17 21:00:51 -07:00
seisatsu
43b926c41b Fix typo in post.php that breaks Tesseract OCR
Fixes a typo in post.php that causes an uninitialized variable "$value"
to be referenced instead of the correct variable "$txt". This caused all
attempts to use the Tesseract OCR feature to fail with an error.
2024-09-06 15:23:37 -05:00
fowr
eeb55133eb
add missing context file to composer autoload 2024-08-25 17:01:31 -03:00
fowr
cd5c57f717
fix oversights introduced in captcha; 2024-08-25 17:01:06 -03:00
Lorenzo Yario
1672646213
Merge pull request #802 from Zankaria/resouce-ver-fix
Fix a couple of things with PR #801
2024-08-24 22:16:39 -07:00
Zankaria
6ea8fd5bf3 config.php: add missing default configuration, remove db settings 2024-08-21 14:05:29 +02:00
Lorenzo Yario
bf32a24b96
Merge pull request #798 from Zankaria/link-fallback
Add a fallback if link() fails
2024-08-20 23:36:56 -07:00
Lorenzo Yario
187f16693c
Merge pull request #801 from perdedora/resource_version
Resource version
2024-08-20 23:36:38 -07:00
fowr
13b587cf62
add resource_version to avoid cache. 2024-08-20 21:27:22 -03:00
Zankaria
1191dfb193 pages.php: add copy fallback in case of already existing file when moving thread. 2024-08-18 01:06:45 +02:00
Lorenzo Yario
5ee48c5865
Merge pull request #793 from Zankaria/captcha-rework-vichan
Refactor (again) the captcha backend + add support for dynamic captchas
2024-08-16 21:43:31 -07:00
Lorenzo Yario
60135bbb89
Merge pull request #794 from Zankaria/fix-di
Forgot to convert a method call in #784
2024-08-16 21:43:24 -07:00
Lorenzo Yario
39876f3cc7
Merge pull request #795 from Zankaria/api-modern
Modernize (a bit) the Api class
2024-08-16 21:43:17 -07:00
Lorenzo Yario
84a3bedd18
Merge pull request #796 from Zankaria/mod-use-di
Add the context dependency injection container to the mod pages
2024-08-16 21:43:07 -07:00
Lorenzo Yario
90dead0394
Merge pull request #797 from Zankaria/function-themes
Split the theme functions from functions.php
2024-08-16 21:42:43 -07:00
fowr
4f68166870 api.php: using depedency injection instead of globals 2024-08-16 19:26:19 +02:00
Zankaria
d408ed0413 theme.php: rename functions to snake_case 2024-08-16 18:33:02 +02:00
Zankaria
453ae795f5 composer.json: autoload theme.php 2024-08-16 18:24:48 +02:00
Zankaria
b2df2ab2a5 theme.php: extract theme functions from functions.php 2024-08-16 18:24:26 +02:00
Zankaria
81aebef2f4 pages.php: use modern array syntax for new empty arrays 2024-08-15 17:21:41 +02:00
Zankaria
b64bac5eb8 pages.php: use the context to access the configuration array 2024-08-15 17:13:43 +02:00
Zankaria
524ae94624 mod.php: pass the context to the mod pages 2024-08-15 17:13:34 +02:00
Zankaria
19082aec56 mod.php: use modern array syntax 2024-08-15 17:13:27 +02:00
Zankaria
e640217a8f post.php: fix DI method call 2024-08-15 16:39:12 +02:00
Zankaria
165ea5308a config.php: ops, wrong name for the native captcha 2024-08-15 16:33:56 +02:00
Zankaria
f7073d5d7e post.php: do not verify the poster IP if the captcha is dynamic 2024-08-15 16:22:58 +02:00
Zankaria
cb5fb68c5e header.html: format 2024-08-15 16:21:02 +02:00
Zankaria
fb92e5fb68 config.php: rework captcha configuration 2024-08-15 16:20:58 +02:00
Zankaria
a275d04efa captcha-queries.php: refactor NativeCaptchaQuery to use DI 2024-08-15 15:17:54 +02:00
Zankaria
933594194c captcha-queries.php: refactor remote captcha queries to use DI and abstract the implementation better 2024-08-15 15:11:32 +02:00
Zankaria
e825e7aac5 Add dynamic captcha support 2024-08-15 14:37:59 +02:00
fowr
d4b4cf5825
script to expand original filename if truncated 2024-08-12 15:35:16 -03:00
fowr
3e72171889
download original filename without javascript. 2024-08-12 15:34:48 -03:00
fowr
d8391eb34a
fixes #747 and also fix when loading next page on ukko wrong value. e.g >>thread_<cite_id> 2024-08-12 15:23:19 -03:00
Lorenzo Yario
e16dc142b7
Merge pull request #786 from Zankaria/config-opcache
Make configuration sub-arrays more opcache friendly
2024-08-11 17:03:17 -07:00
Lorenzo Yario
420ec4a852
Merge pull request #787 from Zankaria/mod-page
Unify mod page handlers
2024-08-11 17:02:57 -07:00
Zankaria
4d8a4db338 api.php: partially modernize the Api class 2024-08-11 16:14:03 +02:00
Zankaria
d1b06acbe9 mod.php: remove last mod_page_* handler, use only mod_* for mod pages 2024-08-11 15:45:24 +02:00
Zankaria
80be41f47a config.php: make config sub-arrays more opcache friendly 2024-08-11 15:39:21 +02:00
Lorenzo Yario
f3e81c80d9
Merge pull request #784 from Zankaria/micro-di
(What should be a) much better Dependency Injection implementation
2024-08-11 03:05:47 -07:00
Lorenzo Yario
b3ae38da57
Merge pull request #785 from Zankaria/batch-maintenance
Batch maintenance
2024-08-11 03:05:31 -07:00
Zankaria
51e0616eb8 bans.php: fix forgot to extract the mask 2024-08-11 12:04:12 +02:00
Zankaria
7e4acbb6d2 bans.php: FIX every IP matching to any ban that was going to expire eventually 2024-08-11 12:04:12 +02:00
Zankaria
ffe855222e bans.php: do not deserialize post that does not exist 2024-08-11 12:04:12 +02:00
Zankaria
1e0a95ce83 maintenance.php: fix and update logs 2024-08-11 12:04:12 +02:00
Zankaria
c4e3541b15 config.php: purge_bans configuration into the proper section 2024-08-11 12:04:12 +02:00
Zankaria
ede7591702 bans.php: refactor to expose the moratorium on ban deletion from the database.
Also fixes the 'purge_bans' configuration for non-cache deployments.
2024-08-11 12:04:12 +02:00
Zankaria
c057c6df29 functions.php: skip ban deletion on auto_maintenance disabled 2024-08-11 12:04:12 +02:00
Zankaria
4d97e69620 maintenance.php: add purging antispam to the tool 2024-08-11 12:04:12 +02:00
Zankaria
e5bbdb9d28 functions.php: make automated antispam puring optional 2024-08-11 12:04:12 +02:00
Zankaria
cc5e96eb9d bans.php: use modern array syntax 2024-08-11 12:04:12 +02:00
Zankaria
82b8eb1e74 bans.php: group deletion policy 2024-08-11 12:04:12 +02:00
Zankaria
2298d4433f bans.php: use modify inplace 2024-08-11 12:04:12 +02:00
Zankaria
accca93084 Add maintenance.php to the tools 2024-08-11 12:04:12 +02:00
Zankaria
cbaf19cb7a bans.php: make the purge configurable 2024-08-11 12:04:12 +02:00
Zankaria
75714505a0 config.php: add auto_maintenance configuration option 2024-08-11 12:04:10 +02:00
Zankaria
36476f6341 bans.php: split find implementations 2024-08-11 11:53:27 +02:00
Zankaria
980b2ef7bf bans.php: split findSingle implementations 2024-08-11 10:28:08 +02:00
Zankaria
e4707ee2a8 Delete stale unreferenced ban appeals via foreign key constrain 2024-08-11 10:20:02 +02:00
Zankaria
00cc1f434d anti-bot.php: add comments to _create_antibot 2024-08-11 10:17:14 +02:00
Zankaria
ee20bf574a functions.php: format _create_antibot 2024-08-11 10:14:54 +02:00
Zankaria
609da43548 anti-bot.php: trim 2024-08-11 10:13:44 +02:00
Zankaria
0c074016e7 context.php: much better Dependency Injection implementation 2024-08-11 10:06:20 +02:00
Weav
562ad74a12
Merge branch 'vichan-devel:master' into fix-poster-id 2024-08-09 14:16:42 -03:00
fowr
73fce5e571
fix: wrong poster id for op; 2024-08-09 14:10:18 -03:00
Lorenzo Yario
e5d423e595
revert column behavior 2024-08-09 00:43:43 -07:00
Lorenzo Yario
230cc252c5
add changes from #782 2024-08-09 00:39:08 -07:00
Lorenzo Yario
0fbf2f6f77
Merge pull request #781 from Zankaria/main-js-refactor
Refactor main.js
2024-08-08 23:41:29 -07:00
Lorenzo Yario
db20a350a1
Merge pull request #782 from perdedora/fix-cycle
fix: proper delete posts in a cyclical thread
2024-08-08 23:40:49 -07:00
fowr
7f45f31aa8
fix: proper delete posts in a cyclical thread 2024-08-06 11:49:51 -03:00
Zankaria
4445254b00 expand-video.js: onReady 2024-08-05 19:21:09 +02:00
Zankaria
85b03c0fb0 expand-video.js: format 2024-08-05 19:20:55 +02:00
Zankaria
d9a333a69f expand-all-images.js: onReady 2024-08-05 19:15:42 +02:00
Zankaria
e92e9469a8 expand-all-images.js: format 2024-08-05 19:15:26 +02:00
Zankaria
41f9aed606 inline-expanding-filename.js: onReady 2024-08-05 19:14:06 +02:00
Zankaria
5306f1d1f9 inline-expanding-filename.js: format 2024-08-05 19:14:04 +02:00
Zankaria
2749567c3f download-original.js: onReply 2024-08-05 19:11:32 +02:00
Zankaria
44e9a5aa57 download-original.js: format 2024-08-05 19:11:23 +02:00
Zankaria
5550bc4212 catalog-search.js: onReady 2024-08-05 19:09:05 +02:00
Zankaria
d9d05ddbf5 catalog-search.js: format 2024-08-05 19:08:55 +02:00
Zankaria
d9feb5cfa7 catalog.html: onReady 2024-08-05 19:03:24 +02:00
Zankaria
b7f46a239d show-backlinks.js: onReady 2024-08-05 19:01:27 +02:00
Zankaria
2728966c1c show-backlinks.js: format 2024-08-05 19:01:08 +02:00
Zankaria
ed46907a6c post-hover.js: onReady 2024-08-05 19:00:12 +02:00
Zankaria
31444d654a post-hover.js: format 2024-08-05 18:59:44 +02:00
Zankaria
6bea01b00b youtube.js: onReady 2024-08-05 18:50:35 +02:00
Zankaria
023e59d88f youtube.js: format 2024-08-05 18:50:23 +02:00
Zankaria
b822a76b23 style-select.js: onReply 2024-08-05 18:48:04 +02:00
Zankaria
902c558237 style-select.js: format 2024-08-05 18:47:53 +02:00
Zankaria
a018772267 show-backlinks.js: onReady 2024-08-05 18:38:38 +02:00
Zankaria
fa341b29d0 smartphone-spoiler.js: format 2024-08-05 18:38:35 +02:00
Zankaria
6daae3ec92 show-backlinks.js: onReady 2024-08-05 18:35:21 +02:00
Zankaria
da0f26485a show-backlinks.js: trim 2024-08-05 18:34:55 +02:00
Zankaria
8b773b124e main.js: rename onready to camelCase 2024-08-05 18:33:15 +02:00
Zankaria
c327a0439e main.js: rename init_stylechooser to camelCase 2024-08-04 15:52:15 +02:00
Zankaria
4b5e40f575 main.js: format 2024-08-04 15:50:04 +02:00
Zankaria
eb67076e60 main.js: rename dopost to camelCase 2024-08-04 15:47:38 +02:00
Zankaria
990f27e80b main.js: format getCookie 2024-08-04 15:44:12 +02:00
Zankaria
8d3bfedc72 main.js: trim 2024-08-04 15:42:02 +02:00
Lorenzo Yario
d78f865645
Merge pull request #779 from Zankaria/ban-query-truncate
Fix new_ban failing due to the message body being too big
2024-08-03 23:16:11 -07:00
Lorenzo Yario
283973c141
Merge pull request #780 from seisatsu/dev
Add a spam filter that unshortens urls (resubmission)
2024-08-03 23:15:56 -07:00
seisatsu
098edb9cd7 Add a spam filter that unshortens urls 2024-07-31 12:44:39 -05:00
Zankaria
4bc69be4bc bans.php: fix new_ban failing due to the message body being too big 2024-07-27 15:27:44 +02:00
Lorenzo Yario
a20f618d80
Create uboachan-gray.css
#777
2024-07-19 11:30:08 -07:00
Lorenzo Yario
d7468bb93b
Merge pull request #772 from vichan-devel/RealAngeleno-templateupdates
Update catalog and the index: Wildcard and excluding boards
2024-07-14 04:40:09 -07:00
Lorenzo Yario
541f31f4d1
allow excluding boards from the boardlist 2024-07-14 04:38:11 -07:00
Lorenzo Yario
c223b1c55d
allow board exclusion for the boardlist 2024-07-14 04:37:28 -07:00
Lorenzo Yario
409f571955
switch to columns of two. Make it optional for a video and other things. 2024-07-14 04:21:21 -07:00
Lorenzo Yario
d23d1526e8
forgot to remove implode() 2024-07-14 03:41:04 -07:00
Lorenzo Yario
9a80ae2434
make the wildcard the default 2024-07-14 03:38:09 -07:00
Lorenzo Yario
dba38b10d4
allow wildcard for catalog theme 2024-07-14 03:26:06 -07:00
Lorenzo Yario
89a31794d9
fixing and standardizing something i did at like 4am with tripcode disabling 2024-07-04 11:29:18 -07:00
Lorenzo Yario
1a780ce9cb
move are you sure prompt to the js file 2024-06-27 02:18:06 -07:00
Lorenzo Yario
fe8fa0da8a
move to the js file 2024-06-27 02:13:54 -07:00
Lorenzo Yario
e12cbf6d80
Merge pull request #767 from Zankaria/patch-1
docker: remove PEAR leftovers
2024-06-24 17:02:10 -07:00
Lorenzo Yario
0df33b4956
Merge pull request #768 from Zankaria/docker-patch
Improve docker composer
2024-06-24 17:01:56 -07:00
Zankaria
ef1939500c docker: use modern compose file naming scheme 2024-06-23 15:54:21 +02:00
Zankaria
555d14b7ae docker: make the compose local instance folder file parametrizable 2024-06-23 15:53:42 +02:00
Zankaria
281f391205
docker: remove PEAR leftovers 2024-06-23 13:13:45 +02:00
Lorenzo Yario
b21865853b
fix the rest of local-time.js somehow 2024-06-20 20:38:35 -07:00
Lorenzo Yario
5b0a7fb975
Merge pull request #765 from perdedora/hash-passwords
Hash poster passwords
2024-06-20 16:05:55 -07:00
fowr
e9f1d59209
posts.sql: update column password 2024-06-20 10:33:51 -03:00
Weav
fee67b6719
Merge branch 'dev' into hash-passwords 2024-06-20 13:21:05 +00:00
fowr
fff9b88c6d
hash poster passwords 2024-06-20 10:11:47 -03:00
Lorenzo Yario
8d37f1dd2c
Merge pull request #764 from Zankaria/patch-1
install.php: fix https check
2024-06-20 03:40:13 -07:00
Zankaria
991ed657fb
install.php: fix https check 2024-06-15 00:53:49 +02:00
Lorenzo Yario
ad532d1d2d
Merge pull request #762 from vichan-devel/RealAngeleno-patch-12
fix local-time somehow
2024-06-06 00:02:35 -07:00
Lorenzo Yario
d2f1b7e0e0
fix local-time somehow
this fixes it. i don't know how. don't ask. i'm not a javascript guy much.
2024-06-06 00:02:06 -07:00
Lorenzo Yario
2d6b599b26
Merge pull request #761 from vichan-devel/parsedown
Parsedown
2024-06-05 18:49:27 -07:00
Lorenzo Yario
bd7eb130ea
use current website 2024-06-05 18:08:23 -07:00
Lorenzo Yario
f852172e9b
no they're not 2024-06-05 18:05:43 -07:00
Lorenzo Yario
809ab99c9b
allow images for markdown 2024-06-05 18:02:08 -07:00
Lorenzo Yario
5c99c8395e
add parsedown 2024-06-05 18:00:24 -07:00
Lorenzo Yario
878a67389a
Merge pull request #758 from Zankaria/refactor-ban-log
Refactor ban log
2024-05-22 23:15:30 -07:00
Lorenzo Yario
d990c344d4
Merge pull request #759 from Zankaria/fake-copy
Use hardlinks instead of copying the file
2024-05-22 23:14:49 -07:00
Zankaria
88a48befd4 pages.php: use link to create hardlinks instead of full file copy for thread moving 2024-05-23 00:05:43 +02:00
Zankaria
7e4dd5567b bans.php: always print modLog 2024-05-22 22:14:38 +02:00
Zankaria
8963ebfce9 bans.php: simplify modLog string 2024-05-22 22:14:08 +02:00
Lorenzo Yario
9236e10f37
add ) 2024-05-20 04:32:30 -07:00
Lorenzo Yario
9aaed32c57
Merge pull request #752 from vichan-devel/RealAngeleno-cattychanges
remove redundant second catalog button on catalog-link.js
2024-05-18 03:26:03 -07:00
Lorenzo Yario
055d31d2db
Merge pull request #749 from vichan-devel/RealAngeleno-datetime
fix bug with datetime
2024-05-17 19:03:51 -07:00
Lorenzo Yario
de39780194
Merge pull request #753 from Zankaria/fix-install
Fix HTTPS check
2024-05-17 15:05:48 -07:00
Zankaria
37658f1817 install.php: fix HTTPS check 2024-05-17 14:13:40 +02:00
Lorenzo Yario
00be5e6ced
remove redundant second catalog button 2024-05-17 00:51:24 -07:00
Lorenzo Yario
605f198d8c
fix bug with datetime 2024-05-16 22:51:20 -07:00
Lorenzo Yario
021e20f373
Merge pull request #714 from Zankaria/fix-string-interning
Fix: substitute deprecated string interning syntax
2024-05-09 22:48:10 -07:00
Lorenzo Yario
b0e6580845
Merge pull request #736 from papereth/patch-1
Fix Exif leak in JPEG orientation conversion code
2024-05-08 17:44:48 -07:00
papereth
0dd064b2ea
Fix Exif leak in JPEG orientation conversion code
https://github.com/vichan-devel/vichan/issues/735
2024-05-04 15:01:17 +01:00
Zankaria
c91c58ed07 Fix: substitute deprecated string interning syntax 2024-04-03 19:18:41 +02:00
90 changed files with 4116 additions and 3042 deletions

69
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,69 @@
name: Bug Report
description: File a bug report for Vichan
title: "[BUG] "
labels: ["bug"]
assignees: []
body:
- type: markdown
attributes:
value: |
**Thank you for reporting a bug! Please provide as much detail as possible.**
Before submitting, check the [Vichan Wiki](https://vichan.info) to see if there's already a solution to your problem.
- type: textarea
id: bug_description
attributes:
label: "Describe the bug"
description: "A clear and concise description of what the bug is."
placeholder: "Posting doesn't go through and displays a collation error. The exact error message given is the text below and I've attached a screenshot..."
validations:
required: true
- type: textarea
id: steps_to_reproduce
attributes:
label: "Steps to Reproduce"
description: "Provide step-by-step instructions to reproduce the issue. If you're unsure on how, that is alright, just try and explain as well as you can."
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
render: markdown
validations:
required: true
- type: textarea
id: expected_behavior
attributes:
label: "Expected Behavior"
description: "What did you expect to happen?"
placeholder: "Expected behavior here..."
render: markdown
validations:
required: true
- type: textarea
id: server_specs
attributes:
label: "Server Specifications"
description: "Provide details about your server environment. If you're unsure about any of this, you might be using shared hosting (Hostinger, HostGator, Serv00, etc). If so, put the name of your hosting provider here."
placeholder: |
- OS: (Ubuntu, CentOS, Windows Server 2025, etc.)
- PHP Version: (e.g., 7.4, 8.0, 8.4)
- Web Server: (Apache, NGINX, etc.)
- Database: (MySQL, MariaDB, etc.)
- Vichan Version: (5.2.0, 5.3.0 (dev branch), etc)
render: markdown
validations:
required: true
- type: textarea
id: additional_context
attributes:
label: "Additional Context"
description: "Any other details we should know?"
placeholder: "Add any additional context here..."
render: markdown

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -9,7 +9,7 @@ services:
depends_on: depends_on:
- db - db
volumes: volumes:
- ./local-instances/1/www:/var/www/html - ./local-instances/${INSTANCE:-0}/www:/var/www/html
- ./docker/nginx/vichan.conf:/etc/nginx/conf.d/default.conf - ./docker/nginx/vichan.conf:/etc/nginx/conf.d/default.conf
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf
- ./docker/nginx/proxy.conf:/etc/nginx/conf.d/proxy.conf - ./docker/nginx/proxy.conf:/etc/nginx/conf.d/proxy.conf
@ -21,7 +21,7 @@ services:
context: . context: .
dockerfile: ./docker/php/Dockerfile dockerfile: ./docker/php/Dockerfile
volumes: volumes:
- ./local-instances/1/www:/var/www - ./local-instances/${INSTANCE:-0}/www:/var/www
- ./docker/php/www.conf:/usr/local/etc/php-fpm.d/www.conf - ./docker/php/www.conf:/usr/local/etc/php-fpm.d/www.conf
- ./docker/php/jit.ini:/usr/local/etc/php/conf.d/jit.ini - ./docker/php/jit.ini:/usr/local/etc/php/conf.d/jit.ini
@ -37,4 +37,4 @@ services:
MYSQL_DATABASE: vichan MYSQL_DATABASE: vichan
MYSQL_ROOT_PASSWORD: password MYSQL_ROOT_PASSWORD: password
volumes: volumes:
- ./local-instances/1/mysql:/var/lib/mysql - ./local-instances/${INSTANCE:-0}/mysql:/var/lib/mysql

View File

@ -18,7 +18,8 @@
"gettext/gettext": "^5.5", "gettext/gettext": "^5.5",
"mrclay/minify": "^2.1.6", "mrclay/minify": "^2.1.6",
"geoip/geoip": "^1.17", "geoip/geoip": "^1.17",
"dapphp/securimage": "^4.0" "dapphp/securimage": "^4.0",
"erusev/parsedown": "^1.7.4"
}, },
"autoload": { "autoload": {
"classmap": ["inc/"], "classmap": ["inc/"],
@ -33,12 +34,13 @@
"inc/lock.php", "inc/lock.php",
"inc/queue.php", "inc/queue.php",
"inc/functions.php", "inc/functions.php",
"inc/functions/dice.php",
"inc/functions/format.php",
"inc/functions/net.php", "inc/functions/net.php",
"inc/functions/num.php", "inc/functions/num.php",
"inc/functions/format.php", "inc/functions/theme.php",
"inc/driver/http-driver.php", "inc/service/captcha-queries.php",
"inc/driver/log-driver.php", "inc/context.php"
"inc/service/captcha-queries.php"
] ]
}, },
"license": "Tinyboard + vichan", "license": "Tinyboard + vichan",

View File

@ -1,6 +1,6 @@
# Based on https://github.com/dead-guru/devichan/blob/master/php-fpm/Dockerfile # Based on https://github.com/dead-guru/devichan/blob/master/php-fpm/Dockerfile
FROM composer AS composer FROM composer:lts AS composer
FROM php:8.1-fpm-alpine FROM php:8.1-fpm-alpine
RUN apk add --no-cache \ RUN apk add --no-cache \
@ -64,7 +64,8 @@ RUN apk add --no-cache \
imagemagick-dev \ imagemagick-dev \
pcre-dev \ pcre-dev \
$PHPIZE_DEPS \ $PHPIZE_DEPS \
&& rm -rf /var/cache/* && rm -rf /var/cache/* \
&& rm -rf /tmp/pear
RUN rmdir /var/www/html \ RUN rmdir /var/www/html \
&& install -d -m 744 -o www-data -g www-data /var/www \ && install -d -m 744 -o www-data -g www-data /var/www \
&& install -d -m 700 -o www-data -g www-data /var/tmp/vichan \ && install -d -m 700 -o www-data -g www-data /var/tmp/vichan \

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 $inner = [];
public function get(string $key) {
return isset(self::$inner[$key]) ? self::$inner[$key] : null;
}
public function set(string $key, $value, $expires = false): void {
self::$inner[$key] = $value;
}
public function delete(string $key): void {
unset(self::$inner[$key]);
}
public function flush(): void {
self::$inner = [];
}
}

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);
/**
* Set a key-value pair.
*
* @param string $key The key.
* @param mixed $value The value.
* @param int|false $expires After how many seconds the pair will expire. Use false or ignore this parameter to keep
* the value until it gets evicted to make space for more items. Some drivers will always
* ignore this parameter and store the pair until it's removed.
*/
public function set(string $key, $value, $expires = false);
/**
* Delete a key-value pair.
*
* @param string $key The key.
*/
public function delete(string $key);
/**
* Delete all the key-value pairs.
*/
public function flush();
}

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,131 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
/**
* Honestly this is just a wrapper for cURL. Still useful to mock it and have an OOP API on PHP 7.
*/
class HttpDriver {
private $inner;
private int $timeout;
private int $max_file_size;
private function resetTowards(string $url, int $timeout): void {
\curl_reset($this->inner);
\curl_setopt_array($this->inner, [
\CURLOPT_URL => $url,
\CURLOPT_TIMEOUT => $timeout,
\CURLOPT_USERAGENT => 'Tinyboard',
\CURLOPT_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS,
]);
}
public function __construct(int $timeout, int $max_file_size) {
$this->inner = \curl_init();
$this->timeout = $timeout;
$this->max_file_size = $max_file_size;
}
public function __destruct() {
\curl_close($this->inner);
}
/**
* Execute a GET request.
*
* @param string $endpoint Uri endpoint.
* @param ?array $data Optional GET parameters.
* @param int $timeout Optional request timeout in seconds. Use the default timeout if 0.
* @return string Returns the body of the response.
* @throws RuntimeException Throws on IO error.
*/
public function requestGet(string $endpoint, ?array $data, int $timeout = 0): string {
if (!empty($data)) {
$endpoint .= '?' . \http_build_query($data);
}
if ($timeout == 0) {
$timeout = $this->timeout;
}
$this->resetTowards($endpoint, $timeout);
\curl_setopt($this->inner, \CURLOPT_RETURNTRANSFER, true);
$ret = \curl_exec($this->inner);
if ($ret === false) {
throw new \RuntimeException(\curl_error($this->inner));
}
return $ret;
}
/**
* Execute a POST request.
*
* @param string $endpoint Uri endpoint.
* @param ?array $data Optional POST parameters.
* @param int $timeout Optional request timeout in seconds. Use the default timeout if 0.
* @return string Returns the body of the response.
* @throws RuntimeException Throws on IO error.
*/
public function requestPost(string $endpoint, ?array $data, int $timeout = 0): string {
if ($timeout == 0) {
$timeout = $this->timeout;
}
$this->resetTowards($endpoint, $timeout);
\curl_setopt($this->inner, \CURLOPT_POST, true);
if (!empty($data)) {
\curl_setopt($this->inner, \CURLOPT_POSTFIELDS, \http_build_query($data));
}
\curl_setopt($this->inner, \CURLOPT_RETURNTRANSFER, true);
$ret = \curl_exec($this->inner);
if ($ret === false) {
throw new \RuntimeException(\curl_error($this->inner));
}
return $ret;
}
/**
* Download the url's target with curl.
*
* @param string $url Url to the file to download.
* @param ?array $data Optional GET parameters.
* @param resource $fd File descriptor to save the content to.
* @param int $timeout Optional request timeout in seconds. Use the default timeout if 0.
* @return bool Returns true on success, false if the file was too large.
* @throws RuntimeException Throws on IO error.
*/
public function requestGetInto(string $endpoint, ?array $data, $fd, int $timeout = 0): bool {
if (!empty($data)) {
$endpoint .= '?' . \http_build_query($data);
}
if ($timeout == 0) {
$timeout = $this->timeout;
}
$this->resetTowards($endpoint, $timeout);
// Adapted from: https://stackoverflow.com/a/17642638
$opt = (\PHP_MAJOR_VERSION >= 8 && \PHP_MINOR_VERSION >= 2) ? \CURLOPT_XFERINFOFUNCTION : \CURLOPT_PROGRESSFUNCTION;
\curl_setopt_array($this->inner, [
\CURLOPT_NOPROGRESS => false,
$opt => fn($res, $next_dl, $dl, $next_up, $up) => (int)($dl <= $this->max_file_size),
\CURLOPT_FAILONERROR => true,
\CURLOPT_FOLLOWLOCATION => false,
\CURLOPT_FILE => $fd,
\CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
]);
$ret = \curl_exec($this->inner);
if ($ret === false) {
if (\curl_errno($this->inner) === CURLE_ABORTED_BY_CALLBACK) {
return false;
}
throw new \RuntimeException(\curl_error($this->inner));
}
return true;
}
}

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,48 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
class RedisCacheDriver implements CacheDriver {
private string $prefix;
private \Redis $inner;
public function __construct(string $prefix, string $host, int $port, ?string $password, string $database) {
$this->inner = new \Redis();
$this->inner->connect($host, $port);
if ($password) {
$this->inner->auth($password);
}
if (!$this->inner->select($database)) {
throw new \RuntimeException('Unable to connect to Redis!');
}
$$this->prefix = $prefix;
}
public function get(string $key): mixed {
$ret = $this->inner->get($this->prefix . $key);
if ($ret === false) {
return null;
}
return \json_decode($ret, true);
}
public function set(string $key, mixed $value, mixed $expires = false): void {
if ($expires === false) {
$this->inner->set($this->prefix . $key, \json_encode($value));
} else {
$expires = $expires * 1000; // Seconds to milliseconds.
$this->inner->setex($this->prefix . $key, $expires, \json_encode($value));
}
}
public function delete(string $key): void {
$this->inner->del($this->prefix . $key);
}
public function flush(): void {
$this->inner->flushDB();
}
}

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

@ -1,191 +1,5 @@
<?php <?php
/* /*
* Copyright (c) 2010-2013 Tinyboard Development Group * Anti-bot.php has been deprecated and removed due to its functions not being necessary and being easily bypassable, by both customized and uncustomized spambots.
*/ */
defined('TINYBOARD') or exit;
$hidden_inputs_twig = array();
class AntiBot {
public $salt, $inputs = array(), $index = 0;
public static function randomString($length, $uppercase = false, $special_chars = false, $unicode_chars = false) {
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
if ($uppercase)
$chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
if ($special_chars)
$chars .= ' ~!@#$%^&*()_+,./;\'[]\\{}|:<>?=-` ';
if ($unicode_chars) {
$len = strlen($chars) / 10;
for ($n = 0; $n < $len; $n++)
$chars .= mb_convert_encoding('&#' . mt_rand(0x2600, 0x26FF) . ';', 'UTF-8', 'HTML-ENTITIES');
}
$chars = preg_split('//u', $chars, -1, PREG_SPLIT_NO_EMPTY);
$ch = array();
// fill up $ch until we reach $length
while (count($ch) < $length) {
$n = $length - count($ch);
$keys = array_rand($chars, $n > count($chars) ? count($chars) : $n);
if ($n == 1) {
$ch[] = $chars[$keys];
break;
}
shuffle($keys);
foreach ($keys as $key)
$ch[] = $chars[$key];
}
$chars = $ch;
return implode('', $chars);
}
public static function make_confusing($string) {
$chars = preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY);
foreach ($chars as &$c) {
if (mt_rand(0, 3) != 0)
$c = utf8tohtml($c);
else
$c = mb_encode_numericentity($c, array(0, 0xffff, 0, 0xffff), 'UTF-8');
}
return implode('', $chars);
}
public function __construct(array $salt = array()) {
global $config;
if (!empty($salt)) {
// create a salted hash of the "extra salt"
$this->salt = implode(':', $salt);
} else {
$this->salt = '';
}
shuffle($config['spam']['hidden_input_names']);
$input_count = mt_rand($config['spam']['hidden_inputs_min'], $config['spam']['hidden_inputs_max']);
$hidden_input_names_x = 0;
for ($x = 0; $x < $input_count ; $x++) {
if ($hidden_input_names_x === false || mt_rand(0, 2) == 0) {
// Use an obscure name
$name = $this->randomString(mt_rand(10, 40), false, false, $config['spam']['unicode']);
} else {
// Use a pre-defined confusing name
$name = $config['spam']['hidden_input_names'][$hidden_input_names_x++];
if ($hidden_input_names_x >= count($config['spam']['hidden_input_names']))
$hidden_input_names_x = false;
}
if (mt_rand(0, 2) == 0) {
// Value must be null
$this->inputs[$name] = '';
} elseif (mt_rand(0, 4) == 0) {
// Numeric value
$this->inputs[$name] = (string)mt_rand(0, 100000);
} else {
// Obscure value
$this->inputs[$name] = $this->randomString(mt_rand(5, 100), true, true, $config['spam']['unicode']);
}
}
}
public static function space() {
if (mt_rand(0, 3) != 0)
return ' ';
return str_repeat(' ', mt_rand(1, 3));
}
public function html($count = false) {
global $config;
$elements = array(
'<input type="hidden" name="%name%" value="%value%">',
'<input type="hidden" value="%value%" name="%name%">',
'<input name="%name%" value="%value%" type="hidden">',
'<input value="%value%" name="%name%" type="hidden">',
'<input style="display:none" type="text" name="%name%" value="%value%">',
'<input style="display:none" type="text" value="%value%" name="%name%">',
'<span style="display:none"><input type="text" name="%name%" value="%value%"></span>',
'<div style="display:none"><input type="text" name="%name%" value="%value%"></div>',
'<div style="display:none"><input type="text" name="%name%" value="%value%"></div>',
'<textarea style="display:none" name="%name%">%value%</textarea>',
'<textarea name="%name%" style="display:none">%value%</textarea>'
);
$html = '';
if ($count === false) {
$count = mt_rand(1, (int)abs(count($this->inputs) / 15) + 1);
}
if ($count === true) {
// all elements
$inputs = array_slice($this->inputs, $this->index);
} else {
$inputs = array_slice($this->inputs, $this->index, $count);
}
$this->index += count($inputs);
foreach ($inputs as $name => $value) {
$element = false;
while (!$element) {
$element = $elements[array_rand($elements)];
$element = str_replace(' ', self::space(), $element);
if (mt_rand(0, 5) == 0)
$element = str_replace('>', self::space() . '>', $element);
if (strpos($element, 'textarea') !== false && $value == '') {
// There have been some issues with mobile web browsers and empty <textarea>'s.
$element = false;
}
}
$element = str_replace('%name%', utf8tohtml($name), $element);
if (mt_rand(0, 2) == 0)
$value = $this->make_confusing($value);
else
$value = utf8tohtml($value);
if (strpos($element, 'textarea') === false)
$value = str_replace('"', '&quot;', $value);
$element = str_replace('%value%', $value, $element);
$html .= $element;
}
return $html;
}
public function reset() {
$this->index = 0;
}
public function hash() {
global $config;
// This is the tricky part: create a hash to validate it after
// First, sort the keys in alphabetical order (A-Z)
$inputs = $this->inputs;
ksort($inputs);
$hash = '';
// Iterate through each input
foreach ($inputs as $name => $value) {
$hash .= $name . '=' . $value;
}
// Add a salt to the hash
$hash .= $config['cookies']['salt'];
// Use SHA1 for the hash
return sha1($hash . $this->salt);
}
}

View File

@ -9,14 +9,49 @@ defined('TINYBOARD') or exit;
* Class for generating json API compatible with 4chan API * Class for generating json API compatible with 4chan API
*/ */
class Api { class Api {
function __construct(){ private bool $show_filename;
global $config; private bool $hide_email;
/** private bool $country_flags;
* Translation from local fields to fields in 4chan-style API private array $postFields;
*/
$this->config = $config;
$this->postFields = array( private const INTS = [
'no' => 1,
'resto' => 1,
'time' => 1,
'tn_w' => 1,
'tn_h' => 1,
'w' => 1,
'h' => 1,
'fsize' => 1,
'omitted_posts' => 1,
'omitted_images' => 1,
'replies' => 1,
'images' => 1,
'sticky' => 1,
'locked' => 1,
'last_modified' => 1
];
private const THREADS_PAGE_FIELDS = [
'id' => 'no',
'bump' => 'last_modified'
];
private const FILE_FIELDS = [
'thumbheight' => 'tn_h',
'thumbwidth' => 'tn_w',
'height' => 'h',
'width' => 'w',
'size' => 'fsize'
];
public function __construct(bool $show_filename, bool $hide_email, bool $country_flags) {
// Translation from local fields to fields in 4chan-style API
$this->show_filename = $show_filename;
$this->hide_email = $hide_email;
$this->country_flags = $country_flags;
$this->postFields = [
'id' => 'no', 'id' => 'no',
'thread' => 'resto', 'thread' => 'resto',
'subject' => 'sub', 'subject' => 'sub',
@ -35,91 +70,65 @@ class Api {
'cycle' => 'cyclical', 'cycle' => 'cyclical',
'bump' => 'last_modified', 'bump' => 'last_modified',
'embed' => 'embed', 'embed' => 'embed',
); ];
$this->threadsPageFields = array(
'id' => 'no',
'bump' => 'last_modified'
);
$this->fileFields = array(
'thumbheight' => 'tn_h',
'thumbwidth' => 'tn_w',
'height' => 'h',
'width' => 'w',
'size' => 'fsize',
);
if (isset($config['api']['extra_fields']) && gettype($config['api']['extra_fields']) == 'array'){ if (isset($config['api']['extra_fields']) && gettype($config['api']['extra_fields']) == 'array'){
$this->postFields = array_merge($this->postFields, $config['api']['extra_fields']); $this->postFields = array_merge($this->postFields, $config['api']['extra_fields']);
} }
} }
private static $ints = array(
'no' => 1,
'resto' => 1,
'time' => 1,
'tn_w' => 1,
'tn_h' => 1,
'w' => 1,
'h' => 1,
'fsize' => 1,
'omitted_posts' => 1,
'omitted_images' => 1,
'replies' => 1,
'images' => 1,
'sticky' => 1,
'locked' => 1,
'last_modified' => 1
);
private function translateFields($fields, $object, &$apiPost) { private function translateFields($fields, $object, &$apiPost) {
foreach ($fields as $local => $translated) { foreach ($fields as $local => $translated) {
if (!isset($object->$local)) if (!isset($object->$local)) {
continue; continue;
}
$toInt = isset(self::$ints[$translated]); $toInt = isset(self::INTS[$translated]);
$val = $object->$local; $val = $object->$local;
if (isset($this->config['hide_email']) && $this->config['hide_email'] && $local === 'email') { if ($this->hide_email && $local === 'email') {
$val = ''; $val = '';
} }
if ($val !== null && $val !== '') { if ($val !== null && $val !== '') {
$apiPost[$translated] = $toInt ? (int) $val : $val; $apiPost[$translated] = $toInt ? (int) $val : $val;
} }
} }
} }
private function translateFile($file, $post, &$apiPost) { private function translateFile($file, $post, &$apiPost) {
$this->translateFields($this->fileFields, $file, $apiPost); $this->translateFields(self::FILE_FIELDS, $file, $apiPost);
$dotPos = strrpos($file->file, '.'); $dotPos = strrpos($file->file, '.');
$apiPost['ext'] = substr($file->file, $dotPos); $apiPost['ext'] = substr($file->file, $dotPos);
$apiPost['tim'] = substr($file->file, 0, $dotPos); $apiPost['tim'] = substr($file->file, 0, $dotPos);
if (isset($this->config['show_filename']) && $this->config['show_filename']) {
if ($this->show_filename) {
$apiPost['filename'] = @substr($file->name, 0, strrpos($file->name, '.')); $apiPost['filename'] = @substr($file->name, 0, strrpos($file->name, '.'));
} } else {
else {
$apiPost['filename'] = substr($file->file, 0, $dotPos); $apiPost['filename'] = substr($file->file, 0, $dotPos);
} }
if (isset ($file->hash) && $file->hash) { if (isset ($file->hash) && $file->hash) {
$apiPost['md5'] = base64_encode(hex2bin($file->hash)); $apiPost['md5'] = base64_encode(hex2bin($file->hash));
} } elseif (isset ($post->filehash) && $post->filehash) {
else if (isset ($post->filehash) && $post->filehash) {
$apiPost['md5'] = base64_encode(hex2bin($post->filehash)); $apiPost['md5'] = base64_encode(hex2bin($post->filehash));
} }
} }
private function translatePost($post, $threadsPage = false) { private function translatePost($post, bool $threadsPage = false) {
global $config, $board; global $config, $board;
$apiPost = array();
$fields = $threadsPage ? $this->threadsPageFields : $this->postFields; $apiPost = [];
$fields = $threadsPage ? self::THREADS_PAGE_FIELDS : $this->postFields;
$this->translateFields($fields, $post, $apiPost); $this->translateFields($fields, $post, $apiPost);
if (isset($config['poster_ids']) && $config['poster_ids']) $apiPost['id'] = poster_id($post->ip, $post->thread, $board['uri']);
if ($threadsPage) return $apiPost; if (isset($config['poster_ids']) && $config['poster_ids']) {
$apiPost['id'] = poster_id($post->ip, $post->thread ?? $post->id);
}
if ($threadsPage) {
return $apiPost;
}
// Handle country field // Handle country field
if (isset($post->body_nomarkup) && $this->config['country_flags']) { if (isset($post->body_nomarkup) && $this->country_flags) {
$modifiers = extract_modifiers($post->body_nomarkup); $modifiers = extract_modifiers($post->body_nomarkup);
if (isset($modifiers['flag']) && isset($modifiers['flag alt']) && preg_match('/^[a-z]{2}$/', $modifiers['flag'])) { if (isset($modifiers['flag']) && isset($modifiers['flag alt']) && preg_match('/^[a-z]{2}$/', $modifiers['flag'])) {
$country = strtoupper($modifiers['flag']); $country = strtoupper($modifiers['flag']);
@ -139,12 +148,15 @@ class Api {
if (isset($post->files) && $post->files && !$threadsPage) { if (isset($post->files) && $post->files && !$threadsPage) {
$file = $post->files[0]; $file = $post->files[0];
$this->translateFile($file, $post, $apiPost); $this->translateFile($file, $post, $apiPost);
if (sizeof($post->files) > 1) {
$extra_files = array();
foreach ($post->files as $i => $f) {
if ($i == 0) continue;
$extra_file = array(); if (sizeof($post->files) > 1) {
$extra_files = [];
foreach ($post->files as $i => $f) {
if ($i == 0) {
continue;
}
$extra_file = [];
$this->translateFile($f, $post, $extra_file); $this->translateFile($f, $post, $extra_file);
$extra_files[] = $extra_file; $extra_files[] = $extra_file;
@ -156,8 +168,8 @@ class Api {
return $apiPost; return $apiPost;
} }
function translateThread(Thread $thread, $threadsPage = false) { public function translateThread(Thread $thread, bool $threadsPage = false) {
$apiPosts = array(); $apiPosts = [];
$op = $this->translatePost($thread, $threadsPage); $op = $this->translatePost($thread, $threadsPage);
if (!$threadsPage) $op['resto'] = 0; if (!$threadsPage) $op['resto'] = 0;
$apiPosts['posts'][] = $op; $apiPosts['posts'][] = $op;
@ -169,16 +181,16 @@ class Api {
return $apiPosts; return $apiPosts;
} }
function translatePage(array $threads) { public function translatePage(array $threads) {
$apiPage = array(); $apiPage = [];
foreach ($threads as $thread) { foreach ($threads as $thread) {
$apiPage['threads'][] = $this->translateThread($thread); $apiPage['threads'][] = $this->translateThread($thread);
} }
return $apiPage; return $apiPage;
} }
function translateCatalogPage(array $threads, $threadsPage = false) { public function translateCatalogPage(array $threads, bool $threadsPage = false) {
$apiPage = array(); $apiPage = [];
foreach ($threads as $thread) { foreach ($threads as $thread) {
$ts = $this->translateThread($thread, $threadsPage); $ts = $this->translateThread($thread, $threadsPage);
$apiPage['threads'][] = current($ts['posts']); $apiPage['threads'][] = current($ts['posts']);
@ -186,8 +198,8 @@ class Api {
return $apiPage; return $apiPage;
} }
function translateCatalog($catalog, $threadsPage = false) { public function translateCatalog($catalog, bool $threadsPage = false) {
$apiCatalog = array(); $apiCatalog = [];
foreach ($catalog as $page => $threads) { foreach ($catalog as $page => $threads) {
$apiPage = $this->translateCatalogPage($threads, $threadsPage); $apiPage = $this->translateCatalogPage($threads, $threadsPage);
$apiPage['page'] = $page; $apiPage['page'] = $page;

View File

@ -4,6 +4,10 @@ use Vichan\Functions\Format;
use Lifo\IP\CIDR; use Lifo\IP\CIDR;
class Bans { class Bans {
static private function shouldDelete(array $ban, bool $require_ban_view) {
return $ban['expires'] && ($ban['seen'] || !$require_ban_view) && $ban['expires'] < time();
}
static private function deleteBans(array $ban_ids) { static private function deleteBans(array $ban_ids) {
$len = count($ban_ids); $len = count($ban_ids);
if ($len === 1) { if ($len === 1) {
@ -11,7 +15,7 @@ class Bans {
$query->bindValue(':id', $ban_ids[0], PDO::PARAM_INT); $query->bindValue(':id', $ban_ids[0], PDO::PARAM_INT);
$query->execute() or error(db_error()); $query->execute() or error(db_error());
rebuildThemes('bans'); Vichan\Functions\Theme\rebuild_themes('bans');
} elseif ($len >= 1) { } elseif ($len >= 1) {
// Build the query. // Build the query.
$query = 'DELETE FROM ``bans`` WHERE `id` IN ('; $query = 'DELETE FROM ``bans`` WHERE `id` IN (';
@ -29,10 +33,131 @@ class Bans {
$query->execute() or error(db_error()); $query->execute() or error(db_error());
rebuildThemes('bans'); Vichan\Functions\Theme\rebuild_themes('bans');
} }
} }
static private function findSingleAutoGc(string $ip, int $ban_id, bool $require_ban_view) {
// Use OR in the query to also garbage collect bans.
$query = prepare(
'SELECT ``bans``.* FROM ``bans``
WHERE ((`ipstart` = :ip OR (:ip >= `ipstart` AND :ip <= `ipend`)) OR (``bans``.id = :id))
ORDER BY `expires` IS NULL, `expires` DESC'
);
$query->bindValue(':id', $ban_id);
$query->bindValue(':ip', inet_pton($ip));
$query->execute() or error(db_error($query));
$found_ban = null;
$to_delete_list = [];
while ($ban = $query->fetch(PDO::FETCH_ASSOC)) {
if (self::shouldDelete($ban, $require_ban_view)) {
$to_delete_list[] = $ban['id'];
} elseif ($ban['id'] === $ban_id) {
if ($ban['post']) {
$ban['post'] = json_decode($ban['post'], true);
}
$ban['mask'] = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
$found_ban = $ban;
}
}
self::deleteBans($to_delete_list);
return $found_ban;
}
static private function findSingleNoGc(int $ban_id) {
$query = prepare(
'SELECT ``bans``.* FROM ``bans``
WHERE ``bans``.id = :id
ORDER BY `expires` IS NULL, `expires` DESC
LIMIT 1'
);
$query->bindValue(':id', $ban_id);
$query->execute() or error(db_error($query));
$ret = $query->fetch(PDO::FETCH_ASSOC);
if ($query->rowCount() == 0) {
return null;
} else {
if ($ret['post']) {
$ret['post'] = json_decode($ret['post'], true);
}
$ret['mask'] = self::range_to_string([$ret['ipstart'], $ret['ipend']]);
return $ret;
}
}
static private function findAutoGc(?string $ip, $board, bool $get_mod_info, bool $require_ban_view, ?int $ban_id): array {
$query = prepare('SELECT ``bans``.*' . ($get_mod_info ? ', `username`' : '') . ' FROM ``bans``
' . ($get_mod_info ? 'LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`' : '') . '
WHERE
(' . ($board !== false ? '(`board` IS NULL OR `board` = :board) AND' : '') . '
(`ipstart` = :ip OR (:ip >= `ipstart` AND :ip <= `ipend`)) OR (``bans``.id = :id))
ORDER BY `expires` IS NULL, `expires` DESC');
if ($board !== false) {
$query->bindValue(':board', $board, PDO::PARAM_STR);
}
$query->bindValue(':id', $ban_id);
$query->bindValue(':ip', inet_pton($ip));
$query->execute() or error(db_error($query));
$ban_list = [];
$to_delete_list = [];
while ($ban = $query->fetch(PDO::FETCH_ASSOC)) {
if (self::shouldDelete($ban, $require_ban_view)) {
$to_delete_list[] = $ban['id'];
} else {
if ($ban['post']) {
$ban['post'] = json_decode($ban['post'], true);
}
$ban['mask'] = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
$ban_list[] = $ban;
}
}
self::deleteBans($to_delete_list);
return $ban_list;
}
static private function findNoGc(?string $ip, string $board, bool $get_mod_info, ?int $ban_id): array {
$query = prepare('SELECT ``bans``.*' . ($get_mod_info ? ', `username`' : '') . ' FROM ``bans``
' . ($get_mod_info ? 'LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`' : '') . '
WHERE
(' . ($board !== false ? '(`board` IS NULL OR `board` = :board) AND' : '') . '
(`ipstart` = :ip OR (:ip >= `ipstart` AND :ip <= `ipend`)) OR (``bans``.id = :id))
AND (`expires` IS NULL OR `expires` >= :curr_time)
ORDER BY `expires` IS NULL, `expires` DESC');
if ($board !== false) {
$query->bindValue(':board', $board, PDO::PARAM_STR);
}
$query->bindValue(':id', $ban_id);
$query->bindValue(':ip', inet_pton($ip));
$query->bindValue(':curr_time', time());
$query->execute() or error(db_error($query));
$ban_list = $query->fetchAll(PDO::FETCH_ASSOC);
array_walk($ban_list, function (&$ban, $_index) {
if ($ban['post']) {
$ban['post'] = json_decode($ban['post'], true);
}
$ban['mask'] = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
});
return $ban_list;
}
static public function range_to_string($mask) { static public function range_to_string($mask) {
list($ipstart, $ipend) = $mask; list($ipstart, $ipend) = $mask;
@ -56,7 +181,7 @@ class Bans {
$cidr = new CIDR($mask); $cidr = new CIDR($mask);
$range = $cidr->getRange(); $range = $cidr->getRange();
return array(inet_pton($range[0]), inet_pton($range[1])); return [ inet_pton($range[0]), inet_pton($range[1]) ];
} }
public static function parse_time($str) { public static function parse_time($str) {
@ -140,82 +265,25 @@ class Bans {
return false; return false;
} }
return array($ipstart, $ipend); return [$ipstart, $ipend];
} }
static public function findSingle(string $ip, int $ban_id, bool $require_ban_view): array|null { static public function findSingle(string $ip, int $ban_id, bool $require_ban_view, bool $auto_gc) {
/** if ($auto_gc) {
* Use OR in the query to also garbage collect bans. Ideally we should move the whole GC procedure to a separate return self::findSingleAutoGc($ip, $ban_id, $require_ban_view);
* script, but it will require a more important restructuring. } else {
*/ return self::findSingleNoGc($ban_id);
$query = prepare(
'SELECT ``bans``.* FROM ``bans``
WHERE ((`ipstart` = :ip OR (:ip >= `ipstart` AND :ip <= `ipend`)) OR (``bans``.id = :id))
ORDER BY `expires` IS NULL, `expires` DESC'
);
$query->bindValue(':id', $ban_id);
$query->bindValue(':ip', inet_pton($ip));
$query->execute() or error(db_error($query));
$found_ban = null;
$to_delete_list = [];
while ($ban = $query->fetch(PDO::FETCH_ASSOC)) {
if ($ban['expires'] && ($ban['seen'] || !$require_ban_view) && $ban['expires'] < time()) {
$to_delete_list[] = $ban['id'];
} elseif ($ban['id'] === $ban_id) {
if ($ban['post']) {
$ban['post'] = json_decode($ban['post'], true);
}
$ban['mask'] = self::range_to_string(array($ban['ipstart'], $ban['ipend']));
$ban['cmask'] = cloak_mask($ban['mask']);
$found_ban = $ban;
}
} }
self::deleteBans($to_delete_list);
return $found_ban;
} }
static public function find($ip, $board = false, $get_mod_info = false, $banid = null) { static public function find(?string $ip, $board = false, bool $get_mod_info = false, ?int $ban_id = null, bool $auto_gc = true) {
global $config; global $config;
$query = prepare('SELECT ``bans``.*' . ($get_mod_info ? ', `username`' : '') . ' FROM ``bans`` if ($auto_gc) {
' . ($get_mod_info ? 'LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`' : '') . ' return self::findAutoGc($ip, $board, $get_mod_info, $config['require_ban_view'], $ban_id);
WHERE } else {
(' . ($board !== false ? '(`board` IS NULL OR `board` = :board) AND' : '') . ' return self::findNoGc($ip, $board, $get_mod_info, $ban_id);
(`ipstart` = :ip OR (:ip >= `ipstart` AND :ip <= `ipend`)) OR (``bans``.id = :id))
ORDER BY `expires` IS NULL, `expires` DESC');
if ($board !== false)
$query->bindValue(':board', $board, PDO::PARAM_STR);
$query->bindValue(':id', $banid);
$query->bindValue(':ip', inet_pton($ip));
$query->execute() or error(db_error($query));
$ban_list = array();
$to_delete_list = [];
while ($ban = $query->fetch(PDO::FETCH_ASSOC)) {
if ($ban['expires'] && ($ban['seen'] || !$config['require_ban_view']) && $ban['expires'] < time()) {
$to_delete_list[] = $ban['id'];
} else {
if ($ban['post'])
$ban['post'] = json_decode($ban['post'], true);
$ban['mask'] = self::range_to_string(array($ban['ipstart'], $ban['ipend']));
$ban['cmask'] = cloak_mask($ban['mask']);
$ban_list[] = $ban;
}
} }
self::deleteBans($to_delete_list);
return $ban_list;
} }
static public function stream_json($out = false, $filter_ips = false, $filter_staff = false, $board_access = false) { static public function stream_json($out = false, $filter_ips = false, $filter_staff = false, $board_access = false) {
@ -231,8 +299,7 @@ class Bans {
$end = end($bans); $end = end($bans);
foreach ($bans as &$ban) { foreach ($bans as &$ban) {
$uncloaked_mask = self::range_to_string(array($ban['ipstart'], $ban['ipend'])); $ban['mask'] = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
$ban['mask'] = cloak_mask($uncloaked_mask);
if ($ban['post']) { if ($ban['post']) {
$post = json_decode($ban['post']); $post = json_decode($ban['post']);
@ -276,12 +343,24 @@ class Bans {
static public function seen($ban_id) { static public function seen($ban_id) {
$query = query("UPDATE ``bans`` SET `seen` = 1 WHERE `id` = " . (int)$ban_id) or error(db_error()); $query = query("UPDATE ``bans`` SET `seen` = 1 WHERE `id` = " . (int)$ban_id) or error(db_error());
rebuildThemes('bans'); Vichan\Functions\Theme\rebuild_themes('bans');
} }
static public function purge() { static public function purge($require_seen, $moratorium) {
$query = query("DELETE FROM ``bans`` WHERE `expires` IS NOT NULL AND `expires` < " . time() . " AND `seen` = 1") or error(db_error()); if ($require_seen) {
rebuildThemes('bans'); $query = prepare("DELETE FROM ``bans`` WHERE `expires` IS NOT NULL AND `expires` + :moratorium < :curr_time AND `seen` = 1");
} else {
$query = prepare("DELETE FROM ``bans`` WHERE `expires` IS NOT NULL AND `expires` + :moratorium < :curr_time");
}
$query->bindValue(':moratorium', $moratorium);
$query->bindValue(':curr_time', time());
$query->execute() or error(db_error($query));
$affected = $query->rowCount();
if ($affected > 0) {
Vichan\Functions\Theme\rebuild_themes('bans');
}
return $affected;
} }
static public function delete($ban_id, $modlog = false, $boards = false, $dont_rebuild = false) { static public function delete($ban_id, $modlog = false, $boards = false, $dont_rebuild = false) {
@ -299,8 +378,7 @@ class Bans {
if ($boards !== false && !in_array($ban['board'], $boards)) if ($boards !== false && !in_array($ban['board'], $boards))
error($config['error']['noaccess']); error($config['error']['noaccess']);
$mask = self::range_to_string(array($ban['ipstart'], $ban['ipend'])); $mask = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
$cloaked_mask = cloak_mask($mask);
modLog("Removed ban #{$ban_id} for " . modLog("Removed ban #{$ban_id} for " .
(filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$cloaked_mask\">$cloaked_mask</a>" : $cloaked_mask)); (filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$cloaked_mask\">$cloaked_mask</a>" : $cloaked_mask));
@ -308,7 +386,7 @@ class Bans {
query("DELETE FROM ``bans`` WHERE `id` = " . (int)$ban_id) or error(db_error()); query("DELETE FROM ``bans`` WHERE `id` = " . (int)$ban_id) or error(db_error());
if (!$dont_rebuild) rebuildThemes('bans'); if (!$dont_rebuild) Vichan\Functions\Theme\rebuild_themes('bans');
return true; return true;
} }
@ -365,23 +443,29 @@ class Bans {
openBoard($post['board']); openBoard($post['board']);
$post['board'] = $board['uri']; $post['board'] = $board['uri'];
/*
* The body can be so long to make the json longer than 64KBs, causing the query to fail.
* Truncate it to a safe length (32KBs). It could probably be longer, but if the deleted body is THAT big
* already, the likelihood of it being just assorted spam/garbage is about 101%.
*/
// We're on UTF-8 only, right...?
$post['body'] = mb_strcut($post['body'], 0, 32768);
$query->bindValue(':post', json_encode($post)); $query->bindValue(':post', json_encode($post));
} else } else
$query->bindValue(':post', null, PDO::PARAM_NULL); $query->bindValue(':post', null, PDO::PARAM_NULL);
$query->execute() or error(db_error($query)); $query->execute() or error(db_error($query));
if (isset($mod['id']) && $mod['id'] == $mod_id) {
modLog('Created a new ' .
($length > 0 ? preg_replace('/^(\d+) (\w+?)s?$/', '$1-$2', Format\until($length)) : 'permanent') .
' ban on ' .
($ban_board ? '/' . $ban_board . '/' : 'all boards') .
' for ' .
(filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$cloaked_mask\">$cloaked_mask</a>" : $cloaked_mask) .
' (<small>#' . $pdo->lastInsertId() . '</small>)' .
' with ' . ($reason ? 'reason: ' . utf8tohtml($reason) . '' : 'no reason'));
}
rebuildThemes('bans'); $ban_len = $length > 0 ? preg_replace('/^(\d+) (\w+?)s?$/', '$1-$2', Format\until($length)) : 'permanent';
$ban_board = $ban_board ? "/$ban_board/" : 'all boards';
$ban_ip = filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$cloaked_mask\">$cloaked_mask</a>" : $cloaked_mask;
$ban_id = $pdo->lastInsertId();
$ban_reason = $reason ? 'reason: ' . utf8tohtml($reason) : 'no reason';
modLog("Created a new $ban_len ban on $ban_board for $ban_ip (<small># $ban_id </small>) with $ban_reason");
Vichan\Functions\Theme\rebuild_themes('bans');
return $pdo->lastInsertId(); return $pdo->lastInsertId();
} }

View File

@ -4,164 +4,89 @@
* Copyright (c) 2010-2013 Tinyboard Development Group * Copyright (c) 2010-2013 Tinyboard Development Group
*/ */
use Vichan\Data\Driver\{CacheDriver, ApcuCacheDriver, ArrayCacheDriver, FsCacheDriver, MemcachedCacheDriver, NoneCacheDriver, RedisCacheDriver};
defined('TINYBOARD') or exit; defined('TINYBOARD') or exit;
class Cache { class Cache {
private static $cache; private static function buildCache(): CacheDriver {
public static function init() {
global $config; global $config;
switch ($config['cache']['enabled']) { switch ($config['cache']['enabled']) {
case 'memcached': case 'memcached':
self::$cache = new Memcached(); return new MemcachedCacheDriver(
self::$cache->addServers($config['cache']['memcached']); $config['cache']['prefix'],
break; $config['cache']['memcached']
);
case 'redis': case 'redis':
self::$cache = new Redis(); return new RedisCacheDriver(
self::$cache->connect($config['cache']['redis'][0], $config['cache']['redis'][1]); $config['cache']['prefix'],
if ($config['cache']['redis'][2]) { $config['cache']['redis'][0],
self::$cache->auth($config['cache']['redis'][2]); $config['cache']['redis'][1],
} $config['cache']['redis'][2],
self::$cache->select($config['cache']['redis'][3]) or die('cache select failure'); $config['cache']['redis'][3]
break; );
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': case 'php':
self::$cache = array(); default:
break; return new ArrayCacheDriver();
} }
} }
public static function getCache(): CacheDriver {
static $cache;
return $cache ??= self::buildCache();
}
public static function get($key) { public static function get($key) {
global $config, $debug; global $config, $debug;
$key = $config['cache']['prefix'] . $key; $ret = self::getCache()->get($key);
if ($ret === null) {
$data = false; $ret = false;
switch ($config['cache']['enabled']) {
case 'memcached':
if (!self::$cache)
self::init();
$data = self::$cache->get($key);
break;
case 'apcu':
$data = apcu_fetch($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;
} }
if ($config['debug']) if ($config['debug']) {
$debug['cached'][] = $key . ($data === false ? ' (miss)' : ' (hit)'); $debug['cached'][] = $config['cache']['prefix'] . $key . ($ret === false ? ' (miss)' : ' (hit)');
}
return $data; return $ret;
} }
public static function set($key, $value, $expires = false) { public static function set($key, $value, $expires = false) {
global $config, $debug; global $config, $debug;
$key = $config['cache']['prefix'] . $key; if (!$expires) {
if (!$expires)
$expires = $config['cache']['timeout']; $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 'apcu':
apcu_store($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']) self::getCache()->set($key, $value, $expires);
$debug['cached'][] = $key . ' (set)';
if ($config['debug']) {
$debug['cached'][] = $config['cache']['prefix'] . $key . ' (set)';
}
} }
public static function delete($key) { public static function delete($key) {
global $config, $debug; global $config, $debug;
$key = $config['cache']['prefix'] . $key; self::getCache()->delete($key);
switch ($config['cache']['enabled']) { if ($config['debug']) {
case 'memcached': $debug['cached'][] = $config['cache']['prefix'] . $key . ' (deleted)';
if (!self::$cache)
self::init();
self::$cache->delete($key);
break;
case 'redis':
if (!self::$cache)
self::init();
self::$cache->del($key);
break;
case 'apcu':
apcu_delete($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'][] = $key . ' (deleted)';
} }
public static function flush() { public static function flush() {
global $config; self::getCache()->flush();
switch ($config['cache']['enabled']) {
case 'memcached':
if (!self::$cache)
self::init();
return self::$cache->flush();
case 'apcu':
return apcu_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();
}
return false; return false;
} }
} }

View File

@ -68,18 +68,25 @@
// Deprecated, use 'log_system'. // Deprecated, use 'log_system'.
$config['syslog'] = false; $config['syslog'] = false;
$config['log_system'] = []; $config['log_system'] = [
// Log all error messages and unauthorized login attempts. /*
// Can be "syslog", "error_log" (default), "file", "stderr" or "none". * Log all error messages and unauthorized login attempts.
$config['log_system']['type'] = 'error_log'; * Can be "syslog", "error_log" (default), "file", or "stderr".
// The application name used by the logging system. Defaults to "tinyboard" for backwards compatibility. */
$config['log_system']['name'] = 'tinyboard'; 'type' => 'error_log',
// Only relevant if 'log_system' is set to "syslog". If true, double print the logs also in stderr. // The application name used by the logging system. Defaults to "tinyboard" for backwards compatibility.
// Defaults to false. 'name' => 'tinyboard',
$config['log_system']['syslog_stderr'] = false; /*
// Only relevant if "log_system" is set to `file`. Sets the file that vichan will log to. * Only relevant if 'log_system' is set to "syslog". If true, double print the logs also in stderr. Defaults to
// Defaults to '/var/log/vichan.log'. * false.
$config['log_system']['file_path'] = '/var/log/vichan.log'; */
'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. // Use `host` via shell_exec() to lookup hostnames, avoiding query timeouts. May not work on your system.
// Requires safe_mode to be disabled. // Requires safe_mode to be disabled.
@ -92,6 +99,11 @@
// to the environment path (seperated by :). // to the environment path (seperated by :).
$config['shell_path'] = '/usr/local/bin'; $config['shell_path'] = '/usr/local/bin';
// Automatically execute some maintenance tasks when some pages are opened, which may result in higher
// latencies.
// If set to false, ensure to periodically invoke the tools/maintenance.php script.
$config['auto_maintenance'] = true;
/* /*
* ==================== * ====================
* Database settings * Database settings
@ -127,17 +139,26 @@
/* /*
* On top of the static file caching system, you can enable the additional caching system which is * 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 * designed to minimize request processing can significantly increase speed when posting or using
* moderator interface. APC is the recommended method of caching. * the moderator interface.
* *
* https://github.com/vichan-devel/vichan/wiki/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'] = 'php';
// 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'; // $config['cache']['enabled'] = 'apcu';
// The Memcache server. Requires the memcached extension, with a final D.
// $config['cache']['enabled'] = 'memcached'; // $config['cache']['enabled'] = 'memcached';
// The Redis server. Requires the extension.
// $config['cache']['enabled'] = 'redis'; // $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'; // $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. // Timeout for cached objects such as posts and HTML.
$config['cache']['timeout'] = 60 * 60 * 48; // 48 hours $config['cache']['timeout'] = 60 * 60 * 48; // 48 hours
@ -205,6 +226,9 @@
// Used to salt secure tripcodes ("##trip") and poster IDs (if enabled). // Used to salt secure tripcodes ("##trip") and poster IDs (if enabled).
$config['secure_trip_salt'] = ')(*&^%$#@!98765432190zyxwvutsrqponmlkjihgfedcba'; $config['secure_trip_salt'] = ')(*&^%$#@!98765432190zyxwvutsrqponmlkjihgfedcba';
// Used to salt poster passwords.
$config['secure_password_salt'] = 'wKJSb7M5SyzMcFWD2gPO3j2RYUSO9B789!@#$%^&*()';
/* /*
* ==================== * ====================
* Flood/spam settings * Flood/spam settings
@ -251,83 +275,6 @@
// To prevent bump attacks; returns the thread to last position after the last post is deleted. // To prevent bump attacks; returns the thread to last position after the last post is deleted.
$config['anti_bump_flood'] = false; $config['anti_bump_flood'] = false;
/*
* Introduction to vichan's spam filter:
*
* In simple terms, whenever a posting form on a page is generated (which happens whenever a
* post is made), vichan will add a random amount of hidden, obscure fields to it to
* confuse bots and upset hackers. These fields and their respective obscure values are
* validated upon posting with a 160-bit "hash". That hash can only be used as many times
* as you specify; otherwise, flooding bots could just keep reusing the same hash.
* Once a new set of inputs (and the hash) are generated, old hashes for the same thread
* and board are set to expire. Because you have to reload the page to get the new set
* of inputs and hash, if they expire too quickly and more than one person is viewing the
* page at a given time, vichan would return false positives (depending on how long the
* user sits on the page before posting). If your imageboard is quite fast/popular, set
* $config['spam']['hidden_inputs_max_pass'] and $config['spam']['hidden_inputs_expire'] to
* something higher to avoid false positives.
*
* See also: https://github.com/vichan-devel/vichan/wiki/your_request_looks_automated
*
*/
// Number of hidden fields to generate.
$config['spam']['hidden_inputs_min'] = 4;
$config['spam']['hidden_inputs_max'] = 12;
// How many times can a "hash" be used to post?
$config['spam']['hidden_inputs_max_pass'] = 12;
// How soon after regeneration do hashes expire (in seconds)?
$config['spam']['hidden_inputs_expire'] = 60 * 60 * 3; // three hours
// Whether to use Unicode characters in hidden input names and values.
$config['spam']['unicode'] = true;
// These are fields used to confuse the bots. Make sure they aren't actually used by vichan, or it won't work.
$config['spam']['hidden_input_names'] = array(
'user',
'username',
'login',
'search',
'q',
'url',
'firstname',
'lastname',
'text',
'message'
);
// Always update this when adding new valid fields to the post form, or EVERYTHING WILL BE DETECTED AS SPAM!
$config['spam']['valid_inputs'] = array(
'hash',
'board',
'thread',
'mod',
'name',
'email',
'subject',
'post',
'body',
'password',
'sticky',
'lock',
'raw',
'embed',
'g-recaptcha-response',
'h-captcha-response',
'captcha_cookie',
'captcha_text',
'spoiler',
'page',
'file_url',
'json_response',
'user_flag',
'no_country',
'tag',
'simple_spam'
);
// Enable simple anti-spam measure. Requires the end-user to answer a question before making a post. // Enable simple anti-spam measure. Requires the end-user to answer a question before making a post.
// Works very well against uncustomized spam. Answers are case-insensitive. // Works very well against uncustomized spam. Answers are case-insensitive.
// $config['simple_spam'] = array ( // $config['simple_spam'] = array (
@ -336,39 +283,39 @@
//); //);
$config['simple_spam'] = false; $config['simple_spam'] = false;
// Enable reCaptcha to make spam even harder. Rarely necessary. $config['captcha'] = [
$config['recaptcha'] = false; // Can be false, 'recaptcha', 'hcaptcha' or 'native'.
'provider' => false,
// Public and private key pair from https://www.google.com/recaptcha/admin/create /*
$config['recaptcha_public'] = '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f'; * If not false, the captcha is dynamically injected on the client if the web server set the `captcha-required`
$config['recaptcha_private'] = '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_'; * cookie to 1. The configuration value should be set the IP for which the captcha should be verified.
*
// Enable hCaptcha as an alternative to reCAPTCHA. * Example:
$config['hcaptcha'] = false; *
* // Verify the captcha for users sending posts from the loopback address.
// Public and private key pair for using hCaptcha. * $config['captcha']['dynamic'] = '127.0.0.1';
$config['hcaptcha_public'] = '7a4b21e0-dc53-46f2-a9f8-91d2e74b63a0'; */
$config['hcaptcha_private'] = '0x4e9A01bE637b51dC41a7Ea9865C3fDe4aB72Cf17'; 'dynamic' => false,
'recaptcha' => [
// Enable Custom Captcha you need to change a couple of settings 'sitekey' => '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI',
//Read more at: /inc/captcha/readme.md 'secret' => '6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe',
$config['captcha'] = array(); ],
'hcaptcha' => [
// Enable custom captcha provider 'sitekey' => '10000000-ffff-ffff-ffff-000000000001',
$config['captcha']['enabled'] = false; 'secret' => '0x0000000000000000000000000000000000000000',
],
//New thread captcha // To enable the native captcha you need to change a couple of settings. Read more at: /inc/captcha/readme.md
//Require solving a captcha to post a thread. 'native' => [
//Default off. // Custom captcha get provider path (if not working get the absolute path aka your url).
$config['new_thread_capt'] = false; 'provider_get' => '/inc/captcha/entrypoint.php',
// Custom captcha check provider path
// Custom captcha get provider path (if not working get the absolute path aka your url.) 'provider_check' => '/inc/captcha/entrypoint.php',
$config['captcha']['provider_get'] = '../inc/captcha/entrypoint.php'; // Custom captcha extra field (eg. charset)
// Custom captcha check provider path 'extra' => 'abcdefghijklmnopqrstuvwxyz',
$config['captcha']['provider_check'] = '../inc/captcha/entrypoint.php'; // New thread captcha. Require solving a captcha to post a thread.
'new_thread_capt' => false
// Custom captcha extra field (eg. charset) ]
$config['captcha']['extra'] = 'abcdefghijklmnopqrstuvwxyz'; ];
// Ability to lock a board for normal users and still allow mods to post. Could also be useful for making an archive board // 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; $config['board_locked'] = false;
@ -508,6 +455,17 @@
// 'action' => 'reject' // 'action' => 'reject'
// ); // );
// Example: Expand shortened links in a post, looking for and blocking URLs that lead to an unwanted
// endpoint. Many botspam posts include a variety of shortened URLs which all point to the same few
// webhosts. You can use this filter to block the endpoint webhost instead of just the apparent URL.
// $config['filters'][] = array(
// 'condition' => array(
// 'unshorten' => '/endpoint.net/i',
// ),
// 'action' => 'reject',
// 'message' => 'None of that, please.'
// );
// Filter flood prevention conditions ("flood-match") depend on a table which contains a cache of recent // Filter flood prevention conditions ("flood-match") depend on a table which contains a cache of recent
// posts across all boards. This table is automatically purged of older posts, determining the maximum // posts across all boards. This table is automatically purged of older posts, determining the maximum
// "age" by looking at each filter. However, when determining the maximum age, vichan does not look // "age" by looking at each filter. However, when determining the maximum age, vichan does not look
@ -686,6 +644,9 @@
); );
*/ */
// Maximum number inline of dice rolls per markup.
$config['max_roll_count'] = 50;
// Allow dice rolling: an email field of the form "dice XdY+/-Z" will result in X Y-sided dice rolled and summed, // Allow dice rolling: an email field of the form "dice XdY+/-Z" will result in X Y-sided dice rolled and summed,
// with the modifier Z added, with the result displayed at the top of the post body. // with the modifier Z added, with the result displayed at the top of the post body.
$config['allow_roll'] = false; $config['allow_roll'] = false;
@ -733,6 +694,9 @@
//); //);
$config['premade_ban_reasons'] = false; $config['premade_ban_reasons'] = false;
// How often (minimum) to purge the ban list of expired bans (which have been seen).
$config['purge_bans'] = 60 * 60 * 12; // 12 hours
// Allow users to appeal bans through vichan. // Allow users to appeal bans through vichan.
$config['ban_appeals'] = false; $config['ban_appeals'] = false;
@ -754,11 +718,15 @@
* ==================== * ====================
*/ */
// "Wiki" markup syntax ($config['wiki_markup'] in pervious versions): $config['markup'] = [
$config['markup'][] = array("/'''(.+?)'''/", "<strong>\$1</strong>"); // Inline dice roll markup.
$config['markup'][] = array("/''(.+?)''/", "<em>\$1</em>"); [ "/!([-+]?\d+)?([d])([-+]?\d+)([-+]\d+)?/iu", fn($m) => inline_dice_roll_markup($m, 'static/d10.svg') ],
$config['markup'][] = array("/\*\*(.+?)\*\*/", "<span class=\"spoiler\">\$1</span>"); // "Wiki" markup syntax ($config['wiki_markup'] in pervious versions):
$config['markup'][] = array("/^[ |\t]*==(.+?)==[ |\t]*$/m", "<span class=\"heading\">\$1</span>"); [ "/'''(.+?)'''/", "<strong>\$1</strong>" ],
[ "/''(.+?)''/", "<em>\$1</em>" ],
[ "/\*\*(.+?)\*\*/", "<span class=\"spoiler\">\$1</span>" ],
[ "/^[ |\t]*==(.+?)==[ |\t]*$/m", "<span class=\"heading\">\$1</span>" ],
];
// Code markup. This should be set to a regular expression, using tags you want to use. Examples: // Code markup. This should be set to a regular expression, using tags you want to use. Examples:
// "/\[code\](.*?)\[\/code\]/is" // "/\[code\](.*?)\[\/code\]/is"
@ -863,12 +831,14 @@
$config['ie_mime_type_detection'] = '/<(?:body|head|html|img|plaintext|pre|script|table|title|a href|channel|scriptlet)/i'; $config['ie_mime_type_detection'] = '/<(?:body|head|html|img|plaintext|pre|script|table|title|a href|channel|scriptlet)/i';
// Allowed image file extensions. // Allowed image file extensions.
$config['allowed_ext'][] = 'jpg'; $config['allowed_ext'] = [
$config['allowed_ext'][] = 'jpeg'; 'jpg',
$config['allowed_ext'][] = 'bmp'; 'jpeg',
$config['allowed_ext'][] = 'gif'; 'bmp',
$config['allowed_ext'][] = 'png'; 'gif',
$config['allowed_ext'][] = 'webp'; 'png',
'webp'
];
// $config['allowed_ext'][] = 'svg'; // $config['allowed_ext'][] = 'svg';
// Allowed extensions for OP. Inherits from the above setting if set to false. Otherwise, it overrides both allowed_ext and // Allowed extensions for OP. Inherits from the above setting if set to false. Otherwise, it overrides both allowed_ext and
@ -886,10 +856,12 @@
// }; // };
// Thumbnail to use for the non-image file uploads. // Thumbnail to use for the non-image file uploads.
$config['file_icons']['default'] = 'file.png'; $config['file_icons'] = [
$config['file_icons']['zip'] = 'zip.png'; 'default' => 'file.png',
$config['file_icons']['webm'] = 'video.png'; 'zip' => 'zip.png',
$config['file_icons']['mp4'] = 'video.png'; 'webm' => 'video.png',
'mp4' => 'video.png'
];
// Example: Custom thumbnail for certain file extension. // Example: Custom thumbnail for certain file extension.
// $config['file_icons']['extension'] = 'some_file.png'; // $config['file_icons']['extension'] = 'some_file.png';
@ -921,11 +893,13 @@
$config['show_filename'] = true; $config['show_filename'] = true;
// WebM Settings // WebM Settings
$config['webm']['use_ffmpeg'] = false; $config['webm'] = [
$config['webm']['allow_audio'] = false; 'use_ffmpeg' => false,
$config['webm']['max_length'] = 120; 'allow_audio' => false,
$config['webm']['ffmpeg_path'] = 'ffmpeg'; 'max_length' => 120,
$config['webm']['ffprobe_path'] = 'ffprobe'; 'ffmpeg_path' => 'ffmpeg',
'ffprobe_path' => 'ffprobe'
];
// Display image identification links for ImgOps, regex.info/exif, Google Images and iqdb. // Display image identification links for ImgOps, regex.info/exif, Google Images and iqdb.
$config['image_identification'] = false; $config['image_identification'] = false;
@ -1034,8 +1008,11 @@
// Custom stylesheets available for the user to choose. See the "stylesheets/" folder for a list of // Custom stylesheets available for the user to choose. See the "stylesheets/" folder for a list of
// available stylesheets (or create your own). // available stylesheets (or create your own).
$config['stylesheets']['Yotsuba B'] = ''; // Default; there is no additional/custom stylesheet for this. $config['stylesheets'] = [
$config['stylesheets']['Yotsuba'] = 'yotsuba.css'; // Default; there is no additional/custom stylesheet for this.
'Yotsuba B' => '',
'Yotsuba' => 'yotsuba.css'
];
// $config['stylesheets']['Futaba'] = 'futaba.css'; // $config['stylesheets']['Futaba'] = 'futaba.css';
// $config['stylesheets']['Dark'] = 'dark.css'; // $config['stylesheets']['Dark'] = 'dark.css';
@ -1117,6 +1094,10 @@
// <tinyboard flag style>. // <tinyboard flag style>.
$config['flag_style'] = 'width:16px;height:11px;'; $config['flag_style'] = 'width:16px;height:11px;';
// Lazy loading
// https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading
$config['content_lazy_loading'] = false;
/* /*
* ==================== * ====================
* Javascript * Javascript
@ -1149,6 +1130,10 @@
// Minify Javascript using http://code.google.com/p/minify/. // Minify Javascript using http://code.google.com/p/minify/.
$config['minify_js'] = false; $config['minify_js'] = false;
// Version number for main.js (or $config['url_javascript']).
// You can use this to bypass the user's browsers and CDN caches.
$config['resource_version'] = 0;
// Dispatch thumbnail loading and image configuration with JavaScript. It will need a certain javascript // Dispatch thumbnail loading and image configuration with JavaScript. It will need a certain javascript
// code to work. // code to work.
$config['javascript_image_dispatch'] = false; $config['javascript_image_dispatch'] = false;
@ -1165,10 +1150,10 @@
// Custom embedding (YouTube, vimeo, etc.) // Custom embedding (YouTube, vimeo, etc.)
// It's very important that you match the entire input (with ^ and $) or things will not work correctly. // It's very important that you match the entire input (with ^ and $) or things will not work correctly.
// Be careful when creating a new embed, because depending on the URL you end up exposing yourself to an XSS. // Be careful when creating a new embed, because depending on the URL you end up exposing yourself to an XSS.
$config['embedding'] = array( $config['embedding'] = array(
array( array(
'/^https?:\/\/(\w+\.)?youtube\.com\/watch\?v=([a-zA-Z0-9\-_]{10,11})?$/i', '/^https?:\/\/(\w+\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([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="https://www.youtube.com/embed/$2"></iframe>' '<iframe style="float: left; margin: 10px 20px;" width="%%tb_width%%" height="%%tb_height%%" frameborder="0" id="ytplayer" src="https://www.youtube.com/embed/$3"></iframe>'
), ),
array( array(
'/^https?:\/\/(\w+\.)?vimeo\.com\/(\d{2,10})(\?.+)?$/i', '/^https?:\/\/(\w+\.)?vimeo\.com\/(\d{2,10})(\?.+)?$/i',
@ -1179,13 +1164,37 @@
'<iframe style="float: left; margin: 10px 20px;" width="%%tb_width%%" height="%%tb_height%%" frameborder="0" src="https://www.dailymotion.com/embed/video/$2" allowfullscreen></iframe>' '<iframe style="float: left; margin: 10px 20px;" width="%%tb_width%%" height="%%tb_height%%" frameborder="0" src="https://www.dailymotion.com/embed/video/$2" allowfullscreen></iframe>'
), ),
array( array(
'/^https?:\/\/(\w+\.)?metacafe\.com\/watch\/(\d+)\/([a-zA-Z0-9_\-.]+)\/(\?[^\'"<>]+)?$/i', '/^https?:\/\/(www\.)?rumble\.com\/embed\/([a-zA-Z0-9]+)(\/\?[^\'"<>]*)?$/i',
'<iframe style="float: left; margin: 10px 20px;" width="%%tb_width%%" height="%%tb_height%%" frameborder="0" src="https://www.metacafe.com/embed/$2/$3/" allowfullscreen></iframe>' '<iframe class="rumble" width="%%tb_width%%" height="%%tb_height%%" src="https://rumble.com/embed/$2/" frameborder="0" allowfullscreen></iframe>'
), ),
array( array(
'/^https?:\/\/(\w+\.)?vocaroo\.com\/([a-zA-Z0-9]{2,12})$/i', '/^https?:\/\/(www\.)?bitchute\.com\/(?:video|embed)\/([a-zA-Z0-9]+)(\/)?(\?[^\'"<>]*)?$/i',
'<iframe style="float: left; margin: 10px 20px;" width="300" height="60" frameborder="0" src="https://vocaroo.com/embed/$2"></iframe>' '<iframe allowfullscreen="true" width="%%tb_width%%" height="%%tb_height%%" scrolling="no" frameborder="0" style="border: none;" src="https://www.bitchute.com/embed/$2"></iframe>'
) ),
array(
'/^https?:\/\/(?:www\.)?odysee\.com\/(?:@[^\/]+\/)?([-a-zA-Z0-9_]+:[a-zA-Z0-9]+)(\/)?(\?[^\'"<>]*)?$/i',
'<iframe width="%%tb_width%%" height="%%tb_height%%" src="https://odysee.com/$/embed/$1" allowfullscreen></iframe>'
),
array(
'/^https?:\/\/(www\.)?kick\.com\/([a-zA-Z0-9_]+)(\?[^\'"<>]*)?$/i',
'<iframe src="https://player.kick.com/$2" height="%%tb_height%%" width="%%tb_width%%" frameborder="0" scrolling="no" allowfullscreen="true"></iframe>'
),
/*
//Both TikTok and Instagram are commented out since they contain some extra scripting you might not want natively on your website.
array(
'/^https?:\/\/(www\.)?tiktok\.com\/@([a-zA-Z0-9_.]+)\/video\/([0-9]+)(\?[^\'"<>]*)?$/i',
'<blockquote class="tiktok-embed" cite="https://www.tiktok.com/@$2/video/$3" data-video-id="$3" style="max-width: %%tb_width%%px;min-width: 325px;"><section></section></blockquote><script async src="https://www.tiktok.com/embed.js"></script>'
),
array(
'/^https?:\/\/(www\.)?instagram\.com\/(p|reel|tv)\/([a-zA-Z0-9_-]+)(\/)?(\?[^\'"<>]*)?$/i',
'<blockquote class="instagram-media" data-instgrm-permalink="https://www.instagram.com/$2/$3/" data-instgrm-version="14" style="max-width: %%tb_width%%px; min-width: 326px; width: 100%;"></blockquote><script async src="//www.instagram.com/embed.js"></script>'
),
*/
array(
'/^https?:\/\/(\w+\.)?(vocaroo\.com\/|voca\.ro\/)([a-zA-Z0-9]{2,12})$/i',
'<iframe style="float: left; margin: 10px 20px;" width="300" height="60" frameborder="0" src="https://vocaroo.com/embed/$3"></iframe>'
),
); );
// Embedding width and height. // Embedding width and height.
@ -1198,86 +1207,86 @@
* ==================== * ====================
*/ */
// Error messages $config['error'] = [
$config['error']['bot'] = _('You look like a bot.'); // General error messages
$config['error']['referer'] = _('Your browser sent an invalid or no HTTP referer.'); 'bot' => _('You look like a bot.'),
$config['error']['toolong'] = _('The %s field was too long.'); 'referer' => _('Your browser sent an invalid or no HTTP referer.'),
$config['error']['toolong_body'] = _('The body was too long.'); 'toolong' => _('The %s field was too long.'),
$config['error']['tooshort_body'] = _('The body was too short or empty.'); 'toolong_body' => _('The body was too long.'),
$config['error']['toomanylines'] = _('Your post contains too many lines!'); 'tooshort_body' => _('The body was too short or empty.'),
$config['error']['noimage'] = _('You must upload an image.'); 'toomanylines' => _('Your post contains too many lines!'),
$config['error']['toomanyimages'] = _('You have attempted to upload too many images!'); 'noimage' => _('You must upload an image.'),
$config['error']['nomove'] = _('The server failed to handle your upload.'); 'toomanyimages' => _('You have attempted to upload too many images!'),
$config['error']['fileext'] = _('Unsupported image format.'); 'nomove' => _('The server failed to handle your upload.'),
$config['error']['noboard'] = _('Invalid board!'); 'fileext' => _('Unsupported image format.'),
$config['error']['nonexistant'] = _('Thread specified does not exist.'); 'noboard' => _('Invalid board!'),
$config['error']['nopost'] = _('Post specified does not exist.'); 'nonexistant' => _('Thread specified does not exist.'),
$config['error']['locked'] = _('Thread locked. You may not reply at this time.'); 'nopost' => _('Post specified does not exist.'),
$config['error']['reply_hard_limit'] = _('Thread has reached its maximum reply limit.'); 'locked' => _('Thread locked. You may not reply at this time.'),
$config['error']['image_hard_limit'] = _('Thread has reached its maximum image limit.'); 'reply_hard_limit' => _('Thread has reached its maximum reply limit.'),
$config['error']['nopost'] = _('You didn\'t make a post.'); 'image_hard_limit' => _('Thread has reached its maximum image limit.'),
$config['error']['flood'] = _('Flood detected; Post discarded.'); 'nopost' => _('You didn\'t make a post.'),
$config['error']['too_many_threads'] = _('The hourly thread limit has been reached. Please post in an existing thread.'); 'flood' => _('Flood detected; Post discarded.'),
$config['error']['spam'] = _('Your request looks automated; Post discarded.'); 'too_many_threads' => _('The hourly thread limit has been reached. Please post in an existing thread.'),
$config['error']['simple_spam'] = _('You must answer the question to make a new thread. See the last field.'); 'spam' => _('Your request looks automated; Post discarded.'),
$config['error']['unoriginal'] = _('Unoriginal content!'); 'simple_spam' => _('You must answer the question to make a new thread. See the last field.'),
$config['error']['muted'] = _('Unoriginal content! You have been muted for %d seconds.'); 'unoriginal' => _('Unoriginal content!'),
$config['error']['youaremuted'] = _('You are muted! Expires in %d seconds.'); 'muted' => _('Unoriginal content! You have been muted for %d seconds.'),
$config['error']['dnsbl'] = _('Your IP address is listed in %s.'); 'youaremuted' => _('You are muted! Expires in %d seconds.'),
$config['error']['toomanylinks'] = _('Too many links; flood detected.'); 'dnsbl' => _('Your IP address is listed in %s.'),
$config['error']['toomanycites'] = _('Too many cites; post discarded.'); 'toomanylinks' => _('Too many links; flood detected.'),
$config['error']['toomanycross'] = _('Too many cross-board links; post discarded.'); 'toomanycites' => _('Too many cites; post discarded.'),
$config['error']['nodelete'] = _('You didn\'t select anything to delete.'); 'toomanycross' => _('Too many cross-board links; post discarded.'),
$config['error']['noreport'] = _('You didn\'t select anything to report.'); 'nodelete' => _('You didn\'t select anything to delete.'),
$config['error']['toolongreport'] = _('The reason was too long.'); 'noreport' => _('You didn\'t select anything to report.'),
$config['error']['toomanyreports'] = _('You can\'t report that many posts at once.'); 'toolongreport' => _('The reason was too long.'),
$config['error']['noban'] = _('That ban doesn\'t exist or is not for you.'); 'toomanyreports' => _('You can\'t report that many posts at once.'),
$config['error']['tooshortban'] = _('You cannot appeal a ban of this length.'); 'noban' => _('That ban doesn\'t exist or is not for you.'),
$config['error']['toolongappeal'] = _('The appeal was too long.'); 'tooshortban' => _('You cannot appeal a ban of this length.'),
$config['error']['toomanyappeals'] = _('You cannot appeal this ban again.'); 'toolongappeal' => _('The appeal was too long.'),
$config['error']['pendingappeal'] = _('There is already a pending appeal for this ban.'); 'toomanyappeals' => _('You cannot appeal this ban again.'),
$config['error']['invalidpassword'] = _('Wrong password…'); 'pendingappeal' => _('There is already a pending appeal for this ban.'),
$config['error']['invalidimg'] = _('Invalid image.'); 'invalidpassword' => _('Wrong password…'),
$config['error']['phpfileserror'] = _('Upload failure (file #%index%): Error code %code%. Refer to <a href="http://php.net/manual/en/features.file-upload.errors.php">http://php.net/manual/en/features.file-upload.errors.php</a>; post discarded.'); 'invalidimg' => _('Invalid image.'),
$config['error']['unknownext'] = _('Unknown file extension.'); 'phpfileserror' => _('Upload failure (file #%index%): Error code %code%. Refer to <a href=>"http://php.net/manual/en/features.file-upload.errors.php">http://php.net/manual/en/features.file-upload.errors.php</a>; post discarded.'),
$config['error']['filesize'] = _('Maximum file size: %maxsz% bytes<br>Your file\'s size: %filesz% bytes'); 'unknownext' => _('Unknown file extension.'),
$config['error']['maxsize'] = _('The file was too big.'); 'filesize' => _('Maximum file size: %maxsz% bytes<br>Your file\'s size: %filesz% bytes'),
$config['error']['genwebmerror'] = _('There was a problem processing your webm.'); 'maxsize' => _('The file was too big.'),
$config['error']['webmerror'] = _('There was a problem processing your webm.');//Is this error used anywhere ? 'genwebmerror' => _('There was a problem processing your webm.'),
$config['error']['invalidwebm'] = _('Invalid webm uploaded.'); 'invalidwebm' => _('Invalid webm uploaded.'),
$config['error']['webmhasaudio'] = _('The uploaded webm contains an audio or another type of additional stream.'); 'webmhasaudio' => _('The uploaded webm contains an audio or another type of additional stream.'),
$config['error']['webmtoolong'] =_('The uploaded webm is longer than %d seconds.'); 'webmtoolong' =>_('The uploaded webm is longer than %d seconds.'),
$config['error']['fileexists'] = _('That file <a href="%s">already exists</a>!'); 'fileexists' => _('That file <a href=>"%s">already exists</a>!'),
$config['error']['fileexistsinthread'] = _('That file <a href="%s">already exists</a> in this thread!'); 'fileexistsinthread' => _('That file <a href=>"%s">already exists</a> in this thread!'),
$config['error']['delete_too_soon'] = _('You\'ll have to wait another %s before deleting that.'); 'delete_too_soon' => _('You\'ll have to wait another %s before deleting that.'),
$config['error']['delete_too_late'] = _('You cannot delete a post this old.'); 'delete_too_late' => _('You cannot delete a post this old.'),
$config['error']['mime_exploit'] = _('MIME type detection XSS exploit (IE) detected; post discarded.'); 'mime_exploit' => _('MIME type detection XSS exploit (IE) detected; post discarded.'),
$config['error']['invalid_embed'] = _('Couldn\'t make sense of the URL of the video you tried to embed.'); 'invalid_embed' => _('Couldn\'t make sense of the URL of the video you tried to embed.'),
$config['error']['captcha'] = _('You seem to have mistyped the verification.'); 'captcha' => _('You seem to have mistyped the verification.'),
$config['error']['flag_undefined'] = _('The flag %s is undefined, your PHP version is too old!'); 'flag_undefined' => _('The flag %s is undefined, your PHP version is too old!'),
$config['error']['flag_wrongtype'] = _('defined_flags_accumulate(): The flag %s is of the wrong type!'); 'flag_wrongtype' => _('defined_flags_accumulate(): The flag %s is of the wrong type!'),
$config['error']['remote_io_error'] = _('IO error while interacting with a remote service.'); 'remote_io_error' => _('IO error while interacting with a remote service.'),
$config['error']['local_io_error'] = _('IO error while interacting with a local resource or service.'); 'local_io_error' => _('IO error while interacting with a local resource or service.'),
// Moderator errors
// Moderator errors 'toomanyunban' => _('You are only allowed to unban %s users at a time. You tried to unban %u users.'),
$config['error']['toomanyunban'] = _('You are only allowed to unban %s users at a time. You tried to unban %u users.'); 'invalid' => _('Invalid username and/or password.'),
$config['error']['invalid'] = _('Invalid username and/or password.'); 'insecure' => _('Login on insecure connections is disabled.'),
$config['error']['insecure'] = _('Login on insecure connections is disabled.'); 'notamod' => _('You are not a mod…'),
$config['error']['notamod'] = _('You are not a mod…'); 'invalidafter' => _('Invalid username and/or password. Your user may have been deleted or changed.'),
$config['error']['invalidafter'] = _('Invalid username and/or password. Your user may have been deleted or changed.'); 'malformed' => _('Invalid/malformed cookies.'),
$config['error']['malformed'] = _('Invalid/malformed cookies.'); 'missedafield' => _('Your browser didn\'t submit an input when it should have.'),
$config['error']['missedafield'] = _('Your browser didn\'t submit an input when it should have.'); 'required' => _('The %s field is required.'),
$config['error']['required'] = _('The %s field is required.'); 'invalidfield' => _('The %s field was invalid.'),
$config['error']['invalidfield'] = _('The %s field was invalid.'); 'boardexists' => _('There is already a %s board.'),
$config['error']['boardexists'] = _('There is already a %s board.'); 'noaccess' => _('You don\'t have permission to do that.'),
$config['error']['noaccess'] = _('You don\'t have permission to do that.'); 'invalidpost' => _('That post doesn\'t exist…'),
$config['error']['invalidpost'] = _('That post doesn\'t exist…'); '404' => _('Page not found.'),
$config['error']['404'] = _('Page not found.'); 'modexists' => _('That mod <a href="?/users/%d">already exists</a>!'),
$config['error']['modexists'] = _('That mod <a href="?/users/%d">already exists</a>!'); 'invalidtheme' => _('That theme doesn\'t exist!'),
$config['error']['invalidtheme'] = _('That theme doesn\'t exist!'); 'csrf' => _('Invalid security token! Please go back and try again.'),
$config['error']['csrf'] = _('Invalid security token! Please go back and try again.'); 'badsyntax' => _('Your code contained PHP syntax errors. Please go back and correct them. PHP says: ')
$config['error']['badsyntax'] = _('Your code contained PHP syntax errors. Please go back and correct them. PHP says: '); ];
/* /*
* ========================= * =========================
@ -1299,8 +1308,8 @@
// The scheme and domain. This is used to get the site's absolute URL (eg. for image identification links). // The scheme and domain. This is used to get the site's absolute URL (eg. for image identification links).
// If you use the CLI tools, it would be wise to override this setting. // If you use the CLI tools, it would be wise to override this setting.
$config['domain'] = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') ? 'https://' : 'http://'; $config['domain'] = ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') ? 'https://' : 'http://')
$config['domain'] .= isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost'; . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost');
// If for some reason the folders and static HTML index files aren't in the current working direcotry, // If for some reason the folders and static HTML index files aren't in the current working direcotry,
// enter the directory path here. Otherwise, keep it false. // enter the directory path here. Otherwise, keep it false.
@ -1380,22 +1389,22 @@
// Board directory, followed by a forward-slash (/). // Board directory, followed by a forward-slash (/).
$config['board_path'] = '%s/'; $config['board_path'] = '%s/';
// Misc directories. // Misc directories.
$config['dir']['img'] = 'src/'; $config['dir'] = [
$config['dir']['thumb'] = 'thumb/'; 'img' => 'src/',
$config['dir']['res'] = 'res/'; 'thumb' => 'thumb/',
'res' => 'res/',
// For load balancing, having a seperate server (and domain/subdomain) for serving static content is // For load balancing, having a seperate server (and domain/subdomain) for serving static content is
// possible. This can either be a directory or a URL. Defaults to $config['root'] . 'static/'. // possible. This can either be a directory or a URL. Defaults to $config['root'] . 'static/'.
// $config['dir']['static'] = 'http://static.example.org/'; // $config['dir']['static'] = 'http://static.example.org/';
// Where to store the .html templates. This folder and the template files must exist.
// Where to store the .html templates. This folder and the template files must exist. 'template' => getcwd() . '/templates',
$config['dir']['template'] = getcwd() . '/templates'; // Location of vichan "themes".
// Location of vichan "themes". 'themes' => getcwd() . '/templates/themes',
$config['dir']['themes'] = getcwd() . '/templates/themes'; // Same as above, but a URI (accessable by web interface).
// Same as above, but a URI (accessable by web interface). 'themes_uri' => 'templates/themes',
$config['dir']['themes_uri'] = 'templates/themes'; // Home directory. Used by themes.
// Home directory. Used by themes. 'home' => ''
$config['dir']['home'] = ''; ];
// Location of a blank 1x1 gif file. Only used when country_flags_condensed is enabled // Location of a blank 1x1 gif file. Only used when country_flags_condensed is enabled
// $config['image_blank'] = 'static/blank.gif'; // $config['image_blank'] = 'static/blank.gif';
@ -1468,13 +1477,19 @@
// 5. enable smart_build_helper (see below) // 5. enable smart_build_helper (see below)
// 6. edit the strategies (see inc/functions.php for the builtin ones). You can use lambdas. I will test // 6. edit the strategies (see inc/functions.php for the builtin ones). You can use lambdas. I will test
// various ones and include one that works best for me. // various ones and include one that works best for me.
$config['generation_strategies'] = array(); $config['generation_strategies'] = [
// Add a sane strategy. It forces to immediately generate a page user is about to land on. Otherwise, /*
// it has no opinion, so it needs a fallback strategy. * Add a sane strategy. It forces to immediately generate a page user is about to land on. Otherwise,
$config['generation_strategies'][] = 'strategy_sane'; * it has no opinion, so it needs a fallback strategy.
// Add an immediate catch-all strategy. This is the default function of imageboards: generate all pages */
// on post time. 'strategy_sane',
$config['generation_strategies'][] = 'strategy_immediate'; /*
* Add an immediate catch-all strategy. This is the default function of imageboards: generate all pages
* on post time.
*/
'strategy_immediate',
];
// NOT RECOMMENDED: Instead of an all-"immediate" strategy, you can use an all-"build_on_load" one (used // NOT RECOMMENDED: Instead of an all-"immediate" strategy, you can use an all-"build_on_load" one (used
// to be initialized using $config['smart_build']; ) for all pages instead of those to be build // to be initialized using $config['smart_build']; ) for all pages instead of those to be build
// immediately. A rebuild done in this mode should remove all your static files // immediately. A rebuild done in this mode should remove all your static files
@ -1491,7 +1506,7 @@
$config['page_404'] = '/404.html'; $config['page_404'] = '/404.html';
// Extra controller entrypoints. Controller is used only by smart_build and advanced build. // Extra controller entrypoints. Controller is used only by smart_build and advanced build.
$config['controller_entrypoints'] = array(); $config['controller_entrypoints'] = [];
/* /*
* ==================== * ====================
@ -1499,33 +1514,84 @@
* ==================== * ====================
*/ */
// Limit how many bans can be removed via the ban list. Set to false (or zero) for no limit.
$config['mod']['unban_limit'] = false;
// Whether or not to lock moderator sessions to IP addresses. This makes cookie theft ineffective.
$config['mod']['lock_ip'] = true;
// The page that is first shown when a moderator logs in. Defaults to the dashboard (?/).
$config['mod']['default'] = '/';
// Mod links (full HTML). // Mod links (full HTML).
$config['mod']['link_delete'] = '[D]'; $config['mod'] = [
$config['mod']['link_ban'] = '[B]'; // Limit how many bans can be removed via the ban list. Set to false (or zero) for no limit.
$config['mod']['link_bandelete'] = '[B&amp;D]'; 'unban_limit' => false,
$config['mod']['link_deletefile'] = '[F]'; // Whether or not to lock moderator sessions to IP addresses. This makes cookie theft less effective.
$config['mod']['link_spoilerimage'] = '[S]'; 'lock_ip' => true,
$config['mod']['link_deletebyip'] = '[D+]'; // The page that is first shown when a moderator logs in. Defaults to the dashboard (?/).
$config['mod']['link_deletebyip_global'] = '[D++]'; 'default' => '/',
$config['mod']['link_sticky'] = '[Sticky]'; // Do DNS lookups on IP addresses to get their hostname for the moderator IP pages (?/IP/x.x.x.x).
$config['mod']['link_desticky'] = '[-Sticky]'; 'dns_lookup' => true,
$config['mod']['link_lock'] = '[Lock]'; // How many recent posts, per board, to show in ?/IP/x.x.x.x.
$config['mod']['link_unlock'] = '[-Lock]'; 'ip_recentposts' => 5,
$config['mod']['link_bumplock'] = '[Sage]'; // Number of posts to display on the reports page.
$config['mod']['link_bumpunlock'] = '[-Sage]'; 'recent_reports' => 10,
$config['mod']['link_editpost'] = '[Edit]'; // Number of actions to show per page in the moderation log.
$config['mod']['link_move'] = '[Move]'; 'modlog_page' => 350,
$config['mod']['link_cycle'] = '[Cycle]'; // Number of bans to show per page in the ban list.
$config['mod']['link_uncycle'] = '[-Cycle]'; 'banlist_page'=> 350,
// Number of news entries to display per page.
'news_page' => 40,
// Number of results to display per page.
'search_page' => 200,
// Number of entries to show per page in the moderator noticeboard.
'noticeboard_page' => 50,
// Number of entries to summarize and display on the dashboard.
'noticeboard_dashboard' => 5,
// Check public ban message by default.
'check_ban_message' => false,
// Default public ban message. In public ban messages, %length% is replaced with "for x days" or
// "permanently" (with %LENGTH% being the uppercase equivalent).
'default_ban_message' => _('USER WAS BANNED FOR THIS POST'),
// $config['mod']['default_ban_message'] = 'USER WAS BANNED %LENGTH% FOR THIS POST';
// HTML to append to post bodies for public bans messages (where "%s" is the message).
'ban_message' => '<span class="public_ban">(%s)</span>',
// When moving a thread to another board and choosing to keep a "shadow thread", an automated post (with
// a capcode) will be made, linking to the new location for the thread. "%s" will be replaced with a
// standard cross-board post citation (>>>/board/xxx)
'shadow_mesage' => _('Moved to %s.'),
// Capcode to use when posting the above message.
'shadow_capcode' => 'Mod',
// Name to use when posting the above message. If false, $config['anonymous'] will be used.
'shadow_name' => false,
// PHP time limit for ?/rebuild. A value of 0 should cause PHP to wait indefinitely.
'rebuild_timelimit' => 0,
// PM snippet (for ?/inbox) length in characters.
'snippet_length' => 75,
// Edit raw HTML in posts by default.
'raw_html_default' => false,
// Automatically dismiss all reports regarding a thread when it is locked.
'dismiss_reports_on_lock' => true,
// Replace ?/config with a simple text editor for editing inc/instance-config.php.
'config_editor_php' => false,
'link_delete' => '[D]',
'link_ban' => '[B]',
'link_bandelete' => '[B&amp;D]',
'link_deletefile' => '[F]',
'link_spoilerimage' => '[S]',
'link_deletebyip' => '[D+]',
'link_deletebyip_global' => '[D++]',
'link_sticky' => '[Sticky]',
'link_desticky' => '[-Sticky]',
'link_lock' => '[Lock]',
'link_unlock' => '[-Lock]',
'link_bumplock' => '[Sage]',
'link_bumpunlock' => '[-Sage]',
'link_editpost' => '[Edit]',
'link_move' => '[Move]',
'link_cycle' => '[Cycle]',
'link_uncycle' => '[-Cycle]'
];
// Moderator capcodes. // Moderator capcodes.
$config['capcode'] = ' <span class="capcode">## %s</span>'; $config['capcode'] = ' <span class="capcode">## %s</span>';
@ -1550,63 +1616,6 @@
// Enable the moving of single replies // Enable the moving of single replies
$config['move_replies'] = false; $config['move_replies'] = false;
// How often (minimum) to purge the ban list of expired bans (which have been seen). Only works when
// $config['cache'] is enabled and working.
$config['purge_bans'] = 60 * 60 * 12; // 12 hours
// 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;
// Number of posts to display on the reports page.
$config['mod']['recent_reports'] = 10;
// Number of actions to show per page in the moderation log.
$config['mod']['modlog_page'] = 350;
// Number of bans to show per page in the ban list.
$config['mod']['banlist_page'] = 350;
// Number of news entries to display per page.
$config['mod']['news_page'] = 40;
// Number of results to display per page.
$config['mod']['search_page'] = 200;
// Number of entries to show per page in the moderator noticeboard.
$config['mod']['noticeboard_page'] = 50;
// Number of entries to summarize and display on the dashboard.
$config['mod']['noticeboard_dashboard'] = 5;
// Check public ban message by default.
$config['mod']['check_ban_message'] = false;
// Default public ban message. In public ban messages, %length% is replaced with "for x days" or
// "permanently" (with %LENGTH% being the uppercase equivalent).
$config['mod']['default_ban_message'] = _('USER WAS BANNED FOR THIS POST');
// $config['mod']['default_ban_message'] = 'USER WAS BANNED %LENGTH% FOR THIS POST';
// HTML to append to post bodies for public bans messages (where "%s" is the message).
$config['mod']['ban_message'] = '<span class="public_ban">(%s)</span>';
// When moving a thread to another board and choosing to keep a "shadow thread", an automated post (with
// a capcode) will be made, linking to the new location for the thread. "%s" will be replaced with a
// standard cross-board post citation (>>>/board/xxx)
$config['mod']['shadow_mesage'] = _('Moved to %s.');
// Capcode to use when posting the above message.
$config['mod']['shadow_capcode'] = 'Mod';
// Name to use when posting the above message. If false, $config['anonymous'] will be used.
$config['mod']['shadow_name'] = false;
// PHP time limit for ?/rebuild. A value of 0 should cause PHP to wait indefinitely.
$config['mod']['rebuild_timelimit'] = 0;
// PM snippet (for ?/inbox) length in characters.
$config['mod']['snippet_length'] = 75;
// Edit raw HTML in posts by default.
$config['mod']['raw_html_default'] = false;
// Automatically dismiss all reports regarding a thread when it is locked.
$config['mod']['dismiss_reports_on_lock'] = true;
// Replace ?/config with a simple text editor for editing inc/instance-config.php.
$config['mod']['config_editor_php'] = false;
/* /*
* ==================== * ====================
* Mod permissions * Mod permissions
@ -1616,13 +1625,13 @@
// Probably best not to change this unless you are smart enough to figure out what you're doing. If you // Probably best not to change this unless you are smart enough to figure out what you're doing. If you
// decide to change it, remember that it is impossible to redefinite/overwrite groups; you may only add // decide to change it, remember that it is impossible to redefinite/overwrite groups; you may only add
// new ones. // new ones.
$config['mod']['groups'] = array( $config['mod']['groups'] = [
10 => 'Janitor', 10 => 'Janitor',
20 => 'Mod', 20 => 'Mod',
30 => 'Admin', 30 => 'Admin',
// 98 => 'God', // 98 => 'God',
99 => 'Disabled' 99 => 'Disabled'
); ];
// If you add stuff to the above, you'll need to call this function immediately after. // If you add stuff to the above, you'll need to call this function immediately after.
define_groups(); define_groups();
@ -1632,11 +1641,11 @@
// define_groups(); // define_groups();
// Capcode permissions. // Capcode permissions.
$config['mod']['capcode'] = array( $config['mod']['capcode'] = [
// JANITOR => array('Janitor'), // JANITOR => [ 'Janitor' ],
MOD => array('Mod'), MOD => [ 'Mod' ],
ADMIN => true ADMIN => true
); ];
// Example: Allow mods to post with "## Moderator" as well // Example: Allow mods to post with "## Moderator" as well
// $config['mod']['capcode'][MOD][] = 'Moderator'; // $config['mod']['capcode'][MOD][] = 'Moderator';
@ -1838,23 +1847,20 @@
*/ */
// Public post search settings // Public post search settings
$config['search'] = array(); $config['search'] = [
// Enable the search form
// Enable the search form 'enable' => false,
$config['search']['enable'] = false; // Maximal number of queries per IP address per minutes
'queries_per_minutes' => [ 15, 2 ],
// Global maximal number of queries per minutes
'queries_per_minutes_all' => [ 50, 2 ],
// Limit of search results
'search_limit' => 100,
];
// Enable search in the board index. // Enable search in the board index.
$config['board_search'] = false; $config['board_search'] = false;
// Maximal number of queries per IP address per minutes
$config['search']['queries_per_minutes'] = Array(15, 2);
// Global maximal number of queries per minutes
$config['search']['queries_per_minutes_all'] = Array(50, 2);
// Limit of search results
$config['search']['search_limit'] = 100;
// Boards for searching // Boards for searching
//$config['search']['boards'] = array('a', 'b', 'c', 'd', 'e'); //$config['search']['boards'] = array('a', 'b', 'c', 'd', 'e');
@ -1909,31 +1915,33 @@
* state. Please join #nntpchan on Rizon in order to peer with someone. * state. Please join #nntpchan on Rizon in order to peer with someone.
*/ */
$config['nntpchan'] = array(); $config['nntpchan'] = [
// Enable NNTPChan integration
// Enable NNTPChan integration 'enabled'=> false,
$config['nntpchan']['enabled'] = false; // NNTP server
'server' => "localhost:1119",
// NNTP server /*
$config['nntpchan']['server'] = "localhost:1119"; * Global dispatch array. Add your boards to it to enable them. Please make
* sure that this setting is set in a global context.
// Global dispatch array. Add your boards to it to enable them. Please make */
// sure that this setting is set in a global context. 'dispatch' => [
$config['nntpchan']['dispatch'] = array(); // 'overchan.test' => 'test' // 'overchan.test' => 'test'
],
// Trusted peer - an IP address of your NNTPChan instance. This peer will have /*
// increased capabilities, eg.: will evade spamfilter. * Trusted peer - an IP address of your NNTPChan instance. This peer will have increased capabilities, eg.: will
$config['nntpchan']['trusted_peer'] = '127.0.0.1'; * evade spamfilter.
*/
// Salt for message ID generation. Keep it long and secure. 'trusted_peer' => '127.0.0.1',
$config['nntpchan']['salt'] = 'change_me+please'; // Salt for message ID generation. Keep it long and secure.
'salt' => 'change_me+please',
// A local message ID domain. Make sure to change it. // A local message ID domain. Make sure to change it.
$config['nntpchan']['domain'] = 'example.vichan.net'; 'domain' => 'example.vichan.net',
/*
// An NNTPChan group name. * An NNTPChan group name.
// Please set this setting in your board/config.php, not globally. * Please set this setting in your board/config.php, not globally.
$config['nntpchan']['group'] = false; // eg. 'overchan.test' */
'group' => false, // eg. 'overchan.test'
];

View File

@ -1,66 +1,91 @@
<?php <?php
namespace Vichan; namespace Vichan;
use Vichan\Driver\{HttpDriver, HttpDrivers, Log, LogDrivers}; use Vichan\Data\Driver\{CacheDriver, HttpDriver, ErrorLogLogDriver, FileLogDriver, LogDriver, StderrLogDriver, SyslogLogDriver};
use Vichan\Service\HCaptchaQuery;
use Vichan\Service\NativeCaptchaQuery;
use Vichan\Service\ReCaptchaQuery;
use Vichan\Service\RemoteCaptchaQuery;
defined('TINYBOARD') or exit; defined('TINYBOARD') or exit;
interface DependencyFactory {
public function buildLogDriver(): Log;
public function buildHttpDriver(): HttpDriver;
}
class WebDependencyFactory implements DependencyFactory {
private array $config;
public function __construct(array $config) {
$this->config = $config;
}
public function buildLogDriver(): Log {
$name = $this->config['log_system']['name'];
$level = $this->config['debug'] ? Log::DEBUG : Log::NOTICE;
$backend = $this->config['log_system']['type'];
// Check 'syslog' for backwards compatibility.
if ((isset($this->config['syslog']) && $this->config['syslog']) || $backend === 'syslog') {
return LogDrivers::syslog($name, $level, $this->config['log_system']['syslog_stderr']);
} elseif ($backend === 'file') {
return LogDrivers::file($name, $level, $this->config['log_system']['file_path']);
} elseif ($backend === 'stderr') {
return LogDrivers::stderr($name, $level);
} elseif ($backend === 'none') {
return LogDrivers::none();
} else {
return LogDrivers::error_log($name, $level);
}
}
public function buildHttpDriver(): HttpDriver {
return HttpDrivers::getHttpDriver(
$this->config['upload_by_url_timeout'],
$this->config['max_filesize']
);
}
}
class Context { class Context {
private DependencyFactory $factory; private array $definitions;
private ?Log $log;
private ?HttpDriver $http;
public function __construct(array $definitions) {
public function __construct(DependencyFactory $factory) { $this->definitions = $definitions;
$this->factory = $factory;
} }
public function getLog(): Log { public function get(string $name){
return $this->log ??= $this->factory->buildLogDriver(); if (!isset($this->definitions[$name])) {
} throw new \RuntimeException("Could not find a dependency named $name");
}
public function getHttpDriver(): HttpDriver { $ret = $this->definitions[$name];
return $this->http ??= $this->factory->buildHttpDriver(); if (is_callable($ret) && !is_string($ret) && !is_array($ret)) {
$ret = $ret($this);
$this->definitions[$name] = $ret;
}
return $ret;
} }
} }
function build_context(array $config): Context {
return new Context([
'config' => $config,
LogDriver::class => function($c) {
$config = $c->get('config');
$name = $config['log_system']['name'];
$level = $config['debug'] ? LogDriver::DEBUG : LogDriver::NOTICE;
$backend = $config['log_system']['type'];
// Check 'syslog' for backwards compatibility.
if ((isset($config['syslog']) && $config['syslog']) || $backend === 'syslog') {
return new SyslogLogDriver($name, $level, $this->config['log_system']['syslog_stderr']);
} elseif ($backend === 'file') {
return new FileLogDriver($name, $level, $this->config['log_system']['file_path']);
} elseif ($backend === 'stderr') {
return new StderrLogDriver($name, $level);
} else {
return new ErrorLogLogDriver($name, $level);
}
},
HttpDriver::class => function($c) {
$config = $c->get('config');
return new HttpDriver($config['upload_by_url_timeout'], $config['max_filesize']);
},
RemoteCaptchaQuery::class => function($c) {
$config = $c->get('config');
$http = $c->get(HttpDriver::class);
switch ($config['captcha']['provider']) {
case 'recaptcha':
return new ReCaptchaQuery($http, $config['captcha']['recaptcha']['secret']);
case 'hcaptcha':
return new HCaptchaQuery(
$http,
$config['captcha']['hcaptcha']['secret'],
$config['captcha']['hcaptcha']['sitekey']
);
default:
throw new \RuntimeException('No remote captcha service available');
}
},
NativeCaptchaQuery::class => function($c) {
$config = $c->get('config');
if ($config['captcha']['provider'] !== 'native') {
throw new \RuntimeException('No native captcha service available');
}
return new NativeCaptchaQuery(
$c->get(HttpDriver::class),
$config['domain'],
$config['captcha']['native']['provider_check'],
$config['captcha']['native']['extra']
);
},
CacheDriver::class => function($c) {
// Use the global for backwards compatibility.
return \cache::getCache();
}
]);
}

View File

@ -85,24 +85,24 @@ function sb_api($b) { global $config, $build_pages;
} }
function sb_ukko() { function sb_ukko() {
rebuildTheme("ukko", "post-thread"); Vichan\Functions\Theme\rebuild_theme("ukko", "post-thread");
return true; return true;
} }
function sb_catalog($b) { function sb_catalog($b) {
if (!openBoard($b)) return false; if (!openBoard($b)) return false;
rebuildTheme("catalog", "post-thread", $b); Vichan\Functions\Theme\rebuild_theme("catalog", "post-thread", $b);
return true; return true;
} }
function sb_recent() { function sb_recent() {
rebuildTheme("recent", "post-thread"); Vichan\Functions\Theme\rebuild_theme("recent", "post-thread");
return true; return true;
} }
function sb_sitemap() { function sb_sitemap() {
rebuildTheme("sitemap", "all"); Vichan\Functions\Theme\rebuild_theme("sitemap", "all");
return true; return true;
} }

View File

@ -401,7 +401,18 @@ class Post {
public function build($index=false) { public function build($index=false) {
global $board, $config; global $board, $config;
return Element($config['file_post_reply'], array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index, 'mod' => $this->mod)); $options = [
'config' => $config,
'board' => $board,
'post' => &$this,
'index' => $index,
'mod' => $this->mod
];
if ($this->mod) {
$options['pm'] = create_pm_header();
}
return Element($config['file_post_reply'], $options);
} }
}; };
@ -465,10 +476,22 @@ class Thread {
event('show-thread', $this); event('show-thread', $this);
$options = [
'config' => $config,
'board' => $board,
'post' => &$this,
'index' => $index,
'hasnoko50' => $hasnoko50,
'isnoko50' => $isnoko50,
'mod' => $this->mod
];
if ($this->mod) {
$options['pm'] = create_pm_header();
}
$file = ($index && $config['file_board']) ? $config['file_post_thread_fileboard'] : $config['file_post_thread']; $file = ($index && $config['file_board']) ? $config['file_post_thread_fileboard'] : $config['file_post_thread'];
$built = Element($file, array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index, 'hasnoko50' => $hasnoko50, 'isnoko50' => $isnoko50, 'mod' => $this->mod)); $built = Element($file, $options);
return $built; return $built;
} }
}; };

View File

@ -1,151 +0,0 @@
<?php // Honestly this is just a wrapper for cURL. Still useful to mock it and have an OOP API on PHP 7.
namespace Vichan\Driver;
use RuntimeException;
defined('TINYBOARD') or exit;
class HttpDrivers {
private const DEFAULT_USER_AGENT = 'Tinyboard';
public static function getHttpDriver(int $timeout, int $max_file_size): HttpDriver {
return new HttpDriver($timeout, self::DEFAULT_USER_AGENT, $max_file_size);
}
}
class HttpDriver {
private mixed $inner;
private int $timeout;
private string $user_agent;
private int $max_file_size;
private function resetTowards(string $url, int $timeout): void {
curl_reset($this->inner);
curl_setopt_array($this->inner, array(
CURLOPT_URL => $url,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_USERAGENT => $this->user_agent,
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
));
}
private function setSizeLimit(): void {
// Adapted from: https://stackoverflow.com/a/17642638
curl_setopt($this->inner, CURLOPT_NOPROGRESS, false);
if (PHP_MAJOR_VERSION >= 8 && PHP_MINOR_VERSION >= 2) {
curl_setopt($this->inner, CURLOPT_XFERINFOFUNCTION, function($res, $next_dl, $dl, $next_up, $up) {
return (int)($dl <= $this->max_file_size);
});
} else {
curl_setopt($this->inner, CURLOPT_PROGRESSFUNCTION, function($res, $next_dl, $dl, $next_up, $up) {
return (int)($dl <= $this->max_file_size);
});
}
}
function __construct($timeout, $user_agent, $max_file_size) {
$this->inner = curl_init();
$this->timeout = $timeout;
$this->user_agent = $user_agent;
$this->max_file_size = $max_file_size;
}
function __destruct() {
curl_close($this->inner);
}
/**
* Execute a GET request.
*
* @param string $endpoint Uri endpoint.
* @param ?array $data Optional GET parameters.
* @param int $timeout Optional request timeout in seconds. Use the default timeout if 0.
* @return string Returns the body of the response.
* @throws RuntimeException Throws on IO error.
*/
public function requestGet(string $endpoint, ?array $data, int $timeout = 0): string {
if (!empty($data)) {
$endpoint .= '?' . http_build_query($data);
}
if ($timeout == 0) {
$timeout = $this->timeout;
}
$this->resetTowards($endpoint, $timeout);
curl_setopt($this->inner, CURLOPT_RETURNTRANSFER, true);
$ret = curl_exec($this->inner);
if ($ret === false) {
throw new \RuntimeException(curl_error($this->inner));
}
return $ret;
}
/**
* Execute a POST request.
*
* @param string $endpoint Uri endpoint.
* @param ?array $data Optional POST parameters.
* @param int $timeout Optional request timeout in seconds. Use the default timeout if 0.
* @return string Returns the body of the response.
* @throws RuntimeException Throws on IO error.
*/
public function requestPost(string $endpoint, ?array $data, int $timeout = 0): string {
if ($timeout == 0) {
$timeout = $this->timeout;
}
$this->resetTowards($endpoint, $timeout);
curl_setopt($this->inner, CURLOPT_POST, true);
if (!empty($data)) {
curl_setopt($this->inner, CURLOPT_POSTFIELDS, http_build_query($data));
}
curl_setopt($this->inner, CURLOPT_RETURNTRANSFER, true);
$ret = curl_exec($this->inner);
if ($ret === false) {
throw new \RuntimeException(curl_error($this->inner));
}
return $ret;
}
/**
* Download the url's target with curl.
*
* @param string $url Url to the file to download.
* @param ?array $data Optional GET parameters.
* @param resource $fd File descriptor to save the content to.
* @param int $timeout Optional request timeout in seconds. Use the default timeout if 0.
* @return bool Returns true on success, false if the file was too large.
* @throws RuntimeException Throws on IO error.
*/
public function requestGetInto(string $endpoint, ?array $data, mixed $fd, int $timeout = 0): bool {
if (!empty($data)) {
$endpoint .= '?' . http_build_query($data);
}
if ($timeout == 0) {
$timeout = $this->timeout;
}
$this->resetTowards($endpoint, $timeout);
curl_setopt($this->inner, CURLOPT_FAILONERROR, true);
curl_setopt($this->inner, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($this->inner, CURLOPT_FILE, $fd);
curl_setopt($this->inner, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
$this->setSizeLimit();
$ret = curl_exec($this->inner);
if ($ret === false) {
if (curl_errno($this->inner) === CURLE_ABORTED_BY_CALLBACK) {
return false;
}
throw new \RuntimeException(curl_error($this->inner));
}
return true;
}
}

View File

@ -1,189 +0,0 @@
<?php // Logging
namespace Vichan\Driver;
use InvalidArgumentException;
use RuntimeException;
defined('TINYBOARD') or exit;
class LogDrivers {
public static function levelToString(int $level): string {
switch ($level) {
case Log::EMERG:
return 'EMERG';
case Log::ERROR:
return 'ERROR';
case Log::WARNING:
return 'WARNING';
case Log::NOTICE:
return 'NOTICE';
case Log::INFO:
return 'INFO';
case Log::DEBUG:
return 'DEBUG';
default:
throw new InvalidArgumentException('Not a logging level');
}
}
/**
* Log to syslog.
*/
public static function syslog(string $name, int $level, bool $print_stderr): Log {
$flags = LOG_ODELAY;
if ($print_stderr) {
$flags |= LOG_PERROR;
}
if (!openlog($name, $flags, LOG_USER)) {
throw new RuntimeException('Unable to open syslog');
}
return new class($level) implements Log {
private $level;
public function __construct(int $level) {
$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);
}
}
}
};
}
/**
* Log via the php function error_log.
*/
public static function error_log(string $name, int $level): Log {
return new class($name, $level) implements Log {
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 = LogDrivers::levelToString($level);
$line = "{$this->name} $lv: $message";
error_log($line, 0, null, null);
}
}
};
}
/**
* Log to a file.
*/
public static function file(string $name, int $level, string $file_path): Log {
/*
* 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.
*/
$fd = fopen($file_path, 'a');
if ($fd === false) {
throw new RuntimeException("Unable to open log file at $file_path");
}
$logger = new class($name, $level, $fd) implements Log {
private string $name;
private int $level;
private mixed $fd;
public function __construct(string $name, int $level, mixed $fd) {
$this->name = $name;
$this->level = $level;
$this->fd = $fd;
}
public function log(int $level, string $message): void {
if ($level <= $this->level) {
$lv = LogDrivers::levelToString($level);
$line = "{$this->name} $lv: $message\n";
flock($this->fd, LOCK_EX);
fwrite($this->fd, $line);
flock($this->fd, LOCK_UN);
}
}
public function close() {
fclose($this->fd);
}
};
// Close the file on shutdown.
register_shutdown_function([$logger, 'close']);
return $logger;
}
/**
* Log to php's standard error file stream.
*/
public static function stderr(string $name, int $level): Log {
return new class($name, $level) implements Log {
private $name;
private $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 = LogDrivers::levelToString($level);
fwrite(STDERR, "{$this->name} $lv: $message\n");
}
}
};
}
/**
* No-op logging system.
*/
public static function none(): Log {
return new class() implements Log {
public function log($level, $message): void {
// No-op.
}
};
}
}
interface Log {
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

@ -136,6 +136,14 @@ class Filter {
return $post['board'] == $match; return $post['board'] == $match;
case 'password': case 'password':
return $post['password'] == $match; return $post['password'] == $match;
case 'unshorten':
$extracted_urls = get_urls($post['body_nomarkup']);
foreach ($extracted_urls as $url) {
if (preg_match($match, trace_url($url))) {
return true;
}
}
return false;
default: default:
error('Unknown filter condition: ' . $condition); error('Unknown filter condition: ' . $condition);
} }

View File

@ -241,7 +241,7 @@ function loadConfig() {
$config['version'] = $__version; $config['version'] = $__version;
if ($config['allow_roll']) { if ($config['allow_roll']) {
event_handler('post', 'diceRoller'); event_handler('post', 'email_dice_roll');
} }
if (in_array('webm', $config['allowed_ext_files']) || in_array('mp4', $config['allowed_ext_files'])) { if (in_array('webm', $config['allowed_ext_files']) || in_array('mp4', $config['allowed_ext_files'])) {
@ -391,114 +391,6 @@ function define_groups() {
ksort($config['mod']['groups']); ksort($config['mod']['groups']);
} }
function create_antibot($board, $thread = null) {
require_once dirname(__FILE__) . '/anti-bot.php';
return _create_antibot($board, $thread);
}
function rebuildThemes($action, $boardname = false) {
global $config, $board, $current_locale;
// Save the global variables
$_config = $config;
$_board = $board;
// List themes
if ($themes = Cache::get("themes")) {
// OK, we already have themes loaded
}
else {
$query = query("SELECT `theme` FROM ``theme_settings`` WHERE `name` IS NULL AND `value` IS NULL") or error(db_error());
$themes = array();
while ($theme = $query->fetch(PDO::FETCH_ASSOC)) {
$themes[] = $theme;
}
Cache::set("themes", $themes);
}
foreach ($themes as $theme) {
// Restore them
$config = $_config;
$board = $_board;
// Reload the locale
if ($config['locale'] != $current_locale) {
$current_locale = $config['locale'];
init_locale($config['locale']);
}
if (PHP_SAPI === 'cli') {
echo "Rebuilding theme ".$theme['theme']."... ";
}
rebuildTheme($theme['theme'], $action, $boardname);
if (PHP_SAPI === 'cli') {
echo "done\n";
}
}
// Restore them again
$config = $_config;
$board = $_board;
// Reload the locale
if ($config['locale'] != $current_locale) {
$current_locale = $config['locale'];
init_locale($config['locale']);
}
}
function loadThemeConfig($_theme) {
global $config;
if (!file_exists($config['dir']['themes'] . '/' . $_theme . '/info.php'))
return false;
// Load theme information into $theme
include $config['dir']['themes'] . '/' . $_theme . '/info.php';
return $theme;
}
function rebuildTheme($theme, $action, $board = false) {
global $config, $_theme;
$_theme = $theme;
$theme = loadThemeConfig($_theme);
if (file_exists($config['dir']['themes'] . '/' . $_theme . '/theme.php')) {
require_once $config['dir']['themes'] . '/' . $_theme . '/theme.php';
$theme['build_function']($action, themeSettings($_theme), $board);
}
}
function themeSettings($theme) {
if ($settings = Cache::get("theme_settings_".$theme)) {
return $settings;
}
$query = prepare("SELECT `name`, `value` FROM ``theme_settings`` WHERE `theme` = :theme AND `name` IS NOT NULL");
$query->bindValue(':theme', $theme);
$query->execute() or error(db_error($query));
$settings = array();
while ($s = $query->fetch(PDO::FETCH_ASSOC)) {
$settings[$s['name']] = $s['value'];
}
Cache::set("theme_settings_".$theme, $settings);
return $settings;
}
function sprintf3($str, $vars, $delim = '%') { function sprintf3($str, $vars, $delim = '%') {
$replaces = array(); $replaces = array();
foreach ($vars as $k => $v) { foreach ($vars as $k => $v) {
@ -876,11 +768,13 @@ function checkBan($board = false) {
} }
foreach ($ips as $ip) { foreach ($ips as $ip) {
$bans = Bans::find($ip, $board, $config['show_modname']); $bans = Bans::find($ip, $board, $config['show_modname'], null, $config['auto_maintenance']);
foreach ($bans as &$ban) { foreach ($bans as &$ban) {
if ($ban['expires'] && $ban['expires'] < time()) { if ($ban['expires'] && $ban['expires'] < time()) {
Bans::delete($ban['id']); if ($config['auto_maintenance']) {
Bans::delete($ban['id']);
}
if ($config['require_ban_view'] && !$ban['seen']) { if ($config['require_ban_view'] && !$ban['seen']) {
if (!isset($_POST['json_response'])) { if (!isset($_POST['json_response'])) {
displayBan($ban); displayBan($ban);
@ -900,17 +794,20 @@ function checkBan($board = false) {
} }
} }
// I'm not sure where else to put this. It doesn't really matter where; it just needs to be called every if ($config['auto_maintenance']) {
// now and then to keep the ban list tidy. // I'm not sure where else to put this. It doesn't really matter where; it just needs to be called every
if ($config['cache']['enabled'] && $last_time_purged = cache::get('purged_bans_last')) { // now and then to keep the ban list tidy.
if (time() - $last_time_purged < $config['purge_bans'] ) if ($config['cache']['enabled']) {
return; $last_time_purged = cache::get('purged_bans_last');
if ($last_time_purged !== false && time() - $last_time_purged > $config['purge_bans']) {
Bans::purge($config['require_ban_view'], $config['purge_bans']);
cache::set('purged_bans_last', time());
}
} else {
// Purge every time.
Bans::purge($config['require_ban_view'], $config['purge_bans']);
}
} }
Bans::purge();
if ($config['cache']['enabled'])
cache::set('purged_bans_last', time());
} }
function threadLocked($id) { function threadLocked($id) {
@ -1398,7 +1295,14 @@ function index($page, $mod=false, $brief = false) {
} }
if ($config['file_board']) { if ($config['file_board']) {
$body = Element($config['file_fileboard'], array('body' => $body, 'mod' => $mod)); $options = [
'body' => $body,
'mod' => $mod
];
if ($mod) {
$options['pm'] = create_pm_header();
}
$body = Element($config['file_fileboard'], $options);
} }
return array( return array(
@ -1581,7 +1485,7 @@ function checkMute() {
if ($config['cache']['enabled']) { if ($config['cache']['enabled']) {
// Cached mute? // Cached mute?
if (($mute = cache::get("mute_${_SERVER['REMOTE_ADDR']}")) && ($mutetime = cache::get("mutetime_${_SERVER['REMOTE_ADDR']}"))) { if (($mute = cache::get("mute_{$_SERVER['REMOTE_ADDR']}")) && ($mutetime = cache::get("mutetime_{$_SERVER['REMOTE_ADDR']}"))) {
error(sprintf($config['error']['youaremuted'], $mute['time'] + $mutetime - time())); error(sprintf($config['error']['youaremuted'], $mute['time'] + $mutetime - time()));
} }
} }
@ -1600,8 +1504,8 @@ function checkMute() {
if ($mute['time'] + $mutetime > time()) { if ($mute['time'] + $mutetime > time()) {
if ($config['cache']['enabled']) { if ($config['cache']['enabled']) {
cache::set("mute_${_SERVER['REMOTE_ADDR']}", $mute, $mute['time'] + $mutetime - time()); cache::set("mute_{$_SERVER['REMOTE_ADDR']}", $mute, $mute['time'] + $mutetime - time());
cache::set("mutetime_${_SERVER['REMOTE_ADDR']}", $mutetime, $mute['time'] + $mutetime - time()); cache::set("mutetime_{$_SERVER['REMOTE_ADDR']}", $mutetime, $mute['time'] + $mutetime - time());
} }
// Not expired yet // Not expired yet
error(sprintf($config['error']['youaremuted'], $mute['time'] + $mutetime - time())); error(sprintf($config['error']['youaremuted'], $mute['time'] + $mutetime - time()));
@ -1612,34 +1516,10 @@ function checkMute() {
} }
} }
function _create_antibot($board, $thread) { function purge_old_antispam() {
global $config, $purged_old_antispam; $query = prepare('DELETE FROM ``antispam`` WHERE `expires` < UNIX_TIMESTAMP()');
$query->execute() or error(db_error());
$antibot = new AntiBot(array($board, $thread)); return $query->rowCount();
if (!isset($purged_old_antispam)) {
$purged_old_antispam = true;
query('DELETE FROM ``antispam`` WHERE `expires` < UNIX_TIMESTAMP()') or error(db_error());
}
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']);
$query->execute() or error(db_error($query));
$query = prepare('INSERT INTO ``antispam`` VALUES (:board, :thread, :hash, UNIX_TIMESTAMP(), NULL, 0)');
$query->bindValue(':board', $board);
$query->bindValue(':thread', $thread);
$query->bindValue(':hash', $antibot->hash());
$query->execute() or error(db_error($query));
return $antibot;
} }
function checkSpam(array $extra_salt = array()) { function checkSpam(array $extra_salt = array()) {
@ -1704,15 +1584,18 @@ function incrementSpamHash($hash) {
} }
function buildIndex($global_api = "yes") { function buildIndex($global_api = "yes") {
global $board, $config, $build_pages; global $board, $config, $build_pages, $mod;
$catalog_api_action = generation_strategy('sb_api', array($board['uri'])); $catalog_api_action = generation_strategy('sb_api', array($board['uri']));
$pages = null; $pages = null;
$antibot = null;
if ($config['api']['enabled']) { if ($config['api']['enabled']) {
$api = new Api(); $api = new Api(
$config['show_filename'],
$config['hide_email'],
$config['country_flags']
);
$catalog = array(); $catalog = array();
} }
@ -1757,21 +1640,15 @@ function buildIndex($global_api = "yes") {
if ($wont_build_this_page) continue; if ($wont_build_this_page) continue;
} }
if ($config['try_smarter']) {
$antibot = create_antibot($board['uri'], 0 - $page);
$content['current_page'] = $page;
}
elseif (!$antibot) {
$antibot = create_antibot($board['uri']);
}
$antibot->reset();
if (!$pages) { if (!$pages) {
$pages = getPages(); $pages = getPages();
} }
$content['pages'] = $pages; $content['pages'] = $pages;
$content['pages'][$page-1]['selected'] = true; $content['pages'][$page-1]['selected'] = true;
$content['btn'] = getPageButtons($content['pages']); $content['btn'] = getPageButtons($content['pages']);
$content['antibot'] = $antibot; if ($mod) {
$content['pm'] = create_pm_header();
}
file_write($filename, Element($config['file_board_index'], $content)); file_write($filename, Element($config['file_board_index'], $content));
} }
@ -2352,9 +2229,8 @@ function buildThread($id, $return = false, $mod = false) {
error($config['error']['nonexistant']); error($config['error']['nonexistant']);
$hasnoko50 = $thread->postCount() >= $config['noko50_min']; $hasnoko50 = $thread->postCount() >= $config['noko50_min'];
$antibot = $mod || $return ? false : create_antibot($board['uri'], $id);
$body = Element($config['file_thread'], array( $options = [
'board' => $board, 'board' => $board,
'thread' => $thread, 'thread' => $thread,
'body' => $thread->build(), 'body' => $thread->build(),
@ -2363,14 +2239,22 @@ function buildThread($id, $return = false, $mod = false) {
'mod' => $mod, 'mod' => $mod,
'hasnoko50' => $hasnoko50, 'hasnoko50' => $hasnoko50,
'isnoko50' => false, 'isnoko50' => false,
'antibot' => $antibot,
'boardlist' => createBoardlist($mod), 'boardlist' => createBoardlist($mod),
'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index']) 'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index'])
)); ];
if ($mod) {
$options['pm'] = create_pm_header();
}
$body = Element($config['file_thread'], $options);
// json api // json api
if ($config['api']['enabled'] && !$mod) { if ($config['api']['enabled'] && !$mod) {
$api = new Api(); $api = new Api(
$config['show_filename'],
$config['hide_email'],
$config['country_flags']
);
$json = json_encode($api->translateThread($thread)); $json = json_encode($api->translateThread($thread));
$jsonFilename = $board['dir'] . $config['dir']['res'] . $id . '.json'; $jsonFilename = $board['dir'] . $config['dir']['res'] . $id . '.json';
file_write($jsonFilename, $json); file_write($jsonFilename, $json);
@ -2391,20 +2275,17 @@ function buildThread($id, $return = false, $mod = false) {
} elseif ($action == 'rebuild') { } elseif ($action == 'rebuild') {
$noko50fn = $board['dir'] . $config['dir']['res'] . link_for($thread, true); $noko50fn = $board['dir'] . $config['dir']['res'] . link_for($thread, true);
if ($hasnoko50 || file_exists($noko50fn)) { if ($hasnoko50 || file_exists($noko50fn)) {
buildThread50($id, $return, $mod, $thread, $antibot); buildThread50($id, $return, $mod, $thread);
} }
file_write($board['dir'] . $config['dir']['res'] . link_for($thread), $body); file_write($board['dir'] . $config['dir']['res'] . link_for($thread), $body);
} }
} }
function buildThread50($id, $return = false, $mod = false, $thread = null, $antibot = false) { function buildThread50($id, $return = false, $mod = false, $thread = null) {
global $board, $config, $build_pages; global $board, $config;
$id = round($id); $id = round($id);
if ($antibot)
$antibot->reset();
if (!$thread) { if (!$thread) {
$query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id ORDER BY `thread`,`id` DESC LIMIT :limit", $board['uri'])); $query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id ORDER BY `thread`,`id` DESC LIMIT :limit", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT); $query->bindValue(':id', $id, PDO::PARAM_INT);
@ -2457,7 +2338,7 @@ function buildThread50($id, $return = false, $mod = false, $thread = null, $anti
$hasnoko50 = $thread->postCount() >= $config['noko50_min']; $hasnoko50 = $thread->postCount() >= $config['noko50_min'];
$body = Element($config['file_thread'], array( $options = [
'board' => $board, 'board' => $board,
'thread' => $thread, 'thread' => $thread,
'body' => $thread->build(false, true), 'body' => $thread->build(false, true),
@ -2466,10 +2347,14 @@ function buildThread50($id, $return = false, $mod = false, $thread = null, $anti
'mod' => $mod, 'mod' => $mod,
'hasnoko50' => $hasnoko50, 'hasnoko50' => $hasnoko50,
'isnoko50' => true, 'isnoko50' => true,
'antibot' => $mod ? false : ($antibot ? $antibot : create_antibot($board['uri'], $id)),
'boardlist' => createBoardlist($mod), 'boardlist' => createBoardlist($mod),
'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index']) 'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index'])
)); ];
if ($mod) {
$options['pm'] = create_pm_header();
}
$body = Element($config['file_thread'], $options);
if ($return) { if ($return) {
return $body; return $body;
@ -2656,65 +2541,6 @@ function shell_exec_error($command, $suppress_stdout = false) {
return $return === 'TB_SUCCESS' ? false : $return; return $return === 'TB_SUCCESS' ? false : $return;
} }
/* Die rolling:
* If "dice XdY+/-Z" is in the email field (where X or +/-Z may be
* missing), X Y-sided dice are rolled and summed, with the modifier Z
* added on. The result is displayed at the top of the post.
*/
function diceRoller($post) {
global $config;
if(strpos(strtolower($post->email), 'dice%20') === 0) {
$dicestr = str_split(substr($post->email, strlen('dice%20')));
// Get params
$diceX = '';
$diceY = '';
$diceZ = '';
$curd = 'diceX';
for($i = 0; $i < count($dicestr); $i ++) {
if(is_numeric($dicestr[$i])) {
$$curd .= $dicestr[$i];
} else if($dicestr[$i] == 'd') {
$curd = 'diceY';
} else if($dicestr[$i] == '-' || $dicestr[$i] == '+') {
$curd = 'diceZ';
$$curd = $dicestr[$i];
}
}
// Default values for X and Z
if($diceX == '') {
$diceX = '1';
}
if($diceZ == '') {
$diceZ = '+0';
}
// Intify them
$diceX = intval($diceX);
$diceY = intval($diceY);
$diceZ = intval($diceZ);
// Continue only if we have valid values
if($diceX > 0 && $diceY > 0) {
$dicerolls = array();
$dicesum = $diceZ;
for($i = 0; $i < $diceX; $i++) {
$roll = rand(1, $diceY);
$dicerolls[] = $roll;
$dicesum += $roll;
}
// Prepend the result to the post body
$modifier = ($diceZ != 0) ? ((($diceZ < 0) ? ' - ' : ' + ') . abs($diceZ)) : '';
$dicesum = ($diceX > 1) ? ' = ' . $dicesum : '';
$post->body = '<table class="diceroll"><tr><td><img src="'.$config['dir']['static'].'d10.svg" alt="Dice roll" width="24"></td><td>Rolled ' . implode(', ', $dicerolls) . $modifier . $dicesum . '</td></tr></table><br/>' . $post->body;
}
}
}
function slugify($post) { function slugify($post) {
global $config; global $config;
@ -2820,7 +2646,6 @@ function purify_html($s) {
function markdown($s) { function markdown($s) {
$pd = new Parsedown(); $pd = new Parsedown();
$pd->setMarkupEscaped(true); $pd->setMarkupEscaped(true);
$pd->setimagesEnabled(false);
return $pd->text($s); return $pd->text($s);
} }
@ -3023,3 +2848,34 @@ function check_thread_limit($post) {
return $r['count'] >= $config['max_threads_per_hour']; return $r['count'] >= $config['max_threads_per_hour'];
} }
} }
function hashPassword($password) {
global $config;
return hash('sha3-256', $password . $config['secure_password_salt']);
}
// Thanks to https://gist.github.com/marijn/3901938
function trace_url($url) {
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_FOLLOWLOCATION => TRUE, // the magic sauce
CURLOPT_RETURNTRANSFER => TRUE,
CURLOPT_SSL_VERIFYHOST => FALSE, // suppress certain SSL errors
CURLOPT_SSL_VERIFYPEER => FALSE,
CURLOPT_TIMEOUT => 30,
));
curl_exec($ch);
$url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
curl_close($ch);
return $url;
}
// Thanks to https://stackoverflow.com/questions/10002227/linkify-regex-function-php-daring-fireball-method/10002262#10002262
function get_urls($body) {
$regex = '(?xi)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))';
$result = preg_match_all("#$regex#i", $body, $match);
return $match[0];
}

114
inc/functions/dice.php Normal file
View File

@ -0,0 +1,114 @@
<?php
namespace Vichan\Functions\Dice;
function _get_or_default_int(array $arr, int $index, int $default) {
return (isset($arr[$index]) && is_numeric($arr[$index])) ? (int)$arr[$index] : $default;
}
/* Die rolling:
* If "dice XdY+/-Z" is in the email field (where X or +/-Z may be
* missing), X Y-sided dice are rolled and summed, with the modifier Z
* added on. The result is displayed at the top of the post.
*/
function email_dice_roll($post) {
global $config;
if(strpos(strtolower($post->email), 'dice%20') === 0) {
$dicestr = str_split(substr($post->email, strlen('dice%20')));
// Get params
$diceX = '';
$diceY = '';
$diceZ = '';
$curd = 'diceX';
for($i = 0; $i < count($dicestr); $i ++) {
if(is_numeric($dicestr[$i])) {
$$curd .= $dicestr[$i];
} else if($dicestr[$i] == 'd') {
$curd = 'diceY';
} else if($dicestr[$i] == '-' || $dicestr[$i] == '+') {
$curd = 'diceZ';
$$curd = $dicestr[$i];
}
}
// Default values for X and Z
if($diceX == '') {
$diceX = '1';
}
if($diceZ == '') {
$diceZ = '+0';
}
// Intify them
$diceX = intval($diceX);
$diceY = intval($diceY);
$diceZ = intval($diceZ);
// Continue only if we have valid values
if($diceX > 0 && $diceY > 0) {
$dicerolls = array();
$dicesum = $diceZ;
for($i = 0; $i < $diceX; $i++) {
$roll = rand(1, $diceY);
$dicerolls[] = $roll;
$dicesum += $roll;
}
// Prepend the result to the post body
$modifier = ($diceZ != 0) ? ((($diceZ < 0) ? ' - ' : ' + ') . abs($diceZ)) : '';
$dicesum = ($diceX > 1) ? ' = ' . $dicesum : '';
$post->body = '<table class="diceroll"><tr><td><img src="'.$config['dir']['static'].'d10.svg" alt="Dice roll" width="24"></td><td>Rolled ' . implode(', ', $dicerolls) . $modifier . $dicesum . '</td></tr></table><br/>' . $post->body;
}
}
}
/**
* Rolls a dice and generates the appropriate html from the markup.
* @param array $matches The array of the matches according to the default configuration.
* 1 -> The number of dices to roll.
* 3 -> The number faces of the dices.
* 4 -> The offset to apply to the dice.
* @param string $img_path Path to the image to use relative to the root. Null if none.
* @return string The html to replace the original markup with.
*/
function inline_dice_roll_markup(array $matches, ?string $img_path): string {
global $config;
$dice_count = _get_or_default_int($matches, 1, 1);
$dice_faces = _get_or_default_int($matches, 3, 6);
$dice_offset = _get_or_default_int($matches, 4, 0);
// Clamp between 1 and max_roll_count.
$dice_count = max(min($dice_count, $config['max_roll_count']), 1);
// Must be at least 2.
if ($dice_faces < 2) {
$dice_faces = 6;
}
$tot = 0;
for ($i = 0; $i < $dice_count; $i++) {
$tot += mt_rand(1, $dice_faces);
}
// Ensure that final result is at least an integer.
$tot = abs((int)($dice_offset + $tot));
if ($img_path !== null) {
$img_text = "<img src='{$config['root']}{$img_path}' alt='dice' title='dice' class=\"inline-dice\"/>";
} else {
$img_text = '';
}
if ($dice_offset === 0) {
$dice_offset_text = '';
} elseif ($dice_offset > 0) {
$dice_offset_text = "+{$dice_offset}";
} else {
$dice_offset_text = (string)$dice_offset;
}
return "<span>$img_text {$dice_count}d{$dice_faces}{$dice_offset_text} = <b>$tot</b></span>";
}

98
inc/functions/theme.php Normal file
View File

@ -0,0 +1,98 @@
<?php
namespace Vichan\Functions\Theme;
function rebuild_themes(string $action, $boardname = false): void {
global $config, $board, $current_locale;
// Save the global variables
$_config = $config;
$_board = $board;
// List themes
if ($themes = \Cache::get("themes")) {
// OK, we already have themes loaded
} else {
$query = query("SELECT `theme` FROM ``theme_settings`` WHERE `name` IS NULL AND `value` IS NULL") or error(db_error());
$themes = $query->fetchAll(\PDO::FETCH_NUM);
\Cache::set("themes", $themes);
}
foreach ($themes as $theme) {
// Restore them
$config = $_config;
$board = $_board;
// Reload the locale
if ($config['locale'] != $current_locale) {
$current_locale = $config['locale'];
init_locale($config['locale']);
}
if (PHP_SAPI === 'cli') {
echo "Rebuilding theme ".$theme[0]."... ";
}
rebuild_theme($theme[0], $action, $boardname);
if (PHP_SAPI === 'cli') {
echo "done\n";
}
}
// Restore them again
$config = $_config;
$board = $_board;
// Reload the locale
if ($config['locale'] != $current_locale) {
$current_locale = $config['locale'];
init_locale($config['locale']);
}
}
function load_theme_config($_theme) {
global $config;
if (!file_exists($config['dir']['themes'] . '/' . $_theme . '/info.php')) {
return false;
}
// Load theme information into $theme
include $config['dir']['themes'] . '/' . $_theme . '/info.php';
return $theme;
}
function rebuild_theme($theme, string $action, $board = false) {
global $config, $_theme;
$_theme = $theme;
$theme = load_theme_config($_theme);
if (file_exists($config['dir']['themes'] . '/' . $_theme . '/theme.php')) {
require_once $config['dir']['themes'] . '/' . $_theme . '/theme.php';
$theme['build_function']($action, theme_settings($_theme), $board);
}
}
function theme_settings($theme): array {
if ($settings = \Cache::get("theme_settings_" . $theme)) {
return $settings;
}
$query = prepare("SELECT `name`, `value` FROM ``theme_settings`` WHERE `theme` = :theme AND `name` IS NOT NULL");
$query->bindValue(':theme', $theme);
$query->execute() or error(db_error($query));
$settings = [];
while ($s = $query->fetch(\PDO::FETCH_ASSOC)) {
$settings[$s['name']] = $s['value'];
}
\Cache::set("theme_settings_".$theme, $settings);
return $settings;
}

View File

@ -1,84 +1,76 @@
<?php <?php
class Locks { class Locks {
private static function filesystem(string $key): Lock|false { private static function filesystem(string $key) {
$key = str_replace('/', '::', $key); $key = str_replace('/', '::', $key);
$key = str_replace("\0", '', $key); $key = str_replace("\0", '', $key);
$fd = fopen("tmp/locks/$key", "w"); $fd = fopen("tmp/locks/$key", "w");
if ($fd === false) { if ($fd === false) {
return false; return false;
} }
return new class($fd) implements Lock { return new class($fd) implements Lock {
// Resources have no type in php. // Resources have no type in PHP.
private mixed $f; private $f;
public function __construct($fd) {
$this->f = $fd;
}
function __construct($fd) { public function get(bool $nonblock = false) {
$this->f = $fd; $wouldblock = false;
} flock($this->f, LOCK_SH | ($nonblock ? LOCK_NB : 0), $wouldblock);
if ($nonblock && $wouldblock) {
return false;
}
return $this;
}
public function get(bool $nonblock = false): Lock|false { public function get_ex(bool $nonblock = false) {
$wouldblock = false; $wouldblock = false;
flock($this->f, LOCK_SH | ($nonblock ? LOCK_NB : 0), $wouldblock); flock($this->f, LOCK_EX | ($nonblock ? LOCK_NB : 0), $wouldblock);
if ($nonblock && $wouldblock) { if ($nonblock && $wouldblock) {
return false; return false;
} }
return $this; return $this;
} }
public function get_ex(bool $nonblock = false): Lock|false { public function free() {
$wouldblock = false; flock($this->f, LOCK_UN);
flock($this->f, LOCK_EX | ($nonblock ? LOCK_NB : 0), $wouldblock); return $this;
if ($nonblock && $wouldblock) { }
return false; };
} }
return $this;
}
public function free(): Lock { public static function none() {
flock($this->f, LOCK_UN); return new class() implements Lock {
return $this; public function get(bool $nonblock = false) {
} return $this;
}; }
}
/** public function get_ex(bool $nonblock = false) {
* No-op. Can be used for mocking. return $this;
*/ }
public static function none(): Lock|false {
return new class() implements Lock {
public function get(bool $nonblock = false): Lock|false {
return $this;
}
public function get_ex(bool $nonblock = false): Lock|false { public function free() {
return $this; return $this;
} }
};
}
public function free(): Lock { public static function get_lock(array $config, string $key) {
return $this; if ($config['lock']['enabled'] == 'fs') {
} return self::filesystem($key);
}; } else {
} return self::none();
}
public static function get_lock(array $config, string $key): Lock|false { }
if ($config['lock']['enabled'] == 'fs') {
return self::filesystem($key);
} else {
return self::none();
}
}
} }
interface Lock { interface Lock {
// Get a shared lock public function get(bool $nonblock = false);
public function get(bool $nonblock = false): Lock|false;
// Get an exclusive lock public function get_ex(bool $nonblock = false);
public function get_ex(bool $nonblock = false): Lock|false;
// Free a lock public function free();
public function free(): Lock;
} }

View File

@ -4,12 +4,13 @@
* Copyright (c) 2010-2013 Tinyboard Development Group * Copyright (c) 2010-2013 Tinyboard Development Group
*/ */
use Vichan\Context;
use Vichan\Functions\Net; use Vichan\Functions\Net;
defined('TINYBOARD') or exit; defined('TINYBOARD') or exit;
// create a hash/salt pair for validate logins // create a hash/salt pair for validate logins
function mkhash(string $username, string $password, mixed $salt = false): array|string { function mkhash(string $username, $password = null, $salt = false) {
global $config; global $config;
if (!$salt) { if (!$salt) {
@ -78,7 +79,7 @@ function calc_cookie_name(bool $is_https, bool $is_path_jailed, string $base_nam
} }
} }
function login(string $username, string $password): array|false { function login(string $username, string $password) {
global $mod, $config; global $mod, $config;
$query = prepare("SELECT `id`, `type`, `boards`, `password`, `version` FROM ``mods`` WHERE BINARY `username` = :username"); $query = prepare("SELECT `id`, `type`, `boards`, `password`, `version` FROM ``mods`` WHERE BINARY `username` = :username");
@ -195,7 +196,7 @@ function modLog(string $action, ?string $_board = null): void {
} }
} }
function create_pm_header(): mixed { function create_pm_header() {
global $mod, $config; global $mod, $config;
if ($config['cache']['enabled'] && ($header = cache::get('pm_unread_' . $mod['id'])) != false) { if ($config['cache']['enabled'] && ($header = cache::get('pm_unread_' . $mod['id'])) != false) {
@ -232,7 +233,7 @@ function make_secure_link_token(string $uri): string {
return substr(sha1($config['cookies']['salt'] . '-' . $uri . '-' . $mod['id']), 0, 8); return substr(sha1($config['cookies']['salt'] . '-' . $uri . '-' . $mod['id']), 0, 8);
} }
function check_login(bool $prompt = false): void { function check_login(Context $ctx, bool $prompt = false): void {
global $config, $mod; global $config, $mod;
$is_https = Net\is_connection_secure($config['cookies']['secure_login_only'] === 1); $is_https = Net\is_connection_secure($config['cookies']['secure_login_only'] === 1);
@ -246,7 +247,9 @@ function check_login(bool $prompt = false): void {
if (count($cookie) != 3) { if (count($cookie) != 3) {
// Malformed cookies // Malformed cookies
destroyCookies(); destroyCookies();
if ($prompt) mod_login(); if ($prompt) {
mod_login($ctx);
}
exit; exit;
} }
@ -259,7 +262,9 @@ function check_login(bool $prompt = false): void {
if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) { if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) {
// Malformed cookies // Malformed cookies
destroyCookies(); destroyCookies();
if ($prompt) mod_login(); if ($prompt) {
mod_login($ctx);
}
exit; exit;
} }

View File

@ -65,6 +65,10 @@ function config_vars() {
$temp_comment = false; $temp_comment = false;
} }
if (preg_match('!^\s*\$config\[(\'log_system\'|\'captcha\')\]!', $line)) {
continue;
}
if (preg_match('!^\s*// ([^$].*)$!', $line, $matches)) { if (preg_match('!^\s*// ([^$].*)$!', $line, $matches)) {
if ($var['default'] !== false) { if ($var['default'] !== false) {
$line = ''; $line = '';

File diff suppressed because it is too large Load Diff

View File

@ -73,7 +73,7 @@ class Queues {
}; };
} }
public static function get_queue(array $config, string $name): Queue|false { public static function get_queue(array $config, string $name) {
if (!isset(self::$queues[$name])) { if (!isset(self::$queues[$name])) {
if ($config['queue']['enabled'] == 'fs') { if ($config['queue']['enabled'] == 'fs') {
$lock = Locks::get_lock($config, $name); $lock = Locks::get_lock($config, $name);

View File

@ -1,100 +1,141 @@
<?php // Verify captchas server side. <?php // Verify captchas server side.
namespace Vichan\Service; namespace Vichan\Service;
use Vichan\Driver\HttpDriver; use Vichan\Data\Driver\HttpDriver;
defined('TINYBOARD') or exit; defined('TINYBOARD') or exit;
class RemoteCaptchaQuery { class ReCaptchaQuery implements RemoteCaptchaQuery {
private HttpDriver $http; private HttpDriver $http;
private string $secret; private string $secret;
private string $endpoint;
/** /**
* Creates a new CaptchaRemoteQueries instance using the google recaptcha service. * Creates a new ReCaptchaQuery using the google recaptcha service.
* *
* @param HttpDriver $http The http client. * @param HttpDriver $http The http client.
* @param string $secret Server side secret. * @param string $secret Server side secret.
* @return CaptchaRemoteQueries A new captcha query instance. * @return ReCaptchaQuery A new ReCaptchaQuery query instance.
*/ */
public static function withRecaptcha(HttpDriver $http, string $secret): RemoteCaptchaQuery { public function __construct(HttpDriver $http, string $secret) {
return new self($http, $secret, 'https://www.google.com/recaptcha/api/siteverify');
}
/**
* Creates a new CaptchaRemoteQueries instance using the hcaptcha service.
*
* @param HttpDriver $http The http client.
* @param string $secret Server side secret.
* @return CaptchaRemoteQueries A new captcha query instance.
*/
public static function withHCaptcha(HttpDriver $http, string $secret): RemoteCaptchaQuery {
return new self($http, $secret, 'https://hcaptcha.com/siteverify');
}
private function __construct(HttpDriver $http, string $secret, string $endpoint) {
$this->http = $http; $this->http = $http;
$this->secret = $secret; $this->secret = $secret;
$this->endpoint = $endpoint;
} }
/** public function responseField(): string {
* Checks if the user at the remote ip passed the captcha. return 'g-recaptcha-response';
* }
* @param string $response User provided response.
* @param string $remote_ip User ip.
* @return bool Returns true if the user passed the captcha.
* @throws RuntimeException|JsonException Throws on IO errors or if it fails to decode the answer.
*/
public function verify(string $response, string $remote_ip): bool {
$data = array(
'secret' => $this->secret,
'response' => $response,
'remoteip' => $remote_ip
);
$ret = $this->http->requestGet($this->endpoint, $data); public function verify(string $response, ?string $remote_ip): bool {
$data = [
'secret' => $this->secret,
'response' => $response
];
if ($remote_ip !== null) {
$data['remoteip'] = $remote_ip;
}
$ret = $this->http->requestGet('https://www.google.com/recaptcha/api/siteverify', $data);
$resp = json_decode($ret, true, 16, JSON_THROW_ON_ERROR); $resp = json_decode($ret, true, 16, JSON_THROW_ON_ERROR);
return isset($resp['success']) && $resp['success']; return isset($resp['success']) && $resp['success'];
} }
} }
class HCaptchaQuery implements RemoteCaptchaQuery {
private HttpDriver $http;
private string $secret;
private string $sitekey;
/**
* Creates a new HCaptchaQuery using the hCaptcha service.
*
* @param HttpDriver $http The http client.
* @param string $secret Server side secret.
* @return HCaptchaQuery A new hCaptcha query instance.
*/
public function __construct(HttpDriver $http, string $secret, string $sitekey) {
$this->http = $http;
$this->secret = $secret;
$this->sitekey = $sitekey;
}
public function responseField(): string {
return 'h-captcha-response';
}
public function verify(string $response, ?string $remote_ip): bool {
$data = [
'secret' => $this->secret,
'response' => $response,
'sitekey' => $this->sitekey
];
if ($remote_ip !== null) {
$data['remoteip'] = $remote_ip;
}
$ret = $this->http->requestGet('https://hcaptcha.com/siteverify', $data);
$resp = json_decode($ret, true, 16, JSON_THROW_ON_ERROR);
return isset($resp['success']) && $resp['success'];
}
}
interface RemoteCaptchaQuery {
/**
* Name of the response field in the form data expected by the implementation.
*
* @return string The name of the field.
*/
public function responseField(): string;
/**
* Checks if the user at the remote ip passed the captcha.
*
* @param string $response User provided response.
* @param ?string $remote_ip User ip. Leave to null to only check the response value.
* @return bool Returns true if the user passed the captcha.
* @throws RuntimeException|JsonException Throws on IO errors or if it fails to decode the answer.
*/
public function verify(string $response, ?string $remote_ip): bool;
}
class NativeCaptchaQuery { class NativeCaptchaQuery {
private HttpDriver $http; private HttpDriver $http;
private string $domain; private string $domain;
private string $provider_check; private string $provider_check;
private string $extra;
/** /**
* @param HttpDriver $http The http client. * @param HttpDriver $http The http client.
* @param string $domain The server's domain. * @param string $domain The server's domain.
* @param string $provider_check Path to the endpoint. * @param string $provider_check Path to the endpoint.
* @param string $extra Extra http parameters.
*/ */
function __construct(HttpDriver $http, string $domain, string $provider_check) { function __construct(HttpDriver $http, string $domain, string $provider_check, string $extra) {
$this->http = $http; $this->http = $http;
$this->domain = $domain; $this->domain = $domain;
$this->provider_check = $provider_check; $this->provider_check = $provider_check;
$this->extra = $extra;
} }
/** /**
* Checks if the user at the remote ip passed the native vichan captcha. * Checks if the user at the remote ip passed the native vichan captcha.
* *
* @param string $extra Extra http parameters.
* @param string $user_text Remote user's text input. * @param string $user_text Remote user's text input.
* @param string $user_cookie Remote user cookie. * @param string $user_cookie Remote user cookie.
* @return bool Returns true if the user passed the check. * @return bool Returns true if the user passed the check.
* @throws RuntimeException Throws on IO errors. * @throws RuntimeException Throws on IO errors.
*/ */
public function verify(string $extra, string $user_text, string $user_cookie): bool { public function verify(string $user_text, string $user_cookie): bool {
$data = array( $data = [
'mode' => 'check', 'mode' => 'check',
'text' => $user_text, 'text' => $user_text,
'extra' => $extra, 'extra' => $this->extra,
'cookie' => $user_cookie 'cookie' => $user_cookie
); ];
$ret = $this->http->requestGet($this->domain . '/' . $this->provider_check, $data); $ret = $this->http->requestGet($this->domain . '/' . $this->provider_check, $data);
return $ret === '1'; return $ret === '1';

View File

@ -34,10 +34,6 @@ function Element($templateFile, array $options) {
if (!$twig) if (!$twig)
load_twig(); load_twig();
if (function_exists('create_pm_header') && ((isset($options['mod']) && $options['mod']) || isset($options['__mod'])) && !preg_match('!^mod/!', $templateFile)) {
$options['pm'] = create_pm_header();
}
if (isset($options['body']) && $config['debug']) { if (isset($options['body']) && $config['debug']) {
$_debug = $debug; $_debug = $debug;
@ -167,8 +163,12 @@ function twig_push_filter($array, $value) {
} }
function twig_date_filter($date, $format) { function twig_date_filter($date, $format) {
$date = new DateTime($date, new DateTimeZone('UTC')); if (is_numeric($date)) {
return $date->format($format); $date = new DateTime("@$date", new DateTimeZone('UTC'));
} else {
$date = new DateTime($date, new DateTimeZone('UTC'));
}
return $date->format($format);
} }
function twig_hasPermission_filter($mod, $permission, $board = null) { function twig_hasPermission_filter($mod, $permission, $board = null) {

View File

@ -1,7 +1,7 @@
<?php <?php
// Installation/upgrade file // Installation/upgrade file
define('VERSION', '5.2.0'); define('VERSION', '5.2.1');
require 'inc/bootstrap.php'; require 'inc/bootstrap.php';
loadConfig(); loadConfig();
@ -689,10 +689,8 @@ if ($step == 0) {
echo Element('page.html', $page); echo Element('page.html', $page);
} elseif ($step == 1) { } elseif ($step == 1) {
//The HTTPS check doesn't work properly when in those arrays, so let's run it here and pass along the result during the actual check. // The HTTPS check doesn't work properly when in those arrays, so let's run it here and pass along the result during the actual check.
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { $httpsvalue = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
$httpsvalue = true;
}
$page['title'] = 'Pre-installation test'; $page['title'] = 'Pre-installation test';
$can_exec = true; $can_exec = true;
@ -873,8 +871,8 @@ if ($step == 0) {
), ),
array( array(
'category' => 'Misc', 'category' => 'Misc',
'name' => 'HTTPS not being used', 'name' => 'HTTPS being used',
'result' => $httpsvalue = true, 'result' => $httpsvalue,
'required' => false, 'required' => false,
'message' => 'You are not currently using https for vichan, or at least for your backend server. If this intentional, add "$config[\'cookies\'][\'secure_login_only\'] = 0;" (or 1 if using a proxy) on a new line under "Additional configuration" on the next page.' 'message' => 'You are not currently using https for vichan, or at least for your backend server. If this intentional, add "$config[\'cookies\'][\'secure_login_only\'] = 0;" (or 1 if using a proxy) on a new line under "Additional configuration" on the next page.'
), ),
@ -923,6 +921,7 @@ if ($step == 0) {
$sg = new SaltGen(); $sg = new SaltGen();
$config['cookies']['salt'] = $sg->generate(); $config['cookies']['salt'] = $sg->generate();
$config['secure_trip_salt'] = $sg->generate(); $config['secure_trip_salt'] = $sg->generate();
$config['secure_password_salt'] = $sg->generate();
echo Element('page.html', array( echo Element('page.html', array(
'body' => Element('installer/config.html', array( 'body' => Element('installer/config.html', array(

View File

@ -294,7 +294,8 @@ CREATE TABLE IF NOT EXISTS `ban_appeals` (
`message` text NOT NULL, `message` text NOT NULL,
`denied` tinyint(1) NOT NULL, `denied` tinyint(1) NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `ban_id` (`ban_id`) KEY `ban_id` (`ban_id`),
CONSTRAINT `fk_ban_id` FOREIGN KEY (`ban_id`) REFERENCES `bans`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
-- -------------------------------------------------------- -- --------------------------------------------------------

View File

@ -30,17 +30,7 @@ function catalog() {
var link = document.createElement('a'); var link = document.createElement('a');
link.href = catalog_url; link.href = catalog_url;
if (pages) { if (!pages) {
link.textContent = _('Catalog');
link.style.color = '#F10000';
link.style.padding = '4px';
link.style.paddingLeft = '9px';
link.style.borderLeft = '1px solid';
link.style.borderLeftColor = '#A8A8A8';
link.style.textDecoration = "underline";
pages.appendChild(link);
} else {
link.textContent = '['+_('Catalog')+']'; link.textContent = '['+_('Catalog')+']';
link.style.paddingLeft = '10px'; link.style.paddingLeft = '10px';
link.style.textDecoration = "underline"; link.style.textDecoration = "underline";

View File

@ -8,21 +8,21 @@
* $config['additional_javascript'][] = 'js/comment-toolbar.js'; * $config['additional_javascript'][] = 'js/comment-toolbar.js';
*/ */
if (active_page == 'catalog') { if (active_page == 'catalog') {
onready(function () { onReady(function() {
'use strict'; 'use strict';
// 'true' = enable shortcuts // 'true' = enable shortcuts
var useKeybinds = true; let useKeybinds = true;
// trigger the search 400ms after last keystroke // trigger the search 400ms after last keystroke
var delay = 400; let delay = 400;
var timeoutHandle; let timeoutHandle;
//search and hide none matching threads // search and hide none matching threads
function filter(search_term) { function filter(search_term) {
$('.replies').each(function () { $('.replies').each(function () {
var subject = $(this).children('.intro').text().toLowerCase(); let subject = $(this).children('.intro').text().toLowerCase();
var comment = $(this).clone().children().remove(':lt(2)').end().text().trim().toLowerCase(); let comment = $(this).clone().children().remove(':lt(2)').end().text().trim().toLowerCase();
search_term = search_term.toLowerCase(); search_term = search_term.toLowerCase();
if (subject.indexOf(search_term) == -1 && comment.indexOf(search_term) == -1) { if (subject.indexOf(search_term) == -1 && comment.indexOf(search_term) == -1) {
@ -34,7 +34,7 @@ if (active_page == 'catalog') {
} }
function searchToggle() { function searchToggle() {
var button = $('#catalog_search_button'); let button = $('#catalog_search_button');
if (!button.data('expanded')) { if (!button.data('expanded')) {
button.data('expanded', '1'); button.data('expanded', '1');
@ -59,7 +59,7 @@ if (active_page == 'catalog') {
}); });
if (useKeybinds) { if (useKeybinds) {
// 's' // 's'
$('body').on('keydown', function (e) { $('body').on('keydown', function (e) {
if (e.which === 83 && e.target.tagName === 'BODY' && !(e.ctrlKey || e.altKey || e.shiftKey)) { if (e.which === 83 && e.target.tagName === 'BODY' && !(e.ctrlKey || e.altKey || e.shiftKey)) {
e.preventDefault(); e.preventDefault();
@ -70,7 +70,7 @@ if (active_page == 'catalog') {
} }
} }
}); });
// 'esc' // 'esc'
$('.catalog_search').on('keydown', 'input#search_field', function (e) { $('.catalog_search').on('keydown', 'input#search_field', function (e) {
if (e.which === 27 && !(e.ctrlKey || e.altKey || e.shiftKey)) { if (e.which === 27 && !(e.ctrlKey || e.altKey || e.shiftKey)) {
window.clearTimeout(timeoutHandle); window.clearTimeout(timeoutHandle);

View File

@ -15,9 +15,9 @@
* *
*/ */
onready(function(){ onReady(function() {
var do_original_filename = function() { let doOriginalFilename = function() {
var filename, truncated; let filename, truncated;
if ($(this).attr('title')) { if ($(this).attr('title')) {
filename = $(this).attr('title'); filename = $(this).attr('title');
truncated = true; truncated = true;
@ -34,9 +34,9 @@ onready(function(){
); );
}; };
$('.postfilename').each(do_original_filename); $('.postfilename').each(doOriginalFilename);
$(document).on('new_post', function(e, post) { $(document).on('new_post', function(e, post) {
$(post).find('.postfilename').each(do_original_filename); $(post).find('.postfilename').each(doOriginalFilename);
}); });
}); });

View File

@ -16,37 +16,42 @@
* *
*/ */
if (active_page == 'ukko' || active_page == 'thread' || active_page == 'index') if (active_page == 'ukko' || active_page == 'thread' || active_page == 'index') {
onready(function(){ onReady(function() {
$('hr:first').before('<div id="expand-all-images" style="text-align:right"><a class="unimportant" href="javascript:void(0)"></a></div>'); $('hr:first').before('<div id="expand-all-images" style="text-align:right"><a class="unimportant" href="javascript:void(0)"></a></div>');
$('div#expand-all-images a') $('div#expand-all-images a')
.text(_('Expand all images')) .text(_('Expand all images'))
.click(function() { .click(function() {
$('a img.post-image').each(function() { $('a img.post-image').each(function() {
// Don't expand YouTube embeds // Don't expand YouTube embeds
if ($(this).parent().parent().hasClass('video-container')) if ($(this).parent().parent().hasClass('video-container')) {
return; return;
}
// or WEBM // or WEBM
if (/^\/player\.php\?/.test($(this).parent().attr('href'))) if (/^\/player\.php\?/.test($(this).parent().attr('href'))) {
return; return;
}
if (!$(this).parent().data('expanded')) if (!$(this).parent().data('expanded')) {
$(this).parent().click(); $(this).parent().click();
}); }
if (!$('#shrink-all-images').length) {
$('hr:first').before('<div id="shrink-all-images" style="text-align:right"><a class="unimportant" href="javascript:void(0)"></a></div>');
}
$('div#shrink-all-images a')
.text(_('Shrink all images'))
.click(function(){
$('a img.full-image').each(function() {
if ($(this).parent().data('expanded'))
$(this).parent().click();
});
$(this).parent().remove();
}); });
});
}); if (!$('#shrink-all-images').length) {
$('hr:first').before('<div id="shrink-all-images" style="text-align:right"><a class="unimportant" href="javascript:void(0)"></a></div>');
}
$('div#shrink-all-images a')
.text(_('Shrink all images'))
.click(function() {
$('a img.full-image').each(function() {
if ($(this).parent().data('expanded')) {
$(this).parent().click();
}
});
$(this).parent().remove();
});
});
});
}

53
js/expand-filename.js Normal file
View File

@ -0,0 +1,53 @@
/*
* expand-filename.js
* https://github.com/vichan-devel/vichan/blob/master/js/expand-filename.js
*
* Released under the MIT license
* Copyright (c) 2024 Perdedora <weav@anche.no>
*
* Usage:
* $config['additional_javascript'][] = 'js/expand-filename.js';
*
*/
function doFilename(element) {
const filenames = element.querySelectorAll('[data-truncate="true"]');
filenames.forEach(filename => {
filename.addEventListener('mouseover', event => addHover(event.target));
filename.addEventListener('mouseout', event => removeHover(event.target));
});
}
function addHover(element) {
element.dataset.truncatedFilename = element.textContent;
element.textContent = element.download;
}
function removeHover(element) {
element.textContent = element.dataset.truncatedFilename;
delete element.dataset.truncatedFilename;
}
document.addEventListener('DOMContentLoaded', () => {
doFilename(document);
// Create a MutationObserver to watch for new elements
const observer = new MutationObserver(mutationsList => {
mutationsList.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(addedNode => {
if (addedNode.nodeType === Node.ELEMENT_NODE) {
// Apply `doFilename` to newly added elements
doFilename(addedNode);
}
});
}
});
});
// Start observing the document body for changes
observer.observe(document.body, {
childList: true,
subtree: true
});
});

View File

@ -1,39 +0,0 @@
/*
* expand-too-long.js
* https://github.com/vichan-devel/vichan/blob/master/js/expand-too-long.js
*
* Released under the MIT license
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
*
* Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/expand-too-long.js';
*
*/
$(function() {
var do_expand = function() {
$(this).find('a').click(function(e) {
e.preventDefault();
var url = $(this).attr('href');
var body = $(this).parents('.body');
$.ajax({
url: url,
context: document.body,
success: function(data) {
var content = $(data).find('#'+url.split('#')[1]).parent().parent().find(".body").first().html();
body.html(content);
}
});
});
};
$('.toolong').each(do_expand);
$(document).on("new_post", function(e, post) {
$(post).find('.toolong').each(do_expand)
});
});

View File

@ -2,243 +2,283 @@
/* Note: This code expects the global variable configRoot to be set. */ /* Note: This code expects the global variable configRoot to be set. */
if (typeof _ == 'undefined') { if (typeof _ == 'undefined') {
var _ = function(a) { return a; }; var _ = function(a) {
return a;
};
} }
function setupVideo(thumb, url) { function setupVideo(thumb, url) {
if (thumb.videoAlreadySetUp) return; if (thumb.videoAlreadySetUp) {
thumb.videoAlreadySetUp = true; return;
}
thumb.videoAlreadySetUp = true;
var video = null; let video = null;
var videoContainer, videoHide; let videoContainer, videoHide;
var expanded = false; let expanded = false;
var hovering = false; let hovering = false;
var loop = true; let loop = true;
var loopControls = [document.createElement("span"), document.createElement("span")]; let loopControls = [document.createElement("span"), document.createElement("span")];
var fileInfo = thumb.parentNode.querySelector(".fileinfo"); let fileInfo = thumb.parentNode.querySelector(".fileinfo");
var mouseDown = false; let mouseDown = false;
function unexpand() { function unexpand() {
if (expanded) { if (expanded) {
expanded = false; expanded = false;
if (video.pause) video.pause(); if (video.pause) {
videoContainer.style.display = "none"; video.pause();
thumb.style.display = "inline"; }
video.style.maxWidth = "inherit"; videoContainer.style.display = "none";
video.style.maxHeight = "inherit"; thumb.style.display = "inline";
} video.style.maxWidth = "inherit";
} video.style.maxHeight = "inherit";
}
}
function unhover() { function unhover() {
if (hovering) { if (hovering) {
hovering = false; hovering = false;
if (video.pause) video.pause(); if (video.pause) {
videoContainer.style.display = "none"; video.pause();
video.style.maxWidth = "inherit"; }
video.style.maxHeight = "inherit"; videoContainer.style.display = "none";
} video.style.maxWidth = "inherit";
} video.style.maxHeight = "inherit";
}
}
// Create video element if does not exist yet // Create video element if does not exist yet
function getVideo() { function getVideo() {
if (video == null) { if (video == null) {
video = document.createElement("video"); video = document.createElement("video");
video.src = url; video.src = url;
video.loop = loop; video.loop = loop;
video.innerText = _("Your browser does not support HTML5 video."); video.innerText = _("Your browser does not support HTML5 video.");
videoHide = document.createElement("img"); videoHide = document.createElement("img");
videoHide.src = configRoot + "static/collapse.gif"; videoHide.src = configRoot + "static/collapse.gif";
videoHide.alt = "[ - ]"; videoHide.alt = "[ - ]";
videoHide.title = "Collapse video"; videoHide.title = "Collapse video";
videoHide.style.marginLeft = "-15px"; videoHide.style.marginLeft = "-15px";
videoHide.style.cssFloat = "left"; videoHide.style.cssFloat = "left";
videoHide.addEventListener("click", unexpand, false); videoHide.addEventListener("click", unexpand, false);
videoContainer = document.createElement("div"); videoContainer = document.createElement("div");
videoContainer.style.paddingLeft = "15px"; videoContainer.style.paddingLeft = "15px";
videoContainer.style.display = "none"; videoContainer.style.display = "none";
videoContainer.appendChild(videoHide); videoContainer.appendChild(videoHide);
videoContainer.appendChild(video); videoContainer.appendChild(video);
thumb.parentNode.insertBefore(videoContainer, thumb.nextSibling); thumb.parentNode.insertBefore(videoContainer, thumb.nextSibling);
// Dragging to the left collapses the video // Dragging to the left collapses the video
video.addEventListener("mousedown", function(e) { video.addEventListener("mousedown", function(e) {
if (e.button == 0) mouseDown = true; if (e.button == 0) mouseDown = true;
}, false); }, false);
video.addEventListener("mouseup", function(e) { video.addEventListener("mouseup", function(e) {
if (e.button == 0) mouseDown = false; if (e.button == 0) mouseDown = false;
}, false); }, false);
video.addEventListener("mouseenter", function(e) { video.addEventListener("mouseenter", function(e) {
mouseDown = false; mouseDown = false;
}, false); }, false);
video.addEventListener("mouseout", function(e) { video.addEventListener("mouseout", function(e) {
if (mouseDown && e.clientX - video.getBoundingClientRect().left <= 0) { if (mouseDown && e.clientX - video.getBoundingClientRect().left <= 0) {
unexpand(); unexpand();
} }
mouseDown = false; mouseDown = false;
}, false); }, false);
} }
} }
// Clicking on thumbnail expands video function setVolume() {
thumb.addEventListener("click", function(e) { const volume = setting("videovolume");
if (setting("videoexpand") && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { video.volume = volume;
getVideo(); video.muted = (volume === 0);
expanded = true; }
hovering = false;
video.style.position = "static"; // Clicking on thumbnail expands video
video.style.pointerEvents = "inherit"; thumb.addEventListener("click", function(e) {
video.style.display = "inline"; if (setting("videoexpand") && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
videoHide.style.display = "inline"; getVideo();
videoContainer.style.display = "block"; expanded = true;
videoContainer.style.position = "static"; hovering = false;
video.parentNode.parentNode.removeAttribute('style');
thumb.style.display = "none";
video.muted = (setting("videovolume") == 0); video.style.position = "static";
video.volume = setting("videovolume"); video.style.pointerEvents = "inherit";
video.controls = true; video.style.display = "inline";
if (video.readyState == 0) { videoHide.style.display = "inline";
video.addEventListener("loadedmetadata", expand2, false); videoContainer.style.display = "block";
} else { videoContainer.style.position = "static";
setTimeout(expand2, 0); video.parentNode.parentNode.removeAttribute('style');
} thumb.style.display = "none";
video.play();
e.preventDefault();
}
}, false);
function expand2() { setVolume();
video.style.maxWidth = "100%"; video.controls = true;
video.style.maxHeight = window.innerHeight + "px"; if (video.readyState == 0) {
var bottom = video.getBoundingClientRect().bottom; video.addEventListener("loadedmetadata", expand2, false);
if (bottom > window.innerHeight) { } else {
window.scrollBy(0, bottom - window.innerHeight); setTimeout(expand2, 0);
} }
// work around Firefox volume control bug let promise = video.play();
video.volume = Math.max(setting("videovolume") - 0.001, 0); if (promise !== undefined) {
video.volume = setting("videovolume"); promise.then(_ => {
} }).catch(_ => {
video.muted = true;
video.play();
});
}
e.preventDefault();
}
}, false);
// Hovering over thumbnail displays video function expand2() {
thumb.addEventListener("mouseover", function(e) { video.style.maxWidth = "100%";
if (setting("videohover")) { video.style.maxHeight = window.innerHeight + "px";
getVideo(); var bottom = video.getBoundingClientRect().bottom;
expanded = false; if (bottom > window.innerHeight) {
hovering = true; window.scrollBy(0, bottom - window.innerHeight);
}
// work around Firefox volume control bug
video.volume = Math.max(setting("videovolume") - 0.001, 0);
video.volume = setting("videovolume");
}
var docRight = document.documentElement.getBoundingClientRect().right; // Hovering over thumbnail displays video
var thumbRight = thumb.querySelector("img, video").getBoundingClientRect().right; thumb.addEventListener("mouseover", function(e) {
var maxWidth = docRight - thumbRight - 20; if (setting("videohover")) {
if (maxWidth < 250) maxWidth = 250; getVideo();
expanded = false;
hovering = true;
video.style.position = "fixed"; let docRight = document.documentElement.getBoundingClientRect().right;
video.style.right = "0px"; let thumbRight = thumb.querySelector("img, video").getBoundingClientRect().right;
video.style.top = "0px"; let maxWidth = docRight - thumbRight - 20;
var docRight = document.documentElement.getBoundingClientRect().right; if (maxWidth < 250) {
var thumbRight = thumb.querySelector("img, video").getBoundingClientRect().right; maxWidth = 250;
video.style.maxWidth = maxWidth + "px"; }
video.style.maxHeight = "100%";
video.style.pointerEvents = "none";
video.style.display = "inline"; video.style.position = "fixed";
videoHide.style.display = "none"; video.style.right = "0px";
videoContainer.style.display = "inline"; video.style.top = "0px";
videoContainer.style.position = "fixed"; docRight = document.documentElement.getBoundingClientRect().right;
thumbRight = thumb.querySelector("img, video").getBoundingClientRect().right;
video.style.maxWidth = maxWidth + "px";
video.style.maxHeight = "100%";
video.style.pointerEvents = "none";
video.muted = (setting("videovolume") == 0); video.style.display = "inline";
video.volume = setting("videovolume"); videoHide.style.display = "none";
video.controls = false; videoContainer.style.display = "inline";
video.play(); videoContainer.style.position = "fixed";
}
}, false);
thumb.addEventListener("mouseout", unhover, false); setVolume();
video.controls = false;
let promise = video.play();
if (promise !== undefined) {
promise.then(_ => {
}).catch(_ => {
video.muted = true;
video.play();
});
}
}
}, false);
// Scroll wheel on thumbnail adjusts default volume thumb.addEventListener("mouseout", unhover, false);
thumb.addEventListener("wheel", function(e) {
if (setting("videohover")) {
var volume = setting("videovolume");
if (e.deltaY > 0) volume -= 0.1;
if (e.deltaY < 0) volume += 0.1;
if (volume < 0) volume = 0;
if (volume > 1) volume = 1;
if (video != null) {
video.muted = (volume == 0);
video.volume = volume;
}
changeSetting("videovolume", volume);
e.preventDefault();
}
}, false);
// [play once] vs [loop] controls // Scroll wheel on thumbnail adjusts default volume
function setupLoopControl(i) { thumb.addEventListener("wheel", function(e) {
loopControls[i].addEventListener("click", function(e) { if (setting("videohover")) {
loop = (i != 0); var volume = setting("videovolume");
thumb.href = thumb.href.replace(/([\?&])loop=\d+/, "$1loop=" + i); if (e.deltaY > 0) {
if (video != null) { volume -= 0.1;
video.loop = loop; }
if (loop && video.currentTime >= video.duration) { if (e.deltaY < 0) {
video.currentTime = 0; volume += 0.1;
} }
} if (volume < 0) {
loopControls[i].style.fontWeight = "bold"; volume = 0;
loopControls[1-i].style.fontWeight = "inherit"; }
}, false); if (volume > 1) {
} volume = 1;
}
if (video != null) {
video.muted = (volume == 0);
video.volume = volume;
}
changeSetting("videovolume", volume);
e.preventDefault();
}
}, false);
loopControls[0].textContent = _("[play once]"); // [play once] vs [loop] controls
loopControls[1].textContent = _("[loop]"); function setupLoopControl(i) {
loopControls[1].style.fontWeight = "bold"; loopControls[i].addEventListener("click", function(e) {
for (var i = 0; i < 2; i++) { loop = (i != 0);
setupLoopControl(i); thumb.href = thumb.href.replace(/([\?&])loop=\d+/, "$1loop=" + i);
loopControls[i].style.whiteSpace = "nowrap"; if (video != null) {
fileInfo.appendChild(document.createTextNode(" ")); video.loop = loop;
fileInfo.appendChild(loopControls[i]); if (loop && video.currentTime >= video.duration) {
} video.currentTime = 0;
}
}
loopControls[i].style.fontWeight = "bold";
loopControls[1-i].style.fontWeight = "inherit";
}, false);
}
loopControls[0].textContent = _("[play once]");
loopControls[1].textContent = _("[loop]");
loopControls[1].style.fontWeight = "bold";
for (var i = 0; i < 2; i++) {
setupLoopControl(i);
loopControls[i].style.whiteSpace = "nowrap";
fileInfo.appendChild(document.createTextNode(" "));
fileInfo.appendChild(loopControls[i]);
}
} }
function setupVideosIn(element) { function setupVideosIn(element) {
var thumbs = element.querySelectorAll("a.file"); let thumbs = element.querySelectorAll("a.file");
for (var i = 0; i < thumbs.length; i++) { for (let i = 0; i < thumbs.length; i++) {
if (/\.webm$|\.mp4$/.test(thumbs[i].pathname)) { if (/\.webm$|\.mp4$/.test(thumbs[i].pathname)) {
setupVideo(thumbs[i], thumbs[i].href); setupVideo(thumbs[i], thumbs[i].href);
} else { } else {
var m = thumbs[i].search.match(/\bv=([^&]*)/); let m = thumbs[i].search.match(/\bv=([^&]*)/);
if (m != null) { if (m != null) {
var url = decodeURIComponent(m[1]); let url = decodeURIComponent(m[1]);
if (/\.webm$|\.mp4$/.test(url)) setupVideo(thumbs[i], url); if (/\.webm$|\.mp4$/.test(url)) {
} setupVideo(thumbs[i], url);
} }
} }
}
}
} }
onready(function(){ onReady(function(){
// Insert menu from settings.js // Insert menu from settings.js
if (typeof settingsMenu != "undefined" && typeof Options == "undefined") if (typeof settingsMenu != "undefined" && typeof Options == "undefined") {
document.body.insertBefore(settingsMenu, document.getElementsByTagName("hr")[0]); document.body.insertBefore(settingsMenu, document.getElementsByTagName("hr")[0]);
}
// Setup Javascript events for videos in document now // Setup Javascript events for videos in document now
setupVideosIn(document); setupVideosIn(document);
// Setup Javascript events for videos added by updater // Setup Javascript events for videos added by updater
if (window.MutationObserver) { if (window.MutationObserver) {
var observer = new MutationObserver(function(mutations) { let observer = new MutationObserver(function(mutations) {
for (var i = 0; i < mutations.length; i++) { for (let i = 0; i < mutations.length; i++) {
var additions = mutations[i].addedNodes; let additions = mutations[i].addedNodes;
if (additions == null) continue; if (additions == null) {
for (var j = 0; j < additions.length; j++) { continue;
var node = additions[j]; }
if (node.nodeType == 1) { for (let j = 0; j < additions.length; j++) {
setupVideosIn(node); let node = additions[j];
} if (node.nodeType == 1) {
} setupVideosIn(node);
} }
}); }
observer.observe(document.body, {childList: true, subtree: true}); }
} });
observer.observe(document.body, {childList: true, subtree: true});
}
}); });

View File

@ -13,21 +13,21 @@
* *
*/ */
onready(function(){ onReady(function() {
var inline_expanding_filename = function() { let inlineExpandingFilename = function() {
$(this).find(".fileinfo > a").click(function(){ $(this).find(".fileinfo > a").click(function() {
var imagelink = $(this).parent().parent().find('a[target="_blank"]:first'); let imagelink = $(this).parent().parent().find('a[target="_blank"]:first');
if(imagelink.length > 0) { if (imagelink.length > 0) {
imagelink.click(); imagelink.click();
return false; return false;
} }
}); });
}; };
$('div[id^="thread_"]').each(inline_expanding_filename); $('div[id^="thread_"]').each(inlineExpandingFilename);
// allow to work with auto-reload.js, etc. // allow to work with auto-reload.js, etc.
$(document).on('new_post', function(e, post) { $(document).on('new_post', function(e, post) {
inline_expanding_filename.call(post); inlineExpandingFilename.call(post);
}); });
}); });

View File

@ -17,29 +17,45 @@ $(document).ready(function(){
'use strict'; 'use strict';
var iso8601 = function(s) { var iso8601 = function(s) {
s = s.replace(/\.\d\d\d+/,""); // remove milliseconds var parts = s.split('T');
s = s.replace(/-/,"/").replace(/-/,"/"); if (parts.length === 2) {
s = s.replace(/T/," ").replace(/Z/," UTC"); var timeParts = parts[1].split(':');
s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 if (timeParts.length === 3) {
var seconds = timeParts[2];
if (seconds.length > 2) {
seconds = seconds.substr(0, 2) + '.' + seconds.substr(2);
}
// Ensure seconds part is valid
if (parseFloat(seconds) > 59) {
seconds = '59';
}
timeParts[2] = seconds;
}
parts[1] = timeParts.join(':');
}
s = parts.join('T');
if (!s.endsWith('Z')) {
s += 'Z';
}
return new Date(s); return new Date(s);
}; };
var zeropad = function(num, count) { var zeropad = function(num, count) {
return [Math.pow(10, count - num.toString().length), num].join('').substr(1); return [Math.pow(10, count - num.toString().length), num].join('').substr(1);
}; };
var dateformat = (typeof strftime === 'undefined') ? function(t) { var dateformat = (typeof strftime === 'undefined') ? function(t) {
return zeropad(t.getMonth() + 1, 2) + "/" + zeropad(t.getDate(), 2) + "/" + t.getFullYear().toString().substring(2) + return zeropad(t.getMonth() + 1, 2) + "/" + zeropad(t.getDate(), 2) + "/" + t.getFullYear().toString().substring(2) +
" (" + [_("Sun"), _("Mon"), _("Tue"), _("Wed"), _("Thu"), _("Fri"), _("Sat"), _("Sun")][t.getDay()] + ") " + " (" + ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][t.getDay()] + ") " +
// time // time
zeropad(t.getHours(), 2) + ":" + zeropad(t.getMinutes(), 2) + ":" + zeropad(t.getSeconds(), 2); zeropad(t.getHours(), 2) + ":" + zeropad(t.getMinutes(), 2) + ":" + zeropad(t.getSeconds(), 2);
} : function(t) { } : function(t) {
// post_date is defined in templates/main.js // post_date is defined in templates/main.js
return strftime(window.post_date, t, datelocale); return strftime(window.post_date, t, datelocale);
}; };
function timeDifference(current, previous) { function timeDifference(current, previous) {
var msPerMinute = 60 * 1000; var msPerMinute = 60 * 1000;
var msPerHour = msPerMinute * 60; var msPerHour = msPerMinute * 60;
var msPerDay = msPerHour * 24; var msPerDay = msPerHour * 24;
@ -51,15 +67,15 @@ $(document).ready(function(){
if (elapsed < msPerMinute) { if (elapsed < msPerMinute) {
return 'Just now'; return 'Just now';
} else if (elapsed < msPerHour) { } else if (elapsed < msPerHour) {
return Math.round(elapsed/msPerMinute) + (Math.round(elapsed/msPerMinute)<=1 ? ' minute ago':' minutes ago'); return Math.round(elapsed / msPerMinute) + (Math.round(elapsed / msPerMinute) <= 1 ? ' minute ago' : ' minutes ago');
} else if (elapsed < msPerDay ) { } else if (elapsed < msPerDay) {
return Math.round(elapsed/msPerHour ) + (Math.round(elapsed/msPerHour)<=1 ? ' hour ago':' hours ago'); return Math.round(elapsed / msPerHour) + (Math.round(elapsed / msPerHour) <= 1 ? ' hour ago' : ' hours ago');
} else if (elapsed < msPerMonth) { } else if (elapsed < msPerMonth) {
return Math.round(elapsed/msPerDay) + (Math.round(elapsed/msPerDay)<=1 ? ' day ago':' days ago'); return Math.round(elapsed / msPerDay) + (Math.round(elapsed / msPerDay) <= 1 ? ' day ago' : ' days ago');
} else if (elapsed < msPerYear) { } else if (elapsed < msPerYear) {
return Math.round(elapsed/msPerMonth) + (Math.round(elapsed/msPerMonth)<=1 ? ' month ago':' months ago'); return Math.round(elapsed / msPerMonth) + (Math.round(elapsed / msPerMonth) <= 1 ? ' month ago' : ' months ago');
} else { } else {
return Math.round(elapsed/msPerYear ) + (Math.round(elapsed/msPerYear)<=1 ? ' year ago':' years ago'); return Math.round(elapsed / msPerYear) + (Math.round(elapsed / msPerYear) <= 1 ? ' year ago' : ' years ago');
} }
} }
@ -67,20 +83,19 @@ $(document).ready(function(){
var times = elem.getElementsByTagName('time'); var times = elem.getElementsByTagName('time');
var currentTime = Date.now(); var currentTime = Date.now();
for(var i = 0; i < times.length; i++) { for (var i = 0; i < times.length; i++) {
var t = times[i].getAttribute('datetime'); var t = times[i].getAttribute('datetime');
var postTime = new Date(t); var postTime = iso8601(t);
times[i].setAttribute('data-local', 'true'); times[i].setAttribute('data-local', 'true');
if (localStorage.show_relative_time === 'false') { if (localStorage.show_relative_time === 'false') {
times[i].innerHTML = dateformat(iso8601(t)); times[i].innerHTML = dateformat(postTime);
times[i].setAttribute('title', timeDifference(currentTime, postTime.getTime())); times[i].setAttribute('title', timeDifference(currentTime, postTime.getTime()));
} else { } else {
times[i].innerHTML = timeDifference(currentTime, postTime.getTime()); times[i].innerHTML = timeDifference(currentTime, postTime.getTime());
times[i].setAttribute('title', dateformat(iso8601(t))); times[i].setAttribute('title', dateformat(postTime));
} }
} }
}; };
@ -101,7 +116,7 @@ $(document).ready(function(){
}); });
if (localStorage.show_relative_time !== 'false') { if (localStorage.show_relative_time !== 'false') {
$('#show-relative-time>input').attr('checked','checked'); $('#show-relative-time>input').attr('checked', 'checked');
interval_id = setInterval(do_localtime, 30000, document); interval_id = setInterval(do_localtime, 30000, document);
} }
@ -113,3 +128,4 @@ $(document).ready(function(){
do_localtime(document); do_localtime(document);
}); });

View File

@ -60,8 +60,8 @@ var banlist_init = function(token, my_boards, inMod) {
// duration? // duration?
expires: {name: _("Expires"), width: "235px", fmt: function(f) { expires: {name: _("Expires"), width: "235px", fmt: function(f) {
if (!f.expires || f.expires == 0) return "<em>"+_("never")+"</em>"; if (!f.expires || f.expires == 0) return "<em>"+_("never")+"</em>";
return strftime(window.post_date, new Date((f.expires|0)*1000), datelocale) + var formattedDate = strftime("%m/%d/%Y (%a) %H:%M:%S", new Date((f.expires|0)*1000), datelocale);
((f.expires < time()) ? "" : " <small>"+_("in ")+until(f.expires|0)+"</small>"); return formattedDate + ((f.expires < time()) ? "" : " <small>"+_("in ")+until(f.expires|0)+"</small>");
} }, } },
username: {name: _("Staff"), width: "100px", fmt: function(f) { username: {name: _("Staff"), width: "100px", fmt: function(f) {
var pre='',suf='',un=f.username; var pre='',suf='',un=f.username;
@ -129,14 +129,16 @@ var banlist_init = function(token, my_boards, inMod) {
$(".banform").on("submit", function() { return false; }); $(".banform").on("submit", function() { return false; });
$("#unban").on("click", function() { $("#unban").on("click", function() {
$(".banform .hiddens").remove(); if (confirm('Are you sure you want to unban the selected IPs?')) {
$("<input type='hidden' name='unban' value='unban' class='hiddens'>").appendTo(".banform"); $(".banform .hiddens").remove();
$("<input type='hidden' name='unban' value='unban' class='hiddens'>").appendTo(".banform");
$.each(selected, function(e) { $.each(selected, function(e) {
$("<input type='hidden' name='ban_"+e+"' value='unban' class='hiddens'>").appendTo(".banform"); $("<input type='hidden' name='ban_"+e+"' value='unban' class='hiddens'>").appendTo(".banform");
}); });
$(".banform").off("submit").submit(); $(".banform").off("submit").submit();
}
}); });
if (device_type == 'desktop') { if (device_type == 'desktop') {

View File

@ -13,59 +13,62 @@
* *
*/ */
onready(function(){ onReady(function() {
var dont_fetch_again = []; let dontFetchAgain = [];
init_hover = function() { initHover = function() {
var $link = $(this); let link = $(this);
let id;
let matches;
var id; if (link.is('[data-thread]')) {
var matches; id = link.attr('data-thread');
} else if (matches = link.text().match(/^>>(?:>\/([^\/]+)\/)?(\d+)$/)) {
if ($link.is('[data-thread]')) {
id = $link.attr('data-thread');
}
else if(matches = $link.text().match(/^>>(?:>\/([^\/]+)\/)?(\d+)$/)) {
id = matches[2]; id = matches[2];
} } else {
else {
return; return;
} }
var board = $(this); let board = $(this);
while (board.data('board') === undefined) { while (board.data('board') === undefined) {
board = board.parent(); board = board.parent();
} }
var threadid; let threadid;
if ($link.is('[data-thread]')) threadid = 0; if (link.is('[data-thread]')) {
else threadid = board.attr('id').replace("thread_", ""); threadid = 0;
} else {
threadid = board.attr('id').replace("thread_", "");
}
board = board.data('board'); board = board.data('board');
var parentboard = board; let parentboard = board;
if ($link.is('[data-thread]')) parentboard = $('form[name="post"] input[name="board"]').val(); if (link.is('[data-thread]')) {
else if (matches[1] !== undefined) board = matches[1]; parentboard = $('form[name="post"] input[name="board"]').val();
} else if (matches[1] !== undefined) {
board = matches[1];
}
var $post = false; let post = false;
var hovering = false; let hovering = false;
var hovered_at; let hoveredAt;
$link.hover(function(e) { link.hover(function(e) {
hovering = true; hovering = true;
hovered_at = {'x': e.pageX, 'y': e.pageY}; hoveredAt = {'x': e.pageX, 'y': e.pageY};
var start_hover = function($link) { let startHover = function(link) {
if ($post.is(':visible') && if (post.is(':visible') &&
$post.offset().top >= $(window).scrollTop() && post.offset().top >= $(window).scrollTop() &&
$post.offset().top + $post.height() <= $(window).scrollTop() + $(window).height()) { post.offset().top + post.height() <= $(window).scrollTop() + $(window).height()) {
// post is in view // post is in view
$post.addClass('highlighted'); post.addClass('highlighted');
} else { } else {
var $newPost = $post.clone(); let newPost = post.clone();
$newPost.find('>.reply, >br').remove(); newPost.find('>.reply, >br').remove();
$newPost.find('span.mentioned').remove(); newPost.find('span.mentioned').remove();
$newPost.find('a.post_anchor').remove(); newPost.find('a.post_anchor').remove();
$newPost newPost
.attr('id', 'post-hover-' + id) .attr('id', 'post-hover-' + id)
.attr('data-board', board) .attr('data-board', board)
.addClass('post-hover') .addClass('post-hover')
@ -76,95 +79,99 @@ onready(function(){
.css('font-style', 'normal') .css('font-style', 'normal')
.css('z-index', '100') .css('z-index', '100')
.addClass('reply').addClass('post') .addClass('reply').addClass('post')
.insertAfter($link.parent()) .insertAfter(link.parent())
$link.trigger('mousemove'); link.trigger('mousemove');
} }
}; };
$post = $('[data-board="' + board + '"] div.post#reply_' + id + ', [data-board="' + board + '"]div#thread_' + id); post = $('[data-board="' + board + '"] div.post#reply_' + id + ', [data-board="' + board + '"]div#thread_' + id);
if($post.length > 0) { if (post.length > 0) {
start_hover($(this)); startHover($(this));
} else { } else {
var url = $link.attr('href').replace(/#.*$/, ''); let url = link.attr('href').replace(/#.*$/, '');
if($.inArray(url, dont_fetch_again) != -1) { if ($.inArray(url, dontFetchAgain) != -1) {
return; return;
} }
dont_fetch_again.push(url); dontFetchAgain.push(url);
$.ajax({ $.ajax({
url: url, url: url,
context: document.body, context: document.body,
success: function(data) { success: function(data) {
var mythreadid = $(data).find('div[id^="thread_"]').attr('id').replace("thread_", ""); let mythreadid = $(data).find('div[id^="thread_"]').attr('id').replace("thread_", "");
if (mythreadid == threadid && parentboard == board) { if (mythreadid == threadid && parentboard == board) {
$(data).find('div.post.reply').each(function() { $(data).find('div.post.reply').each(function() {
if($('[data-board="' + board + '"] #' + $(this).attr('id')).length == 0) { if ($('[data-board="' + board + '"] #' + $(this).attr('id')).length == 0) {
$('[data-board="' + board + '"]#thread_' + threadid + " .post.reply:first").before($(this).hide().addClass('hidden')); $('[data-board="' + board + '"]#thread_' + threadid + " .post.reply:first").before($(this).hide().addClass('hidden'));
} }
}); });
} } else if ($('[data-board="' + board + '"]#thread_' + mythreadid).length > 0) {
else if ($('[data-board="' + board + '"]#thread_'+mythreadid).length > 0) {
$(data).find('div.post.reply').each(function() { $(data).find('div.post.reply').each(function() {
if($('[data-board="' + board + '"] #' + $(this).attr('id')).length == 0) { if ($('[data-board="' + board + '"] #' + $(this).attr('id')).length == 0) {
$('[data-board="' + board + '"]#thread_' + mythreadid + " .post.reply:first").before($(this).hide().addClass('hidden')); $('[data-board="' + board + '"]#thread_' + mythreadid + " .post.reply:first").before($(this).hide().addClass('hidden'));
} }
}); });
} } else {
else {
$(data).find('div[id^="thread_"]').hide().attr('data-cached', 'yes').prependTo('form[name="postcontrols"]'); $(data).find('div[id^="thread_"]').hide().attr('data-cached', 'yes').prependTo('form[name="postcontrols"]');
} }
$post = $('[data-board="' + board + '"] div.post#reply_' + id + ', [data-board="' + board + '"]div#thread_' + id); post = $('[data-board="' + board + '"] div.post#reply_' + id + ', [data-board="' + board + '"]div#thread_' + id);
if(hovering && $post.length > 0) { if (hovering && post.length > 0) {
start_hover($link); startHover(link);
} }
} }
}); });
} }
}, function() { }, function() {
hovering = false; hovering = false;
if(!$post) if (!post) {
return; return;
$post.removeClass('highlighted');
if($post.hasClass('hidden') || $post.data('cached') == 'yes')
$post.css('display', 'none');
$('.post-hover').remove();
}).mousemove(function(e) {
if(!$post)
return;
var $hover = $('#post-hover-' + id + '[data-board="' + board + '"]');
if($hover.length == 0)
return;
var scrollTop = $(window).scrollTop();
if ($link.is("[data-thread]")) scrollTop = 0;
var epy = e.pageY;
if ($link.is("[data-thread]")) epy -= $(window).scrollTop();
var top = (epy ? epy : hovered_at['y']) - 10;
if(epy < scrollTop + 15) {
top = scrollTop;
} else if(epy > scrollTop + $(window).height() - $hover.height() - 15) {
top = scrollTop + $(window).height() - $hover.height() - 15;
} }
post.removeClass('highlighted');
if (post.hasClass('hidden') || post.data('cached') == 'yes') {
post.css('display', 'none');
}
$('.post-hover').remove();
}).mousemove(function(e) {
if (!post) {
return;
}
$hover.css('left', (e.pageX ? e.pageX : hovered_at['x'])).css('top', top); let hover = $('#post-hover-' + id + '[data-board="' + board + '"]');
if (hover.length == 0) {
return;
}
let scrollTop = $(window).scrollTop();
if (link.is("[data-thread]")) {
scrollTop = 0;
}
let epy = e.pageY;
if (link.is("[data-thread]")) {
epy -= $(window).scrollTop();
}
let top = (epy ? epy : hoveredAt['y']) - 10;
if (epy < scrollTop + 15) {
top = scrollTop;
} else if (epy > scrollTop + $(window).height() - hover.height() - 15) {
top = scrollTop + $(window).height() - hover.height() - 15;
}
hover.css('left', (e.pageX ? e.pageX : hoveredAt['x'])).css('top', top);
}); });
}; };
$('div.body a:not([rel="nofollow"])').each(init_hover); $('div.body a:not([rel="nofollow"])').each(initHover);
// allow to work with auto-reload.js, etc. // allow to work with auto-reload.js, etc.
$(document).on('new_post', function(e, post) { $(document).on('new_post', function(e, post) {
$(post).find('div.body a:not([rel="nofollow"])').each(init_hover); $(post).find('div.body a:not([rel="nofollow"])').each(initHover);
}); });
}); });

View File

@ -10,12 +10,12 @@
* Usage: * Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js'; * $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/quote-selection.js'; * $config['additional_javascript'][] = 'js/quote-selection.js';
*
*/ */
$(document).ready(function(){ $(document).ready(function() {
if (!window.getSelection) if (!window.getSelection) {
return; return;
}
$.fn.selectRange = function(start, end) { $.fn.selectRange = function(start, end) {
return this.each(function() { return this.each(function() {
@ -23,7 +23,7 @@ $(document).ready(function(){
this.focus(); this.focus();
this.setSelectionRange(start, end); this.setSelectionRange(start, end);
} else if (this.createTextRange) { } else if (this.createTextRange) {
var range = this.createTextRange(); let range = this.createTextRange();
range.collapse(true); range.collapse(true);
range.moveEnd('character', end); range.moveEnd('character', end);
range.moveStart('character', start); range.moveStart('character', start);
@ -32,81 +32,74 @@ $(document).ready(function(){
}); });
}; };
var altKey = false; let altKey = false;
var ctrlKey = false; let ctrlKey = false;
var metaKey = false; let metaKey = false;
$(document).keyup(function(e) { $(document).keyup(function(e) {
if (e.keyCode == 18) if (e.keyCode == 18) {
altKey = false; altKey = false;
else if (e.keyCode == 17) } else if (e.keyCode == 17) {
ctrlKey = false; ctrlKey = false;
else if (e.keyCode == 91) } else if (e.keyCode == 91) {
metaKey = false; metaKey = false;
}
}); });
$(document).keydown(function(e) { $(document).keydown(function(e) {
if (e.altKey) if (e.altKey) {
altKey = true; altKey = true;
else if (e.ctrlKey) } else if (e.ctrlKey) {
ctrlKey = true; ctrlKey = true;
else if (e.metaKey) } else if (e.metaKey) {
metaKey = true; metaKey = true;
}
if (altKey || ctrlKey || metaKey) { if (altKey || ctrlKey || metaKey) {
// console.log('CTRL/ALT/Something used. Ignoring');
return; return;
} }
if (e.keyCode < 48 || e.keyCode > 90) if (e.keyCode < 48 || e.keyCode > 90) {
return;
var selection = window.getSelection();
var $post = $(selection.anchorNode).parents('.post');
if ($post.length == 0) {
// console.log('Start of selection was not post div', $(selection.anchorNode).parent());
return; return;
} }
var postID = $post.find('.post_no:eq(1)').text(); let selection = window.getSelection();
let post = $(selection.anchorNode).parents('.post');
if (post.length == 0) {
return;
}
let postID = post.find('.post_no:eq(1)').text();
if (postID != $(selection.focusNode).parents('.post').find('.post_no:eq(1)').text()) { if (postID != $(selection.focusNode).parents('.post').find('.post_no:eq(1)').text()) {
// console.log('Selection left post div', $(selection.focusNode).parent());
return; return;
} }
; let selectedText = selection.toString();
var selectedText = selection.toString();
// console.log('Selected text: ' + selectedText.replace(/\n/g, '\\n').replace(/\r/g, '\\r'));
if ($('body').hasClass('debug')) if ($('body').hasClass('debug')) {
alert(selectedText); alert(selectedText);
}
if (selectedText.length == 0) if (selectedText.length == 0) {
return; return;
}
var body = $('textarea#body')[0]; let body = $('textarea#body')[0];
var last_quote = body.value.match(/[\S.]*(^|[\S\s]*)>>(\d+)/); let last_quote = body.value.match(/[\S.]*(^|[\S\s]*)>>(\d+)/);
if (last_quote) if (last_quote) {
last_quote = last_quote[2]; last_quote = last_quote[2];
}
/* to solve some bugs on weird browsers, we need to replace \r\n with \n and then undo that after */ /* to solve some bugs on weird browsers, we need to replace \r\n with \n and then undo that after */
var quote = (last_quote != postID ? '>>' + postID + '\r\n' : '') + $.trim(selectedText).replace(/\r\n/g, '\n').replace(/^/mg, '>').replace(/\n/g, '\r\n') + '\r\n'; let quote = (last_quote != postID ? '>>' + postID + '\r\n' : '') + $.trim(selectedText).replace(/\r\n/g, '\n').replace(/^/mg, '>').replace(/\n/g, '\r\n') + '\r\n';
// console.log('Deselecting text');
selection.removeAllRanges(); selection.removeAllRanges();
if (document.selection) { if (body.selectionStart || body.selectionStart == '0') {
// IE let start = body.selectionStart;
body.focus(); let end = body.selectionEnd;
var sel = document.selection.createRange();
sel.text = quote;
body.focus();
} else if (body.selectionStart || body.selectionStart == '0') {
// Mozilla
var start = body.selectionStart;
var end = body.selectionEnd;
if (!body.value.substring(0, start).match(/(^|\n)$/)) { if (!body.value.substring(0, start).match(/(^|\n)$/)) {
quote = '\r\n\r\n' + quote; quote = '\r\n\r\n' + quote;
@ -121,4 +114,3 @@ $(document).ready(function(){
} }
}); });
}); });

View File

@ -13,38 +13,42 @@
* *
*/ */
onready(function(){ onReady(function() {
var showBackLinks = function() { let showBackLinks = function() {
var reply_id = $(this).attr('id').replace(/(^reply_)|(^op_)/, ''); let reply_id = $(this).attr('id').replace(/(^reply_)|(^op_)/, '');
$(this).find('div.body a:not([rel="nofollow"])').each(function() { $(this).find('div.body a:not([rel="nofollow"])').each(function() {
var id, post, $mentioned; let id, post, $mentioned;
if(id = $(this).text().match(/^>>(\d+)$/)) if (id = $(this).text().match(/^>>(\d+)$/)) {
id = id[1]; id = id[1];
else } else {
return; return;
}
$post = $('#reply_' + id); $post = $('#reply_' + id);
if($post.length == 0){ if ($post.length == 0){
$post = $('#op_' + id); $post = $('#op_' + id);
if($post.length == 0) if ($post.length == 0) {
return; return;
}
} }
$mentioned = $post.find('p.intro span.mentioned'); $mentioned = $post.find('p.intro span.mentioned');
if($mentioned.length == 0) if($mentioned.length == 0) {
$mentioned = $('<span class="mentioned unimportant"></span>').appendTo($post.find('p.intro')); $mentioned = $('<span class="mentioned unimportant"></span>').appendTo($post.find('p.intro'));
}
if ($mentioned.find('a.mentioned-' + reply_id).length != 0) if ($mentioned.find('a.mentioned-' + reply_id).length != 0) {
return; return;
}
var $link = $('<a class="mentioned-' + reply_id + '" onclick="highlightReply(\'' + reply_id + '\');" href="#' + reply_id + '">&gt;&gt;' + let link = $('<a class="mentioned-' + reply_id + '" onclick="highlightReply(\'' + reply_id + '\');" href="#' + reply_id + '">&gt;&gt;' +
reply_id + '</a>'); reply_id + '</a>');
$link.appendTo($mentioned) link.appendTo($mentioned)
if (window.init_hover) { if (window.init_hover) {
$link.each(init_hover); link.each(init_hover);
} }
}); });
}; };
@ -52,10 +56,11 @@ onready(function(){
$('div.post.reply').each(showBackLinks); $('div.post.reply').each(showBackLinks);
$('div.post.op').each(showBackLinks); $('div.post.op').each(showBackLinks);
$(document).on('new_post', function(e, post) { $(document).on('new_post', function(e, post) {
showBackLinks.call(post);
if ($(post).hasClass("op")) { if ($(post).hasClass("op")) {
$(post).find('div.post.reply').each(showBackLinks); $(post).find('div.post.reply').each(showBackLinks);
} else {
$(post).parent().find('div.post.reply').each(showBackLinks);
} }
}); });
}); });

View File

@ -12,11 +12,11 @@
* *
*/ */
onready(function(){ onReady(function() {
if(device_type == 'mobile') { if (device_type == 'mobile') {
var fix_spoilers = function(where) { let fix_spoilers = function(where) {
var spoilers = where.getElementsByClassName('spoiler'); let spoilers = where.getElementsByClassName('spoiler');
for(var i = 0; i < spoilers.length; i++) { for (let i = 0; i < spoilers.length; i++) {
spoilers[i].onmousedown = function() { spoilers[i].onmousedown = function() {
this.style.color = 'white'; this.style.color = 'white';
}; };
@ -24,11 +24,10 @@ onready(function(){
}; };
fix_spoilers(document); fix_spoilers(document);
// allow to work with auto-reload.js, etc. // allow to work with auto-reload.js, etc.
$(document).on('new_post', function(e, post) { $(document).on('new_post', function(e, post) {
fix_spoilers(post); fix_spoilers(post);
}); });
} }
}); });

View File

@ -14,17 +14,18 @@
* *
*/ */
onready(function(){ onReady(function() {
var stylesDiv = $('div.styles'); let stylesDiv = $('div.styles');
var stylesSelect = $('<select></select>'); let stylesSelect = $('<select></select>');
var i = 1; let i = 1;
stylesDiv.children().each(function() { stylesDiv.children().each(function() {
var opt = $('<option></option>') let opt = $('<option></option>')
.html(this.innerHTML.replace(/(^\[|\]$)/g, '')) .html(this.innerHTML.replace(/(^\[|\]$)/g, ''))
.val(i); .val(i);
if ($(this).hasClass('selected')) if ($(this).hasClass('selected')) {
opt.attr('selected', true); opt.attr('selected', true);
}
stylesSelect.append(opt); stylesSelect.append(opt);
$(this).attr('id', 'style-select-' + i); $(this).attr('id', 'style-select-' + i);
i++; i++;
@ -42,4 +43,3 @@ onready(function(){
.append(stylesSelect) .append(stylesSelect)
); );
}); });

View File

@ -22,13 +22,12 @@
* *
*/ */
onReady(function() {
onready(function(){ let do_embed_yt = function(tag) {
var do_embed_yt = function(tag) {
$('div.video-container a', tag).click(function() { $('div.video-container a', tag).click(function() {
var videoID = $(this.parentNode).data('video'); let videoID = $(this.parentNode).data('video');
$(this.parentNode).html('<iframe style="float:left;margin: 10px 20px" type="text/html" '+ $(this.parentNode).html('<iframe style="float:left;margin: 10px 20px" type="text/html" ' +
'width="360" height="270" src="//www.youtube.com/embed/' + videoID + 'width="360" height="270" src="//www.youtube.com/embed/' + videoID +
'?autoplay=1&html5=1" allowfullscreen frameborder="0"/>'); '?autoplay=1&html5=1" allowfullscreen frameborder="0"/>');
@ -37,9 +36,8 @@ onready(function(){
}; };
do_embed_yt(document); do_embed_yt(document);
// allow to work with auto-reload.js, etc. // allow to work with auto-reload.js, etc.
$(document).on('new_post', function(e, post) { $(document).on('new_post', function(e, post) {
do_embed_yt(post); do_embed_yt(post);
}); });
}); });

41
mod.php
View File

@ -1,21 +1,24 @@
<?php <?php
/* /*
* Copyright (c) 2010-2014 Tinyboard Development Group * Copyright (c) 2010-2014 Tinyboard Development Group
*/ */
require_once 'inc/bootstrap.php'; require_once 'inc/bootstrap.php';
if ($config['debug']) if ($config['debug']) {
$parse_start_time = microtime(true); $parse_start_time = microtime(true);
}
require_once 'inc/mod/pages.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']) : ''; $query = isset($_SERVER['QUERY_STRING']) ? rawurldecode($_SERVER['QUERY_STRING']) : '';
$pages = array( $pages = [
'' => ':?/', // redirect to dashboard '' => ':?/', // redirect to dashboard
'/' => 'dashboard', // dashboard '/' => 'dashboard', // dashboard
'/confirm/(.+)' => 'confirm', // confirm action (if javascript didn't work) '/confirm/(.+)' => 'confirm', // confirm action (if javascript didn't work)
@ -109,14 +112,14 @@ $pages = array(
str_replace('%d', '(\d+)', preg_quote($config['file_page'], '!')) => 'view_thread', str_replace('%d', '(\d+)', preg_quote($config['file_page'], '!')) => 'view_thread',
'/(\%b)/' . preg_quote($config['dir']['res'], '!') . '/(\%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'], '!') . '/(\%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) { if (!$mod) {
$pages = array('!^(.+)?$!' => 'login'); $pages = [ '!^(.+)?$!' => 'login' ];
} elseif (isset($_GET['status'], $_GET['r'])) { } elseif (isset($_GET['status'], $_GET['r'])) {
header('Location: ' . $_GET['r'], true, (int)$_GET['status']); header('Location: ' . $_GET['r'], true, (int)$_GET['status']);
exit; exit;
@ -126,10 +129,11 @@ if (isset($config['mod']['custom_pages'])) {
$pages = array_merge($pages, $config['mod']['custom_pages']); $pages = array_merge($pages, $config['mod']['custom_pages']);
} }
$new_pages = array(); $new_pages = [];
foreach ($pages as $key => $callback) { foreach ($pages as $key => $callback) {
if (is_string($callback) && preg_match('/^secure /', $callback)) if (is_string($callback) && preg_match('/^secure /', $callback)) {
$key .= '(/(?P<token>[a-f0-9]{8}))?'; $key .= '(/(?P<token>[a-f0-9]{8}))?';
}
$key = str_replace('\%b', '?P<board>' . sprintf(substr($config['board_path'], 0, -1), $config['board_regex']), $key); $key = str_replace('\%b', '?P<board>' . sprintf(substr($config['board_path'], 0, -1), $config['board_regex']), $key);
$new_pages[(!empty($key) and $key[0] == '!') ? $key : '!^' . $key . '(?:&[^&=]+=[^&]*)*$!u'] = $callback; $new_pages[(!empty($key) and $key[0] == '!') ? $key : '!^' . $key . '(?:&[^&=]+=[^&]*)*$!u'] = $callback;
} }
@ -137,7 +141,7 @@ $pages = $new_pages;
foreach ($pages as $uri => $handler) { foreach ($pages as $uri => $handler) {
if (preg_match($uri, $query, $matches)) { 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'])) { if (isset($matches['board'])) {
$board_match = $matches['board']; $board_match = $matches['board'];
@ -157,7 +161,7 @@ foreach ($pages as $uri => $handler) {
if ($secure_post_only) if ($secure_post_only)
error($config['error']['csrf']); error($config['error']['csrf']);
else { else {
mod_confirm(substr($query, 1)); mod_confirm($ctx, substr($query, 1));
exit; exit;
} }
} }
@ -172,24 +176,20 @@ foreach ($pages as $uri => $handler) {
} }
if ($config['debug']) { if ($config['debug']) {
$debug['mod_page'] = array( $debug['mod_page'] = [
'req' => $query, 'req' => $query,
'match' => $uri, 'match' => $uri,
'handler' => $handler, 'handler' => $handler,
); ];
$debug['time']['parse_mod_req'] = '~' . round((microtime(true) - $parse_start_time) * 1000, 2) . 'ms'; $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).
// we don't want to call named parameters (PHP 8) $matches = array_values($matches);
$matches = array_values($matches);
}
if (is_string($handler)) { if (is_string($handler)) {
if ($handler[0] == ':') { if ($handler[0] == ':') {
header('Location: ' . substr($handler, 1), true, $config['redirect_http']); 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")) { } elseif (is_callable("mod_$handler")) {
call_user_func_array("mod_$handler", $matches); call_user_func_array("mod_$handler", $matches);
} else { } else {
@ -206,4 +206,3 @@ foreach ($pages as $uri => $handler) {
} }
error($config['error']['404']); error($config['error']['404']);

168
post.php
View File

@ -6,7 +6,7 @@
require_once 'inc/bootstrap.php'; require_once 'inc/bootstrap.php';
use Vichan\{Context, WebDependencyFactory}; use Vichan\{Context, WebDependencyFactory};
use Vichan\Driver\{HttpDriver, Log}; use Vichan\Data\Driver\{LogDriver, HttpDriver};
use Vichan\Service\{RemoteCaptchaQuery, NativeCaptchaQuery}; use Vichan\Service\{RemoteCaptchaQuery, NativeCaptchaQuery};
use Vichan\Functions\Format; use Vichan\Functions\Format;
@ -167,12 +167,47 @@ function strip_image_metadata(string $img_path): int {
return $ret; return $ret;
} }
/**
* Delete posts in a cyclical thread.
*
* @param string $boardUri The URI of the board.
* @param int $threadId The ID of the thread.
* @param int $cycleLimit The number of most recent posts to retain.
*/
function delete_cyclical_posts(string $boardUri, int $threadId, int $cycleLimit): void
{
$query = prepare(sprintf('
SELECT p.`id`
FROM ``posts_%s`` p
LEFT JOIN (
SELECT `id`
FROM ``posts_%s``
WHERE `thread` = :thread
ORDER BY `id` DESC
LIMIT :limit
) recent_posts ON p.id = recent_posts.id
WHERE p.thread = :thread
AND recent_posts.id IS NULL',
$boardUri, $boardUri
));
$query->bindValue(':thread', $threadId, PDO::PARAM_INT);
$query->bindValue(':limit', $cycleLimit, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
$ids = $query->fetchAll(PDO::FETCH_COLUMN);
foreach ($ids as $id) {
deletePost($id, false);
}
}
/** /**
* Method handling functions * Method handling functions
*/ */
$dropped_post = false; $dropped_post = false;
$context = new Context(new WebDependencyFactory($config)); $context = Vichan\build_context($config);
// Is it a post coming from NNTP? Let's extract it and pretend it's a normal post. // Is it a post coming from NNTP? Let's extract it and pretend it's a normal post.
if (isset($_GET['Newsgroups']) && $config['nntpchan']['enabled']) { if (isset($_GET['Newsgroups']) && $config['nntpchan']['enabled']) {
@ -251,7 +286,7 @@ if (isset($_GET['Newsgroups']) && $config['nntpchan']['enabled']) {
$content = file_get_contents("php://input"); $content = file_get_contents("php://input");
} }
elseif ($ct == 'multipart/mixed' || $ct == 'multipart/form-data') { elseif ($ct == 'multipart/mixed' || $ct == 'multipart/form-data') {
$context->getLog()->log(Log::DEBUG, 'MM: Files: ' . print_r($GLOBALS, true)); $context->get(LogDriver::class)->log(LogDriver::DEBUG, 'MM: Files: ' . print_r($GLOBALS, true));
$content = ''; $content = '';
@ -347,10 +382,11 @@ if (isset($_POST['delete'])) {
if (!isset($_POST['board'], $_POST['password'])) if (!isset($_POST['board'], $_POST['password']))
error($config['error']['bot']); error($config['error']['bot']);
$password = &$_POST['password']; if (empty($_POST['password'])){
if ($password == '')
error($config['error']['invalidpassword']); error($config['error']['invalidpassword']);
}
$password = hashPassword($_POST['password']);
$delete = array(); $delete = array();
foreach ($_POST as $post => $value) { foreach ($_POST as $post => $value) {
@ -398,10 +434,12 @@ if (isset($_POST['delete'])) {
error(sprintf($config['error']['delete_too_late'], Format\until($post['time'] + $config['max_delete_time']))); error(sprintf($config['error']['delete_too_late'], Format\until($post['time'] + $config['max_delete_time'])));
} }
if ($password != '' && $post['password'] != $password && (!$thread || $thread['password'] != $password)) if (!hash_equals($post['password'], $password) && (!$thread || !hash_equals($thread['password'], $password))) {
error($config['error']['invalidpassword']); error($config['error']['invalidpassword']);
}
if ($post['time'] > time() - $config['delete_time'] && (!$thread || $thread['password'] != $password)) {
if ($post['time'] > time() - $config['delete_time'] && (!$thread || !hash_equals($thread['password'], $password))) {
error(sprintf($config['error']['delete_too_soon'], Format\until($post['time'] + $config['delete_time']))); error(sprintf($config['error']['delete_too_soon'], Format\until($post['time'] + $config['delete_time'])));
} }
@ -416,8 +454,8 @@ if (isset($_POST['delete'])) {
modLog("User at $ip deleted their own post #$id"); modLog("User at $ip deleted their own post #$id");
} }
$context->getLog()->log( $context->get(LogDriver::class)->log(
Log::INFO, LogDriver::INFO,
'Deleted post: /' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '') 'Deleted post: /' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '')
); );
} }
@ -439,7 +477,7 @@ if (isset($_POST['delete'])) {
if (function_exists('fastcgi_finish_request')) if (function_exists('fastcgi_finish_request'))
@fastcgi_finish_request(); @fastcgi_finish_request();
rebuildThemes('post-delete', $board['uri']); Vichan\Functions\Theme\rebuild_themes('post-delete', $board['uri']);
} elseif (isset($_POST['report'])) { } elseif (isset($_POST['report'])) {
if (!isset($_POST['board'], $_POST['reason'])) if (!isset($_POST['board'], $_POST['reason']))
@ -479,12 +517,12 @@ if (isset($_POST['delete'])) {
try { try {
$query = new NativeCaptchaQuery( $query = new NativeCaptchaQuery(
$context->getHttpDriver(), $context->get(HttpDriver::class),
$config['domain'], $config['domain'],
$config['captcha']['provider_check'] $config['captcha']['provider_check'],
$config['captcha']['extra']
); );
$success = $query->verify( $success = $query->verify(
$config['captcha']['extra'],
$_POST['captcha_text'], $_POST['captcha_text'],
$_POST['captcha_cookie'] $_POST['captcha_cookie']
); );
@ -493,7 +531,7 @@ if (isset($_POST['delete'])) {
error($config['error']['captcha']); error($config['error']['captcha']);
} }
} catch (RuntimeException $e) { } catch (RuntimeException $e) {
$context->getLog()->log(Log::ERROR, "Native captcha IO exception: {$e->getMessage()}"); $context->get(LogDriver::class)->log(LogDriver::ERROR, "Native captcha IO exception: {$e->getMessage()}");
error($config['error']['local_io_error']); error($config['error']['local_io_error']);
} }
} }
@ -512,7 +550,7 @@ if (isset($_POST['delete'])) {
$post = $query->fetch(PDO::FETCH_ASSOC); $post = $query->fetch(PDO::FETCH_ASSOC);
if ($post === false) { if ($post === false) {
$context->getLog()->log(Log::INFO, "Failed to report non-existing post #{$id} in {$board['dir']}"); $context->get(LogDriver::class)->log(LogDriver::INFO, "Failed to report non-existing post #{$id} in {$board['dir']}");
error($config['error']['nopost']); error($config['error']['nopost']);
} }
@ -521,8 +559,8 @@ if (isset($_POST['delete'])) {
error($error); error($error);
} }
$context->getLog()->log( $context->get(LogDriver::class)->log(
Log::INFO, LogDriver::INFO,
'Reported post: /' 'Reported post: /'
. $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '') . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '')
. " for \"$reason\"" . " for \"$reason\""
@ -591,10 +629,15 @@ if (isset($_POST['delete'])) {
// Check for CAPTCHA right after opening the board so the "return" link is in there. // Check for CAPTCHA right after opening the board so the "return" link is in there.
try { try {
$provider = $config['captcha']['provider'];
$new_thread_capt = $config['captcha']['native']['new_thread_capt'];
$dynamic = $config['captcha']['dynamic'];
// With our custom captcha provider // With our custom captcha provider
if ($config['captcha']['enabled'] || ($post['op'] && $config['new_thread_capt'])) { if (($provider === 'native' && !$new_thread_capt)
$query = new NativeCaptchaQuery($context->getHttpDriver(), $config['domain'], $config['captcha']['provider_check']); || ($provider === 'native' && $new_thread_capt && $post['op'])) {
$success = $query->verify($config['captcha']['extra'], $_POST['captcha_text'], $_POST['captcha_cookie']); $query = $context->get(NativeCaptchaQuery::class);
$success = $query->verify($_POST['captcha_text'], $_POST['captcha_cookie']);
if (!$success) { if (!$success) {
error( error(
@ -610,36 +653,30 @@ if (isset($_POST['delete'])) {
} }
} }
// Remote 3rd party captchas. // Remote 3rd party captchas.
else { elseif ($provider && (!$dynamic || $dynamic === $_SERVER['REMOTE_ADDR'])) {
// recaptcha $query = $content->get(RemoteCaptchaQuery::class);
if ($config['recaptcha']) { $field = $query->responseField();
if (!isset($_POST['g-recaptcha-response'])) {
error($config['error']['bot']);
}
$response = $_POST['g-recaptcha-response'];
$query = RemoteCaptchaQuery::withRecaptcha($context->getHttpDriver(), $config['recaptcha_private']);
}
// hCaptcha
elseif ($config['hcaptcha']) {
if (!isset($_POST['h-captcha-response'])) {
error($config['error']['bot']);
}
$response = $_POST['h-captcha-response'];
$query = RemoteCaptchaQuery::withHCaptcha($context->getHttpDriver(), $config['hcaptcha_private']);
}
if (isset($query, $response)) { if (!isset($_POST[$field])) {
$success = $query->verify($response, $_SERVER['REMOTE_ADDR']); error($config['error']['bot']);
if (!$success) { }
error($config['error']['captcha']); $response = $_POST[$field];
} /*
* Do not query with the IP if the mode is dynamic. This config is meant for proxies and internal
* loopback addresses.
*/
$ip = $dynamic ? null : $_SERVER['REMOTE_ADDR'];
$success = $query->verify($response, $ip);
if (!$success) {
error($config['error']['captcha']);
} }
} }
} catch (RuntimeException $e) { } catch (RuntimeException $e) {
$context->getLog()->log(Log::ERROR, "Captcha IO exception: {$e->getMessage()}"); $context->get(LogDriver::class)->log(LogDriver::ERROR, "Captcha IO exception: {$e->getMessage()}");
error($config['error']['remote_io_error']); error($config['error']['remote_io_error']);
} catch (JsonException $e) { } catch (JsonException $e) {
$context->getLog()->log(Log::ERROR, "Bad JSON reply to captcha: {$e->getMessage()}"); $context->get(LogDriver::class)->log(LogDriver::ERROR, "Bad JSON reply to captcha: {$e->getMessage()}");
error($config['error']['remote_io_error']); error($config['error']['remote_io_error']);
} }
@ -657,7 +694,7 @@ if (isset($_POST['delete'])) {
if ($post['mod'] = isset($_POST['mod']) && $_POST['mod']) { if ($post['mod'] = isset($_POST['mod']) && $_POST['mod']) {
check_login(false); check_login($context, false);
if (!$mod) { if (!$mod) {
// Liar. You're not a mod. // Liar. You're not a mod.
error($config['error']['notamod']); error($config['error']['notamod']);
@ -675,12 +712,6 @@ if (isset($_POST['delete'])) {
error($config['error']['noaccess']); error($config['error']['noaccess']);
} }
if (!$post['mod']) {
$post['antispam_hash'] = checkSpam(array($board['uri'], isset($post['thread']) ? $post['thread'] : ($config['try_smarter'] && isset($_POST['page']) ? 0 - (int)$_POST['page'] : null)));
if ($post['antispam_hash'] === true)
error($config['error']['spam']);
}
if ($config['robot_enable'] && $config['robot_mute']) { if ($config['robot_enable'] && $config['robot_mute']) {
checkMute(); checkMute();
} }
@ -747,7 +778,7 @@ if (isset($_POST['delete'])) {
try { try {
$ret = download_file_from_url( $ret = download_file_from_url(
$context->getHttpDriver(), $context->get(HttpDriver::class),
$_POST['file_url'], $_POST['file_url'],
$config['upload_by_url_timeout'], $config['upload_by_url_timeout'],
$allowed_extensions, $allowed_extensions,
@ -770,7 +801,7 @@ if (isset($_POST['delete'])) {
$post['subject'] = $_POST['subject']; $post['subject'] = $_POST['subject'];
$post['email'] = str_replace(' ', '%20', htmlspecialchars($_POST['email'])); $post['email'] = str_replace(' ', '%20', htmlspecialchars($_POST['email']));
$post['body'] = $_POST['body']; $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)); $post['has_file'] = (!isset($post['embed']) && (($post['op'] && !isset($post['no_longer_require_an_image_for_op']) && $config['force_image_op']) || count($_FILES) > 0));
if (!$dropped_post) { if (!$dropped_post) {
@ -850,7 +881,7 @@ if (isset($_POST['delete'])) {
$trip = generate_tripcode($post['name']); $trip = generate_tripcode($post['name']);
$post['name'] = $trip[0]; $post['name'] = $trip[0];
if ($config['disable_tripcodes'] = true && !$mod) { if ($config['disable_tripcodes'] && !$mod) {
$post['trip'] = ''; $post['trip'] = '';
} }
else { else {
@ -928,8 +959,6 @@ if (isset($_POST['delete'])) {
error($config['error']['toolong_body']); error($config['error']['toolong_body']);
if (!$mod && substr_count($post['body'], "\n") >= $config['maximum_lines']) if (!$mod && substr_count($post['body'], "\n") >= $config['maximum_lines'])
error($config['error']['toomanylines']); error($config['error']['toomanylines']);
if (mb_strlen($post['password']) > 20)
error(sprintf($config['error']['toolong'], 'password'));
} }
wordfilters($post['body']); wordfilters($post['body']);
@ -1056,9 +1085,6 @@ if (isset($_POST['delete'])) {
error($config['error']['maxsize']); error($config['error']['maxsize']);
} }
// If, on the basis of the file extension, the image file has metadata we can operate on.
$file_image_has_operable_metadata = $file['extension'] === 'jpg' || $file['extension'] === 'jpeg' || $file['extension'] === 'webp' || $file['extension'] == 'png';
$file['exif_stripped'] = false; $file['exif_stripped'] = false;
if ($file_image_has_operable_metadata && $config['convert_auto_orient']) { if ($file_image_has_operable_metadata && $config['convert_auto_orient']) {
@ -1130,7 +1156,7 @@ if (isset($_POST['delete'])) {
try { try {
$file['size'] = strip_image_metadata($file['tmp_name']); $file['size'] = strip_image_metadata($file['tmp_name']);
} catch (RuntimeException $e) { } catch (RuntimeException $e) {
$context->getLog()->log(Log::ERROR, "Could not strip image metadata: {$e->getMessage()}"); $context->get(LogDriver::class)->log(LogDriver::ERROR, "Could not strip image metadata: {$e->getMessage()}");
// Since EXIF metadata can countain sensible info, fail the request. // Since EXIF metadata can countain sensible info, fail the request.
error(_('Could not strip EXIF metadata!'), null, $error); error(_('Could not strip EXIF metadata!'), null, $error);
} }
@ -1165,10 +1191,10 @@ if (isset($_POST['delete'])) {
if ($txt !== '') { if ($txt !== '') {
// This one has an effect, that the body is appended to a post body. So you can write a correct // This one has an effect, that the body is appended to a post body. So you can write a correct
// spamfilter. // spamfilter.
$post['body_nomarkup'] .= "<tinyboard ocr image $key>" . htmlspecialchars($value) . "</tinyboard>"; $post['body_nomarkup'] .= "<tinyboard ocr image $key>" . htmlspecialchars($txt) . "</tinyboard>";
} }
} catch (RuntimeException $e) { } catch (RuntimeException $e) {
$context->getLog()->log(Log::ERROR, "Could not OCR image: {$e->getMessage()}"); $context->get(LogDriver::class)->log(LogDriver::ERROR, "Could not OCR image: {$e->getMessage()}");
} }
} }
} }
@ -1294,11 +1320,7 @@ if (isset($_POST['delete'])) {
// Handle cyclical threads // Handle cyclical threads
if (!$post['op'] && isset($thread['cycle']) && $thread['cycle']) { if (!$post['op'] && isset($thread['cycle']) && $thread['cycle']) {
// Query is a bit weird due to "This version of MariaDB doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'" (MariaDB Ver 15.1 Distrib 10.0.17-MariaDB, for Linux (x86_64)) delete_cyclical_posts($board['uri'], $post['thread'], $config['cycle_limit']);
$query = prepare(sprintf('DELETE FROM ``posts_%s`` WHERE `thread` = :thread AND `id` NOT IN (SELECT `id` FROM (SELECT `id` FROM ``posts_%s`` WHERE `thread` = :thread ORDER BY `id` DESC LIMIT :limit) i)', $board['uri'], $board['uri']));
$query->bindValue(':thread', $post['thread']);
$query->bindValue(':limit', $config['cycle_limit'], PDO::PARAM_INT);
$query->execute() or error(db_error($query));
} }
if (isset($post['antispam_hash'])) { if (isset($post['antispam_hash'])) {
@ -1366,8 +1388,8 @@ if (isset($_POST['delete'])) {
buildThread($post['op'] ? $id : $post['thread']); buildThread($post['op'] ? $id : $post['thread']);
$context->getLog()->log( $context->get(LogDriver::class)->log(
Log::INFO, LogDriver::INFO,
'New post: /' . $board['dir'] . $config['dir']['res'] . link_for($post) . (!$post['op'] ? '#' . $id : '') 'New post: /' . $board['dir'] . $config['dir']['res'] . link_for($post) . (!$post['op'] ? '#' . $id : '')
); );
@ -1400,9 +1422,9 @@ if (isset($_POST['delete'])) {
@fastcgi_finish_request(); @fastcgi_finish_request();
if ($post['op']) if ($post['op'])
rebuildThemes('post-thread', $board['uri']); Vichan\Functions\Theme\rebuild_themes('post-thread', $board['uri']);
else else
rebuildThemes('post', $board['uri']); Vichan\Functions\Theme\rebuild_themes('post', $board['uri']);
} elseif (isset($_POST['appeal'])) { } elseif (isset($_POST['appeal'])) {
if (!isset($_POST['ban_id'])) if (!isset($_POST['ban_id']))
@ -1410,7 +1432,7 @@ if (isset($_POST['delete'])) {
$ban_id = (int)$_POST['ban_id']; $ban_id = (int)$_POST['ban_id'];
$ban = Bans::findSingle($_SERVER['REMOTE_ADDR'], $ban_id, $config['require_ban_view']); $ban = Bans::findSingle($_SERVER['REMOTE_ADDR'], $ban_id, $config['require_ban_view'], $config['auto_maintenance']);
if (empty($ban)) { if (empty($ban)) {
error($config['error']['noban']); error($config['error']['noban']);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -917,10 +917,6 @@ pre {
.poster_id { .poster_id {
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none; user-select: none;
} }
.poster_id:hover { .poster_id:hover {
@ -1046,6 +1042,20 @@ div.boardlist a {
cursor: pointer; cursor: pointer;
} }
/* Inline dice */
.dice-option table {
border: 1px dotted black;
margin: 0;
border-collapse: collapse;
}
.dice-option table td {
text-align: center;
border-left: 1px dotted black;
padding-left: 2px;
padding-right: 2px;
padding-bottom: 2px;
}
#youtube-size input { #youtube-size input {
width: 50px; width: 50px;
} }
@ -1196,8 +1206,7 @@ table.fileboard .intro a {
#gallery_images img { #gallery_images img {
opacity: 0.6; opacity: 0.6;
-webkit-transition: all 0.5s; transition: all 0.5s;
transition: all 0.5s;
} }
#gallery_images img:hover, #gallery_images img.active { #gallery_images img:hover, #gallery_images img.active {
@ -1205,9 +1214,7 @@ table.fileboard .intro a {
} }
#gallery_images img.active { #gallery_images img.active {
-webkit-box-shadow: 0px 0px 29px 2px rgba(255,255,255,1); box-shadow: 0px 0px 29px 2px rgba(255,255,255,1);
-moz-box-shadow: 0px 0px 29px 2px rgba(255,255,255,1);
box-shadow: 0px 0px 29px 2px rgba(255,255,255,1);
z-index: 1; z-index: 1;
} }
@ -1227,12 +1234,35 @@ div.mix {
display: inline-block; display: inline-block;
} }
.ban-reason-table .warning-reason-table tr td:first-child { .ban-reason-table {
text-align: right; margin: 10px auto;
padding-right: 10px; border-collapse: collapse;
width: auto;
font-size: 10pt;
} }
.ban-reason-table .warning-reason-table tr:hover td { .ban-reason-table th,
cursor: pointer; .ban-reason-table td {
background-color: rgba(100%,100%,100%,0.2); border: 1px solid #98E;
padding: 4px 8px;
text-align: left;
}
.ban-reason-table th {
background-color: #98E;
color: black;
font-weight: bold;
}
.ban-reason-table tr:nth-child(even) td {
background-color: #EEF2FF;
}
.ban-reason-table tr:nth-child(odd) td {
background-color: #FFFFFF;
}
.ban-reason-table tr:hover td {
background-color: #D6DAF0;
cursor: pointer;
} }

View File

@ -0,0 +1,121 @@
body {
background: #1C1C1C;
color: #AAA;
}
a:link, a:visited, p.intro a.email span.name {
color: #8080E0;
}
a:hover, a:link:hover, a:visited:hover {
color: #f33;
}
a.post_no {
color: 999;
}
div.post.reply {
background: #383838;
border: 1px solid #000000;
transition: 0.3s;
}
div.post.reply.highlighted {
/*background: #202020;*/
border: 1px solid #000000;
border-left: 1px solid #D03030;
background: #282828;
/*border: none;*/
transition: 0.3s;
}
/*Changed this*/
div.post.reply div.body a {
color: #8080E0;
}
p.intro span.subject {
color: #8080E0;
}
p.intro span.capcode, p.intro a.capcode, p.intro a.nametag {
color: #F33;
margin-left: 0;
}
form table tr th {
background: #383838;
color: #CCC;
}
div.ban h2 {
background: #504040;
color: inherit;
}
div.ban {
border-color: #cccccc;
background: #404040;
}
div.ban p {
color: black;
}
div.pages {
background: #404040;
border-color: #000000;
}
div.pages a.selected {
color: #101010;
}
hr {
border-color: gray;
}
div.boardlist {
color: #a0a0a0;
}
div.boardlist a {
color: #a9a9a9;
}
div.report {
color: gray;
}
table.modlog tr th {
background: #555;
}
input, input[type="text"], input[type="password"], textarea {
color: #AAA;
background: #111;
border: 1px solid #000;
}
div.banner {
background-color: #833;
}
div.banner, div.banner a {
color: #000000;
}
div.title, h1 {
color: #B03030;
}
h2 {
color: #B03030;
}
div.blotter {
color: #D33;
}
.category {
background: #603030;
color: #141414;
border-color: #a9a9a9;
}
#maintable {
background: #404040;
}
#announcement {
background: #404040;
color: #D04040;
}
.post_wrap {
background: #404040;
}
.post_body {
background: #303030;
}
header div.subtitle {
color: #B03030;
}
span.heading {
color: #D03030;
}
#options_div {
background-color: #404040;
}

View File

@ -2,6 +2,6 @@
<p class="unimportant" style="margin-top:20px;text-align:center;">- Tinyboard + <p class="unimportant" style="margin-top:20px;text-align:center;">- Tinyboard +
<a href="https://github.com/vichan-devel/vichan">vichan</a> {{ config.version }} - <a href="https://github.com/vichan-devel/vichan">vichan</a> {{ config.version }} -
<br>Tinyboard Copyright &copy; 2010-2014 Tinyboard Development Group <br>Tinyboard Copyright &copy; 2010-2014 Tinyboard Development Group
<br><a href="https://github.com/vichan-devel/vichan">vichan</a> Copyright &copy; 2012-2024 vichan-devel</p> <br><a href="https://github.com/vichan-devel/vichan">vichan</a> Copyright &copy; 2012-2025 vichan-devel</p>
{% for footer in config.footer %}<p class="unimportant" style="text-align:center;">{{ footer }}</p>{% endfor %} {% for footer in config.footer %}<p class="unimportant" style="text-align:center;">{{ footer }}</p>{% endfor %}
</footer> </footer>

View File

@ -1,55 +1,55 @@
<link rel="stylesheet" media="screen" href="{{ config.url_stylesheet }}"> <link rel="stylesheet" media="screen" href="{{ config.url_stylesheet }}?v={{ config.resource_version }}">
{% if config.url_favicon %}<link rel="shortcut icon" href="{{ config.url_favicon }}">{% endif %} {% if config.url_favicon %}<link rel="shortcut icon" href="{{ config.url_favicon }}">{% endif %}
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
{% if config.meta_keywords %}<meta name="keywords" content="{{ config.meta_keywords }}">{% endif %} {% if config.meta_keywords %}<meta name="keywords" content="{{ config.meta_keywords }}">{% endif %}
{% if config.default_stylesheet.1 != '' %}<link rel="stylesheet" type="text/css" id="stylesheet" href="{{ config.uri_stylesheets }}{{ config.default_stylesheet.1 }}">{% endif %} {% if config.default_stylesheet.1 != '' %}<link rel="stylesheet" type="text/css" id="stylesheet" href="{{ config.uri_stylesheets }}{{ config.default_stylesheet.1 }}?v={{ config.resource_version }}">{% endif %}
{% if config.font_awesome %}<link rel="stylesheet" href="{{ config.root }}{{ config.font_awesome_css }}">{% endif %} {% if config.font_awesome %}<link rel="stylesheet" href="{{ config.root }}{{ config.font_awesome_css }}?v={{ config.resource_version }}">{% endif %}
{% if config.country_flags_condensed %}<link rel="stylesheet" href="{{ config.root }}{{ config.country_flags_condensed_css }}">{% endif %} {% if config.country_flags_condensed %}<link rel="stylesheet" href="{{ config.root }}{{ config.country_flags_condensed_css }}?v={{ config.resource_version }}">{% endif %}
<script type="text/javascript"> <script type="text/javascript">
var configRoot="{{ config.root }}"; var configRoot="{{ config.root }}";
var inMod = {% if mod %}true{% else %}false{% endif %}; var inMod = {% if mod %} true {% else %} false {% endif %};
var modRoot="{{ config.root }}"+(inMod ? "mod.php?/" : ""); var modRoot = "{{ config.root }}" + (inMod ? "mod.php?/" : "");
</script> </script>
{% if not nojavascript %} {% if not nojavascript %}
<script type="text/javascript" src="{{ config.url_javascript }}"></script> <script type="text/javascript" src="{{ config.url_javascript }}?v={{ config.resource_version }} data-resource-version="{{ config.resource_version }}"></script>
{% if not config.additional_javascript_compile %} {% if not config.additional_javascript_compile %}
{% for javascript in config.additional_javascript %}<script type="text/javascript" src="{{ config.additional_javascript_url }}{{ javascript }}"></script>{% endfor %} {% for javascript in config.additional_javascript %}<script type="text/javascript" src="{{ config.additional_javascript_url }}{{ javascript }}?v={{ config.resource_version }}"></script>{% endfor %}
{% endif %} {% endif %}
{% if mod %} {% if mod %}
<script type="text/javascript" src="/js/mod/mod_snippets.js"></script> <script type="text/javascript" src="/js/mod/mod_snippets.js?v={{ config.resource_version }}"></script>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if config.recaptcha %}<script src="//www.recaptcha.net/recaptcha/api.js"></script> {% if config.captcha.provider == 'recaptcha' %}<script src="//www.recaptcha.net/recaptcha/api.js"></script>
<style type="text/css">{% verbatim %} <style type="text/css">{% verbatim %}
#recaptcha_area { #recaptcha_area {
float: none !important; float: none !important;
padding: 0 !important; padding: 0 !important;
} }
#recaptcha_logo, #recaptcha_privacy { #recaptcha_logo, #recaptcha_privacy {
display: none; display: none;
} }
#recaptcha_table { #recaptcha_table {
border: none !important; border: none !important;
} }
#recaptcha_table tr:first-child { #recaptcha_table tr:first-child {
height: auto; height: auto;
} }
.recaptchatable img { .recaptchatable img {
float: none !important; float: none !important;
} }
#recaptcha_response_field { #recaptcha_response_field {
font-size: 10pt !important; font-size: 10pt !important;
border: 1px solid #a9a9a9 !important; border: 1px solid #a9a9a9 !important;
padding: 1px !important; padding: 1px !important;
} }
td.recaptcha_image_cell { td.recaptcha_image_cell {
background: transparent !important; background: transparent !important;
} }
.recaptchatable, #recaptcha_area tr, #recaptcha_area td, #recaptcha_area th { .recaptchatable, #recaptcha_area tr, #recaptcha_area td, #recaptcha_area th {
padding: 0 !important; padding: 0 !important;
} }
{% endverbatim %}</style>{% endif %} {% endverbatim %}</style>{% endif %}
{% if config.hcaptcha %} {% if config.captcha.provider.hcaptcha %}
<script src="https://js.hcaptcha.com/1/api.js" async defer></script> <script src="https://js.hcaptcha.com/1/api.js" async defer></script>
{% endif %} {% endif %}

View File

@ -88,6 +88,9 @@
<label for="secure_trip_salt">Secure trip (##) salt:</label> <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"> <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> <label for="more">Additional configuration:</label>
<textarea id="more" name="more">{{ more }}</textarea> <textarea id="more" name="more">{{ more }}</textarea>
</fieldset> </fieldset>

View File

@ -18,92 +18,92 @@ function _(s) {
* > alert(fmt(_("{0} users"), [3])); * > alert(fmt(_("{0} users"), [3]));
* 3 uzytkownikow * 3 uzytkownikow
*/ */
function fmt(s,a) { function fmt(s, a) {
return s.replace(/\{([0-9]+)\}/g, function(x) { return a[x[1]]; }); return s.replace(/\{([0-9]+)\}/g, function(x) { return a[x[1]]; });
} }
function until($timestamp) { function until(timestamp) {
var $difference = $timestamp - Date.now()/1000|0, $num; let difference = timestamp - Date.now() / 1000 | 0;
switch(true){ switch (true) {
case ($difference < 60): case (difference < 60):
return "" + $difference + ' ' + _('second(s)'); return "" + difference + ' ' + _('second(s)');
case ($difference < 3600): //60*60 = 3600 case (difference < 3600): // 60 * 60 = 3600
return "" + ($num = Math.round($difference/(60))) + ' ' + _('minute(s)'); return "" + Math.round(difference / 60) + ' ' + _('minute(s)');
case ($difference < 86400): //60*60*24 = 86400 case (difference < 86400): // 60 * 60 * 24 = 86400
return "" + ($num = Math.round($difference/(3600))) + ' ' + _('hour(s)'); return "" + Math.round(difference / 3600) + ' ' + _('hour(s)');
case ($difference < 604800): //60*60*24*7 = 604800 case (difference < 604800): // 60 * 60 * 24 * 7 = 604800
return "" + ($num = Math.round($difference/(86400))) + ' ' + _('day(s)'); return "" + Math.round(difference / 86400) + ' ' + _('day(s)');
case ($difference < 31536000): //60*60*24*365 = 31536000 case (difference < 31536000): // 60 * 60 * 24 * 365 = 31536000
return "" + ($num = Math.round($difference/(604800))) + ' ' + _('week(s)'); return "" + Math.round(difference / 604800) + ' ' + _('week(s)');
default: default:
return "" + ($num = Math.round($difference/(31536000))) + ' ' + _('year(s)'); return "" + Math.round(difference / 31536000) + ' ' + _('year(s)');
} }
} }
function ago($timestamp) { function ago(timestamp) {
var $difference = (Date.now()/1000|0) - $timestamp, $num; let difference = (Date.now() / 1000 | 0) - timestamp;
switch(true){ switch (true) {
case ($difference < 60) : case (difference < 60):
return "" + $difference + ' ' + _('second(s)'); return "" + difference + ' ' + _('second(s)');
case ($difference < 3600): //60*60 = 3600 case (difference < 3600): /// 60 * 60 = 3600
return "" + ($num = Math.round($difference/(60))) + ' ' + _('minute(s)'); return "" + Math.round(difference/(60)) + ' ' + _('minute(s)');
case ($difference < 86400): //60*60*24 = 86400 case (difference < 86400): // 60 * 60 * 24 = 86400
return "" + ($num = Math.round($difference/(3600))) + ' ' + _('hour(s)'); return "" + Math.round(difference/(3600)) + ' ' + _('hour(s)');
case ($difference < 604800): //60*60*24*7 = 604800 case (difference < 604800): // 60 * 60 * 24 * 7 = 604800
return "" + ($num = Math.round($difference/(86400))) + ' ' + _('day(s)'); return "" + Math.round(difference/(86400)) + ' ' + _('day(s)');
case ($difference < 31536000): //60*60*24*365 = 31536000 case (difference < 31536000): // 60 * 60 * 24 * 365 = 31536000
return "" + ($num = Math.round($difference/(604800))) + ' ' + _('week(s)'); return "" + Math.round(difference/(604800)) + ' ' + _('week(s)');
default: default:
return "" + ($num = Math.round($difference/(31536000))) + ' ' + _('year(s)'); return "" + Math.round(difference/(31536000)) + ' ' + _('year(s)');
} }
} }
var datelocale = var datelocale =
{ days: [_('Sunday'), _('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday')] { days: [_('Sunday'), _('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday')]
, shortDays: [_("Sun"), _("Mon"), _("Tue"), _("Wed"), _("Thu"), _("Fri"), _("Sat")] , shortDays: [_("Sun"), _("Mon"), _("Tue"), _("Wed"), _("Thu"), _("Fri"), _("Sat")]
, months: [_('January'), _('February'), _('March'), _('April'), _('May'), _('June'), _('July'), _('August'), _('September'), _('October'), _('November'), _('December')] , months: [_('January'), _('February'), _('March'), _('April'), _('May'), _('June'), _('July'), _('August'), _('September'), _('October'), _('November'), _('December')]
, shortMonths: [_('Jan'), _('Feb'), _('Mar'), _('Apr'), _('May'), _('Jun'), _('Jul'), _('Aug'), _('Sep'), _('Oct'), _('Nov'), _('Dec')] , shortMonths: [_('Jan'), _('Feb'), _('Mar'), _('Apr'), _('May'), _('Jun'), _('Jul'), _('Aug'), _('Sep'), _('Oct'), _('Nov'), _('Dec')]
, AM: _('AM') , AM: _('AM')
, PM: _('PM') , PM: _('PM')
, am: _('am') , am: _('am')
, pm: _('pm') , pm: _('pm')
}; };
function alert(a, do_confirm, confirm_ok_action, confirm_cancel_action) { function alert(a, do_confirm, confirm_ok_action, confirm_cancel_action) {
var handler, div, bg, closebtn, okbtn; let handler, div, bg, closebtn, okbtn;
var close = function() { let close = function() {
handler.fadeOut(400, function() { handler.remove(); }); handler.fadeOut(400, function() { handler.remove(); });
return false; return false;
}; };
handler = $("<div id='alert_handler'></div>").hide().appendTo('body'); handler = $("<div id='alert_handler'></div>").hide().appendTo('body');
bg = $("<div id='alert_background'></div>").appendTo(handler); bg = $("<div id='alert_background'></div>").appendTo(handler);
div = $("<div id='alert_div'></div>").appendTo(handler); div = $("<div id='alert_div'></div>").appendTo(handler);
closebtn = $("<a id='alert_close' href='javascript:void(0)'><i class='fa fa-times'></i></div>") closebtn = $("<a id='alert_close' href='javascript:void(0)'><i class='fa fa-times'></i></div>")
.appendTo(div); .appendTo(div);
$("<div id='alert_message'></div>").html(a).appendTo(div); $("<div id='alert_message'></div>").html(a).appendTo(div);
okbtn = $("<button class='button alert_button'>"+_("OK")+"</button>").appendTo(div); okbtn = $("<button class='button alert_button'>"+_("OK")+"</button>").appendTo(div);
if (do_confirm) { if (do_confirm) {
confirm_ok_action = (typeof confirm_ok_action !== "function") ? function(){} : confirm_ok_action; confirm_ok_action = (typeof confirm_ok_action !== "function") ? function(){} : confirm_ok_action;
confirm_cancel_action = (typeof confirm_cancel_action !== "function") ? function(){} : confirm_cancel_action; confirm_cancel_action = (typeof confirm_cancel_action !== "function") ? function(){} : confirm_cancel_action;
okbtn.click(confirm_ok_action); okbtn.click(confirm_ok_action);
$("<button class='button alert_button'>"+_("Cancel")+"</button>").click(confirm_cancel_action).click(close).appendTo(div); $("<button class='button alert_button'>"+_("Cancel")+"</button>").click(confirm_cancel_action).click(close).appendTo(div);
bg.click(confirm_cancel_action); bg.click(confirm_cancel_action);
okbtn.click(confirm_cancel_action); okbtn.click(confirm_cancel_action);
closebtn.click(confirm_cancel_action); closebtn.click(confirm_cancel_action);
} }
bg.click(close); bg.click(close);
okbtn.click(close); okbtn.click(close);
closebtn.click(close); closebtn.click(close);
handler.fadeIn(400); handler.fadeIn(400);
} }
var saved = {}; var saved = {};
@ -133,20 +133,46 @@ function changeStyle(styleName, link) {
{% verbatim %} {% verbatim %}
if (!document.getElementById('stylesheet')) { if (!document.getElementById('stylesheet')) {
var s = document.createElement('link'); let s = document.createElement('link');
s.rel = 'stylesheet'; s.rel = 'stylesheet';
s.type = 'text/css'; s.type = 'text/css';
s.id = 'stylesheet'; s.id = 'stylesheet';
var x = document.getElementsByTagName('head')[0]; let x = document.getElementsByTagName('head')[0];
x.appendChild(s); x.appendChild(s);
} }
document.getElementById('stylesheet').href = styles[styleName]; let mainStylesheetElement = document.getElementById('stylesheet');
let userStylesheetElement = document.getElementById('stylesheet-user');
// Override main stylesheet with the user selected one.
if (!userStylesheetElement) {
userStylesheetElement = document.createElement('link');
userStylesheetElement.rel = 'stylesheet';
userStylesheetElement.media = 'none';
userStylesheetElement.type = 'text/css';
userStylesheetElement.id = 'stylesheet';
let x = document.getElementsByTagName('head')[0];
x.appendChild(userStylesheetElement);
}
// When the new one is loaded, disable the old one
userStylesheetElement.onload = function() {
this.media = 'all';
mainStylesheetElement.media = 'none';
}
let style = styles[styleName];
if (style !== '') {
// Add the version of the resource if the style is not the embedded one.
style += `?v=${resourceVersion}`;
}
document.getElementById('stylesheet').href = style;
selectedstyle = styleName; selectedstyle = styleName;
if (document.getElementsByClassName('styles').length != 0) { if (document.getElementsByClassName('styles').length != 0) {
var styleLinks = document.getElementsByClassName('styles')[0].childNodes; let styleLinks = document.getElementsByClassName('styles')[0].childNodes;
for (var i = 0; i < styleLinks.length; i++) { for (let i = 0; i < styleLinks.length; i++) {
styleLinks[i].className = ''; styleLinks[i].className = '';
} }
} }
@ -155,12 +181,14 @@ function changeStyle(styleName, link) {
link.className = 'selected'; link.className = 'selected';
} }
if (typeof $ != 'undefined') if (typeof $ != 'undefined') {
$(window).trigger('stylesheet', styleName); $(window).trigger('stylesheet', styleName);
}
} }
{% endverbatim %} {% endverbatim %}
var resourceVersion = document.currentScript.getAttribute('data-resource-version');
{% if config.stylesheets_board %} {% if config.stylesheets_board %}
{% verbatim %} {% verbatim %}
@ -170,7 +198,7 @@ function changeStyle(styleName, link) {
var stylesheet_choices = JSON.parse(localStorage.board_stylesheets); var stylesheet_choices = JSON.parse(localStorage.board_stylesheets);
if (board_name && stylesheet_choices[board_name]) { if (board_name && stylesheet_choices[board_name]) {
for (var styleName in styles) { for (let styleName in styles) {
if (styleName == stylesheet_choices[board_name]) { if (styleName == stylesheet_choices[board_name]) {
changeStyle(styleName); changeStyle(styleName);
break; break;
@ -181,7 +209,7 @@ function changeStyle(styleName, link) {
{% else %} {% else %}
{% verbatim %} {% verbatim %}
if (localStorage.stylesheet) { if (localStorage.stylesheet) {
for (var styleName in styles) { for (let styleName in styles) {
if (styleName == localStorage.stylesheet) { if (styleName == localStorage.stylesheet) {
changeStyle(styleName); changeStyle(styleName);
break; break;
@ -192,12 +220,12 @@ function changeStyle(styleName, link) {
{% endif %} {% endif %}
{% verbatim %} {% verbatim %}
function init_stylechooser() { function initStyleChooser() {
var newElement = document.createElement('div'); let newElement = document.createElement('div');
newElement.className = 'styles'; newElement.className = 'styles';
for (styleName in styles) { for (styleName in styles) {
var style = document.createElement('a'); let style = document.createElement('a');
style.innerHTML = '[' + styleName + ']'; style.innerHTML = '[' + styleName + ']';
style.onclick = function() { style.onclick = function() {
changeStyle(this.innerHTML.substring(1, this.innerHTML.length - 1), this); changeStyle(this.innerHTML.substring(1, this.innerHTML.length - 1), this);
@ -212,46 +240,78 @@ function init_stylechooser() {
document.getElementsByTagName('body')[0].insertBefore(newElement, document.getElementsByTagName('body')[0].lastChild.nextSibling); document.getElementsByTagName('body')[0].insertBefore(newElement, document.getElementsByTagName('body')[0].lastChild.nextSibling);
} }
function get_cookie(cookie_name) { function getCookie(cookie_name) {
var results = document.cookie.match ( '(^|;) ?' + cookie_name + '=([^;]*)(;|$)'); let results = document.cookie.match('(^|;) ?' + cookie_name + '=([^;]*)(;|$)');
if (results) if (results) {
return (unescape(results[2])); return unescape(results[2]);
else } else {
return null; return null;
}
} }
{% endverbatim %}
{% if config.captcha.dynamic %}
function is_dynamic_captcha_enabled() {
let cookie = get_cookie('require-captcha');
return cookie === '1';
}
function get_captcha_pub_key() {
{% if config.captcha.provider == 'recaptcha' %}
return "{{ config.captcha.recaptcha.sitekey }}";
{% else %}
return null;
{% endif %}
}
function init_dynamic_captcha() {
if (!is_dynamic_captcha_enabled()) {
let pub_key = get_captcha_pub_key();
if (!pub_key) {
console.error("Missing public captcha key!");
return;
}
let captcha_hook = document.getElementById('captcha');
captcha_hook.style = "";
}
}
{% endif %}
{% verbatim %}
function highlightReply(id) { function highlightReply(id) {
if (typeof window.event != "undefined" && event.which == 2) { if (typeof window.event != "undefined" && event.which == 2) {
// don't highlight on middle click // don't highlight on middle click
return true; return true;
} }
var divs = document.getElementsByTagName('div'); let divs = document.getElementsByTagName('div');
for (var i = 0; i < divs.length; i++) for (let i = 0; i < divs.length; i++) {
{ if (divs[i].className.indexOf('post') != -1) {
if (divs[i].className.indexOf('post') != -1)
divs[i].className = divs[i].className.replace(/highlighted/, ''); divs[i].className = divs[i].className.replace(/highlighted/, '');
}
} }
if (id) { if (id) {
var post = document.getElementById('reply_'+id); let post = document.getElementById('reply_' + id);
if (post) if (post) {
post.className += ' highlighted'; post.className += ' highlighted';
window.location.hash = id; }
window.location.hash = id;
} }
return true; return true;
} }
function generatePassword() { function generatePassword() {
var pass = ''; let pass = '';
var chars = '{% endverbatim %}{{ config.genpassword_chars }}{% verbatim %}'; let chars = '{% endverbatim %}{{ config.genpassword_chars }}{% verbatim %}';
for (var i = 0; i < 8; i++) { for (let i = 0; i < 8; i++) {
var rnd = Math.floor(Math.random() * chars.length); let rnd = Math.floor(Math.random() * chars.length);
pass += chars.substring(rnd, rnd + 1); pass += chars.substring(rnd, rnd + 1);
} }
return pass; return pass;
} }
function dopost(form) { function doPost(form) {
if (form.elements['name']) { if (form.elements['name']) {
localStorage.name = form.elements['name'].value.replace(/( |^)## .+$/, ''); localStorage.name = form.elements['name'].value.replace(/( |^)## .+$/, '');
} }
@ -269,18 +329,14 @@ function dopost(form) {
} }
function citeReply(id, with_link) { function citeReply(id, with_link) {
var textarea = document.getElementById('body'); let textarea = document.getElementById('body');
if (!textarea) {
return false;
}
if (!textarea) return false; if (textarea.selectionStart || textarea.selectionStart == '0') {
let start = textarea.selectionStart;
if (document.selection) { let end = textarea.selectionEnd;
// IE
textarea.focus();
var sel = document.selection.createRange();
sel.text = '>>' + id + '\n';
} else if (textarea.selectionStart || textarea.selectionStart == '0') {
var start = textarea.selectionStart;
var end = textarea.selectionEnd;
textarea.value = textarea.value.substring(0, start) + '>>' + id + '\n' + textarea.value.substring(end, textarea.value.length); textarea.value = textarea.value.substring(0, start) + '>>' + id + '\n' + textarea.value.substring(end, textarea.value.length);
textarea.selectionStart += ('>>' + id).length + 1; textarea.selectionStart += ('>>' + id).length + 1;
@ -290,10 +346,10 @@ function citeReply(id, with_link) {
textarea.value += '>>' + id + '\n'; textarea.value += '>>' + id + '\n';
} }
if (typeof $ != 'undefined') { if (typeof $ != 'undefined') {
var select = document.getSelection().toString(); let select = document.getSelection().toString();
if (select) { if (select) {
var body = $('#reply_' + id + ', #op_' + id).find('div.body'); // TODO: support for OPs let body = $('#reply_' + id + ', #op_' + id).find('div.body'); // TODO: support for OPs
var index = body.text().indexOf(select.replace('\n', '')); // for some reason this only works like this let index = body.text().indexOf(select.replace('\n', '')); // for some reason this only works like this
if (index > -1) { if (index > -1) {
textarea.value += '>' + select + '\n'; textarea.value += '>' + select + '\n';
} }
@ -308,25 +364,29 @@ function citeReply(id, with_link) {
function rememberStuff() { function rememberStuff() {
if (document.forms.post) { if (document.forms.post) {
if (document.forms.post.password) { if (document.forms.post.password) {
if (!localStorage.password) if (!localStorage.password) {
localStorage.password = generatePassword(); localStorage.password = generatePassword();
}
document.forms.post.password.value = localStorage.password; document.forms.post.password.value = localStorage.password;
} }
if (localStorage.name && document.forms.post.elements['name']) if (localStorage.name && document.forms.post.elements['name']) {
document.forms.post.elements['name'].value = localStorage.name; document.forms.post.elements['name'].value = localStorage.name;
if (localStorage.email && document.forms.post.elements['email']) }
if (localStorage.email && document.forms.post.elements['email']) {
document.forms.post.elements['email'].value = localStorage.email; document.forms.post.elements['email'].value = localStorage.email;
}
if (window.location.hash.indexOf('q') == 1) if (window.location.hash.indexOf('q') == 1) {
citeReply(window.location.hash.substring(2), true); citeReply(window.location.hash.substring(2), true);
}
if (sessionStorage.body) { if (sessionStorage.body) {
var saved = JSON.parse(sessionStorage.body); let saved = JSON.parse(sessionStorage.body);
if (get_cookie('{% endverbatim %}{{ config.cookies.js }}{% verbatim %}')) { if (getCookie('{% endverbatim %}{{ config.cookies.js }}{% verbatim %}')) {
// Remove successful posts // Remove successful posts
var successful = JSON.parse(get_cookie('{% endverbatim %}{{ config.cookies.js }}{% verbatim %}')); let successful = JSON.parse(getCookie('{% endverbatim %}{{ config.cookies.js }}{% verbatim %}'));
for (var url in successful) { for (let url in successful) {
saved[url] = null; saved[url] = null;
} }
sessionStorage.body = JSON.stringify(saved); sessionStorage.body = JSON.stringify(saved);
@ -350,14 +410,15 @@ var script_settings = function(script_name) {
this.get = function(var_name, default_val) { this.get = function(var_name, default_val) {
if (typeof tb_settings == 'undefined' || if (typeof tb_settings == 'undefined' ||
typeof tb_settings[this.script_name] == 'undefined' || typeof tb_settings[this.script_name] == 'undefined' ||
typeof tb_settings[this.script_name][var_name] == 'undefined') typeof tb_settings[this.script_name][var_name] == 'undefined') {
return default_val; return default_val;
}
return tb_settings[this.script_name][var_name]; return tb_settings[this.script_name][var_name];
} }
}; };
function init() { function init() {
init_stylechooser(); initStyleChooser();
{% endverbatim %} {% endverbatim %}
{% if config.allow_delete %} {% if config.allow_delete %}
@ -376,12 +437,12 @@ var RecaptchaOptions = {
}; };
onready_callbacks = []; onready_callbacks = [];
function onready(fnc) { function onReady(fnc) {
onready_callbacks.push(fnc); onready_callbacks.push(fnc);
} }
function ready() { function ready() {
for (var i = 0; i < onready_callbacks.length; i++) { for (let i = 0; i < onready_callbacks.length; i++) {
onready_callbacks[i](); onready_callbacks[i]();
} }
} }
@ -391,11 +452,11 @@ function ready() {
var post_date = "{{ config.post_date }}"; var post_date = "{{ config.post_date }}";
var max_images = {{ config.max_images }}; var max_images = {{ config.max_images }};
onready(init); onReady(init);
{% if config.google_analytics %}{% verbatim %} {% if config.google_analytics %}{% verbatim %}
var _gaq = _gaq || [];_gaq.push(['_setAccount', '{% endverbatim %}{{ config.google_analytics }}{% verbatim %}']);{% endverbatim %}{% if config.google_analytics_domain %}{% verbatim %}_gaq.push(['_setDomainName', '{% endverbatim %}{{ config.google_analytics_domain }}{% verbatim %}']){% endverbatim %}{% endif %}{% if not config.google_analytics_domain %}{% verbatim %}_gaq.push(['_setDomainName', 'none']){% endverbatim %}{% endif %}{% verbatim %};_gaq.push(['_trackPageview']);(function() {var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;ga.src = ('https:' == document.location.protocol ? 'https://' : 'http://') + 'stats.g.doubleclick.net/dc.js';var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);})();{% endverbatim %}{% endif %} var _gaq = _gaq || [];_gaq.push(['_setAccount', '{% endverbatim %}{{ config.google_analytics }}{% verbatim %}']);{% endverbatim %}{% if config.google_analytics_domain %}{% verbatim %}_gaq.push(['_setDomainName', '{% endverbatim %}{{ config.google_analytics_domain }}{% verbatim %}']){% endverbatim %}{% endif %}{% if not config.google_analytics_domain %}{% verbatim %}_gaq.push(['_setDomainName', 'none']){% endverbatim %}{% endif %}{% verbatim %};_gaq.push(['_trackPageview']);(function() {var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;ga.src = ('https:' == document.location.protocol ? 'https://' : 'http://') + 'stats.g.doubleclick.net/dc.js';let s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);})();{% endverbatim %}{% endif %}
{% if config.statcounter_project and config.statcounter_security %} {% if config.statcounter_project and config.statcounter_security %}
var sc = document.createElement('script'); var sc = document.createElement('script');
@ -404,4 +465,3 @@ sc.innerHTML = 'var sc_project={{ config.statcounter_project }};var sc_invisible
var s = document.getElementsByTagName('script')[0]; var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(sc, s); s.parentNode.insertBefore(sc, s);
{% endif %} {% endif %}

View File

@ -11,7 +11,7 @@
</tr> </tr>
<tr> <tr>
<th>{% trans 'IP' %}</th> <th>{% trans 'IP' %}</th>
<td>{{ ban.cmask }}</td> <td><a class="ip-link" style="margin:0;" href="?/IP/{{ ban.mask|cloak_mask }}">{{ ban.mask|cloak_mask }}</a></td>
</tr> </tr>
<tr> <tr>
<th>{% trans 'Reason' %}</th> <th>{% trans 'Reason' %}</th>

View File

@ -1,11 +1,11 @@
<script src='main.js'></script> <script src='main.js?v={{ config.resource_version }}' data-resource-version="{{ config.resource_version }}"></script>
<script src='js/jquery.min.js'></script> <script src='js/jquery.min.js?v={{ config.resource_version }}'></script>
<script src='js/mobile-style.js'></script> <script src='js/mobile-style.js?v={{ config.resource_version }}'></script>
<script src='js/strftime.min.js'></script> <script src='js/strftime.min.js?v={{ config.resource_version }}'></script>
<script src='js/longtable/longtable.js'></script> <script src='js/longtable/longtable.js?v={{ config.resource_version }}'></script>
<script src='js/mod/ban-list.js'></script> <script src='js/mod/ban-list.js?v={{ config.resource_version }}'></script>
<link rel='stylesheet' href='stylesheets/longtable/longtable.css'> <link rel='stylesheet' href='stylesheets/longtable/longtable.css?v={{ config.resource_version }}'>
<link rel='stylesheet' href='stylesheets/mod/ban-list.css'> <link rel='stylesheet' href='stylesheets/mod/ban-list.css?v={{ config.resource_version }}'>
<form action="?/bans" method="post" class="banform"> <form action="?/bans" method="post" class="banform">
{% if token %} {% if token %}
@ -21,7 +21,7 @@
<div class='buttons'> <div class='buttons'>
<input type="text" id="search" placeholder="{% trans %}Search{% endtrans %}"> <input type="text" id="search" placeholder="{% trans %}Search{% endtrans %}">
{% if mod %} {% if mod %}
<input type="submit" name="unban" id="unban" onclick="return confirm('Are you sure you want to unban the selected IPs?');" value="{% trans 'Unban selected' %}"> <input type="submit" name="unban" id="unban" value="{% trans 'Unban selected' %}">
{% endif %} {% endif %}
</div> </div>

View File

@ -70,7 +70,7 @@
<li> <li>
<a href="?/inbox"> <a href="?/inbox">
{% trans 'PM inbox' %} {% trans 'PM inbox' %}
{% if unread_pms > 0 %}<strong>{%endif %}({{ unread_pms }} unread){% if unread_pms > 0 %}</strong>{%endif %} {% if unread_pms > 0 %}<strong>{%endif %} ({{ unread_pms }} unread){% if unread_pms > 0 %}</strong>{%endif %}
</a> </a>
</li> </li>
</ul> </ul>
@ -166,7 +166,7 @@
<li> <li>
A newer version of vichan A newer version of vichan
(<strong>v{{ newer_release.massive }}.{{ newer_release.major }}.{{ newer_release.minor }}</strong>) is available! (<strong>v{{ newer_release.massive }}.{{ newer_release.major }}.{{ newer_release.minor }}</strong>) is available!
See <a href="https://engine.vichan.net">https://engine.vichan.net/</a> for upgrade instructions. See <a href="https://vichan.info">https://vichan.info/</a> for upgrade instructions.
</li> </li>
</ul> </ul>
</fieldset> </fieldset>

View File

@ -5,7 +5,7 @@
<tr> <tr>
<th>{% trans %}Markup method{% endtrans %} <th>{% trans %}Markup method{% endtrans %}
{% set allowed_html = config.allowed_html %} {% set allowed_html = config.allowed_html %}
{% trans %}<p class="unimportant">"markdown" is provided by <a href="http://parsedown.org/">parsedown</a>. Note: images disabled.</p> {% trans %}<p class="unimportant">"markdown" is provided by <a href="http://parsedown.org/">parsedown</a></p>
<p class="unimportant">"html" allows the following tags:<br/>{{ allowed_html }}</p> <p class="unimportant">"html" allows the following tags:<br/>{{ allowed_html }}</p>
<p class="unimportant">"infinity" is the same as what is used in posts.</p> <p class="unimportant">"infinity" is the same as what is used in posts.</p>
<p class="unimportant">This page will not convert between formats,<br/>choose it once or do the conversion yourself!</p>{% endtrans %} <p class="unimportant">This page will not convert between formats,<br/>choose it once or do the conversion yourself!</p>{% endtrans %}

View File

@ -7,7 +7,7 @@
{% trans 'Username' %} {% trans 'Username' %}
</th> </th>
<td> <td>
<input type="text" name="username" size="20" maxlength="30" value="{{ username|e }}"> <input type="text" name="username" size="20" maxlength="30" value="{{ username|e }}" autofocus>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -1,4 +1,4 @@
<script type="text/javascript" src="js/jquery.min.js"></script> <script type="text/javascript" src="js/jquery.min.js?v={{ config.resource_version }}"></script>
<div style="text-align:center"> <div style="text-align:center">
<p class="unimportant"> <p class="unimportant">
{% if board %} {% if board %}

View File

@ -1,4 +1,4 @@
<script src="{{ config.additional_javascript_url }}js/mod/recent-posts.js"></script> <script src="{{ config.additional_javascript_url }}js/mod/recent-posts.js?v={{ config.resource_version }}"></script>
{% if not posts|length %} {% if not posts|length %}
<p style="text-align:center" class="unimportant">({% trans 'There are no active posts.' %})</p> <p style="text-align:center" class="unimportant">({% trans 'There are no active posts.' %})</p>
{% else %} {% else %}

View File

@ -103,7 +103,7 @@
<fieldset> <fieldset>
<legend>{% trans 'New ban' %}</legend> <legend>{% trans 'New ban' %}</legend>
{% set redirect = '?/IP/' ~ ip|cloak_ip ~ '#bans' %} {% set redirect = '?/IP/' ~ ip|cloak_ip ~ '#bans' %}
{% include 'mod/ban_form.html' %} {% include 'mod/ban_form.html' with { 'reasons': config.premade_ban_reasons } %}
</fieldset> </fieldset>
{% endif %} {% endif %}

View File

@ -1,13 +1,13 @@
{% if post.embed %} {% if post.embed %}
{{ post.embed }} {{ post.embed }}
{% else %} {% else %}
<div class="files"> <div class="files {% if post.num_files > 1 %} multifile{% endif %}">
{% for file in post.files %} {% for file in post.files %}
<div class="file{% if post.num_files > 1 %} multifile" style="width:{{ file.thumbwidth + 40 }}px"{% else %}"{% endif %}> <div class="file{% if post.num_files > 1 %} multifile" style="width:{{ file.thumbwidth + 40 }}px"{% else %}"{% endif %}>
{% if file.file == 'deleted' %} {% if file.file == 'deleted' %}
<img class="post-image deleted" src="{{ config.root }}{{ config.image_deleted }}" alt="" /> <img class="post-image deleted" src="{{ config.root }}{{ config.image_deleted }}" alt="" />
{% else %} {% else %}
<p class="fileinfo">File: <a href="{{ config.uri_img }}{{ file.file }}">{{ file.file }}</a> <span class="unimportant"> <p class="fileinfo"><span>File: <a href="{{ config.uri_img }}{{ file.file }}">{{ file.file }}</a></span><span class="unimportant">
( (
{% if file.thumb == 'spoiler' %} {% if file.thumb == 'spoiler' %}
{% trans %}Spoiler Image{% endtrans %}, {% trans %}Spoiler Image{% endtrans %},
@ -21,18 +21,22 @@
{% endif %} {% endif %}
{% if config.show_filename and file.filename %} {% if config.show_filename and file.filename %}
, ,
{% if file.filename|length > config.max_filename_display %} {% if file.thumb == 'spoiler' %}
<span class="postfilename" title="{{ file.filename|e|bidi_cleanup }}">{{ file.filename|truncate_filename(config.max_filename_display)|e|bidi_cleanup }}</span> <a href="{{ config.uri_img }}{{ file.file|e|bidi_cleanup }}" download="{{ file.filename|e|bidi_cleanup }}" title="{% trans %}Spoiler Image{% endtrans %}">{% trans %}Spoiler Image{% endtrans %}</a>
{% elseif file.filename|length > config.max_filename_display %}
<a href="{{ config.uri_img }}{{ file.file|e|bidi_cleanup }}" download="{{ file.filename|e|bidi_cleanup }}" title="Save as original filename: {{ file.filename|e|bidi_cleanup }}">{{ file.filename|truncate_filename(config.max_filename_display)|e|bidi_cleanup }}</a>
{% else %} {% else %}
<span class="postfilename">{{ file.filename|e|bidi_cleanup }}</span> <a href="{{ config.uri_img }}{{ file.file|e|bidi_cleanup }}" download="{{ file.filename|e|bidi_cleanup }}" title="Save as original filename">{{ file.filename|e|bidi_cleanup }}</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
) )
</span>
{% include "post/image_identification.html" %} {% include "post/image_identification.html" %}
{% include "post/file_controls.html" %}</span></p> {% include "post/file_controls.html" %}
</p>
{% include "post/image.html" with {'post':file} %} {% include "post/image.html" with {'post':file} %}
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}

View File

@ -20,7 +20,7 @@
{{ config.file_thumb|sprintf(config.file_icons.default) }} {{ config.file_thumb|sprintf(config.file_icons.default) }}
{% endif %} {% endif %}
" "
style="width:{{ post.thumbwidth }}px;height:{{ post.thumbheight }}px" style="width:{{ post.thumbwidth }}px;height:{{ post.thumbheight }}px" {% if config.content_lazy_loading %}loading="lazy"{% endif %}
> >
</video> </video>
{% else %} {% else %}
@ -39,7 +39,7 @@
{{ config.uri_thumb }}{{ post.thumb }} {{ config.uri_thumb }}{{ post.thumb }}
{% endif %} {% endif %}
" "
style="width:{{ post.thumbwidth }}px;height:{{ post.thumbheight }}px" alt="" style="width:{{ post.thumbwidth }}px;height:{{ post.thumbheight }}px" {% if config.content_lazy_loading %}loading="lazy"{% endif %} alt=""
/> />
{% endif %} {% endif %}
</a> </a>

View File

@ -1,9 +1,6 @@
<form name="post" onsubmit="return dopost(this);" enctype="multipart/form-data" action="{{ config.post_url }}" method="post"> <form name="post" onsubmit="return doPost(this);" enctype="multipart/form-data" action="{{ config.post_url }}" method="post">
{{ antibot.html() }}
{% if id %}<input type="hidden" name="thread" value="{{ id }}">{% endif %} {% if id %}<input type="hidden" name="thread" value="{{ id }}">{% endif %}
{{ antibot.html() }}
<input type="hidden" name="board" value="{{ board.uri }}"> <input type="hidden" name="board" value="{{ board.uri }}">
{{ antibot.html() }}
{% if current_page %} {% if current_page %}
<input type="hidden" name="page" value="{{ current_page }}"> <input type="hidden" name="page" value="{{ current_page }}">
{% endif %} {% endif %}
@ -12,11 +9,9 @@
{% if not config.field_disable_name or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr> {% if not config.field_disable_name or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr>
<th> <th>
{% trans %}Name{% endtrans %} {% trans %}Name{% endtrans %}
{{ antibot.html() }}
</th> </th>
<td> <td>
<input type="text" name="name" size="25" maxlength="35" autocomplete="off"> {% if config.allow_no_country and config.country_flags %}<input id="no_country" name="no_country" type="checkbox"> <label for="no_country">{% trans %}Don't show my flag{% endtrans %}</label>{% endif %} <input type="text" name="name" size="25" maxlength="35" autocomplete="off"> {% if config.allow_no_country and config.country_flags %}<input id="no_country" name="no_country" type="checkbox"> <label for="no_country">{% trans %}Don't show my flag{% endtrans %}</label>{% endif %}
{{ antibot.html() }}
</td> </td>
</tr>{% endif %} </tr>{% endif %}
{% if not config.field_disable_email or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr> {% if not config.field_disable_email or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr>
@ -26,7 +21,6 @@
{% else %} {% else %}
{% trans %}Email{% endtrans %} {% trans %}Email{% endtrans %}
{% endif %} {% endif %}
{{ antibot.html() }}
</th> </th>
<td> <td>
{% if (mod and not post.mod|hasPermission(config.mod.bypass_field_disable, board.uri) and config.field_email_selectbox) or (not mod and config.field_email_selectbox) %} {% if (mod and not post.mod|hasPermission(config.mod.bypass_field_disable, board.uri) and config.field_email_selectbox) or (not mod and config.field_email_selectbox) %}
@ -39,17 +33,14 @@
{% else %} {% else %}
<input type="text" name="email" size="25" maxlength="40" autocomplete="off"> <input type="text" name="email" size="25" maxlength="40" autocomplete="off">
{% endif %} {% endif %}
{{ antibot.html() }}
{% if not (not (config.field_disable_subject or (id and config.field_disable_reply_subject)) or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri))) %} {% if not (not (config.field_disable_subject or (id and config.field_disable_reply_subject)) or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri))) %}
<input accesskey="s" style="margin-left:2px;" type="submit" name="post" value="{% if id %}{{ config.button_reply }}{% else %}{{ config.button_newtopic }}{% endif %}" />{% if config.spoiler_images %} <input id="spoiler" name="spoiler" type="checkbox"> <label for="spoiler">{% trans %}Spoiler Image{% endtrans %}</label> {% endif %} <input accesskey="s" style="margin-left:2px;" type="submit" name="post" value="{% if id %}{{ config.button_reply }}{% else %}{{ config.button_newtopic }}{% endif %}" />{% if config.spoiler_images %} <input id="spoiler" name="spoiler" type="checkbox"> <label for="spoiler">{% trans %}Spoiler Image{% endtrans %}</label> {% endif %}
{% endif %} {% endif %}
{{ antibot.html() }}
</td> </td>
</tr>{% endif %} </tr>{% endif %}
{% if not (config.field_disable_subject or (id and config.field_disable_reply_subject)) or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr> {% if not (config.field_disable_subject or (id and config.field_disable_reply_subject)) or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr>
<th> <th>
{% trans %}Subject{% endtrans %} {% trans %}Subject{% endtrans %}
{{ antibot.html() }}
</th> </th>
<td> <td>
<input style="float:left;" type="text" name="subject" size="25" maxlength="100" autocomplete="off"> <input style="float:left;" type="text" name="subject" size="25" maxlength="100" autocomplete="off">
@ -60,11 +51,9 @@
<tr> <tr>
<th> <th>
{% trans %}Comment{% endtrans %} {% trans %}Comment{% endtrans %}
{{ antibot.html() }}
</th> </th>
<td> <td>
<textarea name="body" id="body" rows="5" cols="35"></textarea> <textarea name="body" id="body" rows="5" cols="35"></textarea>
{{ antibot.html() }}
{% if not (not (config.field_disable_subject or (id and config.field_disable_reply_subject)) or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri))) %} {% if not (not (config.field_disable_subject or (id and config.field_disable_reply_subject)) or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri))) %}
{% if not (not config.field_disable_email or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri))) %} {% if not (not config.field_disable_email or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri))) %}
<input accesskey="s" style="margin-left:2px;" type="submit" name="post" value="{% if id %}{{ config.button_reply }}{% else %}{{ config.button_newtopic }}{% endif %}" />{% if config.spoiler_images %} <input id="spoiler" name="spoiler" type="checkbox"> <label for="spoiler">{% trans %}Spoiler Image{% endtrans %}</label>{% endif %} <input accesskey="s" style="margin-left:2px;" type="submit" name="post" value="{% if id %}{{ config.button_reply }}{% else %}{{ config.button_newtopic }}{% endif %}" />{% if config.spoiler_images %} <input id="spoiler" name="spoiler" type="checkbox"> <label for="spoiler">{% trans %}Spoiler Image{% endtrans %}</label>{% endif %}
@ -72,57 +61,57 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% if config.recaptcha %} {% if config.captcha.provider == 'recaptcha' %}
{% if config.captcha.dynamic %}
<tr id="captcha" style="display: none;">
{% else %}
<tr> <tr>
{% endif %}
<th> <th>
{% trans %}Verification{% endtrans %} {% trans %}Verification{% endtrans %}
{{ antibot.html() }}
</th> </th>
<td> <td>
<div class="g-recaptcha" data-sitekey="{{ config.recaptcha_public }}"></div> <div class="g-recaptcha" data-sitekey="{{ config.captcha.recaptcha.sitekey }}"></div>
{{ antibot.html() }}
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
{% if config.hcaptcha %} {% if config.captcha.provider == 'hcaptcha' %}
<tr> <tr>
<th> <th>
{% trans %}Verification{% endtrans %} {% trans %}Verification{% endtrans %}
{{ antibot.html() }}
</th> </th>
<td> <td>
<div class="h-captcha" data-sitekey="{{ config.hcaptcha_public }}"></div> <div class="h-captcha" data-sitekey="{{ config.captcha.hcaptcha.sitekey }}"></div>
{{ antibot.html() }}
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
{% if config.captcha.enabled %} {% if config.captcha.provider == 'native' %}
<tr class='captcha'> <tr class='captcha'>
<th> <th>
{% trans %}Verification{% endtrans %} {% trans %}Verification{% endtrans %}
</th> </th>
<td> <td>
<script>load_captcha("{{ config.captcha.provider_get }}", "{{ config.captcha.extra }}");</script> <script>load_captcha("{{ config.captcha.native.provider_get }}", "{{ config.captcha.native.extra }}");</script>
<noscript> <noscript>
<input class='captcha_text' type='text' name='captcha_text' size='32' maxlength='6' autocomplete='off'> <input class='captcha_text' type='text' name='captcha_text' size='32' maxlength='6' autocomplete='off'>
<div class="captcha_html"> <div class="captcha_html">
<img src="/{{ config.captcha.provider_get }}?mode=get&raw=1"> <img src="/{{ config.captcha.native.provider_get }}?mode=get&raw=1">
</div> </div>
</noscript> </noscript>
</td> </td>
</tr> </tr>
{% elseif config.new_thread_capt %} {% elseif config.captcha.native.new_thread_capt %}
{% if not id %} {% if not id %}
<tr class='captcha'> <tr class='captcha'>
<th> <th>
{% trans %}Verification{% endtrans %} {% trans %}Verification{% endtrans %}
</th> </th>
<td> <td>
<script>load_captcha("{{ config.captcha.provider_get }}", "{{ config.captcha.extra }}");</script> <script>load_captcha("{{ config.captcha.native.provider_get }}", "{{ config.captcha.native.extra }}");</script>
<noscript> <noscript>
<input class='captcha_text' type='text' name='captcha_text' size='32' maxlength='6' autocomplete='off'> <input class='captcha_text' type='text' name='captcha_text' size='32' maxlength='6' autocomplete='off'>
<div class="captcha_html"> <div class="captcha_html">
<img src="/{{ config.captcha.provider_get }}?mode=get&raw=1"> <img src="/{{ config.captcha.native.provider_get }}?mode=get&raw=1">
</div> </div>
</noscript> </noscript>
</td> </td>
@ -169,14 +158,12 @@
<input style="display:inline" type="text" id="file_url" name="file_url" size="35"> <input style="display:inline" type="text" id="file_url" name="file_url" size="35">
</div> </div>
{% endif %} {% endif %}
{{ antibot.html() }}
</td> </td>
</tr> </tr>
{% if config.enable_embedding %} {% if config.enable_embedding %}
<tr id="upload_embed"> <tr id="upload_embed">
<th> <th>
{% trans %}Embed{% endtrans %} {% trans %}Embed{% endtrans %}
{{ antibot.html() }}
</th> </th>
<td> <td>
<input type="text" name="embed" value="" size="30" maxlength="120" autocomplete="off"> <input type="text" name="embed" value="" size="30" maxlength="120" autocomplete="off">
@ -207,27 +194,21 @@
{% if not config.field_disable_password or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr> {% if not config.field_disable_password or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr>
<th> <th>
{% trans %}Password{% endtrans %} {% trans %}Password{% endtrans %}
{{ antibot.html() }}
</th> </th>
<td> <td>
<input type="text" name="password" value="" size="12" maxlength="18" autocomplete="off"> <input type="text" name="password" value="" size="12" maxlength="18" autocomplete="off">
<span class="unimportant">{% trans %}(For file deletion.){% endtrans %}</span> <span class="unimportant">{% trans %}(For file deletion.){% endtrans %}</span>
{{ antibot.html() }}
</td> </td>
</tr>{% endif %} </tr>{% endif %}
{% if config.simple_spam and not id %}<tr> {% if config.simple_spam and not id %}<tr>
<th> <th>
{{ config.simple_spam.question }} {{ config.simple_spam.question }}
{{ antibot.html() }}
</th> </th>
<td> <td>
<input type="text" name="simple_spam" value="" size="12" maxlength="18" autocomplete="off"> <input type="text" name="simple_spam" value="" size="12" maxlength="18" autocomplete="off">
{{ antibot.html() }}
</td> </td>
</tr>{% endif %} </tr>{% endif %}
</table> </table>
{{ antibot.html(true) }}
<input type="hidden" name="hash" value="{{ antibot.hash() }}">
</form> </form>
<script type="text/javascript">{% verbatim %} <script type="text/javascript">{% verbatim %}

View File

@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS ``posts_{{ board }}`` (
`files` text DEFAULT NULL, `files` text DEFAULT NULL,
`num_files` int(11) DEFAULT 0, `num_files` int(11) DEFAULT 0,
`filehash` text CHARACTER SET ascii, `filehash` text CHARACTER SET ascii,
`password` varchar(20) DEFAULT NULL, `password` varchar(64) DEFAULT NULL,
`ip` varchar(39) CHARACTER SET ascii NOT NULL, `ip` varchar(39) CHARACTER SET ascii NOT NULL,
`sticky` int(1) NOT NULL, `sticky` int(1) NOT NULL,
`locked` int(1) NOT NULL, `locked` int(1) NOT NULL,

View File

@ -51,7 +51,7 @@
{% else %} {% else %}
<img src="{{post.file}}" <img src="{{post.file}}"
{% endif %} {% endif %}
id="img-{{ post.id }}" data-subject="{% if post.subject %}{{ post.subject|e }}{% endif %}" data-name="{{ post.name|e }}" data-muhdifference="{{ post.muhdifference }}" class="{{post.board}} thread-image" title="{{post.bump|date('M d H:i')}}"> id="img-{{ post.id }}" data-subject="{% if post.subject %}{{ post.subject|e }}{% endif %}" data-name="{{ post.name|e }}" data-muhdifference="{{ post.muhdifference }}" class="{{post.board}} thread-image" title="{{post.bump|date('%b %d %H:%M')}} {% if config.content_lazy_loading %}loading="lazy"{% endif %}">
</a> </a>
<div class="replies"> <div class="replies">
<strong>R: {{ post.reply_count }} / I: {{ post.image_count }}{% if post.sticky %} (sticky){% endif %}</strong> <strong>R: {{ post.reply_count }} / I: {{ post.image_count }}{% if post.sticky %} (sticky){% endif %}</strong>
@ -80,7 +80,7 @@
{% endverbatim %} {% endverbatim %}
{% for name, uri in config.stylesheets %}{% verbatim %}'{% endverbatim %}{{ name|addslashes }}{% verbatim %}' : '{% endverbatim %}/stylesheets/{{ uri|addslashes }}{% verbatim %}', {% for name, uri in config.stylesheets %}{% verbatim %}'{% endverbatim %}{{ name|addslashes }}{% verbatim %}' : '{% endverbatim %}/stylesheets/{{ uri|addslashes }}{% verbatim %}',
{% endverbatim %}{% endfor %}{% verbatim %} {% endverbatim %}{% endfor %}{% verbatim %}
}; onready(init); }; onReady(init);
{% endverbatim %}</script> {% endverbatim %}</script>
<script type="text/javascript">{% verbatim %} <script type="text/javascript">{% verbatim %}

View File

@ -17,17 +17,12 @@
'default' => 'Catalog' 'default' => 'Catalog'
); );
$__boards = listBoards();
$__default_boards = Array();
foreach ($__boards as $__board)
$__default_boards[] = $__board['uri'];
$theme['config'][] = Array( $theme['config'][] = Array(
'title' => 'Included boards', 'title' => 'Included boards',
'name' => 'boards', 'name' => 'boards',
'type' => 'text', 'type' => 'text',
'comment' => '(space seperated)', 'comment' => '(space seperated)',
'default' => implode(' ', $__default_boards) 'default' => '*'
); );
$theme['config'][] = Array( $theme['config'][] = Array(

View File

@ -1,6 +1,15 @@
<?php <?php
require 'info.php'; require 'info.php';
function get_all_boards() {
$boards = [];
$query = query("SELECT uri FROM ``boards``") or error(db_error());
while ($board = $query->fetch(PDO::FETCH_ASSOC)) {
$boards[] = $board['uri'];
}
return $boards;
}
function catalog_build($action, $settings, $board) { function catalog_build($action, $settings, $board) {
global $config; global $config;
@ -13,6 +22,11 @@
$boards = explode(' ', $settings['boards']); $boards = explode(' ', $settings['boards']);
if (in_array('*', $boards)) {
$boards = get_all_boards();
}
if ($action == 'all') { if ($action == 'all') {
foreach ($boards as $board) { foreach ($boards as $board) {
$b = new Catalog(); $b = new Catalog();

View File

@ -34,17 +34,28 @@
</ul> </ul>
</fieldset> </fieldset>
<br> <br>
<div class="mainBox"> {% if settings.description or settings.imageofnow or settings.quoteofnow or settings.videoofnow %}
<br> <div class="mainBox">
<div class="description">{{ settings.description }}</div> <br>
<br> {% if settings.description %}
<img class="imageofnow" src="{{ settings.imageofnow }}"> <div class="description">{{ settings.description }}</div>
<br> <br>
<div class="quoteofnow">{{ settings.quoteofnow }}</div> {% endif %}
<br> {% if settings.imageofnow %}
<iframe class ="videoofnow" width="560" height="315" src="{{ settings.videoofnow }}"></iframe> <img class="imageofnow" src="{{ settings.imageofnow }}">
<br> <br>
</div> {% endif %}
{% if settings.quoteofnow %}
<div class="quoteofnow">{{ settings.quoteofnow }}</div>
<br>
{% endif %}
{% if settings.videoofnow %}
<iframe class="videoofnow" width="560" height="315" src="{{ settings.videoofnow }}"></iframe>
<br>
{% endif %}
</div>
{% endif %}
<div class="ban"> <div class="ban">
{% if news|length == 0 %} {% if news|length == 0 %}
<p style="text-align:center" class="unimportant">(No news to show.)</p> <p style="text-align:center" class="unimportant">(No news to show.)</p>

View File

@ -74,12 +74,19 @@
); );
$theme['config'][] = Array( $theme['config'][] = Array(
'title' => 'Excluded boards', 'title' => 'Excluded boards (recent posts)',
'name' => 'exclude', 'name' => 'exclude',
'type' => 'text', 'type' => 'text',
'comment' => '(space seperated)' 'comment' => '(space seperated)'
); );
$theme['config'][] = Array(
'title' => 'Excluded boards (boardlist)',
'name' => 'excludeboardlist',
'type' => 'text',
'comment' => '(space seperated)'
);
$theme['config'][] = Array( $theme['config'][] = Array(
'title' => '# of recent images', 'title' => '# of recent images',
'name' => 'limit_images', 'name' => 'limit_images',

View File

@ -159,6 +159,13 @@
$query = query("SELECT * FROM ``news`` ORDER BY `time` DESC" . ($settings['no_recent'] ? ' LIMIT ' . $settings['no_recent'] : '')) or error(db_error()); $query = query("SELECT * FROM ``news`` ORDER BY `time` DESC" . ($settings['no_recent'] ? ' LIMIT ' . $settings['no_recent'] : '')) or error(db_error());
$news = $query->fetchAll(PDO::FETCH_ASSOC); $news = $query->fetchAll(PDO::FETCH_ASSOC);
// Excluded boards for the boardlist
$excluded_boards = isset($settings['excludeboardlist']) ? explode(' ', $settings['excludeboardlist']) : [];
$boardlist = array_filter($boards, function($board) use ($excluded_boards) {
return !in_array($board['uri'], $excluded_boards);
});
return Element('themes/index/index.html', Array( return Element('themes/index/index.html', Array(
'settings' => $settings, 'settings' => $settings,
'config' => $config, 'config' => $config,
@ -167,7 +174,7 @@
'recent_posts' => $recent_posts, 'recent_posts' => $recent_posts,
'stats' => $stats, 'stats' => $stats,
'news' => $news, 'news' => $news,
'boards' => listBoards() 'boards' => $boardlist
)); ));
} }
}; };

View File

@ -4,11 +4,11 @@
<head> <head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title>{{ settings.title }}</title> <title>{{ settings.title }}</title>
<link rel="stylesheet" media="screen" href="{{ config.url_stylesheet }}"/> <link rel="stylesheet" media="screen" href="{{ config.url_stylesheet }}?v={{ config.resource_version }}"/>
<link rel="stylesheet" media="screen" href="{{ config.root }}{{ settings.css }}"/> <link rel="stylesheet" media="screen" href="{{ config.root }}{{ settings.css }}?v={{ config.resource_version }}"/>
{% if config.url_favicon %}<link rel="shortcut icon" href="{{ config.url_favicon }}" />{% endif %} {% if config.url_favicon %}<link rel="shortcut icon" href="{{ config.url_favicon }}" />{% endif %}
{% if config.default_stylesheet.1 != '' %}<link rel="stylesheet" type="text/css" id="stylesheet" href="{{ config.uri_stylesheets }}{{ config.default_stylesheet.1 }}">{% endif %} {% if config.default_stylesheet.1 != '' %}<link rel="stylesheet" type="text/css" id="stylesheet" href="{{ config.uri_stylesheets }}{{ config.default_stylesheet.1 }}?v={{ config.resource_version }}">{% endif %}
{% if config.font_awesome %}<link rel="stylesheet" href="{{ config.root }}{{ config.font_awesome_css }}">{% endif %} {% if config.font_awesome %}<link rel="stylesheet" href="{{ config.root }}{{ config.font_awesome_css }}?v={{ config.resource_version }}">{% endif %}
{% include 'header.html' %} {% include 'header.html' %}
</head> </head>
<body> <body>

View File

@ -16,7 +16,7 @@
<meta name="description" content="{{ board.url }} - {{ board.title|e }} - {{ meta_subject }}" /> <meta name="description" content="{{ board.url }} - {{ board.title|e }} - {{ meta_subject }}" />
<meta name="twitter:card" value="summary"> <meta name="twitter:card" value="summary">
<meta name="twitter:title" content="{{ meta_subject }}" /> <meta name="twitter:title" content="{{ meta_subject }}" />
<meta name="twitter:description" content="{{ thread.body_nomarkup|e }}" /> <meta name="twitter:description" content="{{ thread.body_nomarkup|remove_modifiers|e }}" />
{% if thread.files.0.thumb %}<meta name="twitter:image" content="{{ config.domain }}/{{ board.uri }}/{{ config.dir.thumb }}{{ thread.files.0.thumb }}" />{% endif %} {% if thread.files.0.thumb %}<meta name="twitter:image" content="{{ config.domain }}/{{ board.uri }}/{{ config.dir.thumb }}{{ thread.files.0.thumb }}" />{% endif %}
<meta property="og:title" content="{{ meta_subject }}" /> <meta property="og:title" content="{{ meta_subject }}" />
<meta property="og:type" content="article" /> <meta property="og:type" content="article" />

17
tools/hash-passwords.php Normal file
View File

@ -0,0 +1,17 @@
<?php
require_once dirname(__FILE__) . '/inc/cli.php';
$boards = listBoards();
foreach ($boards as &$_board) {
query(sprintf('ALTER TABLE ``posts_%s`` MODIFY `password` varchar(64) DEFAULT NULL;', $_board['uri'])) or error(db_error());
$query = prepare(sprintf("SELECT DISTINCT `password` FROM ``posts_%s``", $_board['uri']));
$query->execute() or error(db_error($query));
while($entry = $query->fetch(PDO::FETCH_ASSOC)) {
$update_query = prepare(sprintf("UPDATE ``posts_%s`` SET `password` = :password WHERE `password` = :password_org", $_board['uri']));
$update_query->bindValue(':password', hashPassword($entry['password']));
$update_query->bindValue(':password_org', $entry['password']);
$update_query->execute() or error(db_error());
}
}

40
tools/maintenance.php Normal file
View File

@ -0,0 +1,40 @@
<?php
/**
* Performs maintenance tasks. Invoke this periodically if the auto_maintenance configuration option is turned off.
*/
require dirname(__FILE__) . '/inc/cli.php';
echo "Clearing expired bans...\n";
$start = microtime(true);
$deleted_count = Bans::purge($config['require_ban_view'], $config['purge_bans']);
$delta = microtime(true) - $start;
echo "Deleted $deleted_count expired bans in $delta seconds!\n";
$time_tot = $delta;
$deleted_tot = $deleted_count;
echo "Clearing old antispam...\n";
$start = microtime(true);
$deleted_count = purge_old_antispam();
$delta = microtime(true) - $start;
echo "Deleted $deleted_count expired antispam in $delta seconds!\n";
$time_tot = $delta;
$deleted_tot = $deleted_count;
if ($config['cache']['enabled'] === 'fs') {
$fs_cache = new Vichan\Data\Driver\FsCacheDriver(
$config['cache']['prefix'],
"tmp/cache/{$config['cache']['prefix']}",
'.lock',
false
);
$start = microtime(true);
$fs_cache->collect();
$delta = microtime(true) - $start;
echo "Deleted $deleted_count expired filesystem cache items in $delta seconds!\n";
$time_tot = $delta;
$deleted_tot = $deleted_count;
}
$time_tot = number_format((float)$time_tot, 4, '.', '');
modLog("Deleted $deleted_tot expired entries in {$time_tot}s with maintenance tool");