forked from GithubBackups/vichan
Compare commits
1 Commits
master
...
bumplock_i
Author | SHA1 | Date | |
---|---|---|---|
|
9e77f7b7c4 |
@ -1,4 +0,0 @@
|
|||||||
**/.git
|
|
||||||
**/.gitignore
|
|
||||||
/local-instances
|
|
||||||
**/.gitkeep
|
|
69
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
69
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -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
|
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -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.
|
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -44,6 +44,5 @@ Thumbs.db
|
|||||||
#vichan custom
|
#vichan custom
|
||||||
favicon.ico
|
favicon.ico
|
||||||
/static/spoiler.png
|
/static/spoiler.png
|
||||||
/local-instances
|
|
||||||
|
|
||||||
/vendor/
|
/vendor/
|
||||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -7,6 +7,3 @@
|
|||||||
path = inc/lib/parsedown
|
path = inc/lib/parsedown
|
||||||
url = https://github.com/vichan-devel/parsedown.git
|
url = https://github.com/vichan-devel/parsedown.git
|
||||||
branch = master
|
branch = master
|
||||||
[submodule "js/twemoji"]
|
|
||||||
path = js/twemoji
|
|
||||||
url = https://github.com/basedgentoo/twemoji
|
|
||||||
|
39
README.md
39
README.md
@ -1,15 +1,22 @@
|
|||||||
vichan - A lightweight and full featured PHP imageboard.
|
vichan - A lightweight and full featured PHP imageboard.
|
||||||
========================================================
|
========================================================
|
||||||
|
|
||||||
**Please do not contact Fredrick Brennan in regards to vichan issues.**
|
**Vichan has next to no active development<!--, however you can still pay for support. Basic support costs $40/hr, and is only payable in BTC. New features depend on what you want. Email COPYPASTE <AT> KITTENS <DOT> PH if you're interested—Vichan forks such as OpenIB are included in this offer-->.**
|
||||||
|
|
||||||
As of 29 August 2022 it supports PHP8.1.
|
As of 29 August 2022, though, it supports PHP8.1.
|
||||||
|
|
||||||
About
|
About
|
||||||
------------
|
------------
|
||||||
vichan is a free light-weight, fast, highly configurable and user-friendly
|
vichan is a free light-weight, fast, highly configurable and user-friendly
|
||||||
imageboard software package. It is written in PHP and has few dependencies.
|
imageboard software package. It is written in PHP and has few dependencies.
|
||||||
|
|
||||||
|
*Security problems can be reported to the development team: DEVELOPMENT \<AT\> VICHAN \<DOT\> NET.*
|
||||||
|
|
||||||
|
While there is currently no active development besides fixing security problems, we don't exclude the possibility to refactor the code in order to meet today's standards and continue our work from the point where [@czaks](https://github.com/czaks) retired in 2017.
|
||||||
|
Before this milestone is achieved though, we strongly urge you to consider other imageboard packages. It is the opinion of the vichan development team that no new vichan imageboards should be deployed at the moment, and other imageboard packages used instead.
|
||||||
|
|
||||||
|
For support, feel free to join our [IRC channel](https://webchat.6an.org/?channels=vichan-dev) at irc.6an.org.
|
||||||
|
|
||||||
Some documentation may be found on our [wiki](https://github.com/vichan-devel/vichan/wiki). (feel free to contribute)
|
Some documentation may be found on our [wiki](https://github.com/vichan-devel/vichan/wiki). (feel free to contribute)
|
||||||
|
|
||||||
History
|
History
|
||||||
@ -18,19 +25,16 @@ vichan is a fork of (now defunc'd) [Tinyboard](http://github.com/savetheinternet
|
|||||||
a great imageboard package, actively building on it and adding a lot of features and other
|
a great imageboard package, actively building on it and adding a lot of features and other
|
||||||
improvements.
|
improvements.
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Maintainer timeline
|
### Maintainer timeline
|
||||||
1. [@perdedora](https://github.com/perdedora) and [@RealAngeleno](https://github.com/RealAngeleno) - 2023-Present.
|
1. [@h00j](https://github.com/h00j) (2021 - 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)
|
2. [@ctrlcctrlv](https://github.com/ctrlcctrlv) (2017 - 2021)
|
||||||
3. [@h00j](https://github.com/h00j) (2021 - ???)
|
3. [@czaks](https://github.com/czaks) (2014 - 2017) (The author of vichan fork)
|
||||||
4. [@ctrlcctrlv](https://github.com/ctrlcctrlv) (2017 - 2021)
|
4. [@savetheinternet](https://github.com/savetheinternet) (2010 - 2014) (The creator of Tinyboard)
|
||||||
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
|
Requirements
|
||||||
------------
|
------------
|
||||||
1. PHP >= 7.4
|
1. PHP >= 5.4 (we still try to keep compatibility with php 5.3 as much as possible)
|
||||||
|
PHP 7.0 is explicitly supported. PHP 7.2 works as well, but may cause as yet unreported bugs.
|
||||||
2. MySQL/MariaDB server
|
2. MySQL/MariaDB server
|
||||||
3. [mbstring](http://www.php.net/manual/en/mbstring.installation.php)
|
3. [mbstring](http://www.php.net/manual/en/mbstring.installation.php)
|
||||||
4. [PHP GD](http://www.php.net/manual/en/intro.image.php)
|
4. [PHP GD](http://www.php.net/manual/en/intro.image.php)
|
||||||
@ -42,7 +46,9 @@ We try to make sure vichan is compatible with all major web servers. vichan does
|
|||||||
### Recommended
|
### Recommended
|
||||||
1. MySQL/MariaDB server >= 5.5.3
|
1. MySQL/MariaDB server >= 5.5.3
|
||||||
2. ImageMagick (command-line ImageMagick or GraphicsMagick preferred).
|
2. ImageMagick (command-line ImageMagick or GraphicsMagick preferred).
|
||||||
3. [APCu (Alternative PHP Cache)](http://php.net/manual/en/book.apcu.php),
|
3. ~~[APC (Alternative PHP Cache)](http://php.net/manual/en/book.apc.php)~~,
|
||||||
|
[APCu (Alternative PHP Cache)](http://php.net/manual/en/book.apcu.php),
|
||||||
|
[XCache](http://xcache.lighttpd.net/),
|
||||||
[Memcached](http://www.php.net/manual/en/intro.memcached.php) or
|
[Memcached](http://www.php.net/manual/en/intro.memcached.php) or
|
||||||
[Redis](https://redis.io/docs/about/)
|
[Redis](https://redis.io/docs/about/)
|
||||||
|
|
||||||
@ -55,7 +61,8 @@ You can contribute to vichan by:
|
|||||||
|
|
||||||
Installation
|
Installation
|
||||||
-------------
|
-------------
|
||||||
1. Get the latest development version with:
|
1. Download and extract vichan to your web directory or get the latest
|
||||||
|
development version with:
|
||||||
|
|
||||||
git clone git://github.com/vichan-devel/vichan.git
|
git clone git://github.com/vichan-devel/vichan.git
|
||||||
|
|
||||||
@ -119,11 +126,6 @@ WebM support
|
|||||||
------------
|
------------
|
||||||
Read `inc/lib/webm/README.md` for information about enabling webm.
|
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 API
|
||||||
----------
|
----------
|
||||||
vichan provides by default a 4chan-compatible JSON API. For documentation on this, see:
|
vichan provides by default a 4chan-compatible JSON API. For documentation on this, see:
|
||||||
@ -132,3 +134,4 @@ https://github.com/vichan-devel/vichan-API/ .
|
|||||||
License
|
License
|
||||||
--------
|
--------
|
||||||
See [LICENSE.md](http://github.com/vichan-devel/vichan/blob/master/LICENSE.md).
|
See [LICENSE.md](http://github.com/vichan-devel/vichan/blob/master/LICENSE.md).
|
||||||
|
|
||||||
|
21
b.php
21
b.php
@ -1,8 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
|
$dir = "static/banners/";
|
||||||
|
$files = scandir($dir);
|
||||||
|
$images = array_diff($files, array('.', '..'));
|
||||||
|
$name = $images[array_rand($images)];
|
||||||
|
// open the file in a binary mode
|
||||||
|
$fp = fopen($dir . $name, 'rb');
|
||||||
|
|
||||||
$files = scandir('static/banners/', SCANDIR_SORT_NONE);
|
// send the right headers
|
||||||
$files = array_diff($files, ['.', '..']);
|
header('Cache-Control: no-cache, no-store, must-revalidate'); // HTTP 1.1
|
||||||
|
header('Pragma: no-cache'); // HTTP 1.0
|
||||||
|
header('Expires: 0'); // Proxies
|
||||||
|
header('Content-Type: ' . $fp['type']);
|
||||||
|
header('Content-Length: ' . $fp['bytes']);
|
||||||
|
|
||||||
$name = $files[array_rand($files)];
|
// dump the picture and stop the script
|
||||||
header("Location: /static/banners/$name", true, 307);
|
fpassthru($fp);
|
||||||
header('Cache-Control: no-cache');
|
exit;
|
||||||
|
?>
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
require_once 'inc/functions.php';
|
||||||
|
require_once 'inc/bans.php';
|
||||||
require_once 'inc/bootstrap.php';
|
require_once 'inc/bootstrap.php';
|
||||||
checkBan();
|
checkBan();
|
||||||
|
|
||||||
|
40
compose.yml
40
compose.yml
@ -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
|
|
@ -2,24 +2,16 @@
|
|||||||
"name": "vichan-devel/vichan",
|
"name": "vichan-devel/vichan",
|
||||||
"description": "vichan imageboard",
|
"description": "vichan imageboard",
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"config": {
|
|
||||||
"platform": {
|
|
||||||
"php": "7.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.4",
|
"ext-mbstring": ">=5.4",
|
||||||
"ext-mbstring": ">=7.4",
|
"ext-gd": ">=5.4",
|
||||||
"ext-gd": ">=7.4",
|
"ext-pdo": ">=5.4",
|
||||||
"ext-pdo": ">=7.4",
|
"twig/twig": "^1.44.2",
|
||||||
"twig/twig": "^2.9",
|
|
||||||
"phpmyadmin/twig-i18n-extension": "^4.0",
|
|
||||||
"lifo/ip": "^1.0",
|
"lifo/ip": "^1.0",
|
||||||
"gettext/gettext": "^5.5",
|
"gettext/gettext": "^1.0",
|
||||||
"mrclay/minify": "^2.1.6",
|
"mrclay/minify": "^2.1.6",
|
||||||
"geoip/geoip": "^1.17",
|
"geoip/geoip": "^1.17",
|
||||||
"dapphp/securimage": "^4.0",
|
"dapphp/securimage": "^4.0"
|
||||||
"erusev/parsedown": "^1.7.4"
|
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"classmap": ["inc/"],
|
"classmap": ["inc/"],
|
||||||
@ -33,14 +25,8 @@
|
|||||||
"inc/mod/auth.php",
|
"inc/mod/auth.php",
|
||||||
"inc/lock.php",
|
"inc/lock.php",
|
||||||
"inc/queue.php",
|
"inc/queue.php",
|
||||||
"inc/functions.php",
|
"inc/polyfill.php",
|
||||||
"inc/functions/dice.php",
|
"inc/functions.php"
|
||||||
"inc/functions/format.php",
|
|
||||||
"inc/functions/net.php",
|
|
||||||
"inc/functions/num.php",
|
|
||||||
"inc/functions/theme.php",
|
|
||||||
"inc/service/captcha-queries.php",
|
|
||||||
"inc/context.php"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"license": "Tinyboard + vichan",
|
"license": "Tinyboard + vichan",
|
||||||
|
416
composer.lock
generated
416
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "72e79f203581eea6e6b0455147b25878",
|
"content-hash": "1e3723687369c82eea457d2dded76b74",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "dapphp/securimage",
|
"name": "dapphp/securimage",
|
||||||
@ -31,12 +31,12 @@
|
|||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
|
||||||
"Securimage\\": "./"
|
|
||||||
},
|
|
||||||
"classmap": [
|
"classmap": [
|
||||||
"securimage.php"
|
"securimage.php"
|
||||||
]
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Securimage\\": "./"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
"license": [
|
"license": [
|
||||||
@ -114,43 +114,37 @@
|
|||||||
"issues": "https://github.com/maxmind/geoip-api-php/issues",
|
"issues": "https://github.com/maxmind/geoip-api-php/issues",
|
||||||
"source": "https://github.com/maxmind/geoip-api-php/tree/master"
|
"source": "https://github.com/maxmind/geoip-api-php/tree/master"
|
||||||
},
|
},
|
||||||
"abandoned": "geoip2/geoip2",
|
|
||||||
"time": "2016-05-16T19:06:50+00:00"
|
"time": "2016-05-16T19:06:50+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "gettext/gettext",
|
"name": "gettext/gettext",
|
||||||
"version": "v5.7.0",
|
"version": "v1.1.5",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/php-gettext/Gettext.git",
|
"url": "https://github.com/php-gettext/Gettext.git",
|
||||||
"reference": "8657e580747bb3baacccdcebe69cac094661e404"
|
"reference": "1bdf755a1b49f0614d6fc29f446df567eb62cd5c"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/php-gettext/Gettext/zipball/8657e580747bb3baacccdcebe69cac094661e404",
|
"url": "https://api.github.com/repos/php-gettext/Gettext/zipball/1bdf755a1b49f0614d6fc29f446df567eb62cd5c",
|
||||||
"reference": "8657e580747bb3baacccdcebe69cac094661e404",
|
"reference": "1bdf755a1b49f0614d6fc29f446df567eb62cd5c",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"gettext/languages": "^2.3",
|
"php": ">=5.3.0"
|
||||||
"php": "^7.2|^8.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"brick/varexporter": "^0.3.5",
|
|
||||||
"friendsofphp/php-cs-fixer": "^3.2",
|
|
||||||
"oscarotero/php-cs-fixer-config": "^2.0",
|
|
||||||
"phpunit/phpunit": "^8.0|^9.0",
|
|
||||||
"squizlabs/php_codesniffer": "^3.0"
|
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-0": {
|
||||||
"Gettext\\": "src"
|
"Gettext": ""
|
||||||
}
|
},
|
||||||
|
"files": [
|
||||||
|
"Gettext/translator_functions.php"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
"license": [
|
"license": [
|
||||||
"MIT"
|
"AGPL-3.0"
|
||||||
],
|
],
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
@ -160,123 +154,33 @@
|
|||||||
"role": "Developer"
|
"role": "Developer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "PHP gettext manager",
|
"description": "PHP - JS gettext conversor",
|
||||||
"homepage": "https://github.com/php-gettext/Gettext",
|
"homepage": "https://github.com/oscarotero/Gettext",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"JS",
|
"JS",
|
||||||
"gettext",
|
"gettext",
|
||||||
"i18n",
|
"i18n",
|
||||||
"mo",
|
|
||||||
"po",
|
|
||||||
"translation"
|
"translation"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"email": "oom@oscarotero.com",
|
"email": "oom@oscarotero.com",
|
||||||
"issues": "https://github.com/php-gettext/Gettext/issues",
|
"issues": "https://github.com/oscarotero/Gettext/issues",
|
||||||
"source": "https://github.com/php-gettext/Gettext/tree/v5.7.0"
|
"source": "https://github.com/php-gettext/Gettext/tree/v1.1.5"
|
||||||
},
|
},
|
||||||
"funding": [
|
"time": "2014-10-22T15:53:45+00:00"
|
||||||
{
|
|
||||||
"url": "https://paypal.me/oscarotero",
|
|
||||||
"type": "custom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/oscarotero",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://www.patreon.com/misteroom",
|
|
||||||
"type": "patreon"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2022-07-27T19:54:55+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "gettext/languages",
|
|
||||||
"version": "2.10.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/php-gettext/Languages.git",
|
|
||||||
"reference": "4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/php-gettext/Languages/zipball/4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab",
|
|
||||||
"reference": "4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": ">=5.3"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.4"
|
|
||||||
},
|
|
||||||
"bin": [
|
|
||||||
"bin/export-plural-rules"
|
|
||||||
],
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Gettext\\Languages\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Michele Locati",
|
|
||||||
"email": "mlocati@gmail.com",
|
|
||||||
"role": "Developer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "gettext languages with plural rules",
|
|
||||||
"homepage": "https://github.com/php-gettext/Languages",
|
|
||||||
"keywords": [
|
|
||||||
"cldr",
|
|
||||||
"i18n",
|
|
||||||
"internationalization",
|
|
||||||
"l10n",
|
|
||||||
"language",
|
|
||||||
"languages",
|
|
||||||
"localization",
|
|
||||||
"php",
|
|
||||||
"plural",
|
|
||||||
"plural rules",
|
|
||||||
"plurals",
|
|
||||||
"translate",
|
|
||||||
"translations",
|
|
||||||
"unicode"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/php-gettext/Languages/issues",
|
|
||||||
"source": "https://github.com/php-gettext/Languages/tree/2.10.0"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://paypal.me/mlocati",
|
|
||||||
"type": "custom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/mlocati",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2022-10-18T15:00:10+00:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "lifo/ip",
|
"name": "lifo/ip",
|
||||||
"version": "v1.1.1",
|
"version": "v1.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/lifo101/ip.git",
|
"url": "https://github.com/lifo101/ip.git",
|
||||||
"reference": "4c4cf5b554884be93f1d0422eaec8d6426993229"
|
"reference": "b6a36dab288d7aea155698808bfc6649799fe413"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/lifo101/ip/zipball/4c4cf5b554884be93f1d0422eaec8d6426993229",
|
"url": "https://api.github.com/repos/lifo101/ip/zipball/b6a36dab288d7aea155698808bfc6649799fe413",
|
||||||
"reference": "4c4cf5b554884be93f1d0422eaec8d6426993229",
|
"reference": "b6a36dab288d7aea155698808bfc6649799fe413",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -308,9 +212,9 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/lifo101/ip/issues",
|
"issues": "https://github.com/lifo101/ip/issues",
|
||||||
"source": "https://github.com/lifo101/ip/tree/v1.1.1"
|
"source": "https://github.com/lifo101/ip/tree/master"
|
||||||
},
|
},
|
||||||
"time": "2022-07-12T15:45:54+00:00"
|
"time": "2020-04-02T11:09:10+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "mrclay/minify",
|
"name": "mrclay/minify",
|
||||||
@ -363,89 +267,30 @@
|
|||||||
},
|
},
|
||||||
"time": "2017-11-03T21:04:01+00:00"
|
"time": "2017-11-03T21:04:01+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "phpmyadmin/twig-i18n-extension",
|
|
||||||
"version": "v4.0.1",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/phpmyadmin/twig-i18n-extension.git",
|
|
||||||
"reference": "c0d0dd171cd1c7733bf152fd44b61055843df052"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/phpmyadmin/twig-i18n-extension/zipball/c0d0dd171cd1c7733bf152fd44b61055843df052",
|
|
||||||
"reference": "c0d0dd171cd1c7733bf152fd44b61055843df052",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": "^7.1 || ^8.0",
|
|
||||||
"twig/twig": "^1.42.3|^2.0|^3.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpmyadmin/coding-standard": "^3.0.0",
|
|
||||||
"phpmyadmin/motranslator": "^5.2",
|
|
||||||
"phpstan/phpstan": "^0.12.66",
|
|
||||||
"phpunit/phpunit": "^7 || ^8 || ^9"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"PhpMyAdmin\\Twig\\Extensions\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Fabien Potencier",
|
|
||||||
"email": "fabien@symfony.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "The phpMyAdmin Team",
|
|
||||||
"email": "developers@phpmyadmin.net",
|
|
||||||
"homepage": "https://www.phpmyadmin.net/team/"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Internationalization support for Twig via the gettext library",
|
|
||||||
"keywords": [
|
|
||||||
"gettext",
|
|
||||||
"i18n"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/phpmyadmin/twig-i18n-extension/issues",
|
|
||||||
"source": "https://github.com/phpmyadmin/twig-i18n-extension"
|
|
||||||
},
|
|
||||||
"time": "2021-06-10T15:53:38+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-ctype",
|
"name": "symfony/polyfill-ctype",
|
||||||
"version": "v1.27.0",
|
"version": "v1.23.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||||
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a"
|
"reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a",
|
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce",
|
||||||
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a",
|
"reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.1"
|
"php": ">=7.1"
|
||||||
},
|
},
|
||||||
"provide": {
|
|
||||||
"ext-ctype": "*"
|
|
||||||
},
|
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"ext-ctype": "For best performance"
|
"ext-ctype": "For best performance"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-main": "1.27-dev"
|
"dev-main": "1.23-dev"
|
||||||
},
|
},
|
||||||
"thanks": {
|
"thanks": {
|
||||||
"name": "symfony/polyfill",
|
"name": "symfony/polyfill",
|
||||||
@ -453,12 +298,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"files": [
|
|
||||||
"bootstrap.php"
|
|
||||||
],
|
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Symfony\\Polyfill\\Ctype\\": ""
|
"Symfony\\Polyfill\\Ctype\\": ""
|
||||||
}
|
},
|
||||||
|
"files": [
|
||||||
|
"bootstrap.php"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
"license": [
|
"license": [
|
||||||
@ -483,7 +328,7 @@
|
|||||||
"portable"
|
"portable"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0"
|
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -499,195 +344,34 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2022-11-03T14:55:06+00:00"
|
"time": "2021-02-19T12:13:01+00:00"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "symfony/polyfill-mbstring",
|
|
||||||
"version": "v1.27.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
|
||||||
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
|
|
||||||
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": ">=7.1"
|
|
||||||
},
|
|
||||||
"provide": {
|
|
||||||
"ext-mbstring": "*"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"ext-mbstring": "For best performance"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-main": "1.27-dev"
|
|
||||||
},
|
|
||||||
"thanks": {
|
|
||||||
"name": "symfony/polyfill",
|
|
||||||
"url": "https://github.com/symfony/polyfill"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"files": [
|
|
||||||
"bootstrap.php"
|
|
||||||
],
|
|
||||||
"psr-4": {
|
|
||||||
"Symfony\\Polyfill\\Mbstring\\": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Nicolas Grekas",
|
|
||||||
"email": "p@tchwork.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Symfony Community",
|
|
||||||
"homepage": "https://symfony.com/contributors"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Symfony polyfill for the Mbstring extension",
|
|
||||||
"homepage": "https://symfony.com",
|
|
||||||
"keywords": [
|
|
||||||
"compatibility",
|
|
||||||
"mbstring",
|
|
||||||
"polyfill",
|
|
||||||
"portable",
|
|
||||||
"shim"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://symfony.com/sponsor",
|
|
||||||
"type": "custom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/fabpot",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
|
||||||
"type": "tidelift"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2022-11-03T14:55:06+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "symfony/polyfill-php72",
|
|
||||||
"version": "v1.27.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/symfony/polyfill-php72.git",
|
|
||||||
"reference": "869329b1e9894268a8a61dabb69153029b7a8c97"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/869329b1e9894268a8a61dabb69153029b7a8c97",
|
|
||||||
"reference": "869329b1e9894268a8a61dabb69153029b7a8c97",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": ">=7.1"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-main": "1.27-dev"
|
|
||||||
},
|
|
||||||
"thanks": {
|
|
||||||
"name": "symfony/polyfill",
|
|
||||||
"url": "https://github.com/symfony/polyfill"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"files": [
|
|
||||||
"bootstrap.php"
|
|
||||||
],
|
|
||||||
"psr-4": {
|
|
||||||
"Symfony\\Polyfill\\Php72\\": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Nicolas Grekas",
|
|
||||||
"email": "p@tchwork.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Symfony Community",
|
|
||||||
"homepage": "https://symfony.com/contributors"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
|
|
||||||
"homepage": "https://symfony.com",
|
|
||||||
"keywords": [
|
|
||||||
"compatibility",
|
|
||||||
"polyfill",
|
|
||||||
"portable",
|
|
||||||
"shim"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"source": "https://github.com/symfony/polyfill-php72/tree/v1.27.0"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://symfony.com/sponsor",
|
|
||||||
"type": "custom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/fabpot",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
|
||||||
"type": "tidelift"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2022-11-03T14:55:06+00:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "twig/twig",
|
"name": "twig/twig",
|
||||||
"version": "v2.15.4",
|
"version": "v1.44.5",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/twigphp/Twig.git",
|
"url": "https://github.com/twigphp/Twig.git",
|
||||||
"reference": "3e059001d6d597dd50ea7c74dd2464b4adea48d3"
|
"reference": "dd4353357c5a116322e92a00d16043a31881a81e"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/3e059001d6d597dd50ea7c74dd2464b4adea48d3",
|
"url": "https://api.github.com/repos/twigphp/Twig/zipball/dd4353357c5a116322e92a00d16043a31881a81e",
|
||||||
"reference": "3e059001d6d597dd50ea7c74dd2464b4adea48d3",
|
"reference": "dd4353357c5a116322e92a00d16043a31881a81e",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.1.3",
|
"php": ">=7.2.5",
|
||||||
"symfony/polyfill-ctype": "^1.8",
|
"symfony/polyfill-ctype": "^1.8"
|
||||||
"symfony/polyfill-mbstring": "^1.3",
|
|
||||||
"symfony/polyfill-php72": "^1.8"
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"psr/container": "^1.0",
|
"psr/container": "^1.0",
|
||||||
"symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0"
|
"symfony/phpunit-bridge": "^4.4.9|^5.0.9"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "2.15-dev"
|
"dev-master": "1.44-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@ -726,7 +410,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/twigphp/Twig/issues",
|
"issues": "https://github.com/twigphp/Twig/issues",
|
||||||
"source": "https://github.com/twigphp/Twig/tree/v2.15.4"
|
"source": "https://github.com/twigphp/Twig/tree/v1.44.5"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -738,7 +422,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2022-12-27T12:26:20+00:00"
|
"time": "2021-09-17T08:35:19+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [],
|
"packages-dev": [],
|
||||||
@ -753,5 +437,5 @@
|
|||||||
"ext-pdo": ">=5.4"
|
"ext-pdo": ">=5.4"
|
||||||
},
|
},
|
||||||
"platform-dev": [],
|
"platform-dev": [],
|
||||||
"plugin-api-version": "2.3.0"
|
"plugin-api-version": "2.0.0"
|
||||||
}
|
}
|
||||||
|
@ -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.
|
|
@ -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
|
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
|
@ -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; }
|
|
||||||
}
|
|
@ -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
|
|
@ -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" ]
|
|
@ -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
|
|
@ -1,2 +0,0 @@
|
|||||||
opcache.jit_buffer_size=192M
|
|
||||||
opcache.jit=tracing
|
|
@ -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
|
|
@ -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
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 = [];
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
188
inc/anti-bot.php
188
inc/anti-bot.php
@ -1,5 +1,191 @@
|
|||||||
<?php
|
<?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.
|
* Copyright (c) 2010-2013 Tinyboard Development Group
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
defined('TINYBOARD') or exit;
|
||||||
|
|
||||||
|
$hidden_inputs_twig = array();
|
||||||
|
|
||||||
|
class AntiBot {
|
||||||
|
public $salt, $inputs = array(), $index = 0;
|
||||||
|
|
||||||
|
public static function randomString($length, $uppercase = false, $special_chars = false, $unicode_chars = false) {
|
||||||
|
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
if ($uppercase)
|
||||||
|
$chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
if ($special_chars)
|
||||||
|
$chars .= ' ~!@#$%^&*()_+,./;\'[]\\{}|:<>?=-` ';
|
||||||
|
if ($unicode_chars) {
|
||||||
|
$len = strlen($chars) / 10;
|
||||||
|
for ($n = 0; $n < $len; $n++)
|
||||||
|
$chars .= mb_convert_encoding('&#' . mt_rand(0x2600, 0x26FF) . ';', 'UTF-8', 'HTML-ENTITIES');
|
||||||
|
}
|
||||||
|
|
||||||
|
$chars = preg_split('//u', $chars, -1, PREG_SPLIT_NO_EMPTY);
|
||||||
|
|
||||||
|
$ch = array();
|
||||||
|
|
||||||
|
// fill up $ch until we reach $length
|
||||||
|
while (count($ch) < $length) {
|
||||||
|
$n = $length - count($ch);
|
||||||
|
$keys = array_rand($chars, $n > count($chars) ? count($chars) : $n);
|
||||||
|
if ($n == 1) {
|
||||||
|
$ch[] = $chars[$keys];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
shuffle($keys);
|
||||||
|
foreach ($keys as $key)
|
||||||
|
$ch[] = $chars[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
$chars = $ch;
|
||||||
|
|
||||||
|
return implode('', $chars);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function make_confusing($string) {
|
||||||
|
$chars = preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY);
|
||||||
|
|
||||||
|
foreach ($chars as &$c) {
|
||||||
|
if (mt_rand(0, 3) != 0)
|
||||||
|
$c = utf8tohtml($c);
|
||||||
|
else
|
||||||
|
$c = mb_encode_numericentity($c, array(0, 0xffff, 0, 0xffff), 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode('', $chars);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __construct(array $salt = array()) {
|
||||||
|
global $config;
|
||||||
|
|
||||||
|
if (!empty($salt)) {
|
||||||
|
// create a salted hash of the "extra salt"
|
||||||
|
$this->salt = implode(':', $salt);
|
||||||
|
} else {
|
||||||
|
$this->salt = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
shuffle($config['spam']['hidden_input_names']);
|
||||||
|
|
||||||
|
$input_count = mt_rand($config['spam']['hidden_inputs_min'], $config['spam']['hidden_inputs_max']);
|
||||||
|
$hidden_input_names_x = 0;
|
||||||
|
|
||||||
|
for ($x = 0; $x < $input_count ; $x++) {
|
||||||
|
if ($hidden_input_names_x === false || mt_rand(0, 2) == 0) {
|
||||||
|
// Use an obscure name
|
||||||
|
$name = $this->randomString(mt_rand(10, 40), false, false, $config['spam']['unicode']);
|
||||||
|
} else {
|
||||||
|
// Use a pre-defined confusing name
|
||||||
|
$name = $config['spam']['hidden_input_names'][$hidden_input_names_x++];
|
||||||
|
if ($hidden_input_names_x >= count($config['spam']['hidden_input_names']))
|
||||||
|
$hidden_input_names_x = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mt_rand(0, 2) == 0) {
|
||||||
|
// Value must be null
|
||||||
|
$this->inputs[$name] = '';
|
||||||
|
} elseif (mt_rand(0, 4) == 0) {
|
||||||
|
// Numeric value
|
||||||
|
$this->inputs[$name] = (string)mt_rand(0, 100000);
|
||||||
|
} else {
|
||||||
|
// Obscure value
|
||||||
|
$this->inputs[$name] = $this->randomString(mt_rand(5, 100), true, true, $config['spam']['unicode']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function space() {
|
||||||
|
if (mt_rand(0, 3) != 0)
|
||||||
|
return ' ';
|
||||||
|
return str_repeat(' ', mt_rand(1, 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function html($count = false) {
|
||||||
|
global $config;
|
||||||
|
|
||||||
|
$elements = array(
|
||||||
|
'<input type="hidden" name="%name%" value="%value%">',
|
||||||
|
'<input type="hidden" value="%value%" name="%name%">',
|
||||||
|
'<input name="%name%" value="%value%" type="hidden">',
|
||||||
|
'<input value="%value%" name="%name%" type="hidden">',
|
||||||
|
'<input style="display:none" type="text" name="%name%" value="%value%">',
|
||||||
|
'<input style="display:none" type="text" value="%value%" name="%name%">',
|
||||||
|
'<span style="display:none"><input type="text" name="%name%" value="%value%"></span>',
|
||||||
|
'<div style="display:none"><input type="text" name="%name%" value="%value%"></div>',
|
||||||
|
'<div style="display:none"><input type="text" name="%name%" value="%value%"></div>',
|
||||||
|
'<textarea style="display:none" name="%name%">%value%</textarea>',
|
||||||
|
'<textarea name="%name%" style="display:none">%value%</textarea>'
|
||||||
|
);
|
||||||
|
|
||||||
|
$html = '';
|
||||||
|
|
||||||
|
if ($count === false) {
|
||||||
|
$count = mt_rand(1, abs(count($this->inputs) / 15) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($count === true) {
|
||||||
|
// all elements
|
||||||
|
$inputs = array_slice($this->inputs, $this->index);
|
||||||
|
} else {
|
||||||
|
$inputs = array_slice($this->inputs, $this->index, $count);
|
||||||
|
}
|
||||||
|
$this->index += count($inputs);
|
||||||
|
|
||||||
|
foreach ($inputs as $name => $value) {
|
||||||
|
$element = false;
|
||||||
|
while (!$element) {
|
||||||
|
$element = $elements[array_rand($elements)];
|
||||||
|
$element = str_replace(' ', self::space(), $element);
|
||||||
|
if (mt_rand(0, 5) == 0)
|
||||||
|
$element = str_replace('>', self::space() . '>', $element);
|
||||||
|
if (strpos($element, 'textarea') !== false && $value == '') {
|
||||||
|
// There have been some issues with mobile web browsers and empty <textarea>'s.
|
||||||
|
$element = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$element = str_replace('%name%', utf8tohtml($name), $element);
|
||||||
|
|
||||||
|
if (mt_rand(0, 2) == 0)
|
||||||
|
$value = $this->make_confusing($value);
|
||||||
|
else
|
||||||
|
$value = utf8tohtml($value);
|
||||||
|
|
||||||
|
if (strpos($element, 'textarea') === false)
|
||||||
|
$value = str_replace('"', '"', $value);
|
||||||
|
|
||||||
|
$element = str_replace('%value%', $value, $element);
|
||||||
|
|
||||||
|
$html .= $element;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reset() {
|
||||||
|
$this->index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hash() {
|
||||||
|
global $config;
|
||||||
|
|
||||||
|
// This is the tricky part: create a hash to validate it after
|
||||||
|
// First, sort the keys in alphabetical order (A-Z)
|
||||||
|
$inputs = $this->inputs;
|
||||||
|
ksort($inputs);
|
||||||
|
|
||||||
|
$hash = '';
|
||||||
|
// Iterate through each input
|
||||||
|
foreach ($inputs as $name => $value) {
|
||||||
|
$hash .= $name . '=' . $value;
|
||||||
|
}
|
||||||
|
// Add a salt to the hash
|
||||||
|
$hash .= $config['cookies']['salt'];
|
||||||
|
|
||||||
|
// Use SHA1 for the hash
|
||||||
|
return sha1($hash . $this->salt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
146
inc/api.php
146
inc/api.php
@ -9,49 +9,14 @@ defined('TINYBOARD') or exit;
|
|||||||
* Class for generating json API compatible with 4chan API
|
* Class for generating json API compatible with 4chan API
|
||||||
*/
|
*/
|
||||||
class Api {
|
class Api {
|
||||||
private bool $show_filename;
|
function __construct(){
|
||||||
private bool $hide_email;
|
global $config;
|
||||||
private bool $country_flags;
|
/**
|
||||||
private array $postFields;
|
* Translation from local fields to fields in 4chan-style API
|
||||||
|
*/
|
||||||
|
$this->config = $config;
|
||||||
|
|
||||||
private const INTS = [
|
$this->postFields = array(
|
||||||
'no' => 1,
|
|
||||||
'resto' => 1,
|
|
||||||
'time' => 1,
|
|
||||||
'tn_w' => 1,
|
|
||||||
'tn_h' => 1,
|
|
||||||
'w' => 1,
|
|
||||||
'h' => 1,
|
|
||||||
'fsize' => 1,
|
|
||||||
'omitted_posts' => 1,
|
|
||||||
'omitted_images' => 1,
|
|
||||||
'replies' => 1,
|
|
||||||
'images' => 1,
|
|
||||||
'sticky' => 1,
|
|
||||||
'locked' => 1,
|
|
||||||
'last_modified' => 1
|
|
||||||
];
|
|
||||||
|
|
||||||
private const THREADS_PAGE_FIELDS = [
|
|
||||||
'id' => 'no',
|
|
||||||
'bump' => 'last_modified'
|
|
||||||
];
|
|
||||||
|
|
||||||
private const FILE_FIELDS = [
|
|
||||||
'thumbheight' => 'tn_h',
|
|
||||||
'thumbwidth' => 'tn_w',
|
|
||||||
'height' => 'h',
|
|
||||||
'width' => 'w',
|
|
||||||
'size' => 'fsize'
|
|
||||||
];
|
|
||||||
|
|
||||||
public function __construct(bool $show_filename, bool $hide_email, bool $country_flags) {
|
|
||||||
// Translation from local fields to fields in 4chan-style API
|
|
||||||
$this->show_filename = $show_filename;
|
|
||||||
$this->hide_email = $hide_email;
|
|
||||||
$this->country_flags = $country_flags;
|
|
||||||
|
|
||||||
$this->postFields = [
|
|
||||||
'id' => 'no',
|
'id' => 'no',
|
||||||
'thread' => 'resto',
|
'thread' => 'resto',
|
||||||
'subject' => 'sub',
|
'subject' => 'sub',
|
||||||
@ -70,65 +35,83 @@ class Api {
|
|||||||
'cycle' => 'cyclical',
|
'cycle' => 'cyclical',
|
||||||
'bump' => 'last_modified',
|
'bump' => 'last_modified',
|
||||||
'embed' => 'embed',
|
'embed' => 'embed',
|
||||||
];
|
);
|
||||||
|
|
||||||
|
$this->threadsPageFields = array(
|
||||||
|
'id' => 'no',
|
||||||
|
'bump' => 'last_modified'
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->fileFields = array(
|
||||||
|
'thumbheight' => 'tn_h',
|
||||||
|
'thumbwidth' => 'tn_w',
|
||||||
|
'height' => 'h',
|
||||||
|
'width' => 'w',
|
||||||
|
'size' => 'fsize',
|
||||||
|
);
|
||||||
|
|
||||||
if (isset($config['api']['extra_fields']) && gettype($config['api']['extra_fields']) == 'array'){
|
if (isset($config['api']['extra_fields']) && gettype($config['api']['extra_fields']) == 'array'){
|
||||||
$this->postFields = array_merge($this->postFields, $config['api']['extra_fields']);
|
$this->postFields = array_merge($this->postFields, $config['api']['extra_fields']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static $ints = array(
|
||||||
|
'no' => 1,
|
||||||
|
'resto' => 1,
|
||||||
|
'time' => 1,
|
||||||
|
'tn_w' => 1,
|
||||||
|
'tn_h' => 1,
|
||||||
|
'w' => 1,
|
||||||
|
'h' => 1,
|
||||||
|
'fsize' => 1,
|
||||||
|
'omitted_posts' => 1,
|
||||||
|
'omitted_images' => 1,
|
||||||
|
'replies' => 1,
|
||||||
|
'images' => 1,
|
||||||
|
'sticky' => 1,
|
||||||
|
'locked' => 1,
|
||||||
|
'last_modified' => 1
|
||||||
|
);
|
||||||
|
|
||||||
private function translateFields($fields, $object, &$apiPost) {
|
private function translateFields($fields, $object, &$apiPost) {
|
||||||
foreach ($fields as $local => $translated) {
|
foreach ($fields as $local => $translated) {
|
||||||
if (!isset($object->$local)) {
|
if (!isset($object->$local))
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
$toInt = isset(self::INTS[$translated]);
|
$toInt = isset(self::$ints[$translated]);
|
||||||
$val = $object->$local;
|
$val = $object->$local;
|
||||||
if ($this->hide_email && $local === 'email') {
|
|
||||||
$val = '';
|
|
||||||
}
|
|
||||||
if ($val !== null && $val !== '') {
|
if ($val !== null && $val !== '') {
|
||||||
$apiPost[$translated] = $toInt ? (int) $val : $val;
|
$apiPost[$translated] = $toInt ? (int) $val : $val;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function translateFile($file, $post, &$apiPost) {
|
private function translateFile($file, $post, &$apiPost) {
|
||||||
$this->translateFields(self::FILE_FIELDS, $file, $apiPost);
|
$this->translateFields($this->fileFields, $file, $apiPost);
|
||||||
|
$apiPost['filename'] = @substr($file->name, 0, strrpos($file->name, '.'));
|
||||||
$dotPos = strrpos($file->file, '.');
|
$dotPos = strrpos($file->file, '.');
|
||||||
$apiPost['ext'] = substr($file->file, $dotPos);
|
$apiPost['ext'] = substr($file->file, $dotPos);
|
||||||
$apiPost['tim'] = substr($file->file, 0, $dotPos);
|
$apiPost['tim'] = substr($file->file, 0, $dotPos);
|
||||||
|
|
||||||
if ($this->show_filename) {
|
|
||||||
$apiPost['filename'] = @substr($file->name, 0, strrpos($file->name, '.'));
|
|
||||||
} else {
|
|
||||||
$apiPost['filename'] = substr($file->file, 0, $dotPos);
|
|
||||||
}
|
|
||||||
if (isset ($file->hash) && $file->hash) {
|
if (isset ($file->hash) && $file->hash) {
|
||||||
$apiPost['md5'] = base64_encode(hex2bin($file->hash));
|
$apiPost['md5'] = base64_encode(hex2bin($file->hash));
|
||||||
} elseif (isset ($post->filehash) && $post->filehash) {
|
}
|
||||||
|
else if (isset ($post->filehash) && $post->filehash) {
|
||||||
$apiPost['md5'] = base64_encode(hex2bin($post->filehash));
|
$apiPost['md5'] = base64_encode(hex2bin($post->filehash));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function translatePost($post, bool $threadsPage = false) {
|
private function translatePost($post, $threadsPage = false) {
|
||||||
global $config, $board;
|
global $config, $board;
|
||||||
|
$apiPost = array();
|
||||||
$apiPost = [];
|
$fields = $threadsPage ? $this->threadsPageFields : $this->postFields;
|
||||||
$fields = $threadsPage ? self::THREADS_PAGE_FIELDS : $this->postFields;
|
|
||||||
$this->translateFields($fields, $post, $apiPost);
|
$this->translateFields($fields, $post, $apiPost);
|
||||||
|
|
||||||
|
if (isset($config['poster_ids']) && $config['poster_ids']) $apiPost['id'] = poster_id($post->ip, $post->thread, $board['uri']);
|
||||||
if (isset($config['poster_ids']) && $config['poster_ids']) {
|
if ($threadsPage) return $apiPost;
|
||||||
$apiPost['id'] = poster_id($post->ip, $post->thread ?? $post->id);
|
|
||||||
}
|
|
||||||
if ($threadsPage) {
|
|
||||||
return $apiPost;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle country field
|
// Handle country field
|
||||||
if (isset($post->body_nomarkup) && $this->country_flags) {
|
if (isset($post->body_nomarkup) && $this->config['country_flags']) {
|
||||||
$modifiers = extract_modifiers($post->body_nomarkup);
|
$modifiers = extract_modifiers($post->body_nomarkup);
|
||||||
if (isset($modifiers['flag']) && isset($modifiers['flag alt']) && preg_match('/^[a-z]{2}$/', $modifiers['flag'])) {
|
if (isset($modifiers['flag']) && isset($modifiers['flag alt']) && preg_match('/^[a-z]{2}$/', $modifiers['flag'])) {
|
||||||
$country = strtoupper($modifiers['flag']);
|
$country = strtoupper($modifiers['flag']);
|
||||||
@ -148,15 +131,12 @@ class Api {
|
|||||||
if (isset($post->files) && $post->files && !$threadsPage) {
|
if (isset($post->files) && $post->files && !$threadsPage) {
|
||||||
$file = $post->files[0];
|
$file = $post->files[0];
|
||||||
$this->translateFile($file, $post, $apiPost);
|
$this->translateFile($file, $post, $apiPost);
|
||||||
|
|
||||||
if (sizeof($post->files) > 1) {
|
if (sizeof($post->files) > 1) {
|
||||||
$extra_files = [];
|
$extra_files = array();
|
||||||
foreach ($post->files as $i => $f) {
|
foreach ($post->files as $i => $f) {
|
||||||
if ($i == 0) {
|
if ($i == 0) continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$extra_file = [];
|
$extra_file = array();
|
||||||
$this->translateFile($f, $post, $extra_file);
|
$this->translateFile($f, $post, $extra_file);
|
||||||
|
|
||||||
$extra_files[] = $extra_file;
|
$extra_files[] = $extra_file;
|
||||||
@ -168,8 +148,8 @@ class Api {
|
|||||||
return $apiPost;
|
return $apiPost;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function translateThread(Thread $thread, bool $threadsPage = false) {
|
function translateThread(Thread $thread, $threadsPage = false) {
|
||||||
$apiPosts = [];
|
$apiPosts = array();
|
||||||
$op = $this->translatePost($thread, $threadsPage);
|
$op = $this->translatePost($thread, $threadsPage);
|
||||||
if (!$threadsPage) $op['resto'] = 0;
|
if (!$threadsPage) $op['resto'] = 0;
|
||||||
$apiPosts['posts'][] = $op;
|
$apiPosts['posts'][] = $op;
|
||||||
@ -181,16 +161,16 @@ class Api {
|
|||||||
return $apiPosts;
|
return $apiPosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function translatePage(array $threads) {
|
function translatePage(array $threads) {
|
||||||
$apiPage = [];
|
$apiPage = array();
|
||||||
foreach ($threads as $thread) {
|
foreach ($threads as $thread) {
|
||||||
$apiPage['threads'][] = $this->translateThread($thread);
|
$apiPage['threads'][] = $this->translateThread($thread);
|
||||||
}
|
}
|
||||||
return $apiPage;
|
return $apiPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function translateCatalogPage(array $threads, bool $threadsPage = false) {
|
function translateCatalogPage(array $threads, $threadsPage = false) {
|
||||||
$apiPage = [];
|
$apiPage = array();
|
||||||
foreach ($threads as $thread) {
|
foreach ($threads as $thread) {
|
||||||
$ts = $this->translateThread($thread, $threadsPage);
|
$ts = $this->translateThread($thread, $threadsPage);
|
||||||
$apiPage['threads'][] = current($ts['posts']);
|
$apiPage['threads'][] = current($ts['posts']);
|
||||||
@ -198,8 +178,8 @@ class Api {
|
|||||||
return $apiPage;
|
return $apiPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function translateCatalog($catalog, bool $threadsPage = false) {
|
function translateCatalog($catalog, $threadsPage = false) {
|
||||||
$apiCatalog = [];
|
$apiCatalog = array();
|
||||||
foreach ($catalog as $page => $threads) {
|
foreach ($catalog as $page => $threads) {
|
||||||
$apiPage = $this->translateCatalogPage($threads, $threadsPage);
|
$apiPage = $this->translateCatalogPage($threads, $threadsPage);
|
||||||
$apiPage['page'] = $page;
|
$apiPage['page'] = $page;
|
||||||
|
257
inc/bans.php
257
inc/bans.php
@ -1,163 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Vichan\Functions\Format;
|
|
||||||
use Lifo\IP\CIDR;
|
use Lifo\IP\CIDR;
|
||||||
|
|
||||||
class Bans {
|
class Bans {
|
||||||
static private function shouldDelete(array $ban, bool $require_ban_view) {
|
|
||||||
return $ban['expires'] && ($ban['seen'] || !$require_ban_view) && $ban['expires'] < time();
|
|
||||||
}
|
|
||||||
|
|
||||||
static private function deleteBans(array $ban_ids) {
|
|
||||||
$len = count($ban_ids);
|
|
||||||
if ($len === 1) {
|
|
||||||
$query = prepare('DELETE FROM ``bans`` WHERE `id` = :id');
|
|
||||||
$query->bindValue(':id', $ban_ids[0], PDO::PARAM_INT);
|
|
||||||
$query->execute() or error(db_error());
|
|
||||||
|
|
||||||
Vichan\Functions\Theme\rebuild_themes('bans');
|
|
||||||
} elseif ($len >= 1) {
|
|
||||||
// Build the query.
|
|
||||||
$query = 'DELETE FROM ``bans`` WHERE `id` IN (';
|
|
||||||
for ($i = 0; $i < $len; $i++) {
|
|
||||||
$query .= ":id{$i},";
|
|
||||||
}
|
|
||||||
// Substitute the last comma with a parenthesis.
|
|
||||||
substr_replace($query, ')', strlen($query) - 1);
|
|
||||||
|
|
||||||
// Bind the params
|
|
||||||
$query = prepare($query);
|
|
||||||
for ($i = 0; $i < $len; $i++) {
|
|
||||||
$query->bindValue(":id{$i}", (int)$ban_ids[$i], PDO::PARAM_INT);
|
|
||||||
}
|
|
||||||
|
|
||||||
$query->execute() or error(db_error());
|
|
||||||
|
|
||||||
Vichan\Functions\Theme\rebuild_themes('bans');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static private function findSingleAutoGc(string $ip, int $ban_id, bool $require_ban_view) {
|
|
||||||
// Use OR in the query to also garbage collect bans.
|
|
||||||
$query = prepare(
|
|
||||||
'SELECT ``bans``.* FROM ``bans``
|
|
||||||
WHERE ((`ipstart` = :ip OR (:ip >= `ipstart` AND :ip <= `ipend`)) OR (``bans``.id = :id))
|
|
||||||
ORDER BY `expires` IS NULL, `expires` DESC'
|
|
||||||
);
|
|
||||||
|
|
||||||
$query->bindValue(':id', $ban_id);
|
|
||||||
$query->bindValue(':ip', inet_pton($ip));
|
|
||||||
|
|
||||||
$query->execute() or error(db_error($query));
|
|
||||||
|
|
||||||
$found_ban = null;
|
|
||||||
$to_delete_list = [];
|
|
||||||
|
|
||||||
while ($ban = $query->fetch(PDO::FETCH_ASSOC)) {
|
|
||||||
if (self::shouldDelete($ban, $require_ban_view)) {
|
|
||||||
$to_delete_list[] = $ban['id'];
|
|
||||||
} elseif ($ban['id'] === $ban_id) {
|
|
||||||
if ($ban['post']) {
|
|
||||||
$ban['post'] = json_decode($ban['post'], true);
|
|
||||||
}
|
|
||||||
$ban['mask'] = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
|
|
||||||
$found_ban = $ban;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self::deleteBans($to_delete_list);
|
|
||||||
|
|
||||||
return $found_ban;
|
|
||||||
}
|
|
||||||
|
|
||||||
static private function findSingleNoGc(int $ban_id) {
|
|
||||||
$query = prepare(
|
|
||||||
'SELECT ``bans``.* FROM ``bans``
|
|
||||||
WHERE ``bans``.id = :id
|
|
||||||
ORDER BY `expires` IS NULL, `expires` DESC
|
|
||||||
LIMIT 1'
|
|
||||||
);
|
|
||||||
|
|
||||||
$query->bindValue(':id', $ban_id);
|
|
||||||
|
|
||||||
$query->execute() or error(db_error($query));
|
|
||||||
$ret = $query->fetch(PDO::FETCH_ASSOC);
|
|
||||||
if ($query->rowCount() == 0) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
if ($ret['post']) {
|
|
||||||
$ret['post'] = json_decode($ret['post'], true);
|
|
||||||
}
|
|
||||||
$ret['mask'] = self::range_to_string([$ret['ipstart'], $ret['ipend']]);
|
|
||||||
|
|
||||||
return $ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static private function findAutoGc(?string $ip, $board, bool $get_mod_info, bool $require_ban_view, ?int $ban_id): array {
|
|
||||||
$query = prepare('SELECT ``bans``.*' . ($get_mod_info ? ', `username`' : '') . ' FROM ``bans``
|
|
||||||
' . ($get_mod_info ? 'LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`' : '') . '
|
|
||||||
WHERE
|
|
||||||
(' . ($board !== false ? '(`board` IS NULL OR `board` = :board) AND' : '') . '
|
|
||||||
(`ipstart` = :ip OR (:ip >= `ipstart` AND :ip <= `ipend`)) OR (``bans``.id = :id))
|
|
||||||
ORDER BY `expires` IS NULL, `expires` DESC');
|
|
||||||
|
|
||||||
if ($board !== false) {
|
|
||||||
$query->bindValue(':board', $board, PDO::PARAM_STR);
|
|
||||||
}
|
|
||||||
|
|
||||||
$query->bindValue(':id', $ban_id);
|
|
||||||
$query->bindValue(':ip', inet_pton($ip));
|
|
||||||
$query->execute() or error(db_error($query));
|
|
||||||
|
|
||||||
$ban_list = [];
|
|
||||||
$to_delete_list = [];
|
|
||||||
|
|
||||||
while ($ban = $query->fetch(PDO::FETCH_ASSOC)) {
|
|
||||||
if (self::shouldDelete($ban, $require_ban_view)) {
|
|
||||||
$to_delete_list[] = $ban['id'];
|
|
||||||
} else {
|
|
||||||
if ($ban['post']) {
|
|
||||||
$ban['post'] = json_decode($ban['post'], true);
|
|
||||||
}
|
|
||||||
$ban['mask'] = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
|
|
||||||
$ban_list[] = $ban;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self::deleteBans($to_delete_list);
|
|
||||||
|
|
||||||
return $ban_list;
|
|
||||||
}
|
|
||||||
|
|
||||||
static private function findNoGc(?string $ip, string $board, bool $get_mod_info, ?int $ban_id): array {
|
|
||||||
$query = prepare('SELECT ``bans``.*' . ($get_mod_info ? ', `username`' : '') . ' FROM ``bans``
|
|
||||||
' . ($get_mod_info ? 'LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`' : '') . '
|
|
||||||
WHERE
|
|
||||||
(' . ($board !== false ? '(`board` IS NULL OR `board` = :board) AND' : '') . '
|
|
||||||
(`ipstart` = :ip OR (:ip >= `ipstart` AND :ip <= `ipend`)) OR (``bans``.id = :id))
|
|
||||||
AND (`expires` IS NULL OR `expires` >= :curr_time)
|
|
||||||
ORDER BY `expires` IS NULL, `expires` DESC');
|
|
||||||
|
|
||||||
if ($board !== false) {
|
|
||||||
$query->bindValue(':board', $board, PDO::PARAM_STR);
|
|
||||||
}
|
|
||||||
|
|
||||||
$query->bindValue(':id', $ban_id);
|
|
||||||
$query->bindValue(':ip', inet_pton($ip));
|
|
||||||
$query->bindValue(':curr_time', time());
|
|
||||||
$query->execute() or error(db_error($query));
|
|
||||||
|
|
||||||
$ban_list = $query->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
array_walk($ban_list, function (&$ban, $_index) {
|
|
||||||
if ($ban['post']) {
|
|
||||||
$ban['post'] = json_decode($ban['post'], true);
|
|
||||||
}
|
|
||||||
$ban['mask'] = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
|
|
||||||
});
|
|
||||||
return $ban_list;
|
|
||||||
}
|
|
||||||
|
|
||||||
static public function range_to_string($mask) {
|
static public function range_to_string($mask) {
|
||||||
list($ipstart, $ipend) = $mask;
|
list($ipstart, $ipend) = $mask;
|
||||||
|
|
||||||
@ -181,7 +26,7 @@ class Bans {
|
|||||||
$cidr = new CIDR($mask);
|
$cidr = new CIDR($mask);
|
||||||
$range = $cidr->getRange();
|
$range = $cidr->getRange();
|
||||||
|
|
||||||
return [ inet_pton($range[0]), inet_pton($range[1]) ];
|
return array(inet_pton($range[0]), inet_pton($range[1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function parse_time($str) {
|
public static function parse_time($str) {
|
||||||
@ -265,27 +110,42 @@ class Bans {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [$ipstart, $ipend];
|
return array($ipstart, $ipend);
|
||||||
}
|
}
|
||||||
|
|
||||||
static public function findSingle(string $ip, int $ban_id, bool $require_ban_view, bool $auto_gc) {
|
static public function find($ip, $board = false, $get_mod_info = false) {
|
||||||
if ($auto_gc) {
|
|
||||||
return self::findSingleAutoGc($ip, $ban_id, $require_ban_view);
|
|
||||||
} else {
|
|
||||||
return self::findSingleNoGc($ban_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static public function find(?string $ip, $board = false, bool $get_mod_info = false, ?int $ban_id = null, bool $auto_gc = true) {
|
|
||||||
global $config;
|
global $config;
|
||||||
|
|
||||||
if ($auto_gc) {
|
$query = prepare('SELECT ``bans``.*' . ($get_mod_info ? ', `username`' : '') . ' FROM ``bans``
|
||||||
return self::findAutoGc($ip, $board, $get_mod_info, $config['require_ban_view'], $ban_id);
|
' . ($get_mod_info ? 'LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`' : '') . '
|
||||||
|
WHERE
|
||||||
|
(' . ($board !== false ? '(`board` IS NULL OR `board` = :board) AND' : '') . '
|
||||||
|
(`ipstart` = :ip OR (:ip >= `ipstart` AND :ip <= `ipend`)))
|
||||||
|
ORDER BY `expires` IS NULL, `expires` DESC');
|
||||||
|
|
||||||
|
if ($board !== false)
|
||||||
|
$query->bindValue(':board', $board, PDO::PARAM_STR);
|
||||||
|
|
||||||
|
$query->bindValue(':ip', inet_pton($ip));
|
||||||
|
$query->execute() or error(db_error($query));
|
||||||
|
|
||||||
|
$ban_list = array();
|
||||||
|
|
||||||
|
while ($ban = $query->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
if ($ban['expires'] && ($ban['seen'] || !$config['require_ban_view']) && $ban['expires'] < time()) {
|
||||||
|
self::delete($ban['id']);
|
||||||
} else {
|
} else {
|
||||||
return self::findNoGc($ip, $board, $get_mod_info, $ban_id);
|
if ($ban['post'])
|
||||||
|
$ban['post'] = json_decode($ban['post'], true);
|
||||||
|
$ban['mask'] = self::range_to_string(array($ban['ipstart'], $ban['ipend']));
|
||||||
|
$ban['cmask'] = cloak_mask($ban['mask']);
|
||||||
|
$ban_list[] = $ban;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $ban_list;
|
||||||
|
}
|
||||||
|
|
||||||
static public function stream_json($out = false, $filter_ips = false, $filter_staff = false, $board_access = false) {
|
static public function stream_json($out = false, $filter_ips = false, $filter_staff = false, $board_access = false) {
|
||||||
$query = query("SELECT ``bans``.*, `username` FROM ``bans``
|
$query = query("SELECT ``bans``.*, `username` FROM ``bans``
|
||||||
LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`
|
LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`
|
||||||
@ -299,7 +159,8 @@ class Bans {
|
|||||||
$end = end($bans);
|
$end = end($bans);
|
||||||
|
|
||||||
foreach ($bans as &$ban) {
|
foreach ($bans as &$ban) {
|
||||||
$ban['mask'] = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
|
$uncloaked_mask = self::range_to_string(array($ban['ipstart'], $ban['ipend']));
|
||||||
|
$ban['mask'] = cloak_mask($uncloaked_mask);
|
||||||
|
|
||||||
if ($ban['post']) {
|
if ($ban['post']) {
|
||||||
$post = json_decode($ban['post']);
|
$post = json_decode($ban['post']);
|
||||||
@ -343,24 +204,12 @@ class Bans {
|
|||||||
|
|
||||||
static public function seen($ban_id) {
|
static public function seen($ban_id) {
|
||||||
$query = query("UPDATE ``bans`` SET `seen` = 1 WHERE `id` = " . (int)$ban_id) or error(db_error());
|
$query = query("UPDATE ``bans`` SET `seen` = 1 WHERE `id` = " . (int)$ban_id) or error(db_error());
|
||||||
Vichan\Functions\Theme\rebuild_themes('bans');
|
rebuildThemes('bans');
|
||||||
}
|
}
|
||||||
|
|
||||||
static public function purge($require_seen, $moratorium) {
|
static public function purge() {
|
||||||
if ($require_seen) {
|
$query = query("DELETE FROM ``bans`` WHERE `expires` IS NOT NULL AND `expires` < " . time() . " AND `seen` = 1") or error(db_error());
|
||||||
$query = prepare("DELETE FROM ``bans`` WHERE `expires` IS NOT NULL AND `expires` + :moratorium < :curr_time AND `seen` = 1");
|
rebuildThemes('bans');
|
||||||
} else {
|
|
||||||
$query = prepare("DELETE FROM ``bans`` WHERE `expires` IS NOT NULL AND `expires` + :moratorium < :curr_time");
|
|
||||||
}
|
|
||||||
$query->bindValue(':moratorium', $moratorium);
|
|
||||||
$query->bindValue(':curr_time', time());
|
|
||||||
$query->execute() or error(db_error($query));
|
|
||||||
|
|
||||||
$affected = $query->rowCount();
|
|
||||||
if ($affected > 0) {
|
|
||||||
Vichan\Functions\Theme\rebuild_themes('bans');
|
|
||||||
}
|
|
||||||
return $affected;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static public function delete($ban_id, $modlog = false, $boards = false, $dont_rebuild = false) {
|
static public function delete($ban_id, $modlog = false, $boards = false, $dont_rebuild = false) {
|
||||||
@ -378,7 +227,8 @@ class Bans {
|
|||||||
if ($boards !== false && !in_array($ban['board'], $boards))
|
if ($boards !== false && !in_array($ban['board'], $boards))
|
||||||
error($config['error']['noaccess']);
|
error($config['error']['noaccess']);
|
||||||
|
|
||||||
$mask = self::range_to_string([$ban['ipstart'], $ban['ipend']]);
|
$mask = self::range_to_string(array($ban['ipstart'], $ban['ipend']));
|
||||||
|
$cloaked_mask = cloak_mask($mask);
|
||||||
|
|
||||||
modLog("Removed ban #{$ban_id} for " .
|
modLog("Removed ban #{$ban_id} for " .
|
||||||
(filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$cloaked_mask\">$cloaked_mask</a>" : $cloaked_mask));
|
(filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$cloaked_mask\">$cloaked_mask</a>" : $cloaked_mask));
|
||||||
@ -386,7 +236,7 @@ class Bans {
|
|||||||
|
|
||||||
query("DELETE FROM ``bans`` WHERE `id` = " . (int)$ban_id) or error(db_error());
|
query("DELETE FROM ``bans`` WHERE `id` = " . (int)$ban_id) or error(db_error());
|
||||||
|
|
||||||
if (!$dont_rebuild) Vichan\Functions\Theme\rebuild_themes('bans');
|
if (!$dont_rebuild) rebuildThemes('bans');
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -439,33 +289,24 @@ class Bans {
|
|||||||
$query->bindValue(':board', null, PDO::PARAM_NULL);
|
$query->bindValue(':board', null, PDO::PARAM_NULL);
|
||||||
|
|
||||||
if ($post) {
|
if ($post) {
|
||||||
if (!isset($board['uri']))
|
|
||||||
openBoard($post['board']);
|
|
||||||
|
|
||||||
$post['board'] = $board['uri'];
|
$post['board'] = $board['uri'];
|
||||||
/*
|
|
||||||
* The body can be so long to make the json longer than 64KBs, causing the query to fail.
|
|
||||||
* Truncate it to a safe length (32KBs). It could probably be longer, but if the deleted body is THAT big
|
|
||||||
* already, the likelihood of it being just assorted spam/garbage is about 101%.
|
|
||||||
*/
|
|
||||||
// We're on UTF-8 only, right...?
|
|
||||||
$post['body'] = mb_strcut($post['body'], 0, 32768);
|
|
||||||
|
|
||||||
$query->bindValue(':post', json_encode($post));
|
$query->bindValue(':post', json_encode($post));
|
||||||
} else
|
} else
|
||||||
$query->bindValue(':post', null, PDO::PARAM_NULL);
|
$query->bindValue(':post', null, PDO::PARAM_NULL);
|
||||||
|
|
||||||
$query->execute() or error(db_error($query));
|
$query->execute() or error(db_error($query));
|
||||||
|
if (isset($mod['id']) && $mod['id'] == $mod_id) {
|
||||||
|
modLog('Created a new ' .
|
||||||
|
($length > 0 ? preg_replace('/^(\d+) (\w+?)s?$/', '$1-$2', until($length)) : 'permanent') .
|
||||||
|
' ban on ' .
|
||||||
|
($ban_board ? '/' . $ban_board . '/' : 'all boards') .
|
||||||
|
' for ' .
|
||||||
|
(filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$cloaked_mask\">$cloaked_mask</a>" : $cloaked_mask) .
|
||||||
|
' (<small>#' . $pdo->lastInsertId() . '</small>)' .
|
||||||
|
' with ' . ($reason ? 'reason: ' . utf8tohtml($reason) . '' : 'no reason'));
|
||||||
|
}
|
||||||
|
|
||||||
$ban_len = $length > 0 ? preg_replace('/^(\d+) (\w+?)s?$/', '$1-$2', Format\until($length)) : 'permanent';
|
rebuildThemes('bans');
|
||||||
$ban_board = $ban_board ? "/$ban_board/" : 'all boards';
|
|
||||||
$ban_ip = filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$cloaked_mask\">$cloaked_mask</a>" : $cloaked_mask;
|
|
||||||
$ban_id = $pdo->lastInsertId();
|
|
||||||
$ban_reason = $reason ? 'reason: ' . utf8tohtml($reason) : 'no reason';
|
|
||||||
|
|
||||||
modLog("Created a new $ban_len ban on $ban_board for $ban_ip (<small># $ban_id </small>) with $ban_reason");
|
|
||||||
|
|
||||||
Vichan\Functions\Theme\rebuild_themes('bans');
|
|
||||||
|
|
||||||
return $pdo->lastInsertId();
|
return $pdo->lastInsertId();
|
||||||
}
|
}
|
||||||
|
198
inc/cache.php
198
inc/cache.php
@ -4,89 +4,185 @@
|
|||||||
* Copyright (c) 2010-2013 Tinyboard Development Group
|
* Copyright (c) 2010-2013 Tinyboard Development Group
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use Vichan\Data\Driver\{CacheDriver, ApcuCacheDriver, ArrayCacheDriver, FsCacheDriver, MemcachedCacheDriver, NoneCacheDriver, RedisCacheDriver};
|
|
||||||
|
|
||||||
defined('TINYBOARD') or exit;
|
defined('TINYBOARD') or exit;
|
||||||
|
|
||||||
|
|
||||||
class Cache {
|
class Cache {
|
||||||
private static function buildCache(): CacheDriver {
|
private static $cache;
|
||||||
|
public static function init() {
|
||||||
global $config;
|
global $config;
|
||||||
|
|
||||||
switch ($config['cache']['enabled']) {
|
switch ($config['cache']['enabled']) {
|
||||||
case 'memcached':
|
case 'memcached':
|
||||||
return new MemcachedCacheDriver(
|
self::$cache = new Memcached();
|
||||||
$config['cache']['prefix'],
|
self::$cache->addServers($config['cache']['memcached']);
|
||||||
$config['cache']['memcached']
|
break;
|
||||||
);
|
|
||||||
case 'redis':
|
case 'redis':
|
||||||
return new RedisCacheDriver(
|
self::$cache = new Redis();
|
||||||
$config['cache']['prefix'],
|
self::$cache->connect($config['cache']['redis'][0], $config['cache']['redis'][1]);
|
||||||
$config['cache']['redis'][0],
|
if ($config['cache']['redis'][2]) {
|
||||||
$config['cache']['redis'][1],
|
self::$cache->auth($config['cache']['redis'][2]);
|
||||||
$config['cache']['redis'][2],
|
}
|
||||||
$config['cache']['redis'][3]
|
self::$cache->select($config['cache']['redis'][3]) or die('cache select failure');
|
||||||
);
|
break;
|
||||||
case 'apcu':
|
|
||||||
return new ApcuCacheDriver;
|
|
||||||
case 'fs':
|
|
||||||
return new FsCacheDriver(
|
|
||||||
$config['cache']['prefix'],
|
|
||||||
"tmp/cache/{$config['cache']['prefix']}",
|
|
||||||
'.lock',
|
|
||||||
$config['auto_maintenance'] ? 1000 : false
|
|
||||||
);
|
|
||||||
case 'none':
|
|
||||||
return new NoneCacheDriver();
|
|
||||||
case 'php':
|
case 'php':
|
||||||
default:
|
self::$cache = array();
|
||||||
return new ArrayCacheDriver();
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getCache(): CacheDriver {
|
|
||||||
static $cache;
|
|
||||||
return $cache ??= self::buildCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function get($key) {
|
public static function get($key) {
|
||||||
global $config, $debug;
|
global $config, $debug;
|
||||||
|
|
||||||
$ret = self::getCache()->get($key);
|
$key = $config['cache']['prefix'] . $key;
|
||||||
if ($ret === null) {
|
|
||||||
$ret = false;
|
$data = false;
|
||||||
|
switch ($config['cache']['enabled']) {
|
||||||
|
case 'memcached':
|
||||||
|
if (!self::$cache)
|
||||||
|
self::init();
|
||||||
|
$data = self::$cache->get($key);
|
||||||
|
break;
|
||||||
|
case 'apc':
|
||||||
|
$data = apc_fetch($key);
|
||||||
|
break;
|
||||||
|
case 'apcu':
|
||||||
|
$data = apcu_fetch($key);
|
||||||
|
break;
|
||||||
|
case 'xcache':
|
||||||
|
$data = xcache_get($key);
|
||||||
|
break;
|
||||||
|
case 'php':
|
||||||
|
$data = isset(self::$cache[$key]) ? self::$cache[$key] : false;
|
||||||
|
break;
|
||||||
|
case 'fs':
|
||||||
|
$key = str_replace('/', '::', $key);
|
||||||
|
$key = str_replace("\0", '', $key);
|
||||||
|
if (!file_exists('tmp/cache/'.$key)) {
|
||||||
|
$data = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$data = file_get_contents('tmp/cache/'.$key);
|
||||||
|
$data = json_decode($data, true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'redis':
|
||||||
|
if (!self::$cache)
|
||||||
|
self::init();
|
||||||
|
$data = json_decode(self::$cache->get($key), true);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($config['debug']) {
|
if ($config['debug'])
|
||||||
$debug['cached'][] = $config['cache']['prefix'] . $key . ($ret === false ? ' (miss)' : ' (hit)');
|
$debug['cached'][] = $key . ($data === false ? ' (miss)' : ' (hit)');
|
||||||
}
|
|
||||||
|
|
||||||
return $ret;
|
return $data;
|
||||||
}
|
}
|
||||||
public static function set($key, $value, $expires = false) {
|
public static function set($key, $value, $expires = false) {
|
||||||
global $config, $debug;
|
global $config, $debug;
|
||||||
|
|
||||||
if (!$expires) {
|
$key = $config['cache']['prefix'] . $key;
|
||||||
|
|
||||||
|
if (!$expires)
|
||||||
$expires = $config['cache']['timeout'];
|
$expires = $config['cache']['timeout'];
|
||||||
|
|
||||||
|
switch ($config['cache']['enabled']) {
|
||||||
|
case 'memcached':
|
||||||
|
if (!self::$cache)
|
||||||
|
self::init();
|
||||||
|
self::$cache->set($key, $value, $expires);
|
||||||
|
break;
|
||||||
|
case 'redis':
|
||||||
|
if (!self::$cache)
|
||||||
|
self::init();
|
||||||
|
self::$cache->setex($key, $expires, json_encode($value));
|
||||||
|
break;
|
||||||
|
case 'apc':
|
||||||
|
apc_store($key, $value, $expires);
|
||||||
|
break;
|
||||||
|
case 'apcu':
|
||||||
|
apcu_store($key, $value, $expires);
|
||||||
|
break;
|
||||||
|
case 'xcache':
|
||||||
|
xcache_set($key, $value, $expires);
|
||||||
|
break;
|
||||||
|
case 'fs':
|
||||||
|
$key = str_replace('/', '::', $key);
|
||||||
|
$key = str_replace("\0", '', $key);
|
||||||
|
file_put_contents('tmp/cache/'.$key, json_encode($value));
|
||||||
|
break;
|
||||||
|
case 'php':
|
||||||
|
self::$cache[$key] = $value;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
self::getCache()->set($key, $value, $expires);
|
if ($config['debug'])
|
||||||
|
$debug['cached'][] = $key . ' (set)';
|
||||||
if ($config['debug']) {
|
|
||||||
$debug['cached'][] = $config['cache']['prefix'] . $key . ' (set)';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
public static function delete($key) {
|
public static function delete($key) {
|
||||||
global $config, $debug;
|
global $config, $debug;
|
||||||
|
|
||||||
self::getCache()->delete($key);
|
$key = $config['cache']['prefix'] . $key;
|
||||||
|
|
||||||
if ($config['debug']) {
|
switch ($config['cache']['enabled']) {
|
||||||
$debug['cached'][] = $config['cache']['prefix'] . $key . ' (deleted)';
|
case 'memcached':
|
||||||
|
if (!self::$cache)
|
||||||
|
self::init();
|
||||||
|
self::$cache->delete($key);
|
||||||
|
break;
|
||||||
|
case 'redis':
|
||||||
|
if (!self::$cache)
|
||||||
|
self::init();
|
||||||
|
self::$cache->del($key);
|
||||||
|
break;
|
||||||
|
case 'apc':
|
||||||
|
apc_delete($key);
|
||||||
|
break;
|
||||||
|
case 'apcu':
|
||||||
|
apcu_delete($key);
|
||||||
|
break;
|
||||||
|
case 'xcache':
|
||||||
|
xcache_unset($key);
|
||||||
|
break;
|
||||||
|
case 'fs':
|
||||||
|
$key = str_replace('/', '::', $key);
|
||||||
|
$key = str_replace("\0", '', $key);
|
||||||
|
@unlink('tmp/cache/'.$key);
|
||||||
|
break;
|
||||||
|
case 'php':
|
||||||
|
unset(self::$cache[$key]);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($config['debug'])
|
||||||
|
$debug['cached'][] = $key . ' (deleted)';
|
||||||
}
|
}
|
||||||
public static function flush() {
|
public static function flush() {
|
||||||
self::getCache()->flush();
|
global $config;
|
||||||
|
|
||||||
|
switch ($config['cache']['enabled']) {
|
||||||
|
case 'memcached':
|
||||||
|
if (!self::$cache)
|
||||||
|
self::init();
|
||||||
|
return self::$cache->flush();
|
||||||
|
case 'apc':
|
||||||
|
return apc_clear_cache('user');
|
||||||
|
case 'apcu':
|
||||||
|
return apcu_clear_cache('user');
|
||||||
|
case 'php':
|
||||||
|
self::$cache = array();
|
||||||
|
break;
|
||||||
|
case 'fs':
|
||||||
|
$files = glob('tmp/cache/*');
|
||||||
|
foreach ($files as $file) {
|
||||||
|
unlink($file);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'redis':
|
||||||
|
if (!self::$cache)
|
||||||
|
self::init();
|
||||||
|
return self::$cache->flushDB();
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ class CzaksCaptcha {
|
|||||||
function mutate_sizes() {
|
function mutate_sizes() {
|
||||||
foreach ($this->content as &$v) {
|
foreach ($this->content as &$v) {
|
||||||
if (!isset ($v['font-size']))
|
if (!isset ($v['font-size']))
|
||||||
$v['font-size'] = rand(intval($this->height/3) - 4, intval($this->height/3) + 8);
|
$v['font-size'] = rand($this->height/3 - 4, $this->height/3 + 8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function mutate_positions() {
|
function mutate_positions() {
|
||||||
|
9
inc/captcha/dbschema.sql
Normal file
9
inc/captcha/dbschema.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
SET NAMES utf8;
|
||||||
|
|
||||||
|
CREATE TABLE `captchas` (
|
||||||
|
`cookie` VARCHAR(50),
|
||||||
|
`extra` VARCHAR(200),
|
||||||
|
`text` VARCHAR(255),
|
||||||
|
`created_at` INT(11),
|
||||||
|
PRIMARY KEY (cookie, extra)
|
||||||
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
|
@ -1,5 +1,7 @@
|
|||||||
I integrated this from: https://github.com/ctrlcctrlv/infinity/commit/62a6dac022cb338f7b719d0c35a64ab3efc64658
|
I integrated this from: https://github.com/ctrlcctrlv/infinity/commit/62a6dac022cb338f7b719d0c35a64ab3efc64658
|
||||||
|
|
||||||
|
<strike>First import the captcha/dbschema.sql in your database</strike> it is no longer required.
|
||||||
|
|
||||||
In inc/captcha/config.php change the database_name database_user database_password to your own settings.
|
In inc/captcha/config.php change the database_name database_user database_password to your own settings.
|
||||||
|
|
||||||
Add js/captcha.js in your secrets.php or config.php
|
Add js/captcha.js in your secrets.php or config.php
|
||||||
|
825
inc/config.php
825
inc/config.php
File diff suppressed because it is too large
Load Diff
@ -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();
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
@ -85,24 +85,24 @@ function sb_api($b) { global $config, $build_pages;
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sb_ukko() {
|
function sb_ukko() {
|
||||||
Vichan\Functions\Theme\rebuild_theme("ukko", "post-thread");
|
rebuildTheme("ukko", "post-thread");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sb_catalog($b) {
|
function sb_catalog($b) {
|
||||||
if (!openBoard($b)) return false;
|
if (!openBoard($b)) return false;
|
||||||
|
|
||||||
Vichan\Functions\Theme\rebuild_theme("catalog", "post-thread", $b);
|
rebuildTheme("catalog", "post-thread", $b);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sb_recent() {
|
function sb_recent() {
|
||||||
Vichan\Functions\Theme\rebuild_theme("recent", "post-thread");
|
rebuildTheme("recent", "post-thread");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sb_sitemap() {
|
function sb_sitemap() {
|
||||||
Vichan\Functions\Theme\rebuild_theme("sitemap", "all");
|
rebuildTheme("sitemap", "all");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ function createBoardlist($mod=false) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function error($message, $priority = true, $debug_stuff = []) {
|
function error($message, $priority = true, $debug_stuff = false) {
|
||||||
global $board, $mod, $config, $db_error;
|
global $board, $mod, $config, $db_error;
|
||||||
|
|
||||||
if ($config['syslog'] && $priority !== false) {
|
if ($config['syslog'] && $priority !== false) {
|
||||||
@ -348,25 +348,8 @@ class Post {
|
|||||||
$this->{$key} = $value;
|
$this->{$key} = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($this->files) && $this->files) {
|
if (isset($this->files) && $this->files)
|
||||||
$this->files = is_string($this->files) ? json_decode($this->files) : $this->files;
|
$this->files = @json_decode($this->files);
|
||||||
// Compatibility for posts before individual file hashing
|
|
||||||
foreach ($this->files as $i => &$file) {
|
|
||||||
if (empty($file)) {
|
|
||||||
unset($this->files[$i]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (is_array($file)) {
|
|
||||||
if (!isset($file['hash'])) {
|
|
||||||
$file['hash'] = $this->filehash;
|
|
||||||
}
|
|
||||||
} else if (is_object($file)) {
|
|
||||||
if (!isset($file->hash)) {
|
|
||||||
$file->hash = $this->filehash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->subject = utf8tohtml($this->subject);
|
$this->subject = utf8tohtml($this->subject);
|
||||||
$this->name = utf8tohtml($this->name);
|
$this->name = utf8tohtml($this->name);
|
||||||
@ -401,18 +384,7 @@ class Post {
|
|||||||
public function build($index=false) {
|
public function build($index=false) {
|
||||||
global $board, $config;
|
global $board, $config;
|
||||||
|
|
||||||
$options = [
|
return Element($config['file_post_reply'], array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index, 'mod' => $this->mod));
|
||||||
'config' => $config,
|
|
||||||
'board' => $board,
|
|
||||||
'post' => &$this,
|
|
||||||
'index' => $index,
|
|
||||||
'mod' => $this->mod
|
|
||||||
];
|
|
||||||
if ($this->mod) {
|
|
||||||
$options['pm'] = create_pm_header();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Element($config['file_post_reply'], $options);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -427,7 +399,7 @@ class Thread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isset($this->files))
|
if (isset($this->files))
|
||||||
$this->files = is_string($this->files) ? json_decode($this->files) : $this->files;
|
$this->files = @json_decode($this->files);
|
||||||
|
|
||||||
$this->subject = utf8tohtml($this->subject);
|
$this->subject = utf8tohtml($this->subject);
|
||||||
$this->name = utf8tohtml($this->name);
|
$this->name = utf8tohtml($this->name);
|
||||||
@ -476,22 +448,10 @@ class Thread {
|
|||||||
|
|
||||||
event('show-thread', $this);
|
event('show-thread', $this);
|
||||||
|
|
||||||
$options = [
|
|
||||||
'config' => $config,
|
|
||||||
'board' => $board,
|
|
||||||
'post' => &$this,
|
|
||||||
'index' => $index,
|
|
||||||
'hasnoko50' => $hasnoko50,
|
|
||||||
'isnoko50' => $isnoko50,
|
|
||||||
'mod' => $this->mod
|
|
||||||
];
|
|
||||||
if ($this->mod) {
|
|
||||||
$options['pm'] = create_pm_header();
|
|
||||||
}
|
|
||||||
|
|
||||||
$file = ($index && $config['file_board']) ? $config['file_post_thread_fileboard'] : $config['file_post_thread'];
|
$file = ($index && $config['file_board']) ? $config['file_post_thread_fileboard'] : $config['file_post_thread'];
|
||||||
$built = Element($file, $options);
|
$built = Element($file, array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index, 'hasnoko50' => $hasnoko50, 'isnoko50' => $isnoko50, 'mod' => $this->mod));
|
||||||
|
|
||||||
return $built;
|
return $built;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -11,27 +11,24 @@ class Filter {
|
|||||||
private $condition;
|
private $condition;
|
||||||
private $post;
|
private $post;
|
||||||
|
|
||||||
|
|
||||||
public function __construct(array $arr) {
|
public function __construct(array $arr) {
|
||||||
foreach ($arr as $key => $value) {
|
foreach ($arr as $key => $value)
|
||||||
$this->$key = $value;
|
$this->$key = $value;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public function match($condition, $match) {
|
public function match($condition, $match) {
|
||||||
$condition = strtolower($condition);
|
$condition = strtolower($condition);
|
||||||
|
|
||||||
$post = &$this->post;
|
$post = &$this->post;
|
||||||
|
|
||||||
switch($condition) {
|
switch($condition) {
|
||||||
case 'custom':
|
case 'custom':
|
||||||
if (!is_callable($match)) {
|
if (!is_callable($match))
|
||||||
error('Custom condition for filter is not callable!');
|
error('Custom condition for filter is not callable!');
|
||||||
}
|
|
||||||
return $match($post);
|
return $match($post);
|
||||||
case 'flood-match':
|
case 'flood-match':
|
||||||
if (!is_array($match)) {
|
if (!is_array($match))
|
||||||
error('Filter condition "flood-match" must be an array.');
|
error('Filter condition "flood-match" must be an array.');
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out "flood" table entries which do not match this filter.
|
// Filter out "flood" table entries which do not match this filter.
|
||||||
|
|
||||||
@ -41,32 +38,26 @@ class Filter {
|
|||||||
foreach ($match as $flood_match_arg) {
|
foreach ($match as $flood_match_arg) {
|
||||||
switch ($flood_match_arg) {
|
switch ($flood_match_arg) {
|
||||||
case 'ip':
|
case 'ip':
|
||||||
if ($flood_post['ip'] != $_SERVER['REMOTE_ADDR']) {
|
if ($flood_post['ip'] != $_SERVER['REMOTE_ADDR'])
|
||||||
continue 3;
|
continue 3;
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'body':
|
case 'body':
|
||||||
if ($flood_post['posthash'] != make_comment_hex($post['body_nomarkup'])) {
|
if ($flood_post['posthash'] != make_comment_hex($post['body_nomarkup']))
|
||||||
continue 3;
|
continue 3;
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'file':
|
case 'file':
|
||||||
if (!isset($post['filehash'])) {
|
if (!isset($post['filehash']))
|
||||||
return false;
|
return false;
|
||||||
}
|
if ($flood_post['filehash'] != $post['filehash'])
|
||||||
if ($flood_post['filehash'] != $post['filehash']) {
|
|
||||||
continue 3;
|
continue 3;
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'board':
|
case 'board':
|
||||||
if ($flood_post['board'] != $post['board']) {
|
if ($flood_post['board'] != $post['board'])
|
||||||
continue 3;
|
continue 3;
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'isreply':
|
case 'isreply':
|
||||||
if ($flood_post['isreply'] == $post['op']) {
|
if ($flood_post['isreply'] == $post['op'])
|
||||||
continue 3;
|
continue 3;
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
error('Invalid filter flood condition: ' . $flood_match_arg);
|
error('Invalid filter flood condition: ' . $flood_match_arg);
|
||||||
@ -76,6 +67,7 @@ class Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->flood_check = $flood_check_matched;
|
$this->flood_check = $flood_check_matched;
|
||||||
|
|
||||||
return !empty($this->flood_check);
|
return !empty($this->flood_check);
|
||||||
case 'flood-time':
|
case 'flood-time':
|
||||||
foreach ($this->flood_check as $flood_post) {
|
foreach ($this->flood_check as $flood_post) {
|
||||||
@ -105,9 +97,8 @@ class Filter {
|
|||||||
case 'filehash':
|
case 'filehash':
|
||||||
return $match === $post['filehash'];
|
return $match === $post['filehash'];
|
||||||
case 'filename':
|
case 'filename':
|
||||||
if (!$post['files']) {
|
if (!$post['files'])
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($post['files'] as $file) {
|
foreach ($post['files'] as $file) {
|
||||||
if (preg_match($match, $file['filename'])) {
|
if (preg_match($match, $file['filename'])) {
|
||||||
@ -116,9 +107,8 @@ class Filter {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
case 'extension':
|
case 'extension':
|
||||||
if (!$post['files']) {
|
if (!$post['files'])
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($post['files'] as $file) {
|
foreach ($post['files'] as $file) {
|
||||||
if (preg_match($match, $file['extension'])) {
|
if (preg_match($match, $file['extension'])) {
|
||||||
@ -136,14 +126,6 @@ class Filter {
|
|||||||
return $post['board'] == $match;
|
return $post['board'] == $match;
|
||||||
case 'password':
|
case 'password':
|
||||||
return $post['password'] == $match;
|
return $post['password'] == $match;
|
||||||
case 'unshorten':
|
|
||||||
$extracted_urls = get_urls($post['body_nomarkup']);
|
|
||||||
foreach ($extracted_urls as $url) {
|
|
||||||
if (preg_match($match, trace_url($url))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
default:
|
default:
|
||||||
error('Unknown filter condition: ' . $condition);
|
error('Unknown filter condition: ' . $condition);
|
||||||
}
|
}
|
||||||
@ -161,14 +143,12 @@ class Filter {
|
|||||||
$query->bindValue(':body', "Autoban message: ".$this->post['body']);
|
$query->bindValue(':body', "Autoban message: ".$this->post['body']);
|
||||||
$query->execute() or error(db_error($query));
|
$query->execute() or error(db_error($query));
|
||||||
}
|
}
|
||||||
if (isset($this->action)) {
|
if (isset ($this->action)) switch($this->action) {
|
||||||
switch($this->action) {
|
|
||||||
case 'reject':
|
case 'reject':
|
||||||
error(isset($this->message) ? $this->message : 'Posting throttled by filter.');
|
error(isset($this->message) ? $this->message : 'Posting throttled by filter.');
|
||||||
case 'ban':
|
case 'ban':
|
||||||
if (!isset($this->reason)) {
|
if (!isset($this->reason))
|
||||||
error('The ban action requires a reason.');
|
error('The ban action requires a reason.');
|
||||||
}
|
|
||||||
|
|
||||||
$this->expires = isset($this->expires) ? $this->expires : false;
|
$this->expires = isset($this->expires) ? $this->expires : false;
|
||||||
$this->reject = isset($this->reject) ? $this->reject : true;
|
$this->reject = isset($this->reject) ? $this->reject : true;
|
||||||
@ -177,9 +157,8 @@ class Filter {
|
|||||||
Bans::new_ban($_SERVER['REMOTE_ADDR'], $this->reason, $this->expires, $this->all_boards ? false : $board['uri'], -1);
|
Bans::new_ban($_SERVER['REMOTE_ADDR'], $this->reason, $this->expires, $this->all_boards ? false : $board['uri'], -1);
|
||||||
|
|
||||||
if ($this->reject) {
|
if ($this->reject) {
|
||||||
if (isset($this->message)) {
|
if (isset($this->message))
|
||||||
error($message);
|
error($message);
|
||||||
}
|
|
||||||
|
|
||||||
checkBan($board['uri']);
|
checkBan($board['uri']);
|
||||||
exit;
|
exit;
|
||||||
@ -190,7 +169,6 @@ class Filter {
|
|||||||
error('Unknown filter action: ' . $this->action);
|
error('Unknown filter action: ' . $this->action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public function check(array $post) {
|
public function check(array $post) {
|
||||||
$this->post = $post;
|
$this->post = $post;
|
||||||
@ -198,14 +176,11 @@ class Filter {
|
|||||||
if ($condition[0] == '!') {
|
if ($condition[0] == '!') {
|
||||||
$NOT = true;
|
$NOT = true;
|
||||||
$condition = substr($condition, 1);
|
$condition = substr($condition, 1);
|
||||||
} else {
|
} else $NOT = false;
|
||||||
$NOT = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->match($condition, $value) == $NOT) {
|
if ($this->match($condition, $value) == $NOT)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,16 +192,15 @@ function purge_flood_table() {
|
|||||||
// aware of flood filters in other board configurations. You can solve this problem by settings the
|
// aware of flood filters in other board configurations. You can solve this problem by settings the
|
||||||
// config variable $config['flood_cache'] (seconds).
|
// config variable $config['flood_cache'] (seconds).
|
||||||
|
|
||||||
if ($config['flood_cache'] != -1) {
|
if (isset($config['flood_cache'])) {
|
||||||
$max_time = &$config['flood_cache'];
|
$max_time = &$config['flood_cache'];
|
||||||
} else {
|
} else {
|
||||||
$max_time = 0;
|
$max_time = 0;
|
||||||
foreach ($config['filters'] as $filter) {
|
foreach ($config['filters'] as $filter) {
|
||||||
if (isset($filter['condition']['flood-time'])) {
|
if (isset($filter['condition']['flood-time']))
|
||||||
$max_time = max($max_time, $filter['condition']['flood-time']);
|
$max_time = max($max_time, $filter['condition']['flood-time']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$time = time() - $max_time;
|
$time = time() - $max_time;
|
||||||
|
|
||||||
@ -236,9 +210,8 @@ function purge_flood_table() {
|
|||||||
function do_filters(array $post) {
|
function do_filters(array $post) {
|
||||||
global $config;
|
global $config;
|
||||||
|
|
||||||
if (!isset($config['filters']) || empty($config['filters'])) {
|
if (!isset($config['filters']) || empty($config['filters']))
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($config['filters'] as $filter) {
|
foreach ($config['filters'] as $filter) {
|
||||||
if (isset($filter['condition']['flood-match'])) {
|
if (isset($filter['condition']['flood-match'])) {
|
||||||
@ -267,10 +240,10 @@ function do_filters(array $post) {
|
|||||||
foreach ($config['filters'] as $filter_array) {
|
foreach ($config['filters'] as $filter_array) {
|
||||||
$filter = new Filter($filter_array);
|
$filter = new Filter($filter_array);
|
||||||
$filter->flood_check = $flood_check;
|
$filter->flood_check = $flood_check;
|
||||||
if ($filter->check($post)) {
|
if ($filter->check($post))
|
||||||
$filter->action();
|
$filter->action();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
purge_flood_table();
|
purge_flood_table();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,9 @@ loadConfig();
|
|||||||
|
|
||||||
function init_locale($locale, $error='error') {
|
function init_locale($locale, $error='error') {
|
||||||
if (extension_loaded('gettext')) {
|
if (extension_loaded('gettext')) {
|
||||||
setlocale(LC_ALL, $locale);
|
if (setlocale(LC_ALL, $locale) === false) {
|
||||||
|
//$error('The specified locale (' . $locale . ') does not exist on your platform!');
|
||||||
|
}
|
||||||
bindtextdomain('tinyboard', './inc/locale');
|
bindtextdomain('tinyboard', './inc/locale');
|
||||||
bind_textdomain_codeset('tinyboard', 'UTF-8');
|
bind_textdomain_codeset('tinyboard', 'UTF-8');
|
||||||
textdomain('tinyboard');
|
textdomain('tinyboard');
|
||||||
@ -54,8 +56,7 @@ function loadConfig() {
|
|||||||
|
|
||||||
if (isset($config['cache_config']) &&
|
if (isset($config['cache_config']) &&
|
||||||
$config['cache_config'] &&
|
$config['cache_config'] &&
|
||||||
$config = Cache::get('config_' . $boardsuffix))
|
$config = Cache::get('config_' . $boardsuffix ) ) {
|
||||||
{
|
|
||||||
$events = Cache::get('events_' . $boardsuffix );
|
$events = Cache::get('events_' . $boardsuffix );
|
||||||
|
|
||||||
define_groups();
|
define_groups();
|
||||||
@ -68,7 +69,8 @@ function loadConfig() {
|
|||||||
$current_locale = $config['locale'];
|
$current_locale = $config['locale'];
|
||||||
init_locale($config['locale'], $error);
|
init_locale($config['locale'], $error);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$config = array();
|
$config = array();
|
||||||
|
|
||||||
reset_events();
|
reset_events();
|
||||||
@ -205,8 +207,6 @@ function loadConfig() {
|
|||||||
$config['image_bumplocked'] = $config['dir']['static'] . 'sage.gif';
|
$config['image_bumplocked'] = $config['dir']['static'] . 'sage.gif';
|
||||||
if (!isset($config['image_deleted']))
|
if (!isset($config['image_deleted']))
|
||||||
$config['image_deleted'] = $config['dir']['static'] . 'deleted.png';
|
$config['image_deleted'] = $config['dir']['static'] . 'deleted.png';
|
||||||
if (!isset($config['image_cyclical']))
|
|
||||||
$config['image_cyclical'] = $config['dir']['static'] . 'cycle.png';
|
|
||||||
|
|
||||||
if (isset($board)) {
|
if (isset($board)) {
|
||||||
if (!isset($config['uri_thumb']))
|
if (!isset($config['uri_thumb']))
|
||||||
@ -240,14 +240,13 @@ function loadConfig() {
|
|||||||
$__version = file_exists('.installed') ? trim(file_get_contents('.installed')) : false;
|
$__version = file_exists('.installed') ? trim(file_get_contents('.installed')) : false;
|
||||||
$config['version'] = $__version;
|
$config['version'] = $__version;
|
||||||
|
|
||||||
if ($config['allow_roll']) {
|
if ($config['allow_roll'])
|
||||||
event_handler('post', 'email_dice_roll');
|
event_handler('post', 'diceRoller');
|
||||||
}
|
|
||||||
|
|
||||||
if (in_array('webm', $config['allowed_ext_files']) || in_array('mp4', $config['allowed_ext_files'])) {
|
if (in_array('webm', $config['allowed_ext_files']) ||
|
||||||
|
in_array('mp4', $config['allowed_ext_files']))
|
||||||
event_handler('post', 'postHandler');
|
event_handler('post', 'postHandler');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Effectful config processing below:
|
// Effectful config processing below:
|
||||||
|
|
||||||
date_default_timezone_set($config['timezone']);
|
date_default_timezone_set($config['timezone']);
|
||||||
@ -279,7 +278,8 @@ function loadConfig() {
|
|||||||
if ($config['cache']['enabled'])
|
if ($config['cache']['enabled'])
|
||||||
require_once 'inc/cache.php';
|
require_once 'inc/cache.php';
|
||||||
|
|
||||||
if (in_array('webm', $config['allowed_ext_files']) || in_array('mp4', $config['allowed_ext_files']))
|
if (in_array('webm', $config['allowed_ext_files']) ||
|
||||||
|
in_array('mp4', $config['allowed_ext_files']))
|
||||||
require_once 'inc/lib/webm/posthandler.php';
|
require_once 'inc/lib/webm/posthandler.php';
|
||||||
|
|
||||||
event('load-config');
|
event('load-config');
|
||||||
@ -391,6 +391,114 @@ function define_groups() {
|
|||||||
ksort($config['mod']['groups']);
|
ksort($config['mod']['groups']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function create_antibot($board, $thread = null) {
|
||||||
|
require_once dirname(__FILE__) . '/anti-bot.php';
|
||||||
|
|
||||||
|
return _create_antibot($board, $thread);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebuildThemes($action, $boardname = false) {
|
||||||
|
global $config, $board, $current_locale;
|
||||||
|
|
||||||
|
// Save the global variables
|
||||||
|
$_config = $config;
|
||||||
|
$_board = $board;
|
||||||
|
|
||||||
|
// List themes
|
||||||
|
if ($themes = Cache::get("themes")) {
|
||||||
|
// OK, we already have themes loaded
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$query = query("SELECT `theme` FROM ``theme_settings`` WHERE `name` IS NULL AND `value` IS NULL") or error(db_error());
|
||||||
|
|
||||||
|
$themes = array();
|
||||||
|
|
||||||
|
while ($theme = $query->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
$themes[] = $theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache::set("themes", $themes);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($themes as $theme) {
|
||||||
|
// Restore them
|
||||||
|
$config = $_config;
|
||||||
|
$board = $_board;
|
||||||
|
|
||||||
|
// Reload the locale
|
||||||
|
if ($config['locale'] != $current_locale) {
|
||||||
|
$current_locale = $config['locale'];
|
||||||
|
init_locale($config['locale']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PHP_SAPI === 'cli') {
|
||||||
|
echo "Rebuilding theme ".$theme['theme']."... ";
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuildTheme($theme['theme'], $action, $boardname);
|
||||||
|
|
||||||
|
if (PHP_SAPI === 'cli') {
|
||||||
|
echo "done\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore them again
|
||||||
|
$config = $_config;
|
||||||
|
$board = $_board;
|
||||||
|
|
||||||
|
// Reload the locale
|
||||||
|
if ($config['locale'] != $current_locale) {
|
||||||
|
$current_locale = $config['locale'];
|
||||||
|
init_locale($config['locale']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function loadThemeConfig($_theme) {
|
||||||
|
global $config;
|
||||||
|
|
||||||
|
if (!file_exists($config['dir']['themes'] . '/' . $_theme . '/info.php'))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Load theme information into $theme
|
||||||
|
include $config['dir']['themes'] . '/' . $_theme . '/info.php';
|
||||||
|
|
||||||
|
return $theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebuildTheme($theme, $action, $board = false) {
|
||||||
|
global $config, $_theme;
|
||||||
|
$_theme = $theme;
|
||||||
|
|
||||||
|
$theme = loadThemeConfig($_theme);
|
||||||
|
|
||||||
|
if (file_exists($config['dir']['themes'] . '/' . $_theme . '/theme.php')) {
|
||||||
|
require_once $config['dir']['themes'] . '/' . $_theme . '/theme.php';
|
||||||
|
|
||||||
|
$theme['build_function']($action, themeSettings($_theme), $board);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function themeSettings($theme) {
|
||||||
|
if ($settings = Cache::get("theme_settings_".$theme)) {
|
||||||
|
return $settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = prepare("SELECT `name`, `value` FROM ``theme_settings`` WHERE `theme` = :theme AND `name` IS NOT NULL");
|
||||||
|
$query->bindValue(':theme', $theme);
|
||||||
|
$query->execute() or error(db_error($query));
|
||||||
|
|
||||||
|
$settings = array();
|
||||||
|
while ($s = $query->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
$settings[$s['name']] = $s['value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache::set("theme_settings_".$theme, $settings);
|
||||||
|
|
||||||
|
return $settings;
|
||||||
|
}
|
||||||
|
|
||||||
function sprintf3($str, $vars, $delim = '%') {
|
function sprintf3($str, $vars, $delim = '%') {
|
||||||
$replaces = array();
|
$replaces = array();
|
||||||
foreach ($vars as $k => $v) {
|
foreach ($vars as $k => $v) {
|
||||||
@ -407,11 +515,12 @@ function mb_substr_replace($string, $replacement, $start, $length) {
|
|||||||
function setupBoard($array) {
|
function setupBoard($array) {
|
||||||
global $board, $config;
|
global $board, $config;
|
||||||
|
|
||||||
$board = [
|
$board = array(
|
||||||
'uri' => $array['uri'],
|
'uri' => $array['uri'],
|
||||||
'title' => $array['title'],
|
'title' => $array['title'],
|
||||||
'subtitle' => $array['subtitle'],
|
'subtitle' => $array['subtitle'],
|
||||||
];
|
#'indexed' => $array['indexed'],
|
||||||
|
);
|
||||||
|
|
||||||
// older versions
|
// older versions
|
||||||
$board['name'] = &$board['title'];
|
$board['name'] = &$board['title'];
|
||||||
@ -525,8 +634,14 @@ function file_write($path, $data, $simple = false, $skip_purge = false) {
|
|||||||
global $config, $debug;
|
global $config, $debug;
|
||||||
|
|
||||||
if (preg_match('/^remote:\/\/(.+)\:(.+)$/', $path, $m)) {
|
if (preg_match('/^remote:\/\/(.+)\:(.+)$/', $path, $m)) {
|
||||||
if (isset($config['remote'])) {
|
if (isset($config['remote'][$m[1]])) {
|
||||||
error('Remote server support has been removed');
|
require_once 'inc/remote.php';
|
||||||
|
|
||||||
|
$remote = new Remote($config['remote'][$m[1]]);
|
||||||
|
$remote->write($data, $m[2]);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
error('Invalid remote server: ' . $m[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -607,19 +722,13 @@ function file_unlink($path) {
|
|||||||
$debug['unlink'][] = $path;
|
$debug['unlink'][] = $path;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file_exists($path)) {
|
|
||||||
$ret = @unlink($path);
|
$ret = @unlink($path);
|
||||||
} else {
|
|
||||||
$ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($config['gzip_static']) {
|
if ($config['gzip_static']) {
|
||||||
$gzpath = "$path.gz";
|
$gzpath = "$path.gz";
|
||||||
|
|
||||||
if (file_exists($gzpath)) {
|
|
||||||
@unlink($gzpath);
|
@unlink($gzpath);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($config['purge']) && $path[0] != '/' && isset($_SERVER['HTTP_HOST'])) {
|
if (isset($config['purge']) && $path[0] != '/' && isset($_SERVER['HTTP_HOST'])) {
|
||||||
// Purge cache
|
// Purge cache
|
||||||
@ -692,6 +801,42 @@ function listBoards($just_uri = false) {
|
|||||||
return $boards;
|
return $boards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function until($timestamp) {
|
||||||
|
$difference = $timestamp - time();
|
||||||
|
switch(TRUE){
|
||||||
|
case ($difference < 60):
|
||||||
|
return $difference . ' ' . ngettext('second', 'seconds', $difference);
|
||||||
|
case ($difference < 3600): //60*60 = 3600
|
||||||
|
return ($num = round($difference/(60))) . ' ' . ngettext('minute', 'minutes', $num);
|
||||||
|
case ($difference < 86400): //60*60*24 = 86400
|
||||||
|
return ($num = round($difference/(3600))) . ' ' . ngettext('hour', 'hours', $num);
|
||||||
|
case ($difference < 604800): //60*60*24*7 = 604800
|
||||||
|
return ($num = round($difference/(86400))) . ' ' . ngettext('day', 'days', $num);
|
||||||
|
case ($difference < 31536000): //60*60*24*365 = 31536000
|
||||||
|
return ($num = round($difference/(604800))) . ' ' . ngettext('week', 'weeks', $num);
|
||||||
|
default:
|
||||||
|
return ($num = round($difference/(31536000))) . ' ' . ngettext('year', 'years', $num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ago($timestamp) {
|
||||||
|
$difference = time() - $timestamp;
|
||||||
|
switch(TRUE){
|
||||||
|
case ($difference < 60) :
|
||||||
|
return $difference . ' ' . ngettext('second', 'seconds', $difference);
|
||||||
|
case ($difference < 3600): //60*60 = 3600
|
||||||
|
return ($num = round($difference/(60))) . ' ' . ngettext('minute', 'minutes', $num);
|
||||||
|
case ($difference < 86400): //60*60*24 = 86400
|
||||||
|
return ($num = round($difference/(3600))) . ' ' . ngettext('hour', 'hours', $num);
|
||||||
|
case ($difference < 604800): //60*60*24*7 = 604800
|
||||||
|
return ($num = round($difference/(86400))) . ' ' . ngettext('day', 'days', $num);
|
||||||
|
case ($difference < 31536000): //60*60*24*365 = 31536000
|
||||||
|
return ($num = round($difference/(604800))) . ' ' . ngettext('week', 'weeks', $num);
|
||||||
|
default:
|
||||||
|
return ($num = round($difference/(31536000))) . ' ' . ngettext('year', 'years', $num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function displayBan($ban) {
|
function displayBan($ban) {
|
||||||
global $config, $board;
|
global $config, $board;
|
||||||
|
|
||||||
@ -768,13 +913,11 @@ function checkBan($board = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($ips as $ip) {
|
foreach ($ips as $ip) {
|
||||||
$bans = Bans::find($ip, $board, $config['show_modname'], null, $config['auto_maintenance']);
|
$bans = Bans::find($ip, $board, $config['show_modname']);
|
||||||
|
|
||||||
foreach ($bans as &$ban) {
|
foreach ($bans as &$ban) {
|
||||||
if ($ban['expires'] && $ban['expires'] < time()) {
|
if ($ban['expires'] && $ban['expires'] < time()) {
|
||||||
if ($config['auto_maintenance']) {
|
|
||||||
Bans::delete($ban['id']);
|
Bans::delete($ban['id']);
|
||||||
}
|
|
||||||
if ($config['require_ban_view'] && !$ban['seen']) {
|
if ($config['require_ban_view'] && !$ban['seen']) {
|
||||||
if (!isset($_POST['json_response'])) {
|
if (!isset($_POST['json_response'])) {
|
||||||
displayBan($ban);
|
displayBan($ban);
|
||||||
@ -794,21 +937,18 @@ function checkBan($board = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($config['auto_maintenance']) {
|
|
||||||
// I'm not sure where else to put this. It doesn't really matter where; it just needs to be called every
|
// I'm not sure where else to put this. It doesn't really matter where; it just needs to be called every
|
||||||
// now and then to keep the ban list tidy.
|
// now and then to keep the ban list tidy.
|
||||||
if ($config['cache']['enabled']) {
|
if ($config['cache']['enabled'] && $last_time_purged = cache::get('purged_bans_last')) {
|
||||||
$last_time_purged = cache::get('purged_bans_last');
|
if (time() - $last_time_purged < $config['purge_bans'] )
|
||||||
if ($last_time_purged !== false && time() - $last_time_purged > $config['purge_bans']) {
|
return;
|
||||||
Bans::purge($config['require_ban_view'], $config['purge_bans']);
|
}
|
||||||
|
|
||||||
|
Bans::purge();
|
||||||
|
|
||||||
|
if ($config['cache']['enabled'])
|
||||||
cache::set('purged_bans_last', time());
|
cache::set('purged_bans_last', time());
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Purge every time.
|
|
||||||
Bans::purge($config['require_ban_view'], $config['purge_bans']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function threadLocked($id) {
|
function threadLocked($id) {
|
||||||
global $board;
|
global $board;
|
||||||
@ -1295,14 +1435,7 @@ function index($page, $mod=false, $brief = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($config['file_board']) {
|
if ($config['file_board']) {
|
||||||
$options = [
|
$body = Element($config['file_fileboard'], array('body' => $body, 'mod' => $mod));
|
||||||
'body' => $body,
|
|
||||||
'mod' => $mod
|
|
||||||
];
|
|
||||||
if ($mod) {
|
|
||||||
$options['pm'] = create_pm_header();
|
|
||||||
}
|
|
||||||
$body = Element($config['file_fileboard'], $options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
@ -1485,7 +1618,7 @@ function checkMute() {
|
|||||||
|
|
||||||
if ($config['cache']['enabled']) {
|
if ($config['cache']['enabled']) {
|
||||||
// Cached mute?
|
// Cached mute?
|
||||||
if (($mute = cache::get("mute_{$_SERVER['REMOTE_ADDR']}")) && ($mutetime = cache::get("mutetime_{$_SERVER['REMOTE_ADDR']}"))) {
|
if (($mute = cache::get("mute_${_SERVER['REMOTE_ADDR']}")) && ($mutetime = cache::get("mutetime_${_SERVER['REMOTE_ADDR']}"))) {
|
||||||
error(sprintf($config['error']['youaremuted'], $mute['time'] + $mutetime - time()));
|
error(sprintf($config['error']['youaremuted'], $mute['time'] + $mutetime - time()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1504,8 +1637,8 @@ function checkMute() {
|
|||||||
|
|
||||||
if ($mute['time'] + $mutetime > time()) {
|
if ($mute['time'] + $mutetime > time()) {
|
||||||
if ($config['cache']['enabled']) {
|
if ($config['cache']['enabled']) {
|
||||||
cache::set("mute_{$_SERVER['REMOTE_ADDR']}", $mute, $mute['time'] + $mutetime - time());
|
cache::set("mute_${_SERVER['REMOTE_ADDR']}", $mute, $mute['time'] + $mutetime - time());
|
||||||
cache::set("mutetime_{$_SERVER['REMOTE_ADDR']}", $mutetime, $mute['time'] + $mutetime - time());
|
cache::set("mutetime_${_SERVER['REMOTE_ADDR']}", $mutetime, $mute['time'] + $mutetime - time());
|
||||||
}
|
}
|
||||||
// Not expired yet
|
// Not expired yet
|
||||||
error(sprintf($config['error']['youaremuted'], $mute['time'] + $mutetime - time()));
|
error(sprintf($config['error']['youaremuted'], $mute['time'] + $mutetime - time()));
|
||||||
@ -1516,10 +1649,34 @@ function checkMute() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function purge_old_antispam() {
|
function _create_antibot($board, $thread) {
|
||||||
$query = prepare('DELETE FROM ``antispam`` WHERE `expires` < UNIX_TIMESTAMP()');
|
global $config, $purged_old_antispam;
|
||||||
$query->execute() or error(db_error());
|
|
||||||
return $query->rowCount();
|
$antibot = new AntiBot(array($board, $thread));
|
||||||
|
|
||||||
|
if (!isset($purged_old_antispam)) {
|
||||||
|
$purged_old_antispam = true;
|
||||||
|
query('DELETE FROM ``antispam`` WHERE `expires` < UNIX_TIMESTAMP()') or error(db_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($thread)
|
||||||
|
$query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` = :thread AND `expires` IS NULL');
|
||||||
|
else
|
||||||
|
$query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` IS NULL AND `expires` IS NULL');
|
||||||
|
|
||||||
|
$query->bindValue(':board', $board);
|
||||||
|
if ($thread)
|
||||||
|
$query->bindValue(':thread', $thread);
|
||||||
|
$query->bindValue(':expires', $config['spam']['hidden_inputs_expire']);
|
||||||
|
$query->execute() or error(db_error($query));
|
||||||
|
|
||||||
|
$query = prepare('INSERT INTO ``antispam`` VALUES (:board, :thread, :hash, UNIX_TIMESTAMP(), NULL, 0)');
|
||||||
|
$query->bindValue(':board', $board);
|
||||||
|
$query->bindValue(':thread', $thread);
|
||||||
|
$query->bindValue(':hash', $antibot->hash());
|
||||||
|
$query->execute() or error(db_error($query));
|
||||||
|
|
||||||
|
return $antibot;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkSpam(array $extra_salt = array()) {
|
function checkSpam(array $extra_salt = array()) {
|
||||||
@ -1584,18 +1741,15 @@ function incrementSpamHash($hash) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildIndex($global_api = "yes") {
|
function buildIndex($global_api = "yes") {
|
||||||
global $board, $config, $build_pages, $mod;
|
global $board, $config, $build_pages;
|
||||||
|
|
||||||
$catalog_api_action = generation_strategy('sb_api', array($board['uri']));
|
$catalog_api_action = generation_strategy('sb_api', array($board['uri']));
|
||||||
|
|
||||||
$pages = null;
|
$pages = null;
|
||||||
|
$antibot = null;
|
||||||
|
|
||||||
if ($config['api']['enabled']) {
|
if ($config['api']['enabled']) {
|
||||||
$api = new Api(
|
$api = new Api();
|
||||||
$config['show_filename'],
|
|
||||||
$config['hide_email'],
|
|
||||||
$config['country_flags']
|
|
||||||
);
|
|
||||||
$catalog = array();
|
$catalog = array();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1614,21 +1768,6 @@ function buildIndex($global_api = "yes") {
|
|||||||
if (!$content)
|
if (!$content)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Tries to avoid rebuilding if the body is the same as the one in cache.
|
|
||||||
if ($config['cache']['enabled']) {
|
|
||||||
$contentHash = md5(json_encode($content['body']));
|
|
||||||
$contentHashKey = '_index_hashed_'. $board['uri'] . '_' . $page;
|
|
||||||
$cachedHash = cache::get($contentHashKey);
|
|
||||||
if ($cachedHash == $contentHash){
|
|
||||||
if ($config['api']['enabled']) {
|
|
||||||
// this is needed for the thread.json and catalog.json rebuilding below, which includes all pages.
|
|
||||||
$catalog[$page-1] = $content['threads'];
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
cache::set($contentHashKey, $contentHash, 3600);
|
|
||||||
}
|
|
||||||
|
|
||||||
// json api
|
// json api
|
||||||
if ($config['api']['enabled']) {
|
if ($config['api']['enabled']) {
|
||||||
$threads = $content['threads'];
|
$threads = $content['threads'];
|
||||||
@ -1640,15 +1779,21 @@ function buildIndex($global_api = "yes") {
|
|||||||
if ($wont_build_this_page) continue;
|
if ($wont_build_this_page) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($config['try_smarter']) {
|
||||||
|
$antibot = create_antibot($board['uri'], 0 - $page);
|
||||||
|
$content['current_page'] = $page;
|
||||||
|
}
|
||||||
|
elseif (!$antibot) {
|
||||||
|
$antibot = create_antibot($board['uri']);
|
||||||
|
}
|
||||||
|
$antibot->reset();
|
||||||
if (!$pages) {
|
if (!$pages) {
|
||||||
$pages = getPages();
|
$pages = getPages();
|
||||||
}
|
}
|
||||||
$content['pages'] = $pages;
|
$content['pages'] = $pages;
|
||||||
$content['pages'][$page-1]['selected'] = true;
|
$content['pages'][$page-1]['selected'] = true;
|
||||||
$content['btn'] = getPageButtons($content['pages']);
|
$content['btn'] = getPageButtons($content['pages']);
|
||||||
if ($mod) {
|
$content['antibot'] = $antibot;
|
||||||
$content['pm'] = create_pm_header();
|
|
||||||
}
|
|
||||||
|
|
||||||
file_write($filename, Element($config['file_board_index'], $content));
|
file_write($filename, Element($config['file_board_index'], $content));
|
||||||
}
|
}
|
||||||
@ -1873,7 +2018,7 @@ function extract_modifiers($body) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function remove_modifiers($body) {
|
function remove_modifiers($body) {
|
||||||
return $body ? preg_replace('@<tinyboard ([\w\s]+)>(.+?)</tinyboard>@usm', '', $body) : null;
|
return preg_replace('@<tinyboard ([\w\s]+)>(.+?)</tinyboard>@usm', '', $body);
|
||||||
}
|
}
|
||||||
|
|
||||||
function markup(&$body, $track_cites = false, $op = false) {
|
function markup(&$body, $track_cites = false, $op = false) {
|
||||||
@ -1941,7 +2086,7 @@ function markup(&$body, $track_cites = false, $op = false) {
|
|||||||
$tracked_cites = array();
|
$tracked_cites = array();
|
||||||
|
|
||||||
// Cites
|
// Cites
|
||||||
if (isset($board) && preg_match_all('/(^|[\s(])>>(\d+?)((?=[\s,.)?!])|$)/m', $body, $cites, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
|
if (isset($board) && preg_match_all('/(^|\s)>>(\d+?)((?=[\s,.)?!])|$)/m', $body, $cites, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
|
||||||
if (count($cites[0]) > $config['max_cites']) {
|
if (count($cites[0]) > $config['max_cites']) {
|
||||||
error($config['error']['toomanycites']);
|
error($config['error']['toomanycites']);
|
||||||
}
|
}
|
||||||
@ -1988,7 +2133,7 @@ function markup(&$body, $track_cites = false, $op = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cross-board linking
|
// Cross-board linking
|
||||||
if (preg_match_all('/(^|[\s(])>>>\/(' . $config['board_regex'] . 'f?)\/(\d+)?((?=[\s,.)?!])|$)/um', $body, $cites, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
|
if (preg_match_all('/(^|\s)>>>\/(' . $config['board_regex'] . 'f?)\/(\d+)?((?=[\s,.)?!])|$)/um', $body, $cites, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
|
||||||
if (count($cites[0]) > $config['max_cites']) {
|
if (count($cites[0]) > $config['max_cites']) {
|
||||||
error($config['error']['toomanycross']);
|
error($config['error']['toomanycross']);
|
||||||
}
|
}
|
||||||
@ -2142,7 +2287,6 @@ function escape_markup_modifiers($string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function defined_flags_accumulate($desired_flags) {
|
function defined_flags_accumulate($desired_flags) {
|
||||||
global $config;
|
|
||||||
$output_flags = 0x0;
|
$output_flags = 0x0;
|
||||||
foreach ($desired_flags as $flagname) {
|
foreach ($desired_flags as $flagname) {
|
||||||
if (defined($flagname)) {
|
if (defined($flagname)) {
|
||||||
@ -2159,8 +2303,8 @@ function defined_flags_accumulate($desired_flags) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function utf8tohtml($utf8) {
|
function utf8tohtml($utf8) {
|
||||||
$flags = defined_flags_accumulate(['ENT_NOQUOTES', 'ENT_SUBSTITUTE', 'ENT_DISALLOWED']);
|
$flags = defined_flags_accumulate(['ENT_QUOTES', 'ENT_SUBSTITUTE', 'ENT_DISALLOWED']);
|
||||||
return $utf8 ? htmlspecialchars($utf8, $flags, 'UTF-8') : '';
|
return htmlspecialchars($utf8, $flags, 'UTF-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
function ordutf8($string, &$offset) {
|
function ordutf8($string, &$offset) {
|
||||||
@ -2186,11 +2330,19 @@ function ordutf8($string, &$offset) {
|
|||||||
return $code;
|
return $code;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit Non_Spacing_Mark and Enclosing_Mark characters
|
|
||||||
function strip_combining_chars($str) {
|
function strip_combining_chars($str) {
|
||||||
global $config;
|
$chars = preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY);
|
||||||
$limit = strval($config['max_combining_chars']+1);
|
$str = '';
|
||||||
return preg_replace('/(\p{Me}|\p{Mn}){'.$limit.',}/u','', $str);
|
foreach ($chars as $char) {
|
||||||
|
$o = 0;
|
||||||
|
$ord = ordutf8($char, $o);
|
||||||
|
|
||||||
|
if ( ($ord >= 768 && $ord <= 879) || ($ord >= 1536 && $ord <= 1791) || ($ord >= 3655 && $ord <= 3659) || ($ord >= 7616 && $ord <= 7679) || ($ord >= 8400 && $ord <= 8447) || ($ord >= 65056 && $ord <= 65071))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$str .= $char;
|
||||||
|
}
|
||||||
|
return $str;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildThread($id, $return = false, $mod = false) {
|
function buildThread($id, $return = false, $mod = false) {
|
||||||
@ -2229,8 +2381,9 @@ function buildThread($id, $return = false, $mod = false) {
|
|||||||
error($config['error']['nonexistant']);
|
error($config['error']['nonexistant']);
|
||||||
|
|
||||||
$hasnoko50 = $thread->postCount() >= $config['noko50_min'];
|
$hasnoko50 = $thread->postCount() >= $config['noko50_min'];
|
||||||
|
$antibot = $mod || $return ? false : create_antibot($board['uri'], $id);
|
||||||
|
|
||||||
$options = [
|
$body = Element($config['file_thread'], array(
|
||||||
'board' => $board,
|
'board' => $board,
|
||||||
'thread' => $thread,
|
'thread' => $thread,
|
||||||
'body' => $thread->build(),
|
'body' => $thread->build(),
|
||||||
@ -2239,22 +2392,14 @@ function buildThread($id, $return = false, $mod = false) {
|
|||||||
'mod' => $mod,
|
'mod' => $mod,
|
||||||
'hasnoko50' => $hasnoko50,
|
'hasnoko50' => $hasnoko50,
|
||||||
'isnoko50' => false,
|
'isnoko50' => false,
|
||||||
|
'antibot' => $antibot,
|
||||||
'boardlist' => createBoardlist($mod),
|
'boardlist' => createBoardlist($mod),
|
||||||
'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index'])
|
'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index'])
|
||||||
];
|
));
|
||||||
if ($mod) {
|
|
||||||
$options['pm'] = create_pm_header();
|
|
||||||
}
|
|
||||||
|
|
||||||
$body = Element($config['file_thread'], $options);
|
|
||||||
|
|
||||||
// json api
|
// json api
|
||||||
if ($config['api']['enabled'] && !$mod) {
|
if ($config['api']['enabled'] && !$mod) {
|
||||||
$api = new Api(
|
$api = new Api();
|
||||||
$config['show_filename'],
|
|
||||||
$config['hide_email'],
|
|
||||||
$config['country_flags']
|
|
||||||
);
|
|
||||||
$json = json_encode($api->translateThread($thread));
|
$json = json_encode($api->translateThread($thread));
|
||||||
$jsonFilename = $board['dir'] . $config['dir']['res'] . $id . '.json';
|
$jsonFilename = $board['dir'] . $config['dir']['res'] . $id . '.json';
|
||||||
file_write($jsonFilename, $json);
|
file_write($jsonFilename, $json);
|
||||||
@ -2275,17 +2420,20 @@ function buildThread($id, $return = false, $mod = false) {
|
|||||||
} elseif ($action == 'rebuild') {
|
} elseif ($action == 'rebuild') {
|
||||||
$noko50fn = $board['dir'] . $config['dir']['res'] . link_for($thread, true);
|
$noko50fn = $board['dir'] . $config['dir']['res'] . link_for($thread, true);
|
||||||
if ($hasnoko50 || file_exists($noko50fn)) {
|
if ($hasnoko50 || file_exists($noko50fn)) {
|
||||||
buildThread50($id, $return, $mod, $thread);
|
buildThread50($id, $return, $mod, $thread, $antibot);
|
||||||
}
|
}
|
||||||
|
|
||||||
file_write($board['dir'] . $config['dir']['res'] . link_for($thread), $body);
|
file_write($board['dir'] . $config['dir']['res'] . link_for($thread), $body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildThread50($id, $return = false, $mod = false, $thread = null) {
|
function buildThread50($id, $return = false, $mod = false, $thread = null, $antibot = false) {
|
||||||
global $board, $config;
|
global $board, $config, $build_pages;
|
||||||
$id = round($id);
|
$id = round($id);
|
||||||
|
|
||||||
|
if ($antibot)
|
||||||
|
$antibot->reset();
|
||||||
|
|
||||||
if (!$thread) {
|
if (!$thread) {
|
||||||
$query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id ORDER BY `thread`,`id` DESC LIMIT :limit", $board['uri']));
|
$query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id ORDER BY `thread`,`id` DESC LIMIT :limit", $board['uri']));
|
||||||
$query->bindValue(':id', $id, PDO::PARAM_INT);
|
$query->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
@ -2338,7 +2486,7 @@ function buildThread50($id, $return = false, $mod = false, $thread = null) {
|
|||||||
|
|
||||||
$hasnoko50 = $thread->postCount() >= $config['noko50_min'];
|
$hasnoko50 = $thread->postCount() >= $config['noko50_min'];
|
||||||
|
|
||||||
$options = [
|
$body = Element($config['file_thread'], array(
|
||||||
'board' => $board,
|
'board' => $board,
|
||||||
'thread' => $thread,
|
'thread' => $thread,
|
||||||
'body' => $thread->build(false, true),
|
'body' => $thread->build(false, true),
|
||||||
@ -2347,14 +2495,10 @@ function buildThread50($id, $return = false, $mod = false, $thread = null) {
|
|||||||
'mod' => $mod,
|
'mod' => $mod,
|
||||||
'hasnoko50' => $hasnoko50,
|
'hasnoko50' => $hasnoko50,
|
||||||
'isnoko50' => true,
|
'isnoko50' => true,
|
||||||
|
'antibot' => $mod ? false : ($antibot ? $antibot : create_antibot($board['uri'], $id)),
|
||||||
'boardlist' => createBoardlist($mod),
|
'boardlist' => createBoardlist($mod),
|
||||||
'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index'])
|
'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index'])
|
||||||
];
|
));
|
||||||
if ($mod) {
|
|
||||||
$options['pm'] = create_pm_header();
|
|
||||||
}
|
|
||||||
|
|
||||||
$body = Element($config['file_thread'], $options);
|
|
||||||
|
|
||||||
if ($return) {
|
if ($return) {
|
||||||
return $body;
|
return $body;
|
||||||
@ -2425,6 +2569,35 @@ function generate_tripcode($name) {
|
|||||||
return array($name, $trip);
|
return array($name, $trip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Highest common factor
|
||||||
|
function hcf($a, $b){
|
||||||
|
$gcd = 1;
|
||||||
|
if ($a>$b) {
|
||||||
|
$a = $a+$b;
|
||||||
|
$b = $a-$b;
|
||||||
|
$a = $a-$b;
|
||||||
|
}
|
||||||
|
if ($b==(round($b/$a))*$a)
|
||||||
|
$gcd=$a;
|
||||||
|
else {
|
||||||
|
for ($i=round($a/2);$i;$i--) {
|
||||||
|
if ($a == round($a/$i)*$i && $b == round($b/$i)*$i) {
|
||||||
|
$gcd = $i;
|
||||||
|
$i = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $gcd;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fraction($numerator, $denominator, $sep) {
|
||||||
|
$gcf = hcf($numerator, $denominator);
|
||||||
|
$numerator = $numerator / $gcf;
|
||||||
|
$denominator = $denominator / $gcf;
|
||||||
|
|
||||||
|
return "{$numerator}{$sep}{$denominator}";
|
||||||
|
}
|
||||||
|
|
||||||
function getPostByHash($hash) {
|
function getPostByHash($hash) {
|
||||||
global $board;
|
global $board;
|
||||||
$query = prepare(sprintf("SELECT `id`,`thread` FROM ``posts_%s`` WHERE `filehash` = :hash", $board['uri']));
|
$query = prepare(sprintf("SELECT `id`,`thread` FROM ``posts_%s`` WHERE `filehash` = :hash", $board['uri']));
|
||||||
@ -2541,6 +2714,65 @@ function shell_exec_error($command, $suppress_stdout = false) {
|
|||||||
return $return === 'TB_SUCCESS' ? false : $return;
|
return $return === 'TB_SUCCESS' ? false : $return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Die rolling:
|
||||||
|
* If "dice XdY+/-Z" is in the email field (where X or +/-Z may be
|
||||||
|
* missing), X Y-sided dice are rolled and summed, with the modifier Z
|
||||||
|
* added on. The result is displayed at the top of the post.
|
||||||
|
*/
|
||||||
|
function diceRoller($post) {
|
||||||
|
global $config;
|
||||||
|
if(strpos(strtolower($post->email), 'dice%20') === 0) {
|
||||||
|
$dicestr = str_split(substr($post->email, strlen('dice%20')));
|
||||||
|
|
||||||
|
// Get params
|
||||||
|
$diceX = '';
|
||||||
|
$diceY = '';
|
||||||
|
$diceZ = '';
|
||||||
|
|
||||||
|
$curd = 'diceX';
|
||||||
|
for($i = 0; $i < count($dicestr); $i ++) {
|
||||||
|
if(is_numeric($dicestr[$i])) {
|
||||||
|
$$curd .= $dicestr[$i];
|
||||||
|
} else if($dicestr[$i] == 'd') {
|
||||||
|
$curd = 'diceY';
|
||||||
|
} else if($dicestr[$i] == '-' || $dicestr[$i] == '+') {
|
||||||
|
$curd = 'diceZ';
|
||||||
|
$$curd = $dicestr[$i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default values for X and Z
|
||||||
|
if($diceX == '') {
|
||||||
|
$diceX = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
if($diceZ == '') {
|
||||||
|
$diceZ = '+0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intify them
|
||||||
|
$diceX = intval($diceX);
|
||||||
|
$diceY = intval($diceY);
|
||||||
|
$diceZ = intval($diceZ);
|
||||||
|
|
||||||
|
// Continue only if we have valid values
|
||||||
|
if($diceX > 0 && $diceY > 0) {
|
||||||
|
$dicerolls = array();
|
||||||
|
$dicesum = $diceZ;
|
||||||
|
for($i = 0; $i < $diceX; $i++) {
|
||||||
|
$roll = rand(1, $diceY);
|
||||||
|
$dicerolls[] = $roll;
|
||||||
|
$dicesum += $roll;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepend the result to the post body
|
||||||
|
$modifier = ($diceZ != 0) ? ((($diceZ < 0) ? ' - ' : ' + ') . abs($diceZ)) : '';
|
||||||
|
$dicesum = ($diceX > 1) ? ' = ' . $dicesum : '';
|
||||||
|
$post->body = '<table class="diceroll"><tr><td><img src="'.$config['dir']['static'].'d10.svg" alt="Dice roll" width="24"></td><td>Rolled ' . implode(', ', $dicerolls) . $modifier . $dicesum . '</td></tr></table><br/>' . $post->body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function slugify($post) {
|
function slugify($post) {
|
||||||
global $config;
|
global $config;
|
||||||
|
|
||||||
@ -2631,6 +2863,24 @@ function prettify_textarea($s){
|
|||||||
return str_replace("\t", '	', str_replace("\n", ' ', htmlentities($s)));
|
return str_replace("\t", '	', str_replace("\n", ' ', htmlentities($s)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*class HTMLPurifier_URIFilter_NoExternalImages extends HTMLPurifier_URIFilter {
|
||||||
|
public $name = 'NoExternalImages';
|
||||||
|
public function filter(&$uri, $c, $context) {
|
||||||
|
global $config;
|
||||||
|
$ct = $context->get('CurrentToken');
|
||||||
|
|
||||||
|
if (!$ct || $ct->name !== 'img') return true;
|
||||||
|
|
||||||
|
if (!isset($uri->host) && !isset($uri->scheme)) return true;
|
||||||
|
|
||||||
|
if (!in_array($uri->scheme . '://' . $uri->host . '/', $config['allowed_offsite_urls'])) {
|
||||||
|
error('No off-site links in board announcement images.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
function purify_html($s) {
|
function purify_html($s) {
|
||||||
global $config;
|
global $config;
|
||||||
|
|
||||||
@ -2646,6 +2896,7 @@ function purify_html($s) {
|
|||||||
function markdown($s) {
|
function markdown($s) {
|
||||||
$pd = new Parsedown();
|
$pd = new Parsedown();
|
||||||
$pd->setMarkupEscaped(true);
|
$pd->setMarkupEscaped(true);
|
||||||
|
$pd->setimagesEnabled(false);
|
||||||
|
|
||||||
return $pd->text($s);
|
return $pd->text($s);
|
||||||
}
|
}
|
||||||
@ -2664,20 +2915,7 @@ function generation_strategy($fun, $array=array()) { global $config;
|
|||||||
return 'rebuild';
|
return 'rebuild';
|
||||||
case 'defer':
|
case 'defer':
|
||||||
// Ok, it gets interesting here :)
|
// Ok, it gets interesting here :)
|
||||||
$queue = Queues::get_queue($config, 'generate');
|
get_queue('generate')->push(serialize(array('build', $fun, $array, $action)));
|
||||||
if ($queue === false) {
|
|
||||||
if ($config['syslog']) {
|
|
||||||
_syslog(LOG_ERR, "Could not initialize generate queue, falling back to immediate rebuild strategy");
|
|
||||||
}
|
|
||||||
return 'rebuild';
|
|
||||||
}
|
|
||||||
$ret = $queue->push(serialize(array('build', $fun, $array, $action)));
|
|
||||||
if ($ret === false) {
|
|
||||||
if ($config['syslog']) {
|
|
||||||
_syslog(LOG_ERR, "Could not push item in the queue, falling back to immediate rebuild strategy");
|
|
||||||
}
|
|
||||||
return 'rebuild';
|
|
||||||
}
|
|
||||||
return 'ignore';
|
return 'ignore';
|
||||||
case 'build_on_load':
|
case 'build_on_load':
|
||||||
return 'delete';
|
return 'delete';
|
||||||
@ -2776,7 +3014,7 @@ function cloak_ip($ip) {
|
|||||||
if (strlen($ipbytes) >= 16)
|
if (strlen($ipbytes) >= 16)
|
||||||
$ipbytes = substr($ipbytes, 0, 16);
|
$ipbytes = substr($ipbytes, 0, 16);
|
||||||
|
|
||||||
$cyphertext = openssl_encrypt($ipbytes, 'aes-256-ctr', $ipcrypt_key, OPENSSL_RAW_DATA);
|
$cyphertext = openssl_encrypt($ipbytes, 'rc4-40', $ipcrypt_key, OPENSSL_RAW_DATA);
|
||||||
|
|
||||||
$ret = $config['ipcrypt_prefix'].':' . base32_encode($cyphertext);
|
$ret = $config['ipcrypt_prefix'].':' . base32_encode($cyphertext);
|
||||||
if (isset($tld) && !empty($tld)) {
|
if (isset($tld) && !empty($tld)) {
|
||||||
@ -2799,7 +3037,7 @@ function uncloak_ip($ip) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (substr($ip, 0, strlen($config['ipcrypt_prefix']) + 1) === $config['ipcrypt_prefix'].':') {
|
if (substr($ip, 0, strlen($config['ipcrypt_prefix']) + 1) === $config['ipcrypt_prefix'].':') {
|
||||||
$plaintext = openssl_decrypt(base32_decode($juice), 'aes-256-ctr', $ipcrypt_key, OPENSSL_RAW_DATA);
|
$plaintext = openssl_decrypt(base32_decode($juice), 'rc4-40', $ipcrypt_key, OPENSSL_RAW_DATA);
|
||||||
|
|
||||||
if ($plaintext === false || strlen($plaintext) == 0)
|
if ($plaintext === false || strlen($plaintext) == 0)
|
||||||
return '#ERROR';
|
return '#ERROR';
|
||||||
@ -2835,47 +3073,3 @@ function uncloak_mask($mask) {
|
|||||||
|
|
||||||
return $mask;
|
return $mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
function check_thread_limit($post) {
|
|
||||||
global $config, $board;
|
|
||||||
if (!isset($config['max_threads_per_hour']) || !$config['max_threads_per_hour']) return false;
|
|
||||||
|
|
||||||
if ($post['op']) {
|
|
||||||
$query = prepare(sprintf('SELECT COUNT(*) AS `count` FROM ``posts_%s`` WHERE `thread` IS NULL AND FROM_UNIXTIME(`time`) > DATE_SUB(NOW(), INTERVAL 1 HOUR);', $board['uri']));
|
|
||||||
$query->execute() or error(db_error($query));
|
|
||||||
$r = $query->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
return $r['count'] >= $config['max_threads_per_hour'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hashPassword($password) {
|
|
||||||
global $config;
|
|
||||||
|
|
||||||
return hash('sha3-256', $password . $config['secure_password_salt']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thanks to https://gist.github.com/marijn/3901938
|
|
||||||
function trace_url($url) {
|
|
||||||
$ch = curl_init($url);
|
|
||||||
curl_setopt_array($ch, array(
|
|
||||||
CURLOPT_FOLLOWLOCATION => TRUE, // the magic sauce
|
|
||||||
CURLOPT_RETURNTRANSFER => TRUE,
|
|
||||||
CURLOPT_SSL_VERIFYHOST => FALSE, // suppress certain SSL errors
|
|
||||||
CURLOPT_SSL_VERIFYPEER => FALSE,
|
|
||||||
CURLOPT_TIMEOUT => 30,
|
|
||||||
));
|
|
||||||
curl_exec($ch);
|
|
||||||
$url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
|
|
||||||
curl_close($ch);
|
|
||||||
return $url;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thanks to https://stackoverflow.com/questions/10002227/linkify-regex-function-php-daring-fireball-method/10002262#10002262
|
|
||||||
function get_urls($body) {
|
|
||||||
$regex = '(?xi)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))';
|
|
||||||
|
|
||||||
$result = preg_match_all("#$regex#i", $body, $match);
|
|
||||||
|
|
||||||
return $match[0];
|
|
||||||
}
|
|
||||||
|
@ -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>";
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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}";
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -291,7 +291,6 @@ class ImageConvert extends ImageBase {
|
|||||||
} else {
|
} else {
|
||||||
rename($this->temp, $src);
|
rename($this->temp, $src);
|
||||||
chmod($src, 0664);
|
chmod($src, 0664);
|
||||||
$this->temp = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public function width() {
|
public function width() {
|
||||||
@ -301,11 +300,9 @@ class ImageConvert extends ImageBase {
|
|||||||
return $this->height;
|
return $this->height;
|
||||||
}
|
}
|
||||||
public function destroy() {
|
public function destroy() {
|
||||||
if ($this->temp !== false) {
|
|
||||||
@unlink($this->temp);
|
@unlink($this->temp);
|
||||||
$this->temp = false;
|
$this->temp = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
public function resize() {
|
public function resize() {
|
||||||
global $config;
|
global $config;
|
||||||
|
|
||||||
@ -327,6 +324,11 @@ class ImageConvert extends ImageBase {
|
|||||||
error(_('Failed to resize image!'), null, $error);
|
error(_('Failed to resize image!'), null, $error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if ($config['convert_manual_orient'] && ($this->format == 'jpg' || $this->format == 'jpeg'))
|
||||||
|
$convert_args = str_replace('-auto-orient', ImageConvert::jpeg_exif_orientation($this->src), $config['convert_args']);
|
||||||
|
elseif ($config['convert_manual_orient'])
|
||||||
|
$convert_args = str_replace('-auto-orient', '', $config['convert_args']);
|
||||||
|
else
|
||||||
$convert_args = &$config['convert_args'];
|
$convert_args = &$config['convert_args'];
|
||||||
|
|
||||||
if (($error = shell_exec_error(($this->gm ? 'gm ' : '') . 'convert ' .
|
if (($error = shell_exec_error(($this->gm ? 'gm ' : '') . 'convert ' .
|
||||||
@ -346,8 +348,12 @@ class ImageConvert extends ImageBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if ($config['convert_manual_orient'] && ($this->format == 'jpg' || $this->format == 'jpeg'))
|
||||||
|
$convert_args = str_replace('-auto-orient', ImageConvert::jpeg_exif_orientation($this->src), $config['convert_args']);
|
||||||
|
elseif ($config['convert_manual_orient'])
|
||||||
|
$convert_args = str_replace('-auto-orient', '', $config['convert_args']);
|
||||||
|
else
|
||||||
$convert_args = &$config['convert_args'];
|
$convert_args = &$config['convert_args'];
|
||||||
|
|
||||||
if (($error = shell_exec_error(($this->gm ? 'gm ' : '') . 'convert ' .
|
if (($error = shell_exec_error(($this->gm ? 'gm ' : '') . 'convert ' .
|
||||||
sprintf($convert_args,
|
sprintf($convert_args,
|
||||||
$this->width,
|
$this->width,
|
||||||
@ -374,6 +380,69 @@ class ImageConvert extends ImageBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For when -auto-orient doesn't exist (older versions)
|
||||||
|
static public function jpeg_exif_orientation($src, $exif = false) {
|
||||||
|
if (!$exif) {
|
||||||
|
$exif = @exif_read_data($src);
|
||||||
|
if (!isset($exif['Orientation']))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch($exif['Orientation']) {
|
||||||
|
case 1:
|
||||||
|
// Normal
|
||||||
|
return false;
|
||||||
|
case 2:
|
||||||
|
// 888888
|
||||||
|
// 88
|
||||||
|
// 8888
|
||||||
|
// 88
|
||||||
|
// 88
|
||||||
|
|
||||||
|
return '-flop';
|
||||||
|
case 3:
|
||||||
|
|
||||||
|
// 88
|
||||||
|
// 88
|
||||||
|
// 8888
|
||||||
|
// 88
|
||||||
|
// 888888
|
||||||
|
|
||||||
|
return '-flip -flop';
|
||||||
|
case 4:
|
||||||
|
// 88
|
||||||
|
// 88
|
||||||
|
// 8888
|
||||||
|
// 88
|
||||||
|
// 888888
|
||||||
|
|
||||||
|
return '-flip';
|
||||||
|
case 5:
|
||||||
|
// 8888888888
|
||||||
|
// 88 88
|
||||||
|
// 88
|
||||||
|
|
||||||
|
return '-rotate 90 -flop';
|
||||||
|
case 6:
|
||||||
|
// 88
|
||||||
|
// 88 88
|
||||||
|
// 8888888888
|
||||||
|
|
||||||
|
return '-rotate 90';
|
||||||
|
case 7:
|
||||||
|
// 88
|
||||||
|
// 88 88
|
||||||
|
// 8888888888
|
||||||
|
|
||||||
|
return '-rotate "-90" -flop';
|
||||||
|
case 8:
|
||||||
|
// 8888888888
|
||||||
|
// 88 88
|
||||||
|
// 88
|
||||||
|
|
||||||
|
return '-rotate "-90"';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImagePNG extends ImageBase {
|
class ImagePNG extends ImageBase {
|
||||||
@ -428,11 +497,7 @@ class ImageBMP extends ImageBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImageWEBP extends ImageBase {
|
|
||||||
public function from() {
|
if (PHP_MAJOR_VERSION <= 7 && PHP_MINOR_VERSION < 2) {
|
||||||
$this->image = @imagecreatefromwebp($this->src);
|
include 'inc/image/bmp.php';
|
||||||
}
|
|
||||||
public function to($src) {
|
|
||||||
imagewebp($this->image, $src);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
186
inc/image/bmp.php
Normal file
186
inc/image/bmp.php
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* These functions provide a polyfill for old versions of PHP.
|
||||||
|
* PHP >= 7.2.0 provides these functions by default.
|
||||||
|
*
|
||||||
|
* These functions were submitted by user DHKold to the PHP manual in 2005.
|
||||||
|
* http://php.net/manual/en/function.imagecreate.php#53879
|
||||||
|
*
|
||||||
|
* As per http://php.net/manual/en/about.notes.php ("About user notes", PHP
|
||||||
|
* Manual), all PHP User Contributed Notes are "...considered part of the PHP
|
||||||
|
* manual, and are therefore covered by the same license that covers this
|
||||||
|
* documentation."
|
||||||
|
*
|
||||||
|
* Therefore, as per http://php.net/manual/en/copyright.php ("Copyright", PHP
|
||||||
|
* Manual), the code below is licensed under the terms of the Creative Commons
|
||||||
|
* Attribution 3.0 license, AKA CC-BY 3.0.
|
||||||
|
* http://creativecommons.org/licenses/by/3.0/
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*********************************************/
|
||||||
|
/* Function: imagecreatefrombmp */
|
||||||
|
/* Author: DHKold */
|
||||||
|
/* Contact: admin@dhkold.com */
|
||||||
|
/* Date: The 15th of June 2005 */
|
||||||
|
/* Version: 2.0B */
|
||||||
|
/*********************************************/
|
||||||
|
function imagecreatefrombmp($filename) {
|
||||||
|
if (! $f1 = fopen($filename,"rb")) return FALSE;
|
||||||
|
$FILE = unpack("vfile_type/Vfile_size/Vreserved/Vbitmap_offset", fread($f1,14));
|
||||||
|
if ($FILE['file_type'] != 19778) return FALSE;
|
||||||
|
$BMP = unpack('Vheader_size/Vwidth/Vheight/vplanes/vbits_per_pixel'.
|
||||||
|
'/Vcompression/Vsize_bitmap/Vhoriz_resolution'.
|
||||||
|
'/Vvert_resolution/Vcolors_used/Vcolors_important', fread($f1,40));
|
||||||
|
$BMP['colors'] = pow(2,$BMP['bits_per_pixel']);
|
||||||
|
if ($BMP['size_bitmap'] == 0) $BMP['size_bitmap'] = $FILE['file_size'] - $FILE['bitmap_offset'];
|
||||||
|
$BMP['bytes_per_pixel'] = $BMP['bits_per_pixel']/8;
|
||||||
|
$BMP['bytes_per_pixel2'] = ceil($BMP['bytes_per_pixel']);
|
||||||
|
$BMP['decal'] = ($BMP['width']*$BMP['bytes_per_pixel']/4);
|
||||||
|
$BMP['decal'] -= floor($BMP['width']*$BMP['bytes_per_pixel']/4);
|
||||||
|
$BMP['decal'] = 4-(4*$BMP['decal']);
|
||||||
|
if ($BMP['decal'] == 4) $BMP['decal'] = 0;
|
||||||
|
|
||||||
|
$PALETTE = array();
|
||||||
|
if ($BMP['colors'] < 16777216)
|
||||||
|
{
|
||||||
|
$PALETTE = unpack('V'.$BMP['colors'], fread($f1,$BMP['colors']*4));
|
||||||
|
}
|
||||||
|
|
||||||
|
$IMG = fread($f1,$BMP['size_bitmap']);
|
||||||
|
$VIDE = chr(0);
|
||||||
|
|
||||||
|
$res = imagecreatetruecolor($BMP['width'],$BMP['height']);
|
||||||
|
$P = 0;
|
||||||
|
$Y = $BMP['height']-1;
|
||||||
|
while ($Y >= 0)
|
||||||
|
{
|
||||||
|
$X=0;
|
||||||
|
while ($X < $BMP['width'])
|
||||||
|
{
|
||||||
|
if ($BMP['bits_per_pixel'] == 24)
|
||||||
|
$COLOR = unpack("V",substr($IMG,$P,3).$VIDE);
|
||||||
|
elseif ($BMP['bits_per_pixel'] == 16)
|
||||||
|
{
|
||||||
|
$COLOR = unpack("n",substr($IMG,$P,2));
|
||||||
|
$COLOR[1] = $PALETTE[$COLOR[1]+1];
|
||||||
|
}
|
||||||
|
elseif ($BMP['bits_per_pixel'] == 8)
|
||||||
|
{
|
||||||
|
$COLOR = unpack("n",$VIDE.substr($IMG,$P,1));
|
||||||
|
$COLOR[1] = $PALETTE[$COLOR[1]+1];
|
||||||
|
}
|
||||||
|
elseif ($BMP['bits_per_pixel'] == 4)
|
||||||
|
{
|
||||||
|
$COLOR = unpack("n",$VIDE.substr($IMG,floor($P),1));
|
||||||
|
if (($P*2)%2 == 0) $COLOR[1] = ($COLOR[1] >> 4) ; else $COLOR[1] = ($COLOR[1] & 0x0F);
|
||||||
|
$COLOR[1] = $PALETTE[$COLOR[1]+1];
|
||||||
|
}
|
||||||
|
elseif ($BMP['bits_per_pixel'] == 1)
|
||||||
|
{
|
||||||
|
$COLOR = unpack("n",$VIDE.substr($IMG,floor($P),1));
|
||||||
|
if (($P*8)%8 == 0) $COLOR[1] = $COLOR[1] >>7;
|
||||||
|
elseif (($P*8)%8 == 1) $COLOR[1] = ($COLOR[1] & 0x40)>>6;
|
||||||
|
elseif (($P*8)%8 == 2) $COLOR[1] = ($COLOR[1] & 0x20)>>5;
|
||||||
|
elseif (($P*8)%8 == 3) $COLOR[1] = ($COLOR[1] & 0x10)>>4;
|
||||||
|
elseif (($P*8)%8 == 4) $COLOR[1] = ($COLOR[1] & 0x8)>>3;
|
||||||
|
elseif (($P*8)%8 == 5) $COLOR[1] = ($COLOR[1] & 0x4)>>2;
|
||||||
|
elseif (($P*8)%8 == 6) $COLOR[1] = ($COLOR[1] & 0x2)>>1;
|
||||||
|
elseif (($P*8)%8 == 7) $COLOR[1] = ($COLOR[1] & 0x1);
|
||||||
|
$COLOR[1] = $PALETTE[$COLOR[1]+1];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return FALSE;
|
||||||
|
imagesetpixel($res,$X,$Y,$COLOR[1]);
|
||||||
|
$X++;
|
||||||
|
$P += $BMP['bytes_per_pixel'];
|
||||||
|
}
|
||||||
|
$Y--;
|
||||||
|
$P+=$BMP['decal'];
|
||||||
|
}
|
||||||
|
fclose($f1);
|
||||||
|
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function imagebmp(&$img, $filename='') {
|
||||||
|
$widthOrig = imagesx($img);
|
||||||
|
$widthFloor = ((floor($widthOrig/16))*16);
|
||||||
|
$widthCeil = ((ceil($widthOrig/16))*16);
|
||||||
|
$height = imagesy($img);
|
||||||
|
|
||||||
|
$size = ($widthCeil*$height*3)+54;
|
||||||
|
|
||||||
|
// Bitmap File Header
|
||||||
|
$result = 'BM'; // header (2b)
|
||||||
|
$result .= int_to_dword($size); // size of file (4b)
|
||||||
|
$result .= int_to_dword(0); // reserved (4b)
|
||||||
|
$result .= int_to_dword(54); // byte location in the file which is first byte of IMAGE (4b)
|
||||||
|
// Bitmap Info Header
|
||||||
|
$result .= int_to_dword(40); // Size of BITMAPINFOHEADER (4b)
|
||||||
|
$result .= int_to_dword($widthCeil); // width of bitmap (4b)
|
||||||
|
$result .= int_to_dword($height); // height of bitmap (4b)
|
||||||
|
$result .= int_to_word(1); // biPlanes = 1 (2b)
|
||||||
|
$result .= int_to_word(24); // biBitCount = {1 (mono) or 4 (16 clr ) or 8 (256 clr) or 24 (16 Mil)} (2b
|
||||||
|
$result .= int_to_dword(0); // RLE COMPRESSION (4b)
|
||||||
|
$result .= int_to_dword(0); // width x height (4b)
|
||||||
|
$result .= int_to_dword(0); // biXPelsPerMeter (4b)
|
||||||
|
$result .= int_to_dword(0); // biYPelsPerMeter (4b)
|
||||||
|
$result .= int_to_dword(0); // Number of palettes used (4b)
|
||||||
|
$result .= int_to_dword(0); // Number of important colour (4b)
|
||||||
|
|
||||||
|
// is faster than chr()
|
||||||
|
$arrChr = array();
|
||||||
|
for ($i=0; $i<256; $i++){
|
||||||
|
$arrChr[$i] = chr($i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates image data
|
||||||
|
$bgfillcolor = array('red'=>0, 'green'=>0, 'blue'=>0);
|
||||||
|
|
||||||
|
// bottom to top - left to right - attention blue green red !!!
|
||||||
|
$y=$height-1;
|
||||||
|
for ($y2=0; $y2<$height; $y2++) {
|
||||||
|
for ($x=0; $x<$widthFloor; ) {
|
||||||
|
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
|
||||||
|
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
|
||||||
|
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
|
||||||
|
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
|
||||||
|
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
|
||||||
|
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
|
||||||
|
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
|
||||||
|
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
|
||||||
|
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
|
||||||
|
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
|
||||||
|
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
|
||||||
|
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
|
||||||
|
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
|
||||||
|
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
|
||||||
|
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
|
||||||
|
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
|
||||||
|
}
|
||||||
|
for ($x=$widthFloor; $x<$widthCeil; $x++) {
|
||||||
|
$rgb = ($x<$widthOrig) ? imagecolorsforindex($img, imagecolorat($img, $x, $y)) : $bgfillcolor;
|
||||||
|
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
|
||||||
|
}
|
||||||
|
$y--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// see imagegif
|
||||||
|
if ($filename == '') {
|
||||||
|
echo $result;
|
||||||
|
} else {
|
||||||
|
$file = fopen($filename, 'wb');
|
||||||
|
fwrite($file, $result);
|
||||||
|
fclose($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// imagebmp helpers
|
||||||
|
function int_to_dword($n) {
|
||||||
|
return chr($n & 255).chr(($n >> 8) & 255).chr(($n >> 16) & 255).chr(($n >> 24) & 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
function int_to_word($n) {
|
||||||
|
return chr($n & 255).chr(($n >> 8) & 255);
|
||||||
|
}
|
||||||
|
?>
|
45
inc/lib/twig/extensions/Extension/I18n.php
Normal file
45
inc/lib/twig/extensions/Extension/I18n.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Twig.
|
||||||
|
*
|
||||||
|
* (c) 2010 Fabien Potencier
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
class Twig_Extensions_Extension_I18n extends Twig_Extension
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns the token parser instances to add to the existing list.
|
||||||
|
*
|
||||||
|
* @return array An array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances
|
||||||
|
*/
|
||||||
|
public function getTokenParsers()
|
||||||
|
{
|
||||||
|
return array(new Twig_Extensions_TokenParser_Trans());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of filters to add to the existing list.
|
||||||
|
*
|
||||||
|
* @return array An array of filters
|
||||||
|
*/
|
||||||
|
public function getFilters()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
new Twig_SimpleFilter('trans', 'gettext'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the extension.
|
||||||
|
*
|
||||||
|
* @return string The extension name
|
||||||
|
*/
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return 'i18n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
137
inc/lib/twig/extensions/Extension/Tinyboard.php
Normal file
137
inc/lib/twig/extensions/Extension/Tinyboard.php
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Twig_Extensions_Extension_Tinyboard extends Twig_Extension
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns a list of filters to add to the existing list.
|
||||||
|
*
|
||||||
|
* @return array An array of filters
|
||||||
|
*/
|
||||||
|
public function getFilters()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
new Twig_SimpleFilter('filesize', 'format_bytes'),
|
||||||
|
new Twig_SimpleFilter('truncate', 'twig_truncate_filter'),
|
||||||
|
new Twig_SimpleFilter('truncate_body', 'truncate'),
|
||||||
|
new Twig_SimpleFilter('truncate_filename', 'twig_filename_truncate_filter'),
|
||||||
|
new Twig_SimpleFilter('extension', 'twig_extension_filter'),
|
||||||
|
new Twig_SimpleFilter('sprintf', 'sprintf'),
|
||||||
|
new Twig_SimpleFilter('capcode', 'capcode'),
|
||||||
|
new Twig_SimpleFilter('remove_modifiers', 'remove_modifiers'),
|
||||||
|
new Twig_SimpleFilter('hasPermission', 'twig_hasPermission_filter'),
|
||||||
|
new Twig_SimpleFilter('date', 'twig_date_filter'),
|
||||||
|
new Twig_SimpleFilter('poster_id', 'poster_id'),
|
||||||
|
new Twig_SimpleFilter('remove_whitespace', 'twig_remove_whitespace_filter'),
|
||||||
|
new Twig_SimpleFilter('count', 'count'),
|
||||||
|
new Twig_SimpleFilter('ago', 'ago'),
|
||||||
|
new Twig_SimpleFilter('until', 'until'),
|
||||||
|
new Twig_SimpleFilter('push', 'twig_push_filter'),
|
||||||
|
new Twig_SimpleFilter('bidi_cleanup', 'bidi_cleanup'),
|
||||||
|
new Twig_SimpleFilter('addslashes', 'addslashes'),
|
||||||
|
new Twig_SimpleFilter('cloak_ip', 'cloak_ip'),
|
||||||
|
new Twig_SimpleFilter('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_SimpleFunction('time', 'time'),
|
||||||
|
new Twig_SimpleFunction('floor', 'floor'),
|
||||||
|
new Twig_SimpleFunction('timezone', 'twig_timezone_function'),
|
||||||
|
new Twig_SimpleFunction('hiddenInputs', 'hiddenInputs'),
|
||||||
|
new Twig_SimpleFunction('hiddenInputsHash', 'hiddenInputsHash'),
|
||||||
|
new Twig_SimpleFunction('ratio', 'twig_ratio_function'),
|
||||||
|
new Twig_SimpleFunction('secure_link_confirm', 'twig_secure_link_confirm'),
|
||||||
|
new Twig_SimpleFunction('secure_link', 'twig_secure_link'),
|
||||||
|
new Twig_SimpleFunction('link_for', 'link_for')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the extension.
|
||||||
|
*
|
||||||
|
* @return string The extension name
|
||||||
|
*/
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return 'tinyboard';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function twig_timezone_function() {
|
||||||
|
return 'Z';
|
||||||
|
}
|
||||||
|
|
||||||
|
function twig_push_filter($array, $value) {
|
||||||
|
array_push($array, $value);
|
||||||
|
return $array;
|
||||||
|
}
|
||||||
|
|
||||||
|
function twig_remove_whitespace_filter($data) {
|
||||||
|
return preg_replace('/[\t\r\n]/', '', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function twig_date_filter($date, $format) {
|
||||||
|
return gmstrftime($format, $date);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
133
inc/lib/twig/extensions/Node/Trans.php
Normal file
133
inc/lib/twig/extensions/Node/Trans.php
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Twig.
|
||||||
|
*
|
||||||
|
* (c) 2010 Fabien Potencier
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a trans node.
|
||||||
|
*
|
||||||
|
* @package twig
|
||||||
|
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
|
||||||
|
*/
|
||||||
|
class Twig_Extensions_Node_Trans extends Twig_Node
|
||||||
|
{
|
||||||
|
public function __construct(Twig_NodeInterface $body, Twig_NodeInterface $plural = null, Twig_Node_Expression $count = null, $lineno, $tag = null)
|
||||||
|
{
|
||||||
|
parent::__construct(array('count' => $count, 'body' => $body, 'plural' => $plural), array(), $lineno, $tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compiles the node to PHP.
|
||||||
|
*
|
||||||
|
* @param Twig_Compiler A Twig_Compiler instance
|
||||||
|
*/
|
||||||
|
public function compile(Twig_Compiler $compiler)
|
||||||
|
{
|
||||||
|
$compiler->addDebugInfo($this);
|
||||||
|
|
||||||
|
list($msg, $vars) = $this->compileString($this->getNode('body'));
|
||||||
|
|
||||||
|
if (null !== $this->getNode('plural')) {
|
||||||
|
list($msg1, $vars1) = $this->compileString($this->getNode('plural'));
|
||||||
|
|
||||||
|
$vars = array_merge($vars, $vars1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$function = null === $this->getNode('plural') ? 'gettext' : 'ngettext';
|
||||||
|
|
||||||
|
if ($vars) {
|
||||||
|
$compiler
|
||||||
|
->write('echo strtr('.$function.'(')
|
||||||
|
->subcompile($msg)
|
||||||
|
;
|
||||||
|
|
||||||
|
if (null !== $this->getNode('plural')) {
|
||||||
|
$compiler
|
||||||
|
->raw(', ')
|
||||||
|
->subcompile($msg1)
|
||||||
|
->raw(', abs(')
|
||||||
|
->subcompile($this->getNode('count'))
|
||||||
|
->raw(')')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
$compiler->raw('), array(');
|
||||||
|
|
||||||
|
foreach ($vars as $var) {
|
||||||
|
if ('count' === $var->getAttribute('name')) {
|
||||||
|
$compiler
|
||||||
|
->string('%count%')
|
||||||
|
->raw(' => abs(')
|
||||||
|
->subcompile($this->getNode('count'))
|
||||||
|
->raw('), ')
|
||||||
|
;
|
||||||
|
} else {
|
||||||
|
$compiler
|
||||||
|
->string('%'.$var->getAttribute('name').'%')
|
||||||
|
->raw(' => ')
|
||||||
|
->subcompile($var)
|
||||||
|
->raw(', ')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$compiler->raw("));\n");
|
||||||
|
} else {
|
||||||
|
$compiler
|
||||||
|
->write('echo '.$function.'(')
|
||||||
|
->subcompile($msg)
|
||||||
|
;
|
||||||
|
|
||||||
|
if (null !== $this->getNode('plural')) {
|
||||||
|
$compiler
|
||||||
|
->raw(', ')
|
||||||
|
->subcompile($msg1)
|
||||||
|
->raw(', abs(')
|
||||||
|
->subcompile($this->getNode('count'))
|
||||||
|
->raw(')')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
$compiler->raw(");\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function compileString(Twig_NodeInterface $body)
|
||||||
|
{
|
||||||
|
if ($body instanceof Twig_Node_Expression_Name || $body instanceof Twig_Node_Expression_Constant || $body instanceof Twig_Node_Expression_TempName) {
|
||||||
|
return array($body, array());
|
||||||
|
}
|
||||||
|
|
||||||
|
$vars = array();
|
||||||
|
if (count($body)) {
|
||||||
|
$msg = '';
|
||||||
|
|
||||||
|
foreach ($body as $node) {
|
||||||
|
if (get_class($node) === 'Twig_Node' && $node->getNode(0) instanceof Twig_Node_SetTemp) {
|
||||||
|
$node = $node->getNode(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($node instanceof Twig_Node_Print) {
|
||||||
|
$n = $node->getNode('expr');
|
||||||
|
while ($n instanceof Twig_Node_Expression_Filter) {
|
||||||
|
$n = $n->getNode('node');
|
||||||
|
}
|
||||||
|
$msg .= sprintf('%%%s%%', $n->getAttribute('name'));
|
||||||
|
$vars[] = new Twig_Node_Expression_Name($n->getAttribute('name'), $n->getLine());
|
||||||
|
} else {
|
||||||
|
$msg .= $node->getAttribute('data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$msg = $body->getAttribute('data');
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(new Twig_Node(array(new Twig_Node_Expression_Constant(trim($msg), $body->getLine()))), $vars);
|
||||||
|
}
|
||||||
|
}
|
80
inc/lib/twig/extensions/TokenParser/Trans.php
Normal file
80
inc/lib/twig/extensions/TokenParser/Trans.php
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Twig.
|
||||||
|
*
|
||||||
|
* (c) 2010 Fabien Potencier
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
class Twig_Extensions_TokenParser_Trans extends Twig_TokenParser
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Parses a token and returns a node.
|
||||||
|
*
|
||||||
|
* @param Twig_Token $token A Twig_Token instance
|
||||||
|
*
|
||||||
|
* @return Twig_NodeInterface A Twig_NodeInterface instance
|
||||||
|
*/
|
||||||
|
public function parse(Twig_Token $token)
|
||||||
|
{
|
||||||
|
$lineno = $token->getLine();
|
||||||
|
$stream = $this->parser->getStream();
|
||||||
|
$count = null;
|
||||||
|
$plural = null;
|
||||||
|
|
||||||
|
if (!$stream->test(Twig_Token::BLOCK_END_TYPE)) {
|
||||||
|
$body = $this->parser->getExpressionParser()->parseExpression();
|
||||||
|
} else {
|
||||||
|
$stream->expect(Twig_Token::BLOCK_END_TYPE);
|
||||||
|
$body = $this->parser->subparse(array($this, 'decideForFork'));
|
||||||
|
if ('plural' === $stream->next()->getValue()) {
|
||||||
|
$count = $this->parser->getExpressionParser()->parseExpression();
|
||||||
|
$stream->expect(Twig_Token::BLOCK_END_TYPE);
|
||||||
|
$plural = $this->parser->subparse(array($this, 'decideForEnd'), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stream->expect(Twig_Token::BLOCK_END_TYPE);
|
||||||
|
|
||||||
|
$this->checkTransString($body, $lineno);
|
||||||
|
|
||||||
|
return new Twig_Extensions_Node_Trans($body, $plural, $count, $lineno, $this->getTag());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function decideForFork(Twig_Token $token)
|
||||||
|
{
|
||||||
|
return $token->test(array('plural', 'endtrans'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function decideForEnd(Twig_Token $token)
|
||||||
|
{
|
||||||
|
return $token->test('endtrans');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the tag name associated with this token parser.
|
||||||
|
*
|
||||||
|
* @param string The tag name
|
||||||
|
*/
|
||||||
|
public function getTag()
|
||||||
|
{
|
||||||
|
return 'trans';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function checkTransString(Twig_NodeInterface $body, $lineno)
|
||||||
|
{
|
||||||
|
foreach ($body as $i => $node) {
|
||||||
|
if (
|
||||||
|
$node instanceof Twig_Node_Text
|
||||||
|
||
|
||||||
|
($node instanceof Twig_Node_Print && $node->getNode('expr') instanceof Twig_Node_Expression_Name)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Twig_Error_Syntax(sprintf('The text to be translated with "trans" can only contain references to simple variables'), $lineno);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2287,6 +2287,8 @@ msgstr "usuń"
|
|||||||
msgid "New note"
|
msgid "New note"
|
||||||
msgstr "Nowa notka"
|
msgstr "Nowa notka"
|
||||||
|
|
||||||
|
msgid "New telegram"
|
||||||
|
msgstr "Nowa depesza"
|
||||||
|
|
||||||
#. line 94
|
#. line 94
|
||||||
#. line 7
|
#. line 7
|
||||||
|
73
inc/lock.php
73
inc/lock.php
@ -1,76 +1,39 @@
|
|||||||
<?php
|
<?php
|
||||||
class Locks {
|
class Lock {
|
||||||
private static function filesystem(string $key) {
|
function __construct($key) { global $config;
|
||||||
|
if ($config['lock']['enabled'] == 'fs') {
|
||||||
$key = str_replace('/', '::', $key);
|
$key = str_replace('/', '::', $key);
|
||||||
$key = str_replace("\0", '', $key);
|
$key = str_replace("\0", '', $key);
|
||||||
|
|
||||||
$fd = fopen("tmp/locks/$key", "w");
|
$this->f = fopen("tmp/locks/$key", "w");
|
||||||
if ($fd === false) {
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new class($fd) implements Lock {
|
// Get a shared lock
|
||||||
// Resources have no type in PHP.
|
function get($nonblock = false) { global $config;
|
||||||
private $f;
|
if ($config['lock']['enabled'] == 'fs') {
|
||||||
|
|
||||||
public function __construct($fd) {
|
|
||||||
$this->f = $fd;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get(bool $nonblock = false) {
|
|
||||||
$wouldblock = false;
|
$wouldblock = false;
|
||||||
flock($this->f, LOCK_SH | ($nonblock ? LOCK_NB : 0), $wouldblock);
|
flock($this->f, LOCK_SH | ($nonblock ? LOCK_NB : 0), $wouldblock);
|
||||||
if ($nonblock && $wouldblock) {
|
if ($nonblock && $wouldblock) return false;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_ex(bool $nonblock = false) {
|
// Get an exclusive lock
|
||||||
|
function get_ex($nonblock = false) { global $config;
|
||||||
|
if ($config['lock']['enabled'] == 'fs') {
|
||||||
$wouldblock = false;
|
$wouldblock = false;
|
||||||
flock($this->f, LOCK_EX | ($nonblock ? LOCK_NB : 0), $wouldblock);
|
flock($this->f, LOCK_EX | ($nonblock ? LOCK_NB : 0), $wouldblock);
|
||||||
if ($nonblock && $wouldblock) {
|
if ($nonblock && $wouldblock) return false;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function free() {
|
// Free a lock
|
||||||
flock($this->f, LOCK_UN);
|
function free() { global $config;
|
||||||
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') {
|
if ($config['lock']['enabled'] == 'fs') {
|
||||||
return self::filesystem($key);
|
flock($this->f, LOCK_UN);
|
||||||
} else {
|
}
|
||||||
return self::none();
|
return $this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
interface Lock {
|
|
||||||
public function get(bool $nonblock = false);
|
|
||||||
|
|
||||||
public function get_ex(bool $nonblock = false);
|
|
||||||
|
|
||||||
public function free();
|
|
||||||
}
|
|
||||||
|
169
inc/mod/auth.php
169
inc/mod/auth.php
@ -4,13 +4,10 @@
|
|||||||
* Copyright (c) 2010-2013 Tinyboard Development Group
|
* Copyright (c) 2010-2013 Tinyboard Development Group
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use Vichan\Context;
|
|
||||||
use Vichan\Functions\Net;
|
|
||||||
|
|
||||||
defined('TINYBOARD') or exit;
|
defined('TINYBOARD') or exit;
|
||||||
|
|
||||||
// create a hash/salt pair for validate logins
|
// create a hash/salt pair for validate logins
|
||||||
function mkhash(string $username, $password = null, $salt = false) {
|
function mkhash($username, $password, $salt = false) {
|
||||||
global $config;
|
global $config;
|
||||||
|
|
||||||
if (!$salt) {
|
if (!$salt) {
|
||||||
@ -34,52 +31,55 @@ function mkhash(string $username, $password = null, $salt = false) {
|
|||||||
), 0, 20
|
), 0, 20
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isset($generated_salt)) {
|
if (isset($generated_salt))
|
||||||
return [ $hash, $salt ];
|
return array($hash, $salt);
|
||||||
} else {
|
else
|
||||||
return $hash;
|
return $hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function crypt_password_old($password) {
|
||||||
|
$salt = generate_salt();
|
||||||
|
$password = hash('sha256', $salt . sha1($password));
|
||||||
|
return array($salt, $password);
|
||||||
}
|
}
|
||||||
|
|
||||||
function crypt_password(string $password): array {
|
function crypt_password($password) {
|
||||||
global $config;
|
global $config;
|
||||||
// `salt` database field is reused as a version value. We don't want it to be 0.
|
// `salt` database field is reused as a version value. We don't want it to be 0.
|
||||||
$version = $config['password_crypt_version'] ? $config['password_crypt_version'] : 1;
|
$version = $config['password_crypt_version'] ? $config['password_crypt_version'] : 1;
|
||||||
$new_salt = generate_salt();
|
$new_salt = generate_salt();
|
||||||
$password = crypt($password, $config['password_crypt'] . $new_salt . "$");
|
$password = crypt($password, $config['password_crypt'] . $new_salt . "$");
|
||||||
return [ $version, $password ];
|
return array($version, $password);
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_password(string $password, string $salt, string $test): array {
|
function test_password($password, $salt, $test) {
|
||||||
|
global $config;
|
||||||
|
|
||||||
// Version = 0 denotes an old password hashing schema. In the same column, the
|
// Version = 0 denotes an old password hashing schema. In the same column, the
|
||||||
// password hash was kept previously
|
// password hash was kept previously
|
||||||
$version = strlen($salt) <= 8 ? (int)$salt : 0;
|
$version = (strlen($salt) <= 8) ? (int) $salt : 0;
|
||||||
|
|
||||||
if ($version == 0) {
|
if ($version == 0) {
|
||||||
$comp = hash('sha256', $salt . sha1($test));
|
$comp = hash('sha256', $salt . sha1($test));
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$comp = crypt($test, $password);
|
$comp = crypt($test, $password);
|
||||||
}
|
}
|
||||||
return [ $version, hash_equals($password, $comp) ];
|
return array($version, hash_equals($password, $comp));
|
||||||
}
|
}
|
||||||
|
|
||||||
function generate_salt(): string {
|
function generate_salt() {
|
||||||
|
// mcrypt_create_iv() was deprecated in PHP 7.1.0, only use it if we're below that version number.
|
||||||
|
if (PHP_VERSION_ID < 70100) {
|
||||||
|
// 128 bits of entropy
|
||||||
|
return strtr(base64_encode(mcrypt_create_iv(16, MCRYPT_DEV_URANDOM)), '+', '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, use random_bytes()
|
||||||
return strtr(base64_encode(random_bytes(16)), '+', '.');
|
return strtr(base64_encode(random_bytes(16)), '+', '.');
|
||||||
}
|
}
|
||||||
|
|
||||||
function calc_cookie_name(bool $is_https, bool $is_path_jailed, string $base_name): string {
|
function login($username, $password) {
|
||||||
if ($is_https) {
|
|
||||||
if ($is_path_jailed) {
|
|
||||||
return "__Host-$base_name";
|
|
||||||
} else {
|
|
||||||
return "__Secure-$base_name";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return $base_name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function login(string $username, string $password) {
|
|
||||||
global $mod, $config;
|
global $mod, $config;
|
||||||
|
|
||||||
$query = prepare("SELECT `id`, `type`, `boards`, `password`, `version` FROM ``mods`` WHERE BINARY `username` = :username");
|
$query = prepare("SELECT `id`, `type`, `boards`, `password`, `version` FROM ``mods`` WHERE BINARY `username` = :username");
|
||||||
@ -100,83 +100,40 @@ function login(string $username, string $password) {
|
|||||||
$query->execute() or error(db_error($query));
|
$query->execute() or error(db_error($query));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $mod = [
|
return $mod = array(
|
||||||
'id' => $user['id'],
|
'id' => $user['id'],
|
||||||
'type' => $user['type'],
|
'type' => $user['type'],
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'hash' => mkhash($username, $user['password']),
|
'hash' => mkhash($username, $user['password']),
|
||||||
'boards' => explode(',', $user['boards'])
|
'boards' => explode(',', $user['boards'])
|
||||||
];
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCookies(): void {
|
function setCookies() {
|
||||||
global $mod, $config;
|
global $mod, $config;
|
||||||
if (!$mod) {
|
if (!$mod)
|
||||||
error('setCookies() was called for a non-moderator!');
|
error('setCookies() was called for a non-moderator!');
|
||||||
|
|
||||||
|
setcookie($config['cookies']['mod'],
|
||||||
|
$mod['username'] . // username
|
||||||
|
':' .
|
||||||
|
$mod['hash'][0] . // password
|
||||||
|
':' .
|
||||||
|
$mod['hash'][1], // salt
|
||||||
|
time() + $config['cookies']['expire'], $config['cookies']['jail'] ? $config['cookies']['path'] : '/', null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', $config['cookies']['httponly']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$is_https = Net\is_connection_secure($config['cookies']['secure_login_only'] === 1);
|
function destroyCookies() {
|
||||||
$is_path_jailed = $config['cookies']['jail'];
|
|
||||||
$name = calc_cookie_name($is_https, $is_path_jailed, $config['cookies']['mod']);
|
|
||||||
|
|
||||||
// <username>:<password>:<salt>
|
|
||||||
$value = "{$mod['username']}:{$mod['hash'][0]}:{$mod['hash'][1]}";
|
|
||||||
|
|
||||||
$options = [
|
|
||||||
'expires' => time() + $config['cookies']['expire'],
|
|
||||||
'path' => $is_path_jailed ? $config['cookies']['path'] : '/',
|
|
||||||
'secure' => $is_https,
|
|
||||||
'httponly' => $config['cookies']['httponly'],
|
|
||||||
'samesite' => 'Strict'
|
|
||||||
];
|
|
||||||
|
|
||||||
setcookie($name, $value, $options);
|
|
||||||
}
|
|
||||||
|
|
||||||
function destroyCookies(): void {
|
|
||||||
global $config;
|
global $config;
|
||||||
$base_name = $config['cookies']['mod'];
|
// Delete the cookies
|
||||||
$del_time = time() - 60 * 60 * 24 * 365; // 1 year.
|
setcookie($config['cookies']['mod'], 'deleted', time() - $config['cookies']['expire'], $config['cookies']['jail']?$config['cookies']['path'] : '/', null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true);
|
||||||
$jailed_path = $config['cookies']['jail'] ? $config['cookies']['path'] : '/';
|
|
||||||
$http_only = $config['cookies']['httponly'];
|
|
||||||
|
|
||||||
$options_multi = [
|
|
||||||
$base_name => [
|
|
||||||
'expires' => $del_time,
|
|
||||||
'path' => $jailed_path ,
|
|
||||||
'secure' => false,
|
|
||||||
'httponly' => $http_only,
|
|
||||||
'samesite' => 'Strict'
|
|
||||||
],
|
|
||||||
"__Host-$base_name" => [
|
|
||||||
'expires' => $del_time,
|
|
||||||
'path' => $jailed_path,
|
|
||||||
'secure' => true,
|
|
||||||
'httponly' => $http_only,
|
|
||||||
'samesite' => 'Strict'
|
|
||||||
],
|
|
||||||
"__Secure-$base_name" => [
|
|
||||||
'expires' => $del_time,
|
|
||||||
'path' => '/',
|
|
||||||
'secure' => true,
|
|
||||||
'httponly' => $http_only,
|
|
||||||
'samesite' => 'Strict'
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($options_multi as $name => $options) {
|
|
||||||
if (isset($_COOKIE[$name])) {
|
|
||||||
setcookie($name, 'deleted', $options);
|
|
||||||
unset($_COOKIE[$name]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function modLog(string $action, ?string $_board = null): void {
|
function modLog($action, $_board=null) {
|
||||||
global $mod, $board, $config;
|
global $mod, $board, $config;
|
||||||
$query = prepare("INSERT INTO ``modlogs`` VALUES (:id, :ip, :board, :time, :text)");
|
$query = prepare("INSERT INTO ``modlogs`` VALUES (:id, :ip, :board, :time, :text)");
|
||||||
$query->bindValue(':id', (isset($mod['id']) ? $mod['id'] : -1), PDO::PARAM_INT);
|
$query->bindValue(':id', (isset($mod['id']) ? $mod['id'] : -1), PDO::PARAM_INT);
|
||||||
@ -191,18 +148,16 @@ function modLog(string $action, ?string $_board = null): void {
|
|||||||
$query->bindValue(':board', null, PDO::PARAM_NULL);
|
$query->bindValue(':board', null, PDO::PARAM_NULL);
|
||||||
$query->execute() or error(db_error($query));
|
$query->execute() or error(db_error($query));
|
||||||
|
|
||||||
if ($config['syslog']) {
|
if ($config['syslog'])
|
||||||
_syslog(LOG_INFO, '[mod/' . $mod['username'] . ']: ' . $action);
|
_syslog(LOG_INFO, '[mod/' . $mod['username'] . ']: ' . $action);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function create_pm_header() {
|
function create_pm_header() {
|
||||||
global $mod, $config;
|
global $mod, $config;
|
||||||
|
|
||||||
if ($config['cache']['enabled'] && ($header = cache::get('pm_unread_' . $mod['id'])) != false) {
|
if ($config['cache']['enabled'] && ($header = cache::get('pm_unread_' . $mod['id'])) != false) {
|
||||||
if ($header === true) {
|
if ($header === true)
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
return $header;
|
return $header;
|
||||||
}
|
}
|
||||||
@ -211,45 +166,35 @@ function create_pm_header() {
|
|||||||
$query->bindValue(':id', $mod['id'], PDO::PARAM_INT);
|
$query->bindValue(':id', $mod['id'], PDO::PARAM_INT);
|
||||||
$query->execute() or error(db_error($query));
|
$query->execute() or error(db_error($query));
|
||||||
|
|
||||||
if ($pm = $query->fetch(PDO::FETCH_ASSOC)) {
|
if ($pm = $query->fetch(PDO::FETCH_ASSOC))
|
||||||
$header = [ 'id' => $pm['id'], 'waiting' => $query->rowCount() - 1 ];
|
$header = array('id' => $pm['id'], 'waiting' => $query->rowCount() - 1);
|
||||||
} else {
|
else
|
||||||
$header = true;
|
$header = true;
|
||||||
}
|
|
||||||
|
|
||||||
if ($config['cache']['enabled']) {
|
if ($config['cache']['enabled'])
|
||||||
cache::set('pm_unread_' . $mod['id'], $header);
|
cache::set('pm_unread_' . $mod['id'], $header);
|
||||||
}
|
|
||||||
|
|
||||||
if ($header === true) {
|
if ($header === true)
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
return $header;
|
return $header;
|
||||||
}
|
}
|
||||||
|
|
||||||
function make_secure_link_token(string $uri): string {
|
function make_secure_link_token($uri) {
|
||||||
global $mod, $config;
|
global $mod, $config;
|
||||||
return substr(sha1($config['cookies']['salt'] . '-' . $uri . '-' . $mod['id']), 0, 8);
|
return substr(sha1($config['cookies']['salt'] . '-' . $uri . '-' . $mod['id']), 0, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
function check_login(Context $ctx, bool $prompt = false): void {
|
function check_login($prompt = false) {
|
||||||
global $config, $mod;
|
global $config, $mod;
|
||||||
|
|
||||||
$is_https = Net\is_connection_secure($config['cookies']['secure_login_only'] === 1);
|
|
||||||
$is_path_jailed = $config['cookies']['jail'];
|
|
||||||
$expected_cookie_name = calc_cookie_name($is_https, $is_path_jailed, $config['cookies']['mod']);
|
|
||||||
|
|
||||||
// Validate session
|
// Validate session
|
||||||
if (isset($_COOKIE[$expected_cookie_name])) {
|
if (isset($_COOKIE[$config['cookies']['mod']])) {
|
||||||
// Should be username:hash:salt
|
// Should be username:hash:salt
|
||||||
$cookie = explode(':', $_COOKIE[$expected_cookie_name]);
|
$cookie = explode(':', $_COOKIE[$config['cookies']['mod']]);
|
||||||
if (count($cookie) != 3) {
|
if (count($cookie) != 3) {
|
||||||
// Malformed cookies
|
// Malformed cookies
|
||||||
destroyCookies();
|
destroyCookies();
|
||||||
if ($prompt) {
|
if ($prompt) mod_login();
|
||||||
mod_login($ctx);
|
|
||||||
}
|
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,9 +207,7 @@ function check_login(Context $ctx, bool $prompt = false): void {
|
|||||||
if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) {
|
if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) {
|
||||||
// Malformed cookies
|
// Malformed cookies
|
||||||
destroyCookies();
|
destroyCookies();
|
||||||
if ($prompt) {
|
if ($prompt) mod_login();
|
||||||
mod_login($ctx);
|
|
||||||
}
|
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
10
inc/mod/ban.php
Normal file
10
inc/mod/ban.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2013 Tinyboard Development Group
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('TINYBOARD') or exit;
|
||||||
|
|
||||||
|
|
||||||
|
// This file is no longer used.
|
@ -65,10 +65,6 @@ function config_vars() {
|
|||||||
$temp_comment = false;
|
$temp_comment = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preg_match('!^\s*\$config\[(\'log_system\'|\'captcha\')\]!', $line)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preg_match('!^\s*// ([^$].*)$!', $line, $matches)) {
|
if (preg_match('!^\s*// ([^$].*)$!', $line, $matches)) {
|
||||||
if ($var['default'] !== false) {
|
if ($var['default'] !== false) {
|
||||||
$line = '';
|
$line = '';
|
||||||
|
File diff suppressed because it is too large
Load Diff
28
inc/polyfill.php
Normal file
28
inc/polyfill.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// PHP 5.4
|
||||||
|
|
||||||
|
if (!function_exists('hex2bin')) {
|
||||||
|
function hex2bin($data) {
|
||||||
|
return pack("H*" , $hex_string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHP 5.6
|
||||||
|
|
||||||
|
if (!function_exists('hash_equals')) {
|
||||||
|
function hash_equals($ours, $theirs) {
|
||||||
|
$ours = (string)$ours;
|
||||||
|
$theirs = (string)$theirs;
|
||||||
|
|
||||||
|
$tlen = strlen($theirs);
|
||||||
|
$olen = strlen($ours);
|
||||||
|
|
||||||
|
$answer = 0;
|
||||||
|
for ($i = 0; $i < $tlen; $i++) {
|
||||||
|
$answer |= ord($ours[$olen > $i ? $i : 0]) ^ ord($theirs[$i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $answer === 0 && $olen === $tlen;
|
||||||
|
}
|
||||||
|
}
|
@ -1,98 +1,49 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
class Queues {
|
class Queue {
|
||||||
private static $queues = array();
|
function __construct($key) { global $config;
|
||||||
|
if ($config['queue']['enabled'] == 'fs') {
|
||||||
|
$this->lock = new Lock($key);
|
||||||
/**
|
|
||||||
* 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('/', '::', $key);
|
||||||
$key = str_replace("\0", '', $key);
|
$key = str_replace("\0", '', $key);
|
||||||
$key = "tmp/queue/$key/";
|
$this->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 {
|
function push($str) { global $config;
|
||||||
|
if ($config['queue']['enabled'] == 'fs') {
|
||||||
$this->lock->get_ex();
|
$this->lock->get_ex();
|
||||||
$ret = file_put_contents($this->key . microtime(true), $str);
|
file_put_contents($this->key.microtime(true), $str);
|
||||||
$this->lock->free();
|
$this->lock->free();
|
||||||
return $ret !== false;
|
}
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function pop(int $n = 1): array {
|
function pop($n = 1) { global $config;
|
||||||
|
if ($config['queue']['enabled'] == 'fs') {
|
||||||
$this->lock->get_ex();
|
$this->lock->get_ex();
|
||||||
$dir = opendir($this->key);
|
$dir = opendir($this->key);
|
||||||
$paths = array();
|
$paths = array();
|
||||||
|
|
||||||
while ($n > 0) {
|
while ($n > 0) {
|
||||||
$path = readdir($dir);
|
$path = readdir($dir);
|
||||||
if ($path === false) {
|
if ($path === FALSE) break;
|
||||||
break;
|
elseif ($path == '.' || $path == '..') continue;
|
||||||
} elseif ($path == '.' || $path == '..') {
|
else { $paths[] = $path; $n--; }
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
$paths[] = $path;
|
|
||||||
$n--;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$out = array();
|
$out = array();
|
||||||
foreach ($paths as $v) {
|
foreach ($paths as $v) {
|
||||||
$out []= file_get_contents($this->key.$v);
|
$out []= file_get_contents($this->key.$v);
|
||||||
unlink($this->key.$v);
|
unlink($this->key.$v);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->lock->free();
|
$this->lock->free();
|
||||||
return $out;
|
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 {
|
// Don't use the constructor. Use the get_queue function.
|
||||||
// Push a string in the queue.
|
$queues = array();
|
||||||
public function push(string $str): bool;
|
|
||||||
|
|
||||||
// Get a string from the queue.
|
function get_queue($name) { global $queues;
|
||||||
public function pop(int $n = 1): array;
|
return $queues[$name] = isset ($queues[$name]) ? $queues[$name] : new Queue($name);
|
||||||
}
|
}
|
||||||
|
64
inc/remote.php
Normal file
64
inc/remote.php
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2013 Tinyboard Development Group
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('TINYBOARD') or exit;
|
||||||
|
|
||||||
|
class Remote {
|
||||||
|
public function __construct($config) {
|
||||||
|
foreach ($config as $name => $value) {
|
||||||
|
$this->{$name} = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$methods = array();
|
||||||
|
|
||||||
|
if (!isset($this->auth['method']))
|
||||||
|
error('Unspecified authentication method.');
|
||||||
|
|
||||||
|
// Connect
|
||||||
|
$this->connection = ssh2_connect($this->host, isset($this->port) ? $this->port : 22, $methods);
|
||||||
|
|
||||||
|
switch ($this->auth['method']) {
|
||||||
|
case 'pubkey':
|
||||||
|
|
||||||
|
if (!isset($this->auth['public']))
|
||||||
|
error('Public key filename not specified.');
|
||||||
|
if (!isset($this->auth['private']))
|
||||||
|
error('Private key filename not specified.');
|
||||||
|
|
||||||
|
if (!ssh2_auth_pubkey_file($this->connection, $this->auth['username'], $this->auth['public'], $this->auth['private'], isset($this->auth['passphrase']) ? $this->auth['passphrase']: null))
|
||||||
|
error('Public key authentication failed.');
|
||||||
|
break;
|
||||||
|
case 'plain':
|
||||||
|
if (!ssh2_auth_password($this->connection, $this->auth['username'], $this->auth['password']))
|
||||||
|
error('Plain-text authentication failed.');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
error('Unknown authentication method: "' . $this->auth['method'] . '".');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function write($data, $remote_path) {
|
||||||
|
global $config;
|
||||||
|
|
||||||
|
switch ($this->type) {
|
||||||
|
case 'sftp':
|
||||||
|
$sftp = ssh2_sftp($this->connection);
|
||||||
|
file_write('ssh2.sftp://' . $sftp . $remote_path, $data, true);
|
||||||
|
break;
|
||||||
|
case 'scp':
|
||||||
|
$file = tempnam($config['tmp'], 'tinyboard-scp');
|
||||||
|
// Write to temp file
|
||||||
|
file_write($file, $data);
|
||||||
|
|
||||||
|
ssh2_scp_send($this->connection, $file, $remote_path, 0755);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
error('Unknown send method.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1 +0,0 @@
|
|||||||
<?php
|
|
@ -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';
|
|
||||||
}
|
|
||||||
}
|
|
184
inc/template.php
184
inc/template.php
@ -10,22 +10,16 @@ $twig = false;
|
|||||||
|
|
||||||
function load_twig() {
|
function load_twig() {
|
||||||
global $twig, $config;
|
global $twig, $config;
|
||||||
|
$loader = new Twig_Loader_Filesystem($config['dir']['template']);
|
||||||
$cache_dir = "{$config['dir']['template']}/cache/";
|
|
||||||
|
|
||||||
$loader = new Twig\Loader\FilesystemLoader($config['dir']['template']);
|
|
||||||
$loader->setPaths($config['dir']['template']);
|
$loader->setPaths($config['dir']['template']);
|
||||||
$twig = new Twig\Environment($loader, array(
|
$twig = new Twig_Environment($loader, array(
|
||||||
'autoescape' => false,
|
'autoescape' => false,
|
||||||
'cache' => is_writable('templates/') || (is_dir($cache_dir) && is_writable($cache_dir)) ?
|
'cache' => is_writable('templates') || (is_dir('templates/cache') && is_writable('templates/cache')) ?
|
||||||
new TinyboardTwigCache($cache_dir) : false,
|
"{$config['dir']['template']}/cache" : false,
|
||||||
'debug' => $config['debug'],
|
'debug' => $config['debug']
|
||||||
'auto_reload' => $config['twig_auto_reload']
|
|
||||||
));
|
));
|
||||||
if ($config['debug'])
|
$twig->addExtension(new Twig_Extensions_Extension_Tinyboard());
|
||||||
$twig->addExtension(new \Twig\Extension\DebugExtension());
|
$twig->addExtension(new Twig_Extensions_Extension_I18n());
|
||||||
$twig->addExtension(new Tinyboard());
|
|
||||||
$twig->addExtension(new PhpMyAdmin\Twig\Extensions\I18nExtension());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Element($templateFile, array $options) {
|
function Element($templateFile, array $options) {
|
||||||
@ -34,6 +28,10 @@ function Element($templateFile, array $options) {
|
|||||||
if (!$twig)
|
if (!$twig)
|
||||||
load_twig();
|
load_twig();
|
||||||
|
|
||||||
|
if (function_exists('create_pm_header') && ((isset($options['mod']) && $options['mod']) || isset($options['__mod'])) && !preg_match('!^mod/!', $templateFile)) {
|
||||||
|
$options['pm'] = create_pm_header();
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($options['body']) && $config['debug']) {
|
if (isset($options['body']) && $config['debug']) {
|
||||||
$_debug = $debug;
|
$_debug = $debug;
|
||||||
|
|
||||||
@ -56,7 +54,7 @@ function Element($templateFile, array $options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read the template file
|
// Read the template file
|
||||||
if (@file_get_contents("{$config['dir']['template']}/{$templateFile}")) {
|
if (@file_get_contents("{$config['dir']['template']}/${templateFile}")) {
|
||||||
$body = $twig->render($templateFile, $options);
|
$body = $twig->render($templateFile, $options);
|
||||||
|
|
||||||
if ($config['minify_html'] && preg_match('/\.html$/', $templateFile)) {
|
if ($config['minify_html'] && preg_match('/\.html$/', $templateFile)) {
|
||||||
@ -65,163 +63,7 @@ function Element($templateFile, array $options) {
|
|||||||
|
|
||||||
return $body;
|
return $body;
|
||||||
} else {
|
} else {
|
||||||
throw new Exception("Template file '{$templateFile}' does not exist or is empty in '{$config['dir']['template']}'!");
|
throw new Exception("Template file '${templateFile}' does not exist or is empty in '{$config['dir']['template']}'!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TinyboardTwigCache extends Twig\Cache\FilesystemCache {
|
|
||||||
private string $directory;
|
|
||||||
|
|
||||||
public function __construct(string $directory) {
|
|
||||||
parent::__construct($directory);
|
|
||||||
$this->directory = $directory;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function was removed in Twig 2.x due to developer views on the Twig library.
|
|
||||||
* Who says we can't keep it for ourselves though?
|
|
||||||
*/
|
|
||||||
public function clear() {
|
|
||||||
$iter = new RecursiveIteratorIterator(
|
|
||||||
new RecursiveDirectoryIterator($this->directory),
|
|
||||||
RecursiveIteratorIterator::LEAVES_ONLY
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($iter as $file) {
|
|
||||||
if ($file->isFile()) {
|
|
||||||
@unlink($file->getPathname());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
62
install.php
62
install.php
@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
// Installation/upgrade file
|
// Installation/upgrade file
|
||||||
define('VERSION', '5.2.1');
|
define('VERSION', '5.1.4');
|
||||||
require 'inc/bootstrap.php';
|
require 'inc/bootstrap.php';
|
||||||
loadConfig();
|
loadConfig();
|
||||||
|
|
||||||
@ -142,7 +142,7 @@ if (file_exists($config['has_installed'])) {
|
|||||||
query(sprintf("ALTER TABLE `posts_%s` CHANGE `subject` `subject` VARCHAR( 100 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL", $_board['uri'])) or error(db_error());
|
query(sprintf("ALTER TABLE `posts_%s` CHANGE `subject` `subject` VARCHAR( 100 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL", $_board['uri'])) or error(db_error());
|
||||||
}
|
}
|
||||||
case 'v0.9.3-dev-6':
|
case 'v0.9.3-dev-6':
|
||||||
// change to InnoDB
|
// change to MyISAM
|
||||||
$tables = array(
|
$tables = array(
|
||||||
'bans', 'boards', 'ip_notes', 'modlogs', 'mods', 'mutes', 'noticeboard', 'pms', 'reports', 'robot', 'theme_settings', 'news'
|
'bans', 'boards', 'ip_notes', 'modlogs', 'mods', 'mutes', 'noticeboard', 'pms', 'reports', 'robot', 'theme_settings', 'news'
|
||||||
);
|
);
|
||||||
@ -151,7 +151,7 @@ if (file_exists($config['has_installed'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($tables as &$table) {
|
foreach ($tables as &$table) {
|
||||||
query("ALTER TABLE `{$table}` ENGINE = INNODB DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci") or error(db_error());
|
query("ALTER TABLE `{$table}` ENGINE = MYISAM DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci") or error(db_error());
|
||||||
}
|
}
|
||||||
case 'v0.9.3-dev-7':
|
case 'v0.9.3-dev-7':
|
||||||
foreach ($boards as &$board) {
|
foreach ($boards as &$board) {
|
||||||
@ -212,7 +212,7 @@ if (file_exists($config['has_installed'])) {
|
|||||||
foreach ($boards as &$board) {
|
foreach ($boards as &$board) {
|
||||||
query(sprintf("ALTER TABLE `posts_%s` ADD `body_nomarkup` TEXT NULL AFTER `body`", $board['uri'])) or error(db_error());
|
query(sprintf("ALTER TABLE `posts_%s` ADD `body_nomarkup` TEXT NULL AFTER `body`", $board['uri'])) or error(db_error());
|
||||||
}
|
}
|
||||||
query("CREATE TABLE IF NOT EXISTS `cites` ( `board` varchar(8) NOT NULL, `post` int(11) NOT NULL, `target_board` varchar(8) NOT NULL, `target` int(11) NOT NULL, KEY `target` (`target_board`,`target`), KEY `post` (`board`,`post`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;") or error(db_error());
|
query("CREATE TABLE IF NOT EXISTS `cites` ( `board` varchar(8) NOT NULL, `post` int(11) NOT NULL, `target_board` varchar(8) NOT NULL, `target` int(11) NOT NULL, KEY `target` (`target_board`,`target`), KEY `post` (`board`,`post`)) ENGINE=MyISAM DEFAULT CHARSET=utf8;") or error(db_error());
|
||||||
case 'v0.9.5-dev-2':
|
case 'v0.9.5-dev-2':
|
||||||
query("ALTER TABLE `boards`
|
query("ALTER TABLE `boards`
|
||||||
CHANGE `uri` `uri` VARCHAR( 15 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
|
CHANGE `uri` `uri` VARCHAR( 15 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
|
||||||
@ -235,7 +235,7 @@ if (file_exists($config['has_installed'])) {
|
|||||||
`passed` smallint(6) NOT NULL,
|
`passed` smallint(6) NOT NULL,
|
||||||
PRIMARY KEY (`hash`),
|
PRIMARY KEY (`hash`),
|
||||||
KEY `board` (`board`,`thread`)
|
KEY `board` (`board`,`thread`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;") or error(db_error());
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8;") or error(db_error());
|
||||||
case 'v0.9.6-dev-2':
|
case 'v0.9.6-dev-2':
|
||||||
query("ALTER TABLE `boards`
|
query("ALTER TABLE `boards`
|
||||||
DROP `id`,
|
DROP `id`,
|
||||||
@ -462,7 +462,7 @@ if (file_exists($config['has_installed'])) {
|
|||||||
KEY `posthash` (`posthash`),
|
KEY `posthash` (`posthash`),
|
||||||
KEY `filehash` (`filehash`),
|
KEY `filehash` (`filehash`),
|
||||||
KEY `time` (`time`)
|
KEY `time` (`time`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=ascii COLLATE=ascii_bin AUTO_INCREMENT=1 ;") or error(db_error());
|
) ENGINE=MyISAM DEFAULT CHARSET=ascii COLLATE=ascii_bin AUTO_INCREMENT=1 ;") or error(db_error());
|
||||||
case 'v0.9.6-dev-19':
|
case 'v0.9.6-dev-19':
|
||||||
query("UPDATE ``mods`` SET `type` = 10 WHERE `type` = 0") or error(db_error());
|
query("UPDATE ``mods`` SET `type` = 10 WHERE `type` = 0") or error(db_error());
|
||||||
query("UPDATE ``mods`` SET `type` = 20 WHERE `type` = 1") or error(db_error());
|
query("UPDATE ``mods`` SET `type` = 20 WHERE `type` = 1") or error(db_error());
|
||||||
@ -483,7 +483,7 @@ if (file_exists($config['has_installed'])) {
|
|||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `expires` (`expires`),
|
KEY `expires` (`expires`),
|
||||||
KEY `ipstart` (`ipstart`,`ipend`)
|
KEY `ipstart` (`ipstart`,`ipend`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1") or error(db_error());
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1") or error(db_error());
|
||||||
$listquery = query("SELECT * FROM ``bans`` ORDER BY `id`") or error(db_error());
|
$listquery = query("SELECT * FROM ``bans`` ORDER BY `id`") or error(db_error());
|
||||||
while ($ban = $listquery->fetch(PDO::FETCH_ASSOC)) {
|
while ($ban = $listquery->fetch(PDO::FETCH_ASSOC)) {
|
||||||
$query = prepare("INSERT INTO ``bans_new_temp`` VALUES
|
$query = prepare("INSERT INTO ``bans_new_temp`` VALUES
|
||||||
@ -538,7 +538,7 @@ if (file_exists($config['has_installed'])) {
|
|||||||
`denied` tinyint(1) NOT NULL,
|
`denied` tinyint(1) NOT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `ban_id` (`ban_id`)
|
KEY `ban_id` (`ban_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;") or error(db_error());
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;") or error(db_error());
|
||||||
case 'v0.9.6-dev-22':
|
case 'v0.9.6-dev-22':
|
||||||
case 'v0.9.6-dev-22 + <a href="https://int.vichan.net/devel/">vichan-devel-4.4.91</a>':
|
case 'v0.9.6-dev-22 + <a href="https://int.vichan.net/devel/">vichan-devel-4.4.91</a>':
|
||||||
case 'v0.9.6-dev-22 + <a href="https://int.vichan.net/devel/">vichan-devel-4.4.92</a>':
|
case 'v0.9.6-dev-22 + <a href="https://int.vichan.net/devel/">vichan-devel-4.4.92</a>':
|
||||||
@ -634,10 +634,10 @@ if (file_exists($config['has_installed'])) {
|
|||||||
`text` varchar(255),
|
`text` varchar(255),
|
||||||
`created_at` int(11),
|
`created_at` int(11),
|
||||||
PRIMARY KEY (`cookie`,`extra`)
|
PRIMARY KEY (`cookie`,`extra`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;') or error(db_error());
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;') or error(db_error());
|
||||||
case false:
|
case false:
|
||||||
// TODO: enhance Tinyboard -> vichan upgrade path.
|
// TODO: enhance Tinyboard -> vichan upgrade path.
|
||||||
query("CREATE TABLE IF NOT EXISTS ``search_queries`` ( `ip` varchar(39) NOT NULL, `time` int(11) NOT NULL, `query` text NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;") or error(db_error());
|
query("CREATE TABLE IF NOT EXISTS ``search_queries`` ( `ip` varchar(39) NOT NULL, `time` int(11) NOT NULL, `query` text NOT NULL) ENGINE=MyISAM DEFAULT CHARSET=utf8;") or error(db_error());
|
||||||
|
|
||||||
// Update version number
|
// Update version number
|
||||||
file_write($config['has_installed'], VERSION);
|
file_write($config['has_installed'], VERSION);
|
||||||
@ -689,8 +689,6 @@ if ($step == 0) {
|
|||||||
|
|
||||||
echo Element('page.html', $page);
|
echo Element('page.html', $page);
|
||||||
} elseif ($step == 1) {
|
} elseif ($step == 1) {
|
||||||
// The HTTPS check doesn't work properly when in those arrays, so let's run it here and pass along the result during the actual check.
|
|
||||||
$httpsvalue = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
|
|
||||||
$page['title'] = 'Pre-installation test';
|
$page['title'] = 'Pre-installation test';
|
||||||
|
|
||||||
$can_exec = true;
|
$can_exec = true;
|
||||||
@ -731,10 +729,17 @@ if ($step == 0) {
|
|||||||
$tests = array(
|
$tests = array(
|
||||||
array(
|
array(
|
||||||
'category' => 'PHP',
|
'category' => 'PHP',
|
||||||
'name' => 'PHP ≥ 7.4',
|
'name' => 'PHP ≥ 5.4',
|
||||||
'result' => PHP_VERSION_ID >= 50400,
|
'result' => PHP_VERSION_ID >= 50400,
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'message' => 'vichan requires PHP 7.4 or better.',
|
'message' => 'vichan requires PHP 5.4 or better.',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'category' => 'PHP',
|
||||||
|
'name' => 'PHP ≥ 5.6',
|
||||||
|
'result' => PHP_VERSION_ID >= 50600,
|
||||||
|
'required' => false,
|
||||||
|
'message' => 'vichan works best on PHP 5.6 or better.',
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
'category' => 'PHP',
|
'category' => 'PHP',
|
||||||
@ -851,14 +856,14 @@ if ($step == 0) {
|
|||||||
array(
|
array(
|
||||||
'category' => 'File permissions',
|
'category' => 'File permissions',
|
||||||
'name' => getcwd() . '/templates/cache',
|
'name' => getcwd() . '/templates/cache',
|
||||||
'result' => is_dir('templates/cache/') && is_writable('templates/cache/'),
|
'result' => is_writable('templates') || (is_dir('templates/cache') && is_writable('templates/cache')),
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'message' => 'You must give vichan permission to create (and write to) the <code>templates/cache</code> directory or performance will be drastically reduced.'
|
'message' => 'You must give vichan permission to create (and write to) the <code>templates/cache</code> directory or performance will be drastically reduced.'
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
'category' => 'File permissions',
|
'category' => 'File permissions',
|
||||||
'name' => getcwd() . '/tmp/cache',
|
'name' => getcwd() . '/tmp/cache',
|
||||||
'result' => is_dir('tmp/cache/') && is_writable('tmp/cache/'),
|
'result' => is_dir('tmp/cache') && is_writable('tmp/cache'),
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'message' => 'You must give vichan permission to write to the <code>tmp/cache</code> directory.'
|
'message' => 'You must give vichan permission to write to the <code>tmp/cache</code> directory.'
|
||||||
),
|
),
|
||||||
@ -871,17 +876,12 @@ if ($step == 0) {
|
|||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
'category' => 'Misc',
|
'category' => 'Misc',
|
||||||
'name' => 'HTTPS being used',
|
'name' => 'Caching available (APC(u), XCache, Memcached or Redis)',
|
||||||
'result' => $httpsvalue,
|
'result' => extension_loaded('apcu') || extension_loaded('apc') ||
|
||||||
|
extension_loaded('xcache') || extension_loaded('memcached') ||
|
||||||
|
extension_loaded('redis'),
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'message' => 'You are not currently using https for vichan, or at least for your backend server. If this intentional, add "$config[\'cookies\'][\'secure_login_only\'] = 0;" (or 1 if using a proxy) on a new line under "Additional configuration" on the next page.'
|
'message' => 'You will not be able to enable the additional caching system, designed to minimize SQL queries and significantly improve performance. <a href="http://php.net/manual/en/book.apc.php">APC</a> is the recommended method of caching, but <a href="http://xcache.lighttpd.net/">XCache</a>, <a href="http://www.php.net/manual/en/intro.memcached.php">Memcached</a> and <a href="http://pecl.php.net/package/redis">Redis</a> are also supported.'
|
||||||
),
|
|
||||||
array(
|
|
||||||
'category' => 'Misc',
|
|
||||||
'name' => 'Caching available (APCu, Memcached or Redis)',
|
|
||||||
'result' => extension_loaded('apcu') || extension_loaded('memcached') || extension_loaded('redis'),
|
|
||||||
'required' => false,
|
|
||||||
'message' => 'You will not be able to enable the additional caching system, designed to minimize SQL queries and significantly improve performance. <a href="https://www.php.net/manual/en/book.apcu.php">APCu</a> is the recommended method of caching, but <a href="http://www.php.net/manual/en/intro.memcached.php">Memcached</a> and <a href="http://pecl.php.net/package/redis">Redis</a> are also supported.'
|
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
'category' => 'Misc',
|
'category' => 'Misc',
|
||||||
@ -921,7 +921,6 @@ if ($step == 0) {
|
|||||||
$sg = new SaltGen();
|
$sg = new SaltGen();
|
||||||
$config['cookies']['salt'] = $sg->generate();
|
$config['cookies']['salt'] = $sg->generate();
|
||||||
$config['secure_trip_salt'] = $sg->generate();
|
$config['secure_trip_salt'] = $sg->generate();
|
||||||
$config['secure_password_salt'] = $sg->generate();
|
|
||||||
|
|
||||||
echo Element('page.html', array(
|
echo Element('page.html', array(
|
||||||
'body' => Element('installer/config.html', array(
|
'body' => Element('installer/config.html', array(
|
||||||
@ -991,16 +990,12 @@ if ($step == 0) {
|
|||||||
$queries[] = Element('posts.sql', array('board' => 'b'));
|
$queries[] = Element('posts.sql', array('board' => 'b'));
|
||||||
|
|
||||||
$sql_errors = '';
|
$sql_errors = '';
|
||||||
$sql_err_count = 0;
|
|
||||||
foreach ($queries as $query) {
|
foreach ($queries as $query) {
|
||||||
if ($mysql_version < 50503)
|
if ($mysql_version < 50503)
|
||||||
$query = preg_replace('/(CHARSET=|CHARACTER SET )utf8mb4/', '$1utf8', $query);
|
$query = preg_replace('/(CHARSET=|CHARACTER SET )utf8mb4/', '$1utf8', $query);
|
||||||
$query = preg_replace('/^([\w\s]*)`([0-9a-zA-Z$_\x{0080}-\x{FFFF}]+)`/u', '$1``$2``', $query);
|
$query = preg_replace('/^([\w\s]*)`([0-9a-zA-Z$_\x{0080}-\x{FFFF}]+)`/u', '$1``$2``', $query);
|
||||||
if (!query($query)) {
|
if (!query($query))
|
||||||
$sql_err_count++;
|
$sql_errors .= '<li>' . db_error() . '</li>';
|
||||||
$error = db_error();
|
|
||||||
$sql_errors .= "<li>$sql_err_count<ul><li>$query</li><li>$error</li></ul></li>";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$page['title'] = 'Installation complete';
|
$page['title'] = 'Installation complete';
|
||||||
@ -1039,3 +1034,4 @@ if ($step == 0) {
|
|||||||
|
|
||||||
echo Element('page.html', $page);
|
echo Element('page.html', $page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
58
install.sql
58
install.sql
@ -31,7 +31,7 @@ CREATE TABLE IF NOT EXISTS `antispam` (
|
|||||||
PRIMARY KEY (`hash`),
|
PRIMARY KEY (`hash`),
|
||||||
KEY `board` (`board`,`thread`),
|
KEY `board` (`board`,`thread`),
|
||||||
KEY `expires` (`expires`)
|
KEY `expires` (`expires`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=ascii COLLATE=ascii_bin;
|
) ENGINE=MyISAM DEFAULT CHARSET=ascii COLLATE=ascii_bin;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ CREATE TABLE IF NOT EXISTS `bans` (
|
|||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `expires` (`expires`),
|
KEY `expires` (`expires`),
|
||||||
KEY `ipstart` (`ipstart`,`ipend`)
|
KEY `ipstart` (`ipstart`,`ipend`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ CREATE TABLE IF NOT EXISTS `boards` (
|
|||||||
`subtitle` tinytext,
|
`subtitle` tinytext,
|
||||||
-- `indexed` boolean default true,
|
-- `indexed` boolean default true,
|
||||||
PRIMARY KEY (`uri`)
|
PRIMARY KEY (`uri`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Dumping data for table `boards`
|
-- Dumping data for table `boards`
|
||||||
@ -89,7 +89,7 @@ CREATE TABLE IF NOT EXISTS `cites` (
|
|||||||
`target` int(11) NOT NULL,
|
`target` int(11) NOT NULL,
|
||||||
KEY `target` (`target_board`,`target`),
|
KEY `target` (`target_board`,`target`),
|
||||||
KEY `post` (`board`,`post`)
|
KEY `post` (`board`,`post`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@ -105,7 +105,7 @@ CREATE TABLE IF NOT EXISTS `ip_notes` (
|
|||||||
`body` text NOT NULL,
|
`body` text NOT NULL,
|
||||||
UNIQUE KEY `id` (`id`),
|
UNIQUE KEY `id` (`id`),
|
||||||
KEY `ip_lookup` (`ip`, `time`)
|
KEY `ip_lookup` (`ip`, `time`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@ -121,7 +121,7 @@ CREATE TABLE IF NOT EXISTS `modlogs` (
|
|||||||
`text` text NOT NULL,
|
`text` text NOT NULL,
|
||||||
KEY `time` (`time`),
|
KEY `time` (`time`),
|
||||||
KEY `mod`(`mod`)
|
KEY `mod`(`mod`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@ -138,7 +138,7 @@ CREATE TABLE IF NOT EXISTS `mods` (
|
|||||||
`boards` text CHARACTER SET utf8 NOT NULL,
|
`boards` text CHARACTER SET utf8 NOT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `id` (`id`,`username`)
|
UNIQUE KEY `id` (`id`,`username`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Dumping data for table `mods`
|
-- Dumping data for table `mods`
|
||||||
@ -157,7 +157,7 @@ CREATE TABLE IF NOT EXISTS `mutes` (
|
|||||||
`ip` varchar(39) NOT NULL,
|
`ip` varchar(39) NOT NULL,
|
||||||
`time` int(11) NOT NULL,
|
`time` int(11) NOT NULL,
|
||||||
KEY `ip` (`ip`)
|
KEY `ip` (`ip`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=ascii;
|
) ENGINE=MyISAM DEFAULT CHARSET=ascii;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@ -173,7 +173,7 @@ CREATE TABLE IF NOT EXISTS `news` (
|
|||||||
`body` text NOT NULL,
|
`body` text NOT NULL,
|
||||||
UNIQUE KEY `id` (`id`),
|
UNIQUE KEY `id` (`id`),
|
||||||
KEY `time` (`time`)
|
KEY `time` (`time`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@ -189,7 +189,7 @@ CREATE TABLE IF NOT EXISTS `noticeboard` (
|
|||||||
`body` text NOT NULL,
|
`body` text NOT NULL,
|
||||||
UNIQUE KEY `id` (`id`),
|
UNIQUE KEY `id` (`id`),
|
||||||
KEY `time` (`time`)
|
KEY `time` (`time`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@ -206,7 +206,7 @@ CREATE TABLE IF NOT EXISTS `pms` (
|
|||||||
`unread` tinyint(1) NOT NULL,
|
`unread` tinyint(1) NOT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `to` (`to`, `unread`)
|
KEY `to` (`to`, `unread`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@ -222,7 +222,7 @@ CREATE TABLE IF NOT EXISTS `reports` (
|
|||||||
`post` int(11) NOT NULL,
|
`post` int(11) NOT NULL,
|
||||||
`reason` text NOT NULL,
|
`reason` text NOT NULL,
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@ -233,7 +233,7 @@ CREATE TABLE IF NOT EXISTS `reports` (
|
|||||||
CREATE TABLE IF NOT EXISTS `robot` (
|
CREATE TABLE IF NOT EXISTS `robot` (
|
||||||
`hash` varchar(40) COLLATE ascii_bin NOT NULL COMMENT 'SHA1',
|
`hash` varchar(40) COLLATE ascii_bin NOT NULL COMMENT 'SHA1',
|
||||||
PRIMARY KEY (`hash`)
|
PRIMARY KEY (`hash`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=ascii COLLATE=ascii_bin;
|
) ENGINE=MyISAM DEFAULT CHARSET=ascii COLLATE=ascii_bin;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@ -245,7 +245,7 @@ CREATE TABLE IF NOT EXISTS `search_queries` (
|
|||||||
`ip` varchar(39) NOT NULL,
|
`ip` varchar(39) NOT NULL,
|
||||||
`time` int(11) NOT NULL,
|
`time` int(11) NOT NULL,
|
||||||
`query` text NOT NULL
|
`query` text NOT NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@ -258,7 +258,7 @@ CREATE TABLE IF NOT EXISTS `theme_settings` (
|
|||||||
`name` varchar(40) DEFAULT NULL,
|
`name` varchar(40) DEFAULT NULL,
|
||||||
`value` text,
|
`value` text,
|
||||||
KEY `theme` (`theme`)
|
KEY `theme` (`theme`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@ -279,7 +279,7 @@ CREATE TABLE IF NOT EXISTS `flood` (
|
|||||||
KEY `posthash` (`posthash`),
|
KEY `posthash` (`posthash`),
|
||||||
KEY `filehash` (`filehash`),
|
KEY `filehash` (`filehash`),
|
||||||
KEY `time` (`time`)
|
KEY `time` (`time`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=ascii COLLATE=ascii_bin AUTO_INCREMENT=1 ;
|
) ENGINE=MyISAM DEFAULT CHARSET=ascii COLLATE=ascii_bin AUTO_INCREMENT=1 ;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@ -294,9 +294,8 @@ CREATE TABLE IF NOT EXISTS `ban_appeals` (
|
|||||||
`message` text NOT NULL,
|
`message` text NOT NULL,
|
||||||
`denied` tinyint(1) NOT NULL,
|
`denied` tinyint(1) NOT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `ban_id` (`ban_id`),
|
KEY `ban_id` (`ban_id`)
|
||||||
CONSTRAINT `fk_ban_id` FOREIGN KEY (`ban_id`) REFERENCES `bans`(`id`) ON DELETE CASCADE
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@ -313,7 +312,7 @@ CREATE TABLE IF NOT EXISTS `pages` (
|
|||||||
`content` text,
|
`content` text,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `u_pages` (`name`,`board`)
|
UNIQUE KEY `u_pages` (`name`,`board`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@ -331,7 +330,7 @@ CREATE TABLE IF NOT EXISTS `nntp_references` (
|
|||||||
PRIMARY KEY (`message_id_digest`),
|
PRIMARY KEY (`message_id_digest`),
|
||||||
UNIQUE KEY `message_id` (`message_id`),
|
UNIQUE KEY `message_id` (`message_id`),
|
||||||
UNIQUE KEY `u_board_id` (`board`, `id`)
|
UNIQUE KEY `u_board_id` (`board`, `id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@ -345,8 +344,23 @@ CREATE TABLE IF NOT EXISTS `captchas` (
|
|||||||
`text` VARCHAR(255),
|
`text` VARCHAR(255),
|
||||||
`created_at` INT(11),
|
`created_at` INT(11),
|
||||||
PRIMARY KEY (`cookie`,`extra`)
|
PRIMARY KEY (`cookie`,`extra`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `telegrams`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `telegrams` (
|
||||||
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`mod_id` int(11) unsigned NOT NULL,
|
||||||
|
`ip` varchar(39) CHARACTER SET ascii NOT NULL,
|
||||||
|
`message` text NOT NULL,
|
||||||
|
`seen` tinyint(1) NOT NULL DEFAULT FALSE,
|
||||||
|
`created_at` INT(11),
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||||
|
@ -15,9 +15,6 @@
|
|||||||
* //$config['additional_javascript'][] = 'js/titlebar-notifications.js';
|
* //$config['additional_javascript'][] = 'js/titlebar-notifications.js';
|
||||||
* $config['additional_javascript'][] = 'js/auto-reload.js';
|
* $config['additional_javascript'][] = 'js/auto-reload.js';
|
||||||
*
|
*
|
||||||
* You must have boardlinks or else this script will not load.
|
|
||||||
* Search for "$config['boards'] = array(" within your inc/config.php and add something similar to your instance-config.php.
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
@ -244,3 +241,4 @@ $(document).ready(function(){
|
|||||||
auto_update(poll_interval_delay);
|
auto_update(poll_interval_delay);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -14,23 +14,28 @@
|
|||||||
|
|
||||||
function catalog() {
|
function catalog() {
|
||||||
var board = $("input[name='board']");
|
var board = $("input[name='board']");
|
||||||
var boardValue = board.first().val();
|
|
||||||
|
|
||||||
var catalog_url = '';
|
var catalog_url = configRoot + board.first().val() + "/catalog.html";
|
||||||
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 pages = document.getElementsByClassName('pages')[0];
|
||||||
var bottom = document.getElementsByClassName('boardlist bottom')[0];
|
var bottom = document.getElementsByClassName('boardlist bottom')[0]
|
||||||
var subtitle = document.getElementsByClassName('subtitle')[0];
|
var subtitle = document.getElementsByClassName('subtitle')[0];
|
||||||
|
|
||||||
var link = document.createElement('a');
|
var link = document.createElement('a');
|
||||||
link.href = catalog_url;
|
link.href = catalog_url;
|
||||||
|
|
||||||
if (!pages) {
|
if (pages) {
|
||||||
|
link.textContent = _('Catalog');
|
||||||
|
link.style.color = '#F10000';
|
||||||
|
link.style.padding = '4px';
|
||||||
|
link.style.paddingLeft = '9px';
|
||||||
|
link.style.borderLeft = '1px solid'
|
||||||
|
link.style.borderLeftColor = '#A8A8A8';
|
||||||
|
link.style.textDecoration = "underline";
|
||||||
|
|
||||||
|
pages.appendChild(link)
|
||||||
|
}
|
||||||
|
else {
|
||||||
link.textContent = '['+_('Catalog')+']';
|
link.textContent = '['+_('Catalog')+']';
|
||||||
link.style.paddingLeft = '10px';
|
link.style.paddingLeft = '10px';
|
||||||
link.style.textDecoration = "underline";
|
link.style.textDecoration = "underline";
|
||||||
|
@ -8,21 +8,21 @@
|
|||||||
* $config['additional_javascript'][] = 'js/comment-toolbar.js';
|
* $config['additional_javascript'][] = 'js/comment-toolbar.js';
|
||||||
*/
|
*/
|
||||||
if (active_page == 'catalog') {
|
if (active_page == 'catalog') {
|
||||||
onReady(function() {
|
onready(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// 'true' = enable shortcuts
|
// 'true' = enable shortcuts
|
||||||
let useKeybinds = true;
|
var useKeybinds = true;
|
||||||
|
|
||||||
// trigger the search 400ms after last keystroke
|
// trigger the search 400ms after last keystroke
|
||||||
let delay = 400;
|
var delay = 400;
|
||||||
let timeoutHandle;
|
var timeoutHandle;
|
||||||
|
|
||||||
//search and hide none matching threads
|
//search and hide none matching threads
|
||||||
function filter(search_term) {
|
function filter(search_term) {
|
||||||
$('.replies').each(function () {
|
$('.replies').each(function () {
|
||||||
let subject = $(this).children('.intro').text().toLowerCase();
|
var subject = $(this).children('.intro').text().toLowerCase();
|
||||||
let comment = $(this).clone().children().remove(':lt(2)').end().text().trim().toLowerCase();
|
var comment = $(this).clone().children().remove(':lt(2)').end().text().trim().toLowerCase();
|
||||||
search_term = search_term.toLowerCase();
|
search_term = search_term.toLowerCase();
|
||||||
|
|
||||||
if (subject.indexOf(search_term) == -1 && comment.indexOf(search_term) == -1) {
|
if (subject.indexOf(search_term) == -1 && comment.indexOf(search_term) == -1) {
|
||||||
@ -34,7 +34,7 @@ if (active_page == 'catalog') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function searchToggle() {
|
function searchToggle() {
|
||||||
let button = $('#catalog_search_button');
|
var button = $('#catalog_search_button');
|
||||||
|
|
||||||
if (!button.data('expanded')) {
|
if (!button.data('expanded')) {
|
||||||
button.data('expanded', '1');
|
button.data('expanded', '1');
|
||||||
|
@ -15,9 +15,9 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
onReady(function() {
|
onready(function(){
|
||||||
let doOriginalFilename = function() {
|
var do_original_filename = function() {
|
||||||
let filename, truncated;
|
var filename, truncated;
|
||||||
if ($(this).attr('title')) {
|
if ($(this).attr('title')) {
|
||||||
filename = $(this).attr('title');
|
filename = $(this).attr('title');
|
||||||
truncated = true;
|
truncated = true;
|
||||||
@ -34,9 +34,9 @@ onReady(function() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
$('.postfilename').each(doOriginalFilename);
|
$('.postfilename').each(do_original_filename);
|
||||||
|
|
||||||
$(document).on('new_post', function(e, post) {
|
$(document).on('new_post', function(e, post) {
|
||||||
$(post).find('.postfilename').each(doOriginalFilename);
|
$(post).find('.postfilename').each(do_original_filename);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -16,26 +16,23 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (active_page == 'ukko' || active_page == 'thread' || active_page == 'index') {
|
if (active_page == 'ukko' || active_page == 'thread' || active_page == 'index')
|
||||||
onReady(function() {
|
onready(function(){
|
||||||
$('hr:first').before('<div id="expand-all-images" style="text-align:right"><a class="unimportant" href="javascript:void(0)"></a></div>');
|
$('hr:first').before('<div id="expand-all-images" style="text-align:right"><a class="unimportant" href="javascript:void(0)"></a></div>');
|
||||||
$('div#expand-all-images a')
|
$('div#expand-all-images a')
|
||||||
.text(_('Expand all images'))
|
.text(_('Expand all images'))
|
||||||
.click(function() {
|
.click(function() {
|
||||||
$('a img.post-image').each(function() {
|
$('a img.post-image').each(function() {
|
||||||
// Don't expand YouTube embeds
|
// Don't expand YouTube embeds
|
||||||
if ($(this).parent().parent().hasClass('video-container')) {
|
if ($(this).parent().parent().hasClass('video-container'))
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
// or WEBM
|
// or WEBM
|
||||||
if (/^\/player\.php\?/.test($(this).parent().attr('href'))) {
|
if (/^\/player\.php\?/.test($(this).parent().attr('href')))
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (!$(this).parent().data('expanded')) {
|
if (!$(this).parent().data('expanded'))
|
||||||
$(this).parent().click();
|
$(this).parent().click();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!$('#shrink-all-images').length) {
|
if (!$('#shrink-all-images').length) {
|
||||||
@ -46,12 +43,10 @@ if (active_page == 'ukko' || active_page == 'thread' || active_page == 'index')
|
|||||||
.text(_('Shrink all images'))
|
.text(_('Shrink all images'))
|
||||||
.click(function(){
|
.click(function(){
|
||||||
$('a img.full-image').each(function() {
|
$('a img.full-image').each(function() {
|
||||||
if ($(this).parent().data('expanded')) {
|
if ($(this).parent().data('expanded'))
|
||||||
$(this).parent().click();
|
$(this).parent().click();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
$(this).parent().remove();
|
$(this).parent().remove();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
@ -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
|
|
||||||
});
|
|
||||||
});
|
|
39
js/expand-too-long.js
Normal file
39
js/expand-too-long.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* expand-too-long.js
|
||||||
|
* https://github.com/vichan-devel/vichan/blob/master/js/expand-too-long.js
|
||||||
|
*
|
||||||
|
* Released under the MIT license
|
||||||
|
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* $config['additional_javascript'][] = 'js/jquery.min.js';
|
||||||
|
* $config['additional_javascript'][] = 'js/expand-too-long.js';
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
var do_expand = function() {
|
||||||
|
$(this).find('a').click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var url = $(this).attr('href');
|
||||||
|
var body = $(this).parents('.body');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
context: document.body,
|
||||||
|
success: function(data) {
|
||||||
|
var content = $(data).find('#'+url.split('#')[1]).parent().parent().find(".body").first().html();
|
||||||
|
|
||||||
|
body.html(content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$('.toolong').each(do_expand);
|
||||||
|
|
||||||
|
$(document).on("new_post", function(e, post) {
|
||||||
|
$(post).find('.toolong').each(do_expand)
|
||||||
|
});
|
||||||
|
});
|
@ -2,32 +2,26 @@
|
|||||||
/* Note: This code expects the global variable configRoot to be set. */
|
/* Note: This code expects the global variable configRoot to be set. */
|
||||||
|
|
||||||
if (typeof _ == 'undefined') {
|
if (typeof _ == 'undefined') {
|
||||||
var _ = function(a) {
|
var _ = function(a) { return a; };
|
||||||
return a;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupVideo(thumb, url) {
|
function setupVideo(thumb, url) {
|
||||||
if (thumb.videoAlreadySetUp) {
|
if (thumb.videoAlreadySetUp) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
thumb.videoAlreadySetUp = true;
|
thumb.videoAlreadySetUp = true;
|
||||||
|
|
||||||
let video = null;
|
var video = null;
|
||||||
let videoContainer, videoHide;
|
var videoContainer, videoHide;
|
||||||
let expanded = false;
|
var expanded = false;
|
||||||
let hovering = false;
|
var hovering = false;
|
||||||
let loop = true;
|
var loop = true;
|
||||||
let loopControls = [document.createElement("span"), document.createElement("span")];
|
var loopControls = [document.createElement("span"), document.createElement("span")];
|
||||||
let fileInfo = thumb.parentNode.querySelector(".fileinfo");
|
var fileInfo = thumb.parentNode.querySelector(".fileinfo");
|
||||||
let mouseDown = false;
|
var mouseDown = false;
|
||||||
|
|
||||||
function unexpand() {
|
function unexpand() {
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
expanded = false;
|
expanded = false;
|
||||||
if (video.pause) {
|
if (video.pause) video.pause();
|
||||||
video.pause();
|
|
||||||
}
|
|
||||||
videoContainer.style.display = "none";
|
videoContainer.style.display = "none";
|
||||||
thumb.style.display = "inline";
|
thumb.style.display = "inline";
|
||||||
video.style.maxWidth = "inherit";
|
video.style.maxWidth = "inherit";
|
||||||
@ -38,9 +32,7 @@ function setupVideo(thumb, url) {
|
|||||||
function unhover() {
|
function unhover() {
|
||||||
if (hovering) {
|
if (hovering) {
|
||||||
hovering = false;
|
hovering = false;
|
||||||
if (video.pause) {
|
if (video.pause) video.pause();
|
||||||
video.pause();
|
|
||||||
}
|
|
||||||
videoContainer.style.display = "none";
|
videoContainer.style.display = "none";
|
||||||
video.style.maxWidth = "inherit";
|
video.style.maxWidth = "inherit";
|
||||||
video.style.maxHeight = "inherit";
|
video.style.maxHeight = "inherit";
|
||||||
@ -89,12 +81,6 @@ function setupVideo(thumb, url) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVolume() {
|
|
||||||
const volume = setting("videovolume");
|
|
||||||
video.volume = volume;
|
|
||||||
video.muted = (volume === 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clicking on thumbnail expands video
|
// Clicking on thumbnail expands video
|
||||||
thumb.addEventListener("click", function(e) {
|
thumb.addEventListener("click", function(e) {
|
||||||
if (setting("videoexpand") && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
if (setting("videoexpand") && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
||||||
@ -111,21 +97,15 @@ function setupVideo(thumb, url) {
|
|||||||
video.parentNode.parentNode.removeAttribute('style');
|
video.parentNode.parentNode.removeAttribute('style');
|
||||||
thumb.style.display = "none";
|
thumb.style.display = "none";
|
||||||
|
|
||||||
setVolume();
|
video.muted = (setting("videovolume") == 0);
|
||||||
|
video.volume = setting("videovolume");
|
||||||
video.controls = true;
|
video.controls = true;
|
||||||
if (video.readyState == 0) {
|
if (video.readyState == 0) {
|
||||||
video.addEventListener("loadedmetadata", expand2, false);
|
video.addEventListener("loadedmetadata", expand2, false);
|
||||||
} else {
|
} else {
|
||||||
setTimeout(expand2, 0);
|
setTimeout(expand2, 0);
|
||||||
}
|
}
|
||||||
let promise = video.play();
|
|
||||||
if (promise !== undefined) {
|
|
||||||
promise.then(_ => {
|
|
||||||
}).catch(_ => {
|
|
||||||
video.muted = true;
|
|
||||||
video.play();
|
video.play();
|
||||||
});
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}, false);
|
}, false);
|
||||||
@ -149,18 +129,16 @@ function setupVideo(thumb, url) {
|
|||||||
expanded = false;
|
expanded = false;
|
||||||
hovering = true;
|
hovering = true;
|
||||||
|
|
||||||
let docRight = document.documentElement.getBoundingClientRect().right;
|
var docRight = document.documentElement.getBoundingClientRect().right;
|
||||||
let thumbRight = thumb.querySelector("img, video").getBoundingClientRect().right;
|
var thumbRight = thumb.querySelector("img, video").getBoundingClientRect().right;
|
||||||
let maxWidth = docRight - thumbRight - 20;
|
var maxWidth = docRight - thumbRight - 20;
|
||||||
if (maxWidth < 250) {
|
if (maxWidth < 250) maxWidth = 250;
|
||||||
maxWidth = 250;
|
|
||||||
}
|
|
||||||
|
|
||||||
video.style.position = "fixed";
|
video.style.position = "fixed";
|
||||||
video.style.right = "0px";
|
video.style.right = "0px";
|
||||||
video.style.top = "0px";
|
video.style.top = "0px";
|
||||||
docRight = document.documentElement.getBoundingClientRect().right;
|
var docRight = document.documentElement.getBoundingClientRect().right;
|
||||||
thumbRight = thumb.querySelector("img, video").getBoundingClientRect().right;
|
var thumbRight = thumb.querySelector("img, video").getBoundingClientRect().right;
|
||||||
video.style.maxWidth = maxWidth + "px";
|
video.style.maxWidth = maxWidth + "px";
|
||||||
video.style.maxHeight = "100%";
|
video.style.maxHeight = "100%";
|
||||||
video.style.pointerEvents = "none";
|
video.style.pointerEvents = "none";
|
||||||
@ -170,16 +148,10 @@ function setupVideo(thumb, url) {
|
|||||||
videoContainer.style.display = "inline";
|
videoContainer.style.display = "inline";
|
||||||
videoContainer.style.position = "fixed";
|
videoContainer.style.position = "fixed";
|
||||||
|
|
||||||
setVolume();
|
video.muted = (setting("videovolume") == 0);
|
||||||
|
video.volume = setting("videovolume");
|
||||||
video.controls = false;
|
video.controls = false;
|
||||||
let promise = video.play();
|
|
||||||
if (promise !== undefined) {
|
|
||||||
promise.then(_ => {
|
|
||||||
}).catch(_ => {
|
|
||||||
video.muted = true;
|
|
||||||
video.play();
|
video.play();
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
@ -189,18 +161,10 @@ function setupVideo(thumb, url) {
|
|||||||
thumb.addEventListener("wheel", function(e) {
|
thumb.addEventListener("wheel", function(e) {
|
||||||
if (setting("videohover")) {
|
if (setting("videohover")) {
|
||||||
var volume = setting("videovolume");
|
var volume = setting("videovolume");
|
||||||
if (e.deltaY > 0) {
|
if (e.deltaY > 0) volume -= 0.1;
|
||||||
volume -= 0.1;
|
if (e.deltaY < 0) volume += 0.1;
|
||||||
}
|
if (volume < 0) volume = 0;
|
||||||
if (e.deltaY < 0) {
|
if (volume > 1) volume = 1;
|
||||||
volume += 0.1;
|
|
||||||
}
|
|
||||||
if (volume < 0) {
|
|
||||||
volume = 0;
|
|
||||||
}
|
|
||||||
if (volume > 1) {
|
|
||||||
volume = 1;
|
|
||||||
}
|
|
||||||
if (video != null) {
|
if (video != null) {
|
||||||
video.muted = (volume == 0);
|
video.muted = (volume == 0);
|
||||||
video.volume = volume;
|
video.volume = volume;
|
||||||
@ -238,41 +202,36 @@ function setupVideo(thumb, url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupVideosIn(element) {
|
function setupVideosIn(element) {
|
||||||
let thumbs = element.querySelectorAll("a.file");
|
var thumbs = element.querySelectorAll("a.file");
|
||||||
for (let i = 0; i < thumbs.length; i++) {
|
for (var i = 0; i < thumbs.length; i++) {
|
||||||
if (/\.webm$|\.mp4$/.test(thumbs[i].pathname)) {
|
if (/\.webm$|\.mp4$/.test(thumbs[i].pathname)) {
|
||||||
setupVideo(thumbs[i], thumbs[i].href);
|
setupVideo(thumbs[i], thumbs[i].href);
|
||||||
} else {
|
} else {
|
||||||
let m = thumbs[i].search.match(/\bv=([^&]*)/);
|
var m = thumbs[i].search.match(/\bv=([^&]*)/);
|
||||||
if (m != null) {
|
if (m != null) {
|
||||||
let url = decodeURIComponent(m[1]);
|
var url = decodeURIComponent(m[1]);
|
||||||
if (/\.webm$|\.mp4$/.test(url)) {
|
if (/\.webm$|\.mp4$/.test(url)) setupVideo(thumbs[i], url);
|
||||||
setupVideo(thumbs[i], url);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onReady(function(){
|
onready(function(){
|
||||||
// Insert menu from settings.js
|
// Insert menu from settings.js
|
||||||
if (typeof settingsMenu != "undefined" && typeof Options == "undefined") {
|
if (typeof settingsMenu != "undefined" && typeof Options == "undefined")
|
||||||
document.body.insertBefore(settingsMenu, document.getElementsByTagName("hr")[0]);
|
document.body.insertBefore(settingsMenu, document.getElementsByTagName("hr")[0]);
|
||||||
}
|
|
||||||
|
|
||||||
// Setup Javascript events for videos in document now
|
// Setup Javascript events for videos in document now
|
||||||
setupVideosIn(document);
|
setupVideosIn(document);
|
||||||
|
|
||||||
// Setup Javascript events for videos added by updater
|
// Setup Javascript events for videos added by updater
|
||||||
if (window.MutationObserver) {
|
if (window.MutationObserver) {
|
||||||
let observer = new MutationObserver(function(mutations) {
|
var observer = new MutationObserver(function(mutations) {
|
||||||
for (let i = 0; i < mutations.length; i++) {
|
for (var i = 0; i < mutations.length; i++) {
|
||||||
let additions = mutations[i].addedNodes;
|
var additions = mutations[i].addedNodes;
|
||||||
if (additions == null) {
|
if (additions == null) continue;
|
||||||
continue;
|
for (var j = 0; j < additions.length; j++) {
|
||||||
}
|
var node = additions[j];
|
||||||
for (let j = 0; j < additions.length; j++) {
|
|
||||||
let node = additions[j];
|
|
||||||
if (node.nodeType == 1) {
|
if (node.nodeType == 1) {
|
||||||
setupVideosIn(node);
|
setupVideosIn(node);
|
||||||
}
|
}
|
||||||
@ -282,3 +241,4 @@ onReady(function(){
|
|||||||
observer.observe(document.body, {childList: true, subtree: true});
|
observer.observe(document.body, {childList: true, subtree: true});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -8,11 +8,6 @@
|
|||||||
*/
|
*/
|
||||||
function init_file_selector(max_images) {
|
function init_file_selector(max_images) {
|
||||||
|
|
||||||
// Temporarily block iOS
|
|
||||||
if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
// add options panel item
|
// add options panel item
|
||||||
if (window.Options && Options.get_tab('general')) {
|
if (window.Options && Options.get_tab('general')) {
|
||||||
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -13,10 +13,10 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
onReady(function() {
|
onready(function(){
|
||||||
let inlineExpandingFilename = function() {
|
var inline_expanding_filename = function() {
|
||||||
$(this).find(".fileinfo > a").click(function(){
|
$(this).find(".fileinfo > a").click(function(){
|
||||||
let imagelink = $(this).parent().parent().find('a[target="_blank"]:first');
|
var imagelink = $(this).parent().parent().find('a[target="_blank"]:first');
|
||||||
if(imagelink.length > 0) {
|
if(imagelink.length > 0) {
|
||||||
imagelink.click();
|
imagelink.click();
|
||||||
return false;
|
return false;
|
||||||
@ -24,10 +24,10 @@ onReady(function() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$('div[id^="thread_"]').each(inlineExpandingFilename);
|
$('div[id^="thread_"]').each(inline_expanding_filename);
|
||||||
|
|
||||||
// allow to work with auto-reload.js, etc.
|
// allow to work with auto-reload.js, etc.
|
||||||
$(document).on('new_post', function(e, post) {
|
$(document).on('new_post', function(e, post) {
|
||||||
inlineExpandingFilename.call(post);
|
inline_expanding_filename.call(post);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -17,45 +17,29 @@ $(document).ready(function(){
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var iso8601 = function(s) {
|
var iso8601 = function(s) {
|
||||||
var parts = s.split('T');
|
s = s.replace(/\.\d\d\d+/,""); // remove milliseconds
|
||||||
if (parts.length === 2) {
|
s = s.replace(/-/,"/").replace(/-/,"/");
|
||||||
var timeParts = parts[1].split(':');
|
s = s.replace(/T/," ").replace(/Z/," UTC");
|
||||||
if (timeParts.length === 3) {
|
s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
|
||||||
var seconds = timeParts[2];
|
|
||||||
if (seconds.length > 2) {
|
|
||||||
seconds = seconds.substr(0, 2) + '.' + seconds.substr(2);
|
|
||||||
}
|
|
||||||
// Ensure seconds part is valid
|
|
||||||
if (parseFloat(seconds) > 59) {
|
|
||||||
seconds = '59';
|
|
||||||
}
|
|
||||||
timeParts[2] = seconds;
|
|
||||||
}
|
|
||||||
parts[1] = timeParts.join(':');
|
|
||||||
}
|
|
||||||
s = parts.join('T');
|
|
||||||
|
|
||||||
if (!s.endsWith('Z')) {
|
|
||||||
s += 'Z';
|
|
||||||
}
|
|
||||||
return new Date(s);
|
return new Date(s);
|
||||||
};
|
};
|
||||||
|
|
||||||
var zeropad = function(num, count) {
|
var zeropad = function(num, count) {
|
||||||
return [Math.pow(10, count - num.toString().length), num].join('').substr(1);
|
return [Math.pow(10, count - num.toString().length), num].join('').substr(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
var dateformat = (typeof strftime === 'undefined') ? function(t) {
|
var dateformat = (typeof strftime === 'undefined') ? function(t) {
|
||||||
return zeropad(t.getMonth() + 1, 2) + "/" + zeropad(t.getDate(), 2) + "/" + t.getFullYear().toString().substring(2) +
|
return zeropad(t.getMonth() + 1, 2) + "/" + zeropad(t.getDate(), 2) + "/" + t.getFullYear().toString().substring(2) +
|
||||||
" (" + ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][t.getDay()] + ") " +
|
" (" + [_("Sun"), _("Mon"), _("Tue"), _("Wed"), _("Thu"), _("Fri"), _("Sat"), _("Sun")][t.getDay()] + ") " +
|
||||||
// time
|
// time
|
||||||
zeropad(t.getHours(), 2) + ":" + zeropad(t.getMinutes(), 2) + ":" + zeropad(t.getSeconds(), 2);
|
zeropad(t.getHours(), 2) + ":" + zeropad(t.getMinutes(), 2) + ":" + zeropad(t.getSeconds(), 2);
|
||||||
|
|
||||||
} : function(t) {
|
} : function(t) {
|
||||||
// post_date is defined in templates/main.js
|
// post_date is defined in templates/main.js
|
||||||
return strftime(window.post_date, t, datelocale);
|
return strftime(window.post_date, t, datelocale);
|
||||||
};
|
};
|
||||||
|
|
||||||
function timeDifference(current, previous) {
|
function timeDifference(current, previous) {
|
||||||
|
|
||||||
var msPerMinute = 60 * 1000;
|
var msPerMinute = 60 * 1000;
|
||||||
var msPerHour = msPerMinute * 60;
|
var msPerHour = msPerMinute * 60;
|
||||||
var msPerDay = msPerHour * 24;
|
var msPerDay = msPerHour * 24;
|
||||||
@ -85,17 +69,18 @@ $(document).ready(function(){
|
|||||||
|
|
||||||
for(var i = 0; i < times.length; i++) {
|
for(var i = 0; i < times.length; i++) {
|
||||||
var t = times[i].getAttribute('datetime');
|
var t = times[i].getAttribute('datetime');
|
||||||
var postTime = iso8601(t);
|
var postTime = new Date(t);
|
||||||
|
|
||||||
times[i].setAttribute('data-local', 'true');
|
times[i].setAttribute('data-local', 'true');
|
||||||
|
|
||||||
if (localStorage.show_relative_time === 'false') {
|
if (localStorage.show_relative_time === 'false') {
|
||||||
times[i].innerHTML = dateformat(postTime);
|
times[i].innerHTML = dateformat(iso8601(t));
|
||||||
times[i].setAttribute('title', timeDifference(currentTime, postTime.getTime()));
|
times[i].setAttribute('title', timeDifference(currentTime, postTime.getTime()));
|
||||||
} else {
|
} else {
|
||||||
times[i].innerHTML = timeDifference(currentTime, postTime.getTime());
|
times[i].innerHTML = timeDifference(currentTime, postTime.getTime());
|
||||||
times[i].setAttribute('title', dateformat(postTime));
|
times[i].setAttribute('title', dateformat(iso8601(t)));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -128,4 +113,3 @@ $(document).ready(function(){
|
|||||||
|
|
||||||
do_localtime(document);
|
do_localtime(document);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ var banlist_init = function(token, my_boards, inMod) {
|
|||||||
}
|
}
|
||||||
return pre+f.mask;
|
return pre+f.mask;
|
||||||
} },
|
} },
|
||||||
reason: {name: _("Reason"), width: "calc(100% - 770px - 6 * 4px)", fmt: function(f) {
|
reason: {name: _("Reason"), width: "calc(100% - 715px - 6 * 4px)", fmt: function(f) {
|
||||||
var add = "", suf = '';
|
var add = "", suf = '';
|
||||||
if (f.seen == 1) add += "<i class='fa fa-check' title='"+_("Seen")+"'></i>";
|
if (f.seen == 1) add += "<i class='fa fa-check' title='"+_("Seen")+"'></i>";
|
||||||
if (f.message) {
|
if (f.message) {
|
||||||
@ -60,8 +60,8 @@ var banlist_init = function(token, my_boards, inMod) {
|
|||||||
// duration?
|
// duration?
|
||||||
expires: {name: _("Expires"), width: "235px", fmt: function(f) {
|
expires: {name: _("Expires"), width: "235px", fmt: function(f) {
|
||||||
if (!f.expires || f.expires == 0) return "<em>"+_("never")+"</em>";
|
if (!f.expires || f.expires == 0) return "<em>"+_("never")+"</em>";
|
||||||
var formattedDate = strftime("%m/%d/%Y (%a) %H:%M:%S", new Date((f.expires|0)*1000), datelocale);
|
return strftime(window.post_date, new Date((f.expires|0)*1000), datelocale) +
|
||||||
return formattedDate + ((f.expires < time()) ? "" : " <small>"+_("in ")+until(f.expires|0)+"</small>");
|
((f.expires < time()) ? "" : " <small>"+_("in ")+until(f.expires|0)+"</small>");
|
||||||
} },
|
} },
|
||||||
username: {name: _("Staff"), width: "100px", fmt: function(f) {
|
username: {name: _("Staff"), width: "100px", fmt: function(f) {
|
||||||
var pre='',suf='',un=f.username;
|
var pre='',suf='',un=f.username;
|
||||||
@ -73,11 +73,6 @@ var banlist_init = function(token, my_boards, inMod) {
|
|||||||
un = "<em>"+_("system")+"</em>";
|
un = "<em>"+_("system")+"</em>";
|
||||||
}
|
}
|
||||||
return pre + un + suf;
|
return pre + un + suf;
|
||||||
} },
|
|
||||||
id: {
|
|
||||||
name: (inMod)?_("Edit"):" ", width: (inMod)?"35px":"0px", fmt: function(f) {
|
|
||||||
if (!inMod) return '';
|
|
||||||
return "<a href='?/edit_ban/"+f.id+"'>Edit</a>";
|
|
||||||
} }
|
} }
|
||||||
}, {}, t);
|
}, {}, t);
|
||||||
|
|
||||||
@ -129,7 +124,6 @@ var banlist_init = function(token, my_boards, inMod) {
|
|||||||
$(".banform").on("submit", function() { return false; });
|
$(".banform").on("submit", function() { return false; });
|
||||||
|
|
||||||
$("#unban").on("click", function() {
|
$("#unban").on("click", function() {
|
||||||
if (confirm('Are you sure you want to unban the selected IPs?')) {
|
|
||||||
$(".banform .hiddens").remove();
|
$(".banform .hiddens").remove();
|
||||||
$("<input type='hidden' name='unban' value='unban' class='hiddens'>").appendTo(".banform");
|
$("<input type='hidden' name='unban' value='unban' class='hiddens'>").appendTo(".banform");
|
||||||
|
|
||||||
@ -138,7 +132,6 @@ var banlist_init = function(token, my_boards, inMod) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$(".banform").off("submit").submit();
|
$(".banform").off("submit").submit();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (device_type == 'desktop') {
|
if (device_type == 'desktop') {
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
@ -375,7 +375,7 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
|
|||||||
|
|
||||||
var list = getList();
|
var list = getList();
|
||||||
var postId = $post.find('.post_no').not('[id]').text();
|
var postId = $post.find('.post_no').not('[id]').text();
|
||||||
var name, trip, uid, subject, comment, flag;
|
var name, trip, uid, subject, comment;
|
||||||
var i, length, array, rule, pattern; // temp variables
|
var i, length, array, rule, pattern; // temp variables
|
||||||
|
|
||||||
var boardId = $post.data('board');
|
var boardId = $post.data('board');
|
||||||
@ -388,7 +388,6 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
|
|||||||
|
|
||||||
var hasTrip = ($post.find('.trip').length > 0);
|
var hasTrip = ($post.find('.trip').length > 0);
|
||||||
var hasSub = ($post.find('.subject').length > 0);
|
var hasSub = ($post.find('.subject').length > 0);
|
||||||
var hasFlag = ($post.find('.flag').length > 0);
|
|
||||||
|
|
||||||
$post.data('hidden', false);
|
$post.data('hidden', false);
|
||||||
$post.data('hiddenByUid', false);
|
$post.data('hiddenByUid', false);
|
||||||
@ -397,7 +396,6 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
|
|||||||
$post.data('hiddenByTrip', false);
|
$post.data('hiddenByTrip', false);
|
||||||
$post.data('hiddenBySubject', false);
|
$post.data('hiddenBySubject', false);
|
||||||
$post.data('hiddenByComment', false);
|
$post.data('hiddenByComment', false);
|
||||||
$post.data('hiddenByFlag', false);
|
|
||||||
|
|
||||||
// add post with matched UID to localList
|
// add post with matched UID to localList
|
||||||
if (hasUID &&
|
if (hasUID &&
|
||||||
@ -438,8 +436,6 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
|
|||||||
});
|
});
|
||||||
comment = array.join(' ');
|
comment = array.join(' ');
|
||||||
|
|
||||||
if (hasFlag)
|
|
||||||
flag = $post.find('.flag').attr('title')
|
|
||||||
|
|
||||||
for (i = 0, length = list.generalFilter.length; i < length; i++) {
|
for (i = 0, length = list.generalFilter.length; i < length; i++) {
|
||||||
rule = list.generalFilter[i];
|
rule = list.generalFilter[i];
|
||||||
@ -471,12 +467,6 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
|
|||||||
hide(post);
|
hide(post);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'flag':
|
|
||||||
if (hasFlag && pattern.test(flag)) {
|
|
||||||
$post.data('hiddenByFlag', true);
|
|
||||||
hide(post);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (rule.type) {
|
switch (rule.type) {
|
||||||
@ -506,13 +496,6 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
|
|||||||
hide(post);
|
hide(post);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'flag':
|
|
||||||
pattern = new RegExp('\\b'+ rule.value+ '\\b');
|
|
||||||
if (hasFlag && pattern.test(flag)) {
|
|
||||||
$post.data('hiddenByFlag', true);
|
|
||||||
hide(post);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -638,8 +621,7 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
trip: 'tripcode',
|
trip: 'tripcode',
|
||||||
sub: 'subject',
|
sub: 'subject',
|
||||||
com: 'comment',
|
com: 'comment'
|
||||||
flag: 'flag'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$ele.empty();
|
$ele.empty();
|
||||||
@ -678,7 +660,6 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
|
|||||||
'<option value="trip">'+_('Tripcode')+'</option>' +
|
'<option value="trip">'+_('Tripcode')+'</option>' +
|
||||||
'<option value="sub">'+_('Subject')+'</option>' +
|
'<option value="sub">'+_('Subject')+'</option>' +
|
||||||
'<option value="com">'+_('Comment')+'</option>' +
|
'<option value="com">'+_('Comment')+'</option>' +
|
||||||
'<option value="flag">'+_('Flag')+'</option>' +
|
|
||||||
'</select>' +
|
'</select>' +
|
||||||
'<input type="text">' +
|
'<input type="text">' +
|
||||||
'<input type="checkbox">' +
|
'<input type="checkbox">' +
|
||||||
|
147
js/post-hover.js
147
js/post-hover.js
@ -13,62 +13,59 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
onReady(function() {
|
onready(function(){
|
||||||
let dontFetchAgain = [];
|
var dont_fetch_again = [];
|
||||||
initHover = function() {
|
init_hover = function() {
|
||||||
let link = $(this);
|
var $link = $(this);
|
||||||
let id;
|
|
||||||
let matches;
|
|
||||||
|
|
||||||
if (link.is('[data-thread]')) {
|
var id;
|
||||||
id = link.attr('data-thread');
|
var matches;
|
||||||
} else if (matches = link.text().match(/^>>(?:>\/([^\/]+)\/)?(\d+)$/)) {
|
|
||||||
|
if ($link.is('[data-thread]')) {
|
||||||
|
id = $link.attr('data-thread');
|
||||||
|
}
|
||||||
|
else if(matches = $link.text().match(/^>>(?:>\/([^\/]+)\/)?(\d+)$/)) {
|
||||||
id = matches[2];
|
id = matches[2];
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let board = $(this);
|
var board = $(this);
|
||||||
while (board.data('board') === undefined) {
|
while (board.data('board') === undefined) {
|
||||||
board = board.parent();
|
board = board.parent();
|
||||||
}
|
}
|
||||||
let threadid;
|
var threadid;
|
||||||
if (link.is('[data-thread]')) {
|
if ($link.is('[data-thread]')) threadid = 0;
|
||||||
threadid = 0;
|
else threadid = board.attr('id').replace("thread_", "");
|
||||||
} else {
|
|
||||||
threadid = board.attr('id').replace("thread_", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
board = board.data('board');
|
board = board.data('board');
|
||||||
|
|
||||||
let parentboard = board;
|
var parentboard = board;
|
||||||
|
|
||||||
if (link.is('[data-thread]')) {
|
if ($link.is('[data-thread]')) parentboard = $('form[name="post"] input[name="board"]').val();
|
||||||
parentboard = $('form[name="post"] input[name="board"]').val();
|
else if (matches[1] !== undefined) board = matches[1];
|
||||||
} else if (matches[1] !== undefined) {
|
|
||||||
board = matches[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
let post = false;
|
var $post = false;
|
||||||
let hovering = false;
|
var hovering = false;
|
||||||
let hoveredAt;
|
var hovered_at;
|
||||||
link.hover(function(e) {
|
$link.hover(function(e) {
|
||||||
hovering = true;
|
hovering = true;
|
||||||
hoveredAt = {'x': e.pageX, 'y': e.pageY};
|
hovered_at = {'x': e.pageX, 'y': e.pageY};
|
||||||
|
|
||||||
let startHover = function(link) {
|
var start_hover = function($link) {
|
||||||
if (post.is(':visible') &&
|
if ($post.is(':visible') &&
|
||||||
post.offset().top >= $(window).scrollTop() &&
|
$post.offset().top >= $(window).scrollTop() &&
|
||||||
post.offset().top + post.height() <= $(window).scrollTop() + $(window).height()) {
|
$post.offset().top + $post.height() <= $(window).scrollTop() + $(window).height()) {
|
||||||
// post is in view
|
// post is in view
|
||||||
post.addClass('highlighted');
|
$post.addClass('highlighted');
|
||||||
} else {
|
} else {
|
||||||
let newPost = post.clone();
|
var $newPost = $post.clone();
|
||||||
newPost.find('>.reply, >br').remove();
|
$newPost.find('>.reply, >br').remove();
|
||||||
newPost.find('span.mentioned').remove();
|
$newPost.find('span.mentioned').remove();
|
||||||
newPost.find('a.post_anchor').remove();
|
$newPost.find('a.post_anchor').remove();
|
||||||
|
|
||||||
newPost
|
$newPost
|
||||||
.attr('id', 'post-hover-' + id)
|
.attr('id', 'post-hover-' + id)
|
||||||
.attr('data-board', board)
|
.attr('data-board', board)
|
||||||
.addClass('post-hover')
|
.addClass('post-hover')
|
||||||
@ -79,28 +76,28 @@ onReady(function() {
|
|||||||
.css('font-style', 'normal')
|
.css('font-style', 'normal')
|
||||||
.css('z-index', '100')
|
.css('z-index', '100')
|
||||||
.addClass('reply').addClass('post')
|
.addClass('reply').addClass('post')
|
||||||
.insertAfter(link.parent())
|
.insertAfter($link.parent())
|
||||||
|
|
||||||
link.trigger('mousemove');
|
$link.trigger('mousemove');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
post = $('[data-board="' + board + '"] div.post#reply_' + id + ', [data-board="' + board + '"]div#thread_' + id);
|
$post = $('[data-board="' + board + '"] div.post#reply_' + id + ', [data-board="' + board + '"]div#thread_' + id);
|
||||||
if (post.length > 0) {
|
if($post.length > 0) {
|
||||||
startHover($(this));
|
start_hover($(this));
|
||||||
} else {
|
} else {
|
||||||
let url = link.attr('href').replace(/#.*$/, '');
|
var url = $link.attr('href').replace(/#.*$/, '');
|
||||||
|
|
||||||
if ($.inArray(url, dontFetchAgain) != -1) {
|
if($.inArray(url, dont_fetch_again) != -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dontFetchAgain.push(url);
|
dont_fetch_again.push(url);
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
context: document.body,
|
context: document.body,
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
let mythreadid = $(data).find('div[id^="thread_"]').attr('id').replace("thread_", "");
|
var mythreadid = $(data).find('div[id^="thread_"]').attr('id').replace("thread_", "");
|
||||||
|
|
||||||
if (mythreadid == threadid && parentboard == board) {
|
if (mythreadid == threadid && parentboard == board) {
|
||||||
$(data).find('div.post.reply').each(function() {
|
$(data).find('div.post.reply').each(function() {
|
||||||
@ -108,70 +105,66 @@ onReady(function() {
|
|||||||
$('[data-board="' + board + '"]#thread_' + threadid + " .post.reply:first").before($(this).hide().addClass('hidden'));
|
$('[data-board="' + board + '"]#thread_' + threadid + " .post.reply:first").before($(this).hide().addClass('hidden'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if ($('[data-board="' + board + '"]#thread_' + mythreadid).length > 0) {
|
}
|
||||||
|
else if ($('[data-board="' + board + '"]#thread_'+mythreadid).length > 0) {
|
||||||
$(data).find('div.post.reply').each(function() {
|
$(data).find('div.post.reply').each(function() {
|
||||||
if($('[data-board="' + board + '"] #' + $(this).attr('id')).length == 0) {
|
if($('[data-board="' + board + '"] #' + $(this).attr('id')).length == 0) {
|
||||||
$('[data-board="' + board + '"]#thread_' + mythreadid + " .post.reply:first").before($(this).hide().addClass('hidden'));
|
$('[data-board="' + board + '"]#thread_' + mythreadid + " .post.reply:first").before($(this).hide().addClass('hidden'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
$(data).find('div[id^="thread_"]').hide().attr('data-cached', 'yes').prependTo('form[name="postcontrols"]');
|
$(data).find('div[id^="thread_"]').hide().attr('data-cached', 'yes').prependTo('form[name="postcontrols"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
post = $('[data-board="' + board + '"] div.post#reply_' + id + ', [data-board="' + board + '"]div#thread_' + id);
|
$post = $('[data-board="' + board + '"] div.post#reply_' + id + ', [data-board="' + board + '"]div#thread_' + id);
|
||||||
|
|
||||||
if (hovering && post.length > 0) {
|
if(hovering && $post.length > 0) {
|
||||||
startHover(link);
|
start_hover($link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, function() {
|
}, function() {
|
||||||
hovering = false;
|
hovering = false;
|
||||||
if (!post) {
|
if(!$post)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
post.removeClass('highlighted');
|
$post.removeClass('highlighted');
|
||||||
if (post.hasClass('hidden') || post.data('cached') == 'yes') {
|
if($post.hasClass('hidden') || $post.data('cached') == 'yes')
|
||||||
post.css('display', 'none');
|
$post.css('display', 'none');
|
||||||
}
|
|
||||||
$('.post-hover').remove();
|
$('.post-hover').remove();
|
||||||
}).mousemove(function(e) {
|
}).mousemove(function(e) {
|
||||||
if (!post) {
|
if(!$post)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
let hover = $('#post-hover-' + id + '[data-board="' + board + '"]');
|
var $hover = $('#post-hover-' + id + '[data-board="' + board + '"]');
|
||||||
if (hover.length == 0) {
|
if($hover.length == 0)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
let scrollTop = $(window).scrollTop();
|
var scrollTop = $(window).scrollTop();
|
||||||
if (link.is("[data-thread]")) {
|
if ($link.is("[data-thread]")) scrollTop = 0;
|
||||||
scrollTop = 0;
|
var epy = e.pageY;
|
||||||
}
|
if ($link.is("[data-thread]")) epy -= $(window).scrollTop();
|
||||||
let epy = e.pageY;
|
|
||||||
if (link.is("[data-thread]")) {
|
|
||||||
epy -= $(window).scrollTop();
|
|
||||||
}
|
|
||||||
|
|
||||||
let top = (epy ? epy : hoveredAt['y']) - 10;
|
var top = (epy ? epy : hovered_at['y']) - 10;
|
||||||
|
|
||||||
if(epy < scrollTop + 15) {
|
if(epy < scrollTop + 15) {
|
||||||
top = scrollTop;
|
top = scrollTop;
|
||||||
} else if (epy > scrollTop + $(window).height() - hover.height() - 15) {
|
} else if(epy > scrollTop + $(window).height() - $hover.height() - 15) {
|
||||||
top = 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);
|
|
||||||
|
$hover.css('left', (e.pageX ? e.pageX : hovered_at['x'])).css('top', top);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$('div.body a:not([rel="nofollow"])').each(initHover);
|
$('div.body a:not([rel="nofollow"])').each(init_hover);
|
||||||
|
|
||||||
// allow to work with auto-reload.js, etc.
|
// allow to work with auto-reload.js, etc.
|
||||||
$(document).on('new_post', function(e, post) {
|
$(document).on('new_post', function(e, post) {
|
||||||
$(post).find('div.body a:not([rel="nofollow"])').each(initHover);
|
$(post).find('div.body a:not([rel="nofollow"])').each(init_hover);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -10,12 +10,12 @@
|
|||||||
* Usage:
|
* Usage:
|
||||||
* $config['additional_javascript'][] = 'js/jquery.min.js';
|
* $config['additional_javascript'][] = 'js/jquery.min.js';
|
||||||
* $config['additional_javascript'][] = 'js/quote-selection.js';
|
* $config['additional_javascript'][] = 'js/quote-selection.js';
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
if (!window.getSelection) {
|
if (!window.getSelection)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
$.fn.selectRange = function(start, end) {
|
$.fn.selectRange = function(start, end) {
|
||||||
return this.each(function() {
|
return this.each(function() {
|
||||||
@ -23,7 +23,7 @@ $(document).ready(function() {
|
|||||||
this.focus();
|
this.focus();
|
||||||
this.setSelectionRange(start, end);
|
this.setSelectionRange(start, end);
|
||||||
} else if (this.createTextRange) {
|
} else if (this.createTextRange) {
|
||||||
let range = this.createTextRange();
|
var range = this.createTextRange();
|
||||||
range.collapse(true);
|
range.collapse(true);
|
||||||
range.moveEnd('character', end);
|
range.moveEnd('character', end);
|
||||||
range.moveStart('character', start);
|
range.moveStart('character', start);
|
||||||
@ -32,74 +32,81 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let altKey = false;
|
var altKey = false;
|
||||||
let ctrlKey = false;
|
var ctrlKey = false;
|
||||||
let metaKey = false;
|
var metaKey = false;
|
||||||
|
|
||||||
$(document).keyup(function(e) {
|
$(document).keyup(function(e) {
|
||||||
if (e.keyCode == 18) {
|
if (e.keyCode == 18)
|
||||||
altKey = false;
|
altKey = false;
|
||||||
} else if (e.keyCode == 17) {
|
else if (e.keyCode == 17)
|
||||||
ctrlKey = false;
|
ctrlKey = false;
|
||||||
} else if (e.keyCode == 91) {
|
else if (e.keyCode == 91)
|
||||||
metaKey = false;
|
metaKey = false;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).keydown(function(e) {
|
$(document).keydown(function(e) {
|
||||||
if (e.altKey) {
|
if (e.altKey)
|
||||||
altKey = true;
|
altKey = true;
|
||||||
} else if (e.ctrlKey) {
|
else if (e.ctrlKey)
|
||||||
ctrlKey = true;
|
ctrlKey = true;
|
||||||
} else if (e.metaKey) {
|
else if (e.metaKey)
|
||||||
metaKey = true;
|
metaKey = true;
|
||||||
}
|
|
||||||
|
|
||||||
if (altKey || ctrlKey || metaKey) {
|
if (altKey || ctrlKey || metaKey) {
|
||||||
|
// console.log('CTRL/ALT/Something used. Ignoring');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.keyCode < 48 || e.keyCode > 90) {
|
if (e.keyCode < 48 || e.keyCode > 90)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var selection = window.getSelection();
|
||||||
|
var $post = $(selection.anchorNode).parents('.post');
|
||||||
|
if ($post.length == 0) {
|
||||||
|
// console.log('Start of selection was not post div', $(selection.anchorNode).parent());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let selection = window.getSelection();
|
var postID = $post.find('.post_no:eq(1)').text();
|
||||||
let post = $(selection.anchorNode).parents('.post');
|
|
||||||
if (post.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let postID = post.find('.post_no:eq(1)').text();
|
|
||||||
|
|
||||||
if (postID != $(selection.focusNode).parents('.post').find('.post_no:eq(1)').text()) {
|
if (postID != $(selection.focusNode).parents('.post').find('.post_no:eq(1)').text()) {
|
||||||
|
// console.log('Selection left post div', $(selection.focusNode).parent());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedText = selection.toString();
|
;
|
||||||
|
var selectedText = selection.toString();
|
||||||
|
// console.log('Selected text: ' + selectedText.replace(/\n/g, '\\n').replace(/\r/g, '\\r'));
|
||||||
|
|
||||||
if ($('body').hasClass('debug')) {
|
if ($('body').hasClass('debug'))
|
||||||
alert(selectedText);
|
alert(selectedText);
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedText.length == 0) {
|
if (selectedText.length == 0)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
let body = $('textarea#body')[0];
|
var body = $('textarea#body')[0];
|
||||||
|
|
||||||
let last_quote = body.value.match(/[\S.]*(^|[\S\s]*)>>(\d+)/);
|
var last_quote = body.value.match(/[\S.]*(^|[\S\s]*)>>(\d+)/);
|
||||||
if (last_quote) {
|
if (last_quote)
|
||||||
last_quote = last_quote[2];
|
last_quote = last_quote[2];
|
||||||
}
|
|
||||||
|
|
||||||
/* to solve some bugs on weird browsers, we need to replace \r\n with \n and then undo that after */
|
/* to solve some bugs on weird browsers, we need to replace \r\n with \n and then undo that after */
|
||||||
let quote = (last_quote != postID ? '>>' + postID + '\r\n' : '') + $.trim(selectedText).replace(/\r\n/g, '\n').replace(/^/mg, '>').replace(/\n/g, '\r\n') + '\r\n';
|
var quote = (last_quote != postID ? '>>' + postID + '\r\n' : '') + $.trim(selectedText).replace(/\r\n/g, '\n').replace(/^/mg, '>').replace(/\n/g, '\r\n') + '\r\n';
|
||||||
|
|
||||||
|
// console.log('Deselecting text');
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
|
|
||||||
if (body.selectionStart || body.selectionStart == '0') {
|
if (document.selection) {
|
||||||
let start = body.selectionStart;
|
// IE
|
||||||
let end = body.selectionEnd;
|
body.focus();
|
||||||
|
var sel = document.selection.createRange();
|
||||||
|
sel.text = quote;
|
||||||
|
body.focus();
|
||||||
|
} else if (body.selectionStart || body.selectionStart == '0') {
|
||||||
|
// Mozilla
|
||||||
|
var start = body.selectionStart;
|
||||||
|
var end = body.selectionEnd;
|
||||||
|
|
||||||
if (!body.value.substring(0, start).match(/(^|\n)$/)) {
|
if (!body.value.substring(0, start).match(/(^|\n)$/)) {
|
||||||
quote = '\r\n\r\n' + quote;
|
quote = '\r\n\r\n' + quote;
|
||||||
@ -114,3 +121,4 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -13,42 +13,38 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
onReady(function() {
|
onready(function(){
|
||||||
let showBackLinks = function() {
|
var showBackLinks = function() {
|
||||||
let reply_id = $(this).attr('id').replace(/(^reply_)|(^op_)/, '');
|
var reply_id = $(this).attr('id').replace(/(^reply_)|(^op_)/, '');
|
||||||
|
|
||||||
$(this).find('div.body a:not([rel="nofollow"])').each(function() {
|
$(this).find('div.body a:not([rel="nofollow"])').each(function() {
|
||||||
let id, post, $mentioned;
|
var id, post, $mentioned;
|
||||||
|
|
||||||
if (id = $(this).text().match(/^>>(\d+)$/)) {
|
if(id = $(this).text().match(/^>>(\d+)$/))
|
||||||
id = id[1];
|
id = id[1];
|
||||||
} else {
|
else
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
$post = $('#reply_' + id);
|
$post = $('#reply_' + id);
|
||||||
if($post.length == 0){
|
if($post.length == 0){
|
||||||
$post = $('#op_' + id);
|
$post = $('#op_' + id);
|
||||||
if ($post.length == 0) {
|
if($post.length == 0)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$mentioned = $post.find('p.intro span.mentioned');
|
$mentioned = $post.find('p.intro span.mentioned');
|
||||||
if($mentioned.length == 0) {
|
if($mentioned.length == 0)
|
||||||
$mentioned = $('<span class="mentioned unimportant"></span>').appendTo($post.find('p.intro'));
|
$mentioned = $('<span class="mentioned unimportant"></span>').appendTo($post.find('p.intro'));
|
||||||
}
|
|
||||||
|
|
||||||
if ($mentioned.find('a.mentioned-' + reply_id).length != 0) {
|
if ($mentioned.find('a.mentioned-' + reply_id).length != 0)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
let link = $('<a class="mentioned-' + reply_id + '" onclick="highlightReply(\'' + reply_id + '\');" href="#' + reply_id + '">>>' +
|
var $link = $('<a class="mentioned-' + reply_id + '" onclick="highlightReply(\'' + reply_id + '\');" href="#' + reply_id + '">>>' +
|
||||||
reply_id + '</a>');
|
reply_id + '</a>');
|
||||||
link.appendTo($mentioned)
|
$link.appendTo($mentioned)
|
||||||
|
|
||||||
if (window.init_hover) {
|
if (window.init_hover) {
|
||||||
link.each(init_hover);
|
$link.each(init_hover);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -57,10 +53,9 @@ onReady(function() {
|
|||||||
$('div.post.op').each(showBackLinks);
|
$('div.post.op').each(showBackLinks);
|
||||||
|
|
||||||
$(document).on('new_post', function(e, post) {
|
$(document).on('new_post', function(e, post) {
|
||||||
|
showBackLinks.call(post);
|
||||||
if ($(post).hasClass("op")) {
|
if ($(post).hasClass("op")) {
|
||||||
$(post).find('div.post.reply').each(showBackLinks);
|
$(post).find('div.post.reply').each(showBackLinks);
|
||||||
} else {
|
|
||||||
$(post).parent().find('div.post.reply').each(showBackLinks);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -12,11 +12,11 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
onReady(function() {
|
onready(function(){
|
||||||
if(device_type == 'mobile') {
|
if(device_type == 'mobile') {
|
||||||
let fix_spoilers = function(where) {
|
var fix_spoilers = function(where) {
|
||||||
let spoilers = where.getElementsByClassName('spoiler');
|
var spoilers = where.getElementsByClassName('spoiler');
|
||||||
for (let i = 0; i < spoilers.length; i++) {
|
for(var i = 0; i < spoilers.length; i++) {
|
||||||
spoilers[i].onmousedown = function() {
|
spoilers[i].onmousedown = function() {
|
||||||
this.style.color = 'white';
|
this.style.color = 'white';
|
||||||
};
|
};
|
||||||
@ -31,3 +31,4 @@ onReady(function() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -14,18 +14,17 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
onReady(function() {
|
onready(function(){
|
||||||
let stylesDiv = $('div.styles');
|
var stylesDiv = $('div.styles');
|
||||||
let stylesSelect = $('<select></select>');
|
var stylesSelect = $('<select></select>');
|
||||||
|
|
||||||
let i = 1;
|
var i = 1;
|
||||||
stylesDiv.children().each(function() {
|
stylesDiv.children().each(function() {
|
||||||
let opt = $('<option></option>')
|
var opt = $('<option></option>')
|
||||||
.html(this.innerHTML.replace(/(^\[|\]$)/g, ''))
|
.html(this.innerHTML.replace(/(^\[|\]$)/g, ''))
|
||||||
.val(i);
|
.val(i);
|
||||||
if ($(this).hasClass('selected')) {
|
if ($(this).hasClass('selected'))
|
||||||
opt.attr('selected', true);
|
opt.attr('selected', true);
|
||||||
}
|
|
||||||
stylesSelect.append(opt);
|
stylesSelect.append(opt);
|
||||||
$(this).attr('id', 'style-select-' + i);
|
$(this).attr('id', 'style-select-' + i);
|
||||||
i++;
|
i++;
|
||||||
@ -43,3 +42,4 @@ onReady(function() {
|
|||||||
.append(stylesSelect)
|
.append(stylesSelect)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
Subproject commit 25dc1757783115136a83af34c42c603ac8b470aa
|
|
@ -22,10 +22,11 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
onReady(function() {
|
|
||||||
let do_embed_yt = function(tag) {
|
onready(function(){
|
||||||
|
var do_embed_yt = function(tag) {
|
||||||
$('div.video-container a', tag).click(function() {
|
$('div.video-container a', tag).click(function() {
|
||||||
let videoID = $(this.parentNode).data('video');
|
var videoID = $(this.parentNode).data('video');
|
||||||
|
|
||||||
$(this.parentNode).html('<iframe style="float:left;margin: 10px 20px" type="text/html" '+
|
$(this.parentNode).html('<iframe style="float:left;margin: 10px 20px" type="text/html" '+
|
||||||
'width="360" height="270" src="//www.youtube.com/embed/' + videoID +
|
'width="360" height="270" src="//www.youtube.com/embed/' + videoID +
|
||||||
@ -41,3 +42,4 @@ onReady(function() {
|
|||||||
do_embed_yt(post);
|
do_embed_yt(post);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
44
mod.php
44
mod.php
@ -1,24 +1,21 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2010-2014 Tinyboard Development Group
|
* Copyright (c) 2010-2014 Tinyboard Development Group
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once 'inc/bootstrap.php';
|
require_once 'inc/bootstrap.php';
|
||||||
|
|
||||||
if ($config['debug']) {
|
if ($config['debug'])
|
||||||
$parse_start_time = microtime(true);
|
$parse_start_time = microtime(true);
|
||||||
}
|
|
||||||
|
|
||||||
require_once 'inc/mod/pages.php';
|
require_once 'inc/mod/pages.php';
|
||||||
|
|
||||||
|
check_login(true);
|
||||||
$ctx = Vichan\build_context($config);
|
|
||||||
|
|
||||||
check_login($ctx, true);
|
|
||||||
|
|
||||||
$query = isset($_SERVER['QUERY_STRING']) ? rawurldecode($_SERVER['QUERY_STRING']) : '';
|
$query = isset($_SERVER['QUERY_STRING']) ? rawurldecode($_SERVER['QUERY_STRING']) : '';
|
||||||
|
|
||||||
$pages = [
|
$pages = array(
|
||||||
'' => ':?/', // redirect to dashboard
|
'' => ':?/', // redirect to dashboard
|
||||||
'/' => 'dashboard', // dashboard
|
'/' => 'dashboard', // dashboard
|
||||||
'/confirm/(.+)' => 'confirm', // confirm action (if javascript didn't work)
|
'/confirm/(.+)' => 'confirm', // confirm action (if javascript didn't work)
|
||||||
@ -58,15 +55,15 @@ $pages = [
|
|||||||
|
|
||||||
'/rebuild' => 'secure_POST rebuild', // rebuild static files
|
'/rebuild' => 'secure_POST rebuild', // rebuild static files
|
||||||
'/reports' => 'reports', // report queue
|
'/reports' => 'reports', // report queue
|
||||||
'/reports/(\d+)/dismiss(&all|&post)?' => 'secure report_dismiss', // dismiss a report
|
'/reports/(\d+)/dismiss(all)?' => 'secure report_dismiss', // dismiss a report
|
||||||
|
|
||||||
'/IP/([\w.:]+)' => 'secure_POST ip', // view ip address
|
'/IP/([\w.:]+)' => 'secure_POST ip', // view ip address
|
||||||
'/IP/([\w.:]+)/remove_note/(\d+)' => 'secure ip_remove_note', // remove note from ip address
|
'/IP/([\w.:]+)/remove_note/(\d+)' => 'secure ip_remove_note', // remove note from ip address
|
||||||
|
'/IP/([\w.:-]+)/remove_telegram/(\d+)' => 'secure ip_remove_telegram', // remove telegram from ip address
|
||||||
|
|
||||||
'/ban' => 'secure_POST ban', // new ban
|
'/ban' => 'secure_POST ban', // new ban
|
||||||
'/bans' => 'secure_POST bans', // ban list
|
'/bans' => 'secure_POST bans', // ban list
|
||||||
'/bans.json' => 'secure bans_json', // ban list JSON
|
'/bans.json' => 'secure bans_json', // ban list JSON
|
||||||
'/edit_ban/(\d+)' => 'secure_POST edit_ban',
|
|
||||||
'/ban-appeals' => 'secure_POST ban_appeals', // view ban appeals
|
'/ban-appeals' => 'secure_POST ban_appeals', // view ban appeals
|
||||||
|
|
||||||
'/recent/(\d+)' => 'recent_posts', // view recent posts
|
'/recent/(\d+)' => 'recent_posts', // view recent posts
|
||||||
@ -99,6 +96,7 @@ $pages = [
|
|||||||
// these pages aren't listed in the dashboard without $config['debug']
|
// these pages aren't listed in the dashboard without $config['debug']
|
||||||
//'/debug/antispam' => 'debug_antispam',
|
//'/debug/antispam' => 'debug_antispam',
|
||||||
//'/debug/recent' => 'debug_recent_posts',
|
//'/debug/recent' => 'debug_recent_posts',
|
||||||
|
//'/debug/apc' => 'debug_apc',
|
||||||
//'/debug/sql' => 'secure_POST debug_sql',
|
//'/debug/sql' => 'secure_POST debug_sql',
|
||||||
|
|
||||||
// This should always be at the end:
|
// This should always be at the end:
|
||||||
@ -112,14 +110,14 @@ $pages = [
|
|||||||
str_replace('%d', '(\d+)', preg_quote($config['file_page'], '!')) => 'view_thread',
|
str_replace('%d', '(\d+)', preg_quote($config['file_page'], '!')) => 'view_thread',
|
||||||
|
|
||||||
'/(\%b)/' . preg_quote($config['dir']['res'], '!') .
|
'/(\%b)/' . preg_quote($config['dir']['res'], '!') .
|
||||||
str_replace([ '%d','%s' ], [ '(\d+)', '[a-z0-9-]+' ], preg_quote($config['file_page50_slug'], '!')) => 'view_thread50',
|
str_replace(array('%d','%s'), array('(\d+)', '[a-z0-9-]+'), preg_quote($config['file_page50_slug'], '!')) => 'view_thread50',
|
||||||
'/(\%b)/' . preg_quote($config['dir']['res'], '!') .
|
'/(\%b)/' . preg_quote($config['dir']['res'], '!') .
|
||||||
str_replace([ '%d','%s' ], [ '(\d+)', '[a-z0-9-]+' ], preg_quote($config['file_page_slug'], '!')) => 'view_thread',
|
str_replace(array('%d','%s'), array('(\d+)', '[a-z0-9-]+'), preg_quote($config['file_page_slug'], '!')) => 'view_thread',
|
||||||
];
|
);
|
||||||
|
|
||||||
|
|
||||||
if (!$mod) {
|
if (!$mod) {
|
||||||
$pages = [ '!^(.+)?$!' => 'login' ];
|
$pages = array('!^(.+)?$!' => 'login');
|
||||||
} elseif (isset($_GET['status'], $_GET['r'])) {
|
} elseif (isset($_GET['status'], $_GET['r'])) {
|
||||||
header('Location: ' . $_GET['r'], true, (int)$_GET['status']);
|
header('Location: ' . $_GET['r'], true, (int)$_GET['status']);
|
||||||
exit;
|
exit;
|
||||||
@ -129,11 +127,10 @@ if (isset($config['mod']['custom_pages'])) {
|
|||||||
$pages = array_merge($pages, $config['mod']['custom_pages']);
|
$pages = array_merge($pages, $config['mod']['custom_pages']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$new_pages = [];
|
$new_pages = array();
|
||||||
foreach ($pages as $key => $callback) {
|
foreach ($pages as $key => $callback) {
|
||||||
if (is_string($callback) && preg_match('/^secure /', $callback)) {
|
if (is_string($callback) && preg_match('/^secure /', $callback))
|
||||||
$key .= '(/(?P<token>[a-f0-9]{8}))?';
|
$key .= '(/(?P<token>[a-f0-9]{8}))?';
|
||||||
}
|
|
||||||
$key = str_replace('\%b', '?P<board>' . sprintf(substr($config['board_path'], 0, -1), $config['board_regex']), $key);
|
$key = str_replace('\%b', '?P<board>' . sprintf(substr($config['board_path'], 0, -1), $config['board_regex']), $key);
|
||||||
$new_pages[(!empty($key) and $key[0] == '!') ? $key : '!^' . $key . '(?:&[^&=]+=[^&]*)*$!u'] = $callback;
|
$new_pages[(!empty($key) and $key[0] == '!') ? $key : '!^' . $key . '(?:&[^&=]+=[^&]*)*$!u'] = $callback;
|
||||||
}
|
}
|
||||||
@ -141,7 +138,7 @@ $pages = $new_pages;
|
|||||||
|
|
||||||
foreach ($pages as $uri => $handler) {
|
foreach ($pages as $uri => $handler) {
|
||||||
if (preg_match($uri, $query, $matches)) {
|
if (preg_match($uri, $query, $matches)) {
|
||||||
$matches[0] = $ctx; // Replace the text captured by the full pattern with a reference to the context.
|
$matches = array_slice($matches, 1);
|
||||||
|
|
||||||
if (isset($matches['board'])) {
|
if (isset($matches['board'])) {
|
||||||
$board_match = $matches['board'];
|
$board_match = $matches['board'];
|
||||||
@ -161,7 +158,7 @@ foreach ($pages as $uri => $handler) {
|
|||||||
if ($secure_post_only)
|
if ($secure_post_only)
|
||||||
error($config['error']['csrf']);
|
error($config['error']['csrf']);
|
||||||
else {
|
else {
|
||||||
mod_confirm($ctx, substr($query, 1));
|
mod_confirm(substr($query, 1));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,20 +173,24 @@ foreach ($pages as $uri => $handler) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($config['debug']) {
|
if ($config['debug']) {
|
||||||
$debug['mod_page'] = [
|
$debug['mod_page'] = array(
|
||||||
'req' => $query,
|
'req' => $query,
|
||||||
'match' => $uri,
|
'match' => $uri,
|
||||||
'handler' => $handler,
|
'handler' => $handler,
|
||||||
];
|
);
|
||||||
$debug['time']['parse_mod_req'] = '~' . round((microtime(true) - $parse_start_time) * 1000, 2) . 'ms';
|
$debug['time']['parse_mod_req'] = '~' . round((microtime(true) - $parse_start_time) * 1000, 2) . 'ms';
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't want to call named parameters (PHP 8).
|
if (is_array($matches)) {
|
||||||
|
// we don't want to call named parameters (PHP 8)
|
||||||
$matches = array_values($matches);
|
$matches = array_values($matches);
|
||||||
|
}
|
||||||
|
|
||||||
if (is_string($handler)) {
|
if (is_string($handler)) {
|
||||||
if ($handler[0] == ':') {
|
if ($handler[0] == ':') {
|
||||||
header('Location: ' . substr($handler, 1), true, $config['redirect_http']);
|
header('Location: ' . substr($handler, 1), true, $config['redirect_http']);
|
||||||
|
} elseif (is_callable("mod_page_$handler")) {
|
||||||
|
call_user_func_array("mod_page_$handler", $matches);
|
||||||
} elseif (is_callable("mod_$handler")) {
|
} elseif (is_callable("mod_$handler")) {
|
||||||
call_user_func_array("mod_$handler", $matches);
|
call_user_func_array("mod_$handler", $matches);
|
||||||
} else {
|
} else {
|
||||||
@ -206,3 +207,4 @@ foreach ($pages as $uri => $handler) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
error($config['error']['404']);
|
error($config['error']['404']);
|
||||||
|
|
||||||
|
636
post.php
636
post.php
@ -5,209 +5,7 @@
|
|||||||
|
|
||||||
require_once 'inc/bootstrap.php';
|
require_once 'inc/bootstrap.php';
|
||||||
|
|
||||||
use Vichan\{Context, WebDependencyFactory};
|
|
||||||
use Vichan\Data\Driver\{LogDriver, HttpDriver};
|
|
||||||
use Vichan\Service\{RemoteCaptchaQuery, NativeCaptchaQuery};
|
|
||||||
use Vichan\Functions\Format;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the md5 hash of the file.
|
|
||||||
*
|
|
||||||
* @param array $config instance configuration.
|
|
||||||
* @param string $file file to the the md5 of.
|
|
||||||
* @return string|false
|
|
||||||
*/
|
|
||||||
function md5_hash_of_file($config, $path) {
|
|
||||||
$cmd = false;
|
|
||||||
if ($config['bsd_md5']) {
|
|
||||||
$cmd = '/sbin/md5 -r';
|
|
||||||
}
|
|
||||||
if ($config['gnu_md5']) {
|
|
||||||
$cmd = 'md5sum';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($cmd) {
|
|
||||||
$output = shell_exec_error($cmd . " " . escapeshellarg($path));
|
|
||||||
$output = explode(' ', $output);
|
|
||||||
return $output[0];
|
|
||||||
} else {
|
|
||||||
return md5_file($path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strip the symbols incompatible with the current database version.
|
|
||||||
*
|
|
||||||
* @param string @input The input string.
|
|
||||||
* @return string The value stripped of incompatible symbols.
|
|
||||||
*/
|
|
||||||
function strip_symbols($input) {
|
|
||||||
if (mysql_version() >= 50503) {
|
|
||||||
return $input; // Assume we're using the utf8mb4 charset
|
|
||||||
} else {
|
|
||||||
// MySQL's `utf8` charset only supports up to 3-byte symbols
|
|
||||||
// Remove anything >= 0x010000
|
|
||||||
|
|
||||||
$chars = preg_split('//u', $input, -1, PREG_SPLIT_NO_EMPTY);
|
|
||||||
$ret = '';
|
|
||||||
foreach ($chars as $char) {
|
|
||||||
$o = 0;
|
|
||||||
$ord = ordutf8($char, $o);
|
|
||||||
if ($ord >= 0x010000) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$ret .= $char;
|
|
||||||
}
|
|
||||||
return $ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download a remote file from the given url.
|
|
||||||
* The file is deleted at shutdown.
|
|
||||||
*
|
|
||||||
* @param HttpDriver $http The http client.
|
|
||||||
* @param string $file_url The url to download the file from.
|
|
||||||
* @param int $request_timeout Timeout to retrieve the file.
|
|
||||||
* @param array $extra_extensions Allowed file extensions.
|
|
||||||
* @param string $tmp_dir Temporary directory to save the file into.
|
|
||||||
* @param array $error_array An array with error codes, used to create exceptions on failure.
|
|
||||||
* @return array|false Returns an array describing the file on success, or false if the file was too large
|
|
||||||
* @throws InvalidArgumentException|RuntimeException Throws on invalid arguments and IO errors.
|
|
||||||
*/
|
|
||||||
function download_file_from_url(HttpDriver $http, $file_url, $request_timeout, $allowed_extensions, $tmp_dir, &$error_array) {
|
|
||||||
if (!preg_match('@^https?://@', $file_url)) {
|
|
||||||
throw new InvalidArgumentException($error_array['invalidimg']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$param_idx = mb_strpos($file_url, '?');
|
|
||||||
if ($param_idx !== false) {
|
|
||||||
$url_without_params = mb_substr($file_url, 0, $param_idx);
|
|
||||||
} else {
|
|
||||||
$url_without_params = $file_url;
|
|
||||||
}
|
|
||||||
|
|
||||||
$extension = strtolower(mb_substr($url_without_params, mb_strrpos($url_without_params, '.') + 1));
|
|
||||||
|
|
||||||
if (!in_array($extension, $allowed_extensions)) {
|
|
||||||
throw new InvalidArgumentException($error_array['unknownext']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$tmp_file = tempnam($tmp_dir, 'url');
|
|
||||||
function unlink_tmp_file($file) {
|
|
||||||
@unlink($file);
|
|
||||||
fatal_error_handler();
|
|
||||||
}
|
|
||||||
register_shutdown_function('unlink_tmp_file', $tmp_file);
|
|
||||||
|
|
||||||
$fd = fopen($tmp_file, 'w');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$success = $http->requestGetInto($url_without_params, null, $fd, $request_timeout);
|
|
||||||
if (!$success) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
fclose($fd);
|
|
||||||
}
|
|
||||||
|
|
||||||
return array(
|
|
||||||
'name' => basename($url_without_params),
|
|
||||||
'tmp_name' => $tmp_file,
|
|
||||||
'file_tmp' => true,
|
|
||||||
'error' => 0,
|
|
||||||
'size' => filesize($tmp_file)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try extract text from the given image.
|
|
||||||
*
|
|
||||||
* @param array $config Instance configuration.
|
|
||||||
* @param string $img_path The file path to the image.
|
|
||||||
* @return string|false Returns a string with the extracted text on success (if any).
|
|
||||||
* @throws RuntimeException Throws if executing tesseract fails.
|
|
||||||
*/
|
|
||||||
function ocr_image(array $config, string $img_path): string {
|
|
||||||
// The default preprocess command is an ImageMagick b/w quantization.
|
|
||||||
$ret = shell_exec_error(
|
|
||||||
sprintf($config['tesseract_preprocess_command'], escapeshellarg($img_path))
|
|
||||||
. ' | tesseract stdin stdout 2>/dev/null'
|
|
||||||
. $config['tesseract_params']
|
|
||||||
);
|
|
||||||
if ($ret === false) {
|
|
||||||
throw new RuntimeException('Unable to run tesseract');
|
|
||||||
}
|
|
||||||
|
|
||||||
return trim($ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trim an image's EXIF metadata
|
|
||||||
*
|
|
||||||
* @param string $img_path The file path to the image.
|
|
||||||
* @return int The size of the stripped file.
|
|
||||||
* @throws RuntimeException Throws on IO errors.
|
|
||||||
*/
|
|
||||||
function strip_image_metadata(string $img_path): int {
|
|
||||||
$err = shell_exec_error('exiftool -overwrite_original -ignoreMinorErrors -q -q -all= -Orientation ' . escapeshellarg($img_path));
|
|
||||||
if ($err === false) {
|
|
||||||
throw new RuntimeException('Could not strip EXIF metadata!');
|
|
||||||
}
|
|
||||||
clearstatcache(true, $img_path);
|
|
||||||
$ret = filesize($img_path);
|
|
||||||
if ($ret === false) {
|
|
||||||
throw new RuntimeException('Could not calculate file size!');
|
|
||||||
}
|
|
||||||
return $ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete posts in a cyclical thread.
|
|
||||||
*
|
|
||||||
* @param string $boardUri The URI of the board.
|
|
||||||
* @param int $threadId The ID of the thread.
|
|
||||||
* @param int $cycleLimit The number of most recent posts to retain.
|
|
||||||
*/
|
|
||||||
function delete_cyclical_posts(string $boardUri, int $threadId, int $cycleLimit): void
|
|
||||||
{
|
|
||||||
$query = prepare(sprintf('
|
|
||||||
SELECT p.`id`
|
|
||||||
FROM ``posts_%s`` p
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT `id`
|
|
||||||
FROM ``posts_%s``
|
|
||||||
WHERE `thread` = :thread
|
|
||||||
ORDER BY `id` DESC
|
|
||||||
LIMIT :limit
|
|
||||||
) recent_posts ON p.id = recent_posts.id
|
|
||||||
WHERE p.thread = :thread
|
|
||||||
AND recent_posts.id IS NULL',
|
|
||||||
$boardUri, $boardUri
|
|
||||||
));
|
|
||||||
|
|
||||||
$query->bindValue(':thread', $threadId, PDO::PARAM_INT);
|
|
||||||
$query->bindValue(':limit', $cycleLimit, PDO::PARAM_INT);
|
|
||||||
|
|
||||||
$query->execute() or error(db_error($query));
|
|
||||||
$ids = $query->fetchAll(PDO::FETCH_COLUMN);
|
|
||||||
|
|
||||||
foreach ($ids as $id) {
|
|
||||||
deletePost($id, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method handling functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
$dropped_post = false;
|
$dropped_post = false;
|
||||||
$context = Vichan\build_context($config);
|
|
||||||
|
|
||||||
// Is it a post coming from NNTP? Let's extract it and pretend it's a normal post.
|
// Is it a post coming from NNTP? Let's extract it and pretend it's a normal post.
|
||||||
if (isset($_GET['Newsgroups']) && $config['nntpchan']['enabled']) {
|
if (isset($_GET['Newsgroups']) && $config['nntpchan']['enabled']) {
|
||||||
@ -286,7 +84,7 @@ if (isset($_GET['Newsgroups']) && $config['nntpchan']['enabled']) {
|
|||||||
$content = file_get_contents("php://input");
|
$content = file_get_contents("php://input");
|
||||||
}
|
}
|
||||||
elseif ($ct == 'multipart/mixed' || $ct == 'multipart/form-data') {
|
elseif ($ct == 'multipart/mixed' || $ct == 'multipart/form-data') {
|
||||||
$context->get(LogDriver::class)->log(LogDriver::DEBUG, 'MM: Files: ' . print_r($GLOBALS, true));
|
_syslog(LOG_INFO, "MM: Files: ".print_r($GLOBALS, true)); // Debug
|
||||||
|
|
||||||
$content = '';
|
$content = '';
|
||||||
|
|
||||||
@ -353,7 +151,7 @@ if (isset($_GET['Newsgroups']) && $config['nntpchan']['enabled']) {
|
|||||||
$ret[] = ">>".$v['id'];
|
$ret[] = ">>".$v['id'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return implode(", ", $ret);
|
return implode($ret, ", ");
|
||||||
}
|
}
|
||||||
}, $content);
|
}, $content);
|
||||||
|
|
||||||
@ -382,11 +180,10 @@ if (isset($_POST['delete'])) {
|
|||||||
if (!isset($_POST['board'], $_POST['password']))
|
if (!isset($_POST['board'], $_POST['password']))
|
||||||
error($config['error']['bot']);
|
error($config['error']['bot']);
|
||||||
|
|
||||||
if (empty($_POST['password'])){
|
$password = &$_POST['password'];
|
||||||
error($config['error']['invalidpassword']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$password = hashPassword($_POST['password']);
|
if ($password == '')
|
||||||
|
error($config['error']['invalidpassword']);
|
||||||
|
|
||||||
$delete = array();
|
$delete = array();
|
||||||
foreach ($_POST as $post => $value) {
|
foreach ($_POST as $post => $value) {
|
||||||
@ -430,33 +227,25 @@ if (isset($_POST['delete'])) {
|
|||||||
$thread = $thread_query->fetch(PDO::FETCH_ASSOC);
|
$thread = $thread_query->fetch(PDO::FETCH_ASSOC);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($post['time'] < time() - $config['max_delete_time'] && $config['max_delete_time'] != false) {
|
if ($password != '' && $post['password'] != $password && (!$thread || $thread['password'] != $password))
|
||||||
error(sprintf($config['error']['delete_too_late'], Format\until($post['time'] + $config['max_delete_time'])));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hash_equals($post['password'], $password) && (!$thread || !hash_equals($thread['password'], $password))) {
|
|
||||||
error($config['error']['invalidpassword']);
|
error($config['error']['invalidpassword']);
|
||||||
|
|
||||||
|
if ($post['time'] > time() - $config['delete_time'] && (!$thread || $thread['password'] != $password)) {
|
||||||
|
error(sprintf($config['error']['delete_too_soon'], until($post['time'] + $config['delete_time'])));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if ($post['time'] > time() - $config['delete_time'] && (!$thread || !hash_equals($thread['password'], $password))) {
|
|
||||||
error(sprintf($config['error']['delete_too_soon'], Format\until($post['time'] + $config['delete_time'])));
|
|
||||||
}
|
|
||||||
|
|
||||||
$ip = $_SERVER['REMOTE_ADDR'];
|
|
||||||
if (isset($_POST['file'])) {
|
if (isset($_POST['file'])) {
|
||||||
// Delete just the file
|
// Delete just the file
|
||||||
deleteFile($id);
|
deleteFile($id);
|
||||||
modLog("User at $ip deleted file from their own post #$id");
|
modLog("User deleted file from his own post #$id");
|
||||||
} else {
|
} else {
|
||||||
// Delete entire post
|
// Delete entire post
|
||||||
deletePost($id);
|
deletePost($id);
|
||||||
modLog("User at $ip deleted their own post #$id");
|
modLog("User deleted his own post #$id");
|
||||||
}
|
}
|
||||||
|
|
||||||
$context->get(LogDriver::class)->log(
|
_syslog(LOG_INFO, 'Deleted post: ' .
|
||||||
LogDriver::INFO,
|
'/' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '')
|
||||||
'Deleted post: /' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '')
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -477,7 +266,7 @@ if (isset($_POST['delete'])) {
|
|||||||
if (function_exists('fastcgi_finish_request'))
|
if (function_exists('fastcgi_finish_request'))
|
||||||
@fastcgi_finish_request();
|
@fastcgi_finish_request();
|
||||||
|
|
||||||
Vichan\Functions\Theme\rebuild_themes('post-delete', $board['uri']);
|
rebuildThemes('post-delete', $board['uri']);
|
||||||
|
|
||||||
} elseif (isset($_POST['report'])) {
|
} elseif (isset($_POST['report'])) {
|
||||||
if (!isset($_POST['board'], $_POST['reason']))
|
if (!isset($_POST['board'], $_POST['reason']))
|
||||||
@ -509,61 +298,45 @@ if (isset($_POST['delete'])) {
|
|||||||
if (count($report) > $config['report_limit'])
|
if (count($report) > $config['report_limit'])
|
||||||
error($config['error']['toomanyreports']);
|
error($config['error']['toomanyreports']);
|
||||||
|
|
||||||
|
if ($config['report_captcha'] && !isset($_POST['captcha_text'], $_POST['captcha_cookie'])) {
|
||||||
if ($config['report_captcha']) {
|
|
||||||
if (!isset($_POST['captcha_text'], $_POST['captcha_cookie'])) {
|
|
||||||
error($config['error']['bot']);
|
error($config['error']['bot']);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if ($config['report_captcha']) {
|
||||||
$query = new NativeCaptchaQuery(
|
$ch = curl_init($config['domain'].'/'.$config['captcha']['provider_check'] . "?" . http_build_query([
|
||||||
$context->get(HttpDriver::class),
|
'mode' => 'check',
|
||||||
$config['domain'],
|
'text' => $_POST['captcha_text'],
|
||||||
$config['captcha']['provider_check'],
|
'extra' => $config['captcha']['extra'],
|
||||||
$config['captcha']['extra']
|
'cookie' => $_POST['captcha_cookie']
|
||||||
);
|
]));
|
||||||
$success = $query->verify(
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||||
$_POST['captcha_text'],
|
$resp = curl_exec($ch);
|
||||||
$_POST['captcha_cookie']
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!$success) {
|
if ($resp !== '1') {
|
||||||
error($config['error']['captcha']);
|
error($config['error']['captcha']);
|
||||||
}
|
}
|
||||||
} catch (RuntimeException $e) {
|
|
||||||
$context->get(LogDriver::class)->log(LogDriver::ERROR, "Native captcha IO exception: {$e->getMessage()}");
|
|
||||||
error($config['error']['local_io_error']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$reason = escape_markup_modifiers($_POST['reason']);
|
$reason = escape_markup_modifiers($_POST['reason']);
|
||||||
markup($reason);
|
markup($reason);
|
||||||
|
|
||||||
if (mb_strlen($reason) > $config['report_max_length']) {
|
|
||||||
error($config['error']['toolongreport']);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($report as &$id) {
|
foreach ($report as &$id) {
|
||||||
$query = prepare(sprintf("SELECT `id`, `thread` FROM ``posts_%s`` WHERE `id` = :id", $board['uri']));
|
$query = prepare(sprintf("SELECT `id`, `thread` FROM ``posts_%s`` WHERE `id` = :id", $board['uri']));
|
||||||
$query->bindValue(':id', $id, PDO::PARAM_INT);
|
$query->bindValue(':id', $id, PDO::PARAM_INT);
|
||||||
$query->execute() or error(db_error($query));
|
$query->execute() or error(db_error($query));
|
||||||
|
|
||||||
$post = $query->fetch(PDO::FETCH_ASSOC);
|
$post = $query->fetch(PDO::FETCH_ASSOC);
|
||||||
if ($post === false) {
|
|
||||||
$context->get(LogDriver::class)->log(LogDriver::INFO, "Failed to report non-existing post #{$id} in {$board['dir']}");
|
|
||||||
error($config['error']['nopost']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$error = event('report', array('ip' => $_SERVER['REMOTE_ADDR'], 'board' => $board['uri'], 'post' => $post, 'reason' => $reason, 'link' => link_for($post)));
|
$error = event('report', array('ip' => $_SERVER['REMOTE_ADDR'], 'board' => $board['uri'], 'post' => $post, 'reason' => $reason, 'link' => link_for($post)));
|
||||||
|
|
||||||
if ($error) {
|
if ($error) {
|
||||||
error($error);
|
error($error);
|
||||||
}
|
}
|
||||||
|
|
||||||
$context->get(LogDriver::class)->log(
|
if ($config['syslog'])
|
||||||
LogDriver::INFO,
|
_syslog(LOG_INFO, 'Reported post: ' .
|
||||||
'Reported post: /'
|
'/' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '') .
|
||||||
. $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '')
|
' for "' . $reason . '"'
|
||||||
. " for \"$reason\""
|
|
||||||
);
|
);
|
||||||
$query = prepare("INSERT INTO ``reports`` VALUES (NULL, :time, :ip, :board, :post, :reason)");
|
$query = prepare("INSERT INTO ``reports`` VALUES (NULL, :time, :ip, :board, :post, :reason)");
|
||||||
$query->bindValue(':time', time(), PDO::PARAM_INT);
|
$query->bindValue(':time', time(), PDO::PARAM_INT);
|
||||||
@ -618,68 +391,41 @@ if (isset($_POST['delete'])) {
|
|||||||
|
|
||||||
|
|
||||||
if (!$dropped_post) {
|
if (!$dropped_post) {
|
||||||
if ($config['simple_spam'] && $post['op']) {
|
|
||||||
if (!isset($_POST['simple_spam']) || strtolower($config['simple_spam']['answer']) != strtolower($_POST['simple_spam'])) {
|
|
||||||
error($config['error']['simple_spam']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if banned
|
// Check if banned
|
||||||
checkBan($board['uri']);
|
checkBan($board['uri']);
|
||||||
|
|
||||||
// Check for CAPTCHA right after opening the board so the "return" link is in there.
|
// Check for CAPTCHA right after opening the board so the "return" link is in there
|
||||||
try {
|
if ($config['recaptcha']) {
|
||||||
$provider = $config['captcha']['provider'];
|
if (!isset($_POST['g-recaptcha-response']))
|
||||||
$new_thread_capt = $config['captcha']['native']['new_thread_capt'];
|
|
||||||
$dynamic = $config['captcha']['dynamic'];
|
|
||||||
|
|
||||||
// With our custom captcha provider
|
|
||||||
if (($provider === 'native' && !$new_thread_capt)
|
|
||||||
|| ($provider === 'native' && $new_thread_capt && $post['op'])) {
|
|
||||||
$query = $context->get(NativeCaptchaQuery::class);
|
|
||||||
$success = $query->verify($_POST['captcha_text'], $_POST['captcha_cookie']);
|
|
||||||
|
|
||||||
if (!$success) {
|
|
||||||
error(
|
|
||||||
"{$config['error']['captcha']}
|
|
||||||
<script>
|
|
||||||
if (actually_load_captcha !== undefined)
|
|
||||||
actually_load_captcha(
|
|
||||||
\"{$config['captcha']['provider_get']}\",
|
|
||||||
\"{$config['captcha']['extra']}\"
|
|
||||||
);
|
|
||||||
</script>"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remote 3rd party captchas.
|
|
||||||
elseif ($provider && (!$dynamic || $dynamic === $_SERVER['REMOTE_ADDR'])) {
|
|
||||||
$query = $content->get(RemoteCaptchaQuery::class);
|
|
||||||
$field = $query->responseField();
|
|
||||||
|
|
||||||
if (!isset($_POST[$field])) {
|
|
||||||
error($config['error']['bot']);
|
error($config['error']['bot']);
|
||||||
}
|
|
||||||
$response = $_POST[$field];
|
|
||||||
/*
|
|
||||||
* Do not query with the IP if the mode is dynamic. This config is meant for proxies and internal
|
|
||||||
* loopback addresses.
|
|
||||||
*/
|
|
||||||
$ip = $dynamic ? null : $_SERVER['REMOTE_ADDR'];
|
|
||||||
|
|
||||||
$success = $query->verify($response, $ip);
|
// Check what reCAPTCHA has to say...
|
||||||
if (!$success) {
|
$resp = json_decode(file_get_contents(sprintf('https://www.recaptcha.net/recaptcha/api/siteverify?secret=%s&response=%s&remoteip=%s',
|
||||||
|
$config['recaptcha_private'],
|
||||||
|
urlencode($_POST['g-recaptcha-response']),
|
||||||
|
$_SERVER['REMOTE_ADDR'])), true);
|
||||||
|
|
||||||
|
if (!$resp['success']) {
|
||||||
error($config['error']['captcha']);
|
error($config['error']['captcha']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (RuntimeException $e) {
|
// Same, but now with our custom captcha provider
|
||||||
$context->get(LogDriver::class)->log(LogDriver::ERROR, "Captcha IO exception: {$e->getMessage()}");
|
if (($config['captcha']['enabled']) || (($post['op']) && ($config['new_thread_capt'])) ) {
|
||||||
error($config['error']['remote_io_error']);
|
$ch = curl_init($config['domain'].'/'.$config['captcha']['provider_check'] . "?" . http_build_query([
|
||||||
} catch (JsonException $e) {
|
'mode' => 'check',
|
||||||
$context->get(LogDriver::class)->log(LogDriver::ERROR, "Bad JSON reply to captcha: {$e->getMessage()}");
|
'text' => $_POST['captcha_text'],
|
||||||
error($config['error']['remote_io_error']);
|
'extra' => $config['captcha']['extra'],
|
||||||
}
|
'cookie' => $_POST['captcha_cookie']
|
||||||
|
]));
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||||
|
$resp = curl_exec($ch);
|
||||||
|
|
||||||
|
if ($resp !== '1') {
|
||||||
|
error($config['error']['captcha'] .
|
||||||
|
'<script>if (actually_load_captcha !== undefined) actually_load_captcha("'.$config['captcha']['provider_get'].'", "'.$config['captcha']['extra'].'");</script>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!(($post['op'] && $_POST['post'] == $config['button_newtopic']) ||
|
if (!(($post['op'] && $_POST['post'] == $config['button_newtopic']) ||
|
||||||
(!$post['op'] && $_POST['post'] == $config['button_reply'])))
|
(!$post['op'] && $_POST['post'] == $config['button_reply'])))
|
||||||
@ -694,7 +440,7 @@ if (isset($_POST['delete'])) {
|
|||||||
|
|
||||||
|
|
||||||
if ($post['mod'] = isset($_POST['mod']) && $_POST['mod']) {
|
if ($post['mod'] = isset($_POST['mod']) && $_POST['mod']) {
|
||||||
check_login($context, false);
|
check_login(false);
|
||||||
if (!$mod) {
|
if (!$mod) {
|
||||||
// Liar. You're not a mod.
|
// Liar. You're not a mod.
|
||||||
error($config['error']['notamod']);
|
error($config['error']['notamod']);
|
||||||
@ -712,6 +458,12 @@ if (isset($_POST['delete'])) {
|
|||||||
error($config['error']['noaccess']);
|
error($config['error']['noaccess']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$post['mod']) {
|
||||||
|
$post['antispam_hash'] = checkSpam(array($board['uri'], isset($post['thread']) ? $post['thread'] : ($config['try_smarter'] && isset($_POST['page']) ? 0 - (int)$_POST['page'] : null)));
|
||||||
|
if ($post['antispam_hash'] === true)
|
||||||
|
error($config['error']['spam']);
|
||||||
|
}
|
||||||
|
|
||||||
if ($config['robot_enable'] && $config['robot_mute']) {
|
if ($config['robot_enable'] && $config['robot_mute']) {
|
||||||
checkMute();
|
checkMute();
|
||||||
}
|
}
|
||||||
@ -769,39 +521,65 @@ if (isset($_POST['delete'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($config['allow_upload_by_url'] && isset($_POST['file_url']) && !empty($_POST['file_url'])) {
|
if ($config['allow_upload_by_url'] && isset($_POST['file_url']) && !empty($_POST['file_url'])) {
|
||||||
$allowed_extensions = $config['allowed_ext_files'];
|
$post['file_url'] = $_POST['file_url'];
|
||||||
|
if (!preg_match('@^https?://@', $post['file_url']))
|
||||||
|
error($config['error']['invalidimg']);
|
||||||
|
|
||||||
|
if (mb_strpos($post['file_url'], '?') !== false)
|
||||||
|
$url_without_params = mb_substr($post['file_url'], 0, mb_strpos($post['file_url'], '?'));
|
||||||
|
else
|
||||||
|
$url_without_params = $post['file_url'];
|
||||||
|
|
||||||
|
$post['extension'] = strtolower(mb_substr($url_without_params, mb_strrpos($url_without_params, '.') + 1));
|
||||||
|
|
||||||
// Add allowed extensions for OP, if enabled.
|
|
||||||
if ($post['op'] && $config['allowed_ext_op']) {
|
if ($post['op'] && $config['allowed_ext_op']) {
|
||||||
array_merge($allowed_extensions, $config['allowed_ext_op']);
|
if (!in_array($post['extension'], $config['allowed_ext_op']))
|
||||||
|
error($config['error']['unknownext']);
|
||||||
}
|
}
|
||||||
|
else if (!in_array($post['extension'], $config['allowed_ext']) && !in_array($post['extension'], $config['allowed_ext_files']))
|
||||||
|
error($config['error']['unknownext']);
|
||||||
|
|
||||||
try {
|
$post['file_tmp'] = tempnam($config['tmp'], 'url');
|
||||||
$ret = download_file_from_url(
|
function unlink_tmp_file($file) {
|
||||||
$context->get(HttpDriver::class),
|
@unlink($file);
|
||||||
$_POST['file_url'],
|
fatal_error_handler();
|
||||||
$config['upload_by_url_timeout'],
|
}
|
||||||
$allowed_extensions,
|
register_shutdown_function('unlink_tmp_file', $post['file_tmp']);
|
||||||
$config['tmp'],
|
|
||||||
$config['error']
|
$fp = fopen($post['file_tmp'], 'w');
|
||||||
|
|
||||||
|
$curl = curl_init();
|
||||||
|
curl_setopt($curl, CURLOPT_URL, $post['file_url']);
|
||||||
|
curl_setopt($curl, CURLOPT_FAILONERROR, true);
|
||||||
|
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, false);
|
||||||
|
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5);
|
||||||
|
curl_setopt($curl, CURLOPT_TIMEOUT, $config['upload_by_url_timeout']);
|
||||||
|
curl_setopt($curl, CURLOPT_USERAGENT, 'Tinyboard');
|
||||||
|
curl_setopt($curl, CURLOPT_BINARYTRANSFER, true);
|
||||||
|
curl_setopt($curl, CURLOPT_FILE, $fp);
|
||||||
|
curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
|
||||||
|
|
||||||
|
if (curl_exec($curl) === false)
|
||||||
|
error($config['error']['nomove'] . '<br/>Curl says: ' . curl_error($curl));
|
||||||
|
|
||||||
|
curl_close($curl);
|
||||||
|
|
||||||
|
fclose($fp);
|
||||||
|
|
||||||
|
$_FILES['file'] = array(
|
||||||
|
'name' => basename($url_without_params),
|
||||||
|
'tmp_name' => $post['file_tmp'],
|
||||||
|
'file_tmp' => true,
|
||||||
|
'error' => 0,
|
||||||
|
'size' => filesize($post['file_tmp'])
|
||||||
);
|
);
|
||||||
if ($ret === false) {
|
|
||||||
error(sprintf3($config['error']['filesize'], array(
|
|
||||||
'filesz' => 'more than that',
|
|
||||||
'maxsz' => number_format($config['max_filesize'])
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
$_FILES['file'] = $ret;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
error($e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$post['name'] = $_POST['name'] != '' ? $_POST['name'] : $config['anonymous'];
|
$post['name'] = $_POST['name'] != '' ? $_POST['name'] : $config['anonymous'];
|
||||||
$post['subject'] = $_POST['subject'];
|
$post['subject'] = $_POST['subject'];
|
||||||
$post['email'] = str_replace(' ', '%20', htmlspecialchars($_POST['email']));
|
$post['email'] = str_replace(' ', '%20', htmlspecialchars($_POST['email']));
|
||||||
$post['body'] = $_POST['body'];
|
$post['body'] = $_POST['body'];
|
||||||
$post['password'] = hashPassword($_POST['password']);
|
$post['password'] = $_POST['password'];
|
||||||
$post['has_file'] = (!isset($post['embed']) && (($post['op'] && !isset($post['no_longer_require_an_image_for_op']) && $config['force_image_op']) || count($_FILES) > 0));
|
$post['has_file'] = (!isset($post['embed']) && (($post['op'] && !isset($post['no_longer_require_an_image_for_op']) && $config['force_image_op']) || count($_FILES) > 0));
|
||||||
|
|
||||||
if (!$dropped_post) {
|
if (!$dropped_post) {
|
||||||
@ -881,12 +659,7 @@ if (isset($_POST['delete'])) {
|
|||||||
|
|
||||||
$trip = generate_tripcode($post['name']);
|
$trip = generate_tripcode($post['name']);
|
||||||
$post['name'] = $trip[0];
|
$post['name'] = $trip[0];
|
||||||
if ($config['disable_tripcodes'] && !$mod) {
|
|
||||||
$post['trip'] = '';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$post['trip'] = isset($trip[1]) ? $trip[1] : ''; // XX: Dropped posts and tripcodes
|
$post['trip'] = isset($trip[1]) ? $trip[1] : ''; // XX: Dropped posts and tripcodes
|
||||||
}
|
|
||||||
|
|
||||||
$noko = false;
|
$noko = false;
|
||||||
if (strtolower($post['email']) == 'noko') {
|
if (strtolower($post['email']) == 'noko') {
|
||||||
@ -957,8 +730,8 @@ if (isset($_POST['delete'])) {
|
|||||||
error(sprintf($config['error']['toolong'], 'subject'));
|
error(sprintf($config['error']['toolong'], 'subject'));
|
||||||
if (!$mod && mb_strlen($post['body']) > $config['max_body'])
|
if (!$mod && mb_strlen($post['body']) > $config['max_body'])
|
||||||
error($config['error']['toolong_body']);
|
error($config['error']['toolong_body']);
|
||||||
if (!$mod && substr_count($post['body'], "\n") >= $config['maximum_lines'])
|
if (mb_strlen($post['password']) > 20)
|
||||||
error($config['error']['toomanylines']);
|
error(sprintf($config['error']['toolong'], 'password'));
|
||||||
}
|
}
|
||||||
wordfilters($post['body']);
|
wordfilters($post['body']);
|
||||||
|
|
||||||
@ -991,12 +764,13 @@ if (isset($_POST['delete'])) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($config['user_flag'] && isset($_POST['user_flag']) && !empty($_POST['user_flag'])) {
|
if ($config['user_flag'] && isset($_POST['user_flag']))
|
||||||
|
if (!empty($_POST['user_flag']) ){
|
||||||
|
|
||||||
$user_flag = $_POST['user_flag'];
|
$user_flag = $_POST['user_flag'];
|
||||||
|
|
||||||
if (!isset($config['user_flags'][$user_flag])) {
|
if (!isset($config['user_flags'][$user_flag]))
|
||||||
error(_('Invalid flag selection!'));
|
error(_('Invalid flag selection!'));
|
||||||
}
|
|
||||||
|
|
||||||
$flag_alt = isset($user_flag_alt) ? $user_flag_alt : $config['user_flags'][$user_flag];
|
$flag_alt = isset($user_flag_alt) ? $user_flag_alt : $config['user_flags'][$user_flag];
|
||||||
|
|
||||||
@ -1014,12 +788,32 @@ if (isset($_POST['delete'])) {
|
|||||||
$post['body'] .= "\n<tinyboard proxy>".$proxy."</tinyboard>";
|
$post['body'] .= "\n<tinyboard proxy>".$proxy."</tinyboard>";
|
||||||
}
|
}
|
||||||
|
|
||||||
$post['body_nomarkup'] = strip_symbols($post['body']);
|
if (mysql_version() >= 50503) {
|
||||||
|
$post['body_nomarkup'] = $post['body']; // Assume we're using the utf8mb4 charset
|
||||||
|
} else {
|
||||||
|
// MySQL's `utf8` charset only supports up to 3-byte symbols
|
||||||
|
// Remove anything >= 0x010000
|
||||||
|
|
||||||
|
$chars = preg_split('//u', $post['body'], -1, PREG_SPLIT_NO_EMPTY);
|
||||||
|
$post['body_nomarkup'] = '';
|
||||||
|
foreach ($chars as $char) {
|
||||||
|
$o = 0;
|
||||||
|
$ord = ordutf8($char, $o);
|
||||||
|
if ($ord >= 0x010000)
|
||||||
|
continue;
|
||||||
|
$post['body_nomarkup'] .= $char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$post['tracked_cites'] = markup($post['body'], true);
|
$post['tracked_cites'] = markup($post['body'], true);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if ($post['has_file']) {
|
if ($post['has_file']) {
|
||||||
|
$md5cmd = false;
|
||||||
|
if ($config['bsd_md5']) $md5cmd = '/sbin/md5 -r';
|
||||||
|
if ($config['gnu_md5']) $md5cmd = 'md5sum';
|
||||||
|
|
||||||
$allhashes = '';
|
$allhashes = '';
|
||||||
|
|
||||||
foreach ($post['files'] as $key => &$file) {
|
foreach ($post['files'] as $key => &$file) {
|
||||||
@ -1040,7 +834,14 @@ if (isset($_POST['delete'])) {
|
|||||||
if (!is_readable($upload))
|
if (!is_readable($upload))
|
||||||
error($config['error']['nomove']);
|
error($config['error']['nomove']);
|
||||||
|
|
||||||
$hash = md5_hash_of_file($config, $upload);
|
if ($md5cmd) {
|
||||||
|
$output = shell_exec_error($md5cmd . " " . escapeshellarg($upload));
|
||||||
|
$output = explode(' ', $output);
|
||||||
|
$hash = $output[0];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$hash = md5_file($upload);
|
||||||
|
}
|
||||||
|
|
||||||
$file['hash'] = $hash;
|
$file['hash'] = $hash;
|
||||||
$allhashes .= $hash;
|
$allhashes .= $hash;
|
||||||
@ -1065,7 +866,7 @@ if (isset($_POST['delete'])) {
|
|||||||
if ($file['is_an_image']) {
|
if ($file['is_an_image']) {
|
||||||
if ($config['ie_mime_type_detection'] !== false) {
|
if ($config['ie_mime_type_detection'] !== false) {
|
||||||
// Check IE MIME type detection XSS exploit
|
// Check IE MIME type detection XSS exploit
|
||||||
$buffer = file_get_contents($upload, false, null, 0, 255);
|
$buffer = file_get_contents($upload, null, null, null, 255);
|
||||||
if (preg_match($config['ie_mime_type_detection'], $buffer)) {
|
if (preg_match($config['ie_mime_type_detection'], $buffer)) {
|
||||||
undoImage($post);
|
undoImage($post);
|
||||||
error($config['error']['mime_exploit']);
|
error($config['error']['mime_exploit']);
|
||||||
@ -1078,26 +879,43 @@ if (isset($_POST['delete'])) {
|
|||||||
if (!$size = @getimagesize($file['tmp_name'])) {
|
if (!$size = @getimagesize($file['tmp_name'])) {
|
||||||
error($config['error']['invalidimg']);
|
error($config['error']['invalidimg']);
|
||||||
}
|
}
|
||||||
if (!in_array($size[2], array(IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_BMP, IMAGETYPE_WEBP))) {
|
if (!in_array($size[2], array(IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_BMP))) {
|
||||||
error($config['error']['invalidimg']);
|
error($config['error']['invalidimg']);
|
||||||
}
|
}
|
||||||
if ($size[0] > $config['max_width'] || $size[1] > $config['max_height']) {
|
if ($size[0] > $config['max_width'] || $size[1] > $config['max_height']) {
|
||||||
error($config['error']['maxsize']);
|
error($config['error']['maxsize']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$file['exif_stripped'] = false;
|
|
||||||
|
|
||||||
if ($file_image_has_operable_metadata && $config['convert_auto_orient']) {
|
if ($config['convert_auto_orient'] && ($file['extension'] == 'jpg' || $file['extension'] == 'jpeg')) {
|
||||||
// The following code corrects the image orientation.
|
// The following code corrects the image orientation.
|
||||||
// Currently only works with the 'convert' option selected but it could easily be expanded to work with the rest if you can be bothered.
|
// Currently only works with the 'convert' option selected but it could easily be expanded to work with the rest if you can be bothered.
|
||||||
if (!($config['redraw_image'] || (($config['strip_exif'] && !$config['use_exiftool'])))) {
|
if (!($config['redraw_image'] || (($config['strip_exif'] && !$config['use_exiftool']) && ($file['extension'] == 'jpg' || $file['extension'] == 'jpeg')))) {
|
||||||
if (in_array($config['thumb_method'], array('convert', 'convert+gifsicle', 'gm', 'gm+gifsicle'))) {
|
if (in_array($config['thumb_method'], array('convert', 'convert+gifsicle', 'gm', 'gm+gifsicle'))) {
|
||||||
$exif = @exif_read_data($file['tmp_name']);
|
$exif = @exif_read_data($file['tmp_name']);
|
||||||
$gm = in_array($config['thumb_method'], array('gm', 'gm+gifsicle'));
|
$gm = in_array($config['thumb_method'], array('gm', 'gm+gifsicle'));
|
||||||
if (isset($exif['Orientation']) && $exif['Orientation'] != 1) {
|
if (isset($exif['Orientation']) && $exif['Orientation'] != 1) {
|
||||||
|
if ($config['convert_manual_orient']) {
|
||||||
|
$error = shell_exec_error(($gm ? 'gm ' : '') . 'convert ' .
|
||||||
|
escapeshellarg($file['tmp_name']) . ' ' .
|
||||||
|
ImageConvert::jpeg_exif_orientation(false, $exif) . ' ' .
|
||||||
|
($config['strip_exif'] ? '+profile "*"' :
|
||||||
|
($config['use_exiftool'] ? '' : '+profile "*"')
|
||||||
|
) . ' ' .
|
||||||
|
escapeshellarg($file['tmp_name']));
|
||||||
|
if ($config['use_exiftool'] && !$config['strip_exif']) {
|
||||||
|
if ($exiftool_error = shell_exec_error(
|
||||||
|
'exiftool -overwrite_original -q -q -orientation=1 -n ' .
|
||||||
|
escapeshellarg($file['tmp_name'])))
|
||||||
|
error(_('exiftool failed!'), null, $exiftool_error);
|
||||||
|
} else {
|
||||||
|
// TODO: Find another way to remove the Orientation tag from the EXIF profile
|
||||||
|
// without needing `exiftool`.
|
||||||
|
}
|
||||||
|
} else {
|
||||||
$error = shell_exec_error(($gm ? 'gm ' : '') . 'convert ' .
|
$error = shell_exec_error(($gm ? 'gm ' : '') . 'convert ' .
|
||||||
escapeshellarg($file['tmp_name']) . ' -auto-orient ' . escapeshellarg($upload));
|
escapeshellarg($file['tmp_name']) . ' -auto-orient ' . escapeshellarg($upload));
|
||||||
|
}
|
||||||
if ($error)
|
if ($error)
|
||||||
error(_('Could not auto-orient image!'), null, $error);
|
error(_('Could not auto-orient image!'), null, $error);
|
||||||
$size = @getimagesize($file['tmp_name']);
|
$size = @getimagesize($file['tmp_name']);
|
||||||
@ -1151,15 +969,11 @@ if (isset($_POST['delete'])) {
|
|||||||
|
|
||||||
$dont_copy_file = false;
|
$dont_copy_file = false;
|
||||||
|
|
||||||
if ($config['redraw_image'] || ($file_image_has_operable_metadata && !$file['exif_stripped'] && $config['strip_exif'])) {
|
if ($config['redraw_image'] || (!@$file['exif_stripped'] && $config['strip_exif'] && ($file['extension'] == 'jpg' || $file['extension'] == 'jpeg'))) {
|
||||||
if (!$config['redraw_image'] && $config['use_exiftool']) {
|
if (!$config['redraw_image'] && $config['use_exiftool']) {
|
||||||
try {
|
if($error = shell_exec_error('exiftool -overwrite_original -ignoreMinorErrors -q -q -all= ' .
|
||||||
$file['size'] = strip_image_metadata($file['tmp_name']);
|
escapeshellarg($file['tmp_name'])))
|
||||||
} catch (RuntimeException $e) {
|
|
||||||
$context->get(LogDriver::class)->log(LogDriver::ERROR, "Could not strip image metadata: {$e->getMessage()}");
|
|
||||||
// Since EXIF metadata can countain sensible info, fail the request.
|
|
||||||
error(_('Could not strip EXIF metadata!'), null, $error);
|
error(_('Could not strip EXIF metadata!'), null, $error);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$image->to($file['file']);
|
$image->to($file['file']);
|
||||||
$dont_copy_file = true;
|
$dont_copy_file = true;
|
||||||
@ -1168,6 +982,7 @@ if (isset($_POST['delete'])) {
|
|||||||
$image->destroy();
|
$image->destroy();
|
||||||
} else {
|
} else {
|
||||||
// not an image
|
// not an image
|
||||||
|
//copy($config['file_thumb'], $post['thumb']);
|
||||||
$file['thumb'] = 'file';
|
$file['thumb'] = 'file';
|
||||||
|
|
||||||
$size = @getimagesize(sprintf($config['file_thumb'],
|
$size = @getimagesize(sprintf($config['file_thumb'],
|
||||||
@ -1185,16 +1000,23 @@ if (isset($_POST['delete'])) {
|
|||||||
$fname = $file['thumb'];
|
$fname = $file['thumb'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($fname !== 'spoiler') { // We don't have that much CPU time, do we?
|
if ($fname == 'spoiler') { // We don't have that much CPU time, do we?
|
||||||
try {
|
}
|
||||||
$txt = ocr_image($config, $fname);
|
else {
|
||||||
if ($txt !== '') {
|
$tmpname = "tmp/tesseract/".rand(0,10000000);
|
||||||
|
|
||||||
|
// Preprocess command is an ImageMagick b/w quantization
|
||||||
|
$error = shell_exec_error(sprintf($config['tesseract_preprocess_command'], escapeshellarg($fname)) . " | " .
|
||||||
|
'tesseract stdin '.escapeshellarg($tmpname).' '.$config['tesseract_params']);
|
||||||
|
$tmpname .= ".txt";
|
||||||
|
|
||||||
|
$value = @file_get_contents($tmpname);
|
||||||
|
@unlink($tmpname);
|
||||||
|
|
||||||
|
if ($value && trim($value)) {
|
||||||
// This one has an effect, that the body is appended to a post body. So you can write a correct
|
// This one has an effect, that the body is appended to a post body. So you can write a correct
|
||||||
// spamfilter.
|
// spamfilter.
|
||||||
$post['body_nomarkup'] .= "<tinyboard ocr image $key>" . htmlspecialchars($txt) . "</tinyboard>";
|
$post['body_nomarkup'] .= "<tinyboard ocr image $key>".htmlspecialchars($value)."</tinyboard>";
|
||||||
}
|
|
||||||
} catch (RuntimeException $e) {
|
|
||||||
$context->get(LogDriver::class)->log(LogDriver::ERROR, "Could not OCR image: {$e->getMessage()}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1275,6 +1097,8 @@ if (isset($_POST['delete'])) {
|
|||||||
}
|
}
|
||||||
$post = (array)$post;
|
$post = (array)$post;
|
||||||
|
|
||||||
|
if ($post['files'])
|
||||||
|
$post['files'] = $post['files'];
|
||||||
$post['num_files'] = sizeof($post['files']);
|
$post['num_files'] = sizeof($post['files']);
|
||||||
|
|
||||||
$post['id'] = $id = post($post);
|
$post['id'] = $id = post($post);
|
||||||
@ -1320,7 +1144,11 @@ if (isset($_POST['delete'])) {
|
|||||||
|
|
||||||
// Handle cyclical threads
|
// Handle cyclical threads
|
||||||
if (!$post['op'] && isset($thread['cycle']) && $thread['cycle']) {
|
if (!$post['op'] && isset($thread['cycle']) && $thread['cycle']) {
|
||||||
delete_cyclical_posts($board['uri'], $post['thread'], $config['cycle_limit']);
|
// Query is a bit weird due to "This version of MariaDB doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'" (MariaDB Ver 15.1 Distrib 10.0.17-MariaDB, for Linux (x86_64))
|
||||||
|
$query = prepare(sprintf('DELETE FROM ``posts_%s`` WHERE `thread` = :thread AND `id` NOT IN (SELECT `id` FROM (SELECT `id` FROM ``posts_%s`` WHERE `thread` = :thread ORDER BY `id` DESC LIMIT :limit) i)', $board['uri'], $board['uri']));
|
||||||
|
$query->bindValue(':thread', $post['thread']);
|
||||||
|
$query->bindValue(':limit', $config['cycle_limit'], PDO::PARAM_INT);
|
||||||
|
$query->execute() or error(db_error($query));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($post['antispam_hash'])) {
|
if (isset($post['antispam_hash'])) {
|
||||||
@ -1343,22 +1171,14 @@ if (isset($_POST['delete'])) {
|
|||||||
|
|
||||||
if (isset($_SERVER['HTTP_REFERER'])) {
|
if (isset($_SERVER['HTTP_REFERER'])) {
|
||||||
// Tell Javascript that we posted successfully
|
// Tell Javascript that we posted successfully
|
||||||
if (isset($_COOKIE[$config['cookies']['js']])) {
|
if (isset($_COOKIE[$config['cookies']['js']]))
|
||||||
$js = json_decode($_COOKIE[$config['cookies']['js']]);
|
$js = json_decode($_COOKIE[$config['cookies']['js']]);
|
||||||
} else {
|
else
|
||||||
$js = (object) array();
|
$js = (object) array();
|
||||||
}
|
|
||||||
// Tell it to delete the cached post for referer
|
// Tell it to delete the cached post for referer
|
||||||
$js->{$_SERVER['HTTP_REFERER']} = true;
|
$js->{$_SERVER['HTTP_REFERER']} = true;
|
||||||
|
// Encode and set cookie
|
||||||
// Encode and set cookie.
|
setcookie($config['cookies']['js'], json_encode($js), 0, $config['cookies']['jail'] ? $config['cookies']['path'] : '/', null, false, false);
|
||||||
$options = [
|
|
||||||
'expires' => 0,
|
|
||||||
'path' => $config['cookies']['jail'] ? $config['cookies']['path'] : '/',
|
|
||||||
'httponly' => false,
|
|
||||||
'samesite' => 'Strict'
|
|
||||||
];
|
|
||||||
setcookie($config['cookies']['js'], json_encode($js), $options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$root = $post['mod'] ? $config['root'] . $config['file_mod'] . '?/' : $config['root'];
|
$root = $post['mod'] ? $config['root'] . $config['file_mod'] . '?/' : $config['root'];
|
||||||
@ -1388,13 +1208,20 @@ if (isset($_POST['delete'])) {
|
|||||||
|
|
||||||
buildThread($post['op'] ? $id : $post['thread']);
|
buildThread($post['op'] ? $id : $post['thread']);
|
||||||
|
|
||||||
$context->get(LogDriver::class)->log(
|
if ($config['syslog'])
|
||||||
LogDriver::INFO,
|
_syslog(LOG_INFO, 'New post: /' . $board['dir'] . $config['dir']['res'] .
|
||||||
'New post: /' . $board['dir'] . $config['dir']['res'] . link_for($post) . (!$post['op'] ? '#' . $id : '')
|
link_for($post) . (!$post['op'] ? '#' . $id : ''));
|
||||||
);
|
|
||||||
|
|
||||||
if (!$post['mod']) header('X-Associated-Content: "' . $redirect . '"');
|
if (!$post['mod']) header('X-Associated-Content: "' . $redirect . '"');
|
||||||
|
|
||||||
|
// Any telegrams to show?
|
||||||
|
$query = prepare('SELECT * FROM ``telegrams`` WHERE ``ip`` = :ip AND ``seen`` = 0');
|
||||||
|
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
|
||||||
|
$query->execute() or error(db_error($query));
|
||||||
|
$telegrams = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (count($telegrams) > 0)
|
||||||
|
goto skip_redirect;
|
||||||
|
|
||||||
if (!isset($_POST['json_response'])) {
|
if (!isset($_POST['json_response'])) {
|
||||||
header('Location: ' . $redirect, true, $config['redirect_http']);
|
header('Location: ' . $redirect, true, $config['redirect_http']);
|
||||||
@ -1406,6 +1233,8 @@ if (isset($_POST['delete'])) {
|
|||||||
'id' => $id
|
'id' => $id
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
skip_redirect:
|
||||||
|
|
||||||
|
|
||||||
if ($config['try_smarter'] && $post['op'])
|
if ($config['try_smarter'] && $post['op'])
|
||||||
$build_pages = range(1, $config['max_pages']);
|
$build_pages = range(1, $config['max_pages']);
|
||||||
@ -1417,14 +1246,28 @@ if (isset($_POST['delete'])) {
|
|||||||
|
|
||||||
buildIndex();
|
buildIndex();
|
||||||
|
|
||||||
|
if (count($telegrams) > 0) {
|
||||||
|
$ids = implode(', ', array_map(function($x) { return (int)$x['id']; }, $telegrams));
|
||||||
|
query("UPDATE ``telegrams`` SET ``seen`` = 1 WHERE ``id`` IN({$ids})") or error(db_error());
|
||||||
|
die(Element('page.html', array(
|
||||||
|
'title' => _('Important message from Moderation'),
|
||||||
|
'config' => $config,
|
||||||
|
'body' => Element('important.html', array(
|
||||||
|
'config' => $config,
|
||||||
|
'redirect' => $redirect,
|
||||||
|
'telegrams' => $telegrams,
|
||||||
|
))
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
// We are already done, let's continue our heavy-lifting work in the background (if we run off FastCGI)
|
// We are already done, let's continue our heavy-lifting work in the background (if we run off FastCGI)
|
||||||
if (function_exists('fastcgi_finish_request'))
|
if (function_exists('fastcgi_finish_request'))
|
||||||
@fastcgi_finish_request();
|
@fastcgi_finish_request();
|
||||||
|
|
||||||
if ($post['op'])
|
if ($post['op'])
|
||||||
Vichan\Functions\Theme\rebuild_themes('post-thread', $board['uri']);
|
rebuildThemes('post-thread', $board['uri']);
|
||||||
else
|
else
|
||||||
Vichan\Functions\Theme\rebuild_themes('post', $board['uri']);
|
rebuildThemes('post', $board['uri']);
|
||||||
|
|
||||||
} elseif (isset($_POST['appeal'])) {
|
} elseif (isset($_POST['appeal'])) {
|
||||||
if (!isset($_POST['ban_id']))
|
if (!isset($_POST['ban_id']))
|
||||||
@ -1432,31 +1275,32 @@ if (isset($_POST['delete'])) {
|
|||||||
|
|
||||||
$ban_id = (int)$_POST['ban_id'];
|
$ban_id = (int)$_POST['ban_id'];
|
||||||
|
|
||||||
$ban = Bans::findSingle($_SERVER['REMOTE_ADDR'], $ban_id, $config['require_ban_view'], $config['auto_maintenance']);
|
$bans = Bans::find($_SERVER['REMOTE_ADDR']);
|
||||||
|
foreach ($bans as $_ban) {
|
||||||
|
if ($_ban['id'] == $ban_id) {
|
||||||
|
$ban = $_ban;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($ban)) {
|
if (!isset($ban)) {
|
||||||
error($config['error']['noban']);
|
error(_("That ban doesn't exist or is not for you."));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($ban['expires'] && $ban['expires'] - $ban['created'] <= $config['ban_appeals_min_length']) {
|
if ($ban['expires'] && $ban['expires'] - $ban['created'] <= $config['ban_appeals_min_length']) {
|
||||||
error($config['error']['tooshortban']);
|
error(_("You cannot appeal a ban of this length."));
|
||||||
}
|
}
|
||||||
|
|
||||||
$query = query("SELECT `denied` FROM ``ban_appeals`` WHERE `ban_id` = $ban_id") or error(db_error());
|
$query = query("SELECT `denied` FROM ``ban_appeals`` WHERE `ban_id` = $ban_id") or error(db_error());
|
||||||
$ban_appeals = $query->fetchAll(PDO::FETCH_COLUMN);
|
$ban_appeals = $query->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
if (count($ban_appeals) >= $config['ban_appeals_max']) {
|
if (count($ban_appeals) >= $config['ban_appeals_max']) {
|
||||||
error($config['error']['toomanyappeals']);
|
error(_("You cannot appeal this ban again."));
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($ban_appeals as $is_denied) {
|
foreach ($ban_appeals as $is_denied) {
|
||||||
if (!$is_denied) {
|
if (!$is_denied)
|
||||||
error($config['error']['pendingappeal']);
|
error(_("There is already a pending appeal for this ban."));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strlen($_POST['appeal']) > $config['ban_appeal_max_chars']) {
|
|
||||||
error($config['error']['toolongappeal']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$query = prepare("INSERT INTO ``ban_appeals`` VALUES (NULL, :ban_id, :time, :message, 0)");
|
$query = prepare("INSERT INTO ``ban_appeals`` VALUES (NULL, :ban_id, :time, :message, 0)");
|
||||||
|
@ -9,22 +9,15 @@
|
|||||||
$queries_per_minutes_all = $config['search']['queries_per_minutes_all'];
|
$queries_per_minutes_all = $config['search']['queries_per_minutes_all'];
|
||||||
$search_limit = $config['search']['search_limit'];
|
$search_limit = $config['search']['search_limit'];
|
||||||
|
|
||||||
//Is there a whitelist? Let's list those boards and if not, let's list everything.
|
|
||||||
if (isset($config['search']['boards'])) {
|
if (isset($config['search']['boards'])) {
|
||||||
$boards = $config['search']['boards'];
|
$boards = $config['search']['boards'];
|
||||||
} else {
|
} else {
|
||||||
$boards = listBoards(TRUE);
|
$boards = listBoards(TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Let's remove any disallowed boards from the above list (the blacklist)
|
|
||||||
if (isset($config['search']['disallowed_boards'])) {
|
|
||||||
$boards = array_values(array_diff($boards, $config['search']['disallowed_boards']));
|
|
||||||
}
|
|
||||||
|
|
||||||
$body = Element('search_form.html', Array('boards' => $boards, 'board' => isset($_GET['board']) ? $_GET['board'] : false, 'search' => isset($_GET['search']) ? str_replace('"', '"', utf8tohtml($_GET['search'])) : false));
|
$body = Element('search_form.html', Array('boards' => $boards, 'board' => isset($_GET['board']) ? $_GET['board'] : false, 'search' => isset($_GET['search']) ? str_replace('"', '"', utf8tohtml($_GET['search'])) : false));
|
||||||
|
|
||||||
if(isset($_GET['search']) && !empty($_GET['search']) && isset($_GET['board']) && in_array($_GET['board'], $boards)) {
|
if(isset($_GET['search']) && !empty($_GET['search']) && isset($_GET['board']) && in_array($_GET['board'], $boards)) {
|
||||||
|
|
||||||
$phrase = $_GET['search'];
|
$phrase = $_GET['search'];
|
||||||
$_body = '';
|
$_body = '';
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 15 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user