Compare commits

..

2 Commits

Author SHA1 Message Date
Fredrick Brennan
2d092461fc Vichan has reached EOL (oops i eated it)
Close #543.
Close #544.
Close #543.
Close #417.
Close #384.
Close #351.
Close #350.
Close #345.
Close #327.
Close #188.
2023-03-23 06:09:51 -04:00
Fredrick Brennan
d4e25308ad
Vichan has reached EOL. 2023-03-21 03:30:15 -04:00
921 changed files with 5683 additions and 8555 deletions

View File

@ -1,4 +0,0 @@
**/.git
**/.gitignore
/local-instances
**/.gitkeep

View File

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

View File

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

135
README.md
View File

@ -1,134 +1,7 @@
vichan - A lightweight and full featured PHP imageboard. **Vichan has reached END OF LIFE (EOL). See [#543](https://github.com/vichan-devel/vichan/issues/543). It will receive NO FURTHER PATCHES, neither for security or anything else, unless the situation therein described substantially changes. Thank you for your years of interest, but this horse is dead. ~ Fredrick R. Brennan <<copypaste@kittens.ph>>**
========================================================
**Please do not contact Fredrick Brennan in regards to vichan issues.** To see this repository as it appeared before Vichan reached EOL, please see commit `d4e25308ad784c0bdc7acfba879b094f46de9807`. It is tagged `final-pre-eol`.
As of 29 August 2022 it supports PHP8.1. # oops i eated it
About ![](vichan/static/OOPS_I_EATED_IT.png)
------------
vichan is a free light-weight, fast, highly configurable and user-friendly
imageboard software package. It is written in PHP and has few dependencies.
Some documentation may be found on our [wiki](https://github.com/vichan-devel/vichan/wiki). (feel free to contribute)
History
------------
vichan is a fork of (now defunc'd) [Tinyboard](http://github.com/savetheinternet/Tinyboard),
a great imageboard package, actively building on it and adding a lot of features and other
improvements.
![](static/doc/timeline.svg)
### Maintainer timeline
1. [@perdedora](https://github.com/perdedora) and [@RealAngeleno](https://github.com/RealAngeleno) - 2023-Present.
2. Development Commission lead by [@basedgentoo](https://github.com/basedgentoo), [@kuz-sysadmin](https://github.com/kuz-sysadmin), and [@RealAngeleno](https://github.com/RealAngeleno). (2023 - 2023)
3. [@h00j](https://github.com/h00j) (2021 - ???)
4. [@ctrlcctrlv](https://github.com/ctrlcctrlv) (2017 - 2021)
5. [@czaks](https://github.com/czaks) (2014 - 2017) (The author of vichan fork)
6. [@savetheinternet](https://github.com/savetheinternet) (2010 - 2014) (The creator of Tinyboard)
Requirements
------------
1. PHP >= 7.4
2. MySQL/MariaDB server
3. [mbstring](http://www.php.net/manual/en/mbstring.installation.php)
4. [PHP GD](http://www.php.net/manual/en/intro.image.php)
5. [PHP PDO](http://www.php.net/manual/en/intro.pdo.php)
6. A Unix-like OS, preferrably FreeBSD or GNU/Linux
We try to make sure vichan is compatible with all major web servers. vichan does not include an Apache `.htaccess` file nor does it need one.
### Recommended
1. MySQL/MariaDB server >= 5.5.3
2. ImageMagick (command-line ImageMagick or GraphicsMagick preferred).
3. [APCu (Alternative PHP Cache)](http://php.net/manual/en/book.apcu.php),
[Memcached](http://www.php.net/manual/en/intro.memcached.php) or
[Redis](https://redis.io/docs/about/)
Contributing
------------
You can contribute to vichan by:
* Developing patches/improvements/translations and using GitHub to submit pull requests
* Providing feedback and suggestions
* Writing/editing documentation
Installation
-------------
1. Get the latest development version with:
git clone git://github.com/vichan-devel/vichan.git
2. run ```composer install``` inside the directory
3. Navigate to ```install.php``` in your web browser and follow the
prompts.
4. vichan should now be installed. Log in to ```mod.php``` with the
default username and password combination: **admin / password**.
Please remember to change the administrator account password.
See also: [Configuration Basics](https://github.com/vichan-devel/vichan/wiki/config).
Upgrade
-------
To upgrade from any version of Tinyboard or vichan:
Either run ```git pull``` to update your files, if you used git, or
backup your ```inc/instance-config.php```, replace all your files in place
(don't remove boards etc.), then put ```inc/instance-config.php``` back and
finally run ```install.php```.
To migrate from a Kusaba X board, use http://github.com/vichan-devel/Tinyboard-Migration
Demo
--------
Demo with the most updated version of [Vichan](https://vichan.27chan.org).
1. PHP 8.1
2. MySQL 5.7
3. KeyDB 6.2.1 (Redis)
4. NGINX 1.14.0
Support
--------
vichan is still beta software -- there are bound to be bugs. If you find a
bug, please report it.
CLI tools
-----------------
There are a few command line interface tools, based on Tinyboard-Tools. These need
to be launched from a Unix shell account (SSH, or something). They are located in a ```tools/```
directory.
You actually don't need these tools for your imageboard functioning, they are aimed
at the power users. You won't be able to run these from shared hosting accounts
(i.e. all free web servers).
Oekaki
------
vichan makes use of [wPaint](https://github.com/websanova/wPaint) for oekaki. After you pull the repository, however, you will need to download wPaint separately using git's `submodule` feature. Use the following commands:
```
git submodule init
git submodule update
```
To enable oekaki, add all the scripts listed in `js/wpaint.js` to your `instance-config.php`.
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:
https://github.com/vichan-devel/vichan-API/ .
License
--------
See [LICENSE.md](http://github.com/vichan-devel/vichan/blob/master/LICENSE.md).

8
b.php
View File

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

View File

@ -1,40 +0,0 @@
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

View File

@ -1,20 +0,0 @@
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.

View File

@ -1,8 +0,0 @@
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

View File

@ -1,34 +0,0 @@
# 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;
}

View File

@ -1,40 +0,0 @@
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;

View File

@ -1,66 +0,0 @@
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; }
}

View File

@ -1,88 +0,0 @@
# 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

View File

@ -1,16 +0,0 @@
# 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" ]

View File

@ -1,87 +0,0 @@
#!/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

View File

@ -1,2 +0,0 @@
opcache.jit_buffer_size=192M
opcache.jit=tracing

View File

@ -1,13 +0,0 @@
[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

View File

@ -1,7 +0,0 @@
zend_extension=xdebug
[xdebug]
xdebug.mode = profile
xdebug.start_with_request = start
error_reporting = E_ALL
xdebug.output_dir = /var/www/xdebug_out

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
<?php
/*
* 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.
*/

View File

@ -1,92 +0,0 @@
<?php
/*
* 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 function buildCache(): CacheDriver {
global $config;
switch ($config['cache']['enabled']) {
case 'memcached':
return new MemcachedCacheDriver(
$config['cache']['prefix'],
$config['cache']['memcached']
);
case 'redis':
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':
default:
return new ArrayCacheDriver();
}
}
public static function getCache(): CacheDriver {
static $cache;
return $cache ??= self::buildCache();
}
public static function get($key) {
global $config, $debug;
$ret = self::getCache()->get($key);
if ($ret === null) {
$ret = false;
}
if ($config['debug']) {
$debug['cached'][] = $config['cache']['prefix'] . $key . ($ret === false ? ' (miss)' : ' (hit)');
}
return $ret;
}
public static function set($key, $value, $expires = false) {
global $config, $debug;
if (!$expires) {
$expires = $config['cache']['timeout'];
}
self::getCache()->set($key, $value, $expires);
if ($config['debug']) {
$debug['cached'][] = $config['cache']['prefix'] . $key . ' (set)';
}
}
public static function delete($key) {
global $config, $debug;
self::getCache()->delete($key);
if ($config['debug']) {
$debug['cached'][] = $config['cache']['prefix'] . $key . ' (deleted)';
}
}
public static function flush() {
self::getCache()->flush();
return false;
}
}

View File

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

View File

@ -1,114 +0,0 @@
<?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>";
}

View File

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

View File

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

View File

@ -1,33 +0,0 @@
<?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}";
}

View File

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

View File

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

View File

@ -1,98 +0,0 @@
<?php
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);
$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;
}
public function push(string $str): bool {
$this->lock->get_ex();
$ret = file_put_contents($this->key . microtime(true), $str);
$this->lock->free();
return $ret !== false;
}
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--;
}
}
$out = array();
foreach ($paths as $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) {
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];
}
}
interface Queue {
// Push a string in the queue.
public function push(string $str): bool;
// Get a string from the queue.
public function pop(int $n = 1): array;
}

View File

@ -1 +0,0 @@
<?php

View File

@ -1,143 +0,0 @@
<?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';
}
}

View File

@ -1,227 +0,0 @@
<?php
/*
* Copyright (c) 2010-2013 Tinyboard Development Group
*/
require_once 'inc/bootstrap.php';
defined('TINYBOARD') or exit;
$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($cache_dir) && is_writable($cache_dir)) ?
new TinyboardTwigCache($cache_dir) : false,
'debug' => $config['debug'],
'auto_reload' => $config['twig_auto_reload']
));
if ($config['debug'])
$twig->addExtension(new \Twig\Extension\DebugExtension());
$twig->addExtension(new Tinyboard());
$twig->addExtension(new PhpMyAdmin\Twig\Extensions\I18nExtension());
}
function Element($templateFile, array $options) {
global $config, $debug, $twig, $build_pages;
if (!$twig)
load_twig();
if (isset($options['body']) && $config['debug']) {
$_debug = $debug;
if (isset($debug['start'])) {
$_debug['time']['total'] = '~' . round((microtime(true) - $_debug['start']) * 1000, 2) . 'ms';
$_debug['time']['init'] = '~' . round(($_debug['start_debug'] - $_debug['start']) * 1000, 2) . 'ms';
unset($_debug['start']);
unset($_debug['start_debug']);
}
if ($config['try_smarter'] && isset($build_pages) && !empty($build_pages))
$_debug['build_pages'] = $build_pages;
$_debug['included'] = get_included_files();
$_debug['memory'] = round(memory_get_usage(true) / (1024 * 1024), 2) . ' MiB';
$_debug['time']['db_queries'] = '~' . round($_debug['time']['db_queries'] * 1000, 2) . 'ms';
$_debug['time']['exec'] = '~' . round($_debug['time']['exec'] * 1000, 2) . 'ms';
$options['body'] .=
'<h3>Debug</h3><pre style="white-space: pre-wrap;font-size: 10px;">' .
str_replace("\n", '<br/>', utf8tohtml(print_r($_debug, true))) .
'</pre>';
}
// Read the template file
if (@file_get_contents("{$config['dir']['template']}/{$templateFile}")) {
$body = $twig->render($templateFile, $options);
if ($config['minify_html'] && preg_match('/\.html$/', $templateFile)) {
$body = trim(preg_replace("/[\t\r\n]/", '', $body));
}
return $body;
} else {
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());
}
}
}
}
class Tinyboard extends Twig\Extension\AbstractExtension
{
/**
* Returns a list of filters to add to the existing list.
*
* @return array An array of filters
*/
public function getFilters()
{
return array(
new Twig\TwigFilter('filesize', 'format_bytes'),
new Twig\TwigFilter('truncate', 'twig_truncate_filter'),
new Twig\TwigFilter('truncate_body', 'truncate'),
new Twig\TwigFilter('truncate_filename', 'twig_filename_truncate_filter'),
new Twig\TwigFilter('extension', 'twig_extension_filter'),
new Twig\TwigFilter('sprintf', 'sprintf'),
new Twig\TwigFilter('capcode', 'capcode'),
new Twig\TwigFilter('remove_modifiers', 'remove_modifiers'),
new Twig\TwigFilter('hasPermission', 'twig_hasPermission_filter'),
new Twig\TwigFilter('date', 'twig_date_filter'),
new Twig\TwigFilter('poster_id', 'poster_id'),
new Twig\TwigFilter('count', 'count'),
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'),
new Twig\TwigFilter('cloak_ip', 'cloak_ip'),
new Twig\TwigFilter('cloak_mask', 'cloak_mask'),
);
}
/**
* Returns a list of functions to add to the existing list.
*
* @return array An array of filters
*/
public function getFunctions()
{
return array(
new Twig\TwigFunction('time', 'time'),
new Twig\TwigFunction('floor', 'floor'),
new Twig\TwigFunction('hiddenInputs', 'hiddenInputs'),
new Twig\TwigFunction('hiddenInputsHash', 'hiddenInputsHash'),
new Twig\TwigFunction('ratio', 'twig_ratio_function'),
new Twig\TwigFunction('secure_link_confirm', 'twig_secure_link_confirm'),
new Twig\TwigFunction('secure_link', 'twig_secure_link'),
new Twig\TwigFunction('link_for', 'link_for')
);
}
/**
* Returns the name of the extension.
*
* @return string The extension name
*/
public function getName()
{
return 'tinyboard';
}
}
function twig_push_filter($array, $value) {
array_push($array, $value);
return $array;
}
function twig_date_filter($date, $format) {
if (is_numeric($date)) {
$date = new DateTime("@$date", new DateTimeZone('UTC'));
} else {
$date = new DateTime($date, new DateTimeZone('UTC'));
}
return $date->format($format);
}
function twig_hasPermission_filter($mod, $permission, $board = null) {
return hasPermission($permission, $board, $mod);
}
function twig_extension_filter($value, $case_insensitive = true) {
$ext = mb_substr($value, mb_strrpos($value, '.') + 1);
if($case_insensitive)
$ext = mb_strtolower($ext);
return $ext;
}
function twig_sprintf_filter( $value, $var) {
return sprintf($value, $var);
}
function twig_truncate_filter($value, $length = 30, $preserve = false, $separator = '…') {
if (mb_strlen($value) > $length) {
if ($preserve) {
if (false !== ($breakpoint = mb_strpos($value, ' ', $length))) {
$length = $breakpoint;
}
}
return mb_substr($value, 0, $length) . $separator;
}
return $value;
}
function twig_filename_truncate_filter($value, $length = 30, $separator = '…') {
if (mb_strlen($value) > $length) {
$value = strrev($value);
$array = array_reverse(explode(".", $value, 2));
$array = array_map("strrev", $array);
$filename = &$array[0];
$extension = isset($array[1]) ? $array[1] : false;
$filename = mb_substr($filename, 0, $length - ($extension ? mb_strlen($extension) + 1 : 0)) . $separator;
return implode(".", $array);
}
return $value;
}
function twig_ratio_function($w, $h) {
return fraction($w, $h, ':');
}
function twig_secure_link_confirm($text, $title, $confirm_message, $href) {
global $config;
return '<a onclick="if (event.which==2) return true;if (confirm(\'' . htmlentities(addslashes($confirm_message)) . '\')) document.location=\'?/' . htmlspecialchars(addslashes($href . '/' . make_secure_link_token($href))) . '\';return false;" title="' . htmlentities($title) . '" href="?/' . $href . '">' . $text . '</a>';
}
function twig_secure_link($href) {
return $href . '/' . make_secure_link_token($href);
}

View File

@ -1,53 +0,0 @@
/*
* catalog-link.js - This script puts a link to the catalog below the board
* subtitle and next to the board list.
* https://github.com/vichan-devel/Tinyboard/blob/master/js/catalog-link.js
*
* Released under the MIT license
* Copyright (c) 2013 copypaste <wizardchan@hush.com>
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
*
* Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/catalog-link.js';
*/
function catalog() {
var board = $("input[name='board']");
var boardValue = board.first().val();
var catalog_url = '';
if (window.location.href.includes('mod.php?/')) {
catalog_url = configRoot + 'mod.php?/' + boardValue + '/catalog.html';
} else {
catalog_url = configRoot + boardValue + '/catalog.html';
}
var pages = document.getElementsByClassName('pages')[0];
var bottom = document.getElementsByClassName('boardlist bottom')[0];
var subtitle = document.getElementsByClassName('subtitle')[0];
var link = document.createElement('a');
link.href = catalog_url;
if (!pages) {
link.textContent = '['+_('Catalog')+']';
link.style.paddingLeft = '10px';
link.style.textDecoration = "underline";
document.body.insertBefore(link, bottom);
}
if (subtitle) {
var link2 = document.createElement('a');
link2.textContent = _('Catalog');
link2.href = catalog_url;
var br = document.createElement('br');
subtitle.appendChild(br);
subtitle.appendChild(link2);
}
}
if (active_page == 'thread' || active_page == 'index') {
$(document).ready(catalog);
}

View File

@ -1,57 +0,0 @@
/*
* expand-all-images.js
* https://github.com/savetheinternet/Tinyboard/blob/master/js/expand-all-images.js
*
* Adds an "Expand all images" button to the top of the page.
*
* Released under the MIT license
* Copyright (c) 2012-2013 Michael Save <savetheinternet@tinyboard.org>
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
* Copyright (c) 2014 sinuca <#55ch@rizon.net>
*
* Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/inline-expanding.js';
* $config['additional_javascript'][] = 'js/expand-all-images.js';
*
*/
if (active_page == 'ukko' || active_page == 'thread' || active_page == 'index') {
onReady(function() {
$('hr:first').before('<div id="expand-all-images" style="text-align:right"><a class="unimportant" href="javascript:void(0)"></a></div>');
$('div#expand-all-images a')
.text(_('Expand all images'))
.click(function() {
$('a img.post-image').each(function() {
// Don't expand YouTube embeds
if ($(this).parent().parent().hasClass('video-container')) {
return;
}
// or WEBM
if (/^\/player\.php\?/.test($(this).parent().attr('href'))) {
return;
}
if (!$(this).parent().data('expanded')) {
$(this).parent().click();
}
});
if (!$('#shrink-all-images').length) {
$('hr:first').before('<div id="shrink-all-images" style="text-align:right"><a class="unimportant" href="javascript:void(0)"></a></div>');
}
$('div#shrink-all-images a')
.text(_('Shrink all images'))
.click(function() {
$('a img.full-image').each(function() {
if ($(this).parent().data('expanded')) {
$(this).parent().click();
}
});
$(this).parent().remove();
});
});
});
}

View File

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

View File

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

View File

@ -1,23 +0,0 @@
/*
* Adds 4chan-like [Start a New Thread] and [Post a Reply] buttons to pages.
*
* Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/hide-form.js';
*
*/
$(document).ready(() => {
if (active_page !== 'index' && active_page !== 'thread')
return;
let form_el = $('form[name="post"]');
let form_msg = active_page === 'index' ? 'Start a New Thread' : 'Post a Reply';
form_el.hide();
form_el.after(`<div id="show-post-form" style="font-size:175%;text-align:center;font-weight:bold">[<a href="#" style="text-decoration:none">${_(form_msg)}</a>]</div>`);
$('div#show-post-form').click(() => {
$('div#show-post-form').hide();
form_el.show();
});
});

View File

@ -1,12 +0,0 @@
/*
* mod_snippets.js
*
* Javascript snippets to be loaded when in mod mode
*
*/
function populateFormJQuery(frm, data) {
$.each(data, function(key, value){
$('[name='+key+']', frm).val(value);
});
}

View File

@ -1,177 +0,0 @@
/*
* post-hover.js
* https://github.com/savetheinternet/Tinyboard/blob/master/js/post-hover.js
*
* Released under the MIT license
* Copyright (c) 2012 Michael Save <savetheinternet@tinyboard.org>
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
* Copyright (c) 2013 Macil Tech <maciltech@gmail.com>
*
* Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/post-hover.js';
*
*/
onReady(function() {
let dontFetchAgain = [];
initHover = function() {
let link = $(this);
let id;
let matches;
if (link.is('[data-thread]')) {
id = link.attr('data-thread');
} else if (matches = link.text().match(/^>>(?:>\/([^\/]+)\/)?(\d+)$/)) {
id = matches[2];
} else {
return;
}
let board = $(this);
while (board.data('board') === undefined) {
board = board.parent();
}
let threadid;
if (link.is('[data-thread]')) {
threadid = 0;
} else {
threadid = board.attr('id').replace("thread_", "");
}
board = board.data('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];
}
let post = false;
let hovering = false;
let hoveredAt;
link.hover(function(e) {
hovering = true;
hoveredAt = {'x': e.pageX, 'y': e.pageY};
let startHover = function(link) {
if (post.is(':visible') &&
post.offset().top >= $(window).scrollTop() &&
post.offset().top + post.height() <= $(window).scrollTop() + $(window).height()) {
// post is in view
post.addClass('highlighted');
} else {
let newPost = post.clone();
newPost.find('>.reply, >br').remove();
newPost.find('span.mentioned').remove();
newPost.find('a.post_anchor').remove();
newPost
.attr('id', 'post-hover-' + id)
.attr('data-board', board)
.addClass('post-hover')
.css('border-style', 'solid')
.css('box-shadow', '1px 1px 1px #999')
.css('display', 'block')
.css('position', 'absolute')
.css('font-style', 'normal')
.css('z-index', '100')
.addClass('reply').addClass('post')
.insertAfter(link.parent())
link.trigger('mousemove');
}
};
post = $('[data-board="' + board + '"] div.post#reply_' + id + ', [data-board="' + board + '"]div#thread_' + id);
if (post.length > 0) {
startHover($(this));
} else {
let url = link.attr('href').replace(/#.*$/, '');
if ($.inArray(url, dontFetchAgain) != -1) {
return;
}
dontFetchAgain.push(url);
$.ajax({
url: url,
context: document.body,
success: function(data) {
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) {
$('[data-board="' + board + '"]#thread_' + threadid + " .post.reply:first").before($(this).hide().addClass('hidden'));
}
});
} 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) {
$('[data-board="' + board + '"]#thread_' + mythreadid + " .post.reply:first").before($(this).hide().addClass('hidden'));
}
});
} 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);
if (hovering && post.length > 0) {
startHover(link);
}
}
});
}
}, function() {
hovering = false;
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;
}
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(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(initHover);
});
});

@ -1 +0,0 @@
Subproject commit 25dc1757783115136a83af34c42c603ac8b470aa

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 460 B

View File

@ -1,9 +0,0 @@
%.png: *.gv
dot -Tpng $< -v -o $@
%.svg: *.gv
dot -Tsvg $< -v -o $@
.PHONY: all
all:
make timeline.svg timeline.png

View File

@ -1,88 +0,0 @@
digraph {
graph [ranksep=1,
overlap=false];
fontname="sans-serif";
subgraph main {
edge [fontname="sans-serif", fontsize=8];
node [fontname="sans-serif",
shape=box,
style="rounded,filled"
];
fourchan [label=<Proprietary 4chan.org software<BR/><B><FONT POINT-SIZE="9">(@moot&nbsp;<I>et al.</I>)</FONT></B><BR/>>];
fourchan -> fourchon [xlabel=<inspired&nbsp;>, style=dotted, penwidth=2, color=darkred];
fourchon [label=<Proprietary 4chon.net fork<BR/><B><FONT POINT-SIZE="9">(@savetheinternet)</FONT></B><BR/>20092014>];
fourchon -> tb [penwidth=2, color=darkred];
tb [label=<Tinyboard<BR/><B><FONT POINT-SIZE="9">(@savetheinternet)</FONT></B><BR/>20102014>];
tb -> fourchon;
ponychan [label=<Proprietary ponychan.net fork<BR/><B><FONT POINT-SIZE="9">(???)</FONT></B><BR/>20122015 (as MLPchan)<BR/>2015>];
tb -> ponychan;
wiz [label=<Proprietary Wizardchan fork<BR/><B><FONT POINT-SIZE="9">(@mrpacific)</FONT></B><BR/>20112012>];
tb -> wiz;
wizk [label=<Production Wizardchan fork<BR/><B><FONT POINT-SIZE="9">(@copypaste)</FONT></B><BR/>20122013>];
wiz -> wizk;
wizk -> tb;
wizk -> vichan;
vichan -> wizk;
wizana [label=<Proprietary Wizardchan fork №2<BR/><B><FONT POINT-SIZE="9">(@anachronos)</FONT></B><BR/>2013≈2016>];
wizk -> wizana;
wizchan [label=<Proprietary wizchan fork<BR/><B><FONT POINT-SIZE="9">(too many to count)</FONT></B><BR/>2016>];
wizana -> wizchan;
vichan [color=gold,
label=<vichan<BR/><B><FONT POINT-SIZE="9">(@czaks)</FONT></B><BR/>2012>];
vichannet [label=<Production vichan.net software<BR/><B><FONT POINT-SIZE="9">(Polish-language imageboard)<BR/>(@czaks)</FONT></B><BR/>2012>];
tb -> vichannet [penwidth=2, color=darkred];
vichan -> tb;
vichan -> vichannet;
leftypol [label=<leftypol.org<BR/><B><FONT POINT-SIZE="9">(@discomrade&nbsp;<I>et al.</I>)</FONT></B><BR/>≈2016>];
vichan -> leftypol;
inf [label=<infinity<BR/><B><FONT POINT-SIZE="9">(@copypaste /<BR/>★コピペ)</FONT></B><BR/>20132017>];
vichan -> inf;
infco [label=<Production 8chan.co fork<BR/><B><FONT POINT-SIZE="9">(@copypaste /<BR/>★コピペ)</FONT></B><BR/>20132016>];
vichan -> infco;
lc [label=<lainchan<BR/><B><FONT POINT-SIZE="9">(@appleman1234)</FONT></B><BR/>2016>];
vichan -> lc;
npf [label=<NPFchan<BR/><B><FONT POINT-SIZE="9">(@fallenPineapple)</FONT></B><BR/>20172019>];
vichan -> npf;
kuz [label=<Proprietary KolymaNET fork<BR/><B><FONT POINT-SIZE="9">(@kuz)</FONT></B><BR/>d/b/a soyjak.party<BR/>≈2020>];
vichan -> kuz;
vichannet -> vichan [color=darkred, penwidth=2];
leftypol -> vichan [style=dashed];
inf -> tb [style=dotted];
inf -> vichan;
infpl [label=<Production 8ch.pl fork<BR/><B><FONT POINT-SIZE="9">(@czaks)</FONT></B><BR/>20142016>];
inf -> infpl;
oib [label=<OpenIB<BR/><B><FONT POINT-SIZE="9">(@kormiku)</FONT></B><BR/>≈20172019>];
inf -> oib;
infco -> vichan [rank=min];
infco -> inf;
infpl -> vichan [style=dotted];
infpl -> inf;
e8ch [label=<Proprietary 8chan software<BR/><B><FONT POINT-SIZE="9">(@kormiku)</FONT></B><BR/>≈20172019>];
oib -> e8ch;
e8kun [label=<Proprietary 8kun software<BR/>≈2019≈2022&nbsp;<B><FONT POINT-SIZE="9">(@kormiku)</FONT></B><BR/>2022&nbsp;<B><FONT POINT-SIZE="9">(Jim Watkins)</FONT></B>>];
e8ch -> e8kun;
kuz -> vichan;
}
subgraph B {
subgraph title {
TITLE [fontname="sans-serif",
fontsize=16,
label=<<FONT POINT-SIZE="24"><FONT FACE="monospace">vichan</FONT>&nbsp;Timeline</FONT><BR/>…as of 20230329>,
shape=box,
width=10];
}
subgraph sig {
graph [rank=sink];
SIG [label=<<FONT FACE="serif">by ★コピペ</FONT>>,
labelloc=b,
shape=none];
}
{
TITLE -> fourchan -> fourchon -> tb -> vichan -> infco -> kuz -> SIG [arrowhead=0,
penwidth=0,
weight=max];
}
}
}
// vim: ts=2 sw=2 et

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

View File

@ -1,429 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 7.1.0 (0)
-->
<!-- Pages: 1 -->
<svg width="1118pt" height="1367pt"
viewBox="0.00 0.00 1117.50 1367.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1363)">
<polygon fill="white" stroke="none" points="-4,4 -4,-1363 1113.5,-1363 1113.5,4 -4,4"/>
<!-- fourchan -->
<g id="node1" class="node">
<title>fourchan</title>
<path fill="lightgrey" stroke="black" d="M511,-1239C511,-1239 325,-1239 325,-1239 319,-1239 313,-1233 313,-1227 313,-1227 313,-1215 313,-1215 313,-1209 319,-1203 325,-1203 325,-1203 511,-1203 511,-1203 517,-1203 523,-1209 523,-1215 523,-1215 523,-1227 523,-1227 523,-1233 517,-1239 511,-1239"/>
<text text-anchor="start" x="321" y="-1222.3" font-family="sans-serif" font-size="14.00">Proprietary 4chan.org software</text>
<text text-anchor="start" x="386.5" y="-1212.3" font-family="sans-serif" font-weight="bold" font-size="9.00">(@moot </text>
<text text-anchor="start" x="423.5" y="-1212.3" font-family="sans-serif" font-weight="bold" font-style="italic" font-size="9.00">et al.</text>
<text text-anchor="start" x="445.5" y="-1212.3" font-family="sans-serif" font-weight="bold" font-size="9.00">)</text>
</g>
<!-- fourchon -->
<g id="node2" class="node">
<title>fourchon</title>
<path fill="lightgrey" stroke="black" d="M497,-1131C497,-1131 339,-1131 339,-1131 333,-1131 327,-1125 327,-1119 327,-1119 327,-1098 327,-1098 327,-1092 333,-1086 339,-1086 339,-1086 497,-1086 497,-1086 503,-1086 509,-1092 509,-1098 509,-1098 509,-1119 509,-1119 509,-1125 503,-1131 497,-1131"/>
<text text-anchor="start" x="335" y="-1116.8" font-family="sans-serif" font-size="14.00">Proprietary 4chon.net fork</text>
<text text-anchor="start" x="376.5" y="-1106.8" font-family="sans-serif" font-weight="bold" font-size="9.00">(@savetheinternet)</text>
<text text-anchor="start" x="380.5" y="-1093.8" font-family="sans-serif" font-size="14.00">20092014</text>
</g>
<!-- fourchan&#45;&gt;fourchon -->
<g id="edge1" class="edge">
<title>fourchan&#45;&gt;fourchon</title>
<path fill="none" stroke="darkred" stroke-width="2" stroke-dasharray="1,5" d="M412.94,-1202.73C411.23,-1187.05 410.82,-1163.45 411.7,-1143.92"/>
<polygon fill="darkred" stroke="darkred" stroke-width="2" points="415.17,-1144.42 412.31,-1134.22 408.19,-1143.98 415.17,-1144.42"/>
<text text-anchor="start" x="379.25" y="-1169.58" font-family="sans-serif" font-size="8.00">inspired </text>
</g>
<!-- fourchan&#45;&gt;fourchon -->
<g id="edge35" class="edge">
<title>fourchan&#45;&gt;fourchon</title>
<path fill="none" stroke="black" stroke-width="0" d="M423.06,-1202.73C425.16,-1183.43 425.3,-1152.14 423.49,-1131.2"/>
</g>
<!-- tb -->
<g id="node3" class="node">
<title>tb</title>
<path fill="lightgrey" stroke="black" d="M455.5,-1014C455.5,-1014 380.5,-1014 380.5,-1014 374.5,-1014 368.5,-1008 368.5,-1002 368.5,-1002 368.5,-981 368.5,-981 368.5,-975 374.5,-969 380.5,-969 380.5,-969 455.5,-969 455.5,-969 461.5,-969 467.5,-975 467.5,-981 467.5,-981 467.5,-1002 467.5,-1002 467.5,-1008 461.5,-1014 455.5,-1014"/>
<text text-anchor="start" x="386" y="-999.8" font-family="sans-serif" font-size="14.00">Tinyboard</text>
<text text-anchor="start" x="376.5" y="-989.8" font-family="sans-serif" font-weight="bold" font-size="9.00">(@savetheinternet)</text>
<text text-anchor="start" x="380.5" y="-976.8" font-family="sans-serif" font-size="14.00">20102014</text>
</g>
<!-- fourchon&#45;&gt;tb -->
<g id="edge2" class="edge">
<title>fourchon&#45;&gt;tb</title>
<path fill="none" stroke="darkred" stroke-width="2" d="M407.13,-1085.6C404.29,-1069.05 403.75,-1046.02 405.51,-1027.01"/>
<polygon fill="darkred" stroke="darkred" stroke-width="2" points="408.95,-1027.69 406.76,-1017.32 402.01,-1026.79 408.95,-1027.69"/>
</g>
<!-- fourchon&#45;&gt;tb -->
<g id="edge36" class="edge">
<title>fourchon&#45;&gt;tb</title>
<path fill="none" stroke="black" stroke-width="0" d="M428.87,-1085.6C432.38,-1065.14 432.38,-1034.76 428.86,-1014.32"/>
</g>
<!-- tb&#45;&gt;fourchon -->
<g id="edge3" class="edge">
<title>tb&#45;&gt;fourchon</title>
<path fill="none" stroke="black" d="M418,-1014.32C418,-1031.24 418,-1054.99 418,-1074.27"/>
<polygon fill="black" stroke="black" points="414.5,-1074.09 418,-1084.09 421.5,-1074.09 414.5,-1074.09"/>
</g>
<!-- ponychan -->
<g id="node4" class="node">
<title>ponychan</title>
<path fill="lightgrey" stroke="black" d="M292.5,-897C292.5,-897 113.5,-897 113.5,-897 107.5,-897 101.5,-891 101.5,-885 101.5,-885 101.5,-850 101.5,-850 101.5,-844 107.5,-838 113.5,-838 113.5,-838 292.5,-838 292.5,-838 298.5,-838 304.5,-844 304.5,-850 304.5,-850 304.5,-885 304.5,-885 304.5,-891 298.5,-897 292.5,-897"/>
<text text-anchor="start" x="109.5" y="-882.8" font-family="sans-serif" font-size="14.00">Proprietary ponychan.net fork</text>
<text text-anchor="start" x="193" y="-872.8" font-family="sans-serif" font-weight="bold" font-size="9.00">(???)</text>
<text text-anchor="start" x="122.5" y="-859.8" font-family="sans-serif" font-size="14.00">20122015 (as MLPchan)</text>
<text text-anchor="start" x="182" y="-845.8" font-family="sans-serif" font-size="14.00">2015</text>
</g>
<!-- tb&#45;&gt;ponychan -->
<g id="edge4" class="edge">
<title>tb&#45;&gt;ponychan</title>
<path fill="none" stroke="black" d="M379.56,-968.69C347.41,-950.44 300.96,-924.09 263.77,-902.98"/>
<polygon fill="black" stroke="black" points="265.78,-900.1 255.36,-898.21 262.33,-906.19 265.78,-900.1"/>
</g>
<!-- wiz -->
<g id="node5" class="node">
<title>wiz</title>
<path fill="lightgrey" stroke="black" d="M262.5,-766C262.5,-766 93.5,-766 93.5,-766 87.5,-766 81.5,-760 81.5,-754 81.5,-754 81.5,-733 81.5,-733 81.5,-727 87.5,-721 93.5,-721 93.5,-721 262.5,-721 262.5,-721 268.5,-721 274.5,-727 274.5,-733 274.5,-733 274.5,-754 274.5,-754 274.5,-760 268.5,-766 262.5,-766"/>
<text text-anchor="start" x="89.5" y="-751.8" font-family="sans-serif" font-size="14.00">Proprietary Wizardchan fork</text>
<text text-anchor="start" x="150.5" y="-741.8" font-family="sans-serif" font-weight="bold" font-size="9.00">(@mrpacific)</text>
<text text-anchor="start" x="140.5" y="-728.8" font-family="sans-serif" font-size="14.00">20112012</text>
</g>
<!-- tb&#45;&gt;wiz -->
<g id="edge5" class="edge">
<title>tb&#45;&gt;wiz</title>
<path fill="none" stroke="black" d="M403.64,-968.72C377.51,-929.24 323.9,-848.93 313,-838 287.45,-812.38 254.05,-789.18 226.87,-772.3"/>
<polygon fill="black" stroke="black" points="228.81,-769.39 218.45,-767.16 225.17,-775.36 228.81,-769.39"/>
</g>
<!-- vichan -->
<g id="node7" class="node">
<title>vichan</title>
<path fill="gold" stroke="gold" d="M578,-766C578,-766 544,-766 544,-766 538,-766 532,-760 532,-754 532,-754 532,-733 532,-733 532,-727 538,-721 544,-721 544,-721 578,-721 578,-721 584,-721 590,-727 590,-733 590,-733 590,-754 590,-754 590,-760 584,-766 578,-766"/>
<text text-anchor="start" x="540" y="-751.8" font-family="sans-serif" font-size="14.00">vichan</text>
<text text-anchor="start" x="541.5" y="-741.8" font-family="sans-serif" font-weight="bold" font-size="9.00">(@czaks)</text>
<text text-anchor="start" x="540" y="-728.8" font-family="sans-serif" font-size="14.00">2012</text>
</g>
<!-- tb&#45;&gt;vichan -->
<g id="edge37" class="edge">
<title>tb&#45;&gt;vichan</title>
<path fill="none" stroke="black" stroke-width="0" d="M411.49,-968.66C408.54,-937.5 412.44,-879.45 437,-838 459.23,-800.48 502.44,-771.65 531.71,-756.46"/>
</g>
<!-- vichannet -->
<g id="node10" class="node">
<title>vichannet</title>
<path fill="lightgrey" stroke="black" d="M655,-894.5C655,-894.5 467,-894.5 467,-894.5 461,-894.5 455,-888.5 455,-882.5 455,-882.5 455,-852.5 455,-852.5 455,-846.5 461,-840.5 467,-840.5 467,-840.5 655,-840.5 655,-840.5 661,-840.5 667,-846.5 667,-852.5 667,-852.5 667,-882.5 667,-882.5 667,-888.5 661,-894.5 655,-894.5"/>
<text text-anchor="start" x="463" y="-880.3" font-family="sans-serif" font-size="14.00">Production vichan.net software</text>
<text text-anchor="start" x="496" y="-870.3" font-family="sans-serif" font-weight="bold" font-size="9.00">(Polish&#45;language imageboard)</text>
<text text-anchor="start" x="541.5" y="-861.3" font-family="sans-serif" font-weight="bold" font-size="9.00">(@czaks)</text>
<text text-anchor="start" x="540" y="-848.3" font-family="sans-serif" font-size="14.00">2012</text>
</g>
<!-- tb&#45;&gt;vichannet -->
<g id="edge12" class="edge">
<title>tb&#45;&gt;vichannet</title>
<path fill="none" stroke="darkred" stroke-width="2" d="M443.57,-968.69C464.89,-950.5 495.66,-924.24 520.36,-903.17"/>
<polygon fill="darkred" stroke="darkred" stroke-width="2" points="522.54,-905.91 527.88,-896.76 518,-900.58 522.54,-905.91"/>
</g>
<!-- wizk -->
<g id="node6" class="node">
<title>wizk</title>
<path fill="lightgrey" stroke="black" d="M192.5,-644.5C192.5,-644.5 25.5,-644.5 25.5,-644.5 19.5,-644.5 13.5,-638.5 13.5,-632.5 13.5,-632.5 13.5,-611.5 13.5,-611.5 13.5,-605.5 19.5,-599.5 25.5,-599.5 25.5,-599.5 192.5,-599.5 192.5,-599.5 198.5,-599.5 204.5,-605.5 204.5,-611.5 204.5,-611.5 204.5,-632.5 204.5,-632.5 204.5,-638.5 198.5,-644.5 192.5,-644.5"/>
<text text-anchor="start" x="21.5" y="-630.3" font-family="sans-serif" font-size="14.00">Production Wizardchan fork</text>
<text text-anchor="start" x="80" y="-620.3" font-family="sans-serif" font-weight="bold" font-size="9.00">(@copypaste)</text>
<text text-anchor="start" x="71.5" y="-607.3" font-family="sans-serif" font-size="14.00">20122013</text>
</g>
<!-- wiz&#45;&gt;wizk -->
<g id="edge6" class="edge">
<title>wiz&#45;&gt;wizk</title>
<path fill="none" stroke="black" d="M165.34,-720.58C154.69,-702.14 139.33,-675.52 127.33,-654.74"/>
<polygon fill="black" stroke="black" points="130.36,-653 122.33,-646.09 124.3,-656.5 130.36,-653"/>
</g>
<!-- wizk&#45;&gt;tb -->
<g id="edge7" class="edge">
<title>wizk&#45;&gt;tb</title>
<path fill="none" stroke="black" d="M164.73,-644.82C201.69,-661.43 249.39,-687.19 283,-721 352.23,-790.64 392.51,-902.99 409.07,-958.02"/>
<polygon fill="black" stroke="black" points="405.65,-958.79 411.83,-967.4 412.37,-956.82 405.65,-958.79"/>
</g>
<!-- wizk&#45;&gt;vichan -->
<g id="edge8" class="edge">
<title>wizk&#45;&gt;vichan</title>
<path fill="none" stroke="black" d="M197.39,-644.95C295.12,-670.22 448.2,-710.79 520.6,-730.7"/>
<polygon fill="black" stroke="black" points="519.47,-734.02 530.04,-733.31 521.34,-727.27 519.47,-734.02"/>
</g>
<!-- wizana -->
<g id="node8" class="node">
<title>wizana</title>
<path fill="lightgrey" stroke="black" d="M206,-516C206,-516 12,-516 12,-516 6,-516 0,-510 0,-504 0,-504 0,-483 0,-483 0,-477 6,-471 12,-471 12,-471 206,-471 206,-471 212,-471 218,-477 218,-483 218,-483 218,-504 218,-504 218,-510 212,-516 206,-516"/>
<text text-anchor="start" x="8" y="-501.8" font-family="sans-serif" font-size="14.00">Proprietary Wizardchan fork №2</text>
<text text-anchor="start" x="77" y="-491.8" font-family="sans-serif" font-weight="bold" font-size="9.00">(@anachronos)</text>
<text text-anchor="start" x="67.5" y="-478.8" font-family="sans-serif" font-size="14.00">2013≈2016</text>
</g>
<!-- wizk&#45;&gt;wizana -->
<g id="edge10" class="edge">
<title>wizk&#45;&gt;wizana</title>
<path fill="none" stroke="black" d="M109,-599.25C109,-579.61 109,-550.41 109,-527.83"/>
<polygon fill="black" stroke="black" points="112.5,-527.9 109,-517.9 105.5,-527.9 112.5,-527.9"/>
</g>
<!-- vichan&#45;&gt;tb -->
<g id="edge13" class="edge">
<title>vichan&#45;&gt;tb</title>
<path fill="none" stroke="black" d="M531.7,-764.74C507.21,-781.06 473.69,-806.45 455,-838 433.13,-874.92 427.64,-925 424.44,-957.53"/>
<polygon fill="black" stroke="black" points="420.98,-956.86 423.48,-967.15 427.95,-957.55 420.98,-956.86"/>
</g>
<!-- vichan&#45;&gt;wizk -->
<g id="edge9" class="edge">
<title>vichan&#45;&gt;wizk</title>
<path fill="none" stroke="black" d="M531.72,-735.65C465.16,-718.99 300.79,-675.74 197.7,-647.93"/>
<polygon fill="black" stroke="black" points="198.71,-644.58 188.14,-645.35 196.88,-651.34 198.71,-644.58"/>
</g>
<!-- vichan&#45;&gt;vichannet -->
<g id="edge14" class="edge">
<title>vichan&#45;&gt;vichannet</title>
<path fill="none" stroke="black" d="M566.33,-766.44C567.81,-783.69 568.13,-808.15 567.3,-828.56"/>
<polygon fill="black" stroke="black" points="563.81,-828.34 566.76,-838.52 570.8,-828.72 563.81,-828.34"/>
</g>
<!-- leftypol -->
<g id="node11" class="node">
<title>leftypol</title>
<path fill="lightgrey" stroke="black" d="M602,-644.5C602,-644.5 520,-644.5 520,-644.5 514,-644.5 508,-638.5 508,-632.5 508,-632.5 508,-611.5 508,-611.5 508,-605.5 514,-599.5 520,-599.5 520,-599.5 602,-599.5 602,-599.5 608,-599.5 614,-605.5 614,-611.5 614,-611.5 614,-632.5 614,-632.5 614,-638.5 608,-644.5 602,-644.5"/>
<text text-anchor="start" x="525.5" y="-630.3" font-family="sans-serif" font-size="14.00">leftypol.org</text>
<text text-anchor="start" x="516" y="-620.3" font-family="sans-serif" font-weight="bold" font-size="9.00">(@discomrade </text>
<text text-anchor="start" x="580" y="-620.3" font-family="sans-serif" font-weight="bold" font-style="italic" font-size="9.00">et al.</text>
<text text-anchor="start" x="602" y="-620.3" font-family="sans-serif" font-weight="bold" font-size="9.00">)</text>
<text text-anchor="start" x="536" y="-607.3" font-family="sans-serif" font-size="14.00">≈2016</text>
</g>
<!-- vichan&#45;&gt;leftypol -->
<g id="edge15" class="edge">
<title>vichan&#45;&gt;leftypol</title>
<path fill="none" stroke="black" d="M555.64,-720.58C554.09,-702.56 553.84,-676.73 554.88,-656.17"/>
<polygon fill="black" stroke="black" points="558.37,-656.5 555.55,-646.29 551.38,-656.03 558.37,-656.5"/>
</g>
<!-- inf -->
<g id="node12" class="node">
<title>inf</title>
<path fill="lightgrey" stroke="black" d="M941.5,-520.5C941.5,-520.5 874.5,-520.5 874.5,-520.5 868.5,-520.5 862.5,-514.5 862.5,-508.5 862.5,-508.5 862.5,-478.5 862.5,-478.5 862.5,-472.5 868.5,-466.5 874.5,-466.5 874.5,-466.5 941.5,-466.5 941.5,-466.5 947.5,-466.5 953.5,-472.5 953.5,-478.5 953.5,-478.5 953.5,-508.5 953.5,-508.5 953.5,-514.5 947.5,-520.5 941.5,-520.5"/>
<text text-anchor="start" x="885.5" y="-506.3" font-family="sans-serif" font-size="14.00">infinity</text>
<text text-anchor="start" x="877" y="-496.3" font-family="sans-serif" font-weight="bold" font-size="9.00">(@copypaste /</text>
<text text-anchor="start" x="888.5" y="-487.3" font-family="sans-serif" font-weight="bold" font-size="9.00">★コピペ)</text>
<text text-anchor="start" x="870.5" y="-474.3" font-family="sans-serif" font-size="14.00">20132017</text>
</g>
<!-- vichan&#45;&gt;inf -->
<g id="edge16" class="edge">
<title>vichan&#45;&gt;inf</title>
<path fill="none" stroke="black" d="M590.41,-740.54C646.2,-735.39 767.43,-716.7 839,-649 871.87,-617.91 886.99,-566.73 895.86,-531.87"/>
<polygon fill="black" stroke="black" points="899.25,-532.74 898.28,-522.19 892.46,-531.04 899.25,-532.74"/>
</g>
<!-- infco -->
<g id="node13" class="node">
<title>infco</title>
<path fill="lightgrey" stroke="black" d="M793.5,-649C793.5,-649 644.5,-649 644.5,-649 638.5,-649 632.5,-643 632.5,-637 632.5,-637 632.5,-607 632.5,-607 632.5,-601 638.5,-595 644.5,-595 644.5,-595 793.5,-595 793.5,-595 799.5,-595 805.5,-601 805.5,-607 805.5,-607 805.5,-637 805.5,-637 805.5,-643 799.5,-649 793.5,-649"/>
<text text-anchor="start" x="640.5" y="-634.8" font-family="sans-serif" font-size="14.00">Production 8chan.co fork</text>
<text text-anchor="start" x="688" y="-624.8" font-family="sans-serif" font-weight="bold" font-size="9.00">(@copypaste /</text>
<text text-anchor="start" x="699.5" y="-615.8" font-family="sans-serif" font-weight="bold" font-size="9.00">★コピペ)</text>
<text text-anchor="start" x="681.5" y="-602.8" font-family="sans-serif" font-size="14.00">20132016</text>
</g>
<!-- vichan&#45;&gt;infco -->
<g id="edge17" class="edge">
<title>vichan&#45;&gt;infco</title>
<path fill="none" stroke="black" d="M579.25,-720.58C600.09,-702.46 633.98,-676.46 663.36,-655.85"/>
<polygon fill="black" stroke="black" points="665.25,-658.8 671.47,-650.23 661.26,-653.05 665.25,-658.8"/>
</g>
<!-- vichan&#45;&gt;infco -->
<g id="edge38" class="edge">
<title>vichan&#45;&gt;infco</title>
<path fill="none" stroke="black" stroke-width="0" d="M590.36,-727.37C620.99,-707.77 667.26,-673.3 695.56,-649.36"/>
</g>
<!-- lc -->
<g id="node14" class="node">
<title>lc</title>
<path fill="lightgrey" stroke="black" d="M305.5,-644.5C305.5,-644.5 234.5,-644.5 234.5,-644.5 228.5,-644.5 222.5,-638.5 222.5,-632.5 222.5,-632.5 222.5,-611.5 222.5,-611.5 222.5,-605.5 228.5,-599.5 234.5,-599.5 234.5,-599.5 305.5,-599.5 305.5,-599.5 311.5,-599.5 317.5,-605.5 317.5,-611.5 317.5,-611.5 317.5,-632.5 317.5,-632.5 317.5,-638.5 311.5,-644.5 305.5,-644.5"/>
<text text-anchor="start" x="242.5" y="-630.3" font-family="sans-serif" font-size="14.00">lainchan</text>
<text text-anchor="start" x="230.5" y="-620.3" font-family="sans-serif" font-weight="bold" font-size="9.00">(@appleman1234)</text>
<text text-anchor="start" x="249" y="-607.3" font-family="sans-serif" font-size="14.00">2016</text>
</g>
<!-- vichan&#45;&gt;lc -->
<g id="edge18" class="edge">
<title>vichan&#45;&gt;lc</title>
<path fill="none" stroke="black" d="M531.63,-731.28C488.94,-714.87 407.08,-682.97 327.68,-649.22"/>
<polygon fill="black" stroke="black" points="329.47,-646.18 318.9,-645.47 326.73,-652.61 329.47,-646.18"/>
</g>
<!-- npf -->
<g id="node15" class="node">
<title>npf</title>
<path fill="lightgrey" stroke="black" d="M422,-644.5C422,-644.5 348,-644.5 348,-644.5 342,-644.5 336,-638.5 336,-632.5 336,-632.5 336,-611.5 336,-611.5 336,-605.5 342,-599.5 348,-599.5 348,-599.5 422,-599.5 422,-599.5 428,-599.5 434,-605.5 434,-611.5 434,-611.5 434,-632.5 434,-632.5 434,-638.5 428,-644.5 422,-644.5"/>
<text text-anchor="start" x="356.5" y="-630.3" font-family="sans-serif" font-size="14.00">NPFchan</text>
<text text-anchor="start" x="344" y="-620.3" font-family="sans-serif" font-weight="bold" font-size="9.00">(@fallenPineapple)</text>
<text text-anchor="start" x="347.5" y="-607.3" font-family="sans-serif" font-size="14.00">20172019</text>
</g>
<!-- vichan&#45;&gt;npf -->
<g id="edge19" class="edge">
<title>vichan&#45;&gt;npf</title>
<path fill="none" stroke="black" d="M531.54,-722.5C502.77,-702.97 458.73,-673.06 426.44,-651.13"/>
<polygon fill="black" stroke="black" points="428.73,-648.46 418.49,-645.74 424.8,-654.25 428.73,-648.46"/>
</g>
<!-- kuz -->
<g id="node16" class="node">
<title>kuz</title>
<path fill="lightgrey" stroke="black" d="M554,-523C554,-523 388,-523 388,-523 382,-523 376,-517 376,-511 376,-511 376,-476 376,-476 376,-470 382,-464 388,-464 388,-464 554,-464 554,-464 560,-464 566,-470 566,-476 566,-476 566,-511 566,-511 566,-517 560,-523 554,-523"/>
<text text-anchor="start" x="384" y="-508.8" font-family="sans-serif" font-size="14.00">Proprietary KolymaNET fork</text>
<text text-anchor="start" x="456" y="-498.8" font-family="sans-serif" font-weight="bold" font-size="9.00">(@kuz)</text>
<text text-anchor="start" x="413.5" y="-485.8" font-family="sans-serif" font-size="14.00">d/b/a soyjak.party</text>
<text text-anchor="start" x="446" y="-471.8" font-family="sans-serif" font-size="14.00">≈2020</text>
</g>
<!-- vichan&#45;&gt;kuz -->
<g id="edge20" class="edge">
<title>vichan&#45;&gt;kuz</title>
<path fill="none" stroke="black" d="M535.37,-720.53C519.54,-702.45 500.68,-675.56 490,-649 475.05,-611.83 468.04,-566.43 466.48,-534.49"/>
<polygon fill="black" stroke="black" points="469.99,-534.77 466.19,-524.88 462.99,-534.98 469.99,-534.77"/>
</g>
<!-- wizchan -->
<g id="node9" class="node">
<title>wizchan</title>
<path fill="lightgrey" stroke="black" d="M181.5,-392C181.5,-392 36.5,-392 36.5,-392 30.5,-392 24.5,-386 24.5,-380 24.5,-380 24.5,-359 24.5,-359 24.5,-353 30.5,-347 36.5,-347 36.5,-347 181.5,-347 181.5,-347 187.5,-347 193.5,-353 193.5,-359 193.5,-359 193.5,-380 193.5,-380 193.5,-386 187.5,-392 181.5,-392"/>
<text text-anchor="start" x="32.5" y="-377.8" font-family="sans-serif" font-size="14.00">Proprietary wizchan fork</text>
<text text-anchor="start" x="67" y="-367.8" font-family="sans-serif" font-weight="bold" font-size="9.00">(too many to count)</text>
<text text-anchor="start" x="88" y="-354.8" font-family="sans-serif" font-size="14.00">2016</text>
</g>
<!-- wizana&#45;&gt;wizchan -->
<g id="edge11" class="edge">
<title>wizana&#45;&gt;wizchan</title>
<path fill="none" stroke="black" d="M109,-470.69C109,-452.04 109,-424.91 109,-403.59"/>
<polygon fill="black" stroke="black" points="112.5,-403.64 109,-393.64 105.5,-403.64 112.5,-403.64"/>
</g>
<!-- vichannet&#45;&gt;vichan -->
<g id="edge21" class="edge">
<title>vichannet&#45;&gt;vichan</title>
<path fill="none" stroke="darkred" stroke-width="2" d="M555.32,-840.03C554.08,-822.23 553.92,-798.6 554.84,-779.29"/>
<polygon fill="darkred" stroke="darkred" stroke-width="2" points="558.32,-779.66 555.48,-769.46 551.34,-779.21 558.32,-779.66"/>
</g>
<!-- leftypol&#45;&gt;vichan -->
<g id="edge22" class="edge">
<title>leftypol&#45;&gt;vichan</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M566.35,-644.78C567.91,-662.76 568.16,-688.58 567.13,-709.17"/>
<polygon fill="black" stroke="black" points="563.64,-708.86 566.46,-719.07 570.62,-709.33 563.64,-708.86"/>
</g>
<!-- inf&#45;&gt;tb -->
<g id="edge23" class="edge">
<title>inf&#45;&gt;tb</title>
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M909.06,-520.87C909.36,-552.64 906.67,-606.98 888,-649 829.14,-781.52 795.22,-814.46 676,-897 615.35,-938.99 533.69,-964.39 478.62,-977.91"/>
<polygon fill="black" stroke="black" points="478.01,-974.45 469.1,-980.18 479.64,-981.26 478.01,-974.45"/>
</g>
<!-- inf&#45;&gt;vichan -->
<g id="edge24" class="edge">
<title>inf&#45;&gt;vichan</title>
<path fill="none" stroke="black" d="M910.89,-520.72C908.19,-554.9 893.58,-614.39 857,-649 785.65,-716.5 664.94,-735.28 601.86,-740.5"/>
<polygon fill="black" stroke="black" points="601.63,-737 591.92,-741.23 602.15,-743.98 601.63,-737"/>
</g>
<!-- infpl -->
<g id="node17" class="node">
<title>infpl</title>
<path fill="lightgrey" stroke="black" d="M936.5,-392C936.5,-392 805.5,-392 805.5,-392 799.5,-392 793.5,-386 793.5,-380 793.5,-380 793.5,-359 793.5,-359 793.5,-353 799.5,-347 805.5,-347 805.5,-347 936.5,-347 936.5,-347 942.5,-347 948.5,-353 948.5,-359 948.5,-359 948.5,-380 948.5,-380 948.5,-386 942.5,-392 936.5,-392"/>
<text text-anchor="start" x="801.5" y="-377.8" font-family="sans-serif" font-size="14.00">Production 8ch.pl fork</text>
<text text-anchor="start" x="851.5" y="-367.8" font-family="sans-serif" font-weight="bold" font-size="9.00">(@czaks)</text>
<text text-anchor="start" x="833.5" y="-354.8" font-family="sans-serif" font-size="14.00">20142016</text>
</g>
<!-- inf&#45;&gt;infpl -->
<g id="edge25" class="edge">
<title>inf&#45;&gt;infpl</title>
<path fill="none" stroke="black" d="M894.29,-466.03C887.46,-447.72 879.9,-423.22 874.96,-403.62"/>
<polygon fill="black" stroke="black" points="878.38,-402.84 872.67,-393.91 871.56,-404.45 878.38,-402.84"/>
</g>
<!-- oib -->
<g id="node18" class="node">
<title>oib</title>
<path fill="lightgrey" stroke="black" d="M1053.5,-392C1053.5,-392 978.5,-392 978.5,-392 972.5,-392 966.5,-386 966.5,-380 966.5,-380 966.5,-359 966.5,-359 966.5,-353 972.5,-347 978.5,-347 978.5,-347 1053.5,-347 1053.5,-347 1059.5,-347 1065.5,-353 1065.5,-359 1065.5,-359 1065.5,-380 1065.5,-380 1065.5,-386 1059.5,-392 1053.5,-392"/>
<text text-anchor="start" x="991.5" y="-377.8" font-family="sans-serif" font-size="14.00">OpenIB</text>
<text text-anchor="start" x="991" y="-367.8" font-family="sans-serif" font-weight="bold" font-size="9.00">(@kormiku)</text>
<text text-anchor="start" x="974.5" y="-354.8" font-family="sans-serif" font-size="14.00">≈20172019</text>
</g>
<!-- inf&#45;&gt;oib -->
<g id="edge26" class="edge">
<title>inf&#45;&gt;oib</title>
<path fill="none" stroke="black" d="M931.43,-466.03C948.4,-446.86 971.38,-420.91 989.1,-400.89"/>
<polygon fill="black" stroke="black" points="991.57,-403.38 995.58,-393.57 986.33,-398.74 991.57,-403.38"/>
</g>
<!-- infco&#45;&gt;vichan -->
<g id="edge27" class="edge">
<title>infco&#45;&gt;vichan</title>
<path fill="none" stroke="black" d="M684.14,-649.37C658.97,-668.4 625,-694.09 599.12,-713.67"/>
<polygon fill="black" stroke="black" points="597.05,-710.85 591.19,-719.67 601.27,-716.43 597.05,-710.85"/>
</g>
<!-- infco&#45;&gt;inf -->
<g id="edge28" class="edge">
<title>infco&#45;&gt;inf</title>
<path fill="none" stroke="black" d="M758.16,-594.79C787.46,-575.18 827.71,-548.24 859.12,-527.21"/>
<polygon fill="black" stroke="black" points="860.94,-530.21 867.31,-521.74 857.05,-524.39 860.94,-530.21"/>
</g>
<!-- infco&#45;&gt;kuz -->
<g id="edge39" class="edge">
<title>infco&#45;&gt;kuz</title>
<path fill="none" stroke="black" stroke-width="0" d="M667.31,-594.64C626.57,-573.85 569.82,-544.91 527.81,-523.48"/>
</g>
<!-- kuz&#45;&gt;vichan -->
<g id="edge33" class="edge">
<title>kuz&#45;&gt;vichan</title>
<path fill="none" stroke="black" d="M479.33,-523.37C484.44,-555.15 491.22,-607.27 508,-649 517.01,-671.41 531.85,-694.06 543.72,-711.44"/>
<polygon fill="black" stroke="black" points="540.56,-713.02 549.1,-719.28 546.33,-709.06 540.56,-713.02"/>
</g>
<!-- SIG -->
<g id="node22" class="node">
<title>SIG</title>
<text text-anchor="start" x="433.5" y="-2.8" font-family="serif" font-size="14.00">by ★コピペ</text>
</g>
<!-- kuz&#45;&gt;SIG -->
<g id="edge40" class="edge">
<title>kuz&#45;&gt;SIG</title>
<path fill="none" stroke="black" stroke-width="0" d="M471,-463.59C471,-374.44 471,-105.57 471,-35.73"/>
</g>
<!-- infpl&#45;&gt;vichan -->
<g id="edge29" class="edge">
<title>infpl&#45;&gt;vichan</title>
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M866.9,-392.37C854.7,-457.19 819.24,-644.05 815,-649 760.35,-712.81 658.15,-733.09 601.47,-739.53"/>
<polygon fill="black" stroke="black" points="601.19,-736.04 591.6,-740.54 601.91,-743 601.19,-736.04"/>
</g>
<!-- infpl&#45;&gt;inf -->
<g id="edge30" class="edge">
<title>infpl&#45;&gt;inf</title>
<path fill="none" stroke="black" d="M882.98,-392.44C889.76,-409.85 897.58,-434.6 902.94,-455.13"/>
<polygon fill="black" stroke="black" points="899.48,-455.7 905.29,-464.56 906.27,-454.01 899.48,-455.7"/>
</g>
<!-- e8ch -->
<g id="node19" class="node">
<title>e8ch</title>
<path fill="lightgrey" stroke="black" d="M1097.5,-275C1097.5,-275 934.5,-275 934.5,-275 928.5,-275 922.5,-269 922.5,-263 922.5,-263 922.5,-242 922.5,-242 922.5,-236 928.5,-230 934.5,-230 934.5,-230 1097.5,-230 1097.5,-230 1103.5,-230 1109.5,-236 1109.5,-242 1109.5,-242 1109.5,-263 1109.5,-263 1109.5,-269 1103.5,-275 1097.5,-275"/>
<text text-anchor="start" x="930.5" y="-260.8" font-family="sans-serif" font-size="14.00">Proprietary 8chan software</text>
<text text-anchor="start" x="991" y="-250.8" font-family="sans-serif" font-weight="bold" font-size="9.00">(@kormiku)</text>
<text text-anchor="start" x="974.5" y="-237.8" font-family="sans-serif" font-size="14.00">≈20172019</text>
</g>
<!-- oib&#45;&gt;e8ch -->
<g id="edge31" class="edge">
<title>oib&#45;&gt;e8ch</title>
<path fill="none" stroke="black" d="M1016,-346.6C1016,-329.65 1016,-305.91 1016,-286.64"/>
<polygon fill="black" stroke="black" points="1019.5,-286.83 1016,-276.83 1012.5,-286.83 1019.5,-286.83"/>
</g>
<!-- e8kun -->
<g id="node20" class="node">
<title>e8kun</title>
<path fill="lightgrey" stroke="black" d="M1094,-158C1094,-158 938,-158 938,-158 932,-158 926,-152 926,-146 926,-146 926,-120 926,-120 926,-114 932,-108 938,-108 938,-108 1094,-108 1094,-108 1100,-108 1106,-114 1106,-120 1106,-120 1106,-146 1106,-146 1106,-152 1100,-158 1094,-158"/>
<text text-anchor="start" x="934" y="-143.8" font-family="sans-serif" font-size="14.00">Proprietary 8kun software</text>
<text text-anchor="start" x="944" y="-129.8" font-family="sans-serif" font-size="14.00">≈2019≈2022 </text>
<text text-anchor="start" x="1038" y="-129.8" font-family="sans-serif" font-weight="bold" font-size="9.00">(@kormiku)</text>
<text text-anchor="start" x="964" y="-115.8" font-family="sans-serif" font-size="14.00">2022 </text>
<text text-anchor="start" x="1009" y="-115.8" font-family="sans-serif" font-weight="bold" font-size="9.00">(Jim Watkins)</text>
</g>
<!-- e8ch&#45;&gt;e8kun -->
<g id="edge32" class="edge">
<title>e8ch&#45;&gt;e8kun</title>
<path fill="none" stroke="black" d="M1016,-229.67C1016,-212.78 1016,-189.03 1016,-169.42"/>
<polygon fill="black" stroke="black" points="1019.5,-169.65 1016,-159.65 1012.5,-169.65 1019.5,-169.65"/>
</g>
<!-- TITLE -->
<g id="node21" class="node">
<title>TITLE</title>
<polygon fill="none" stroke="black" points="778,-1359 58,-1359 58,-1311 778,-1311 778,-1359"/>
<text text-anchor="start" x="325" y="-1336.8" font-family="monospace" font-size="24.00">vichan</text>
<text text-anchor="start" x="411" y="-1336.8" font-family="sans-serif" font-size="24.00"> Timeline</text>
<text text-anchor="start" x="353.5" y="-1319.2" font-family="sans-serif" font-size="16.00">…as of 20230329</text>
</g>
<!-- TITLE&#45;&gt;fourchan -->
<g id="edge34" class="edge">
<title>TITLE&#45;&gt;fourchan</title>
<path fill="none" stroke="black" stroke-width="0" d="M418,-1310.52C418,-1289.23 418,-1258.33 418,-1239.23"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 633 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 871 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 647 B

View File

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

View File

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

View File

@ -1,84 +0,0 @@
<p>
Any changes you make here will simply be appended to <code>{{ file }}</code>. If you wish to make the most of Tinyboard's customizability, you can instead edit the file directly. This page is intended for making quick changes and for those who don't have a basic understanding of PHP code.
</p>
{% if boards|length %}
<ul>
{% if board %}
<li><a href="?/config">Edit site-wide config</a></li>
{% endif %}
{% for _board in boards %}
{% if _board.uri != board %}
<li>
<a href="?/config/{{ _board.uri }}">Edit config for {{ config.board_abbreviation|sprintf(_board.uri) }}</a>
</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
<form method="post" action="">
<input type="hidden" name="token" value="{{ token }}">
<table class="mod config-editor">
<tr>
<th class="minimal">{% trans 'Name' %}</th>
<th>{% trans 'Value' %}</th>
<th class="minimal">{% trans 'Type' %}</th>
<th>{% trans 'Description' %}</th>
</tr>
{% for var in conf %}
{% if var.type != 'array' %}
{% if var.name|length == 1 %}
{% set name = 'cf_' ~ var.name %}
{% else %}
{% set name = 'cf_' ~ var.name|join('/') %}
{% endif %}
<tr>
<th class="minimal">
{% if var.name|length == 1 %}
{{ var.name }}
{% else %}
{{ var.name|join(' &rarr; ') }}
{% endif %}
</th>
<td>
{% if var.type == 'string' %}
<input name="{{ name }}" type="text" value="{{ var.value|e }}">
{% elseif var.permissions %}
<select name="{{ name }}">
{% for group_value, group_name in config.mod.groups %}
<option value="{{ group_value }}"{% if var.value == group_value %} selected{% endif %}>
{{ group_name }}
</option>
{% endfor %}
</select>
{% elseif var.type == 'integer' %}
<input name="{{ name }}" type="number" value="{{ var.value|e }}">
{% elseif var.type == 'boolean' %}
<input name="{{ name }}" type="checkbox" {% if var.value %}checked{% endif %}>
{% else %}
?
{% endif %}
{% if var.type == 'integer' or var.type == 'boolean' %}
<small>Default: <code>{{ var.default }}</code></small>
{% endif %}
</td>
<td class="minimal">
{{ var.type|e }}
</td>
<td style="word-wrap:break-word;width:50%">
{{ var.comment|join(' ') }}
</td>
</tr>
{% endif %}
{% endfor %}
</table>
<ul style="padding:0;text-align:center;list-style:none">
<li><input name="save" type="submit" value="{% trans 'Save changes' %}"></li>
</ul>
</form>

View File

@ -1,20 +0,0 @@
<table class="modlog">
<tr>
<th class="minimal">Key</th>
<th class="minimal">Hits</th>
<th class="minimal">Created</th>
<th class="minimal">Expires</th>
<th class="minimal">Size</th>
</tr>
{% for var in cached_vars %}
{% if (var.ctime is defined ? var.ctime : var.creation_time) + var.ttl > time() %}
<tr>
<td class="minimal">{{ var.key is defined ? var.key : var.info }}</td>
<td class="minimal">{{ var.nhits is defined ? var.nhits : var.num_hits }}</td>
<td class="minimal">{{ (var.ctime is defined ? var.ctime : var.creation_time)|ago }} ago</td>
<td class="minimal">{{ ((var.ctime is defined ? var.ctime : var.creation_time) + var.ttl)|until }} (ttl: {{ (time() + var.ttl)|until }})</td>
<td class="minimal">{{ var.mem_size }} bytes</td>
</tr>
{% endif %}
{% endfor %}
</table>

View File

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

View File

@ -1 +0,0 @@
<time datetime="{{ post.time|date('Y-m-d\\TH:i:s\Z') }}">{{ post.time|date(config.post_date) }}</time>

View File

@ -1,91 +0,0 @@
{% apply spaceless %}
<!doctype html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<script type='text/javascript'>
var active_page = "catalog"
, board_name = "{{ board }}";
</script>
{% include 'header.html' %}
<title>{{ board }} - Catalog</title>
</head>
<body class="8chan vichan {% if mod %}is-moderator{% else %}is-not-moderator{% endif %} theme-catalog active-catalog" data-stylesheet="{% if config.default_stylesheet.1 != '' %}{{ config.default_stylesheet.1 }}{% else %}default{% endif %}">
{{ boardlist.top }}
<header>
<h1>{{ settings.title }} (<a href="{{link}}">/{{ board }}/</a>)</h1>
<div class="subtitle">{{ settings.subtitle }}
{% if mod %}<p><a href="?/">{% trans %}Return to dashboard{% endtrans %}</a></p>{% endif %}
</div>
</header>
<span>{% trans 'Sort by' %}: </span>
<select id="sort_by" style="display: inline-block">
<option selected value="bump:desc">{% trans 'Bump order' %}</option>
<option value="time:desc">{% trans 'Creation date' %}</option>
<option value="reply:desc">{% trans 'Reply count' %}</option>
<option value="random:desc">{% trans 'Random' %}</option>
</select>
<span>{% trans 'Image size' %}: </span>
<select id="image_size" style="display: inline-block">
<option value="vsmall">{% trans 'Very small' %}</option>
<option selected value="small">{% trans 'Small' %}</option>
<option value="large">{% trans 'Large' %}</option>
</select>
<div class="threads">
<div id="Grid">
{% for post in recent_posts %}
<div class="mix"
data-reply="{{ post.reply_count }}"
data-bump="{{ post.bump }}"
data-time="{{ post.time }}"
data-id="{{ post.id }}"
data-sticky="{% if post.sticky %}true{% else %}false{% endif %}"
data-locked="{% if post.locked %}true{% else %}false{% endif %}"
>
<div class="thread grid-li grid-size-small">
<a href="{{post.link}}">
{% if post.youtube %}
<img src="//img.youtube.com/vi/{{ post.youtube }}/0.jpg"
{% 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')}} {% 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>
{% if post.subject %}
<p class="intro">
<span class="subject">
{{ post.subject|e }}
</span>
</p>
{% else %}
<br />
{% endif %}
{{ post.body }}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<hr/>
{% include 'footer.html' %}
<script type="text/javascript">{% verbatim %}
var styles = {
{% endverbatim %}
{% for name, uri in config.stylesheets %}{% verbatim %}'{% endverbatim %}{{ name|addslashes }}{% verbatim %}' : '{% endverbatim %}/stylesheets/{{ uri|addslashes }}{% verbatim %}',
{% endverbatim %}{% endfor %}{% verbatim %}
}; onReady(init);
{% endverbatim %}</script>
<script type="text/javascript">{% verbatim %}
ready();
{% endverbatim %}</script>
</body>
</html>
{% endapply %}

View File

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

View File

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

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