forked from GithubBackups/vichan
Compare commits
303 Commits
RealAngele
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
83b644f0f5 | ||
|
953635f807 | ||
|
5968118231 | ||
|
03da3b8db0 | ||
|
f27e5a6989 | ||
|
0866adc89f | ||
|
5259f88262 | ||
|
87420966b2 | ||
|
8350645a70 | ||
|
16a6112b5b | ||
|
9468c9d372 | ||
|
8aa965b77f | ||
|
b08dbcff14 | ||
|
f90291faf0 | ||
|
e67f3f94bf | ||
|
4244b0a86c | ||
|
ac3f69a7eb | ||
|
7bc1f67b4e | ||
|
54f32bc75d | ||
|
5f1598367c | ||
|
3987b2a25d | ||
|
6ec0c17ac9 | ||
|
2c2d476cea | ||
|
717795ddff | ||
|
d1e57290e0 | ||
|
0c92315837 | ||
|
c026e85b3c | ||
|
8d76087710 | ||
|
50d57a4464 | ||
|
96099945b9 | ||
|
8c6cf17a97 | ||
|
b3f0658594 | ||
|
33efeda647 | ||
|
92096b43b8 | ||
|
f3cb2552ce | ||
|
440e3126c9 | ||
|
47cbbbc972 | ||
|
74959aaed9 | ||
|
2d9e5009f8 | ||
|
372784ecd2 | ||
|
0f8a5fa926 | ||
|
0a870ebdb3 | ||
|
3b9b23035e | ||
|
40571f2001 | ||
|
c4cd4d3c12 | ||
|
00fc21322c | ||
|
6606c182b5 | ||
|
58c2f095dc | ||
|
8cd4cae56e | ||
|
762a0edefd | ||
|
c94ab113de | ||
|
32f0cb3a5f | ||
|
2466a3d859 | ||
|
f3f7c0c75c | ||
|
29ee5aeb1d | ||
|
6a22a51a72 | ||
|
a9059fab66 | ||
|
9d590eed2a | ||
|
e7aa695775 | ||
|
f419ae046c | ||
|
0ed3513e66 | ||
|
a376a5a2e3 | ||
|
b2ca26dba5 | ||
|
6f5b0ae6d5 | ||
|
5f2f653993 | ||
|
0ff0e707d6 | ||
|
26ffd1aa72 | ||
|
2e91c1ed3d | ||
|
efd4810e83 | ||
|
9dacdf59b1 | ||
|
b889b10626 | ||
|
b5a9dc4d1a | ||
|
d6677bb90c | ||
|
c88acfc4f0 | ||
|
58f7302936 | ||
|
76f6c721e9 | ||
|
c1307feeb5 | ||
|
e65bfa87c4 | ||
|
b67ff982e2 | ||
|
fba88643ec | ||
|
ad1d56d092 | ||
|
ceccbfc5b7 | ||
|
b8c53fbbcd | ||
|
27e4bd833a | ||
|
fe7a667441 | ||
|
115f28807a | ||
|
003e8f6d3b | ||
|
243e4894fa | ||
|
589435b667 | ||
|
f138b4b887 | ||
|
ace2f2e83b | ||
|
3d406aeab2 | ||
|
66d2f90171 | ||
|
5ea42fa0e2 | ||
|
b57d9bfbb3 | ||
|
82ea1815fd | ||
|
7c305f58bf | ||
|
59d0dd9083 | ||
|
f60b4d190f | ||
|
88a81a6d74 | ||
|
3fe44653f2 | ||
|
0e8909ac4e | ||
|
767b8fd8c3 | ||
|
f9127dd478 | ||
|
6c6ec65b02 | ||
|
23f3d15a52 | ||
|
d88d6c814a | ||
|
65a668d3a8 | ||
|
b501852ea4 | ||
|
927a837216 | ||
|
79af4b34dd | ||
|
36d48951c1 | ||
|
5dab17e5f4 | ||
|
c97c61aeca | ||
|
59551a2042 | ||
|
1682272f75 | ||
|
3eed312b6b | ||
|
cae85a6a0c | ||
|
25b2b6bc6e | ||
|
7377885de9 | ||
|
f421e25e63 | ||
|
3e9ad58e97 | ||
|
f4ff39c876 | ||
|
e6133ef00f | ||
|
13ca053e06 | ||
|
44b31eff0b | ||
|
785643a3bd | ||
|
e70c087f5f | ||
|
428a686f47 | ||
|
c95877bdcb | ||
|
380ae8c675 | ||
|
2f37b3ce51 | ||
|
65008dec98 | ||
|
0e8aeca4af | ||
|
bdd7090e75 | ||
|
fd309443ea | ||
|
4332b70363 | ||
|
cb6d6f13dd | ||
|
6b60f841d4 | ||
|
3c5484a7c2 | ||
|
fcf5c3d73a | ||
|
9fbc816205 | ||
|
bd5c2c61b9 | ||
|
c1788d0792 | ||
|
ccfcd03c95 | ||
|
e1a4ae5336 | ||
|
1effe1648b | ||
|
36737b77a8 | ||
|
a457b905bf | ||
|
3291dc27f9 | ||
|
8c27b5261c | ||
|
16bb704154 | ||
|
1db5c788dd | ||
|
2b3eae89f1 | ||
|
ce844b9270 | ||
|
43b926c41b | ||
|
eeb55133eb | ||
|
cd5c57f717 | ||
|
1672646213 | ||
|
6ea8fd5bf3 | ||
|
bf32a24b96 | ||
|
187f16693c | ||
|
13b587cf62 | ||
|
1191dfb193 | ||
|
5ee48c5865 | ||
|
60135bbb89 | ||
|
39876f3cc7 | ||
|
84a3bedd18 | ||
|
90dead0394 | ||
|
4f68166870 | ||
|
d408ed0413 | ||
|
453ae795f5 | ||
|
b2df2ab2a5 | ||
|
81aebef2f4 | ||
|
b64bac5eb8 | ||
|
524ae94624 | ||
|
19082aec56 | ||
|
e640217a8f | ||
|
165ea5308a | ||
|
f7073d5d7e | ||
|
cb5fb68c5e | ||
|
fb92e5fb68 | ||
|
a275d04efa | ||
|
933594194c | ||
|
e825e7aac5 | ||
|
d4b4cf5825 | ||
|
3e72171889 | ||
|
d8391eb34a | ||
|
e16dc142b7 | ||
|
420ec4a852 | ||
|
4d8a4db338 | ||
|
d1b06acbe9 | ||
|
80be41f47a | ||
|
f3e81c80d9 | ||
|
b3ae38da57 | ||
|
51e0616eb8 | ||
|
7e4acbb6d2 | ||
|
ffe855222e | ||
|
1e0a95ce83 | ||
|
c4e3541b15 | ||
|
ede7591702 | ||
|
c057c6df29 | ||
|
4d97e69620 | ||
|
e5bbdb9d28 | ||
|
cc5e96eb9d | ||
|
82b8eb1e74 | ||
|
2298d4433f | ||
|
accca93084 | ||
|
cbaf19cb7a | ||
|
75714505a0 | ||
|
36476f6341 | ||
|
980b2ef7bf | ||
|
e4707ee2a8 | ||
|
00cc1f434d | ||
|
ee20bf574a | ||
|
609da43548 | ||
|
0c074016e7 | ||
|
562ad74a12 | ||
|
73fce5e571 | ||
|
e5d423e595 | ||
|
230cc252c5 | ||
|
0fbf2f6f77 | ||
|
db20a350a1 | ||
|
7f45f31aa8 | ||
|
4445254b00 | ||
|
85b03c0fb0 | ||
|
d9a333a69f | ||
|
e92e9469a8 | ||
|
41f9aed606 | ||
|
5306f1d1f9 | ||
|
2749567c3f | ||
|
44e9a5aa57 | ||
|
5550bc4212 | ||
|
d9d05ddbf5 | ||
|
d9feb5cfa7 | ||
|
b7f46a239d | ||
|
2728966c1c | ||
|
ed46907a6c | ||
|
31444d654a | ||
|
6bea01b00b | ||
|
023e59d88f | ||
|
b822a76b23 | ||
|
902c558237 | ||
|
a018772267 | ||
|
fa341b29d0 | ||
|
6daae3ec92 | ||
|
da0f26485a | ||
|
8b773b124e | ||
|
c327a0439e | ||
|
4b5e40f575 | ||
|
eb67076e60 | ||
|
990f27e80b | ||
|
8d3bfedc72 | ||
|
d78f865645 | ||
|
283973c141 | ||
|
098edb9cd7 | ||
|
4bc69be4bc | ||
|
a20f618d80 | ||
|
d7468bb93b | ||
|
541f31f4d1 | ||
|
c223b1c55d | ||
|
409f571955 | ||
|
d23d1526e8 | ||
|
9a80ae2434 | ||
|
dba38b10d4 | ||
|
89a31794d9 | ||
|
1a780ce9cb | ||
|
fe8fa0da8a | ||
|
e12cbf6d80 | ||
|
0df33b4956 | ||
|
ef1939500c | ||
|
555d14b7ae | ||
|
281f391205 | ||
|
b21865853b | ||
|
5b0a7fb975 | ||
|
e9f1d59209 | ||
|
fee67b6719 | ||
|
fff9b88c6d | ||
|
8d37f1dd2c | ||
|
991ed657fb | ||
|
ad532d1d2d | ||
|
d2f1b7e0e0 | ||
|
2d6b599b26 | ||
|
bd7eb130ea | ||
|
f852172e9b | ||
|
809ab99c9b | ||
|
5c99c8395e | ||
|
878a67389a | ||
|
d990c344d4 | ||
|
88a48befd4 | ||
|
7e4dd5567b | ||
|
8963ebfce9 | ||
|
9236e10f37 | ||
|
9aaed32c57 | ||
|
055d31d2db | ||
|
de39780194 | ||
|
37658f1817 | ||
|
00be5e6ced | ||
|
605f198d8c | ||
|
021e20f373 | ||
|
b0e6580845 | ||
|
0dd064b2ea | ||
|
c91c58ed07 |
69
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
69
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
name: Bug Report
|
||||
description: File a bug report for Vichan
|
||||
title: "[BUG] "
|
||||
labels: ["bug"]
|
||||
assignees: []
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Thank you for reporting a bug! Please provide as much detail as possible.**
|
||||
|
||||
Before submitting, check the [Vichan Wiki](https://vichan.info) to see if there's already a solution to your problem.
|
||||
|
||||
- type: textarea
|
||||
id: bug_description
|
||||
attributes:
|
||||
label: "Describe the bug"
|
||||
description: "A clear and concise description of what the bug is."
|
||||
placeholder: "Posting doesn't go through and displays a collation error. The exact error message given is the text below and I've attached a screenshot..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps_to_reproduce
|
||||
attributes:
|
||||
label: "Steps to Reproduce"
|
||||
description: "Provide step-by-step instructions to reproduce the issue. If you're unsure on how, that is alright, just try and explain as well as you can."
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected_behavior
|
||||
attributes:
|
||||
label: "Expected Behavior"
|
||||
description: "What did you expect to happen?"
|
||||
placeholder: "Expected behavior here..."
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: server_specs
|
||||
attributes:
|
||||
label: "Server Specifications"
|
||||
description: "Provide details about your server environment. If you're unsure about any of this, you might be using shared hosting (Hostinger, HostGator, Serv00, etc). If so, put the name of your hosting provider here."
|
||||
placeholder: |
|
||||
- OS: (Ubuntu, CentOS, Windows Server 2025, etc.)
|
||||
- PHP Version: (e.g., 7.4, 8.0, 8.4)
|
||||
- Web Server: (Apache, NGINX, etc.)
|
||||
- Database: (MySQL, MariaDB, etc.)
|
||||
- Vichan Version: (5.2.0, 5.3.0 (dev branch), etc)
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional_context
|
||||
attributes:
|
||||
label: "Additional Context"
|
||||
description: "Any other details we should know?"
|
||||
placeholder: "Add any additional context here..."
|
||||
render: markdown
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
@ -9,7 +9,7 @@ services:
|
||||
depends_on:
|
||||
- db
|
||||
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/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./docker/nginx/proxy.conf:/etc/nginx/conf.d/proxy.conf
|
||||
@ -21,7 +21,7 @@ services:
|
||||
context: .
|
||||
dockerfile: ./docker/php/Dockerfile
|
||||
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/jit.ini:/usr/local/etc/php/conf.d/jit.ini
|
||||
|
||||
@ -37,4 +37,4 @@ services:
|
||||
MYSQL_DATABASE: vichan
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
volumes:
|
||||
- ./local-instances/1/mysql:/var/lib/mysql
|
||||
- ./local-instances/${INSTANCE:-0}/mysql:/var/lib/mysql
|
@ -18,7 +18,8 @@
|
||||
"gettext/gettext": "^5.5",
|
||||
"mrclay/minify": "^2.1.6",
|
||||
"geoip/geoip": "^1.17",
|
||||
"dapphp/securimage": "^4.0"
|
||||
"dapphp/securimage": "^4.0",
|
||||
"erusev/parsedown": "^1.7.4"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": ["inc/"],
|
||||
@ -33,12 +34,13 @@
|
||||
"inc/lock.php",
|
||||
"inc/queue.php",
|
||||
"inc/functions.php",
|
||||
"inc/functions/dice.php",
|
||||
"inc/functions/format.php",
|
||||
"inc/functions/net.php",
|
||||
"inc/functions/num.php",
|
||||
"inc/functions/format.php",
|
||||
"inc/driver/http-driver.php",
|
||||
"inc/driver/log-driver.php",
|
||||
"inc/service/captcha-queries.php"
|
||||
"inc/functions/theme.php",
|
||||
"inc/service/captcha-queries.php",
|
||||
"inc/context.php"
|
||||
]
|
||||
},
|
||||
"license": "Tinyboard + vichan",
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
RUN apk add --no-cache \
|
||||
@ -64,7 +64,8 @@ RUN apk add --no-cache \
|
||||
imagemagick-dev \
|
||||
pcre-dev \
|
||||
$PHPIZE_DEPS \
|
||||
&& rm -rf /var/cache/*
|
||||
&& rm -rf /var/cache/* \
|
||||
&& rm -rf /tmp/pear
|
||||
RUN rmdir /var/www/html \
|
||||
&& install -d -m 744 -o www-data -g www-data /var/www \
|
||||
&& install -d -m 700 -o www-data -g www-data /var/tmp/vichan \
|
||||
|
28
inc/Data/Driver/ApcuCacheDriver.php
Normal file
28
inc/Data/Driver/ApcuCacheDriver.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
class ApcuCacheDriver implements CacheDriver {
|
||||
public function get(string $key): mixed {
|
||||
$success = false;
|
||||
$ret = \apcu_fetch($key, $success);
|
||||
if ($success === false) {
|
||||
return null;
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||
\apcu_store($key, $value, (int)$expires);
|
||||
}
|
||||
|
||||
public function delete(string $key): void {
|
||||
\apcu_delete($key);
|
||||
}
|
||||
|
||||
public function flush(): void {
|
||||
\apcu_clear_cache();
|
||||
}
|
||||
}
|
28
inc/Data/Driver/ArrayCacheDriver.php
Normal file
28
inc/Data/Driver/ArrayCacheDriver.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
/**
|
||||
* A simple process-wide PHP array.
|
||||
*/
|
||||
class ArrayCacheDriver implements CacheDriver {
|
||||
private static $inner = [];
|
||||
|
||||
public function get(string $key) {
|
||||
return isset(self::$inner[$key]) ? self::$inner[$key] : null;
|
||||
}
|
||||
|
||||
public function set(string $key, $value, $expires = false): void {
|
||||
self::$inner[$key] = $value;
|
||||
}
|
||||
|
||||
public function delete(string $key): void {
|
||||
unset(self::$inner[$key]);
|
||||
}
|
||||
|
||||
public function flush(): void {
|
||||
self::$inner = [];
|
||||
}
|
||||
}
|
38
inc/Data/Driver/CacheDriver.php
Normal file
38
inc/Data/Driver/CacheDriver.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
interface CacheDriver {
|
||||
/**
|
||||
* Get the value of associated with the key.
|
||||
*
|
||||
* @param string $key The key of the value.
|
||||
* @return mixed|null The value associated with the key, or null if there is none.
|
||||
*/
|
||||
public function get(string $key);
|
||||
|
||||
/**
|
||||
* Set a key-value pair.
|
||||
*
|
||||
* @param string $key The key.
|
||||
* @param mixed $value The value.
|
||||
* @param int|false $expires After how many seconds the pair will expire. Use false or ignore this parameter to keep
|
||||
* the value until it gets evicted to make space for more items. Some drivers will always
|
||||
* ignore this parameter and store the pair until it's removed.
|
||||
*/
|
||||
public function set(string $key, $value, $expires = false);
|
||||
|
||||
/**
|
||||
* Delete a key-value pair.
|
||||
*
|
||||
* @param string $key The key.
|
||||
*/
|
||||
public function delete(string $key);
|
||||
|
||||
/**
|
||||
* Delete all the key-value pairs.
|
||||
*/
|
||||
public function flush();
|
||||
}
|
28
inc/Data/Driver/ErrorLogLogDriver.php
Normal file
28
inc/Data/Driver/ErrorLogLogDriver.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
/**
|
||||
* Log via the php function error_log.
|
||||
*/
|
||||
class ErrorLogLogDriver implements LogDriver {
|
||||
use LogTrait;
|
||||
|
||||
private string $name;
|
||||
private int $level;
|
||||
|
||||
public function __construct(string $name, int $level) {
|
||||
$this->name = $name;
|
||||
$this->level = $level;
|
||||
}
|
||||
|
||||
public function log(int $level, string $message): void {
|
||||
if ($level <= $this->level) {
|
||||
$lv = $this->levelToString($level);
|
||||
$line = "{$this->name} $lv: $message";
|
||||
\error_log($line, 0, null, null);
|
||||
}
|
||||
}
|
||||
}
|
61
inc/Data/Driver/FileLogDriver.php
Normal file
61
inc/Data/Driver/FileLogDriver.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
/**
|
||||
* Log to a file.
|
||||
*/
|
||||
class FileLogDriver implements LogDriver {
|
||||
use LogTrait;
|
||||
|
||||
private string $name;
|
||||
private int $level;
|
||||
private mixed $fd;
|
||||
|
||||
public function __construct(string $name, int $level, string $file_path) {
|
||||
/*
|
||||
* error_log is slow as hell in it's 3rd mode, so use fopen + file locking instead.
|
||||
* https://grobmeier.solutions/performance-ofnonblocking-write-to-files-via-php-21082009.html
|
||||
*
|
||||
* Whatever file appending is atomic is contentious:
|
||||
* - There are no POSIX guarantees: https://stackoverflow.com/a/7237901
|
||||
* - But linus suggested they are on linux, on some filesystems: https://web.archive.org/web/20151201111541/http://article.gmane.org/gmane.linux.kernel/43445
|
||||
* - But it doesn't seem to be always the case: https://www.notthewizard.com/2014/06/17/are-files-appends-really-atomic/
|
||||
*
|
||||
* So we just use file locking to be sure.
|
||||
*/
|
||||
|
||||
$this->fd = \fopen($file_path, 'a');
|
||||
if ($this->fd === false) {
|
||||
throw new \RuntimeException("Unable to open log file at $file_path");
|
||||
}
|
||||
|
||||
$this->name = $name;
|
||||
$this->level = $level;
|
||||
|
||||
// In some cases PHP does not run the destructor.
|
||||
\register_shutdown_function([$this, 'close']);
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
$this->close();
|
||||
}
|
||||
|
||||
public function log(int $level, string $message): void {
|
||||
if ($level <= $this->level) {
|
||||
$lv = $this->levelToString($level);
|
||||
$line = "{$this->name} $lv: $message\n";
|
||||
\flock($this->fd, LOCK_EX);
|
||||
\fwrite($this->fd, $line);
|
||||
\fflush($this->fd);
|
||||
\flock($this->fd, LOCK_UN);
|
||||
}
|
||||
}
|
||||
|
||||
public function close() {
|
||||
\flock($this->fd, LOCK_UN);
|
||||
\fclose($this->fd);
|
||||
}
|
||||
}
|
155
inc/Data/Driver/FsCachedriver.php
Normal file
155
inc/Data/Driver/FsCachedriver.php
Normal file
@ -0,0 +1,155 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
class FsCacheDriver implements CacheDriver {
|
||||
private string $prefix;
|
||||
private string $base_path;
|
||||
private mixed $lock_fd;
|
||||
private int|false $collect_chance_den;
|
||||
|
||||
|
||||
private function prepareKey(string $key): string {
|
||||
$key = \str_replace('/', '::', $key);
|
||||
$key = \str_replace("\0", '', $key);
|
||||
return $this->prefix . $key;
|
||||
}
|
||||
|
||||
private function sharedLockCache(): void {
|
||||
\flock($this->lock_fd, LOCK_SH);
|
||||
}
|
||||
|
||||
private function exclusiveLockCache(): void {
|
||||
\flock($this->lock_fd, LOCK_EX);
|
||||
}
|
||||
|
||||
private function unlockCache(): void {
|
||||
\flock($this->lock_fd, LOCK_UN);
|
||||
}
|
||||
|
||||
private function collectImpl(): int {
|
||||
/*
|
||||
* A read lock is ok, since it's alright if we delete expired items from under the feet of other processes, and
|
||||
* no other process add new cache items or refresh existing ones.
|
||||
*/
|
||||
$files = \glob($this->base_path . $this->prefix . '*', \GLOB_NOSORT);
|
||||
$count = 0;
|
||||
foreach ($files as $file) {
|
||||
$data = \file_get_contents($file);
|
||||
$wrapped = \json_decode($data, true, 512, \JSON_THROW_ON_ERROR);
|
||||
if ($wrapped['expires'] !== false && $wrapped['expires'] <= \time()) {
|
||||
if (@\unlink($file)) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function maybeCollect(): void {
|
||||
if ($this->collect_chance_den !== false && \mt_rand(0, $this->collect_chance_den - 1) === 0) {
|
||||
$this->collect_chance_den = false; // Collect only once per instance (aka process).
|
||||
$this->collectImpl();
|
||||
}
|
||||
}
|
||||
|
||||
public function __construct(string $prefix, string $base_path, string $lock_file, int|false $collect_chance_den) {
|
||||
if ($base_path[\strlen($base_path) - 1] !== '/') {
|
||||
$base_path = "$base_path/";
|
||||
}
|
||||
|
||||
if (!\is_dir($base_path)) {
|
||||
throw new \RuntimeException("$base_path is not a directory!");
|
||||
}
|
||||
|
||||
if (!\is_writable($base_path)) {
|
||||
throw new \RuntimeException("$base_path is not writable!");
|
||||
}
|
||||
|
||||
$this->lock_fd = \fopen($base_path . $lock_file, 'w');
|
||||
if ($this->lock_fd === false) {
|
||||
throw new \RuntimeException('Unable to open the lock file!');
|
||||
}
|
||||
|
||||
$this->prefix = $prefix;
|
||||
$this->base_path = $base_path;
|
||||
$this->collect_chance_den = $collect_chance_den;
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
$this->close();
|
||||
}
|
||||
|
||||
public function get(string $key): mixed {
|
||||
$key = $this->prepareKey($key);
|
||||
|
||||
$this->sharedLockCache();
|
||||
|
||||
// Collect expired items first so if the target key is expired we shortcut to failure in the next lines.
|
||||
$this->maybeCollect();
|
||||
|
||||
$fd = \fopen($this->base_path . $key, 'r');
|
||||
if ($fd === false) {
|
||||
$this->unlockCache();
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = \stream_get_contents($fd);
|
||||
\fclose($fd);
|
||||
$this->unlockCache();
|
||||
$wrapped = \json_decode($data, true, 512, \JSON_THROW_ON_ERROR);
|
||||
|
||||
if ($wrapped['expires'] !== false && $wrapped['expires'] <= \time()) {
|
||||
// Already expired, leave it there since we already released the lock and pretend it doesn't exist.
|
||||
return null;
|
||||
} else {
|
||||
return $wrapped['inner'];
|
||||
}
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||
$key = $this->prepareKey($key);
|
||||
|
||||
$wrapped = [
|
||||
'expires' => $expires ? \time() + $expires : false,
|
||||
'inner' => $value
|
||||
];
|
||||
|
||||
$data = \json_encode($wrapped);
|
||||
$this->exclusiveLockCache();
|
||||
$this->maybeCollect();
|
||||
\file_put_contents($this->base_path . $key, $data);
|
||||
$this->unlockCache();
|
||||
}
|
||||
|
||||
public function delete(string $key): void {
|
||||
$key = $this->prepareKey($key);
|
||||
|
||||
$this->exclusiveLockCache();
|
||||
@\unlink($this->base_path . $key);
|
||||
$this->maybeCollect();
|
||||
$this->unlockCache();
|
||||
}
|
||||
|
||||
public function collect(): int {
|
||||
$this->sharedLockCache();
|
||||
$count = $this->collectImpl();
|
||||
$this->unlockCache();
|
||||
return $count;
|
||||
}
|
||||
|
||||
public function flush(): void {
|
||||
$this->exclusiveLockCache();
|
||||
$files = \glob($this->base_path . $this->prefix . '*', \GLOB_NOSORT);
|
||||
foreach ($files as $file) {
|
||||
@\unlink($file);
|
||||
}
|
||||
$this->unlockCache();
|
||||
}
|
||||
|
||||
public function close(): void {
|
||||
\fclose($this->lock_fd);
|
||||
}
|
||||
}
|
131
inc/Data/Driver/HttpDriver.php
Normal file
131
inc/Data/Driver/HttpDriver.php
Normal file
@ -0,0 +1,131 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
/**
|
||||
* Honestly this is just a wrapper for cURL. Still useful to mock it and have an OOP API on PHP 7.
|
||||
*/
|
||||
class HttpDriver {
|
||||
private $inner;
|
||||
private int $timeout;
|
||||
private int $max_file_size;
|
||||
|
||||
|
||||
private function resetTowards(string $url, int $timeout): void {
|
||||
\curl_reset($this->inner);
|
||||
\curl_setopt_array($this->inner, [
|
||||
\CURLOPT_URL => $url,
|
||||
\CURLOPT_TIMEOUT => $timeout,
|
||||
\CURLOPT_USERAGENT => 'Tinyboard',
|
||||
\CURLOPT_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS,
|
||||
]);
|
||||
}
|
||||
|
||||
public function __construct(int $timeout, int $max_file_size) {
|
||||
$this->inner = \curl_init();
|
||||
$this->timeout = $timeout;
|
||||
$this->max_file_size = $max_file_size;
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
\curl_close($this->inner);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a GET request.
|
||||
*
|
||||
* @param string $endpoint Uri endpoint.
|
||||
* @param ?array $data Optional GET parameters.
|
||||
* @param int $timeout Optional request timeout in seconds. Use the default timeout if 0.
|
||||
* @return string Returns the body of the response.
|
||||
* @throws RuntimeException Throws on IO error.
|
||||
*/
|
||||
public function requestGet(string $endpoint, ?array $data, int $timeout = 0): string {
|
||||
if (!empty($data)) {
|
||||
$endpoint .= '?' . \http_build_query($data);
|
||||
}
|
||||
if ($timeout == 0) {
|
||||
$timeout = $this->timeout;
|
||||
}
|
||||
|
||||
$this->resetTowards($endpoint, $timeout);
|
||||
\curl_setopt($this->inner, \CURLOPT_RETURNTRANSFER, true);
|
||||
$ret = \curl_exec($this->inner);
|
||||
|
||||
if ($ret === false) {
|
||||
throw new \RuntimeException(\curl_error($this->inner));
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a POST request.
|
||||
*
|
||||
* @param string $endpoint Uri endpoint.
|
||||
* @param ?array $data Optional POST parameters.
|
||||
* @param int $timeout Optional request timeout in seconds. Use the default timeout if 0.
|
||||
* @return string Returns the body of the response.
|
||||
* @throws RuntimeException Throws on IO error.
|
||||
*/
|
||||
public function requestPost(string $endpoint, ?array $data, int $timeout = 0): string {
|
||||
if ($timeout == 0) {
|
||||
$timeout = $this->timeout;
|
||||
}
|
||||
|
||||
$this->resetTowards($endpoint, $timeout);
|
||||
\curl_setopt($this->inner, \CURLOPT_POST, true);
|
||||
if (!empty($data)) {
|
||||
\curl_setopt($this->inner, \CURLOPT_POSTFIELDS, \http_build_query($data));
|
||||
}
|
||||
\curl_setopt($this->inner, \CURLOPT_RETURNTRANSFER, true);
|
||||
$ret = \curl_exec($this->inner);
|
||||
|
||||
if ($ret === false) {
|
||||
throw new \RuntimeException(\curl_error($this->inner));
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the url's target with curl.
|
||||
*
|
||||
* @param string $url Url to the file to download.
|
||||
* @param ?array $data Optional GET parameters.
|
||||
* @param resource $fd File descriptor to save the content to.
|
||||
* @param int $timeout Optional request timeout in seconds. Use the default timeout if 0.
|
||||
* @return bool Returns true on success, false if the file was too large.
|
||||
* @throws RuntimeException Throws on IO error.
|
||||
*/
|
||||
public function requestGetInto(string $endpoint, ?array $data, $fd, int $timeout = 0): bool {
|
||||
if (!empty($data)) {
|
||||
$endpoint .= '?' . \http_build_query($data);
|
||||
}
|
||||
if ($timeout == 0) {
|
||||
$timeout = $this->timeout;
|
||||
}
|
||||
|
||||
$this->resetTowards($endpoint, $timeout);
|
||||
// Adapted from: https://stackoverflow.com/a/17642638
|
||||
$opt = (\PHP_MAJOR_VERSION >= 8 && \PHP_MINOR_VERSION >= 2) ? \CURLOPT_XFERINFOFUNCTION : \CURLOPT_PROGRESSFUNCTION;
|
||||
\curl_setopt_array($this->inner, [
|
||||
\CURLOPT_NOPROGRESS => false,
|
||||
$opt => fn($res, $next_dl, $dl, $next_up, $up) => (int)($dl <= $this->max_file_size),
|
||||
\CURLOPT_FAILONERROR => true,
|
||||
\CURLOPT_FOLLOWLOCATION => false,
|
||||
\CURLOPT_FILE => $fd,
|
||||
\CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
|
||||
]);
|
||||
$ret = \curl_exec($this->inner);
|
||||
|
||||
if ($ret === false) {
|
||||
if (\curl_errno($this->inner) === CURLE_ABORTED_BY_CALLBACK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new \RuntimeException(\curl_error($this->inner));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
22
inc/Data/Driver/LogDriver.php
Normal file
22
inc/Data/Driver/LogDriver.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
interface LogDriver {
|
||||
public const EMERG = \LOG_EMERG;
|
||||
public const ERROR = \LOG_ERR;
|
||||
public const WARNING = \LOG_WARNING;
|
||||
public const NOTICE = \LOG_NOTICE;
|
||||
public const INFO = \LOG_INFO;
|
||||
public const DEBUG = \LOG_DEBUG;
|
||||
|
||||
/**
|
||||
* Log a message if the level of relevancy is at least the minimum.
|
||||
*
|
||||
* @param int $level Message level. Use Log interface constants.
|
||||
* @param string $message The message to log.
|
||||
*/
|
||||
public function log(int $level, string $message): void;
|
||||
}
|
26
inc/Data/Driver/LogTrait.php
Normal file
26
inc/Data/Driver/LogTrait.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
trait LogTrait {
|
||||
public static function levelToString(int $level): string {
|
||||
switch ($level) {
|
||||
case LogDriver::EMERG:
|
||||
return 'EMERG';
|
||||
case LogDriver::ERROR:
|
||||
return 'ERROR';
|
||||
case LogDriver::WARNING:
|
||||
return 'WARNING';
|
||||
case LogDriver::NOTICE:
|
||||
return 'NOTICE';
|
||||
case LogDriver::INFO:
|
||||
return 'INFO';
|
||||
case LogDriver::DEBUG:
|
||||
return 'DEBUG';
|
||||
default:
|
||||
throw new \InvalidArgumentException('Not a logging level');
|
||||
}
|
||||
}
|
||||
}
|
43
inc/Data/Driver/MemcacheCacheDriver.php
Normal file
43
inc/Data/Driver/MemcacheCacheDriver.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
class MemcachedCacheDriver implements CacheDriver {
|
||||
private \Memcached $inner;
|
||||
|
||||
public function __construct(string $prefix, string $memcached_server) {
|
||||
$this->inner = new \Memcached();
|
||||
if (!$this->inner->setOption(\Memcached::OPT_BINARY_PROTOCOL, true)) {
|
||||
throw new \RuntimeException('Unable to set the memcached protocol!');
|
||||
}
|
||||
if (!$this->inner->setOption(\Memcached::OPT_PREFIX_KEY, $prefix)) {
|
||||
throw new \RuntimeException('Unable to set the memcached prefix!');
|
||||
}
|
||||
if (!$this->inner->addServers($memcached_server)) {
|
||||
throw new \RuntimeException('Unable to add the memcached server!');
|
||||
}
|
||||
}
|
||||
|
||||
public function get(string $key): mixed {
|
||||
$ret = $this->inner->get($key);
|
||||
// If the returned value is false but the retrival was a success, then the value stored was a boolean false.
|
||||
if ($ret === false && $this->inner->getResultCode() !== \Memcached::RES_SUCCESS) {
|
||||
return null;
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||
$this->inner->set($key, $value, (int)$expires);
|
||||
}
|
||||
|
||||
public function delete(string $key): void {
|
||||
$this->inner->delete($key);
|
||||
}
|
||||
|
||||
public function flush(): void {
|
||||
$this->inner->flush();
|
||||
}
|
||||
}
|
26
inc/Data/Driver/NoneCacheDriver.php
Normal file
26
inc/Data/Driver/NoneCacheDriver.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
/**
|
||||
* No-op cache. Useful for testing.
|
||||
*/
|
||||
class NoneCacheDriver implements CacheDriver {
|
||||
public function get(string $key): mixed {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
public function delete(string $key): void {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
public function flush(): void {
|
||||
// No-op.
|
||||
}
|
||||
}
|
48
inc/Data/Driver/RedisCacheDriver.php
Normal file
48
inc/Data/Driver/RedisCacheDriver.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
class RedisCacheDriver implements CacheDriver {
|
||||
private string $prefix;
|
||||
private \Redis $inner;
|
||||
|
||||
public function __construct(string $prefix, string $host, int $port, ?string $password, string $database) {
|
||||
$this->inner = new \Redis();
|
||||
$this->inner->connect($host, $port);
|
||||
if ($password) {
|
||||
$this->inner->auth($password);
|
||||
}
|
||||
if (!$this->inner->select($database)) {
|
||||
throw new \RuntimeException('Unable to connect to Redis!');
|
||||
}
|
||||
|
||||
$$this->prefix = $prefix;
|
||||
}
|
||||
|
||||
public function get(string $key): mixed {
|
||||
$ret = $this->inner->get($this->prefix . $key);
|
||||
if ($ret === false) {
|
||||
return null;
|
||||
}
|
||||
return \json_decode($ret, true);
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||
if ($expires === false) {
|
||||
$this->inner->set($this->prefix . $key, \json_encode($value));
|
||||
} else {
|
||||
$expires = $expires * 1000; // Seconds to milliseconds.
|
||||
$this->inner->setex($this->prefix . $key, $expires, \json_encode($value));
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(string $key): void {
|
||||
$this->inner->del($this->prefix . $key);
|
||||
}
|
||||
|
||||
public function flush(): void {
|
||||
$this->inner->flushDB();
|
||||
}
|
||||
}
|
27
inc/Data/Driver/StderrLogDriver.php
Normal file
27
inc/Data/Driver/StderrLogDriver.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
/**
|
||||
* Log to php's standard error file stream.
|
||||
*/
|
||||
class StderrLogDriver implements LogDriver {
|
||||
use LogTrait;
|
||||
|
||||
private string $name;
|
||||
private int $level;
|
||||
|
||||
public function __construct(string $name, int $level) {
|
||||
$this->name = $name;
|
||||
$this->level = $level;
|
||||
}
|
||||
|
||||
public function log(int $level, string $message): void {
|
||||
if ($level <= $this->level) {
|
||||
$lv = $this->levelToString($level);
|
||||
\fwrite(\STDERR, "{$this->name} $lv: $message\n");
|
||||
}
|
||||
}
|
||||
}
|
35
inc/Data/Driver/SyslogLogDriver.php
Normal file
35
inc/Data/Driver/SyslogLogDriver.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
/**
|
||||
* Log to syslog.
|
||||
*/
|
||||
class SyslogLogDriver implements LogDriver {
|
||||
private int $level;
|
||||
|
||||
public function __construct(string $name, int $level, bool $print_stderr) {
|
||||
$flags = \LOG_ODELAY;
|
||||
if ($print_stderr) {
|
||||
$flags |= \LOG_PERROR;
|
||||
}
|
||||
|
||||
if (!\openlog($name, $flags, \LOG_USER)) {
|
||||
throw new \RuntimeException('Unable to open syslog');
|
||||
}
|
||||
|
||||
$this->level = $level;
|
||||
}
|
||||
|
||||
public function log(int $level, string $message): void {
|
||||
if ($level <= $this->level) {
|
||||
if (isset($_SERVER['REMOTE_ADDR'], $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'])) {
|
||||
// CGI
|
||||
\syslog($level, "$message - client: {$_SERVER['REMOTE_ADDR']}, request: \"{$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']}\"");
|
||||
} else {
|
||||
\syslog($level, $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
188
inc/anti-bot.php
188
inc/anti-bot.php
@ -1,191 +1,5 @@
|
||||
<?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('"', '"', $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);
|
||||
}
|
||||
}
|
||||
|
146
inc/api.php
146
inc/api.php
@ -9,14 +9,49 @@ defined('TINYBOARD') or exit;
|
||||
* Class for generating json API compatible with 4chan API
|
||||
*/
|
||||
class Api {
|
||||
function __construct(){
|
||||
global $config;
|
||||
/**
|
||||
* Translation from local fields to fields in 4chan-style API
|
||||
*/
|
||||
$this->config = $config;
|
||||
private bool $show_filename;
|
||||
private bool $hide_email;
|
||||
private bool $country_flags;
|
||||
private array $postFields;
|
||||
|
||||
$this->postFields = array(
|
||||
private const INTS = [
|
||||
'no' => 1,
|
||||
'resto' => 1,
|
||||
'time' => 1,
|
||||
'tn_w' => 1,
|
||||
'tn_h' => 1,
|
||||
'w' => 1,
|
||||
'h' => 1,
|
||||
'fsize' => 1,
|
||||
'omitted_posts' => 1,
|
||||
'omitted_images' => 1,
|
||||
'replies' => 1,
|
||||
'images' => 1,
|
||||
'sticky' => 1,
|
||||
'locked' => 1,
|
||||
'last_modified' => 1
|
||||
];
|
||||
|
||||
private const THREADS_PAGE_FIELDS = [
|
||||
'id' => 'no',
|
||||
'bump' => 'last_modified'
|
||||
];
|
||||
|
||||
private const FILE_FIELDS = [
|
||||
'thumbheight' => 'tn_h',
|
||||
'thumbwidth' => 'tn_w',
|
||||
'height' => 'h',
|
||||
'width' => 'w',
|
||||
'size' => 'fsize'
|
||||
];
|
||||
|
||||
public function __construct(bool $show_filename, bool $hide_email, bool $country_flags) {
|
||||
// Translation from local fields to fields in 4chan-style API
|
||||
$this->show_filename = $show_filename;
|
||||
$this->hide_email = $hide_email;
|
||||
$this->country_flags = $country_flags;
|
||||
|
||||
$this->postFields = [
|
||||
'id' => 'no',
|
||||
'thread' => 'resto',
|
||||
'subject' => 'sub',
|
||||
@ -35,91 +70,65 @@ class Api {
|
||||
'cycle' => 'cyclical',
|
||||
'bump' => 'last_modified',
|
||||
'embed' => 'embed',
|
||||
);
|
||||
|
||||
$this->threadsPageFields = array(
|
||||
'id' => 'no',
|
||||
'bump' => 'last_modified'
|
||||
);
|
||||
|
||||
$this->fileFields = array(
|
||||
'thumbheight' => 'tn_h',
|
||||
'thumbwidth' => 'tn_w',
|
||||
'height' => 'h',
|
||||
'width' => 'w',
|
||||
'size' => 'fsize',
|
||||
);
|
||||
];
|
||||
|
||||
if (isset($config['api']['extra_fields']) && gettype($config['api']['extra_fields']) == 'array'){
|
||||
$this->postFields = array_merge($this->postFields, $config['api']['extra_fields']);
|
||||
}
|
||||
}
|
||||
|
||||
private static $ints = array(
|
||||
'no' => 1,
|
||||
'resto' => 1,
|
||||
'time' => 1,
|
||||
'tn_w' => 1,
|
||||
'tn_h' => 1,
|
||||
'w' => 1,
|
||||
'h' => 1,
|
||||
'fsize' => 1,
|
||||
'omitted_posts' => 1,
|
||||
'omitted_images' => 1,
|
||||
'replies' => 1,
|
||||
'images' => 1,
|
||||
'sticky' => 1,
|
||||
'locked' => 1,
|
||||
'last_modified' => 1
|
||||
);
|
||||
|
||||
private function translateFields($fields, $object, &$apiPost) {
|
||||
foreach ($fields as $local => $translated) {
|
||||
if (!isset($object->$local))
|
||||
if (!isset($object->$local)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$toInt = isset(self::$ints[$translated]);
|
||||
$toInt = isset(self::INTS[$translated]);
|
||||
$val = $object->$local;
|
||||
if (isset($this->config['hide_email']) && $this->config['hide_email'] && $local === 'email') {
|
||||
if ($this->hide_email && $local === 'email') {
|
||||
$val = '';
|
||||
}
|
||||
if ($val !== null && $val !== '') {
|
||||
$apiPost[$translated] = $toInt ? (int) $val : $val;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private function translateFile($file, $post, &$apiPost) {
|
||||
$this->translateFields($this->fileFields, $file, $apiPost);
|
||||
$this->translateFields(self::FILE_FIELDS, $file, $apiPost);
|
||||
$dotPos = strrpos($file->file, '.');
|
||||
$apiPost['ext'] = substr($file->file, $dotPos);
|
||||
$apiPost['tim'] = substr($file->file, 0, $dotPos);
|
||||
if (isset($this->config['show_filename']) && $this->config['show_filename']) {
|
||||
|
||||
if ($this->show_filename) {
|
||||
$apiPost['filename'] = @substr($file->name, 0, strrpos($file->name, '.'));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$apiPost['filename'] = substr($file->file, 0, $dotPos);
|
||||
}
|
||||
if (isset ($file->hash) && $file->hash) {
|
||||
$apiPost['md5'] = base64_encode(hex2bin($file->hash));
|
||||
}
|
||||
else if (isset ($post->filehash) && $post->filehash) {
|
||||
} elseif (isset ($post->filehash) && $post->filehash) {
|
||||
$apiPost['md5'] = base64_encode(hex2bin($post->filehash));
|
||||
}
|
||||
}
|
||||
|
||||
private function translatePost($post, $threadsPage = false) {
|
||||
private function translatePost($post, bool $threadsPage = false) {
|
||||
global $config, $board;
|
||||
$apiPost = array();
|
||||
$fields = $threadsPage ? $this->threadsPageFields : $this->postFields;
|
||||
|
||||
$apiPost = [];
|
||||
$fields = $threadsPage ? self::THREADS_PAGE_FIELDS : $this->postFields;
|
||||
$this->translateFields($fields, $post, $apiPost);
|
||||
|
||||
if (isset($config['poster_ids']) && $config['poster_ids']) $apiPost['id'] = poster_id($post->ip, $post->thread, $board['uri']);
|
||||
if ($threadsPage) return $apiPost;
|
||||
|
||||
if (isset($config['poster_ids']) && $config['poster_ids']) {
|
||||
$apiPost['id'] = poster_id($post->ip, $post->thread ?? $post->id);
|
||||
}
|
||||
if ($threadsPage) {
|
||||
return $apiPost;
|
||||
}
|
||||
|
||||
// Handle country field
|
||||
if (isset($post->body_nomarkup) && $this->config['country_flags']) {
|
||||
if (isset($post->body_nomarkup) && $this->country_flags) {
|
||||
$modifiers = extract_modifiers($post->body_nomarkup);
|
||||
if (isset($modifiers['flag']) && isset($modifiers['flag alt']) && preg_match('/^[a-z]{2}$/', $modifiers['flag'])) {
|
||||
$country = strtoupper($modifiers['flag']);
|
||||
@ -139,12 +148,15 @@ class Api {
|
||||
if (isset($post->files) && $post->files && !$threadsPage) {
|
||||
$file = $post->files[0];
|
||||
$this->translateFile($file, $post, $apiPost);
|
||||
|
||||
if (sizeof($post->files) > 1) {
|
||||
$extra_files = array();
|
||||
$extra_files = [];
|
||||
foreach ($post->files as $i => $f) {
|
||||
if ($i == 0) continue;
|
||||
|
||||
$extra_file = array();
|
||||
if ($i == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$extra_file = [];
|
||||
$this->translateFile($f, $post, $extra_file);
|
||||
|
||||
$extra_files[] = $extra_file;
|
||||
@ -156,8 +168,8 @@ class Api {
|
||||
return $apiPost;
|
||||
}
|
||||
|
||||
function translateThread(Thread $thread, $threadsPage = false) {
|
||||
$apiPosts = array();
|
||||
public function translateThread(Thread $thread, bool $threadsPage = false) {
|
||||
$apiPosts = [];
|
||||
$op = $this->translatePost($thread, $threadsPage);
|
||||
if (!$threadsPage) $op['resto'] = 0;
|
||||
$apiPosts['posts'][] = $op;
|
||||
@ -169,16 +181,16 @@ class Api {
|
||||
return $apiPosts;
|
||||
}
|
||||
|
||||
function translatePage(array $threads) {
|
||||
$apiPage = array();
|
||||
public function translatePage(array $threads) {
|
||||
$apiPage = [];
|
||||
foreach ($threads as $thread) {
|
||||
$apiPage['threads'][] = $this->translateThread($thread);
|
||||
}
|
||||
return $apiPage;
|
||||
}
|
||||
|
||||
function translateCatalogPage(array $threads, $threadsPage = false) {
|
||||
$apiPage = array();
|
||||
public function translateCatalogPage(array $threads, bool $threadsPage = false) {
|
||||
$apiPage = [];
|
||||
foreach ($threads as $thread) {
|
||||
$ts = $this->translateThread($thread, $threadsPage);
|
||||
$apiPage['threads'][] = current($ts['posts']);
|
||||
@ -186,8 +198,8 @@ class Api {
|
||||
return $apiPage;
|
||||
}
|
||||
|
||||
function translateCatalog($catalog, $threadsPage = false) {
|
||||
$apiCatalog = array();
|
||||
public function translateCatalog($catalog, bool $threadsPage = false) {
|
||||
$apiCatalog = [];
|
||||
foreach ($catalog as $page => $threads) {
|
||||
$apiPage = $this->translateCatalogPage($threads, $threadsPage);
|
||||
$apiPage['page'] = $page;
|
||||
|
266
inc/bans.php
266
inc/bans.php
@ -4,6 +4,10 @@ use Vichan\Functions\Format;
|
||||
use Lifo\IP\CIDR;
|
||||
|
||||
class Bans {
|
||||
static private function shouldDelete(array $ban, bool $require_ban_view) {
|
||||
return $ban['expires'] && ($ban['seen'] || !$require_ban_view) && $ban['expires'] < time();
|
||||
}
|
||||
|
||||
static private function deleteBans(array $ban_ids) {
|
||||
$len = count($ban_ids);
|
||||
if ($len === 1) {
|
||||
@ -11,7 +15,7 @@ class Bans {
|
||||
$query->bindValue(':id', $ban_ids[0], PDO::PARAM_INT);
|
||||
$query->execute() or error(db_error());
|
||||
|
||||
rebuildThemes('bans');
|
||||
Vichan\Functions\Theme\rebuild_themes('bans');
|
||||
} elseif ($len >= 1) {
|
||||
// Build the query.
|
||||
$query = 'DELETE FROM ``bans`` WHERE `id` IN (';
|
||||
@ -29,10 +33,131 @@ class Bans {
|
||||
|
||||
$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) {
|
||||
list($ipstart, $ipend) = $mask;
|
||||
|
||||
@ -56,7 +181,7 @@ class Bans {
|
||||
$cidr = new CIDR($mask);
|
||||
$range = $cidr->getRange();
|
||||
|
||||
return array(inet_pton($range[0]), inet_pton($range[1]));
|
||||
return [ inet_pton($range[0]), inet_pton($range[1]) ];
|
||||
}
|
||||
|
||||
public static function parse_time($str) {
|
||||
@ -140,82 +265,25 @@ class Bans {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array($ipstart, $ipend);
|
||||
return [$ipstart, $ipend];
|
||||
}
|
||||
|
||||
static public function findSingle(string $ip, int $ban_id, bool $require_ban_view): array|null {
|
||||
/**
|
||||
* Use OR in the query to also garbage collect bans. Ideally we should move the whole GC procedure to a separate
|
||||
* script, but it will require a more important restructuring.
|
||||
*/
|
||||
$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;
|
||||
}
|
||||
static public function findSingle(string $ip, int $ban_id, bool $require_ban_view, bool $auto_gc) {
|
||||
if ($auto_gc) {
|
||||
return self::findSingleAutoGc($ip, $ban_id, $require_ban_view);
|
||||
} else {
|
||||
return self::findSingleNoGc($ban_id);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
$query = prepare('SELECT ``bans``.*' . ($get_mod_info ? ', `username`' : '') . ' FROM ``bans``
|
||||
' . ($get_mod_info ? 'LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`' : '') . '
|
||||
WHERE
|
||||
(' . ($board !== false ? '(`board` IS NULL OR `board` = :board) AND' : '') . '
|
||||
(`ipstart` = :ip OR (:ip >= `ipstart` AND :ip <= `ipend`)) OR (``bans``.id = :id))
|
||||
ORDER BY `expires` IS NULL, `expires` DESC');
|
||||
|
||||
if ($board !== false)
|
||||
$query->bindValue(':board', $board, PDO::PARAM_STR);
|
||||
|
||||
$query->bindValue(':id', $banid);
|
||||
$query->bindValue(':ip', inet_pton($ip));
|
||||
|
||||
$query->execute() or error(db_error($query));
|
||||
|
||||
$ban_list = array();
|
||||
$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;
|
||||
}
|
||||
if ($auto_gc) {
|
||||
return self::findAutoGc($ip, $board, $get_mod_info, $config['require_ban_view'], $ban_id);
|
||||
} else {
|
||||
return self::findNoGc($ip, $board, $get_mod_info, $ban_id);
|
||||
}
|
||||
|
||||
self::deleteBans($to_delete_list);
|
||||
|
||||
return $ban_list;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
foreach ($bans as &$ban) {
|
||||
$uncloaked_mask = self::range_to_string(array($ban['ipstart'], $ban['ipend']));
|
||||
$ban['mask'] = cloak_mask($uncloaked_mask);
|
||||
$ban['mask'] = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
|
||||
|
||||
if ($ban['post']) {
|
||||
$post = json_decode($ban['post']);
|
||||
@ -276,12 +343,24 @@ class Bans {
|
||||
|
||||
static public function seen($ban_id) {
|
||||
$query = query("UPDATE ``bans`` SET `seen` = 1 WHERE `id` = " . (int)$ban_id) or error(db_error());
|
||||
rebuildThemes('bans');
|
||||
Vichan\Functions\Theme\rebuild_themes('bans');
|
||||
}
|
||||
|
||||
static public function purge() {
|
||||
$query = query("DELETE FROM ``bans`` WHERE `expires` IS NOT NULL AND `expires` < " . time() . " AND `seen` = 1") or error(db_error());
|
||||
rebuildThemes('bans');
|
||||
static public function purge($require_seen, $moratorium) {
|
||||
if ($require_seen) {
|
||||
$query = prepare("DELETE FROM ``bans`` WHERE `expires` IS NOT NULL AND `expires` + :moratorium < :curr_time AND `seen` = 1");
|
||||
} else {
|
||||
$query = prepare("DELETE FROM ``bans`` WHERE `expires` IS NOT NULL AND `expires` + :moratorium < :curr_time");
|
||||
}
|
||||
$query->bindValue(':moratorium', $moratorium);
|
||||
$query->bindValue(':curr_time', time());
|
||||
$query->execute() or error(db_error($query));
|
||||
|
||||
$affected = $query->rowCount();
|
||||
if ($affected > 0) {
|
||||
Vichan\Functions\Theme\rebuild_themes('bans');
|
||||
}
|
||||
return $affected;
|
||||
}
|
||||
|
||||
static public function delete($ban_id, $modlog = false, $boards = false, $dont_rebuild = false) {
|
||||
@ -299,8 +378,7 @@ class Bans {
|
||||
if ($boards !== false && !in_array($ban['board'], $boards))
|
||||
error($config['error']['noaccess']);
|
||||
|
||||
$mask = self::range_to_string(array($ban['ipstart'], $ban['ipend']));
|
||||
$cloaked_mask = cloak_mask($mask);
|
||||
$mask = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
|
||||
|
||||
modLog("Removed ban #{$ban_id} for " .
|
||||
(filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$cloaked_mask\">$cloaked_mask</a>" : $cloaked_mask));
|
||||
@ -308,7 +386,7 @@ class Bans {
|
||||
|
||||
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;
|
||||
}
|
||||
@ -365,23 +443,29 @@ class Bans {
|
||||
openBoard($post['board']);
|
||||
|
||||
$post['board'] = $board['uri'];
|
||||
/*
|
||||
* The body can be so long to make the json longer than 64KBs, causing the query to fail.
|
||||
* Truncate it to a safe length (32KBs). It could probably be longer, but if the deleted body is THAT big
|
||||
* already, the likelihood of it being just assorted spam/garbage is about 101%.
|
||||
*/
|
||||
// We're on UTF-8 only, right...?
|
||||
$post['body'] = mb_strcut($post['body'], 0, 32768);
|
||||
|
||||
$query->bindValue(':post', json_encode($post));
|
||||
} else
|
||||
$query->bindValue(':post', null, PDO::PARAM_NULL);
|
||||
|
||||
$query->execute() or error(db_error($query));
|
||||
if (isset($mod['id']) && $mod['id'] == $mod_id) {
|
||||
modLog('Created a new ' .
|
||||
($length > 0 ? preg_replace('/^(\d+) (\w+?)s?$/', '$1-$2', 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();
|
||||
}
|
||||
|
177
inc/cache.php
177
inc/cache.php
@ -4,164 +4,89 @@
|
||||
* Copyright (c) 2010-2013 Tinyboard Development Group
|
||||
*/
|
||||
|
||||
use Vichan\Data\Driver\{CacheDriver, ApcuCacheDriver, ArrayCacheDriver, FsCacheDriver, MemcachedCacheDriver, NoneCacheDriver, RedisCacheDriver};
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
class Cache {
|
||||
private static $cache;
|
||||
public static function init() {
|
||||
private static function buildCache(): CacheDriver {
|
||||
global $config;
|
||||
|
||||
switch ($config['cache']['enabled']) {
|
||||
case 'memcached':
|
||||
self::$cache = new Memcached();
|
||||
self::$cache->addServers($config['cache']['memcached']);
|
||||
break;
|
||||
return new MemcachedCacheDriver(
|
||||
$config['cache']['prefix'],
|
||||
$config['cache']['memcached']
|
||||
);
|
||||
case 'redis':
|
||||
self::$cache = new Redis();
|
||||
self::$cache->connect($config['cache']['redis'][0], $config['cache']['redis'][1]);
|
||||
if ($config['cache']['redis'][2]) {
|
||||
self::$cache->auth($config['cache']['redis'][2]);
|
||||
}
|
||||
self::$cache->select($config['cache']['redis'][3]) or die('cache select failure');
|
||||
break;
|
||||
return new RedisCacheDriver(
|
||||
$config['cache']['prefix'],
|
||||
$config['cache']['redis'][0],
|
||||
$config['cache']['redis'][1],
|
||||
$config['cache']['redis'][2],
|
||||
$config['cache']['redis'][3]
|
||||
);
|
||||
case 'apcu':
|
||||
return new ApcuCacheDriver;
|
||||
case 'fs':
|
||||
return new FsCacheDriver(
|
||||
$config['cache']['prefix'],
|
||||
"tmp/cache/{$config['cache']['prefix']}",
|
||||
'.lock',
|
||||
$config['auto_maintenance'] ? 1000 : false
|
||||
);
|
||||
case 'none':
|
||||
return new NoneCacheDriver();
|
||||
case 'php':
|
||||
self::$cache = array();
|
||||
break;
|
||||
default:
|
||||
return new ArrayCacheDriver();
|
||||
}
|
||||
}
|
||||
|
||||
public static function getCache(): CacheDriver {
|
||||
static $cache;
|
||||
return $cache ??= self::buildCache();
|
||||
}
|
||||
|
||||
public static function get($key) {
|
||||
global $config, $debug;
|
||||
|
||||
$key = $config['cache']['prefix'] . $key;
|
||||
|
||||
$data = false;
|
||||
switch ($config['cache']['enabled']) {
|
||||
case 'memcached':
|
||||
if (!self::$cache)
|
||||
self::init();
|
||||
$data = self::$cache->get($key);
|
||||
break;
|
||||
case '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;
|
||||
$ret = self::getCache()->get($key);
|
||||
if ($ret === null) {
|
||||
$ret = false;
|
||||
}
|
||||
|
||||
if ($config['debug'])
|
||||
$debug['cached'][] = $key . ($data === false ? ' (miss)' : ' (hit)');
|
||||
if ($config['debug']) {
|
||||
$debug['cached'][] = $config['cache']['prefix'] . $key . ($ret === false ? ' (miss)' : ' (hit)');
|
||||
}
|
||||
|
||||
return $data;
|
||||
return $ret;
|
||||
}
|
||||
public static function set($key, $value, $expires = false) {
|
||||
global $config, $debug;
|
||||
|
||||
$key = $config['cache']['prefix'] . $key;
|
||||
|
||||
if (!$expires)
|
||||
if (!$expires) {
|
||||
$expires = $config['cache']['timeout'];
|
||||
|
||||
switch ($config['cache']['enabled']) {
|
||||
case 'memcached':
|
||||
if (!self::$cache)
|
||||
self::init();
|
||||
self::$cache->set($key, $value, $expires);
|
||||
break;
|
||||
case 'redis':
|
||||
if (!self::$cache)
|
||||
self::init();
|
||||
self::$cache->setex($key, $expires, json_encode($value));
|
||||
break;
|
||||
case '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'])
|
||||
$debug['cached'][] = $key . ' (set)';
|
||||
self::getCache()->set($key, $value, $expires);
|
||||
|
||||
if ($config['debug']) {
|
||||
$debug['cached'][] = $config['cache']['prefix'] . $key . ' (set)';
|
||||
}
|
||||
}
|
||||
public static function delete($key) {
|
||||
global $config, $debug;
|
||||
|
||||
$key = $config['cache']['prefix'] . $key;
|
||||
self::getCache()->delete($key);
|
||||
|
||||
switch ($config['cache']['enabled']) {
|
||||
case 'memcached':
|
||||
if (!self::$cache)
|
||||
self::init();
|
||||
self::$cache->delete($key);
|
||||
break;
|
||||
case 'redis':
|
||||
if (!self::$cache)
|
||||
self::init();
|
||||
self::$cache->del($key);
|
||||
break;
|
||||
case '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'][] = $config['cache']['prefix'] . $key . ' (deleted)';
|
||||
}
|
||||
|
||||
if ($config['debug'])
|
||||
$debug['cached'][] = $key . ' (deleted)';
|
||||
}
|
||||
public static function flush() {
|
||||
global $config;
|
||||
|
||||
switch ($config['cache']['enabled']) {
|
||||
case 'memcached':
|
||||
if (!self::$cache)
|
||||
self::init();
|
||||
return self::$cache->flush();
|
||||
case '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();
|
||||
}
|
||||
|
||||
self::getCache()->flush();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
782
inc/config.php
782
inc/config.php
@ -68,18 +68,25 @@
|
||||
// Deprecated, use 'log_system'.
|
||||
$config['syslog'] = false;
|
||||
|
||||
$config['log_system'] = [];
|
||||
// Log all error messages and unauthorized login attempts.
|
||||
// Can be "syslog", "error_log" (default), "file", "stderr" or "none".
|
||||
$config['log_system']['type'] = 'error_log';
|
||||
// The application name used by the logging system. Defaults to "tinyboard" for backwards compatibility.
|
||||
$config['log_system']['name'] = 'tinyboard';
|
||||
// Only relevant if 'log_system' is set to "syslog". If true, double print the logs also in stderr.
|
||||
// Defaults to false.
|
||||
$config['log_system']['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'.
|
||||
$config['log_system']['file_path'] = '/var/log/vichan.log';
|
||||
$config['log_system'] = [
|
||||
/*
|
||||
* Log all error messages and unauthorized login attempts.
|
||||
* Can be "syslog", "error_log" (default), "file", or "stderr".
|
||||
*/
|
||||
'type' => 'error_log',
|
||||
// The application name used by the logging system. Defaults to "tinyboard" for backwards compatibility.
|
||||
'name' => 'tinyboard',
|
||||
/*
|
||||
* Only relevant if 'log_system' is set to "syslog". If true, double print the logs also in stderr. Defaults to
|
||||
* false.
|
||||
*/
|
||||
'syslog_stderr' => false,
|
||||
/*
|
||||
* Only relevant if "log_system" is set to `file`. Sets the file that vichan will log to. Defaults to
|
||||
* '/var/log/vichan.log'.
|
||||
*/
|
||||
'file_path' => '/var/log/vichan.log',
|
||||
];
|
||||
|
||||
// Use `host` via shell_exec() to lookup hostnames, avoiding query timeouts. May not work on your system.
|
||||
// Requires safe_mode to be disabled.
|
||||
@ -92,6 +99,11 @@
|
||||
// to the environment path (seperated by :).
|
||||
$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
|
||||
@ -127,17 +139,26 @@
|
||||
|
||||
/*
|
||||
* On top of the static file caching system, you can enable the additional caching system which is
|
||||
* designed to minimize SQL queries and can significantly increase speed when posting or using the
|
||||
* moderator interface. APC is the recommended method of caching.
|
||||
* designed to minimize request processing can significantly increase speed when posting or using
|
||||
* the moderator interface.
|
||||
*
|
||||
* https://github.com/vichan-devel/vichan/wiki/cache
|
||||
*/
|
||||
|
||||
// Uses a PHP array. MUST NOT be used in multiprocess environments.
|
||||
$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';
|
||||
// The Memcache server. Requires the memcached extension, with a final D.
|
||||
// $config['cache']['enabled'] = 'memcached';
|
||||
// The Redis server. Requires the extension.
|
||||
// $config['cache']['enabled'] = 'redis';
|
||||
// Use the local cache folder. Slower than native but available out of the box and compatible with multiprocess
|
||||
// environments. You can mount a ram-based filesystem in the cache directory to improve performance.
|
||||
// $config['cache']['enabled'] = 'fs';
|
||||
// Technically available, offers a no-op fake cache. Don't use this outside of testing or debugging.
|
||||
// $config['cache']['enabled'] = 'none';
|
||||
|
||||
// Timeout for cached objects such as posts and HTML.
|
||||
$config['cache']['timeout'] = 60 * 60 * 48; // 48 hours
|
||||
@ -205,6 +226,9 @@
|
||||
// Used to salt secure tripcodes ("##trip") and poster IDs (if enabled).
|
||||
$config['secure_trip_salt'] = ')(*&^%$#@!98765432190zyxwvutsrqponmlkjihgfedcba';
|
||||
|
||||
// Used to salt poster passwords.
|
||||
$config['secure_password_salt'] = 'wKJSb7M5SyzMcFWD2gPO3j2RYUSO9B789!@#$%^&*()';
|
||||
|
||||
/*
|
||||
* ====================
|
||||
* Flood/spam settings
|
||||
@ -251,83 +275,6 @@
|
||||
// To prevent bump attacks; returns the thread to last position after the last post is deleted.
|
||||
$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.
|
||||
// Works very well against uncustomized spam. Answers are case-insensitive.
|
||||
// $config['simple_spam'] = array (
|
||||
@ -336,39 +283,39 @@
|
||||
//);
|
||||
$config['simple_spam'] = false;
|
||||
|
||||
// Enable reCaptcha to make spam even harder. Rarely necessary.
|
||||
$config['recaptcha'] = false;
|
||||
|
||||
// Public and private key pair from https://www.google.com/recaptcha/admin/create
|
||||
$config['recaptcha_public'] = '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f';
|
||||
$config['recaptcha_private'] = '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_';
|
||||
|
||||
// Enable hCaptcha as an alternative to reCAPTCHA.
|
||||
$config['hcaptcha'] = false;
|
||||
|
||||
// Public and private key pair for using hCaptcha.
|
||||
$config['hcaptcha_public'] = '7a4b21e0-dc53-46f2-a9f8-91d2e74b63a0';
|
||||
$config['hcaptcha_private'] = '0x4e9A01bE637b51dC41a7Ea9865C3fDe4aB72Cf17';
|
||||
|
||||
// Enable Custom Captcha you need to change a couple of settings
|
||||
//Read more at: /inc/captcha/readme.md
|
||||
$config['captcha'] = array();
|
||||
|
||||
// Enable custom captcha provider
|
||||
$config['captcha']['enabled'] = false;
|
||||
|
||||
//New thread captcha
|
||||
//Require solving a captcha to post a thread.
|
||||
//Default off.
|
||||
$config['new_thread_capt'] = false;
|
||||
|
||||
// Custom captcha get provider path (if not working get the absolute path aka your url.)
|
||||
$config['captcha']['provider_get'] = '../inc/captcha/entrypoint.php';
|
||||
// Custom captcha check provider path
|
||||
$config['captcha']['provider_check'] = '../inc/captcha/entrypoint.php';
|
||||
|
||||
// Custom captcha extra field (eg. charset)
|
||||
$config['captcha']['extra'] = 'abcdefghijklmnopqrstuvwxyz';
|
||||
$config['captcha'] = [
|
||||
// Can be false, 'recaptcha', 'hcaptcha' or 'native'.
|
||||
'provider' => false,
|
||||
/*
|
||||
* If not false, the captcha is dynamically injected on the client if the web server set the `captcha-required`
|
||||
* cookie to 1. The configuration value should be set the IP for which the captcha should be verified.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* // Verify the captcha for users sending posts from the loopback address.
|
||||
* $config['captcha']['dynamic'] = '127.0.0.1';
|
||||
*/
|
||||
'dynamic' => false,
|
||||
'recaptcha' => [
|
||||
'sitekey' => '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI',
|
||||
'secret' => '6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe',
|
||||
],
|
||||
'hcaptcha' => [
|
||||
'sitekey' => '10000000-ffff-ffff-ffff-000000000001',
|
||||
'secret' => '0x0000000000000000000000000000000000000000',
|
||||
],
|
||||
// To enable the native captcha you need to change a couple of settings. Read more at: /inc/captcha/readme.md
|
||||
'native' => [
|
||||
// Custom captcha get provider path (if not working get the absolute path aka your url).
|
||||
'provider_get' => '/inc/captcha/entrypoint.php',
|
||||
// Custom captcha check provider path
|
||||
'provider_check' => '/inc/captcha/entrypoint.php',
|
||||
// Custom captcha extra field (eg. charset)
|
||||
'extra' => 'abcdefghijklmnopqrstuvwxyz',
|
||||
// New thread captcha. Require solving a captcha to post a thread.
|
||||
'new_thread_capt' => false
|
||||
]
|
||||
];
|
||||
|
||||
// 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;
|
||||
@ -508,6 +455,17 @@
|
||||
// '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
|
||||
// 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
|
||||
@ -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,
|
||||
// with the modifier Z added, with the result displayed at the top of the post body.
|
||||
$config['allow_roll'] = false;
|
||||
@ -733,6 +694,9 @@
|
||||
//);
|
||||
$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.
|
||||
$config['ban_appeals'] = false;
|
||||
|
||||
@ -754,11 +718,15 @@
|
||||
* ====================
|
||||
*/
|
||||
|
||||
// "Wiki" markup syntax ($config['wiki_markup'] in pervious versions):
|
||||
$config['markup'][] = array("/'''(.+?)'''/", "<strong>\$1</strong>");
|
||||
$config['markup'][] = array("/''(.+?)''/", "<em>\$1</em>");
|
||||
$config['markup'][] = array("/\*\*(.+?)\*\*/", "<span class=\"spoiler\">\$1</span>");
|
||||
$config['markup'][] = array("/^[ |\t]*==(.+?)==[ |\t]*$/m", "<span class=\"heading\">\$1</span>");
|
||||
$config['markup'] = [
|
||||
// Inline dice roll markup.
|
||||
[ "/!([-+]?\d+)?([d])([-+]?\d+)([-+]\d+)?/iu", fn($m) => inline_dice_roll_markup($m, 'static/d10.svg') ],
|
||||
// "Wiki" markup syntax ($config['wiki_markup'] in pervious versions):
|
||||
[ "/'''(.+?)'''/", "<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\](.*?)\[\/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';
|
||||
|
||||
// Allowed image file extensions.
|
||||
$config['allowed_ext'][] = 'jpg';
|
||||
$config['allowed_ext'][] = 'jpeg';
|
||||
$config['allowed_ext'][] = 'bmp';
|
||||
$config['allowed_ext'][] = 'gif';
|
||||
$config['allowed_ext'][] = 'png';
|
||||
$config['allowed_ext'][] = 'webp';
|
||||
$config['allowed_ext'] = [
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'bmp',
|
||||
'gif',
|
||||
'png',
|
||||
'webp'
|
||||
];
|
||||
// $config['allowed_ext'][] = 'svg';
|
||||
|
||||
// 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.
|
||||
$config['file_icons']['default'] = 'file.png';
|
||||
$config['file_icons']['zip'] = 'zip.png';
|
||||
$config['file_icons']['webm'] = 'video.png';
|
||||
$config['file_icons']['mp4'] = 'video.png';
|
||||
$config['file_icons'] = [
|
||||
'default' => 'file.png',
|
||||
'zip' => 'zip.png',
|
||||
'webm' => 'video.png',
|
||||
'mp4' => 'video.png'
|
||||
];
|
||||
// Example: Custom thumbnail for certain file extension.
|
||||
// $config['file_icons']['extension'] = 'some_file.png';
|
||||
|
||||
@ -921,11 +893,13 @@
|
||||
$config['show_filename'] = true;
|
||||
|
||||
// WebM Settings
|
||||
$config['webm']['use_ffmpeg'] = false;
|
||||
$config['webm']['allow_audio'] = false;
|
||||
$config['webm']['max_length'] = 120;
|
||||
$config['webm']['ffmpeg_path'] = 'ffmpeg';
|
||||
$config['webm']['ffprobe_path'] = 'ffprobe';
|
||||
$config['webm'] = [
|
||||
'use_ffmpeg' => false,
|
||||
'allow_audio' => false,
|
||||
'max_length' => 120,
|
||||
'ffmpeg_path' => 'ffmpeg',
|
||||
'ffprobe_path' => 'ffprobe'
|
||||
];
|
||||
|
||||
// Display image identification links for ImgOps, regex.info/exif, Google Images and iqdb.
|
||||
$config['image_identification'] = false;
|
||||
@ -1034,8 +1008,11 @@
|
||||
|
||||
// Custom stylesheets available for the user to choose. See the "stylesheets/" folder for a list of
|
||||
// available stylesheets (or create your own).
|
||||
$config['stylesheets']['Yotsuba B'] = ''; // Default; there is no additional/custom stylesheet for this.
|
||||
$config['stylesheets']['Yotsuba'] = 'yotsuba.css';
|
||||
$config['stylesheets'] = [
|
||||
// Default; there is no additional/custom stylesheet for this.
|
||||
'Yotsuba B' => '',
|
||||
'Yotsuba' => 'yotsuba.css'
|
||||
];
|
||||
// $config['stylesheets']['Futaba'] = 'futaba.css';
|
||||
// $config['stylesheets']['Dark'] = 'dark.css';
|
||||
|
||||
@ -1117,6 +1094,10 @@
|
||||
// <tinyboard flag style>.
|
||||
$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
|
||||
@ -1149,6 +1130,10 @@
|
||||
// Minify Javascript using http://code.google.com/p/minify/.
|
||||
$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
|
||||
// code to work.
|
||||
$config['javascript_image_dispatch'] = false;
|
||||
@ -1165,11 +1150,11 @@
|
||||
// Custom embedding (YouTube, vimeo, etc.)
|
||||
// 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.
|
||||
$config['embedding'] = array(
|
||||
$config['embedding'] = array(
|
||||
array(
|
||||
'/^https?:\/\/(\w+\.)?youtube\.com\/watch\?v=([a-zA-Z0-9\-_]{10,11})?$/i',
|
||||
'<iframe style="float: left; margin: 10px 20px;" width="%%tb_width%%" height="%%tb_height%%" frameborder="0" id="ytplayer" src="https://www.youtube.com/embed/$2"></iframe>'
|
||||
),
|
||||
'/^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/$3"></iframe>'
|
||||
),
|
||||
array(
|
||||
'/^https?:\/\/(\w+\.)?vimeo\.com\/(\d{2,10})(\?.+)?$/i',
|
||||
'<iframe style="float: left; margin: 10px 20px;" width="%%tb_width%%" height="%%tb_height%%" frameborder="0" src="https://player.vimeo.com/video/$2"></iframe>'
|
||||
@ -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>'
|
||||
),
|
||||
array(
|
||||
'/^https?:\/\/(\w+\.)?metacafe\.com\/watch\/(\d+)\/([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>'
|
||||
'/^https?:\/\/(www\.)?rumble\.com\/embed\/([a-zA-Z0-9]+)(\/\?[^\'"<>]*)?$/i',
|
||||
'<iframe class="rumble" width="%%tb_width%%" height="%%tb_height%%" src="https://rumble.com/embed/$2/" frameborder="0" allowfullscreen></iframe>'
|
||||
),
|
||||
array(
|
||||
'/^https?:\/\/(\w+\.)?vocaroo\.com\/([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/$2"></iframe>'
|
||||
)
|
||||
'/^https?:\/\/(www\.)?bitchute\.com\/(?:video|embed)\/([a-zA-Z0-9]+)(\/)?(\?[^\'"<>]*)?$/i',
|
||||
'<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.
|
||||
@ -1198,86 +1207,86 @@
|
||||
* ====================
|
||||
*/
|
||||
|
||||
// Error messages
|
||||
$config['error']['bot'] = _('You look like a bot.');
|
||||
$config['error']['referer'] = _('Your browser sent an invalid or no HTTP referer.');
|
||||
$config['error']['toolong'] = _('The %s field was too long.');
|
||||
$config['error']['toolong_body'] = _('The body was too long.');
|
||||
$config['error']['tooshort_body'] = _('The body was too short or empty.');
|
||||
$config['error']['toomanylines'] = _('Your post contains too many lines!');
|
||||
$config['error']['noimage'] = _('You must upload an image.');
|
||||
$config['error']['toomanyimages'] = _('You have attempted to upload too many images!');
|
||||
$config['error']['nomove'] = _('The server failed to handle your upload.');
|
||||
$config['error']['fileext'] = _('Unsupported image format.');
|
||||
$config['error']['noboard'] = _('Invalid board!');
|
||||
$config['error']['nonexistant'] = _('Thread specified does not exist.');
|
||||
$config['error']['nopost'] = _('Post specified does not exist.');
|
||||
$config['error']['locked'] = _('Thread locked. You may not reply at this time.');
|
||||
$config['error']['reply_hard_limit'] = _('Thread has reached its maximum reply limit.');
|
||||
$config['error']['image_hard_limit'] = _('Thread has reached its maximum image limit.');
|
||||
$config['error']['nopost'] = _('You didn\'t make a post.');
|
||||
$config['error']['flood'] = _('Flood detected; Post discarded.');
|
||||
$config['error']['too_many_threads'] = _('The hourly thread limit has been reached. Please post in an existing thread.');
|
||||
$config['error']['spam'] = _('Your request looks automated; Post discarded.');
|
||||
$config['error']['simple_spam'] = _('You must answer the question to make a new thread. See the last field.');
|
||||
$config['error']['unoriginal'] = _('Unoriginal content!');
|
||||
$config['error']['muted'] = _('Unoriginal content! You have been muted for %d seconds.');
|
||||
$config['error']['youaremuted'] = _('You are muted! Expires in %d seconds.');
|
||||
$config['error']['dnsbl'] = _('Your IP address is listed in %s.');
|
||||
$config['error']['toomanylinks'] = _('Too many links; flood detected.');
|
||||
$config['error']['toomanycites'] = _('Too many cites; post discarded.');
|
||||
$config['error']['toomanycross'] = _('Too many cross-board links; post discarded.');
|
||||
$config['error']['nodelete'] = _('You didn\'t select anything to delete.');
|
||||
$config['error']['noreport'] = _('You didn\'t select anything to report.');
|
||||
$config['error']['toolongreport'] = _('The reason was too long.');
|
||||
$config['error']['toomanyreports'] = _('You can\'t report that many posts at once.');
|
||||
$config['error']['noban'] = _('That ban doesn\'t exist or is not for you.');
|
||||
$config['error']['tooshortban'] = _('You cannot appeal a ban of this length.');
|
||||
$config['error']['toolongappeal'] = _('The appeal was too long.');
|
||||
$config['error']['toomanyappeals'] = _('You cannot appeal this ban again.');
|
||||
$config['error']['pendingappeal'] = _('There is already a pending appeal for this ban.');
|
||||
$config['error']['invalidpassword'] = _('Wrong password…');
|
||||
$config['error']['invalidimg'] = _('Invalid image.');
|
||||
$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.');
|
||||
$config['error']['unknownext'] = _('Unknown file extension.');
|
||||
$config['error']['filesize'] = _('Maximum file size: %maxsz% bytes<br>Your file\'s size: %filesz% bytes');
|
||||
$config['error']['maxsize'] = _('The file was too big.');
|
||||
$config['error']['genwebmerror'] = _('There was a problem processing your webm.');
|
||||
$config['error']['webmerror'] = _('There was a problem processing your webm.');//Is this error used anywhere ?
|
||||
$config['error']['invalidwebm'] = _('Invalid webm uploaded.');
|
||||
$config['error']['webmhasaudio'] = _('The uploaded webm contains an audio or another type of additional stream.');
|
||||
$config['error']['webmtoolong'] =_('The uploaded webm is longer than %d seconds.');
|
||||
$config['error']['fileexists'] = _('That file <a href="%s">already exists</a>!');
|
||||
$config['error']['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.');
|
||||
$config['error']['delete_too_late'] = _('You cannot delete a post this old.');
|
||||
$config['error']['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.');
|
||||
$config['error']['captcha'] = _('You seem to have mistyped the verification.');
|
||||
$config['error']['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!');
|
||||
$config['error']['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.');
|
||||
$config['error'] = [
|
||||
// General error messages
|
||||
'bot' => _('You look like a bot.'),
|
||||
'referer' => _('Your browser sent an invalid or no HTTP referer.'),
|
||||
'toolong' => _('The %s field was too long.'),
|
||||
'toolong_body' => _('The body was too long.'),
|
||||
'tooshort_body' => _('The body was too short or empty.'),
|
||||
'toomanylines' => _('Your post contains too many lines!'),
|
||||
'noimage' => _('You must upload an image.'),
|
||||
'toomanyimages' => _('You have attempted to upload too many images!'),
|
||||
'nomove' => _('The server failed to handle your upload.'),
|
||||
'fileext' => _('Unsupported image format.'),
|
||||
'noboard' => _('Invalid board!'),
|
||||
'nonexistant' => _('Thread specified does not exist.'),
|
||||
'nopost' => _('Post specified does not exist.'),
|
||||
'locked' => _('Thread locked. You may not reply at this time.'),
|
||||
'reply_hard_limit' => _('Thread has reached its maximum reply limit.'),
|
||||
'image_hard_limit' => _('Thread has reached its maximum image limit.'),
|
||||
'nopost' => _('You didn\'t make a post.'),
|
||||
'flood' => _('Flood detected; Post discarded.'),
|
||||
'too_many_threads' => _('The hourly thread limit has been reached. Please post in an existing thread.'),
|
||||
'spam' => _('Your request looks automated; Post discarded.'),
|
||||
'simple_spam' => _('You must answer the question to make a new thread. See the last field.'),
|
||||
'unoriginal' => _('Unoriginal content!'),
|
||||
'muted' => _('Unoriginal content! You have been muted for %d seconds.'),
|
||||
'youaremuted' => _('You are muted! Expires in %d seconds.'),
|
||||
'dnsbl' => _('Your IP address is listed in %s.'),
|
||||
'toomanylinks' => _('Too many links; flood detected.'),
|
||||
'toomanycites' => _('Too many cites; post discarded.'),
|
||||
'toomanycross' => _('Too many cross-board links; post discarded.'),
|
||||
'nodelete' => _('You didn\'t select anything to delete.'),
|
||||
'noreport' => _('You didn\'t select anything to report.'),
|
||||
'toolongreport' => _('The reason was too long.'),
|
||||
'toomanyreports' => _('You can\'t report that many posts at once.'),
|
||||
'noban' => _('That ban doesn\'t exist or is not for you.'),
|
||||
'tooshortban' => _('You cannot appeal a ban of this length.'),
|
||||
'toolongappeal' => _('The appeal was too long.'),
|
||||
'toomanyappeals' => _('You cannot appeal this ban again.'),
|
||||
'pendingappeal' => _('There is already a pending appeal for this ban.'),
|
||||
'invalidpassword' => _('Wrong password…'),
|
||||
'invalidimg' => _('Invalid image.'),
|
||||
'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.'),
|
||||
'unknownext' => _('Unknown file extension.'),
|
||||
'filesize' => _('Maximum file size: %maxsz% bytes<br>Your file\'s size: %filesz% bytes'),
|
||||
'maxsize' => _('The file was too big.'),
|
||||
'genwebmerror' => _('There was a problem processing your webm.'),
|
||||
'invalidwebm' => _('Invalid webm uploaded.'),
|
||||
'webmhasaudio' => _('The uploaded webm contains an audio or another type of additional stream.'),
|
||||
'webmtoolong' =>_('The uploaded webm is longer than %d seconds.'),
|
||||
'fileexists' => _('That file <a href=>"%s">already exists</a>!'),
|
||||
'fileexistsinthread' => _('That file <a href=>"%s">already exists</a> in this thread!'),
|
||||
'delete_too_soon' => _('You\'ll have to wait another %s before deleting that.'),
|
||||
'delete_too_late' => _('You cannot delete a post this old.'),
|
||||
'mime_exploit' => _('MIME type detection XSS exploit (IE) detected; post discarded.'),
|
||||
'invalid_embed' => _('Couldn\'t make sense of the URL of the video you tried to embed.'),
|
||||
'captcha' => _('You seem to have mistyped the verification.'),
|
||||
'flag_undefined' => _('The flag %s is undefined, your PHP version is too old!'),
|
||||
'flag_wrongtype' => _('defined_flags_accumulate(): The flag %s is of the wrong type!'),
|
||||
'remote_io_error' => _('IO error while interacting with a remote service.'),
|
||||
'local_io_error' => _('IO error while interacting with a local resource or service.'),
|
||||
|
||||
|
||||
// Moderator errors
|
||||
$config['error']['toomanyunban'] = _('You are only allowed to unban %s users at a time. You tried to unban %u users.');
|
||||
$config['error']['invalid'] = _('Invalid username and/or password.');
|
||||
$config['error']['insecure'] = _('Login on insecure connections is disabled.');
|
||||
$config['error']['notamod'] = _('You are not a mod…');
|
||||
$config['error']['invalidafter'] = _('Invalid username and/or password. Your user may have been deleted or changed.');
|
||||
$config['error']['malformed'] = _('Invalid/malformed cookies.');
|
||||
$config['error']['missedafield'] = _('Your browser didn\'t submit an input when it should have.');
|
||||
$config['error']['required'] = _('The %s field is required.');
|
||||
$config['error']['invalidfield'] = _('The %s field was invalid.');
|
||||
$config['error']['boardexists'] = _('There is already a %s board.');
|
||||
$config['error']['noaccess'] = _('You don\'t have permission to do that.');
|
||||
$config['error']['invalidpost'] = _('That post doesn\'t exist…');
|
||||
$config['error']['404'] = _('Page not found.');
|
||||
$config['error']['modexists'] = _('That mod <a href="?/users/%d">already exists</a>!');
|
||||
$config['error']['invalidtheme'] = _('That theme doesn\'t exist!');
|
||||
$config['error']['csrf'] = _('Invalid security token! Please go back and try again.');
|
||||
$config['error']['badsyntax'] = _('Your code contained PHP syntax errors. Please go back and correct them. PHP says: ');
|
||||
// Moderator errors
|
||||
'toomanyunban' => _('You are only allowed to unban %s users at a time. You tried to unban %u users.'),
|
||||
'invalid' => _('Invalid username and/or password.'),
|
||||
'insecure' => _('Login on insecure connections is disabled.'),
|
||||
'notamod' => _('You are not a mod…'),
|
||||
'invalidafter' => _('Invalid username and/or password. Your user may have been deleted or changed.'),
|
||||
'malformed' => _('Invalid/malformed cookies.'),
|
||||
'missedafield' => _('Your browser didn\'t submit an input when it should have.'),
|
||||
'required' => _('The %s field is required.'),
|
||||
'invalidfield' => _('The %s field was invalid.'),
|
||||
'boardexists' => _('There is already a %s board.'),
|
||||
'noaccess' => _('You don\'t have permission to do that.'),
|
||||
'invalidpost' => _('That post doesn\'t exist…'),
|
||||
'404' => _('Page not found.'),
|
||||
'modexists' => _('That mod <a href="?/users/%d">already exists</a>!'),
|
||||
'invalidtheme' => _('That theme doesn\'t exist!'),
|
||||
'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: ')
|
||||
];
|
||||
|
||||
/*
|
||||
* =========================
|
||||
@ -1299,8 +1308,8 @@
|
||||
|
||||
// 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.
|
||||
$config['domain'] = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') ? 'https://' : 'http://';
|
||||
$config['domain'] .= isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost';
|
||||
$config['domain'] = ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') ? 'https://' : 'http://')
|
||||
. (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,
|
||||
// enter the directory path here. Otherwise, keep it false.
|
||||
@ -1380,22 +1389,22 @@
|
||||
// Board directory, followed by a forward-slash (/).
|
||||
$config['board_path'] = '%s/';
|
||||
// Misc directories.
|
||||
$config['dir']['img'] = 'src/';
|
||||
$config['dir']['thumb'] = 'thumb/';
|
||||
$config['dir']['res'] = 'res/';
|
||||
|
||||
// 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/'.
|
||||
// $config['dir']['static'] = 'http://static.example.org/';
|
||||
|
||||
// Where to store the .html templates. This folder and the template files must exist.
|
||||
$config['dir']['template'] = getcwd() . '/templates';
|
||||
// Location of vichan "themes".
|
||||
$config['dir']['themes'] = getcwd() . '/templates/themes';
|
||||
// Same as above, but a URI (accessable by web interface).
|
||||
$config['dir']['themes_uri'] = 'templates/themes';
|
||||
// Home directory. Used by themes.
|
||||
$config['dir']['home'] = '';
|
||||
$config['dir'] = [
|
||||
'img' => 'src/',
|
||||
'thumb' => 'thumb/',
|
||||
'res' => 'res/',
|
||||
// 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/'.
|
||||
// $config['dir']['static'] = 'http://static.example.org/';
|
||||
// Where to store the .html templates. This folder and the template files must exist.
|
||||
'template' => getcwd() . '/templates',
|
||||
// Location of vichan "themes".
|
||||
'themes' => getcwd() . '/templates/themes',
|
||||
// Same as above, but a URI (accessable by web interface).
|
||||
'themes_uri' => 'templates/themes',
|
||||
// Home directory. Used by themes.
|
||||
'home' => ''
|
||||
];
|
||||
|
||||
// Location of a blank 1x1 gif file. Only used when country_flags_condensed is enabled
|
||||
// $config['image_blank'] = 'static/blank.gif';
|
||||
@ -1468,13 +1477,19 @@
|
||||
// 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
|
||||
// various ones and include one that works best for me.
|
||||
$config['generation_strategies'] = array();
|
||||
// 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.
|
||||
$config['generation_strategies'][] = 'strategy_sane';
|
||||
// Add an immediate catch-all strategy. This is the default function of imageboards: generate all pages
|
||||
// on post time.
|
||||
$config['generation_strategies'][] = 'strategy_immediate';
|
||||
$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.
|
||||
*/
|
||||
'strategy_sane',
|
||||
/*
|
||||
* 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
|
||||
// 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
|
||||
@ -1491,7 +1506,7 @@
|
||||
$config['page_404'] = '/404.html';
|
||||
|
||||
// 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).
|
||||
$config['mod']['link_delete'] = '[D]';
|
||||
$config['mod']['link_ban'] = '[B]';
|
||||
$config['mod']['link_bandelete'] = '[B&D]';
|
||||
$config['mod']['link_deletefile'] = '[F]';
|
||||
$config['mod']['link_spoilerimage'] = '[S]';
|
||||
$config['mod']['link_deletebyip'] = '[D+]';
|
||||
$config['mod']['link_deletebyip_global'] = '[D++]';
|
||||
$config['mod']['link_sticky'] = '[Sticky]';
|
||||
$config['mod']['link_desticky'] = '[-Sticky]';
|
||||
$config['mod']['link_lock'] = '[Lock]';
|
||||
$config['mod']['link_unlock'] = '[-Lock]';
|
||||
$config['mod']['link_bumplock'] = '[Sage]';
|
||||
$config['mod']['link_bumpunlock'] = '[-Sage]';
|
||||
$config['mod']['link_editpost'] = '[Edit]';
|
||||
$config['mod']['link_move'] = '[Move]';
|
||||
$config['mod']['link_cycle'] = '[Cycle]';
|
||||
$config['mod']['link_uncycle'] = '[-Cycle]';
|
||||
$config['mod'] = [
|
||||
// Limit how many bans can be removed via the ban list. Set to false (or zero) for no limit.
|
||||
'unban_limit' => false,
|
||||
// Whether or not to lock moderator sessions to IP addresses. This makes cookie theft less effective.
|
||||
'lock_ip' => true,
|
||||
// The page that is first shown when a moderator logs in. Defaults to the dashboard (?/).
|
||||
'default' => '/',
|
||||
// Do DNS lookups on IP addresses to get their hostname for the moderator IP pages (?/IP/x.x.x.x).
|
||||
'dns_lookup' => true,
|
||||
// How many recent posts, per board, to show in ?/IP/x.x.x.x.
|
||||
'ip_recentposts' => 5,
|
||||
// Number of posts to display on the reports page.
|
||||
'recent_reports' => 10,
|
||||
// Number of actions to show per page in the moderation log.
|
||||
'modlog_page' => 350,
|
||||
// Number of bans to show per page in the ban list.
|
||||
'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&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.
|
||||
$config['capcode'] = ' <span class="capcode">## %s</span>';
|
||||
@ -1550,63 +1616,6 @@
|
||||
// Enable the moving of single replies
|
||||
$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
|
||||
@ -1616,13 +1625,13 @@
|
||||
// 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
|
||||
// new ones.
|
||||
$config['mod']['groups'] = array(
|
||||
$config['mod']['groups'] = [
|
||||
10 => 'Janitor',
|
||||
20 => 'Mod',
|
||||
30 => 'Admin',
|
||||
// 98 => 'God',
|
||||
99 => 'Disabled'
|
||||
);
|
||||
];
|
||||
|
||||
// If you add stuff to the above, you'll need to call this function immediately after.
|
||||
define_groups();
|
||||
@ -1632,11 +1641,11 @@
|
||||
// define_groups();
|
||||
|
||||
// Capcode permissions.
|
||||
$config['mod']['capcode'] = array(
|
||||
// JANITOR => array('Janitor'),
|
||||
MOD => array('Mod'),
|
||||
$config['mod']['capcode'] = [
|
||||
// JANITOR => [ 'Janitor' ],
|
||||
MOD => [ 'Mod' ],
|
||||
ADMIN => true
|
||||
);
|
||||
];
|
||||
|
||||
// Example: Allow mods to post with "## Moderator" as well
|
||||
// $config['mod']['capcode'][MOD][] = 'Moderator';
|
||||
@ -1838,23 +1847,20 @@
|
||||
*/
|
||||
|
||||
// Public post search settings
|
||||
$config['search'] = array();
|
||||
|
||||
// Enable the search form
|
||||
$config['search']['enable'] = false;
|
||||
$config['search'] = [
|
||||
// Enable the search form
|
||||
'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.
|
||||
$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
|
||||
//$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.
|
||||
*/
|
||||
|
||||
$config['nntpchan'] = array();
|
||||
|
||||
// Enable NNTPChan integration
|
||||
$config['nntpchan']['enabled'] = false;
|
||||
|
||||
// NNTP server
|
||||
$config['nntpchan']['server'] = "localhost:1119";
|
||||
|
||||
// Global dispatch array. Add your boards to it to enable them. Please make
|
||||
// sure that this setting is set in a global context.
|
||||
$config['nntpchan']['dispatch'] = array(); // 'overchan.test' => 'test'
|
||||
|
||||
// Trusted peer - an IP address of your NNTPChan instance. This peer will have
|
||||
// increased capabilities, eg.: will evade spamfilter.
|
||||
$config['nntpchan']['trusted_peer'] = '127.0.0.1';
|
||||
|
||||
// Salt for message ID generation. Keep it long and secure.
|
||||
$config['nntpchan']['salt'] = 'change_me+please';
|
||||
|
||||
// A local message ID domain. Make sure to change it.
|
||||
$config['nntpchan']['domain'] = 'example.vichan.net';
|
||||
|
||||
// An NNTPChan group name.
|
||||
// Please set this setting in your board/config.php, not globally.
|
||||
$config['nntpchan']['group'] = false; // eg. 'overchan.test'
|
||||
$config['nntpchan'] = [
|
||||
// Enable NNTPChan integration
|
||||
'enabled'=> false,
|
||||
// NNTP server
|
||||
'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.
|
||||
*/
|
||||
'dispatch' => [
|
||||
// 'overchan.test' => 'test'
|
||||
],
|
||||
/*
|
||||
* Trusted peer - an IP address of your NNTPChan instance. This peer will have increased capabilities, eg.: will
|
||||
* evade spamfilter.
|
||||
*/
|
||||
'trusted_peer' => '127.0.0.1',
|
||||
// Salt for message ID generation. Keep it long and secure.
|
||||
'salt' => 'change_me+please',
|
||||
// A local message ID domain. Make sure to change it.
|
||||
'domain' => 'example.vichan.net',
|
||||
/*
|
||||
* An NNTPChan group name.
|
||||
* Please set this setting in your board/config.php, not globally.
|
||||
*/
|
||||
'group' => false, // eg. 'overchan.test'
|
||||
];
|
||||
|
||||
|
||||
|
||||
|
131
inc/context.php
131
inc/context.php
@ -1,66 +1,91 @@
|
||||
<?php
|
||||
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;
|
||||
|
||||
|
||||
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 {
|
||||
private DependencyFactory $factory;
|
||||
private ?Log $log;
|
||||
private ?HttpDriver $http;
|
||||
private array $definitions;
|
||||
|
||||
|
||||
public function __construct(DependencyFactory $factory) {
|
||||
$this->factory = $factory;
|
||||
public function __construct(array $definitions) {
|
||||
$this->definitions = $definitions;
|
||||
}
|
||||
|
||||
public function getLog(): Log {
|
||||
return $this->log ??= $this->factory->buildLogDriver();
|
||||
}
|
||||
public function get(string $name){
|
||||
if (!isset($this->definitions[$name])) {
|
||||
throw new \RuntimeException("Could not find a dependency named $name");
|
||||
}
|
||||
|
||||
public function getHttpDriver(): HttpDriver {
|
||||
return $this->http ??= $this->factory->buildHttpDriver();
|
||||
$ret = $this->definitions[$name];
|
||||
if (is_callable($ret) && !is_string($ret) && !is_array($ret)) {
|
||||
$ret = $ret($this);
|
||||
$this->definitions[$name] = $ret;
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
|
||||
function build_context(array $config): Context {
|
||||
return new Context([
|
||||
'config' => $config,
|
||||
LogDriver::class => function($c) {
|
||||
$config = $c->get('config');
|
||||
|
||||
$name = $config['log_system']['name'];
|
||||
$level = $config['debug'] ? LogDriver::DEBUG : LogDriver::NOTICE;
|
||||
$backend = $config['log_system']['type'];
|
||||
|
||||
// Check 'syslog' for backwards compatibility.
|
||||
if ((isset($config['syslog']) && $config['syslog']) || $backend === 'syslog') {
|
||||
return new SyslogLogDriver($name, $level, $this->config['log_system']['syslog_stderr']);
|
||||
} elseif ($backend === 'file') {
|
||||
return new FileLogDriver($name, $level, $this->config['log_system']['file_path']);
|
||||
} elseif ($backend === 'stderr') {
|
||||
return new StderrLogDriver($name, $level);
|
||||
} else {
|
||||
return new ErrorLogLogDriver($name, $level);
|
||||
}
|
||||
},
|
||||
HttpDriver::class => function($c) {
|
||||
$config = $c->get('config');
|
||||
return new HttpDriver($config['upload_by_url_timeout'], $config['max_filesize']);
|
||||
},
|
||||
RemoteCaptchaQuery::class => function($c) {
|
||||
$config = $c->get('config');
|
||||
$http = $c->get(HttpDriver::class);
|
||||
switch ($config['captcha']['provider']) {
|
||||
case 'recaptcha':
|
||||
return new ReCaptchaQuery($http, $config['captcha']['recaptcha']['secret']);
|
||||
case 'hcaptcha':
|
||||
return new HCaptchaQuery(
|
||||
$http,
|
||||
$config['captcha']['hcaptcha']['secret'],
|
||||
$config['captcha']['hcaptcha']['sitekey']
|
||||
);
|
||||
default:
|
||||
throw new \RuntimeException('No remote captcha service available');
|
||||
}
|
||||
},
|
||||
NativeCaptchaQuery::class => function($c) {
|
||||
$config = $c->get('config');
|
||||
if ($config['captcha']['provider'] !== 'native') {
|
||||
throw new \RuntimeException('No native captcha service available');
|
||||
}
|
||||
return new NativeCaptchaQuery(
|
||||
$c->get(HttpDriver::class),
|
||||
$config['domain'],
|
||||
$config['captcha']['native']['provider_check'],
|
||||
$config['captcha']['native']['extra']
|
||||
);
|
||||
},
|
||||
CacheDriver::class => function($c) {
|
||||
// Use the global for backwards compatibility.
|
||||
return \cache::getCache();
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
@ -85,24 +85,24 @@ function sb_api($b) { global $config, $build_pages;
|
||||
}
|
||||
|
||||
function sb_ukko() {
|
||||
rebuildTheme("ukko", "post-thread");
|
||||
Vichan\Functions\Theme\rebuild_theme("ukko", "post-thread");
|
||||
return true;
|
||||
}
|
||||
|
||||
function sb_catalog($b) {
|
||||
if (!openBoard($b)) return false;
|
||||
|
||||
rebuildTheme("catalog", "post-thread", $b);
|
||||
Vichan\Functions\Theme\rebuild_theme("catalog", "post-thread", $b);
|
||||
return true;
|
||||
}
|
||||
|
||||
function sb_recent() {
|
||||
rebuildTheme("recent", "post-thread");
|
||||
Vichan\Functions\Theme\rebuild_theme("recent", "post-thread");
|
||||
return true;
|
||||
}
|
||||
|
||||
function sb_sitemap() {
|
||||
rebuildTheme("sitemap", "all");
|
||||
Vichan\Functions\Theme\rebuild_theme("sitemap", "all");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
169
inc/display.php
169
inc/display.php
@ -9,7 +9,7 @@ if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
joaoptm78@gmail.com
|
||||
http://www.php.net/manual/en/function.filesize.php#100097
|
||||
*/
|
||||
@ -21,7 +21,7 @@ function format_bytes($size) {
|
||||
|
||||
function doBoardListPart($list, $root, &$boards) {
|
||||
global $config;
|
||||
|
||||
|
||||
$body = '';
|
||||
foreach ($list as $key => $board) {
|
||||
if (is_array($board))
|
||||
@ -34,21 +34,21 @@ function doBoardListPart($list, $root, &$boards) {
|
||||
if (isset ($boards[$board])) {
|
||||
$title = ' title="'.$boards[$board].'"';
|
||||
}
|
||||
|
||||
|
||||
$body .= ' <a href="' . $root . $board . '/' . $config['file_index'] . '"'.$title.'>' . $board . '</a> /';
|
||||
}
|
||||
}
|
||||
}
|
||||
$body = preg_replace('/\/$/', '', $body);
|
||||
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
function createBoardlist($mod=false) {
|
||||
global $config;
|
||||
|
||||
|
||||
if (!isset($config['boards'])) return array('top'=>'','bottom'=>'');
|
||||
|
||||
|
||||
$xboards = listBoards();
|
||||
$boards = array();
|
||||
foreach ($xboards as $val) {
|
||||
@ -59,12 +59,12 @@ function createBoardlist($mod=false) {
|
||||
|
||||
if ($config['boardlist_wrap_bracket'] && !preg_match('/\] $/', $body))
|
||||
$body = '[' . $body . ']';
|
||||
|
||||
|
||||
$body = trim($body);
|
||||
|
||||
// Message compact-boardlist.js faster, so that page looks less ugly during loading
|
||||
$top = "<script type='text/javascript'>if (typeof do_boardlist != 'undefined') do_boardlist();</script>";
|
||||
|
||||
|
||||
return array(
|
||||
'top' => '<div class="boardlist">' . $body . '</div>' . $top,
|
||||
'bottom' => '<div class="boardlist bottom">' . $body . '</div>'
|
||||
@ -73,12 +73,12 @@ function createBoardlist($mod=false) {
|
||||
|
||||
function error($message, $priority = true, $debug_stuff = []) {
|
||||
global $board, $mod, $config, $db_error;
|
||||
|
||||
|
||||
if ($config['syslog'] && $priority !== false) {
|
||||
// Use LOG_NOTICE instead of LOG_ERR or LOG_WARNING because most error message are not significant.
|
||||
_syslog($priority !== true ? $priority : LOG_NOTICE, $message);
|
||||
}
|
||||
|
||||
|
||||
if (defined('STDIN')) {
|
||||
// Running from CLI
|
||||
echo('Error: ' . $message . "\n");
|
||||
@ -113,7 +113,7 @@ function error($message, $priority = true, $debug_stuff = []) {
|
||||
};
|
||||
|
||||
|
||||
if ($debug_stuff)
|
||||
if ($debug_stuff)
|
||||
$debug_stuff = array_filter($debug_stuff, $debug_callback);
|
||||
|
||||
die(Element($config['file_page_template'], array(
|
||||
@ -132,7 +132,7 @@ function error($message, $priority = true, $debug_stuff = []) {
|
||||
|
||||
function loginForm($error=false, $username=false, $redirect=false) {
|
||||
global $config;
|
||||
|
||||
|
||||
die(Element($config['file_page_template'], array(
|
||||
'index' => $config['root'],
|
||||
'title' => _('Login'),
|
||||
@ -149,34 +149,34 @@ function loginForm($error=false, $username=false, $redirect=false) {
|
||||
|
||||
function pm_snippet($body, $len=null) {
|
||||
global $config;
|
||||
|
||||
|
||||
if (!isset($len))
|
||||
$len = &$config['mod']['snippet_length'];
|
||||
|
||||
|
||||
// Replace line breaks with some whitespace
|
||||
$body = preg_replace('@<br/?>@i', ' ', $body);
|
||||
|
||||
|
||||
// Strip tags
|
||||
$body = strip_tags($body);
|
||||
|
||||
|
||||
// Unescape HTML characters, to avoid splitting them in half
|
||||
$body = html_entity_decode($body, ENT_COMPAT, 'UTF-8');
|
||||
|
||||
|
||||
// calculate strlen() so we can add "..." after if needed
|
||||
$strlen = mb_strlen($body);
|
||||
|
||||
|
||||
$body = mb_substr($body, 0, $len);
|
||||
|
||||
|
||||
// Re-escape the characters.
|
||||
return '<em>' . utf8tohtml($body) . ($strlen > $len ? '…' : '') . '</em>';
|
||||
}
|
||||
|
||||
function capcode($cap) {
|
||||
global $config;
|
||||
|
||||
|
||||
if (!$cap)
|
||||
return false;
|
||||
|
||||
|
||||
$capcode = array();
|
||||
if (isset($config['custom_capcode'][$cap])) {
|
||||
if (is_array($config['custom_capcode'][$cap])) {
|
||||
@ -191,59 +191,59 @@ function capcode($cap) {
|
||||
} else {
|
||||
$capcode['cap'] = sprintf($config['capcode'], $cap);
|
||||
}
|
||||
|
||||
|
||||
return $capcode;
|
||||
}
|
||||
|
||||
function truncate($body, $url, $max_lines = false, $max_chars = false) {
|
||||
global $config;
|
||||
|
||||
|
||||
if ($max_lines === false)
|
||||
$max_lines = $config['body_truncate'];
|
||||
if ($max_chars === false)
|
||||
$max_chars = $config['body_truncate_char'];
|
||||
|
||||
|
||||
// We don't want to risk truncating in the middle of an HTML comment.
|
||||
// It's easiest just to remove them all first.
|
||||
$body = preg_replace('/<!--.*?-->/s', '', $body);
|
||||
|
||||
|
||||
$original_body = $body;
|
||||
|
||||
|
||||
$lines = substr_count($body, '<br/>');
|
||||
|
||||
|
||||
// Limit line count
|
||||
if ($lines > $max_lines) {
|
||||
if (preg_match('/(((.*?)<br\/>){' . $max_lines . '})/', $body, $m))
|
||||
$body = $m[0];
|
||||
}
|
||||
|
||||
|
||||
$body = mb_substr($body, 0, $max_chars);
|
||||
|
||||
|
||||
if ($body != $original_body) {
|
||||
// Remove any corrupt tags at the end
|
||||
$body = preg_replace('/<([\w]+)?([^>]*)?$/', '', $body);
|
||||
|
||||
|
||||
// Open tags
|
||||
if (preg_match_all('/<([\w]+)[^>]*>/', $body, $open_tags)) {
|
||||
|
||||
|
||||
$tags = array();
|
||||
for ($x=0;$x<count($open_tags[0]);$x++) {
|
||||
if (!preg_match('/\/(\s+)?>$/', $open_tags[0][$x]))
|
||||
$tags[] = $open_tags[1][$x];
|
||||
}
|
||||
|
||||
|
||||
// List successfully closed tags
|
||||
if (preg_match_all('/(<\/([\w]+))>/', $body, $closed_tags)) {
|
||||
for ($x=0;$x<count($closed_tags[0]);$x++) {
|
||||
unset($tags[array_search($closed_tags[2][$x], $tags)]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// remove broken HTML entity at the end (if existent)
|
||||
$body = preg_replace('/&[^;]+$/', '', $body);
|
||||
|
||||
|
||||
$tags_no_close_needed = array("colgroup", "dd", "dt", "li", "optgroup", "option", "p", "tbody", "td", "tfoot", "th", "thead", "tr", "br", "img");
|
||||
|
||||
|
||||
// Close any open tags
|
||||
foreach ($tags as &$tag) {
|
||||
if (!in_array($tag, $tags_no_close_needed))
|
||||
@ -253,10 +253,10 @@ function truncate($body, $url, $max_lines = false, $max_chars = false) {
|
||||
// remove broken HTML entity at the end (if existent)
|
||||
$body = preg_replace('/&[^;]*$/', '', $body);
|
||||
}
|
||||
|
||||
|
||||
$body .= '<span class="toolong">'.sprintf(_('Post too long. Click <a href="%s">here</a> to view the full text.'), $url).'</span>';
|
||||
}
|
||||
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
@ -266,21 +266,21 @@ function bidi_cleanup($data) {
|
||||
|
||||
$explicits = '\xE2\x80\xAA|\xE2\x80\xAB|\xE2\x80\xAD|\xE2\x80\xAE';
|
||||
$pdf = '\xE2\x80\xAC';
|
||||
|
||||
|
||||
preg_match_all("!$explicits!", $data, $m1, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
|
||||
preg_match_all("!$pdf!", $data, $m2, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
|
||||
|
||||
|
||||
if (count($m1) || count($m2)){
|
||||
|
||||
|
||||
$p = array();
|
||||
foreach ($m1 as $m){ $p[$m[0][1]] = 'push'; }
|
||||
foreach ($m2 as $m){ $p[$m[0][1]] = 'pop'; }
|
||||
ksort($p);
|
||||
|
||||
|
||||
$offset = 0;
|
||||
$stack = 0;
|
||||
foreach ($p as $pos => $type){
|
||||
|
||||
|
||||
if ($type == 'push'){
|
||||
$stack++;
|
||||
}else{
|
||||
@ -294,15 +294,15 @@ function bidi_cleanup($data) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# now add some pops if your stack is bigger than 0
|
||||
for ($i=0; $i<$stack; $i++){
|
||||
$data .= "\xE2\x80\xAC";
|
||||
}
|
||||
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@ -317,24 +317,24 @@ function secure_link($href) {
|
||||
|
||||
function embed_html($link) {
|
||||
global $config;
|
||||
|
||||
|
||||
foreach ($config['embedding'] as $embed) {
|
||||
if ($html = preg_replace($embed[0], $embed[1], $link)) {
|
||||
if ($html == $link)
|
||||
continue; // Nope
|
||||
|
||||
|
||||
$html = str_replace('%%tb_width%%', $config['embed_width'], $html);
|
||||
$html = str_replace('%%tb_height%%', $config['embed_height'], $html);
|
||||
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($link[0] == '<') {
|
||||
// Prior to v0.9.6-dev-8, HTML code for embedding was stored in the database instead of the link.
|
||||
return $link;
|
||||
}
|
||||
|
||||
|
||||
return 'Embedding error.';
|
||||
}
|
||||
|
||||
@ -343,7 +343,7 @@ class Post {
|
||||
global $config;
|
||||
if (!isset($root))
|
||||
$root = &$config['root'];
|
||||
|
||||
|
||||
foreach ($post as $key => $value) {
|
||||
$this->{$key} = $value;
|
||||
}
|
||||
@ -367,22 +367,22 @@ class Post {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$this->subject = utf8tohtml($this->subject);
|
||||
$this->name = utf8tohtml($this->name);
|
||||
$this->mod = $mod;
|
||||
$this->root = $root;
|
||||
|
||||
|
||||
if ($this->embed)
|
||||
$this->embed = embed_html($this->embed);
|
||||
|
||||
|
||||
$this->modifiers = extract_modifiers($this->body_nomarkup);
|
||||
|
||||
|
||||
if ($config['always_regenerate_markup']) {
|
||||
$this->body = $this->body_nomarkup;
|
||||
markup($this->body);
|
||||
}
|
||||
|
||||
|
||||
if ($this->mod)
|
||||
// Fix internal links
|
||||
// Very complicated regex
|
||||
@ -394,14 +394,25 @@ class Post {
|
||||
}
|
||||
public function link($pre = '', $page = false) {
|
||||
global $config, $board;
|
||||
|
||||
|
||||
return $this->root . $board['dir'] . $config['dir']['res'] . link_for((array)$this, $page == '50') . '#' . $pre . $this->id;
|
||||
}
|
||||
|
||||
|
||||
public function build($index=false) {
|
||||
global $board, $config;
|
||||
|
||||
return Element($config['file_post_reply'], array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index, 'mod' => $this->mod));
|
||||
|
||||
$options = [
|
||||
'config' => $config,
|
||||
'board' => $board,
|
||||
'post' => &$this,
|
||||
'index' => $index,
|
||||
'mod' => $this->mod
|
||||
];
|
||||
if ($this->mod) {
|
||||
$options['pm'] = create_pm_header();
|
||||
}
|
||||
|
||||
return Element($config['file_post_reply'], $options);
|
||||
}
|
||||
};
|
||||
|
||||
@ -410,14 +421,14 @@ class Thread {
|
||||
global $config;
|
||||
if (!isset($root))
|
||||
$root = &$config['root'];
|
||||
|
||||
|
||||
foreach ($post as $key => $value) {
|
||||
$this->{$key} = $value;
|
||||
}
|
||||
|
||||
|
||||
if (isset($this->files))
|
||||
$this->files = is_string($this->files) ? json_decode($this->files) : $this->files;
|
||||
|
||||
|
||||
$this->subject = utf8tohtml($this->subject);
|
||||
$this->name = utf8tohtml($this->name);
|
||||
$this->mod = $mod;
|
||||
@ -427,17 +438,17 @@ class Thread {
|
||||
$this->posts = array();
|
||||
$this->omitted = 0;
|
||||
$this->omitted_images = 0;
|
||||
|
||||
|
||||
if ($this->embed)
|
||||
$this->embed = embed_html($this->embed);
|
||||
|
||||
|
||||
$this->modifiers = extract_modifiers($this->body_nomarkup);
|
||||
|
||||
|
||||
if ($config['always_regenerate_markup']) {
|
||||
$this->body = $this->body_nomarkup;
|
||||
markup($this->body);
|
||||
}
|
||||
|
||||
|
||||
if ($this->mod)
|
||||
// Fix internal links
|
||||
// Very complicated regex
|
||||
@ -449,7 +460,7 @@ class Thread {
|
||||
}
|
||||
public function link($pre = '', $page = false) {
|
||||
global $config, $board;
|
||||
|
||||
|
||||
return $this->root . $board['dir'] . $config['dir']['res'] . link_for((array)$this, $page == '50') . '#' . $pre . $this->id;
|
||||
}
|
||||
public function add(Post $post) {
|
||||
@ -460,15 +471,27 @@ class Thread {
|
||||
}
|
||||
public function build($index=false, $isnoko50=false) {
|
||||
global $board, $config, $debug;
|
||||
|
||||
|
||||
$hasnoko50 = $this->postCount() >= $config['noko50_min'];
|
||||
|
||||
|
||||
event('show-thread', $this);
|
||||
|
||||
$options = [
|
||||
'config' => $config,
|
||||
'board' => $board,
|
||||
'post' => &$this,
|
||||
'index' => $index,
|
||||
'hasnoko50' => $hasnoko50,
|
||||
'isnoko50' => $isnoko50,
|
||||
'mod' => $this->mod
|
||||
];
|
||||
if ($this->mod) {
|
||||
$options['pm'] = create_pm_header();
|
||||
}
|
||||
|
||||
$file = ($index && $config['file_board']) ? $config['file_post_thread_fileboard'] : $config['file_post_thread'];
|
||||
$built = Element($file, array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index, 'hasnoko50' => $hasnoko50, 'isnoko50' => $isnoko50, 'mod' => $this->mod));
|
||||
|
||||
$built = Element($file, $options);
|
||||
|
||||
return $built;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -136,6 +136,14 @@ class Filter {
|
||||
return $post['board'] == $match;
|
||||
case 'password':
|
||||
return $post['password'] == $match;
|
||||
case 'unshorten':
|
||||
$extracted_urls = get_urls($post['body_nomarkup']);
|
||||
foreach ($extracted_urls as $url) {
|
||||
if (preg_match($match, trace_url($url))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
error('Unknown filter condition: ' . $condition);
|
||||
}
|
||||
|
@ -241,7 +241,7 @@ function loadConfig() {
|
||||
$config['version'] = $__version;
|
||||
|
||||
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'])) {
|
||||
@ -391,114 +391,6 @@ function define_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 = '%') {
|
||||
$replaces = array();
|
||||
foreach ($vars as $k => $v) {
|
||||
@ -876,11 +768,13 @@ function checkBan($board = false) {
|
||||
}
|
||||
|
||||
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) {
|
||||
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 (!isset($_POST['json_response'])) {
|
||||
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
|
||||
// now and then to keep the ban list tidy.
|
||||
if ($config['cache']['enabled'] && $last_time_purged = cache::get('purged_bans_last')) {
|
||||
if (time() - $last_time_purged < $config['purge_bans'] )
|
||||
return;
|
||||
if ($config['auto_maintenance']) {
|
||||
// I'm not sure where else to put this. It doesn't really matter where; it just needs to be called every
|
||||
// now and then to keep the ban list tidy.
|
||||
if ($config['cache']['enabled']) {
|
||||
$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) {
|
||||
@ -1398,7 +1295,14 @@ function index($page, $mod=false, $brief = false) {
|
||||
}
|
||||
|
||||
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(
|
||||
@ -1581,7 +1485,7 @@ function checkMute() {
|
||||
|
||||
if ($config['cache']['enabled']) {
|
||||
// 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()));
|
||||
}
|
||||
}
|
||||
@ -1600,8 +1504,8 @@ function checkMute() {
|
||||
|
||||
if ($mute['time'] + $mutetime > time()) {
|
||||
if ($config['cache']['enabled']) {
|
||||
cache::set("mute_${_SERVER['REMOTE_ADDR']}", $mute, $mute['time'] + $mutetime - time());
|
||||
cache::set("mutetime_${_SERVER['REMOTE_ADDR']}", $mutetime, $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());
|
||||
}
|
||||
// Not expired yet
|
||||
error(sprintf($config['error']['youaremuted'], $mute['time'] + $mutetime - time()));
|
||||
@ -1612,34 +1516,10 @@ function checkMute() {
|
||||
}
|
||||
}
|
||||
|
||||
function _create_antibot($board, $thread) {
|
||||
global $config, $purged_old_antispam;
|
||||
|
||||
$antibot = new AntiBot(array($board, $thread));
|
||||
|
||||
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 purge_old_antispam() {
|
||||
$query = prepare('DELETE FROM ``antispam`` WHERE `expires` < UNIX_TIMESTAMP()');
|
||||
$query->execute() or error(db_error());
|
||||
return $query->rowCount();
|
||||
}
|
||||
|
||||
function checkSpam(array $extra_salt = array()) {
|
||||
@ -1704,15 +1584,18 @@ function incrementSpamHash($hash) {
|
||||
}
|
||||
|
||||
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']));
|
||||
|
||||
$pages = null;
|
||||
$antibot = null;
|
||||
|
||||
if ($config['api']['enabled']) {
|
||||
$api = new Api();
|
||||
$api = new Api(
|
||||
$config['show_filename'],
|
||||
$config['hide_email'],
|
||||
$config['country_flags']
|
||||
);
|
||||
$catalog = array();
|
||||
}
|
||||
|
||||
@ -1757,21 +1640,15 @@ function buildIndex($global_api = "yes") {
|
||||
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) {
|
||||
$pages = getPages();
|
||||
}
|
||||
$content['pages'] = $pages;
|
||||
$content['pages'][$page-1]['selected'] = true;
|
||||
$content['btn'] = getPageButtons($content['pages']);
|
||||
$content['antibot'] = $antibot;
|
||||
if ($mod) {
|
||||
$content['pm'] = create_pm_header();
|
||||
}
|
||||
|
||||
file_write($filename, Element($config['file_board_index'], $content));
|
||||
}
|
||||
@ -2352,9 +2229,8 @@ function buildThread($id, $return = false, $mod = false) {
|
||||
error($config['error']['nonexistant']);
|
||||
|
||||
$hasnoko50 = $thread->postCount() >= $config['noko50_min'];
|
||||
$antibot = $mod || $return ? false : create_antibot($board['uri'], $id);
|
||||
|
||||
$body = Element($config['file_thread'], array(
|
||||
$options = [
|
||||
'board' => $board,
|
||||
'thread' => $thread,
|
||||
'body' => $thread->build(),
|
||||
@ -2363,14 +2239,22 @@ function buildThread($id, $return = false, $mod = false) {
|
||||
'mod' => $mod,
|
||||
'hasnoko50' => $hasnoko50,
|
||||
'isnoko50' => false,
|
||||
'antibot' => $antibot,
|
||||
'boardlist' => createBoardlist($mod),
|
||||
'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
|
||||
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));
|
||||
$jsonFilename = $board['dir'] . $config['dir']['res'] . $id . '.json';
|
||||
file_write($jsonFilename, $json);
|
||||
@ -2391,20 +2275,17 @@ function buildThread($id, $return = false, $mod = false) {
|
||||
} elseif ($action == 'rebuild') {
|
||||
$noko50fn = $board['dir'] . $config['dir']['res'] . link_for($thread, true);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function buildThread50($id, $return = false, $mod = false, $thread = null, $antibot = false) {
|
||||
global $board, $config, $build_pages;
|
||||
function buildThread50($id, $return = false, $mod = false, $thread = null) {
|
||||
global $board, $config;
|
||||
$id = round($id);
|
||||
|
||||
if ($antibot)
|
||||
$antibot->reset();
|
||||
|
||||
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->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'];
|
||||
|
||||
$body = Element($config['file_thread'], array(
|
||||
$options = [
|
||||
'board' => $board,
|
||||
'thread' => $thread,
|
||||
'body' => $thread->build(false, true),
|
||||
@ -2466,10 +2347,14 @@ function buildThread50($id, $return = false, $mod = false, $thread = null, $anti
|
||||
'mod' => $mod,
|
||||
'hasnoko50' => $hasnoko50,
|
||||
'isnoko50' => true,
|
||||
'antibot' => $mod ? false : ($antibot ? $antibot : create_antibot($board['uri'], $id)),
|
||||
'boardlist' => createBoardlist($mod),
|
||||
'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) {
|
||||
return $body;
|
||||
@ -2656,65 +2541,6 @@ function shell_exec_error($command, $suppress_stdout = false) {
|
||||
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) {
|
||||
global $config;
|
||||
|
||||
@ -2820,7 +2646,6 @@ function purify_html($s) {
|
||||
function markdown($s) {
|
||||
$pd = new Parsedown();
|
||||
$pd->setMarkupEscaped(true);
|
||||
$pd->setimagesEnabled(false);
|
||||
|
||||
return $pd->text($s);
|
||||
}
|
||||
@ -3023,3 +2848,34 @@ function check_thread_limit($post) {
|
||||
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
114
inc/functions/dice.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
namespace Vichan\Functions\Dice;
|
||||
|
||||
function _get_or_default_int(array $arr, int $index, int $default) {
|
||||
return (isset($arr[$index]) && is_numeric($arr[$index])) ? (int)$arr[$index] : $default;
|
||||
}
|
||||
|
||||
|
||||
/* Die rolling:
|
||||
* If "dice XdY+/-Z" is in the email field (where X or +/-Z may be
|
||||
* missing), X Y-sided dice are rolled and summed, with the modifier Z
|
||||
* added on. The result is displayed at the top of the post.
|
||||
*/
|
||||
function email_dice_roll($post) {
|
||||
global $config;
|
||||
if(strpos(strtolower($post->email), 'dice%20') === 0) {
|
||||
$dicestr = str_split(substr($post->email, strlen('dice%20')));
|
||||
|
||||
// Get params
|
||||
$diceX = '';
|
||||
$diceY = '';
|
||||
$diceZ = '';
|
||||
|
||||
$curd = 'diceX';
|
||||
for($i = 0; $i < count($dicestr); $i ++) {
|
||||
if(is_numeric($dicestr[$i])) {
|
||||
$$curd .= $dicestr[$i];
|
||||
} else if($dicestr[$i] == 'd') {
|
||||
$curd = 'diceY';
|
||||
} else if($dicestr[$i] == '-' || $dicestr[$i] == '+') {
|
||||
$curd = 'diceZ';
|
||||
$$curd = $dicestr[$i];
|
||||
}
|
||||
}
|
||||
|
||||
// Default values for X and Z
|
||||
if($diceX == '') {
|
||||
$diceX = '1';
|
||||
}
|
||||
|
||||
if($diceZ == '') {
|
||||
$diceZ = '+0';
|
||||
}
|
||||
|
||||
// Intify them
|
||||
$diceX = intval($diceX);
|
||||
$diceY = intval($diceY);
|
||||
$diceZ = intval($diceZ);
|
||||
|
||||
// Continue only if we have valid values
|
||||
if($diceX > 0 && $diceY > 0) {
|
||||
$dicerolls = array();
|
||||
$dicesum = $diceZ;
|
||||
for($i = 0; $i < $diceX; $i++) {
|
||||
$roll = rand(1, $diceY);
|
||||
$dicerolls[] = $roll;
|
||||
$dicesum += $roll;
|
||||
}
|
||||
|
||||
// Prepend the result to the post body
|
||||
$modifier = ($diceZ != 0) ? ((($diceZ < 0) ? ' - ' : ' + ') . abs($diceZ)) : '';
|
||||
$dicesum = ($diceX > 1) ? ' = ' . $dicesum : '';
|
||||
$post->body = '<table class="diceroll"><tr><td><img src="'.$config['dir']['static'].'d10.svg" alt="Dice roll" width="24"></td><td>Rolled ' . implode(', ', $dicerolls) . $modifier . $dicesum . '</td></tr></table><br/>' . $post->body;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rolls a dice and generates the appropriate html from the markup.
|
||||
* @param array $matches The array of the matches according to the default configuration.
|
||||
* 1 -> The number of dices to roll.
|
||||
* 3 -> The number faces of the dices.
|
||||
* 4 -> The offset to apply to the dice.
|
||||
* @param string $img_path Path to the image to use relative to the root. Null if none.
|
||||
* @return string The html to replace the original markup with.
|
||||
*/
|
||||
function inline_dice_roll_markup(array $matches, ?string $img_path): string {
|
||||
global $config;
|
||||
|
||||
$dice_count = _get_or_default_int($matches, 1, 1);
|
||||
$dice_faces = _get_or_default_int($matches, 3, 6);
|
||||
$dice_offset = _get_or_default_int($matches, 4, 0);
|
||||
|
||||
// Clamp between 1 and max_roll_count.
|
||||
$dice_count = max(min($dice_count, $config['max_roll_count']), 1);
|
||||
// Must be at least 2.
|
||||
if ($dice_faces < 2) {
|
||||
$dice_faces = 6;
|
||||
}
|
||||
|
||||
$tot = 0;
|
||||
for ($i = 0; $i < $dice_count; $i++) {
|
||||
$tot += mt_rand(1, $dice_faces);
|
||||
}
|
||||
// Ensure that final result is at least an integer.
|
||||
$tot = abs((int)($dice_offset + $tot));
|
||||
|
||||
|
||||
if ($img_path !== null) {
|
||||
$img_text = "<img src='{$config['root']}{$img_path}' alt='dice' title='dice' class=\"inline-dice\"/>";
|
||||
} else {
|
||||
$img_text = '';
|
||||
}
|
||||
|
||||
if ($dice_offset === 0) {
|
||||
$dice_offset_text = '';
|
||||
} elseif ($dice_offset > 0) {
|
||||
$dice_offset_text = "+{$dice_offset}";
|
||||
} else {
|
||||
$dice_offset_text = (string)$dice_offset;
|
||||
}
|
||||
|
||||
return "<span>$img_text {$dice_count}d{$dice_faces}{$dice_offset_text} = <b>$tot</b></span>";
|
||||
}
|
98
inc/functions/theme.php
Normal file
98
inc/functions/theme.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
namespace Vichan\Functions\Theme;
|
||||
|
||||
|
||||
function rebuild_themes(string $action, $boardname = false): void {
|
||||
global $config, $board, $current_locale;
|
||||
|
||||
// Save the global variables
|
||||
$_config = $config;
|
||||
$_board = $board;
|
||||
|
||||
// List themes
|
||||
if ($themes = \Cache::get("themes")) {
|
||||
// OK, we already have themes loaded
|
||||
} else {
|
||||
$query = query("SELECT `theme` FROM ``theme_settings`` WHERE `name` IS NULL AND `value` IS NULL") or error(db_error());
|
||||
$themes = $query->fetchAll(\PDO::FETCH_NUM);
|
||||
|
||||
\Cache::set("themes", $themes);
|
||||
}
|
||||
|
||||
foreach ($themes as $theme) {
|
||||
// Restore them
|
||||
$config = $_config;
|
||||
$board = $_board;
|
||||
|
||||
// Reload the locale
|
||||
if ($config['locale'] != $current_locale) {
|
||||
$current_locale = $config['locale'];
|
||||
init_locale($config['locale']);
|
||||
}
|
||||
|
||||
if (PHP_SAPI === 'cli') {
|
||||
echo "Rebuilding theme ".$theme[0]."... ";
|
||||
}
|
||||
|
||||
rebuild_theme($theme[0], $action, $boardname);
|
||||
|
||||
if (PHP_SAPI === 'cli') {
|
||||
echo "done\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Restore them again
|
||||
$config = $_config;
|
||||
$board = $_board;
|
||||
|
||||
// Reload the locale
|
||||
if ($config['locale'] != $current_locale) {
|
||||
$current_locale = $config['locale'];
|
||||
init_locale($config['locale']);
|
||||
}
|
||||
}
|
||||
|
||||
function load_theme_config($_theme) {
|
||||
global $config;
|
||||
|
||||
if (!file_exists($config['dir']['themes'] . '/' . $_theme . '/info.php')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load theme information into $theme
|
||||
include $config['dir']['themes'] . '/' . $_theme . '/info.php';
|
||||
|
||||
return $theme;
|
||||
}
|
||||
|
||||
function rebuild_theme($theme, string $action, $board = false) {
|
||||
global $config, $_theme;
|
||||
$_theme = $theme;
|
||||
|
||||
$theme = load_theme_config($_theme);
|
||||
|
||||
if (file_exists($config['dir']['themes'] . '/' . $_theme . '/theme.php')) {
|
||||
require_once $config['dir']['themes'] . '/' . $_theme . '/theme.php';
|
||||
|
||||
$theme['build_function']($action, theme_settings($_theme), $board);
|
||||
}
|
||||
}
|
||||
|
||||
function theme_settings($theme): array {
|
||||
if ($settings = \Cache::get("theme_settings_" . $theme)) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
$query = prepare("SELECT `name`, `value` FROM ``theme_settings`` WHERE `theme` = :theme AND `name` IS NOT NULL");
|
||||
$query->bindValue(':theme', $theme);
|
||||
$query->execute() or error(db_error($query));
|
||||
|
||||
$settings = [];
|
||||
while ($s = $query->fetch(\PDO::FETCH_ASSOC)) {
|
||||
$settings[$s['name']] = $s['value'];
|
||||
}
|
||||
|
||||
\Cache::set("theme_settings_".$theme, $settings);
|
||||
|
||||
return $settings;
|
||||
}
|
124
inc/lock.php
124
inc/lock.php
@ -1,84 +1,76 @@
|
||||
<?php
|
||||
|
||||
class Locks {
|
||||
private static function filesystem(string $key): Lock|false {
|
||||
$key = str_replace('/', '::', $key);
|
||||
$key = str_replace("\0", '', $key);
|
||||
private static function filesystem(string $key) {
|
||||
$key = str_replace('/', '::', $key);
|
||||
$key = str_replace("\0", '', $key);
|
||||
|
||||
$fd = fopen("tmp/locks/$key", "w");
|
||||
if ($fd === false) {
|
||||
return false;
|
||||
}
|
||||
$fd = fopen("tmp/locks/$key", "w");
|
||||
if ($fd === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new class($fd) implements Lock {
|
||||
// Resources have no type in php.
|
||||
private mixed $f;
|
||||
return new class($fd) implements Lock {
|
||||
// Resources have no type in PHP.
|
||||
private $f;
|
||||
|
||||
public function __construct($fd) {
|
||||
$this->f = $fd;
|
||||
}
|
||||
|
||||
function __construct($fd) {
|
||||
$this->f = $fd;
|
||||
}
|
||||
public function get(bool $nonblock = false) {
|
||||
$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 {
|
||||
$wouldblock = false;
|
||||
flock($this->f, LOCK_SH | ($nonblock ? LOCK_NB : 0), $wouldblock);
|
||||
if ($nonblock && $wouldblock) {
|
||||
return false;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
public function get_ex(bool $nonblock = false) {
|
||||
$wouldblock = false;
|
||||
flock($this->f, LOCK_EX | ($nonblock ? LOCK_NB : 0), $wouldblock);
|
||||
if ($nonblock && $wouldblock) {
|
||||
return false;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function get_ex(bool $nonblock = false): Lock|false {
|
||||
$wouldblock = false;
|
||||
flock($this->f, LOCK_EX | ($nonblock ? LOCK_NB : 0), $wouldblock);
|
||||
if ($nonblock && $wouldblock) {
|
||||
return false;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
public function free() {
|
||||
flock($this->f, LOCK_UN);
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public function free(): Lock {
|
||||
flock($this->f, LOCK_UN);
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
}
|
||||
public static function none() {
|
||||
return new class() implements Lock {
|
||||
public function get(bool $nonblock = false) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op. Can be used for mocking.
|
||||
*/
|
||||
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) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function get_ex(bool $nonblock = false): Lock|false {
|
||||
return $this;
|
||||
}
|
||||
public function free() {
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public function free(): Lock {
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static function get_lock(array $config, string $key): Lock|false {
|
||||
if ($config['lock']['enabled'] == 'fs') {
|
||||
return self::filesystem($key);
|
||||
} else {
|
||||
return self::none();
|
||||
}
|
||||
}
|
||||
public static function get_lock(array $config, string $key) {
|
||||
if ($config['lock']['enabled'] == 'fs') {
|
||||
return self::filesystem($key);
|
||||
} else {
|
||||
return self::none();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Lock {
|
||||
// Get a shared lock
|
||||
public function get(bool $nonblock = false): Lock|false;
|
||||
public function get(bool $nonblock = false);
|
||||
|
||||
// Get an exclusive lock
|
||||
public function get_ex(bool $nonblock = false): Lock|false;
|
||||
public function get_ex(bool $nonblock = false);
|
||||
|
||||
// Free a lock
|
||||
public function free(): Lock;
|
||||
public function free();
|
||||
}
|
||||
|
@ -4,12 +4,13 @@
|
||||
* Copyright (c) 2010-2013 Tinyboard Development Group
|
||||
*/
|
||||
|
||||
use Vichan\Context;
|
||||
use Vichan\Functions\Net;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
// 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;
|
||||
|
||||
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;
|
||||
|
||||
$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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function check_login(bool $prompt = false): void {
|
||||
function check_login(Context $ctx, bool $prompt = false): void {
|
||||
global $config, $mod;
|
||||
|
||||
$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) {
|
||||
// Malformed cookies
|
||||
destroyCookies();
|
||||
if ($prompt) mod_login();
|
||||
if ($prompt) {
|
||||
mod_login($ctx);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
@ -259,7 +262,9 @@ function check_login(bool $prompt = false): void {
|
||||
if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) {
|
||||
// Malformed cookies
|
||||
destroyCookies();
|
||||
if ($prompt) mod_login();
|
||||
if ($prompt) {
|
||||
mod_login($ctx);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
|
@ -64,6 +64,10 @@ function config_vars() {
|
||||
$var['comment'][] = $temp_comment;
|
||||
$temp_comment = false;
|
||||
}
|
||||
|
||||
if (preg_match('!^\s*\$config\[(\'log_system\'|\'captcha\')\]!', $line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('!^\s*// ([^$].*)$!', $line, $matches)) {
|
||||
if ($var['default'] !== false) {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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 ($config['queue']['enabled'] == 'fs') {
|
||||
$lock = Locks::get_lock($config, $name);
|
||||
|
@ -1,100 +1,141 @@
|
||||
<?php // Verify captchas server side.
|
||||
namespace Vichan\Service;
|
||||
|
||||
use Vichan\Driver\HttpDriver;
|
||||
use Vichan\Data\Driver\HttpDriver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
class RemoteCaptchaQuery {
|
||||
class ReCaptchaQuery implements RemoteCaptchaQuery {
|
||||
private HttpDriver $http;
|
||||
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 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 {
|
||||
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) {
|
||||
public function __construct(HttpDriver $http, string $secret) {
|
||||
$this->http = $http;
|
||||
$this->secret = $secret;
|
||||
$this->endpoint = $endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user at the remote ip passed the captcha.
|
||||
*
|
||||
* @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
|
||||
);
|
||||
public function responseField(): string {
|
||||
return 'g-recaptcha-response';
|
||||
}
|
||||
|
||||
$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);
|
||||
|
||||
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 {
|
||||
private HttpDriver $http;
|
||||
private string $domain;
|
||||
private string $provider_check;
|
||||
|
||||
private string $extra;
|
||||
|
||||
/**
|
||||
* @param HttpDriver $http The http client.
|
||||
* @param string $domain The server's domain.
|
||||
* @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->domain = $domain;
|
||||
$this->provider_check = $provider_check;
|
||||
$this->extra = $extra;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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_cookie Remote user cookie.
|
||||
* @return bool Returns true if the user passed the check.
|
||||
* @throws RuntimeException Throws on IO errors.
|
||||
*/
|
||||
public function verify(string $extra, string $user_text, string $user_cookie): bool {
|
||||
$data = array(
|
||||
public function verify(string $user_text, string $user_cookie): bool {
|
||||
$data = [
|
||||
'mode' => 'check',
|
||||
'text' => $user_text,
|
||||
'extra' => $extra,
|
||||
'extra' => $this->extra,
|
||||
'cookie' => $user_cookie
|
||||
);
|
||||
];
|
||||
|
||||
$ret = $this->http->requestGet($this->domain . '/' . $this->provider_check, $data);
|
||||
return $ret === '1';
|
||||
|
@ -34,10 +34,6 @@ function Element($templateFile, array $options) {
|
||||
if (!$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']) {
|
||||
$_debug = $debug;
|
||||
|
||||
@ -167,8 +163,12 @@ function twig_push_filter($array, $value) {
|
||||
}
|
||||
|
||||
function twig_date_filter($date, $format) {
|
||||
$date = new DateTime($date, new DateTimeZone('UTC'));
|
||||
return $date->format($format);
|
||||
if (is_numeric($date)) {
|
||||
$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) {
|
||||
|
13
install.php
13
install.php
@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
// Installation/upgrade file
|
||||
define('VERSION', '5.2.0');
|
||||
define('VERSION', '5.2.1');
|
||||
require 'inc/bootstrap.php';
|
||||
loadConfig();
|
||||
|
||||
@ -689,10 +689,8 @@ if ($step == 0) {
|
||||
|
||||
echo Element('page.html', $page);
|
||||
} 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.
|
||||
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
||||
$httpsvalue = true;
|
||||
}
|
||||
// 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.
|
||||
$httpsvalue = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
|
||||
$page['title'] = 'Pre-installation test';
|
||||
|
||||
$can_exec = true;
|
||||
@ -873,8 +871,8 @@ if ($step == 0) {
|
||||
),
|
||||
array(
|
||||
'category' => 'Misc',
|
||||
'name' => 'HTTPS not being used',
|
||||
'result' => $httpsvalue = true,
|
||||
'name' => 'HTTPS being used',
|
||||
'result' => $httpsvalue,
|
||||
'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.'
|
||||
),
|
||||
@ -923,6 +921,7 @@ if ($step == 0) {
|
||||
$sg = new SaltGen();
|
||||
$config['cookies']['salt'] = $sg->generate();
|
||||
$config['secure_trip_salt'] = $sg->generate();
|
||||
$config['secure_password_salt'] = $sg->generate();
|
||||
|
||||
echo Element('page.html', array(
|
||||
'body' => Element('installer/config.html', array(
|
||||
|
@ -294,7 +294,8 @@ CREATE TABLE IF NOT EXISTS `ban_appeals` (
|
||||
`message` text NOT NULL,
|
||||
`denied` tinyint(1) NOT NULL,
|
||||
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 ;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
@ -30,17 +30,7 @@ function catalog() {
|
||||
var link = document.createElement('a');
|
||||
link.href = catalog_url;
|
||||
|
||||
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 {
|
||||
if (!pages) {
|
||||
link.textContent = '['+_('Catalog')+']';
|
||||
link.style.paddingLeft = '10px';
|
||||
link.style.textDecoration = "underline";
|
||||
|
@ -2,27 +2,27 @@
|
||||
* catalog-search.js
|
||||
* - Search and filters threads when on catalog view
|
||||
* - Optional shortcuts 's' and 'esc' to open and close the search.
|
||||
*
|
||||
*
|
||||
* Usage:
|
||||
* $config['additional_javascript'][] = 'js/jquery.min.js';
|
||||
* $config['additional_javascript'][] = 'js/comment-toolbar.js';
|
||||
*/
|
||||
if (active_page == 'catalog') {
|
||||
onready(function () {
|
||||
onReady(function() {
|
||||
'use strict';
|
||||
|
||||
// 'true' = enable shortcuts
|
||||
var useKeybinds = true;
|
||||
// 'true' = enable shortcuts
|
||||
let useKeybinds = true;
|
||||
|
||||
// trigger the search 400ms after last keystroke
|
||||
var delay = 400;
|
||||
var timeoutHandle;
|
||||
// trigger the search 400ms after last keystroke
|
||||
let delay = 400;
|
||||
let timeoutHandle;
|
||||
|
||||
//search and hide none matching threads
|
||||
// search and hide none matching threads
|
||||
function filter(search_term) {
|
||||
$('.replies').each(function () {
|
||||
var subject = $(this).children('.intro').text().toLowerCase();
|
||||
var comment = $(this).clone().children().remove(':lt(2)').end().text().trim().toLowerCase();
|
||||
let subject = $(this).children('.intro').text().toLowerCase();
|
||||
let comment = $(this).clone().children().remove(':lt(2)').end().text().trim().toLowerCase();
|
||||
search_term = search_term.toLowerCase();
|
||||
|
||||
if (subject.indexOf(search_term) == -1 && comment.indexOf(search_term) == -1) {
|
||||
@ -34,7 +34,7 @@ if (active_page == 'catalog') {
|
||||
}
|
||||
|
||||
function searchToggle() {
|
||||
var button = $('#catalog_search_button');
|
||||
let button = $('#catalog_search_button');
|
||||
|
||||
if (!button.data('expanded')) {
|
||||
button.data('expanded', '1');
|
||||
@ -59,18 +59,18 @@ if (active_page == 'catalog') {
|
||||
});
|
||||
|
||||
if (useKeybinds) {
|
||||
// 's'
|
||||
// 's'
|
||||
$('body').on('keydown', function (e) {
|
||||
if (e.which === 83 && e.target.tagName === 'BODY' && !(e.ctrlKey || e.altKey || e.shiftKey)) {
|
||||
e.preventDefault();
|
||||
if ($('#search_field').length !== 0) {
|
||||
if ($('#search_field').length !== 0) {
|
||||
$('#search_field').focus();
|
||||
} else {
|
||||
searchToggle();
|
||||
}
|
||||
}
|
||||
});
|
||||
// 'esc'
|
||||
// 'esc'
|
||||
$('.catalog_search').on('keydown', 'input#search_field', function (e) {
|
||||
if (e.which === 27 && !(e.ctrlKey || e.altKey || e.shiftKey)) {
|
||||
window.clearTimeout(timeoutHandle);
|
||||
|
@ -15,16 +15,16 @@
|
||||
*
|
||||
*/
|
||||
|
||||
onready(function(){
|
||||
var do_original_filename = function() {
|
||||
var filename, truncated;
|
||||
onReady(function() {
|
||||
let doOriginalFilename = function() {
|
||||
let filename, truncated;
|
||||
if ($(this).attr('title')) {
|
||||
filename = $(this).attr('title');
|
||||
truncated = true;
|
||||
} else {
|
||||
filename = $(this).text();
|
||||
}
|
||||
|
||||
|
||||
$(this).replaceWith(
|
||||
$('<a></a>')
|
||||
.attr('download', filename)
|
||||
@ -34,9 +34,9 @@ onready(function(){
|
||||
);
|
||||
};
|
||||
|
||||
$('.postfilename').each(do_original_filename);
|
||||
$('.postfilename').each(doOriginalFilename);
|
||||
|
||||
$(document).on('new_post', function(e, post) {
|
||||
$(post).find('.postfilename').each(do_original_filename);
|
||||
$(document).on('new_post', function(e, post) {
|
||||
$(post).find('.postfilename').each(doOriginalFilename);
|
||||
});
|
||||
});
|
||||
|
@ -16,37 +16,42 @@
|
||||
*
|
||||
*/
|
||||
|
||||
if (active_page == 'ukko' || active_page == 'thread' || active_page == 'index')
|
||||
onready(function(){
|
||||
$('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')
|
||||
.text(_('Expand all images'))
|
||||
.click(function() {
|
||||
$('a img.post-image').each(function() {
|
||||
// Don't expand YouTube embeds
|
||||
if ($(this).parent().parent().hasClass('video-container'))
|
||||
return;
|
||||
if (active_page == 'ukko' || active_page == 'thread' || active_page == 'index') {
|
||||
onReady(function() {
|
||||
$('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')
|
||||
.text(_('Expand all images'))
|
||||
.click(function() {
|
||||
$('a img.post-image').each(function() {
|
||||
// Don't expand YouTube embeds
|
||||
if ($(this).parent().parent().hasClass('video-container')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// or WEBM
|
||||
if (/^\/player\.php\?/.test($(this).parent().attr('href')))
|
||||
return;
|
||||
// or WEBM
|
||||
if (/^\/player\.php\?/.test($(this).parent().attr('href'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$(this).parent().data('expanded'))
|
||||
$(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 (!$(this).parent().data('expanded')) {
|
||||
$(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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
53
js/expand-filename.js
Normal file
53
js/expand-filename.js
Normal 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
|
||||
});
|
||||
});
|
@ -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)
|
||||
});
|
||||
});
|
@ -2,243 +2,283 @@
|
||||
/* Note: This code expects the global variable configRoot to be set. */
|
||||
|
||||
if (typeof _ == 'undefined') {
|
||||
var _ = function(a) { return a; };
|
||||
var _ = function(a) {
|
||||
return a;
|
||||
};
|
||||
}
|
||||
|
||||
function setupVideo(thumb, url) {
|
||||
if (thumb.videoAlreadySetUp) return;
|
||||
thumb.videoAlreadySetUp = true;
|
||||
if (thumb.videoAlreadySetUp) {
|
||||
return;
|
||||
}
|
||||
thumb.videoAlreadySetUp = true;
|
||||
|
||||
var video = null;
|
||||
var videoContainer, videoHide;
|
||||
var expanded = false;
|
||||
var hovering = false;
|
||||
var loop = true;
|
||||
var loopControls = [document.createElement("span"), document.createElement("span")];
|
||||
var fileInfo = thumb.parentNode.querySelector(".fileinfo");
|
||||
var mouseDown = false;
|
||||
let video = null;
|
||||
let videoContainer, videoHide;
|
||||
let expanded = false;
|
||||
let hovering = false;
|
||||
let loop = true;
|
||||
let loopControls = [document.createElement("span"), document.createElement("span")];
|
||||
let fileInfo = thumb.parentNode.querySelector(".fileinfo");
|
||||
let mouseDown = false;
|
||||
|
||||
function unexpand() {
|
||||
if (expanded) {
|
||||
expanded = false;
|
||||
if (video.pause) video.pause();
|
||||
videoContainer.style.display = "none";
|
||||
thumb.style.display = "inline";
|
||||
video.style.maxWidth = "inherit";
|
||||
video.style.maxHeight = "inherit";
|
||||
}
|
||||
}
|
||||
function unexpand() {
|
||||
if (expanded) {
|
||||
expanded = false;
|
||||
if (video.pause) {
|
||||
video.pause();
|
||||
}
|
||||
videoContainer.style.display = "none";
|
||||
thumb.style.display = "inline";
|
||||
video.style.maxWidth = "inherit";
|
||||
video.style.maxHeight = "inherit";
|
||||
}
|
||||
}
|
||||
|
||||
function unhover() {
|
||||
if (hovering) {
|
||||
hovering = false;
|
||||
if (video.pause) video.pause();
|
||||
videoContainer.style.display = "none";
|
||||
video.style.maxWidth = "inherit";
|
||||
video.style.maxHeight = "inherit";
|
||||
}
|
||||
}
|
||||
function unhover() {
|
||||
if (hovering) {
|
||||
hovering = false;
|
||||
if (video.pause) {
|
||||
video.pause();
|
||||
}
|
||||
videoContainer.style.display = "none";
|
||||
video.style.maxWidth = "inherit";
|
||||
video.style.maxHeight = "inherit";
|
||||
}
|
||||
}
|
||||
|
||||
// Create video element if does not exist yet
|
||||
function getVideo() {
|
||||
if (video == null) {
|
||||
video = document.createElement("video");
|
||||
video.src = url;
|
||||
video.loop = loop;
|
||||
video.innerText = _("Your browser does not support HTML5 video.");
|
||||
// Create video element if does not exist yet
|
||||
function getVideo() {
|
||||
if (video == null) {
|
||||
video = document.createElement("video");
|
||||
video.src = url;
|
||||
video.loop = loop;
|
||||
video.innerText = _("Your browser does not support HTML5 video.");
|
||||
|
||||
videoHide = document.createElement("img");
|
||||
videoHide.src = configRoot + "static/collapse.gif";
|
||||
videoHide.alt = "[ - ]";
|
||||
videoHide.title = "Collapse video";
|
||||
videoHide.style.marginLeft = "-15px";
|
||||
videoHide.style.cssFloat = "left";
|
||||
videoHide.addEventListener("click", unexpand, false);
|
||||
videoHide = document.createElement("img");
|
||||
videoHide.src = configRoot + "static/collapse.gif";
|
||||
videoHide.alt = "[ - ]";
|
||||
videoHide.title = "Collapse video";
|
||||
videoHide.style.marginLeft = "-15px";
|
||||
videoHide.style.cssFloat = "left";
|
||||
videoHide.addEventListener("click", unexpand, false);
|
||||
|
||||
videoContainer = document.createElement("div");
|
||||
videoContainer.style.paddingLeft = "15px";
|
||||
videoContainer.style.display = "none";
|
||||
videoContainer.appendChild(videoHide);
|
||||
videoContainer.appendChild(video);
|
||||
thumb.parentNode.insertBefore(videoContainer, thumb.nextSibling);
|
||||
videoContainer = document.createElement("div");
|
||||
videoContainer.style.paddingLeft = "15px";
|
||||
videoContainer.style.display = "none";
|
||||
videoContainer.appendChild(videoHide);
|
||||
videoContainer.appendChild(video);
|
||||
thumb.parentNode.insertBefore(videoContainer, thumb.nextSibling);
|
||||
|
||||
// Dragging to the left collapses the video
|
||||
video.addEventListener("mousedown", function(e) {
|
||||
if (e.button == 0) mouseDown = true;
|
||||
}, false);
|
||||
video.addEventListener("mouseup", function(e) {
|
||||
if (e.button == 0) mouseDown = false;
|
||||
}, false);
|
||||
video.addEventListener("mouseenter", function(e) {
|
||||
mouseDown = false;
|
||||
}, false);
|
||||
video.addEventListener("mouseout", function(e) {
|
||||
if (mouseDown && e.clientX - video.getBoundingClientRect().left <= 0) {
|
||||
unexpand();
|
||||
}
|
||||
mouseDown = false;
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
// Dragging to the left collapses the video
|
||||
video.addEventListener("mousedown", function(e) {
|
||||
if (e.button == 0) mouseDown = true;
|
||||
}, false);
|
||||
video.addEventListener("mouseup", function(e) {
|
||||
if (e.button == 0) mouseDown = false;
|
||||
}, false);
|
||||
video.addEventListener("mouseenter", function(e) {
|
||||
mouseDown = false;
|
||||
}, false);
|
||||
video.addEventListener("mouseout", function(e) {
|
||||
if (mouseDown && e.clientX - video.getBoundingClientRect().left <= 0) {
|
||||
unexpand();
|
||||
}
|
||||
mouseDown = false;
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Clicking on thumbnail expands video
|
||||
thumb.addEventListener("click", function(e) {
|
||||
if (setting("videoexpand") && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
||||
getVideo();
|
||||
expanded = true;
|
||||
hovering = false;
|
||||
function setVolume() {
|
||||
const volume = setting("videovolume");
|
||||
video.volume = volume;
|
||||
video.muted = (volume === 0);
|
||||
}
|
||||
|
||||
video.style.position = "static";
|
||||
video.style.pointerEvents = "inherit";
|
||||
video.style.display = "inline";
|
||||
videoHide.style.display = "inline";
|
||||
videoContainer.style.display = "block";
|
||||
videoContainer.style.position = "static";
|
||||
video.parentNode.parentNode.removeAttribute('style');
|
||||
thumb.style.display = "none";
|
||||
// Clicking on thumbnail expands video
|
||||
thumb.addEventListener("click", function(e) {
|
||||
if (setting("videoexpand") && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
||||
getVideo();
|
||||
expanded = true;
|
||||
hovering = false;
|
||||
|
||||
video.muted = (setting("videovolume") == 0);
|
||||
video.volume = setting("videovolume");
|
||||
video.controls = true;
|
||||
if (video.readyState == 0) {
|
||||
video.addEventListener("loadedmetadata", expand2, false);
|
||||
} else {
|
||||
setTimeout(expand2, 0);
|
||||
}
|
||||
video.play();
|
||||
e.preventDefault();
|
||||
}
|
||||
}, false);
|
||||
video.style.position = "static";
|
||||
video.style.pointerEvents = "inherit";
|
||||
video.style.display = "inline";
|
||||
videoHide.style.display = "inline";
|
||||
videoContainer.style.display = "block";
|
||||
videoContainer.style.position = "static";
|
||||
video.parentNode.parentNode.removeAttribute('style');
|
||||
thumb.style.display = "none";
|
||||
|
||||
function expand2() {
|
||||
video.style.maxWidth = "100%";
|
||||
video.style.maxHeight = window.innerHeight + "px";
|
||||
var bottom = video.getBoundingClientRect().bottom;
|
||||
if (bottom > window.innerHeight) {
|
||||
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");
|
||||
}
|
||||
setVolume();
|
||||
video.controls = true;
|
||||
if (video.readyState == 0) {
|
||||
video.addEventListener("loadedmetadata", expand2, false);
|
||||
} else {
|
||||
setTimeout(expand2, 0);
|
||||
}
|
||||
let promise = video.play();
|
||||
if (promise !== undefined) {
|
||||
promise.then(_ => {
|
||||
}).catch(_ => {
|
||||
video.muted = true;
|
||||
video.play();
|
||||
});
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
}, false);
|
||||
|
||||
// Hovering over thumbnail displays video
|
||||
thumb.addEventListener("mouseover", function(e) {
|
||||
if (setting("videohover")) {
|
||||
getVideo();
|
||||
expanded = false;
|
||||
hovering = true;
|
||||
function expand2() {
|
||||
video.style.maxWidth = "100%";
|
||||
video.style.maxHeight = window.innerHeight + "px";
|
||||
var bottom = video.getBoundingClientRect().bottom;
|
||||
if (bottom > window.innerHeight) {
|
||||
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;
|
||||
var thumbRight = thumb.querySelector("img, video").getBoundingClientRect().right;
|
||||
var maxWidth = docRight - thumbRight - 20;
|
||||
if (maxWidth < 250) maxWidth = 250;
|
||||
// Hovering over thumbnail displays video
|
||||
thumb.addEventListener("mouseover", function(e) {
|
||||
if (setting("videohover")) {
|
||||
getVideo();
|
||||
expanded = false;
|
||||
hovering = true;
|
||||
|
||||
video.style.position = "fixed";
|
||||
video.style.right = "0px";
|
||||
video.style.top = "0px";
|
||||
var docRight = document.documentElement.getBoundingClientRect().right;
|
||||
var thumbRight = thumb.querySelector("img, video").getBoundingClientRect().right;
|
||||
video.style.maxWidth = maxWidth + "px";
|
||||
video.style.maxHeight = "100%";
|
||||
video.style.pointerEvents = "none";
|
||||
let docRight = document.documentElement.getBoundingClientRect().right;
|
||||
let thumbRight = thumb.querySelector("img, video").getBoundingClientRect().right;
|
||||
let maxWidth = docRight - thumbRight - 20;
|
||||
if (maxWidth < 250) {
|
||||
maxWidth = 250;
|
||||
}
|
||||
|
||||
video.style.display = "inline";
|
||||
videoHide.style.display = "none";
|
||||
videoContainer.style.display = "inline";
|
||||
videoContainer.style.position = "fixed";
|
||||
video.style.position = "fixed";
|
||||
video.style.right = "0px";
|
||||
video.style.top = "0px";
|
||||
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.volume = setting("videovolume");
|
||||
video.controls = false;
|
||||
video.play();
|
||||
}
|
||||
}, false);
|
||||
video.style.display = "inline";
|
||||
videoHide.style.display = "none";
|
||||
videoContainer.style.display = "inline";
|
||||
videoContainer.style.position = "fixed";
|
||||
|
||||
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("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);
|
||||
thumb.addEventListener("mouseout", unhover, false);
|
||||
|
||||
// [play once] vs [loop] controls
|
||||
function setupLoopControl(i) {
|
||||
loopControls[i].addEventListener("click", function(e) {
|
||||
loop = (i != 0);
|
||||
thumb.href = thumb.href.replace(/([\?&])loop=\d+/, "$1loop=" + i);
|
||||
if (video != null) {
|
||||
video.loop = loop;
|
||||
if (loop && video.currentTime >= video.duration) {
|
||||
video.currentTime = 0;
|
||||
}
|
||||
}
|
||||
loopControls[i].style.fontWeight = "bold";
|
||||
loopControls[1-i].style.fontWeight = "inherit";
|
||||
}, false);
|
||||
}
|
||||
// Scroll wheel on thumbnail adjusts default volume
|
||||
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);
|
||||
|
||||
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]);
|
||||
}
|
||||
// [play once] vs [loop] controls
|
||||
function setupLoopControl(i) {
|
||||
loopControls[i].addEventListener("click", function(e) {
|
||||
loop = (i != 0);
|
||||
thumb.href = thumb.href.replace(/([\?&])loop=\d+/, "$1loop=" + i);
|
||||
if (video != null) {
|
||||
video.loop = loop;
|
||||
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) {
|
||||
var thumbs = element.querySelectorAll("a.file");
|
||||
for (var i = 0; i < thumbs.length; i++) {
|
||||
if (/\.webm$|\.mp4$/.test(thumbs[i].pathname)) {
|
||||
setupVideo(thumbs[i], thumbs[i].href);
|
||||
} else {
|
||||
var m = thumbs[i].search.match(/\bv=([^&]*)/);
|
||||
if (m != null) {
|
||||
var url = decodeURIComponent(m[1]);
|
||||
if (/\.webm$|\.mp4$/.test(url)) setupVideo(thumbs[i], url);
|
||||
}
|
||||
}
|
||||
}
|
||||
let thumbs = element.querySelectorAll("a.file");
|
||||
for (let i = 0; i < thumbs.length; i++) {
|
||||
if (/\.webm$|\.mp4$/.test(thumbs[i].pathname)) {
|
||||
setupVideo(thumbs[i], thumbs[i].href);
|
||||
} else {
|
||||
let m = thumbs[i].search.match(/\bv=([^&]*)/);
|
||||
if (m != null) {
|
||||
let url = decodeURIComponent(m[1]);
|
||||
if (/\.webm$|\.mp4$/.test(url)) {
|
||||
setupVideo(thumbs[i], url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onready(function(){
|
||||
// Insert menu from settings.js
|
||||
if (typeof settingsMenu != "undefined" && typeof Options == "undefined")
|
||||
document.body.insertBefore(settingsMenu, document.getElementsByTagName("hr")[0]);
|
||||
onReady(function(){
|
||||
// Insert menu from settings.js
|
||||
if (typeof settingsMenu != "undefined" && typeof Options == "undefined") {
|
||||
document.body.insertBefore(settingsMenu, document.getElementsByTagName("hr")[0]);
|
||||
}
|
||||
|
||||
// Setup Javascript events for videos in document now
|
||||
setupVideosIn(document);
|
||||
// Setup Javascript events for videos in document now
|
||||
setupVideosIn(document);
|
||||
|
||||
// Setup Javascript events for videos added by updater
|
||||
if (window.MutationObserver) {
|
||||
var observer = new MutationObserver(function(mutations) {
|
||||
for (var i = 0; i < mutations.length; i++) {
|
||||
var additions = mutations[i].addedNodes;
|
||||
if (additions == null) continue;
|
||||
for (var j = 0; j < additions.length; j++) {
|
||||
var node = additions[j];
|
||||
if (node.nodeType == 1) {
|
||||
setupVideosIn(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, {childList: true, subtree: true});
|
||||
}
|
||||
// Setup Javascript events for videos added by updater
|
||||
if (window.MutationObserver) {
|
||||
let observer = new MutationObserver(function(mutations) {
|
||||
for (let i = 0; i < mutations.length; i++) {
|
||||
let additions = mutations[i].addedNodes;
|
||||
if (additions == null) {
|
||||
continue;
|
||||
}
|
||||
for (let j = 0; j < additions.length; j++) {
|
||||
let node = additions[j];
|
||||
if (node.nodeType == 1) {
|
||||
setupVideosIn(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, {childList: true, subtree: true});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -13,21 +13,21 @@
|
||||
*
|
||||
*/
|
||||
|
||||
onready(function(){
|
||||
var inline_expanding_filename = function() {
|
||||
$(this).find(".fileinfo > a").click(function(){
|
||||
var imagelink = $(this).parent().parent().find('a[target="_blank"]:first');
|
||||
if(imagelink.length > 0) {
|
||||
onReady(function() {
|
||||
let inlineExpandingFilename = function() {
|
||||
$(this).find(".fileinfo > a").click(function() {
|
||||
let imagelink = $(this).parent().parent().find('a[target="_blank"]:first');
|
||||
if (imagelink.length > 0) {
|
||||
imagelink.click();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$('div[id^="thread_"]').each(inline_expanding_filename);
|
||||
|
||||
// allow to work with auto-reload.js, etc.
|
||||
$(document).on('new_post', function(e, post) {
|
||||
inline_expanding_filename.call(post);
|
||||
});
|
||||
$('div[id^="thread_"]').each(inlineExpandingFilename);
|
||||
|
||||
// allow to work with auto-reload.js, etc.
|
||||
$(document).on('new_post', function(e, post) {
|
||||
inlineExpandingFilename.call(post);
|
||||
});
|
||||
});
|
||||
|
@ -17,29 +17,45 @@ $(document).ready(function(){
|
||||
'use strict';
|
||||
|
||||
var iso8601 = function(s) {
|
||||
s = s.replace(/\.\d\d\d+/,""); // remove milliseconds
|
||||
s = s.replace(/-/,"/").replace(/-/,"/");
|
||||
s = s.replace(/T/," ").replace(/Z/," UTC");
|
||||
s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
|
||||
var parts = s.split('T');
|
||||
if (parts.length === 2) {
|
||||
var timeParts = parts[1].split(':');
|
||||
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);
|
||||
};
|
||||
|
||||
var zeropad = function(num, count) {
|
||||
return [Math.pow(10, count - num.toString().length), num].join('').substr(1);
|
||||
};
|
||||
|
||||
var dateformat = (typeof strftime === 'undefined') ? function(t) {
|
||||
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
|
||||
zeropad(t.getHours(), 2) + ":" + zeropad(t.getMinutes(), 2) + ":" + zeropad(t.getSeconds(), 2);
|
||||
|
||||
} : function(t) {
|
||||
// post_date is defined in templates/main.js
|
||||
return strftime(window.post_date, t, datelocale);
|
||||
};
|
||||
|
||||
function timeDifference(current, previous) {
|
||||
|
||||
var msPerMinute = 60 * 1000;
|
||||
var msPerHour = msPerMinute * 60;
|
||||
var msPerDay = msPerHour * 24;
|
||||
@ -51,36 +67,35 @@ $(document).ready(function(){
|
||||
if (elapsed < msPerMinute) {
|
||||
return 'Just now';
|
||||
} else if (elapsed < msPerHour) {
|
||||
return Math.round(elapsed/msPerMinute) + (Math.round(elapsed/msPerMinute)<=1 ? ' minute ago':' minutes ago');
|
||||
} else if (elapsed < msPerDay ) {
|
||||
return Math.round(elapsed/msPerHour ) + (Math.round(elapsed/msPerHour)<=1 ? ' hour ago':' hours ago');
|
||||
return Math.round(elapsed / msPerMinute) + (Math.round(elapsed / msPerMinute) <= 1 ? ' minute ago' : ' minutes ago');
|
||||
} else if (elapsed < msPerDay) {
|
||||
return Math.round(elapsed / msPerHour) + (Math.round(elapsed / msPerHour) <= 1 ? ' hour ago' : ' hours ago');
|
||||
} 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) {
|
||||
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 {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
var do_localtime = function(elem) {
|
||||
var do_localtime = function(elem) {
|
||||
var times = elem.getElementsByTagName('time');
|
||||
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 postTime = new Date(t);
|
||||
var postTime = iso8601(t);
|
||||
|
||||
times[i].setAttribute('data-local', 'true');
|
||||
|
||||
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()));
|
||||
} else {
|
||||
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') {
|
||||
$('#show-relative-time>input').attr('checked','checked');
|
||||
$('#show-relative-time>input').attr('checked', 'checked');
|
||||
interval_id = setInterval(do_localtime, 30000, document);
|
||||
}
|
||||
|
||||
@ -113,3 +128,4 @@ $(document).ready(function(){
|
||||
|
||||
do_localtime(document);
|
||||
});
|
||||
|
||||
|
@ -60,8 +60,8 @@ var banlist_init = function(token, my_boards, inMod) {
|
||||
// duration?
|
||||
expires: {name: _("Expires"), width: "235px", fmt: function(f) {
|
||||
if (!f.expires || f.expires == 0) return "<em>"+_("never")+"</em>";
|
||||
return strftime(window.post_date, new Date((f.expires|0)*1000), datelocale) +
|
||||
((f.expires < time()) ? "" : " <small>"+_("in ")+until(f.expires|0)+"</small>");
|
||||
var formattedDate = strftime("%m/%d/%Y (%a) %H:%M:%S", new Date((f.expires|0)*1000), datelocale);
|
||||
return formattedDate + ((f.expires < time()) ? "" : " <small>"+_("in ")+until(f.expires|0)+"</small>");
|
||||
} },
|
||||
username: {name: _("Staff"), width: "100px", fmt: function(f) {
|
||||
var pre='',suf='',un=f.username;
|
||||
@ -129,14 +129,16 @@ var banlist_init = function(token, my_boards, inMod) {
|
||||
$(".banform").on("submit", function() { return false; });
|
||||
|
||||
$("#unban").on("click", function() {
|
||||
$(".banform .hiddens").remove();
|
||||
$("<input type='hidden' name='unban' value='unban' class='hiddens'>").appendTo(".banform");
|
||||
|
||||
$.each(selected, function(e) {
|
||||
$("<input type='hidden' name='ban_"+e+"' value='unban' class='hiddens'>").appendTo(".banform");
|
||||
});
|
||||
|
||||
$(".banform").off("submit").submit();
|
||||
if (confirm('Are you sure you want to unban the selected IPs?')) {
|
||||
$(".banform .hiddens").remove();
|
||||
$("<input type='hidden' name='unban' value='unban' class='hiddens'>").appendTo(".banform");
|
||||
|
||||
$.each(selected, function(e) {
|
||||
$("<input type='hidden' name='ban_"+e+"' value='unban' class='hiddens'>").appendTo(".banform");
|
||||
});
|
||||
|
||||
$(".banform").off("submit").submit();
|
||||
}
|
||||
});
|
||||
|
||||
if (device_type == 'desktop') {
|
||||
|
189
js/post-hover.js
189
js/post-hover.js
@ -13,59 +13,62 @@
|
||||
*
|
||||
*/
|
||||
|
||||
onready(function(){
|
||||
var dont_fetch_again = [];
|
||||
init_hover = function() {
|
||||
var $link = $(this);
|
||||
|
||||
var id;
|
||||
var matches;
|
||||
onReady(function() {
|
||||
let dontFetchAgain = [];
|
||||
initHover = function() {
|
||||
let link = $(this);
|
||||
let id;
|
||||
let matches;
|
||||
|
||||
if ($link.is('[data-thread]')) {
|
||||
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];
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
var board = $(this);
|
||||
|
||||
let board = $(this);
|
||||
while (board.data('board') === undefined) {
|
||||
board = board.parent();
|
||||
}
|
||||
var threadid;
|
||||
if ($link.is('[data-thread]')) threadid = 0;
|
||||
else threadid = board.attr('id').replace("thread_", "");
|
||||
let threadid;
|
||||
if (link.is('[data-thread]')) {
|
||||
threadid = 0;
|
||||
} else {
|
||||
threadid = board.attr('id').replace("thread_", "");
|
||||
}
|
||||
|
||||
board = board.data('board');
|
||||
|
||||
var parentboard = board;
|
||||
|
||||
if ($link.is('[data-thread]')) parentboard = $('form[name="post"] input[name="board"]').val();
|
||||
else if (matches[1] !== undefined) board = matches[1];
|
||||
let parentboard = board;
|
||||
|
||||
var $post = false;
|
||||
var hovering = false;
|
||||
var hovered_at;
|
||||
$link.hover(function(e) {
|
||||
if (link.is('[data-thread]')) {
|
||||
parentboard = $('form[name="post"] input[name="board"]').val();
|
||||
} else if (matches[1] !== undefined) {
|
||||
board = matches[1];
|
||||
}
|
||||
|
||||
let post = false;
|
||||
let hovering = false;
|
||||
let hoveredAt;
|
||||
link.hover(function(e) {
|
||||
hovering = true;
|
||||
hovered_at = {'x': e.pageX, 'y': e.pageY};
|
||||
|
||||
var start_hover = function($link) {
|
||||
if ($post.is(':visible') &&
|
||||
$post.offset().top >= $(window).scrollTop() &&
|
||||
$post.offset().top + $post.height() <= $(window).scrollTop() + $(window).height()) {
|
||||
// post is in view
|
||||
$post.addClass('highlighted');
|
||||
} else {
|
||||
var $newPost = $post.clone();
|
||||
$newPost.find('>.reply, >br').remove();
|
||||
$newPost.find('span.mentioned').remove();
|
||||
$newPost.find('a.post_anchor').remove();
|
||||
hoveredAt = {'x': e.pageX, 'y': e.pageY};
|
||||
|
||||
$newPost
|
||||
let startHover = function(link) {
|
||||
if (post.is(':visible') &&
|
||||
post.offset().top >= $(window).scrollTop() &&
|
||||
post.offset().top + post.height() <= $(window).scrollTop() + $(window).height()) {
|
||||
// post is in view
|
||||
post.addClass('highlighted');
|
||||
} else {
|
||||
let newPost = post.clone();
|
||||
newPost.find('>.reply, >br').remove();
|
||||
newPost.find('span.mentioned').remove();
|
||||
newPost.find('a.post_anchor').remove();
|
||||
|
||||
newPost
|
||||
.attr('id', 'post-hover-' + id)
|
||||
.attr('data-board', board)
|
||||
.addClass('post-hover')
|
||||
@ -76,95 +79,99 @@ onready(function(){
|
||||
.css('font-style', 'normal')
|
||||
.css('z-index', '100')
|
||||
.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);
|
||||
if($post.length > 0) {
|
||||
start_hover($(this));
|
||||
|
||||
post = $('[data-board="' + board + '"] div.post#reply_' + id + ', [data-board="' + board + '"]div#thread_' + id);
|
||||
if (post.length > 0) {
|
||||
startHover($(this));
|
||||
} else {
|
||||
var url = $link.attr('href').replace(/#.*$/, '');
|
||||
|
||||
if($.inArray(url, dont_fetch_again) != -1) {
|
||||
let url = link.attr('href').replace(/#.*$/, '');
|
||||
|
||||
if ($.inArray(url, dontFetchAgain) != -1) {
|
||||
return;
|
||||
}
|
||||
dont_fetch_again.push(url);
|
||||
|
||||
dontFetchAgain.push(url);
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
context: document.body,
|
||||
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) {
|
||||
$(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'));
|
||||
}
|
||||
});
|
||||
}
|
||||
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() {
|
||||
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'));
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$(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) {
|
||||
start_hover($link);
|
||||
if (hovering && post.length > 0) {
|
||||
startHover(link);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, function() {
|
||||
hovering = false;
|
||||
if(!$post)
|
||||
if (!post) {
|
||||
return;
|
||||
|
||||
$post.removeClass('highlighted');
|
||||
if($post.hasClass('hidden') || $post.data('cached') == 'yes')
|
||||
$post.css('display', 'none');
|
||||
}
|
||||
|
||||
post.removeClass('highlighted');
|
||||
if (post.hasClass('hidden') || post.data('cached') == 'yes') {
|
||||
post.css('display', 'none');
|
||||
}
|
||||
$('.post-hover').remove();
|
||||
}).mousemove(function(e) {
|
||||
if(!$post)
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
$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.
|
||||
$(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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -10,20 +10,20 @@
|
||||
* Usage:
|
||||
* $config['additional_javascript'][] = 'js/jquery.min.js';
|
||||
* $config['additional_javascript'][] = 'js/quote-selection.js';
|
||||
*
|
||||
*/
|
||||
|
||||
$(document).ready(function(){
|
||||
if (!window.getSelection)
|
||||
$(document).ready(function() {
|
||||
if (!window.getSelection) {
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
$.fn.selectRange = function(start, end) {
|
||||
return this.each(function() {
|
||||
if (this.setSelectionRange) {
|
||||
this.focus();
|
||||
this.setSelectionRange(start, end);
|
||||
} else if (this.createTextRange) {
|
||||
var range = this.createTextRange();
|
||||
let range = this.createTextRange();
|
||||
range.collapse(true);
|
||||
range.moveEnd('character', end);
|
||||
range.moveStart('character', start);
|
||||
@ -31,88 +31,81 @@ $(document).ready(function(){
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var altKey = false;
|
||||
var ctrlKey = false;
|
||||
var metaKey = false;
|
||||
|
||||
|
||||
let altKey = false;
|
||||
let ctrlKey = false;
|
||||
let metaKey = false;
|
||||
|
||||
$(document).keyup(function(e) {
|
||||
if (e.keyCode == 18)
|
||||
if (e.keyCode == 18) {
|
||||
altKey = false;
|
||||
else if (e.keyCode == 17)
|
||||
} else if (e.keyCode == 17) {
|
||||
ctrlKey = false;
|
||||
else if (e.keyCode == 91)
|
||||
} else if (e.keyCode == 91) {
|
||||
metaKey = false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$(document).keydown(function(e) {
|
||||
if (e.altKey)
|
||||
if (e.altKey) {
|
||||
altKey = true;
|
||||
else if (e.ctrlKey)
|
||||
} else if (e.ctrlKey) {
|
||||
ctrlKey = true;
|
||||
else if (e.metaKey)
|
||||
} else if (e.metaKey) {
|
||||
metaKey = true;
|
||||
|
||||
}
|
||||
|
||||
if (altKey || ctrlKey || metaKey) {
|
||||
// console.log('CTRL/ALT/Something used. Ignoring');
|
||||
return;
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
if (e.keyCode < 48 || e.keyCode > 90) {
|
||||
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()) {
|
||||
// console.log('Selection left post div', $(selection.focusNode).parent());
|
||||
return;
|
||||
}
|
||||
|
||||
;
|
||||
var selectedText = selection.toString();
|
||||
// console.log('Selected text: ' + selectedText.replace(/\n/g, '\\n').replace(/\r/g, '\\r'));
|
||||
|
||||
if ($('body').hasClass('debug'))
|
||||
|
||||
let selectedText = selection.toString();
|
||||
|
||||
if ($('body').hasClass('debug')) {
|
||||
alert(selectedText);
|
||||
|
||||
if (selectedText.length == 0)
|
||||
}
|
||||
|
||||
if (selectedText.length == 0) {
|
||||
return;
|
||||
|
||||
var body = $('textarea#body')[0];
|
||||
|
||||
var last_quote = body.value.match(/[\S.]*(^|[\S\s]*)>>(\d+)/);
|
||||
if (last_quote)
|
||||
}
|
||||
|
||||
let body = $('textarea#body')[0];
|
||||
|
||||
let last_quote = body.value.match(/[\S.]*(^|[\S\s]*)>>(\d+)/);
|
||||
if (last_quote) {
|
||||
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 */
|
||||
var 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');
|
||||
let quote = (last_quote != postID ? '>>' + postID + '\r\n' : '') + $.trim(selectedText).replace(/\r\n/g, '\n').replace(/^/mg, '>').replace(/\n/g, '\r\n') + '\r\n';
|
||||
|
||||
selection.removeAllRanges();
|
||||
|
||||
if (document.selection) {
|
||||
// IE
|
||||
body.focus();
|
||||
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.selectionStart || body.selectionStart == '0') {
|
||||
let start = body.selectionStart;
|
||||
let end = body.selectionEnd;
|
||||
|
||||
if (!body.value.substring(0, start).match(/(^|\n)$/)) {
|
||||
quote = '\r\n\r\n' + quote;
|
||||
}
|
||||
|
||||
body.value = body.value.substring(0, start) + quote + body.value.substring(end, body.value.length);
|
||||
|
||||
body.value = body.value.substring(0, start) + quote + body.value.substring(end, body.value.length);
|
||||
$(body).selectRange(start + quote.length, start + quote.length);
|
||||
} else {
|
||||
// ???
|
||||
@ -121,4 +114,3 @@ $(document).ready(function(){
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
*
|
||||
* Released under the MIT license
|
||||
* Copyright (c) 2012 Michael Save <savetheinternet@tinyboard.org>
|
||||
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
|
||||
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
|
||||
*
|
||||
* Usage:
|
||||
* $config['additional_javascript'][] = 'js/jquery.min.js';
|
||||
@ -13,49 +13,54 @@
|
||||
*
|
||||
*/
|
||||
|
||||
onready(function(){
|
||||
var showBackLinks = function() {
|
||||
var reply_id = $(this).attr('id').replace(/(^reply_)|(^op_)/, '');
|
||||
|
||||
onReady(function() {
|
||||
let showBackLinks = function() {
|
||||
let reply_id = $(this).attr('id').replace(/(^reply_)|(^op_)/, '');
|
||||
|
||||
$(this).find('div.body a:not([rel="nofollow"])').each(function() {
|
||||
var id, post, $mentioned;
|
||||
|
||||
if(id = $(this).text().match(/^>>(\d+)$/))
|
||||
let id, post, $mentioned;
|
||||
|
||||
if (id = $(this).text().match(/^>>(\d+)$/)) {
|
||||
id = id[1];
|
||||
else
|
||||
} else {
|
||||
return;
|
||||
|
||||
$post = $('#reply_' + id);
|
||||
if($post.length == 0){
|
||||
$post = $('#op_' + id);
|
||||
if($post.length == 0)
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$post = $('#reply_' + id);
|
||||
if ($post.length == 0){
|
||||
$post = $('#op_' + id);
|
||||
if ($post.length == 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$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'));
|
||||
|
||||
if ($mentioned.find('a.mentioned-' + reply_id).length != 0)
|
||||
}
|
||||
|
||||
if ($mentioned.find('a.mentioned-' + reply_id).length != 0) {
|
||||
return;
|
||||
|
||||
var $link = $('<a class="mentioned-' + reply_id + '" onclick="highlightReply(\'' + reply_id + '\');" href="#' + reply_id + '">>>' +
|
||||
}
|
||||
|
||||
let link = $('<a class="mentioned-' + reply_id + '" onclick="highlightReply(\'' + reply_id + '\');" href="#' + reply_id + '">>>' +
|
||||
reply_id + '</a>');
|
||||
$link.appendTo($mentioned)
|
||||
|
||||
link.appendTo($mentioned)
|
||||
|
||||
if (window.init_hover) {
|
||||
$link.each(init_hover);
|
||||
link.each(init_hover);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
$('div.post.reply').each(showBackLinks);
|
||||
$('div.post.op').each(showBackLinks);
|
||||
|
||||
$(document).on('new_post', function(e, post) {
|
||||
showBackLinks.call(post);
|
||||
$(document).on('new_post', function(e, post) {
|
||||
if ($(post).hasClass("op")) {
|
||||
$(post).find('div.post.reply').each(showBackLinks);
|
||||
} else {
|
||||
$(post).parent().find('div.post.reply').each(showBackLinks);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -4,7 +4,7 @@
|
||||
*
|
||||
* Released under the MIT license
|
||||
* Copyright (c) 2012 Michael Save <savetheinternet@tinyboard.org>
|
||||
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
|
||||
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
|
||||
*
|
||||
* Usage:
|
||||
* $config['additional_javascript'][] = 'js/mobile-style.js';
|
||||
@ -12,11 +12,11 @@
|
||||
*
|
||||
*/
|
||||
|
||||
onready(function(){
|
||||
if(device_type == 'mobile') {
|
||||
var fix_spoilers = function(where) {
|
||||
var spoilers = where.getElementsByClassName('spoiler');
|
||||
for(var i = 0; i < spoilers.length; i++) {
|
||||
onReady(function() {
|
||||
if (device_type == 'mobile') {
|
||||
let fix_spoilers = function(where) {
|
||||
let spoilers = where.getElementsByClassName('spoiler');
|
||||
for (let i = 0; i < spoilers.length; i++) {
|
||||
spoilers[i].onmousedown = function() {
|
||||
this.style.color = 'white';
|
||||
};
|
||||
@ -24,11 +24,10 @@ onready(function(){
|
||||
};
|
||||
fix_spoilers(document);
|
||||
|
||||
// allow to work with auto-reload.js, etc.
|
||||
$(document).on('new_post', function(e, post) {
|
||||
fix_spoilers(post);
|
||||
});
|
||||
|
||||
// allow to work with auto-reload.js, etc.
|
||||
$(document).on('new_post', function(e, post) {
|
||||
fix_spoilers(post);
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
*
|
||||
* Released under the MIT license
|
||||
* Copyright (c) 2013 Michael Save <savetheinternet@tinyboard.org>
|
||||
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
|
||||
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
|
||||
*
|
||||
* Usage:
|
||||
* $config['additional_javascript'][] = 'js/jquery.min.js';
|
||||
@ -14,32 +14,32 @@
|
||||
*
|
||||
*/
|
||||
|
||||
onready(function(){
|
||||
var stylesDiv = $('div.styles');
|
||||
var stylesSelect = $('<select></select>');
|
||||
|
||||
var i = 1;
|
||||
onReady(function() {
|
||||
let stylesDiv = $('div.styles');
|
||||
let stylesSelect = $('<select></select>');
|
||||
|
||||
let i = 1;
|
||||
stylesDiv.children().each(function() {
|
||||
var opt = $('<option></option>')
|
||||
let opt = $('<option></option>')
|
||||
.html(this.innerHTML.replace(/(^\[|\]$)/g, ''))
|
||||
.val(i);
|
||||
if ($(this).hasClass('selected'))
|
||||
if ($(this).hasClass('selected')) {
|
||||
opt.attr('selected', true);
|
||||
}
|
||||
stylesSelect.append(opt);
|
||||
$(this).attr('id', 'style-select-' + i);
|
||||
i++;
|
||||
});
|
||||
|
||||
|
||||
stylesSelect.change(function() {
|
||||
$('#style-select-' + $(this).val()).click();
|
||||
});
|
||||
|
||||
|
||||
stylesDiv.hide();
|
||||
|
||||
|
||||
stylesDiv.after(
|
||||
$('<div id="style-select" style="float:right;margin-bottom:10px"></div>')
|
||||
.text(_('Style: '))
|
||||
.append(stylesSelect)
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
*
|
||||
* Released under the MIT license
|
||||
* Copyright (c) 2013 Michael Save <savetheinternet@tinyboard.org>
|
||||
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
|
||||
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
|
||||
*
|
||||
* Usage:
|
||||
* $config['embedding'] = array();
|
||||
@ -22,13 +22,12 @@
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
onready(function(){
|
||||
var do_embed_yt = function(tag) {
|
||||
onReady(function() {
|
||||
let do_embed_yt = function(tag) {
|
||||
$('div.video-container a', tag).click(function() {
|
||||
var videoID = $(this.parentNode).data('video');
|
||||
|
||||
$(this.parentNode).html('<iframe style="float:left;margin: 10px 20px" type="text/html" '+
|
||||
let videoID = $(this.parentNode).data('video');
|
||||
|
||||
$(this.parentNode).html('<iframe style="float:left;margin: 10px 20px" type="text/html" ' +
|
||||
'width="360" height="270" src="//www.youtube.com/embed/' + videoID +
|
||||
'?autoplay=1&html5=1" allowfullscreen frameborder="0"/>');
|
||||
|
||||
@ -37,9 +36,8 @@ onready(function(){
|
||||
};
|
||||
do_embed_yt(document);
|
||||
|
||||
// allow to work with auto-reload.js, etc.
|
||||
$(document).on('new_post', function(e, post) {
|
||||
do_embed_yt(post);
|
||||
});
|
||||
// allow to work with auto-reload.js, etc.
|
||||
$(document).on('new_post', function(e, post) {
|
||||
do_embed_yt(post);
|
||||
});
|
||||
});
|
||||
|
||||
|
41
mod.php
41
mod.php
@ -1,21 +1,24 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (c) 2010-2014 Tinyboard Development Group
|
||||
*/
|
||||
|
||||
require_once 'inc/bootstrap.php';
|
||||
|
||||
if ($config['debug'])
|
||||
if ($config['debug']) {
|
||||
$parse_start_time = microtime(true);
|
||||
}
|
||||
|
||||
require_once 'inc/mod/pages.php';
|
||||
|
||||
check_login(true);
|
||||
|
||||
$ctx = Vichan\build_context($config);
|
||||
|
||||
check_login($ctx, true);
|
||||
|
||||
$query = isset($_SERVER['QUERY_STRING']) ? rawurldecode($_SERVER['QUERY_STRING']) : '';
|
||||
|
||||
$pages = array(
|
||||
$pages = [
|
||||
'' => ':?/', // redirect to dashboard
|
||||
'/' => 'dashboard', // dashboard
|
||||
'/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',
|
||||
|
||||
'/(\%b)/' . preg_quote($config['dir']['res'], '!') .
|
||||
str_replace(array('%d','%s'), array('(\d+)', '[a-z0-9-]+'), preg_quote($config['file_page50_slug'], '!')) => 'view_thread50',
|
||||
str_replace([ '%d','%s' ], [ '(\d+)', '[a-z0-9-]+' ], preg_quote($config['file_page50_slug'], '!')) => 'view_thread50',
|
||||
'/(\%b)/' . preg_quote($config['dir']['res'], '!') .
|
||||
str_replace(array('%d','%s'), array('(\d+)', '[a-z0-9-]+'), preg_quote($config['file_page_slug'], '!')) => 'view_thread',
|
||||
);
|
||||
str_replace([ '%d','%s' ], [ '(\d+)', '[a-z0-9-]+' ], preg_quote($config['file_page_slug'], '!')) => 'view_thread',
|
||||
];
|
||||
|
||||
|
||||
if (!$mod) {
|
||||
$pages = array('!^(.+)?$!' => 'login');
|
||||
$pages = [ '!^(.+)?$!' => 'login' ];
|
||||
} elseif (isset($_GET['status'], $_GET['r'])) {
|
||||
header('Location: ' . $_GET['r'], true, (int)$_GET['status']);
|
||||
exit;
|
||||
@ -126,10 +129,11 @@ if (isset($config['mod']['custom_pages'])) {
|
||||
$pages = array_merge($pages, $config['mod']['custom_pages']);
|
||||
}
|
||||
|
||||
$new_pages = array();
|
||||
$new_pages = [];
|
||||
foreach ($pages as $key => $callback) {
|
||||
if (is_string($callback) && preg_match('/^secure /', $callback))
|
||||
if (is_string($callback) && preg_match('/^secure /', $callback)) {
|
||||
$key .= '(/(?P<token>[a-f0-9]{8}))?';
|
||||
}
|
||||
$key = str_replace('\%b', '?P<board>' . sprintf(substr($config['board_path'], 0, -1), $config['board_regex']), $key);
|
||||
$new_pages[(!empty($key) and $key[0] == '!') ? $key : '!^' . $key . '(?:&[^&=]+=[^&]*)*$!u'] = $callback;
|
||||
}
|
||||
@ -137,7 +141,7 @@ $pages = $new_pages;
|
||||
|
||||
foreach ($pages as $uri => $handler) {
|
||||
if (preg_match($uri, $query, $matches)) {
|
||||
$matches = array_slice($matches, 1);
|
||||
$matches[0] = $ctx; // Replace the text captured by the full pattern with a reference to the context.
|
||||
|
||||
if (isset($matches['board'])) {
|
||||
$board_match = $matches['board'];
|
||||
@ -157,7 +161,7 @@ foreach ($pages as $uri => $handler) {
|
||||
if ($secure_post_only)
|
||||
error($config['error']['csrf']);
|
||||
else {
|
||||
mod_confirm(substr($query, 1));
|
||||
mod_confirm($ctx, substr($query, 1));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@ -172,24 +176,20 @@ foreach ($pages as $uri => $handler) {
|
||||
}
|
||||
|
||||
if ($config['debug']) {
|
||||
$debug['mod_page'] = array(
|
||||
$debug['mod_page'] = [
|
||||
'req' => $query,
|
||||
'match' => $uri,
|
||||
'handler' => $handler,
|
||||
);
|
||||
];
|
||||
$debug['time']['parse_mod_req'] = '~' . round((microtime(true) - $parse_start_time) * 1000, 2) . 'ms';
|
||||
}
|
||||
|
||||
if (is_array($matches)) {
|
||||
// we don't want to call named parameters (PHP 8)
|
||||
$matches = array_values($matches);
|
||||
}
|
||||
// We don't want to call named parameters (PHP 8).
|
||||
$matches = array_values($matches);
|
||||
|
||||
if (is_string($handler)) {
|
||||
if ($handler[0] == ':') {
|
||||
header('Location: ' . substr($handler, 1), true, $config['redirect_http']);
|
||||
} elseif (is_callable("mod_page_$handler")) {
|
||||
call_user_func_array("mod_page_$handler", $matches);
|
||||
} elseif (is_callable("mod_$handler")) {
|
||||
call_user_func_array("mod_$handler", $matches);
|
||||
} else {
|
||||
@ -206,4 +206,3 @@ foreach ($pages as $uri => $handler) {
|
||||
}
|
||||
|
||||
error($config['error']['404']);
|
||||
|
||||
|
168
post.php
168
post.php
@ -6,7 +6,7 @@
|
||||
require_once 'inc/bootstrap.php';
|
||||
|
||||
use Vichan\{Context, WebDependencyFactory};
|
||||
use Vichan\Driver\{HttpDriver, Log};
|
||||
use Vichan\Data\Driver\{LogDriver, HttpDriver};
|
||||
use Vichan\Service\{RemoteCaptchaQuery, NativeCaptchaQuery};
|
||||
use Vichan\Functions\Format;
|
||||
|
||||
@ -167,12 +167,47 @@ function strip_image_metadata(string $img_path): int {
|
||||
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
|
||||
*/
|
||||
|
||||
$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.
|
||||
if (isset($_GET['Newsgroups']) && $config['nntpchan']['enabled']) {
|
||||
@ -251,7 +286,7 @@ if (isset($_GET['Newsgroups']) && $config['nntpchan']['enabled']) {
|
||||
$content = file_get_contents("php://input");
|
||||
}
|
||||
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 = '';
|
||||
|
||||
@ -347,10 +382,11 @@ if (isset($_POST['delete'])) {
|
||||
if (!isset($_POST['board'], $_POST['password']))
|
||||
error($config['error']['bot']);
|
||||
|
||||
$password = &$_POST['password'];
|
||||
|
||||
if ($password == '')
|
||||
if (empty($_POST['password'])){
|
||||
error($config['error']['invalidpassword']);
|
||||
}
|
||||
|
||||
$password = hashPassword($_POST['password']);
|
||||
|
||||
$delete = array();
|
||||
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'])));
|
||||
}
|
||||
|
||||
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']);
|
||||
}
|
||||
|
||||
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'])));
|
||||
}
|
||||
|
||||
@ -416,8 +454,8 @@ if (isset($_POST['delete'])) {
|
||||
modLog("User at $ip deleted their own post #$id");
|
||||
}
|
||||
|
||||
$context->getLog()->log(
|
||||
Log::INFO,
|
||||
$context->get(LogDriver::class)->log(
|
||||
LogDriver::INFO,
|
||||
'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'))
|
||||
@fastcgi_finish_request();
|
||||
|
||||
rebuildThemes('post-delete', $board['uri']);
|
||||
Vichan\Functions\Theme\rebuild_themes('post-delete', $board['uri']);
|
||||
|
||||
} elseif (isset($_POST['report'])) {
|
||||
if (!isset($_POST['board'], $_POST['reason']))
|
||||
@ -479,12 +517,12 @@ if (isset($_POST['delete'])) {
|
||||
|
||||
try {
|
||||
$query = new NativeCaptchaQuery(
|
||||
$context->getHttpDriver(),
|
||||
$context->get(HttpDriver::class),
|
||||
$config['domain'],
|
||||
$config['captcha']['provider_check']
|
||||
$config['captcha']['provider_check'],
|
||||
$config['captcha']['extra']
|
||||
);
|
||||
$success = $query->verify(
|
||||
$config['captcha']['extra'],
|
||||
$_POST['captcha_text'],
|
||||
$_POST['captcha_cookie']
|
||||
);
|
||||
@ -493,7 +531,7 @@ if (isset($_POST['delete'])) {
|
||||
error($config['error']['captcha']);
|
||||
}
|
||||
} 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']);
|
||||
}
|
||||
}
|
||||
@ -512,7 +550,7 @@ if (isset($_POST['delete'])) {
|
||||
|
||||
$post = $query->fetch(PDO::FETCH_ASSOC);
|
||||
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']);
|
||||
}
|
||||
|
||||
@ -521,8 +559,8 @@ if (isset($_POST['delete'])) {
|
||||
error($error);
|
||||
}
|
||||
|
||||
$context->getLog()->log(
|
||||
Log::INFO,
|
||||
$context->get(LogDriver::class)->log(
|
||||
LogDriver::INFO,
|
||||
'Reported post: /'
|
||||
. $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '')
|
||||
. " 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.
|
||||
try {
|
||||
$provider = $config['captcha']['provider'];
|
||||
$new_thread_capt = $config['captcha']['native']['new_thread_capt'];
|
||||
$dynamic = $config['captcha']['dynamic'];
|
||||
|
||||
// With our custom captcha provider
|
||||
if ($config['captcha']['enabled'] || ($post['op'] && $config['new_thread_capt'])) {
|
||||
$query = new NativeCaptchaQuery($context->getHttpDriver(), $config['domain'], $config['captcha']['provider_check']);
|
||||
$success = $query->verify($config['captcha']['extra'], $_POST['captcha_text'], $_POST['captcha_cookie']);
|
||||
if (($provider === 'native' && !$new_thread_capt)
|
||||
|| ($provider === 'native' && $new_thread_capt && $post['op'])) {
|
||||
$query = $context->get(NativeCaptchaQuery::class);
|
||||
$success = $query->verify($_POST['captcha_text'], $_POST['captcha_cookie']);
|
||||
|
||||
if (!$success) {
|
||||
error(
|
||||
@ -610,36 +653,30 @@ if (isset($_POST['delete'])) {
|
||||
}
|
||||
}
|
||||
// Remote 3rd party captchas.
|
||||
else {
|
||||
// recaptcha
|
||||
if ($config['recaptcha']) {
|
||||
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']);
|
||||
}
|
||||
elseif ($provider && (!$dynamic || $dynamic === $_SERVER['REMOTE_ADDR'])) {
|
||||
$query = $content->get(RemoteCaptchaQuery::class);
|
||||
$field = $query->responseField();
|
||||
|
||||
if (isset($query, $response)) {
|
||||
$success = $query->verify($response, $_SERVER['REMOTE_ADDR']);
|
||||
if (!$success) {
|
||||
error($config['error']['captcha']);
|
||||
}
|
||||
if (!isset($_POST[$field])) {
|
||||
error($config['error']['bot']);
|
||||
}
|
||||
$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) {
|
||||
$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']);
|
||||
} 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']);
|
||||
}
|
||||
|
||||
@ -657,7 +694,7 @@ if (isset($_POST['delete'])) {
|
||||
|
||||
|
||||
if ($post['mod'] = isset($_POST['mod']) && $_POST['mod']) {
|
||||
check_login(false);
|
||||
check_login($context, false);
|
||||
if (!$mod) {
|
||||
// Liar. You're not a mod.
|
||||
error($config['error']['notamod']);
|
||||
@ -675,12 +712,6 @@ if (isset($_POST['delete'])) {
|
||||
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']) {
|
||||
checkMute();
|
||||
}
|
||||
@ -747,7 +778,7 @@ if (isset($_POST['delete'])) {
|
||||
|
||||
try {
|
||||
$ret = download_file_from_url(
|
||||
$context->getHttpDriver(),
|
||||
$context->get(HttpDriver::class),
|
||||
$_POST['file_url'],
|
||||
$config['upload_by_url_timeout'],
|
||||
$allowed_extensions,
|
||||
@ -770,7 +801,7 @@ if (isset($_POST['delete'])) {
|
||||
$post['subject'] = $_POST['subject'];
|
||||
$post['email'] = str_replace(' ', '%20', htmlspecialchars($_POST['email']));
|
||||
$post['body'] = $_POST['body'];
|
||||
$post['password'] = $_POST['password'];
|
||||
$post['password'] = hashPassword($_POST['password']);
|
||||
$post['has_file'] = (!isset($post['embed']) && (($post['op'] && !isset($post['no_longer_require_an_image_for_op']) && $config['force_image_op']) || count($_FILES) > 0));
|
||||
|
||||
if (!$dropped_post) {
|
||||
@ -850,7 +881,7 @@ if (isset($_POST['delete'])) {
|
||||
|
||||
$trip = generate_tripcode($post['name']);
|
||||
$post['name'] = $trip[0];
|
||||
if ($config['disable_tripcodes'] = true && !$mod) {
|
||||
if ($config['disable_tripcodes'] && !$mod) {
|
||||
$post['trip'] = '';
|
||||
}
|
||||
else {
|
||||
@ -928,8 +959,6 @@ if (isset($_POST['delete'])) {
|
||||
error($config['error']['toolong_body']);
|
||||
if (!$mod && substr_count($post['body'], "\n") >= $config['maximum_lines'])
|
||||
error($config['error']['toomanylines']);
|
||||
if (mb_strlen($post['password']) > 20)
|
||||
error(sprintf($config['error']['toolong'], 'password'));
|
||||
}
|
||||
wordfilters($post['body']);
|
||||
|
||||
@ -1056,9 +1085,6 @@ if (isset($_POST['delete'])) {
|
||||
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;
|
||||
|
||||
if ($file_image_has_operable_metadata && $config['convert_auto_orient']) {
|
||||
@ -1130,7 +1156,7 @@ if (isset($_POST['delete'])) {
|
||||
try {
|
||||
$file['size'] = strip_image_metadata($file['tmp_name']);
|
||||
} 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.
|
||||
error(_('Could not strip EXIF metadata!'), null, $error);
|
||||
}
|
||||
@ -1165,10 +1191,10 @@ if (isset($_POST['delete'])) {
|
||||
if ($txt !== '') {
|
||||
// This one has an effect, that the body is appended to a post body. So you can write a correct
|
||||
// spamfilter.
|
||||
$post['body_nomarkup'] .= "<tinyboard ocr image $key>" . htmlspecialchars($value) . "</tinyboard>";
|
||||
$post['body_nomarkup'] .= "<tinyboard ocr image $key>" . htmlspecialchars($txt) . "</tinyboard>";
|
||||
}
|
||||
} 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
|
||||
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))
|
||||
$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));
|
||||
delete_cyclical_posts($board['uri'], $post['thread'], $config['cycle_limit']);
|
||||
}
|
||||
|
||||
if (isset($post['antispam_hash'])) {
|
||||
@ -1366,8 +1388,8 @@ if (isset($_POST['delete'])) {
|
||||
|
||||
buildThread($post['op'] ? $id : $post['thread']);
|
||||
|
||||
$context->getLog()->log(
|
||||
Log::INFO,
|
||||
$context->get(LogDriver::class)->log(
|
||||
LogDriver::INFO,
|
||||
'New post: /' . $board['dir'] . $config['dir']['res'] . link_for($post) . (!$post['op'] ? '#' . $id : '')
|
||||
);
|
||||
|
||||
@ -1400,9 +1422,9 @@ if (isset($_POST['delete'])) {
|
||||
@fastcgi_finish_request();
|
||||
|
||||
if ($post['op'])
|
||||
rebuildThemes('post-thread', $board['uri']);
|
||||
Vichan\Functions\Theme\rebuild_themes('post-thread', $board['uri']);
|
||||
else
|
||||
rebuildThemes('post', $board['uri']);
|
||||
Vichan\Functions\Theme\rebuild_themes('post', $board['uri']);
|
||||
|
||||
} elseif (isset($_POST['appeal'])) {
|
||||
if (!isset($_POST['ban_id']))
|
||||
@ -1410,7 +1432,7 @@ if (isset($_POST['delete'])) {
|
||||
|
||||
$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)) {
|
||||
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 |
@ -917,10 +917,6 @@ pre {
|
||||
.poster_id {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.poster_id:hover {
|
||||
@ -1046,6 +1042,20 @@ div.boardlist a {
|
||||
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 {
|
||||
width: 50px;
|
||||
}
|
||||
@ -1196,8 +1206,7 @@ table.fileboard .intro a {
|
||||
|
||||
#gallery_images img {
|
||||
opacity: 0.6;
|
||||
-webkit-transition: all 0.5s;
|
||||
transition: all 0.5s;
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
#gallery_images img:hover, #gallery_images img.active {
|
||||
@ -1205,9 +1214,7 @@ table.fileboard .intro a {
|
||||
}
|
||||
|
||||
#gallery_images img.active {
|
||||
-webkit-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);
|
||||
box-shadow: 0px 0px 29px 2px rgba(255,255,255,1);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@ -1227,12 +1234,35 @@ div.mix {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ban-reason-table .warning-reason-table tr td:first-child {
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
.ban-reason-table {
|
||||
margin: 10px auto;
|
||||
border-collapse: collapse;
|
||||
width: auto;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
.ban-reason-table .warning-reason-table tr:hover td {
|
||||
cursor: pointer;
|
||||
background-color: rgba(100%,100%,100%,0.2);
|
||||
.ban-reason-table th,
|
||||
.ban-reason-table td {
|
||||
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;
|
||||
}
|
||||
|
121
stylesheets/uboachan-gray.css
Normal file
121
stylesheets/uboachan-gray.css
Normal 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;
|
||||
}
|
@ -2,6 +2,6 @@
|
||||
<p class="unimportant" style="margin-top:20px;text-align:center;">- Tinyboard +
|
||||
<a href="https://github.com/vichan-devel/vichan">vichan</a> {{ config.version }} -
|
||||
<br>Tinyboard Copyright © 2010-2014 Tinyboard Development Group
|
||||
<br><a href="https://github.com/vichan-devel/vichan">vichan</a> Copyright © 2012-2024 vichan-devel</p>
|
||||
<br><a href="https://github.com/vichan-devel/vichan">vichan</a> Copyright © 2012-2025 vichan-devel</p>
|
||||
{% for footer in config.footer %}<p class="unimportant" style="text-align:center;">{{ footer }}</p>{% endfor %}
|
||||
</footer>
|
||||
|
@ -1,55 +1,55 @@
|
||||
<link rel="stylesheet" media="screen" href="{{ config.url_stylesheet }}">
|
||||
{% 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 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.default_stylesheet.1 != '' %}<link rel="stylesheet" type="text/css" id="stylesheet" href="{{ config.uri_stylesheets }}{{ config.default_stylesheet.1 }}">{% endif %}
|
||||
{% if config.font_awesome %}<link rel="stylesheet" href="{{ config.root }}{{ config.font_awesome_css }}">{% endif %}
|
||||
{% if config.country_flags_condensed %}<link rel="stylesheet" href="{{ config.root }}{{ config.country_flags_condensed_css }}">{% endif %}
|
||||
<script type="text/javascript">
|
||||
var configRoot="{{ config.root }}";
|
||||
var inMod = {% if mod %}true{% else %}false{% endif %};
|
||||
var modRoot="{{ config.root }}"+(inMod ? "mod.php?/" : "");
|
||||
</script>
|
||||
{% if not nojavascript %}
|
||||
<script type="text/javascript" src="{{ config.url_javascript }}"></script>
|
||||
{% if not config.additional_javascript_compile %}
|
||||
{% for javascript in config.additional_javascript %}<script type="text/javascript" src="{{ config.additional_javascript_url }}{{ javascript }}"></script>{% endfor %}
|
||||
{% endif %}
|
||||
{% if mod %}
|
||||
<script type="text/javascript" src="/js/mod/mod_snippets.js"></script>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if config.recaptcha %}<script src="//www.recaptcha.net/recaptcha/api.js"></script>
|
||||
<style type="text/css">{% verbatim %}
|
||||
#recaptcha_area {
|
||||
float: none !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
#recaptcha_logo, #recaptcha_privacy {
|
||||
display: none;
|
||||
}
|
||||
#recaptcha_table {
|
||||
border: none !important;
|
||||
}
|
||||
#recaptcha_table tr:first-child {
|
||||
height: auto;
|
||||
}
|
||||
.recaptchatable img {
|
||||
float: none !important;
|
||||
}
|
||||
#recaptcha_response_field {
|
||||
font-size: 10pt !important;
|
||||
border: 1px solid #a9a9a9 !important;
|
||||
padding: 1px !important;
|
||||
}
|
||||
td.recaptcha_image_cell {
|
||||
background: transparent !important;
|
||||
}
|
||||
.recaptchatable, #recaptcha_area tr, #recaptcha_area td, #recaptcha_area th {
|
||||
padding: 0 !important;
|
||||
}
|
||||
{% endverbatim %}</style>{% endif %}
|
||||
{% if config.hcaptcha %}
|
||||
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
|
||||
{% endif %}
|
||||
<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 %}
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<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.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 }}?v={{ config.resource_version }}">{% 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">
|
||||
var configRoot="{{ config.root }}";
|
||||
var inMod = {% if mod %} true {% else %} false {% endif %};
|
||||
var modRoot = "{{ config.root }}" + (inMod ? "mod.php?/" : "");
|
||||
</script>
|
||||
{% if not nojavascript %}
|
||||
<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 %}
|
||||
{% for javascript in config.additional_javascript %}<script type="text/javascript" src="{{ config.additional_javascript_url }}{{ javascript }}?v={{ config.resource_version }}"></script>{% endfor %}
|
||||
{% endif %}
|
||||
{% if mod %}
|
||||
<script type="text/javascript" src="/js/mod/mod_snippets.js?v={{ config.resource_version }}"></script>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if config.captcha.provider == 'recaptcha' %}<script src="//www.recaptcha.net/recaptcha/api.js"></script>
|
||||
<style type="text/css">{% verbatim %}
|
||||
#recaptcha_area {
|
||||
float: none !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
#recaptcha_logo, #recaptcha_privacy {
|
||||
display: none;
|
||||
}
|
||||
#recaptcha_table {
|
||||
border: none !important;
|
||||
}
|
||||
#recaptcha_table tr:first-child {
|
||||
height: auto;
|
||||
}
|
||||
.recaptchatable img {
|
||||
float: none !important;
|
||||
}
|
||||
#recaptcha_response_field {
|
||||
font-size: 10pt !important;
|
||||
border: 1px solid #a9a9a9 !important;
|
||||
padding: 1px !important;
|
||||
}
|
||||
td.recaptcha_image_cell {
|
||||
background: transparent !important;
|
||||
}
|
||||
.recaptchatable, #recaptcha_area tr, #recaptcha_area td, #recaptcha_area th {
|
||||
padding: 0 !important;
|
||||
}
|
||||
{% endverbatim %}</style>{% endif %}
|
||||
{% if config.captcha.provider.hcaptcha %}
|
||||
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
|
||||
{% endif %}
|
||||
|
@ -88,6 +88,9 @@
|
||||
<label for="secure_trip_salt">Secure trip (##) salt:</label>
|
||||
<input type="text" id="secure_trip_salt" name="secure_trip_salt" value="{{ config.secure_trip_salt }}" size="40">
|
||||
|
||||
<label for="secure_password_salt">Poster password salt:</label>
|
||||
<input type="text" id="secure_password_salt" name="secure_password_salt" value="{{ config.secure_password_salt }}" size="40">
|
||||
|
||||
<label for="more">Additional configuration:</label>
|
||||
<textarea id="more" name="more">{{ more }}</textarea>
|
||||
</fieldset>
|
||||
|
@ -18,92 +18,92 @@ function _(s) {
|
||||
* > alert(fmt(_("{0} users"), [3]));
|
||||
* 3 uzytkownikow
|
||||
*/
|
||||
function fmt(s,a) {
|
||||
function fmt(s, a) {
|
||||
return s.replace(/\{([0-9]+)\}/g, function(x) { return a[x[1]]; });
|
||||
}
|
||||
|
||||
function until($timestamp) {
|
||||
var $difference = $timestamp - Date.now()/1000|0, $num;
|
||||
switch(true){
|
||||
case ($difference < 60):
|
||||
return "" + $difference + ' ' + _('second(s)');
|
||||
case ($difference < 3600): //60*60 = 3600
|
||||
return "" + ($num = Math.round($difference/(60))) + ' ' + _('minute(s)');
|
||||
case ($difference < 86400): //60*60*24 = 86400
|
||||
return "" + ($num = Math.round($difference/(3600))) + ' ' + _('hour(s)');
|
||||
case ($difference < 604800): //60*60*24*7 = 604800
|
||||
return "" + ($num = Math.round($difference/(86400))) + ' ' + _('day(s)');
|
||||
case ($difference < 31536000): //60*60*24*365 = 31536000
|
||||
return "" + ($num = Math.round($difference/(604800))) + ' ' + _('week(s)');
|
||||
default:
|
||||
return "" + ($num = Math.round($difference/(31536000))) + ' ' + _('year(s)');
|
||||
}
|
||||
function until(timestamp) {
|
||||
let difference = timestamp - Date.now() / 1000 | 0;
|
||||
switch (true) {
|
||||
case (difference < 60):
|
||||
return "" + difference + ' ' + _('second(s)');
|
||||
case (difference < 3600): // 60 * 60 = 3600
|
||||
return "" + Math.round(difference / 60) + ' ' + _('minute(s)');
|
||||
case (difference < 86400): // 60 * 60 * 24 = 86400
|
||||
return "" + Math.round(difference / 3600) + ' ' + _('hour(s)');
|
||||
case (difference < 604800): // 60 * 60 * 24 * 7 = 604800
|
||||
return "" + Math.round(difference / 86400) + ' ' + _('day(s)');
|
||||
case (difference < 31536000): // 60 * 60 * 24 * 365 = 31536000
|
||||
return "" + Math.round(difference / 604800) + ' ' + _('week(s)');
|
||||
default:
|
||||
return "" + Math.round(difference / 31536000) + ' ' + _('year(s)');
|
||||
}
|
||||
}
|
||||
|
||||
function ago($timestamp) {
|
||||
var $difference = (Date.now()/1000|0) - $timestamp, $num;
|
||||
switch(true){
|
||||
case ($difference < 60) :
|
||||
return "" + $difference + ' ' + _('second(s)');
|
||||
case ($difference < 3600): //60*60 = 3600
|
||||
return "" + ($num = Math.round($difference/(60))) + ' ' + _('minute(s)');
|
||||
case ($difference < 86400): //60*60*24 = 86400
|
||||
return "" + ($num = Math.round($difference/(3600))) + ' ' + _('hour(s)');
|
||||
case ($difference < 604800): //60*60*24*7 = 604800
|
||||
return "" + ($num = Math.round($difference/(86400))) + ' ' + _('day(s)');
|
||||
case ($difference < 31536000): //60*60*24*365 = 31536000
|
||||
return "" + ($num = Math.round($difference/(604800))) + ' ' + _('week(s)');
|
||||
default:
|
||||
return "" + ($num = Math.round($difference/(31536000))) + ' ' + _('year(s)');
|
||||
}
|
||||
function ago(timestamp) {
|
||||
let difference = (Date.now() / 1000 | 0) - timestamp;
|
||||
switch (true) {
|
||||
case (difference < 60):
|
||||
return "" + difference + ' ' + _('second(s)');
|
||||
case (difference < 3600): /// 60 * 60 = 3600
|
||||
return "" + Math.round(difference/(60)) + ' ' + _('minute(s)');
|
||||
case (difference < 86400): // 60 * 60 * 24 = 86400
|
||||
return "" + Math.round(difference/(3600)) + ' ' + _('hour(s)');
|
||||
case (difference < 604800): // 60 * 60 * 24 * 7 = 604800
|
||||
return "" + Math.round(difference/(86400)) + ' ' + _('day(s)');
|
||||
case (difference < 31536000): // 60 * 60 * 24 * 365 = 31536000
|
||||
return "" + Math.round(difference/(604800)) + ' ' + _('week(s)');
|
||||
default:
|
||||
return "" + Math.round(difference/(31536000)) + ' ' + _('year(s)');
|
||||
}
|
||||
}
|
||||
|
||||
var datelocale =
|
||||
{ days: [_('Sunday'), _('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday')]
|
||||
, shortDays: [_("Sun"), _("Mon"), _("Tue"), _("Wed"), _("Thu"), _("Fri"), _("Sat")]
|
||||
, 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')]
|
||||
, AM: _('AM')
|
||||
, PM: _('PM')
|
||||
, am: _('am')
|
||||
, pm: _('pm')
|
||||
};
|
||||
{ days: [_('Sunday'), _('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday')]
|
||||
, shortDays: [_("Sun"), _("Mon"), _("Tue"), _("Wed"), _("Thu"), _("Fri"), _("Sat")]
|
||||
, 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')]
|
||||
, AM: _('AM')
|
||||
, PM: _('PM')
|
||||
, am: _('am')
|
||||
, pm: _('pm')
|
||||
};
|
||||
|
||||
|
||||
function alert(a, do_confirm, confirm_ok_action, confirm_cancel_action) {
|
||||
var handler, div, bg, closebtn, okbtn;
|
||||
var close = function() {
|
||||
handler.fadeOut(400, function() { handler.remove(); });
|
||||
return false;
|
||||
};
|
||||
let handler, div, bg, closebtn, okbtn;
|
||||
let close = function() {
|
||||
handler.fadeOut(400, function() { handler.remove(); });
|
||||
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);
|
||||
closebtn = $("<a id='alert_close' href='javascript:void(0)'><i class='fa fa-times'></i></div>")
|
||||
.appendTo(div);
|
||||
div = $("<div id='alert_div'></div>").appendTo(handler);
|
||||
closebtn = $("<a id='alert_close' href='javascript:void(0)'><i class='fa fa-times'></i></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) {
|
||||
confirm_ok_action = (typeof confirm_ok_action !== "function") ? function(){} : confirm_ok_action;
|
||||
confirm_cancel_action = (typeof confirm_cancel_action !== "function") ? function(){} : confirm_cancel_action;
|
||||
okbtn.click(confirm_ok_action);
|
||||
$("<button class='button alert_button'>"+_("Cancel")+"</button>").click(confirm_cancel_action).click(close).appendTo(div);
|
||||
bg.click(confirm_cancel_action);
|
||||
okbtn.click(confirm_cancel_action);
|
||||
closebtn.click(confirm_cancel_action);
|
||||
}
|
||||
if (do_confirm) {
|
||||
confirm_ok_action = (typeof confirm_ok_action !== "function") ? function(){} : confirm_ok_action;
|
||||
confirm_cancel_action = (typeof confirm_cancel_action !== "function") ? function(){} : confirm_cancel_action;
|
||||
okbtn.click(confirm_ok_action);
|
||||
$("<button class='button alert_button'>"+_("Cancel")+"</button>").click(confirm_cancel_action).click(close).appendTo(div);
|
||||
bg.click(confirm_cancel_action);
|
||||
okbtn.click(confirm_cancel_action);
|
||||
closebtn.click(confirm_cancel_action);
|
||||
}
|
||||
|
||||
bg.click(close);
|
||||
okbtn.click(close);
|
||||
closebtn.click(close);
|
||||
bg.click(close);
|
||||
okbtn.click(close);
|
||||
closebtn.click(close);
|
||||
|
||||
handler.fadeIn(400);
|
||||
handler.fadeIn(400);
|
||||
}
|
||||
|
||||
var saved = {};
|
||||
@ -131,46 +131,74 @@ function changeStyle(styleName, link) {
|
||||
localStorage.stylesheet = styleName;
|
||||
{% endif %}
|
||||
{% verbatim %}
|
||||
|
||||
|
||||
if (!document.getElementById('stylesheet')) {
|
||||
var s = document.createElement('link');
|
||||
let s = document.createElement('link');
|
||||
s.rel = 'stylesheet';
|
||||
s.type = 'text/css';
|
||||
s.id = 'stylesheet';
|
||||
var x = document.getElementsByTagName('head')[0];
|
||||
let x = document.getElementsByTagName('head')[0];
|
||||
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;
|
||||
|
||||
|
||||
if (document.getElementsByClassName('styles').length != 0) {
|
||||
var styleLinks = document.getElementsByClassName('styles')[0].childNodes;
|
||||
for (var i = 0; i < styleLinks.length; i++) {
|
||||
let styleLinks = document.getElementsByClassName('styles')[0].childNodes;
|
||||
for (let i = 0; i < styleLinks.length; i++) {
|
||||
styleLinks[i].className = '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (link) {
|
||||
link.className = 'selected';
|
||||
}
|
||||
|
||||
if (typeof $ != 'undefined')
|
||||
|
||||
if (typeof $ != 'undefined') {
|
||||
$(window).trigger('stylesheet', styleName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
{% endverbatim %}
|
||||
var resourceVersion = document.currentScript.getAttribute('data-resource-version');
|
||||
{% if config.stylesheets_board %}
|
||||
{% verbatim %}
|
||||
|
||||
|
||||
if (!localStorage.board_stylesheets) {
|
||||
localStorage.board_stylesheets = '{}';
|
||||
}
|
||||
|
||||
|
||||
var stylesheet_choices = JSON.parse(localStorage.board_stylesheets);
|
||||
if (board_name && stylesheet_choices[board_name]) {
|
||||
for (var styleName in styles) {
|
||||
for (let styleName in styles) {
|
||||
if (styleName == stylesheet_choices[board_name]) {
|
||||
changeStyle(styleName);
|
||||
break;
|
||||
@ -181,7 +209,7 @@ function changeStyle(styleName, link) {
|
||||
{% else %}
|
||||
{% verbatim %}
|
||||
if (localStorage.stylesheet) {
|
||||
for (var styleName in styles) {
|
||||
for (let styleName in styles) {
|
||||
if (styleName == localStorage.stylesheet) {
|
||||
changeStyle(styleName);
|
||||
break;
|
||||
@ -192,12 +220,12 @@ function changeStyle(styleName, link) {
|
||||
{% endif %}
|
||||
{% verbatim %}
|
||||
|
||||
function init_stylechooser() {
|
||||
var newElement = document.createElement('div');
|
||||
function initStyleChooser() {
|
||||
let newElement = document.createElement('div');
|
||||
newElement.className = 'styles';
|
||||
|
||||
|
||||
for (styleName in styles) {
|
||||
var style = document.createElement('a');
|
||||
let style = document.createElement('a');
|
||||
style.innerHTML = '[' + styleName + ']';
|
||||
style.onclick = function() {
|
||||
changeStyle(this.innerHTML.substring(1, this.innerHTML.length - 1), this);
|
||||
@ -207,51 +235,83 @@ function init_stylechooser() {
|
||||
}
|
||||
style.href = 'javascript:void(0);';
|
||||
newElement.appendChild(style);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
document.getElementsByTagName('body')[0].insertBefore(newElement, document.getElementsByTagName('body')[0].lastChild.nextSibling);
|
||||
}
|
||||
|
||||
function get_cookie(cookie_name) {
|
||||
var results = document.cookie.match ( '(^|;) ?' + cookie_name + '=([^;]*)(;|$)');
|
||||
if (results)
|
||||
return (unescape(results[2]));
|
||||
else
|
||||
function getCookie(cookie_name) {
|
||||
let results = document.cookie.match('(^|;) ?' + cookie_name + '=([^;]*)(;|$)');
|
||||
if (results) {
|
||||
return unescape(results[2]);
|
||||
} else {
|
||||
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) {
|
||||
if (typeof window.event != "undefined" && event.which == 2) {
|
||||
// don't highlight on middle click
|
||||
return true;
|
||||
}
|
||||
|
||||
var divs = document.getElementsByTagName('div');
|
||||
for (var i = 0; i < divs.length; i++)
|
||||
{
|
||||
if (divs[i].className.indexOf('post') != -1)
|
||||
|
||||
let divs = document.getElementsByTagName('div');
|
||||
for (let i = 0; i < divs.length; i++) {
|
||||
if (divs[i].className.indexOf('post') != -1) {
|
||||
divs[i].className = divs[i].className.replace(/highlighted/, '');
|
||||
}
|
||||
}
|
||||
if (id) {
|
||||
var post = document.getElementById('reply_'+id);
|
||||
if (post)
|
||||
let post = document.getElementById('reply_' + id);
|
||||
if (post) {
|
||||
post.className += ' highlighted';
|
||||
window.location.hash = id;
|
||||
}
|
||||
window.location.hash = id;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function generatePassword() {
|
||||
var pass = '';
|
||||
var chars = '{% endverbatim %}{{ config.genpassword_chars }}{% verbatim %}';
|
||||
for (var i = 0; i < 8; i++) {
|
||||
var rnd = Math.floor(Math.random() * chars.length);
|
||||
let pass = '';
|
||||
let chars = '{% endverbatim %}{{ config.genpassword_chars }}{% verbatim %}';
|
||||
for (let i = 0; i < 8; i++) {
|
||||
let rnd = Math.floor(Math.random() * chars.length);
|
||||
pass += chars.substring(rnd, rnd + 1);
|
||||
}
|
||||
return pass;
|
||||
}
|
||||
|
||||
function dopost(form) {
|
||||
function doPost(form) {
|
||||
if (form.elements['name']) {
|
||||
localStorage.name = form.elements['name'].value.replace(/( |^)## .+$/, '');
|
||||
}
|
||||
@ -261,28 +321,24 @@ function dopost(form) {
|
||||
if (form.elements['email'] && form.elements['email'].value != 'sage') {
|
||||
localStorage.email = form.elements['email'].value;
|
||||
}
|
||||
|
||||
|
||||
saved[document.location] = form.elements['body'].value;
|
||||
sessionStorage.body = JSON.stringify(saved);
|
||||
|
||||
|
||||
return form.elements['body'].value != "" || (form.elements['file'] && form.elements['file'].value != "") || (form.elements.file_url && form.elements['file_url'].value != "");
|
||||
}
|
||||
|
||||
function citeReply(id, with_link) {
|
||||
var textarea = document.getElementById('body');
|
||||
let textarea = document.getElementById('body');
|
||||
if (!textarea) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!textarea) return false;
|
||||
|
||||
if (document.selection) {
|
||||
// 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;
|
||||
if (textarea.selectionStart || textarea.selectionStart == '0') {
|
||||
let start = textarea.selectionStart;
|
||||
let end = textarea.selectionEnd;
|
||||
textarea.value = textarea.value.substring(0, start) + '>>' + id + '\n' + textarea.value.substring(end, textarea.value.length);
|
||||
|
||||
|
||||
textarea.selectionStart += ('>>' + id).length + 1;
|
||||
textarea.selectionEnd = textarea.selectionStart;
|
||||
} else {
|
||||
@ -290,10 +346,10 @@ function citeReply(id, with_link) {
|
||||
textarea.value += '>>' + id + '\n';
|
||||
}
|
||||
if (typeof $ != 'undefined') {
|
||||
var select = document.getSelection().toString();
|
||||
let select = document.getSelection().toString();
|
||||
if (select) {
|
||||
var 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 body = $('#reply_' + id + ', #op_' + id).find('div.body'); // TODO: support for OPs
|
||||
let index = body.text().indexOf(select.replace('\n', '')); // for some reason this only works like this
|
||||
if (index > -1) {
|
||||
textarea.value += '>' + select + '\n';
|
||||
}
|
||||
@ -308,36 +364,40 @@ function citeReply(id, with_link) {
|
||||
function rememberStuff() {
|
||||
if (document.forms.post) {
|
||||
if (document.forms.post.password) {
|
||||
if (!localStorage.password)
|
||||
if (!localStorage.password) {
|
||||
localStorage.password = generatePassword();
|
||||
}
|
||||
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;
|
||||
if (localStorage.email && document.forms.post.elements['email'])
|
||||
}
|
||||
if (localStorage.email && document.forms.post.elements['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);
|
||||
|
||||
}
|
||||
|
||||
if (sessionStorage.body) {
|
||||
var saved = JSON.parse(sessionStorage.body);
|
||||
if (get_cookie('{% endverbatim %}{{ config.cookies.js }}{% verbatim %}')) {
|
||||
let saved = JSON.parse(sessionStorage.body);
|
||||
if (getCookie('{% endverbatim %}{{ config.cookies.js }}{% verbatim %}')) {
|
||||
// Remove successful posts
|
||||
var successful = JSON.parse(get_cookie('{% endverbatim %}{{ config.cookies.js }}{% verbatim %}'));
|
||||
for (var url in successful) {
|
||||
let successful = JSON.parse(getCookie('{% endverbatim %}{{ config.cookies.js }}{% verbatim %}'));
|
||||
for (let url in successful) {
|
||||
saved[url] = null;
|
||||
}
|
||||
sessionStorage.body = JSON.stringify(saved);
|
||||
|
||||
|
||||
document.cookie = '{% endverbatim %}{{ config.cookies.js }}{% verbatim %}={};expires=0;path=/;';
|
||||
}
|
||||
if (saved[document.location]) {
|
||||
document.forms.post.body.value = saved[document.location];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (localStorage.body) {
|
||||
document.forms.post.body.value = localStorage.body;
|
||||
localStorage.body = '';
|
||||
@ -350,23 +410,24 @@ var script_settings = function(script_name) {
|
||||
this.get = function(var_name, default_val) {
|
||||
if (typeof tb_settings == '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 tb_settings[this.script_name][var_name];
|
||||
}
|
||||
};
|
||||
|
||||
function init() {
|
||||
init_stylechooser();
|
||||
initStyleChooser();
|
||||
|
||||
{% endverbatim %}
|
||||
{% endverbatim %}
|
||||
{% if config.allow_delete %}
|
||||
if (document.forms.postcontrols) {
|
||||
document.forms.postcontrols.password.value = localStorage.password;
|
||||
}
|
||||
{% endif %}
|
||||
{% verbatim %}
|
||||
|
||||
|
||||
if (window.location.hash.indexOf('q') != 1 && window.location.hash.substring(1))
|
||||
highlightReply(window.location.hash.substring(1));
|
||||
}
|
||||
@ -376,12 +437,12 @@ var RecaptchaOptions = {
|
||||
};
|
||||
|
||||
onready_callbacks = [];
|
||||
function onready(fnc) {
|
||||
function onReady(fnc) {
|
||||
onready_callbacks.push(fnc);
|
||||
}
|
||||
|
||||
function ready() {
|
||||
for (var i = 0; i < onready_callbacks.length; i++) {
|
||||
for (let i = 0; i < onready_callbacks.length; i++) {
|
||||
onready_callbacks[i]();
|
||||
}
|
||||
}
|
||||
@ -391,11 +452,11 @@ function ready() {
|
||||
var post_date = "{{ config.post_date }}";
|
||||
var max_images = {{ config.max_images }};
|
||||
|
||||
onready(init);
|
||||
onReady(init);
|
||||
|
||||
{% 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 %}
|
||||
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];
|
||||
s.parentNode.insertBefore(sc, s);
|
||||
{% endif %}
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<th>{% trans 'Reason' %}</th>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<script src='main.js'></script>
|
||||
<script src='js/jquery.min.js'></script>
|
||||
<script src='js/mobile-style.js'></script>
|
||||
<script src='js/strftime.min.js'></script>
|
||||
<script src='js/longtable/longtable.js'></script>
|
||||
<script src='js/mod/ban-list.js'></script>
|
||||
<link rel='stylesheet' href='stylesheets/longtable/longtable.css'>
|
||||
<link rel='stylesheet' href='stylesheets/mod/ban-list.css'>
|
||||
<script src='main.js?v={{ config.resource_version }}' data-resource-version="{{ config.resource_version }}"></script>
|
||||
<script src='js/jquery.min.js?v={{ config.resource_version }}'></script>
|
||||
<script src='js/mobile-style.js?v={{ config.resource_version }}'></script>
|
||||
<script src='js/strftime.min.js?v={{ config.resource_version }}'></script>
|
||||
<script src='js/longtable/longtable.js?v={{ config.resource_version }}'></script>
|
||||
<script src='js/mod/ban-list.js?v={{ config.resource_version }}'></script>
|
||||
<link rel='stylesheet' href='stylesheets/longtable/longtable.css?v={{ config.resource_version }}'>
|
||||
<link rel='stylesheet' href='stylesheets/mod/ban-list.css?v={{ config.resource_version }}'>
|
||||
|
||||
<form action="?/bans" method="post" class="banform">
|
||||
{% if token %}
|
||||
@ -21,7 +21,7 @@
|
||||
<div class='buttons'>
|
||||
<input type="text" id="search" placeholder="{% trans %}Search{% endtrans %}">
|
||||
{% 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 %}
|
||||
</div>
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
{% if token_json %}
|
||||
<script>$(function(){ banlist_init("{{ token_json }}", {{ boards }}); });</script>
|
||||
|
@ -6,7 +6,7 @@
|
||||
{% for board in boards %}
|
||||
<li>
|
||||
<a href="?/{{ config.board_path|sprintf(board.uri) }}{{ config.file_index }}">{{ config.board_abbreviation|sprintf(board.uri) }}</a>
|
||||
-
|
||||
-
|
||||
{{ board.title|e }}
|
||||
{% if board.subtitle %}
|
||||
<small>—
|
||||
@ -70,7 +70,7 @@
|
||||
<li>
|
||||
<a href="?/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>
|
||||
</li>
|
||||
</ul>
|
||||
@ -166,7 +166,7 @@
|
||||
<li>
|
||||
A newer version of vichan
|
||||
(<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>
|
||||
</ul>
|
||||
</fieldset>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<tr>
|
||||
<th>{% trans %}Markup method{% endtrans %}
|
||||
{% 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">"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 %}
|
||||
|
@ -7,7 +7,7 @@
|
||||
{% trans 'Username' %}
|
||||
</th>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -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">
|
||||
<p class="unimportant">
|
||||
{% if board %}
|
||||
@ -27,7 +27,7 @@
|
||||
<table>
|
||||
<tr><th>{% trans %}URL{% endtrans %}</th><th>{% trans %}Title{% endtrans %}</th></tr>
|
||||
<tr><td><input type="text" name="page"></td><td><input type="text" name="title"></td>
|
||||
</table>
|
||||
</table>
|
||||
<input type="submit" value="{% trans %}Create{% endtrans %}">
|
||||
</form>
|
||||
|
||||
|
@ -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 %}
|
||||
<p style="text-align:center" class="unimportant">({% trans 'There are no active posts.' %})</p>
|
||||
{% else %}
|
||||
|
@ -103,7 +103,7 @@
|
||||
<fieldset>
|
||||
<legend>{% trans 'New ban' %}</legend>
|
||||
{% set redirect = '?/IP/' ~ ip|cloak_ip ~ '#bans' %}
|
||||
{% include 'mod/ban_form.html' %}
|
||||
{% include 'mod/ban_form.html' with { 'reasons': config.premade_ban_reasons } %}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
|
||||
|
@ -1,16 +1,16 @@
|
||||
{% if post.embed %}
|
||||
{{ post.embed }}
|
||||
{% else %}
|
||||
<div class="files">
|
||||
{% for file in post.files %}
|
||||
<div class="file{% if post.num_files > 1 %} multifile" style="width:{{ file.thumbwidth + 40 }}px"{% else %}"{% endif %}>
|
||||
{% else %}
|
||||
<div class="files {% if post.num_files > 1 %} multifile{% endif %}">
|
||||
{% for file in post.files %}
|
||||
<div class="file{% if post.num_files > 1 %} multifile" style="width:{{ file.thumbwidth + 40 }}px"{% else %}"{% endif %}>
|
||||
{% if file.file == 'deleted' %}
|
||||
<img class="post-image deleted" src="{{ config.root }}{{ config.image_deleted }}" alt="" />
|
||||
{% 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' %}
|
||||
{% trans %}Spoiler Image{% endtrans %},
|
||||
{% trans %}Spoiler Image{% endtrans %},
|
||||
{% endif %}
|
||||
{{ file.size|filesize }}
|
||||
{% if file.width and file.height %}
|
||||
@ -21,18 +21,22 @@
|
||||
{% endif %}
|
||||
{% if config.show_filename and file.filename %}
|
||||
,
|
||||
{% if file.filename|length > config.max_filename_display %}
|
||||
<span class="postfilename" title="{{ file.filename|e|bidi_cleanup }}">{{ file.filename|truncate_filename(config.max_filename_display)|e|bidi_cleanup }}</span>
|
||||
{% if file.thumb == 'spoiler' %}
|
||||
<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 %}
|
||||
<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 %}
|
||||
)
|
||||
</span>
|
||||
{% 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} %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
@ -20,7 +20,7 @@
|
||||
{{ config.file_thumb|sprintf(config.file_icons.default) }}
|
||||
{% 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>
|
||||
{% else %}
|
||||
@ -39,7 +39,7 @@
|
||||
{{ config.uri_thumb }}{{ post.thumb }}
|
||||
{% 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 %}
|
||||
</a>
|
||||
|
@ -1,9 +1,6 @@
|
||||
<form name="post" onsubmit="return dopost(this);" enctype="multipart/form-data" action="{{ config.post_url }}" method="post">
|
||||
{{ antibot.html() }}
|
||||
<form name="post" onsubmit="return doPost(this);" enctype="multipart/form-data" action="{{ config.post_url }}" method="post">
|
||||
{% if id %}<input type="hidden" name="thread" value="{{ id }}">{% endif %}
|
||||
{{ antibot.html() }}
|
||||
<input type="hidden" name="board" value="{{ board.uri }}">
|
||||
{{ antibot.html() }}
|
||||
{% if current_page %}
|
||||
<input type="hidden" name="page" value="{{ current_page }}">
|
||||
{% 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>
|
||||
<th>
|
||||
{% trans %}Name{% endtrans %}
|
||||
{{ antibot.html() }}
|
||||
</th>
|
||||
<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 %}
|
||||
{{ antibot.html() }}
|
||||
</td>
|
||||
</tr>{% endif %}
|
||||
{% 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 %}
|
||||
{% trans %}Email{% endtrans %}
|
||||
{% endif %}
|
||||
{{ antibot.html() }}
|
||||
</th>
|
||||
<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) %}
|
||||
@ -39,17 +33,14 @@
|
||||
{% else %}
|
||||
<input type="text" name="email" size="25" maxlength="40" autocomplete="off">
|
||||
{% 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))) %}
|
||||
<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 %}
|
||||
{{ antibot.html() }}
|
||||
</td>
|
||||
</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>
|
||||
<th>
|
||||
{% trans %}Subject{% endtrans %}
|
||||
{{ antibot.html() }}
|
||||
</th>
|
||||
<td>
|
||||
<input style="float:left;" type="text" name="subject" size="25" maxlength="100" autocomplete="off">
|
||||
@ -60,11 +51,9 @@
|
||||
<tr>
|
||||
<th>
|
||||
{% trans %}Comment{% endtrans %}
|
||||
{{ antibot.html() }}
|
||||
</th>
|
||||
<td>
|
||||
<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_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 %}
|
||||
@ -72,57 +61,57 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if config.recaptcha %}
|
||||
{% if config.captcha.provider == 'recaptcha' %}
|
||||
{% if config.captcha.dynamic %}
|
||||
<tr id="captcha" style="display: none;">
|
||||
{% else %}
|
||||
<tr>
|
||||
{% endif %}
|
||||
<th>
|
||||
{% trans %}Verification{% endtrans %}
|
||||
{{ antibot.html() }}
|
||||
</th>
|
||||
<td>
|
||||
<div class="g-recaptcha" data-sitekey="{{ config.recaptcha_public }}"></div>
|
||||
{{ antibot.html() }}
|
||||
<div class="g-recaptcha" data-sitekey="{{ config.captcha.recaptcha.sitekey }}"></div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if config.hcaptcha %}
|
||||
{% if config.captcha.provider == 'hcaptcha' %}
|
||||
<tr>
|
||||
<th>
|
||||
{% trans %}Verification{% endtrans %}
|
||||
{{ antibot.html() }}
|
||||
</th>
|
||||
<td>
|
||||
<div class="h-captcha" data-sitekey="{{ config.hcaptcha_public }}"></div>
|
||||
{{ antibot.html() }}
|
||||
<div class="h-captcha" data-sitekey="{{ config.captcha.hcaptcha.sitekey }}"></div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if config.captcha.enabled %}
|
||||
{% if config.captcha.provider == 'native' %}
|
||||
<tr class='captcha'>
|
||||
<th>
|
||||
{% trans %}Verification{% endtrans %}
|
||||
</th>
|
||||
<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>
|
||||
<input class='captcha_text' type='text' name='captcha_text' size='32' maxlength='6' autocomplete='off'>
|
||||
<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>
|
||||
</noscript>
|
||||
</td>
|
||||
</tr>
|
||||
{% elseif config.new_thread_capt %}
|
||||
{% if not id %}
|
||||
{% elseif config.captcha.native.new_thread_capt %}
|
||||
{% if not id %}
|
||||
<tr class='captcha'>
|
||||
<th>
|
||||
{% trans %}Verification{% endtrans %}
|
||||
</th>
|
||||
<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>
|
||||
<input class='captcha_text' type='text' name='captcha_text' size='32' maxlength='6' autocomplete='off'>
|
||||
<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>
|
||||
</noscript>
|
||||
</td>
|
||||
@ -165,18 +154,16 @@
|
||||
|
||||
{% if config.allow_upload_by_url %}
|
||||
<div style="float:none;text-align:left" id="upload_url">
|
||||
<label for="file_url">{% trans %}Or URL{% endtrans %}</label>:
|
||||
<label for="file_url">{% trans %}Or URL{% endtrans %}</label>:
|
||||
<input style="display:inline" type="text" id="file_url" name="file_url" size="35">
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ antibot.html() }}
|
||||
</td>
|
||||
</tr>
|
||||
{% if config.enable_embedding %}
|
||||
<tr id="upload_embed">
|
||||
<th>
|
||||
{% trans %}Embed{% endtrans %}
|
||||
{{ antibot.html() }}
|
||||
</th>
|
||||
<td>
|
||||
<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>
|
||||
<th>
|
||||
{% trans %}Password{% endtrans %}
|
||||
{{ antibot.html() }}
|
||||
</th>
|
||||
<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>
|
||||
{{ antibot.html() }}
|
||||
</td>
|
||||
</tr>{% endif %}
|
||||
{% if config.simple_spam and not id %}<tr>
|
||||
<th>
|
||||
{{ config.simple_spam.question }}
|
||||
{{ antibot.html() }}
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="simple_spam" value="" size="12" maxlength="18" autocomplete="off">
|
||||
{{ antibot.html() }}
|
||||
<input type="text" name="simple_spam" value="" size="12" maxlength="18" autocomplete="off">
|
||||
</td>
|
||||
</tr>{% endif %}
|
||||
</table>
|
||||
{{ antibot.html(true) }}
|
||||
<input type="hidden" name="hash" value="{{ antibot.hash() }}">
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">{% verbatim %}
|
||||
|
@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS ``posts_{{ board }}`` (
|
||||
`files` text DEFAULT NULL,
|
||||
`num_files` int(11) DEFAULT 0,
|
||||
`filehash` text CHARACTER SET ascii,
|
||||
`password` varchar(20) DEFAULT NULL,
|
||||
`password` varchar(64) DEFAULT NULL,
|
||||
`ip` varchar(39) CHARACTER SET ascii NOT NULL,
|
||||
`sticky` int(1) NOT NULL,
|
||||
`locked` int(1) NOT NULL,
|
||||
|
@ -51,7 +51,7 @@
|
||||
{% else %}
|
||||
<img src="{{post.file}}"
|
||||
{% 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>
|
||||
<div class="replies">
|
||||
<strong>R: {{ post.reply_count }} / I: {{ post.image_count }}{% if post.sticky %} (sticky){% endif %}</strong>
|
||||
@ -80,7 +80,7 @@
|
||||
{% endverbatim %}
|
||||
{% for name, uri in config.stylesheets %}{% verbatim %}'{% endverbatim %}{{ name|addslashes }}{% verbatim %}' : '{% endverbatim %}/stylesheets/{{ uri|addslashes }}{% verbatim %}',
|
||||
{% endverbatim %}{% endfor %}{% verbatim %}
|
||||
}; onready(init);
|
||||
}; onReady(init);
|
||||
{% endverbatim %}</script>
|
||||
|
||||
<script type="text/javascript">{% verbatim %}
|
||||
|
@ -17,17 +17,12 @@
|
||||
'default' => 'Catalog'
|
||||
);
|
||||
|
||||
$__boards = listBoards();
|
||||
$__default_boards = Array();
|
||||
foreach ($__boards as $__board)
|
||||
$__default_boards[] = $__board['uri'];
|
||||
|
||||
$theme['config'][] = Array(
|
||||
'title' => 'Included boards',
|
||||
'name' => 'boards',
|
||||
'type' => 'text',
|
||||
'comment' => '(space seperated)',
|
||||
'default' => implode(' ', $__default_boards)
|
||||
'default' => '*'
|
||||
);
|
||||
|
||||
$theme['config'][] = Array(
|
||||
|
@ -1,6 +1,15 @@
|
||||
<?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) {
|
||||
global $config;
|
||||
|
||||
@ -13,6 +22,11 @@
|
||||
|
||||
$boards = explode(' ', $settings['boards']);
|
||||
|
||||
if (in_array('*', $boards)) {
|
||||
$boards = get_all_boards();
|
||||
}
|
||||
|
||||
|
||||
if ($action == 'all') {
|
||||
foreach ($boards as $board) {
|
||||
$b = new Catalog();
|
||||
|
@ -34,17 +34,28 @@
|
||||
</ul>
|
||||
</fieldset>
|
||||
<br>
|
||||
<div class="mainBox">
|
||||
<br>
|
||||
<div class="description">{{ settings.description }}</div>
|
||||
<br>
|
||||
<img class="imageofnow" src="{{ settings.imageofnow }}">
|
||||
<br>
|
||||
<div class="quoteofnow">{{ settings.quoteofnow }}</div>
|
||||
<br>
|
||||
<iframe class ="videoofnow" width="560" height="315" src="{{ settings.videoofnow }}"></iframe>
|
||||
<br>
|
||||
</div>
|
||||
{% if settings.description or settings.imageofnow or settings.quoteofnow or settings.videoofnow %}
|
||||
<div class="mainBox">
|
||||
<br>
|
||||
{% if settings.description %}
|
||||
<div class="description">{{ settings.description }}</div>
|
||||
<br>
|
||||
{% endif %}
|
||||
{% if settings.imageofnow %}
|
||||
<img class="imageofnow" src="{{ settings.imageofnow }}">
|
||||
<br>
|
||||
{% 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">
|
||||
{% if news|length == 0 %}
|
||||
<p style="text-align:center" class="unimportant">(No news to show.)</p>
|
||||
|
@ -74,12 +74,19 @@
|
||||
);
|
||||
|
||||
$theme['config'][] = Array(
|
||||
'title' => 'Excluded boards',
|
||||
'title' => 'Excluded boards (recent posts)',
|
||||
'name' => 'exclude',
|
||||
'type' => 'text',
|
||||
'comment' => '(space seperated)'
|
||||
);
|
||||
|
||||
$theme['config'][] = Array(
|
||||
'title' => 'Excluded boards (boardlist)',
|
||||
'name' => 'excludeboardlist',
|
||||
'type' => 'text',
|
||||
'comment' => '(space seperated)'
|
||||
);
|
||||
|
||||
$theme['config'][] = Array(
|
||||
'title' => '# of recent images',
|
||||
'name' => 'limit_images',
|
||||
|
@ -158,6 +158,13 @@
|
||||
$settings['no_recent'] = (int) $settings['no_recent'];
|
||||
$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);
|
||||
|
||||
// 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(
|
||||
'settings' => $settings,
|
||||
@ -167,7 +174,7 @@
|
||||
'recent_posts' => $recent_posts,
|
||||
'stats' => $stats,
|
||||
'news' => $news,
|
||||
'boards' => listBoards()
|
||||
'boards' => $boardlist
|
||||
));
|
||||
}
|
||||
};
|
||||
|
@ -4,11 +4,11 @@
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<title>{{ settings.title }}</title>
|
||||
<link rel="stylesheet" media="screen" href="{{ config.url_stylesheet }}"/>
|
||||
<link rel="stylesheet" media="screen" href="{{ config.root }}{{ settings.css }}"/>
|
||||
<link rel="stylesheet" media="screen" href="{{ config.url_stylesheet }}?v={{ config.resource_version }}"/>
|
||||
<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.default_stylesheet.1 != '' %}<link rel="stylesheet" type="text/css" id="stylesheet" href="{{ config.uri_stylesheets }}{{ config.default_stylesheet.1 }}">{% endif %}
|
||||
{% if config.font_awesome %}<link rel="stylesheet" href="{{ config.root }}{{ config.font_awesome_css }}">{% 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 }}?v={{ config.resource_version }}">{% endif %}
|
||||
{% include 'header.html' %}
|
||||
</head>
|
||||
<body>
|
||||
@ -17,7 +17,7 @@
|
||||
<h1>{{ settings.title }}</h1>
|
||||
<div class="subtitle">{{ settings.subtitle }}</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="box-wrap">
|
||||
<div class="box left">
|
||||
<h2>Recent Images</h2>
|
||||
@ -36,7 +36,7 @@
|
||||
<ul>
|
||||
{% for post in recent_posts %}
|
||||
<li>
|
||||
<strong>{{ post.board_name }}</strong>:
|
||||
<strong>{{ post.board_name }}</strong>:
|
||||
<a href="{{ post.link }}">
|
||||
{{ post.snippet }}
|
||||
</a>
|
||||
@ -53,11 +53,11 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<hr/>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
{% endapply %}
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="description" content="{{ board.url }} - {{ board.title|e }} - {{ meta_subject }}" />
|
||||
<meta name="twitter:card" value="summary">
|
||||
<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 %}
|
||||
<meta property="og:title" content="{{ meta_subject }}" />
|
||||
<meta property="og:type" content="article" />
|
||||
|
17
tools/hash-passwords.php
Normal file
17
tools/hash-passwords.php
Normal 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
40
tools/maintenance.php
Normal 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");
|
Loading…
x
Reference in New Issue
Block a user