commit
e7c779796e
616 changed files with 41020 additions and 40006 deletions
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<buildpath>
|
||||
<buildpathentry kind="src" path=""/>
|
||||
<buildpathentry kind="con" path="org.eclipse.php.core.LANGUAGE"/>
|
||||
</buildpath>
|
7
.dockerignore
Normal file
7
.dockerignore
Normal file
|
@ -0,0 +1,7 @@
|
|||
vendor
|
||||
.git
|
||||
*.phar
|
||||
data
|
||||
images
|
||||
thumbs
|
||||
*.sqlite
|
21
.editorconfig
Normal file
21
.editorconfig
Normal file
|
@ -0,0 +1,21 @@
|
|||
# In retrospect I'm less of a fan of tabs for indentation, because
|
||||
# while they're better when they work, they're worse when they don't
|
||||
# work, and so many people use terrible editors when they don't work
|
||||
# that everything is inconsistent... but tabs are what Shimmie went
|
||||
# with back in the 90's, so that's what we use now, and we deal with
|
||||
# the pain of making sure everybody configures their editor properly
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,css,php}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.php text eol=lf
|
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Server Software, if you're the server admin (please complete the following information):**
|
||||
- Shimmie version
|
||||
- Database [mysql, postgres, ...]
|
||||
- Web server [apache, nginx, ...]
|
||||
|
||||
**Client Software (please complete the following information):**
|
||||
- Device [e.g. iphone, windows desktop]
|
||||
- Browser [e.g. chrome, safari]
|
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**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.
|
13
.github/workflows/docker.yml
vendored
Normal file
13
.github/workflows/docker.yml
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
name: Docker Push
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Publish to Registry
|
||||
uses: elgohr/Publish-Docker-Github-Action@master
|
||||
with:
|
||||
name: shish2k/shimmie2
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
76
.github/workflows/tests.yml
vendored
Normal file
76
.github/workflows/tests.yml
vendored
Normal file
|
@ -0,0 +1,76 @@
|
|||
name: Unit Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: PHP ${{ matrix.php }} / DB ${{ matrix.database }}
|
||||
strategy:
|
||||
max-parallel: 3
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: ['7.3']
|
||||
database: ['pgsql', 'mysql', 'sqlite']
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@master
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
coverage: pcov
|
||||
extensions: mbstring
|
||||
|
||||
- name: Set up database
|
||||
run: |
|
||||
mkdir -p data/config
|
||||
if [[ "${{ matrix.database }}" == "pgsql" ]]; then
|
||||
sudo apt update && sudo apt-get install -y postgresql postgresql-client ;
|
||||
psql --version ;
|
||||
sudo -u postgres psql -c "SELECT set_config('log_statement', 'all', false);" -U postgres ;
|
||||
sudo -u postgres psql -c "CREATE USER shimmie WITH PASSWORD 'shimmie';" -U postgres ;
|
||||
sudo -u postgres psql -c "CREATE DATABASE shimmie WITH OWNER shimmie;" -U postgres ;
|
||||
fi
|
||||
if [[ "${{ matrix.database }}" == "mysql" ]]; then
|
||||
sudo systemctl start mysql ;
|
||||
mysql --version ;
|
||||
mysql -e "SET GLOBAL general_log = 'ON';" -uroot -proot ;
|
||||
mysql -e "CREATE DATABASE shimmie;" -uroot -proot ;
|
||||
fi
|
||||
if [[ "${{ matrix.database }}" == "sqlite" ]]; then
|
||||
sudo apt update && sudo apt-get install -y sqlite3 ;
|
||||
sqlite3 --version ;
|
||||
fi
|
||||
|
||||
- name: Check versions
|
||||
run: php -v && composer -V
|
||||
|
||||
- name: Validate composer.json and composer.lock
|
||||
run: composer validate
|
||||
|
||||
- name: Install PHP dependencies
|
||||
run: composer install --prefer-dist --no-progress --no-suggest
|
||||
|
||||
- name: Install shimmie
|
||||
run: php index.php
|
||||
|
||||
- name: Run test suite
|
||||
run: |
|
||||
if [[ "${{ matrix.database }}" == "pgsql" ]]; then
|
||||
export DSN="pgsql:user=shimmie;password=shimmie;host=127.0.0.1;dbname=shimmie"
|
||||
fi
|
||||
if [[ "${{ matrix.database }}" == "mysql" ]]; then
|
||||
export DSN="mysql:user=root;password=root;host=127.0.0.1;dbname=shimmie"
|
||||
fi
|
||||
if [[ "${{ matrix.database }}" == "sqlite" ]]; then
|
||||
export DSN="sqlite:data/shimmie.sqlite"
|
||||
fi
|
||||
vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover=data/coverage.clover
|
||||
|
||||
- name: Upload coverage
|
||||
run: |
|
||||
wget https://scrutinizer-ci.com/ocular.phar
|
||||
php ocular.phar code-coverage:upload --format=php-clover data/coverage.clover
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -2,10 +2,11 @@ backup
|
|||
data
|
||||
images
|
||||
thumbs
|
||||
!lib/images
|
||||
*.phar
|
||||
*.sqlite
|
||||
/lib/vendor/
|
||||
*.cache
|
||||
.devcontainer
|
||||
trace.json
|
||||
|
||||
#Composer
|
||||
composer.phar
|
||||
|
@ -54,7 +55,6 @@ Icon
|
|||
*.un~
|
||||
Session.vim
|
||||
.netrwhist
|
||||
*~
|
||||
|
||||
|
||||
### PhpStorm ###
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
# rather than link to images/ha/hash and have an ugly filename,
|
||||
# we link to images/hash/tags.ext; mod_rewrite splits things so
|
||||
# that shimmie sees hash and the user sees tags.ext
|
||||
RewriteRule ^_images/([0-9a-f]{2})([0-9a-f]{30}).*$ images/$1/$1$2 [L]
|
||||
RewriteRule ^_thumbs/([0-9a-f]{2})([0-9a-f]{30}).*$ thumbs/$1/$1$2 [L]
|
||||
RewriteRule ^_images/([0-9a-f]{2})([0-9a-f]{30}).*$ data/images/$1/$1$2 [L]
|
||||
RewriteRule ^_thumbs/([0-9a-f]{2})([0-9a-f]{30}).*$ data/thumbs/$1/$1$2 [L]
|
||||
|
||||
# any requests for files which don't physically exist should be handled by index.php
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
|
@ -27,7 +27,7 @@
|
|||
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
<FilesMatch "([0-9a-f]{32}|\.(gif|jpe?g|png|css|js))$">
|
||||
<FilesMatch "([0-9a-f]{32}|\.(gif|jpe?g|png|webp|css|js))$">
|
||||
<IfModule mod_headers.c>
|
||||
Header set Cache-Control "public, max-age=2629743"
|
||||
</IfModule>
|
||||
|
@ -46,6 +46,7 @@
|
|||
AddType image/jpeg jpg jpeg
|
||||
AddType image/gif gif
|
||||
AddType image/png png
|
||||
AddType image/webp webp
|
||||
|
||||
#EXT: handle_ico
|
||||
AddType image/x-icon ico ani cur
|
||||
|
|
19
.php_cs.dist
Normal file
19
.php_cs.dist
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->exclude('ext/amazon_s3/lib')
|
||||
->exclude('vendor')
|
||||
->exclude('data')
|
||||
->in(__DIR__)
|
||||
;
|
||||
|
||||
return PhpCsFixer\Config::create()
|
||||
->setRules([
|
||||
'@PSR2' => true,
|
||||
//'strict_param' => true,
|
||||
'array_syntax' => ['syntax' => 'short'],
|
||||
])
|
||||
->setFinder($finder)
|
||||
;
|
||||
|
||||
?>
|
23
.project
23
.project
|
@ -1,23 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>Shimmie 2</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.wst.validation.validationbuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.dltk.core.scriptbuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.php.core.PHPNature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
|
|
@ -3,7 +3,17 @@ imports:
|
|||
- php
|
||||
|
||||
filter:
|
||||
excluded_paths: [lib/*,ext/*/lib/*,ext/tagger/script.js,ext/chatbox/*]
|
||||
excluded_paths: [ext/*/lib/*,ext/tagger/script.js,tests/*]
|
||||
|
||||
build:
|
||||
nodes:
|
||||
analysis:
|
||||
tests:
|
||||
before:
|
||||
- mkdir -p data/config
|
||||
- cp tests/defines.php data/config/shimmie.conf.php
|
||||
override:
|
||||
- php-scrutinizer-run
|
||||
|
||||
tools:
|
||||
external_code_coverage: true
|
||||
|
|
62
.travis.yml
62
.travis.yml
|
@ -1,62 +0,0 @@
|
|||
language: php
|
||||
php:
|
||||
- 5.6
|
||||
- 7.0
|
||||
- 7.1
|
||||
|
||||
sudo: false
|
||||
|
||||
env:
|
||||
matrix:
|
||||
- DB=mysql
|
||||
- DB=pgsql
|
||||
- DB=sqlite
|
||||
allow_failures:
|
||||
- DB=sqlite
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- vendor
|
||||
- $HOME/.composer/cache
|
||||
|
||||
before_install:
|
||||
- travis_retry composer self-update && composer --version #travis is bad at updating composer
|
||||
- if [ -n "$GH_TOKEN" ]; then composer config github-oauth.github.com ${GH_TOKEN}; fi;
|
||||
|
||||
install:
|
||||
- mkdir -p data/config
|
||||
- |
|
||||
if [[ "$DB" == "pgsql" ]]; then
|
||||
psql -c "SELECT set_config('log_statement', 'all', false);" -U postgres ;
|
||||
psql -c "CREATE DATABASE shimmie;" -U postgres ;
|
||||
echo '<?php define("DATABASE_DSN", "pgsql:user=postgres;password=;host=;dbname=shimmie");' > data/config/auto_install.conf.php ;
|
||||
fi
|
||||
- |
|
||||
if [[ "$DB" == "mysql" ]]; then
|
||||
mysql -e "SET GLOBAL general_log = 'ON';" -uroot ;
|
||||
mysql -e "CREATE DATABASE shimmie;" -uroot ;
|
||||
echo '<?php define("DATABASE_DSN", "mysql:user=root;password=;host=localhost;dbname=shimmie");' > data/config/auto_install.conf.php ;
|
||||
fi
|
||||
- if [[ "$DB" == "sqlite" ]]; then echo '<?php define("DATABASE_DSN", "sqlite:shimmie.sqlite");' > data/config/auto_install.conf.php ; fi
|
||||
- composer install
|
||||
- php install.php
|
||||
|
||||
script:
|
||||
- vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover=data/coverage.clover
|
||||
|
||||
after_failure:
|
||||
- head -n 100 data/config/*
|
||||
- ls /var/run/mysql*
|
||||
# All of the below commands require sudo, which we can't use without losing some speed & caching.
|
||||
# SEE: https://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
# - ls /var/log/*mysql*
|
||||
# - cat /var/log/mysql.err
|
||||
# - cat /var/log/mysql.log
|
||||
# - cat /var/log/mysql/error.log
|
||||
# - cat /var/log/mysql/slow.log
|
||||
# - ls /var/log/postgresql
|
||||
# - cat /var/log/postgresql/postgresql*
|
||||
|
||||
after_script:
|
||||
- wget https://scrutinizer-ci.com/ocular.phar
|
||||
- php ocular.phar code-coverage:upload --format=php-clover data/coverage.clover
|
21
Dockerfile
Normal file
21
Dockerfile
Normal file
|
@ -0,0 +1,21 @@
|
|||
FROM debian:stable-slim
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
EXPOSE 8000
|
||||
RUN apt update && apt install -y curl
|
||||
HEALTHCHECK --interval=5m --timeout=3s CMD curl --fail http://127.0.0.1:8000/ || exit 1
|
||||
|
||||
RUN apt install -y php7.3-cli php7.3-gd php7.3-pgsql php7.3-mysql php7.3-sqlite3 php7.3-zip php7.3-dom php7.3-mbstring php-xdebug
|
||||
RUN apt install -y composer imagemagick vim zip unzip
|
||||
|
||||
COPY composer.json composer.lock /app/
|
||||
WORKDIR /app
|
||||
RUN composer install
|
||||
|
||||
COPY . /app/
|
||||
RUN echo '=== Installing ===' && mkdir -p data/config && echo "<?php \$dsn = \"sqlite:data/shimmie.sqlite\";" > data/config/auto_install.conf.php && php index.php && \
|
||||
echo '=== Smoke Test ===' && php index.php get-page /post/list && \
|
||||
echo '=== Unit Tests ===' && ./vendor/bin/phpunit --configuration tests/phpunit.xml && \
|
||||
echo '=== Coverage ===' && ./vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-text && \
|
||||
echo '=== Cleaning ===' && rm -rf data
|
||||
RUN chmod +x /app/tests/docker-init.sh
|
||||
CMD "/app/tests/docker-init.sh"
|
|
@ -10,15 +10,9 @@
|
|||
|
||||
# Shimmie
|
||||
|
||||
[![Build Status](https://travis-ci.org/shish/shimmie2.svg?branch=master)](https://travis-ci.org/shish/shimmie2)
|
||||
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/shish/shimmie2/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=master)
|
||||
[![Unit Tests](https://github.com/shish/shimmie2/workflows/Unit%20Tests/badge.svg)](https://github.com/shish/shimmie2/actions)
|
||||
[![Code Quality](https://scrutinizer-ci.com/g/shish/shimmie2/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=master)
|
||||
[![Code Coverage](https://scrutinizer-ci.com/g/shish/shimmie2/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=master)
|
||||
(master)
|
||||
|
||||
[![Build Status](https://travis-ci.org/shish/shimmie2.svg?branch=develop)](https://travis-ci.org/shish/shimmie2)
|
||||
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/shish/shimmie2/badges/quality-score.png?b=develop)](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=develop)
|
||||
[![Code Coverage](https://scrutinizer-ci.com/g/shish/shimmie2/badges/coverage.png?b=develop)](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=develop)
|
||||
(develop)
|
||||
|
||||
This is the main branch of Shimmie, if you know anything at all about running
|
||||
websites, this is the version to use.
|
||||
|
@ -28,8 +22,11 @@ check out one of the versioned branches.
|
|||
|
||||
# Requirements
|
||||
|
||||
- MySQL/MariaDB 5.1+ (with experimental support for PostgreSQL 9+ and SQLite 3)
|
||||
- [Stable PHP](https://en.wikipedia.org/wiki/PHP#Release_history) (5.6+ as of writing)
|
||||
- These are generally based on "whatever is in Debian Stable", because that's
|
||||
conservative without being TOO painfully out of date, and is a nice target
|
||||
for the unit test Docker build.
|
||||
- A database: PostgreSQL 11+ / MariaDB 10.3+ / SQLite 3.27+
|
||||
- [Stable PHP](https://en.wikipedia.org/wiki/PHP#Release_history) (7.3+ as of writing)
|
||||
- GD or ImageMagick
|
||||
|
||||
# Installation
|
||||
|
@ -44,46 +41,41 @@ check out one of the versioned branches.
|
|||
|
||||
# Installation (Development)
|
||||
|
||||
1. Download shimmie via the "Download Zip" button on the [develop](https://github.com/shish/shimmie2/tree/develop) branch.
|
||||
1. Download shimmie via the "Download Zip" button on the [master](https://github.com/shish/shimmie2/tree/master) branch.
|
||||
2. Unzip shimmie into a folder on the web host
|
||||
3. Install [Composer](https://getcomposer.org/). (If you don't already have it)
|
||||
4. Run `composer install` in the shimmie folder.
|
||||
5. Follow instructions noted in "Installation" starting from step 3.
|
||||
|
||||
## Upgrade from 2.3.X
|
||||
# Docker
|
||||
|
||||
1. Backup your current files and database!
|
||||
2. Unzip into a clean folder
|
||||
3. Copy across the images, thumbs, and data folders
|
||||
4. Move `old/config.php` to `new/data/config/shimmie.conf.php`
|
||||
5. Edit `shimmie.conf.php` to use the new database connection format:
|
||||
Useful for testing in a known-good environment, this command will build a
|
||||
simple debian image and run all the unit tests inside it:
|
||||
|
||||
OLD Format:
|
||||
```php
|
||||
$database_dsn = "<proto>://<username>:<password>@<host>/<database>";
|
||||
```
|
||||
docker build -t shimmie .
|
||||
```
|
||||
|
||||
NEW Format:
|
||||
```php
|
||||
define("DATABASE_DSN", "<proto>:user=<username>;password=<password>;host=<host>;dbname=<database>");
|
||||
Once you have an image which has passed all tests, you can then run it to get
|
||||
a live system:
|
||||
|
||||
```
|
||||
docker run -p 0.0.0.0:8123:8000 shimmie
|
||||
```
|
||||
|
||||
The rest should be automatic~
|
||||
|
||||
If there are any errors with the upgrade process, `in_upgrade=true` will
|
||||
be left in the config table and the process will be paused for the admin
|
||||
to investigate.
|
||||
|
||||
Deleting this config entry and refreshing the page should continue the upgrade from where it left off.
|
||||
Then you can visit your server on port 8123 to see the site.
|
||||
|
||||
Note that the docker image is entirely self-contained and has no persistence
|
||||
(assuming you use the sqlite database); each `docker run` will give a clean
|
||||
un-installed image.
|
||||
|
||||
### Upgrade from earlier versions
|
||||
|
||||
I very much recommend going via each major release in turn (eg, 2.0.6
|
||||
-> 2.1.3 -> 2.2.4 -> 2.3.0 rather than 2.0.6 -> 2.3.0).
|
||||
|
||||
While the basic database and file formats haven't changed *completely*, it's different
|
||||
enough to be a pain.
|
||||
While the basic database and file formats haven't changed *completely*, it's
|
||||
different enough to be a pain.
|
||||
|
||||
|
||||
## Custom Configuration
|
||||
|
@ -91,7 +83,7 @@ enough to be a pain.
|
|||
Various aspects of Shimmie can be configured to suit your site specific needs
|
||||
via the file `data/config/shimmie.conf.php` (created after installation).
|
||||
|
||||
Take a look at `core/sys_config.inc.php` for the available options that can
|
||||
Take a look at `core/sys_config.php` for the available options that can
|
||||
be used.
|
||||
|
||||
|
||||
|
@ -100,35 +92,36 @@ be used.
|
|||
User classes can be added to or altered by placing them in
|
||||
`data/config/user-classes.conf.php`.
|
||||
|
||||
For example, one can override the default anonymous "allow nothing" permissions like so:
|
||||
For example, one can override the default anonymous "allow nothing"
|
||||
permissions like so:
|
||||
|
||||
```php
|
||||
new UserClass("anonymous", "base", array(
|
||||
"create_comment" => True,
|
||||
"edit_image_tag" => True,
|
||||
"edit_image_source" => True,
|
||||
"create_image_report" => True,
|
||||
));
|
||||
new UserClass("anonymous", "base", [
|
||||
Permissions::CREATE_COMMENT => True,
|
||||
Permissions::EDIT_IMAGE_TAG => True,
|
||||
Permissions::EDIT_IMAGE_SOURCE => True,
|
||||
Permissions::CREATE_IMAGE_REPORT => True,
|
||||
]);
|
||||
```
|
||||
|
||||
For a moderator class, being a regular user who can delete images and comments:
|
||||
|
||||
```php
|
||||
new UserClass("moderator", "user", array(
|
||||
"delete_image" => True,
|
||||
"delete_comment" => True,
|
||||
));
|
||||
new UserClass("moderator", "user", [
|
||||
Permissions::DELETE_IMAGE => True,
|
||||
Permissions::DELETE_COMMENT => True,
|
||||
]);
|
||||
```
|
||||
|
||||
For a list of permissions, see `core/userclass.class.php`
|
||||
For a list of permissions, see `core/permissions.php`
|
||||
|
||||
|
||||
# Development Info
|
||||
|
||||
ui-* cookies are for the client-side scripts only; in some configurations
|
||||
ui-\* cookies are for the client-side scripts only; in some configurations
|
||||
(eg with varnish cache) they will be stripped before they reach the server
|
||||
|
||||
shm-* CSS classes are for javascript to hook into; if you're customising
|
||||
shm-\* CSS classes are for javascript to hook into; if you're customising
|
||||
themes, be careful with these, and avoid styling them, eg:
|
||||
|
||||
- shm-thumb = outermost element of a thumbnail
|
||||
|
@ -141,16 +134,12 @@ themes, be careful with these, and avoid styling them, eg:
|
|||
- shm-clink = a link to a comment, flash the target element when clicked
|
||||
* data-clink-sel
|
||||
|
||||
Documentation: http://shimmie.shishnet.org/doc/
|
||||
|
||||
Please tell me if those docs are lacking in any way, so that they can be
|
||||
improved for the next person who uses them
|
||||
|
||||
|
||||
# Contact
|
||||
|
||||
IRC: `#shimmie` on [Freenode](irc.freenode.net)
|
||||
|
||||
Email: webmaster at shishnet.org
|
||||
|
||||
Issue/Bug tracker: http://github.com/shish/shimmie2/issues
|
65
SPEED.md
Normal file
65
SPEED.md
Normal file
|
@ -0,0 +1,65 @@
|
|||
Notes for any sites which require extra performance
|
||||
===================================================
|
||||
|
||||
Image Serving
|
||||
-------------
|
||||
Firstly, make sure your webserver is configured properly and nice URLs are
|
||||
enabled, so that images will be served straight from disk by the webserver
|
||||
instead of via PHP. If you're serving images via PHP, then your site might
|
||||
melt under the load of 5 concurrent users...
|
||||
|
||||
Add a Cache
|
||||
-----------
|
||||
eg installing memcached, then setting
|
||||
`define("CACHE_DSN", "memcache://127.0.0.1:11211")` - a bunch of stuff will
|
||||
get served from the high-speed cache instead of the SQL database.
|
||||
|
||||
`SPEED_HAX`
|
||||
-----------
|
||||
Setting this to true will make a bunch of changes which reduce the correctness
|
||||
of the software and increase admin workload for the sake of speed. You almost
|
||||
certainly don't want to set this, but if you do (eg you're trying to run a
|
||||
site with 10,000 concurrent users on a single server), it can be a huge help.
|
||||
|
||||
Notable behaviour changes:
|
||||
|
||||
- Database schema upgrades are no longer automatic; you'll need to run
|
||||
`php index.php db-upgrade` from the CLI each time you update the code.
|
||||
- Mapping from Events to Extensions is cached - you'll need to delete
|
||||
`data/cache/shm_event_listeners.php` after each code change, and after
|
||||
enabling or disabling any extensions.
|
||||
- Tag lists (eg alphabetic, popularity, map) are cached and you'll need
|
||||
to delete them manually when you feel like it
|
||||
- Anonymous users can only search for 3 tags at once
|
||||
- We only show the first 500 pages of results for any query, except for
|
||||
the most simple (no tags, or one positive tag)
|
||||
- We only ever show the first 5,000 results for complex queries
|
||||
- Only comments from the past 24 hours show up in /comment/list
|
||||
- Web crawlers are blocked from creating too many nonsense searches
|
||||
- The first 10 pages in the index get extra caching
|
||||
- RSS is limited to 10 pages
|
||||
- HTML for thumbnails is cached
|
||||
|
||||
`WH_SPLITS`
|
||||
-----------
|
||||
Store files as `images/ab/cd/...` instead of `images/ab/...`, which can
|
||||
reduce filesystem load when you have millions of images.
|
||||
|
||||
Multiple Image Servers
|
||||
----------------------
|
||||
Image links don't have to be `/images/$hash.$ext` on the local server, they
|
||||
can be full URLs, and include weighted random parts, eg:
|
||||
|
||||
`https://{fred=3,leo=1}.mysite.com/images/$hash.$ext` - the software will then
|
||||
use consistent hashing to map 75% of the files to `fred.mysite.com` and 25% to
|
||||
`leo.mysite.com` - then you can install Varnish or Squid or something as a
|
||||
caching reverse-proxy.
|
||||
|
||||
Profiling
|
||||
---------
|
||||
`define()`'ing `TRACE_FILE` to a filename and `TRACE_THRESHOLD` to a number
|
||||
of seconds will result in JSON event traces being dumped into that file
|
||||
whenever a page takes longer than the threshold to load. These traces can
|
||||
then be loaded into the chrome trace viewer (chrome://tracing/) and you'll
|
||||
get a breakdown of page performance by extension, event, database, and cache
|
||||
queries.
|
|
@ -1,6 +1,8 @@
|
|||
{
|
||||
"name": "shish/shimmie2",
|
||||
"description": "A tag-based image gallery",
|
||||
"type" : "project",
|
||||
"license" : "GPL-2.0",
|
||||
"license" : "GPL-2.0-or-later",
|
||||
"minimum-stability" : "dev",
|
||||
|
||||
"repositories" : [
|
||||
|
@ -23,46 +25,43 @@
|
|||
],
|
||||
|
||||
"require" : {
|
||||
"php" : ">=5.6",
|
||||
"php" : ">=7.3",
|
||||
"ext-pdo": "*",
|
||||
"ext-json": "*",
|
||||
|
||||
"flexihash/flexihash" : "^2.0.0",
|
||||
"ifixit/php-akismet" : "1.*",
|
||||
"google/recaptcha" : "~1.1",
|
||||
"dapphp/securimage" : "3.6.*",
|
||||
"shish/eventtracer-php" : "dev-master",
|
||||
"shish/ffsphp" : "0.0.*",
|
||||
"shish/microcrud" : "dev-master",
|
||||
"shish/microhtml" : "^1.0.0",
|
||||
"enshrined/svg-sanitize" : "0.13.*",
|
||||
|
||||
"bower-asset/jquery" : "1.12.3",
|
||||
"bower-asset/jquery-timeago" : "1.5.2",
|
||||
"bower-asset/jquery" : "1.12.*",
|
||||
"bower-asset/jquery-timeago" : "1.5.*",
|
||||
"bower-asset/tablesorter" : "dev-master",
|
||||
"bower-asset/mediaelement" : "2.21.1",
|
||||
"bower-asset/js-cookie" : "2.1.1"
|
||||
"bower-asset/mediaelement" : "2.21.*",
|
||||
"bower-asset/js-cookie" : "2.1.*"
|
||||
},
|
||||
|
||||
"require-dev" : {
|
||||
"phpunit/phpunit" : "5.*"
|
||||
"phpunit/phpunit" : "8.*"
|
||||
},
|
||||
|
||||
"vendor-copy": {
|
||||
"vendor/bower-asset/jquery/dist/jquery.min.js" : "lib/vendor/js/jquery-1.12.3.min.js",
|
||||
"vendor/bower-asset/jquery/dist/jquery.min.map" : "lib/vendor/js/jquery-1.12.3.min.map",
|
||||
"vendor/bower-asset/jquery-timeago/jquery.timeago.js" : "lib/vendor/js/jquery.timeago.js",
|
||||
"vendor/bower-asset/tablesorter/jquery.tablesorter.min.js" : "lib/vendor/js/jquery.tablesorter.min.js",
|
||||
"vendor/bower-asset/mediaelement/build/flashmediaelement.swf" : "lib/vendor/swf/flashmediaelement.swf",
|
||||
"vendor/bower-asset/js-cookie/src/js.cookie.js" : "lib/vendor/js/js.cookie.js"
|
||||
},
|
||||
|
||||
"scripts": {
|
||||
"pre-install-cmd" : [
|
||||
"php -r \"array_map('unlink', array_merge(glob('lib/vendor/js/j*.{js,map}', GLOB_BRACE), glob('lib/vendor/css/*.css'), glob('lib/vendor/swf/*.swf')));\""
|
||||
],
|
||||
"pre-update-cmd" : [
|
||||
"php -r \"array_map('unlink', array_merge(glob('lib/vendor/js/j*.{js,map}', GLOB_BRACE), glob('lib/vendor/css/*.css'), glob('lib/vendor/swf/*.swf')));\""
|
||||
],
|
||||
|
||||
"post-install-cmd" : [
|
||||
"php -r \"array_map('copy', array_keys(json_decode(file_get_contents('composer.json'), TRUE)['vendor-copy']), json_decode(file_get_contents('composer.json'), TRUE)['vendor-copy']);\""
|
||||
],
|
||||
"post-update-cmd" : [
|
||||
"php -r \"array_map('copy', array_keys(json_decode(file_get_contents('composer.json'), TRUE)['vendor-copy']), json_decode(file_get_contents('composer.json'), TRUE)['vendor-copy']);\""
|
||||
]
|
||||
"suggest": {
|
||||
"ext-memcache": "memcache caching",
|
||||
"ext-memcached": "memcached caching",
|
||||
"ext-apc": "apc caching",
|
||||
"ext-redis": "redis caching",
|
||||
"ext-dom": "some extensions",
|
||||
"ext-curl": "some extensions",
|
||||
"ext-ctype": "some extensions",
|
||||
"ext-json": "some extensions",
|
||||
"ext-zip": "self-updater extension",
|
||||
"ext-zlib": "anti-spam",
|
||||
"ext-xml": "some extensions",
|
||||
"ext-gd": "GD-based thumbnailing"
|
||||
}
|
||||
}
|
||||
|
|
1225
composer.lock
generated
1225
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,51 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* Load all the files into memory, sanitise the environment, but don't
|
||||
* actually do anything as far as the app is concerned
|
||||
*/
|
||||
|
||||
global $config, $database, $user, $page;
|
||||
|
||||
require_once "core/sys_config.inc.php";
|
||||
require_once "core/util.inc.php";
|
||||
require_once "lib/context.php";
|
||||
require_once "vendor/autoload.php";
|
||||
require_once "core/imageboard.pack.php";
|
||||
|
||||
// set up and purify the environment
|
||||
_version_check();
|
||||
_sanitise_environment();
|
||||
|
||||
// load base files
|
||||
ctx_log_start("Opening files");
|
||||
$_shm_files = array_merge(
|
||||
zglob("core/*.php"),
|
||||
zglob("ext/{".ENABLED_EXTS."}/main.php")
|
||||
);
|
||||
foreach($_shm_files as $_shm_filename) {
|
||||
if(basename($_shm_filename)[0] != "_") {
|
||||
require_once $_shm_filename;
|
||||
}
|
||||
}
|
||||
unset($_shm_files);
|
||||
unset($_shm_filename);
|
||||
ctx_log_endok();
|
||||
|
||||
// connect to the database
|
||||
ctx_log_start("Connecting to DB");
|
||||
$database = new Database();
|
||||
$config = new DatabaseConfig($database);
|
||||
ctx_log_endok();
|
||||
|
||||
// load the theme parts
|
||||
ctx_log_start("Loading themelets");
|
||||
foreach(_get_themelet_files(get_theme()) as $themelet) {
|
||||
require_once $themelet;
|
||||
}
|
||||
unset($themelet);
|
||||
$page = class_exists("CustomPage") ? new CustomPage() : new Page();
|
||||
ctx_log_endok();
|
||||
|
||||
// hook up event handlers
|
||||
_load_event_listeners();
|
||||
send_event(new InitExtEvent());
|
686
core/basepage.php
Normal file
686
core/basepage.php
Normal file
|
@ -0,0 +1,686 @@
|
|||
<?php declare(strict_types=1);
|
||||
require_once "core/event.php";
|
||||
|
||||
/**
|
||||
* \page themes Themes
|
||||
*
|
||||
* Each extension has a theme with a specific name -- eg. the extension Setup
|
||||
* which is stored in ext/setup/main.php will have a theme called SetupTheme
|
||||
* stored in ext/setup/theme.php. If you want to customise it, create a class
|
||||
* in the file themes/mytheme/setup.theme.php called CustomSetupTheme which
|
||||
* extends SetupTheme and overrides some of its methods.
|
||||
*
|
||||
* Generally an extension should only deal with processing data; whenever it
|
||||
* wants to display something, it should pass the data to be displayed to the
|
||||
* theme object, and the theme will add the data into the global $page
|
||||
* structure.
|
||||
*
|
||||
* A page should make sure that all the data it outputs is free from dangerous
|
||||
* data by using html_escape(), url_escape(), or int_escape() as appropriate.
|
||||
*
|
||||
* Because some HTML can be placed anywhere according to the theme, coming up
|
||||
* with the correct way to link to a page can be hard -- thus we have the
|
||||
* make_link() function, which will take a path like "post/list" and turn it
|
||||
* into a full and correct link, eg /myboard/post/list, /foo/index.php?q=post/list,
|
||||
* etc depending on how things are set up. This should always be used to link
|
||||
* to pages rather than hardcoding a path.
|
||||
*
|
||||
* Various other common functions are available as part of the Themelet class.
|
||||
*/
|
||||
|
||||
abstract class PageMode
|
||||
{
|
||||
const REDIRECT = 'redirect';
|
||||
const DATA = 'data';
|
||||
const PAGE = 'page';
|
||||
const FILE = 'file';
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Page
|
||||
*
|
||||
* A data structure for holding all the bits of data that make up a page.
|
||||
*
|
||||
* The various extensions all add whatever they want to this structure,
|
||||
* then Layout turns it into HTML.
|
||||
*/
|
||||
class BasePage
|
||||
{
|
||||
/** @var string */
|
||||
public $mode = PageMode::PAGE;
|
||||
/** @var string */
|
||||
private $type = "text/html; charset=utf-8";
|
||||
|
||||
/**
|
||||
* Set what this page should do; "page", "data", or "redirect".
|
||||
*/
|
||||
public function set_mode(string $mode): void
|
||||
{
|
||||
$this->mode = $mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the page's MIME type.
|
||||
*/
|
||||
public function set_type(string $type): void
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (@$_GET["flash"]) {
|
||||
$this->flash[] = $_GET['flash'];
|
||||
unset($_GET["flash"]);
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
|
||||
/** @var string; public only for unit test */
|
||||
public $data = "";
|
||||
|
||||
/** @var string */
|
||||
private $file = null;
|
||||
|
||||
/** @var bool */
|
||||
private $file_delete = false;
|
||||
|
||||
/** @var string */
|
||||
private $filename = null;
|
||||
|
||||
private $disposition = null;
|
||||
|
||||
/**
|
||||
* Set the raw data to be sent.
|
||||
*/
|
||||
public function set_data(string $data): void
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function set_file(string $file, bool $delete = false): void
|
||||
{
|
||||
$this->file = $file;
|
||||
$this->file_delete = $delete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the recommended download filename.
|
||||
*/
|
||||
public function set_filename(string $filename, string $disposition = "attachment"): void
|
||||
{
|
||||
$this->filename = $filename;
|
||||
$this->disposition = $disposition;
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
|
||||
/** @var string */
|
||||
public $redirect = "";
|
||||
|
||||
/**
|
||||
* Set the URL to redirect to (remember to use make_link() if linking
|
||||
* to a page in the same site).
|
||||
*/
|
||||
public function set_redirect(string $redirect): void
|
||||
{
|
||||
$this->redirect = $redirect;
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
|
||||
/** @var int */
|
||||
public $code = 200;
|
||||
|
||||
/** @var string */
|
||||
public $title = "";
|
||||
|
||||
/** @var string */
|
||||
public $heading = "";
|
||||
|
||||
/** @var string */
|
||||
public $subheading = "";
|
||||
|
||||
/** @var string[] */
|
||||
public $html_headers = [];
|
||||
|
||||
/** @var string[] */
|
||||
public $http_headers = [];
|
||||
|
||||
/** @var string[][] */
|
||||
public $cookies = [];
|
||||
|
||||
/** @var Block[] */
|
||||
public $blocks = [];
|
||||
|
||||
/** @var string[] */
|
||||
public $flash = [];
|
||||
|
||||
/**
|
||||
* Set the HTTP status code
|
||||
*/
|
||||
public function set_code(int $code): void
|
||||
{
|
||||
$this->code = $code;
|
||||
}
|
||||
|
||||
public function set_title(string $title): void
|
||||
{
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
public function set_heading(string $heading): void
|
||||
{
|
||||
$this->heading = $heading;
|
||||
}
|
||||
|
||||
public function set_subheading(string $subheading): void
|
||||
{
|
||||
$this->subheading = $subheading;
|
||||
}
|
||||
|
||||
public function flash(string $message): void
|
||||
{
|
||||
$this->flash[] = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a line to the HTML head section.
|
||||
*/
|
||||
public function add_html_header(string $line, int $position = 50): void
|
||||
{
|
||||
while (isset($this->html_headers[$position])) {
|
||||
$position++;
|
||||
}
|
||||
$this->html_headers[$position] = $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a http header to be sent to the client.
|
||||
*/
|
||||
public function add_http_header(string $line, int $position = 50): void
|
||||
{
|
||||
while (isset($this->http_headers[$position])) {
|
||||
$position++;
|
||||
}
|
||||
$this->http_headers[$position] = $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* The counterpart for get_cookie, this works like php's
|
||||
* setcookie method, but prepends the site-wide cookie prefix to
|
||||
* the $name argument before doing anything.
|
||||
*/
|
||||
public function add_cookie(string $name, string $value, int $time, string $path): void
|
||||
{
|
||||
$full_name = COOKIE_PREFIX . "_" . $name;
|
||||
$this->cookies[] = [$full_name, $value, $time, $path];
|
||||
}
|
||||
|
||||
public function get_cookie(string $name): ?string
|
||||
{
|
||||
$full_name = COOKIE_PREFIX . "_" . $name;
|
||||
if (isset($_COOKIE[$full_name])) {
|
||||
return $_COOKIE[$full_name];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the HTML headers that are currently set and return as a string.
|
||||
*/
|
||||
public function get_all_html_headers(): string
|
||||
{
|
||||
$data = '';
|
||||
ksort($this->html_headers);
|
||||
foreach ($this->html_headers as $line) {
|
||||
$data .= "\t\t" . $line . "\n";
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Block of data to the page.
|
||||
*/
|
||||
public function add_block(Block $block): void
|
||||
{
|
||||
$this->blocks[] = $block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a block which contains the given text
|
||||
* (Useful for unit tests)
|
||||
*/
|
||||
public function find_block(string $text): ?Block
|
||||
{
|
||||
foreach ($this->blocks as $block) {
|
||||
if ($block->header == $text) {
|
||||
return $block;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
|
||||
/**
|
||||
* Display the page according to the mode and data given.
|
||||
*/
|
||||
public function display(): void
|
||||
{
|
||||
header("HTTP/1.0 {$this->code} Shimmie");
|
||||
header("Content-type: " . $this->type);
|
||||
header("X-Powered-By: Shimmie-" . VERSION);
|
||||
|
||||
if (!headers_sent()) {
|
||||
foreach ($this->http_headers as $head) {
|
||||
header($head);
|
||||
}
|
||||
foreach ($this->cookies as $c) {
|
||||
setcookie($c[0], $c[1], $c[2], $c[3]);
|
||||
}
|
||||
} else {
|
||||
print "Error: Headers have already been sent to the client.";
|
||||
}
|
||||
|
||||
switch ($this->mode) {
|
||||
case PageMode::PAGE:
|
||||
usort($this->blocks, "blockcmp");
|
||||
$this->add_auto_html_headers();
|
||||
$this->render();
|
||||
break;
|
||||
case PageMode::DATA:
|
||||
header("Content-Length: " . strlen($this->data));
|
||||
if (!is_null($this->filename)) {
|
||||
header('Content-Disposition: ' . $this->disposition . '; filename=' . $this->filename);
|
||||
}
|
||||
print $this->data;
|
||||
break;
|
||||
case PageMode::FILE:
|
||||
if (!is_null($this->filename)) {
|
||||
header('Content-Disposition: ' . $this->disposition . '; filename=' . $this->filename);
|
||||
}
|
||||
|
||||
// https://gist.github.com/codler/3906826
|
||||
$size = filesize($this->file); // File size
|
||||
$length = $size; // Content length
|
||||
$start = 0; // Start byte
|
||||
$end = $size - 1; // End byte
|
||||
|
||||
header("Content-Length: " . $size);
|
||||
header('Accept-Ranges: bytes');
|
||||
|
||||
if (isset($_SERVER['HTTP_RANGE'])) {
|
||||
list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
|
||||
if (strpos($range, ',') !== false) {
|
||||
header('HTTP/1.1 416 Requested Range Not Satisfiable');
|
||||
header("Content-Range: bytes $start-$end/$size");
|
||||
break;
|
||||
}
|
||||
if ($range == '-') {
|
||||
$c_start = $size - (int)substr($range, 1);
|
||||
$c_end = $end;
|
||||
} else {
|
||||
$range = explode('-', $range);
|
||||
$c_start = (int)$range[0];
|
||||
$c_end = (isset($range[1]) && is_numeric($range[1])) ? (int)$range[1] : $size;
|
||||
}
|
||||
$c_end = ($c_end > $end) ? $end : $c_end;
|
||||
if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
|
||||
header('HTTP/1.1 416 Requested Range Not Satisfiable');
|
||||
header("Content-Range: bytes $start-$end/$size");
|
||||
break;
|
||||
}
|
||||
$start = $c_start;
|
||||
$end = $c_end;
|
||||
$length = $end - $start + 1;
|
||||
header('HTTP/1.1 206 Partial Content');
|
||||
}
|
||||
header("Content-Range: bytes $start-$end/$size");
|
||||
header("Content-Length: " . $length);
|
||||
|
||||
try {
|
||||
stream_file($this->file, $start, $end);
|
||||
} finally {
|
||||
if ($this->file_delete === true) {
|
||||
unlink($this->file);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case PageMode::REDIRECT:
|
||||
if ($this->flash) {
|
||||
$this->redirect .= (strpos($this->redirect, "?") === false) ? "?" : "&";
|
||||
$this->redirect .= "flash=" . url_escape(implode("\n", $this->flash));
|
||||
}
|
||||
header('Location: ' . $this->redirect);
|
||||
print 'You should be redirected to <a href="' . $this->redirect . '">' . $this->redirect . '</a>';
|
||||
break;
|
||||
default:
|
||||
print "Invalid page mode";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function grabs all the CSS and JavaScript files sprinkled throughout Shimmie's folders,
|
||||
* concatenates them together into two large files (one for CSS and one for JS) and then stores
|
||||
* them in the /cache/ directory for serving to the user.
|
||||
*
|
||||
* Why do this? Two reasons:
|
||||
* 1. Reduces the number of files the user's browser needs to download.
|
||||
* 2. Allows these cached files to be compressed/minified by the admin.
|
||||
*
|
||||
* TODO: This should really be configurable somehow...
|
||||
*/
|
||||
public function add_auto_html_headers(): void
|
||||
{
|
||||
global $config;
|
||||
|
||||
$data_href = get_base_href();
|
||||
$theme_name = $config->get_string(SetupConfig::THEME, 'default');
|
||||
|
||||
$this->add_html_header("<script type='text/javascript'>base_href = '$data_href';</script>", 40);
|
||||
|
||||
# static handler will map these to themes/foo/static/bar.ico or ext/static_files/static/bar.ico
|
||||
$this->add_html_header("<link rel='icon' type='image/x-icon' href='$data_href/favicon.ico'>", 41);
|
||||
$this->add_html_header("<link rel='apple-touch-icon' href='$data_href/apple-touch-icon.png'>", 42);
|
||||
|
||||
//We use $config_latest to make sure cache is reset if config is ever updated.
|
||||
$config_latest = 0;
|
||||
foreach (zglob("data/config/*") as $conf) {
|
||||
$config_latest = max($config_latest, filemtime($conf));
|
||||
}
|
||||
|
||||
/*** Generate CSS cache files ***/
|
||||
$css_latest = $config_latest;
|
||||
$css_files = array_merge(
|
||||
zglob("ext/{" . Extension::get_enabled_extensions_as_string() . "}/style.css"),
|
||||
zglob("themes/$theme_name/style.css")
|
||||
);
|
||||
foreach ($css_files as $css) {
|
||||
$css_latest = max($css_latest, filemtime($css));
|
||||
}
|
||||
$css_md5 = md5(serialize($css_files));
|
||||
$css_cache_file = data_path("cache/style/{$theme_name}.{$css_latest}.{$css_md5}.css");
|
||||
if (!file_exists($css_cache_file)) {
|
||||
$css_data = "";
|
||||
foreach ($css_files as $file) {
|
||||
$file_data = file_get_contents($file);
|
||||
$pattern = '/url[\s]*\([\s]*["\']?([^"\'\)]+)["\']?[\s]*\)/';
|
||||
$replace = 'url("../../../' . dirname($file) . '/$1")';
|
||||
$file_data = preg_replace($pattern, $replace, $file_data);
|
||||
$css_data .= $file_data . "\n";
|
||||
}
|
||||
file_put_contents($css_cache_file, $css_data);
|
||||
}
|
||||
$this->add_html_header("<link rel='stylesheet' href='$data_href/$css_cache_file' type='text/css'>", 43);
|
||||
|
||||
/*** Generate JS cache files ***/
|
||||
$js_latest = $config_latest;
|
||||
$js_files = array_merge(
|
||||
[
|
||||
"vendor/bower-asset/jquery/dist/jquery.min.js",
|
||||
"vendor/bower-asset/jquery-timeago/jquery.timeago.js",
|
||||
"vendor/bower-asset/tablesorter/jquery.tablesorter.min.js",
|
||||
"vendor/bower-asset/js-cookie/src/js.cookie.js",
|
||||
"ext/static_files/modernizr-3.3.1.custom.js",
|
||||
],
|
||||
zglob("ext/{" . Extension::get_enabled_extensions_as_string() . "}/script.js"),
|
||||
zglob("themes/$theme_name/script.js")
|
||||
);
|
||||
foreach ($js_files as $js) {
|
||||
$js_latest = max($js_latest, filemtime($js));
|
||||
}
|
||||
$js_md5 = md5(serialize($js_files));
|
||||
$js_cache_file = data_path("cache/script/{$theme_name}.{$js_latest}.{$js_md5}.js");
|
||||
if (!file_exists($js_cache_file)) {
|
||||
$js_data = "";
|
||||
foreach ($js_files as $file) {
|
||||
$js_data .= file_get_contents($file) . "\n";
|
||||
}
|
||||
file_put_contents($js_cache_file, $js_data);
|
||||
}
|
||||
$this->add_html_header("<script defer src='$data_href/$js_cache_file' type='text/javascript'></script>", 44);
|
||||
}
|
||||
|
||||
protected function get_nav_links()
|
||||
{
|
||||
$pnbe = send_event(new PageNavBuildingEvent());
|
||||
|
||||
$nav_links = $pnbe->links;
|
||||
|
||||
$active_link = null;
|
||||
// To save on event calls, we check if one of the top-level links has already been marked as active
|
||||
foreach ($nav_links as $link) {
|
||||
if ($link->active===true) {
|
||||
$active_link = $link;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$sub_links = null;
|
||||
// If one is, we just query for sub-menu options under that one tab
|
||||
if ($active_link!==null) {
|
||||
$psnbe = send_event(new PageSubNavBuildingEvent($active_link->name));
|
||||
$sub_links = $psnbe->links;
|
||||
} else {
|
||||
// Otherwise we query for the sub-items under each of the tabs
|
||||
foreach ($nav_links as $link) {
|
||||
$psnbe = send_event(new PageSubNavBuildingEvent($link->name));
|
||||
|
||||
// Now we check for a current link so we can identify the sub-links to show
|
||||
foreach ($psnbe->links as $sub_link) {
|
||||
if ($sub_link->active===true) {
|
||||
$sub_links = $psnbe->links;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If the active link has been detected, we break out
|
||||
if ($sub_links!==null) {
|
||||
$link->active = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sub_links = $sub_links??[];
|
||||
usort($nav_links, "sort_nav_links");
|
||||
usort($sub_links, "sort_nav_links");
|
||||
|
||||
return [$nav_links, $sub_links];
|
||||
}
|
||||
|
||||
/**
|
||||
* turns the Page into HTML
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$head_html = $this->head_html();
|
||||
$body_html = $this->body_html();
|
||||
|
||||
print <<<EOD
|
||||
<!doctype html>
|
||||
<html class="no-js" lang="en">
|
||||
$head_html
|
||||
$body_html
|
||||
</html>
|
||||
EOD;
|
||||
}
|
||||
|
||||
protected function head_html(): string
|
||||
{
|
||||
$html_header_html = $this->get_all_html_headers();
|
||||
|
||||
return "
|
||||
<head>
|
||||
<title>{$this->title}</title>
|
||||
$html_header_html
|
||||
</head>
|
||||
";
|
||||
}
|
||||
|
||||
protected function body_html(): string
|
||||
{
|
||||
$left_block_html = "";
|
||||
$main_block_html = "";
|
||||
$sub_block_html = "";
|
||||
|
||||
foreach ($this->blocks as $block) {
|
||||
switch ($block->section) {
|
||||
case "left":
|
||||
$left_block_html .= $block->get_html(true);
|
||||
break;
|
||||
case "main":
|
||||
$main_block_html .= $block->get_html(false);
|
||||
break;
|
||||
case "subheading":
|
||||
$sub_block_html .= $block->get_html(false);
|
||||
break;
|
||||
default:
|
||||
print "<p>error: {$block->header} using an unknown section ({$block->section})";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$wrapper = "";
|
||||
if (strlen($this->heading) > 100) {
|
||||
$wrapper = ' style="height: 3em; overflow: auto;"';
|
||||
}
|
||||
|
||||
$footer_html = $this->footer_html();
|
||||
$flash_html = $this->flash ? "<b id='flash'>".nl2br(html_escape(implode("\n", $this->flash)))."</b>" : "";
|
||||
return "
|
||||
<body>
|
||||
<header>
|
||||
<h1$wrapper>{$this->heading}</h1>
|
||||
$sub_block_html
|
||||
</header>
|
||||
<nav>
|
||||
$left_block_html
|
||||
</nav>
|
||||
<article>
|
||||
$flash_html
|
||||
$main_block_html
|
||||
</article>
|
||||
<footer>
|
||||
$footer_html
|
||||
</footer>
|
||||
</body>
|
||||
";
|
||||
}
|
||||
|
||||
protected function footer_html(): string
|
||||
{
|
||||
$debug = get_debug_info();
|
||||
$contact_link = contact_link();
|
||||
$contact = empty($contact_link) ? "" : "<br><a href='$contact_link'>Contact</a>";
|
||||
|
||||
return "
|
||||
Images © their respective owners,
|
||||
<a href=\"https://code.shishnet.org/shimmie2/\">Shimmie</a> ©
|
||||
<a href=\"https://www.shishnet.org/\">Shish</a> &
|
||||
<a href=\"https://github.com/shish/shimmie2/graphs/contributors\">The Team</a>
|
||||
2007-2020,
|
||||
based on the Danbooru concept.
|
||||
$debug
|
||||
$contact
|
||||
";
|
||||
}
|
||||
}
|
||||
|
||||
class PageNavBuildingEvent extends Event
|
||||
{
|
||||
public $links = [];
|
||||
|
||||
public function add_nav_link(string $name, Link $link, string $desc, ?bool $active = null, int $order = 50)
|
||||
{
|
||||
$this->links[] = new NavLink($name, $link, $desc, $active, $order);
|
||||
}
|
||||
}
|
||||
|
||||
class PageSubNavBuildingEvent extends Event
|
||||
{
|
||||
public $parent;
|
||||
|
||||
public $links = [];
|
||||
|
||||
public function __construct(string $parent)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->parent= $parent;
|
||||
}
|
||||
|
||||
public function add_nav_link(string $name, Link $link, string $desc, ?bool $active = null, int $order = 50)
|
||||
{
|
||||
$this->links[] = new NavLink($name, $link, $desc, $active, $order);
|
||||
}
|
||||
}
|
||||
|
||||
class NavLink
|
||||
{
|
||||
public $name;
|
||||
public $link;
|
||||
public $description;
|
||||
public $order;
|
||||
public $active = false;
|
||||
|
||||
public function __construct(String $name, Link $link, String $description, ?bool $active = null, int $order = 50)
|
||||
{
|
||||
global $config;
|
||||
|
||||
$this->name = $name;
|
||||
$this->link = $link;
|
||||
$this->description = $description;
|
||||
$this->order = $order;
|
||||
if ($active==null) {
|
||||
$query = ltrim(_get_query(), "/");
|
||||
if ($query === "") {
|
||||
// This indicates the front page, so we check what's set as the front page
|
||||
$front_page = trim($config->get_string(SetupConfig::FRONT_PAGE), "/");
|
||||
|
||||
if ($front_page === $link->page) {
|
||||
$this->active = true;
|
||||
} else {
|
||||
$this->active = self::is_active([$link->page], $front_page);
|
||||
}
|
||||
} elseif ($query===$link->page) {
|
||||
$this->active = true;
|
||||
} else {
|
||||
$this->active = self::is_active([$link->page]);
|
||||
}
|
||||
} else {
|
||||
$this->active = $active;
|
||||
}
|
||||
}
|
||||
|
||||
public static function is_active(array $pages_matched, string $url = null): bool
|
||||
{
|
||||
/**
|
||||
* Woo! We can actually SEE THE CURRENT PAGE!! (well... see it highlighted in the menu.)
|
||||
*/
|
||||
$url = $url??ltrim(_get_query(), "/");
|
||||
|
||||
$re1='.*?';
|
||||
$re2='((?:[a-z][a-z_]+))';
|
||||
|
||||
if (preg_match_all("/".$re1.$re2."/is", $url, $matches)) {
|
||||
$url=$matches[1][0];
|
||||
}
|
||||
|
||||
$count_pages_matched = count($pages_matched);
|
||||
|
||||
for ($i=0; $i < $count_pages_matched; $i++) {
|
||||
if ($url == $pages_matched[$i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function sort_nav_links(NavLink $a, NavLink $b)
|
||||
{
|
||||
return $a->order - $b->order;
|
||||
}
|
|
@ -1,166 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class BaseThemelet
|
||||
*
|
||||
* A collection of common functions for theme parts
|
||||
*/
|
||||
class BaseThemelet {
|
||||
|
||||
/**
|
||||
* Generic error message display
|
||||
*
|
||||
* @param int $code
|
||||
* @param string $title
|
||||
* @param string $message
|
||||
*/
|
||||
public function display_error(/*int*/ $code, /*string*/ $title, /*string*/ $message) {
|
||||
global $page;
|
||||
$page->set_code($code);
|
||||
$page->set_title($title);
|
||||
$page->set_heading($title);
|
||||
$has_nav = false;
|
||||
foreach($page->blocks as $block) {
|
||||
if($block->header == "Navigation") {
|
||||
$has_nav = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!$has_nav) {
|
||||
$page->add_block(new NavBlock());
|
||||
}
|
||||
$page->add_block(new Block("Error", $message));
|
||||
}
|
||||
|
||||
/**
|
||||
* A specific, common error message
|
||||
*/
|
||||
public function display_permission_denied() {
|
||||
$this->display_error(403, "Permission Denied", "You do not have permission to access this page");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generic thumbnail code; returns HTML rather than adding
|
||||
* a block since thumbs tend to go inside blocks...
|
||||
*
|
||||
* @param Image $image
|
||||
* @return string
|
||||
*/
|
||||
public function build_thumb_html(Image $image) {
|
||||
global $config;
|
||||
|
||||
$i_id = (int) $image->id;
|
||||
$h_view_link = make_link('post/view/'.$i_id);
|
||||
$h_thumb_link = $image->get_thumb_link();
|
||||
$h_tip = html_escape($image->get_tooltip());
|
||||
$h_tags = html_escape(strtolower($image->get_tag_list()));
|
||||
|
||||
$extArr = array_flip(array('swf', 'svg', 'mp3')); //List of thumbless filetypes
|
||||
if(!isset($extArr[$image->ext])){
|
||||
$tsize = get_thumbnail_size($image->width, $image->height);
|
||||
}else{
|
||||
//Use max thumbnail size if using thumbless filetype
|
||||
$tsize = get_thumbnail_size($config->get_int('thumb_width'), $config->get_int('thumb_height'));
|
||||
}
|
||||
|
||||
$custom_classes = "";
|
||||
if(class_exists("Relationships")){
|
||||
if(property_exists($image, 'parent_id') && $image->parent_id !== NULL){ $custom_classes .= "shm-thumb-has_parent "; }
|
||||
if(property_exists($image, 'has_children') && bool_escape($image->has_children)){ $custom_classes .= "shm-thumb-has_child "; }
|
||||
}
|
||||
|
||||
return "<a href='$h_view_link' class='thumb shm-thumb shm-thumb-link {$custom_classes}' data-tags='$h_tags' data-post-id='$i_id'>".
|
||||
"<img id='thumb_$i_id' title='$h_tip' alt='$h_tip' height='{$tsize[1]}' width='{$tsize[0]}' src='$h_thumb_link'>".
|
||||
"</a>\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a generic paginator.
|
||||
*
|
||||
* @param Page $page
|
||||
* @param string $base
|
||||
* @param string $query
|
||||
* @param int $page_number
|
||||
* @param int $total_pages
|
||||
* @param bool $show_random
|
||||
*/
|
||||
public function display_paginator(Page $page, $base, $query, $page_number, $total_pages, $show_random = FALSE) {
|
||||
if($total_pages == 0) $total_pages = 1;
|
||||
$body = $this->build_paginator($page_number, $total_pages, $base, $query, $show_random);
|
||||
$page->add_block(new Block(null, $body, "main", 90, "paginator"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a single HTML link.
|
||||
*
|
||||
* @param string $base_url
|
||||
* @param string $query
|
||||
* @param string $page
|
||||
* @param string $name
|
||||
* @return string
|
||||
*/
|
||||
private function gen_page_link($base_url, $query, $page, $name) {
|
||||
$link = make_link($base_url.'/'.$page, $query);
|
||||
return '<a href="'.$link.'">'.$name.'</a>';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $base_url
|
||||
* @param string $query
|
||||
* @param string $page
|
||||
* @param int $current_page
|
||||
* @param string $name
|
||||
* @return string
|
||||
*/
|
||||
private function gen_page_link_block($base_url, $query, $page, $current_page, $name) {
|
||||
$paginator = "";
|
||||
if($page == $current_page) $paginator .= "<b>";
|
||||
$paginator .= $this->gen_page_link($base_url, $query, $page, $name);
|
||||
if($page == $current_page) $paginator .= "</b>";
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the paginator.
|
||||
*
|
||||
* @param int $current_page
|
||||
* @param int $total_pages
|
||||
* @param string $base_url
|
||||
* @param string $query
|
||||
* @param bool $show_random
|
||||
* @return string
|
||||
*/
|
||||
private function build_paginator($current_page, $total_pages, $base_url, $query, $show_random) {
|
||||
$next = $current_page + 1;
|
||||
$prev = $current_page - 1;
|
||||
|
||||
$at_start = ($current_page <= 1 || $total_pages <= 1);
|
||||
$at_end = ($current_page >= $total_pages);
|
||||
|
||||
$first_html = $at_start ? "First" : $this->gen_page_link($base_url, $query, 1, "First");
|
||||
$prev_html = $at_start ? "Prev" : $this->gen_page_link($base_url, $query, $prev, "Prev");
|
||||
|
||||
$random_html = "-";
|
||||
if($show_random) {
|
||||
$rand = mt_rand(1, $total_pages);
|
||||
$random_html = $this->gen_page_link($base_url, $query, $rand, "Random");
|
||||
}
|
||||
|
||||
$next_html = $at_end ? "Next" : $this->gen_page_link($base_url, $query, $next, "Next");
|
||||
$last_html = $at_end ? "Last" : $this->gen_page_link($base_url, $query, $total_pages, "Last");
|
||||
|
||||
$start = $current_page-5 > 1 ? $current_page-5 : 1;
|
||||
$end = $start+10 < $total_pages ? $start+10 : $total_pages;
|
||||
|
||||
$pages = array();
|
||||
foreach(range($start, $end) as $i) {
|
||||
$pages[] = $this->gen_page_link_block($base_url, $query, $i, $current_page, $i);
|
||||
}
|
||||
$pages_html = implode(" | ", $pages);
|
||||
|
||||
return $first_html.' | '.$prev_html.' | '.$random_html.' | '.$next_html.' | '.$last_html
|
||||
.'<br><< '.$pages_html.' >>';
|
||||
}
|
||||
}
|
||||
|
149
core/basethemelet.php
Normal file
149
core/basethemelet.php
Normal file
|
@ -0,0 +1,149 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Class BaseThemelet
|
||||
*
|
||||
* A collection of common functions for theme parts
|
||||
*/
|
||||
class BaseThemelet
|
||||
{
|
||||
|
||||
/**
|
||||
* Generic error message display
|
||||
*/
|
||||
public function display_error(int $code, string $title, string $message): void
|
||||
{
|
||||
global $page;
|
||||
$page->set_code($code);
|
||||
$page->set_title($title);
|
||||
$page->set_heading($title);
|
||||
$has_nav = false;
|
||||
foreach ($page->blocks as $block) {
|
||||
if ($block->header == "Navigation") {
|
||||
$has_nav = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$has_nav) {
|
||||
$page->add_block(new NavBlock());
|
||||
}
|
||||
$page->add_block(new Block("Error", $message));
|
||||
}
|
||||
|
||||
/**
|
||||
* A specific, common error message
|
||||
*/
|
||||
public function display_permission_denied(): void
|
||||
{
|
||||
$this->display_error(403, "Permission Denied", "You do not have permission to access this page");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generic thumbnail code; returns HTML rather than adding
|
||||
* a block since thumbs tend to go inside blocks...
|
||||
*/
|
||||
public function build_thumb_html(Image $image): string
|
||||
{
|
||||
global $config;
|
||||
|
||||
$i_id = (int) $image->id;
|
||||
$h_view_link = make_link('post/view/'.$i_id);
|
||||
$h_thumb_link = $image->get_thumb_link();
|
||||
$h_tip = html_escape($image->get_tooltip());
|
||||
$h_tags = html_escape(strtolower($image->get_tag_list()));
|
||||
|
||||
$extArr = array_flip(['swf', 'svg', 'mp3']); //List of thumbless filetypes
|
||||
if (!isset($extArr[$image->ext])) {
|
||||
$tsize = get_thumbnail_size($image->width, $image->height);
|
||||
} else {
|
||||
//Use max thumbnail size if using thumbless filetype
|
||||
$tsize = get_thumbnail_size($config->get_int(ImageConfig::THUMB_WIDTH), $config->get_int(ImageConfig::THUMB_WIDTH));
|
||||
}
|
||||
|
||||
$custom_classes = "";
|
||||
if (class_exists("Relationships")) {
|
||||
if (property_exists($image, 'parent_id') && $image->parent_id !== null) {
|
||||
$custom_classes .= "shm-thumb-has_parent ";
|
||||
}
|
||||
if (property_exists($image, 'has_children') && bool_escape($image->has_children)) {
|
||||
$custom_classes .= "shm-thumb-has_child ";
|
||||
}
|
||||
}
|
||||
|
||||
return "<a href='$h_view_link' class='thumb shm-thumb shm-thumb-link {$custom_classes}' data-tags='$h_tags' data-post-id='$i_id'>".
|
||||
"<img id='thumb_$i_id' title='$h_tip' alt='$h_tip' height='{$tsize[1]}' width='{$tsize[0]}' src='$h_thumb_link'>".
|
||||
"</a>\n";
|
||||
}
|
||||
|
||||
public function display_paginator(Page $page, string $base, ?string $query, int $page_number, int $total_pages, bool $show_random = false)
|
||||
{
|
||||
if ($total_pages == 0) {
|
||||
$total_pages = 1;
|
||||
}
|
||||
$body = $this->build_paginator($page_number, $total_pages, $base, $query, $show_random);
|
||||
$page->add_block(new Block(null, $body, "main", 90, "paginator"));
|
||||
|
||||
$page->add_html_header("<link rel='first' href='".make_http(make_link($base.'/1', $query))."'>");
|
||||
if ($page_number < $total_pages) {
|
||||
$page->add_html_header("<link rel='prefetch' href='".make_http(make_link($base.'/'.($page_number+1), $query))."'>");
|
||||
$page->add_html_header("<link rel='next' href='".make_http(make_link($base.'/'.($page_number+1), $query))."'>");
|
||||
}
|
||||
if ($page_number > 1) {
|
||||
$page->add_html_header("<link rel='previous' href='".make_http(make_link($base.'/'.($page_number-1), $query))."'>");
|
||||
}
|
||||
$page->add_html_header("<link rel='last' href='".make_http(make_link($base.'/'.$total_pages, $query))."'>");
|
||||
}
|
||||
|
||||
private function gen_page_link(string $base_url, ?string $query, int $page, string $name): string
|
||||
{
|
||||
$link = make_link($base_url.'/'.$page, $query);
|
||||
return '<a href="'.$link.'">'.$name.'</a>';
|
||||
}
|
||||
|
||||
private function gen_page_link_block(string $base_url, ?string $query, int $page, int $current_page, string $name): string
|
||||
{
|
||||
$paginator = "";
|
||||
if ($page == $current_page) {
|
||||
$paginator .= "<b>";
|
||||
}
|
||||
$paginator .= $this->gen_page_link($base_url, $query, $page, $name);
|
||||
if ($page == $current_page) {
|
||||
$paginator .= "</b>";
|
||||
}
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
private function build_paginator(int $current_page, int $total_pages, string $base_url, ?string $query, bool $show_random): string
|
||||
{
|
||||
$next = $current_page + 1;
|
||||
$prev = $current_page - 1;
|
||||
|
||||
$at_start = ($current_page <= 1 || $total_pages <= 1);
|
||||
$at_end = ($current_page >= $total_pages);
|
||||
|
||||
$first_html = $at_start ? "First" : $this->gen_page_link($base_url, $query, 1, "First");
|
||||
$prev_html = $at_start ? "Prev" : $this->gen_page_link($base_url, $query, $prev, "Prev");
|
||||
|
||||
$random_html = "-";
|
||||
if ($show_random) {
|
||||
$rand = mt_rand(1, $total_pages);
|
||||
$random_html = $this->gen_page_link($base_url, $query, $rand, "Random");
|
||||
}
|
||||
|
||||
$next_html = $at_end ? "Next" : $this->gen_page_link($base_url, $query, $next, "Next");
|
||||
$last_html = $at_end ? "Last" : $this->gen_page_link($base_url, $query, $total_pages, "Last");
|
||||
|
||||
$start = $current_page-5 > 1 ? $current_page-5 : 1;
|
||||
$end = $start+10 < $total_pages ? $start+10 : $total_pages;
|
||||
|
||||
$pages = [];
|
||||
foreach (range($start, $end) as $i) {
|
||||
$pages[] = $this->gen_page_link_block($base_url, $query, $i, $current_page, (string)$i);
|
||||
}
|
||||
$pages_html = implode(" | ", $pages);
|
||||
|
||||
return $first_html.' | '.$prev_html.' | '.$random_html.' | '.$next_html.' | '.$last_html
|
||||
.'<br><< '.$pages_html.' >>';
|
||||
}
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class Block
|
||||
*
|
||||
* A basic chunk of a page.
|
||||
*/
|
||||
class Block {
|
||||
/**
|
||||
* The block's title.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $header;
|
||||
|
||||
/**
|
||||
* The content of the block.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $body;
|
||||
|
||||
/**
|
||||
* Where the block should be placed. The default theme supports
|
||||
* "main" and "left", other themes can add their own areas.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $section;
|
||||
|
||||
/**
|
||||
* How far down the section the block should appear, higher
|
||||
* numbers appear lower. The scale is 0-100 by convention,
|
||||
* though any number or string will work.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $position;
|
||||
|
||||
/**
|
||||
* A unique ID for the block.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* Should this block count as content for the sake of
|
||||
* the 404 handler
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $is_content = true;
|
||||
|
||||
/**
|
||||
* Construct a block.
|
||||
*
|
||||
* @param string $header
|
||||
* @param string $body
|
||||
* @param string $section
|
||||
* @param int $position
|
||||
* @param null|int $id A unique ID for the block (generated automatically if null).
|
||||
*/
|
||||
public function __construct($header, $body, /*string*/ $section="main", /*int*/ $position=50, $id=null) {
|
||||
$this->header = $header;
|
||||
$this->body = $body;
|
||||
$this->section = $section;
|
||||
$this->position = $position;
|
||||
|
||||
if(is_null($id)) {
|
||||
$id = (empty($header) ? md5($body) : $header) . $section;
|
||||
}
|
||||
$this->id = preg_replace('/[^\w]/', '',str_replace(' ', '_', $id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML for this block.
|
||||
*
|
||||
* @param bool $hidable
|
||||
* @return string
|
||||
*/
|
||||
public function get_html($hidable=false) {
|
||||
$h = $this->header;
|
||||
$b = $this->body;
|
||||
$i = $this->id;
|
||||
$html = "<section id='$i'>";
|
||||
$h_toggler = $hidable ? " shm-toggler" : "";
|
||||
if(!empty($h)) $html .= "<h3 data-toggle-sel='#$i' class='$h_toggler'>$h</h3>";
|
||||
if(!empty($b)) $html .= "<div class='blockbody'>$b</div>";
|
||||
$html .= "</section>\n";
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class NavBlock
|
||||
*
|
||||
* A generic navigation block with a link to the main page.
|
||||
*
|
||||
* Used because "new NavBlock()" is easier than "new Block('Navigation', ..."
|
||||
*
|
||||
*/
|
||||
class NavBlock extends Block {
|
||||
public function __construct() {
|
||||
parent::__construct("Navigation", "<a href='".make_link()."'>Index</a>", "left", 0);
|
||||
}
|
||||
}
|
105
core/block.php
Normal file
105
core/block.php
Normal file
|
@ -0,0 +1,105 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Class Block
|
||||
*
|
||||
* A basic chunk of a page.
|
||||
*/
|
||||
class Block
|
||||
{
|
||||
/**
|
||||
* The block's title.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $header;
|
||||
|
||||
/**
|
||||
* The content of the block.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $body;
|
||||
|
||||
/**
|
||||
* Where the block should be placed. The default theme supports
|
||||
* "main" and "left", other themes can add their own areas.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $section;
|
||||
|
||||
/**
|
||||
* How far down the section the block should appear, higher
|
||||
* numbers appear lower. The scale is 0-100 by convention,
|
||||
* though any number will work.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $position;
|
||||
|
||||
/**
|
||||
* A unique ID for the block.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* Should this block count as content for the sake of
|
||||
* the 404 handler
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $is_content = true;
|
||||
|
||||
public function __construct(string $header=null, string $body=null, string $section="main", int $position=50, string $id=null)
|
||||
{
|
||||
$this->header = $header;
|
||||
$this->body = $body;
|
||||
$this->section = $section;
|
||||
$this->position = $position;
|
||||
|
||||
if (is_null($id)) {
|
||||
$id = (empty($header) ? md5($body ?? '') : $header) . $section;
|
||||
}
|
||||
$this->id = preg_replace('/[^\w-]/', '', str_replace(' ', '_', $id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML for this block.
|
||||
*/
|
||||
public function get_html(bool $hidable=false): string
|
||||
{
|
||||
$h = $this->header;
|
||||
$b = $this->body;
|
||||
$i = $this->id;
|
||||
$html = "<section id='$i'>";
|
||||
$h_toggler = $hidable ? " shm-toggler" : "";
|
||||
if (!empty($h)) {
|
||||
$html .= "<h3 data-toggle-sel='#$i' class='$h_toggler'>$h</h3>";
|
||||
}
|
||||
if (!empty($b)) {
|
||||
$html .= "<div class='blockbody'>$b</div>";
|
||||
}
|
||||
$html .= "</section>\n";
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class NavBlock
|
||||
*
|
||||
* A generic navigation block with a link to the main page.
|
||||
*
|
||||
* Used because "new NavBlock()" is easier than "new Block('Navigation', ..."
|
||||
*
|
||||
*/
|
||||
class NavBlock extends Block
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct("Navigation", "<a href='".make_link()."'>Index</a>", "left", 0);
|
||||
}
|
||||
}
|
199
core/cacheengine.php
Normal file
199
core/cacheengine.php
Normal file
|
@ -0,0 +1,199 @@
|
|||
<?php declare(strict_types=1);
|
||||
interface CacheEngine
|
||||
{
|
||||
public function get(string $key);
|
||||
public function set(string $key, $val, int $time=0);
|
||||
public function delete(string $key);
|
||||
}
|
||||
|
||||
class NoCache implements CacheEngine
|
||||
{
|
||||
public function get(string $key)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
public function set(string $key, $val, int $time=0)
|
||||
{
|
||||
}
|
||||
public function delete(string $key)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class MemcachedCache implements CacheEngine
|
||||
{
|
||||
/** @var ?Memcached */
|
||||
public $memcache=null;
|
||||
|
||||
public function __construct(string $args)
|
||||
{
|
||||
$hp = explode(":", $args);
|
||||
$this->memcache = new Memcached;
|
||||
#$this->memcache->setOption(Memcached::OPT_COMPRESSION, False);
|
||||
#$this->memcache->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP);
|
||||
#$this->memcache->setOption(Memcached::OPT_PREFIX_KEY, phpversion());
|
||||
$this->memcache->addServer($hp[0], (int)$hp[1]);
|
||||
}
|
||||
|
||||
public function get(string $key)
|
||||
{
|
||||
$key = urlencode($key);
|
||||
|
||||
$val = $this->memcache->get($key);
|
||||
$res = $this->memcache->getResultCode();
|
||||
|
||||
if ($res == Memcached::RES_SUCCESS) {
|
||||
return $val;
|
||||
} elseif ($res == Memcached::RES_NOTFOUND) {
|
||||
return false;
|
||||
} else {
|
||||
error_log("Memcached error during get($key): $res");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function set(string $key, $val, int $time=0)
|
||||
{
|
||||
$key = urlencode($key);
|
||||
|
||||
$this->memcache->set($key, $val, $time);
|
||||
$res = $this->memcache->getResultCode();
|
||||
if ($res != Memcached::RES_SUCCESS) {
|
||||
error_log("Memcached error during set($key): $res");
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(string $key)
|
||||
{
|
||||
$key = urlencode($key);
|
||||
|
||||
$this->memcache->delete($key);
|
||||
$res = $this->memcache->getResultCode();
|
||||
if ($res != Memcached::RES_SUCCESS && $res != Memcached::RES_NOTFOUND) {
|
||||
error_log("Memcached error during delete($key): $res");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class APCCache implements CacheEngine
|
||||
{
|
||||
public function __construct(string $args)
|
||||
{
|
||||
// $args is not used, but is passed in when APC cache is created.
|
||||
}
|
||||
|
||||
public function get(string $key)
|
||||
{
|
||||
return apc_fetch($key);
|
||||
}
|
||||
|
||||
public function set(string $key, $val, int $time=0)
|
||||
{
|
||||
apc_store($key, $val, $time);
|
||||
}
|
||||
|
||||
public function delete(string $key)
|
||||
{
|
||||
apc_delete($key);
|
||||
}
|
||||
}
|
||||
|
||||
class RedisCache implements CacheEngine
|
||||
{
|
||||
private $redis=null;
|
||||
|
||||
public function __construct(string $args)
|
||||
{
|
||||
$this->redis = new Redis();
|
||||
$hp = explode(":", $args);
|
||||
$this->redis->pconnect($hp[0], (int)$hp[1]);
|
||||
$this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
|
||||
$this->redis->setOption(Redis::OPT_PREFIX, 'shm:');
|
||||
}
|
||||
|
||||
public function get(string $key)
|
||||
{
|
||||
return $this->redis->get($key);
|
||||
}
|
||||
|
||||
public function set(string $key, $val, int $time=0)
|
||||
{
|
||||
if ($time > 0) {
|
||||
$this->redis->setEx($key, $time, $val);
|
||||
} else {
|
||||
$this->redis->set($key, $val);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(string $key)
|
||||
{
|
||||
$this->redis->del($key);
|
||||
}
|
||||
}
|
||||
|
||||
class Cache
|
||||
{
|
||||
public $engine;
|
||||
public $hits=0;
|
||||
public $misses=0;
|
||||
public $time=0;
|
||||
|
||||
public function __construct(?string $dsn)
|
||||
{
|
||||
$matches = [];
|
||||
$c = null;
|
||||
if ($dsn && preg_match("#(.*)://(.*)#", $dsn, $matches) && !isset($_GET['DISABLE_CACHE'])) {
|
||||
if ($matches[1] == "memcached") {
|
||||
$c = new MemcachedCache($matches[2]);
|
||||
} elseif ($matches[1] == "apc") {
|
||||
$c = new APCCache($matches[2]);
|
||||
} elseif ($matches[1] == "redis") {
|
||||
$c = new RedisCache($matches[2]);
|
||||
}
|
||||
} else {
|
||||
$c = new NoCache();
|
||||
}
|
||||
$this->engine = $c;
|
||||
}
|
||||
|
||||
public function get(string $key)
|
||||
{
|
||||
global $_tracer;
|
||||
$_tracer->begin("Cache Query", ["key"=>$key]);
|
||||
$val = $this->engine->get($key);
|
||||
if ($val !== false) {
|
||||
$res = "hit";
|
||||
$this->hits++;
|
||||
} else {
|
||||
$res = "miss";
|
||||
$this->misses++;
|
||||
}
|
||||
$_tracer->end(null, ["result"=>$res]);
|
||||
return $val;
|
||||
}
|
||||
|
||||
public function set(string $key, $val, int $time=0)
|
||||
{
|
||||
global $_tracer;
|
||||
$_tracer->begin("Cache Set", ["key"=>$key, "time"=>$time]);
|
||||
$this->engine->set($key, $val, $time);
|
||||
$_tracer->end();
|
||||
}
|
||||
|
||||
public function delete(string $key)
|
||||
{
|
||||
global $_tracer;
|
||||
$_tracer->begin("Cache Delete", ["key"=>$key]);
|
||||
$this->engine->delete($key);
|
||||
$_tracer->end();
|
||||
}
|
||||
|
||||
public function get_hits(): int
|
||||
{
|
||||
return $this->hits;
|
||||
}
|
||||
public function get_misses(): int
|
||||
{
|
||||
return $this->misses;
|
||||
}
|
||||
}
|
60
core/captcha.php
Normal file
60
core/captcha.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php declare(strict_types=1);
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* CAPTCHA abstraction *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
use ReCaptcha\ReCaptcha;
|
||||
|
||||
function captcha_get_html(): string
|
||||
{
|
||||
global $config, $user;
|
||||
|
||||
if (DEBUG && ip_in_range($_SERVER['REMOTE_ADDR'], "127.0.0.0/8")) {
|
||||
return "";
|
||||
}
|
||||
|
||||
$captcha = "";
|
||||
if ($user->is_anonymous() && $config->get_bool("comment_captcha")) {
|
||||
$r_publickey = $config->get_string("api_recaptcha_pubkey");
|
||||
if (!empty($r_publickey)) {
|
||||
$captcha = "
|
||||
<div class=\"g-recaptcha\" data-sitekey=\"{$r_publickey}\"></div>
|
||||
<script type=\"text/javascript\" src=\"https://www.google.com/recaptcha/api.js\"></script>";
|
||||
} else {
|
||||
session_start();
|
||||
$captcha = Securimage::getCaptchaHtml(['securimage_path' => './vendor/dapphp/securimage/']);
|
||||
}
|
||||
}
|
||||
return $captcha;
|
||||
}
|
||||
|
||||
function captcha_check(): bool
|
||||
{
|
||||
global $config, $user;
|
||||
|
||||
if (DEBUG && ip_in_range($_SERVER['REMOTE_ADDR'], "127.0.0.0/8")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($user->is_anonymous() && $config->get_bool("comment_captcha")) {
|
||||
$r_privatekey = $config->get_string('api_recaptcha_privkey');
|
||||
if (!empty($r_privatekey)) {
|
||||
$recaptcha = new ReCaptcha($r_privatekey);
|
||||
$resp = $recaptcha->verify($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']);
|
||||
|
||||
if (!$resp->isSuccess()) {
|
||||
log_info("core", "Captcha failed (ReCaptcha): " . implode("", $resp->getErrorCodes()));
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
session_start();
|
||||
$securimg = new Securimage();
|
||||
if ($securimg->check($_POST['captcha_code']) === false) {
|
||||
log_info("core", "Captcha failed (Securimage)");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
|
@ -1,429 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Interface Config
|
||||
*
|
||||
* An abstract interface for altering a name:value pair list.
|
||||
*/
|
||||
interface Config {
|
||||
/**
|
||||
* Save the list of name:value pairs to wherever they came from,
|
||||
* so that the next time a page is loaded it will use the new
|
||||
* configuration.
|
||||
*
|
||||
* @param null|string $name
|
||||
* @return mixed|void
|
||||
*/
|
||||
public function save(/*string*/ $name=null);
|
||||
|
||||
//@{ /*--------------------------------- SET ------------------------------------------------------*/
|
||||
/**
|
||||
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
||||
* @param string $name
|
||||
* @param null|int $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_int(/*string*/ $name, $value);
|
||||
|
||||
/**
|
||||
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
||||
* @param string $name
|
||||
* @param null|string $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_string(/*string*/ $name, $value);
|
||||
|
||||
/**
|
||||
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
||||
* @param string $name
|
||||
* @param null|bool|string $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_bool(/*string*/ $name, $value);
|
||||
|
||||
/**
|
||||
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
||||
* @param string $name
|
||||
* @param array $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_array(/*string*/ $name, $value);
|
||||
//@} /*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@{ /*-------------------------------- SET DEFAULT -----------------------------------------------*/
|
||||
/**
|
||||
* Set a configuration option to a new value, if there is no value currently.
|
||||
*
|
||||
* Extensions should generally call these from their InitExtEvent handlers.
|
||||
* This has the advantage that the values will show up in the "advanced" setup
|
||||
* page where they can be modified, while calling get_* with a "default"
|
||||
* parameter won't show up.
|
||||
*
|
||||
* @param string $name
|
||||
* @param int $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_default_int(/*string*/ $name, $value);
|
||||
|
||||
/**
|
||||
* Set a configuration option to a new value, if there is no value currently.
|
||||
*
|
||||
* Extensions should generally call these from their InitExtEvent handlers.
|
||||
* This has the advantage that the values will show up in the "advanced" setup
|
||||
* page where they can be modified, while calling get_* with a "default"
|
||||
* parameter won't show up.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_default_string(/*string*/ $name, $value);
|
||||
|
||||
/**
|
||||
* Set a configuration option to a new value, if there is no value currently.
|
||||
*
|
||||
* Extensions should generally call these from their InitExtEvent handlers.
|
||||
* This has the advantage that the values will show up in the "advanced" setup
|
||||
* page where they can be modified, while calling get_* with a "default"
|
||||
* parameter won't show up.
|
||||
*
|
||||
* @param string $name
|
||||
* @param bool $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_default_bool(/*string*/ $name, /*bool*/ $value);
|
||||
|
||||
/**
|
||||
* Set a configuration option to a new value, if there is no value currently.
|
||||
*
|
||||
* Extensions should generally call these from their InitExtEvent handlers.
|
||||
* This has the advantage that the values will show up in the "advanced" setup
|
||||
* page where they can be modified, while calling get_* with a "default"
|
||||
* parameter won't show up.
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_default_array(/*string*/ $name, $value);
|
||||
//@} /*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@{ /*--------------------------------- GET ------------------------------------------------------*/
|
||||
/**
|
||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||
* @param string $name
|
||||
* @param null|int $default
|
||||
* @return int
|
||||
*/
|
||||
public function get_int(/*string*/ $name, $default=null);
|
||||
|
||||
/**
|
||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||
* @param string $name
|
||||
* @param null|string $default
|
||||
* @return string
|
||||
*/
|
||||
public function get_string(/*string*/ $name, $default=null);
|
||||
|
||||
/**
|
||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||
* @param string $name
|
||||
* @param null|bool|string $default
|
||||
* @return bool
|
||||
*/
|
||||
public function get_bool(/*string*/ $name, $default=null);
|
||||
|
||||
/**
|
||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||
* @param string $name
|
||||
* @param array|null $default
|
||||
* @return array
|
||||
*/
|
||||
public function get_array(/*string*/ $name, $default=array());
|
||||
//@} /*--------------------------------------------------------------------------------------------*/
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class BaseConfig
|
||||
*
|
||||
* Common methods for manipulating the list, loading and saving is
|
||||
* left to the concrete implementation
|
||||
*/
|
||||
abstract class BaseConfig implements Config {
|
||||
public $values = array();
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param int|null $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_int(/*string*/ $name, $value) {
|
||||
$this->values[$name] = parse_shorthand_int($value);
|
||||
$this->save($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param null|string $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_string(/*string*/ $name, $value) {
|
||||
$this->values[$name] = $value;
|
||||
$this->save($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param bool|null|string $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_bool(/*string*/ $name, $value) {
|
||||
$this->values[$name] = (($value == 'on' || $value === true) ? 'Y' : 'N');
|
||||
$this->save($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param array $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_array(/*string*/ $name, $value) {
|
||||
assert(isset($value) && is_array($value));
|
||||
$this->values[$name] = implode(",", $value);
|
||||
$this->save($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param int $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_default_int(/*string*/ $name, $value) {
|
||||
if(is_null($this->get($name))) {
|
||||
$this->values[$name] = parse_shorthand_int($value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param null|string $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_default_string(/*string*/ $name, $value) {
|
||||
if(is_null($this->get($name))) {
|
||||
$this->values[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param bool $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_default_bool(/*string*/ $name, /*bool*/ $value) {
|
||||
if(is_null($this->get($name))) {
|
||||
$this->values[$name] = (($value == 'on' || $value === true) ? 'Y' : 'N');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param array $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_default_array(/*string*/ $name, $value) {
|
||||
assert(isset($value) && is_array($value));
|
||||
if(is_null($this->get($name))) {
|
||||
$this->values[$name] = implode(",", $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param null|int $default
|
||||
* @return int
|
||||
*/
|
||||
public function get_int(/*string*/ $name, $default=null) {
|
||||
return (int)($this->get($name, $default));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param null|string $default
|
||||
* @return null|string
|
||||
*/
|
||||
public function get_string(/*string*/ $name, $default=null) {
|
||||
return $this->get($name, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param null|bool|string $default
|
||||
* @return bool
|
||||
*/
|
||||
public function get_bool(/*string*/ $name, $default=null) {
|
||||
return bool_escape($this->get($name, $default));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param array $default
|
||||
* @return array
|
||||
*/
|
||||
public function get_array(/*string*/ $name, $default=array()) {
|
||||
return explode(",", $this->get($name, ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param null|mixed $default
|
||||
* @return null|mixed
|
||||
*/
|
||||
private function get(/*string*/ $name, $default=null) {
|
||||
if(isset($this->values[$name])) {
|
||||
return $this->values[$name];
|
||||
}
|
||||
else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class HardcodeConfig
|
||||
*
|
||||
* For testing, mostly.
|
||||
*/
|
||||
class HardcodeConfig extends BaseConfig {
|
||||
public function __construct($dict) {
|
||||
$this->values = $dict;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|string $name
|
||||
* @return mixed|void
|
||||
*/
|
||||
public function save(/*string*/ $name=null) {
|
||||
// static config is static
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class StaticConfig
|
||||
*
|
||||
* Loads the config list from a PHP file; the file should be in the format:
|
||||
*
|
||||
* <?php
|
||||
* $config['foo'] = "bar";
|
||||
* $config['baz'] = "qux";
|
||||
* ?>
|
||||
*/
|
||||
class StaticConfig extends BaseConfig {
|
||||
/**
|
||||
* @param string $filename
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct($filename) {
|
||||
if(file_exists($filename)) {
|
||||
$config = array();
|
||||
require_once $filename;
|
||||
if(!empty($config)) {
|
||||
$this->values = $config;
|
||||
}
|
||||
else {
|
||||
throw new Exception("Config file '$filename' doesn't contain any config");
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new Exception("Config file '$filename' missing");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|string $name
|
||||
* @return mixed|void
|
||||
*/
|
||||
public function save(/*string*/ $name=null) {
|
||||
// static config is static
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class DatabaseConfig
|
||||
*
|
||||
* Loads the config list from a table in a given database, the table should
|
||||
* be called config and have the schema:
|
||||
*
|
||||
* \code
|
||||
* CREATE TABLE config(
|
||||
* name VARCHAR(255) NOT NULL,
|
||||
* value TEXT
|
||||
* );
|
||||
* \endcode
|
||||
*/
|
||||
class DatabaseConfig extends BaseConfig {
|
||||
/** @var Database */
|
||||
private $database = null;
|
||||
|
||||
/**
|
||||
* Load the config table from a database.
|
||||
*
|
||||
* @param Database $database
|
||||
*/
|
||||
public function __construct(Database $database) {
|
||||
$this->database = $database;
|
||||
|
||||
$cached = $this->database->cache->get("config");
|
||||
if($cached) {
|
||||
$this->values = $cached;
|
||||
}
|
||||
else {
|
||||
$this->values = array();
|
||||
foreach($this->database->get_all("SELECT name, value FROM config") as $row) {
|
||||
$this->values[$row["name"]] = $row["value"];
|
||||
}
|
||||
$this->database->cache->set("config", $this->values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current values as the new config table.
|
||||
*
|
||||
* @param null|string $name
|
||||
* @return mixed|void
|
||||
*/
|
||||
public function save(/*string*/ $name=null) {
|
||||
if(is_null($name)) {
|
||||
reset($this->values); // rewind the array to the first element
|
||||
foreach($this->values as $name => $value) {
|
||||
$this->save(/*string*/ $name);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->database->Execute("DELETE FROM config WHERE name = :name", array("name"=>$name));
|
||||
$this->database->Execute("INSERT INTO config VALUES (:name, :value)", array("name"=>$name, "value"=>$this->values[$name]));
|
||||
}
|
||||
// rather than deleting and having some other request(s) do a thundering
|
||||
// herd of race-conditioned updates, just save the updated version once here
|
||||
$this->database->cache->set("config", $this->values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class MockConfig
|
||||
*/
|
||||
class MockConfig extends HardcodeConfig {
|
||||
/**
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct($config=array()) {
|
||||
$config["db_version"] = "999";
|
||||
$config["anon_id"] = "0";
|
||||
parent::__construct($config);
|
||||
}
|
||||
}
|
||||
|
338
core/config.php
Normal file
338
core/config.php
Normal file
|
@ -0,0 +1,338 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Interface Config
|
||||
*
|
||||
* An abstract interface for altering a name:value pair list.
|
||||
*/
|
||||
interface Config
|
||||
{
|
||||
/**
|
||||
* Save the list of name:value pairs to wherever they came from,
|
||||
* so that the next time a page is loaded it will use the new
|
||||
* configuration.
|
||||
*/
|
||||
public function save(string $name=null): void;
|
||||
|
||||
//@{ /*--------------------------------- SET ------------------------------------------------------*/
|
||||
/**
|
||||
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
||||
*/
|
||||
public function set_int(string $name, ?int $value): void;
|
||||
|
||||
/**
|
||||
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
||||
*/
|
||||
public function set_float(string $name, ?float $value): void;
|
||||
|
||||
/**
|
||||
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
||||
*/
|
||||
public function set_string(string $name, ?string $value): void;
|
||||
|
||||
/**
|
||||
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
||||
*/
|
||||
public function set_bool(string $name, ?bool $value): void;
|
||||
|
||||
/**
|
||||
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
||||
*/
|
||||
public function set_array(string $name, array $value): void;
|
||||
//@} /*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@{ /*-------------------------------- SET DEFAULT -----------------------------------------------*/
|
||||
/**
|
||||
* Set a configuration option to a new value, if there is no value currently.
|
||||
*
|
||||
* Extensions should generally call these from their InitExtEvent handlers.
|
||||
* This has the advantage that the values will show up in the "advanced" setup
|
||||
* page where they can be modified, while calling get_* with a "default"
|
||||
* parameter won't show up.
|
||||
*/
|
||||
public function set_default_int(string $name, int $value): void;
|
||||
|
||||
/**
|
||||
* Set a configuration option to a new value, if there is no value currently.
|
||||
*
|
||||
* Extensions should generally call these from their InitExtEvent handlers.
|
||||
* This has the advantage that the values will show up in the "advanced" setup
|
||||
* page where they can be modified, while calling get_* with a "default"
|
||||
* parameter won't show up.
|
||||
*/
|
||||
public function set_default_float(string $name, float $value): void;
|
||||
|
||||
/**
|
||||
* Set a configuration option to a new value, if there is no value currently.
|
||||
*
|
||||
* Extensions should generally call these from their InitExtEvent handlers.
|
||||
* This has the advantage that the values will show up in the "advanced" setup
|
||||
* page where they can be modified, while calling get_* with a "default"
|
||||
* parameter won't show up.
|
||||
*/
|
||||
public function set_default_string(string $name, string $value): void;
|
||||
|
||||
/**
|
||||
* Set a configuration option to a new value, if there is no value currently.
|
||||
*
|
||||
* Extensions should generally call these from their InitExtEvent handlers.
|
||||
* This has the advantage that the values will show up in the "advanced" setup
|
||||
* page where they can be modified, while calling get_* with a "default"
|
||||
* parameter won't show up.
|
||||
*/
|
||||
public function set_default_bool(string $name, bool $value): void;
|
||||
|
||||
/**
|
||||
* Set a configuration option to a new value, if there is no value currently.
|
||||
*
|
||||
* Extensions should generally call these from their InitExtEvent handlers.
|
||||
* This has the advantage that the values will show up in the "advanced" setup
|
||||
* page where they can be modified, while calling get_* with a "default"
|
||||
* parameter won't show up.
|
||||
*/
|
||||
public function set_default_array(string $name, array $value): void;
|
||||
//@} /*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@{ /*--------------------------------- GET ------------------------------------------------------*/
|
||||
/**
|
||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||
*/
|
||||
public function get_int(string $name, ?int $default=null): ?int;
|
||||
|
||||
/**
|
||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||
*/
|
||||
public function get_float(string $name, ?float $default=null): ?float;
|
||||
|
||||
/**
|
||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||
*/
|
||||
public function get_string(string $name, ?string $default=null): ?string;
|
||||
|
||||
/**
|
||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||
*/
|
||||
public function get_bool(string $name, ?bool $default=null): ?bool;
|
||||
|
||||
/**
|
||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||
*/
|
||||
public function get_array(string $name, ?array $default=[]): ?array;
|
||||
//@} /*--------------------------------------------------------------------------------------------*/
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class BaseConfig
|
||||
*
|
||||
* Common methods for manipulating the list, loading and saving is
|
||||
* left to the concrete implementation
|
||||
*/
|
||||
abstract class BaseConfig implements Config
|
||||
{
|
||||
public $values = [];
|
||||
|
||||
public function set_int(string $name, ?int $value): void
|
||||
{
|
||||
$this->values[$name] = is_null($value) ? null : $value;
|
||||
$this->save($name);
|
||||
}
|
||||
|
||||
public function set_float(string $name, ?float $value): void
|
||||
{
|
||||
$this->values[$name] = $value;
|
||||
$this->save($name);
|
||||
}
|
||||
|
||||
public function set_string(string $name, ?string $value): void
|
||||
{
|
||||
$this->values[$name] = $value;
|
||||
$this->save($name);
|
||||
}
|
||||
|
||||
public function set_bool(string $name, ?bool $value): void
|
||||
{
|
||||
$this->values[$name] = $value ? 'Y' : 'N';
|
||||
$this->save($name);
|
||||
}
|
||||
|
||||
public function set_array(string $name, ?array $value): void
|
||||
{
|
||||
if ($value!=null) {
|
||||
$this->values[$name] = implode(",", $value);
|
||||
} else {
|
||||
$this->values[$name] = null;
|
||||
}
|
||||
$this->save($name);
|
||||
}
|
||||
|
||||
public function set_default_int(string $name, int $value): void
|
||||
{
|
||||
if (is_null($this->get($name))) {
|
||||
$this->values[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public function set_default_float(string $name, float $value): void
|
||||
{
|
||||
if (is_null($this->get($name))) {
|
||||
$this->values[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public function set_default_string(string $name, string $value): void
|
||||
{
|
||||
if (is_null($this->get($name))) {
|
||||
$this->values[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public function set_default_bool(string $name, bool $value): void
|
||||
{
|
||||
if (is_null($this->get($name))) {
|
||||
$this->values[$name] = $value ? 'Y' : 'N';
|
||||
}
|
||||
}
|
||||
|
||||
public function set_default_array(string $name, array $value): void
|
||||
{
|
||||
if (is_null($this->get($name))) {
|
||||
$this->values[$name] = implode(",", $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function get_int(string $name, ?int $default=null): ?int
|
||||
{
|
||||
return (int)($this->get($name, $default));
|
||||
}
|
||||
|
||||
public function get_float(string $name, ?float $default=null): ?float
|
||||
{
|
||||
return (float)($this->get($name, $default));
|
||||
}
|
||||
|
||||
public function get_string(string $name, ?string $default=null): ?string
|
||||
{
|
||||
$val = $this->get($name, $default);
|
||||
if (!is_string($val) && !is_null($val)) {
|
||||
throw new SCoreException("$name is not a string: $val");
|
||||
}
|
||||
return $val;
|
||||
}
|
||||
|
||||
public function get_bool(string $name, ?bool $default=null): ?bool
|
||||
{
|
||||
return bool_escape($this->get($name, $default));
|
||||
}
|
||||
|
||||
public function get_array(string $name, ?array $default=[]): ?array
|
||||
{
|
||||
return explode(",", $this->get($name, ""));
|
||||
}
|
||||
|
||||
private function get(string $name, $default=null)
|
||||
{
|
||||
if (isset($this->values[$name])) {
|
||||
return $this->values[$name];
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class DatabaseConfig
|
||||
*
|
||||
* Loads the config list from a table in a given database, the table should
|
||||
* be called config and have the schema:
|
||||
*
|
||||
* \code
|
||||
* CREATE TABLE config(
|
||||
* name VARCHAR(255) NOT NULL,
|
||||
* value TEXT
|
||||
* );
|
||||
* \endcode
|
||||
*/
|
||||
class DatabaseConfig extends BaseConfig
|
||||
{
|
||||
/** @var Database */
|
||||
private $database = null;
|
||||
|
||||
private $table_name;
|
||||
private $sub_column;
|
||||
private $sub_value;
|
||||
|
||||
public function __construct(
|
||||
Database $database,
|
||||
string $table_name = "config",
|
||||
string $sub_column = null,
|
||||
string $sub_value = null
|
||||
) {
|
||||
global $cache;
|
||||
|
||||
$this->database = $database;
|
||||
$this->table_name = $table_name;
|
||||
$this->sub_value = $sub_value;
|
||||
$this->sub_column = $sub_column;
|
||||
|
||||
$cache_name = "config";
|
||||
if (!empty($sub_value)) {
|
||||
$cache_name .= "_".$sub_value;
|
||||
}
|
||||
|
||||
$cached = $cache->get($cache_name);
|
||||
if ($cached) {
|
||||
$this->values = $cached;
|
||||
} else {
|
||||
$this->values = [];
|
||||
|
||||
$query = "SELECT name, value FROM {$this->table_name}";
|
||||
$args = [];
|
||||
|
||||
if (!empty($sub_column)&&!empty($sub_value)) {
|
||||
$query .= " WHERE $sub_column = :sub_value";
|
||||
$args["sub_value"] = $sub_value;
|
||||
}
|
||||
|
||||
foreach ($this->database->get_all($query, $args) as $row) {
|
||||
$this->values[$row["name"]] = $row["value"];
|
||||
}
|
||||
$cache->set($cache_name, $this->values);
|
||||
}
|
||||
}
|
||||
|
||||
public function save(string $name=null): void
|
||||
{
|
||||
global $cache;
|
||||
|
||||
if (is_null($name)) {
|
||||
reset($this->values); // rewind the array to the first element
|
||||
foreach ($this->values as $name => $value) {
|
||||
$this->save($name);
|
||||
}
|
||||
} else {
|
||||
$query = "DELETE FROM {$this->table_name} WHERE name = :name";
|
||||
$args = ["name"=>$name];
|
||||
$cols = ["name","value"];
|
||||
$params = [":name",":value"];
|
||||
if (!empty($this->sub_column)&&!empty($this->sub_value)) {
|
||||
$query .= " AND $this->sub_column = :sub_value";
|
||||
$args["sub_value"] = $this->sub_value;
|
||||
$cols[] = $this->sub_column;
|
||||
$params[] = ":sub_value";
|
||||
}
|
||||
|
||||
$this->database->Execute($query, $args);
|
||||
|
||||
$args["value"] =$this->values[$name];
|
||||
$this->database->Execute(
|
||||
"INSERT INTO {$this->table_name} (".join(",", $cols).") VALUES (".join(",", $params).")",
|
||||
$args
|
||||
);
|
||||
}
|
||||
// rather than deleting and having some other request(s) do a thundering
|
||||
// herd of race-conditioned updates, just save the updated version once here
|
||||
$cache->set("config", $this->values);
|
||||
}
|
||||
}
|
|
@ -1,963 +0,0 @@
|
|||
<?php
|
||||
/** @privatesection */
|
||||
// Querylet {{{
|
||||
class Querylet {
|
||||
/** @var string */
|
||||
public $sql;
|
||||
/** @var array */
|
||||
public $variables;
|
||||
|
||||
/**
|
||||
* @param string $sql
|
||||
* @param array $variables
|
||||
*/
|
||||
public function __construct($sql, $variables=array()) {
|
||||
$this->sql = $sql;
|
||||
$this->variables = $variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Querylet $querylet
|
||||
*/
|
||||
public function append($querylet) {
|
||||
assert('!is_null($querylet)');
|
||||
$this->sql .= $querylet->sql;
|
||||
$this->variables = array_merge($this->variables, $querylet->variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sql
|
||||
*/
|
||||
public function append_sql($sql) {
|
||||
$this->sql .= $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $var
|
||||
*/
|
||||
public function add_variable($var) {
|
||||
$this->variables[] = $var;
|
||||
}
|
||||
}
|
||||
|
||||
class TagQuerylet {
|
||||
/** @var string */
|
||||
public $tag;
|
||||
/** @var bool */
|
||||
public $positive;
|
||||
|
||||
/**
|
||||
* @param string $tag
|
||||
* @param bool $positive
|
||||
*/
|
||||
public function __construct($tag, $positive) {
|
||||
$this->tag = $tag;
|
||||
$this->positive = $positive;
|
||||
}
|
||||
}
|
||||
|
||||
class ImgQuerylet {
|
||||
/** @var \Querylet */
|
||||
public $qlet;
|
||||
/** @var bool */
|
||||
public $positive;
|
||||
|
||||
/**
|
||||
* @param \Querylet $qlet
|
||||
* @param bool $positive
|
||||
*/
|
||||
public function __construct($qlet, $positive) {
|
||||
$this->qlet = $qlet;
|
||||
$this->positive = $positive;
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ db engines
|
||||
class DBEngine {
|
||||
/** @var null|string */
|
||||
public $name = null;
|
||||
|
||||
/**
|
||||
* @param \PDO $db
|
||||
*/
|
||||
public function init($db) {}
|
||||
|
||||
/**
|
||||
* @param string $scoreql
|
||||
* @return string
|
||||
*/
|
||||
public function scoreql_to_sql($scoreql) {
|
||||
return $scoreql;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
public function create_table_sql($name, $data) {
|
||||
return 'CREATE TABLE '.$name.' ('.$data.')';
|
||||
}
|
||||
}
|
||||
class MySQL extends DBEngine {
|
||||
/** @var string */
|
||||
public $name = "mysql";
|
||||
|
||||
/**
|
||||
* @param \PDO $db
|
||||
*/
|
||||
public function init($db) {
|
||||
$db->exec("SET NAMES utf8;");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
public function scoreql_to_sql($data) {
|
||||
$data = str_replace("SCORE_AIPK", "INTEGER PRIMARY KEY auto_increment", $data);
|
||||
$data = str_replace("SCORE_INET", "VARCHAR(45)", $data);
|
||||
$data = str_replace("SCORE_BOOL_Y", "'Y'", $data);
|
||||
$data = str_replace("SCORE_BOOL_N", "'N'", $data);
|
||||
$data = str_replace("SCORE_BOOL", "ENUM('Y', 'N')", $data);
|
||||
$data = str_replace("SCORE_DATETIME", "DATETIME", $data);
|
||||
$data = str_replace("SCORE_NOW", "\"1970-01-01\"", $data);
|
||||
$data = str_replace("SCORE_STRNORM", "", $data);
|
||||
$data = str_replace("SCORE_ILIKE", "LIKE", $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
public function create_table_sql($name, $data) {
|
||||
$data = $this->scoreql_to_sql($data);
|
||||
$ctes = "ENGINE=InnoDB DEFAULT CHARSET='utf8'";
|
||||
return 'CREATE TABLE '.$name.' ('.$data.') '.$ctes;
|
||||
}
|
||||
}
|
||||
class PostgreSQL extends DBEngine {
|
||||
/** @var string */
|
||||
public $name = "pgsql";
|
||||
|
||||
/**
|
||||
* @param \PDO $db
|
||||
*/
|
||||
public function init($db) {
|
||||
if(array_key_exists('REMOTE_ADDR', $_SERVER)) {
|
||||
$db->exec("SET application_name TO 'shimmie [{$_SERVER['REMOTE_ADDR']}]';");
|
||||
}
|
||||
else {
|
||||
$db->exec("SET application_name TO 'shimmie [local]';");
|
||||
}
|
||||
$db->exec("SET statement_timeout TO 10000;");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
public function scoreql_to_sql($data) {
|
||||
$data = str_replace("SCORE_AIPK", "SERIAL PRIMARY KEY", $data);
|
||||
$data = str_replace("SCORE_INET", "INET", $data);
|
||||
$data = str_replace("SCORE_BOOL_Y", "'t'", $data);
|
||||
$data = str_replace("SCORE_BOOL_N", "'f'", $data);
|
||||
$data = str_replace("SCORE_BOOL", "BOOL", $data);
|
||||
$data = str_replace("SCORE_DATETIME", "TIMESTAMP", $data);
|
||||
$data = str_replace("SCORE_NOW", "current_timestamp", $data);
|
||||
$data = str_replace("SCORE_STRNORM", "lower", $data);
|
||||
$data = str_replace("SCORE_ILIKE", "ILIKE", $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
public function create_table_sql($name, $data) {
|
||||
$data = $this->scoreql_to_sql($data);
|
||||
return "CREATE TABLE $name ($data)";
|
||||
}
|
||||
}
|
||||
|
||||
// shimmie functions for export to sqlite
|
||||
function _unix_timestamp($date) { return strtotime($date); }
|
||||
function _now() { return date("Y-m-d h:i:s"); }
|
||||
function _floor($a) { return floor($a); }
|
||||
function _log($a, $b=null) {
|
||||
if(is_null($b)) return log($a);
|
||||
else return log($a, $b);
|
||||
}
|
||||
function _isnull($a) { return is_null($a); }
|
||||
function _md5($a) { return md5($a); }
|
||||
function _concat($a, $b) { return $a . $b; }
|
||||
function _lower($a) { return strtolower($a); }
|
||||
function _rand() { return rand(); }
|
||||
function _ln($n) { return log($n); }
|
||||
|
||||
class SQLite extends DBEngine {
|
||||
/** @var string */
|
||||
public $name = "sqlite";
|
||||
|
||||
/**
|
||||
* @param \PDO $db
|
||||
*/
|
||||
public function init($db) {
|
||||
ini_set('sqlite.assoc_case', 0);
|
||||
$db->exec("PRAGMA foreign_keys = ON;");
|
||||
$db->sqliteCreateFunction('UNIX_TIMESTAMP', '_unix_timestamp', 1);
|
||||
$db->sqliteCreateFunction('now', '_now', 0);
|
||||
$db->sqliteCreateFunction('floor', '_floor', 1);
|
||||
$db->sqliteCreateFunction('log', '_log');
|
||||
$db->sqliteCreateFunction('isnull', '_isnull', 1);
|
||||
$db->sqliteCreateFunction('md5', '_md5', 1);
|
||||
$db->sqliteCreateFunction('concat', '_concat', 2);
|
||||
$db->sqliteCreateFunction('lower', '_lower', 1);
|
||||
$db->sqliteCreateFunction('rand', '_rand', 0);
|
||||
$db->sqliteCreateFunction('ln', '_ln', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
public function scoreql_to_sql($data) {
|
||||
$data = str_replace("SCORE_AIPK", "INTEGER PRIMARY KEY", $data);
|
||||
$data = str_replace("SCORE_INET", "VARCHAR(45)", $data);
|
||||
$data = str_replace("SCORE_BOOL_Y", "'Y'", $data);
|
||||
$data = str_replace("SCORE_BOOL_N", "'N'", $data);
|
||||
$data = str_replace("SCORE_BOOL", "CHAR(1)", $data);
|
||||
$data = str_replace("SCORE_NOW", "\"1970-01-01\"", $data);
|
||||
$data = str_replace("SCORE_STRNORM", "lower", $data);
|
||||
$data = str_replace("SCORE_ILIKE", "LIKE", $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
public function create_table_sql($name, $data) {
|
||||
$data = $this->scoreql_to_sql($data);
|
||||
$cols = array();
|
||||
$extras = "";
|
||||
foreach(explode(",", $data) as $bit) {
|
||||
$matches = array();
|
||||
if(preg_match("/(UNIQUE)? ?INDEX\s*\((.*)\)/", $bit, $matches)) {
|
||||
$uni = $matches[1];
|
||||
$col = $matches[2];
|
||||
$extras .= "CREATE $uni INDEX {$name}_{$col} ON {$name}({$col});";
|
||||
}
|
||||
else {
|
||||
$cols[] = $bit;
|
||||
}
|
||||
}
|
||||
$cols_redone = implode(", ", $cols);
|
||||
return "CREATE TABLE $name ($cols_redone); $extras";
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ cache engines
|
||||
interface CacheEngine {
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function get($key);
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $val
|
||||
* @param integer $time
|
||||
* @return void
|
||||
*/
|
||||
public function set($key, $val, $time=0);
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function delete($key);
|
||||
|
||||
/**
|
||||
* @return integer
|
||||
*/
|
||||
public function get_hits();
|
||||
|
||||
/**
|
||||
* @return integer
|
||||
*/
|
||||
public function get_misses();
|
||||
}
|
||||
class NoCache implements CacheEngine {
|
||||
public function get($key) {return false;}
|
||||
public function set($key, $val, $time=0) {}
|
||||
public function delete($key) {}
|
||||
|
||||
public function get_hits() {return 0;}
|
||||
public function get_misses() {return 0;}
|
||||
}
|
||||
class MemcacheCache implements CacheEngine {
|
||||
/** @var \Memcache|null */
|
||||
public $memcache=null;
|
||||
/** @var int */
|
||||
private $hits=0;
|
||||
/** @var int */
|
||||
private $misses=0;
|
||||
|
||||
/**
|
||||
* @param string $args
|
||||
*/
|
||||
public function __construct($args) {
|
||||
$hp = explode(":", $args);
|
||||
$this->memcache = new Memcache;
|
||||
@$this->memcache->pconnect($hp[0], $hp[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return array|bool|string
|
||||
*/
|
||||
public function get($key) {
|
||||
assert('!is_null($key)');
|
||||
$val = $this->memcache->get($key);
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
$hit = $val === false ? "miss" : "hit";
|
||||
file_put_contents("data/cache.log", "Cache $hit: $key\n", FILE_APPEND);
|
||||
}
|
||||
if($val !== false) {
|
||||
$this->hits++;
|
||||
return $val;
|
||||
}
|
||||
else {
|
||||
$this->misses++;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $val
|
||||
* @param integer $time
|
||||
*/
|
||||
public function set($key, $val, $time=0) {
|
||||
assert('!is_null($key)');
|
||||
$this->memcache->set($key, $val, false, $time);
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
file_put_contents("data/cache.log", "Cache set: $key ($time)\n", FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
*/
|
||||
public function delete($key) {
|
||||
assert('!is_null($key)');
|
||||
$this->memcache->delete($key);
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
file_put_contents("data/cache.log", "Cache delete: $key\n", FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function get_hits() {return $this->hits;}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function get_misses() {return $this->misses;}
|
||||
}
|
||||
class MemcachedCache implements CacheEngine {
|
||||
/** @var \Memcached|null */
|
||||
public $memcache=null;
|
||||
/** @var int */
|
||||
private $hits=0;
|
||||
/** @var int */
|
||||
private $misses=0;
|
||||
|
||||
/**
|
||||
* @param string $args
|
||||
*/
|
||||
public function __construct($args) {
|
||||
$hp = explode(":", $args);
|
||||
$this->memcache = new Memcached;
|
||||
#$this->memcache->setOption(Memcached::OPT_COMPRESSION, False);
|
||||
#$this->memcache->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP);
|
||||
#$this->memcache->setOption(Memcached::OPT_PREFIX_KEY, phpversion());
|
||||
$this->memcache->addServer($hp[0], $hp[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return array|bool|string
|
||||
*/
|
||||
public function get($key) {
|
||||
assert('!is_null($key)');
|
||||
$key = urlencode($key);
|
||||
|
||||
$val = $this->memcache->get($key);
|
||||
$res = $this->memcache->getResultCode();
|
||||
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
$hit = $res == Memcached::RES_SUCCESS ? "hit" : "miss";
|
||||
file_put_contents("data/cache.log", "Cache $hit: $key\n", FILE_APPEND);
|
||||
}
|
||||
if($res == Memcached::RES_SUCCESS) {
|
||||
$this->hits++;
|
||||
return $val;
|
||||
}
|
||||
else if($res == Memcached::RES_NOTFOUND) {
|
||||
$this->misses++;
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
error_log("Memcached error during get($key): $res");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $val
|
||||
* @param int $time
|
||||
*/
|
||||
public function set($key, $val, $time=0) {
|
||||
assert('!is_null($key)');
|
||||
$key = urlencode($key);
|
||||
|
||||
$this->memcache->set($key, $val, $time);
|
||||
$res = $this->memcache->getResultCode();
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
file_put_contents("data/cache.log", "Cache set: $key ($time)\n", FILE_APPEND);
|
||||
}
|
||||
if($res != Memcached::RES_SUCCESS) {
|
||||
error_log("Memcached error during set($key): $res");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
*/
|
||||
public function delete($key) {
|
||||
assert('!is_null($key)');
|
||||
$key = urlencode($key);
|
||||
|
||||
$this->memcache->delete($key);
|
||||
$res = $this->memcache->getResultCode();
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
file_put_contents("data/cache.log", "Cache delete: $key\n", FILE_APPEND);
|
||||
}
|
||||
if($res != Memcached::RES_SUCCESS && $res != Memcached::RES_NOTFOUND) {
|
||||
error_log("Memcached error during delete($key): $res");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function get_hits() {return $this->hits;}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function get_misses() {return $this->misses;}
|
||||
}
|
||||
|
||||
class APCCache implements CacheEngine {
|
||||
public $hits=0, $misses=0;
|
||||
|
||||
public function __construct($args) {
|
||||
// $args is not used, but is passed in when APC cache is created.
|
||||
}
|
||||
|
||||
public function get($key) {
|
||||
assert('!is_null($key)');
|
||||
$val = apc_fetch($key);
|
||||
if($val) {
|
||||
$this->hits++;
|
||||
return $val;
|
||||
}
|
||||
else {
|
||||
$this->misses++;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function set($key, $val, $time=0) {
|
||||
assert('!is_null($key)');
|
||||
apc_store($key, $val, $time);
|
||||
}
|
||||
|
||||
public function delete($key) {
|
||||
assert('!is_null($key)');
|
||||
apc_delete($key);
|
||||
}
|
||||
|
||||
public function get_hits() {return $this->hits;}
|
||||
public function get_misses() {return $this->misses;}
|
||||
}
|
||||
// }}}
|
||||
/** @publicsection */
|
||||
|
||||
/**
|
||||
* A class for controlled database access
|
||||
*/
|
||||
class Database {
|
||||
/**
|
||||
* The PDO database connection object, for anyone who wants direct access.
|
||||
* @var null|PDO
|
||||
*/
|
||||
private $db = null;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
public $dbtime = 0.0;
|
||||
|
||||
/**
|
||||
* Meta info about the database engine.
|
||||
* @var DBEngine|null
|
||||
*/
|
||||
private $engine = null;
|
||||
|
||||
/**
|
||||
* The currently active cache engine.
|
||||
* @var CacheEngine|null
|
||||
*/
|
||||
public $cache = null;
|
||||
|
||||
/**
|
||||
* A boolean flag to track if we already have an active transaction.
|
||||
* (ie: True if beginTransaction() already called)
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $transaction = false;
|
||||
|
||||
/**
|
||||
* How many queries this DB object has run
|
||||
*/
|
||||
public $query_count = 0;
|
||||
|
||||
/**
|
||||
* For now, only connect to the cache, as we will pretty much certainly
|
||||
* need it. There are some pages where all the data is in cache, so the
|
||||
* DB connection is on-demand.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->connect_cache();
|
||||
}
|
||||
|
||||
private function connect_cache() {
|
||||
$matches = array();
|
||||
if(defined("CACHE_DSN") && CACHE_DSN && preg_match("#(memcache|memcached|apc)://(.*)#", CACHE_DSN, $matches)) {
|
||||
if($matches[1] == "memcache") {
|
||||
$this->cache = new MemcacheCache($matches[2]);
|
||||
}
|
||||
else if($matches[1] == "memcached") {
|
||||
$this->cache = new MemcachedCache($matches[2]);
|
||||
}
|
||||
else if($matches[1] == "apc") {
|
||||
$this->cache = new APCCache($matches[2]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->cache = new NoCache();
|
||||
}
|
||||
}
|
||||
|
||||
private function connect_db() {
|
||||
# FIXME: detect ADODB URI, automatically translate PDO DSN
|
||||
|
||||
/*
|
||||
* Why does the abstraction layer act differently depending on the
|
||||
* back-end? Because PHP is deliberately retarded.
|
||||
*
|
||||
* http://stackoverflow.com/questions/237367
|
||||
*/
|
||||
$matches = array(); $db_user=null; $db_pass=null;
|
||||
if(preg_match("/user=([^;]*)/", DATABASE_DSN, $matches)) $db_user=$matches[1];
|
||||
if(preg_match("/password=([^;]*)/", DATABASE_DSN, $matches)) $db_pass=$matches[1];
|
||||
|
||||
// https://bugs.php.net/bug.php?id=70221
|
||||
$ka = DATABASE_KA;
|
||||
if(version_compare(PHP_VERSION, "6.9.9") == 1 && $this->get_driver_name() == "sqlite") {
|
||||
$ka = false;
|
||||
}
|
||||
|
||||
$db_params = array(
|
||||
PDO::ATTR_PERSISTENT => $ka,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
||||
);
|
||||
$this->db = new PDO(DATABASE_DSN, $db_user, $db_pass, $db_params);
|
||||
|
||||
$this->connect_engine();
|
||||
$this->engine->init($this->db);
|
||||
|
||||
$this->beginTransaction();
|
||||
}
|
||||
|
||||
private function connect_engine() {
|
||||
if(preg_match("/^([^:]*)/", DATABASE_DSN, $matches)) $db_proto=$matches[1];
|
||||
else throw new SCoreException("Can't figure out database engine");
|
||||
|
||||
if($db_proto === "mysql") {
|
||||
$this->engine = new MySQL();
|
||||
}
|
||||
else if($db_proto === "pgsql") {
|
||||
$this->engine = new PostgreSQL();
|
||||
}
|
||||
else if($db_proto === "sqlite") {
|
||||
$this->engine = new SQLite();
|
||||
}
|
||||
else {
|
||||
die('Unknown PDO driver: '.$db_proto);
|
||||
}
|
||||
}
|
||||
|
||||
public function beginTransaction() {
|
||||
if ($this->transaction === false) {
|
||||
$this->db->beginTransaction();
|
||||
$this->transaction = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean|null
|
||||
* @throws SCoreException
|
||||
*/
|
||||
public function commit() {
|
||||
if(!is_null($this->db)) {
|
||||
if ($this->transaction === true) {
|
||||
$this->transaction = false;
|
||||
return $this->db->commit();
|
||||
}
|
||||
else {
|
||||
throw new SCoreException("<p><b>Database Transaction Error:</b> Unable to call commit() as there is no transaction currently open.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean|null
|
||||
* @throws SCoreException
|
||||
*/
|
||||
public function rollback() {
|
||||
if(!is_null($this->db)) {
|
||||
if ($this->transaction === true) {
|
||||
$this->transaction = false;
|
||||
return $this->db->rollback();
|
||||
}
|
||||
else {
|
||||
throw new SCoreException("<p><b>Database Transaction Error:</b> Unable to call rollback() as there is no transaction currently open.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $input
|
||||
* @return string
|
||||
*/
|
||||
public function escape($input) {
|
||||
if(is_null($this->db)) $this->connect_db();
|
||||
return $this->db->Quote($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $input
|
||||
* @return string
|
||||
*/
|
||||
public function scoreql_to_sql($input) {
|
||||
if(is_null($this->engine)) $this->connect_engine();
|
||||
return $this->engine->scoreql_to_sql($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string
|
||||
*/
|
||||
public function get_driver_name() {
|
||||
if(is_null($this->engine)) $this->connect_engine();
|
||||
return $this->engine->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|PDO $db
|
||||
* @param string $sql
|
||||
*/
|
||||
private function count_execs($db, $sql, $inputarray) {
|
||||
if((DEBUG_SQL === true) || (is_null(DEBUG_SQL) && @$_GET['DEBUG_SQL'])) {
|
||||
$sql = trim(preg_replace('/\s+/msi', ' ', $sql));
|
||||
if(isset($inputarray) && is_array($inputarray) && !empty($inputarray)) {
|
||||
$text = $sql." -- ".join(", ", $inputarray)."\n";
|
||||
}
|
||||
else {
|
||||
$text = $sql."\n";
|
||||
}
|
||||
file_put_contents("data/sql.log", $text, FILE_APPEND);
|
||||
}
|
||||
if(!is_array($inputarray)) $this->query_count++;
|
||||
# handle 2-dimensional input arrays
|
||||
else if(is_array(reset($inputarray))) $this->query_count += sizeof($inputarray);
|
||||
else $this->query_count++;
|
||||
}
|
||||
|
||||
private function count_time($method, $start) {
|
||||
if((DEBUG_SQL === true) || (is_null(DEBUG_SQL) && @$_GET['DEBUG_SQL'])) {
|
||||
$text = $method.":".(microtime(true) - $start)."\n";
|
||||
file_put_contents("data/sql.log", $text, FILE_APPEND);
|
||||
}
|
||||
$this->dbtime += microtime(true) - $start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return an PDO result-set.
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return PDOStatement
|
||||
* @throws SCoreException
|
||||
*/
|
||||
public function execute($query, $args=array()) {
|
||||
try {
|
||||
if(is_null($this->db)) $this->connect_db();
|
||||
$this->count_execs($this->db, $query, $args);
|
||||
$stmt = $this->db->prepare($query);
|
||||
if (!array_key_exists(0, $args)) {
|
||||
foreach($args as $name=>$value) {
|
||||
if(is_numeric($value)) {
|
||||
$stmt->bindValue(':'.$name, $value, PDO::PARAM_INT);
|
||||
}
|
||||
else {
|
||||
$stmt->bindValue(':'.$name, $value, PDO::PARAM_STR);
|
||||
}
|
||||
}
|
||||
$stmt->execute();
|
||||
}
|
||||
else {
|
||||
$stmt->execute($args);
|
||||
}
|
||||
return $stmt;
|
||||
}
|
||||
catch(PDOException $pdoe) {
|
||||
throw new SCoreException($pdoe->getMessage()."<p><b>Query:</b> ".$query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a 2D array.
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public function get_all($query, $args=array()) {
|
||||
$_start = microtime(true);
|
||||
$data = $this->execute($query, $args)->fetchAll();
|
||||
$this->count_time("get_all", $_start);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a single row.
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_row($query, $args=array()) {
|
||||
$_start = microtime(true);
|
||||
$row = $this->execute($query, $args)->fetch();
|
||||
$this->count_time("get_row", $_start);
|
||||
return $row ? $row : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the first column of each row.
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public function get_col($query, $args=array()) {
|
||||
$_start = microtime(true);
|
||||
$stmt = $this->execute($query, $args);
|
||||
$res = array();
|
||||
foreach($stmt as $row) {
|
||||
$res[] = $row[0];
|
||||
}
|
||||
$this->count_time("get_col", $_start);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the the first row => the second rown.
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public function get_pairs($query, $args=array()) {
|
||||
$_start = microtime(true);
|
||||
$stmt = $this->execute($query, $args);
|
||||
$res = array();
|
||||
foreach($stmt as $row) {
|
||||
$res[$row[0]] = $row[1];
|
||||
}
|
||||
$this->count_time("get_pairs", $_start);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a single value.
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_one($query, $args=array()) {
|
||||
$_start = microtime(true);
|
||||
$row = $this->execute($query, $args)->fetch();
|
||||
$this->count_time("get_one", $_start);
|
||||
return $row[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the last inserted row.
|
||||
*
|
||||
* @param string|null $seq
|
||||
* @return int
|
||||
*/
|
||||
public function get_last_insert_id($seq) {
|
||||
if($this->engine->name == "pgsql") {
|
||||
return $this->db->lastInsertId($seq);
|
||||
}
|
||||
else {
|
||||
return $this->db->lastInsertId();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a table from pseudo-SQL.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $data
|
||||
*/
|
||||
public function create_table($name, $data) {
|
||||
if(is_null($this->engine)) { $this->connect_engine(); }
|
||||
$data = trim($data, ", \t\n\r\0\x0B"); // mysql doesn't like trailing commas
|
||||
$this->execute($this->engine->create_table_sql($name, $data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of tables present in the current database.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function count_tables() {
|
||||
|
||||
if(is_null($this->db) || is_null($this->engine)) $this->connect_db();
|
||||
|
||||
if($this->engine->name === "mysql") {
|
||||
return count(
|
||||
$this->get_all("SHOW TABLES")
|
||||
);
|
||||
} else if ($this->engine->name === "pgsql") {
|
||||
return count(
|
||||
$this->get_all("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")
|
||||
);
|
||||
} else if ($this->engine->name === "sqlite") {
|
||||
return count(
|
||||
$this->get_all("SELECT name FROM sqlite_master WHERE type = 'table'")
|
||||
);
|
||||
} else {
|
||||
// Hard to find a universal way to do this...
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MockDatabase extends Database {
|
||||
/** @var int */
|
||||
private $query_id = 0;
|
||||
/** @var array */
|
||||
private $responses = array();
|
||||
/** @var \NoCache|null */
|
||||
public $cache = null;
|
||||
|
||||
/**
|
||||
* @param array $responses
|
||||
*/
|
||||
public function __construct($responses = array()) {
|
||||
$this->cache = new NoCache();
|
||||
$this->responses = $responses;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $params
|
||||
* @return PDOStatement
|
||||
*/
|
||||
public function execute($query, $params=array()) {
|
||||
log_debug("mock-database",
|
||||
"QUERY: " . $query .
|
||||
"\nARGS: " . var_export($params, true) .
|
||||
"\nRETURN: " . var_export($this->responses[$this->query_id], true)
|
||||
);
|
||||
return $this->responses[$this->query_id++];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return PDOStatement
|
||||
*/
|
||||
public function get_all($query, $args=array()) {return $this->execute($query, $args);}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return PDOStatement
|
||||
*/
|
||||
public function get_row($query, $args=array()) {return $this->execute($query, $args);}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return PDOStatement
|
||||
*/
|
||||
public function get_col($query, $args=array()) {return $this->execute($query, $args);}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return PDOStatement
|
||||
*/
|
||||
public function get_pairs($query, $args=array()) {return $this->execute($query, $args);}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return PDOStatement
|
||||
*/
|
||||
public function get_one($query, $args=array()) {return $this->execute($query, $args);}
|
||||
|
||||
/**
|
||||
* @param null|string $seq
|
||||
* @return int|string
|
||||
*/
|
||||
public function get_last_insert_id($seq) {return $this->query_id;}
|
||||
|
||||
/**
|
||||
* @param string $sql
|
||||
* @return string
|
||||
*/
|
||||
public function scoreql_to_sql($sql) {return $sql;}
|
||||
public function create_table($name, $def) {}
|
||||
public function connect_engine() {}
|
||||
}
|
||||
|
326
core/database.php
Normal file
326
core/database.php
Normal file
|
@ -0,0 +1,326 @@
|
|||
<?php declare(strict_types=1);
|
||||
use FFSPHP\PDO;
|
||||
|
||||
abstract class DatabaseDriver
|
||||
{
|
||||
public const MYSQL = "mysql";
|
||||
public const PGSQL = "pgsql";
|
||||
public const SQLITE = "sqlite";
|
||||
}
|
||||
|
||||
/**
|
||||
* A class for controlled database access
|
||||
*/
|
||||
class Database
|
||||
{
|
||||
/** @var string */
|
||||
private $dsn;
|
||||
|
||||
/**
|
||||
* The PDO database connection object, for anyone who wants direct access.
|
||||
* @var null|PDO
|
||||
*/
|
||||
private $db = null;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
public $dbtime = 0.0;
|
||||
|
||||
/**
|
||||
* Meta info about the database engine.
|
||||
* @var DBEngine|null
|
||||
*/
|
||||
private $engine = null;
|
||||
|
||||
/**
|
||||
* A boolean flag to track if we already have an active transaction.
|
||||
* (ie: True if beginTransaction() already called)
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $transaction = false;
|
||||
|
||||
/**
|
||||
* How many queries this DB object has run
|
||||
*/
|
||||
public $query_count = 0;
|
||||
|
||||
public function __construct(string $dsn)
|
||||
{
|
||||
$this->dsn = $dsn;
|
||||
}
|
||||
|
||||
private function connect_db(): void
|
||||
{
|
||||
$this->db = new PDO($this->dsn, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
|
||||
$this->connect_engine();
|
||||
$this->engine->init($this->db);
|
||||
|
||||
$this->beginTransaction();
|
||||
}
|
||||
|
||||
private function connect_engine(): void
|
||||
{
|
||||
if (preg_match("/^([^:]*)/", $this->dsn, $matches)) {
|
||||
$db_proto=$matches[1];
|
||||
} else {
|
||||
throw new SCoreException("Can't figure out database engine");
|
||||
}
|
||||
|
||||
if ($db_proto === DatabaseDriver::MYSQL) {
|
||||
$this->engine = new MySQL();
|
||||
} elseif ($db_proto === DatabaseDriver::PGSQL) {
|
||||
$this->engine = new PostgreSQL();
|
||||
} elseif ($db_proto === DatabaseDriver::SQLITE) {
|
||||
$this->engine = new SQLite();
|
||||
} else {
|
||||
die('Unknown PDO driver: '.$db_proto);
|
||||
}
|
||||
}
|
||||
|
||||
public function beginTransaction(): void
|
||||
{
|
||||
if ($this->transaction === false) {
|
||||
$this->db->beginTransaction();
|
||||
$this->transaction = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function commit(): bool
|
||||
{
|
||||
if (!is_null($this->db) && $this->transaction === true) {
|
||||
$this->transaction = false;
|
||||
return $this->db->commit();
|
||||
} else {
|
||||
throw new SCoreException("Unable to call commit() as there is no transaction currently open.");
|
||||
}
|
||||
}
|
||||
|
||||
public function rollback(): bool
|
||||
{
|
||||
if (!is_null($this->db) && $this->transaction === true) {
|
||||
$this->transaction = false;
|
||||
return $this->db->rollback();
|
||||
} else {
|
||||
throw new SCoreException("Unable to call rollback() as there is no transaction currently open.");
|
||||
}
|
||||
}
|
||||
|
||||
public function scoreql_to_sql(string $input): string
|
||||
{
|
||||
if (is_null($this->engine)) {
|
||||
$this->connect_engine();
|
||||
}
|
||||
return $this->engine->scoreql_to_sql($input);
|
||||
}
|
||||
|
||||
public function scoresql_value_prepare($input)
|
||||
{
|
||||
if (is_null($this->engine)) {
|
||||
$this->connect_engine();
|
||||
}
|
||||
if ($input===true) {
|
||||
return $this->engine->BOOL_Y;
|
||||
} elseif ($input===false) {
|
||||
return $this->engine->BOOL_N;
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
public function get_driver_name(): string
|
||||
{
|
||||
if (is_null($this->engine)) {
|
||||
$this->connect_engine();
|
||||
}
|
||||
return $this->engine->name;
|
||||
}
|
||||
|
||||
private function count_time(string $method, float $start, string $query, ?array $args): void
|
||||
{
|
||||
global $_tracer, $tracer_enabled;
|
||||
$dur = microtime(true) - $start;
|
||||
if ($tracer_enabled) {
|
||||
$query = trim(preg_replace('/^[\t ]+/m', '', $query)); // trim leading whitespace
|
||||
$_tracer->complete($start * 1000000, $dur * 1000000, "DB Query", ["query"=>$query, "args"=>$args, "method"=>$method]);
|
||||
}
|
||||
$this->query_count++;
|
||||
$this->dbtime += $dur;
|
||||
}
|
||||
|
||||
public function set_timeout(int $time): void
|
||||
{
|
||||
$this->engine->set_timeout($this->db, $time);
|
||||
}
|
||||
|
||||
public function execute(string $query, array $args = []): PDOStatement
|
||||
{
|
||||
try {
|
||||
if (is_null($this->db)) {
|
||||
$this->connect_db();
|
||||
}
|
||||
return $this->db->execute(
|
||||
"-- " . str_replace("%2F", "/", urlencode($_GET['q'] ?? '')). "\n" .
|
||||
$query,
|
||||
$args
|
||||
);
|
||||
} catch (PDOException $pdoe) {
|
||||
throw new SCoreException($pdoe->getMessage(), $query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a 2D array.
|
||||
*/
|
||||
public function get_all(string $query, array $args = []): array
|
||||
{
|
||||
$_start = microtime(true);
|
||||
$data = $this->execute($query, $args)->fetchAll();
|
||||
$this->count_time("get_all", $_start, $query, $args);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a iterable object for use with generators.
|
||||
*/
|
||||
public function get_all_iterable(string $query, array $args = []): PDOStatement
|
||||
{
|
||||
$_start = microtime(true);
|
||||
$data = $this->execute($query, $args);
|
||||
$this->count_time("get_all_iterable", $_start, $query, $args);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a single row.
|
||||
*/
|
||||
public function get_row(string $query, array $args = []): ?array
|
||||
{
|
||||
$_start = microtime(true);
|
||||
$row = $this->execute($query, $args)->fetch();
|
||||
$this->count_time("get_row", $_start, $query, $args);
|
||||
return $row ? $row : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the first column of each row.
|
||||
*/
|
||||
public function get_col(string $query, array $args = []): array
|
||||
{
|
||||
$_start = microtime(true);
|
||||
$res = $this->execute($query, $args)->fetchAll(PDO::FETCH_COLUMN);
|
||||
$this->count_time("get_col", $_start, $query, $args);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the first column of each row as a single iterable object.
|
||||
*/
|
||||
public function get_col_iterable(string $query, array $args = []): Generator
|
||||
{
|
||||
$_start = microtime(true);
|
||||
$stmt = $this->execute($query, $args);
|
||||
$this->count_time("get_col_iterable", $_start, $query, $args);
|
||||
foreach ($stmt as $row) {
|
||||
yield $row[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the the first column => the second column.
|
||||
*/
|
||||
public function get_pairs(string $query, array $args = []): array
|
||||
{
|
||||
$_start = microtime(true);
|
||||
$res = $this->execute($query, $args)->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
$this->count_time("get_pairs", $_start, $query, $args);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a single value, or null.
|
||||
*/
|
||||
public function get_one(string $query, array $args = [])
|
||||
{
|
||||
$_start = microtime(true);
|
||||
$row = $this->execute($query, $args)->fetch();
|
||||
$this->count_time("get_one", $_start, $query, $args);
|
||||
return $row ? $row[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and returns a bool indicating if any data was returned
|
||||
*/
|
||||
public function exists(string $query, array $args = []): bool
|
||||
{
|
||||
$_start = microtime(true);
|
||||
$row = $this->execute($query, $args)->fetch();
|
||||
$this->count_time("exists", $_start, $query, $args);
|
||||
if ($row==null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the last inserted row.
|
||||
*/
|
||||
public function get_last_insert_id(string $seq): int
|
||||
{
|
||||
if ($this->engine->name == DatabaseDriver::PGSQL) {
|
||||
$id = $this->db->lastInsertId($seq);
|
||||
} else {
|
||||
$id = $this->db->lastInsertId();
|
||||
}
|
||||
assert(is_numeric($id));
|
||||
return (int)$id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a table from pseudo-SQL.
|
||||
*/
|
||||
public function create_table(string $name, string $data): void
|
||||
{
|
||||
if (is_null($this->engine)) {
|
||||
$this->connect_engine();
|
||||
}
|
||||
$data = trim($data, ", \t\n\r\0\x0B"); // mysql doesn't like trailing commas
|
||||
$this->execute($this->engine->create_table_sql($name, $data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of tables present in the current database.
|
||||
*
|
||||
* @throws SCoreException
|
||||
*/
|
||||
public function count_tables(): int
|
||||
{
|
||||
if (is_null($this->db) || is_null($this->engine)) {
|
||||
$this->connect_db();
|
||||
}
|
||||
|
||||
if ($this->engine->name === DatabaseDriver::MYSQL) {
|
||||
return count(
|
||||
$this->get_all("SHOW TABLES")
|
||||
);
|
||||
} elseif ($this->engine->name === DatabaseDriver::PGSQL) {
|
||||
return count(
|
||||
$this->get_all("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")
|
||||
);
|
||||
} elseif ($this->engine->name === DatabaseDriver::SQLITE) {
|
||||
return count(
|
||||
$this->get_all("SELECT name FROM sqlite_master WHERE type = 'table'")
|
||||
);
|
||||
} else {
|
||||
throw new SCoreException("Can't count tables for database type {$this->engine->name}");
|
||||
}
|
||||
}
|
||||
|
||||
public function raw_db(): PDO
|
||||
{
|
||||
return $this->db;
|
||||
}
|
||||
}
|
219
core/dbengine.php
Normal file
219
core/dbengine.php
Normal file
|
@ -0,0 +1,219 @@
|
|||
<?php declare(strict_types=1);
|
||||
abstract class SCORE
|
||||
{
|
||||
const AIPK = "SCORE_AIPK";
|
||||
const INET = "SCORE_INET";
|
||||
const BOOL_Y = "SCORE_BOOL_Y";
|
||||
const BOOL_N = "SCORE_BOOL_N";
|
||||
const BOOL = "SCORE_BOOL";
|
||||
}
|
||||
|
||||
abstract class DBEngine
|
||||
{
|
||||
/** @var null|string */
|
||||
public $name = null;
|
||||
|
||||
public $BOOL_Y = null;
|
||||
public $BOOL_N = null;
|
||||
|
||||
public function init(PDO $db)
|
||||
{
|
||||
}
|
||||
|
||||
public function scoreql_to_sql(string $scoreql): string
|
||||
{
|
||||
return $scoreql;
|
||||
}
|
||||
|
||||
public function create_table_sql(string $name, string $data): string
|
||||
{
|
||||
return 'CREATE TABLE '.$name.' ('.$data.')';
|
||||
}
|
||||
|
||||
abstract public function set_timeout(PDO $db, int $time);
|
||||
}
|
||||
|
||||
class MySQL extends DBEngine
|
||||
{
|
||||
/** @var string */
|
||||
public $name = DatabaseDriver::MYSQL;
|
||||
|
||||
public $BOOL_Y = 'Y';
|
||||
public $BOOL_N = 'N';
|
||||
|
||||
public function init(PDO $db)
|
||||
{
|
||||
$db->exec("SET NAMES utf8;");
|
||||
}
|
||||
|
||||
public function scoreql_to_sql(string $data): string
|
||||
{
|
||||
$data = str_replace(SCORE::AIPK, "INTEGER PRIMARY KEY auto_increment", $data);
|
||||
$data = str_replace(SCORE::INET, "VARCHAR(45)", $data);
|
||||
$data = str_replace(SCORE::BOOL_Y, "'$this->BOOL_Y'", $data);
|
||||
$data = str_replace(SCORE::BOOL_N, "'$this->BOOL_N'", $data);
|
||||
$data = str_replace(SCORE::BOOL, "ENUM('Y', 'N')", $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function create_table_sql(string $name, string $data): string
|
||||
{
|
||||
$data = $this->scoreql_to_sql($data);
|
||||
$ctes = "ENGINE=InnoDB DEFAULT CHARSET='utf8'";
|
||||
return 'CREATE TABLE '.$name.' ('.$data.') '.$ctes;
|
||||
}
|
||||
|
||||
public function set_timeout(PDO $db, int $time): void
|
||||
{
|
||||
// These only apply to read-only queries, which appears to be the best we can to mysql-wise
|
||||
// $db->exec("SET SESSION MAX_EXECUTION_TIME=".$time.";");
|
||||
}
|
||||
}
|
||||
|
||||
class PostgreSQL extends DBEngine
|
||||
{
|
||||
|
||||
|
||||
/** @var string */
|
||||
public $name = DatabaseDriver::PGSQL;
|
||||
|
||||
public $BOOL_Y = "true";
|
||||
public $BOOL_N = "false";
|
||||
|
||||
public function init(PDO $db)
|
||||
{
|
||||
if (array_key_exists('REMOTE_ADDR', $_SERVER)) {
|
||||
$db->exec("SET application_name TO 'shimmie [{$_SERVER['REMOTE_ADDR']}]';");
|
||||
} else {
|
||||
$db->exec("SET application_name TO 'shimmie [local]';");
|
||||
}
|
||||
$this->set_timeout($db, DATABASE_TIMEOUT);
|
||||
}
|
||||
|
||||
public function scoreql_to_sql(string $data): string
|
||||
{
|
||||
$data = str_replace(SCORE::AIPK, "INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY", $data);
|
||||
$data = str_replace(SCORE::INET, "INET", $data);
|
||||
$data = str_replace(SCORE::BOOL_Y, "true", $data);
|
||||
$data = str_replace(SCORE::BOOL_N, "false", $data);
|
||||
$data = str_replace(SCORE::BOOL, "BOOL", $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function create_table_sql(string $name, string $data): string
|
||||
{
|
||||
$data = $this->scoreql_to_sql($data);
|
||||
return "CREATE TABLE $name ($data)";
|
||||
}
|
||||
|
||||
public function set_timeout(PDO $db, int $time): void
|
||||
{
|
||||
$db->exec("SET statement_timeout TO ".$time.";");
|
||||
}
|
||||
}
|
||||
|
||||
// shimmie functions for export to sqlite
|
||||
function _unix_timestamp($date)
|
||||
{
|
||||
return strtotime($date);
|
||||
}
|
||||
function _now()
|
||||
{
|
||||
return date("Y-m-d H:i:s");
|
||||
}
|
||||
function _floor($a)
|
||||
{
|
||||
return floor($a);
|
||||
}
|
||||
function _log($a, $b=null)
|
||||
{
|
||||
if (is_null($b)) {
|
||||
return log($a);
|
||||
} else {
|
||||
return log($a, $b);
|
||||
}
|
||||
}
|
||||
function _isnull($a)
|
||||
{
|
||||
return is_null($a);
|
||||
}
|
||||
function _md5($a)
|
||||
{
|
||||
return md5($a);
|
||||
}
|
||||
function _concat($a, $b)
|
||||
{
|
||||
return $a . $b;
|
||||
}
|
||||
function _lower($a)
|
||||
{
|
||||
return strtolower($a);
|
||||
}
|
||||
function _rand()
|
||||
{
|
||||
return rand();
|
||||
}
|
||||
function _ln($n)
|
||||
{
|
||||
return log($n);
|
||||
}
|
||||
|
||||
class SQLite extends DBEngine
|
||||
{
|
||||
/** @var string */
|
||||
public $name = DatabaseDriver::SQLITE;
|
||||
|
||||
public $BOOL_Y = 'Y';
|
||||
public $BOOL_N = 'N';
|
||||
|
||||
|
||||
public function init(PDO $db)
|
||||
{
|
||||
ini_set('sqlite.assoc_case', '0');
|
||||
$db->exec("PRAGMA foreign_keys = ON;");
|
||||
$db->sqliteCreateFunction('UNIX_TIMESTAMP', '_unix_timestamp', 1);
|
||||
$db->sqliteCreateFunction('now', '_now', 0);
|
||||
$db->sqliteCreateFunction('floor', '_floor', 1);
|
||||
$db->sqliteCreateFunction('log', '_log');
|
||||
$db->sqliteCreateFunction('isnull', '_isnull', 1);
|
||||
$db->sqliteCreateFunction('md5', '_md5', 1);
|
||||
$db->sqliteCreateFunction('concat', '_concat', 2);
|
||||
$db->sqliteCreateFunction('lower', '_lower', 1);
|
||||
$db->sqliteCreateFunction('rand', '_rand', 0);
|
||||
$db->sqliteCreateFunction('ln', '_ln', 1);
|
||||
}
|
||||
|
||||
public function scoreql_to_sql(string $data): string
|
||||
{
|
||||
$data = str_replace(SCORE::AIPK, "INTEGER PRIMARY KEY", $data);
|
||||
$data = str_replace(SCORE::INET, "VARCHAR(45)", $data);
|
||||
$data = str_replace(SCORE::BOOL_Y, "'$this->BOOL_Y'", $data);
|
||||
$data = str_replace(SCORE::BOOL_N, "'$this->BOOL_N'", $data);
|
||||
$data = str_replace(SCORE::BOOL, "CHAR(1)", $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function create_table_sql(string $name, string $data): string
|
||||
{
|
||||
$data = $this->scoreql_to_sql($data);
|
||||
$cols = [];
|
||||
$extras = "";
|
||||
foreach (explode(",", $data) as $bit) {
|
||||
$matches = [];
|
||||
if (preg_match("/(UNIQUE)? ?INDEX\s*\((.*)\)/", $bit, $matches)) {
|
||||
$uni = $matches[1];
|
||||
$col = $matches[2];
|
||||
$extras .= "CREATE $uni INDEX {$name}_{$col} ON {$name}({$col});";
|
||||
} else {
|
||||
$cols[] = $bit;
|
||||
}
|
||||
}
|
||||
$cols_redone = implode(", ", $cols);
|
||||
return "CREATE TABLE $name ($cols_redone); $extras";
|
||||
}
|
||||
|
||||
public function set_timeout(PDO $db, int $time): void
|
||||
{
|
||||
// There doesn't seem to be such a thing for SQLite, so it does nothing
|
||||
}
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class Email
|
||||
*
|
||||
* A generic email.
|
||||
*/
|
||||
class Email {
|
||||
/** @var string */
|
||||
public $to;
|
||||
/** @var string */
|
||||
public $subject;
|
||||
/** @var string */
|
||||
public $header;
|
||||
/** @var null|string */
|
||||
public $style;
|
||||
/** @var null|string */
|
||||
public $header_img;
|
||||
/** @var null|string */
|
||||
public $sitename;
|
||||
/** @var null|string */
|
||||
public $sitedomain;
|
||||
/** @var null|string */
|
||||
public $siteemail;
|
||||
/** @var string */
|
||||
public $date;
|
||||
/** @var string */
|
||||
public $body;
|
||||
/** @var null|string */
|
||||
public $footer;
|
||||
|
||||
/**
|
||||
* @param string $to
|
||||
* @param string $subject
|
||||
* @param string $header
|
||||
* @param string $body
|
||||
*/
|
||||
public function __construct($to, $subject, $header, $body) {
|
||||
global $config;
|
||||
$this->to = $to;
|
||||
|
||||
$sub_prefix = $config->get_string("mail_sub");
|
||||
|
||||
if(!isset($sub_prefix)){
|
||||
$this->subject = $subject;
|
||||
}
|
||||
else{
|
||||
$this->subject = $sub_prefix." ".$subject;
|
||||
}
|
||||
|
||||
$this->style = $config->get_string("mail_style");
|
||||
|
||||
$this->header = html_escape($header);
|
||||
$this->header_img = $config->get_string("mail_img");
|
||||
$this->sitename = $config->get_string("site_title");
|
||||
$this->sitedomain = make_http(make_link(""));
|
||||
$this->siteemail = $config->get_string("site_email");
|
||||
$this->date = date("F j, Y");
|
||||
$this->body = $body;
|
||||
$this->footer = $config->get_string("mail_fot");
|
||||
}
|
||||
|
||||
public function send() {
|
||||
$headers = "From: ".$this->sitename." <".$this->siteemail.">\r\n";
|
||||
$headers .= "Reply-To: ".$this->siteemail."\r\n";
|
||||
$headers .= "X-Mailer: PHP/" . phpversion(). "\r\n";
|
||||
$headers .= "errors-to: ".$this->siteemail."\r\n";
|
||||
$headers .= "Date: " . date(DATE_RFC2822);
|
||||
$headers .= 'MIME-Version: 1.0' . "\r\n";
|
||||
$headers .= 'Content-type: text/html; charset=iso-8859-1' . "\r\n";
|
||||
$message = '
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="'.$this->style.'" type="text/css">
|
||||
</head>
|
||||
|
||||
<body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0" bgcolor="#EEEEEE" >
|
||||
<table width="100%" cellpadding="10" cellspacing="0" class="backgroundTable" bgcolor="#EEEEEE" >
|
||||
<tr>
|
||||
<td valign="top" align="center">
|
||||
|
||||
|
||||
<table width="550" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
<td style="background-color:#FFFFFF;border-top:0px solid #333333;border-bottom:10px solid #FFFFFF;"><center><a href="'.$this->sitedomain.'"><IMG SRC="'.$this->header_img.'" alt="'.$this->sitename.'" name="Header" BORDER="0" align="center" title="'.$this->sitename.'"></a>
|
||||
</center></td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
<table width="550" cellpadding="20" cellspacing="0" bgcolor="#FFFFFF">
|
||||
<tr>
|
||||
<td bgcolor="#FFFFFF" valign="top" style="font-size:12px;color:#000000;line-height:150%;font-family:trebuchet ms;">
|
||||
|
||||
<p>
|
||||
<span style="font-size:20px; font-weight:bold; color:#3399FF; font-family:arial; line-height:110%;">'.$this->header.'</span><br>
|
||||
<span style="font-size:11px;font-weight:normal;color:#666666;font-style:italic;font-family:arial;">'.$this->date.'</span><br>
|
||||
</p>
|
||||
<p>'.$this->body.'</p>
|
||||
<p>'.$this->footer.'</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="background-color:#FFFFCC;border-top:10px solid #FFFFFF;" valign="top">
|
||||
<span style="font-size:10px;color:#996600;line-height:100%;font-family:verdana;">
|
||||
This email was sent to you since you are a member of <a href="'.$this->sitedomain.'">'.$this->sitename.'</a>. To change your email preferences, visit your <a href="'.make_http(make_link("preferences")).'">Account preferences</a>.<br />
|
||||
|
||||
<br />
|
||||
Contact us:<br />
|
||||
<a href="'.$this->siteemail.'">'.$this->siteemail.'</a><br /><br />
|
||||
Copyright (C) <a href="'.$this->sitedomain.'">'.$this->sitename.'</a><br />
|
||||
</span></td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
';
|
||||
$sent = mail($this->to, $this->subject, $message, $headers);
|
||||
if($sent){
|
||||
log_info("mail", "Sent message '$this->subject' to '$this->to'");
|
||||
}
|
||||
else{
|
||||
log_info("mail", "Error sending message '$this->subject' to '$this->to'");
|
||||
}
|
||||
|
||||
return $sent;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,325 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Generic parent class for all events.
|
||||
*
|
||||
* An event is anything that can be passed around via send_event($blah)
|
||||
*/
|
||||
abstract class Event {
|
||||
public function __construct() {}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A wake-up call for extensions. Upon recieving an InitExtEvent an extension
|
||||
* should check that it's database tables are there and install them if not,
|
||||
* and set any defaults with Config::set_default_int() and such.
|
||||
*
|
||||
* This event is sent before $user is set to anything
|
||||
*/
|
||||
class InitExtEvent extends Event {}
|
||||
|
||||
|
||||
/**
|
||||
* A signal that a page has been requested.
|
||||
*
|
||||
* User requests /view/42 -> an event is generated with $args = array("view",
|
||||
* "42"); when an event handler asks $event->page_matches("view"), it returns
|
||||
* true and ignores the matched part, such that $event->count_args() = 1 and
|
||||
* $event->get_arg(0) = "42"
|
||||
*/
|
||||
class PageRequestEvent extends Event {
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $args;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $arg_count;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $part_count;
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*/
|
||||
public function __construct($path) {
|
||||
global $config;
|
||||
|
||||
// trim starting slashes
|
||||
$path = ltrim($path, "/");
|
||||
|
||||
// if path is not specified, use the default front page
|
||||
if(empty($path)) { /* empty is faster than strlen */
|
||||
$path = $config->get_string('front_page');
|
||||
}
|
||||
|
||||
// break the path into parts
|
||||
$args = explode('/', $path);
|
||||
|
||||
// voodoo so that an arg can contain a slash; is
|
||||
// this still needed?
|
||||
if(strpos($path, "^") !== FALSE) {
|
||||
$unescaped = array();
|
||||
foreach($args as $part) {
|
||||
$unescaped[] = _decaret($part);
|
||||
}
|
||||
$args = $unescaped;
|
||||
}
|
||||
|
||||
$this->args = $args;
|
||||
$this->arg_count = count($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the requested path matches a given pattern.
|
||||
*
|
||||
* If it matches, store the remaining path elements in $args
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function page_matches(/*string*/ $name) {
|
||||
$parts = explode("/", $name);
|
||||
$this->part_count = count($parts);
|
||||
|
||||
if($this->part_count > $this->arg_count) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for($i=0; $i<$this->part_count; $i++) {
|
||||
if($parts[$i] != $this->args[$i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the n th argument of the page request (if it exists.)
|
||||
*
|
||||
* @param int $n
|
||||
* @return string|null The argument (string) or NULL
|
||||
*/
|
||||
public function get_arg(/*int*/ $n) {
|
||||
$offset = $this->part_count + $n;
|
||||
if($offset >= 0 && $offset < $this->arg_count) {
|
||||
return $this->args[$offset];
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of arguments the page request has.
|
||||
* @return int
|
||||
*/
|
||||
public function count_args() {
|
||||
return int_escape($this->arg_count - $this->part_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* Many things use these functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function get_search_terms() {
|
||||
$search_terms = array();
|
||||
if($this->count_args() === 2) {
|
||||
$search_terms = Tag::explode($this->get_arg(0));
|
||||
}
|
||||
return $search_terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function get_page_number() {
|
||||
$page_number = 1;
|
||||
if($this->count_args() === 1) {
|
||||
$page_number = int_escape($this->get_arg(0));
|
||||
}
|
||||
else if($this->count_args() === 2) {
|
||||
$page_number = int_escape($this->get_arg(1));
|
||||
}
|
||||
if($page_number === 0) $page_number = 1; // invalid -> 0
|
||||
return $page_number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function get_page_size() {
|
||||
global $config;
|
||||
return $config->get_int('index_images');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sent when index.php is called from the command line
|
||||
*/
|
||||
class CommandEvent extends Event {
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $cmd = "help";
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $args = array();
|
||||
|
||||
/**
|
||||
* @param string[] $args
|
||||
*/
|
||||
public function __construct(/*array(string)*/ $args) {
|
||||
global $user;
|
||||
|
||||
$opts = array();
|
||||
$log_level = SCORE_LOG_WARNING;
|
||||
$arg_count = count($args);
|
||||
|
||||
for($i=1; $i<$arg_count; $i++) {
|
||||
switch($args[$i]) {
|
||||
case '-u':
|
||||
$user = User::by_name($args[++$i]);
|
||||
if(is_null($user)) {
|
||||
die("Unknown user");
|
||||
}
|
||||
break;
|
||||
case '-q':
|
||||
$log_level += 10;
|
||||
break;
|
||||
case '-v':
|
||||
$log_level -= 10;
|
||||
break;
|
||||
default:
|
||||
$opts[] = $args[$i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
define("CLI_LOG_LEVEL", $log_level);
|
||||
|
||||
if(count($opts) > 0) {
|
||||
$this->cmd = $opts[0];
|
||||
$this->args = array_slice($opts, 1);
|
||||
}
|
||||
else {
|
||||
print "\n";
|
||||
print "Usage: php {$args[0]} [flags] [command]\n";
|
||||
print "\n";
|
||||
print "Flags:\n";
|
||||
print " -u [username]\n";
|
||||
print " Log in as the specified user\n";
|
||||
print " -q / -v\n";
|
||||
print " Be quieter / more verbose\n";
|
||||
print " Scale is debug - info - warning - error - critical\n";
|
||||
print " Default is to show warnings and above\n";
|
||||
print " \n";
|
||||
print "Currently known commands:\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A signal that some text needs formatting, the event carries
|
||||
* both the text and the result
|
||||
*/
|
||||
class TextFormattingEvent extends Event {
|
||||
/**
|
||||
* For reference
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $original;
|
||||
|
||||
/**
|
||||
* with formatting applied
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $formatted;
|
||||
|
||||
/**
|
||||
* with formatting removed
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $stripped;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
*/
|
||||
public function __construct(/*string*/ $text) {
|
||||
$h_text = html_escape(trim($text));
|
||||
$this->original = $h_text;
|
||||
$this->formatted = $h_text;
|
||||
$this->stripped = $h_text;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A signal that something needs logging
|
||||
*/
|
||||
class LogEvent extends Event {
|
||||
/**
|
||||
* a category, normally the extension name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $section;
|
||||
|
||||
/**
|
||||
* See python...
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $priority = 0;
|
||||
|
||||
/**
|
||||
* Free text to be logged
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $message;
|
||||
|
||||
/**
|
||||
* The time that the event was created
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $time;
|
||||
|
||||
/**
|
||||
* Extra data to be held separate
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $args;
|
||||
|
||||
/**
|
||||
* @param string $section
|
||||
* @param int $priority
|
||||
* @param string $message
|
||||
* @param array $args
|
||||
*/
|
||||
public function __construct($section, $priority, $message, $args) {
|
||||
$this->section = $section;
|
||||
$this->priority = $priority;
|
||||
$this->message = $message;
|
||||
$this->args = $args;
|
||||
$this->time = time();
|
||||
}
|
||||
}
|
||||
|
339
core/event.php
Normal file
339
core/event.php
Normal file
|
@ -0,0 +1,339 @@
|
|||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Generic parent class for all events.
|
||||
*
|
||||
* An event is anything that can be passed around via send_event($blah)
|
||||
*/
|
||||
abstract class Event
|
||||
{
|
||||
public $stop_processing = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return var_export($this, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A wake-up call for extensions. Upon recieving an InitExtEvent an extension
|
||||
* should check that it's database tables are there and install them if not,
|
||||
* and set any defaults with Config::set_default_int() and such.
|
||||
*
|
||||
* This event is sent before $user is set to anything
|
||||
*/
|
||||
class InitExtEvent extends Event
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A signal that a page has been requested.
|
||||
*
|
||||
* User requests /view/42 -> an event is generated with $args = array("view",
|
||||
* "42"); when an event handler asks $event->page_matches("view"), it returns
|
||||
* true and ignores the matched part, such that $event->count_args() = 1 and
|
||||
* $event->get_arg(0) = "42"
|
||||
*/
|
||||
class PageRequestEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $args;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $arg_count;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $part_count;
|
||||
|
||||
public function __construct(string $path)
|
||||
{
|
||||
parent::__construct();
|
||||
global $config;
|
||||
|
||||
// trim starting slashes
|
||||
$path = ltrim($path, "/");
|
||||
|
||||
// if path is not specified, use the default front page
|
||||
if (empty($path)) { /* empty is faster than strlen */
|
||||
$path = $config->get_string(SetupConfig::FRONT_PAGE);
|
||||
}
|
||||
|
||||
// break the path into parts
|
||||
$args = explode('/', $path);
|
||||
|
||||
$this->args = $args;
|
||||
$this->arg_count = count($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the requested path matches a given pattern.
|
||||
*
|
||||
* If it matches, store the remaining path elements in $args
|
||||
*/
|
||||
public function page_matches(string $name): bool
|
||||
{
|
||||
$parts = explode("/", $name);
|
||||
$this->part_count = count($parts);
|
||||
|
||||
if ($this->part_count > $this->arg_count) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for ($i=0; $i<$this->part_count; $i++) {
|
||||
if ($parts[$i] != $this->args[$i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the n th argument of the page request (if it exists.)
|
||||
*/
|
||||
public function get_arg(int $n): string
|
||||
{
|
||||
$offset = $this->part_count + $n;
|
||||
if ($offset >= 0 && $offset < $this->arg_count) {
|
||||
return $this->args[$offset];
|
||||
} else {
|
||||
throw new SCoreException("Requested an invalid argument #$n");
|
||||
}
|
||||
}
|
||||
|
||||
public function try_page_num(int $n): int
|
||||
{
|
||||
if ($this->count_args() > $n) {
|
||||
$i = $this->get_arg($n);
|
||||
if (is_numeric($i) && int_escape($i) > 0) {
|
||||
return int_escape($i);
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of arguments the page request has.
|
||||
*/
|
||||
public function count_args(): int
|
||||
{
|
||||
return $this->arg_count - $this->part_count;
|
||||
}
|
||||
|
||||
/*
|
||||
* Many things use these functions
|
||||
*/
|
||||
|
||||
public function get_search_terms(): array
|
||||
{
|
||||
$search_terms = [];
|
||||
if ($this->count_args() === 2) {
|
||||
$search_terms = Tag::explode(Tag::decaret($this->get_arg(0)));
|
||||
}
|
||||
return $search_terms;
|
||||
}
|
||||
|
||||
public function get_page_number(): int
|
||||
{
|
||||
$page_number = 1;
|
||||
if ($this->count_args() === 1) {
|
||||
$page_number = int_escape($this->get_arg(0));
|
||||
} elseif ($this->count_args() === 2) {
|
||||
$page_number = int_escape($this->get_arg(1));
|
||||
}
|
||||
if ($page_number === 0) {
|
||||
$page_number = 1;
|
||||
} // invalid -> 0
|
||||
return $page_number;
|
||||
}
|
||||
|
||||
public function get_page_size(): int
|
||||
{
|
||||
global $config;
|
||||
return $config->get_int(IndexConfig::IMAGES);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sent when index.php is called from the command line
|
||||
*/
|
||||
class CommandEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $cmd = "help";
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $args = [];
|
||||
|
||||
/**
|
||||
* #param string[] $args
|
||||
*/
|
||||
public function __construct(array $args)
|
||||
{
|
||||
parent::__construct();
|
||||
global $user;
|
||||
|
||||
$opts = [];
|
||||
$log_level = SCORE_LOG_WARNING;
|
||||
$arg_count = count($args);
|
||||
|
||||
for ($i=1; $i<$arg_count; $i++) {
|
||||
switch ($args[$i]) {
|
||||
case '-u':
|
||||
$user = User::by_name($args[++$i]);
|
||||
if (is_null($user)) {
|
||||
die("Unknown user");
|
||||
} else {
|
||||
send_event(new UserLoginEvent($user));
|
||||
}
|
||||
break;
|
||||
case '-q':
|
||||
$log_level += 10;
|
||||
break;
|
||||
case '-v':
|
||||
$log_level -= 10;
|
||||
break;
|
||||
default:
|
||||
$opts[] = $args[$i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defined("CLI_LOG_LEVEL")) {
|
||||
define("CLI_LOG_LEVEL", $log_level);
|
||||
}
|
||||
|
||||
if (count($opts) > 0) {
|
||||
$this->cmd = $opts[0];
|
||||
$this->args = array_slice($opts, 1);
|
||||
} else {
|
||||
print "\n";
|
||||
print "Usage: php {$args[0]} [flags] [command]\n";
|
||||
print "\n";
|
||||
print "Flags:\n";
|
||||
print "\t-u [username]\n";
|
||||
print "\t\tLog in as the specified user\n";
|
||||
print "\t-q / -v\n";
|
||||
print "\t\tBe quieter / more verbose\n";
|
||||
print "\t\tScale is debug - info - warning - error - critical\n";
|
||||
print "\t\tDefault is to show warnings and above\n";
|
||||
print "\n";
|
||||
print "Currently known commands:\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A signal that some text needs formatting, the event carries
|
||||
* both the text and the result
|
||||
*/
|
||||
class TextFormattingEvent extends Event
|
||||
{
|
||||
/**
|
||||
* For reference
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $original;
|
||||
|
||||
/**
|
||||
* with formatting applied
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $formatted;
|
||||
|
||||
/**
|
||||
* with formatting removed
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $stripped;
|
||||
|
||||
public function __construct(string $text)
|
||||
{
|
||||
parent::__construct();
|
||||
// We need to escape before formatting, instead of at display time,
|
||||
// because formatters will add their own HTML tags into the mix and
|
||||
// we don't want to escape those.
|
||||
$h_text = html_escape(trim($text));
|
||||
$this->original = $h_text;
|
||||
$this->formatted = $h_text;
|
||||
$this->stripped = $h_text;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A signal that something needs logging
|
||||
*/
|
||||
class LogEvent extends Event
|
||||
{
|
||||
/**
|
||||
* a category, normally the extension name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $section;
|
||||
|
||||
/**
|
||||
* See python...
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $priority = 0;
|
||||
|
||||
/**
|
||||
* Free text to be logged
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $message;
|
||||
|
||||
/**
|
||||
* The time that the event was created
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $time;
|
||||
|
||||
/**
|
||||
* Extra data to be held separate
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $args;
|
||||
|
||||
public function __construct(string $section, int $priority, string $message)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->section = $section;
|
||||
$this->priority = $priority;
|
||||
$this->message = $message;
|
||||
$this->time = time();
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseUpgradeEvent extends Event
|
||||
{
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class SCoreException
|
||||
*
|
||||
* A base exception to be caught by the upper levels.
|
||||
*/
|
||||
class SCoreException extends Exception {}
|
||||
|
||||
/**
|
||||
* Class PermissionDeniedException
|
||||
*
|
||||
* A fairly common, generic exception.
|
||||
*/
|
||||
class PermissionDeniedException extends SCoreException {}
|
||||
|
||||
/**
|
||||
* Class ImageDoesNotExist
|
||||
*
|
||||
* This exception is used when an Image cannot be found by ID.
|
||||
*
|
||||
* Example: Image::by_id(-1) returns null
|
||||
*/
|
||||
class ImageDoesNotExist extends SCoreException {}
|
||||
|
||||
/*
|
||||
* For validate_input()
|
||||
*/
|
||||
class InvalidInput extends SCoreException {}
|
83
core/exceptions.php
Normal file
83
core/exceptions.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Class SCoreException
|
||||
*
|
||||
* A base exception to be caught by the upper levels.
|
||||
*/
|
||||
class SCoreException extends RuntimeException
|
||||
{
|
||||
/** @var string|null */
|
||||
public $query;
|
||||
|
||||
/** @var string */
|
||||
public $error;
|
||||
|
||||
public function __construct(string $msg, ?string $query=null)
|
||||
{
|
||||
parent::__construct($msg);
|
||||
$this->error = $msg;
|
||||
$this->query = $query;
|
||||
}
|
||||
}
|
||||
|
||||
class InstallerException extends RuntimeException
|
||||
{
|
||||
/** @var string */
|
||||
public $title;
|
||||
|
||||
/** @var string */
|
||||
public $body;
|
||||
|
||||
/** @var int */
|
||||
public $code;
|
||||
|
||||
public function __construct(string $title, string $body, int $code)
|
||||
{
|
||||
parent::__construct($body);
|
||||
$this->title = $title;
|
||||
$this->body = $body;
|
||||
$this->code = $code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class PermissionDeniedException
|
||||
*
|
||||
* A fairly common, generic exception.
|
||||
*/
|
||||
class PermissionDeniedException extends SCoreException
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Class ImageDoesNotExist
|
||||
*
|
||||
* This exception is used when an Image cannot be found by ID.
|
||||
*
|
||||
* Example: Image::by_id(-1) returns null
|
||||
*/
|
||||
class ImageDoesNotExist extends SCoreException
|
||||
{
|
||||
}
|
||||
|
||||
/*
|
||||
* For validate_input()
|
||||
*/
|
||||
class InvalidInput extends SCoreException
|
||||
{
|
||||
}
|
||||
|
||||
/*
|
||||
* This is used by the image resizing code when there is not enough memory to perform a resize.
|
||||
*/
|
||||
class InsufficientMemoryException extends SCoreException
|
||||
{
|
||||
}
|
||||
|
||||
/*
|
||||
* This is used by the image resizing code when there is an error while resizing
|
||||
*/
|
||||
class ImageResizeException extends SCoreException
|
||||
{
|
||||
}
|
|
@ -1,297 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* \page eande Events and Extensions
|
||||
*
|
||||
* An event is a little blob of data saying "something happened", possibly
|
||||
* "something happened, here's the specific data". Events are sent with the
|
||||
* send_event() function. Since events can store data, they can be used to
|
||||
* return data to the extension which sent them, for example:
|
||||
*
|
||||
* \code
|
||||
* $tfe = new TextFormattingEvent($original_text);
|
||||
* send_event($tfe);
|
||||
* $formatted_text = $tfe->formatted;
|
||||
* \endcode
|
||||
*
|
||||
* An extension is something which is capable of reacting to events.
|
||||
*
|
||||
*
|
||||
* \page hello The Hello World Extension
|
||||
*
|
||||
* \code
|
||||
* // ext/hello/main.php
|
||||
* public class HelloEvent extends Event {
|
||||
* public function __construct($username) {
|
||||
* $this->username = $username;
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* public class Hello extends Extension {
|
||||
* public function onPageRequest(PageRequestEvent $event) { // Every time a page request is sent
|
||||
* global $user; // Look at the global "currently logged in user" object
|
||||
* send_event(new HelloEvent($user->name)); // Broadcast a signal saying hello to that user
|
||||
* }
|
||||
* public function onHello(HelloEvent $event) { // When the "Hello" signal is recieved
|
||||
* $this->theme->display_hello($event->username); // Display a message on the web page
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // ext/hello/theme.php
|
||||
* public class HelloTheme extends Themelet {
|
||||
* public function display_hello($username) {
|
||||
* global $page;
|
||||
* $h_user = html_escape($username); // Escape the data before adding it to the page
|
||||
* $block = new Block("Hello!", "Hello there $h_user"); // HTML-safe variables start with "h_"
|
||||
* $page->add_block($block); // Add the block to the page
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // ext/hello/test.php
|
||||
* public class HelloTest extends SCorePHPUnitTestCase {
|
||||
* public function testHello() {
|
||||
* $this->get_page("post/list"); // View a page, any page
|
||||
* $this->assert_text("Hello there"); // Check that the specified text is in that page
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // themes/mytheme/hello.theme.php
|
||||
* public class CustomHelloTheme extends HelloTheme { // CustomHelloTheme overrides HelloTheme
|
||||
* public function display_hello($username) { // the display_hello() function is customised
|
||||
* global $page;
|
||||
* $h_user = html_escape($username);
|
||||
* $page->add_block(new Block(
|
||||
* "Hello!",
|
||||
* "Hello there $h_user, look at my snazzy custom theme!"
|
||||
* );
|
||||
* }
|
||||
* }
|
||||
* \endcode
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class Extension
|
||||
*
|
||||
* send_event(BlahEvent()) -> onBlah($event)
|
||||
*
|
||||
* Also loads the theme object into $this->theme if available
|
||||
*
|
||||
* The original concept came from Artanis's Extension extension
|
||||
* --> http://github.com/Artanis/simple-extension/tree/master
|
||||
* Then re-implemented by Shish after he broke the forum and couldn't
|
||||
* find the thread where the original was posted >_<
|
||||
*/
|
||||
abstract class Extension {
|
||||
/** @var array which DBs this ext supports (blank for 'all') */
|
||||
protected $db_support = [];
|
||||
|
||||
/** @var Themelet this theme's Themelet object */
|
||||
public $theme;
|
||||
|
||||
public function __construct() {
|
||||
$this->theme = $this->get_theme_object(get_called_class());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_live() {
|
||||
global $database;
|
||||
return (
|
||||
empty($this->db_support) ||
|
||||
in_array($database->get_driver_name(), $this->db_support)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the theme object for a given extension.
|
||||
*
|
||||
* @param string $base
|
||||
* @return Themelet
|
||||
*/
|
||||
private function get_theme_object($base) {
|
||||
$custom = 'Custom'.$base.'Theme';
|
||||
$normal = $base.'Theme';
|
||||
|
||||
if(class_exists($custom)) {
|
||||
return new $custom();
|
||||
}
|
||||
elseif(class_exists($normal)) {
|
||||
return new $normal();
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this to change the priority of the extension,
|
||||
* lower numbered ones will recieve events first.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_priority() {
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class FormatterExtension
|
||||
*
|
||||
* Several extensions have this in common, make a common API.
|
||||
*/
|
||||
abstract class FormatterExtension extends Extension {
|
||||
/**
|
||||
* @param TextFormattingEvent $event
|
||||
*/
|
||||
public function onTextFormatting(TextFormattingEvent $event) {
|
||||
$event->formatted = $this->format($event->formatted);
|
||||
$event->stripped = $this->strip($event->stripped);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
abstract public function format(/*string*/ $text);
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
abstract public function strip(/*string*/ $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Class DataHandlerExtension
|
||||
*
|
||||
* This too is a common class of extension with many methods in common,
|
||||
* so we have a base class to extend from.
|
||||
*/
|
||||
abstract class DataHandlerExtension extends Extension {
|
||||
/**
|
||||
* @param DataUploadEvent $event
|
||||
* @throws UploadException
|
||||
*/
|
||||
public function onDataUpload(DataUploadEvent $event) {
|
||||
$supported_ext = $this->supported_ext($event->type);
|
||||
$check_contents = $this->check_contents($event->tmpname);
|
||||
if($supported_ext && $check_contents) {
|
||||
move_upload_to_archive($event);
|
||||
send_event(new ThumbnailGenerationEvent($event->hash, $event->type));
|
||||
|
||||
/* Check if we are replacing an image */
|
||||
if(array_key_exists('replace', $event->metadata) && isset($event->metadata['replace'])) {
|
||||
/* hax: This seems like such a dirty way to do this.. */
|
||||
|
||||
/* Validate things */
|
||||
$image_id = int_escape($event->metadata['replace']);
|
||||
|
||||
/* Check to make sure the image exists. */
|
||||
$existing = Image::by_id($image_id);
|
||||
|
||||
if(is_null($existing)) {
|
||||
throw new UploadException("Image to replace does not exist!");
|
||||
}
|
||||
if ($existing->hash === $event->metadata['hash']) {
|
||||
throw new UploadException("The uploaded image is the same as the one to replace.");
|
||||
}
|
||||
|
||||
// even more hax..
|
||||
$event->metadata['tags'] = $existing->get_tag_list();
|
||||
$image = $this->create_image_from_data(warehouse_path("images", $event->metadata['hash']), $event->metadata);
|
||||
|
||||
if(is_null($image)) {
|
||||
throw new UploadException("Data handler failed to create image object from data");
|
||||
}
|
||||
|
||||
$ire = new ImageReplaceEvent($image_id, $image);
|
||||
send_event($ire);
|
||||
$event->image_id = $image_id;
|
||||
}
|
||||
else {
|
||||
$image = $this->create_image_from_data(warehouse_path("images", $event->hash), $event->metadata);
|
||||
if(is_null($image)) {
|
||||
throw new UploadException("Data handler failed to create image object from data");
|
||||
}
|
||||
$iae = new ImageAdditionEvent($image);
|
||||
send_event($iae);
|
||||
$event->image_id = $iae->image->id;
|
||||
|
||||
// Rating Stuff.
|
||||
if(!empty($event->metadata['rating'])){
|
||||
$rating = $event->metadata['rating'];
|
||||
send_event(new RatingSetEvent($image, $rating));
|
||||
}
|
||||
|
||||
// Locked Stuff.
|
||||
if(!empty($event->metadata['locked'])){
|
||||
$locked = $event->metadata['locked'];
|
||||
send_event(new LockSetEvent($image, !empty($locked)));
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif($supported_ext && !$check_contents){
|
||||
throw new UploadException("Invalid or corrupted file");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ThumbnailGenerationEvent $event
|
||||
*/
|
||||
public function onThumbnailGeneration(ThumbnailGenerationEvent $event) {
|
||||
if($this->supported_ext($event->type)) {
|
||||
if (method_exists($this, 'create_thumb_force') && $event->force == true) {
|
||||
$this->create_thumb_force($event->hash);
|
||||
}
|
||||
else {
|
||||
$this->create_thumb($event->hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DisplayingImageEvent $event
|
||||
*/
|
||||
public function onDisplayingImage(DisplayingImageEvent $event) {
|
||||
global $page;
|
||||
if($this->supported_ext($event->image->ext)) {
|
||||
$this->theme->display_image($page, $event->image);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
public function onSetupBuilding(SetupBuildingEvent $event) {
|
||||
$sb = $this->setup();
|
||||
if($sb) $event->panel->add_block($sb);
|
||||
}
|
||||
|
||||
protected function setup() {}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param string $ext
|
||||
* @return bool
|
||||
*/
|
||||
abstract protected function supported_ext($ext);
|
||||
|
||||
/**
|
||||
* @param string $tmpname
|
||||
* @return bool
|
||||
*/
|
||||
abstract protected function check_contents($tmpname);
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @param array $metadata
|
||||
* @return Image|null
|
||||
*/
|
||||
abstract protected function create_image_from_data($filename, $metadata);
|
||||
|
||||
/**
|
||||
* @param string $hash
|
||||
* @return bool
|
||||
*/
|
||||
abstract protected function create_thumb($hash);
|
||||
}
|
||||
|
505
core/extension.php
Normal file
505
core/extension.php
Normal file
|
@ -0,0 +1,505 @@
|
|||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* \page eande Events and Extensions
|
||||
*
|
||||
* An event is a little blob of data saying "something happened", possibly
|
||||
* "something happened, here's the specific data". Events are sent with the
|
||||
* send_event() function. Since events can store data, they can be used to
|
||||
* return data to the extension which sent them, for example:
|
||||
*
|
||||
* \code
|
||||
* $tfe = send_event(new TextFormattingEvent($original_text));
|
||||
* $formatted_text = $tfe->formatted;
|
||||
* \endcode
|
||||
*
|
||||
* An extension is something which is capable of reacting to events.
|
||||
*
|
||||
*
|
||||
* \page hello The Hello World Extension
|
||||
*
|
||||
* \code
|
||||
* // ext/hello/main.php
|
||||
* public class HelloEvent extends Event {
|
||||
* public function __construct($username) {
|
||||
* $this->username = $username;
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* public class Hello extends Extension {
|
||||
* public function onPageRequest(PageRequestEvent $event) { // Every time a page request is sent
|
||||
* global $user; // Look at the global "currently logged in user" object
|
||||
* send_event(new HelloEvent($user->name)); // Broadcast a signal saying hello to that user
|
||||
* }
|
||||
* public function onHello(HelloEvent $event) { // When the "Hello" signal is recieved
|
||||
* $this->theme->display_hello($event->username); // Display a message on the web page
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // ext/hello/theme.php
|
||||
* public class HelloTheme extends Themelet {
|
||||
* public function display_hello($username) {
|
||||
* global $page;
|
||||
* $h_user = html_escape($username); // Escape the data before adding it to the page
|
||||
* $block = new Block("Hello!", "Hello there $h_user"); // HTML-safe variables start with "h_"
|
||||
* $page->add_block($block); // Add the block to the page
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // ext/hello/test.php
|
||||
* public class HelloTest extends SCorePHPUnitTestCase {
|
||||
* public function testHello() {
|
||||
* $this->get_page("post/list"); // View a page, any page
|
||||
* $this->assert_text("Hello there"); // Check that the specified text is in that page
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // themes/mytheme/hello.theme.php
|
||||
* public class CustomHelloTheme extends HelloTheme { // CustomHelloTheme overrides HelloTheme
|
||||
* public function display_hello($username) { // the display_hello() function is customised
|
||||
* global $page;
|
||||
* $h_user = html_escape($username);
|
||||
* $page->add_block(new Block(
|
||||
* "Hello!",
|
||||
* "Hello there $h_user, look at my snazzy custom theme!"
|
||||
* );
|
||||
* }
|
||||
* }
|
||||
* \endcode
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class Extension
|
||||
*
|
||||
* send_event(BlahEvent()) -> onBlah($event)
|
||||
*
|
||||
* Also loads the theme object into $this->theme if available
|
||||
*
|
||||
* The original concept came from Artanis's Extension extension
|
||||
* --> http://github.com/Artanis/simple-extension/tree/master
|
||||
* Then re-implemented by Shish after he broke the forum and couldn't
|
||||
* find the thread where the original was posted >_<
|
||||
*/
|
||||
abstract class Extension
|
||||
{
|
||||
/** @var string */
|
||||
public $key;
|
||||
|
||||
/** @var Themelet */
|
||||
protected $theme;
|
||||
|
||||
/** @var ExtensionInfo */
|
||||
public $info;
|
||||
|
||||
private static $enabled_extensions = [];
|
||||
|
||||
public function __construct($class = null)
|
||||
{
|
||||
$class = $class ?? get_called_class();
|
||||
$this->theme = $this->get_theme_object($class);
|
||||
$this->info = ExtensionInfo::get_for_extension_class($class);
|
||||
if ($this->info===null) {
|
||||
throw new ScoreException("Info class not found for extension $class");
|
||||
}
|
||||
$this->key = $this->info->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the theme object for a given extension.
|
||||
*/
|
||||
private function get_theme_object(string $base): ?Themelet
|
||||
{
|
||||
$custom = 'Custom'.$base.'Theme';
|
||||
$normal = $base.'Theme';
|
||||
|
||||
if (class_exists($custom)) {
|
||||
return new $custom();
|
||||
} elseif (class_exists($normal)) {
|
||||
return new $normal();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this to change the priority of the extension,
|
||||
* lower numbered ones will receive events first.
|
||||
*/
|
||||
public function get_priority(): int
|
||||
{
|
||||
return 50;
|
||||
}
|
||||
|
||||
public static function determine_enabled_extensions()
|
||||
{
|
||||
self::$enabled_extensions = [];
|
||||
foreach (array_merge(
|
||||
ExtensionInfo::get_core_extensions(),
|
||||
explode(",", EXTRA_EXTS)
|
||||
) as $key) {
|
||||
$ext = ExtensionInfo::get_by_key($key);
|
||||
if ($ext===null || !$ext->is_supported()) {
|
||||
continue;
|
||||
}
|
||||
// FIXME: error if one of our dependencies isn't supported
|
||||
self::$enabled_extensions[] = $ext->key;
|
||||
if (!empty($ext->dependencies)) {
|
||||
foreach ($ext->dependencies as $dep) {
|
||||
self::$enabled_extensions[] = $dep;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function is_enabled(string $key): ?bool
|
||||
{
|
||||
return in_array($key, self::$enabled_extensions);
|
||||
}
|
||||
|
||||
public static function get_enabled_extensions(): array
|
||||
{
|
||||
return self::$enabled_extensions;
|
||||
}
|
||||
public static function get_enabled_extensions_as_string(): string
|
||||
{
|
||||
return implode(",", self::$enabled_extensions);
|
||||
}
|
||||
|
||||
protected function get_version(string $name): int
|
||||
{
|
||||
global $config;
|
||||
return $config->get_int($name, 0);
|
||||
}
|
||||
|
||||
protected function set_version(string $name, int $ver)
|
||||
{
|
||||
global $config;
|
||||
$config->set_int($name, $ver);
|
||||
log_info("upgrade", "Set version for $name to $ver");
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ExtensionInfo
|
||||
{
|
||||
// Every credit you get costs us RAM. It stops now.
|
||||
public const SHISH_NAME = "Shish";
|
||||
public const SHISH_EMAIL = "webmaster@shishnet.org";
|
||||
public const SHIMMIE_URL = "http://code.shishnet.org/shimmie2/";
|
||||
public const SHISH_AUTHOR = [self::SHISH_NAME=>self::SHISH_EMAIL];
|
||||
|
||||
public const LICENSE_GPLV2 = "GPLv2";
|
||||
public const LICENSE_MIT = "MIT";
|
||||
public const LICENSE_WTFPL = "WTFPL";
|
||||
|
||||
public const VISIBLE_ADMIN = "admin";
|
||||
public const VISIBLE_HIDDEN = "hidden";
|
||||
private const VALID_VISIBILITY = [self::VISIBLE_ADMIN, self::VISIBLE_HIDDEN];
|
||||
|
||||
public $key;
|
||||
|
||||
public $core = false;
|
||||
|
||||
public $beta = false;
|
||||
|
||||
public $name;
|
||||
public $authors = [];
|
||||
public $link;
|
||||
public $license;
|
||||
public $version;
|
||||
public $dependencies = [];
|
||||
public $visibility;
|
||||
public $description;
|
||||
public $documentation;
|
||||
|
||||
/** @var array which DBs this ext supports (blank for 'all') */
|
||||
public $db_support = [];
|
||||
|
||||
/** @var bool */
|
||||
private $supported = null;
|
||||
|
||||
/** @var string */
|
||||
private $support_info = null;
|
||||
|
||||
public function is_supported(): bool
|
||||
{
|
||||
if ($this->supported===null) {
|
||||
$this->check_support();
|
||||
}
|
||||
return $this->supported;
|
||||
}
|
||||
|
||||
public function get_support_info(): string
|
||||
{
|
||||
if ($this->supported===null) {
|
||||
$this->check_support();
|
||||
}
|
||||
return $this->support_info;
|
||||
}
|
||||
|
||||
private static $all_info_by_key = [];
|
||||
private static $all_info_by_class = [];
|
||||
private static $core_extensions = [];
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
assert(!empty($this->key), "key field is required");
|
||||
assert(!empty($this->name), "name field is required for extension $this->key");
|
||||
assert(empty($this->visibility) || in_array($this->visibility, self::VALID_VISIBILITY), "Invalid visibility for extension $this->key");
|
||||
assert(is_array($this->db_support), "db_support has to be an array for extension $this->key");
|
||||
assert(is_array($this->authors), "authors has to be an array for extension $this->key");
|
||||
assert(is_array($this->dependencies), "dependencies has to be an array for extension $this->key");
|
||||
}
|
||||
|
||||
public function is_enabled(): bool
|
||||
{
|
||||
return Extension::is_enabled($this->key);
|
||||
}
|
||||
|
||||
private function check_support()
|
||||
{
|
||||
global $database;
|
||||
$this->support_info = "";
|
||||
if (!empty($this->db_support) && !in_array($database->get_driver_name(), $this->db_support)) {
|
||||
$this->support_info .= "Database not supported. ";
|
||||
}
|
||||
// Additional checks here as needed
|
||||
|
||||
$this->supported = empty($this->support_info);
|
||||
}
|
||||
|
||||
public static function get_all(): array
|
||||
{
|
||||
return array_values(self::$all_info_by_key);
|
||||
}
|
||||
|
||||
public static function get_all_keys(): array
|
||||
{
|
||||
return array_keys(self::$all_info_by_key);
|
||||
}
|
||||
|
||||
public static function get_core_extensions(): array
|
||||
{
|
||||
return self::$core_extensions;
|
||||
}
|
||||
|
||||
public static function get_by_key(string $key): ?ExtensionInfo
|
||||
{
|
||||
if (array_key_exists($key, self::$all_info_by_key)) {
|
||||
return self::$all_info_by_key[$key];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static function get_for_extension_class(string $base): ?ExtensionInfo
|
||||
{
|
||||
$normal = $base.'Info';
|
||||
|
||||
if (array_key_exists($normal, self::$all_info_by_class)) {
|
||||
return self::$all_info_by_class[$normal];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static function load_all_extension_info()
|
||||
{
|
||||
foreach (getSubclassesOf("ExtensionInfo") as $class) {
|
||||
$extension_info = new $class();
|
||||
if (array_key_exists($extension_info->key, self::$all_info_by_key)) {
|
||||
throw new ScoreException("Extension Info $class with key $extension_info->key has already been loaded");
|
||||
}
|
||||
|
||||
self::$all_info_by_key[$extension_info->key] = $extension_info;
|
||||
self::$all_info_by_class[$class] = $extension_info;
|
||||
if ($extension_info->core===true) {
|
||||
self::$core_extensions[] = $extension_info->key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class FormatterExtension
|
||||
*
|
||||
* Several extensions have this in common, make a common API.
|
||||
*/
|
||||
abstract class FormatterExtension extends Extension
|
||||
{
|
||||
public function onTextFormatting(TextFormattingEvent $event)
|
||||
{
|
||||
$event->formatted = $this->format($event->formatted);
|
||||
$event->stripped = $this->strip($event->stripped);
|
||||
}
|
||||
|
||||
abstract public function format(string $text): string;
|
||||
abstract public function strip(string $text): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class DataHandlerExtension
|
||||
*
|
||||
* This too is a common class of extension with many methods in common,
|
||||
* so we have a base class to extend from.
|
||||
*/
|
||||
abstract class DataHandlerExtension extends Extension
|
||||
{
|
||||
protected $SUPPORTED_EXT = [];
|
||||
|
||||
protected function move_upload_to_archive(DataUploadEvent $event)
|
||||
{
|
||||
$target = warehouse_path(Image::IMAGE_DIR, $event->hash);
|
||||
if (!@copy($event->tmpname, $target)) {
|
||||
$errors = error_get_last();
|
||||
throw new UploadException(
|
||||
"Failed to copy file from uploads ({$event->tmpname}) to archive ($target): ".
|
||||
"{$errors['type']} / {$errors['message']}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function onDataUpload(DataUploadEvent $event)
|
||||
{
|
||||
$supported_ext = $this->supported_ext($event->type);
|
||||
$check_contents = $this->check_contents($event->tmpname);
|
||||
if ($supported_ext && $check_contents) {
|
||||
$this->move_upload_to_archive($event);
|
||||
send_event(new ThumbnailGenerationEvent($event->hash, $event->type));
|
||||
|
||||
/* Check if we are replacing an image */
|
||||
if (array_key_exists('replace', $event->metadata) && isset($event->metadata['replace'])) {
|
||||
/* hax: This seems like such a dirty way to do this.. */
|
||||
|
||||
/* Validate things */
|
||||
$image_id = int_escape($event->metadata['replace']);
|
||||
|
||||
/* Check to make sure the image exists. */
|
||||
$existing = Image::by_id($image_id);
|
||||
|
||||
if (is_null($existing)) {
|
||||
throw new UploadException("Image to replace does not exist!");
|
||||
}
|
||||
if ($existing->hash === $event->metadata['hash']) {
|
||||
throw new UploadException("The uploaded image is the same as the one to replace.");
|
||||
}
|
||||
|
||||
// even more hax..
|
||||
$event->metadata['tags'] = $existing->get_tag_list();
|
||||
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->metadata['hash']), $event->metadata);
|
||||
if (is_null($image)) {
|
||||
throw new UploadException("Data handler failed to create image object from data");
|
||||
}
|
||||
try {
|
||||
send_event(new MediaCheckPropertiesEvent($image));
|
||||
} catch (MediaException $e) {
|
||||
throw new UploadException("Unable to scan media properties: ".$e->getMessage());
|
||||
}
|
||||
|
||||
send_event(new ImageReplaceEvent($image_id, $image));
|
||||
$event->image_id = $image_id;
|
||||
} else {
|
||||
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata);
|
||||
if (is_null($image)) {
|
||||
throw new UploadException("Data handler failed to create image object from data");
|
||||
}
|
||||
try {
|
||||
send_event(new MediaCheckPropertiesEvent($image));
|
||||
} catch (MediaException $e) {
|
||||
throw new UploadException("Unable to scan media properties: ".$e->getMessage());
|
||||
}
|
||||
|
||||
$iae = send_event(new ImageAdditionEvent($image));
|
||||
$event->image_id = $iae->image->id;
|
||||
$event->merged = $iae->merged;
|
||||
|
||||
// Rating Stuff.
|
||||
if (!empty($event->metadata['rating'])) {
|
||||
$rating = $event->metadata['rating'];
|
||||
send_event(new RatingSetEvent($image, $rating));
|
||||
}
|
||||
|
||||
// Locked Stuff.
|
||||
if (!empty($event->metadata['locked'])) {
|
||||
$locked = $event->metadata['locked'];
|
||||
send_event(new LockSetEvent($image, !empty($locked)));
|
||||
}
|
||||
}
|
||||
} elseif ($supported_ext && !$check_contents) {
|
||||
// We DO support this extension - but the file looks corrupt
|
||||
throw new UploadException("Invalid or corrupted file");
|
||||
}
|
||||
}
|
||||
|
||||
public function onThumbnailGeneration(ThumbnailGenerationEvent $event)
|
||||
{
|
||||
$result = false;
|
||||
if ($this->supported_ext($event->type)) {
|
||||
if ($event->force) {
|
||||
$result = $this->create_thumb($event->hash, $event->type);
|
||||
} else {
|
||||
$outname = warehouse_path(Image::THUMBNAIL_DIR, $event->hash);
|
||||
if (file_exists($outname)) {
|
||||
return;
|
||||
}
|
||||
$result = $this->create_thumb($event->hash, $event->type);
|
||||
}
|
||||
}
|
||||
if ($result) {
|
||||
$event->generated = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function onDisplayingImage(DisplayingImageEvent $event)
|
||||
{
|
||||
global $page;
|
||||
if ($this->supported_ext($event->image->ext)) {
|
||||
/** @noinspection PhpPossiblePolymorphicInvocationInspection */
|
||||
$this->theme->display_image($page, $event->image);
|
||||
}
|
||||
}
|
||||
|
||||
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
|
||||
{
|
||||
if ($this->supported_ext($event->ext)) {
|
||||
$this->media_check_properties($event);
|
||||
}
|
||||
}
|
||||
|
||||
protected function create_image_from_data(string $filename, array $metadata): Image
|
||||
{
|
||||
global $config;
|
||||
|
||||
$image = new Image();
|
||||
|
||||
$image->filesize = $metadata['size'];
|
||||
$image->hash = $metadata['hash'];
|
||||
$image->filename = (($pos = strpos($metadata['filename'], '?')) !== false) ? substr($metadata['filename'], 0, $pos) : $metadata['filename'];
|
||||
if ($config->get_bool("upload_use_mime")) {
|
||||
$image->ext = get_extension(getMimeType($filename));
|
||||
} else {
|
||||
$image->ext = (($pos = strpos($metadata['extension'], '?')) !== false) ? substr($metadata['extension'], 0, $pos) : $metadata['extension'];
|
||||
}
|
||||
$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
|
||||
$image->source = $metadata['source'];
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
abstract protected function media_check_properties(MediaCheckPropertiesEvent $event): void;
|
||||
abstract protected function check_contents(string $tmpname): bool;
|
||||
abstract protected function create_thumb(string $hash, string $type): bool;
|
||||
|
||||
protected function supported_ext(string $ext): bool
|
||||
{
|
||||
return in_array(strtolower($ext), $this->SUPPORTED_EXT);
|
||||
}
|
||||
|
||||
public static function get_all_supported_exts(): array
|
||||
{
|
||||
$arr = [];
|
||||
foreach (getSubclassesOf("DataHandlerExtension") as $handler) {
|
||||
$arr = array_merge($arr, (new $handler())->SUPPORTED_EXT);
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
149
core/imageboard/event.php
Normal file
149
core/imageboard/event.php
Normal file
|
@ -0,0 +1,149 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* An image is being added to the database.
|
||||
*/
|
||||
class ImageAdditionEvent extends Event
|
||||
{
|
||||
/** @var User */
|
||||
public $user;
|
||||
|
||||
/** @var Image */
|
||||
public $image;
|
||||
|
||||
public $merged = false;
|
||||
|
||||
/**
|
||||
* Inserts a new image into the database with its associated
|
||||
* information. Also calls TagSetEvent to set the tags for
|
||||
* this new image.
|
||||
*/
|
||||
public function __construct(Image $image)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->image = $image;
|
||||
}
|
||||
}
|
||||
|
||||
class ImageAdditionException extends SCoreException
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* An image is being deleted.
|
||||
*/
|
||||
class ImageDeletionEvent extends Event
|
||||
{
|
||||
/** @var Image */
|
||||
public $image;
|
||||
|
||||
/** @var bool */
|
||||
public $force = false;
|
||||
|
||||
/**
|
||||
* Deletes an image.
|
||||
*
|
||||
* Used by things like tags and comments handlers to
|
||||
* clean out related rows in their tables.
|
||||
*/
|
||||
public function __construct(Image $image, bool $force = false)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->image = $image;
|
||||
$this->force = $force;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An image is being replaced.
|
||||
*/
|
||||
class ImageReplaceEvent extends Event
|
||||
{
|
||||
/** @var int */
|
||||
public $id;
|
||||
/** @var Image */
|
||||
public $image;
|
||||
|
||||
/**
|
||||
* Replaces an image.
|
||||
*
|
||||
* Updates an existing ID in the database to use a new image
|
||||
* file, leaving the tags and such unchanged. Also removes
|
||||
* the old image file and thumbnail from the disk.
|
||||
*/
|
||||
public function __construct(int $id, Image $image)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->id = $id;
|
||||
$this->image = $image;
|
||||
}
|
||||
}
|
||||
|
||||
class ImageReplaceException extends SCoreException
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a thumbnail be made for an image object.
|
||||
*/
|
||||
class ThumbnailGenerationEvent extends Event
|
||||
{
|
||||
/** @var string */
|
||||
public $hash;
|
||||
/** @var string */
|
||||
public $type;
|
||||
/** @var bool */
|
||||
public $force;
|
||||
|
||||
/** @var bool */
|
||||
public $generated;
|
||||
|
||||
/**
|
||||
* Request a thumbnail be made for an image object
|
||||
*/
|
||||
public function __construct(string $hash, string $type, bool $force=false)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->hash = $hash;
|
||||
$this->type = $type;
|
||||
$this->force = $force;
|
||||
$this->generated = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* ParseLinkTemplateEvent:
|
||||
* $link -- the formatted text (with each element URL Escape'd)
|
||||
* $text -- the formatted text (not escaped)
|
||||
* $original -- the formatting string, for reference
|
||||
* $image -- the image who's link is being parsed
|
||||
*/
|
||||
class ParseLinkTemplateEvent extends Event
|
||||
{
|
||||
/** @var string */
|
||||
public $link;
|
||||
/** @var string */
|
||||
public $text;
|
||||
/** @var string */
|
||||
public $original;
|
||||
/** @var Image */
|
||||
public $image;
|
||||
|
||||
public function __construct(string $link, Image $image)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->link = $link;
|
||||
$this->text = $link;
|
||||
$this->original = $link;
|
||||
$this->image = $image;
|
||||
}
|
||||
|
||||
public function replace(string $needle, ?string $replace): void
|
||||
{
|
||||
if (!is_null($replace)) {
|
||||
$this->link = str_replace($needle, url_escape($replace), $this->link);
|
||||
$this->text = str_replace($needle, $replace, $this->text);
|
||||
}
|
||||
}
|
||||
}
|
983
core/imageboard/image.php
Normal file
983
core/imageboard/image.php
Normal file
|
@ -0,0 +1,983 @@
|
|||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Class Image
|
||||
*
|
||||
* An object representing an entry in the images table.
|
||||
*
|
||||
* As of 2.2, this no longer necessarily represents an
|
||||
* image per se, but could be a video, sound file, or any
|
||||
* other supported upload type.
|
||||
*/
|
||||
class Image
|
||||
{
|
||||
public const IMAGE_DIR = "images";
|
||||
public const THUMBNAIL_DIR = "thumbs";
|
||||
|
||||
public static $order_sql = null; // this feels ugly
|
||||
|
||||
/** @var null|int */
|
||||
public $id = null;
|
||||
|
||||
/** @var int */
|
||||
public $height;
|
||||
|
||||
/** @var int */
|
||||
public $width;
|
||||
|
||||
/** @var string */
|
||||
public $hash;
|
||||
|
||||
/** @var int */
|
||||
public $filesize;
|
||||
|
||||
/** @var string */
|
||||
public $filename;
|
||||
|
||||
/** @var string */
|
||||
public $ext;
|
||||
|
||||
/** @var string[]|null */
|
||||
public $tag_array;
|
||||
|
||||
/** @var int */
|
||||
public $owner_id;
|
||||
|
||||
/** @var string */
|
||||
public $owner_ip;
|
||||
|
||||
/** @var string */
|
||||
public $posted;
|
||||
|
||||
/** @var string */
|
||||
public $source;
|
||||
|
||||
/** @var boolean */
|
||||
public $locked = false;
|
||||
|
||||
/** @var boolean */
|
||||
public $lossless = null;
|
||||
|
||||
/** @var boolean */
|
||||
public $video = null;
|
||||
|
||||
/** @var boolean */
|
||||
public $image = null;
|
||||
|
||||
/** @var boolean */
|
||||
public $audio = null;
|
||||
|
||||
/** @var int */
|
||||
public $length = null;
|
||||
|
||||
public static $bool_props = ["locked", "lossless", "video", "audio"];
|
||||
public static $int_props = ["id", "owner_id", "height", "width", "filesize", "length"];
|
||||
|
||||
/**
|
||||
* One will very rarely construct an image directly, more common
|
||||
* would be to use Image::by_id, Image::by_hash, etc.
|
||||
*/
|
||||
public function __construct(?array $row=null)
|
||||
{
|
||||
if (!is_null($row)) {
|
||||
foreach ($row as $name => $value) {
|
||||
// some databases use table.name rather than name
|
||||
$name = str_replace("images.", "", $name);
|
||||
|
||||
// hax, this is likely the cause of much scrutinizer-ci complaints.
|
||||
if (is_null($value)) {
|
||||
$this->$name = null;
|
||||
} elseif (in_array($name, self::$bool_props)) {
|
||||
$this->$name = bool_escape((string)$value);
|
||||
} elseif (in_array($name, self::$int_props)) {
|
||||
$this->$name = int_escape((string)$value);
|
||||
} else {
|
||||
$this->$name = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function by_id(int $id): ?Image
|
||||
{
|
||||
global $database;
|
||||
$row = $database->get_row("SELECT * FROM images WHERE images.id=:id", ["id"=>$id]);
|
||||
return ($row ? new Image($row) : null);
|
||||
}
|
||||
|
||||
public static function by_hash(string $hash): ?Image
|
||||
{
|
||||
global $database;
|
||||
$hash = strtolower($hash);
|
||||
$row = $database->get_row("SELECT images.* FROM images WHERE hash=:hash", ["hash"=>$hash]);
|
||||
return ($row ? new Image($row) : null);
|
||||
}
|
||||
|
||||
public static function by_id_or_hash(string $id): ?Image
|
||||
{
|
||||
return (is_numeric($id) && strlen($id) != 32) ? Image::by_id((int)$id) : Image::by_hash($id);
|
||||
}
|
||||
|
||||
public static function by_random(array $tags=[], int $limit_range=0): ?Image
|
||||
{
|
||||
$max = Image::count_images($tags);
|
||||
if ($max < 1) {
|
||||
return null;
|
||||
} // From Issue #22 - opened by HungryFeline on May 30, 2011.
|
||||
if ($limit_range > 0 && $max > $limit_range) {
|
||||
$max = $limit_range;
|
||||
}
|
||||
$rand = mt_rand(0, $max-1);
|
||||
$set = Image::find_images($rand, 1, $tags);
|
||||
if (count($set) > 0) {
|
||||
return $set[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static function find_images_internal(int $start = 0, ?int $limit = null, array $tags=[]): iterable
|
||||
{
|
||||
global $database, $user, $config;
|
||||
|
||||
if ($start < 0) {
|
||||
$start = 0;
|
||||
}
|
||||
if ($limit!=null && $limit < 1) {
|
||||
$limit = 1;
|
||||
}
|
||||
|
||||
if (SPEED_HAX) {
|
||||
if (!$user->can(Permissions::BIG_SEARCH) and count($tags) > 3) {
|
||||
throw new SCoreException("Anonymous users may only search for up to 3 tags at a time");
|
||||
}
|
||||
}
|
||||
|
||||
$order = (Image::$order_sql ?: "images.".$config->get_string(IndexConfig::ORDER));
|
||||
$querylet = Image::build_search_querylet($tags, $order, $limit, $start);
|
||||
$result = $database->get_all_iterable($querylet->sql, $querylet->variables);
|
||||
|
||||
Image::$order_sql = null;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for an array of images
|
||||
*
|
||||
* #param string[] $tags
|
||||
* #return Image[]
|
||||
*/
|
||||
public static function find_images(int $start, ?int $limit = null, array $tags=[]): array
|
||||
{
|
||||
$result = self::find_images_internal($start, $limit, $tags);
|
||||
|
||||
$images = [];
|
||||
foreach ($result as $row) {
|
||||
$images[] = new Image($row);
|
||||
}
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for an array of images, returning a iterable object of Image
|
||||
*/
|
||||
public static function find_images_iterable(int $start = 0, ?int $limit = null, array $tags=[]): Generator
|
||||
{
|
||||
$result = self::find_images_internal($start, $limit, $tags);
|
||||
foreach ($result as $row) {
|
||||
yield new Image($row);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Image-related utility functions
|
||||
*/
|
||||
|
||||
public static function count_total_images(): int
|
||||
{
|
||||
global $cache, $database;
|
||||
$total = $cache->get("image-count");
|
||||
if (!$total) {
|
||||
$total = (int)$database->get_one("SELECT COUNT(*) FROM images");
|
||||
$cache->set("image-count", $total, 600);
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
public static function count_tag(string $tag): int
|
||||
{
|
||||
global $database;
|
||||
return (int)$database->get_one(
|
||||
"SELECT count FROM tags WHERE LOWER(tag) = LOWER(:tag)",
|
||||
["tag"=>$tag]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of image results for a given search
|
||||
*
|
||||
* #param string[] $tags
|
||||
*/
|
||||
public static function count_images(array $tags=[]): int
|
||||
{
|
||||
global $cache, $database;
|
||||
$tag_count = count($tags);
|
||||
|
||||
if ($tag_count === 0) {
|
||||
// total number of images in the DB
|
||||
$total = self::count_total_images();
|
||||
} elseif ($tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) {
|
||||
if (!startsWith($tags[0], "-")) {
|
||||
// one tag - we can look that up directly
|
||||
$total = self::count_tag($tags[0]);
|
||||
} else {
|
||||
// one negative tag - subtract from the total
|
||||
$total = self::count_total_images() - self::count_tag(substr($tags[0], 1));
|
||||
}
|
||||
} else {
|
||||
// complex query
|
||||
// implode(tags) can be too long for memcache...
|
||||
$cache_key = "image-count:" . md5(Tag::implode($tags));
|
||||
$total = $cache->get($cache_key);
|
||||
if (!$total) {
|
||||
if (Extension::is_enabled(RatingsInfo::KEY)) {
|
||||
$tags[] = "rating:*";
|
||||
}
|
||||
$querylet = Image::build_search_querylet($tags);
|
||||
$total = (int)$database->get_one("SELECT COUNT(*) AS cnt FROM ($querylet->sql) AS tbl", $querylet->variables);
|
||||
if (SPEED_HAX && $total > 5000) {
|
||||
// when we have a ton of images, the count
|
||||
// won't change dramatically very often
|
||||
$cache->set($cache_key, $total, 3600);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_null($total)) {
|
||||
return 0;
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of pages for a given search
|
||||
*
|
||||
* #param string[] $tags
|
||||
*/
|
||||
public static function count_pages(array $tags=[]): int
|
||||
{
|
||||
global $config;
|
||||
return (int)ceil(Image::count_images($tags) / $config->get_int(IndexConfig::IMAGES));
|
||||
}
|
||||
|
||||
private static function terms_to_conditions(array $terms): array
|
||||
{
|
||||
$tag_conditions = [];
|
||||
$img_conditions = [];
|
||||
|
||||
/*
|
||||
* Turn a bunch of strings into a bunch of TagCondition
|
||||
* and ImgCondition objects
|
||||
*/
|
||||
$stpe = send_event(new SearchTermParseEvent(null, $terms));
|
||||
if ($stpe->is_querylet_set()) {
|
||||
foreach ($stpe->get_querylets() as $querylet) {
|
||||
$img_conditions[] = new ImgCondition($querylet, true);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($terms as $term) {
|
||||
$positive = true;
|
||||
if (is_string($term) && !empty($term) && ($term[0] == '-')) {
|
||||
$positive = false;
|
||||
$term = substr($term, 1);
|
||||
}
|
||||
if (strlen($term) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stpe = send_event(new SearchTermParseEvent($term, $terms));
|
||||
if ($stpe->is_querylet_set()) {
|
||||
foreach ($stpe->get_querylets() as $querylet) {
|
||||
$img_conditions[] = new ImgCondition($querylet, $positive);
|
||||
}
|
||||
} else {
|
||||
// if the whole match is wild, skip this
|
||||
if (str_replace("*", "", $term) != "") {
|
||||
$tag_conditions[] = new TagCondition($term, $positive);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [$tag_conditions, $img_conditions];
|
||||
}
|
||||
|
||||
/*
|
||||
* Accessors & mutators
|
||||
*/
|
||||
|
||||
/**
|
||||
* Find the next image in the sequence.
|
||||
*
|
||||
* Rather than simply $this_id + 1, one must take into account
|
||||
* deleted images and search queries
|
||||
*
|
||||
* #param string[] $tags
|
||||
*/
|
||||
public function get_next(array $tags=[], bool $next=true): ?Image
|
||||
{
|
||||
global $database;
|
||||
|
||||
if ($next) {
|
||||
$gtlt = "<";
|
||||
$dir = "DESC";
|
||||
} else {
|
||||
$gtlt = ">";
|
||||
$dir = "ASC";
|
||||
}
|
||||
|
||||
if (count($tags) === 0) {
|
||||
$row = $database->get_row('
|
||||
SELECT images.*
|
||||
FROM images
|
||||
WHERE images.id '.$gtlt.' '.$this->id.'
|
||||
ORDER BY images.id '.$dir.'
|
||||
LIMIT 1
|
||||
');
|
||||
} else {
|
||||
$tags[] = 'id'. $gtlt . $this->id;
|
||||
$querylet = Image::build_search_querylet($tags);
|
||||
$querylet->append_sql(' ORDER BY images.id '.$dir.' LIMIT 1');
|
||||
$row = $database->get_row($querylet->sql, $querylet->variables);
|
||||
}
|
||||
|
||||
return ($row ? new Image($row) : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* The reverse of get_next
|
||||
*
|
||||
* #param string[] $tags
|
||||
*/
|
||||
public function get_prev(array $tags=[]): ?Image
|
||||
{
|
||||
return $this->get_next($tags, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the User who owns this Image
|
||||
*/
|
||||
public function get_owner(): User
|
||||
{
|
||||
return User::by_id($this->owner_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the image's owner.
|
||||
*/
|
||||
public function set_owner(User $owner): void
|
||||
{
|
||||
global $database;
|
||||
if ($owner->id != $this->owner_id) {
|
||||
$database->execute("
|
||||
UPDATE images
|
||||
SET owner_id=:owner_id
|
||||
WHERE id=:id
|
||||
", ["owner_id"=>$owner->id, "id"=>$this->id]);
|
||||
log_info("core_image", "Owner for Image #{$this->id} set to {$owner->name}");
|
||||
}
|
||||
}
|
||||
|
||||
public function save_to_db()
|
||||
{
|
||||
global $database, $user;
|
||||
$cut_name = substr($this->filename, 0, 255);
|
||||
|
||||
if (is_null($this->id)) {
|
||||
$database->execute(
|
||||
"INSERT INTO images(
|
||||
owner_id, owner_ip,
|
||||
filename, filesize,
|
||||
hash, ext,
|
||||
width, height,
|
||||
posted, source
|
||||
)
|
||||
VALUES (
|
||||
:owner_id, :owner_ip,
|
||||
:filename, :filesize,
|
||||
:hash, :ext,
|
||||
0, 0,
|
||||
now(), :source
|
||||
)",
|
||||
[
|
||||
"owner_id" => $user->id, "owner_ip" => $_SERVER['REMOTE_ADDR'],
|
||||
"filename" => $cut_name, "filesize" => $this->filesize,
|
||||
"hash" => $this->hash, "ext" => strtolower($this->ext),
|
||||
"source" => $this->source
|
||||
]
|
||||
);
|
||||
$this->id = $database->get_last_insert_id('images_id_seq');
|
||||
} else {
|
||||
$database->execute(
|
||||
"UPDATE images SET ".
|
||||
"filename = :filename, filesize = :filesize, hash = :hash, ".
|
||||
"ext = :ext, width = 0, height = 0, source = :source ".
|
||||
"WHERE id = :id",
|
||||
[
|
||||
"filename" => $cut_name,
|
||||
"filesize" => $this->filesize,
|
||||
"hash" => $this->hash,
|
||||
"ext" => strtolower($this->ext),
|
||||
"source" => $this->source,
|
||||
"id" => $this->id,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$database->execute(
|
||||
"UPDATE images SET ".
|
||||
"lossless = :lossless, ".
|
||||
"video = :video, audio = :audio,image = :image, ".
|
||||
"height = :height, width = :width, ".
|
||||
"length = :length WHERE id = :id",
|
||||
[
|
||||
"id" => $this->id,
|
||||
"width" => $this->width ?? 0,
|
||||
"height" => $this->height ?? 0,
|
||||
"lossless" => $database->scoresql_value_prepare($this->lossless),
|
||||
"video" => $database->scoresql_value_prepare($this->video),
|
||||
"image" => $database->scoresql_value_prepare($this->image),
|
||||
"audio" => $database->scoresql_value_prepare($this->audio),
|
||||
"length" => $this->length
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this image's tags as an array.
|
||||
*
|
||||
* #return string[]
|
||||
*/
|
||||
public function get_tag_array(): array
|
||||
{
|
||||
global $database;
|
||||
if (!isset($this->tag_array)) {
|
||||
$this->tag_array = $database->get_col("
|
||||
SELECT tag
|
||||
FROM image_tags
|
||||
JOIN tags ON image_tags.tag_id = tags.id
|
||||
WHERE image_id=:id
|
||||
ORDER BY tag
|
||||
", ["id"=>$this->id]);
|
||||
}
|
||||
return $this->tag_array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this image's tags as a string.
|
||||
*/
|
||||
public function get_tag_list(): string
|
||||
{
|
||||
return Tag::implode($this->get_tag_array());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for the full size image
|
||||
*/
|
||||
public function get_image_link(): string
|
||||
{
|
||||
return $this->get_link(ImageConfig::ILINK, '_images/$hash/$id%20-%20$tags.$ext', 'image/$id.$ext');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nicely formatted version of the file name
|
||||
*/
|
||||
public function get_nice_image_name(): string
|
||||
{
|
||||
$plte = new ParseLinkTemplateEvent('$id - $tags.$ext', $this);
|
||||
send_event($plte);
|
||||
return $plte->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for the thumbnail
|
||||
*/
|
||||
public function get_thumb_link(): string
|
||||
{
|
||||
global $config;
|
||||
$ext = $config->get_string(ImageConfig::THUMB_TYPE);
|
||||
return $this->get_link(ImageConfig::TLINK, '_thumbs/$hash/thumb.'.$ext, 'thumb/$id.'.$ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check configured template for a link, then try nice URL, then plain URL
|
||||
*/
|
||||
private function get_link(string $template, string $nice, string $plain): string
|
||||
{
|
||||
global $config;
|
||||
|
||||
$image_link = $config->get_string($template);
|
||||
|
||||
if (!empty($image_link)) {
|
||||
if (!(strpos($image_link, "://") > 0) && !startsWith($image_link, "/")) {
|
||||
$image_link = make_link($image_link);
|
||||
}
|
||||
$chosen = $image_link;
|
||||
} elseif ($config->get_bool('nice_urls', false)) {
|
||||
$chosen = make_link($nice);
|
||||
} else {
|
||||
$chosen = make_link($plain);
|
||||
}
|
||||
return $this->parse_link_template($chosen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tooltip for this image, formatted according to the
|
||||
* configured template.
|
||||
*/
|
||||
public function get_tooltip(): string
|
||||
{
|
||||
global $config;
|
||||
$plte = new ParseLinkTemplateEvent($config->get_string(ImageConfig::TIP), $this);
|
||||
send_event($plte);
|
||||
return $plte->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out where the full size image is on disk.
|
||||
*/
|
||||
public function get_image_filename(): string
|
||||
{
|
||||
return warehouse_path(self::IMAGE_DIR, $this->hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out where the thumbnail is on disk.
|
||||
*/
|
||||
public function get_thumb_filename(): string
|
||||
{
|
||||
return warehouse_path(self::THUMBNAIL_DIR, $this->hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the original filename.
|
||||
*/
|
||||
public function get_filename(): string
|
||||
{
|
||||
return $this->filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image's mime type.
|
||||
*/
|
||||
public function get_mime_type(): string
|
||||
{
|
||||
return getMimeType($this->get_image_filename(), $this->get_ext());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image's filename extension
|
||||
*/
|
||||
public function get_ext(): string
|
||||
{
|
||||
return $this->ext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image's source URL
|
||||
*/
|
||||
public function get_source(): ?string
|
||||
{
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the image's source URL
|
||||
*/
|
||||
public function set_source(string $new_source): void
|
||||
{
|
||||
global $database;
|
||||
$old_source = $this->source;
|
||||
if (empty($new_source)) {
|
||||
$new_source = null;
|
||||
}
|
||||
if ($new_source != $old_source) {
|
||||
$database->execute("UPDATE images SET source=:source WHERE id=:id", ["source"=>$new_source, "id"=>$this->id]);
|
||||
log_info("core_image", "Source for Image #{$this->id} set to: $new_source (was $old_source)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the image is locked.
|
||||
*/
|
||||
public function is_locked(): bool
|
||||
{
|
||||
return $this->locked;
|
||||
}
|
||||
|
||||
public function set_locked(bool $tf): void
|
||||
{
|
||||
global $database;
|
||||
$ln = $tf ? "Y" : "N";
|
||||
$sln = $database->scoreql_to_sql('SCORE_BOOL_'.$ln);
|
||||
$sln = str_replace("'", "", $sln);
|
||||
$sln = str_replace('"', "", $sln);
|
||||
if (bool_escape($sln) !== $this->locked) {
|
||||
$database->execute("UPDATE images SET locked=:yn WHERE id=:id", ["yn"=>$sln, "id"=>$this->id]);
|
||||
log_info("core_image", "Setting Image #{$this->id} lock to: $ln");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all tags from this image.
|
||||
*
|
||||
* Normally in preparation to set them to a new set.
|
||||
*/
|
||||
public function delete_tags_from_image(): void
|
||||
{
|
||||
global $database;
|
||||
if ($database->get_driver_name() == DatabaseDriver::MYSQL) {
|
||||
//mysql < 5.6 has terrible subquery optimization, using EXISTS / JOIN fixes this
|
||||
$database->execute(
|
||||
"
|
||||
UPDATE tags t
|
||||
INNER JOIN image_tags it ON t.id = it.tag_id
|
||||
SET count = count - 1
|
||||
WHERE it.image_id = :id",
|
||||
["id"=>$this->id]
|
||||
);
|
||||
} else {
|
||||
$database->execute("
|
||||
UPDATE tags
|
||||
SET count = count - 1
|
||||
WHERE id IN (
|
||||
SELECT tag_id
|
||||
FROM image_tags
|
||||
WHERE image_id = :id
|
||||
)
|
||||
", ["id"=>$this->id]);
|
||||
}
|
||||
$database->execute("
|
||||
DELETE
|
||||
FROM image_tags
|
||||
WHERE image_id=:id
|
||||
", ["id"=>$this->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tags for this image.
|
||||
*/
|
||||
public function set_tags(array $unfiltered_tags): void
|
||||
{
|
||||
global $cache, $database, $page;
|
||||
|
||||
$unfiltered_tags = array_unique($unfiltered_tags);
|
||||
|
||||
$tags = [];
|
||||
foreach ($unfiltered_tags as $tag) {
|
||||
if (mb_strlen($tag, 'UTF-8') > 255) {
|
||||
$page->flash("Can't set a tag longer than 255 characters");
|
||||
continue;
|
||||
}
|
||||
if (startsWith($tag, "-")) {
|
||||
$page->flash("Can't set a tag which starts with a minus");
|
||||
continue;
|
||||
}
|
||||
|
||||
$tags[] = $tag;
|
||||
}
|
||||
|
||||
if (count($tags) <= 0) {
|
||||
throw new SCoreException('Tried to set zero tags');
|
||||
}
|
||||
|
||||
if (Tag::implode($tags) != $this->get_tag_list()) {
|
||||
// delete old
|
||||
$this->delete_tags_from_image();
|
||||
|
||||
$written_tags = [];
|
||||
|
||||
// insert each new tags
|
||||
foreach ($tags as $tag) {
|
||||
$id = $database->get_one(
|
||||
"
|
||||
SELECT id
|
||||
FROM tags
|
||||
WHERE LOWER(tag) = LOWER(:tag)
|
||||
",
|
||||
["tag"=>$tag]
|
||||
);
|
||||
if (empty($id)) {
|
||||
// a new tag
|
||||
$database->execute(
|
||||
"INSERT INTO tags(tag) VALUES (:tag)",
|
||||
["tag"=>$tag]
|
||||
);
|
||||
$database->execute(
|
||||
"INSERT INTO image_tags(image_id, tag_id)
|
||||
VALUES(:id, (SELECT id FROM tags WHERE LOWER(tag) = LOWER(:tag)))",
|
||||
["id"=>$this->id, "tag"=>$tag]
|
||||
);
|
||||
} else {
|
||||
// check if tag has already been written
|
||||
if (in_array($id, $written_tags)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$database->execute("
|
||||
INSERT INTO image_tags(image_id, tag_id)
|
||||
VALUES(:iid, :tid)
|
||||
", ["iid"=>$this->id, "tid"=>$id]);
|
||||
|
||||
array_push($written_tags, $id);
|
||||
}
|
||||
$database->execute(
|
||||
"
|
||||
UPDATE tags
|
||||
SET count = count + 1
|
||||
WHERE LOWER(tag) = LOWER(:tag)
|
||||
",
|
||||
["tag"=>$tag]
|
||||
);
|
||||
}
|
||||
|
||||
log_info("core_image", "Tags for Image #{$this->id} set to: ".Tag::implode($tags));
|
||||
$cache->delete("image-{$this->id}-tags");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this image from the database and disk
|
||||
*/
|
||||
public function delete(): void
|
||||
{
|
||||
global $database;
|
||||
$this->delete_tags_from_image();
|
||||
$database->execute("DELETE FROM images WHERE id=:id", ["id"=>$this->id]);
|
||||
log_info("core_image", 'Deleted Image #'.$this->id.' ('.$this->hash.')');
|
||||
|
||||
unlink($this->get_image_filename());
|
||||
unlink($this->get_thumb_filename());
|
||||
}
|
||||
|
||||
/**
|
||||
* This function removes an image (and thumbnail) from the DISK ONLY.
|
||||
* It DOES NOT remove anything from the database.
|
||||
*/
|
||||
public function remove_image_only(): void
|
||||
{
|
||||
log_info("core_image", 'Removed Image File ('.$this->hash.')');
|
||||
@unlink($this->get_image_filename());
|
||||
@unlink($this->get_thumb_filename());
|
||||
}
|
||||
|
||||
public function parse_link_template(string $tmpl, int $n=0): string
|
||||
{
|
||||
$plte = send_event(new ParseLinkTemplateEvent($tmpl, $this));
|
||||
$tmpl = $plte->link;
|
||||
return load_balance_url($tmpl, $this->hash, $n);
|
||||
}
|
||||
|
||||
private static function tag_or_wildcard_to_ids(string $tag): array
|
||||
{
|
||||
global $database;
|
||||
$sq = "SELECT id FROM tags WHERE LOWER(tag) LIKE LOWER(:tag)";
|
||||
if ($database->get_driver_name() === DatabaseDriver::SQLITE) {
|
||||
$sq .= "ESCAPE '\\'";
|
||||
}
|
||||
return $database->get_col($sq, ["tag" => Tag::sqlify($tag)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* #param string[] $terms
|
||||
*/
|
||||
private static function build_search_querylet(
|
||||
array $tags,
|
||||
?string $order=null,
|
||||
?int $limit=null,
|
||||
?int $offset=null
|
||||
): Querylet {
|
||||
list($tag_conditions, $img_conditions) = self::terms_to_conditions($tags);
|
||||
|
||||
$positive_tag_count = 0;
|
||||
$negative_tag_count = 0;
|
||||
foreach ($tag_conditions as $tq) {
|
||||
if ($tq->positive) {
|
||||
$positive_tag_count++;
|
||||
} else {
|
||||
$negative_tag_count++;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Turn a bunch of Querylet objects into a base query
|
||||
*
|
||||
* Must follow the format
|
||||
*
|
||||
* SELECT images.*
|
||||
* FROM (...) AS images
|
||||
* WHERE (...)
|
||||
*
|
||||
* ie, return a set of images.* columns, and end with a WHERE
|
||||
*/
|
||||
|
||||
// no tags, do a simple search
|
||||
if ($positive_tag_count === 0 && $negative_tag_count === 0) {
|
||||
$query = new Querylet("SELECT images.* FROM images WHERE 1=1");
|
||||
}
|
||||
|
||||
// one tag sorted by ID - we can fetch this from the image_tags table,
|
||||
// and do the offset / limit there, which is 10x faster than fetching
|
||||
// all the image_tags and doing the offset / limit on the result.
|
||||
elseif (
|
||||
(
|
||||
($positive_tag_count === 1 && $negative_tag_count === 0)
|
||||
|| ($positive_tag_count === 0 && $negative_tag_count === 1)
|
||||
)
|
||||
&& empty($img_conditions)
|
||||
&& ($order == "id DESC" || $order == "images.id DESC")
|
||||
&& !is_null($offset)
|
||||
&& !is_null($limit)
|
||||
) {
|
||||
$in = $positive_tag_count === 1 ? "IN" : "NOT IN";
|
||||
// IN (SELECT id FROM tags) is 100x slower than doing a separate
|
||||
// query and then a second query for IN(first_query_results)??
|
||||
$tag_array = self::tag_or_wildcard_to_ids($tag_conditions[0]->tag);
|
||||
if (count($tag_array) == 0) {
|
||||
if ($positive_tag_count == 1) {
|
||||
$query = new Querylet("SELECT images.* FROM images WHERE 1=0");
|
||||
} else {
|
||||
$query = new Querylet("SELECT images.* FROM images WHERE 1=1");
|
||||
}
|
||||
} else {
|
||||
$set = implode(', ', $tag_array);
|
||||
$query = new Querylet("
|
||||
SELECT images.*
|
||||
FROM images INNER JOIN (
|
||||
SELECT it.image_id
|
||||
FROM image_tags it
|
||||
WHERE it.tag_id $in ($set)
|
||||
ORDER BY it.image_id DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
) a on a.image_id = images.id
|
||||
ORDER BY images.id DESC
|
||||
", ["limit"=>$limit, "offset"=>$offset]);
|
||||
// don't offset at the image level because
|
||||
// we already offset at the image_tags level
|
||||
$order = null;
|
||||
$limit = null;
|
||||
$offset = null;
|
||||
}
|
||||
}
|
||||
|
||||
// more than one positive tag, or more than zero negative tags
|
||||
else {
|
||||
$positive_tag_id_array = [];
|
||||
$positive_wildcard_id_array = [];
|
||||
$negative_tag_id_array = [];
|
||||
|
||||
foreach ($tag_conditions as $tq) {
|
||||
$tag_ids = self::tag_or_wildcard_to_ids($tq->tag);
|
||||
$tag_count = count($tag_ids);
|
||||
|
||||
if ($tq->positive) {
|
||||
if ($tag_count== 0) {
|
||||
# one of the positive tags had zero results, therefor there
|
||||
# can be no results; "where 1=0" should shortcut things
|
||||
return new Querylet("SELECT images.* FROM images WHERE 1=0");
|
||||
} elseif ($tag_count==1) {
|
||||
// All wildcard terms that qualify for a single tag can be treated the same as non-wildcards
|
||||
$positive_tag_id_array[] = $tag_ids[0];
|
||||
} else {
|
||||
// Terms that resolve to multiple tags act as an OR within themselves
|
||||
// and as an AND in relation to all other terms,
|
||||
$positive_wildcard_id_array[] = $tag_ids;
|
||||
}
|
||||
} else {
|
||||
// Unlike positive criteria, negative criteria are all handled in an OR fashion,
|
||||
// so we can just compile them all into a single sub-query.
|
||||
$negative_tag_id_array = array_merge($negative_tag_id_array, $tag_ids);
|
||||
}
|
||||
}
|
||||
|
||||
assert($positive_tag_id_array || $positive_wildcard_id_array || $negative_tag_id_array, @$_GET['q']);
|
||||
if (!empty($positive_tag_id_array) || !empty($positive_wildcard_id_array)) {
|
||||
$inner_joins = [];
|
||||
if (!empty($positive_tag_id_array)) {
|
||||
foreach ($positive_tag_id_array as $tag) {
|
||||
$inner_joins[] = "= $tag";
|
||||
}
|
||||
}
|
||||
if (!empty($positive_wildcard_id_array)) {
|
||||
foreach ($positive_wildcard_id_array as $tags) {
|
||||
$positive_tag_id_list = join(', ', $tags);
|
||||
$inner_joins[] = "IN ($positive_tag_id_list)";
|
||||
}
|
||||
}
|
||||
|
||||
$first = array_shift($inner_joins);
|
||||
$sub_query = "SELECT it.image_id FROM image_tags it ";
|
||||
$i = 0;
|
||||
foreach ($inner_joins as $inner_join) {
|
||||
$i++;
|
||||
$sub_query .= " INNER JOIN image_tags it$i ON it$i.image_id = it.image_id AND it$i.tag_id $inner_join ";
|
||||
}
|
||||
if (!empty($negative_tag_id_array)) {
|
||||
$negative_tag_id_list = join(', ', $negative_tag_id_array);
|
||||
$sub_query .= " LEFT JOIN image_tags negative ON negative.image_id = it.image_id AND negative.tag_id IN ($negative_tag_id_list) ";
|
||||
}
|
||||
$sub_query .= "WHERE it.tag_id $first ";
|
||||
if (!empty($negative_tag_id_array)) {
|
||||
$sub_query .= " AND negative.image_id IS NULL";
|
||||
}
|
||||
$sub_query .= " GROUP BY it.image_id ";
|
||||
|
||||
$query = new Querylet("
|
||||
SELECT images.*
|
||||
FROM images
|
||||
INNER JOIN ($sub_query) a on a.image_id = images.id
|
||||
");
|
||||
} elseif (!empty($negative_tag_id_array)) {
|
||||
$negative_tag_id_list = join(', ', $negative_tag_id_array);
|
||||
$query = new Querylet("
|
||||
SELECT images.*
|
||||
FROM images
|
||||
LEFT JOIN image_tags negative ON negative.image_id = images.id AND negative.tag_id in ($negative_tag_id_list)
|
||||
WHERE negative.image_id IS NULL
|
||||
");
|
||||
} else {
|
||||
throw new SCoreException("No criteria specified");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Merge all the image metadata searches into one generic querylet
|
||||
* and append to the base querylet with "AND blah"
|
||||
*/
|
||||
if (!empty($img_conditions)) {
|
||||
$n = 0;
|
||||
$img_sql = "";
|
||||
$img_vars = [];
|
||||
foreach ($img_conditions as $iq) {
|
||||
if ($n++ > 0) {
|
||||
$img_sql .= " AND";
|
||||
}
|
||||
if (!$iq->positive) {
|
||||
$img_sql .= " NOT";
|
||||
}
|
||||
$img_sql .= " (" . $iq->qlet->sql . ")";
|
||||
$img_vars = array_merge($img_vars, $iq->qlet->variables);
|
||||
}
|
||||
$query->append_sql(" AND ");
|
||||
$query->append(new Querylet($img_sql, $img_vars));
|
||||
}
|
||||
|
||||
if (!is_null($order)) {
|
||||
$query->append(new Querylet(" ORDER BY ".$order));
|
||||
}
|
||||
if (!is_null($limit)) {
|
||||
$query->append(new Querylet(" LIMIT :limit ", ["limit" => $limit]));
|
||||
$query->append(new Querylet(" OFFSET :offset ", ["offset" => $offset]));
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
162
core/imageboard/misc.php
Normal file
162
core/imageboard/misc.php
Normal file
|
@ -0,0 +1,162 @@
|
|||
<?php declare(strict_types=1);
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Misc functions *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* Add a directory full of images
|
||||
*
|
||||
* @param string $base
|
||||
* @return array
|
||||
*/
|
||||
function add_dir(string $base): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach (list_files($base) as $full_path) {
|
||||
$short_path = str_replace($base, "", $full_path);
|
||||
$filename = basename($full_path);
|
||||
|
||||
$tags = path_to_tags($short_path);
|
||||
$result = "$short_path (".str_replace(" ", ", ", $tags).")... ";
|
||||
try {
|
||||
add_image($full_path, $filename, $tags);
|
||||
$result .= "ok";
|
||||
} catch (UploadException $ex) {
|
||||
$result .= "failed: ".$ex->getMessage();
|
||||
}
|
||||
$results[] = $result;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DataUploadEvent for a file.
|
||||
*
|
||||
* @param string $tmpname
|
||||
* @param string $filename
|
||||
* @param string $tags
|
||||
* @throws UploadException
|
||||
*/
|
||||
function add_image(string $tmpname, string $filename, string $tags): void
|
||||
{
|
||||
assert(file_exists($tmpname));
|
||||
|
||||
$pathinfo = pathinfo($filename);
|
||||
$metadata = [];
|
||||
$metadata['filename'] = $pathinfo['basename'];
|
||||
if (array_key_exists('extension', $pathinfo)) {
|
||||
$metadata['extension'] = $pathinfo['extension'];
|
||||
}
|
||||
|
||||
$metadata['tags'] = Tag::explode($tags);
|
||||
$metadata['source'] = null;
|
||||
send_event(new DataUploadEvent($tmpname, $metadata));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a full size pair of dimensions, return a pair scaled down to fit
|
||||
* into the configured thumbnail square, with ratio intact.
|
||||
* Optionally uses the High-DPI scaling setting to adjust the final resolution.
|
||||
*
|
||||
* @param int $orig_width
|
||||
* @param int $orig_height
|
||||
* @param bool $use_dpi_scaling Enables the High-DPI scaling.
|
||||
* @return array
|
||||
*/
|
||||
function get_thumbnail_size(int $orig_width, int $orig_height, bool $use_dpi_scaling = false): array
|
||||
{
|
||||
global $config;
|
||||
|
||||
if ($orig_width === 0) {
|
||||
$orig_width = 192;
|
||||
}
|
||||
if ($orig_height === 0) {
|
||||
$orig_height = 192;
|
||||
}
|
||||
|
||||
if ($orig_width > $orig_height * 5) {
|
||||
$orig_width = $orig_height * 5;
|
||||
}
|
||||
if ($orig_height > $orig_width * 5) {
|
||||
$orig_height = $orig_width * 5;
|
||||
}
|
||||
|
||||
|
||||
if ($use_dpi_scaling) {
|
||||
list($max_width, $max_height) = get_thumbnail_max_size_scaled();
|
||||
} else {
|
||||
$max_width = $config->get_int(ImageConfig::THUMB_WIDTH);
|
||||
$max_height = $config->get_int(ImageConfig::THUMB_HEIGHT);
|
||||
}
|
||||
|
||||
$output = get_scaled_by_aspect_ratio($orig_width, $orig_height, $max_width, $max_height);
|
||||
|
||||
if ($output[2] > 1 && $config->get_bool('thumb_upscale')) {
|
||||
return [(int)$orig_width, (int)$orig_height];
|
||||
} else {
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
function get_scaled_by_aspect_ratio(int $original_width, int $original_height, int $max_width, int $max_height) : array
|
||||
{
|
||||
$xscale = ($max_width/ $original_width);
|
||||
$yscale = ($max_height/ $original_height);
|
||||
|
||||
$scale = ($yscale < $xscale) ? $yscale : $xscale ;
|
||||
|
||||
return [(int)($original_width*$scale), (int)($original_height*$scale), $scale];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the thumbnails height and width settings and applies the High-DPI scaling setting before returning the dimensions.
|
||||
*
|
||||
* @return array [width, height]
|
||||
*/
|
||||
function get_thumbnail_max_size_scaled(): array
|
||||
{
|
||||
global $config;
|
||||
|
||||
$scaling = $config->get_int(ImageConfig::THUMB_SCALING);
|
||||
$max_width = $config->get_int(ImageConfig::THUMB_WIDTH) * ($scaling/100);
|
||||
$max_height = $config->get_int(ImageConfig::THUMB_HEIGHT) * ($scaling/100);
|
||||
return [$max_width, $max_height];
|
||||
}
|
||||
|
||||
|
||||
function create_image_thumb(string $hash, string $type, string $engine = null)
|
||||
{
|
||||
$inname = warehouse_path(Image::IMAGE_DIR, $hash);
|
||||
$outname = warehouse_path(Image::THUMBNAIL_DIR, $hash);
|
||||
$tsize = get_thumbnail_max_size_scaled();
|
||||
create_scaled_image($inname, $outname, $tsize, $type, $engine);
|
||||
}
|
||||
|
||||
function create_scaled_image(string $inname, string $outname, array $tsize, string $type, ?string $engine)
|
||||
{
|
||||
global $config;
|
||||
if (empty($engine)) {
|
||||
$engine = $config->get_string(ImageConfig::THUMB_ENGINE);
|
||||
}
|
||||
|
||||
$output_format = $config->get_string(ImageConfig::THUMB_TYPE);
|
||||
if ($output_format=="webp") {
|
||||
$output_format = Media::WEBP_LOSSY;
|
||||
}
|
||||
|
||||
send_event(new MediaResizeEvent(
|
||||
$engine,
|
||||
$inname,
|
||||
$type,
|
||||
$outname,
|
||||
$tsize[0],
|
||||
$tsize[1],
|
||||
false,
|
||||
$output_format,
|
||||
$config->get_int(ImageConfig::THUMB_QUALITY),
|
||||
true,
|
||||
$config->get_bool('thumb_upscale', false)
|
||||
));
|
||||
}
|
58
core/imageboard/search.php
Normal file
58
core/imageboard/search.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php declare(strict_types=1);
|
||||
class Querylet
|
||||
{
|
||||
/** @var string */
|
||||
public $sql;
|
||||
/** @var array */
|
||||
public $variables;
|
||||
|
||||
public function __construct(string $sql, array $variables=[])
|
||||
{
|
||||
$this->sql = $sql;
|
||||
$this->variables = $variables;
|
||||
}
|
||||
|
||||
public function append(Querylet $querylet): void
|
||||
{
|
||||
$this->sql .= $querylet->sql;
|
||||
$this->variables = array_merge($this->variables, $querylet->variables);
|
||||
}
|
||||
|
||||
public function append_sql(string $sql): void
|
||||
{
|
||||
$this->sql .= $sql;
|
||||
}
|
||||
|
||||
public function add_variable($var): void
|
||||
{
|
||||
$this->variables[] = $var;
|
||||
}
|
||||
}
|
||||
|
||||
class TagCondition
|
||||
{
|
||||
/** @var string */
|
||||
public $tag;
|
||||
/** @var bool */
|
||||
public $positive;
|
||||
|
||||
public function __construct(string $tag, bool $positive)
|
||||
{
|
||||
$this->tag = $tag;
|
||||
$this->positive = $positive;
|
||||
}
|
||||
}
|
||||
|
||||
class ImgCondition
|
||||
{
|
||||
/** @var Querylet */
|
||||
public $qlet;
|
||||
/** @var bool */
|
||||
public $positive;
|
||||
|
||||
public function __construct(Querylet $qlet, bool $positive)
|
||||
{
|
||||
$this->qlet = $qlet;
|
||||
$this->positive = $positive;
|
||||
}
|
||||
}
|
218
core/imageboard/tag.php
Normal file
218
core/imageboard/tag.php
Normal file
|
@ -0,0 +1,218 @@
|
|||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Class Tag
|
||||
*
|
||||
* A class for organising the tag related functions.
|
||||
*
|
||||
* All the methods are static, one should never actually use a tag object.
|
||||
*
|
||||
*/
|
||||
class Tag
|
||||
{
|
||||
public static function implode(array $tags): string
|
||||
{
|
||||
sort($tags);
|
||||
$tags = implode(' ', $tags);
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a human-supplied string into a valid tag array.
|
||||
*
|
||||
* #return string[]
|
||||
*/
|
||||
public static function explode(string $tags, bool $tagme=true): array
|
||||
{
|
||||
global $database;
|
||||
|
||||
$tags = explode(' ', trim($tags));
|
||||
|
||||
/* sanitise by removing invisible / dodgy characters */
|
||||
$tag_array = self::sanitize_array($tags);
|
||||
|
||||
/* if user supplied a blank string, add "tagme" */
|
||||
if (count($tag_array) === 0 && $tagme) {
|
||||
$tag_array = ["tagme"];
|
||||
}
|
||||
|
||||
/* resolve aliases */
|
||||
$new = [];
|
||||
$i = 0;
|
||||
$tag_count = count($tag_array);
|
||||
while ($i<$tag_count) {
|
||||
$tag = $tag_array[$i];
|
||||
$negative = '';
|
||||
if (!empty($tag) && ($tag[0] == '-')) {
|
||||
$negative = '-';
|
||||
$tag = substr($tag, 1);
|
||||
}
|
||||
|
||||
$newtags = $database->get_one(
|
||||
"
|
||||
SELECT newtag
|
||||
FROM aliases
|
||||
WHERE LOWER(oldtag)=LOWER(:tag)
|
||||
",
|
||||
["tag"=>$tag]
|
||||
);
|
||||
if (empty($newtags)) {
|
||||
//tag has no alias, use old tag
|
||||
$aliases = [$tag];
|
||||
} else {
|
||||
$aliases = explode(" ", $newtags); // Tag::explode($newtags); - recursion can be infinite
|
||||
}
|
||||
|
||||
foreach ($aliases as $alias) {
|
||||
if (!in_array($alias, $new)) {
|
||||
if ($tag == $alias) {
|
||||
$new[] = $negative.$alias;
|
||||
} elseif (!in_array($alias, $tag_array)) {
|
||||
$tag_array[] = $negative.$alias;
|
||||
$tag_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
|
||||
/* remove any duplicate tags */
|
||||
$tag_array = array_iunique($new);
|
||||
|
||||
/* tidy up */
|
||||
sort($tag_array);
|
||||
|
||||
return $tag_array;
|
||||
}
|
||||
|
||||
public static function sanitize(string $tag): string
|
||||
{
|
||||
$tag = preg_replace("/\s/", "", $tag); # whitespace
|
||||
$tag = preg_replace('/\x20[\x0e\x0f]/', '', $tag); # unicode RTL
|
||||
$tag = preg_replace("/\.+/", ".", $tag); # strings of dots?
|
||||
$tag = preg_replace("/^(\.+[\/\\\\])+/", "", $tag); # trailing slashes?
|
||||
$tag = trim($tag, ", \t\n\r\0\x0B");
|
||||
|
||||
if($tag == ".") $tag = ""; // hard-code one bad case...
|
||||
|
||||
if (mb_strlen($tag, 'UTF-8') > 255) {
|
||||
throw new ScoreException("The tag below is longer than 255 characters, please use a shorter tag.\n$tag\n");
|
||||
}
|
||||
return $tag;
|
||||
}
|
||||
|
||||
public static function compare(array $tags1, array $tags2): bool
|
||||
{
|
||||
if (count($tags1)!==count($tags2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tags1 = array_map("strtolower", $tags1);
|
||||
$tags2 = array_map("strtolower", $tags2);
|
||||
natcasesort($tags1);
|
||||
natcasesort($tags2);
|
||||
|
||||
for ($i = 0; $i < count($tags1); $i++) {
|
||||
if ($tags1[$i]!==$tags2[$i]) {
|
||||
var_dump($tags1);
|
||||
var_dump($tags2);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function get_diff_tags(array $source, array $remove): array
|
||||
{
|
||||
$before = array_map('strtolower', $source);
|
||||
$remove = array_map('strtolower', $remove);
|
||||
$after = [];
|
||||
foreach ($before as $tag) {
|
||||
if (!in_array($tag, $remove)) {
|
||||
$after[] = $tag;
|
||||
}
|
||||
}
|
||||
return $after;
|
||||
}
|
||||
|
||||
public static function sanitize_array(array $tags): array
|
||||
{
|
||||
global $page;
|
||||
$tag_array = [];
|
||||
foreach ($tags as $tag) {
|
||||
try {
|
||||
$tag = Tag::sanitize($tag);
|
||||
} catch (Exception $e) {
|
||||
$page->flash($e->getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($tag)) {
|
||||
$tag_array[] = $tag;
|
||||
}
|
||||
}
|
||||
return $tag_array;
|
||||
}
|
||||
|
||||
public static function sqlify(string $term): string
|
||||
{
|
||||
global $database;
|
||||
if ($database->get_driver_name() === DatabaseDriver::SQLITE) {
|
||||
$term = str_replace('\\', '\\\\', $term);
|
||||
}
|
||||
$term = str_replace('_', '\_', $term);
|
||||
$term = str_replace('%', '\%', $term);
|
||||
$term = str_replace('*', '%', $term);
|
||||
// $term = str_replace("?", "_", $term);
|
||||
return $term;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kind of like urlencode, but using a custom scheme so that
|
||||
* tags always fit neatly between slashes in a URL. Use this
|
||||
* when you want to put an arbitrary tag into a URL.
|
||||
*/
|
||||
public static function caret(string $input): string
|
||||
{
|
||||
$to_caret = [
|
||||
"^" => "^",
|
||||
"/" => "s",
|
||||
"\\" => "b",
|
||||
"?" => "q",
|
||||
"&" => "a",
|
||||
"." => "d",
|
||||
];
|
||||
|
||||
foreach ($to_caret as $from => $to) {
|
||||
$input = str_replace($from, '^' . $to, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this when you want to get a tag out of a URL
|
||||
*/
|
||||
public static function decaret(string $str): string
|
||||
{
|
||||
$from_caret = [
|
||||
"^" => "^",
|
||||
"s" => "/",
|
||||
"b" => "\\",
|
||||
"q" => "?",
|
||||
"a" => "&",
|
||||
"d" => ".",
|
||||
];
|
||||
|
||||
$out = "";
|
||||
$length = strlen($str);
|
||||
for ($i=0; $i<$length; $i++) {
|
||||
if ($str[$i] == "^") {
|
||||
$i++;
|
||||
$out .= $from_caret[$str[$i]] ?? '';
|
||||
} else {
|
||||
$out .= $str[$i];
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
364
core/install.php
Normal file
364
core/install.php
Normal file
|
@ -0,0 +1,364 @@
|
|||
<?php
|
||||
/**
|
||||
* Shimmie Installer
|
||||
*
|
||||
* @package Shimmie
|
||||
* @copyright Copyright (c) 2007-2015, Shish et al.
|
||||
* @author Shish [webmaster at shishnet.org], jgen [jeffgenovy at gmail.com]
|
||||
* @link http://code.shishnet.org/shimmie2/
|
||||
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2
|
||||
*
|
||||
* Initialise the database, check that folder
|
||||
* permissions are set properly.
|
||||
*
|
||||
* This file should be independent of the database
|
||||
* and other such things that aren't ready yet
|
||||
*/
|
||||
|
||||
function install()
|
||||
{
|
||||
date_default_timezone_set('UTC');
|
||||
define("DATABASE_TIMEOUT", 10000);
|
||||
|
||||
if (is_readable("data/config/shimmie.conf.php")) {
|
||||
exit_with_page(
|
||||
"Shimmie is already installed.",
|
||||
"data/config/shimmie.conf.php exists, how did you get here?"
|
||||
);
|
||||
}
|
||||
|
||||
if (!file_exists("vendor/")) {
|
||||
exit_with_page("Install Error", "
|
||||
<p>Shimmie is unable to find the composer vendor directory.</p>
|
||||
<p>Have you followed the composer setup instructions found in the
|
||||
<a href=\"https://github.com/shish/shimmie2#installation-development\">README</a>?</p>
|
||||
<p>If you are not intending to do any development with Shimmie,
|
||||
it is highly recommend you use one of the pre-packaged releases
|
||||
found on <a href=\"https://github.com/shish/shimmie2/releases\">Github</a> instead.</p>
|
||||
");
|
||||
}
|
||||
|
||||
// Pull in necessary files
|
||||
require_once "vendor/autoload.php";
|
||||
global $_tracer;
|
||||
$_tracer = new EventTracer();
|
||||
|
||||
require_once "core/exceptions.php";
|
||||
require_once "core/cacheengine.php";
|
||||
require_once "core/dbengine.php";
|
||||
require_once "core/database.php";
|
||||
require_once "core/util.php";
|
||||
|
||||
$dsn = get_dsn();
|
||||
if ($dsn) {
|
||||
do_install($dsn);
|
||||
} else {
|
||||
ask_questions();
|
||||
}
|
||||
}
|
||||
|
||||
function get_dsn()
|
||||
{
|
||||
if (file_exists("data/config/auto_install.conf.php")) {
|
||||
$dsn = null;
|
||||
/** @noinspection PhpIncludeInspection */
|
||||
require_once "data/config/auto_install.conf.php";
|
||||
} elseif (@$_POST["database_type"] == DatabaseDriver::SQLITE) {
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$id = bin2hex(random_bytes(5));
|
||||
$dsn = "sqlite:data/shimmie.{$id}.sqlite";
|
||||
} elseif (isset($_POST['database_type']) && isset($_POST['database_host']) && isset($_POST['database_user']) && isset($_POST['database_name'])) {
|
||||
$dsn = "{$_POST['database_type']}:user={$_POST['database_user']};password={$_POST['database_password']};host={$_POST['database_host']};dbname={$_POST['database_name']}";
|
||||
} else {
|
||||
$dsn = null;
|
||||
}
|
||||
return $dsn;
|
||||
}
|
||||
|
||||
function do_install($dsn)
|
||||
{
|
||||
try {
|
||||
create_dirs();
|
||||
create_tables(new Database($dsn));
|
||||
write_config($dsn);
|
||||
} catch (InstallerException $e) {
|
||||
exit_with_page($e->title, $e->body, $e->code);
|
||||
}
|
||||
}
|
||||
|
||||
function ask_questions()
|
||||
{
|
||||
$warnings = [];
|
||||
$errors = [];
|
||||
|
||||
if (check_gd_version() == 0 && check_im_version() == 0) {
|
||||
$errors[] = "
|
||||
No thumbnailers could be found - install the imagemagick
|
||||
tools (or the PHP-GD library, if imagemagick is unavailable).
|
||||
";
|
||||
} elseif (check_im_version() == 0) {
|
||||
$warnings[] = "
|
||||
The 'convert' command (from the imagemagick package)
|
||||
could not be found - PHP-GD can be used instead, but
|
||||
the size of thumbnails will be limited.
|
||||
";
|
||||
}
|
||||
|
||||
if (!function_exists('mb_strlen')) {
|
||||
$errors[] = "
|
||||
The mbstring PHP extension is missing - multibyte languages
|
||||
(eg non-english languages) may not work right.
|
||||
";
|
||||
}
|
||||
|
||||
$drivers = PDO::getAvailableDrivers();
|
||||
if (
|
||||
!in_array(DatabaseDriver::MYSQL, $drivers) &&
|
||||
!in_array(DatabaseDriver::PGSQL, $drivers) &&
|
||||
!in_array(DatabaseDriver::SQLITE, $drivers)
|
||||
) {
|
||||
$errors[] = "
|
||||
No database connection library could be found; shimmie needs
|
||||
PDO with either Postgres, MySQL, or SQLite drivers
|
||||
";
|
||||
}
|
||||
|
||||
$db_m = in_array(DatabaseDriver::MYSQL, $drivers) ? '<option value="'. DatabaseDriver::MYSQL .'">MySQL</option>' : "";
|
||||
$db_p = in_array(DatabaseDriver::PGSQL, $drivers) ? '<option value="'. DatabaseDriver::PGSQL .'">PostgreSQL</option>' : "";
|
||||
$db_s = in_array(DatabaseDriver::SQLITE, $drivers) ? '<option value="'. DatabaseDriver::SQLITE .'">SQLite</option>' : "";
|
||||
|
||||
$warn_msg = $warnings ? "<h3>Warnings</h3>".implode("\n<p>", $warnings) : "";
|
||||
$err_msg = $errors ? "<h3>Errors</h3>".implode("\n<p>", $errors) : "";
|
||||
|
||||
exit_with_page(
|
||||
"Install Options",
|
||||
<<<EOD
|
||||
$warn_msg
|
||||
$err_msg
|
||||
|
||||
<h3>Database Install</h3>
|
||||
<form action="index.php" method="POST">
|
||||
<div style="text-align: center;">
|
||||
<table class='form'>
|
||||
<tr>
|
||||
<th>Type:</th>
|
||||
<td><select name="database_type" id="database_type" onchange="update_qs();">
|
||||
$db_m
|
||||
$db_p
|
||||
$db_s
|
||||
</select></td>
|
||||
</tr>
|
||||
<tr class="dbconf mysql pgsql">
|
||||
<th>Host:</th>
|
||||
<td><input type="text" name="database_host" size="40" value="localhost"></td>
|
||||
</tr>
|
||||
<tr class="dbconf mysql pgsql">
|
||||
<th>Username:</th>
|
||||
<td><input type="text" name="database_user" size="40"></td>
|
||||
</tr>
|
||||
<tr class="dbconf mysql pgsql">
|
||||
<th>Password:</th>
|
||||
<td><input type="password" name="database_password" size="40"></td>
|
||||
</tr>
|
||||
<tr class="dbconf mysql pgsql">
|
||||
<th>DB Name:</th>
|
||||
<td><input type="text" name="database_name" size="40" value="shimmie"></td>
|
||||
</tr>
|
||||
<tr><td colspan="2"><input type="submit" value="Go!"></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', update_qs);
|
||||
function q(n) {
|
||||
return document.querySelectorAll(n);
|
||||
}
|
||||
function update_qs() {
|
||||
Array.prototype.forEach.call(q('.dbconf'), function(el, i){
|
||||
el.style.display = 'none';
|
||||
});
|
||||
let seldb = q("#database_type")[0].value || "none";
|
||||
Array.prototype.forEach.call(q('.'+seldb), function(el, i){
|
||||
el.style.display = null;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</form>
|
||||
|
||||
<h3>Help</h3>
|
||||
<p class="dbconf mysql pgsql">
|
||||
Please make sure the database you have chosen exists and is empty.<br>
|
||||
The username provided must have access to create tables within the database.
|
||||
</p>
|
||||
<p class="dbconf sqlite">
|
||||
For SQLite the database name will be a filename on disk, relative to
|
||||
where shimmie was installed.
|
||||
</p>
|
||||
<p class="dbconf none">
|
||||
Drivers can generally be downloaded with your OS package manager;
|
||||
for Debian / Ubuntu you want php-pgsql, php-mysql, or php-sqlite.
|
||||
</p>
|
||||
EOD
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function create_dirs()
|
||||
{
|
||||
$data_exists = file_exists("data") || mkdir("data");
|
||||
$data_writable = is_writable("data") || chmod("data", 0755);
|
||||
|
||||
if (!$data_exists || !$data_writable) {
|
||||
throw new InstallerException(
|
||||
"Directory Permissions Error:",
|
||||
"<p>Shimmie needs to have a 'data' folder in its directory, writable by the PHP user.</p>
|
||||
<p>If you see this error, if probably means the folder is owned by you, and it needs to be writable by the web server.</p>
|
||||
<p>PHP reports that it is currently running as user: ".$_ENV["USER"]." (". $_SERVER["USER"] .")</p>
|
||||
<p>Once you have created this folder and / or changed the ownership of the shimmie folder, hit 'refresh' to continue.</p>",
|
||||
7
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function create_tables(Database $db)
|
||||
{
|
||||
try {
|
||||
if ($db->count_tables() > 0) {
|
||||
throw new InstallerException(
|
||||
"Warning: The Database schema is not empty!",
|
||||
"<p>Please ensure that the database you are installing Shimmie with is empty before continuing.</p>
|
||||
<p>Once you have emptied the database of any tables, please hit 'refresh' to continue.</p>",
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
$db->create_table("aliases", "
|
||||
oldtag VARCHAR(128) NOT NULL,
|
||||
newtag VARCHAR(128) NOT NULL,
|
||||
PRIMARY KEY (oldtag)
|
||||
");
|
||||
$db->execute("CREATE INDEX aliases_newtag_idx ON aliases(newtag)", []);
|
||||
|
||||
$db->create_table("config", "
|
||||
name VARCHAR(128) NOT NULL,
|
||||
value TEXT,
|
||||
PRIMARY KEY (name)
|
||||
");
|
||||
$db->create_table("users", "
|
||||
id SCORE_AIPK,
|
||||
name VARCHAR(32) UNIQUE NOT NULL,
|
||||
pass VARCHAR(250),
|
||||
joindate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
class VARCHAR(32) NOT NULL DEFAULT 'user',
|
||||
email VARCHAR(128)
|
||||
");
|
||||
$db->execute("CREATE INDEX users_name_idx ON users(name)", []);
|
||||
|
||||
$db->execute("INSERT INTO users(name, pass, joindate, class) VALUES(:name, :pass, now(), :class)", ["name" => 'Anonymous', "pass" => null, "class" => 'anonymous']);
|
||||
$db->execute("INSERT INTO config(name, value) VALUES(:name, :value)", ["name" => 'anon_id', "value" => $db->get_last_insert_id('users_id_seq')]);
|
||||
|
||||
if (check_im_version() > 0) {
|
||||
$db->execute("INSERT INTO config(name, value) VALUES(:name, :value)", ["name" => 'thumb_engine', "value" => 'convert']);
|
||||
}
|
||||
|
||||
$db->create_table("images", "
|
||||
id SCORE_AIPK,
|
||||
owner_id INTEGER NOT NULL,
|
||||
owner_ip SCORE_INET NOT NULL,
|
||||
filename VARCHAR(64) NOT NULL,
|
||||
filesize INTEGER NOT NULL,
|
||||
hash CHAR(32) UNIQUE NOT NULL,
|
||||
ext CHAR(4) NOT NULL,
|
||||
source VARCHAR(255),
|
||||
width INTEGER NOT NULL,
|
||||
height INTEGER NOT NULL,
|
||||
posted TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
locked SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N,
|
||||
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT
|
||||
");
|
||||
$db->execute("CREATE INDEX images_owner_id_idx ON images(owner_id)", []);
|
||||
$db->execute("CREATE INDEX images_width_idx ON images(width)", []);
|
||||
$db->execute("CREATE INDEX images_height_idx ON images(height)", []);
|
||||
$db->execute("CREATE INDEX images_hash_idx ON images(hash)", []);
|
||||
|
||||
$db->create_table("tags", "
|
||||
id SCORE_AIPK,
|
||||
tag VARCHAR(64) UNIQUE NOT NULL,
|
||||
count INTEGER NOT NULL DEFAULT 0
|
||||
");
|
||||
$db->execute("CREATE INDEX tags_tag_idx ON tags(tag)", []);
|
||||
|
||||
$db->create_table("image_tags", "
|
||||
image_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
UNIQUE(image_id, tag_id),
|
||||
FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
||||
");
|
||||
$db->execute("CREATE INDEX images_tags_image_id_idx ON image_tags(image_id)", []);
|
||||
$db->execute("CREATE INDEX images_tags_tag_id_idx ON image_tags(tag_id)", []);
|
||||
|
||||
$db->execute("INSERT INTO config(name, value) VALUES('db_version', 11)");
|
||||
$db->commit();
|
||||
} catch (PDOException $e) {
|
||||
throw new InstallerException(
|
||||
"PDO Error:",
|
||||
"<p>An error occurred while trying to create the database tables necessary for Shimmie.</p>
|
||||
<p>Please check and ensure that the database configuration options are all correct.</p>
|
||||
<p>{$e->getMessage()}</p>",
|
||||
3
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function write_config($dsn)
|
||||
{
|
||||
$file_content = "<" . "?php\ndefine('DATABASE_DSN', '$dsn');\n";
|
||||
|
||||
if (!file_exists("data/config")) {
|
||||
mkdir("data/config", 0755, true);
|
||||
}
|
||||
|
||||
if (file_put_contents("data/config/shimmie.conf.php", $file_content, LOCK_EX)) {
|
||||
header("Location: index.php");
|
||||
exit_with_page(
|
||||
"Installation Successful",
|
||||
"<p>If you aren't redirected, <a href=\"index.php\">click here to Continue</a>."
|
||||
);
|
||||
} else {
|
||||
$h_file_content = htmlentities($file_content);
|
||||
throw new InstallerException(
|
||||
"File Permissions Error:",
|
||||
"The web server isn't allowed to write to the config file; please copy
|
||||
the text below, save it as 'data/config/shimmie.conf.php', and upload it into the shimmie
|
||||
folder manually. Make sure that when you save it, there is no whitespace
|
||||
before the \"<?php\".
|
||||
|
||||
<p><textarea cols='80' rows='2'>$h_file_content</textarea>
|
||||
|
||||
<p>Once done, <a href='index.php'>click here to Continue</a>.",
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function exit_with_page($title, $body, $code=0)
|
||||
{
|
||||
print("<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<title>Shimmie Installer</title>
|
||||
<link rel=\"shortcut icon\" href=\"ext/static_files/static/favicon.ico\">
|
||||
<link rel=\"stylesheet\" href=\"ext/static_files/style.css\" type=\"text/css\">
|
||||
</head>
|
||||
<body>
|
||||
<div id=\"installer\">
|
||||
<h1>Shimmie Installer</h1>
|
||||
<h3>$title</h3>
|
||||
<div class=\"container\">
|
||||
$body
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>");
|
||||
exit($code);
|
||||
}
|
73
core/logging.php
Normal file
73
core/logging.php
Normal file
|
@ -0,0 +1,73 @@
|
|||
<?php declare(strict_types=1);
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Logging convenience *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
define("SCORE_LOG_CRITICAL", 50);
|
||||
define("SCORE_LOG_ERROR", 40);
|
||||
define("SCORE_LOG_WARNING", 30);
|
||||
define("SCORE_LOG_INFO", 20);
|
||||
define("SCORE_LOG_DEBUG", 10);
|
||||
define("SCORE_LOG_NOTSET", 0);
|
||||
|
||||
/**
|
||||
* A shorthand way to send a LogEvent
|
||||
*
|
||||
* When parsing a user request, a flash message should give info to the user
|
||||
* When taking action, a log event should be stored by the server
|
||||
* Quite often, both of these happen at once, hence log_*() having $flash
|
||||
*/
|
||||
function log_msg(string $section, int $priority, string $message, ?string $flash=null)
|
||||
{
|
||||
global $page;
|
||||
send_event(new LogEvent($section, $priority, $message));
|
||||
$threshold = defined("CLI_LOG_LEVEL") ? CLI_LOG_LEVEL : 0;
|
||||
|
||||
if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && ($priority >= $threshold)) {
|
||||
print date("c")." $section: $message\n";
|
||||
ob_flush();
|
||||
}
|
||||
if (!is_null($flash)) {
|
||||
$page->flash($flash);
|
||||
}
|
||||
}
|
||||
|
||||
// More shorthand ways of logging
|
||||
function log_debug(string $section, string $message, ?string $flash=null)
|
||||
{
|
||||
log_msg($section, SCORE_LOG_DEBUG, $message, $flash);
|
||||
}
|
||||
function log_info(string $section, string $message, ?string $flash=null)
|
||||
{
|
||||
log_msg($section, SCORE_LOG_INFO, $message, $flash);
|
||||
}
|
||||
function log_warning(string $section, string $message, ?string $flash=null)
|
||||
{
|
||||
log_msg($section, SCORE_LOG_WARNING, $message, $flash);
|
||||
}
|
||||
function log_error(string $section, string $message, ?string $flash=null)
|
||||
{
|
||||
log_msg($section, SCORE_LOG_ERROR, $message, $flash);
|
||||
}
|
||||
function log_critical(string $section, string $message, ?string $flash=null)
|
||||
{
|
||||
log_msg($section, SCORE_LOG_CRITICAL, $message, $flash);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a unique ID for this request, useful for grouping log messages.
|
||||
*/
|
||||
function get_request_id(): string
|
||||
{
|
||||
static $request_id = null;
|
||||
if (!$request_id) {
|
||||
// not completely trustworthy, as a user can spoof this
|
||||
if (@$_SERVER['HTTP_X_VARNISH']) {
|
||||
$request_id = $_SERVER['HTTP_X_VARNISH'];
|
||||
} else {
|
||||
$request_id = "P" . uniqid();
|
||||
}
|
||||
}
|
||||
return $request_id;
|
||||
}
|
|
@ -1,424 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* \page themes Themes
|
||||
*
|
||||
* Each extension has a theme with a specific name -- eg. the extension Setup
|
||||
* which is stored in ext/setup/main.php will have a theme called SetupTheme
|
||||
* stored in ext/setup/theme.php. If you want to customise it, create a class
|
||||
* in the file themes/mytheme/setup.theme.php called CustomSetupTheme which
|
||||
* extends SetupTheme and overrides some of its methods.
|
||||
*
|
||||
* Generally an extension should only deal with processing data; whenever it
|
||||
* wants to display something, it should pass the data to be displayed to the
|
||||
* theme object, and the theme will add the data into the global $page
|
||||
* structure.
|
||||
*
|
||||
* A page should make sure that all the data it outputs is free from dangerous
|
||||
* data by using html_escape(), url_escape(), or int_escape() as appropriate.
|
||||
*
|
||||
* Because some HTML can be placed anywhere according to the theme, coming up
|
||||
* with the correct way to link to a page can be hard -- thus we have the
|
||||
* make_link() function, which will take a path like "post/list" and turn it
|
||||
* into a full and correct link, eg /myboard/post/list, /foo/index.php?q=post/list,
|
||||
* etc depending on how things are set up. This should always be used to link
|
||||
* to pages rather than hardcoding a path.
|
||||
*
|
||||
* Various other common functions are available as part of the Themelet class.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Class Page
|
||||
*
|
||||
* A data structure for holding all the bits of data that make up a page.
|
||||
*
|
||||
* The various extensions all add whatever they want to this structure,
|
||||
* then Layout turns it into HTML.
|
||||
*/
|
||||
class Page {
|
||||
/** @name Overall */
|
||||
//@{
|
||||
/** @var string */
|
||||
public $mode = "page";
|
||||
/** @var string */
|
||||
public $type = "text/html; charset=utf-8";
|
||||
|
||||
/**
|
||||
* Set what this page should do; "page", "data", or "redirect".
|
||||
* @param string $mode
|
||||
*/
|
||||
public function set_mode($mode) {
|
||||
$this->mode = $mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the page's MIME type.
|
||||
* @param string $type
|
||||
*/
|
||||
public function set_type($type) {
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
|
||||
//@}
|
||||
// ==============================================
|
||||
/** @name "data" mode */
|
||||
//@{
|
||||
|
||||
/** @var string; public only for unit test */
|
||||
public $data = "";
|
||||
|
||||
/** @var string; public only for unit test */
|
||||
public $filename = null;
|
||||
|
||||
/**
|
||||
* Set the raw data to be sent.
|
||||
* @param string $data
|
||||
*/
|
||||
public function set_data($data) {
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the recommended download filename.
|
||||
* @param string $filename
|
||||
*/
|
||||
public function set_filename($filename) {
|
||||
$this->filename = $filename;
|
||||
}
|
||||
|
||||
|
||||
//@}
|
||||
// ==============================================
|
||||
/** @name "redirect" mode */
|
||||
//@{
|
||||
|
||||
/** @var string */
|
||||
private $redirect = "";
|
||||
|
||||
/**
|
||||
* Set the URL to redirect to (remember to use make_link() if linking
|
||||
* to a page in the same site).
|
||||
* @param string $redirect
|
||||
*/
|
||||
public function set_redirect($redirect) {
|
||||
$this->redirect = $redirect;
|
||||
}
|
||||
|
||||
|
||||
//@}
|
||||
// ==============================================
|
||||
/** @name "page" mode */
|
||||
//@{
|
||||
|
||||
/** @var int */
|
||||
public $code = 200;
|
||||
|
||||
/** @var string */
|
||||
public $title = "";
|
||||
|
||||
/** @var string */
|
||||
public $heading = "";
|
||||
|
||||
/** @var string */
|
||||
public $subheading = "";
|
||||
|
||||
/** @var string */
|
||||
public $quicknav = "";
|
||||
|
||||
/** @var string[] */
|
||||
public $html_headers = array();
|
||||
|
||||
/** @var string[] */
|
||||
public $http_headers = array();
|
||||
|
||||
/** @var string[][] */
|
||||
public $cookies = array();
|
||||
|
||||
/** @var Block[] */
|
||||
public $blocks = array();
|
||||
|
||||
/**
|
||||
* Set the HTTP status code
|
||||
* @param int $code
|
||||
*/
|
||||
public function set_code($code) {
|
||||
$this->code = $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the window title.
|
||||
* @param string $title
|
||||
*/
|
||||
public function set_title($title) {
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the main heading.
|
||||
* @param string $heading
|
||||
*/
|
||||
public function set_heading($heading) {
|
||||
$this->heading = $heading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sub heading.
|
||||
* @param string $subheading
|
||||
*/
|
||||
public function set_subheading($subheading) {
|
||||
$this->subheading = $subheading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a line to the HTML head section.
|
||||
* @param string $line
|
||||
* @param int $position
|
||||
*/
|
||||
public function add_html_header($line, $position=50) {
|
||||
while(isset($this->html_headers[$position])) $position++;
|
||||
$this->html_headers[$position] = $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a http header to be sent to the client.
|
||||
* @param string $line
|
||||
* @param int $position
|
||||
*/
|
||||
public function add_http_header($line, $position=50) {
|
||||
while(isset($this->http_headers[$position])) $position++;
|
||||
$this->http_headers[$position] = $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* The counterpart for get_cookie, this works like php's
|
||||
* setcookie method, but prepends the site-wide cookie prefix to
|
||||
* the $name argument before doing anything.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @param int $time
|
||||
* @param string $path
|
||||
*/
|
||||
public function add_cookie($name, $value, $time, $path) {
|
||||
$full_name = COOKIE_PREFIX."_".$name;
|
||||
$this->cookies[] = array($full_name, $value, $time, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_cookie(/*string*/ $name) {
|
||||
$full_name = COOKIE_PREFIX."_".$name;
|
||||
if(isset($_COOKIE[$full_name])) {
|
||||
return $_COOKIE[$full_name];
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the HTML headers that are currently set and return as a string.
|
||||
* @return string
|
||||
*/
|
||||
public function get_all_html_headers() {
|
||||
$data = '';
|
||||
ksort($this->html_headers);
|
||||
foreach ($this->html_headers as $line) {
|
||||
$data .= "\t\t" . $line . "\n";
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all currently set HTML headers (Be careful..).
|
||||
*/
|
||||
public function delete_all_html_headers() {
|
||||
$this->html_headers = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Block of data to the page.
|
||||
* @param Block $block
|
||||
*/
|
||||
public function add_block(Block $block) {
|
||||
$this->blocks[] = $block;
|
||||
}
|
||||
|
||||
|
||||
//@}
|
||||
// ==============================================
|
||||
|
||||
/**
|
||||
* Display the page according to the mode and data given.
|
||||
*/
|
||||
public function display() {
|
||||
global $page, $user;
|
||||
|
||||
header("HTTP/1.0 {$this->code} Shimmie");
|
||||
header("Content-type: ".$this->type);
|
||||
header("X-Powered-By: SCore-".SCORE_VERSION);
|
||||
|
||||
if (!headers_sent()) {
|
||||
foreach($this->http_headers as $head) {
|
||||
header($head);
|
||||
}
|
||||
foreach($this->cookies as $c) {
|
||||
setcookie($c[0], $c[1], $c[2], $c[3]);
|
||||
}
|
||||
} else {
|
||||
print "Error: Headers have already been sent to the client.";
|
||||
}
|
||||
|
||||
switch($this->mode) {
|
||||
case "page":
|
||||
if(CACHE_HTTP) {
|
||||
header("Vary: Cookie, Accept-Encoding");
|
||||
if($user->is_anonymous() && $_SERVER["REQUEST_METHOD"] == "GET") {
|
||||
header("Cache-control: public, max-age=600");
|
||||
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 600) . ' GMT');
|
||||
}
|
||||
else {
|
||||
#header("Cache-control: private, max-age=0");
|
||||
header("Cache-control: no-cache");
|
||||
header('Expires: ' . gmdate('D, d M Y H:i:s', time() - 600) . ' GMT');
|
||||
}
|
||||
}
|
||||
#else {
|
||||
# header("Cache-control: no-cache");
|
||||
# header('Expires: ' . gmdate('D, d M Y H:i:s', time() - 600) . ' GMT');
|
||||
#}
|
||||
if($this->get_cookie("flash_message") !== null) {
|
||||
$this->add_cookie("flash_message", "", -1, "/");
|
||||
}
|
||||
usort($this->blocks, "blockcmp");
|
||||
$this->add_auto_html_headers();
|
||||
$layout = new Layout();
|
||||
$layout->display_page($page);
|
||||
break;
|
||||
case "data":
|
||||
header("Content-Length: ".strlen($this->data));
|
||||
if(!is_null($this->filename)) {
|
||||
header('Content-Disposition: attachment; filename='.$this->filename);
|
||||
}
|
||||
print $this->data;
|
||||
break;
|
||||
case "redirect":
|
||||
header('Location: '.$this->redirect);
|
||||
print 'You should be redirected to <a href="'.$this->redirect.'">'.$this->redirect.'</a>';
|
||||
break;
|
||||
default:
|
||||
print "Invalid page mode";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function grabs all the CSS and JavaScript files sprinkled throughout Shimmie's folders,
|
||||
* concatenates them together into two large files (one for CSS and one for JS) and then stores
|
||||
* them in the /cache/ directory for serving to the user.
|
||||
*
|
||||
* Why do this? Two reasons:
|
||||
* 1. Reduces the number of files the user's browser needs to download.
|
||||
* 2. Allows these cached files to be compressed/minified by the admin.
|
||||
*
|
||||
* TODO: This should really be configurable somehow...
|
||||
*/
|
||||
public function add_auto_html_headers() {
|
||||
global $config;
|
||||
|
||||
$data_href = get_base_href();
|
||||
$theme_name = $config->get_string('theme', 'default');
|
||||
|
||||
$this->add_html_header("<script type='text/javascript'>base_href = '$data_href';</script>", 40);
|
||||
|
||||
# 404/static handler will map these to themes/foo/bar.ico or lib/static/bar.ico
|
||||
$this->add_html_header("<link rel='icon' type='image/x-icon' href='$data_href/favicon.ico'>", 41);
|
||||
$this->add_html_header("<link rel='apple-touch-icon' href='$data_href/apple-touch-icon.png'>", 42);
|
||||
|
||||
//We use $config_latest to make sure cache is reset if config is ever updated.
|
||||
$config_latest = 0;
|
||||
foreach(zglob("data/config/*") as $conf) {
|
||||
$config_latest = max($config_latest, filemtime($conf));
|
||||
}
|
||||
|
||||
/*** Generate CSS cache files ***/
|
||||
$css_lib_latest = $config_latest;
|
||||
$css_lib_files = zglob("lib/vendor/css/*.css");
|
||||
foreach($css_lib_files as $css) {
|
||||
$css_lib_latest = max($css_lib_latest, filemtime($css));
|
||||
}
|
||||
$css_lib_md5 = md5(serialize($css_lib_files));
|
||||
$css_lib_cache_file = data_path("cache/style.lib.{$theme_name}.{$css_lib_latest}.{$css_lib_md5}.css");
|
||||
if(!file_exists($css_lib_cache_file)) {
|
||||
$css_lib_data = "";
|
||||
foreach($css_lib_files as $file) {
|
||||
$file_data = file_get_contents($file);
|
||||
$pattern = '/url[\s]*\([\s]*["\']?([^"\'\)]+)["\']?[\s]*\)/';
|
||||
$replace = 'url("../../'.dirname($file).'/$1")';
|
||||
$file_data = preg_replace($pattern, $replace, $file_data);
|
||||
$css_lib_data .= $file_data . "\n";
|
||||
}
|
||||
file_put_contents($css_lib_cache_file, $css_lib_data);
|
||||
}
|
||||
$this->add_html_header("<link rel='stylesheet' href='$data_href/$css_lib_cache_file' type='text/css'>", 43);
|
||||
|
||||
$css_latest = $config_latest;
|
||||
$css_files = array_merge(zglob("lib/shimmie.css"), zglob("ext/{".ENABLED_EXTS."}/style.css"), zglob("themes/$theme_name/style.css"));
|
||||
foreach($css_files as $css) {
|
||||
$css_latest = max($css_latest, filemtime($css));
|
||||
}
|
||||
$css_md5 = md5(serialize($css_files));
|
||||
$css_cache_file = data_path("cache/style.main.{$theme_name}.{$css_latest}.{$css_md5}.css");
|
||||
if(!file_exists($css_cache_file)) {
|
||||
$css_data = "";
|
||||
foreach($css_files as $file) {
|
||||
$file_data = file_get_contents($file);
|
||||
$pattern = '/url[\s]*\([\s]*["\']?([^"\'\)]+)["\']?[\s]*\)/';
|
||||
$replace = 'url("../../'.dirname($file).'/$1")';
|
||||
$file_data = preg_replace($pattern, $replace, $file_data);
|
||||
$css_data .= $file_data . "\n";
|
||||
}
|
||||
file_put_contents($css_cache_file, $css_data);
|
||||
}
|
||||
$this->add_html_header("<link rel='stylesheet' href='$data_href/$css_cache_file' type='text/css'>", 100);
|
||||
|
||||
/*** Generate JS cache files ***/
|
||||
$js_lib_latest = $config_latest;
|
||||
$js_lib_files = zglob("lib/vendor/js/*.js");
|
||||
foreach($js_lib_files as $js) {
|
||||
$js_lib_latest = max($js_lib_latest, filemtime($js));
|
||||
}
|
||||
$js_lib_md5 = md5(serialize($js_lib_files));
|
||||
$js_lib_cache_file = data_path("cache/script.lib.{$theme_name}.{$js_lib_latest}.{$js_lib_md5}.js");
|
||||
if(!file_exists($js_lib_cache_file)) {
|
||||
$js_data = "";
|
||||
foreach($js_lib_files as $file) {
|
||||
$js_data .= file_get_contents($file) . "\n";
|
||||
}
|
||||
file_put_contents($js_lib_cache_file, $js_data);
|
||||
}
|
||||
$this->add_html_header("<script src='$data_href/$js_lib_cache_file' type='text/javascript'></script>", 45);
|
||||
|
||||
$js_latest = $config_latest;
|
||||
$js_files = array_merge(zglob("lib/shimmie.js"), zglob("ext/{".ENABLED_EXTS."}/script.js"), zglob("themes/$theme_name/script.js"));
|
||||
foreach($js_files as $js) {
|
||||
$js_latest = max($js_latest, filemtime($js));
|
||||
}
|
||||
$js_md5 = md5(serialize($js_files));
|
||||
$js_cache_file = data_path("cache/script.main.{$theme_name}.{$js_latest}.{$js_md5}.js");
|
||||
if(!file_exists($js_cache_file)) {
|
||||
$js_data = "";
|
||||
foreach($js_files as $file) {
|
||||
$js_data .= file_get_contents($file) . "\n";
|
||||
}
|
||||
file_put_contents($js_cache_file, $js_data);
|
||||
}
|
||||
$this->add_html_header("<script src='$data_href/$js_cache_file' type='text/javascript'></script>", 100);
|
||||
}
|
||||
}
|
||||
|
||||
class MockPage extends Page {
|
||||
}
|
94
core/permissions.php
Normal file
94
core/permissions.php
Normal file
|
@ -0,0 +1,94 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
abstract class Permissions
|
||||
{
|
||||
public const CHANGE_SETTING = "change_setting"; # modify web-level settings, eg the config table
|
||||
public const OVERRIDE_CONFIG = "override_config"; # modify sys-level settings, eg shimmie.conf.php
|
||||
public const BIG_SEARCH = "big_search"; # search for more than 3 tags at once (speed mode only)
|
||||
|
||||
public const MANAGE_EXTENSION_LIST = "manage_extension_list";
|
||||
public const MANAGE_ALIAS_LIST = "manage_alias_list";
|
||||
public const MANAGE_AUTO_TAG = "manage_auto_tag";
|
||||
public const MASS_TAG_EDIT = "mass_tag_edit";
|
||||
|
||||
public const VIEW_IP = "view_ip"; # view IP addresses associated with things
|
||||
public const BAN_IP = "ban_ip";
|
||||
|
||||
public const CREATE_USER = "create_user";
|
||||
public const EDIT_USER_NAME = "edit_user_name";
|
||||
public const EDIT_USER_PASSWORD = "edit_user_password";
|
||||
public const EDIT_USER_INFO = "edit_user_info"; # email address, etc
|
||||
public const EDIT_USER_CLASS = "edit_user_class";
|
||||
public const DELETE_USER = "delete_user";
|
||||
|
||||
public const CREATE_COMMENT = "create_comment";
|
||||
public const DELETE_COMMENT = "delete_comment";
|
||||
public const BYPASS_COMMENT_CHECKS = "bypass_comment_checks"; # spam etc
|
||||
|
||||
public const REPLACE_IMAGE = "replace_image";
|
||||
public const CREATE_IMAGE = "create_image";
|
||||
public const EDIT_IMAGE_TAG = "edit_image_tag";
|
||||
public const EDIT_IMAGE_SOURCE = "edit_image_source";
|
||||
public const EDIT_IMAGE_OWNER = "edit_image_owner";
|
||||
public const EDIT_IMAGE_LOCK = "edit_image_lock";
|
||||
public const EDIT_IMAGE_TITLE = "edit_image_title";
|
||||
public const EDIT_IMAGE_RELATIONSHIPS = "edit_image_relationships";
|
||||
public const EDIT_IMAGE_ARTIST = "edit_image_artist";
|
||||
public const BULK_EDIT_IMAGE_TAG = "bulk_edit_image_tag";
|
||||
public const BULK_EDIT_IMAGE_SOURCE = "bulk_edit_image_source";
|
||||
public const DELETE_IMAGE = "delete_image";
|
||||
|
||||
public const BAN_IMAGE = "ban_image";
|
||||
|
||||
public const VIEW_EVENTLOG = "view_eventlog";
|
||||
public const IGNORE_DOWNTIME = "ignore_downtime";
|
||||
|
||||
public const CREATE_IMAGE_REPORT = "create_image_report";
|
||||
public const VIEW_IMAGE_REPORT = "view_image_report"; # deal with reported images
|
||||
|
||||
public const WIKI_ADMIN = "wiki_admin";
|
||||
public const EDIT_WIKI_PAGE = "edit_wiki_page";
|
||||
public const DELETE_WIKI_PAGE = "delete_wiki_page";
|
||||
|
||||
public const MANAGE_BLOCKS = "manage_blocks";
|
||||
|
||||
public const MANAGE_ADMINTOOLS = "manage_admintools";
|
||||
|
||||
public const SEND_PM = "send_pm";
|
||||
public const READ_PM = "read_pm";
|
||||
public const VIEW_OTHER_PMS = "view_other_pms";
|
||||
public const EDIT_FEATURE = "edit_feature";
|
||||
public const BULK_EDIT_VOTE = "bulk_edit_vote";
|
||||
public const EDIT_OTHER_VOTE = "edit_other_vote";
|
||||
public const VIEW_SYSINTO = "view_sysinfo";
|
||||
|
||||
public const HELLBANNED = "hellbanned";
|
||||
public const VIEW_HELLBANNED = "view_hellbanned";
|
||||
|
||||
public const PROTECTED = "protected"; # only admins can modify protected users (stops a moderator changing an admin's password)
|
||||
|
||||
public const EDIT_IMAGE_RATING = "edit_image_rating";
|
||||
public const BULK_EDIT_IMAGE_RATING = "bulk_edit_image_rating";
|
||||
|
||||
public const VIEW_TRASH = "view_trash";
|
||||
|
||||
public const PERFORM_BULK_ACTIONS = "perform_bulk_actions";
|
||||
|
||||
public const BULK_ADD = "bulk_add";
|
||||
public const EDIT_FILES = "edit_files";
|
||||
public const EDIT_TAG_CATEGORIES = "edit_tag_categories";
|
||||
public const RESCAN_MEDIA = "rescan_media";
|
||||
public const SEE_IMAGE_VIEW_COUNTS = "see_image_view_counts";
|
||||
|
||||
public const EDIT_FAVOURITES = "edit_favourites";
|
||||
|
||||
public const ARTISTS_ADMIN = "artists_admin";
|
||||
public const BLOTTER_ADMIN = "blotter_admin";
|
||||
public const FORUM_ADMIN = "forum_admin";
|
||||
public const NOTES_ADMIN = "notes_admin";
|
||||
public const POOLS_ADMIN = "pools_admin";
|
||||
public const TIPS_ADMIN = "tips_admin";
|
||||
public const CRON_ADMIN = "cron_admin";
|
||||
public const APPROVE_IMAGE = "approve_image";
|
||||
public const APPROVE_COMMENT = "approve_comment";
|
||||
}
|
822
core/polyfills.php
Normal file
822
core/polyfills.php
Normal file
|
@ -0,0 +1,822 @@
|
|||
<?php declare(strict_types=1);
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Things which should be in the core API *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* Return the unique elements of an array, case insensitively
|
||||
*/
|
||||
function array_iunique(array $array): array
|
||||
{
|
||||
$ok = [];
|
||||
foreach ($array as $element) {
|
||||
$found = false;
|
||||
foreach ($ok as $existing) {
|
||||
if (strtolower($element) == strtolower($existing)) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
$ok[] = $element;
|
||||
}
|
||||
}
|
||||
return $ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out if an IP is in a specified range
|
||||
*
|
||||
* from http://uk.php.net/network
|
||||
*/
|
||||
function ip_in_range(string $IP, string $CIDR): bool
|
||||
{
|
||||
list($net, $mask) = explode("/", $CIDR);
|
||||
|
||||
$ip_net = ip2long($net);
|
||||
$ip_mask = ~((1 << (32 - $mask)) - 1);
|
||||
|
||||
$ip_ip = ip2long($IP);
|
||||
|
||||
$ip_ip_net = $ip_ip & $ip_mask;
|
||||
|
||||
return ($ip_ip_net == $ip_net);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an entire file heirachy
|
||||
*
|
||||
* from a patch by Christian Walde; only intended for use in the
|
||||
* "extension manager" extension, but it seems to fit better here
|
||||
*/
|
||||
function deltree(string $f): void
|
||||
{
|
||||
//Because Windows (I know, bad excuse)
|
||||
if (PHP_OS === 'WINNT') {
|
||||
$real = realpath($f);
|
||||
$path = realpath('./').'\\'.str_replace('/', '\\', $f);
|
||||
if ($path != $real) {
|
||||
rmdir($path);
|
||||
} else {
|
||||
foreach (glob($f.'/*') as $sf) {
|
||||
if (is_dir($sf) && !is_link($sf)) {
|
||||
deltree($sf);
|
||||
} else {
|
||||
unlink($sf);
|
||||
}
|
||||
}
|
||||
rmdir($f);
|
||||
}
|
||||
} else {
|
||||
if (is_link($f)) {
|
||||
unlink($f);
|
||||
} elseif (is_dir($f)) {
|
||||
foreach (glob($f.'/*') as $sf) {
|
||||
if (is_dir($sf) && !is_link($sf)) {
|
||||
deltree($sf);
|
||||
} else {
|
||||
unlink($sf);
|
||||
}
|
||||
}
|
||||
rmdir($f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy an entire file hierarchy
|
||||
*
|
||||
* from a comment on http://uk.php.net/copy
|
||||
*/
|
||||
function full_copy(string $source, string $target): void
|
||||
{
|
||||
if (is_dir($source)) {
|
||||
@mkdir($target);
|
||||
|
||||
$d = dir($source);
|
||||
|
||||
while (false !== ($entry = $d->read())) {
|
||||
if ($entry == '.' || $entry == '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$Entry = $source . '/' . $entry;
|
||||
if (is_dir($Entry)) {
|
||||
full_copy($Entry, $target . '/' . $entry);
|
||||
continue;
|
||||
}
|
||||
copy($Entry, $target . '/' . $entry);
|
||||
}
|
||||
$d->close();
|
||||
} else {
|
||||
copy($source, $target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of all the regular files in a directory and subdirectories
|
||||
*/
|
||||
function list_files(string $base, string $_sub_dir=""): array
|
||||
{
|
||||
assert(is_dir($base));
|
||||
|
||||
$file_list = [];
|
||||
|
||||
$files = [];
|
||||
$dir = opendir("$base/$_sub_dir");
|
||||
while ($f = readdir($dir)) {
|
||||
$files[] = $f;
|
||||
}
|
||||
closedir($dir);
|
||||
sort($files);
|
||||
|
||||
foreach ($files as $filename) {
|
||||
$full_path = "$base/$_sub_dir/$filename";
|
||||
|
||||
if (!is_link($full_path) && is_dir($full_path)) {
|
||||
if (!($filename == "." || $filename == "..")) {
|
||||
//subdirectory found
|
||||
$file_list = array_merge(
|
||||
$file_list,
|
||||
list_files($base, "$_sub_dir/$filename")
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$full_path = str_replace("//", "/", $full_path);
|
||||
$file_list[] = $full_path;
|
||||
}
|
||||
}
|
||||
|
||||
return $file_list;
|
||||
}
|
||||
|
||||
function stream_file(string $file, int $start, int $end): void
|
||||
{
|
||||
$fp = fopen($file, 'r');
|
||||
try {
|
||||
set_time_limit(0);
|
||||
fseek($fp, $start);
|
||||
$buffer = 1024 * 1024;
|
||||
while (!feof($fp) && ($p = ftell($fp)) <= $end) {
|
||||
if ($p + $buffer > $end) {
|
||||
$buffer = $end - $p + 1;
|
||||
}
|
||||
echo fread($fp, $buffer);
|
||||
if (!defined("UNITTEST")) {
|
||||
@ob_flush();
|
||||
}
|
||||
flush();
|
||||
|
||||
// After flush, we can tell if the client browser has disconnected.
|
||||
// This means we can start sending a large file, and if we detect they disappeared
|
||||
// then we can just stop and not waste any more resources or bandwidth.
|
||||
if (connection_status() != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
fclose($fp);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('http_parse_headers')) { #http://www.php.net/manual/en/function.http-parse-headers.php#112917
|
||||
|
||||
/**
|
||||
* #return string[]
|
||||
*/
|
||||
function http_parse_headers(string $raw_headers): array
|
||||
{
|
||||
$headers = []; // $headers = [];
|
||||
|
||||
foreach (explode("\n", $raw_headers) as $i => $h) {
|
||||
$h = explode(':', $h, 2);
|
||||
|
||||
if (isset($h[1])) {
|
||||
if (!isset($headers[$h[0]])) {
|
||||
$headers[$h[0]] = trim($h[1]);
|
||||
} elseif (is_array($headers[$h[0]])) {
|
||||
$tmp = array_merge($headers[$h[0]], [trim($h[1])]);
|
||||
$headers[$h[0]] = $tmp;
|
||||
} else {
|
||||
$tmp = array_merge([$headers[$h[0]]], [trim($h[1])]);
|
||||
$headers[$h[0]] = $tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP Headers can sometimes be lowercase which will cause issues.
|
||||
* In cases like these, we need to make sure to check for them if the camelcase version does not exist.
|
||||
*/
|
||||
function findHeader(array $headers, string $name): ?string
|
||||
{
|
||||
if (!is_array($headers)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$header = null;
|
||||
|
||||
if (array_key_exists($name, $headers)) {
|
||||
$header = $headers[$name];
|
||||
} else {
|
||||
$headers = array_change_key_case($headers); // convert all to lower case.
|
||||
$lc_name = strtolower($name);
|
||||
|
||||
if (array_key_exists($lc_name, $headers)) {
|
||||
$header = $headers[$lc_name];
|
||||
}
|
||||
}
|
||||
|
||||
return $header;
|
||||
}
|
||||
|
||||
if (!function_exists('mb_strlen')) {
|
||||
// TODO: we should warn the admin that they are missing multibyte support
|
||||
function mb_strlen($str, $encoding)
|
||||
{
|
||||
return strlen($str);
|
||||
}
|
||||
function mb_internal_encoding($encoding)
|
||||
{
|
||||
}
|
||||
function mb_strtolower($str)
|
||||
{
|
||||
return strtolower($str);
|
||||
}
|
||||
}
|
||||
|
||||
const MIME_TYPE_MAP = [
|
||||
'jpg' => 'image/jpeg',
|
||||
'gif' => 'image/gif',
|
||||
'png' => 'image/png',
|
||||
'tif' => 'image/tiff',
|
||||
'tiff' => 'image/tiff',
|
||||
'ico' => 'image/x-icon',
|
||||
'swf' => 'application/x-shockwave-flash',
|
||||
'flv' => 'video/x-flv',
|
||||
'svg' => 'image/svg+xml',
|
||||
'pdf' => 'application/pdf',
|
||||
'zip' => 'application/zip',
|
||||
'gz' => 'application/x-gzip',
|
||||
'tar' => 'application/x-tar',
|
||||
'bz' => 'application/x-bzip',
|
||||
'bz2' => 'application/x-bzip2',
|
||||
'txt' => 'text/plain',
|
||||
'asc' => 'text/plain',
|
||||
'htm' => 'text/html',
|
||||
'html' => 'text/html',
|
||||
'css' => 'text/css',
|
||||
'js' => 'text/javascript',
|
||||
'xml' => 'text/xml',
|
||||
'xsl' => 'application/xsl+xml',
|
||||
'ogg' => 'application/ogg',
|
||||
'mp3' => 'audio/mpeg',
|
||||
'wav' => 'audio/x-wav',
|
||||
'avi' => 'video/x-msvideo',
|
||||
'mpg' => 'video/mpeg',
|
||||
'mpeg' => 'video/mpeg',
|
||||
'mov' => 'video/quicktime',
|
||||
'php' => 'text/x-php',
|
||||
'mp4' => 'video/mp4',
|
||||
'ogv' => 'video/ogg',
|
||||
'webm' => 'video/webm',
|
||||
'webp' => 'image/webp',
|
||||
'bmp' =>'image/x-ms-bmp',
|
||||
'psd' => 'image/vnd.adobe.photoshop',
|
||||
'mkv' => 'video/x-matroska'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get MIME type for file
|
||||
*
|
||||
* The contents of this function are taken from the __getMimeType() function
|
||||
* from the "Amazon S3 PHP class" which is Copyright (c) 2008, Donovan Schönknecht
|
||||
* and released under the 'Simplified BSD License'.
|
||||
*/
|
||||
function getMimeType(string $file, string $ext=""): string
|
||||
{
|
||||
// Static extension lookup
|
||||
$ext = strtolower($ext);
|
||||
|
||||
if (array_key_exists($ext, MIME_TYPE_MAP)) {
|
||||
return MIME_TYPE_MAP[$ext];
|
||||
}
|
||||
|
||||
$type = false;
|
||||
// Fileinfo documentation says fileinfo_open() will use the
|
||||
// MAGIC env var for the magic file
|
||||
if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) &&
|
||||
($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) {
|
||||
if (($type = finfo_file($finfo, $file)) !== false) {
|
||||
// Remove the charset and grab the last content-type
|
||||
$type = explode(' ', str_replace('; charset=', ';charset=', $type));
|
||||
$type = array_pop($type);
|
||||
$type = explode(';', $type);
|
||||
$type = trim(array_shift($type));
|
||||
}
|
||||
finfo_close($finfo);
|
||||
|
||||
// If anyone is still using mime_content_type()
|
||||
} elseif (function_exists('mime_content_type')) {
|
||||
$type = trim(mime_content_type($file));
|
||||
}
|
||||
|
||||
if ($type !== false && strlen($type) > 0) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
|
||||
function get_extension(?string $mime_type): ?string
|
||||
{
|
||||
if (empty($mime_type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ext = array_search($mime_type, MIME_TYPE_MAP);
|
||||
return ($ext ? $ext : null);
|
||||
}
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
function getSubclassesOf(string $parent)
|
||||
{
|
||||
$result = [];
|
||||
foreach (get_declared_classes() as $class) {
|
||||
$rclass = new ReflectionClass($class);
|
||||
if (!$rclass->isAbstract() && is_subclass_of($class, $parent)) {
|
||||
$result[] = $class;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like glob, with support for matching very long patterns with braces.
|
||||
*/
|
||||
function zglob(string $pattern): array
|
||||
{
|
||||
$results = [];
|
||||
if (preg_match('/(.*)\{(.*)\}(.*)/', $pattern, $matches)) {
|
||||
$braced = explode(",", $matches[2]);
|
||||
foreach ($braced as $b) {
|
||||
$sub_pattern = $matches[1].$b.$matches[3];
|
||||
$results = array_merge($results, zglob($sub_pattern));
|
||||
}
|
||||
return $results;
|
||||
} else {
|
||||
$r = glob($pattern);
|
||||
if ($r) {
|
||||
return $r;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out the path to the shimmie install directory.
|
||||
*
|
||||
* eg if shimmie is visible at http://foo.com/gallery, this
|
||||
* function should return /gallery
|
||||
*
|
||||
* PHP really, really sucks.
|
||||
*/
|
||||
function get_base_href(): string
|
||||
{
|
||||
if (defined("BASE_HREF") && !empty(BASE_HREF)) {
|
||||
return BASE_HREF;
|
||||
}
|
||||
$possible_vars = ['SCRIPT_NAME', 'PHP_SELF', 'PATH_INFO', 'ORIG_PATH_INFO'];
|
||||
$ok_var = null;
|
||||
foreach ($possible_vars as $var) {
|
||||
if (isset($_SERVER[$var]) && substr($_SERVER[$var], -4) === '.php') {
|
||||
$ok_var = $_SERVER[$var];
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert(!empty($ok_var));
|
||||
$dir = dirname($ok_var);
|
||||
$dir = str_replace("\\", "/", $dir);
|
||||
$dir = str_replace("//", "/", $dir);
|
||||
$dir = rtrim($dir, "/");
|
||||
return $dir;
|
||||
}
|
||||
|
||||
function startsWith(string $haystack, string $needle): bool
|
||||
{
|
||||
$length = strlen($needle);
|
||||
return (substr($haystack, 0, $length) === $needle);
|
||||
}
|
||||
|
||||
function endsWith(string $haystack, string $needle): bool
|
||||
{
|
||||
$length = strlen($needle);
|
||||
$start = $length * -1; //negative
|
||||
return (substr($haystack, $start) === $needle);
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Input / Output Sanitising *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* Make some data safe for printing into HTML
|
||||
*/
|
||||
function html_escape(?string $input): string
|
||||
{
|
||||
if (is_null($input)) {
|
||||
return "";
|
||||
}
|
||||
return htmlentities($input, ENT_QUOTES, "UTF-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescape data that was made safe for printing into HTML
|
||||
*/
|
||||
function html_unescape(string $input): string
|
||||
{
|
||||
return html_entity_decode($input, ENT_QUOTES, "UTF-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure some data is safe to be used in integer context
|
||||
*/
|
||||
function int_escape(?string $input): int
|
||||
{
|
||||
/*
|
||||
Side note, Casting to an integer is FASTER than using intval.
|
||||
http://hakre.wordpress.com/2010/05/13/php-casting-vs-intval/
|
||||
*/
|
||||
if (is_null($input)) {
|
||||
return 0;
|
||||
}
|
||||
return (int)$input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure some data is safe to be used in URL context
|
||||
*/
|
||||
function url_escape(?string $input): string
|
||||
{
|
||||
if (is_null($input)) {
|
||||
return "";
|
||||
}
|
||||
$input = rawurlencode($input);
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn all manner of HTML / INI / JS / DB booleans into a PHP one
|
||||
*/
|
||||
function bool_escape($input): bool
|
||||
{
|
||||
/*
|
||||
Sometimes, I don't like PHP -- this, is one of those times...
|
||||
"a boolean FALSE is not considered a valid boolean value by this function."
|
||||
Yay for Got'chas!
|
||||
http://php.net/manual/en/filter.filters.validate.php
|
||||
*/
|
||||
if (is_bool($input)) {
|
||||
return $input;
|
||||
} elseif (is_int($input)) {
|
||||
return ($input === 1);
|
||||
} else {
|
||||
$value = filter_var($input, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
if (!is_null($value)) {
|
||||
return $value;
|
||||
} else {
|
||||
$input = strtolower(trim($input));
|
||||
return (
|
||||
$input === "y" ||
|
||||
$input === "yes" ||
|
||||
$input === "t" ||
|
||||
$input === "true" ||
|
||||
$input === "on" ||
|
||||
$input === "1"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Some functions require a callback function for escaping,
|
||||
* but we might not want to alter the data
|
||||
*/
|
||||
function no_escape(string $input): string
|
||||
{
|
||||
return $input;
|
||||
}
|
||||
|
||||
function clamp(?int $val, ?int $min=null, ?int $max=null): int
|
||||
{
|
||||
if (!is_numeric($val) || (!is_null($min) && $val < $min)) {
|
||||
$val = $min;
|
||||
}
|
||||
if (!is_null($max) && $val > $max) {
|
||||
$val = $max;
|
||||
}
|
||||
if (!is_null($min) && !is_null($max)) {
|
||||
assert($val >= $min && $val <= $max, "$min <= $val <= $max");
|
||||
}
|
||||
return $val;
|
||||
}
|
||||
|
||||
function xml_tag(string $name, array $attrs=[], array $children=[]): string
|
||||
{
|
||||
$xml = "<$name ";
|
||||
foreach ($attrs as $k => $v) {
|
||||
$xv = str_replace(''', ''', htmlspecialchars((string)$v, ENT_QUOTES));
|
||||
$xml .= "$k=\"$xv\" ";
|
||||
}
|
||||
if (count($children) > 0) {
|
||||
$xml .= ">\n";
|
||||
foreach ($children as $child) {
|
||||
$xml .= xml_tag($child);
|
||||
}
|
||||
$xml .= "</$name>\n";
|
||||
} else {
|
||||
$xml .= "/>\n";
|
||||
}
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Original PHP code by Chirp Internet: www.chirp.com.au
|
||||
* Please acknowledge use of this code by including this header.
|
||||
*/
|
||||
function truncate(string $string, int $limit, string $break=" ", string $pad="..."): string
|
||||
{
|
||||
// return with no change if string is shorter than $limit
|
||||
if (strlen($string) <= $limit) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
// is $break present between $limit and the end of the string?
|
||||
if (false !== ($breakpoint = strpos($string, $break, $limit))) {
|
||||
if ($breakpoint < strlen($string) - 1) {
|
||||
$string = substr($string, 0, $breakpoint) . $pad;
|
||||
}
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a human readable filesize into an integer, eg 1KB -> 1024
|
||||
*/
|
||||
function parse_shorthand_int(string $limit): int
|
||||
{
|
||||
if (preg_match('/^([\d\.]+)([gmk])?b?$/i', (string)$limit, $m)) {
|
||||
$value = $m[1];
|
||||
if (isset($m[2])) {
|
||||
switch (strtolower($m[2])) {
|
||||
/** @noinspection PhpMissingBreakStatementInspection */
|
||||
case 'g': $value *= 1024; // fall through
|
||||
/** @noinspection PhpMissingBreakStatementInspection */
|
||||
// no break
|
||||
case 'm': $value *= 1024; // fall through
|
||||
/** @noinspection PhpMissingBreakStatementInspection */
|
||||
// no break
|
||||
case 'k': $value *= 1024; break;
|
||||
default: $value = -1;
|
||||
}
|
||||
}
|
||||
return (int)$value;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn an integer into a human readable filesize, eg 1024 -> 1KB
|
||||
*/
|
||||
function to_shorthand_int(int $int): string
|
||||
{
|
||||
assert($int >= 0);
|
||||
|
||||
if ($int >= pow(1024, 3)) {
|
||||
return sprintf("%.1fGB", $int / pow(1024, 3));
|
||||
} elseif ($int >= pow(1024, 2)) {
|
||||
return sprintf("%.1fMB", $int / pow(1024, 2));
|
||||
} elseif ($int >= 1024) {
|
||||
return sprintf("%.1fKB", $int / 1024);
|
||||
} else {
|
||||
return (string)$int;
|
||||
}
|
||||
}
|
||||
|
||||
const TIME_UNITS = ["s"=>60,"m"=>60,"h"=>24,"d"=>365,"y"=>PHP_INT_MAX];
|
||||
function format_milliseconds(int $input): string
|
||||
{
|
||||
$output = "";
|
||||
|
||||
$remainder = floor($input / 1000);
|
||||
|
||||
foreach (TIME_UNITS as $unit=>$conversion) {
|
||||
$count = $remainder % $conversion;
|
||||
$remainder = floor($remainder / $conversion);
|
||||
if ($count==0&&$remainder<1) {
|
||||
break;
|
||||
}
|
||||
$output = "$count".$unit." ".$output;
|
||||
}
|
||||
|
||||
return trim($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a date into a time, a date, an "X minutes ago...", etc
|
||||
*/
|
||||
function autodate(string $date, bool $html=true): string
|
||||
{
|
||||
$cpu = date('c', strtotime($date));
|
||||
$hum = date('F j, Y; H:i', strtotime($date));
|
||||
return ($html ? "<time datetime='$cpu'>$hum</time>" : $hum);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given string is a valid date-time. ( Format: yyyy-mm-dd hh:mm:ss )
|
||||
*/
|
||||
function isValidDateTime(string $dateTime): bool
|
||||
{
|
||||
if (preg_match("/^(\d{4})-(\d{2})-(\d{2}) ([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/", $dateTime, $matches)) {
|
||||
if (checkdate((int)$matches[2], (int)$matches[3], (int)$matches[1])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given string is a valid date. ( Format: yyyy-mm-dd )
|
||||
*/
|
||||
function isValidDate(string $date): bool
|
||||
{
|
||||
if (preg_match("/^(\d{4})-(\d{2})-(\d{2})$/", $date, $matches)) {
|
||||
// checkdate wants (month, day, year)
|
||||
if (checkdate((int)$matches[2], (int)$matches[3], (int)$matches[1])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function validate_input(array $inputs): array
|
||||
{
|
||||
$outputs = [];
|
||||
|
||||
foreach ($inputs as $key => $validations) {
|
||||
$flags = explode(',', $validations);
|
||||
|
||||
if (in_array('bool', $flags) && !isset($_POST[$key])) {
|
||||
$_POST[$key] = 'off';
|
||||
}
|
||||
|
||||
if (in_array('optional', $flags)) {
|
||||
if (!isset($_POST[$key]) || trim($_POST[$key]) == "") {
|
||||
$outputs[$key] = null;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!isset($_POST[$key]) || trim($_POST[$key]) == "") {
|
||||
throw new InvalidInput("Input '$key' not set");
|
||||
}
|
||||
|
||||
$value = trim($_POST[$key]);
|
||||
|
||||
if (in_array('user_id', $flags)) {
|
||||
$id = int_escape($value);
|
||||
if (in_array('exists', $flags)) {
|
||||
if (is_null(User::by_id($id))) {
|
||||
throw new InvalidInput("User #$id does not exist");
|
||||
}
|
||||
}
|
||||
$outputs[$key] = $id;
|
||||
} elseif (in_array('user_name', $flags)) {
|
||||
if (strlen($value) < 1) {
|
||||
throw new InvalidInput("Username must be at least 1 character");
|
||||
} elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $value)) {
|
||||
throw new InvalidInput(
|
||||
"Username contains invalid characters. Allowed characters are ".
|
||||
"letters, numbers, dash, and underscore"
|
||||
);
|
||||
}
|
||||
$outputs[$key] = $value;
|
||||
} elseif (in_array('user_class', $flags)) {
|
||||
global $_shm_user_classes;
|
||||
if (!array_key_exists($value, $_shm_user_classes)) {
|
||||
throw new InvalidInput("Invalid user class: ".html_escape($value));
|
||||
}
|
||||
$outputs[$key] = $value;
|
||||
} elseif (in_array('email', $flags)) {
|
||||
$outputs[$key] = trim($value);
|
||||
} elseif (in_array('password', $flags)) {
|
||||
$outputs[$key] = $value;
|
||||
} elseif (in_array('int', $flags)) {
|
||||
$value = trim($value);
|
||||
if (empty($value) || !is_numeric($value)) {
|
||||
throw new InvalidInput("Invalid int: ".html_escape($value));
|
||||
}
|
||||
$outputs[$key] = (int)$value;
|
||||
} elseif (in_array('bool', $flags)) {
|
||||
$outputs[$key] = bool_escape($value);
|
||||
} elseif (in_array('date', $flags)) {
|
||||
$outputs[$key] = date("Y-m-d H:i:s", strtotime(trim($value)));
|
||||
} elseif (in_array('string', $flags)) {
|
||||
if (in_array('trim', $flags)) {
|
||||
$value = trim($value);
|
||||
}
|
||||
if (in_array('lower', $flags)) {
|
||||
$value = strtolower($value);
|
||||
}
|
||||
if (in_array('not-empty', $flags)) {
|
||||
throw new InvalidInput("$key must not be blank");
|
||||
}
|
||||
if (in_array('nullify', $flags)) {
|
||||
if (empty($value)) {
|
||||
$value = null;
|
||||
}
|
||||
}
|
||||
$outputs[$key] = $value;
|
||||
} else {
|
||||
throw new InvalidInput("Unknown validation '$validations'");
|
||||
}
|
||||
}
|
||||
|
||||
return $outputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates all possible directory separators to the appropriate one for the current system,
|
||||
* and removes any duplicate separators.
|
||||
*/
|
||||
function sanitize_path(string $path): string
|
||||
{
|
||||
return preg_replace('|[\\\\/]+|S', DIRECTORY_SEPARATOR, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines all path segments specified, ensuring no duplicate separators occur,
|
||||
* as well as converting all possible separators to the one appropriate for the current system.
|
||||
*/
|
||||
function join_path(string ...$paths): string
|
||||
{
|
||||
$output = "";
|
||||
foreach ($paths as $path) {
|
||||
if (empty($path)) {
|
||||
continue;
|
||||
}
|
||||
$path = sanitize_path($path);
|
||||
if (empty($output)) {
|
||||
$output = $path;
|
||||
} else {
|
||||
$output = rtrim($output, DIRECTORY_SEPARATOR);
|
||||
$path = ltrim($path, DIRECTORY_SEPARATOR);
|
||||
$output .= DIRECTORY_SEPARATOR . $path;
|
||||
}
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform callback on each item returned by an iterator.
|
||||
*/
|
||||
function iterator_map(callable $callback, iterator $iter): Generator
|
||||
{
|
||||
foreach ($iter as $i) {
|
||||
yield call_user_func($callback, $i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform callback on each item returned by an iterator and combine the result into an array.
|
||||
*/
|
||||
function iterator_map_to_array(callable $callback, iterator $iter): array
|
||||
{
|
||||
return iterator_to_array(iterator_map($callback, $iter));
|
||||
}
|
||||
|
||||
function stringer($s)
|
||||
{
|
||||
if (is_array($s)) {
|
||||
if (isset($s[0])) {
|
||||
return "[" . implode(", ", array_map("stringer", $s)) . "]";
|
||||
} else {
|
||||
$pairs = [];
|
||||
foreach ($s as $k=>$v) {
|
||||
$pairs[] = "\"$k\"=>" . stringer($v);
|
||||
}
|
||||
return "[" . implode(", ", $pairs) . "]";
|
||||
}
|
||||
}
|
||||
if (is_string($s)) {
|
||||
return "\"$s\""; // FIXME: handle escaping quotes
|
||||
}
|
||||
return (string)$s;
|
||||
}
|
128
core/send_event.php
Normal file
128
core/send_event.php
Normal file
|
@ -0,0 +1,128 @@
|
|||
<?php declare(strict_types=1);
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Event API *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/** @private */
|
||||
global $_shm_event_listeners;
|
||||
$_shm_event_listeners = [];
|
||||
|
||||
function _load_event_listeners(): void
|
||||
{
|
||||
global $_shm_event_listeners;
|
||||
|
||||
$cache_path = data_path("cache/shm_event_listeners.php");
|
||||
if (SPEED_HAX && file_exists($cache_path)) {
|
||||
require_once($cache_path);
|
||||
} else {
|
||||
_set_event_listeners();
|
||||
|
||||
if (SPEED_HAX) {
|
||||
_dump_event_listeners($_shm_event_listeners, $cache_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _clear_cached_event_listeners(): void
|
||||
{
|
||||
if (file_exists(data_path("cache/shm_event_listeners.php"))) {
|
||||
unlink(data_path("cache/shm_event_listeners.php"));
|
||||
}
|
||||
}
|
||||
|
||||
function _set_event_listeners(): void
|
||||
{
|
||||
global $_shm_event_listeners;
|
||||
$_shm_event_listeners = [];
|
||||
|
||||
foreach (getSubclassesOf("Extension") as $class) {
|
||||
/** @var Extension $extension */
|
||||
$extension = new $class();
|
||||
|
||||
// skip extensions which don't support our current database
|
||||
if (!$extension->info->is_supported()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (get_class_methods($extension) as $method) {
|
||||
if (substr($method, 0, 2) == "on") {
|
||||
$event = substr($method, 2) . "Event";
|
||||
$pos = $extension->get_priority() * 100;
|
||||
while (isset($_shm_event_listeners[$event][$pos])) {
|
||||
$pos += 1;
|
||||
}
|
||||
$_shm_event_listeners[$event][$pos] = $extension;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _dump_event_listeners(array $event_listeners, string $path): void
|
||||
{
|
||||
$p = "<"."?php\n";
|
||||
|
||||
foreach (getSubclassesOf("Extension") as $class) {
|
||||
$p .= "\$$class = new $class(); ";
|
||||
}
|
||||
|
||||
$p .= "\$_shm_event_listeners = array(\n";
|
||||
foreach ($event_listeners as $event => $listeners) {
|
||||
$p .= "\t'$event' => array(\n";
|
||||
foreach ($listeners as $id => $listener) {
|
||||
$p .= "\t\t$id => \$".get_class($listener).",\n";
|
||||
}
|
||||
$p .= "\t),\n";
|
||||
}
|
||||
$p .= ");\n";
|
||||
|
||||
file_put_contents($path, $p);
|
||||
}
|
||||
|
||||
|
||||
/** @private */
|
||||
global $_shm_event_count;
|
||||
$_shm_event_count = 0;
|
||||
|
||||
/**
|
||||
* Send an event to all registered Extensions.
|
||||
*/
|
||||
function send_event(Event $event): Event
|
||||
{
|
||||
global $tracer_enabled;
|
||||
|
||||
global $_shm_event_listeners, $_shm_event_count, $_tracer;
|
||||
if (!isset($_shm_event_listeners[get_class($event)])) {
|
||||
return $event;
|
||||
}
|
||||
$method_name = "on".str_replace("Event", "", get_class($event));
|
||||
|
||||
// send_event() is performance sensitive, and with the number
|
||||
// of times tracer gets called the time starts to add up
|
||||
if ($tracer_enabled) {
|
||||
$_tracer->begin(get_class($event));
|
||||
}
|
||||
// SHIT: http://bugs.php.net/bug.php?id=35106
|
||||
$my_event_listeners = $_shm_event_listeners[get_class($event)];
|
||||
ksort($my_event_listeners);
|
||||
|
||||
foreach ($my_event_listeners as $listener) {
|
||||
if ($tracer_enabled) {
|
||||
$_tracer->begin(get_class($listener));
|
||||
}
|
||||
if (method_exists($listener, $method_name)) {
|
||||
$listener->$method_name($event);
|
||||
}
|
||||
if ($tracer_enabled) {
|
||||
$_tracer->end();
|
||||
}
|
||||
if ($event->stop_processing===true) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$_shm_event_count++;
|
||||
if ($tracer_enabled) {
|
||||
$_tracer->end();
|
||||
}
|
||||
|
||||
return $event;
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* First, load the user-specified settings
|
||||
*/
|
||||
@include_once "data/config/shimmie.conf.php";
|
||||
@include_once "data/config/extensions.conf.php";
|
||||
|
||||
|
||||
/**
|
||||
* For any values that aren't defined in the above files, Shimmie
|
||||
* will set the values to their defaults
|
||||
*
|
||||
* All of these can be over-ridden by placing a 'define' in data/config/shimmie.conf.php
|
||||
*
|
||||
* Do NOT change them in this file. These are the defaults only!
|
||||
*
|
||||
* Example:
|
||||
* define("SPEED_HAX", true);
|
||||
*
|
||||
*/
|
||||
|
||||
/** @private */
|
||||
function _d($name, $value) {if(!defined($name)) define($name, $value);}
|
||||
_d("DATABASE_DSN", null); // string PDO database connection details
|
||||
_d("DATABASE_KA", true); // string Keep database connection alive
|
||||
_d("CACHE_DSN", null); // string cache connection details
|
||||
_d("DEBUG", false); // boolean print various debugging details
|
||||
_d("DEBUG_SQL", false); // boolean dump SQL queries to data/sql.log
|
||||
_d("DEBUG_CACHE", false); // boolean dump cache queries to data/cache.log
|
||||
_d("COVERAGE", false); // boolean activate xdebug coverage monitor
|
||||
_d("CONTEXT", null); // string file to log performance data into
|
||||
_d("CACHE_HTTP", false); // boolean output explicit HTTP caching headers
|
||||
_d("COOKIE_PREFIX", 'shm'); // string if you run multiple galleries with non-shared logins, give them different prefixes
|
||||
_d("SPEED_HAX", false); // boolean do some questionable things in the name of performance
|
||||
_d("COMPILE_ELS", false); // boolean pre-build the list of event listeners
|
||||
_d("NICE_URLS", false); // boolean force niceurl mode
|
||||
_d("SEARCH_ACCEL", false); // boolean use search accelerator
|
||||
_d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse
|
||||
_d("VERSION", '2.6.1'); // string shimmie version
|
||||
_d("TIMEZONE", null); // string timezone
|
||||
_d("CORE_EXTS", "bbcode,user,mail,upload,image,view,handle_pixel,ext_manager,setup,upgrade,handle_404,comment,tag_list,index,tag_edit,alias_editor"); // extensions to always enable
|
||||
_d("EXTRA_EXTS", ""); // string optional extra extensions
|
||||
_d("BASE_URL", null); // string force a specific base URL (default is auto-detect)
|
||||
_d("MIN_PHP_VERSION", '5.6');// string minium supported PHP version
|
||||
|
||||
/*
|
||||
* Calculated settings - you should never need to change these
|
||||
* directly, only the things they're built from
|
||||
*/
|
||||
_d("SCORE_VERSION", 'develop/'.VERSION); // string SCore version
|
||||
_d("ENABLED_EXTS", CORE_EXTS.",".EXTRA_EXTS);
|
||||
|
||||
|
33
core/sys_config.php
Normal file
33
core/sys_config.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* For any values that aren't defined in data/config/*.php,
|
||||
* Shimmie will set the values to their defaults
|
||||
*
|
||||
* All of these can be over-ridden by placing a 'define' in
|
||||
* data/config/shimmie.conf.php
|
||||
*
|
||||
* Do NOT change them in this file. These are the defaults only!
|
||||
*
|
||||
* Example:
|
||||
* define("SPEED_HAX", true);
|
||||
*/
|
||||
|
||||
function _d(string $name, $value): void
|
||||
{
|
||||
if (!defined($name)) {
|
||||
define($name, $value);
|
||||
}
|
||||
}
|
||||
_d("DATABASE_DSN", null); // string PDO database connection details
|
||||
_d("DATABASE_TIMEOUT", 10000);// int Time to wait for each statement to complete
|
||||
_d("CACHE_DSN", null); // string cache connection details
|
||||
_d("DEBUG", false); // boolean print various debugging details
|
||||
_d("COOKIE_PREFIX", 'shm'); // string if you run multiple galleries with non-shared logins, give them different prefixes
|
||||
_d("SPEED_HAX", false); // boolean do some questionable things in the name of performance
|
||||
_d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse
|
||||
_d("VERSION", '2.8-dev'); // string shimmie version
|
||||
_d("TIMEZONE", null); // string timezone
|
||||
_d("EXTRA_EXTS", ""); // string optional extra extensions
|
||||
_d("BASE_HREF", null); // string force a specific base URL (default is auto-detect)
|
||||
_d("TRACE_FILE", null); // string file to log performance data into
|
||||
_d("TRACE_THRESHOLD", 0.0); // float log pages which take more time than this many seconds
|
51
core/tests/basepage.test.php
Normal file
51
core/tests/basepage.test.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once "core/basepage.php";
|
||||
|
||||
class BasePageTest extends TestCase
|
||||
{
|
||||
public function test_page()
|
||||
{
|
||||
$page = new BasePage();
|
||||
$page->set_mode(PageMode::PAGE);
|
||||
ob_start();
|
||||
$page->display();
|
||||
ob_end_clean();
|
||||
$this->assertTrue(true); // doesn't crash
|
||||
}
|
||||
|
||||
public function test_file()
|
||||
{
|
||||
$page = new BasePage();
|
||||
$page->set_mode(PageMode::FILE);
|
||||
$page->set_file("tests/pbx_screenshot.jpg");
|
||||
ob_start();
|
||||
$page->display();
|
||||
ob_end_clean();
|
||||
$this->assertTrue(true); // doesn't crash
|
||||
}
|
||||
|
||||
public function test_data()
|
||||
{
|
||||
$page = new BasePage();
|
||||
$page->set_mode(PageMode::DATA);
|
||||
$page->set_data("hello world");
|
||||
ob_start();
|
||||
$page->display();
|
||||
ob_end_clean();
|
||||
$this->assertTrue(true); // doesn't crash
|
||||
}
|
||||
|
||||
public function test_redirect()
|
||||
{
|
||||
$page = new BasePage();
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect("/new/page");
|
||||
ob_start();
|
||||
$page->display();
|
||||
ob_end_clean();
|
||||
$this->assertTrue(true); // doesn't crash
|
||||
}
|
||||
}
|
17
core/tests/block.test.php
Normal file
17
core/tests/block.test.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once "core/block.php";
|
||||
|
||||
class BlockTest extends TestCase
|
||||
{
|
||||
public function test_basic()
|
||||
{
|
||||
$b = new Block("head", "body");
|
||||
$this->assertEquals(
|
||||
"<section id='headmain'><h3 data-toggle-sel='#headmain' class=''>head</h3><div class='blockbody'>body</div></section>\n",
|
||||
$b->get_html()
|
||||
);
|
||||
}
|
||||
}
|
223
core/tests/polyfills.test.php
Normal file
223
core/tests/polyfills.test.php
Normal file
|
@ -0,0 +1,223 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once "core/polyfills.php";
|
||||
|
||||
class PolyfillsTest extends TestCase
|
||||
{
|
||||
public function test_html_escape()
|
||||
{
|
||||
$this->assertEquals(
|
||||
html_escape("Foo & <waffles>"),
|
||||
"Foo & <waffles>"
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
html_unescape("Foo & <waffles>"),
|
||||
"Foo & <waffles>"
|
||||
);
|
||||
|
||||
$x = "Foo & <waffles>";
|
||||
$this->assertEquals(html_escape(html_unescape($x)), $x);
|
||||
}
|
||||
|
||||
public function test_int_escape()
|
||||
{
|
||||
$this->assertEquals(int_escape(""), 0);
|
||||
$this->assertEquals(int_escape("1"), 1);
|
||||
$this->assertEquals(int_escape("-1"), -1);
|
||||
$this->assertEquals(int_escape("-1.5"), -1);
|
||||
$this->assertEquals(int_escape(null), 0);
|
||||
}
|
||||
|
||||
public function test_url_escape()
|
||||
{
|
||||
$this->assertEquals(url_escape("^\o/^"), "%5E%5Co%2F%5E");
|
||||
$this->assertEquals(url_escape(null), "");
|
||||
}
|
||||
|
||||
public function test_bool_escape()
|
||||
{
|
||||
$this->assertTrue(bool_escape(true));
|
||||
$this->assertFalse(bool_escape(false));
|
||||
|
||||
$this->assertTrue(bool_escape("true"));
|
||||
$this->assertFalse(bool_escape("false"));
|
||||
|
||||
$this->assertTrue(bool_escape("t"));
|
||||
$this->assertFalse(bool_escape("f"));
|
||||
|
||||
$this->assertTrue(bool_escape("T"));
|
||||
$this->assertFalse(bool_escape("F"));
|
||||
|
||||
$this->assertTrue(bool_escape("yes"));
|
||||
$this->assertFalse(bool_escape("no"));
|
||||
|
||||
$this->assertTrue(bool_escape("Yes"));
|
||||
$this->assertFalse(bool_escape("No"));
|
||||
|
||||
$this->assertTrue(bool_escape("on"));
|
||||
$this->assertFalse(bool_escape("off"));
|
||||
|
||||
$this->assertTrue(bool_escape(1));
|
||||
$this->assertFalse(bool_escape(0));
|
||||
|
||||
$this->assertTrue(bool_escape("1"));
|
||||
$this->assertFalse(bool_escape("0"));
|
||||
}
|
||||
|
||||
public function test_clamp()
|
||||
{
|
||||
$this->assertEquals(clamp(0, 5, 10), 5);
|
||||
$this->assertEquals(clamp(5, 5, 10), 5);
|
||||
$this->assertEquals(clamp(7, 5, 10), 7);
|
||||
$this->assertEquals(clamp(10, 5, 10), 10);
|
||||
$this->assertEquals(clamp(15, 5, 10), 10);
|
||||
}
|
||||
|
||||
public function test_xml_tag()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<test foo=\"bar\" >\n<cake />\n</test>\n",
|
||||
xml_tag("test", ["foo"=>"bar"], ["cake"])
|
||||
);
|
||||
}
|
||||
|
||||
public function test_truncate()
|
||||
{
|
||||
$this->assertEquals(truncate("test words", 10), "test words");
|
||||
$this->assertEquals(truncate("test...", 9), "test...");
|
||||
$this->assertEquals(truncate("test...", 6), "test...");
|
||||
$this->assertEquals(truncate("te...", 2), "te...");
|
||||
}
|
||||
|
||||
public function test_to_shorthand_int()
|
||||
{
|
||||
$this->assertEquals(to_shorthand_int(1231231231), "1.1GB");
|
||||
$this->assertEquals(to_shorthand_int(2), "2");
|
||||
}
|
||||
|
||||
public function test_parse_shorthand_int()
|
||||
{
|
||||
$this->assertEquals(parse_shorthand_int("foo"), -1);
|
||||
$this->assertEquals(parse_shorthand_int("32M"), 33554432);
|
||||
$this->assertEquals(parse_shorthand_int("43.4KB"), 44441);
|
||||
$this->assertEquals(parse_shorthand_int("1231231231"), 1231231231);
|
||||
}
|
||||
|
||||
public function test_format_milliseconds()
|
||||
{
|
||||
$this->assertEquals("", format_milliseconds(5));
|
||||
$this->assertEquals("5s", format_milliseconds(5000));
|
||||
$this->assertEquals("1y 213d 16h 53m 20s", format_milliseconds(50000000000));
|
||||
}
|
||||
|
||||
public function test_autodate()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<time datetime='2012-06-23T16:14:22+00:00'>June 23, 2012; 16:14</time>",
|
||||
autodate("2012-06-23 16:14:22")
|
||||
);
|
||||
}
|
||||
|
||||
public function test_validate_input()
|
||||
{
|
||||
$_POST = [
|
||||
"foo" => " bar ",
|
||||
"to_null" => " ",
|
||||
"num" => "42",
|
||||
];
|
||||
$this->assertEquals(
|
||||
["foo"=>"bar"],
|
||||
validate_input(["foo"=>"string,trim,lower"])
|
||||
);
|
||||
//$this->assertEquals(
|
||||
// ["to_null"=>null],
|
||||
// validate_input(["to_null"=>"string,trim,nullify"])
|
||||
//);
|
||||
$this->assertEquals(
|
||||
["num"=>42],
|
||||
validate_input(["num"=>"int"])
|
||||
);
|
||||
}
|
||||
|
||||
public function test_sanitize_path()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"one",
|
||||
sanitize_path("one")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two",
|
||||
sanitize_path("one\\two")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two",
|
||||
sanitize_path("one/two")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two",
|
||||
sanitize_path("one\\\\two")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two",
|
||||
sanitize_path("one//two")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two",
|
||||
sanitize_path("one\\\\\\two")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two",
|
||||
sanitize_path("one///two")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
DIRECTORY_SEPARATOR."one".DIRECTORY_SEPARATOR."two".DIRECTORY_SEPARATOR,
|
||||
sanitize_path("\\/one/\\/\\/two\\/")
|
||||
);
|
||||
}
|
||||
|
||||
public function test_join_path()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"one",
|
||||
join_path("one")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two",
|
||||
join_path("one", "two")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two".DIRECTORY_SEPARATOR."three",
|
||||
join_path("one", "two", "three")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two".DIRECTORY_SEPARATOR."three",
|
||||
join_path("one/two", "three")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
DIRECTORY_SEPARATOR."one".DIRECTORY_SEPARATOR."two".DIRECTORY_SEPARATOR."three".DIRECTORY_SEPARATOR,
|
||||
join_path("\\/////\\\\one/\///"."\\//two\/\\//\\//", "//\/\\\/three/\\/\/")
|
||||
);
|
||||
}
|
||||
|
||||
public function test_stringer()
|
||||
{
|
||||
$this->assertEquals(
|
||||
'["foo"=>"bar", "baz"=>[1, 2, 3], "qux"=>["a"=>"b"]]',
|
||||
stringer(["foo"=>"bar", "baz"=>[1,2,3], "qux"=>["a"=>"b"]])
|
||||
);
|
||||
}
|
||||
}
|
22
core/tests/tag.test.php
Normal file
22
core/tests/tag.test.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once "core/imageboard/tag.php";
|
||||
|
||||
class TagTest extends TestCase
|
||||
{
|
||||
public function test_caret()
|
||||
{
|
||||
$this->assertEquals("foo", Tag::decaret("foo"));
|
||||
$this->assertEquals("foo?", Tag::decaret("foo^q"));
|
||||
$this->assertEquals("a^b/c\\d?e&f", Tag::decaret("a^^b^sc^bd^qe^af"));
|
||||
}
|
||||
|
||||
public function test_decaret()
|
||||
{
|
||||
$this->assertEquals("foo", Tag::caret("foo"));
|
||||
$this->assertEquals("foo^q", Tag::caret("foo?"));
|
||||
$this->assertEquals("a^^b^sc^bd^qe^af", Tag::caret("a^b/c\\d?e&f"));
|
||||
}
|
||||
}
|
42
core/tests/urls.test.php
Normal file
42
core/tests/urls.test.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once "core/urls.php";
|
||||
|
||||
class UrlsTest extends TestCase
|
||||
{
|
||||
public function test_make_link()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"/test/foo",
|
||||
make_link("foo")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"/test/foo",
|
||||
make_link("/foo")
|
||||
);
|
||||
}
|
||||
|
||||
public function test_make_http()
|
||||
{
|
||||
// relative to shimmie install
|
||||
$this->assertEquals(
|
||||
"http://<cli command>/test/foo",
|
||||
make_http("foo")
|
||||
);
|
||||
|
||||
// relative to web server
|
||||
$this->assertEquals(
|
||||
"http://<cli command>/foo",
|
||||
make_http("/foo")
|
||||
);
|
||||
|
||||
// absolute
|
||||
$this->assertEquals(
|
||||
"http://foo.com",
|
||||
make_http("http://foo.com")
|
||||
);
|
||||
}
|
||||
}
|
86
core/tests/util.test.php
Normal file
86
core/tests/util.test.php
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once "core/util.php";
|
||||
|
||||
class UtilTest extends TestCase
|
||||
{
|
||||
public function test_warehouse_path()
|
||||
{
|
||||
$hash = "7ac19c10d6859415";
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", $hash),
|
||||
warehouse_path("base", $hash, false, 0)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", $hash),
|
||||
warehouse_path("base", $hash, false, 1)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", $hash),
|
||||
warehouse_path("base", $hash, false, 2)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", "9c", $hash),
|
||||
warehouse_path("base", $hash, false, 3)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", $hash),
|
||||
warehouse_path("base", $hash, false, 4)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", $hash),
|
||||
warehouse_path("base", $hash, false, 5)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", $hash),
|
||||
warehouse_path("base", $hash, false, 6)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", $hash),
|
||||
warehouse_path("base", $hash, false, 7)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", "15", $hash),
|
||||
warehouse_path("base", $hash, false, 8)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", "15", $hash),
|
||||
warehouse_path("base", $hash, false, 9)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", "15", $hash),
|
||||
warehouse_path("base", $hash, false, 10)
|
||||
);
|
||||
}
|
||||
|
||||
public function test_load_balance_url()
|
||||
{
|
||||
$hash = "7ac19c10d6859415";
|
||||
$ext = "jpg";
|
||||
|
||||
// pseudo-randomly select one of the image servers, balanced in given ratio
|
||||
$this->assertEquals(
|
||||
"https://baz.mycdn.com/7ac19c10d6859415.jpg",
|
||||
load_balance_url("https://{foo=10,bar=5,baz=5}.mycdn.com/$hash.$ext", $hash)
|
||||
);
|
||||
|
||||
// N'th and N+1'th results should be different
|
||||
$this->assertNotEquals(
|
||||
load_balance_url("https://{foo=10,bar=5,baz=5}.mycdn.com/$hash.$ext", $hash, 0),
|
||||
load_balance_url("https://{foo=10,bar=5,baz=5}.mycdn.com/$hash.$ext", $hash, 1)
|
||||
);
|
||||
}
|
||||
}
|
118
core/urls.php
Normal file
118
core/urls.php
Normal file
|
@ -0,0 +1,118 @@
|
|||
<?php declare(strict_types=1);
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* HTML Generation *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
class Link
|
||||
{
|
||||
public $page;
|
||||
public $query;
|
||||
|
||||
public function __construct(?string $page=null, ?string $query=null)
|
||||
{
|
||||
$this->page = $page;
|
||||
$this->query = $query;
|
||||
}
|
||||
|
||||
public function make_link(): string
|
||||
{
|
||||
return make_link($this->page, $this->query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out the correct way to link to a page, taking into account
|
||||
* things like the nice URLs setting.
|
||||
*
|
||||
* eg make_link("post/list") becomes "/v2/index.php?q=post/list"
|
||||
*/
|
||||
function make_link(?string $page=null, ?string $query=null): string
|
||||
{
|
||||
global $config;
|
||||
|
||||
if (is_null($page)) {
|
||||
$page = $config->get_string(SetupConfig::MAIN_PAGE);
|
||||
}
|
||||
|
||||
$install_dir = get_base_href();
|
||||
if (SPEED_HAX || $config->get_bool('nice_urls', false)) {
|
||||
$base = $install_dir;
|
||||
} else {
|
||||
$base = "$install_dir/index.php?q=";
|
||||
}
|
||||
|
||||
if (is_null($query)) {
|
||||
return str_replace("//", "/", $base.'/'.$page);
|
||||
} else {
|
||||
if (strpos($base, "?")) {
|
||||
return $base .'/'. $page .'&'. $query;
|
||||
} elseif (strpos($query, "#") === 0) {
|
||||
return $base .'/'. $page . $query;
|
||||
} else {
|
||||
return $base .'/'. $page .'?'. $query;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Take the current URL and modify some parameters
|
||||
*/
|
||||
function modify_current_url(array $changes): string
|
||||
{
|
||||
return modify_url($_SERVER['QUERY_STRING'], $changes);
|
||||
}
|
||||
|
||||
function modify_url(string $url, array $changes): string
|
||||
{
|
||||
// SHIT: PHP is officially the worst web API ever because it does not
|
||||
// have a built-in function to do this.
|
||||
|
||||
// SHIT: parse_str is magically retarded; not only is it a useless name, it also
|
||||
// didn't return the parsed array, preferring to overwrite global variables with
|
||||
// whatever data the user supplied. Thankfully, 4.0.3 added an extra option to
|
||||
// give it an array to use...
|
||||
$params = [];
|
||||
parse_str($url, $params);
|
||||
|
||||
if (isset($changes['q'])) {
|
||||
$base = $changes['q'];
|
||||
unset($changes['q']);
|
||||
} else {
|
||||
$base = _get_query();
|
||||
}
|
||||
|
||||
if (isset($params['q'])) {
|
||||
unset($params['q']);
|
||||
}
|
||||
|
||||
foreach ($changes as $k => $v) {
|
||||
if (is_null($v) and isset($params[$k])) {
|
||||
unset($params[$k]);
|
||||
}
|
||||
$params[$k] = $v;
|
||||
}
|
||||
|
||||
return make_link($base, http_build_query($params));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Turn a relative link into an absolute one, including hostname
|
||||
*/
|
||||
function make_http(string $link): string
|
||||
{
|
||||
if (strpos($link, "://") > 0) {
|
||||
return $link;
|
||||
}
|
||||
|
||||
if (strlen($link) > 0 && $link[0] != '/') {
|
||||
$link = get_base_href() . '/' . $link;
|
||||
}
|
||||
|
||||
$protocol = is_https_enabled() ? "https://" : "http://";
|
||||
$link = $protocol . $_SERVER["HTTP_HOST"] . $link;
|
||||
$link = str_replace("/./", "/", $link);
|
||||
|
||||
return $link;
|
||||
}
|
|
@ -1,300 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param mixed $row
|
||||
* @return User
|
||||
*/
|
||||
function _new_user($row) {
|
||||
return new User($row);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class User
|
||||
*
|
||||
* An object representing a row in the "users" table.
|
||||
*
|
||||
* The currently logged in user will always be accessible via the global variable $user.
|
||||
*/
|
||||
class User {
|
||||
/** @var int */
|
||||
public $id;
|
||||
|
||||
/** @var string */
|
||||
public $name;
|
||||
|
||||
/** @var string */
|
||||
public $email;
|
||||
|
||||
public $join_date;
|
||||
|
||||
/** @var string */
|
||||
public $passhash;
|
||||
|
||||
/** @var UserClass */
|
||||
public $class;
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Initialisation *
|
||||
* *
|
||||
* User objects shouldn't be created directly, they should be *
|
||||
* fetched from the database like so: *
|
||||
* *
|
||||
* $user = User::by_name("bob"); *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* One will very rarely construct a user directly, more common
|
||||
* would be to use User::by_id, User::by_session, etc.
|
||||
*
|
||||
* @param mixed $row
|
||||
* @throws SCoreException
|
||||
*/
|
||||
public function __construct($row) {
|
||||
global $_shm_user_classes;
|
||||
|
||||
$this->id = int_escape($row['id']);
|
||||
$this->name = $row['name'];
|
||||
$this->email = $row['email'];
|
||||
$this->join_date = $row['joindate'];
|
||||
$this->passhash = $row['pass'];
|
||||
|
||||
if(array_key_exists($row["class"], $_shm_user_classes)) {
|
||||
$this->class = $_shm_user_classes[$row["class"]];
|
||||
}
|
||||
else {
|
||||
throw new SCoreException("User '{$this->name}' has invalid class '{$row["class"]}'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a User by session.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $session
|
||||
* @return null|User
|
||||
*/
|
||||
public static function by_session(/*string*/ $name, /*string*/ $session) {
|
||||
global $config, $database;
|
||||
$row = $database->cache->get("user-session:$name-$session");
|
||||
if(!$row) {
|
||||
if($database->get_driver_name() === "mysql") {
|
||||
$query = "SELECT * FROM users WHERE name = :name AND md5(concat(pass, :ip)) = :sess";
|
||||
}
|
||||
else {
|
||||
$query = "SELECT * FROM users WHERE name = :name AND md5(pass || :ip) = :sess";
|
||||
}
|
||||
$row = $database->get_row($query, array("name"=>$name, "ip"=>get_session_ip($config), "sess"=>$session));
|
||||
$database->cache->set("user-session:$name-$session", $row, 600);
|
||||
}
|
||||
return is_null($row) ? null : new User($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a User by session.
|
||||
* @param int $id
|
||||
* @return null|User
|
||||
*/
|
||||
public static function by_id(/*int*/ $id) {
|
||||
assert('is_numeric($id)', var_export($id, true));
|
||||
global $database;
|
||||
if($id === 1) {
|
||||
$cached = $database->cache->get('user-id:'.$id);
|
||||
if($cached) return new User($cached);
|
||||
}
|
||||
$row = $database->get_row("SELECT * FROM users WHERE id = :id", array("id"=>$id));
|
||||
if($id === 1) $database->cache->set('user-id:'.$id, $row, 600);
|
||||
return is_null($row) ? null : new User($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a User by name.
|
||||
* @param string $name
|
||||
* @return null|User
|
||||
*/
|
||||
public static function by_name(/*string*/ $name) {
|
||||
assert('is_string($name)', var_export($name, true));
|
||||
global $database;
|
||||
$row = $database->get_row($database->scoreql_to_sql("SELECT * FROM users WHERE SCORE_STRNORM(name) = SCORE_STRNORM(:name)"), array("name"=>$name));
|
||||
return is_null($row) ? null : new User($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a User by name and password.
|
||||
* @param string $name
|
||||
* @param string $pass
|
||||
* @return null|User
|
||||
*/
|
||||
public static function by_name_and_pass(/*string*/ $name, /*string*/ $pass) {
|
||||
assert('is_string($name)', var_export($name, true));
|
||||
assert('is_string($pass)', var_export($pass, true));
|
||||
$user = User::by_name($name);
|
||||
if($user) {
|
||||
if($user->passhash == md5(strtolower($name) . $pass)) {
|
||||
$user->set_password($pass);
|
||||
}
|
||||
if(password_verify($pass, $user->passhash)) {
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* useful user object functions start here */
|
||||
|
||||
|
||||
/**
|
||||
* @param string $ability
|
||||
* @return bool
|
||||
*/
|
||||
public function can($ability) {
|
||||
return $this->class->can($ability);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test if this user is anonymous (not logged in).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_anonymous() {
|
||||
global $config;
|
||||
return ($this->id === $config->get_int('anon_id'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if this user is logged in.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_logged_in() {
|
||||
global $config;
|
||||
return ($this->id !== $config->get_int('anon_id'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if this user is an administrator.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_admin() {
|
||||
return ($this->class->name === "admin");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
*/
|
||||
public function set_class(/*string*/ $class) {
|
||||
assert('is_string($class)', var_export($class, true));
|
||||
global $database;
|
||||
$database->Execute("UPDATE users SET class=:class WHERE id=:id", array("class"=>$class, "id"=>$this->id));
|
||||
log_info("core-user", 'Set class for '.$this->name.' to '.$class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @throws Exception
|
||||
*/
|
||||
public function set_name(/*string*/ $name) {
|
||||
global $database;
|
||||
if(User::by_name($name)) {
|
||||
throw new Exception("Desired username is already in use");
|
||||
}
|
||||
$old_name = $this->name;
|
||||
$this->name = $name;
|
||||
$database->Execute("UPDATE users SET name=:name WHERE id=:id", array("name"=>$this->name, "id"=>$this->id));
|
||||
log_info("core-user", "Changed username for {$old_name} to {$this->name}");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $password
|
||||
*/
|
||||
public function set_password(/*string*/ $password) {
|
||||
global $database;
|
||||
$hash = password_hash($password, PASSWORD_BCRYPT);
|
||||
if(is_string($hash)) {
|
||||
$this->passhash = $hash;
|
||||
$database->Execute("UPDATE users SET pass=:hash WHERE id=:id", array("hash"=>$this->passhash, "id"=>$this->id));
|
||||
log_info("core-user", 'Set password for '.$this->name);
|
||||
}
|
||||
else {
|
||||
throw new SCoreException("Failed to hash password");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $address
|
||||
*/
|
||||
public function set_email(/*string*/ $address) {
|
||||
global $database;
|
||||
$database->Execute("UPDATE users SET email=:email WHERE id=:id", array("email"=>$address, "id"=>$this->id));
|
||||
log_info("core-user", 'Set email for '.$this->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a snippet of HTML which will render the user's avatar, be that
|
||||
* a local file, a remote file, a gravatar, a something else, etc.
|
||||
*
|
||||
* @return String of HTML
|
||||
*/
|
||||
public function get_avatar_html() {
|
||||
// FIXME: configurable
|
||||
global $config;
|
||||
if($config->get_string("avatar_host") === "gravatar") {
|
||||
if(!empty($this->email)) {
|
||||
$hash = md5(strtolower($this->email));
|
||||
$s = $config->get_string("avatar_gravatar_size");
|
||||
$d = urlencode($config->get_string("avatar_gravatar_default"));
|
||||
$r = $config->get_string("avatar_gravatar_rating");
|
||||
$cb = date("Y-m-d");
|
||||
return "<img class=\"avatar gravatar\" src=\"http://www.gravatar.com/avatar/$hash.jpg?s=$s&d=$d&r=$r&cacheBreak=$cb\">";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an auth token to be used in POST forms
|
||||
*
|
||||
* password = secret, avoid storing directly
|
||||
* passhash = bcrypt(password), so someone who gets to the database can't get passwords
|
||||
* sesskey = md5(passhash . IP), so if it gets sniffed it can't be used from another IP,
|
||||
* and it can't be used to get the passhash to generate new sesskeys
|
||||
* authtok = md5(sesskey, salt), presented to the user in web forms, to make sure that
|
||||
* the form was generated within the session. Salted and re-hashed so that
|
||||
* reading a web page from the user's cache doesn't give access to the session key
|
||||
*
|
||||
* @return string A string containing auth token (MD5sum)
|
||||
*/
|
||||
public function get_auth_token() {
|
||||
global $config;
|
||||
$salt = DATABASE_DSN;
|
||||
$addr = get_session_ip($config);
|
||||
return md5(md5($this->passhash . $addr) . "salty-csrf-" . $salt);
|
||||
}
|
||||
|
||||
public function get_auth_html() {
|
||||
$at = $this->get_auth_token();
|
||||
return '<input type="hidden" name="auth_token" value="'.$at.'">';
|
||||
}
|
||||
|
||||
public function check_auth_token() {
|
||||
return (isset($_POST["auth_token"]) && $_POST["auth_token"] == $this->get_auth_token());
|
||||
}
|
||||
}
|
||||
|
||||
class MockUser extends User {
|
||||
public function __construct($name) {
|
||||
$row = array(
|
||||
"name" => $name,
|
||||
"id" => 1,
|
||||
"email" => "",
|
||||
"joindate" => "",
|
||||
"pass" => "",
|
||||
"class" => "admin",
|
||||
);
|
||||
parent::__construct($row);
|
||||
}
|
||||
}
|
||||
|
259
core/user.php
Normal file
259
core/user.php
Normal file
|
@ -0,0 +1,259 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
function _new_user(array $row): User
|
||||
{
|
||||
return new User($row);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class User
|
||||
*
|
||||
* An object representing a row in the "users" table.
|
||||
*
|
||||
* The currently logged in user will always be accessible via the global variable $user.
|
||||
*/
|
||||
class User
|
||||
{
|
||||
/** @var int */
|
||||
public $id;
|
||||
|
||||
/** @var string */
|
||||
public $name;
|
||||
|
||||
/** @var string */
|
||||
public $email;
|
||||
|
||||
public $join_date;
|
||||
|
||||
/** @var string */
|
||||
public $passhash;
|
||||
|
||||
/** @var UserClass */
|
||||
public $class;
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Initialisation *
|
||||
* *
|
||||
* User objects shouldn't be created directly, they should be *
|
||||
* fetched from the database like so: *
|
||||
* *
|
||||
* $user = User::by_name("bob"); *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* One will very rarely construct a user directly, more common
|
||||
* would be to use User::by_id, User::by_session, etc.
|
||||
*
|
||||
* @throws SCoreException
|
||||
*/
|
||||
public function __construct(array $row)
|
||||
{
|
||||
global $_shm_user_classes;
|
||||
|
||||
$this->id = int_escape((string)$row['id']);
|
||||
$this->name = $row['name'];
|
||||
$this->email = $row['email'];
|
||||
$this->join_date = $row['joindate'];
|
||||
$this->passhash = $row['pass'];
|
||||
|
||||
if (array_key_exists($row["class"], $_shm_user_classes)) {
|
||||
$this->class = $_shm_user_classes[$row["class"]];
|
||||
} else {
|
||||
throw new SCoreException("User '{$this->name}' has invalid class '{$row["class"]}'");
|
||||
}
|
||||
}
|
||||
|
||||
public static function by_session(string $name, string $session): ?User
|
||||
{
|
||||
global $cache, $config, $database;
|
||||
$row = $cache->get("user-session:$name-$session");
|
||||
if (!$row) {
|
||||
if ($database->get_driver_name() === DatabaseDriver::MYSQL) {
|
||||
$query = "SELECT * FROM users WHERE name = :name AND md5(concat(pass, :ip)) = :sess";
|
||||
} else {
|
||||
$query = "SELECT * FROM users WHERE name = :name AND md5(pass || :ip) = :sess";
|
||||
}
|
||||
$row = $database->get_row($query, ["name"=>$name, "ip"=>get_session_ip($config), "sess"=>$session]);
|
||||
$cache->set("user-session:$name-$session", $row, 600);
|
||||
}
|
||||
return is_null($row) ? null : new User($row);
|
||||
}
|
||||
|
||||
public static function by_id(int $id): ?User
|
||||
{
|
||||
global $cache, $database;
|
||||
if ($id === 1) {
|
||||
$cached = $cache->get('user-id:'.$id);
|
||||
if ($cached) {
|
||||
return new User($cached);
|
||||
}
|
||||
}
|
||||
$row = $database->get_row("SELECT * FROM users WHERE id = :id", ["id"=>$id]);
|
||||
if ($id === 1) {
|
||||
$cache->set('user-id:'.$id, $row, 600);
|
||||
}
|
||||
return is_null($row) ? null : new User($row);
|
||||
}
|
||||
|
||||
public static function by_name(string $name): ?User
|
||||
{
|
||||
global $database;
|
||||
$row = $database->get_row("SELECT * FROM users WHERE LOWER(name) = LOWER(:name)", ["name"=>$name]);
|
||||
return is_null($row) ? null : new User($row);
|
||||
}
|
||||
|
||||
public static function name_to_id(string $name): int
|
||||
{
|
||||
$u = User::by_name($name);
|
||||
if (is_null($u)) {
|
||||
throw new ScoreException("Can't find any user named $name");
|
||||
} else {
|
||||
return $u->id;
|
||||
}
|
||||
}
|
||||
|
||||
public static function by_name_and_pass(string $name, string $pass): ?User
|
||||
{
|
||||
$my_user = User::by_name($name);
|
||||
|
||||
// If user tried to log in as "foo bar" and failed, try "foo_bar"
|
||||
if (!$my_user && strpos($name, " ") !== false) {
|
||||
$my_user = User::by_name(str_replace(" ", "_", $name));
|
||||
}
|
||||
|
||||
if ($my_user) {
|
||||
if ($my_user->passhash == md5(strtolower($name) . $pass)) {
|
||||
log_info("core-user", "Migrating from md5 to bcrypt for $name");
|
||||
$my_user->set_password($pass);
|
||||
}
|
||||
if (password_verify($pass, $my_user->passhash)) {
|
||||
log_info("core-user", "Logged in as $name ({$my_user->class->name})");
|
||||
return $my_user;
|
||||
} else {
|
||||
log_warning("core-user", "Failed to log in as $name (Invalid password)");
|
||||
}
|
||||
} else {
|
||||
log_warning("core-user", "Failed to log in as $name (Invalid username)");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/* useful user object functions start here */
|
||||
|
||||
public function can(string $ability): bool
|
||||
{
|
||||
return $this->class->can($ability);
|
||||
}
|
||||
|
||||
|
||||
public function is_anonymous(): bool
|
||||
{
|
||||
global $config;
|
||||
return ($this->id === $config->get_int('anon_id'));
|
||||
}
|
||||
|
||||
public function is_logged_in(): bool
|
||||
{
|
||||
global $config;
|
||||
return ($this->id !== $config->get_int('anon_id'));
|
||||
}
|
||||
|
||||
public function set_class(string $class): void
|
||||
{
|
||||
global $database;
|
||||
$database->Execute("UPDATE users SET class=:class WHERE id=:id", ["class"=>$class, "id"=>$this->id]);
|
||||
log_info("core-user", 'Set class for '.$this->name.' to '.$class);
|
||||
}
|
||||
|
||||
public function set_name(string $name): void
|
||||
{
|
||||
global $database;
|
||||
if (User::by_name($name)) {
|
||||
throw new ScoreException("Desired username is already in use");
|
||||
}
|
||||
$old_name = $this->name;
|
||||
$this->name = $name;
|
||||
$database->Execute("UPDATE users SET name=:name WHERE id=:id", ["name"=>$this->name, "id"=>$this->id]);
|
||||
log_info("core-user", "Changed username for {$old_name} to {$this->name}");
|
||||
}
|
||||
|
||||
public function set_password(string $password): void
|
||||
{
|
||||
global $database;
|
||||
$hash = password_hash($password, PASSWORD_BCRYPT);
|
||||
if (is_string($hash)) {
|
||||
$this->passhash = $hash;
|
||||
$database->Execute("UPDATE users SET pass=:hash WHERE id=:id", ["hash"=>$this->passhash, "id"=>$this->id]);
|
||||
log_info("core-user", 'Set password for '.$this->name);
|
||||
} else {
|
||||
throw new SCoreException("Failed to hash password");
|
||||
}
|
||||
}
|
||||
|
||||
public function set_email(string $address): void
|
||||
{
|
||||
global $database;
|
||||
$database->Execute("UPDATE users SET email=:email WHERE id=:id", ["email"=>$address, "id"=>$this->id]);
|
||||
log_info("core-user", 'Set email for '.$this->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a snippet of HTML which will render the user's avatar, be that
|
||||
* a local file, a remote file, a gravatar, a something else, etc.
|
||||
*/
|
||||
public function get_avatar_html(): string
|
||||
{
|
||||
// FIXME: configurable
|
||||
global $config;
|
||||
if ($config->get_string("avatar_host") === "gravatar") {
|
||||
if (!empty($this->email)) {
|
||||
$hash = md5(strtolower($this->email));
|
||||
$s = $config->get_string("avatar_gravatar_size");
|
||||
$d = urlencode($config->get_string("avatar_gravatar_default"));
|
||||
$r = $config->get_string("avatar_gravatar_rating");
|
||||
$cb = date("Y-m-d");
|
||||
return "<img alt='avatar' class=\"avatar gravatar\" src=\"https://www.gravatar.com/avatar/$hash.jpg?s=$s&d=$d&r=$r&cacheBreak=$cb\">";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an auth token to be used in POST forms
|
||||
*
|
||||
* password = secret, avoid storing directly
|
||||
* passhash = bcrypt(password), so someone who gets to the database can't get passwords
|
||||
* sesskey = md5(passhash . IP), so if it gets sniffed it can't be used from another IP,
|
||||
* and it can't be used to get the passhash to generate new sesskeys
|
||||
* authtok = md5(sesskey, salt), presented to the user in web forms, to make sure that
|
||||
* the form was generated within the session. Salted and re-hashed so that
|
||||
* reading a web page from the user's cache doesn't give access to the session key
|
||||
*/
|
||||
public function get_auth_token(): string
|
||||
{
|
||||
global $config;
|
||||
$salt = DATABASE_DSN;
|
||||
$addr = get_session_ip($config);
|
||||
return md5(md5($this->passhash . $addr) . "salty-csrf-" . $salt);
|
||||
}
|
||||
|
||||
public function get_auth_html(): string
|
||||
{
|
||||
$at = $this->get_auth_token();
|
||||
return '<input type="hidden" name="auth_token" value="'.$at.'">';
|
||||
}
|
||||
|
||||
public function check_auth_token(): bool
|
||||
{
|
||||
return (isset($_POST["auth_token"]) && $_POST["auth_token"] == $this->get_auth_token());
|
||||
}
|
||||
|
||||
public function ensure_authed(): void
|
||||
{
|
||||
if (!$this->check_auth_token()) {
|
||||
die("Invalid auth token");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,200 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @global UserClass[] $_shm_user_classes
|
||||
*/
|
||||
global $_shm_user_classes;
|
||||
$_shm_user_classes = array();
|
||||
|
||||
/**
|
||||
* Class UserClass
|
||||
*/
|
||||
class UserClass {
|
||||
|
||||
/**
|
||||
* @var null|string
|
||||
*/
|
||||
public $name = null;
|
||||
|
||||
/**
|
||||
* @var \UserClass|null
|
||||
*/
|
||||
public $parent = null;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $abilities = array();
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param null|string $parent
|
||||
* @param array $abilities
|
||||
*/
|
||||
public function __construct($name, $parent=null, $abilities=array()) {
|
||||
global $_shm_user_classes;
|
||||
|
||||
$this->name = $name;
|
||||
$this->abilities = $abilities;
|
||||
|
||||
if(!is_null($parent)) {
|
||||
$this->parent = $_shm_user_classes[$parent];
|
||||
}
|
||||
|
||||
$_shm_user_classes[$name] = $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this class of user can perform an action or has ability.
|
||||
*
|
||||
* @param string $ability
|
||||
* @return bool
|
||||
* @throws SCoreException
|
||||
*/
|
||||
public function can(/*string*/ $ability) {
|
||||
if(array_key_exists($ability, $this->abilities)) {
|
||||
$val = $this->abilities[$ability];
|
||||
return $val;
|
||||
}
|
||||
else if(!is_null($this->parent)) {
|
||||
return $this->parent->can($ability);
|
||||
}
|
||||
else {
|
||||
global $_shm_user_classes;
|
||||
$min_dist = 9999;
|
||||
$min_ability = null;
|
||||
foreach($_shm_user_classes['base']->abilities as $a => $cando) {
|
||||
$v = levenshtein($ability, $a);
|
||||
if($v < $min_dist) {
|
||||
$min_dist = $v;
|
||||
$min_ability = $a;
|
||||
}
|
||||
}
|
||||
throw new SCoreException("Unknown ability '".html_escape($ability)."'. Did the developer mean '".html_escape($min_ability)."'?");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// action_object_attribute
|
||||
// action = create / view / edit / delete
|
||||
// object = image / user / tag / setting
|
||||
new UserClass("base", null, array(
|
||||
"change_setting" => False, # modify web-level settings, eg the config table
|
||||
"override_config" => False, # modify sys-level settings, eg shimmie.conf.php
|
||||
"big_search" => False, # search for more than 3 tags at once (speed mode only)
|
||||
|
||||
"manage_extension_list" => False,
|
||||
"manage_alias_list" => False,
|
||||
"mass_tag_edit" => False,
|
||||
|
||||
"view_ip" => False, # view IP addresses associated with things
|
||||
"ban_ip" => False,
|
||||
|
||||
"edit_user_name" => False,
|
||||
"edit_user_password" => False,
|
||||
"edit_user_info" => False, # email address, etc
|
||||
"edit_user_class" => False,
|
||||
"delete_user" => False,
|
||||
|
||||
"create_comment" => False,
|
||||
"delete_comment" => False,
|
||||
"bypass_comment_checks" => False, # spam etc
|
||||
|
||||
"replace_image" => False,
|
||||
"create_image" => False,
|
||||
"edit_image_tag" => False,
|
||||
"edit_image_source" => False,
|
||||
"edit_image_owner" => False,
|
||||
"edit_image_lock" => False,
|
||||
"bulk_edit_image_tag" => False,
|
||||
"bulk_edit_image_source" => False,
|
||||
"delete_image" => False,
|
||||
|
||||
"ban_image" => False,
|
||||
|
||||
"view_eventlog" => False,
|
||||
"ignore_downtime" => False,
|
||||
|
||||
"create_image_report" => False,
|
||||
"view_image_report" => False, # deal with reported images
|
||||
|
||||
"edit_wiki_page" => False,
|
||||
"delete_wiki_page" => False,
|
||||
|
||||
"manage_blocks" => False,
|
||||
|
||||
"manage_admintools" => False,
|
||||
|
||||
"view_other_pms" => False,
|
||||
"edit_feature" => False,
|
||||
"bulk_edit_vote" => False,
|
||||
"edit_other_vote" => False,
|
||||
"view_sysinfo" => False,
|
||||
|
||||
"hellbanned" => False,
|
||||
"view_hellbanned" => False,
|
||||
|
||||
"protected" => False, # only admins can modify protected users (stops a moderator changing an admin's password)
|
||||
));
|
||||
|
||||
new UserClass("anonymous", "base", array(
|
||||
));
|
||||
|
||||
new UserClass("user", "base", array(
|
||||
"big_search" => True,
|
||||
"create_image" => True,
|
||||
"create_comment" => True,
|
||||
"edit_image_tag" => True,
|
||||
"edit_image_source" => True,
|
||||
"create_image_report" => True,
|
||||
));
|
||||
|
||||
new UserClass("admin", "base", array(
|
||||
"change_setting" => True,
|
||||
"override_config" => True,
|
||||
"big_search" => True,
|
||||
"edit_image_lock" => True,
|
||||
"view_ip" => True,
|
||||
"ban_ip" => True,
|
||||
"edit_user_name" => True,
|
||||
"edit_user_password" => True,
|
||||
"edit_user_info" => True,
|
||||
"edit_user_class" => True,
|
||||
"delete_user" => True,
|
||||
"create_image" => True,
|
||||
"delete_image" => True,
|
||||
"ban_image" => True,
|
||||
"create_comment" => True,
|
||||
"delete_comment" => True,
|
||||
"bypass_comment_checks" => True,
|
||||
"replace_image" => True,
|
||||
"manage_extension_list" => True,
|
||||
"manage_alias_list" => True,
|
||||
"edit_image_tag" => True,
|
||||
"edit_image_source" => True,
|
||||
"edit_image_owner" => True,
|
||||
"bulk_edit_image_tag" => True,
|
||||
"bulk_edit_image_source" => True,
|
||||
"mass_tag_edit" => True,
|
||||
"create_image_report" => True,
|
||||
"view_image_report" => True,
|
||||
"edit_wiki_page" => True,
|
||||
"delete_wiki_page" => True,
|
||||
"view_eventlog" => True,
|
||||
"manage_blocks" => True,
|
||||
"manage_admintools" => True,
|
||||
"ignore_downtime" => True,
|
||||
"view_other_pms" => True,
|
||||
"edit_feature" => True,
|
||||
"bulk_edit_vote" => True,
|
||||
"edit_other_vote" => True,
|
||||
"view_sysinfo" => True,
|
||||
"view_hellbanned" => True,
|
||||
"protected" => True,
|
||||
));
|
||||
|
||||
new UserClass("hellbanned", "user", array(
|
||||
"hellbanned" => True,
|
||||
));
|
||||
|
||||
@include_once "data/config/user-classes.conf.php";
|
||||
|
263
core/userclass.php
Normal file
263
core/userclass.php
Normal file
|
@ -0,0 +1,263 @@
|
|||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* @global UserClass[] $_shm_user_classes
|
||||
*/
|
||||
global $_shm_user_classes;
|
||||
$_shm_user_classes = [];
|
||||
|
||||
/**
|
||||
* Class UserClass
|
||||
*/
|
||||
class UserClass
|
||||
{
|
||||
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
public $name = null;
|
||||
|
||||
/**
|
||||
* @var ?UserClass
|
||||
*/
|
||||
public $parent = null;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $abilities = [];
|
||||
|
||||
public function __construct(string $name, string $parent=null, array $abilities=[])
|
||||
{
|
||||
global $_shm_user_classes;
|
||||
|
||||
$this->name = $name;
|
||||
$this->abilities = $abilities;
|
||||
|
||||
if (!is_null($parent)) {
|
||||
$this->parent = $_shm_user_classes[$parent];
|
||||
}
|
||||
|
||||
$_shm_user_classes[$name] = $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this class of user can perform an action or has ability.
|
||||
*
|
||||
* @throws SCoreException
|
||||
*/
|
||||
public function can(string $ability): bool
|
||||
{
|
||||
if (array_key_exists($ability, $this->abilities)) {
|
||||
return $this->abilities[$ability];
|
||||
} elseif (!is_null($this->parent)) {
|
||||
return $this->parent->can($ability);
|
||||
} else {
|
||||
global $_shm_user_classes;
|
||||
$min_dist = 9999;
|
||||
$min_ability = null;
|
||||
foreach ($_shm_user_classes['base']->abilities as $a => $cando) {
|
||||
$v = levenshtein($ability, $a);
|
||||
if ($v < $min_dist) {
|
||||
$min_dist = $v;
|
||||
$min_ability = $a;
|
||||
}
|
||||
}
|
||||
throw new SCoreException("Unknown ability '$ability'. Did the developer mean '$min_ability'?");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// action_object_attribute
|
||||
// action = create / view / edit / delete
|
||||
// object = image / user / tag / setting
|
||||
new UserClass("base", null, [
|
||||
Permissions::CHANGE_SETTING => false, # modify web-level settings, eg the config table
|
||||
Permissions::OVERRIDE_CONFIG => false, # modify sys-level settings, eg shimmie.conf.php
|
||||
Permissions::BIG_SEARCH => false, # search for more than 3 tags at once (speed mode only)
|
||||
|
||||
Permissions::MANAGE_EXTENSION_LIST => false,
|
||||
Permissions::MANAGE_ALIAS_LIST => false,
|
||||
Permissions::MANAGE_AUTO_TAG => false,
|
||||
Permissions::MASS_TAG_EDIT => false,
|
||||
|
||||
Permissions::VIEW_IP => false, # view IP addresses associated with things
|
||||
Permissions::BAN_IP => false,
|
||||
|
||||
Permissions::CREATE_USER => false,
|
||||
Permissions::EDIT_USER_NAME => false,
|
||||
Permissions::EDIT_USER_PASSWORD => false,
|
||||
Permissions::EDIT_USER_INFO => false, # email address, etc
|
||||
Permissions::EDIT_USER_CLASS => false,
|
||||
Permissions::DELETE_USER => false,
|
||||
|
||||
Permissions::CREATE_COMMENT => false,
|
||||
Permissions::DELETE_COMMENT => false,
|
||||
Permissions::BYPASS_COMMENT_CHECKS => false, # spam etc
|
||||
|
||||
Permissions::REPLACE_IMAGE => false,
|
||||
Permissions::CREATE_IMAGE => false,
|
||||
Permissions::EDIT_IMAGE_TAG => false,
|
||||
Permissions::EDIT_IMAGE_SOURCE => false,
|
||||
Permissions::EDIT_IMAGE_OWNER => false,
|
||||
Permissions::EDIT_IMAGE_LOCK => false,
|
||||
Permissions::EDIT_IMAGE_TITLE => false,
|
||||
Permissions::EDIT_IMAGE_RELATIONSHIPS => false,
|
||||
Permissions::EDIT_IMAGE_ARTIST => false,
|
||||
Permissions::BULK_EDIT_IMAGE_TAG => false,
|
||||
Permissions::BULK_EDIT_IMAGE_SOURCE => false,
|
||||
Permissions::DELETE_IMAGE => false,
|
||||
|
||||
Permissions::BAN_IMAGE => false,
|
||||
|
||||
Permissions::VIEW_EVENTLOG => false,
|
||||
Permissions::IGNORE_DOWNTIME => false,
|
||||
|
||||
Permissions::CREATE_IMAGE_REPORT => false,
|
||||
Permissions::VIEW_IMAGE_REPORT => false, # deal with reported images
|
||||
|
||||
Permissions::WIKI_ADMIN => false,
|
||||
Permissions::EDIT_WIKI_PAGE => false,
|
||||
Permissions::DELETE_WIKI_PAGE => false,
|
||||
|
||||
Permissions::MANAGE_BLOCKS => false,
|
||||
|
||||
Permissions::MANAGE_ADMINTOOLS => false,
|
||||
|
||||
Permissions::SEND_PM => false,
|
||||
Permissions::READ_PM => false,
|
||||
Permissions::VIEW_OTHER_PMS => false,
|
||||
Permissions::EDIT_FEATURE => false,
|
||||
Permissions::BULK_EDIT_VOTE => false,
|
||||
Permissions::EDIT_OTHER_VOTE => false,
|
||||
Permissions::VIEW_SYSINTO => false,
|
||||
|
||||
Permissions::HELLBANNED => false,
|
||||
Permissions::VIEW_HELLBANNED => false,
|
||||
|
||||
Permissions::PROTECTED => false, # only admins can modify protected users (stops a moderator changing an admin's password)
|
||||
|
||||
Permissions::EDIT_IMAGE_RATING => false,
|
||||
Permissions::BULK_EDIT_IMAGE_RATING => false,
|
||||
|
||||
Permissions::VIEW_TRASH => false,
|
||||
|
||||
Permissions::PERFORM_BULK_ACTIONS => false,
|
||||
|
||||
Permissions::BULK_ADD => false,
|
||||
Permissions::EDIT_FILES => false,
|
||||
Permissions::EDIT_TAG_CATEGORIES => false,
|
||||
Permissions::RESCAN_MEDIA => false,
|
||||
Permissions::SEE_IMAGE_VIEW_COUNTS => false,
|
||||
|
||||
Permissions::EDIT_FAVOURITES => false,
|
||||
|
||||
Permissions::ARTISTS_ADMIN => false,
|
||||
Permissions::BLOTTER_ADMIN => false,
|
||||
Permissions::FORUM_ADMIN => false,
|
||||
Permissions::NOTES_ADMIN => false,
|
||||
Permissions::POOLS_ADMIN => false,
|
||||
Permissions::TIPS_ADMIN => false,
|
||||
Permissions::CRON_ADMIN => false,
|
||||
|
||||
Permissions::APPROVE_IMAGE => false,
|
||||
Permissions::APPROVE_COMMENT => false,
|
||||
]);
|
||||
|
||||
// Ghost users can't do anything
|
||||
new UserClass("ghost", "base", [
|
||||
]);
|
||||
|
||||
// Anonymous users can't do anything by default, but
|
||||
// the admin might grant them some permissions
|
||||
new UserClass("anonymous", "base", [
|
||||
Permissions::CREATE_USER => true,
|
||||
]);
|
||||
|
||||
new UserClass("user", "base", [
|
||||
Permissions::BIG_SEARCH => true,
|
||||
Permissions::CREATE_IMAGE => true,
|
||||
Permissions::CREATE_COMMENT => true,
|
||||
Permissions::EDIT_IMAGE_TAG => true,
|
||||
Permissions::EDIT_IMAGE_SOURCE => true,
|
||||
Permissions::EDIT_IMAGE_TITLE => true,
|
||||
Permissions::EDIT_IMAGE_RELATIONSHIPS => true,
|
||||
Permissions::EDIT_IMAGE_ARTIST => true,
|
||||
Permissions::CREATE_IMAGE_REPORT => true,
|
||||
Permissions::EDIT_IMAGE_RATING => true,
|
||||
Permissions::EDIT_FAVOURITES => true,
|
||||
Permissions::SEND_PM => true,
|
||||
Permissions::READ_PM => true,
|
||||
]);
|
||||
|
||||
new UserClass("admin", "base", [
|
||||
Permissions::CHANGE_SETTING => true,
|
||||
Permissions::OVERRIDE_CONFIG => true,
|
||||
Permissions::BIG_SEARCH => true,
|
||||
Permissions::EDIT_IMAGE_LOCK => true,
|
||||
Permissions::VIEW_IP => true,
|
||||
Permissions::BAN_IP => true,
|
||||
Permissions::EDIT_USER_NAME => true,
|
||||
Permissions::EDIT_USER_PASSWORD => true,
|
||||
Permissions::EDIT_USER_INFO => true,
|
||||
Permissions::EDIT_USER_CLASS => true,
|
||||
Permissions::DELETE_USER => true,
|
||||
Permissions::CREATE_IMAGE => true,
|
||||
Permissions::DELETE_IMAGE => true,
|
||||
Permissions::BAN_IMAGE => true,
|
||||
Permissions::CREATE_COMMENT => true,
|
||||
Permissions::DELETE_COMMENT => true,
|
||||
Permissions::BYPASS_COMMENT_CHECKS => true,
|
||||
Permissions::REPLACE_IMAGE => true,
|
||||
Permissions::MANAGE_EXTENSION_LIST => true,
|
||||
Permissions::MANAGE_ALIAS_LIST => true,
|
||||
Permissions::MANAGE_AUTO_TAG => true,
|
||||
Permissions::EDIT_IMAGE_TAG => true,
|
||||
Permissions::EDIT_IMAGE_SOURCE => true,
|
||||
Permissions::EDIT_IMAGE_OWNER => true,
|
||||
Permissions::EDIT_IMAGE_TITLE => true,
|
||||
Permissions::BULK_EDIT_IMAGE_TAG => true,
|
||||
Permissions::BULK_EDIT_IMAGE_SOURCE => true,
|
||||
Permissions::MASS_TAG_EDIT => true,
|
||||
Permissions::CREATE_IMAGE_REPORT => true,
|
||||
Permissions::VIEW_IMAGE_REPORT => true,
|
||||
Permissions::WIKI_ADMIN => true,
|
||||
Permissions::EDIT_WIKI_PAGE => true,
|
||||
Permissions::DELETE_WIKI_PAGE => true,
|
||||
Permissions::VIEW_EVENTLOG => true,
|
||||
Permissions::MANAGE_BLOCKS => true,
|
||||
Permissions::MANAGE_ADMINTOOLS => true,
|
||||
Permissions::IGNORE_DOWNTIME => true,
|
||||
Permissions::SEND_PM => true,
|
||||
Permissions::READ_PM => true,
|
||||
Permissions::VIEW_OTHER_PMS => true,
|
||||
Permissions::EDIT_FEATURE => true,
|
||||
Permissions::BULK_EDIT_VOTE => true,
|
||||
Permissions::EDIT_OTHER_VOTE => true,
|
||||
Permissions::VIEW_SYSINTO => true,
|
||||
Permissions::VIEW_HELLBANNED => true,
|
||||
Permissions::PROTECTED => true,
|
||||
Permissions::EDIT_IMAGE_RATING => true,
|
||||
Permissions::BULK_EDIT_IMAGE_RATING => true,
|
||||
Permissions::VIEW_TRASH => true,
|
||||
Permissions::PERFORM_BULK_ACTIONS => true,
|
||||
Permissions::BULK_ADD => true,
|
||||
Permissions::EDIT_FILES => true,
|
||||
Permissions::EDIT_TAG_CATEGORIES => true,
|
||||
Permissions::RESCAN_MEDIA => true,
|
||||
Permissions::SEE_IMAGE_VIEW_COUNTS => true,
|
||||
Permissions::ARTISTS_ADMIN => true,
|
||||
Permissions::BLOTTER_ADMIN => true,
|
||||
Permissions::FORUM_ADMIN => true,
|
||||
Permissions::NOTES_ADMIN => true,
|
||||
Permissions::POOLS_ADMIN => true,
|
||||
Permissions::TIPS_ADMIN => true,
|
||||
Permissions::CRON_ADMIN => true,
|
||||
Permissions::APPROVE_IMAGE => true,
|
||||
Permissions::APPROVE_COMMENT => true,
|
||||
]);
|
||||
|
||||
new UserClass("hellbanned", "user", [
|
||||
Permissions::HELLBANNED => true,
|
||||
]);
|
||||
|
||||
@include_once "data/config/user-classes.conf.php";
|
1826
core/util.inc.php
1826
core/util.inc.php
File diff suppressed because it is too large
Load diff
745
core/util.php
Normal file
745
core/util.php
Normal file
|
@ -0,0 +1,745 @@
|
|||
<?php declare(strict_types=1);
|
||||
use function MicroHTML\emptyHTML;
|
||||
use function MicroHTML\rawHTML;
|
||||
use function MicroHTML\FORM;
|
||||
use function MicroHTML\INPUT;
|
||||
use function MicroHTML\DIV;
|
||||
use function MicroHTML\PRE;
|
||||
use function MicroHTML\P;
|
||||
use function MicroHTML\TABLE;
|
||||
use function MicroHTML\THEAD;
|
||||
use function MicroHTML\TFOOT;
|
||||
use function MicroHTML\TR;
|
||||
use function MicroHTML\TH;
|
||||
use function MicroHTML\TD;
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Misc *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
const DATA_DIR = "data";
|
||||
|
||||
|
||||
function mtimefile(string $file): string
|
||||
{
|
||||
$data_href = get_base_href();
|
||||
$mtime = filemtime($file);
|
||||
return "$data_href/$file?$mtime";
|
||||
}
|
||||
|
||||
function get_theme(): string
|
||||
{
|
||||
global $config;
|
||||
$theme = $config->get_string(SetupConfig::THEME, "default");
|
||||
if (!file_exists("themes/$theme")) {
|
||||
$theme = "default";
|
||||
}
|
||||
return $theme;
|
||||
}
|
||||
|
||||
function contact_link(): ?string
|
||||
{
|
||||
global $config;
|
||||
$text = $config->get_string('contact_link');
|
||||
if (is_null($text)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
startsWith($text, "http:") ||
|
||||
startsWith($text, "https:") ||
|
||||
startsWith($text, "mailto:")
|
||||
) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
if (strpos($text, "@")) {
|
||||
return "mailto:$text";
|
||||
}
|
||||
|
||||
if (strpos($text, "/")) {
|
||||
return "http://$text";
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if HTTPS is enabled for the server.
|
||||
*/
|
||||
function is_https_enabled(): bool
|
||||
{
|
||||
return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two Block objects, used to sort them before being displayed
|
||||
*/
|
||||
function blockcmp(Block $a, Block $b): int
|
||||
{
|
||||
if ($a->position == $b->position) {
|
||||
return 0;
|
||||
} else {
|
||||
return ($a->position > $b->position) ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out PHP's internal memory limit
|
||||
*/
|
||||
function get_memory_limit(): int
|
||||
{
|
||||
global $config;
|
||||
|
||||
// thumbnail generation requires lots of memory
|
||||
$default_limit = 8*1024*1024; // 8 MB of memory is PHP's default.
|
||||
$shimmie_limit = $config->get_int(MediaConfig::MEM_LIMIT);
|
||||
|
||||
if ($shimmie_limit < 3*1024*1024) {
|
||||
// we aren't going to fit, override
|
||||
$shimmie_limit = $default_limit;
|
||||
}
|
||||
|
||||
/*
|
||||
Get PHP's configured memory limit.
|
||||
Note that this is set to -1 for NO memory limit.
|
||||
|
||||
http://ca2.php.net/manual/en/ini.core.php#ini.memory-limit
|
||||
*/
|
||||
$memory = parse_shorthand_int(ini_get("memory_limit"));
|
||||
|
||||
if ($memory == -1) {
|
||||
// No memory limit.
|
||||
// Return the larger of the set limits.
|
||||
return max($shimmie_limit, $default_limit);
|
||||
} else {
|
||||
// PHP has a memory limit set.
|
||||
if ($shimmie_limit > $memory) {
|
||||
// Shimmie wants more memory than what PHP is currently set for.
|
||||
|
||||
// Attempt to set PHP's memory limit.
|
||||
if (ini_set("memory_limit", "$shimmie_limit") === false) {
|
||||
/* We can't change PHP's limit, oh well, return whatever its currently set to */
|
||||
return $memory;
|
||||
}
|
||||
$memory = parse_shorthand_int(ini_get("memory_limit"));
|
||||
}
|
||||
|
||||
// PHP's memory limit is more than Shimmie needs.
|
||||
return $memory; // return the current setting
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if PHP has the GD library installed
|
||||
*/
|
||||
function check_gd_version(): int
|
||||
{
|
||||
$gdversion = 0;
|
||||
|
||||
if (function_exists('gd_info')) {
|
||||
$gd_info = gd_info();
|
||||
if (substr_count($gd_info['GD Version'], '2.')) {
|
||||
$gdversion = 2;
|
||||
} elseif (substr_count($gd_info['GD Version'], '1.')) {
|
||||
$gdversion = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $gdversion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether ImageMagick's `convert` command
|
||||
* is installed and working
|
||||
*/
|
||||
function check_im_version(): int
|
||||
{
|
||||
$convert_check = exec("convert");
|
||||
|
||||
return (empty($convert_check) ? 0 : 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently active IP, masked to make it not change when the last
|
||||
* octet or two change, for use in session cookies and such
|
||||
*/
|
||||
function get_session_ip(Config $config): string
|
||||
{
|
||||
$mask = $config->get_string("session_hash_mask", "255.255.0.0");
|
||||
$addr = $_SERVER['REMOTE_ADDR'];
|
||||
$addr = inet_ntop(inet_pton($addr) & inet_pton($mask));
|
||||
return $addr;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A shorthand way to send a TextFormattingEvent and get the results.
|
||||
*/
|
||||
function format_text(string $string): string
|
||||
{
|
||||
$tfe = send_event(new TextFormattingEvent($string));
|
||||
return $tfe->formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the path to a file under the data folder based on the file's hash.
|
||||
* This process creates subfolders based on octet pairs from the file's hash.
|
||||
* The calculated folder follows this pattern data/$base/octet_pairs/$hash
|
||||
* @param string $base
|
||||
* @param string $hash
|
||||
* @param bool $create
|
||||
* @param int $splits The number of octet pairs to split the hash into. Caps out at strlen($hash)/2.
|
||||
* @return string
|
||||
*/
|
||||
function warehouse_path(string $base, string $hash, bool $create=true, int $splits = WH_SPLITS): string
|
||||
{
|
||||
$dirs =[DATA_DIR, $base];
|
||||
$splits = min($splits, strlen($hash) / 2);
|
||||
for ($i = 0; $i < $splits; $i++) {
|
||||
$dirs[] = substr($hash, $i * 2, 2);
|
||||
}
|
||||
$dirs[] = $hash;
|
||||
|
||||
$pa = join_path(...$dirs);
|
||||
|
||||
if ($create && !file_exists(dirname($pa))) {
|
||||
mkdir(dirname($pa), 0755, true);
|
||||
}
|
||||
return $pa;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the path to the specified file in the data folder.
|
||||
*/
|
||||
function data_path(string $filename, bool $create = true): string
|
||||
{
|
||||
$filename = join_path("data", $filename);
|
||||
if ($create&&!file_exists(dirname($filename))) {
|
||||
mkdir(dirname($filename), 0755, true);
|
||||
}
|
||||
return $filename;
|
||||
}
|
||||
|
||||
function load_balance_url(string $tmpl, string $hash, int $n=0): string
|
||||
{
|
||||
static $flexihashes = [];
|
||||
$matches = [];
|
||||
if (preg_match("/(.*){(.*)}(.*)/", $tmpl, $matches)) {
|
||||
$pre = $matches[1];
|
||||
$opts = $matches[2];
|
||||
$post = $matches[3];
|
||||
|
||||
if (isset($flexihashes[$opts])) {
|
||||
$flexihash = $flexihashes[$opts];
|
||||
} else {
|
||||
$flexihash = new Flexihash\Flexihash();
|
||||
foreach (explode(",", $opts) as $opt) {
|
||||
$parts = explode("=", $opt);
|
||||
$parts_count = count($parts);
|
||||
$opt_val = "";
|
||||
$opt_weight = 0;
|
||||
if ($parts_count === 2) {
|
||||
$opt_val = $parts[0];
|
||||
$opt_weight = $parts[1];
|
||||
} elseif ($parts_count === 1) {
|
||||
$opt_val = $parts[0];
|
||||
$opt_weight = 1;
|
||||
}
|
||||
$flexihash->addTarget($opt_val, $opt_weight);
|
||||
}
|
||||
$flexihashes[$opts] = $flexihash;
|
||||
}
|
||||
|
||||
// $choice = $flexihash->lookup($pre.$post);
|
||||
$choices = $flexihash->lookupList($hash, $n + 1); // hash doesn't change
|
||||
$choice = $choices[$n];
|
||||
$tmpl = $pre . $choice . $post;
|
||||
}
|
||||
return $tmpl;
|
||||
}
|
||||
|
||||
function transload(string $url, string $mfile): ?array
|
||||
{
|
||||
global $config;
|
||||
|
||||
if ($config->get_string("transload_engine") === "curl" && function_exists("curl_init")) {
|
||||
$ch = curl_init($url);
|
||||
$fp = fopen($mfile, "w");
|
||||
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_VERBOSE, 1);
|
||||
curl_setopt($ch, CURLOPT_HEADER, 1);
|
||||
curl_setopt($ch, CURLOPT_REFERER, $url);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, "Shimmie-".VERSION);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
if ($response === false) {
|
||||
log_warning("core-util", "Failed to transload $url");
|
||||
throw new SCoreException("Failed to fetch $url");
|
||||
}
|
||||
|
||||
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||
$headers = http_parse_headers(implode("\n", preg_split('/\R/', rtrim(substr($response, 0, $header_size)))));
|
||||
$body = substr($response, $header_size);
|
||||
|
||||
curl_close($ch);
|
||||
fwrite($fp, $body);
|
||||
fclose($fp);
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
if ($config->get_string("transload_engine") === "wget") {
|
||||
$s_url = escapeshellarg($url);
|
||||
$s_mfile = escapeshellarg($mfile);
|
||||
system("wget --no-check-certificate $s_url --output-document=$s_mfile");
|
||||
|
||||
return file_exists($mfile) ? ["ok"=>"true"] : null;
|
||||
}
|
||||
|
||||
if ($config->get_string("transload_engine") === "fopen") {
|
||||
$fp_in = @fopen($url, "r");
|
||||
$fp_out = fopen($mfile, "w");
|
||||
if (!$fp_in || !$fp_out) {
|
||||
return null;
|
||||
}
|
||||
$length = 0;
|
||||
while (!feof($fp_in) && $length <= $config->get_int('upload_size')) {
|
||||
$data = fread($fp_in, 8192);
|
||||
$length += strlen($data);
|
||||
fwrite($fp_out, $data);
|
||||
}
|
||||
fclose($fp_in);
|
||||
fclose($fp_out);
|
||||
|
||||
$headers = http_parse_headers(implode("\n", $http_response_header));
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function path_to_tags(string $path): string
|
||||
{
|
||||
$matches = [];
|
||||
$tags = [];
|
||||
if (preg_match("/\d+ - (.+)\.([a-zA-Z0-9]+)/", basename($path), $matches)) {
|
||||
$tags = explode(" ", $matches[1]);
|
||||
}
|
||||
|
||||
$path = dirname($path);
|
||||
$path = str_replace(";", ":", $path);
|
||||
$path = str_replace("__", " ", $path);
|
||||
|
||||
|
||||
$category = "";
|
||||
foreach (explode("/", $path) as $dir) {
|
||||
$category_to_inherit = "";
|
||||
foreach (explode(" ", $dir) as $tag) {
|
||||
$tag = trim($tag);
|
||||
if ($tag=="") {
|
||||
continue;
|
||||
}
|
||||
if (substr_compare($tag, ":", -1) === 0) {
|
||||
// This indicates a tag that ends in a colon,
|
||||
// which is for inheriting to tags on the subfolder
|
||||
$category_to_inherit = $tag;
|
||||
} else {
|
||||
if ($category!=""&&strpos($tag, ":") === false) {
|
||||
// This indicates that category inheritance is active,
|
||||
// and we've encountered a tag that does not specify a category.
|
||||
// So we attach the inherited category to the tag.
|
||||
$tag = $category.$tag;
|
||||
}
|
||||
$tags[] = $tag;
|
||||
}
|
||||
}
|
||||
// Category inheritance only works on the immediate subfolder,
|
||||
// so we hold a category until the next iteration, and then set
|
||||
// it back to an empty string after that iteration
|
||||
$category = $category_to_inherit;
|
||||
}
|
||||
|
||||
return implode(" ", $tags);
|
||||
}
|
||||
|
||||
|
||||
function join_url(string $base, string ...$paths)
|
||||
{
|
||||
$output = $base;
|
||||
foreach ($paths as $path) {
|
||||
$output = rtrim($output, "/");
|
||||
$path = ltrim($path, "/");
|
||||
$output .= "/".$path;
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
function get_dir_contents(string $dir): array
|
||||
{
|
||||
assert(!empty($dir));
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
return [];
|
||||
}
|
||||
return array_diff(
|
||||
scandir(
|
||||
$dir
|
||||
),
|
||||
['..', '.']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns amount of files & total size of dir.
|
||||
*/
|
||||
function scan_dir(string $path): array
|
||||
{
|
||||
$bytestotal = 0;
|
||||
$nbfiles = 0;
|
||||
|
||||
$ite = new RecursiveDirectoryIterator(
|
||||
$path,
|
||||
FilesystemIterator::KEY_AS_PATHNAME |
|
||||
FilesystemIterator::CURRENT_AS_FILEINFO |
|
||||
FilesystemIterator::SKIP_DOTS
|
||||
);
|
||||
foreach (new RecursiveIteratorIterator($ite) as $filename => $cur) {
|
||||
try {
|
||||
$filesize = $cur->getSize();
|
||||
$bytestotal += $filesize;
|
||||
$nbfiles++;
|
||||
} catch (RuntimeException $e) {
|
||||
// This usually just means that the file got eaten by the import
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$size_mb = $bytestotal / 1048576; // to mb
|
||||
$size_mb = number_format($size_mb, 2, '.', '');
|
||||
return ['path' => $path, 'total_files' => $nbfiles, 'total_mb' => $size_mb];
|
||||
}
|
||||
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Debugging functions *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
// SHIT by default this returns the time as a string. And it's not even a
|
||||
// string representation of a number, it's two numbers separated by a space.
|
||||
// What the fuck were the PHP developers smoking.
|
||||
$_shm_load_start = microtime(true);
|
||||
|
||||
/**
|
||||
* Collects some debug information (execution time, memory usage, queries, etc)
|
||||
* and formats it to stick in the footer of the page.
|
||||
*/
|
||||
function get_debug_info(): string
|
||||
{
|
||||
global $cache, $config, $_shm_event_count, $database, $_shm_load_start;
|
||||
|
||||
$i_mem = sprintf("%5.2f", ((memory_get_peak_usage(true)+512)/1024)/1024);
|
||||
|
||||
if ($config->get_string("commit_hash", "unknown") == "unknown") {
|
||||
$commit = "";
|
||||
} else {
|
||||
$commit = " (".$config->get_string("commit_hash").")";
|
||||
}
|
||||
$time = sprintf("%.2f", microtime(true) - $_shm_load_start);
|
||||
$dbtime = sprintf("%.2f", $database->dbtime);
|
||||
$i_files = count(get_included_files());
|
||||
$hits = $cache->get_hits();
|
||||
$miss = $cache->get_misses();
|
||||
|
||||
$debug = "<br>Took $time seconds (db:$dbtime) and {$i_mem}MB of RAM";
|
||||
$debug .= "; Used $i_files files and {$database->query_count} queries";
|
||||
$debug .= "; Sent $_shm_event_count events";
|
||||
$debug .= "; $hits cache hits and $miss misses";
|
||||
$debug .= "; Shimmie version ". VERSION . $commit;
|
||||
|
||||
return $debug;
|
||||
}
|
||||
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Request initialisation stuff *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/** @privatesection
|
||||
* @noinspection PhpIncludeInspection
|
||||
*/
|
||||
|
||||
function require_all(array $files): void
|
||||
{
|
||||
foreach ($files as $filename) {
|
||||
require_once $filename;
|
||||
}
|
||||
}
|
||||
|
||||
function _load_core_files()
|
||||
{
|
||||
require_all(array_merge(
|
||||
zglob("core/*.php"),
|
||||
zglob("core/imageboard/*.php"),
|
||||
zglob("ext/*/info.php")
|
||||
));
|
||||
}
|
||||
|
||||
function _load_theme_files()
|
||||
{
|
||||
require_all(_get_themelet_files(get_theme()));
|
||||
}
|
||||
|
||||
function _sanitise_environment(): void
|
||||
{
|
||||
global $tracer_enabled;
|
||||
|
||||
$min_php = "7.3";
|
||||
if (version_compare(phpversion(), $min_php, ">=") === false) {
|
||||
print "
|
||||
Shimmie does not support versions of PHP lower than $min_php
|
||||
(PHP reports that it is version ".phpversion().").
|
||||
If your web host is running an older version, they are dangerously out of
|
||||
date and you should plan on moving elsewhere.
|
||||
";
|
||||
exit;
|
||||
}
|
||||
|
||||
if (file_exists("images") && !file_exists("data/images")) {
|
||||
die("As of Shimmie 2.7 images and thumbs should be moved to data/images and data/thumbs");
|
||||
}
|
||||
|
||||
if (TIMEZONE) {
|
||||
date_default_timezone_set(TIMEZONE);
|
||||
}
|
||||
|
||||
# ini_set('zend.assertions', '1'); // generate assertions
|
||||
ini_set('assert.exception', '1'); // throw exceptions when failed
|
||||
if (DEBUG) {
|
||||
error_reporting(E_ALL);
|
||||
}
|
||||
|
||||
// The trace system has a certain amount of memory consumption every time it is used,
|
||||
// so to prevent running out of memory during complex operations code that uses it should
|
||||
// check if tracer output is enabled before making use of it.
|
||||
$tracer_enabled = constant('TRACE_FILE')!==null;
|
||||
|
||||
ob_start();
|
||||
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI == 'phpdbg') {
|
||||
if (isset($_SERVER['REMOTE_ADDR'])) {
|
||||
die("CLI with remote addr? Confused, not taking the risk.");
|
||||
}
|
||||
$_SERVER['REMOTE_ADDR'] = "0.0.0.0";
|
||||
$_SERVER['HTTP_HOST'] = "<cli command>";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function _get_themelet_files(string $_theme): array
|
||||
{
|
||||
$base_themelets = [];
|
||||
$base_themelets[] = 'themes/'.$_theme.'/page.class.php';
|
||||
$base_themelets[] = 'themes/'.$_theme.'/themelet.class.php';
|
||||
|
||||
$ext_themelets = zglob("ext/{".Extension::get_enabled_extensions_as_string()."}/theme.php");
|
||||
$custom_themelets = zglob('themes/'.$_theme.'/{'.Extension::get_enabled_extensions_as_string().'}.theme.php');
|
||||
|
||||
return array_merge($base_themelets, $ext_themelets, $custom_themelets);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Used to display fatal errors to the web user.
|
||||
* @noinspection PhpPossiblePolymorphicInvocationInspection
|
||||
*/
|
||||
function _fatal_error(Exception $e): void
|
||||
{
|
||||
$version = VERSION;
|
||||
$message = $e->getMessage();
|
||||
$phpver = phpversion();
|
||||
$query = is_subclass_of($e, "SCoreException") ? $e->query : null;
|
||||
|
||||
//$hash = exec("git rev-parse HEAD");
|
||||
//$h_hash = $hash ? "<p><b>Hash:</b> $hash" : "";
|
||||
//'.$h_hash.'
|
||||
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI == 'phpdbg') {
|
||||
print("Trace: ");
|
||||
$t = array_reverse($e->getTrace());
|
||||
foreach ($t as $n => $f) {
|
||||
$c = $f['class'] ?? '';
|
||||
$t = $f['type'] ?? '';
|
||||
$a = implode(", ", array_map("stringer", $f['args']));
|
||||
print("$n: {$f['file']}({$f['line']}): {$c}{$t}{$f['function']}({$a})\n");
|
||||
}
|
||||
|
||||
print("Message: $message\n");
|
||||
|
||||
if ($query) {
|
||||
print("Query: {$query}\n");
|
||||
}
|
||||
|
||||
print("Version: $version (on $phpver)\n");
|
||||
} else {
|
||||
$q = $query ? "" : "<p><b>Query:</b> " . html_escape($query);
|
||||
header("HTTP/1.0 500 Internal Error");
|
||||
echo '
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Internal error - SCore-'.$version.'</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Internal Error</h1>
|
||||
<p><b>Message:</b> '.html_escape($message).'
|
||||
'.$q.'
|
||||
<p><b>Version:</b> '.$version.' (on '.$phpver.')
|
||||
</body>
|
||||
</html>
|
||||
';
|
||||
}
|
||||
}
|
||||
|
||||
function _get_user(): User
|
||||
{
|
||||
global $config, $page;
|
||||
$my_user = null;
|
||||
if ($page->get_cookie("user") && $page->get_cookie("session")) {
|
||||
$tmp_user = User::by_session($page->get_cookie("user"), $page->get_cookie("session"));
|
||||
if (!is_null($tmp_user)) {
|
||||
$my_user = $tmp_user;
|
||||
}
|
||||
}
|
||||
if (is_null($my_user)) {
|
||||
$my_user = User::by_id($config->get_int("anon_id", 0));
|
||||
}
|
||||
assert(!is_null($my_user));
|
||||
|
||||
return $my_user;
|
||||
}
|
||||
|
||||
function _get_query(): string
|
||||
{
|
||||
return (@$_POST["q"]?:@$_GET["q"])?:"/";
|
||||
}
|
||||
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* HTML Generation *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* Give a HTML string which shows an IP (if the user is allowed to see IPs),
|
||||
* and a link to ban that IP (if the user is allowed to ban IPs)
|
||||
*
|
||||
* FIXME: also check that IP ban ext is installed
|
||||
*/
|
||||
function show_ip(string $ip, string $ban_reason): string
|
||||
{
|
||||
global $user;
|
||||
$u_reason = url_escape($ban_reason);
|
||||
$u_end = url_escape("+1 week");
|
||||
$ban = $user->can(Permissions::BAN_IP) ? ", <a href='".make_link("ip_ban/list", "c_ip=$ip&c_reason=$u_reason&c_expires=$u_end#create")."'>Ban</a>" : "";
|
||||
$ip = $user->can(Permissions::VIEW_IP) ? $ip.$ban : "";
|
||||
return $ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a form tag with relevant auth token and stuff
|
||||
*/
|
||||
function make_form(string $target, string $method="POST", bool $multipart=false, string $form_id="", string $onsubmit=""): string
|
||||
{
|
||||
global $user;
|
||||
if ($method == "GET") {
|
||||
$link = html_escape($target);
|
||||
$target = make_link($target);
|
||||
$extra_inputs = "<input type='hidden' name='q' value='$link'>";
|
||||
} else {
|
||||
$extra_inputs = $user->get_auth_html();
|
||||
}
|
||||
|
||||
$extra = empty($form_id) ? '' : 'id="'. $form_id .'"';
|
||||
if ($multipart) {
|
||||
$extra .= " enctype='multipart/form-data'";
|
||||
}
|
||||
if ($onsubmit) {
|
||||
$extra .= ' onsubmit="'.$onsubmit.'"';
|
||||
}
|
||||
return '<form action="'.$target.'" method="'.$method.'" '.$extra.'>'.$extra_inputs;
|
||||
}
|
||||
|
||||
function SHM_FORM(string $target, string $method="POST", bool $multipart=false, string $form_id="", string $onsubmit="")
|
||||
{
|
||||
global $user;
|
||||
|
||||
$attrs = [
|
||||
"action"=>$target,
|
||||
"method"=>$method
|
||||
];
|
||||
|
||||
if ($form_id) {
|
||||
$attrs["id"] = $form_id;
|
||||
}
|
||||
if ($multipart) {
|
||||
$attrs["enctype"] = 'multipart/form-data';
|
||||
}
|
||||
if ($onsubmit) {
|
||||
$attrs["onsubmit"] = $onsubmit;
|
||||
}
|
||||
return FORM(
|
||||
$attrs,
|
||||
INPUT(["type"=>"hidden", "name"=>"q", "value"=>$target]),
|
||||
$method == "GET" ? "" : rawHTML($user->get_auth_html())
|
||||
);
|
||||
}
|
||||
|
||||
function SHM_SIMPLE_FORM($target, ...$children)
|
||||
{
|
||||
$form = SHM_FORM($target);
|
||||
$form->appendChild(emptyHTML(...$children));
|
||||
return $form;
|
||||
}
|
||||
|
||||
function SHM_SUBMIT(string $text)
|
||||
{
|
||||
return INPUT(["type"=>"submit", "value"=>$text]);
|
||||
}
|
||||
|
||||
function SHM_COMMAND_EXAMPLE(string $ex, string $desc)
|
||||
{
|
||||
return DIV(
|
||||
["class"=>"command_example"],
|
||||
PRE($ex),
|
||||
P($desc)
|
||||
);
|
||||
}
|
||||
|
||||
function SHM_USER_FORM(User $duser, string $target, string $title, $body, $foot)
|
||||
{
|
||||
if (is_string($foot)) {
|
||||
$foot = TFOOT(TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>$foot]))));
|
||||
}
|
||||
return SHM_SIMPLE_FORM(
|
||||
$target,
|
||||
P(
|
||||
INPUT(["type"=>'hidden', "name"=>'id', "value"=>$duser->id]),
|
||||
TABLE(
|
||||
["class"=>"form"],
|
||||
THEAD(TR(TH(["colspan"=>"2"], $title))),
|
||||
$body,
|
||||
$foot
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const BYTE_DENOMINATIONS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
function human_filesize(int $bytes, $decimals = 2)
|
||||
{
|
||||
$factor = floor((strlen(strval($bytes)) - 1) / 3);
|
||||
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @BYTE_DENOMINATIONS[$factor];
|
||||
}
|
23
ext/admin/info.php
Normal file
23
ext/admin/info.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
class AdminPageInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "admin";
|
||||
|
||||
public $key = self::KEY;
|
||||
public $name = "Admin Controls";
|
||||
public $url = self::SHIMMIE_URL;
|
||||
public $authors = self::SHISH_AUTHOR;
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $description = "Various things to make admins' lives easier";
|
||||
public $documentation =
|
||||
"Various moderate-level tools for admins; for advanced, obscure, and possibly dangerous tools see the shimmie2-utils script set
|
||||
<p>Lowercase all tags:
|
||||
<br>Set all tags to lowercase for consistency
|
||||
<p>Recount tag use:
|
||||
<br>If the counts of images per tag get messed up somehow, this will reset them, and remove any unused tags
|
||||
<p>Database dump:
|
||||
<br>Download the contents of the database in plain text format, useful for backups.
|
||||
<p>Image dump:
|
||||
<br>Download all the images as a .zip file (Requires ZipArchive)";
|
||||
}
|
|
@ -1,78 +1,62 @@
|
|||
<?php
|
||||
/**
|
||||
* Name: Admin Controls
|
||||
* Author: Shish <webmaster@shishnet.org>
|
||||
* Link: http://code.shishnet.org/shimmie2/
|
||||
* License: GPLv2
|
||||
* Description: Various things to make admins' lives easier
|
||||
* Documentation:
|
||||
* Various moderate-level tools for admins; for advanced, obscure, and
|
||||
* possibly dangerous tools see the shimmie2-utils script set
|
||||
* <p>Lowercase all tags:
|
||||
* <br>Set all tags to lowercase for consistency
|
||||
* <p>Recount tag use:
|
||||
* <br>If the counts of images per tag get messed up somehow, this will
|
||||
* reset them, and remove any unused tags
|
||||
* <p>Database dump:
|
||||
* <br>Download the contents of the database in plain text format, useful
|
||||
* for backups.
|
||||
* <p>Image dump:
|
||||
* <br>Download all the images as a .zip file (Requires ZipArchive)
|
||||
*/
|
||||
<?php /** @noinspection PhpUnusedPrivateMethodInspection */
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Sent when the admin page is ready to be added to
|
||||
*/
|
||||
class AdminBuildingEvent extends Event {
|
||||
/** @var \Page */
|
||||
class AdminBuildingEvent extends Event
|
||||
{
|
||||
/** @var Page */
|
||||
public $page;
|
||||
|
||||
/**
|
||||
* @param Page $page
|
||||
*/
|
||||
public function __construct(Page $page) {
|
||||
public function __construct(Page $page)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->page = $page;
|
||||
}
|
||||
}
|
||||
|
||||
class AdminActionEvent extends Event {
|
||||
class AdminActionEvent extends Event
|
||||
{
|
||||
/** @var string */
|
||||
public $action;
|
||||
/** @var bool */
|
||||
public $redirect = true;
|
||||
|
||||
/**
|
||||
* @param string $action
|
||||
*/
|
||||
public function __construct(/*string*/ $action) {
|
||||
public function __construct(string $action)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->action = $action;
|
||||
}
|
||||
}
|
||||
|
||||
class AdminPage extends Extension {
|
||||
public function onPageRequest(PageRequestEvent $event) {
|
||||
class AdminPage extends Extension
|
||||
{
|
||||
/** @var AdminPageTheme */
|
||||
protected $theme;
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $page, $user;
|
||||
|
||||
if($event->page_matches("admin")) {
|
||||
if(!$user->can("manage_admintools")) {
|
||||
if ($event->page_matches("admin")) {
|
||||
if (!$user->can(Permissions::MANAGE_ADMINTOOLS)) {
|
||||
$this->theme->display_permission_denied();
|
||||
}
|
||||
else {
|
||||
if($event->count_args() == 0) {
|
||||
} else {
|
||||
if ($event->count_args() == 0) {
|
||||
send_event(new AdminBuildingEvent($page));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$action = $event->get_arg(0);
|
||||
$aae = new AdminActionEvent($action);
|
||||
|
||||
if($user->check_auth_token()) {
|
||||
if ($user->check_auth_token()) {
|
||||
log_info("admin", "Util: $action");
|
||||
set_time_limit(0);
|
||||
send_event($aae);
|
||||
}
|
||||
|
||||
if($aae->redirect) {
|
||||
$page->set_mode("redirect");
|
||||
if ($aae->redirect) {
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("admin"));
|
||||
}
|
||||
}
|
||||
|
@ -80,83 +64,121 @@ class AdminPage extends Extension {
|
|||
}
|
||||
}
|
||||
|
||||
public function onCommand(CommandEvent $event) {
|
||||
if($event->cmd == "help") {
|
||||
print " get-page [query string]\n";
|
||||
print " eg 'get-page post/list'\n\n";
|
||||
public function onCommand(CommandEvent $event)
|
||||
{
|
||||
if ($event->cmd == "help") {
|
||||
print "\tget-page <query string>\n";
|
||||
print "\t\teg 'get-page post/list'\n\n";
|
||||
print "\tpost-page <query string> <urlencoded params>\n";
|
||||
print "\t\teg 'post-page ip_ban/delete id=1'\n\n";
|
||||
print "\tget-token\n";
|
||||
print "\t\tget a CSRF auth token\n\n";
|
||||
print "\tregen-thumb <id / hash>\n";
|
||||
print "\t\tregenerate a thumbnail\n\n";
|
||||
print "\tcache [get|set|del] [key] <value>\n";
|
||||
print "\t\teg 'cache get config'\n\n";
|
||||
}
|
||||
if($event->cmd == "get-page") {
|
||||
if ($event->cmd == "get-page") {
|
||||
global $page;
|
||||
if (isset($event->args[1])) {
|
||||
parse_str($event->args[1], $_GET);
|
||||
}
|
||||
send_event(new PageRequestEvent($event->args[0]));
|
||||
$page->display();
|
||||
}
|
||||
if ($event->cmd == "post-page") {
|
||||
global $page;
|
||||
$_SERVER['REQUEST_METHOD'] = "POST";
|
||||
if (isset($event->args[1])) {
|
||||
parse_str($event->args[1], $_POST);
|
||||
}
|
||||
send_event(new PageRequestEvent($event->args[0]));
|
||||
$page->display();
|
||||
}
|
||||
if ($event->cmd == "get-token") {
|
||||
global $user;
|
||||
print($user->get_auth_token());
|
||||
}
|
||||
if ($event->cmd == "regen-thumb") {
|
||||
$uid = $event->args[0];
|
||||
$image = Image::by_id_or_hash($uid);
|
||||
if ($image) {
|
||||
send_event(new ThumbnailGenerationEvent($image->hash, $image->ext, true));
|
||||
} else {
|
||||
print("No post with ID '$uid'\n");
|
||||
}
|
||||
}
|
||||
if ($event->cmd == "cache") {
|
||||
global $cache;
|
||||
$cmd = $event->args[0];
|
||||
$key = $event->args[1];
|
||||
switch ($cmd) {
|
||||
case "get":
|
||||
var_dump($cache->get($key));
|
||||
break;
|
||||
case "set":
|
||||
$cache->set($key, $event->args[2], 60);
|
||||
break;
|
||||
case "del":
|
||||
$cache->delete($key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onAdminBuilding(AdminBuildingEvent $event) {
|
||||
public function onAdminBuilding(AdminBuildingEvent $event)
|
||||
{
|
||||
$this->theme->display_page();
|
||||
$this->theme->display_form();
|
||||
}
|
||||
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event) {
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if($user->can("manage_admintools")) {
|
||||
if ($event->parent==="system") {
|
||||
if ($user->can(Permissions::MANAGE_ADMINTOOLS)) {
|
||||
$event->add_nav_link("admin", new Link('admin'), "Board Admin");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if ($user->can(Permissions::MANAGE_ADMINTOOLS)) {
|
||||
$event->add_link("Board Admin", make_link("admin"));
|
||||
}
|
||||
}
|
||||
|
||||
public function onAdminAction(AdminActionEvent $event) {
|
||||
public function onAdminAction(AdminActionEvent $event)
|
||||
{
|
||||
$action = $event->action;
|
||||
if(method_exists($this, $action)) {
|
||||
if (method_exists($this, $action)) {
|
||||
$event->redirect = $this->$action();
|
||||
}
|
||||
}
|
||||
|
||||
public function onPostListBuilding(PostListBuildingEvent $event) {
|
||||
global $user;
|
||||
if($user->can("manage_admintools") && !empty($event->search_terms)) {
|
||||
$event->add_control($this->theme->dbq_html(implode(" ", $event->search_terms)));
|
||||
}
|
||||
}
|
||||
|
||||
private function delete_by_query() {
|
||||
global $page;
|
||||
$query = $_POST['query'];
|
||||
$reason = @$_POST['reason'];
|
||||
assert(strlen($query) > 1);
|
||||
|
||||
log_warning("admin", "Mass deleting: $query");
|
||||
$count = 0;
|
||||
foreach(Image::find_images(0, 1000000, Tag::explode($query)) as $image) {
|
||||
if($reason && class_exists("ImageBan")) {
|
||||
send_event(new AddImageHashBanEvent($image->hash, $reason));
|
||||
}
|
||||
send_event(new ImageDeletionEvent($image));
|
||||
$count++;
|
||||
}
|
||||
log_debug("admin", "Deleted $count images", true);
|
||||
|
||||
$page->set_mode("redirect");
|
||||
$page->set_redirect(make_link("post/list"));
|
||||
return false;
|
||||
}
|
||||
|
||||
private function set_tag_case() {
|
||||
private function set_tag_case()
|
||||
{
|
||||
global $database;
|
||||
$database->execute($database->scoreql_to_sql(
|
||||
"UPDATE tags SET tag=:tag1 WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag2)"
|
||||
), array("tag1" => $_POST['tag'], "tag2" => $_POST['tag']));
|
||||
log_info("admin", "Fixed the case of ".html_escape($_POST['tag']), true);
|
||||
$database->execute(
|
||||
"UPDATE tags SET tag=:tag1 WHERE LOWER(tag) = LOWER(:tag2)",
|
||||
["tag1" => $_POST['tag'], "tag2" => $_POST['tag']]
|
||||
);
|
||||
log_info("admin", "Fixed the case of {$_POST['tag']}", "Fixed case");
|
||||
return true;
|
||||
}
|
||||
|
||||
private function lowercase_all_tags() {
|
||||
private function lowercase_all_tags()
|
||||
{
|
||||
global $database;
|
||||
$database->execute("UPDATE tags SET tag=lower(tag)");
|
||||
log_warning("admin", "Set all tags to lowercase", true);
|
||||
log_warning("admin", "Set all tags to lowercase", "Set all tags to lowercase");
|
||||
return true;
|
||||
}
|
||||
|
||||
private function recount_tag_use() {
|
||||
private function recount_tag_use()
|
||||
{
|
||||
global $database;
|
||||
$database->Execute("
|
||||
UPDATE tags
|
||||
|
@ -166,106 +188,7 @@ class AdminPage extends Extension {
|
|||
)
|
||||
");
|
||||
$database->Execute("DELETE FROM tags WHERE count=0");
|
||||
log_warning("admin", "Re-counted tags", true);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private function database_dump() {
|
||||
global $page;
|
||||
|
||||
$matches = array();
|
||||
preg_match("#^(?P<proto>\w+)\:(?:user=(?P<user>\w+)(?:;|$)|password=(?P<password>\w*)(?:;|$)|host=(?P<host>[\w\.\-]+)(?:;|$)|dbname=(?P<dbname>[\w_]+)(?:;|$))+#", DATABASE_DSN, $matches);
|
||||
$software = $matches['proto'];
|
||||
$username = $matches['user'];
|
||||
$password = $matches['password'];
|
||||
$hostname = $matches['host'];
|
||||
$database = $matches['dbname'];
|
||||
|
||||
switch($software) {
|
||||
case 'mysql':
|
||||
$cmd = "mysqldump -h$hostname -u$username -p$password $database";
|
||||
break;
|
||||
case 'pgsql':
|
||||
putenv("PGPASSWORD=$password");
|
||||
$cmd = "pg_dump -h $hostname -U $username $database";
|
||||
break;
|
||||
case 'sqlite':
|
||||
$cmd = "sqlite3 $database .dump";
|
||||
break;
|
||||
default:
|
||||
$cmd = false;
|
||||
}
|
||||
|
||||
//FIXME: .SQL dump is empty if cmd doesn't exist
|
||||
|
||||
if($cmd) {
|
||||
$page->set_mode("data");
|
||||
$page->set_type("application/x-unknown");
|
||||
$page->set_filename('shimmie-'.date('Ymd').'.sql');
|
||||
$page->set_data(shell_exec($cmd));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function download_all_images() {
|
||||
global $database, $page;
|
||||
|
||||
$images = $database->get_all("SELECT hash, ext FROM images");
|
||||
$filename = data_path('imgdump-'.date('Ymd').'.zip');
|
||||
|
||||
$zip = new ZipArchive;
|
||||
if($zip->open($filename, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE) === TRUE){
|
||||
foreach($images as $img){
|
||||
$img_loc = warehouse_path("images", $img["hash"], FALSE);
|
||||
$zip->addFile($img_loc, $img["hash"].".".$img["ext"]);
|
||||
}
|
||||
$zip->close();
|
||||
}
|
||||
|
||||
$page->set_mode("redirect");
|
||||
$page->set_redirect(make_link($filename)); //TODO: Delete file after downloaded?
|
||||
|
||||
return false; // we do want a redirect, but a manual one
|
||||
}
|
||||
|
||||
private function reset_image_ids() {
|
||||
global $database;
|
||||
|
||||
//TODO: Make work with PostgreSQL + SQLite
|
||||
//TODO: Update score_log (Having an optional ID column for score_log would be nice..)
|
||||
preg_match("#^(?P<proto>\w+)\:(?:user=(?P<user>\w+)(?:;|$)|password=(?P<password>\w*)(?:;|$)|host=(?P<host>[\w\.\-]+)(?:;|$)|dbname=(?P<dbname>[\w_]+)(?:;|$))+#", DATABASE_DSN, $matches);
|
||||
|
||||
if($matches['proto'] == "mysql"){
|
||||
$tables = $database->get_col("SELECT TABLE_NAME
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = :db
|
||||
AND REFERENCED_COLUMN_NAME = 'id'
|
||||
AND REFERENCED_TABLE_NAME = 'images'", array("db" => $matches['dbname']));
|
||||
|
||||
$i = 1;
|
||||
$ids = $database->get_col("SELECT id FROM images ORDER BY images.id ASC");
|
||||
foreach($ids as $id){
|
||||
$sql = "SET FOREIGN_KEY_CHECKS=0;
|
||||
UPDATE images SET id={$i} WHERE image_id={$id};";
|
||||
|
||||
foreach($tables as $table){
|
||||
$sql .= "UPDATE {$table} SET image_id={$i} WHERE image_id={$id};";
|
||||
}
|
||||
|
||||
$sql .= " SET FOREIGN_KEY_CHECKS=1;";
|
||||
$database->execute($sql);
|
||||
|
||||
$i++;
|
||||
}
|
||||
$database->execute("ALTER TABLE images AUTO_INCREMENT=".(count($ids) + 1));
|
||||
}elseif($matches['proto'] == "pgsql"){
|
||||
//TODO: Make this work with PostgreSQL
|
||||
}elseif($matches['proto'] == "sqlite"){
|
||||
//TODO: Make this work with SQLite
|
||||
}
|
||||
log_warning("admin", "Re-counted tags", "Re-counted tags");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,84 +1,89 @@
|
|||
<?php
|
||||
class AdminPageTest extends ShimmiePHPUnitTestCase {
|
||||
public function testAuth() {
|
||||
$this->get_page('admin');
|
||||
$this->assert_response(403);
|
||||
$this->assert_title("Permission Denied");
|
||||
<?php declare(strict_types=1);
|
||||
class AdminPageTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testAuth()
|
||||
{
|
||||
send_event(new UserLoginEvent(User::by_name(self::$anon_name)));
|
||||
$page = $this->get_page('admin');
|
||||
$this->assertEquals(403, $page->code);
|
||||
$this->assertEquals("Permission Denied", $page->title);
|
||||
|
||||
$this->log_in_as_user();
|
||||
$this->get_page('admin');
|
||||
$this->assert_response(403);
|
||||
$this->assert_title("Permission Denied");
|
||||
send_event(new UserLoginEvent(User::by_name(self::$user_name)));
|
||||
$page = $this->get_page('admin');
|
||||
$this->assertEquals(403, $page->code);
|
||||
$this->assertEquals("Permission Denied", $page->title);
|
||||
|
||||
$this->log_in_as_admin();
|
||||
$this->get_page('admin');
|
||||
$this->assert_response(200);
|
||||
$this->assert_title("Admin Tools");
|
||||
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
|
||||
$page = $this->get_page('admin');
|
||||
$this->assertEquals(200, $page->code);
|
||||
$this->assertEquals("Admin Tools", $page->title);
|
||||
}
|
||||
|
||||
public function testLowercase() {
|
||||
public function testLowercaseAndSetCase()
|
||||
{
|
||||
// Create a problem
|
||||
$ts = time(); // we need a tag that hasn't been used before
|
||||
|
||||
$this->log_in_as_admin();
|
||||
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
|
||||
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "TeStCase$ts");
|
||||
|
||||
$this->get_page("post/view/$image_id_1");
|
||||
$this->assert_title("Image $image_id_1: TeStCase$ts");
|
||||
// Validate problem
|
||||
$page = $this->get_page("post/view/$image_id_1");
|
||||
$this->assertEquals("Image $image_id_1: TeStCase$ts", $page->title);
|
||||
|
||||
$this->get_page('admin');
|
||||
$this->assert_title("Admin Tools");
|
||||
//$this->click("All tags to lowercase");
|
||||
// Fix
|
||||
send_event(new AdminActionEvent('lowercase_all_tags'));
|
||||
|
||||
// Validate fix
|
||||
$this->get_page("post/view/$image_id_1");
|
||||
$this->assert_title("Image $image_id_1: testcase$ts");
|
||||
|
||||
$this->delete_image($image_id_1);
|
||||
// Change
|
||||
$_POST["tag"] = "TestCase$ts";
|
||||
send_event(new AdminActionEvent('set_tag_case'));
|
||||
|
||||
// Validate change
|
||||
$this->get_page("post/view/$image_id_1");
|
||||
$this->assert_title("Image $image_id_1: TestCase$ts");
|
||||
}
|
||||
|
||||
# FIXME: make sure the admin tools actually work
|
||||
public function testRecount() {
|
||||
$this->log_in_as_admin();
|
||||
$this->get_page('admin');
|
||||
$this->assert_title("Admin Tools");
|
||||
public function testRecount()
|
||||
{
|
||||
global $database;
|
||||
|
||||
//$this->click("Recount tag use");
|
||||
// Create a problem
|
||||
$ts = time(); // we need a tag that hasn't been used before
|
||||
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
|
||||
$database->execute(
|
||||
"INSERT INTO tags(tag, count) VALUES(:tag, :count)",
|
||||
["tag"=>"tes$ts", "count"=>42]
|
||||
);
|
||||
|
||||
// Fix
|
||||
send_event(new AdminActionEvent('recount_tag_use'));
|
||||
|
||||
// Validate fix
|
||||
$this->assertEquals(
|
||||
0,
|
||||
$database->get_one(
|
||||
"SELECT count FROM tags WHERE tag = :tag",
|
||||
["tag"=>"tes$ts"]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function testDump() {
|
||||
$this->log_in_as_admin();
|
||||
$this->get_page('admin');
|
||||
$this->assert_title("Admin Tools");
|
||||
public function testCommands()
|
||||
{
|
||||
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
|
||||
ob_start();
|
||||
send_event(new CommandEvent(["index.php", "help"]));
|
||||
send_event(new CommandEvent(["index.php", "get-page", "post/list"]));
|
||||
send_event(new CommandEvent(["index.php", "post-page", "post/list", "foo=bar"]));
|
||||
send_event(new CommandEvent(["index.php", "get-token"]));
|
||||
send_event(new CommandEvent(["index.php", "regen-thumb", "42"]));
|
||||
ob_end_clean();
|
||||
|
||||
// this calls mysqldump which jams up travis prompting for a password
|
||||
//$this->click("Download database contents");
|
||||
//send_event(new AdminActionEvent('database_dump'));
|
||||
//$this->assert_response(200);
|
||||
}
|
||||
|
||||
public function testDBQ() {
|
||||
$this->log_in_as_user();
|
||||
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "test");
|
||||
$image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "test2");
|
||||
$image_id_3 = $this->post_image("tests/favicon.png", "test");
|
||||
|
||||
$this->get_page("post/list/test/1");
|
||||
//$this->click("Delete All These Images");
|
||||
$_POST['query'] = 'test';
|
||||
//$_POST['reason'] = 'reason'; // non-null-reason = add a hash ban
|
||||
send_event(new AdminActionEvent('delete_by_query'));
|
||||
|
||||
$this->get_page("post/view/$image_id_1");
|
||||
$this->assert_response(404);
|
||||
$this->get_page("post/view/$image_id_2");
|
||||
$this->assert_response(200);
|
||||
$this->get_page("post/view/$image_id_3");
|
||||
$this->assert_response(404);
|
||||
|
||||
$this->delete_image($image_id_1);
|
||||
$this->delete_image($image_id_2);
|
||||
$this->delete_image($image_id_3);
|
||||
// don't crash
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
use function MicroHTML\INPUT;
|
||||
|
||||
class AdminPageTheme extends Themelet {
|
||||
class AdminPageTheme extends Themelet
|
||||
{
|
||||
/*
|
||||
* Show the basics of a page, for other extensions to add to
|
||||
*/
|
||||
public function display_page() {
|
||||
public function display_page()
|
||||
{
|
||||
global $page;
|
||||
|
||||
$page->set_title("Admin Tools");
|
||||
|
@ -12,20 +15,14 @@ class AdminPageTheme extends Themelet {
|
|||
$page->add_block(new NavBlock());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param string $action
|
||||
* @param bool $protected
|
||||
* @return string
|
||||
*/
|
||||
protected function button(/*string*/ $name, /*string*/ $action, /*boolean*/ $protected=false) {
|
||||
protected function button(string $name, string $action, bool $protected=false): string
|
||||
{
|
||||
$c_protected = $protected ? " protected" : "";
|
||||
$html = make_form(make_link("admin/$action"), "POST", false, "admin$c_protected");
|
||||
if($protected) {
|
||||
if ($protected) {
|
||||
$html .= "<input type='submit' id='$action' value='$name' disabled='disabled'>";
|
||||
$html .= "<input type='checkbox' onclick='$(\"#$action\").attr(\"disabled\", !$(this).is(\":checked\"))'>";
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$html .= "<input type='submit' id='$action' value='$name'>";
|
||||
}
|
||||
$html .= "</form>\n";
|
||||
|
@ -38,40 +35,20 @@ class AdminPageTheme extends Themelet {
|
|||
* 'recount tag use'
|
||||
* etc
|
||||
*/
|
||||
public function display_form() {
|
||||
global $page, $database;
|
||||
public function display_form()
|
||||
{
|
||||
global $page;
|
||||
|
||||
$html = "";
|
||||
$html .= $this->button("All tags to lowercase", "lowercase_all_tags", true);
|
||||
$html .= $this->button("Recount tag use", "recount_tag_use", false);
|
||||
if(class_exists('ZipArchive'))
|
||||
$html .= $this->button("Download all images", "download_all_images", false);
|
||||
$html .= $this->button("Download database contents", "database_dump", false);
|
||||
if($database->get_driver_name() == "mysql")
|
||||
$html .= $this->button("Reset image IDs", "reset_image_ids", true);
|
||||
$page->add_block(new Block("Misc Admin Tools", $html));
|
||||
|
||||
$html = make_form(make_link("admin/set_tag_case"), "POST");
|
||||
$html .= "<input type='text' name='tag' placeholder='Enter tag with correct case' class='autocomplete_tags' autocomplete='off'>";
|
||||
$html .= "<input type='submit' value='Set Tag Case'>";
|
||||
$html .= "</form>\n";
|
||||
$html = (string)SHM_SIMPLE_FORM(
|
||||
"admin/set_tag_case",
|
||||
INPUT(["type"=>'text', "name"=>'tag', "placeholder"=>'Enter tag with correct case', "class"=>'autocomplete_tags', "autocomplete"=>'off']),
|
||||
SHM_SUBMIT('Set Tag Case'),
|
||||
);
|
||||
$page->add_block(new Block("Set Tag Case", $html));
|
||||
}
|
||||
|
||||
public function dbq_html($terms) {
|
||||
$h_terms = html_escape($terms);
|
||||
$h_reason = "";
|
||||
if(class_exists("ImageBan")) {
|
||||
$h_reason = "<input type='text' name='reason' placeholder='Ban reason (leave blank to not ban)'>";
|
||||
}
|
||||
$html = make_form(make_link("admin/delete_by_query"), "POST") . "
|
||||
<input type='button' class='shm-unlocker' data-unlock-sel='#dbqsubmit' value='Unlock'>
|
||||
<input type='hidden' name='query' value='$h_terms'>
|
||||
$h_reason
|
||||
<input type='submit' id='dbqsubmit' disabled='true' value='Delete All These Images'>
|
||||
</form>
|
||||
";
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
15
ext/alias_editor/info.php
Normal file
15
ext/alias_editor/info.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
class AliasEditorInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "alias_editor";
|
||||
|
||||
public $key = self::KEY;
|
||||
public $name = "Alias Editor";
|
||||
public $url = self::SHIMMIE_URL;
|
||||
public $authors = self::SHISH_AUTHOR;
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $description = "Edit the alias list";
|
||||
public $documentation = 'The list is visible at <a href="$site/alias/list">/alias/list</a>; only site admins can edit it, other people can view and download it';
|
||||
public $core = true;
|
||||
}
|
|
@ -1,167 +1,198 @@
|
|||
<?php
|
||||
/**
|
||||
* Name: Alias Editor
|
||||
* Author: Shish <webmaster@shishnet.org>
|
||||
* Link: http://code.shishnet.org/shimmie2/
|
||||
* License: GPLv2
|
||||
* Description: Edit the alias list
|
||||
* Documentation:
|
||||
* The list is visible at <a href="$site/alias/list">/alias/list</a>; only
|
||||
* site admins can edit it, other people can view and download it
|
||||
*/
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
class AddAliasEvent extends Event {
|
||||
use MicroCRUD\ActionColumn;
|
||||
use MicroCRUD\TextColumn;
|
||||
use MicroCRUD\Table;
|
||||
|
||||
class AliasTable extends Table
|
||||
{
|
||||
public function __construct(\FFSPHP\PDO $db)
|
||||
{
|
||||
parent::__construct($db);
|
||||
$this->table = "aliases";
|
||||
$this->base_query = "SELECT * FROM aliases";
|
||||
$this->primary_key = "oldtag";
|
||||
$this->size = 100;
|
||||
$this->limit = 1000000;
|
||||
$this->set_columns([
|
||||
new TextColumn("oldtag", "Old Tag"),
|
||||
new TextColumn("newtag", "New Tag"),
|
||||
new ActionColumn("oldtag"),
|
||||
]);
|
||||
$this->order_by = ["oldtag"];
|
||||
$this->table_attrs = ["class" => "zebra"];
|
||||
}
|
||||
}
|
||||
|
||||
class AddAliasEvent extends Event
|
||||
{
|
||||
/** @var string */
|
||||
public $oldtag;
|
||||
/** @var string */
|
||||
public $newtag;
|
||||
|
||||
/**
|
||||
* @param string $oldtag
|
||||
* @param string $newtag
|
||||
*/
|
||||
public function __construct($oldtag, $newtag) {
|
||||
public function __construct(string $oldtag, string $newtag)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->oldtag = trim($oldtag);
|
||||
$this->newtag = trim($newtag);
|
||||
}
|
||||
}
|
||||
|
||||
class AddAliasException extends SCoreException {}
|
||||
class DeleteAliasEvent extends Event
|
||||
{
|
||||
public $oldtag;
|
||||
|
||||
class AliasEditor extends Extension {
|
||||
public function onPageRequest(PageRequestEvent $event) {
|
||||
public function __construct(string $oldtag)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->oldtag = $oldtag;
|
||||
}
|
||||
}
|
||||
|
||||
class AddAliasException extends SCoreException
|
||||
{
|
||||
}
|
||||
|
||||
class AliasEditor extends Extension
|
||||
{
|
||||
/** @var AliasEditorTheme */
|
||||
protected $theme;
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $config, $database, $page, $user;
|
||||
|
||||
if($event->page_matches("alias")) {
|
||||
if($event->get_arg(0) == "add") {
|
||||
if($user->can("manage_alias_list")) {
|
||||
if(isset($_POST['oldtag']) && isset($_POST['newtag'])) {
|
||||
if ($event->page_matches("alias")) {
|
||||
if ($event->get_arg(0) == "add") {
|
||||
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
|
||||
$user->ensure_authed();
|
||||
$input = validate_input(["c_oldtag"=>"string", "c_newtag"=>"string"]);
|
||||
try {
|
||||
$aae = new AddAliasEvent($_POST['oldtag'], $_POST['newtag']);
|
||||
send_event($aae);
|
||||
$page->set_mode("redirect");
|
||||
send_event(new AddAliasEvent($input['c_oldtag'], $input['c_newtag']));
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("alias/list"));
|
||||
}
|
||||
catch(AddAliasException $ex) {
|
||||
} catch (AddAliasException $ex) {
|
||||
$this->theme->display_error(500, "Error adding alias", $ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if($event->get_arg(0) == "remove") {
|
||||
if($user->can("manage_alias_list")) {
|
||||
if(isset($_POST['oldtag'])) {
|
||||
$database->execute("DELETE FROM aliases WHERE oldtag=:oldtag", array("oldtag" => $_POST['oldtag']));
|
||||
log_info("alias_editor", "Deleted alias for ".$_POST['oldtag'], true);
|
||||
|
||||
$page->set_mode("redirect");
|
||||
} elseif ($event->get_arg(0) == "remove") {
|
||||
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
|
||||
$user->ensure_authed();
|
||||
$input = validate_input(["d_oldtag"=>"string"]);
|
||||
send_event(new DeleteAliasEvent($input['d_oldtag']));
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("alias/list"));
|
||||
}
|
||||
} elseif ($event->get_arg(0) == "list") {
|
||||
$t = new AliasTable($database->raw_db());
|
||||
$t->token = $user->get_auth_token();
|
||||
$t->inputs = $_GET;
|
||||
$t->size = $config->get_int('alias_items_per_page', 30);
|
||||
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
|
||||
$t->create_url = make_link("alias/add");
|
||||
$t->delete_url = make_link("alias/remove");
|
||||
}
|
||||
}
|
||||
else if($event->get_arg(0) == "list") {
|
||||
$page_number = $event->get_arg(1);
|
||||
if(is_null($page_number) || !is_numeric($page_number)) {
|
||||
$page_number = 0;
|
||||
}
|
||||
else if ($page_number <= 0) {
|
||||
$page_number = 0;
|
||||
}
|
||||
else {
|
||||
$page_number--;
|
||||
}
|
||||
|
||||
$alias_per_page = $config->get_int('alias_items_per_page', 30);
|
||||
|
||||
$query = "SELECT oldtag, newtag FROM aliases ORDER BY newtag ASC LIMIT :limit OFFSET :offset";
|
||||
$alias = $database->get_pairs($query,
|
||||
array("limit"=>$alias_per_page, "offset"=>$page_number * $alias_per_page)
|
||||
);
|
||||
|
||||
$total_pages = ceil($database->get_one("SELECT COUNT(*) FROM aliases") / $alias_per_page);
|
||||
|
||||
$this->theme->display_aliases($alias, $page_number + 1, $total_pages);
|
||||
}
|
||||
else if($event->get_arg(0) == "export") {
|
||||
$page->set_mode("data");
|
||||
$this->theme->display_aliases($t->table($t->query()), $t->paginator());
|
||||
} elseif ($event->get_arg(0) == "export") {
|
||||
$page->set_mode(PageMode::DATA);
|
||||
$page->set_type("text/csv");
|
||||
$page->set_filename("aliases.csv");
|
||||
$page->set_data($this->get_alias_csv($database));
|
||||
}
|
||||
else if($event->get_arg(0) == "import") {
|
||||
if($user->can("manage_alias_list")) {
|
||||
if(count($_FILES) > 0) {
|
||||
} elseif ($event->get_arg(0) == "import") {
|
||||
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
|
||||
if (count($_FILES) > 0) {
|
||||
$tmp = $_FILES['alias_file']['tmp_name'];
|
||||
$contents = file_get_contents($tmp);
|
||||
$this->add_alias_csv($database, $contents);
|
||||
log_info("alias_editor", "Imported aliases from file", true); # FIXME: how many?
|
||||
$page->set_mode("redirect");
|
||||
log_info("alias_editor", "Imported aliases from file", "Imported aliases"); # FIXME: how many?
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("alias/list"));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$this->theme->display_error(400, "No File Specified", "You have to upload a file");
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$this->theme->display_error(401, "Admins Only", "Only admins can edit the alias list");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onAddAlias(AddAliasEvent $event) {
|
||||
public function onAddAlias(AddAliasEvent $event)
|
||||
{
|
||||
global $database;
|
||||
$pair = array("oldtag" => $event->oldtag, "newtag" => $event->newtag);
|
||||
if($database->get_row("SELECT * FROM aliases WHERE oldtag=:oldtag AND lower(newtag)=lower(:newtag)", $pair)) {
|
||||
throw new AddAliasException("That alias already exists");
|
||||
|
||||
$row = $database->get_row(
|
||||
"SELECT * FROM aliases WHERE lower(oldtag)=lower(:oldtag)",
|
||||
["oldtag"=>$event->oldtag]
|
||||
);
|
||||
if ($row) {
|
||||
throw new AddAliasException("{$row['oldtag']} is already an alias for {$row['newtag']}");
|
||||
}
|
||||
else if($database->get_row("SELECT * FROM aliases WHERE oldtag=:newtag", array("newtag" => $event->newtag))) {
|
||||
throw new AddAliasException("{$event->newtag} is itself an alias");
|
||||
|
||||
$row = $database->get_row(
|
||||
"SELECT * FROM aliases WHERE lower(oldtag)=lower(:newtag)",
|
||||
["newtag" => $event->newtag]
|
||||
);
|
||||
if ($row) {
|
||||
throw new AddAliasException("{$row['oldtag']} is itself an alias for {$row['newtag']}");
|
||||
}
|
||||
else {
|
||||
$database->execute("INSERT INTO aliases(oldtag, newtag) VALUES(:oldtag, :newtag)", $pair);
|
||||
log_info("alias_editor", "Added alias for {$event->oldtag} -> {$event->newtag}", true);
|
||||
|
||||
$database->execute(
|
||||
"INSERT INTO aliases(oldtag, newtag) VALUES(:oldtag, :newtag)",
|
||||
["oldtag" => $event->oldtag, "newtag" => $event->newtag]
|
||||
);
|
||||
log_info("alias_editor", "Added alias for {$event->oldtag} -> {$event->newtag}", "Added alias");
|
||||
}
|
||||
|
||||
public function onDeleteAlias(DeleteAliasEvent $event)
|
||||
{
|
||||
global $database;
|
||||
$database->execute("DELETE FROM aliases WHERE oldtag=:oldtag", ["oldtag" => $event->oldtag]);
|
||||
log_info("alias_editor", "Deleted alias for {$event->oldtag}", "Deleted alias");
|
||||
}
|
||||
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
|
||||
{
|
||||
if ($event->parent=="tags") {
|
||||
$event->add_nav_link("aliases", new Link('alias/list'), "Aliases", NavLink::is_active(["alias"]));
|
||||
}
|
||||
}
|
||||
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event) {
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if($user->can("manage_alias_list")) {
|
||||
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
|
||||
$event->add_link("Alias Editor", make_link("alias/list"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Database $database
|
||||
* @return string
|
||||
*/
|
||||
private function get_alias_csv(Database $database) {
|
||||
private function get_alias_csv(Database $database): string
|
||||
{
|
||||
$csv = "";
|
||||
$aliases = $database->get_pairs("SELECT oldtag, newtag FROM aliases ORDER BY newtag");
|
||||
foreach($aliases as $old => $new) {
|
||||
foreach ($aliases as $old => $new) {
|
||||
$csv .= "\"$old\",\"$new\"\n";
|
||||
}
|
||||
return $csv;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Database $database
|
||||
* @param string $csv
|
||||
*/
|
||||
private function add_alias_csv(Database $database, /*string*/ $csv) {
|
||||
private function add_alias_csv(Database $database, string $csv): int
|
||||
{
|
||||
$csv = str_replace("\r", "\n", $csv);
|
||||
foreach(explode("\n", $csv) as $line) {
|
||||
$i = 0;
|
||||
foreach (explode("\n", $csv) as $line) {
|
||||
$parts = str_getcsv($line);
|
||||
if(count($parts) == 2) {
|
||||
if (count($parts) == 2) {
|
||||
try {
|
||||
$aae = new AddAliasEvent($parts[0], $parts[1]);
|
||||
send_event($aae);
|
||||
}
|
||||
catch(AddAliasException $ex) {
|
||||
send_event(new AddAliasEvent($parts[0], $parts[1]));
|
||||
$i++;
|
||||
} catch (AddAliasException $ex) {
|
||||
$this->theme->display_error(500, "Error adding alias", $ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
return $i;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -170,9 +201,9 @@ class AliasEditor extends Extension {
|
|||
* Add alias *after* mass tag editing, else the MTE will
|
||||
* search for the images and be redirected to the alias,
|
||||
* missing out the images tagged with the old tag.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_priority() {return 60;}
|
||||
public function get_priority(): int
|
||||
{
|
||||
return 60;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,104 +1,85 @@
|
|||
<?php
|
||||
class AliasEditorTest extends ShimmiePHPUnitTestCase {
|
||||
public function testAliasList() {
|
||||
<?php declare(strict_types=1);
|
||||
class AliasEditorTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testAliasList()
|
||||
{
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_response(200);
|
||||
$this->assert_title("Alias List");
|
||||
}
|
||||
|
||||
public function testAliasListReadOnly() {
|
||||
// Check that normal users can't add aliases.
|
||||
public function testAliasListReadOnly()
|
||||
{
|
||||
$this->log_in_as_user();
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_title("Alias List");
|
||||
$this->assert_no_text("Add");
|
||||
|
||||
$this->log_out();
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_title("Alias List");
|
||||
$this->assert_no_text("Add");
|
||||
}
|
||||
|
||||
public function testAliasEditor() {
|
||||
/*
|
||||
**********************************************************************
|
||||
* FIXME: TODO:
|
||||
* For some reason the alias tests always fail when they are running
|
||||
* inside the TravisCI VM environment. I have tried to determine
|
||||
* the exact cause of this, but have been unable to pin it down.
|
||||
*
|
||||
* For now, I am commenting them out until I have more time to
|
||||
* dig into this and determine exactly what is happening.
|
||||
*
|
||||
*********************************************************************
|
||||
*/
|
||||
$this->markTestIncomplete();
|
||||
|
||||
public function testAliasOneToOne()
|
||||
{
|
||||
$this->log_in_as_admin();
|
||||
|
||||
# test one to one
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_title("Alias List");
|
||||
$this->set_field('oldtag', "test1");
|
||||
$this->set_field('newtag', "test2");
|
||||
$this->clickSubmit('Add');
|
||||
$this->assert_no_text("Error adding alias");
|
||||
$this->get_page("alias/export/aliases.csv");
|
||||
$this->assert_no_text("test1");
|
||||
|
||||
send_event(new AddAliasEvent("test1", "test2"));
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_text("test1");
|
||||
|
||||
$this->get_page("alias/export/aliases.csv");
|
||||
$this->assert_text("test1,test2");
|
||||
$this->assert_text('"test1","test2"');
|
||||
|
||||
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
|
||||
$this->get_page("post/view/$image_id"); # check that the tag has been replaced
|
||||
$this->assert_title("Image $image_id: test2");
|
||||
$this->get_page("post/list/test1/1"); # searching for an alias should find the master tag
|
||||
$this->assert_title("Image $image_id: test2");
|
||||
$this->assert_response(302);
|
||||
$this->get_page("post/list/test2/1"); # check that searching for the main tag still works
|
||||
$this->assert_title("Image $image_id: test2");
|
||||
$this->assert_response(302);
|
||||
$this->delete_image($image_id);
|
||||
|
||||
$this->get_page('alias/list');
|
||||
$this->click("Remove");
|
||||
send_event(new DeleteAliasEvent("test1"));
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_title("Alias List");
|
||||
$this->assert_no_text("test1");
|
||||
}
|
||||
|
||||
# test one to many
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_title("Alias List");
|
||||
$this->set_field('oldtag', "onetag");
|
||||
$this->set_field('newtag', "multi tag");
|
||||
$this->click("Add");
|
||||
public function testAliasOneToMany()
|
||||
{
|
||||
$this->log_in_as_admin();
|
||||
|
||||
$this->get_page("alias/export/aliases.csv");
|
||||
$this->assert_no_text("multi");
|
||||
|
||||
send_event(new AddAliasEvent("onetag", "multi tag"));
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_text("multi");
|
||||
$this->assert_text("tag");
|
||||
|
||||
$this->get_page("alias/export/aliases.csv");
|
||||
$this->assert_text("onetag,multi tag");
|
||||
$this->assert_text('"onetag","multi tag"');
|
||||
|
||||
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "onetag");
|
||||
$image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "onetag");
|
||||
// FIXME: known broken
|
||||
//$this->get_page("post/list/onetag/1"); # searching for an aliased tag should find its aliases
|
||||
//$this->assert_title("onetag");
|
||||
//$this->assert_no_text("No Images Found");
|
||||
$this->get_page("post/list/onetag/1"); # searching for an aliased tag should find its aliases
|
||||
$this->assert_title("multi tag");
|
||||
$this->assert_no_text("No Images Found");
|
||||
$this->get_page("post/list/multi/1");
|
||||
$this->assert_title("multi");
|
||||
$this->assert_no_text("No Images Found");
|
||||
$this->get_page("post/list/multi%20tag/1");
|
||||
$this->get_page("post/list/multi tag/1");
|
||||
$this->assert_title("multi tag");
|
||||
$this->assert_no_text("No Images Found");
|
||||
$this->delete_image($image_id_1);
|
||||
$this->delete_image($image_id_2);
|
||||
|
||||
$this->get_page('alias/list');
|
||||
$this->click("Remove");
|
||||
send_event(new DeleteAliasEvent("onetag"));
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_title("Alias List");
|
||||
$this->assert_no_text("test1");
|
||||
|
||||
$this->log_out();
|
||||
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_title("Alias List");
|
||||
$this->assert_no_text("Add");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,60 +1,20 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
class AliasEditorTheme extends Themelet {
|
||||
class AliasEditorTheme extends Themelet
|
||||
{
|
||||
/**
|
||||
* Show a page of aliases.
|
||||
*
|
||||
* Note: $can_manage = whether things like "add new alias" should be shown
|
||||
*
|
||||
* @param array $aliases An array of ($old_tag => $new_tag)
|
||||
* @param int $pageNumber
|
||||
* @param int $totalPages
|
||||
*/
|
||||
public function display_aliases($aliases, $pageNumber, $totalPages) {
|
||||
public function display_aliases($table, $paginator): void
|
||||
{
|
||||
global $page, $user;
|
||||
|
||||
$can_manage = $user->can("manage_alias_list");
|
||||
if($can_manage) {
|
||||
$h_action = "<th width='10%'>Action</th>";
|
||||
$h_add = "
|
||||
<tr>
|
||||
".make_form(make_link("alias/add"))."
|
||||
<td><input type='text' name='oldtag' class='autocomplete_tags' autocomplete='off'></td>
|
||||
<td><input type='text' name='newtag' class='autocomplete_tags' autocomplete='off'></td>
|
||||
<td><input type='submit' value='Add'></td>
|
||||
</form>
|
||||
</tr>
|
||||
";
|
||||
}
|
||||
else {
|
||||
$h_action = "";
|
||||
$h_add = "";
|
||||
}
|
||||
|
||||
$h_aliases = "";
|
||||
foreach($aliases as $old => $new) {
|
||||
$h_old = html_escape($old);
|
||||
$h_new = "<a href='".make_link("post/list/".url_escape($new)."/1")."'>".html_escape($new)."</a>";
|
||||
|
||||
$h_aliases .= "<tr><td>$h_old</td><td>$h_new</td>";
|
||||
if($can_manage) {
|
||||
$h_aliases .= "
|
||||
<td>
|
||||
".make_form(make_link("alias/remove"))."
|
||||
<input type='hidden' name='oldtag' value='$h_old'>
|
||||
<input type='submit' value='Remove'>
|
||||
</form>
|
||||
</td>
|
||||
";
|
||||
}
|
||||
$h_aliases .= "</tr>";
|
||||
}
|
||||
$can_manage = $user->can(Permissions::MANAGE_ALIAS_LIST);
|
||||
$html = "
|
||||
<table id='aliases' class='sortable zebra'>
|
||||
<thead><tr><th>From</th><th>To</th>$h_action</tr></thead>
|
||||
<tbody>$h_aliases</tbody>
|
||||
<tfoot>$h_add</tfoot>
|
||||
</table>
|
||||
$table
|
||||
$paginator
|
||||
<p><a href='".make_link("alias/export/aliases.csv")."' download='aliases.csv'>Download as CSV</a></p>
|
||||
";
|
||||
|
||||
|
@ -69,11 +29,8 @@ class AliasEditorTheme extends Themelet {
|
|||
$page->set_heading("Alias List");
|
||||
$page->add_block(new NavBlock());
|
||||
$page->add_block(new Block("Aliases", $html));
|
||||
if($can_manage) {
|
||||
if ($can_manage) {
|
||||
$page->add_block(new Block("Bulk Upload", $bulk_html, "main", 51));
|
||||
}
|
||||
|
||||
$this->display_paginator($page, "alias/list", null, $pageNumber, $totalPages);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,75 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* Name: Amazon S3 Mirror
|
||||
* Author: Shish <webmaster@shishnet.org>
|
||||
* License: GPLv2
|
||||
* Description: Copy uploaded files to S3
|
||||
* Documentation:
|
||||
*/
|
||||
|
||||
require_once "ext/amazon_s3/lib/S3.php";
|
||||
|
||||
class UploadS3 extends Extension {
|
||||
public function onInitExt(InitExtEvent $event) {
|
||||
global $config;
|
||||
$config->set_default_string("amazon_s3_access", "");
|
||||
$config->set_default_string("amazon_s3_secret", "");
|
||||
$config->set_default_string("amazon_s3_bucket", "");
|
||||
}
|
||||
|
||||
public function onSetupBuilding(SetupBuildingEvent $event) {
|
||||
$sb = new SetupBlock("Amazon S3");
|
||||
$sb->add_text_option("amazon_s3_access", "Access key: ");
|
||||
$sb->add_text_option("amazon_s3_secret", "<br>Secret key: ");
|
||||
$sb->add_text_option("amazon_s3_bucket", "<br>Bucket: ");
|
||||
$event->panel->add_block($sb);
|
||||
}
|
||||
|
||||
public function onImageAddition(ImageAdditionEvent $event) {
|
||||
global $config;
|
||||
$access = $config->get_string("amazon_s3_access");
|
||||
$secret = $config->get_string("amazon_s3_secret");
|
||||
$bucket = $config->get_string("amazon_s3_bucket");
|
||||
if(!empty($bucket)) {
|
||||
log_debug("amazon_s3", "Mirroring Image #".$event->image->id." to S3 #$bucket");
|
||||
$s3 = new S3($access, $secret);
|
||||
$s3->putBucket($bucket, S3::ACL_PUBLIC_READ);
|
||||
$s3->putObjectFile(
|
||||
warehouse_path("thumbs", $event->image->hash),
|
||||
$bucket,
|
||||
'thumbs/'.$event->image->hash,
|
||||
S3::ACL_PUBLIC_READ,
|
||||
array(),
|
||||
array(
|
||||
"Content-Type" => "image/jpeg",
|
||||
"Content-Disposition" => "inline; filename=image-" . $event->image->id . ".jpg",
|
||||
)
|
||||
);
|
||||
$s3->putObjectFile(
|
||||
warehouse_path("images", $event->image->hash),
|
||||
$bucket,
|
||||
'images/'.$event->image->hash,
|
||||
S3::ACL_PUBLIC_READ,
|
||||
array(),
|
||||
array(
|
||||
"Content-Type" => $event->image->get_mime_type(),
|
||||
"Content-Disposition" => "inline; filename=image-" . $event->image->id . "." . $event->image->ext,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function onImageDeletion(ImageDeletionEvent $event) {
|
||||
global $config;
|
||||
$access = $config->get_string("amazon_s3_access");
|
||||
$secret = $config->get_string("amazon_s3_secret");
|
||||
$bucket = $config->get_string("amazon_s3_bucket");
|
||||
if(!empty($bucket)) {
|
||||
log_debug("amazon_s3", "Deleting Image #".$event->image->id." from S3");
|
||||
$s3 = new S3($access, $secret);
|
||||
$s3->deleteObject($bucket, "images/" . $event->image->hash);
|
||||
$s3->deleteObject($bucket, "thumbs/" . $event->image->hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
12
ext/approval/info.php
Normal file
12
ext/approval/info.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
class ApprovalInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "approval";
|
||||
|
||||
public $key = self::KEY;
|
||||
public $name = "Approval";
|
||||
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public $license = self::LICENSE_WTFPL;
|
||||
public $description = "Adds an approval step to the upload/import process.";
|
||||
}
|
255
ext/approval/main.php
Normal file
255
ext/approval/main.php
Normal file
|
@ -0,0 +1,255 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
abstract class ApprovalConfig
|
||||
{
|
||||
const VERSION = "ext_approval_version";
|
||||
const IMAGES = "approve_images";
|
||||
const COMMENTS = "approve_comments";
|
||||
}
|
||||
|
||||
class Approval extends Extension
|
||||
{
|
||||
/** @var ApprovalTheme */
|
||||
protected $theme;
|
||||
|
||||
public function onInitExt(InitExtEvent $event)
|
||||
{
|
||||
global $config;
|
||||
|
||||
$config->set_default_bool(ApprovalConfig::IMAGES, false);
|
||||
$config->set_default_bool(ApprovalConfig::COMMENTS, false);
|
||||
|
||||
Image::$bool_props[] = "approved";
|
||||
}
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $page, $user;
|
||||
|
||||
if ($event->page_matches("approve_image") && $user->can(Permissions::APPROVE_IMAGE)) {
|
||||
// Try to get the image ID
|
||||
$image_id = int_escape($event->get_arg(0));
|
||||
if (empty($image_id)) {
|
||||
$image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null;
|
||||
}
|
||||
if (empty($image_id)) {
|
||||
throw new SCoreException("Can not approve image: No valid Image ID given.");
|
||||
}
|
||||
|
||||
self::approve_image($image_id);
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("post/view/" . $image_id));
|
||||
}
|
||||
|
||||
if ($event->page_matches("disapprove_image") && $user->can(Permissions::APPROVE_IMAGE)) {
|
||||
// Try to get the image ID
|
||||
$image_id = int_escape($event->get_arg(0));
|
||||
if (empty($image_id)) {
|
||||
$image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null;
|
||||
}
|
||||
if (empty($image_id)) {
|
||||
throw new SCoreException("Can not disapprove image: No valid Image ID given.");
|
||||
}
|
||||
|
||||
self::disapprove_image($image_id);
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("post/view/".$image_id));
|
||||
}
|
||||
}
|
||||
|
||||
public function onSetupBuilding(SetupBuildingEvent $event)
|
||||
{
|
||||
$this->theme->display_admin_block($event);
|
||||
}
|
||||
|
||||
public function onAdminBuilding(AdminBuildingEvent $event)
|
||||
{
|
||||
$this->theme->display_admin_form();
|
||||
}
|
||||
|
||||
public function onAdminAction(AdminActionEvent $event)
|
||||
{
|
||||
global $database, $user;
|
||||
|
||||
$action = $event->action;
|
||||
$event->redirect = true;
|
||||
if ($action==="approval") {
|
||||
$approval_action = $_POST["approval_action"];
|
||||
switch ($approval_action) {
|
||||
case "approve_all":
|
||||
$database->set_timeout(300000); // These updates can take a little bit
|
||||
$database->execute(
|
||||
"UPDATE images SET approved = :true, approved_by_id = :approved_by_id WHERE approved = :false",
|
||||
["approved_by_id"=>$user->id, "true"=>true, "false"=>false]
|
||||
);
|
||||
break;
|
||||
case "disapprove_all":
|
||||
$database->set_timeout(300000); // These updates can take a little bit
|
||||
$database->execute(
|
||||
"UPDATE images SET approved = :false, approved_by_id = NULL WHERE approved = :true",
|
||||
["true"=>true, "false"=>false]
|
||||
);
|
||||
break;
|
||||
default:
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onDisplayingImage(DisplayingImageEvent $event)
|
||||
{
|
||||
global $user, $page, $config;
|
||||
|
||||
if ($config->get_bool(ApprovalConfig::IMAGES) && $event->image->approved===false && !$user->can(Permissions::APPROVE_IMAGE)) {
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("post/list"));
|
||||
}
|
||||
}
|
||||
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if ($event->parent=="posts") {
|
||||
if ($user->can(Permissions::APPROVE_IMAGE)) {
|
||||
$event->add_nav_link("posts_unapproved", new Link('/post/list/approved%3Ano/1'), "Pending Approval", null, 60);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const SEARCH_REGEXP = "/^approved:(yes|no)/";
|
||||
public function onSearchTermParse(SearchTermParseEvent $event)
|
||||
{
|
||||
global $user, $database, $config;
|
||||
|
||||
if ($config->get_bool(ApprovalConfig::IMAGES)) {
|
||||
$matches = [];
|
||||
|
||||
if (is_null($event->term) && $this->no_approval_query($event->context)) {
|
||||
$event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_Y ")));
|
||||
}
|
||||
|
||||
if (is_null($event->term)) {
|
||||
return;
|
||||
}
|
||||
if (preg_match(self::SEARCH_REGEXP, strtolower($event->term), $matches)) {
|
||||
if ($user->can(Permissions::APPROVE_IMAGE) && $matches[1] == "no") {
|
||||
$event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_N ")));
|
||||
} else {
|
||||
$event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_Y ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onHelpPageBuilding(HelpPageBuildingEvent $event)
|
||||
{
|
||||
global $user, $config;
|
||||
if ($event->key===HelpPages::SEARCH) {
|
||||
if ($user->can(Permissions::APPROVE_IMAGE) && $config->get_bool(ApprovalConfig::IMAGES)) {
|
||||
$block = new Block();
|
||||
$block->header = "Approval";
|
||||
$block->body = $this->theme->get_help_html();
|
||||
$event->add_block($block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function no_approval_query(array $context): bool
|
||||
{
|
||||
foreach ($context as $term) {
|
||||
if (preg_match(self::SEARCH_REGEXP, $term)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function approve_image($image_id)
|
||||
{
|
||||
global $database, $user;
|
||||
|
||||
$database->execute(
|
||||
"UPDATE images SET approved = :true, approved_by_id = :approved_by_id WHERE id = :id AND approved = :false",
|
||||
["approved_by_id"=>$user->id, "id"=>$image_id, "true"=>true, "false"=>false]
|
||||
);
|
||||
}
|
||||
|
||||
public static function disapprove_image($image_id)
|
||||
{
|
||||
global $database;
|
||||
|
||||
$database->execute(
|
||||
"UPDATE images SET approved = :false, approved_by_id = NULL WHERE id = :id AND approved = :true",
|
||||
["id"=>$image_id, "true"=>true, "false"=>false]
|
||||
);
|
||||
}
|
||||
|
||||
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
|
||||
{
|
||||
global $user, $config;
|
||||
if ($user->can(Permissions::APPROVE_IMAGE) && $config->get_bool(ApprovalConfig::IMAGES)) {
|
||||
$event->add_part($this->theme->get_image_admin_html($event->image));
|
||||
}
|
||||
}
|
||||
|
||||
public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event)
|
||||
{
|
||||
global $user, $config;
|
||||
|
||||
if ($user->can(Permissions::APPROVE_IMAGE)&& $config->get_bool(ApprovalConfig::IMAGES)) {
|
||||
if (in_array("approved:no", $event->search_terms)) {
|
||||
$event->add_action("bulk_approve_image", "Approve", "a");
|
||||
} else {
|
||||
$event->add_action("bulk_disapprove_image", "Disapprove");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onBulkAction(BulkActionEvent $event)
|
||||
{
|
||||
global $page, $user;
|
||||
|
||||
switch ($event->action) {
|
||||
case "bulk_approve_image":
|
||||
if ($user->can(Permissions::APPROVE_IMAGE)) {
|
||||
$total = 0;
|
||||
foreach ($event->items as $image) {
|
||||
self::approve_image($image->id);
|
||||
$total++;
|
||||
}
|
||||
$page->flash("Approved $total items");
|
||||
}
|
||||
break;
|
||||
case "bulk_disapprove_image":
|
||||
if ($user->can(Permissions::APPROVE_IMAGE)) {
|
||||
$total = 0;
|
||||
foreach ($event->items as $image) {
|
||||
self::disapprove_image($image->id);
|
||||
$total++;
|
||||
}
|
||||
$page->flash("Disapproved $total items");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
|
||||
{
|
||||
global $database;
|
||||
|
||||
if ($this->get_version(ApprovalConfig::VERSION) < 1) {
|
||||
$database->execute($database->scoreql_to_sql(
|
||||
"ALTER TABLE images ADD COLUMN approved SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N"
|
||||
));
|
||||
$database->execute(
|
||||
"ALTER TABLE images ADD COLUMN approved_by_id INTEGER NULL"
|
||||
);
|
||||
|
||||
$database->execute("CREATE INDEX images_approved_idx ON images(approved)");
|
||||
$this->set_version(ApprovalConfig::VERSION, 1);
|
||||
}
|
||||
}
|
||||
}
|
61
ext/approval/theme.php
Normal file
61
ext/approval/theme.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php declare(strict_types=1);
|
||||
use function MicroHTML\BR;
|
||||
use function MicroHTML\BUTTON;
|
||||
use function MicroHTML\INPUT;
|
||||
|
||||
class ApprovalTheme extends Themelet
|
||||
{
|
||||
public function get_image_admin_html(Image $image)
|
||||
{
|
||||
if ($image->approved===true) {
|
||||
$html = SHM_SIMPLE_FORM(
|
||||
'disapprove_image/'.$image->id,
|
||||
INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image->id]),
|
||||
SHM_SUBMIT("Disapprove")
|
||||
);
|
||||
} else {
|
||||
$html = SHM_SIMPLE_FORM(
|
||||
'approve_image/'.$image->id,
|
||||
INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image->id]),
|
||||
SHM_SUBMIT("Approve")
|
||||
);
|
||||
}
|
||||
|
||||
return (string)$html;
|
||||
}
|
||||
|
||||
|
||||
public function get_help_html()
|
||||
{
|
||||
return '<p>Search for images that are approved/not approved.</p>
|
||||
<div class="command_example">
|
||||
<pre>approved:yes</pre>
|
||||
<p>Returns images that have been approved.</p>
|
||||
</div>
|
||||
<div class="command_example">
|
||||
<pre>approved:no</pre>
|
||||
<p>Returns images that have not been approved.</p>
|
||||
</div>
|
||||
';
|
||||
}
|
||||
|
||||
public function display_admin_block(SetupBuildingEvent $event)
|
||||
{
|
||||
$sb = new SetupBlock("Approval");
|
||||
$sb->add_bool_option(ApprovalConfig::IMAGES, "Images: ");
|
||||
$event->panel->add_block($sb);
|
||||
}
|
||||
|
||||
public function display_admin_form()
|
||||
{
|
||||
global $page;
|
||||
|
||||
$html = (string)SHM_SIMPLE_FORM(
|
||||
"admin/approval",
|
||||
BUTTON(["name"=>'approval_action', "value"=>'approve_all'], "Approve All Images"),
|
||||
BR(),
|
||||
BUTTON(["name"=>'approval_action', "value"=>'disapprove_all'], "Disapprove All Images"),
|
||||
);
|
||||
$page->add_block(new Block("Approval", $html));
|
||||
}
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Name: Arrow Key Navigation
|
||||
* Author: Drudex Software <support@drudexsoftware.com>
|
||||
* Link: http://www.drudexsoftware.com/
|
||||
* License: GPLv2
|
||||
* Description: Allows viewers no navigate between images using the left & right arrow keys.
|
||||
* Documentation:
|
||||
* Simply enable this extention in the extention manager to enable arrow key navigation.
|
||||
*/
|
||||
class ArrowkeyNavigation extends Extension {
|
||||
/**
|
||||
* Adds functionality for post/view on images.
|
||||
*
|
||||
* @param DisplayingImageEvent $event
|
||||
*/
|
||||
public function onDisplayingImage(DisplayingImageEvent $event) {
|
||||
$prev_url = make_http(make_link("post/prev/".$event->image->id));
|
||||
$next_url = make_http(make_link("post/next/".$event->image->id));
|
||||
$this->add_arrowkeys_code($prev_url, $next_url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds functionality for post/list.
|
||||
*
|
||||
* @param PageRequestEvent $event
|
||||
*/
|
||||
public function onPageRequest(PageRequestEvent $event) {
|
||||
if($event->page_matches("post/list")) {
|
||||
$pageinfo = $this->get_list_pageinfo($event);
|
||||
$prev_url = make_http(make_link("post/list/".$pageinfo["prev"]));
|
||||
$next_url = make_http(make_link("post/list/".$pageinfo["next"]));
|
||||
$this->add_arrowkeys_code($prev_url, $next_url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the javascript to the page with the given urls.
|
||||
*
|
||||
* @param string $prev_url
|
||||
* @param string $next_url
|
||||
*/
|
||||
private function add_arrowkeys_code($prev_url, $next_url) {
|
||||
global $page;
|
||||
|
||||
$page->add_html_header("<script type=\"text/javascript\">
|
||||
(function($){
|
||||
$(document).keyup(function(e) {
|
||||
if($(e.target).is('input', 'textarea')){ return; }
|
||||
if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) { return; }
|
||||
if (e.keyCode == 37) { window.location.href = '{$prev_url}'; }
|
||||
else if (e.keyCode == 39) { window.location.href = '{$next_url}'; }
|
||||
});
|
||||
})(jQuery);
|
||||
</script>", 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns info about the current page number.
|
||||
*
|
||||
* @param PageRequestEvent $event
|
||||
* @return array
|
||||
*/
|
||||
private function get_list_pageinfo(PageRequestEvent $event) {
|
||||
global $config, $database;
|
||||
|
||||
// get the amount of images per page
|
||||
$images_per_page = $config->get_int('index_images');
|
||||
|
||||
// if there are no tags, use default
|
||||
if (is_null($event->get_arg(1))){
|
||||
$prefix = "";
|
||||
$page_number = int_escape($event->get_arg(0));
|
||||
$total_pages = ceil($database->get_one(
|
||||
"SELECT COUNT(*) FROM images") / $images_per_page);
|
||||
}
|
||||
else { // if there are tags, use pages with tags
|
||||
$prefix = url_escape($event->get_arg(0)) . "/";
|
||||
$page_number = int_escape($event->get_arg(1));
|
||||
$total_pages = ceil($database->get_one(
|
||||
"SELECT count FROM tags WHERE tag=:tag",
|
||||
array("tag"=>$event->get_arg(0))) / $images_per_page);
|
||||
}
|
||||
|
||||
// creates previous & next values
|
||||
// When previous first page, go to last page
|
||||
if ($page_number <= 1) $prev = $total_pages;
|
||||
else $prev = $page_number-1;
|
||||
if ($page_number >= $total_pages) $next = 1;
|
||||
else $next = $page_number+1;
|
||||
|
||||
// Create return array
|
||||
$pageinfo = array(
|
||||
"prev" => $prefix.$prev,
|
||||
"next" => $prefix.$next,
|
||||
);
|
||||
|
||||
return $pageinfo;
|
||||
}
|
||||
}
|
||||
|
14
ext/artists/info.php
Normal file
14
ext/artists/info.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
class ArtistsInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "artists";
|
||||
|
||||
public $key = self::KEY;
|
||||
public $name = "Artists System";
|
||||
public $url = self::SHIMMIE_URL;
|
||||
public $authors = ["Sein Kraft"=>"mail@seinkraft.info","Alpha"=>"alpha@furries.com.ar"];
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $description = "Simple artists extension";
|
||||
public $beta = true;
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,9 +1,15 @@
|
|||
<?php
|
||||
class ArtistTest extends ShimmiePHPUnitTestCase {
|
||||
public function testSearch() {
|
||||
# FIXME: check that the results are there
|
||||
$this->get_page("post/list/author=bob/1");
|
||||
#$this->assert_response(200);
|
||||
<?php declare(strict_types=1);
|
||||
class ArtistsTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testSearch()
|
||||
{
|
||||
global $user;
|
||||
$this->log_in_as_user();
|
||||
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot");
|
||||
$image = Image::by_id($image_id);
|
||||
|
||||
send_event(new AuthorSetEvent($image, $user, "bob"));
|
||||
|
||||
$this->assert_search_results(["author=bob"], [$image_id]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
<?php
|
||||
class ArtistsTheme extends Themelet {
|
||||
|
||||
/**
|
||||
* @param string $author
|
||||
* @return string
|
||||
*/
|
||||
public function get_author_editor_html(/*string*/ $author) {
|
||||
<?php declare(strict_types=1);
|
||||
class ArtistsTheme extends Themelet
|
||||
{
|
||||
public function get_author_editor_html(string $author): string
|
||||
{
|
||||
$h_author = html_escape($author);
|
||||
return "
|
||||
<tr>
|
||||
|
@ -18,66 +15,65 @@ class ArtistsTheme extends Themelet {
|
|||
";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $mode
|
||||
* @param null|int $artistID
|
||||
* @param bool $is_admin
|
||||
*/
|
||||
public function sidebar_options(/*string*/ $mode, $artistID=NULL, $is_admin=FALSE) {
|
||||
public function sidebar_options(string $mode, ?int $artistID=null, $is_admin=false): void
|
||||
{
|
||||
global $page, $user;
|
||||
|
||||
$html = "";
|
||||
|
||||
if($mode == "neutral"){
|
||||
if ($mode == "neutral") {
|
||||
$html = "<form method='post' action='".make_link("artist/new_artist")."'>
|
||||
".$user->get_auth_html()."
|
||||
<input type='submit' name='edit' id='edit' value='New Artist'/>
|
||||
</form>";
|
||||
}
|
||||
|
||||
if($mode == "editor"){
|
||||
if ($mode == "editor") {
|
||||
$html = "<form method='post' action='".make_link("artist/new_artist")."'>
|
||||
".$user->get_auth_html()."
|
||||
<input type='submit' name='edit' id='edit' value='New Artist'/>
|
||||
<input type='submit' name='edit' value='New Artist'/>
|
||||
</form>
|
||||
|
||||
<form method='post' action='".make_link("artist/edit_artist")."'>
|
||||
".$user->get_auth_html()."
|
||||
<input type='submit' name='edit' id='edit' value='Edit Artist'/>
|
||||
<input type='submit' name='edit' value='Edit Artist'/>
|
||||
<input type='hidden' name='artist_id' value='".$artistID."'>
|
||||
</form>";
|
||||
|
||||
if($is_admin){
|
||||
if ($is_admin) {
|
||||
$html .= "<form method='post' action='".make_link("artist/nuke_artist")."'>
|
||||
".$user->get_auth_html()."
|
||||
<input type='submit' name='edit' id='edit' value='Delete Artist'/>
|
||||
<input type='submit' name='edit' value='Delete Artist'/>
|
||||
<input type='hidden' name='artist_id' value='".$artistID."'>
|
||||
</form>";
|
||||
}
|
||||
|
||||
$html .= "<form method='post' action='".make_link("artist/add_alias")."'>
|
||||
".$user->get_auth_html()."
|
||||
<input type='submit' name='edit' id='edit' value='Add Alias'/>
|
||||
<input type='submit' name='edit' value='Add Alias'/>
|
||||
<input type='hidden' name='artist_id' value='".$artistID."'>
|
||||
</form>
|
||||
|
||||
<form method='post' action='".make_link("artist/add_member")."'>
|
||||
".$user->get_auth_html()."
|
||||
<input type='submit' name='edit' id='edit' value='Add Member'/>
|
||||
<input type='submit' name='edit' value='Add Member'/>
|
||||
<input type='hidden' name='artist_id' value='".$artistID."'>
|
||||
</form>
|
||||
|
||||
<form method='post' action='".make_link("artist/add_url")."'>
|
||||
".$user->get_auth_html()."
|
||||
<input type='submit' name='edit' id='edit' value='Add Url'/>
|
||||
<input type='submit' name='edit' value='Add Url'/>
|
||||
<input type='hidden' name='artist_id' value='".$artistID."'>
|
||||
</form>";
|
||||
}
|
||||
|
||||
if($html) $page->add_block(new Block("Manage Artists", $html, "left", 10));
|
||||
if ($html) {
|
||||
$page->add_block(new Block("Manage Artists", $html, "left", 10));
|
||||
}
|
||||
}
|
||||
|
||||
public function show_artist_editor($artist, $aliases, $members, $urls) {
|
||||
public function show_artist_editor($artist, $aliases, $members, $urls)
|
||||
{
|
||||
global $user;
|
||||
|
||||
$artistName = $artist['name'];
|
||||
|
@ -136,7 +132,8 @@ class ArtistsTheme extends Themelet {
|
|||
$page->add_block(new Block("Edit artist", $html, "main", 10));
|
||||
}
|
||||
|
||||
public function new_artist_composer() {
|
||||
public function new_artist_composer()
|
||||
{
|
||||
global $page, $user;
|
||||
|
||||
$html = "<form action=".make_link("artist/create")." method='POST'>
|
||||
|
@ -156,7 +153,8 @@ class ArtistsTheme extends Themelet {
|
|||
$page->add_block(new Block("Artists", $html, "main", 10));
|
||||
}
|
||||
|
||||
public function list_artists($artists, $pageNumber, $totalPages) {
|
||||
public function list_artists($artists, $pageNumber, $totalPages)
|
||||
{
|
||||
global $user, $page;
|
||||
|
||||
$html = "<table id='poolsList' class='zebra'>".
|
||||
|
@ -166,31 +164,34 @@ class ArtistsTheme extends Themelet {
|
|||
"<th>Last updater</th>".
|
||||
"<th>Posts</th>";
|
||||
|
||||
if(!$user->is_anonymous()) $html .= "<th colspan='2'>Action</th>"; // space for edit link
|
||||
if (!$user->is_anonymous()) {
|
||||
$html .= "<th colspan='2'>Action</th>";
|
||||
} // space for edit link
|
||||
|
||||
$html .= "</tr></thead>";
|
||||
|
||||
$deletionLinkActionArray = array(
|
||||
$deletionLinkActionArray = [
|
||||
'artist' => 'artist/nuke/',
|
||||
'alias' => 'artist/alias/delete/',
|
||||
'member' => 'artist/member/delete/',
|
||||
);
|
||||
];
|
||||
|
||||
$editionLinkActionArray = array(
|
||||
$editionLinkActionArray = [
|
||||
'artist' => 'artist/edit/',
|
||||
'alias' => 'artist/alias/edit/',
|
||||
'member' => 'artist/member/edit/',
|
||||
);
|
||||
];
|
||||
|
||||
$typeTextArray = array(
|
||||
$typeTextArray = [
|
||||
'artist' => 'Artist',
|
||||
'alias' => 'Alias',
|
||||
'member' => 'Member',
|
||||
);
|
||||
];
|
||||
|
||||
foreach ($artists as $artist) {
|
||||
if ($artist['type'] != 'artist')
|
||||
if ($artist['type'] != 'artist') {
|
||||
$artist['name'] = str_replace("_", " ", $artist['name']);
|
||||
}
|
||||
|
||||
$elementLink = "<a href='".make_link("artist/view/".$artist['artist_id'])."'>".str_replace("_", " ", $artist['name'])."</a>";
|
||||
//$artist_link = "<a href='".make_link("artist/view/".$artist['artist_id'])."'>".str_replace("_", " ", $artist['artist_name'])."</a>";
|
||||
|
@ -212,8 +213,12 @@ class ArtistsTheme extends Themelet {
|
|||
"<td>".$user_link."</td>".
|
||||
"<td>".$artist['posts']."</td>";
|
||||
|
||||
if(!$user->is_anonymous()) $html .= "<td>".$edit_link."</td>";
|
||||
if($user->is_admin()) $html .= "<td>".$del_link."</td>";
|
||||
if (!$user->is_anonymous()) {
|
||||
$html .= "<td>".$edit_link."</td>";
|
||||
}
|
||||
if ($user->can(Permissions::ARTISTS_ADMIN)) {
|
||||
$html .= "<td>".$del_link."</td>";
|
||||
}
|
||||
|
||||
$html .= "</tr>";
|
||||
}
|
||||
|
@ -227,7 +232,8 @@ class ArtistsTheme extends Themelet {
|
|||
$this->display_paginator($page, "artist/list", null, $pageNumber, $totalPages);
|
||||
}
|
||||
|
||||
public function show_new_alias_composer($artistID) {
|
||||
public function show_new_alias_composer($artistID)
|
||||
{
|
||||
global $user;
|
||||
|
||||
$html = '
|
||||
|
@ -245,7 +251,8 @@ class ArtistsTheme extends Themelet {
|
|||
$page->add_block(new Block("Artist Aliases", $html, "main", 20));
|
||||
}
|
||||
|
||||
public function show_new_member_composer($artistID) {
|
||||
public function show_new_member_composer($artistID)
|
||||
{
|
||||
global $user;
|
||||
|
||||
$html = '
|
||||
|
@ -263,7 +270,8 @@ class ArtistsTheme extends Themelet {
|
|||
$page->add_block(new Block("Artist members", $html, "main", 30));
|
||||
}
|
||||
|
||||
public function show_new_url_composer($artistID) {
|
||||
public function show_new_url_composer($artistID)
|
||||
{
|
||||
global $user;
|
||||
|
||||
$html = '
|
||||
|
@ -281,14 +289,15 @@ class ArtistsTheme extends Themelet {
|
|||
$page->add_block(new Block("Artist URLs", $html, "main", 40));
|
||||
}
|
||||
|
||||
public function show_alias_editor($alias) {
|
||||
public function show_alias_editor($alias)
|
||||
{
|
||||
global $user;
|
||||
|
||||
$html = '
|
||||
<form method="POST" action="'.make_link("artist/alias/edited/".$alias['id']).'">
|
||||
'.$user->get_auth_html().'
|
||||
<label for="alias">Alias:</label>
|
||||
<input type="text" name="alias" value="'.$alias['alias'].'" />
|
||||
<input type="text" name="alias" id="alias" value="'.$alias['alias'].'" />
|
||||
<input type="hidden" name="aliasID" value="'.$alias['id'].'" />
|
||||
<input type="submit" value="Submit" />
|
||||
</form>
|
||||
|
@ -298,14 +307,15 @@ class ArtistsTheme extends Themelet {
|
|||
$page->add_block(new Block("Edit Alias", $html, "main", 10));
|
||||
}
|
||||
|
||||
public function show_url_editor($url) {
|
||||
public function show_url_editor($url)
|
||||
{
|
||||
global $user;
|
||||
|
||||
$html = '
|
||||
<form method="POST" action="'.make_link("artist/url/edited/".$url['id']).'">
|
||||
'.$user->get_auth_html().'
|
||||
<label for="url">URL:</label>
|
||||
<input type="text" name="url" value="'.$url['url'].'" />
|
||||
<input type="text" name="url" id="url" value="'.$url['url'].'" />
|
||||
<input type="hidden" name="urlID" value="'.$url['id'].'" />
|
||||
<input type="submit" value="Submit" />
|
||||
</form>
|
||||
|
@ -315,14 +325,15 @@ class ArtistsTheme extends Themelet {
|
|||
$page->add_block(new Block("Edit URL", $html, "main", 10));
|
||||
}
|
||||
|
||||
public function show_member_editor($member) {
|
||||
public function show_member_editor($member)
|
||||
{
|
||||
global $user;
|
||||
|
||||
$html = '
|
||||
<form method="POST" action="'.make_link("artist/member/edited/".$member['id']).'">
|
||||
'.$user->get_auth_html().'
|
||||
<label for="member">Member name:</label>
|
||||
<input type="text" name="name" value="'.$member['name'].'" />
|
||||
<label for="name">Member name:</label>
|
||||
<input type="text" name="name" id="name" value="'.$member['name'].'" />
|
||||
<input type="hidden" name="memberID" value="'.$member['id'].'" />
|
||||
<input type="submit" value="Submit" />
|
||||
</form>
|
||||
|
@ -332,7 +343,8 @@ class ArtistsTheme extends Themelet {
|
|||
$page->add_block(new Block("Edit Member", $html, "main", 10));
|
||||
}
|
||||
|
||||
public function show_artist($artist, $aliases, $members, $urls, $images, $userIsLogged, $userIsAdmin) {
|
||||
public function show_artist($artist, $aliases, $members, $urls, $images, $userIsLogged, $userIsAdmin)
|
||||
{
|
||||
global $page;
|
||||
|
||||
$artist_link = "<a href='".make_link("post/list/".$artist['name']."/1")."'>".str_replace("_", " ", $artist['name'])."</a>";
|
||||
|
@ -343,8 +355,12 @@ class ArtistsTheme extends Themelet {
|
|||
<th></th>
|
||||
<th></th>";
|
||||
|
||||
if ($userIsLogged) $html .= "<th></th>";
|
||||
if ($userIsAdmin) $html .= "<th></th>";
|
||||
if ($userIsLogged) {
|
||||
$html .= "<th></th>";
|
||||
}
|
||||
if ($userIsAdmin) {
|
||||
$html .= "<th></th>";
|
||||
}
|
||||
|
||||
$html .= " <tr>
|
||||
</thead>
|
||||
|
@ -352,8 +368,12 @@ class ArtistsTheme extends Themelet {
|
|||
<tr>
|
||||
<td class='left'>Name:</td>
|
||||
<td class='left'>".$artist_link."</td>";
|
||||
if ($userIsLogged) $html .= "<td></td>";
|
||||
if ($userIsAdmin) $html .= "<td></td>";
|
||||
if ($userIsLogged) {
|
||||
$html .= "<td></td>";
|
||||
}
|
||||
if ($userIsAdmin) {
|
||||
$html .= "<td></td>";
|
||||
}
|
||||
$html .= "</tr>";
|
||||
|
||||
$html .= $this->render_aliases($aliases, $userIsLogged, $userIsAdmin);
|
||||
|
@ -363,8 +383,12 @@ class ArtistsTheme extends Themelet {
|
|||
$html .= "<tr>
|
||||
<td class='left'>Notes:</td>
|
||||
<td class='left'>".$artist["notes"]."</td>";
|
||||
if ($userIsLogged) $html .= "<td></td>";
|
||||
if ($userIsAdmin) $html .= "<td></td>";
|
||||
if ($userIsLogged) {
|
||||
$html .= "<td></td>";
|
||||
}
|
||||
if ($userIsAdmin) {
|
||||
$html .= "<td></td>";
|
||||
}
|
||||
//TODO how will notes be edited? On edit artist? (should there be an editartist?) or on a editnotes?
|
||||
//same question for deletion
|
||||
$html .= "</tr>
|
||||
|
@ -376,7 +400,7 @@ class ArtistsTheme extends Themelet {
|
|||
|
||||
//we show the images for the artist
|
||||
$artist_images = "";
|
||||
foreach($images as $image) {
|
||||
foreach ($images as $image) {
|
||||
$thumb_html = $this->build_thumb_html($image);
|
||||
|
||||
$artist_images .= '<span class="thumb">'.
|
||||
|
@ -387,15 +411,10 @@ class ArtistsTheme extends Themelet {
|
|||
$page->add_block(new Block("Artist Images", $artist_images, "main", 20));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $aliases
|
||||
* @param $userIsLogged
|
||||
* @param $userIsAdmin
|
||||
* @return string
|
||||
*/
|
||||
private function render_aliases($aliases, $userIsLogged, $userIsAdmin) {
|
||||
private function render_aliases(array $aliases, bool $userIsLogged, bool $userIsAdmin): string
|
||||
{
|
||||
$html = "";
|
||||
if(count($aliases) > 0) {
|
||||
if (count($aliases) > 0) {
|
||||
$aliasViewLink = str_replace("_", " ", $aliases[0]['alias_name']); // no link anymore
|
||||
$aliasEditLink = "<a href='" . make_link("artist/alias/edit/" . $aliases[0]['alias_id']) . "'>Edit</a>";
|
||||
$aliasDeleteLink = "<a href='" . make_link("artist/alias/delete/" . $aliases[0]['alias_id']) . "'>Delete</a>";
|
||||
|
@ -404,11 +423,13 @@ class ArtistsTheme extends Themelet {
|
|||
<td class='left'>Aliases:</td>
|
||||
<td class='left'>" . $aliasViewLink . "</td>";
|
||||
|
||||
if ($userIsLogged)
|
||||
if ($userIsLogged) {
|
||||
$html .= "<td class='left'>" . $aliasEditLink . "</td>";
|
||||
}
|
||||
|
||||
if ($userIsAdmin)
|
||||
if ($userIsAdmin) {
|
||||
$html .= "<td class='left'>" . $aliasDeleteLink . "</td>";
|
||||
}
|
||||
|
||||
$html .= "</tr>";
|
||||
|
||||
|
@ -421,10 +442,12 @@ class ArtistsTheme extends Themelet {
|
|||
$html .= "<tr>
|
||||
<td class='left'> </td>
|
||||
<td class='left'>" . $aliasViewLink . "</td>";
|
||||
if ($userIsLogged)
|
||||
if ($userIsLogged) {
|
||||
$html .= "<td class='left'>" . $aliasEditLink . "</td>";
|
||||
if ($userIsAdmin)
|
||||
}
|
||||
if ($userIsAdmin) {
|
||||
$html .= "<td class='left'>" . $aliasDeleteLink . "</td>";
|
||||
}
|
||||
|
||||
$html .= "</tr>";
|
||||
}
|
||||
|
@ -433,15 +456,10 @@ class ArtistsTheme extends Themelet {
|
|||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $members
|
||||
* @param $userIsLogged
|
||||
* @param $userIsAdmin
|
||||
* @return string
|
||||
*/
|
||||
private function render_members($members, $userIsLogged, $userIsAdmin) {
|
||||
private function render_members(array $members, bool $userIsLogged, bool $userIsAdmin): string
|
||||
{
|
||||
$html = "";
|
||||
if(count($members) > 0) {
|
||||
if (count($members) > 0) {
|
||||
$memberViewLink = str_replace("_", " ", $members[0]['name']); // no link anymore
|
||||
$memberEditLink = "<a href='" . make_link("artist/member/edit/" . $members[0]['id']) . "'>Edit</a>";
|
||||
$memberDeleteLink = "<a href='" . make_link("artist/member/delete/" . $members[0]['id']) . "'>Delete</a>";
|
||||
|
@ -449,10 +467,12 @@ class ArtistsTheme extends Themelet {
|
|||
$html .= "<tr>
|
||||
<td class='left'>Members:</td>
|
||||
<td class='left'>" . $memberViewLink . "</td>";
|
||||
if ($userIsLogged)
|
||||
if ($userIsLogged) {
|
||||
$html .= "<td class='left'>" . $memberEditLink . "</td>";
|
||||
if ($userIsAdmin)
|
||||
}
|
||||
if ($userIsAdmin) {
|
||||
$html .= "<td class='left'>" . $memberDeleteLink . "</td>";
|
||||
}
|
||||
|
||||
$html .= "</tr>";
|
||||
|
||||
|
@ -465,10 +485,12 @@ class ArtistsTheme extends Themelet {
|
|||
$html .= "<tr>
|
||||
<td class='left'> </td>
|
||||
<td class='left'>" . $memberViewLink . "</td>";
|
||||
if ($userIsLogged)
|
||||
if ($userIsLogged) {
|
||||
$html .= "<td class='left'>" . $memberEditLink . "</td>";
|
||||
if ($userIsAdmin)
|
||||
}
|
||||
if ($userIsAdmin) {
|
||||
$html .= "<td class='left'>" . $memberDeleteLink . "</td>";
|
||||
}
|
||||
|
||||
$html .= "</tr>";
|
||||
}
|
||||
|
@ -477,15 +499,10 @@ class ArtistsTheme extends Themelet {
|
|||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $urls
|
||||
* @param $userIsLogged
|
||||
* @param $userIsAdmin
|
||||
* @return string
|
||||
*/
|
||||
private function render_urls($urls, $userIsLogged, $userIsAdmin) {
|
||||
private function render_urls(array $urls, bool $userIsLogged, bool $userIsAdmin): string
|
||||
{
|
||||
$html = "";
|
||||
if(count($urls) > 0) {
|
||||
if (count($urls) > 0) {
|
||||
$urlViewLink = "<a href='" . str_replace("_", " ", $urls[0]['url']) . "' target='_blank'>" . str_replace("_", " ", $urls[0]['url']) . "</a>";
|
||||
$urlEditLink = "<a href='" . make_link("artist/url/edit/" . $urls[0]['id']) . "'>Edit</a>";
|
||||
$urlDeleteLink = "<a href='" . make_link("artist/url/delete/" . $urls[0]['id']) . "'>Delete</a>";
|
||||
|
@ -494,11 +511,13 @@ class ArtistsTheme extends Themelet {
|
|||
<td class='left'>URLs:</td>
|
||||
<td class='left'>" . $urlViewLink . "</td>";
|
||||
|
||||
if ($userIsLogged)
|
||||
if ($userIsLogged) {
|
||||
$html .= "<td class='left'>" . $urlEditLink . "</td>";
|
||||
}
|
||||
|
||||
if ($userIsAdmin)
|
||||
if ($userIsAdmin) {
|
||||
$html .= "<td class='left'>" . $urlDeleteLink . "</td>";
|
||||
}
|
||||
|
||||
$html .= "</tr>";
|
||||
|
||||
|
@ -511,11 +530,13 @@ class ArtistsTheme extends Themelet {
|
|||
$html .= "<tr>
|
||||
<td class='left'> </td>
|
||||
<td class='left'>" . $urlViewLink . "</td>";
|
||||
if ($userIsLogged)
|
||||
if ($userIsLogged) {
|
||||
$html .= "<td class='left'>" . $urlEditLink . "</td>";
|
||||
}
|
||||
|
||||
if ($userIsAdmin)
|
||||
if ($userIsAdmin) {
|
||||
$html .= "<td class='left'>" . $urlDeleteLink . "</td>";
|
||||
}
|
||||
|
||||
$html .= "</tr>";
|
||||
}
|
||||
|
@ -525,5 +546,13 @@ class ArtistsTheme extends Themelet {
|
|||
return $html;
|
||||
}
|
||||
|
||||
public function get_help_html()
|
||||
{
|
||||
return '<p>Search for images with a particular artist.</p>
|
||||
<div class="command_example">
|
||||
<pre>artist=leonardo</pre>
|
||||
<p>Returns images with the artist "leonardo".</p>
|
||||
</div>
|
||||
';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
7
ext/auto_tagger/config.php
Normal file
7
ext/auto_tagger/config.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
abstract class AutoTaggerConfig
|
||||
{
|
||||
public const VERSION = "ext_auto_tagger_ver";
|
||||
public const ITEMS_PER_PAGE = "auto_tagger_items_per_page";
|
||||
}
|
12
ext/auto_tagger/info.php
Normal file
12
ext/auto_tagger/info.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
class AutoTaggerInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "auto_tagger";
|
||||
|
||||
public $key = self::KEY;
|
||||
public $name = "Auto-Tagger";
|
||||
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public $license = self::LICENSE_WTFPL;
|
||||
public $description = "Provides several automatic tagging functions";
|
||||
}
|
331
ext/auto_tagger/main.php
Normal file
331
ext/auto_tagger/main.php
Normal file
|
@ -0,0 +1,331 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
use MicroCRUD\ActionColumn;
|
||||
use MicroCRUD\TextColumn;
|
||||
use MicroCRUD\Table;
|
||||
|
||||
class AutoTaggerTable extends Table
|
||||
{
|
||||
public function __construct(\FFSPHP\PDO $db)
|
||||
{
|
||||
parent::__construct($db);
|
||||
$this->table = "auto_tagger";
|
||||
$this->base_query = "SELECT * FROM auto_tag";
|
||||
$this->primary_key = "tag";
|
||||
$this->size = 100;
|
||||
$this->limit = 1000000;
|
||||
$this->set_columns([
|
||||
new TextColumn("tag", "Tag"),
|
||||
new TextColumn("additional_tags", "Additional Tags"),
|
||||
new ActionColumn("tag"),
|
||||
]);
|
||||
$this->order_by = ["tag"];
|
||||
$this->table_attrs = ["class" => "zebra"];
|
||||
}
|
||||
}
|
||||
|
||||
class AddAutoTagEvent extends Event
|
||||
{
|
||||
/** @var string */
|
||||
public $tag;
|
||||
/** @var string */
|
||||
public $additional_tags;
|
||||
|
||||
public function __construct(string $tag, string $additional_tags)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->tag = trim($tag);
|
||||
$this->additional_tags = trim($additional_tags);
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteAutoTagEvent extends Event
|
||||
{
|
||||
public $tag;
|
||||
|
||||
public function __construct(string $tag)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->tag = $tag;
|
||||
}
|
||||
}
|
||||
|
||||
class AddAutoTagException extends SCoreException
|
||||
{
|
||||
}
|
||||
|
||||
class AutoTagger extends Extension
|
||||
{
|
||||
/** @var AutoTaggerTheme */
|
||||
protected $theme;
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $config, $database, $page, $user;
|
||||
|
||||
if ($event->page_matches("auto_tag")) {
|
||||
if ($event->get_arg(0) == "add") {
|
||||
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
|
||||
$user->ensure_authed();
|
||||
$input = validate_input(["c_tag"=>"string", "c_additional_tags"=>"string"]);
|
||||
try {
|
||||
send_event(new AddAutoTagEvent($input['c_tag'], $input['c_additional_tags']));
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("auto_tag/list"));
|
||||
} catch (AddAutoTagException $ex) {
|
||||
$this->theme->display_error(500, "Error adding auto-tag", $ex->getMessage());
|
||||
}
|
||||
}
|
||||
} elseif ($event->get_arg(0) == "remove") {
|
||||
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
|
||||
$user->ensure_authed();
|
||||
$input = validate_input(["d_tag"=>"string"]);
|
||||
send_event(new DeleteAutoTagEvent($input['d_tag']));
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("auto_tag/list"));
|
||||
}
|
||||
} elseif ($event->get_arg(0) == "list") {
|
||||
$t = new AutoTaggerTable($database->raw_db());
|
||||
$t->token = $user->get_auth_token();
|
||||
$t->inputs = $_GET;
|
||||
$t->size = $config->get_int(AutoTaggerConfig::ITEMS_PER_PAGE, 30);
|
||||
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
|
||||
$t->create_url = make_link("auto_tag/add");
|
||||
$t->delete_url = make_link("auto_tag/remove");
|
||||
}
|
||||
$this->theme->display_auto_tagtable($t->table($t->query()), $t->paginator());
|
||||
} elseif ($event->get_arg(0) == "export") {
|
||||
$page->set_mode(PageMode::DATA);
|
||||
$page->set_type("text/csv");
|
||||
$page->set_filename("auto_tag.csv");
|
||||
$page->set_data($this->get_auto_tag_csv($database));
|
||||
} elseif ($event->get_arg(0) == "import") {
|
||||
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
|
||||
if (count($_FILES) > 0) {
|
||||
$tmp = $_FILES['auto_tag_file']['tmp_name'];
|
||||
$contents = file_get_contents($tmp);
|
||||
$count = $this->add_auto_tag_csv($database, $contents);
|
||||
log_info(AutoTaggerInfo::KEY, "Imported $count auto-tag definitions from file from file", "Imported $count auto-tag definitions");
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("auto_tag/list"));
|
||||
} else {
|
||||
$this->theme->display_error(400, "No File Specified", "You have to upload a file");
|
||||
}
|
||||
} else {
|
||||
$this->theme->display_error(401, "Admins Only", "Only admins can edit the auto-tag list");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
|
||||
{
|
||||
if ($event->parent=="tags") {
|
||||
$event->add_nav_link("auto_tag", new Link('auto_tag/list'), "Auto-Tag", NavLink::is_active(["auto_tag"]));
|
||||
}
|
||||
}
|
||||
|
||||
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
|
||||
{
|
||||
global $database;
|
||||
|
||||
// Create the database tables
|
||||
if ($this->get_version(AutoTaggerConfig::VERSION) < 1) {
|
||||
$database->create_table("auto_tag", "
|
||||
tag VARCHAR(128) NOT NULL PRIMARY KEY,
|
||||
additional_tags VARCHAR(2000) NOT NULL
|
||||
");
|
||||
|
||||
if ($database->get_driver_name() == DatabaseDriver::PGSQL) {
|
||||
$database->execute('CREATE INDEX auto_tag_lower_tag_idx ON auto_tag ((lower(tag)))');
|
||||
}
|
||||
$this->set_version(AutoTaggerConfig::VERSION, 1);
|
||||
|
||||
log_info(AutoTaggerInfo::KEY, "extension installed");
|
||||
}
|
||||
}
|
||||
|
||||
public function onTagSet(TagSetEvent $event)
|
||||
{
|
||||
$results = $this->apply_auto_tags($event->tags);
|
||||
if (!empty($results)) {
|
||||
$event->tags = $results;
|
||||
}
|
||||
}
|
||||
|
||||
public function onAddAutoTag(AddAutoTagEvent $event)
|
||||
{
|
||||
global $page;
|
||||
$this->add_auto_tag($event->tag, $event->additional_tags);
|
||||
$page->flash("Added Auto-Tag");
|
||||
}
|
||||
|
||||
public function onDeleteAutoTag(DeleteAutoTagEvent $event)
|
||||
{
|
||||
$this->remove_auto_tag($event->tag);
|
||||
}
|
||||
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
|
||||
$event->add_link("Auto-Tag Editor", make_link("auto_tag/list"));
|
||||
}
|
||||
}
|
||||
|
||||
private function get_auto_tag_csv(Database $database): string
|
||||
{
|
||||
$csv = "";
|
||||
$pairs = $database->get_pairs("SELECT tag, additional_tags FROM auto_tag ORDER BY tag");
|
||||
foreach ($pairs as $old => $new) {
|
||||
$csv .= "\"$old\",\"$new\"\n";
|
||||
}
|
||||
return $csv;
|
||||
}
|
||||
|
||||
private function add_auto_tag_csv(Database $database, string $csv): int
|
||||
{
|
||||
$csv = str_replace("\r", "\n", $csv);
|
||||
$i = 0;
|
||||
foreach (explode("\n", $csv) as $line) {
|
||||
$parts = str_getcsv($line);
|
||||
if (count($parts) == 2) {
|
||||
try {
|
||||
send_event(new AddAutoTagEvent($parts[0], $parts[1]));
|
||||
$i++;
|
||||
} catch (AddAutoTagException $ex) {
|
||||
$this->theme->display_error(500, "Error adding auto-tags", $ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
return $i;
|
||||
}
|
||||
|
||||
private function add_auto_tag(string $tag, string $additional_tags)
|
||||
{
|
||||
global $database;
|
||||
if ($database->exists("SELECT * FROM auto_tag WHERE LOWER(tag)=LOWER(:tag)", ["tag"=>$tag])) {
|
||||
throw new AutoTaggerException("Auto-Tag is already set for that tag");
|
||||
} else {
|
||||
$tag = Tag::sanitize($tag);
|
||||
$additional_tags = Tag::explode($additional_tags);
|
||||
|
||||
$database->execute(
|
||||
"INSERT INTO auto_tag(tag, additional_tags) VALUES(:tag, :additional_tags)",
|
||||
["tag"=>$tag, "additional_tags"=>Tag::implode($additional_tags)]
|
||||
);
|
||||
|
||||
log_info(
|
||||
AutoTaggerInfo::KEY,
|
||||
"Added auto-tag for {$tag} -> {".implode(" ", $additional_tags)."}"
|
||||
);
|
||||
|
||||
// Now we apply it to existing items
|
||||
$this->apply_new_auto_tag($tag);
|
||||
}
|
||||
}
|
||||
|
||||
private function update_auto_tag(string $tag, string $additional_tags): bool
|
||||
{
|
||||
global $database;
|
||||
$result = $database->get_row("SELECT * FROM auto_tag WHERE LOWER(tag)=LOWER(:tag)", ["tag"=>$tag]);
|
||||
|
||||
if ($result===null) {
|
||||
throw new AutoTaggerException("Auto-tag not set for $tag, can't update");
|
||||
} else {
|
||||
$additional_tags = Tag::explode($additional_tags);
|
||||
$current_additional_tags = Tag::explode($result["additional_tags"]);
|
||||
|
||||
if (!Tag::compare($additional_tags, $current_additional_tags)) {
|
||||
$database->execute(
|
||||
"UPDATE auto_tag SET additional_tags = :additional_tags WHERE LOWER(tag)=LOWER(:tag)",
|
||||
["tag"=>$tag, "additional_tags"=>Tag::implode($additional_tags)]
|
||||
);
|
||||
|
||||
log_info(
|
||||
AutoTaggerInfo::KEY,
|
||||
"Updated auto-tag for {$tag} -> {".implode(" ", $additional_tags)."}",
|
||||
"Updated Auto-Tag"
|
||||
);
|
||||
|
||||
// Now we apply it to existing items
|
||||
$this->apply_new_auto_tag($tag);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function apply_new_auto_tag(string $tag)
|
||||
{
|
||||
global $database;
|
||||
$tag_id = $database->get_one("SELECT id FROM tags WHERE LOWER(tag) = LOWER(:tag)", ["tag"=>$tag]);
|
||||
if (!empty($tag_id)) {
|
||||
$image_ids = $database->get_col_iterable("SELECT image_id FROM image_tags WHERE tag_id = :tag_id", ["tag_id"=>$tag_id]);
|
||||
foreach ($image_ids as $image_id) {
|
||||
$image = Image::by_id($image_id);
|
||||
$event = new TagSetEvent($image, $image->get_tag_array());
|
||||
send_event($event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function remove_auto_tag(String $tag)
|
||||
{
|
||||
global $database;
|
||||
|
||||
$database->execute("DELETE FROM auto_tag WHERE LOWER(tag)=LOWER(:tag)", ["tag" => $tag]);
|
||||
}
|
||||
|
||||
/**
|
||||
* #param string[] $tags_mixed
|
||||
*/
|
||||
private function apply_auto_tags(array $tags_mixed): ?array
|
||||
{
|
||||
global $database;
|
||||
|
||||
while (true) {
|
||||
$new_tags = [];
|
||||
foreach ($tags_mixed as $tag) {
|
||||
$additional_tags = $database->get_one(
|
||||
"SELECT additional_tags FROM auto_tag WHERE LOWER(tag) = LOWER(:input)",
|
||||
["input" => $tag]
|
||||
);
|
||||
|
||||
if (!empty($additional_tags)) {
|
||||
$additional_tags = explode(" ", $additional_tags);
|
||||
$new_tags = array_merge(
|
||||
$new_tags,
|
||||
array_udiff($additional_tags, $tags_mixed, 'strcasecmp')
|
||||
);
|
||||
}
|
||||
}
|
||||
if (empty($new_tags)) {
|
||||
break;
|
||||
}
|
||||
$tags_mixed = array_merge($tags_mixed, $new_tags);
|
||||
}
|
||||
|
||||
$results = array_intersect_key(
|
||||
$tags_mixed,
|
||||
array_unique(array_map('strtolower', $tags_mixed))
|
||||
);
|
||||
|
||||
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the priority for this extension.
|
||||
*
|
||||
*/
|
||||
public function get_priority(): int
|
||||
{
|
||||
return 30;
|
||||
}
|
||||
}
|
58
ext/auto_tagger/test.php
Normal file
58
ext/auto_tagger/test.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php declare(strict_types=1);
|
||||
class AutoTaggerTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testAutoTaggerList()
|
||||
{
|
||||
$this->get_page('auto_tag/list');
|
||||
$this->assert_response(200);
|
||||
$this->assert_title("Auto-Tag");
|
||||
}
|
||||
|
||||
public function testAutoTaggerListReadOnly()
|
||||
{
|
||||
$this->log_in_as_user();
|
||||
$this->get_page('auto_tag/list');
|
||||
$this->assert_title("Auto-Tag");
|
||||
$this->assert_no_text("value=\"Add\"");
|
||||
|
||||
$this->log_out();
|
||||
$this->get_page('auto_tag/list');
|
||||
$this->assert_title("Auto-Tag");
|
||||
$this->assert_no_text("value=\"Add\"");
|
||||
}
|
||||
|
||||
public function testAutoTagger()
|
||||
{
|
||||
$this->log_in_as_admin();
|
||||
|
||||
$this->get_page("auto_tag/export/auto_tag.csv");
|
||||
$this->assert_no_text("test1");
|
||||
|
||||
send_event(new AddAutoTagEvent("test1", "test2"));
|
||||
$this->get_page('auto_tag/list');
|
||||
$this->assert_text("test1");
|
||||
$this->assert_text("test2");
|
||||
$this->get_page("auto_tag/export/auto_tag.csv");
|
||||
$this->assert_text('"test1","test2"');
|
||||
|
||||
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
|
||||
$this->get_page("post/view/$image_id"); # check that the tag has been replaced
|
||||
$this->assert_title("Image $image_id: test1 test2");
|
||||
$this->delete_image($image_id);
|
||||
|
||||
send_event(new AddAutoTagEvent("test2", "test3"));
|
||||
|
||||
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
|
||||
$this->get_page("post/view/$image_id"); # check that the tag has been replaced
|
||||
$this->assert_title("Image $image_id: test1 test2 test3");
|
||||
$this->delete_image($image_id);
|
||||
|
||||
send_event(new DeleteAutoTagEvent("test1"));
|
||||
send_event(new DeleteAutoTagEvent("test2"));
|
||||
$this->get_page('auto_tag/list');
|
||||
$this->assert_title("Auto-Tag");
|
||||
$this->assert_no_text("test1");
|
||||
$this->assert_no_text("test2");
|
||||
$this->assert_no_text("test3");
|
||||
}
|
||||
}
|
36
ext/auto_tagger/theme.php
Normal file
36
ext/auto_tagger/theme.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
class AutoTaggerTheme extends Themelet
|
||||
{
|
||||
/**
|
||||
* Show a page of auto-tag definitions.
|
||||
*
|
||||
* Note: $can_manage = whether things like "add new alias" should be shown
|
||||
*/
|
||||
public function display_auto_tagtable($table, $paginator): void
|
||||
{
|
||||
global $page, $user;
|
||||
|
||||
$can_manage = $user->can(Permissions::MANAGE_AUTO_TAG);
|
||||
$html = "
|
||||
$table
|
||||
$paginator
|
||||
<p><a href='".make_link("auto_tag/export/auto_tag.csv")."' download='auto_tag.csv'>Download as CSV</a></p>
|
||||
";
|
||||
|
||||
$bulk_html = "
|
||||
".make_form(make_link("auto_tag/import"), 'post', true)."
|
||||
<input type='file' name='auto_tag_file'>
|
||||
<input type='submit' value='Upload List'>
|
||||
</form>
|
||||
";
|
||||
|
||||
$page->set_title("Auto-Tag List");
|
||||
$page->set_heading("Auto-Tag List");
|
||||
$page->add_block(new NavBlock());
|
||||
$page->add_block(new Block("Auto-Tag", $html));
|
||||
if ($can_manage) {
|
||||
$page->add_block(new Block("Bulk Upload", $bulk_html, "main", 51));
|
||||
}
|
||||
}
|
||||
}
|
11
ext/autocomplete/info.php
Normal file
11
ext/autocomplete/info.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
class AutoCompleteInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "autocomplete";
|
||||
|
||||
public $key = self::KEY;
|
||||
public $name = "Autocomplete";
|
||||
public $authors = ["Daku"=>"admin@codeanimu.net"];
|
||||
public $description = "Adds autocomplete to search & tagging.";
|
||||
}
|
|
@ -14,8 +14,7 @@ ul.tagit li.tagit-choice {
|
|||
-webkit-border-radius: 6px;
|
||||
border: 1px solid #CAD8F3;
|
||||
|
||||
background: none;
|
||||
background-color: #DEE7F8;
|
||||
background: #DEE7F8 none;
|
||||
|
||||
font-weight: normal;
|
||||
}
|
||||
|
|
|
@ -1,44 +1,66 @@
|
|||
<?php
|
||||
/*
|
||||
* Name: Autocomplete
|
||||
* Author: Daku <admin@codeanimu.net>
|
||||
* Description: Adds autocomplete to search & tagging.
|
||||
*/
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
class AutoComplete extends Extension {
|
||||
public function get_priority() {return 30;} // before Home
|
||||
class AutoComplete extends Extension
|
||||
{
|
||||
/** @var AutoCompleteTheme */
|
||||
protected $theme;
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event) {
|
||||
global $page, $database;
|
||||
public function get_priority(): int
|
||||
{
|
||||
return 30;
|
||||
} // before Home
|
||||
|
||||
if($event->page_matches("api/internal/autocomplete")) {
|
||||
if(!isset($_GET["s"])) return;
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $cache, $page, $database;
|
||||
|
||||
if ($event->page_matches("api/internal/autocomplete")) {
|
||||
if (!isset($_GET["s"])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$page->set_mode(PageMode::DATA);
|
||||
$page->set_type("application/json");
|
||||
|
||||
$s = strtolower($_GET["s"]);
|
||||
if (
|
||||
$s == '' ||
|
||||
$s[0] == '_' ||
|
||||
$s[0] == '%' ||
|
||||
strlen($s) > 32
|
||||
) {
|
||||
$page->set_data("{}");
|
||||
return;
|
||||
}
|
||||
|
||||
//$limit = 0;
|
||||
$cache_key = "autocomplete-" . strtolower($_GET["s"]);
|
||||
$cache_key = "autocomplete-$s";
|
||||
$limitSQL = "";
|
||||
$SQLarr = array("search"=>$_GET["s"]."%");
|
||||
if(isset($_GET["limit"]) && $_GET["limit"] !== 0){
|
||||
$s = str_replace('_', '\_', $s);
|
||||
$s = str_replace('%', '\%', $s);
|
||||
$SQLarr = ["search"=>"$s%"]; #, "cat_search"=>"%:$s%"];
|
||||
if (isset($_GET["limit"]) && $_GET["limit"] !== 0) {
|
||||
$limitSQL = "LIMIT :limit";
|
||||
$SQLarr['limit'] = $_GET["limit"];
|
||||
$cache_key .= "-" . $_GET["limit"];
|
||||
}
|
||||
|
||||
$res = $database->cache->get($cache_key);
|
||||
if(!$res) {
|
||||
$res = $database->get_pairs($database->scoreql_to_sql("
|
||||
$res = $cache->get($cache_key);
|
||||
if (!$res) {
|
||||
$res = $database->get_pairs(
|
||||
"
|
||||
SELECT tag, count
|
||||
FROM tags
|
||||
WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:search)
|
||||
WHERE LOWER(tag) LIKE LOWER(:search)
|
||||
-- OR LOWER(tag) LIKE LOWER(:cat_search)
|
||||
AND count > 0
|
||||
ORDER BY count DESC
|
||||
$limitSQL"), $SQLarr
|
||||
$limitSQL",
|
||||
$SQLarr
|
||||
);
|
||||
$database->cache->set($cache_key, $res, 600);
|
||||
$cache->set($cache_key, $res, 600);
|
||||
}
|
||||
|
||||
$page->set_mode("data");
|
||||
$page->set_type("application/json");
|
||||
$page->set_data(json_encode($res));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
$(function(){
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
var metatags = ['order:id', 'order:width', 'order:height', 'order:filesize', 'order:filename'];
|
||||
|
||||
$('[name=search]').tagit({
|
||||
$('[name="search"]').tagit({
|
||||
singleFieldDelimiter: ' ',
|
||||
beforeTagAdded: function(event, ui) {
|
||||
if(metatags.indexOf(ui.tagLabel) !== -1) {
|
||||
|
@ -51,7 +51,7 @@ $(function(){
|
|||
);
|
||||
},
|
||||
error : function (request, status, error) {
|
||||
alert(error);
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -66,7 +66,7 @@ $(function(){
|
|||
if(keyCode == 32) {
|
||||
e.preventDefault();
|
||||
|
||||
$('[name=search]').tagit('createTag', $(this).val());
|
||||
$('.autocomplete_tags').tagit('createTag', $(this).val());
|
||||
$(this).autocomplete('close');
|
||||
} else if (keyCode == 9) {
|
||||
e.preventDefault();
|
||||
|
|
14
ext/autocomplete/test.php
Normal file
14
ext/autocomplete/test.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class AutoCompleteTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testAuth()
|
||||
{
|
||||
send_event(new UserLoginEvent(User::by_name(self::$anon_name)));
|
||||
$page = $this->get_page('api/internal/autocomplete', ["s"=>"not-a-tag"]);
|
||||
$this->assertEquals(200, $page->code);
|
||||
$this->assertEquals(PageMode::DATA, $page->mode);
|
||||
$this->assertEquals("[]", $page->data);
|
||||
}
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
class AutoCompleteTheme extends Themelet {
|
||||
public function build_autocomplete(Page $page) {
|
||||
class AutoCompleteTheme extends Themelet
|
||||
{
|
||||
public function build_autocomplete(Page $page)
|
||||
{
|
||||
$base_href = get_base_href();
|
||||
// TODO: AJAX test and fallback.
|
||||
|
||||
$page->add_html_header("<script src='$base_href/ext/autocomplete/lib/jquery-ui.min.js' type='text/javascript'></script>");
|
||||
$page->add_html_header("<script src='$base_href/ext/autocomplete/lib/tag-it.min.js' type='text/javascript'></script>");
|
||||
$page->add_html_header("<script defer src='$base_href/ext/autocomplete/lib/jquery-ui.min.js' type='text/javascript'></script>");
|
||||
$page->add_html_header("<script defer src='$base_href/ext/autocomplete/lib/tag-it.min.js' type='text/javascript'></script>");
|
||||
$page->add_html_header('<link rel="stylesheet" type="text/css" href="//ajax.googleapis.com/ajax/libs/jqueryui/1/themes/flick/jquery-ui.css">');
|
||||
$page->add_html_header("<link rel='stylesheet' type='text/css' href='$base_href/ext/autocomplete/lib/jquery.tagit.css' />");
|
||||
}
|
||||
|
|
26
ext/ban_words/info.php
Normal file
26
ext/ban_words/info.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
class BanWordsInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "ban_words";
|
||||
|
||||
public $key = self::KEY;
|
||||
public $name = "Comment Word Ban";
|
||||
public $url = self::SHIMMIE_URL;
|
||||
public $authors = self::SHISH_AUTHOR;
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $description = "For stopping spam and other comment abuse";
|
||||
public $documentation =
|
||||
"Allows an administrator to ban certain words
|
||||
from comments. This can be a very simple but effective way
|
||||
of stopping spam; just add \"viagra\", \"porn\", etc to the
|
||||
banned words list.
|
||||
<p>Regex bans are also supported, allowing more complicated
|
||||
bans like <code>/http:.*\.cn\//</code> to block links to
|
||||
chinese websites, or <code>/.*?http.*?http.*?http.*?http.*?/</code>
|
||||
to block comments with four (or more) links in.
|
||||
<p>Note that for non-regex matches, only whole words are
|
||||
matched, eg banning \"sex\" would block the comment \"get free
|
||||
sex call this number\", but allow \"This is a photo of Bob
|
||||
from Essex\"";
|
||||
}
|
|
@ -1,27 +1,9 @@
|
|||
<?php
|
||||
/*
|
||||
* Name: Comment Word Ban
|
||||
* Author: Shish <webmaster@shishnet.org>
|
||||
* Link: http://code.shishnet.org/shimmie2/
|
||||
* License: GPLv2
|
||||
* Description: For stopping spam and other comment abuse
|
||||
* Documentation:
|
||||
* Allows an administrator to ban certain words
|
||||
* from comments. This can be a very simple but effective way
|
||||
* of stopping spam; just add "viagra", "porn", etc to the
|
||||
* banned words list.
|
||||
* <p>Regex bans are also supported, allowing more complicated
|
||||
* bans like <code>/http:.*\.cn\//</code> to block links to
|
||||
* chinese websites, or <code>/.*?http.*?http.*?http.*?http.*?/</code>
|
||||
* to block comments with four (or more) links in.
|
||||
* <p>Note that for non-regex matches, only whole words are
|
||||
* matched, eg banning "sex" would block the comment "get free
|
||||
* sex call this number", but allow "This is a photo of Bob
|
||||
* from Essex"
|
||||
*/
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
class BanWords extends Extension {
|
||||
public function onInitExt(InitExtEvent $event) {
|
||||
class BanWords extends Extension
|
||||
{
|
||||
public function onInitExt(InitExtEvent $event)
|
||||
{
|
||||
global $config;
|
||||
$config->set_default_string('banned_words', "
|
||||
a href=
|
||||
|
@ -53,34 +35,38 @@ xanax
|
|||
");
|
||||
}
|
||||
|
||||
public function onCommentPosting(CommentPostingEvent $event) {
|
||||
public function onCommentPosting(CommentPostingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if(!$user->can("bypass_comment_checks")) {
|
||||
if (!$user->can(Permissions::BYPASS_COMMENT_CHECKS)) {
|
||||
$this->test_text($event->comment, new CommentPostingException("Comment contains banned terms"));
|
||||
}
|
||||
}
|
||||
|
||||
public function onSourceSet(SourceSetEvent $event) {
|
||||
public function onSourceSet(SourceSetEvent $event)
|
||||
{
|
||||
$this->test_text($event->source, new SCoreException("Source contains banned terms"));
|
||||
}
|
||||
|
||||
public function onTagSet(TagSetEvent $event) {
|
||||
public function onTagSet(TagSetEvent $event)
|
||||
{
|
||||
$this->test_text(Tag::implode($event->tags), new SCoreException("Tags contain banned terms"));
|
||||
}
|
||||
|
||||
public function onSetupBuilding(SetupBuildingEvent $event) {
|
||||
public function onSetupBuilding(SetupBuildingEvent $event)
|
||||
{
|
||||
$sb = new SetupBlock("Banned Phrases");
|
||||
$sb->add_label("One per line, lines that start with slashes are treated as regex<br/>");
|
||||
$sb->add_longtext_option("banned_words");
|
||||
$failed = array();
|
||||
foreach($this->get_words() as $word) {
|
||||
if($word[0] == '/') {
|
||||
if(preg_match($word, "") === false) {
|
||||
$failed = [];
|
||||
foreach ($this->get_words() as $word) {
|
||||
if ($word[0] == '/') {
|
||||
if (preg_match($word, "") === false) {
|
||||
$failed[] = $word;
|
||||
}
|
||||
}
|
||||
}
|
||||
if($failed) {
|
||||
if ($failed) {
|
||||
$sb->add_label("Failed regexes: ".join(", ", $failed));
|
||||
}
|
||||
$event->panel->add_block($sb);
|
||||
|
@ -88,40 +74,35 @@ xanax
|
|||
|
||||
/**
|
||||
* Throws if the comment contains banned words.
|
||||
* @param string $comment
|
||||
* @param CommentPostingException|SCoreException $ex
|
||||
* @throws CommentPostingException|SCoreException if the comment contains banned words.
|
||||
*/
|
||||
private function test_text($comment, $ex) {
|
||||
private function test_text(string $comment, SCoreException $ex): void
|
||||
{
|
||||
$comment = strtolower($comment);
|
||||
|
||||
foreach($this->get_words() as $word) {
|
||||
if($word[0] == '/') {
|
||||
foreach ($this->get_words() as $word) {
|
||||
if ($word[0] == '/') {
|
||||
// lines that start with slash are regex
|
||||
if(preg_match($word, $comment) === 1) {
|
||||
if (preg_match($word, $comment) === 1) {
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// other words are literal
|
||||
if(strpos($comment, $word) !== false) {
|
||||
if (strpos($comment, $word) !== false) {
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function get_words() {
|
||||
private function get_words(): array
|
||||
{
|
||||
global $config;
|
||||
$words = array();
|
||||
$words = [];
|
||||
|
||||
$banned = $config->get_string("banned_words");
|
||||
foreach(explode("\n", $banned) as $word) {
|
||||
foreach (explode("\n", $banned) as $word) {
|
||||
$word = trim(strtolower($word));
|
||||
if(strlen($word) == 0) {
|
||||
if (strlen($word) == 0) {
|
||||
// line is blank
|
||||
continue;
|
||||
}
|
||||
|
@ -131,6 +112,8 @@ xanax
|
|||
return $words;
|
||||
}
|
||||
|
||||
public function get_priority() {return 30;}
|
||||
public function get_priority(): int
|
||||
{
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
<?php
|
||||
class BanWordsTest extends ShimmiePHPUnitTestCase {
|
||||
public function check_blocked($image_id, $words) {
|
||||
<?php declare(strict_types=1);
|
||||
class BanWordsTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function check_blocked($image_id, $words)
|
||||
{
|
||||
global $user;
|
||||
try {
|
||||
send_event(new CommentPostingEvent($image_id, $user, $words));
|
||||
$this->fail("Exception not thrown");
|
||||
}
|
||||
catch(CommentPostingException $e) {
|
||||
} catch (CommentPostingException $e) {
|
||||
$this->assertEquals($e->getMessage(), "Comment contains banned terms");
|
||||
}
|
||||
}
|
||||
|
||||
public function testWordBan() {
|
||||
public function testWordBan()
|
||||
{
|
||||
global $config;
|
||||
$config->set_string("banned_words", "viagra\nporn\n\n/http:.*\.cn\//");
|
||||
|
||||
|
@ -30,4 +32,3 @@ class BanWordsTest extends ShimmiePHPUnitTestCase {
|
|||
$this->assert_no_text('http://something.cn/');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
32
ext/bbcode/info.php
Normal file
32
ext/bbcode/info.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
class BBCodeInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "bbcode";
|
||||
|
||||
public $key = self::KEY;
|
||||
public $name = "BBCode";
|
||||
public $url = self::SHIMMIE_URL;
|
||||
public $authors = self::SHISH_AUTHOR;
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $core = true;
|
||||
public $description = "Turns BBCode into HTML";
|
||||
public $documentation =
|
||||
" Supported tags:
|
||||
<ul>
|
||||
<li>[img]url[/img]
|
||||
<li>[url]<a href=\"{self::SHIMMIE_URL}\">http://code.shishnet.org/</a>[/url]
|
||||
<li>[email]<a href=\"mailto:{self::SHISH_EMAIL}\">webmaster@shishnet.org</a>[/email]
|
||||
<li>[b]<b>bold</b>[/b]
|
||||
<li>[i]<i>italic</i>[/i]
|
||||
<li>[u]<u>underline</u>[/u]
|
||||
<li>[s]<s>strikethrough</s>[/s]
|
||||
<li>[sup]<sup>superscript</sup>[/sup]
|
||||
<li>[sub]<sub>subscript</sub>[/sub]
|
||||
<li>[[wiki article]]
|
||||
<li>[[wiki article|with some text]]
|
||||
<li>[quote]text[/quote]
|
||||
<li>[quote=Username]text[/quote]
|
||||
<li>>>123 (link to image #123)
|
||||
</ul>";
|
||||
}
|
|
@ -1,40 +1,14 @@
|
|||
<?php
|
||||
/**
|
||||
* Name: BBCode
|
||||
* Author: Shish <webmaster@shishnet.org>
|
||||
* Link: http://code.shishnet.org/shimmie2/
|
||||
* License: GPLv2
|
||||
* Description: Turns BBCode into HTML
|
||||
* Documentation:
|
||||
* Supported tags:
|
||||
* <ul>
|
||||
* <li>[img]url[/img]
|
||||
* <li>[url]<a href="http://code.shishnet.org/shimmie2/">http://code.shishnet.org/</a>[/url]
|
||||
* <li>[email]<a href="mailto:webmaster@shishnet.org">webmaster@shishnet.org</a>[/email]
|
||||
* <li>[b]<b>bold</b>[/b]
|
||||
* <li>[i]<i>italic</i>[/i]
|
||||
* <li>[u]<u>underline</u>[/u]
|
||||
* <li>[s]<s>strikethrough</s>[/s]
|
||||
* <li>[sup]<sup>superscript</sup>[/sup]
|
||||
* <li>[sub]<sub>subscript</sub>[/sub]
|
||||
* <li>[[wiki article]]
|
||||
* <li>[[wiki article|with some text]]
|
||||
* <li>[quote]text[/quote]
|
||||
* <li>[quote=Username]text[/quote]
|
||||
* <li>>>123 (link to image #123)
|
||||
* </ul>
|
||||
*/
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
class BBCode extends FormatterExtension {
|
||||
/**
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
public function format(/*string*/ $text) {
|
||||
|
||||
class BBCode extends FormatterExtension
|
||||
{
|
||||
public function format(string $text): string
|
||||
{
|
||||
$text = $this->extract_code($text);
|
||||
foreach(array(
|
||||
foreach ([
|
||||
"b", "i", "u", "s", "sup", "sub", "h1", "h2", "h3", "h4",
|
||||
) as $el) {
|
||||
] as $el) {
|
||||
$text = preg_replace("!\[$el\](.*?)\[/$el\]!s", "<$el>$1</$el>", $text);
|
||||
}
|
||||
$text = preg_replace('!^>>([^\d].+)!', '<blockquote><small>$1</small></blockquote>', $text);
|
||||
|
@ -45,19 +19,22 @@ class BBCode extends FormatterExtension {
|
|||
$text = preg_replace('!\[url=((?:https?|ftp|irc|mailto)://.*?)\](.*?)\[/url\]!s', '<a href="$1">$2</a>', $text);
|
||||
$text = preg_replace('!\[url\]((?:https?|ftp|irc|mailto)://.*?)\[/url\]!s', '<a href="$1">$1</a>', $text);
|
||||
$text = preg_replace('!\[email\](.*?)\[/email\]!s', '<a href="mailto:$1">$1</a>', $text);
|
||||
$text = preg_replace('!\[img\](https?:\/\/.*?)\[/img\]!s', '<img src="$1">', $text);
|
||||
$text = preg_replace('!\[img\](https?:\/\/.*?)\[/img\]!s', '<img alt="user image" src="$1">', $text);
|
||||
$text = preg_replace('!\[\[([^\|\]]+)\|([^\]]+)\]\]!s', '<a href="'.make_link('wiki/$1').'">$2</a>', $text);
|
||||
$text = preg_replace('!\[\[([^\]]+)\]\]!s', '<a href="'.make_link('wiki/$1').'">$1</a>', $text);
|
||||
$text = preg_replace("!\n\s*\n!", "\n\n", $text);
|
||||
$text = str_replace("\n", "\n<br>", $text);
|
||||
$text = preg_replace("/\[quote\](.*?)\[\/quote\]/s", "<blockquote><small>\\1</small></blockquote>", $text);
|
||||
$text = preg_replace("/\[quote=(.*?)\](.*?)\[\/quote\]/s", "<blockquote><em>\\1 said:</em><br><small>\\2</small></blockquote>", $text);
|
||||
while(preg_match("/\[list\](.*?)\[\/list\]/s", $text))
|
||||
while (preg_match("/\[list\](.*?)\[\/list\]/s", $text)) {
|
||||
$text = preg_replace("/\[list\](.*?)\[\/list\]/s", "<ul>\\1</ul>", $text);
|
||||
while(preg_match("/\[ul\](.*?)\[\/ul\]/s", $text))
|
||||
}
|
||||
while (preg_match("/\[ul\](.*?)\[\/ul\]/s", $text)) {
|
||||
$text = preg_replace("/\[ul\](.*?)\[\/ul\]/s", "<ul>\\1</ul>", $text);
|
||||
while(preg_match("/\[ol\](.*?)\[\/ol\]/s", $text))
|
||||
}
|
||||
while (preg_match("/\[ol\](.*?)\[\/ol\]/s", $text)) {
|
||||
$text = preg_replace("/\[ol\](.*?)\[\/ol\]/s", "<ol>\\1</ol>", $text);
|
||||
}
|
||||
$text = preg_replace("/\[li\](.*?)\[\/li\]/s", "<li>\\1</li>", $text);
|
||||
$text = preg_replace("#\[\*\]#s", "<li>", $text);
|
||||
$text = preg_replace("#<br><(li|ul|ol|/ul|/ol)>#s", "<\\1>", $text);
|
||||
|
@ -67,15 +44,12 @@ class BBCode extends FormatterExtension {
|
|||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
public function strip(/*string*/ $text) {
|
||||
foreach(array(
|
||||
public function strip(string $text): string
|
||||
{
|
||||
foreach ([
|
||||
"b", "i", "u", "s", "sup", "sub", "h1", "h2", "h3", "h4",
|
||||
"code", "url", "email", "li",
|
||||
) as $el) {
|
||||
] as $el) {
|
||||
$text = preg_replace("!\[$el\](.*?)\[/$el\]!s", '$1', $text);
|
||||
}
|
||||
$text = preg_replace("!\[anchor=(.*?)\](.*?)\[/anchor\]!s", '$2', $text);
|
||||
|
@ -91,32 +65,33 @@ class BBCode extends FormatterExtension {
|
|||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
private function filter_spoiler(/*string*/ $text) {
|
||||
private function filter_spoiler(string $text): string
|
||||
{
|
||||
return str_replace(
|
||||
array("[spoiler]","[/spoiler]"),
|
||||
array("<span style=\"background-color:#000; color:#000;\">","</span>"),
|
||||
$text);
|
||||
["[spoiler]","[/spoiler]"],
|
||||
["<span style=\"background-color:#000; color:#000;\">","</span>"],
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
private function strip_spoiler(/*string*/ $text) {
|
||||
private function strip_spoiler(string $text): string
|
||||
{
|
||||
$l1 = strlen("[spoiler]");
|
||||
$l2 = strlen("[/spoiler]");
|
||||
while(true) {
|
||||
while (true) {
|
||||
$start = strpos($text, "[spoiler]");
|
||||
if($start === false) break;
|
||||
if ($start === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
$end = strpos($text, "[/spoiler]");
|
||||
if($end === false) break;
|
||||
if ($end === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
if($end < $start) break;
|
||||
if ($end < $start) {
|
||||
break;
|
||||
}
|
||||
|
||||
$beginning = substr($text, 0, $start);
|
||||
$middle = str_rot13(substr($text, $start+$l1, ($end-$start-$l1)));
|
||||
|
@ -127,11 +102,8 @@ class BBCode extends FormatterExtension {
|
|||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
private function extract_code(/*string*/ $text) {
|
||||
private function extract_code(string $text): string
|
||||
{
|
||||
# at the end of this function, the only code! blocks should be
|
||||
# the ones we've added -- others may contain malicious content,
|
||||
# which would only appear after decoding
|
||||
|
@ -140,14 +112,20 @@ class BBCode extends FormatterExtension {
|
|||
|
||||
$l1 = strlen("[code]");
|
||||
$l2 = strlen("[/code]");
|
||||
while(true) {
|
||||
while (true) {
|
||||
$start = strpos($text, "[code]");
|
||||
if($start === false) break;
|
||||
if ($start === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
$end = strpos($text, "[/code]", $start);
|
||||
if($end === false) break;
|
||||
if ($end === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
if($end < $start) break;
|
||||
if ($end < $start) {
|
||||
break;
|
||||
}
|
||||
|
||||
$beginning = substr($text, 0, $start);
|
||||
$middle = base64_encode(substr($text, $start+$l1, ($end-$start-$l1)));
|
||||
|
@ -158,19 +136,20 @@ class BBCode extends FormatterExtension {
|
|||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
private function insert_code(/*string*/ $text) {
|
||||
private function insert_code(string $text): string
|
||||
{
|
||||
$l1 = strlen("[code!]");
|
||||
$l2 = strlen("[/code!]");
|
||||
while(true) {
|
||||
while (true) {
|
||||
$start = strpos($text, "[code!]");
|
||||
if($start === false) break;
|
||||
if ($start === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
$end = strpos($text, "[/code!]");
|
||||
if($end === false) break;
|
||||
if ($end === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
$beginning = substr($text, 0, $start);
|
||||
$middle = base64_decode(substr($text, $start+$l1, ($end-$start-$l1)));
|
||||
|
@ -181,4 +160,3 @@ class BBCode extends FormatterExtension {
|
|||
return $text;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue