4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
**/.git
|
||||
**/.gitignore
|
||||
/local-instances
|
||||
**/.gitkeep
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -44,5 +44,6 @@ Thumbs.db
|
||||
#vichan custom
|
||||
favicon.ico
|
||||
/static/spoiler.png
|
||||
/local-instances
|
||||
|
||||
/vendor/
|
||||
|
||||
@@ -119,6 +119,11 @@ WebM support
|
||||
------------
|
||||
Read `inc/lib/webm/README.md` for information about enabling webm.
|
||||
|
||||
Docker
|
||||
------------
|
||||
Vichan comes with a Dockerfile and docker-compose configuration, the latter aimed primarily at development and testing.
|
||||
See the `docker/doc.md` file for more information.
|
||||
|
||||
vichan API
|
||||
----------
|
||||
vichan provides by default a 4chan-compatible JSON API. For documentation on this, see:
|
||||
|
||||
22
b.php
22
b.php
@@ -1,20 +1,8 @@
|
||||
<?php
|
||||
$dir = "static/banners/";
|
||||
$files = scandir($dir, SCANDIR_SORT_NONE);
|
||||
$images = array_diff($files, array('.', '..'));
|
||||
$name = $images[array_rand($images)];
|
||||
// open the file in a binary mode
|
||||
$fp = fopen($dir . $name, 'rb');
|
||||
|
||||
// send the right headers
|
||||
header('Cache-Control: no-cache, no-store, must-revalidate'); // HTTP 1.1
|
||||
header('Pragma: no-cache'); // HTTP 1.0
|
||||
header('Expires: 0'); // Proxies
|
||||
$fstat = fstat($fp);
|
||||
header('Content-Type: ' . mime_content_type($dir . $name));
|
||||
header('Content-Length: ' . $fstat['size']);
|
||||
$files = scandir('static/banners/', SCANDIR_SORT_NONE);
|
||||
$files = array_diff($files, ['.', '..']);
|
||||
|
||||
// dump the picture and stop the script
|
||||
fpassthru($fp);
|
||||
exit;
|
||||
?>
|
||||
$name = $files[array_rand($files)];
|
||||
header("Location: /static/banners/$name", true, 307);
|
||||
header('Cache-Control: no-cache');
|
||||
|
||||
40
compose.yml
Normal file
40
compose.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
services:
|
||||
#nginx webserver + php 8.x
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./docker/nginx/Dockerfile
|
||||
ports:
|
||||
- "9090:80"
|
||||
depends_on:
|
||||
- db
|
||||
volumes:
|
||||
- ./local-instances/${INSTANCE:-0}/www:/var/www/html
|
||||
- ./docker/nginx/vichan.conf:/etc/nginx/conf.d/default.conf
|
||||
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./docker/nginx/proxy.conf:/etc/nginx/conf.d/proxy.conf
|
||||
links:
|
||||
- php
|
||||
|
||||
php:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./docker/php/Dockerfile
|
||||
volumes:
|
||||
- ./local-instances/${INSTANCE:-0}/www:/var/www
|
||||
- ./docker/php/www.conf:/usr/local/etc/php-fpm.d/www.conf
|
||||
- ./docker/php/jit.ini:/usr/local/etc/php/conf.d/jit.ini
|
||||
|
||||
#MySQL Service
|
||||
db:
|
||||
image: mysql:8.0.35
|
||||
container_name: db
|
||||
restart: unless-stopped
|
||||
tty: true
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
MYSQL_DATABASE: vichan
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
volumes:
|
||||
- ./local-instances/${INSTANCE:-0}/mysql:/var/lib/mysql
|
||||
@@ -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/"],
|
||||
@@ -32,7 +33,14 @@
|
||||
"inc/mod/auth.php",
|
||||
"inc/lock.php",
|
||||
"inc/queue.php",
|
||||
"inc/functions.php"
|
||||
"inc/functions.php",
|
||||
"inc/functions/dice.php",
|
||||
"inc/functions/format.php",
|
||||
"inc/functions/net.php",
|
||||
"inc/functions/num.php",
|
||||
"inc/functions/theme.php",
|
||||
"inc/service/captcha-queries.php",
|
||||
"inc/context.php"
|
||||
]
|
||||
},
|
||||
"license": "Tinyboard + vichan",
|
||||
|
||||
20
docker/doc.md
Normal file
20
docker/doc.md
Normal file
@@ -0,0 +1,20 @@
|
||||
The `php-fpm` process runs containerized.
|
||||
The php application always uses `/var/www` as it's work directory and home folder, and if `/var/www` is bind mounted it
|
||||
is necessary to adjust the path passed via FastCGI to `php-fpm` by changing the root directory to `/var/www`.
|
||||
This can achieved in nginx by setting the `fastcgi_param SCRIPT_FILENAME` to `/var/www/$fastcgi_script_name;`
|
||||
|
||||
The default docker compose settings are intended for development and testing purposes.
|
||||
The folder structure expected by compose is as follows
|
||||
|
||||
```
|
||||
<vichan-project>
|
||||
└── local-instances
|
||||
└── 1
|
||||
├── mysql
|
||||
└── www
|
||||
```
|
||||
The vichan container is by itself much less rigid.
|
||||
|
||||
|
||||
Use `docker compose up --build` to start the docker compose.
|
||||
Use `docker compose up --build -d php` to rebuild just the vichan container while the compose is running. Useful for development.
|
||||
8
docker/nginx/Dockerfile
Normal file
8
docker/nginx/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM nginx:1.25.3-alpine
|
||||
|
||||
COPY . /code
|
||||
RUN adduser --system www-data \
|
||||
&& adduser www-data www-data
|
||||
|
||||
CMD [ "nginx", "-g", "daemon off;" ]
|
||||
EXPOSE 80
|
||||
34
docker/nginx/nginx.conf
Normal file
34
docker/nginx/nginx.conf
Normal file
@@ -0,0 +1,34 @@
|
||||
# This and proxy.conf are based on
|
||||
# https://github.com/dead-guru/devichan/blob/master/nginx/nginx.conf
|
||||
|
||||
user www-data;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /dev/stdout warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Switch logging to console out to view via Docker
|
||||
access_log /dev/stdout;
|
||||
error_log /dev/stdout warn;
|
||||
sendfile on;
|
||||
keepalive_timeout 5;
|
||||
|
||||
gzip on;
|
||||
gzip_http_version 1.0;
|
||||
gzip_vary on;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/xml text/plain text/css application/xhtml+xml application/xml application/rss+xml application/atom_xml application/x-javascript application/x-httpd-php;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/sites-available/*.conf;
|
||||
}
|
||||
40
docker/nginx/proxy.conf
Normal file
40
docker/nginx/proxy.conf
Normal file
@@ -0,0 +1,40 @@
|
||||
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=czone:4m max_size=50m inactive=120m;
|
||||
proxy_temp_path /var/tmp/nginx;
|
||||
proxy_cache_key "$scheme://$host$request_uri";
|
||||
|
||||
|
||||
map $http_forwarded_request_id $x_request_id {
|
||||
"" $request_id;
|
||||
default $http_forwarded_request_id;
|
||||
}
|
||||
|
||||
map $http_forwarded_forwarded_host $forwardedhost {
|
||||
"" $host;
|
||||
default $http_forwarded_forwarded_host;
|
||||
}
|
||||
|
||||
|
||||
map $http_x_forwarded_proto $fcgi_https {
|
||||
default "";
|
||||
https on;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_proto $real_scheme {
|
||||
default $scheme;
|
||||
https https;
|
||||
}
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Server $host;
|
||||
|
||||
real_ip_header X-Forwarded-For;
|
||||
|
||||
set_real_ip_from 10.0.0.0/8;
|
||||
set_real_ip_from 172.16.0.0/12;
|
||||
set_real_ip_from 172.18.0.0;
|
||||
set_real_ip_from 192.168.0.0/24;
|
||||
set_real_ip_from 127.0.0.0/8;
|
||||
|
||||
real_ip_recursive on;
|
||||
66
docker/nginx/vichan.conf
Normal file
66
docker/nginx/vichan.conf
Normal file
@@ -0,0 +1,66 @@
|
||||
upstream php-upstream {
|
||||
server php:9000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server ipv6only=on;
|
||||
server_name vichan;
|
||||
root /var/www/html;
|
||||
add_header X-Frame-Options "SAMEORIGIN";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
|
||||
index index.html index.php;
|
||||
|
||||
charset utf-8;
|
||||
|
||||
location ~ ^([^.\?]*[^\/])$ {
|
||||
try_files $uri @addslash;
|
||||
}
|
||||
|
||||
# Expire rules for static content
|
||||
# Media: images, icons, video, audio, HTC
|
||||
location ~* \.(?:jpg|jpeg|gif|png|webp|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
|
||||
expires 1M;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
# CSS and Javascript
|
||||
location ~* \.(?:css|js)$ {
|
||||
expires 1y;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
|
||||
location ~* \.(html)$ {
|
||||
expires -1;
|
||||
}
|
||||
|
||||
location @addslash {
|
||||
return 301 $uri/;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php$is_args$args;
|
||||
}
|
||||
|
||||
client_max_body_size 2G;
|
||||
|
||||
location ~ \.php$ {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Request-Id $x_request_id;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header Forwarded-Request-Id $x_request_id;
|
||||
fastcgi_pass php-upstream;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME /var/www/$fastcgi_script_name;
|
||||
fastcgi_read_timeout 600;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
location = /favicon.ico { access_log off; log_not_found off; }
|
||||
location = /robots.txt { access_log off; log_not_found off; }
|
||||
}
|
||||
88
docker/php/Dockerfile
Normal file
88
docker/php/Dockerfile
Normal file
@@ -0,0 +1,88 @@
|
||||
# Based on https://github.com/dead-guru/devichan/blob/master/php-fpm/Dockerfile
|
||||
|
||||
FROM composer:lts AS composer
|
||||
FROM php:8.1-fpm-alpine
|
||||
|
||||
RUN apk add --no-cache \
|
||||
zlib \
|
||||
zlib-dev \
|
||||
libpng \
|
||||
libpng-dev \
|
||||
libjpeg-turbo \
|
||||
libjpeg-turbo-dev \
|
||||
libwebp \
|
||||
libwebp-dev \
|
||||
libcurl \
|
||||
curl-dev \
|
||||
imagemagick \
|
||||
graphicsmagick \
|
||||
gifsicle \
|
||||
ffmpeg \
|
||||
bind-tools \
|
||||
gettext \
|
||||
gettext-dev \
|
||||
icu-dev \
|
||||
oniguruma \
|
||||
oniguruma-dev \
|
||||
libmcrypt \
|
||||
libmcrypt-dev \
|
||||
lz4-libs \
|
||||
lz4-dev \
|
||||
imagemagick-dev \
|
||||
pcre-dev \
|
||||
$PHPIZE_DEPS \
|
||||
&& docker-php-ext-configure gd \
|
||||
--with-webp=/usr/include/webp \
|
||||
--with-jpeg=/usr/include \
|
||||
&& docker-php-ext-install -j$(nproc) \
|
||||
gd \
|
||||
curl \
|
||||
bcmath \
|
||||
opcache \
|
||||
pdo_mysql \
|
||||
gettext \
|
||||
intl \
|
||||
mbstring \
|
||||
&& pecl update-channels \
|
||||
&& pecl install -o -f igbinary \
|
||||
&& pecl install redis \
|
||||
&& pecl install imagick \
|
||||
$$ docker-php-ext-enable \
|
||||
igbinary \
|
||||
redis \
|
||||
imagick \
|
||||
&& apk del \
|
||||
zlib-dev \
|
||||
libpng-dev \
|
||||
libjpeg-turbo-dev \
|
||||
libwebp-dev \
|
||||
curl-dev \
|
||||
gettext-dev \
|
||||
oniguruma-dev \
|
||||
libmcrypt-dev \
|
||||
lz4-dev \
|
||||
imagemagick-dev \
|
||||
pcre-dev \
|
||||
$PHPIZE_DEPS \
|
||||
&& rm -rf /var/cache/* \
|
||||
&& rm -rf /tmp/pear
|
||||
RUN rmdir /var/www/html \
|
||||
&& install -d -m 744 -o www-data -g www-data /var/www \
|
||||
&& install -d -m 700 -o www-data -g www-data /var/tmp/vichan \
|
||||
&& install -d -m 700 -o www-data -g www-data /var/cache/gen-cache \
|
||||
&& install -d -m 700 -o www-data -g www-data /var/cache/template-cache
|
||||
|
||||
# Copy the bootstrap script.
|
||||
COPY ./docker/php/bootstrap.sh /usr/local/bin/bootstrap.sh
|
||||
|
||||
COPY --from=composer /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
# Copy the actual project (use .dockerignore to exclude stuff).
|
||||
COPY . /code
|
||||
|
||||
# Install the compose depedencies.
|
||||
RUN cd /code && composer install
|
||||
|
||||
WORKDIR "/var/www"
|
||||
CMD [ "bootstrap.sh" ]
|
||||
EXPOSE 9000
|
||||
16
docker/php/Dockerfile.profile
Normal file
16
docker/php/Dockerfile.profile
Normal file
@@ -0,0 +1,16 @@
|
||||
# syntax = devthefuture/dockerfile-x
|
||||
INCLUDE ./docker/php/Dockerfile
|
||||
|
||||
RUN apk add --no-cache \
|
||||
linux-headers \
|
||||
$PHPIZE_DEPS \
|
||||
&& pecl update-channels \
|
||||
&& pecl install xdebug \
|
||||
&& docker-php-ext-enable xdebug \
|
||||
&& apk del \
|
||||
linux-headers \
|
||||
$PHPIZE_DEPS \
|
||||
&& rm -rf /var/cache/*
|
||||
|
||||
ENV XDEBUG_OUT_DIR=/var/www/xdebug_out
|
||||
CMD [ "bootstrap.sh" ]
|
||||
87
docker/php/bootstrap.sh
Executable file
87
docker/php/bootstrap.sh
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
function set_cfg() {
|
||||
if [ -L "/var/www/inc/$1" ]; then
|
||||
echo "INFO: Resetting $1"
|
||||
rm "/var/www/inc/$1"
|
||||
cp "/code/inc/$1" "/var/www/inc/$1"
|
||||
chown www-data "/var/www/inc/$1"
|
||||
chgrp www-data "/var/www/inc/$1"
|
||||
chmod 600 "/var/www/inc/$1"
|
||||
else
|
||||
echo "INFO: Using existing $1"
|
||||
fi
|
||||
}
|
||||
|
||||
if ! mountpoint -q /var/www; then
|
||||
echo "WARNING: '/var/www' is not a mountpoint. All the data will remain inside the container!"
|
||||
fi
|
||||
|
||||
if [ ! -w /var/www ] ; then
|
||||
echo "ERROR: '/var/www' is not writable. Closing."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${XDEBUG_OUT_DIR:-''}" ] ; then
|
||||
echo "INFO: Initializing xdebug out directory at $XDEBUG_OUT_DIR"
|
||||
mkdir -p "$XDEBUG_OUT_DIR"
|
||||
chown www-data "$XDEBUG_OUT_DIR"
|
||||
chgrp www-data "$XDEBUG_OUT_DIR"
|
||||
chmod 755 "$XDEBUG_OUT_DIR"
|
||||
fi
|
||||
|
||||
# Link the entrypoints from the exposed directory.
|
||||
ln -nfs \
|
||||
/code/tools/ \
|
||||
/code/*.php \
|
||||
/code/LICENSE.* \
|
||||
/code/install.sql \
|
||||
/var/www/
|
||||
# Static files accessible from the webserver must be copied.
|
||||
cp -ur /code/static /var/www/
|
||||
cp -ur /code/stylesheets /var/www/
|
||||
|
||||
# Ensure correct permissions are set, since this might be bind mount.
|
||||
chown www-data /var/www
|
||||
chgrp www-data /var/www
|
||||
|
||||
# Initialize an empty robots.txt with the default if it doesn't exist.
|
||||
touch /var/www/robots.txt
|
||||
|
||||
# Link the cache and tmp files directory.
|
||||
ln -nfs /var/tmp/vichan /var/www/tmp
|
||||
|
||||
# Link the javascript directory.
|
||||
ln -nfs /code/js /var/www/
|
||||
|
||||
# Link the html templates directory and it's cache.
|
||||
ln -nfs /code/templates /var/www/
|
||||
ln -nfs -T /var/cache/template-cache /var/www/templates/cache
|
||||
chown -h www-data /var/www/templates/cache
|
||||
chgrp -h www-data /var/www/templates/cache
|
||||
|
||||
# Link the generic cache.
|
||||
ln -nfs -T /var/cache/gen-cache /var/www/tmp/cache
|
||||
chown -h www-data /var/www/tmp/cache
|
||||
chgrp -h www-data /var/www/tmp/cache
|
||||
|
||||
# Create the included files directory and link them
|
||||
install -d -m 700 -o www-data -g www-data /var/www/inc
|
||||
for file in /code/inc/*; do
|
||||
file="${file##*/}"
|
||||
if [ ! -e /var/www/inc/$file ]; then
|
||||
ln -s /code/inc/$file /var/www/inc/
|
||||
fi
|
||||
done
|
||||
|
||||
# Copy an empty instance configuration if the file is a link (it was linked because it did not exist before).
|
||||
set_cfg 'instance-config.php'
|
||||
set_cfg 'secrets.php'
|
||||
|
||||
# Link the composer dependencies.
|
||||
ln -nfs /code/vendor /var/www/
|
||||
|
||||
# Start the php-fpm server.
|
||||
exec php-fpm
|
||||
2
docker/php/jit.ini
Normal file
2
docker/php/jit.ini
Normal file
@@ -0,0 +1,2 @@
|
||||
opcache.jit_buffer_size=192M
|
||||
opcache.jit=tracing
|
||||
13
docker/php/www.conf
Normal file
13
docker/php/www.conf
Normal file
@@ -0,0 +1,13 @@
|
||||
[www]
|
||||
access.log = /proc/self/fd/2
|
||||
|
||||
; Ensure worker stdout and stderr are sent to the main error log.
|
||||
catch_workers_output = yes
|
||||
decorate_workers_output = no
|
||||
|
||||
user = www-data
|
||||
group = www-data
|
||||
|
||||
listen = 127.0.0.1:9000
|
||||
pm = static
|
||||
pm.max_children = 16
|
||||
7
docker/php/xdebug-prof.ini
Normal file
7
docker/php/xdebug-prof.ini
Normal file
@@ -0,0 +1,7 @@
|
||||
zend_extension=xdebug
|
||||
|
||||
[xdebug]
|
||||
xdebug.mode = profile
|
||||
xdebug.start_with_request = start
|
||||
error_reporting = E_ALL
|
||||
xdebug.output_dir = /var/www/xdebug_out
|
||||
28
inc/Data/Driver/ApcuCacheDriver.php
Normal file
28
inc/Data/Driver/ApcuCacheDriver.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
class ApcuCacheDriver implements CacheDriver {
|
||||
public function get(string $key): mixed {
|
||||
$success = false;
|
||||
$ret = \apcu_fetch($key, $success);
|
||||
if ($success === false) {
|
||||
return null;
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||
\apcu_store($key, $value, (int)$expires);
|
||||
}
|
||||
|
||||
public function delete(string $key): void {
|
||||
\apcu_delete($key);
|
||||
}
|
||||
|
||||
public function flush(): void {
|
||||
\apcu_clear_cache();
|
||||
}
|
||||
}
|
||||
28
inc/Data/Driver/ArrayCacheDriver.php
Normal file
28
inc/Data/Driver/ArrayCacheDriver.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace Vichan\Data\Driver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
/**
|
||||
* A simple process-wide PHP array.
|
||||
*/
|
||||
class ArrayCacheDriver implements CacheDriver {
|
||||
private static array $inner = [];
|
||||
|
||||
public function get(string $key): mixed {
|
||||
return isset(self::$inner[$key]) ? self::$inner[$key] : null;
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||
self::$inner[$key] = $value;
|
||||
}
|
||||
|
||||
public function delete(string $key): void {
|
||||
unset(self::$inner[$key]);
|
||||
}
|
||||
|
||||
public function flush(): void {
|
||||
self::$inner = [];
|
||||
}
|
||||
}
|
||||
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): mixed;
|
||||
|
||||
/**
|
||||
* Set a key-value pair.
|
||||
*
|
||||
* @param string $key The key.
|
||||
* @param mixed $value The value.
|
||||
* @param int|false $expires After how many seconds the pair will expire. Use false or ignore this parameter to keep
|
||||
* the value until it gets evicted to make space for more items. Some drivers will always
|
||||
* ignore this parameter and store the pair until it's removed.
|
||||
*/
|
||||
public function set(string $key, mixed $value, mixed $expires = false): void;
|
||||
|
||||
/**
|
||||
* Delete a key-value pair.
|
||||
*
|
||||
* @param string $key The key.
|
||||
*/
|
||||
public function delete(string $key): void;
|
||||
|
||||
/**
|
||||
* Delete all the key-value pairs.
|
||||
*/
|
||||
public function flush(): void;
|
||||
}
|
||||
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 mixed $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, mixed $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, 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);
|
||||
}
|
||||
}
|
||||
|
||||
147
inc/api.php
147
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,64 @@ 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, $board['uri']);
|
||||
}
|
||||
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 +147,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();
|
||||
foreach ($post->files as $i => $f) {
|
||||
if ($i == 0) continue;
|
||||
|
||||
$extra_file = array();
|
||||
if (sizeof($post->files) > 1) {
|
||||
$extra_files = [];
|
||||
foreach ($post->files as $i => $f) {
|
||||
if ($i == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$extra_file = [];
|
||||
$this->translateFile($f, $post, $extra_file);
|
||||
|
||||
$extra_files[] = $extra_file;
|
||||
@@ -156,8 +167,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 +180,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 +197,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;
|
||||
|
||||
265
inc/bans.php
265
inc/bans.php
@@ -1,8 +1,13 @@
|
||||
<?php
|
||||
|
||||
use Vichan\Functions\Format;
|
||||
use Lifo\IP\CIDR;
|
||||
|
||||
class Bans {
|
||||
static private function shouldDelete(array $ban, bool $require_ban_view) {
|
||||
return $ban['expires'] && ($ban['seen'] || !$require_ban_view) && $ban['expires'] < time();
|
||||
}
|
||||
|
||||
static private function deleteBans(array $ban_ids) {
|
||||
$len = count($ban_ids);
|
||||
if ($len === 1) {
|
||||
@@ -10,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 (';
|
||||
@@ -28,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): array|null {
|
||||
// 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): array|null {
|
||||
$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, string|false $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|false $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;
|
||||
|
||||
@@ -55,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) {
|
||||
@@ -139,84 +265,27 @@ 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 {
|
||||
/**
|
||||
* 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): array|null {
|
||||
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, string|false $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'];
|
||||
if ($auto_gc) {
|
||||
return self::findAutoGc($ip, $board, $get_mod_info, $config['require_ban_view'], $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;
|
||||
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) {
|
||||
$query = query("SELECT ``bans``.*, `username` FROM ``bans``
|
||||
LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`
|
||||
@@ -230,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']);
|
||||
@@ -275,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) {
|
||||
@@ -298,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));
|
||||
@@ -307,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;
|
||||
}
|
||||
@@ -364,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', 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();
|
||||
}
|
||||
|
||||
205
inc/cache.php
205
inc/cache.php
@@ -4,192 +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;
|
||||
}
|
||||
}
|
||||
|
||||
class Twig_Cache_TinyboardFilesystem extends Twig\Cache\FilesystemCache
|
||||
{
|
||||
private $directory;
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($directory, $options = 0)
|
||||
{
|
||||
parent::__construct($directory, $options);
|
||||
|
||||
$this->directory = $directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function was removed in Twig 2.x due to developer views on the Twig library. Who says we can't keep it for ourselves though?
|
||||
*/
|
||||
public function clear()
|
||||
{
|
||||
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->directory), RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
|
||||
if ($file->isFile()) {
|
||||
@unlink($file->getPathname());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
731
inc/config.php
731
inc/config.php
@@ -65,9 +65,29 @@
|
||||
// been generated. This keeps the script from querying the database and causing strain when not needed.
|
||||
$config['has_installed'] = '.installed';
|
||||
|
||||
// Use syslog() for logging all error messages and unauthorized login attempts.
|
||||
// Deprecated, use 'log_system'.
|
||||
$config['syslog'] = false;
|
||||
|
||||
$config['log_system'] = [
|
||||
/*
|
||||
* Log all error messages and unauthorized login attempts.
|
||||
* Can be "syslog", "error_log" (default), "file", or "stderr".
|
||||
*/
|
||||
'type' => 'error_log',
|
||||
// The application name used by the logging system. Defaults to "tinyboard" for backwards compatibility.
|
||||
'name' => 'tinyboard',
|
||||
/*
|
||||
* Only relevant if 'log_system' is set to "syslog". If true, double print the logs also in stderr. Defaults to
|
||||
* false.
|
||||
*/
|
||||
'syslog_stderr' => false,
|
||||
/*
|
||||
* Only relevant if "log_system" is set to `file`. Sets the file that vichan will log to. Defaults to
|
||||
* '/var/log/vichan.log'.
|
||||
*/
|
||||
'file_path' => '/var/log/vichan.log',
|
||||
];
|
||||
|
||||
// Use `host` via shell_exec() to lookup hostnames, avoiding query timeouts. May not work on your system.
|
||||
// Requires safe_mode to be disabled.
|
||||
$config['dns_system'] = false;
|
||||
@@ -79,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
|
||||
@@ -114,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
|
||||
@@ -173,7 +207,7 @@
|
||||
|
||||
// How long should the cookies last (in seconds). Defines how long should moderators should remain logged
|
||||
// in (0 = browser session).
|
||||
$config['cookies']['expire'] = 60 * 60 * 24 * 30 * 6; // ~6 months
|
||||
$config['cookies']['expire'] = 60 * 60 * 24 * 7; // 1 week.
|
||||
|
||||
// Make this something long and random for security.
|
||||
$config['cookies']['salt'] = 'abcdefghijklmnopqrstuvwxyz09123456789!@#$%^&*()';
|
||||
@@ -181,9 +215,20 @@
|
||||
// Whether or not you can access the mod cookie in JavaScript. Most users should not need to change this.
|
||||
$config['cookies']['httponly'] = true;
|
||||
|
||||
// Do not allow logins via unsecure connections.
|
||||
// 0 = off. Allow logins on unencrypted HTTP connections. Should only be used in testing environments.
|
||||
// 1 = on, trust HTTP headers. Allow logins on (at least reportedly partial) HTTPS connections. Use this only if you
|
||||
// use a proxy, CDN or load balancer via an unencrypted connection. Be sure to filter 'HTTP_X_FORWARDED_PROTO' in
|
||||
// the remote server, since an attacker could inject the header from the client.
|
||||
// 2 = on, do not trust HTTP headers. Secure default, allow logins only on HTTPS connections.
|
||||
$config['cookies']['secure_login_only'] = 2;
|
||||
|
||||
// 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
|
||||
@@ -230,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 (
|
||||
@@ -315,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';
|
||||
$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
|
||||
$config['captcha']['provider_check'] = '../inc/captcha/entrypoint.php';
|
||||
|
||||
'provider_check' => '../inc/captcha/entrypoint.php',
|
||||
// Custom captcha extra field (eg. charset)
|
||||
$config['captcha']['extra'] = 'abcdefghijklmnopqrstuvwxyz';
|
||||
'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;
|
||||
@@ -487,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
|
||||
@@ -611,6 +590,9 @@
|
||||
// Example: Custom secure tripcode.
|
||||
// $config['custom_tripcode']['##securetrip'] = '!!somethingelse';
|
||||
|
||||
//Disable tripcodes. This will make it so all new posts will act as if no tripcode exists.
|
||||
$config['disable_tripcodes'] = false;
|
||||
|
||||
// Allow users to mark their image as a "spoiler" when posting. The thumbnail will be replaced with a
|
||||
// static spoiler image instead (see $config['spoiler_image']).
|
||||
$config['spoiler_images'] = false;
|
||||
@@ -662,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;
|
||||
@@ -709,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;
|
||||
|
||||
@@ -730,11 +718,15 @@
|
||||
* ====================
|
||||
*/
|
||||
|
||||
$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):
|
||||
$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>");
|
||||
[ "/'''(.+?)'''/", "<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"
|
||||
@@ -839,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
|
||||
@@ -862,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';
|
||||
|
||||
@@ -897,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;
|
||||
@@ -976,11 +974,11 @@
|
||||
|
||||
// Timezone to use for displaying dates/times.
|
||||
$config['timezone'] = 'America/Los_Angeles';
|
||||
// The format string passed to strftime() for displaying dates.
|
||||
// http://www.php.net/manual/en/function.strftime.php
|
||||
$config['post_date'] = '%m/%d/%y (%a) %H:%M:%S';
|
||||
// The format string passed to DateTime::format() for displaying dates. ISO 8601-like by default.
|
||||
// https://www.php.net/manual/en/datetime.format.php
|
||||
$config['post_date'] = 'm/d/y (D) H:i:s';
|
||||
// Same as above, but used for "you are banned' pages.
|
||||
$config['ban_date'] = '%A %e %B, %Y';
|
||||
$config['ban_date'] = 'l j F, Y';
|
||||
|
||||
// The names on the post buttons. (On most imageboards, these are both just "Post").
|
||||
$config['button_newtopic'] = _('New Topic');
|
||||
@@ -1010,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';
|
||||
|
||||
@@ -1093,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
|
||||
@@ -1125,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;
|
||||
@@ -1143,8 +1152,8 @@
|
||||
// Be careful when creating a new embed, because depending on the URL you end up exposing yourself to an XSS.
|
||||
$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',
|
||||
@@ -1159,9 +1168,10 @@
|
||||
'<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>'
|
||||
),
|
||||
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?:\/\/(\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.
|
||||
@@ -1174,83 +1184,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'] = [
|
||||
// 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']['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: ');
|
||||
'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: ')
|
||||
];
|
||||
|
||||
/*
|
||||
* =========================
|
||||
@@ -1272,8 +1285,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.
|
||||
@@ -1353,22 +1366,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/';
|
||||
|
||||
$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.
|
||||
$config['dir']['template'] = getcwd() . '/templates';
|
||||
'template' => getcwd() . '/templates',
|
||||
// Location of vichan "themes".
|
||||
$config['dir']['themes'] = getcwd() . '/templates/themes';
|
||||
'themes' => getcwd() . '/templates/themes',
|
||||
// Same as above, but a URI (accessable by web interface).
|
||||
$config['dir']['themes_uri'] = 'templates/themes';
|
||||
'themes_uri' => 'templates/themes',
|
||||
// Home directory. Used by themes.
|
||||
$config['dir']['home'] = '';
|
||||
'home' => ''
|
||||
];
|
||||
|
||||
// Location of a blank 1x1 gif file. Only used when country_flags_condensed is enabled
|
||||
// $config['image_blank'] = 'static/blank.gif';
|
||||
@@ -1441,13 +1454,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
|
||||
@@ -1464,7 +1483,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'] = [];
|
||||
|
||||
/*
|
||||
* ====================
|
||||
@@ -1472,33 +1491,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>';
|
||||
@@ -1523,63 +1593,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
|
||||
@@ -1589,13 +1602,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();
|
||||
@@ -1605,11 +1618,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';
|
||||
@@ -1811,26 +1824,26 @@
|
||||
*/
|
||||
|
||||
// Public post search settings
|
||||
$config['search'] = array();
|
||||
|
||||
$config['search'] = [
|
||||
// Enable the search form
|
||||
$config['search']['enable'] = false;
|
||||
'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');
|
||||
|
||||
// Blacklist boards for searching, basically the opposite of the one above
|
||||
//$config['search']['disallowed_boards'] = array('j', 'z');
|
||||
|
||||
// Enable public logs? 0: NO, 1: YES, 2: YES, but drop names
|
||||
$config['public_logs'] = 0;
|
||||
|
||||
@@ -1879,31 +1892,33 @@
|
||||
* state. Please join #nntpchan on Rizon in order to peer with someone.
|
||||
*/
|
||||
|
||||
$config['nntpchan'] = array();
|
||||
|
||||
$config['nntpchan'] = [
|
||||
// Enable NNTPChan integration
|
||||
$config['nntpchan']['enabled'] = false;
|
||||
|
||||
'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';
|
||||
|
||||
'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.
|
||||
$config['nntpchan']['salt'] = 'change_me+please';
|
||||
|
||||
'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'
|
||||
'domain' => 'example.vichan.net',
|
||||
/*
|
||||
* An NNTPChan group name.
|
||||
* Please set this setting in your board/config.php, not globally.
|
||||
*/
|
||||
'group' => false, // eg. 'overchan.test'
|
||||
];
|
||||
|
||||
|
||||
|
||||
|
||||
91
inc/context.php
Normal file
91
inc/context.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
namespace Vichan;
|
||||
|
||||
use Vichan\Data\Driver\{CacheDriver, HttpDriver, ErrorLogLogDriver, FileLogDriver, LogDriver, StderrLogDriver, SyslogLogDriver};
|
||||
use Vichan\Service\HCaptchaQuery;
|
||||
use Vichan\Service\NativeCaptchaQuery;
|
||||
use Vichan\Service\ReCaptchaQuery;
|
||||
use Vichan\Service\RemoteCaptchaQuery;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
class Context {
|
||||
private array $definitions;
|
||||
|
||||
public function __construct(array $definitions) {
|
||||
$this->definitions = $definitions;
|
||||
}
|
||||
|
||||
public function get(string $name): mixed {
|
||||
if (!isset($this->definitions[$name])) {
|
||||
throw new \RuntimeException("Could not find a dependency named $name");
|
||||
}
|
||||
|
||||
$ret = $this->definitions[$name];
|
||||
if (is_callable($ret) && !is_string($ret) && !is_array($ret)) {
|
||||
$ret = $ret($this);
|
||||
$this->definitions[$name] = $ret;
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
|
||||
function build_context(array $config): Context {
|
||||
return new Context([
|
||||
'config' => $config,
|
||||
LogDriver::class => function($c) {
|
||||
$config = $c->get('config');
|
||||
|
||||
$name = $config['log_system']['name'];
|
||||
$level = $config['debug'] ? LogDriver::DEBUG : LogDriver::NOTICE;
|
||||
$backend = $config['log_system']['type'];
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ function createBoardlist($mod=false) {
|
||||
);
|
||||
}
|
||||
|
||||
function error($message, $priority = true, $debug_stuff = false) {
|
||||
function error($message, $priority = true, $debug_stuff = []) {
|
||||
global $board, $mod, $config, $db_error;
|
||||
|
||||
if ($config['syslog'] && $priority !== false) {
|
||||
@@ -356,10 +356,17 @@ class Post {
|
||||
unset($this->files[$i]);
|
||||
continue;
|
||||
}
|
||||
if (!isset($file->hash))
|
||||
if (is_array($file)) {
|
||||
if (!isset($file['hash'])) {
|
||||
$file['hash'] = $this->filehash;
|
||||
}
|
||||
} else if (is_object($file)) {
|
||||
if (!isset($file->hash)) {
|
||||
$file->hash = $this->filehash;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->subject = utf8tohtml($this->subject);
|
||||
$this->name = utf8tohtml($this->name);
|
||||
@@ -394,7 +401,18 @@ class Post {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -458,10 +476,22 @@ class Thread {
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -21,9 +21,7 @@ loadConfig();
|
||||
|
||||
function init_locale($locale, $error='error') {
|
||||
if (extension_loaded('gettext')) {
|
||||
if (setlocale(LC_ALL, $locale) === false) {
|
||||
//$error('The specified locale (' . $locale . ') does not exist on your platform!');
|
||||
}
|
||||
setlocale(LC_ALL, $locale);
|
||||
bindtextdomain('tinyboard', './inc/locale');
|
||||
bind_textdomain_codeset('tinyboard', 'UTF-8');
|
||||
textdomain('tinyboard');
|
||||
@@ -56,7 +54,8 @@ function loadConfig() {
|
||||
|
||||
if (isset($config['cache_config']) &&
|
||||
$config['cache_config'] &&
|
||||
$config = Cache::get('config_' . $boardsuffix ) ) {
|
||||
$config = Cache::get('config_' . $boardsuffix))
|
||||
{
|
||||
$events = Cache::get('events_' . $boardsuffix );
|
||||
|
||||
define_groups();
|
||||
@@ -69,8 +68,7 @@ function loadConfig() {
|
||||
$current_locale = $config['locale'];
|
||||
init_locale($config['locale'], $error);
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$config = array();
|
||||
|
||||
reset_events();
|
||||
@@ -242,13 +240,14 @@ function loadConfig() {
|
||||
$__version = file_exists('.installed') ? trim(file_get_contents('.installed')) : false;
|
||||
$config['version'] = $__version;
|
||||
|
||||
if ($config['allow_roll'])
|
||||
event_handler('post', 'diceRoller');
|
||||
if ($config['allow_roll']) {
|
||||
event_handler('post', 'email_dice_roll');
|
||||
}
|
||||
|
||||
if (in_array('webm', $config['allowed_ext_files']) ||
|
||||
in_array('mp4', $config['allowed_ext_files']))
|
||||
if (in_array('webm', $config['allowed_ext_files']) || in_array('mp4', $config['allowed_ext_files'])) {
|
||||
event_handler('post', 'postHandler');
|
||||
}
|
||||
}
|
||||
// Effectful config processing below:
|
||||
|
||||
date_default_timezone_set($config['timezone']);
|
||||
@@ -280,8 +279,7 @@ function loadConfig() {
|
||||
if ($config['cache']['enabled'])
|
||||
require_once 'inc/cache.php';
|
||||
|
||||
if (in_array('webm', $config['allowed_ext_files']) ||
|
||||
in_array('mp4', $config['allowed_ext_files']))
|
||||
if (in_array('webm', $config['allowed_ext_files']) || in_array('mp4', $config['allowed_ext_files']))
|
||||
require_once 'inc/lib/webm/posthandler.php';
|
||||
|
||||
event('load-config');
|
||||
@@ -393,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) {
|
||||
@@ -517,12 +407,11 @@ function mb_substr_replace($string, $replacement, $start, $length) {
|
||||
function setupBoard($array) {
|
||||
global $board, $config;
|
||||
|
||||
$board = array(
|
||||
$board = [
|
||||
'uri' => $array['uri'],
|
||||
'title' => $array['title'],
|
||||
'subtitle' => $array['subtitle'],
|
||||
#'indexed' => $array['indexed'],
|
||||
);
|
||||
];
|
||||
|
||||
// older versions
|
||||
$board['name'] = &$board['title'];
|
||||
@@ -718,13 +607,19 @@ function file_unlink($path) {
|
||||
$debug['unlink'][] = $path;
|
||||
}
|
||||
|
||||
if (file_exists($path)) {
|
||||
$ret = @unlink($path);
|
||||
} else {
|
||||
$ret = true;
|
||||
}
|
||||
|
||||
if ($config['gzip_static']) {
|
||||
$gzpath = "$path.gz";
|
||||
|
||||
if (file_exists($gzpath)) {
|
||||
@unlink($gzpath);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($config['purge']) && $path[0] != '/' && isset($_SERVER['HTTP_HOST'])) {
|
||||
// Purge cache
|
||||
@@ -797,42 +692,6 @@ function listBoards($just_uri = false) {
|
||||
return $boards;
|
||||
}
|
||||
|
||||
function until($timestamp) {
|
||||
$difference = $timestamp - time();
|
||||
switch(TRUE){
|
||||
case ($difference < 60):
|
||||
return $difference . ' ' . ngettext('second', 'seconds', $difference);
|
||||
case ($difference < 3600): //60*60 = 3600
|
||||
return ($num = round($difference/(60))) . ' ' . ngettext('minute', 'minutes', $num);
|
||||
case ($difference < 86400): //60*60*24 = 86400
|
||||
return ($num = round($difference/(3600))) . ' ' . ngettext('hour', 'hours', $num);
|
||||
case ($difference < 604800): //60*60*24*7 = 604800
|
||||
return ($num = round($difference/(86400))) . ' ' . ngettext('day', 'days', $num);
|
||||
case ($difference < 31536000): //60*60*24*365 = 31536000
|
||||
return ($num = round($difference/(604800))) . ' ' . ngettext('week', 'weeks', $num);
|
||||
default:
|
||||
return ($num = round($difference/(31536000))) . ' ' . ngettext('year', 'years', $num);
|
||||
}
|
||||
}
|
||||
|
||||
function ago($timestamp) {
|
||||
$difference = time() - $timestamp;
|
||||
switch(TRUE){
|
||||
case ($difference < 60) :
|
||||
return $difference . ' ' . ngettext('second', 'seconds', $difference);
|
||||
case ($difference < 3600): //60*60 = 3600
|
||||
return ($num = round($difference/(60))) . ' ' . ngettext('minute', 'minutes', $num);
|
||||
case ($difference < 86400): //60*60*24 = 86400
|
||||
return ($num = round($difference/(3600))) . ' ' . ngettext('hour', 'hours', $num);
|
||||
case ($difference < 604800): //60*60*24*7 = 604800
|
||||
return ($num = round($difference/(86400))) . ' ' . ngettext('day', 'days', $num);
|
||||
case ($difference < 31536000): //60*60*24*365 = 31536000
|
||||
return ($num = round($difference/(604800))) . ' ' . ngettext('week', 'weeks', $num);
|
||||
default:
|
||||
return ($num = round($difference/(31536000))) . ' ' . ngettext('year', 'years', $num);
|
||||
}
|
||||
}
|
||||
|
||||
function displayBan($ban) {
|
||||
global $config, $board;
|
||||
|
||||
@@ -909,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()) {
|
||||
if ($config['auto_maintenance']) {
|
||||
Bans::delete($ban['id']);
|
||||
}
|
||||
if ($config['require_ban_view'] && !$ban['seen']) {
|
||||
if (!isset($_POST['json_response'])) {
|
||||
displayBan($ban);
|
||||
@@ -933,17 +794,20 @@ function checkBan($board = false) {
|
||||
}
|
||||
}
|
||||
|
||||
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 (time() - $last_time_purged < $config['purge_bans'] )
|
||||
return;
|
||||
}
|
||||
|
||||
Bans::purge();
|
||||
|
||||
if ($config['cache']['enabled'])
|
||||
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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function threadLocked($id) {
|
||||
@@ -1431,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(
|
||||
@@ -1645,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()) {
|
||||
@@ -1737,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();
|
||||
}
|
||||
|
||||
@@ -1790,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));
|
||||
}
|
||||
@@ -2029,7 +1873,7 @@ function extract_modifiers($body) {
|
||||
}
|
||||
|
||||
function remove_modifiers($body) {
|
||||
return preg_replace('@<tinyboard ([\w\s]+)>(.+?)</tinyboard>@usm', '', $body);
|
||||
return $body ? preg_replace('@<tinyboard ([\w\s]+)>(.+?)</tinyboard>@usm', '', $body) : null;
|
||||
}
|
||||
|
||||
function markup(&$body, $track_cites = false, $op = false) {
|
||||
@@ -2298,6 +2142,7 @@ function escape_markup_modifiers($string) {
|
||||
}
|
||||
|
||||
function defined_flags_accumulate($desired_flags) {
|
||||
global $config;
|
||||
$output_flags = 0x0;
|
||||
foreach ($desired_flags as $flagname) {
|
||||
if (defined($flagname)) {
|
||||
@@ -2315,7 +2160,7 @@ function defined_flags_accumulate($desired_flags) {
|
||||
|
||||
function utf8tohtml($utf8) {
|
||||
$flags = defined_flags_accumulate(['ENT_NOQUOTES', 'ENT_SUBSTITUTE', 'ENT_DISALLOWED']);
|
||||
return htmlspecialchars($utf8, $flags, 'UTF-8');
|
||||
return $utf8 ? htmlspecialchars($utf8, $flags, 'UTF-8') : '';
|
||||
}
|
||||
|
||||
function ordutf8($string, &$offset) {
|
||||
@@ -2384,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(),
|
||||
@@ -2395,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);
|
||||
@@ -2423,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);
|
||||
@@ -2489,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),
|
||||
@@ -2498,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;
|
||||
@@ -2572,35 +2425,6 @@ function generate_tripcode($name) {
|
||||
return array($name, $trip);
|
||||
}
|
||||
|
||||
// Highest common factor
|
||||
function hcf($a, $b){
|
||||
$gcd = 1;
|
||||
if ($a>$b) {
|
||||
$a = $a+$b;
|
||||
$b = $a-$b;
|
||||
$a = $a-$b;
|
||||
}
|
||||
if ($b==(round($b/$a))*$a)
|
||||
$gcd=$a;
|
||||
else {
|
||||
for ($i=round($a/2);$i;$i--) {
|
||||
if ($a == round($a/$i)*$i && $b == round($b/$i)*$i) {
|
||||
$gcd = $i;
|
||||
$i = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $gcd;
|
||||
}
|
||||
|
||||
function fraction($numerator, $denominator, $sep) {
|
||||
$gcf = hcf($numerator, $denominator);
|
||||
$numerator = $numerator / $gcf;
|
||||
$denominator = $denominator / $gcf;
|
||||
|
||||
return "{$numerator}{$sep}{$denominator}";
|
||||
}
|
||||
|
||||
function getPostByHash($hash) {
|
||||
global $board;
|
||||
$query = prepare(sprintf("SELECT `id`,`thread` FROM ``posts_%s`` WHERE `filehash` = :hash", $board['uri']));
|
||||
@@ -2717,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;
|
||||
|
||||
@@ -2866,24 +2631,6 @@ function prettify_textarea($s){
|
||||
return str_replace("\t", '	', str_replace("\n", ' ', htmlentities($s)));
|
||||
}
|
||||
|
||||
/*class HTMLPurifier_URIFilter_NoExternalImages extends HTMLPurifier_URIFilter {
|
||||
public $name = 'NoExternalImages';
|
||||
public function filter(&$uri, $c, $context) {
|
||||
global $config;
|
||||
$ct = $context->get('CurrentToken');
|
||||
|
||||
if (!$ct || $ct->name !== 'img') return true;
|
||||
|
||||
if (!isset($uri->host) && !isset($uri->scheme)) return true;
|
||||
|
||||
if (!in_array($uri->scheme . '://' . $uri->host . '/', $config['allowed_offsite_urls'])) {
|
||||
error('No off-site links in board announcement images.');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}*/
|
||||
|
||||
function purify_html($s) {
|
||||
global $config;
|
||||
|
||||
@@ -2899,7 +2646,6 @@ function purify_html($s) {
|
||||
function markdown($s) {
|
||||
$pd = new Parsedown();
|
||||
$pd->setMarkupEscaped(true);
|
||||
$pd->setimagesEnabled(false);
|
||||
|
||||
return $pd->text($s);
|
||||
}
|
||||
@@ -2918,7 +2664,20 @@ function generation_strategy($fun, $array=array()) { global $config;
|
||||
return 'rebuild';
|
||||
case 'defer':
|
||||
// Ok, it gets interesting here :)
|
||||
get_queue('generate')->push(serialize(array('build', $fun, $array, $action)));
|
||||
$queue = Queues::get_queue($config, 'generate');
|
||||
if ($queue === false) {
|
||||
if ($config['syslog']) {
|
||||
_syslog(LOG_ERR, "Could not initialize generate queue, falling back to immediate rebuild strategy");
|
||||
}
|
||||
return 'rebuild';
|
||||
}
|
||||
$ret = $queue->push(serialize(array('build', $fun, $array, $action)));
|
||||
if ($ret === false) {
|
||||
if ($config['syslog']) {
|
||||
_syslog(LOG_ERR, "Could not push item in the queue, falling back to immediate rebuild strategy");
|
||||
}
|
||||
return 'rebuild';
|
||||
}
|
||||
return 'ignore';
|
||||
case 'build_on_load':
|
||||
return 'delete';
|
||||
@@ -3089,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>";
|
||||
}
|
||||
28
inc/functions/format.php
Normal file
28
inc/functions/format.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace Vichan\Functions\Format;
|
||||
|
||||
|
||||
function format_timestamp(int $delta): string {
|
||||
switch (true) {
|
||||
case $delta < 60:
|
||||
return $delta . ' ' . ngettext('second', 'seconds', $delta);
|
||||
case $delta < 3600: //60*60 = 3600
|
||||
return ($num = round($delta/ 60)) . ' ' . ngettext('minute', 'minutes', $num);
|
||||
case $delta < 86400: //60*60*24 = 86400
|
||||
return ($num = round($delta / 3600)) . ' ' . ngettext('hour', 'hours', $num);
|
||||
case $delta < 604800: //60*60*24*7 = 604800
|
||||
return ($num = round($delta / 86400)) . ' ' . ngettext('day', 'days', $num);
|
||||
case $delta < 31536000: //60*60*24*365 = 31536000
|
||||
return ($num = round($delta / 604800)) . ' ' . ngettext('week', 'weeks', $num);
|
||||
default:
|
||||
return ($num = round($delta / 31536000)) . ' ' . ngettext('year', 'years', $num);
|
||||
}
|
||||
}
|
||||
|
||||
function until(int $timestamp): string {
|
||||
return format_timestamp($timestamp - time());
|
||||
}
|
||||
|
||||
function ago(int $timestamp): string {
|
||||
return format_timestamp(time() - $timestamp);
|
||||
}
|
||||
16
inc/functions/net.php
Normal file
16
inc/functions/net.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
namespace Vichan\Functions\Net;
|
||||
|
||||
|
||||
/**
|
||||
* @param bool $trust_headers. If true, trust the `HTTP_X_FORWARDED_PROTO` header to check if the connection is HTTPS.
|
||||
* @return bool Returns if the client-server connection is an encrypted one (HTTPS).
|
||||
*/
|
||||
function is_connection_secure(bool $trust_headers): bool {
|
||||
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
||||
return true;
|
||||
} elseif ($trust_headers && isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
33
inc/functions/num.php
Normal file
33
inc/functions/num.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace Vichan\Functions\Num;
|
||||
|
||||
// Highest common factor
|
||||
function hcf($a, $b){
|
||||
$gcd = 1;
|
||||
|
||||
if ($a > $b) {
|
||||
$a = $a+$b;
|
||||
$b = $a-$b;
|
||||
$a = $a-$b;
|
||||
}
|
||||
if ($b == (round($b / $a)) * $a) {
|
||||
$gcd = $a;
|
||||
} else {
|
||||
for ($i = round($a / 2); $i; $i--) {
|
||||
if ($a == round($a / $i) * $i && $b == round($b / $i) * $i) {
|
||||
$gcd = $i;
|
||||
$i = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $gcd;
|
||||
}
|
||||
|
||||
function fraction($numerator, $denominator, $sep) {
|
||||
$gcf = hcf($numerator, $denominator);
|
||||
$numerator = $numerator / $gcf;
|
||||
$denominator = $denominator / $gcf;
|
||||
|
||||
return "{$numerator}{$sep}{$denominator}";
|
||||
}
|
||||
98
inc/functions/theme.php
Normal file
98
inc/functions/theme.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
namespace Vichan\Functions\Theme;
|
||||
|
||||
|
||||
function rebuild_themes(string $action, $boardname = false): void {
|
||||
global $config, $board, $current_locale;
|
||||
|
||||
// Save the global variables
|
||||
$_config = $config;
|
||||
$_board = $board;
|
||||
|
||||
// List themes
|
||||
if ($themes = \Cache::get("themes")) {
|
||||
// OK, we already have themes loaded
|
||||
} else {
|
||||
$query = query("SELECT `theme` FROM ``theme_settings`` WHERE `name` IS NULL AND `value` IS NULL") or error(db_error());
|
||||
$themes = $query->fetchAll(\PDO::FETCH_NUM);
|
||||
|
||||
\Cache::set("themes", $themes);
|
||||
}
|
||||
|
||||
foreach ($themes as $theme) {
|
||||
// Restore them
|
||||
$config = $_config;
|
||||
$board = $_board;
|
||||
|
||||
// Reload the locale
|
||||
if ($config['locale'] != $current_locale) {
|
||||
$current_locale = $config['locale'];
|
||||
init_locale($config['locale']);
|
||||
}
|
||||
|
||||
if (PHP_SAPI === 'cli') {
|
||||
echo "Rebuilding theme ".$theme['theme']."... ";
|
||||
}
|
||||
|
||||
rebuild_theme($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 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;
|
||||
}
|
||||
@@ -291,6 +291,7 @@ class ImageConvert extends ImageBase {
|
||||
} else {
|
||||
rename($this->temp, $src);
|
||||
chmod($src, 0664);
|
||||
$this->temp = false;
|
||||
}
|
||||
}
|
||||
public function width() {
|
||||
@@ -300,9 +301,11 @@ class ImageConvert extends ImageBase {
|
||||
return $this->height;
|
||||
}
|
||||
public function destroy() {
|
||||
if ($this->temp !== false) {
|
||||
@unlink($this->temp);
|
||||
$this->temp = false;
|
||||
}
|
||||
}
|
||||
public function resize() {
|
||||
global $config;
|
||||
|
||||
|
||||
79
inc/lock.php
79
inc/lock.php
@@ -1,39 +1,84 @@
|
||||
<?php
|
||||
class Lock {
|
||||
function __construct($key) { global $config;
|
||||
if ($config['lock']['enabled'] == 'fs') {
|
||||
|
||||
class Locks {
|
||||
private static function filesystem(string $key): Lock|false {
|
||||
$key = str_replace('/', '::', $key);
|
||||
$key = str_replace("\0", '', $key);
|
||||
|
||||
$this->f = fopen("tmp/locks/$key", "w");
|
||||
}
|
||||
$fd = fopen("tmp/locks/$key", "w");
|
||||
if ($fd === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get a shared lock
|
||||
function get($nonblock = false) { global $config;
|
||||
if ($config['lock']['enabled'] == 'fs') {
|
||||
return new class($fd) implements Lock {
|
||||
// Resources have no type in php.
|
||||
private mixed $f;
|
||||
|
||||
|
||||
function __construct($fd) {
|
||||
$this->f = $fd;
|
||||
}
|
||||
|
||||
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;
|
||||
if ($nonblock && $wouldblock) {
|
||||
return false;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Get an exclusive lock
|
||||
function get_ex($nonblock = false) { global $config;
|
||||
if ($config['lock']['enabled'] == 'fs') {
|
||||
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;
|
||||
if ($nonblock && $wouldblock) {
|
||||
return false;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Free a lock
|
||||
function free() { global $config;
|
||||
if ($config['lock']['enabled'] == 'fs') {
|
||||
public function free(): Lock {
|
||||
flock($this->f, LOCK_UN);
|
||||
}
|
||||
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): Lock|false {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Lock {
|
||||
// Get a shared lock
|
||||
public function get(bool $nonblock = false): Lock|false;
|
||||
|
||||
// Get an exclusive lock
|
||||
public function get_ex(bool $nonblock = false): Lock|false;
|
||||
|
||||
// Free a lock
|
||||
public function free(): Lock;
|
||||
}
|
||||
|
||||
171
inc/mod/auth.php
171
inc/mod/auth.php
@@ -4,10 +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($username, $password, $salt = false) {
|
||||
function mkhash(string $username, ?string $password, mixed $salt = false): array|string {
|
||||
global $config;
|
||||
|
||||
if (!$salt) {
|
||||
@@ -31,55 +34,52 @@ function mkhash($username, $password, $salt = false) {
|
||||
), 0, 20
|
||||
);
|
||||
|
||||
if (isset($generated_salt))
|
||||
return array($hash, $salt);
|
||||
else
|
||||
if (isset($generated_salt)) {
|
||||
return [ $hash, $salt ];
|
||||
} else {
|
||||
return $hash;
|
||||
}
|
||||
}
|
||||
|
||||
function crypt_password_old($password) {
|
||||
$salt = generate_salt();
|
||||
$password = hash('sha256', $salt . sha1($password));
|
||||
return array($salt, $password);
|
||||
}
|
||||
|
||||
function crypt_password($password) {
|
||||
function crypt_password(string $password): array {
|
||||
global $config;
|
||||
// `salt` database field is reused as a version value. We don't want it to be 0.
|
||||
$version = $config['password_crypt_version'] ? $config['password_crypt_version'] : 1;
|
||||
$new_salt = generate_salt();
|
||||
$password = crypt($password, $config['password_crypt'] . $new_salt . "$");
|
||||
return array($version, $password);
|
||||
return [ $version, $password ];
|
||||
}
|
||||
|
||||
function test_password($password, $salt, $test) {
|
||||
global $config;
|
||||
|
||||
function test_password(string $password, string $salt, string $test): array {
|
||||
// Version = 0 denotes an old password hashing schema. In the same column, the
|
||||
// password hash was kept previously
|
||||
$version = (strlen($salt) <= 8) ? (int) $salt : 0;
|
||||
$version = strlen($salt) <= 8 ? (int)$salt : 0;
|
||||
|
||||
if ($version == 0) {
|
||||
$comp = hash('sha256', $salt . sha1($test));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$comp = crypt($test, $password);
|
||||
}
|
||||
return array($version, hash_equals($password, $comp));
|
||||
return [ $version, hash_equals($password, $comp) ];
|
||||
}
|
||||
|
||||
function generate_salt() {
|
||||
// mcrypt_create_iv() was deprecated in PHP 7.1.0, only use it if we're below that version number.
|
||||
if (PHP_VERSION_ID < 70100) {
|
||||
// 128 bits of entropy
|
||||
return strtr(base64_encode(mcrypt_create_iv(16, MCRYPT_DEV_URANDOM)), '+', '.');
|
||||
}
|
||||
|
||||
// Otherwise, use random_bytes()
|
||||
function generate_salt(): string {
|
||||
return strtr(base64_encode(random_bytes(16)), '+', '.');
|
||||
}
|
||||
|
||||
function login($username, $password) {
|
||||
function calc_cookie_name(bool $is_https, bool $is_path_jailed, string $base_name): string {
|
||||
if ($is_https) {
|
||||
if ($is_path_jailed) {
|
||||
return "__Host-$base_name";
|
||||
} else {
|
||||
return "__Secure-$base_name";
|
||||
}
|
||||
} else {
|
||||
return $base_name;
|
||||
}
|
||||
}
|
||||
|
||||
function login(string $username, string $password): array|false {
|
||||
global $mod, $config;
|
||||
|
||||
$query = prepare("SELECT `id`, `type`, `boards`, `password`, `version` FROM ``mods`` WHERE BINARY `username` = :username");
|
||||
@@ -100,40 +100,83 @@ function login($username, $password) {
|
||||
$query->execute() or error(db_error($query));
|
||||
}
|
||||
|
||||
return $mod = array(
|
||||
return $mod = [
|
||||
'id' => $user['id'],
|
||||
'type' => $user['type'],
|
||||
'username' => $username,
|
||||
'hash' => mkhash($username, $user['password']),
|
||||
'boards' => explode(',', $user['boards'])
|
||||
);
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function setCookies() {
|
||||
function setCookies(): void {
|
||||
global $mod, $config;
|
||||
if (!$mod)
|
||||
if (!$mod) {
|
||||
error('setCookies() was called for a non-moderator!');
|
||||
}
|
||||
|
||||
setcookie($config['cookies']['mod'],
|
||||
$mod['username'] . // username
|
||||
':' .
|
||||
$mod['hash'][0] . // password
|
||||
':' .
|
||||
$mod['hash'][1], // salt
|
||||
time() + $config['cookies']['expire'], $config['cookies']['jail'] ? $config['cookies']['path'] : '/', null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', $config['cookies']['httponly']);
|
||||
$is_https = Net\is_connection_secure($config['cookies']['secure_login_only'] === 1);
|
||||
$is_path_jailed = $config['cookies']['jail'];
|
||||
$name = calc_cookie_name($is_https, $is_path_jailed, $config['cookies']['mod']);
|
||||
|
||||
// <username>:<password>:<salt>
|
||||
$value = "{$mod['username']}:{$mod['hash'][0]}:{$mod['hash'][1]}";
|
||||
|
||||
$options = [
|
||||
'expires' => time() + $config['cookies']['expire'],
|
||||
'path' => $is_path_jailed ? $config['cookies']['path'] : '/',
|
||||
'secure' => $is_https,
|
||||
'httponly' => $config['cookies']['httponly'],
|
||||
'samesite' => 'Strict'
|
||||
];
|
||||
|
||||
setcookie($name, $value, $options);
|
||||
}
|
||||
|
||||
function destroyCookies() {
|
||||
function destroyCookies(): void {
|
||||
global $config;
|
||||
// Delete the cookies
|
||||
setcookie($config['cookies']['mod'], 'deleted', time() - $config['cookies']['expire'], $config['cookies']['jail']?$config['cookies']['path'] : '/', null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true);
|
||||
$base_name = $config['cookies']['mod'];
|
||||
$del_time = time() - 60 * 60 * 24 * 365; // 1 year.
|
||||
$jailed_path = $config['cookies']['jail'] ? $config['cookies']['path'] : '/';
|
||||
$http_only = $config['cookies']['httponly'];
|
||||
|
||||
$options_multi = [
|
||||
$base_name => [
|
||||
'expires' => $del_time,
|
||||
'path' => $jailed_path ,
|
||||
'secure' => false,
|
||||
'httponly' => $http_only,
|
||||
'samesite' => 'Strict'
|
||||
],
|
||||
"__Host-$base_name" => [
|
||||
'expires' => $del_time,
|
||||
'path' => $jailed_path,
|
||||
'secure' => true,
|
||||
'httponly' => $http_only,
|
||||
'samesite' => 'Strict'
|
||||
],
|
||||
"__Secure-$base_name" => [
|
||||
'expires' => $del_time,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => $http_only,
|
||||
'samesite' => 'Strict'
|
||||
]
|
||||
];
|
||||
|
||||
foreach ($options_multi as $name => $options) {
|
||||
if (isset($_COOKIE[$name])) {
|
||||
setcookie($name, 'deleted', $options);
|
||||
unset($_COOKIE[$name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function modLog($action, $_board=null) {
|
||||
function modLog(string $action, ?string $_board = null): void {
|
||||
global $mod, $board, $config;
|
||||
$query = prepare("INSERT INTO ``modlogs`` VALUES (:id, :ip, :board, :time, :text)");
|
||||
$query->bindValue(':id', (isset($mod['id']) ? $mod['id'] : -1), PDO::PARAM_INT);
|
||||
@@ -148,16 +191,18 @@ function modLog($action, $_board=null) {
|
||||
$query->bindValue(':board', null, PDO::PARAM_NULL);
|
||||
$query->execute() or error(db_error($query));
|
||||
|
||||
if ($config['syslog'])
|
||||
if ($config['syslog']) {
|
||||
_syslog(LOG_INFO, '[mod/' . $mod['username'] . ']: ' . $action);
|
||||
}
|
||||
}
|
||||
|
||||
function create_pm_header() {
|
||||
function create_pm_header(): mixed {
|
||||
global $mod, $config;
|
||||
|
||||
if ($config['cache']['enabled'] && ($header = cache::get('pm_unread_' . $mod['id'])) != false) {
|
||||
if ($header === true)
|
||||
if ($header === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $header;
|
||||
}
|
||||
@@ -166,35 +211,45 @@ function create_pm_header() {
|
||||
$query->bindValue(':id', $mod['id'], PDO::PARAM_INT);
|
||||
$query->execute() or error(db_error($query));
|
||||
|
||||
if ($pm = $query->fetch(PDO::FETCH_ASSOC))
|
||||
$header = array('id' => $pm['id'], 'waiting' => $query->rowCount() - 1);
|
||||
else
|
||||
if ($pm = $query->fetch(PDO::FETCH_ASSOC)) {
|
||||
$header = [ 'id' => $pm['id'], 'waiting' => $query->rowCount() - 1 ];
|
||||
} else {
|
||||
$header = true;
|
||||
}
|
||||
|
||||
if ($config['cache']['enabled'])
|
||||
if ($config['cache']['enabled']) {
|
||||
cache::set('pm_unread_' . $mod['id'], $header);
|
||||
}
|
||||
|
||||
if ($header === true)
|
||||
if ($header === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $header;
|
||||
}
|
||||
|
||||
function make_secure_link_token($uri) {
|
||||
function make_secure_link_token(string $uri): string {
|
||||
global $mod, $config;
|
||||
return substr(sha1($config['cookies']['salt'] . '-' . $uri . '-' . $mod['id']), 0, 8);
|
||||
}
|
||||
|
||||
function check_login($prompt = false) {
|
||||
function check_login(Context $ctx, bool $prompt = false): void {
|
||||
global $config, $mod;
|
||||
|
||||
$is_https = Net\is_connection_secure($config['cookies']['secure_login_only'] === 1);
|
||||
$is_path_jailed = $config['cookies']['jail'];
|
||||
$expected_cookie_name = calc_cookie_name($is_https, $is_path_jailed, $config['cookies']['mod']);
|
||||
|
||||
// Validate session
|
||||
if (isset($_COOKIE[$config['cookies']['mod']])) {
|
||||
if (isset($_COOKIE[$expected_cookie_name])) {
|
||||
// Should be username:hash:salt
|
||||
$cookie = explode(':', $_COOKIE[$config['cookies']['mod']]);
|
||||
$cookie = explode(':', $_COOKIE[$expected_cookie_name]);
|
||||
if (count($cookie) != 3) {
|
||||
// Malformed cookies
|
||||
destroyCookies();
|
||||
if ($prompt) mod_login();
|
||||
if ($prompt) {
|
||||
mod_login($ctx);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -207,7 +262,9 @@ function check_login($prompt = false) {
|
||||
if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) {
|
||||
// Malformed cookies
|
||||
destroyCookies();
|
||||
if ($prompt) mod_login();
|
||||
if ($prompt) {
|
||||
mod_login($ctx);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,10 @@ function config_vars() {
|
||||
$temp_comment = false;
|
||||
}
|
||||
|
||||
if (preg_match('!^\s*\$config\[(\'log_system\'|\'captcha\')\]!', $line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('!^\s*// ([^$].*)$!', $line, $matches)) {
|
||||
if ($var['default'] !== false) {
|
||||
$line = '';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,49 +1,98 @@
|
||||
<?php
|
||||
|
||||
class Queue {
|
||||
function __construct($key) { global $config;
|
||||
if ($config['queue']['enabled'] == 'fs') {
|
||||
$this->lock = new Lock($key);
|
||||
class Queues {
|
||||
private static $queues = array();
|
||||
|
||||
|
||||
/**
|
||||
* This queue implementation isn't actually ordered, so it works more as a "bag".
|
||||
*/
|
||||
private static function filesystem(string $key, Lock $lock): Queue {
|
||||
$key = str_replace('/', '::', $key);
|
||||
$key = str_replace("\0", '', $key);
|
||||
$this->key = "tmp/queue/$key/";
|
||||
}
|
||||
$key = "tmp/queue/$key/";
|
||||
|
||||
return new class($key, $lock) implements Queue {
|
||||
private Lock $lock;
|
||||
private string $key;
|
||||
|
||||
|
||||
function __construct(string $key, Lock $lock) {
|
||||
$this->lock = $lock;
|
||||
$this->key = $key;
|
||||
}
|
||||
|
||||
function push($str) { global $config;
|
||||
if ($config['queue']['enabled'] == 'fs') {
|
||||
public function push(string $str): bool {
|
||||
$this->lock->get_ex();
|
||||
file_put_contents($this->key.microtime(true), $str);
|
||||
$ret = file_put_contents($this->key . microtime(true), $str);
|
||||
$this->lock->free();
|
||||
}
|
||||
return $this;
|
||||
return $ret !== false;
|
||||
}
|
||||
|
||||
function pop($n = 1) { global $config;
|
||||
if ($config['queue']['enabled'] == 'fs') {
|
||||
public function pop(int $n = 1): array {
|
||||
$this->lock->get_ex();
|
||||
$dir = opendir($this->key);
|
||||
$paths = array();
|
||||
|
||||
while ($n > 0) {
|
||||
$path = readdir($dir);
|
||||
if ($path === FALSE) break;
|
||||
elseif ($path == '.' || $path == '..') continue;
|
||||
else { $paths[] = $path; $n--; }
|
||||
if ($path === false) {
|
||||
break;
|
||||
} elseif ($path == '.' || $path == '..') {
|
||||
continue;
|
||||
} else {
|
||||
$paths[] = $path;
|
||||
$n--;
|
||||
}
|
||||
}
|
||||
|
||||
$out = array();
|
||||
foreach ($paths as $v) {
|
||||
$out []= file_get_contents($this->key.$v);
|
||||
unlink($this->key.$v);
|
||||
$out[] = file_get_contents($this->key . $v);
|
||||
unlink($this->key . $v);
|
||||
}
|
||||
|
||||
$this->lock->free();
|
||||
return $out;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op. Can be used for mocking.
|
||||
*/
|
||||
public static function none(): Queue {
|
||||
return new class() implements Queue {
|
||||
public function push(string $str): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function pop(int $n = 1): array {
|
||||
return array();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static function get_queue(array $config, string $name): Queue|false {
|
||||
if (!isset(self::$queues[$name])) {
|
||||
if ($config['queue']['enabled'] == 'fs') {
|
||||
$lock = Locks::get_lock($config, $name);
|
||||
if ($lock === false) {
|
||||
return false;
|
||||
}
|
||||
self::$queues[$name] = self::filesystem($name, $lock);
|
||||
} else {
|
||||
self::$queues[$name] = self::none();
|
||||
}
|
||||
}
|
||||
return self::$queues[$name];
|
||||
}
|
||||
}
|
||||
|
||||
// Don't use the constructor. Use the get_queue function.
|
||||
$queues = array();
|
||||
interface Queue {
|
||||
// Push a string in the queue.
|
||||
public function push(string $str): bool;
|
||||
|
||||
function get_queue($name) { global $queues;
|
||||
return $queues[$name] = isset ($queues[$name]) ? $queues[$name] : new Queue($name);
|
||||
// Get a string from the queue.
|
||||
public function pop(int $n = 1): array;
|
||||
}
|
||||
|
||||
143
inc/service/captcha-queries.php
Normal file
143
inc/service/captcha-queries.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php // Verify captchas server side.
|
||||
namespace Vichan\Service;
|
||||
|
||||
use Vichan\Data\Driver\HttpDriver;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
|
||||
class ReCaptchaQuery implements RemoteCaptchaQuery {
|
||||
private HttpDriver $http;
|
||||
private string $secret;
|
||||
|
||||
/**
|
||||
* Creates a new ReCaptchaQuery using the google recaptcha service.
|
||||
*
|
||||
* @param HttpDriver $http The http client.
|
||||
* @param string $secret Server side secret.
|
||||
* @return ReCaptchaQuery A new ReCaptchaQuery query instance.
|
||||
*/
|
||||
public function __construct(HttpDriver $http, string $secret) {
|
||||
$this->http = $http;
|
||||
$this->secret = $secret;
|
||||
}
|
||||
|
||||
public function responseField(): string {
|
||||
return 'g-recaptcha-response';
|
||||
}
|
||||
|
||||
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, 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 $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 $user_text, string $user_cookie): bool {
|
||||
$data = [
|
||||
'mode' => 'check',
|
||||
'text' => $user_text,
|
||||
'extra' => $this->extra,
|
||||
'cookie' => $user_cookie
|
||||
];
|
||||
|
||||
$ret = $this->http->requestGet($this->domain . '/' . $this->provider_check, $data);
|
||||
return $ret === '1';
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,14 @@ $twig = false;
|
||||
function load_twig() {
|
||||
global $twig, $config;
|
||||
|
||||
$cache_dir = "{$config['dir']['template']}/cache/";
|
||||
|
||||
$loader = new Twig\Loader\FilesystemLoader($config['dir']['template']);
|
||||
$loader->setPaths($config['dir']['template']);
|
||||
$twig = new Twig\Environment($loader, array(
|
||||
'autoescape' => false,
|
||||
'cache' => is_writable('templates') || (is_dir('templates/cache') && is_writable('templates/cache')) ?
|
||||
new Twig_Cache_TinyboardFilesystem("{$config['dir']['template']}/cache") : false,
|
||||
'cache' => is_writable('templates/') || (is_dir($cache_dir) && is_writable($cache_dir)) ?
|
||||
new TinyboardTwigCache($cache_dir) : false,
|
||||
'debug' => $config['debug'],
|
||||
'auto_reload' => $config['twig_auto_reload']
|
||||
));
|
||||
@@ -32,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;
|
||||
|
||||
@@ -58,7 +56,7 @@ function Element($templateFile, array $options) {
|
||||
}
|
||||
|
||||
// Read the template file
|
||||
if (@file_get_contents("{$config['dir']['template']}/${templateFile}")) {
|
||||
if (@file_get_contents("{$config['dir']['template']}/{$templateFile}")) {
|
||||
$body = $twig->render($templateFile, $options);
|
||||
|
||||
if ($config['minify_html'] && preg_match('/\.html$/', $templateFile)) {
|
||||
@@ -67,7 +65,33 @@ function Element($templateFile, array $options) {
|
||||
|
||||
return $body;
|
||||
} else {
|
||||
throw new Exception("Template file '${templateFile}' does not exist or is empty in '{$config['dir']['template']}'!");
|
||||
throw new Exception("Template file '{$templateFile}' does not exist or is empty in '{$config['dir']['template']}'!");
|
||||
}
|
||||
}
|
||||
|
||||
class TinyboardTwigCache extends Twig\Cache\FilesystemCache {
|
||||
private string $directory;
|
||||
|
||||
public function __construct(string $directory) {
|
||||
parent::__construct($directory);
|
||||
$this->directory = $directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function was removed in Twig 2.x due to developer views on the Twig library.
|
||||
* Who says we can't keep it for ourselves though?
|
||||
*/
|
||||
public function clear() {
|
||||
$iter = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($this->directory),
|
||||
RecursiveIteratorIterator::LEAVES_ONLY
|
||||
);
|
||||
|
||||
foreach ($iter as $file) {
|
||||
if ($file->isFile()) {
|
||||
@unlink($file->getPathname());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,8 +117,8 @@ class Tinyboard extends Twig\Extension\AbstractExtension
|
||||
new Twig\TwigFilter('date', 'twig_date_filter'),
|
||||
new Twig\TwigFilter('poster_id', 'poster_id'),
|
||||
new Twig\TwigFilter('count', 'count'),
|
||||
new Twig\TwigFilter('ago', 'ago'),
|
||||
new Twig\TwigFilter('until', 'until'),
|
||||
new Twig\TwigFilter('ago', 'Vichan\Functions\Format\ago'),
|
||||
new Twig\TwigFilter('until', 'Vichan\Functions\Format\until'),
|
||||
new Twig\TwigFilter('push', 'twig_push_filter'),
|
||||
new Twig\TwigFilter('bidi_cleanup', 'bidi_cleanup'),
|
||||
new Twig\TwigFilter('addslashes', 'addslashes'),
|
||||
@@ -113,7 +137,6 @@ class Tinyboard extends Twig\Extension\AbstractExtension
|
||||
return array(
|
||||
new Twig\TwigFunction('time', 'time'),
|
||||
new Twig\TwigFunction('floor', 'floor'),
|
||||
new Twig\TwigFunction('timezone', 'twig_timezone_function'),
|
||||
new Twig\TwigFunction('hiddenInputs', 'hiddenInputs'),
|
||||
new Twig\TwigFunction('hiddenInputsHash', 'hiddenInputsHash'),
|
||||
new Twig\TwigFunction('ratio', 'twig_ratio_function'),
|
||||
@@ -134,17 +157,18 @@ class Tinyboard extends Twig\Extension\AbstractExtension
|
||||
}
|
||||
}
|
||||
|
||||
function twig_timezone_function() {
|
||||
return 'Z';
|
||||
}
|
||||
|
||||
function twig_push_filter($array, $value) {
|
||||
array_push($array, $value);
|
||||
return $array;
|
||||
}
|
||||
|
||||
function twig_date_filter($date, $format) {
|
||||
return gmstrftime($format, $date);
|
||||
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) {
|
||||
|
||||
32
install.php
32
install.php
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
// Installation/upgrade file
|
||||
define('VERSION', '5.1.4');
|
||||
define('VERSION', '5.2.0');
|
||||
require 'inc/bootstrap.php';
|
||||
loadConfig();
|
||||
|
||||
@@ -689,6 +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.
|
||||
$httpsvalue = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
|
||||
$page['title'] = 'Pre-installation test';
|
||||
|
||||
$can_exec = true;
|
||||
@@ -734,13 +736,6 @@ if ($step == 0) {
|
||||
'required' => true,
|
||||
'message' => 'vichan requires PHP 7.4 or better.',
|
||||
),
|
||||
array(
|
||||
'category' => 'PHP',
|
||||
'name' => 'PHP ≥ 5.6',
|
||||
'result' => PHP_VERSION_ID >= 50600,
|
||||
'required' => false,
|
||||
'message' => 'vichan works best on PHP 5.6 or better.',
|
||||
),
|
||||
array(
|
||||
'category' => 'PHP',
|
||||
'name' => 'mbstring extension installed',
|
||||
@@ -856,14 +851,14 @@ if ($step == 0) {
|
||||
array(
|
||||
'category' => 'File permissions',
|
||||
'name' => getcwd() . '/templates/cache',
|
||||
'result' => is_writable('templates') || (is_dir('templates/cache') && is_writable('templates/cache')),
|
||||
'result' => is_dir('templates/cache/') && is_writable('templates/cache/'),
|
||||
'required' => true,
|
||||
'message' => 'You must give vichan permission to create (and write to) the <code>templates/cache</code> directory or performance will be drastically reduced.'
|
||||
),
|
||||
array(
|
||||
'category' => 'File permissions',
|
||||
'name' => getcwd() . '/tmp/cache',
|
||||
'result' => is_dir('tmp/cache') && is_writable('tmp/cache'),
|
||||
'result' => is_dir('tmp/cache/') && is_writable('tmp/cache/'),
|
||||
'required' => true,
|
||||
'message' => 'You must give vichan permission to write to the <code>tmp/cache</code> directory.'
|
||||
),
|
||||
@@ -874,6 +869,13 @@ if ($step == 0) {
|
||||
'required' => false,
|
||||
'message' => 'vichan does not have permission to make changes to <code>inc/secrets.php</code>. To complete the installation, you will be asked to manually copy and paste code into the file instead.'
|
||||
),
|
||||
array(
|
||||
'category' => 'Misc',
|
||||
'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.'
|
||||
),
|
||||
array(
|
||||
'category' => 'Misc',
|
||||
'name' => 'Caching available (APCu, Memcached or Redis)',
|
||||
@@ -919,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(
|
||||
@@ -988,12 +991,16 @@ if ($step == 0) {
|
||||
$queries[] = Element('posts.sql', array('board' => 'b'));
|
||||
|
||||
$sql_errors = '';
|
||||
$sql_err_count = 0;
|
||||
foreach ($queries as $query) {
|
||||
if ($mysql_version < 50503)
|
||||
$query = preg_replace('/(CHARSET=|CHARACTER SET )utf8mb4/', '$1utf8', $query);
|
||||
$query = preg_replace('/^([\w\s]*)`([0-9a-zA-Z$_\x{0080}-\x{FFFF}]+)`/u', '$1``$2``', $query);
|
||||
if (!query($query))
|
||||
$sql_errors .= '<li>' . db_error() . '</li>';
|
||||
if (!query($query)) {
|
||||
$sql_err_count++;
|
||||
$error = db_error();
|
||||
$sql_errors .= "<li>$sql_err_count<ul><li>$query</li><li>$error</li></ul></li>";
|
||||
}
|
||||
}
|
||||
|
||||
$page['title'] = 'Installation complete';
|
||||
@@ -1032,4 +1039,3 @@ if ($step == 0) {
|
||||
|
||||
echo Element('page.html', $page);
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -8,21 +8,21 @@
|
||||
* $config['additional_javascript'][] = 'js/comment-toolbar.js';
|
||||
*/
|
||||
if (active_page == 'catalog') {
|
||||
onready(function () {
|
||||
onReady(function() {
|
||||
'use strict';
|
||||
|
||||
// 'true' = enable shortcuts
|
||||
var useKeybinds = true;
|
||||
let useKeybinds = true;
|
||||
|
||||
// trigger the search 400ms after last keystroke
|
||||
var delay = 400;
|
||||
var timeoutHandle;
|
||||
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');
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
*
|
||||
*/
|
||||
|
||||
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;
|
||||
@@ -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);
|
||||
$(post).find('.postfilename').each(doOriginalFilename);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,23 +16,26 @@
|
||||
*
|
||||
*/
|
||||
|
||||
if (active_page == 'ukko' || active_page == 'thread' || active_page == 'index')
|
||||
onready(function(){
|
||||
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'))
|
||||
if ($(this).parent().parent().hasClass('video-container')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// or WEBM
|
||||
if (/^\/player\.php\?/.test($(this).parent().attr('href')))
|
||||
if (/^\/player\.php\?/.test($(this).parent().attr('href'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$(this).parent().data('expanded'))
|
||||
if (!$(this).parent().data('expanded')) {
|
||||
$(this).parent().click();
|
||||
}
|
||||
});
|
||||
|
||||
if (!$('#shrink-all-images').length) {
|
||||
@@ -41,12 +44,14 @@ onready(function(){
|
||||
|
||||
$('div#shrink-all-images a')
|
||||
.text(_('Shrink all images'))
|
||||
.click(function(){
|
||||
.click(function() {
|
||||
$('a img.full-image').each(function() {
|
||||
if ($(this).parent().data('expanded'))
|
||||
if ($(this).parent().data('expanded')) {
|
||||
$(this).parent().click();
|
||||
}
|
||||
});
|
||||
$(this).parent().remove();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -2,26 +2,32 @@
|
||||
/* 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;
|
||||
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();
|
||||
if (video.pause) {
|
||||
video.pause();
|
||||
}
|
||||
videoContainer.style.display = "none";
|
||||
thumb.style.display = "inline";
|
||||
video.style.maxWidth = "inherit";
|
||||
@@ -32,7 +38,9 @@ function setupVideo(thumb, url) {
|
||||
function unhover() {
|
||||
if (hovering) {
|
||||
hovering = false;
|
||||
if (video.pause) video.pause();
|
||||
if (video.pause) {
|
||||
video.pause();
|
||||
}
|
||||
videoContainer.style.display = "none";
|
||||
video.style.maxWidth = "inherit";
|
||||
video.style.maxHeight = "inherit";
|
||||
@@ -129,16 +137,18 @@ function setupVideo(thumb, url) {
|
||||
expanded = false;
|
||||
hovering = true;
|
||||
|
||||
var docRight = document.documentElement.getBoundingClientRect().right;
|
||||
var thumbRight = thumb.querySelector("img, video").getBoundingClientRect().right;
|
||||
var maxWidth = docRight - thumbRight - 20;
|
||||
if (maxWidth < 250) maxWidth = 250;
|
||||
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.position = "fixed";
|
||||
video.style.right = "0px";
|
||||
video.style.top = "0px";
|
||||
var docRight = document.documentElement.getBoundingClientRect().right;
|
||||
var thumbRight = thumb.querySelector("img, video").getBoundingClientRect().right;
|
||||
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";
|
||||
@@ -161,10 +171,18 @@ function setupVideo(thumb, url) {
|
||||
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 (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;
|
||||
@@ -202,36 +220,41 @@ function setupVideo(thumb, url) {
|
||||
}
|
||||
|
||||
function setupVideosIn(element) {
|
||||
var thumbs = element.querySelectorAll("a.file");
|
||||
for (var i = 0; i < thumbs.length; i++) {
|
||||
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 {
|
||||
var m = thumbs[i].search.match(/\bv=([^&]*)/);
|
||||
let m = thumbs[i].search.match(/\bv=([^&]*)/);
|
||||
if (m != null) {
|
||||
var url = decodeURIComponent(m[1]);
|
||||
if (/\.webm$|\.mp4$/.test(url)) setupVideo(thumbs[i], url);
|
||||
let url = decodeURIComponent(m[1]);
|
||||
if (/\.webm$|\.mp4$/.test(url)) {
|
||||
setupVideo(thumbs[i], url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onready(function(){
|
||||
onReady(function(){
|
||||
// Insert menu from settings.js
|
||||
if (typeof settingsMenu != "undefined" && typeof Options == "undefined")
|
||||
if (typeof settingsMenu != "undefined" && typeof Options == "undefined") {
|
||||
document.body.insertBefore(settingsMenu, document.getElementsByTagName("hr")[0]);
|
||||
}
|
||||
|
||||
// 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];
|
||||
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);
|
||||
}
|
||||
@@ -241,4 +264,3 @@ onready(function(){
|
||||
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);
|
||||
$('div[id^="thread_"]').each(inlineExpandingFilename);
|
||||
|
||||
// allow to work with auto-reload.js, etc.
|
||||
$(document).on('new_post', function(e, post) {
|
||||
inline_expanding_filename.call(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,15 +67,15 @@ $(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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,20 +83,19 @@ $(document).ready(function(){
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
@@ -129,6 +129,7 @@ var banlist_init = function(token, my_boards, inMod) {
|
||||
$(".banform").on("submit", function() { return false; });
|
||||
|
||||
$("#unban").on("click", function() {
|
||||
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");
|
||||
|
||||
@@ -137,6 +138,7 @@ var banlist_init = function(token, my_boards, inMod) {
|
||||
});
|
||||
|
||||
$(".banform").off("submit").submit();
|
||||
}
|
||||
});
|
||||
|
||||
if (device_type == 'desktop') {
|
||||
|
||||
@@ -375,7 +375,7 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
|
||||
|
||||
var list = getList();
|
||||
var postId = $post.find('.post_no').not('[id]').text();
|
||||
var name, trip, uid, subject, comment;
|
||||
var name, trip, uid, subject, comment, flag;
|
||||
var i, length, array, rule, pattern; // temp variables
|
||||
|
||||
var boardId = $post.data('board');
|
||||
@@ -388,6 +388,7 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
|
||||
|
||||
var hasTrip = ($post.find('.trip').length > 0);
|
||||
var hasSub = ($post.find('.subject').length > 0);
|
||||
var hasFlag = ($post.find('.flag').length > 0);
|
||||
|
||||
$post.data('hidden', false);
|
||||
$post.data('hiddenByUid', false);
|
||||
@@ -396,6 +397,7 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
|
||||
$post.data('hiddenByTrip', false);
|
||||
$post.data('hiddenBySubject', false);
|
||||
$post.data('hiddenByComment', false);
|
||||
$post.data('hiddenByFlag', false);
|
||||
|
||||
// add post with matched UID to localList
|
||||
if (hasUID &&
|
||||
@@ -436,6 +438,8 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
|
||||
});
|
||||
comment = array.join(' ');
|
||||
|
||||
if (hasFlag)
|
||||
flag = $post.find('.flag').attr('title')
|
||||
|
||||
for (i = 0, length = list.generalFilter.length; i < length; i++) {
|
||||
rule = list.generalFilter[i];
|
||||
@@ -467,6 +471,12 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
|
||||
hide(post);
|
||||
}
|
||||
break;
|
||||
case 'flag':
|
||||
if (hasFlag && pattern.test(flag)) {
|
||||
$post.data('hiddenByFlag', true);
|
||||
hide(post);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (rule.type) {
|
||||
@@ -496,6 +506,13 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
|
||||
hide(post);
|
||||
}
|
||||
break;
|
||||
case 'flag':
|
||||
pattern = new RegExp('\\b'+ rule.value+ '\\b');
|
||||
if (hasFlag && pattern.test(flag)) {
|
||||
$post.data('hiddenByFlag', true);
|
||||
hide(post);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -621,7 +638,8 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
|
||||
name: 'name',
|
||||
trip: 'tripcode',
|
||||
sub: 'subject',
|
||||
com: 'comment'
|
||||
com: 'comment',
|
||||
flag: 'flag'
|
||||
};
|
||||
|
||||
$ele.empty();
|
||||
@@ -660,6 +678,7 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
|
||||
'<option value="trip">'+_('Tripcode')+'</option>' +
|
||||
'<option value="sub">'+_('Subject')+'</option>' +
|
||||
'<option value="com">'+_('Comment')+'</option>' +
|
||||
'<option value="flag">'+_('Flag')+'</option>' +
|
||||
'</select>' +
|
||||
'<input type="text">' +
|
||||
'<input type="checkbox">' +
|
||||
|
||||
171
js/post-hover.js
171
js/post-hover.js
@@ -13,59 +13,62 @@
|
||||
*
|
||||
*/
|
||||
|
||||
onready(function(){
|
||||
var dont_fetch_again = [];
|
||||
init_hover = function() {
|
||||
var $link = $(this);
|
||||
onReady(function() {
|
||||
let dontFetchAgain = [];
|
||||
initHover = function() {
|
||||
let link = $(this);
|
||||
let id;
|
||||
let matches;
|
||||
|
||||
var id;
|
||||
var 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;
|
||||
let parentboard = board;
|
||||
|
||||
if ($link.is('[data-thread]')) parentboard = $('form[name="post"] input[name="board"]').val();
|
||||
else if (matches[1] !== undefined) board = matches[1];
|
||||
if (link.is('[data-thread]')) {
|
||||
parentboard = $('form[name="post"] input[name="board"]').val();
|
||||
} else if (matches[1] !== undefined) {
|
||||
board = matches[1];
|
||||
}
|
||||
|
||||
var $post = false;
|
||||
var hovering = false;
|
||||
var hovered_at;
|
||||
$link.hover(function(e) {
|
||||
let post = false;
|
||||
let hovering = false;
|
||||
let hoveredAt;
|
||||
link.hover(function(e) {
|
||||
hovering = true;
|
||||
hovered_at = {'x': e.pageX, 'y': e.pageY};
|
||||
hoveredAt = {'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()) {
|
||||
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');
|
||||
post.addClass('highlighted');
|
||||
} else {
|
||||
var $newPost = $post.clone();
|
||||
$newPost.find('>.reply, >br').remove();
|
||||
$newPost.find('span.mentioned').remove();
|
||||
$newPost.find('a.post_anchor').remove();
|
||||
let newPost = post.clone();
|
||||
newPost.find('>.reply, >br').remove();
|
||||
newPost.find('span.mentioned').remove();
|
||||
newPost.find('a.post_anchor').remove();
|
||||
|
||||
$newPost
|
||||
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(/#.*$/, '');
|
||||
let url = link.attr('href').replace(/#.*$/, '');
|
||||
|
||||
if($.inArray(url, dont_fetch_again) != -1) {
|
||||
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-hover').remove();
|
||||
}).mousemove(function(e) {
|
||||
if(!$post)
|
||||
return;
|
||||
|
||||
var $hover = $('#post-hover-' + id + '[data-board="' + board + '"]');
|
||||
if($hover.length == 0)
|
||||
return;
|
||||
|
||||
var scrollTop = $(window).scrollTop();
|
||||
if ($link.is("[data-thread]")) scrollTop = 0;
|
||||
var epy = e.pageY;
|
||||
if ($link.is("[data-thread]")) epy -= $(window).scrollTop();
|
||||
|
||||
var top = (epy ? epy : hovered_at['y']) - 10;
|
||||
|
||||
if(epy < scrollTop + 15) {
|
||||
top = scrollTop;
|
||||
} else if(epy > scrollTop + $(window).height() - $hover.height() - 15) {
|
||||
top = scrollTop + $(window).height() - $hover.height() - 15;
|
||||
}
|
||||
|
||||
post.removeClass('highlighted');
|
||||
if (post.hasClass('hidden') || post.data('cached') == 'yes') {
|
||||
post.css('display', 'none');
|
||||
}
|
||||
$('.post-hover').remove();
|
||||
}).mousemove(function(e) {
|
||||
if (!post) {
|
||||
return;
|
||||
}
|
||||
|
||||
$hover.css('left', (e.pageX ? e.pageX : hovered_at['x'])).css('top', top);
|
||||
let hover = $('#post-hover-' + id + '[data-board="' + board + '"]');
|
||||
if (hover.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let scrollTop = $(window).scrollTop();
|
||||
if (link.is("[data-thread]")) {
|
||||
scrollTop = 0;
|
||||
}
|
||||
let epy = e.pageY;
|
||||
if (link.is("[data-thread]")) {
|
||||
epy -= $(window).scrollTop();
|
||||
}
|
||||
|
||||
let top = (epy ? epy : hoveredAt['y']) - 10;
|
||||
|
||||
if (epy < scrollTop + 15) {
|
||||
top = scrollTop;
|
||||
} else if (epy > scrollTop + $(window).height() - hover.height() - 15) {
|
||||
top = scrollTop + $(window).height() - hover.height() - 15;
|
||||
}
|
||||
|
||||
hover.css('left', (e.pageX ? e.pageX : hoveredAt['x'])).css('top', top);
|
||||
});
|
||||
};
|
||||
|
||||
$('div.body a:not([rel="nofollow"])').each(init_hover);
|
||||
$('div.body a:not([rel="nofollow"])').each(initHover);
|
||||
|
||||
// allow to work with auto-reload.js, etc.
|
||||
$(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,12 +10,12 @@
|
||||
* 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() {
|
||||
@@ -23,7 +23,7 @@ $(document).ready(function(){
|
||||
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);
|
||||
@@ -32,81 +32,74 @@ $(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'));
|
||||
let selectedText = selection.toString();
|
||||
|
||||
if ($('body').hasClass('debug'))
|
||||
if ($('body').hasClass('debug')) {
|
||||
alert(selectedText);
|
||||
}
|
||||
|
||||
if (selectedText.length == 0)
|
||||
if (selectedText.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var body = $('textarea#body')[0];
|
||||
let body = $('textarea#body')[0];
|
||||
|
||||
var last_quote = body.value.match(/[\S.]*(^|[\S\s]*)>>(\d+)/);
|
||||
if (last_quote)
|
||||
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';
|
||||
let quote = (last_quote != postID ? '>>' + postID + '\r\n' : '') + $.trim(selectedText).replace(/\r\n/g, '\n').replace(/^/mg, '>').replace(/\n/g, '\r\n') + '\r\n';
|
||||
|
||||
// console.log('Deselecting text');
|
||||
selection.removeAllRanges();
|
||||
|
||||
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;
|
||||
@@ -121,4 +114,3 @@ $(document).ready(function(){
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,38 +13,42 @@
|
||||
*
|
||||
*/
|
||||
|
||||
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;
|
||||
let id, post, $mentioned;
|
||||
|
||||
if(id = $(this).text().match(/^>>(\d+)$/))
|
||||
if (id = $(this).text().match(/^>>(\d+)$/)) {
|
||||
id = id[1];
|
||||
else
|
||||
return;
|
||||
|
||||
$post = $('#reply_' + id);
|
||||
if($post.length == 0){
|
||||
$post = $('#op_' + id);
|
||||
if($post.length == 0)
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
$mentioned = $post.find('p.intro span.mentioned');
|
||||
if($mentioned.length == 0)
|
||||
$mentioned = $('<span class="mentioned unimportant"></span>').appendTo($post.find('p.intro'));
|
||||
|
||||
if ($mentioned.find('a.mentioned-' + reply_id).length != 0)
|
||||
$post = $('#reply_' + id);
|
||||
if ($post.length == 0){
|
||||
$post = $('#op_' + id);
|
||||
if ($post.length == 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var $link = $('<a class="mentioned-' + reply_id + '" onclick="highlightReply(\'' + reply_id + '\');" href="#' + reply_id + '">>>' +
|
||||
$mentioned = $post.find('p.intro span.mentioned');
|
||||
if($mentioned.length == 0) {
|
||||
$mentioned = $('<span class="mentioned unimportant"></span>').appendTo($post.find('p.intro'));
|
||||
}
|
||||
|
||||
if ($mentioned.find('a.mentioned-' + reply_id).length != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
};
|
||||
@@ -31,4 +31,3 @@ onready(function(){
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -14,17 +14,18 @@
|
||||
*
|
||||
*/
|
||||
|
||||
onready(function(){
|
||||
var stylesDiv = $('div.styles');
|
||||
var stylesSelect = $('<select></select>');
|
||||
onReady(function() {
|
||||
let stylesDiv = $('div.styles');
|
||||
let stylesSelect = $('<select></select>');
|
||||
|
||||
var i = 1;
|
||||
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++;
|
||||
@@ -42,4 +43,3 @@ onready(function(){
|
||||
.append(stylesSelect)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
let videoID = $(this.parentNode).data('video');
|
||||
|
||||
$(this.parentNode).html('<iframe style="float:left;margin: 10px 20px" type="text/html" '+
|
||||
$(this.parentNode).html('<iframe style="float:left;margin: 10px 20px" type="text/html" ' +
|
||||
'width="360" height="270" src="//www.youtube.com/embed/' + videoID +
|
||||
'?autoplay=1&html5=1" allowfullscreen frameborder="0"/>');
|
||||
|
||||
@@ -42,4 +41,3 @@ onready(function(){
|
||||
do_embed_yt(post);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
39
mod.php
39
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)
|
||||
// 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']);
|
||||
|
||||
|
||||
312
post.php
312
post.php
@@ -5,6 +5,11 @@
|
||||
|
||||
require_once 'inc/bootstrap.php';
|
||||
|
||||
use Vichan\{Context, WebDependencyFactory};
|
||||
use Vichan\Data\Driver\{LogDriver, HttpDriver};
|
||||
use Vichan\Service\{RemoteCaptchaQuery, NativeCaptchaQuery};
|
||||
use Vichan\Functions\Format;
|
||||
|
||||
/**
|
||||
* Utility functions
|
||||
*/
|
||||
@@ -61,54 +66,27 @@ function strip_symbols($input) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the url's target with curl.
|
||||
*
|
||||
* @param string $url Url to the file to download.
|
||||
* @param int $timeout Request timeout in seconds.
|
||||
* @param File $fd File descriptor to save the content to.
|
||||
* @return null|string Returns a string on error.
|
||||
*/
|
||||
function download_file_into($url, $timeout, $fd) {
|
||||
$err = null;
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_URL, $url);
|
||||
curl_setopt($curl, CURLOPT_FAILONERROR, true);
|
||||
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, false);
|
||||
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
|
||||
curl_setopt($curl, CURLOPT_USERAGENT, 'Tinyboard');
|
||||
curl_setopt($curl, CURLOPT_FILE, $fd);
|
||||
curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
|
||||
curl_setopt($curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
|
||||
|
||||
if (curl_exec($curl) === false) {
|
||||
$err = curl_error($curl);
|
||||
}
|
||||
|
||||
curl_close($curl);
|
||||
return $err;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a remote file from the given url.
|
||||
* The file is deleted at shutdown.
|
||||
*
|
||||
* @param HttpDriver $http The http client.
|
||||
* @param string $file_url The url to download the file from.
|
||||
* @param int $request_timeout Timeout to retrieve the file.
|
||||
* @param array $extra_extensions Allowed file extensions.
|
||||
* @param string $tmp_dir Temporary directory to save the file into.
|
||||
* @param array $error_array An array with error codes, used to create exceptions on failure.
|
||||
* @return array Returns an array describing the file on success.
|
||||
* @throws Exception on error.
|
||||
* @return array|false Returns an array describing the file on success, or false if the file was too large
|
||||
* @throws InvalidArgumentException|RuntimeException Throws on invalid arguments and IO errors.
|
||||
*/
|
||||
function download_file_from_url($file_url, $request_timeout, $allowed_extensions, $tmp_dir, &$error_array) {
|
||||
function download_file_from_url(HttpDriver $http, $file_url, $request_timeout, $allowed_extensions, $tmp_dir, &$error_array) {
|
||||
if (!preg_match('@^https?://@', $file_url)) {
|
||||
throw new InvalidArgumentException($error_array['invalidimg']);
|
||||
}
|
||||
|
||||
if (mb_strpos($file_url, '?') !== false) {
|
||||
$url_without_params = mb_substr($file_url, 0, mb_strpos($file_url, '?'));
|
||||
$param_idx = mb_strpos($file_url, '?');
|
||||
if ($param_idx !== false) {
|
||||
$url_without_params = mb_substr($file_url, 0, $param_idx);
|
||||
} else {
|
||||
$url_without_params = $file_url;
|
||||
}
|
||||
@@ -128,10 +106,13 @@ function download_file_from_url($file_url, $request_timeout, $allowed_extensions
|
||||
|
||||
$fd = fopen($tmp_file, 'w');
|
||||
|
||||
$dl_err = download_file_into($fd, $request_timeout, $fd);
|
||||
try {
|
||||
$success = $http->requestGetInto($url_without_params, null, $fd, $request_timeout);
|
||||
if (!$success) {
|
||||
return false;
|
||||
}
|
||||
} finally {
|
||||
fclose($fd);
|
||||
if ($dl_err !== null) {
|
||||
throw new Exception($error_array['nomove'] . '<br/>Curl says: ' . $dl_err);
|
||||
}
|
||||
|
||||
return array(
|
||||
@@ -165,6 +146,7 @@ function ocr_image(array $config, string $img_path): string {
|
||||
return trim($ret);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Trim an image's EXIF metadata
|
||||
*
|
||||
@@ -173,7 +155,7 @@ function ocr_image(array $config, string $img_path): string {
|
||||
* @throws RuntimeException Throws on IO errors.
|
||||
*/
|
||||
function strip_image_metadata(string $img_path): int {
|
||||
$err = shell_exec_error('exiftool -overwrite_original -ignoreMinorErrors -q -q -all= ' . escapeshellarg($img_path));
|
||||
$err = shell_exec_error('exiftool -overwrite_original -ignoreMinorErrors -q -q -all= -Orientation ' . escapeshellarg($img_path));
|
||||
if ($err === false) {
|
||||
throw new RuntimeException('Could not strip EXIF metadata!');
|
||||
}
|
||||
@@ -225,6 +207,7 @@ function delete_cyclical_posts(string $boardUri, int $threadId, int $cycleLimit)
|
||||
*/
|
||||
|
||||
$dropped_post = false;
|
||||
$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']) {
|
||||
@@ -303,7 +286,7 @@ if (isset($_GET['Newsgroups']) && $config['nntpchan']['enabled']) {
|
||||
$content = file_get_contents("php://input");
|
||||
}
|
||||
elseif ($ct == 'multipart/mixed' || $ct == 'multipart/form-data') {
|
||||
_syslog(LOG_INFO, "MM: Files: ".print_r($GLOBALS, true)); // Debug
|
||||
$context->get(LogDriver::class)->log(LogDriver::DEBUG, 'MM: Files: ' . print_r($GLOBALS, true));
|
||||
|
||||
$content = '';
|
||||
|
||||
@@ -370,7 +353,7 @@ if (isset($_GET['Newsgroups']) && $config['nntpchan']['enabled']) {
|
||||
$ret[] = ">>".$v['id'];
|
||||
}
|
||||
}
|
||||
return implode($ret, ", ");
|
||||
return implode(", ", $ret);
|
||||
}
|
||||
}, $content);
|
||||
|
||||
@@ -399,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) {
|
||||
@@ -447,14 +431,16 @@ if (isset($_POST['delete'])) {
|
||||
}
|
||||
|
||||
if ($post['time'] < time() - $config['max_delete_time'] && $config['max_delete_time'] != false) {
|
||||
error(sprintf($config['error']['delete_too_late'], until($post['time'] + $config['max_delete_time'])));
|
||||
error(sprintf($config['error']['delete_too_late'], Format\until($post['time'] + $config['max_delete_time'])));
|
||||
}
|
||||
|
||||
if ($password != '' && $post['password'] != $password && (!$thread || $thread['password'] != $password))
|
||||
if (!hash_equals($post['password'], $password) && (!$thread || !hash_equals($thread['password'], $password))) {
|
||||
error($config['error']['invalidpassword']);
|
||||
}
|
||||
|
||||
if ($post['time'] > time() - $config['delete_time'] && (!$thread || $thread['password'] != $password)) {
|
||||
error(sprintf($config['error']['delete_too_soon'], until($post['time'] + $config['delete_time'])));
|
||||
|
||||
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'])));
|
||||
}
|
||||
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
@@ -468,8 +454,9 @@ if (isset($_POST['delete'])) {
|
||||
modLog("User at $ip deleted their own post #$id");
|
||||
}
|
||||
|
||||
_syslog(LOG_INFO, 'Deleted post: ' .
|
||||
'/' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '')
|
||||
$context->get(LogDriver::class)->log(
|
||||
LogDriver::INFO,
|
||||
'Deleted post: /' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -490,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']))
|
||||
@@ -522,23 +509,31 @@ if (isset($_POST['delete'])) {
|
||||
if (count($report) > $config['report_limit'])
|
||||
error($config['error']['toomanyreports']);
|
||||
|
||||
if ($config['report_captcha'] && !isset($_POST['captcha_text'], $_POST['captcha_cookie'])) {
|
||||
|
||||
if ($config['report_captcha']) {
|
||||
if (!isset($_POST['captcha_text'], $_POST['captcha_cookie'])) {
|
||||
error($config['error']['bot']);
|
||||
}
|
||||
|
||||
if ($config['report_captcha']) {
|
||||
$ch = curl_init($config['domain'].'/'.$config['captcha']['provider_check'] . "?" . http_build_query([
|
||||
'mode' => 'check',
|
||||
'text' => $_POST['captcha_text'],
|
||||
'extra' => $config['captcha']['extra'],
|
||||
'cookie' => $_POST['captcha_cookie']
|
||||
]));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
$resp = curl_exec($ch);
|
||||
try {
|
||||
$query = new NativeCaptchaQuery(
|
||||
$context->get(HttpDriver::class),
|
||||
$config['domain'],
|
||||
$config['captcha']['provider_check'],
|
||||
$config['captcha']['extra']
|
||||
);
|
||||
$success = $query->verify(
|
||||
$_POST['captcha_text'],
|
||||
$_POST['captcha_cookie']
|
||||
);
|
||||
|
||||
if ($resp !== '1') {
|
||||
if (!$success) {
|
||||
error($config['error']['captcha']);
|
||||
}
|
||||
} catch (RuntimeException $e) {
|
||||
$context->get(LogDriver::class)->log(LogDriver::ERROR, "Native captcha IO exception: {$e->getMessage()}");
|
||||
error($config['error']['local_io_error']);
|
||||
}
|
||||
}
|
||||
|
||||
$reason = escape_markup_modifiers($_POST['reason']);
|
||||
@@ -555,9 +550,7 @@ if (isset($_POST['delete'])) {
|
||||
|
||||
$post = $query->fetch(PDO::FETCH_ASSOC);
|
||||
if ($post === false) {
|
||||
if ($config['syslog']) {
|
||||
_syslog(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']);
|
||||
}
|
||||
|
||||
@@ -566,10 +559,11 @@ if (isset($_POST['delete'])) {
|
||||
error($error);
|
||||
}
|
||||
|
||||
if ($config['syslog'])
|
||||
_syslog(LOG_INFO, 'Reported post: ' .
|
||||
'/' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '') .
|
||||
' for "' . $reason . '"'
|
||||
$context->get(LogDriver::class)->log(
|
||||
LogDriver::INFO,
|
||||
'Reported post: /'
|
||||
. $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '')
|
||||
. " for \"$reason\""
|
||||
);
|
||||
$query = prepare("INSERT INTO ``reports`` VALUES (NULL, :time, :ip, :board, :post, :reason)");
|
||||
$query->bindValue(':time', time(), PDO::PARAM_INT);
|
||||
@@ -633,62 +627,59 @@ if (isset($_POST['delete'])) {
|
||||
// Check if banned
|
||||
checkBan($board['uri']);
|
||||
|
||||
// Check for CAPTCHA right after opening the board so the "return" link is in there
|
||||
if ($config['recaptcha']) {
|
||||
if (!isset($_POST['g-recaptcha-response']))
|
||||
error($config['error']['bot']);
|
||||
// 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'];
|
||||
|
||||
// Check what reCAPTCHA has to say...
|
||||
$resp = json_decode(file_get_contents(sprintf('https://www.recaptcha.net/recaptcha/api/siteverify?secret=%s&response=%s&remoteip=%s',
|
||||
$config['recaptcha_private'],
|
||||
urlencode($_POST['g-recaptcha-response']),
|
||||
$_SERVER['REMOTE_ADDR'])), true);
|
||||
// With our custom captcha provider
|
||||
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 (!$resp['success']) {
|
||||
error($config['error']['captcha']);
|
||||
}
|
||||
}
|
||||
// hCaptcha
|
||||
if ($config['hcaptcha']) {
|
||||
if (!isset($_POST['h-captcha-response'])) {
|
||||
error($config['error']['bot']);
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'secret' => $config['hcaptcha_private'],
|
||||
'response' => $_POST['h-captcha-response'],
|
||||
'remoteip' => $_SERVER['REMOTE_ADDR']
|
||||
if (!$success) {
|
||||
error(
|
||||
"{$config['error']['captcha']}
|
||||
<script>
|
||||
if (actually_load_captcha !== undefined)
|
||||
actually_load_captcha(
|
||||
\"{$config['captcha']['provider_get']}\",
|
||||
\"{$config['captcha']['extra']}\"
|
||||
);
|
||||
</script>"
|
||||
);
|
||||
}
|
||||
}
|
||||
// Remote 3rd party captchas.
|
||||
elseif ($provider && (!$dynamic || $dynamic === $_SERVER['REMOTE_ADDR'])) {
|
||||
$query = $content->get(RemoteCaptchaQuery::class);
|
||||
$field = $query->responseField();
|
||||
|
||||
$hcaptchaverify = curl_init();
|
||||
curl_setopt($hcaptchaverify, CURLOPT_URL, "https://hcaptcha.com/siteverify");
|
||||
curl_setopt($hcaptchaverify, CURLOPT_POST, true);
|
||||
curl_setopt($hcaptchaverify, CURLOPT_POSTFIELDS, http_build_query($data));
|
||||
curl_setopt($hcaptchaverify, CURLOPT_RETURNTRANSFER, true);
|
||||
$hcaptcharesponse = curl_exec($hcaptchaverify);
|
||||
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'];
|
||||
|
||||
$resp = json_decode($hcaptcharesponse, true); // Decoding $hcaptcharesponse instead of $response
|
||||
|
||||
if (!$resp['success']) {
|
||||
$success = $query->verify($response, $ip);
|
||||
if (!$success) {
|
||||
error($config['error']['captcha']);
|
||||
}
|
||||
}
|
||||
// Same, but now with our custom captcha provider
|
||||
if (($config['captcha']['enabled']) || (($post['op']) && ($config['new_thread_capt'])) ) {
|
||||
$ch = curl_init($config['domain'].'/'.$config['captcha']['provider_check'] . "?" . http_build_query([
|
||||
'mode' => 'check',
|
||||
'text' => $_POST['captcha_text'],
|
||||
'extra' => $config['captcha']['extra'],
|
||||
'cookie' => $_POST['captcha_cookie']
|
||||
]));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
$resp = curl_exec($ch);
|
||||
} catch (RuntimeException $e) {
|
||||
$context->get(LogDriver::class)->log(LogDriver::ERROR, "Captcha IO exception: {$e->getMessage()}");
|
||||
error($config['error']['remote_io_error']);
|
||||
} catch (JsonException $e) {
|
||||
$context->get(LogDriver::class)->log(LogDriver::ERROR, "Bad JSON reply to captcha: {$e->getMessage()}");
|
||||
error($config['error']['remote_io_error']);
|
||||
}
|
||||
|
||||
if ($resp !== '1') {
|
||||
error($config['error']['captcha'] .
|
||||
'<script>if (actually_load_captcha !== undefined) actually_load_captcha("'.$config['captcha']['provider_get'].'", "'.$config['captcha']['extra'].'");</script>');
|
||||
}
|
||||
}
|
||||
|
||||
if (!(($post['op'] && $_POST['post'] == $config['button_newtopic']) ||
|
||||
(!$post['op'] && $_POST['post'] == $config['button_reply'])))
|
||||
@@ -703,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']);
|
||||
@@ -721,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();
|
||||
}
|
||||
@@ -792,7 +777,21 @@ if (isset($_POST['delete'])) {
|
||||
}
|
||||
|
||||
try {
|
||||
$_FILES['file'] = download_file_from_url($_POST['file_url'], $config['upload_by_url_timeout'], $allowed_extensions, $config['tmp'], $config['error']);
|
||||
$ret = download_file_from_url(
|
||||
$context->get(HttpDriver::class),
|
||||
$_POST['file_url'],
|
||||
$config['upload_by_url_timeout'],
|
||||
$allowed_extensions,
|
||||
$config['tmp'],
|
||||
$config['error']
|
||||
);
|
||||
if ($ret === false) {
|
||||
error(sprintf3($config['error']['filesize'], array(
|
||||
'filesz' => 'more than that',
|
||||
'maxsz' => number_format($config['max_filesize'])
|
||||
)));
|
||||
}
|
||||
$_FILES['file'] = $ret;
|
||||
} catch (Exception $e) {
|
||||
error($e->getMessage());
|
||||
}
|
||||
@@ -802,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) {
|
||||
@@ -882,7 +881,12 @@ if (isset($_POST['delete'])) {
|
||||
|
||||
$trip = generate_tripcode($post['name']);
|
||||
$post['name'] = $trip[0];
|
||||
if ($config['disable_tripcodes'] && !$mod) {
|
||||
$post['trip'] = '';
|
||||
}
|
||||
else {
|
||||
$post['trip'] = isset($trip[1]) ? $trip[1] : ''; // XX: Dropped posts and tripcodes
|
||||
}
|
||||
|
||||
$noko = false;
|
||||
if (strtolower($post['email']) == 'noko') {
|
||||
@@ -955,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']);
|
||||
|
||||
@@ -1063,7 +1065,7 @@ if (isset($_POST['delete'])) {
|
||||
if ($file['is_an_image']) {
|
||||
if ($config['ie_mime_type_detection'] !== false) {
|
||||
// Check IE MIME type detection XSS exploit
|
||||
$buffer = file_get_contents($upload, null, null, null, 255);
|
||||
$buffer = file_get_contents($upload, false, null, 0, 255);
|
||||
if (preg_match($config['ie_mime_type_detection'], $buffer)) {
|
||||
undoImage($post);
|
||||
error($config['error']['mime_exploit']);
|
||||
@@ -1083,19 +1085,24 @@ if (isset($_POST['delete'])) {
|
||||
error($config['error']['maxsize']);
|
||||
}
|
||||
|
||||
$file['exif_stripped'] = false;
|
||||
|
||||
if ($file_image_has_operable_metadata && $config['convert_auto_orient']) {
|
||||
// The following code corrects the image orientation.
|
||||
if ($config['convert_auto_orient'] && ($size[2] == IMAGETYPE_JPEG)) {
|
||||
// 'redraw_image' should already fix image orientation by itself
|
||||
if (!($config['redraw_image'])) {
|
||||
// Currently only works with the 'convert' option selected but it could easily be expanded to work with the rest if you can be bothered.
|
||||
if (!($config['redraw_image'] || (($config['strip_exif'] && !$config['use_exiftool'])))) {
|
||||
if (in_array($config['thumb_method'], array('convert', 'convert+gifsicle', 'gm', 'gm+gifsicle'))) {
|
||||
$exif = @exif_read_data($file['tmp_name']);
|
||||
$gm = in_array($config['thumb_method'], array('gm', 'gm+gifsicle'));
|
||||
if (isset($exif['Orientation']) && $exif['Orientation'] != 1) {
|
||||
$error = shell_exec_error(($gm ? 'gm ' : '') . 'convert ' .
|
||||
escapeshellarg($file['tmp_name']) . ' -auto-orient ' . escapeshellarg($file['tmp_name']));
|
||||
escapeshellarg($file['tmp_name']) . ' -auto-orient ' . escapeshellarg($upload));
|
||||
|
||||
if ($error)
|
||||
error(_('Could not auto-orient image!'), null, $error);
|
||||
$size = @getimagesize($file['tmp_name']);
|
||||
if ($config['strip_exif'])
|
||||
$file['exif_stripped'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1144,17 +1151,15 @@ if (isset($_POST['delete'])) {
|
||||
|
||||
$dont_copy_file = false;
|
||||
|
||||
if ($config['redraw_image'] || (!@$file['exif_stripped'] && $config['strip_exif'] && ($file['extension'] == 'jpg' || $file['extension'] == 'jpeg'))) {
|
||||
if ($config['redraw_image'] || ($file_image_has_operable_metadata && !$file['exif_stripped'] && $config['strip_exif'])) {
|
||||
if (!$config['redraw_image'] && $config['use_exiftool']) {
|
||||
try {
|
||||
$file['size'] = strip_image_metadata($file['tmp_name']);
|
||||
} catch (RuntimeException $e) {
|
||||
if ($config['syslog']) {
|
||||
_syslog(LOG_ERR, "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);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$image->to($file['file']);
|
||||
$dont_copy_file = true;
|
||||
@@ -1186,12 +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) {
|
||||
if ($config['syslog']) {
|
||||
_syslog(LOG_ERR, "Could not OCR image: {$e->getMessage()}");
|
||||
}
|
||||
$context->get(LogDriver::class)->log(LogDriver::ERROR, "Could not OCR image: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1340,14 +1343,22 @@ if (isset($_POST['delete'])) {
|
||||
|
||||
if (isset($_SERVER['HTTP_REFERER'])) {
|
||||
// Tell Javascript that we posted successfully
|
||||
if (isset($_COOKIE[$config['cookies']['js']]))
|
||||
if (isset($_COOKIE[$config['cookies']['js']])) {
|
||||
$js = json_decode($_COOKIE[$config['cookies']['js']]);
|
||||
else
|
||||
$js = (object) array();
|
||||
} else {
|
||||
$js = (object)array();
|
||||
}
|
||||
// Tell it to delete the cached post for referer
|
||||
$js->{$_SERVER['HTTP_REFERER']} = true;
|
||||
// Encode and set cookie
|
||||
setcookie($config['cookies']['js'], json_encode($js), 0, $config['cookies']['jail'] ? $config['cookies']['path'] : '/', null, false, false);
|
||||
|
||||
// Encode and set cookie.
|
||||
$options = [
|
||||
'expires' => 0,
|
||||
'path' => $config['cookies']['jail'] ? $config['cookies']['path'] : '/',
|
||||
'httponly' => false,
|
||||
'samesite' => 'Strict'
|
||||
];
|
||||
setcookie($config['cookies']['js'], json_encode($js), $options);
|
||||
}
|
||||
|
||||
$root = $post['mod'] ? $config['root'] . $config['file_mod'] . '?/' : $config['root'];
|
||||
@@ -1377,9 +1388,10 @@ if (isset($_POST['delete'])) {
|
||||
|
||||
buildThread($post['op'] ? $id : $post['thread']);
|
||||
|
||||
if ($config['syslog'])
|
||||
_syslog(LOG_INFO, 'New post: /' . $board['dir'] . $config['dir']['res'] .
|
||||
link_for($post) . (!$post['op'] ? '#' . $id : ''));
|
||||
$context->get(LogDriver::class)->log(
|
||||
LogDriver::INFO,
|
||||
'New post: /' . $board['dir'] . $config['dir']['res'] . link_for($post) . (!$post['op'] ? '#' . $id : '')
|
||||
);
|
||||
|
||||
if (!$post['mod']) header('X-Associated-Content: "' . $redirect . '"');
|
||||
|
||||
@@ -1410,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']))
|
||||
@@ -1420,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']);
|
||||
|
||||
@@ -9,15 +9,22 @@
|
||||
$queries_per_minutes_all = $config['search']['queries_per_minutes_all'];
|
||||
$search_limit = $config['search']['search_limit'];
|
||||
|
||||
//Is there a whitelist? Let's list those boards and if not, let's list everything.
|
||||
if (isset($config['search']['boards'])) {
|
||||
$boards = $config['search']['boards'];
|
||||
} else {
|
||||
$boards = listBoards(TRUE);
|
||||
}
|
||||
|
||||
//Let's remove any disallowed boards from the above list (the blacklist)
|
||||
if (isset($config['search']['disallowed_boards'])) {
|
||||
$boards = array_values(array_diff($boards, $config['search']['disallowed_boards']));
|
||||
}
|
||||
|
||||
$body = Element('search_form.html', Array('boards' => $boards, 'board' => isset($_GET['board']) ? $_GET['board'] : false, 'search' => isset($_GET['search']) ? str_replace('"', '"', utf8tohtml($_GET['search'])) : false));
|
||||
|
||||
if(isset($_GET['search']) && !empty($_GET['search']) && isset($_GET['board']) && in_array($_GET['board'], $boards)) {
|
||||
|
||||
$phrase = $_GET['search'];
|
||||
$_body = '';
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 5.8 KiB |
@@ -906,7 +906,8 @@ pre {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-top: 0px;
|
||||
max-width: 620px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.mentioned {
|
||||
@@ -916,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 {
|
||||
@@ -1045,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;
|
||||
}
|
||||
@@ -1195,7 +1206,6 @@ table.fileboard .intro a {
|
||||
|
||||
#gallery_images img {
|
||||
opacity: 0.6;
|
||||
-webkit-transition: all 0.5s;
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
@@ -1204,8 +1214,6 @@ 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);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
1
templates/cache/.gitkeep
vendored
1
templates/cache/.gitkeep
vendored
@@ -1 +0,0 @@
|
||||
|
||||
@@ -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,27 +1,27 @@
|
||||
<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">
|
||||
<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 }}"></script>
|
||||
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 }}"></script>{% endfor %}
|
||||
{% for javascript in config.additional_javascript %}<script type="text/javascript" src="{{ config.additional_javascript_url }}{{ javascript }}?v={{ config.resource_version }}"></script>{% endfor %}
|
||||
{% endif %}
|
||||
{% if mod %}
|
||||
<script type="text/javascript" src="/js/mod/mod_snippets.js"></script>
|
||||
<script type="text/javascript" src="/js/mod/mod_snippets.js?v={{ config.resource_version }}"></script>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if config.recaptcha %}<script src="//www.recaptcha.net/recaptcha/api.js"></script>
|
||||
<style type="text/css">{% verbatim %}
|
||||
{% 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;
|
||||
@@ -49,7 +49,7 @@
|
||||
.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 %}
|
||||
{% endverbatim %}</style>{% endif %}
|
||||
{% if config.captcha.provider.hcaptcha %}
|
||||
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
|
||||
{% endif %}
|
||||
|
||||
@@ -14,6 +14,18 @@
|
||||
</script>
|
||||
|
||||
{% include 'header.html' %}
|
||||
|
||||
{% set meta_subject %}{% if config.thread_subject_in_title and thread.subject %}{{ thread.subject|e }}{% else %}{{ thread.body_nomarkup|remove_modifiers[:256]|e }}{% endif %}{% endset %}
|
||||
|
||||
<meta name="description" content="{{ meta_subject }}" />
|
||||
<meta name="twitter:card" value="summary">
|
||||
<meta name="twitter:title" content="{{ board.url }} - {{ board.title|e }}" />
|
||||
<meta name="twitter:description" content="{{ meta_subject }}" />
|
||||
<meta name="twitter:image" content="{{ config.domain }}/{{ config.logo }}" />
|
||||
<meta property="og:title" content="{{ board.url }} - {{ board.title|e }}" />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:image" content="{{ config.domain }}/{{ config.logo }}" />
|
||||
<meta property="og:description" content="{{ meta_subject }}" />
|
||||
<title>{{ board.url }} - {{ board.title|e }}</title>
|
||||
</head>
|
||||
<body class="8chan vichan {% if mod %}is-moderator{% else %}is-not-moderator{% endif %} active-{% if not no_post_form %}index{% else %}ukko{% endif %}" data-stylesheet="{% if config.default_stylesheet.1 != '' %}{{ config.default_stylesheet.1 }}{% else %}default{% 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,43 +18,43 @@ 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)');
|
||||
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 "" + ($num = Math.round($difference/(31536000))) + ' ' + _('year(s)');
|
||||
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)');
|
||||
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 "" + ($num = Math.round($difference/(31536000))) + ' ' + _('year(s)');
|
||||
return "" + Math.round(difference/(31536000)) + ' ' + _('year(s)');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +71,8 @@ var datelocale =
|
||||
|
||||
|
||||
function alert(a, do_confirm, confirm_ok_action, confirm_cancel_action) {
|
||||
var handler, div, bg, closebtn, okbtn;
|
||||
var close = function() {
|
||||
let handler, div, bg, closebtn, okbtn;
|
||||
let close = function() {
|
||||
handler.fadeOut(400, function() { handler.remove(); });
|
||||
return false;
|
||||
};
|
||||
@@ -133,20 +133,46 @@ function changeStyle(styleName, link) {
|
||||
{% 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 = '';
|
||||
}
|
||||
}
|
||||
@@ -155,12 +181,14 @@ function changeStyle(styleName, 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 %}
|
||||
|
||||
@@ -170,7 +198,7 @@ function changeStyle(styleName, link) {
|
||||
|
||||
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);
|
||||
@@ -212,46 +240,78 @@ function init_stylechooser() {
|
||||
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;
|
||||
}
|
||||
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(/( |^)## .+$/, '');
|
||||
}
|
||||
@@ -269,18 +329,14 @@ function dopost(form) {
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -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,25 +364,29 @@ 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);
|
||||
@@ -350,14 +410,15 @@ 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 %}
|
||||
{% if config.allow_delete %}
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 +1 @@
|
||||
<time datetime="{{ post.time|date('%Y-%m-%dT%H:%M:%S') }}{{ timezone() }}">{{ post.time|date(config.post_date) }}</time>
|
||||
<time datetime="{{ post.time|date('Y-m-d\\TH:i:s\Z') }}">{{ post.time|date(config.post_date) }}</time>
|
||||
|
||||
@@ -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.native.captcha.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 %}
|
||||
{% 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>
|
||||
@@ -169,14 +158,12 @@
|
||||
<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">
|
||||
<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() }}
|
||||
</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,
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
{% else %}
|
||||
<em>no subject</em>
|
||||
{% endif %}
|
||||
<span class="unimportant"> — by {{ entry.name }} at {{ entry.time|date(config.post_date, config.timezone) }}</span>
|
||||
<span class="unimportant"> — by {{ entry.name }} at {{ entry.time|date(config.post_date) }}</span>
|
||||
</h2>
|
||||
<p>{{ entry.body }}</p>
|
||||
{% endfor %}
|
||||
|
||||
@@ -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('%b %d %H:%M')}}">
|
||||
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();
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<meta name="twitter:card" value="summary">
|
||||
<meta name="twitter:title" content="{{ settings.title }}" />
|
||||
<meta name="twitter:image" content="{{ config.domain }}/{{ config.logo }}" />
|
||||
<meta property="og:title" content="{{ settings.title }}" />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content="{{ config.domain }}" />
|
||||
<meta property="og:image" content="{{ config.domain }}/{{ config.logo }}" />
|
||||
<link rel="stylesheet" media="screen" href="{{ config.url_stylesheet }}">
|
||||
<style type="text/css">
|
||||
iframe{border:none;margin:0;padding:0;height:99%;position:absolute}
|
||||
|
||||
@@ -34,17 +34,28 @@
|
||||
</ul>
|
||||
</fieldset>
|
||||
<br>
|
||||
{% 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>
|
||||
<iframe class ="videoofnow" width="560" height="315" src="{{ settings.videoofnow }}"></iframe>
|
||||
{% 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',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user