Compare commits

..

No commits in common. "fembooru" and "v2.6.0" have entirely different histories.

643 changed files with 41692 additions and 42529 deletions

5
.buildpath Normal file
View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<buildpath>
<buildpathentry kind="src" path=""/>
<buildpathentry kind="con" path="org.eclipse.php.core.LANGUAGE"/>
</buildpath>

View File

@ -1,8 +0,0 @@
vendor
.git
*.phar
data
images
thumbs
*.sqlite
Dockerfile

View File

@ -1,21 +0,0 @@
# 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
View File

@ -1 +0,0 @@
*.php text eol=lf

View File

@ -1,28 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
---
**Server Software**
(You can get all these stats from `http://<your site>/system_info`)
- 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]
**What steps trigger this bug**
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
**What did you expect to happen?**
A clear and concise description of what you expected to happen.
**What actually happened?**
If applicable, add screenshots to help explain your problem.

View File

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

View File

@ -1,62 +0,0 @@
name: Create Release
on:
push:
tags:
- 'v*'
jobs:
build:
name: Create Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@master
- name: Get version from tag
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/}
- name: Test version in sys_config
run: grep ${{ steps.get_version.outputs.VERSION }} core/sys_config.php
- name: Build
run: |
composer install --no-dev
cd ..
tar cvzf shimmie2-${{ steps.get_version.outputs.VERSION }}.tgz shimmie2
zip -r shimmie2-${{ steps.get_version.outputs.VERSION }}.zip shimmie2
- name: Create Release
id: create_release
uses: actions/create-release@latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Shimmie ${{ steps.get_version.outputs.VERSION }}
body: Automated release from tags
draft: false
prerelease: false
- name: Upload Zip
id: upload-release-asset-zip
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ../shimmie2-${{ steps.get_version.outputs.VERSION }}.zip
asset_name: shimmie2-${{ steps.get_version.outputs.VERSION }}.zip
asset_content_type: application/zip
- name: Upload Tar
id: upload-release-asset-tar
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ../shimmie2-${{ steps.get_version.outputs.VERSION }}.tgz
asset_name: shimmie2-${{ steps.get_version.outputs.VERSION }}.tgz
asset_content_type: application/gzip

View File

@ -1,95 +0,0 @@
name: Test & Publish
on:
push:
pull_request:
schedule:
- cron: '0 2 * * 0' # Weekly on Sundays at 02:00
jobs:
test:
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 systemctl start postgresql ;
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 TEST_DSN="pgsql:user=shimmie;password=shimmie;host=127.0.0.1;dbname=shimmie"
fi
if [[ "${{ matrix.database }}" == "mysql" ]]; then
export TEST_DSN="mysql:user=root;password=root;host=127.0.0.1;dbname=shimmie"
fi
if [[ "${{ matrix.database }}" == "sqlite" ]]; then
export TEST_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
publish:
name: Publish
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push'
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 }}
cache: ${{ github.event_name != 'schedule' }}
buildoptions: "--build-arg RUN_TESTS=false"

6
.gitignore vendored
View File

@ -2,11 +2,10 @@ backup
data
images
thumbs
!lib/images
*.phar
*.sqlite
*.cache
.devcontainer
trace.json
/lib/vendor/
#Composer
composer.phar
@ -55,6 +54,7 @@ Icon
*.un~
Session.vim
.netrwhist
*~
### PhpStorm ###

View File

@ -17,7 +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|thumbs)/([0-9a-f]{2})([0-9a-f]{30}).*$ data/$1/$2/$2$3 [L]
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]
# any requests for files which don't physically exist should be handled by index.php
RewriteCond %{REQUEST_FILENAME} !-f
@ -26,7 +27,7 @@
<IfModule mod_expires.c>
ExpiresActive On
<FilesMatch "([0-9a-f]{32}|\.(gif|jpe?g|png|webp|css|js))$">
<FilesMatch "([0-9a-f]{32}|\.(gif|jpe?g|png|css|js))$">
<IfModule mod_headers.c>
Header set Cache-Control "public, max-age=2629743"
</IfModule>
@ -45,7 +46,6 @@
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

View File

@ -1,19 +0,0 @@
<?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 Normal file
View File

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

View File

@ -3,17 +3,7 @@ imports:
- php
filter:
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
excluded_paths: [lib/*,ext/*/lib/*,ext/tagger/script.js,ext/chatbox/*]
tools:
external_code_coverage: true

62
.travis.yml Normal file
View File

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

View File

@ -1,49 +0,0 @@
# "Build" shimmie (composer install - done in its own stage so that we don't
# need to include all the composer fluff in the final image)
FROM debian:stable-slim AS app
RUN apt update && apt install -y composer php7.3-gd php7.3-dom php7.3-sqlite3 php-xdebug imagemagick
COPY composer.json composer.lock /app/
WORKDIR /app
RUN composer install --no-dev
COPY . /app/
# Tests in their own image. Really we should inherit from app and then
# `composer install` phpunit on top of that; but for some reason
# `composer install --no-dev && composer install` doesn't install dev
FROM debian:stable-slim AS tests
RUN apt update && apt install -y composer php7.3-gd php7.3-dom php7.3-sqlite3 php-xdebug imagemagick
COPY composer.json composer.lock /app/
WORKDIR /app
RUN composer install
COPY . /app/
ARG RUN_TESTS=true
RUN [ $RUN_TESTS = false ] || (\
echo '=== Installing ===' && mkdir -p data/config && INSTALL_DSN="sqlite:data/shimmie.sqlite" 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)
# Build su-exec so that our final image can be nicer
FROM debian:stable-slim AS suexec
RUN apt-get update && apt-get install -y --no-install-recommends gcc libc-dev curl
RUN curl -k -o /usr/local/bin/su-exec.c https://raw.githubusercontent.com/ncopa/su-exec/master/su-exec.c; \
gcc -Wall /usr/local/bin/su-exec.c -o/usr/local/bin/su-exec; \
chown root:root /usr/local/bin/su-exec; \
chmod 0755 /usr/local/bin/su-exec;
# Actually run shimmie
FROM debian:stable-slim
EXPOSE 8000
HEALTHCHECK --interval=5m --timeout=3s CMD curl --fail http://127.0.0.1:8000/ || exit 1
ENV UID=1000 \
GID=1000
RUN apt update && apt install -y curl \
php7.3-cli php7.3-gd php7.3-pgsql php7.3-mysql php7.3-sqlite3 php7.3-zip php7.3-dom php7.3-mbstring \
imagemagick zip unzip && \
rm -rf /var/lib/apt/lists/*
COPY --from=app /app /app
COPY --from=suexec /usr/local/bin/su-exec /usr/local/bin/su-exec
WORKDIR /app
CMD ["/bin/sh", "/app/tests/docker-init.sh"]

166
README.markdown Normal file
View File

@ -0,0 +1,166 @@
```
_________.__ .__ .__ ________
/ _____/| |__ |__| _____ _____ |__| ____ \_____ \
\_____ \ | | \ | | / \ / \ | |_/ __ \ / ____/
/ \| Y \| || Y Y \| Y Y \| |\ ___/ / \
/_______ /|___| /|__||__|_| /|__|_| /|__| \___ >\_______ \
\/ \/ \/ \/ \/ \/
```
# 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)
[![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.
Alternatively if you want a version that will never have significant changes,
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)
- GD or ImageMagick
# Installation
1. Download the latest release under [Releases](https://github.com/shish/shimmie2/releases).
2. Unzip shimmie into a folder on the web host
3. Create a blank database
4. Visit the folder with a web browser
5. Enter the location of the database
6. Click "install". Hopefully you'll end up at the welcome screen; if
not, you should be given instructions on how to fix any errors~
# Installation (Development)
1. Download shimmie via the "Download Zip" button on the [develop](https://github.com/shish/shimmie2/tree/develop) 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
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:
OLD Format:
```php
$database_dsn = "<proto>://<username>:<password>@<host>/<database>";
```
NEW Format:
```php
define("DATABASE_DSN", "<proto>:user=<username>;password=<password>;host=<host>;dbname=<database>");
```
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.
### 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.
## Custom Configuration
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
be used.
#### Custom User Classes
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:
```php
new UserClass("anonymous", "base", array(
"create_comment" => True,
"edit_image_tag" => True,
"edit_image_source" => True,
"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,
));
```
For a list of permissions, see `core/userclass.class.php`
# Development Info
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
themes, be careful with these, and avoid styling them, eg:
- shm-thumb = outermost element of a thumbnail
* data-tags
* data-post-id
- shm-toggler = click this to toggle elements that match the selector
* data-toggle-sel
- shm-unlocker = click this to unlock elements that match the selector
* data-unlock-sel
- 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
# Licence
All code is released under the [GNU GPL Version 2](http://www.gnu.org/licenses/gpl-2.0.html) unless mentioned otherwise.
If you give shimmie to someone else, you have to give them the source (which
should be easy, as PHP is an interpreted language...). If you want to add
customisations to your own site, then those customisations belong to you,
and you can do what you want with them.

View File

@ -1,43 +0,0 @@
```
_________.__ .__ .__ ________
/ _____/| |__ |__| _____ _____ |__| ____ \_____ \
\_____ \ | | \ | | / \ / \ | |_/ __ \ / ____/
/ \| Y \| || Y Y \| Y Y \| |\ ___/ / \
/_______ /|___| /|__||__|_| /|__|_| /|__| \___ >\_______ \
\/ \/ \/ \/ \/ \/
```
# Shimmie
[![Test & Publish](https://github.com/shish/shimmie2/workflows/Test%20&%20Publish/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)
# Documentation
* [Install straight on disk](https://github.com/shish/shimmie2/wiki/Install)
* [Install in docker container](https://github.com/shish/shimmie2/wiki/Docker)
* [Upgrade process](https://github.com/shish/shimmie2/wiki/Upgrade)
* [Basic settings](https://github.com/shish/shimmie2/wiki/Settings)
* [Advanced config](https://github.com/shish/shimmie2/wiki/Advanced-Config)
* [Developer notes](https://github.com/shish/shimmie2/wiki/Development-Info)
* [High-performance notes](https://github.com/shish/shimmie2/wiki/Performance)
# Contact
Email: webmaster at shishnet.org
Issue/Bug tracker: https://github.com/shish/shimmie2/issues
# Licence
All code is released under the [GNU GPL Version 2](https://www.gnu.org/licenses/gpl-2.0.html) unless mentioned otherwise.
If you give shimmie to someone else, you have to give them the source (which
should be easy, as PHP is an interpreted language...). If you want to add
customisations to your own site, then those customisations belong to you,
and you can do what you want with them.

View File

@ -1,11 +1,13 @@
{
"name": "shish/shimmie2",
"description": "A tag-based image gallery",
"type" : "project",
"license" : "GPL-2.0-or-later",
"license" : "GPL-2.0",
"minimum-stability" : "dev",
"repositories" : [
{
"type": "composer",
"url": "https://asset-packagist.org"
},
{
"type" : "package",
"package" : {
@ -21,47 +23,46 @@
],
"require" : {
"php" : "^7.3",
"ext-pdo": "*",
"ext-json": "*",
"ext-fileinfo": "*",
"php" : ">=5.6",
"flexihash/flexihash" : "^2.0.0",
"ifixit/php-akismet" : "1.*",
"google/recaptcha" : "~1.1",
"dapphp/securimage" : "3.6.*",
"shish/eventtracer-php" : "^2.0.0",
"shish/ffsphp" : "^1.0.0",
"shish/microcrud" : "^2.0.0",
"shish/microhtml" : "^2.0.0",
"enshrined/svg-sanitize" : "0.13.*",
"bower-asset/jquery" : "1.12.*",
"bower-asset/jquery-timeago" : "1.5.*",
"bower-asset/mediaelement" : "2.21.*",
"bower-asset/js-cookie" : "2.1.*"
"bower-asset/jquery" : "1.12.3",
"bower-asset/jquery-timeago" : "1.5.2",
"bower-asset/tablesorter" : "2.*",
"bower-asset/mediaelement" : "2.21.1",
"bower-asset/js-cookie" : "2.1.1"
},
"require-dev" : {
},
"phpunit/phpunit" : "5.*"
},
"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, bulk import/export",
"ext-zlib": "anti-spam",
"ext-xml": "some extensions",
"ext-gd": "GD-based thumbnailing"
},"replace": {
"bower-asset/jquery": ">=1.11.0",
"bower-asset/inputmask": ">=3.2.0",
"bower-asset/punycode": ">=1.3.0",
"bower-asset/yii2-pjax": ">=2.0.0"
}
"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']);\""
]
}
}

1636
composer.lock generated

File diff suppressed because it is too large Load Diff

50
core/_bootstrap.inc.php Normal file
View File

@ -0,0 +1,50 @@
<?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";
// set up and purify the environment
_version_check();
_sanitise_environment();
// load base files
ctx_log_start("Opening files");
$files = array_merge(
zglob("core/*.php"),
zglob("ext/{".ENABLED_EXTS."}/main.php")
);
foreach($files as $filename) {
if(basename($filename)[0] != "_") {
require_once $filename;
}
}
unset($files);
unset($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());

View File

@ -1,667 +0,0 @@
<?php declare(strict_types=1);
require_once "core/event.php";
abstract class PageMode
{
const REDIRECT = 'redirect';
const DATA = 'data';
const PAGE = 'page';
const FILE = 'file';
const MANUAL = 'manual';
}
/**
* 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;
}
// ==============================================
public function send_headers(): void
{
if (!headers_sent()) {
header("HTTP/1.0 {$this->code} Shimmie");
header("Content-type: " . $this->type);
header("X-Powered-By: Shimmie-" . VERSION);
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.";
}
}
/**
* Display the page according to the mode and data given.
*/
public function display(): void
{
if ($this->mode!=PageMode::MANUAL) {
$this->send_headers();
}
switch ($this->mode) {
case PageMode::MANUAL:
break;
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-timeago/jquery.timeago.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 &copy; their respective owners,
<a href=\"https://code.shishnet.org/shimmie2/\">Shimmie</a> &copy;
<a href=\"https://www.shishnet.org/\">Shish</a> &amp;
<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;
}

166
core/basethemelet.class.php Normal file
View File

@ -0,0 +1,166 @@
<?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>&lt;&lt; '.$pages_html.' &gt;&gt;';
}
}

View File

@ -1,149 +0,0 @@
<?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([EXTENSION_FLASH, EXTENSION_SVG, EXTENSION_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>&lt;&lt; '.$pages_html.' &gt;&gt;';
}
}

96
core/block.class.php Normal file
View File

@ -0,0 +1,96 @@
<?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;
/**
* 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;
$this->id = preg_replace('/[^\w]/', '',str_replace(' ', '_', is_null($id) ? (is_null($header) ? md5($body) : $header) . $section : $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);
}
}

View File

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

View File

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

View File

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

429
core/config.class.php Normal file
View File

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

View File

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

866
core/database.class.php Normal file
View File

@ -0,0 +1,866 @@
<?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);
if(class_exists("Memcache")) {
$this->memcache = new Memcache;
@$this->memcache->pconnect($hp[0], $hp[1]);
}
else {
print "no memcache"; exit;
}
}
/**
* @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 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|apc)://(.*)#", CACHE_DSN, $matches)) {
if($matches[1] == "memcache") {
$this->cache = new MemcacheCache($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 ((defined('DEBUG_SQL') && DEBUG_SQL === true) || (!defined('DEBUG_SQL') && @$_GET['DEBUG_SQL'])) {
$fp = @fopen("data/sql.log", "a");
if($fp) {
$sql = trim(preg_replace('/\s+/msi', ' ', $sql));
if(isset($inputarray) && is_array($inputarray) && !empty($inputarray)) {
fwrite($fp, $sql." -- ".join(", ", $inputarray)."\n");
}
else {
fwrite($fp, $sql."\n");
}
fclose($fp);
}
}
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++;
}
/**
* 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->dbtime += microtime(true) - $_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->dbtime += microtime(true) - $_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->dbtime += microtime(true) - $_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->dbtime += microtime(true) - $_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->dbtime += microtime(true) - $_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() {}
}

View File

@ -1,339 +0,0 @@
<?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->begin_transaction();
}
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_nicely(
'Unknown PDO driver: '.$db_proto,
"Please check that this is a valid driver, installing the PHP modules if needed"
);
}
}
public function begin_transaction(): void
{
if ($this->transaction === false) {
$this->db->beginTransaction();
$this->transaction = true;
}
}
public function is_transaction_open(): bool
{
return !is_null($this->db) && $this->transaction === true;
}
public function commit(): bool
{
if ($this->is_transaction_open()) {
$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 ($this->is_transaction_open()) {
$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;
}
public function get_version(): string
{
return $this->engine->get_version($this->db);
}
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;
}
}

View File

@ -1,236 +0,0 @@
<?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);
abstract public function get_version(PDO $db): string;
}
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.";");
}
public function get_version(PDO $db): string
{
return $db->query('select version()')->fetch()[0];
}
}
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]';");
}
if (defined("DATABASE_TIMEOUT")) {
$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.";");
}
public function get_version(PDO $db): string
{
return $db->query('select version()')->fetch()[0];
}
}
// 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
}
public function get_version(PDO $db): string
{
return $db->query('select sqlite_version()')->fetch()[0];
}
}

138
core/email.class.php Normal file
View File

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

325
core/event.class.php Normal file
View File

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

View File

@ -1,344 +0,0 @@
<?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 {
$nm1 = $this->arg_count - 1;
throw new SCoreException("Requested an invalid page argument {$offset} / {$nm1}");
}
}
/**
* If page arg $n is set, then treat that as a 1-indexed page number
* and return a 0-indexed page number less than $max; else return 0
*/
public function try_page_num(int $n, ?int $max=null): int
{
if ($this->count_args() > $n) {
$i = $this->get_arg($n);
if (is_numeric($i) && int_escape($i) > 0) {
return page_number($i, $max);
} else {
return 0;
}
} else {
return 0;
}
}
/**
* 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
{
}

29
core/exceptions.class.php Normal file
View File

@ -0,0 +1,29 @@
<?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 {}

View File

@ -1,83 +0,0 @@
<?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
{
}

297
core/extension.class.php Normal file
View File

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

View File

@ -1,446 +0,0 @@
<?php declare(strict_types=1);
/**
* 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
* --> https://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 = "https://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_MIME = [];
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 (!is_null($event->replace_id)) {
/* hax: This seems like such a dirty way to do this.. */
/* Check to make sure the image exists. */
$existing = Image::by_id($event->replace_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");
}
if (empty($image->ext)) {
throw new UploadException("Unable to determine extension for ". $event->tmpname);
}
try {
send_event(new MediaCheckPropertiesEvent($image));
} catch (MediaException $e) {
throw new UploadException("Unable to scan media properties: ".$e->getMessage());
}
send_event(new ImageReplaceEvent($event->replace_id, $image));
$event->image_id = $event->replace_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");
}
if (empty($image->ext)) {
throw new UploadException("Unable to determine extension for ". $event->tmpname);
}
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_for_file($filename);
}
if (empty($image->ext)) {
$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(get_mime_for_extension($ext), $this->SUPPORTED_MIME);
}
public static function get_all_supported_exts(): array
{
$arr = [];
foreach (getSubclassesOf("DataHandlerExtension") as $handler) {
$handler = (new $handler());
foreach ($handler->SUPPORTED_MIME as $mime) {
$arr = array_merge($arr, get_all_extension_for_mime($mime));
}
}
$arr = array_unique($arr);
return $arr;
}
}

View File

@ -1,448 +0,0 @@
<?php
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* MIME types and extension information and resolvers *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
const EXTENSION_ANI = 'ani';
const EXTENSION_ASC = 'asc';
const EXTENSION_ASF = 'asf';
const EXTENSION_AVI = 'avi';
const EXTENSION_BMP = 'bmp';
const EXTENSION_BZIP = 'bz';
const EXTENSION_BZIP2 = 'bz2';
const EXTENSION_CBR = 'cbr';
const EXTENSION_CBZ = 'cbz';
const EXTENSION_CBT = 'cbt';
const EXTENSION_CBA = 'cbA';
const EXTENSION_CB7 = 'cb7';
const EXTENSION_CSS = 'css';
const EXTENSION_CSV = 'csv';
const EXTENSION_CUR = 'cur';
const EXTENSION_FLASH = 'swf';
const EXTENSION_FLASH_VIDEO = 'flv';
const EXTENSION_GIF = 'gif';
const EXTENSION_GZIP = 'gz';
const EXTENSION_HTML = 'html';
const EXTENSION_HTM = 'htm';
const EXTENSION_ICO = 'ico';
const EXTENSION_JFIF = 'jfif';
const EXTENSION_JFI = 'jfi';
const EXTENSION_JPEG = 'jpeg';
const EXTENSION_JPG = 'jpg';
const EXTENSION_JS = 'js';
const EXTENSION_JSON = 'json';
const EXTENSION_MKV = 'mkv';
const EXTENSION_MP3 = 'mp3';
const EXTENSION_MP4 = 'mp4';
const EXTENSION_M4V = 'm4v';
const EXTENSION_M4A = 'm4a';
const EXTENSION_MPEG = 'mpeg';
const EXTENSION_MPG = 'mpg';
const EXTENSION_OGG = 'ogg';
const EXTENSION_OGG_VIDEO = 'ogv';
const EXTENSION_OGG_AUDIO = 'oga';
const EXTENSION_PDF = 'pdf';
const EXTENSION_PHP = 'php';
const EXTENSION_PHP5 = 'php5';
const EXTENSION_PNG = 'png';
const EXTENSION_PSD = 'psd';
const EXTENSION_MOV = 'mov';
const EXTENSION_RSS = 'rss';
const EXTENSION_SVG = 'svg';
const EXTENSION_TAR = 'tar';
const EXTENSION_TEXT = 'txt';
const EXTENSION_TIFF = 'tiff';
const EXTENSION_TIF = 'tif';
const EXTENSION_WAV = 'wav';
const EXTENSION_WEBM = 'webm';
const EXTENSION_WEBP = 'webp';
const EXTENSION_WMA = 'wma';
const EXTENSION_WMV = 'wmv';
const EXTENSION_XML = 'xml';
const EXTENSION_XSL = 'xsl';
const EXTENSION_ZIP = 'zip';
// Couldn't find a mimetype for ani, so made one up based on it being a riff container
const MIME_TYPE_ANI = 'application/riff+ani';
const MIME_TYPE_ASF = 'video/x-ms-asf';
const MIME_TYPE_AVI = 'video/x-msvideo';
// Went with mime types from http://fileformats.archiveteam.org/wiki/Comic_Book_Archive
const MIME_TYPE_COMIC_ZIP = 'application/vnd.comicbook+zip';
const MIME_TYPE_COMIC_RAR = 'application/vnd.comicbook-rar';
const MIME_TYPE_BMP = 'image/x-ms-bmp';
const MIME_TYPE_BZIP = 'application/x-bzip';
const MIME_TYPE_BZIP2 = 'application/x-bzip2';
const MIME_TYPE_CSS = 'text/css';
const MIME_TYPE_CSV = 'text/csv';
const MIME_TYPE_FLASH = 'application/x-shockwave-flash';
const MIME_TYPE_FLASH_VIDEO = 'video/x-flv';
const MIME_TYPE_GIF = 'image/gif';
const MIME_TYPE_GZIP = 'application/x-gzip';
const MIME_TYPE_HTML = 'text/html';
const MIME_TYPE_ICO = 'image/x-icon';
const MIME_TYPE_JPEG = 'image/jpeg';
const MIME_TYPE_JS = 'text/javascript';
const MIME_TYPE_JSON = 'application/json';
const MIME_TYPE_MKV = 'video/x-matroska';
const MIME_TYPE_MP3 = 'audio/mpeg';
const MIME_TYPE_MP4_AUDIO = 'audio/mp4';
const MIME_TYPE_MP4_VIDEO = 'video/mp4';
const MIME_TYPE_MPEG = 'video/mpeg';
const MIME_TYPE_OCTET_STREAM = 'application/octet-stream';
const MIME_TYPE_OGG = 'application/ogg';
const MIME_TYPE_OGG_VIDEO = 'video/ogg';
const MIME_TYPE_OGG_AUDIO = 'audio/ogg';
const MIME_TYPE_PDF = 'application/pdf';
const MIME_TYPE_PHP = 'text/x-php';
const MIME_TYPE_PNG = 'image/png';
const MIME_TYPE_PSD = 'image/vnd.adobe.photoshop';
const MIME_TYPE_QUICKTIME = 'video/quicktime';
const MIME_TYPE_RSS = 'application/rss+xml';
const MIME_TYPE_SVG = 'image/svg+xml';
const MIME_TYPE_TAR = 'application/x-tar';
const MIME_TYPE_TEXT = 'text/plain';
const MIME_TYPE_TIFF = 'image/tiff';
const MIME_TYPE_WAV = 'audio/x-wav';
const MIME_TYPE_WEBM = 'video/webm';
const MIME_TYPE_WEBP = 'image/webp';
const MIME_TYPE_WIN_BITMAP = 'image/x-win-bitmap';
const MIME_TYPE_XML = 'text/xml';
const MIME_TYPE_XML_APPLICATION = 'application/xml';
const MIME_TYPE_XSL = 'application/xsl+xml';
const MIME_TYPE_ZIP = 'application/zip';
const MIME_TYPE_MAP_NAME = 'name';
const MIME_TYPE_MAP_EXT = 'ext';
const MIME_TYPE_MAP_MIME = 'mime';
// Mime type map. Each entry in the MIME_TYPE_ARRAY represents a kind of file, identified by the "correct" mimetype as the key.
// The value for each entry is a map of twokeys, ext and mime.
// ext's value is an array of all of the extensions that the file type can use, with the "correct" one being first.
// mime's value is an array of all mime types that the file type is known to use, with the current "correct" one being first.
const MIME_TYPE_MAP = [
MIME_TYPE_ANI => [
MIME_TYPE_MAP_NAME => "ANI Cursor",
MIME_TYPE_MAP_EXT => [EXTENSION_ANI],
MIME_TYPE_MAP_MIME => [MIME_TYPE_ANI],
],
MIME_TYPE_AVI => [
MIME_TYPE_MAP_NAME => "AVI",
MIME_TYPE_MAP_EXT => [EXTENSION_AVI],
MIME_TYPE_MAP_MIME => [MIME_TYPE_AVI,'video/avi','video/msvideo'],
],
MIME_TYPE_ASF => [
MIME_TYPE_MAP_NAME => "ASF/WMV",
MIME_TYPE_MAP_EXT => [EXTENSION_ASF,EXTENSION_WMA,EXTENSION_WMV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_ASF,'audio/x-ms-wma','video/x-ms-wmv'],
],
MIME_TYPE_BMP => [
MIME_TYPE_MAP_NAME => "BMP",
MIME_TYPE_MAP_EXT => [EXTENSION_BMP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_BMP],
],
MIME_TYPE_BZIP => [
MIME_TYPE_MAP_NAME => "BZIP",
MIME_TYPE_MAP_EXT => [EXTENSION_BZIP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_BZIP],
],
MIME_TYPE_BZIP2 => [
MIME_TYPE_MAP_NAME => "BZIP2",
MIME_TYPE_MAP_EXT => [EXTENSION_BZIP2],
MIME_TYPE_MAP_MIME => [MIME_TYPE_BZIP2],
],
MIME_TYPE_COMIC_ZIP => [
MIME_TYPE_MAP_NAME => "CBZ",
MIME_TYPE_MAP_EXT => [EXTENSION_CBZ],
MIME_TYPE_MAP_MIME => [MIME_TYPE_COMIC_ZIP],
],
MIME_TYPE_CSS => [
MIME_TYPE_MAP_NAME => "Cascading Style Sheet",
MIME_TYPE_MAP_EXT => [EXTENSION_CSS],
MIME_TYPE_MAP_MIME => [MIME_TYPE_CSS],
],
MIME_TYPE_CSV => [
MIME_TYPE_MAP_NAME => "CSV",
MIME_TYPE_MAP_EXT => [EXTENSION_CSV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_CSV],
],
MIME_TYPE_FLASH => [
MIME_TYPE_MAP_NAME => "Flash",
MIME_TYPE_MAP_EXT => [EXTENSION_FLASH],
MIME_TYPE_MAP_MIME => [MIME_TYPE_FLASH],
],
MIME_TYPE_FLASH_VIDEO => [
MIME_TYPE_MAP_NAME => "Flash Video",
MIME_TYPE_MAP_EXT => [EXTENSION_FLASH_VIDEO],
MIME_TYPE_MAP_MIME => [MIME_TYPE_FLASH_VIDEO,'video/flv'],
],
MIME_TYPE_GIF => [
MIME_TYPE_MAP_NAME => "GIF",
MIME_TYPE_MAP_EXT => [EXTENSION_GIF],
MIME_TYPE_MAP_MIME => [MIME_TYPE_GIF],
],
MIME_TYPE_GZIP => [
MIME_TYPE_MAP_NAME => "GZIP",
MIME_TYPE_MAP_EXT => [EXTENSION_GZIP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_TAR],
],
MIME_TYPE_HTML => [
MIME_TYPE_MAP_NAME => "HTML",
MIME_TYPE_MAP_EXT => [EXTENSION_HTM, EXTENSION_HTML],
MIME_TYPE_MAP_MIME => [MIME_TYPE_HTML],
],
MIME_TYPE_ICO => [
MIME_TYPE_MAP_NAME => "Icon",
MIME_TYPE_MAP_EXT => [EXTENSION_ICO, EXTENSION_CUR],
MIME_TYPE_MAP_MIME => [MIME_TYPE_ICO, MIME_TYPE_WIN_BITMAP],
],
MIME_TYPE_JPEG => [
MIME_TYPE_MAP_NAME => "JPEG",
MIME_TYPE_MAP_EXT => [EXTENSION_JPG, EXTENSION_JPEG, EXTENSION_JFIF, EXTENSION_JFI],
MIME_TYPE_MAP_MIME => [MIME_TYPE_JPEG],
],
MIME_TYPE_JS => [
MIME_TYPE_MAP_NAME => "JavaScript",
MIME_TYPE_MAP_EXT => [EXTENSION_JS],
MIME_TYPE_MAP_MIME => [MIME_TYPE_JS],
],
MIME_TYPE_JSON => [
MIME_TYPE_MAP_NAME => "JSON",
MIME_TYPE_MAP_EXT => [EXTENSION_JSON],
MIME_TYPE_MAP_MIME => [MIME_TYPE_JSON],
],
MIME_TYPE_MKV => [
MIME_TYPE_MAP_NAME => "Matroska",
MIME_TYPE_MAP_EXT => [EXTENSION_MKV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MKV],
],
MIME_TYPE_MP3 => [
MIME_TYPE_MAP_NAME => "MP3",
MIME_TYPE_MAP_EXT => [EXTENSION_MP3],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MP3],
],
MIME_TYPE_MP4_AUDIO => [
MIME_TYPE_MAP_NAME => "MP4 Audio",
MIME_TYPE_MAP_EXT => [EXTENSION_M4A],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MP4_AUDIO,"audio/m4a"],
],
MIME_TYPE_MP4_VIDEO => [
MIME_TYPE_MAP_NAME => "MP4 Video",
MIME_TYPE_MAP_EXT => [EXTENSION_MP4,EXTENSION_M4V],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MP4_VIDEO,'video/x-m4v'],
],
MIME_TYPE_MPEG => [
MIME_TYPE_MAP_NAME => "MPEG",
MIME_TYPE_MAP_EXT => [EXTENSION_MPG,EXTENSION_MPEG],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MPEG],
],
MIME_TYPE_PDF => [
MIME_TYPE_MAP_NAME => "PDF",
MIME_TYPE_MAP_EXT => [EXTENSION_PDF],
MIME_TYPE_MAP_MIME => [MIME_TYPE_PDF],
],
MIME_TYPE_PHP => [
MIME_TYPE_MAP_NAME => "PHP",
MIME_TYPE_MAP_EXT => [EXTENSION_PHP,EXTENSION_PHP5],
MIME_TYPE_MAP_MIME => [MIME_TYPE_PHP],
],
MIME_TYPE_PNG => [
MIME_TYPE_MAP_NAME => "PNG",
MIME_TYPE_MAP_EXT => [EXTENSION_PNG],
MIME_TYPE_MAP_MIME => [MIME_TYPE_PNG],
],
MIME_TYPE_PSD => [
MIME_TYPE_MAP_NAME => "PSD",
MIME_TYPE_MAP_EXT => [EXTENSION_PSD],
MIME_TYPE_MAP_MIME => [MIME_TYPE_PSD],
],
MIME_TYPE_OGG_AUDIO => [
MIME_TYPE_MAP_NAME => "Ogg Vorbis",
MIME_TYPE_MAP_EXT => [EXTENSION_OGG_AUDIO,EXTENSION_OGG],
MIME_TYPE_MAP_MIME => [MIME_TYPE_OGG_AUDIO,MIME_TYPE_OGG],
],
MIME_TYPE_OGG_VIDEO => [
MIME_TYPE_MAP_NAME => "Ogg Theora",
MIME_TYPE_MAP_EXT => [EXTENSION_OGG_VIDEO],
MIME_TYPE_MAP_MIME => [MIME_TYPE_OGG_VIDEO],
],
MIME_TYPE_QUICKTIME => [
MIME_TYPE_MAP_NAME => "Quicktime",
MIME_TYPE_MAP_EXT => [EXTENSION_MOV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_QUICKTIME],
],
MIME_TYPE_RSS => [
MIME_TYPE_MAP_NAME => "RSS",
MIME_TYPE_MAP_EXT => [EXTENSION_RSS],
MIME_TYPE_MAP_MIME => [MIME_TYPE_RSS],
],
MIME_TYPE_SVG => [
MIME_TYPE_MAP_NAME => "SVG",
MIME_TYPE_MAP_EXT => [EXTENSION_SVG],
MIME_TYPE_MAP_MIME => [MIME_TYPE_SVG],
],
MIME_TYPE_TAR => [
MIME_TYPE_MAP_NAME => "TAR",
MIME_TYPE_MAP_EXT => [EXTENSION_TAR],
MIME_TYPE_MAP_MIME => [MIME_TYPE_TAR],
],
MIME_TYPE_TEXT => [
MIME_TYPE_MAP_NAME => "Text",
MIME_TYPE_MAP_EXT => [EXTENSION_TEXT, EXTENSION_ASC],
MIME_TYPE_MAP_MIME => [MIME_TYPE_TEXT],
],
MIME_TYPE_TIFF => [
MIME_TYPE_MAP_NAME => "TIFF",
MIME_TYPE_MAP_EXT => [EXTENSION_TIF,EXTENSION_TIFF],
MIME_TYPE_MAP_MIME => [MIME_TYPE_TIFF],
],
MIME_TYPE_WAV => [
MIME_TYPE_MAP_NAME => "Wave",
MIME_TYPE_MAP_EXT => [EXTENSION_WAV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_WAV],
],
MIME_TYPE_WEBM => [
MIME_TYPE_MAP_NAME => "WebM",
MIME_TYPE_MAP_EXT => [EXTENSION_WEBM],
MIME_TYPE_MAP_MIME => [MIME_TYPE_WEBM],
],
MIME_TYPE_WEBP => [
MIME_TYPE_MAP_NAME => "WebP",
MIME_TYPE_MAP_EXT => [EXTENSION_WEBP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_WEBP],
],
MIME_TYPE_XML => [
MIME_TYPE_MAP_NAME => "XML",
MIME_TYPE_MAP_EXT => [EXTENSION_XML],
MIME_TYPE_MAP_MIME => [MIME_TYPE_XML,MIME_TYPE_XML_APPLICATION],
],
MIME_TYPE_XSL => [
MIME_TYPE_MAP_NAME => "XSL",
MIME_TYPE_MAP_EXT => [EXTENSION_XSL],
MIME_TYPE_MAP_MIME => [MIME_TYPE_XSL],
],
MIME_TYPE_ZIP => [
MIME_TYPE_MAP_NAME => "ZIP",
MIME_TYPE_MAP_EXT => [EXTENSION_ZIP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_ZIP],
],
];
/**
* Returns the mimetype that matches the provided extension.
*/
function get_mime_for_extension(string $ext): ?string
{
$ext = strtolower($ext);
foreach (MIME_TYPE_MAP as $key=>$value) {
if (in_array($ext, $value[MIME_TYPE_MAP_EXT])) {
return $key;
}
}
return null;
}
/**
* Returns the mimetype for the specified file, trying file inspection methods before falling back on extension-based detection.
* @param String $file
* @param String $ext The files extension, for if the current filename somehow lacks the extension
* @return String The extension that was found.
*/
function get_mime(string $file, string $ext=""): string
{
if (!file_exists($file)) {
throw new SCoreException("File not found: ".$file);
}
$type = false;
if (extension_loaded('fileinfo')) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
try {
$type = finfo_file($finfo, $file);
} finally {
finfo_close($finfo);
}
} elseif (function_exists('mime_content_type')) {
// If anyone is still using mime_content_type()
$type = trim(mime_content_type($file));
}
if ($type===false || empty($type)) {
// Checking by extension is our last resort
if ($ext==null||strlen($ext) == 0) {
$ext = pathinfo($file, PATHINFO_EXTENSION);
}
$type = get_mime_for_extension($ext);
}
if ($type !== false && strlen($type) > 0) {
return $type;
}
return MIME_TYPE_OCTET_STREAM;
}
/**
* Returns the file extension associated with the specified mimetype.
*/
function get_extension(?string $mime_type): ?string
{
if (empty($mime_type)) {
return null;
}
if ($mime_type==MIME_TYPE_OCTET_STREAM) {
return null;
}
foreach (MIME_TYPE_MAP as $key=>$value) {
if (in_array($mime_type, $value[MIME_TYPE_MAP_MIME])) {
return $value[MIME_TYPE_MAP_EXT][0];
}
}
return null;
}
/**
* Returns all of the file extensions associated with the specified mimetype.
*/
function get_all_extension_for_mime(?string $mime_type): array
{
$output = [];
if (empty($mime_type)) {
return $output;
}
foreach (MIME_TYPE_MAP as $key=>$value) {
if (in_array($mime_type, $value[MIME_TYPE_MAP_MIME])) {
$output = array_merge($output, $value[MIME_TYPE_MAP_EXT]);
}
}
return $output;
}
/**
* Gets an the extension defined in MIME_TYPE_MAP for a file.
*
* @param String $file_path
* @return String The extension that was found, or null if one can not be found.
*/
function get_extension_for_file(String $file_path): ?String
{
$mime = get_mime($file_path);
if (!empty($mime)) {
if ($mime==MIME_TYPE_OCTET_STREAM) {
return null;
} else {
$ext = get_extension($mime);
}
if (!empty($ext)) {
return $ext;
}
}
return null;
}

1293
core/imageboard.pack.php Normal file

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,186 +0,0 @@
<?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): int
{
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;
$due = new DataUploadEvent($tmpname, $metadata);
send_event($due);
return $due->image_id;
}
/**
* 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;
$fit = $config->get_string(ImageConfig::THUMB_FIT);
if (in_array($fit, [Media::RESIZE_TYPE_FILL, Media::RESIZE_TYPE_STRETCH, Media::RESIZE_TYPE_FIT_BLUR])) {
return [$config->get_int(ImageConfig::THUMB_WIDTH), $config->get_int(ImageConfig::THUMB_HEIGHT)];
}
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)
{
global $config;
$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,
$config->get_string(ImageConfig::THUMB_FIT)
);
}
function create_scaled_image(string $inname, string $outname, array $tsize, string $type, ?string $engine = null, ?string $resize_type = null)
{
global $config;
if (empty($engine)) {
$engine = $config->get_string(ImageConfig::THUMB_ENGINE);
}
if (empty($resize_type)) {
$resize_type = $config->get_string(ImageConfig::THUMB_FIT);
}
$output_format = $config->get_string(ImageConfig::THUMB_TYPE);
if ($output_format==EXTENSION_WEBP) {
$output_format = Media::WEBP_LOSSY;
}
send_event(new MediaResizeEvent(
$engine,
$inname,
$type,
$outname,
$tsize[0],
$tsize[1],
$resize_type,
$output_format,
$config->get_int(ImageConfig::THUMB_QUALITY),
true,
true
));
}

View File

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

View File

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

View File

@ -1,326 +0,0 @@
<?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 https://code.shishnet.org/shimmie2/
* @license https://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');
if (is_readable("data/config/shimmie.conf.php")) {
die_nicely(
"Shimmie is already installed.",
"data/config/shimmie.conf.php exists, how did you get here?"
);
}
// 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 (getenv("INSTALL_DSN")) {
$dsn = getenv("INSTALL_DSN");
;
} 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) {
die_nicely($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) : "";
die_nicely(
"Install Options",
<<<EOD
$warn_msg
$err_msg
<form action="index.php" method="POST">
<table class='form' style="margin: 1em auto;">
<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&nbsp;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>
<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 = $data_exists && (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: ".get_current_user()." (". getmyuid() .")</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?flash=Installation%20complete");
die_nicely(
"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 \"&lt;?php\".
<p><textarea cols='80' rows='2'>$h_file_content</textarea>
<p>Once done, <a href='index.php'>click here to Continue</a>.",
0
);
}
}

View File

@ -1,82 +0,0 @@
<?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);
const LOGGING_LEVEL_NAMES = [
SCORE_LOG_NOTSET=>"Not Set",
SCORE_LOG_DEBUG=>"Debug",
SCORE_LOG_INFO=>"Info",
SCORE_LOG_WARNING=>"Warning",
SCORE_LOG_ERROR=>"Error",
SCORE_LOG_CRITICAL=>"Critical",
];
/**
* 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;
}

424
core/page.class.php Normal file
View File

@ -0,0 +1,424 @@
<?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 {
}

View File

@ -1,106 +0,0 @@
<?php declare(strict_types=1);
// action_object_attribute
// action = create / view / edit / delete
// object = image / user / tag / setting
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 CREATE_OTHER_USER = "create_other_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 VIEW_REGISTRATIONS = "view_registrations";
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";
public const SET_PRIVATE_IMAGE = "set_private_image";
public const SET_OTHERS_PRIVATE_IMAGES = "set_others_private_images";
public const BULK_IMPORT = "bulk_import";
public const BULK_EXPORT = "bulk_export";
public const BULK_DOWNLOAD = "bulk_download";
}

View File

@ -1,777 +0,0 @@
<?php declare(strict_types=1);
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Things which should be in the core API *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
require_once "filetypes.php";
/**
* 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 https://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 https://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 flush_output(): void
{
if (!defined("UNITTEST")) {
@ob_flush();
}
flush();
}
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);
flush_output();
// 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);
}
}
/** @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 https://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;
}
/**
* The opposite of the standard library's parse_url
*/
function unparse_url($parsed_url)
{
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
$user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
$pass = ($user || $pass) ? "$pass@" : '';
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
$query = !empty($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
$fragment = !empty($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
return "$scheme$user$pass$host$port$path$query$fragment";
}
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!
https://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;
}
/**
* Given a 1-indexed numeric-ish thing, return a zero-indexed
* number between 0 and $max
*/
function page_number(string $input, ?int $max=null): int
{
if (!is_numeric($input)) {
$pageNumber = 0;
} elseif ($input <= 0) {
$pageNumber = 0;
} elseif (!is_null($max) && $input >= $max) {
$pageNumber = $max - 1;
} else {
$pageNumber = $input - 1;
}
return $pageNumber;
}
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('&#039;', '&apos;', 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\.]+)([tgmk])?b?$/i', (string)$limit, $m)) {
$value = $m[1];
if (isset($m[2])) {
switch (strtolower($m[2])) {
/** @noinspection PhpMissingBreakStatementInspection */
case 't': $value *= 1024; // fall through
/** @noinspection PhpMissingBreakStatementInspection */
// no break
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, 4)) {
return sprintf("%.1fTB", $int / pow(1024, 4));
} elseif ($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;
}

View File

@ -1,63 +0,0 @@
<?php declare(strict_types=1);
/*
* A small number of PHP-sanity things (eg don't silently ignore errors) to
* be included right at the very start of index.php and tests/bootstrap.php
*/
$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;
}
# ini_set('zend.assertions', '1'); // generate assertions
ini_set('assert.exception', '1'); // throw exceptions when failed
set_error_handler(function ($errNo, $errStr) {
// Should we turn ALL notices into errors? PHP allows a lot of
// terrible things to happen by default...
if (strpos($errStr, 'Use of undefined constant ') === 0) {
throw new Exception("PHP Error#$errNo: $errStr");
} else {
return false;
}
});
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 die_nicely($title, $body, $code=0)
{
print("<!DOCTYPE html>
<html lang='en'>
<head>
<title>Shimmie</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</h1>
<h3>$title</h3>
<div class=\"container\">
$body
</div>
</div>
</body>
</html>");
if ($code != 0) {
http_response_code(500);
}
exit($code);
}

View File

@ -1,128 +0,0 @@
<?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: https://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;
}

53
core/sys_config.inc.php Normal file
View File

@ -0,0 +1,53 @@
<?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.0'); // 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);

View File

@ -1,34 +0,0 @@
<?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);
}
}
$_g = file_exists(".git") ? '+' : '';
_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.4$_g"); // 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

View File

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

View File

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

View File

@ -1,18 +0,0 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
class TestInit extends TestCase
{
public function testInitExt()
{
send_event(new InitExtEvent());
$this->assertTrue(true);
}
public function testDatabaseUpgrade()
{
send_event(new DatabaseUpgradeEvent());
$this->assertTrue(true);
}
}

View File

@ -1,223 +0,0 @@
<?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 &amp; &lt;waffles&gt;"
);
$this->assertEquals(
html_unescape("Foo &amp; &lt;waffles&gt;"),
"Foo & <waffles>"
);
$x = "Foo &amp; &lt;waffles&gt;";
$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"]])
);
}
}

View File

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

View File

@ -1,101 +0,0 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
require_once "core/urls.php";
class UrlsTest extends TestCase
{
public function test_make_link()
{
// basic
$this->assertEquals(
"/test/foo",
make_link("foo")
);
// remove leading slash from path
$this->assertEquals(
"/test/foo",
make_link("/foo")
);
// query
$this->assertEquals(
"/test/foo?a=1&b=2",
make_link("foo", "a=1&b=2")
);
// hash
$this->assertEquals(
"/test/foo#cake",
make_link("foo", null, "cake")
);
// query + hash
$this->assertEquals(
"/test/foo?a=1&b=2#cake",
make_link("foo", "a=1&b=2", "cake")
);
}
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(
"https://foo.com",
make_http("https://foo.com")
);
}
public function test_modify_url()
{
$this->assertEquals(
"/foo/bar?a=3&b=2",
modify_url("/foo/bar?a=1&b=2", ["a"=>"3"])
);
$this->assertEquals(
"https://blah.com/foo/bar?b=2",
modify_url("https://blah.com/foo/bar?a=1&b=2", ["a"=>null])
);
$this->assertEquals(
"/foo/bar",
modify_url("/foo/bar?a=1&b=2", ["a"=>null, "b"=>null])
);
}
public function test_referer_or()
{
unset($_SERVER['HTTP_REFERER']);
$this->assertEquals(
"foo",
referer_or("foo")
);
$_SERVER['HTTP_REFERER'] = "cake";
$this->assertEquals(
"cake",
referer_or("foo")
);
$_SERVER['HTTP_REFERER'] = "cake";
$this->assertEquals(
"foo",
referer_or("foo", ["cake"])
);
}
}

View File

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

View File

@ -1,114 +0,0 @@
<?php declare(strict_types=1);
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 $fragment=null): string
{
global $config;
if (is_null($page)) {
$page = $config->get_string(SetupConfig::MAIN_PAGE);
}
$page = trim($page, "/");
$parts = [];
$install_dir = get_base_href();
if (SPEED_HAX || $config->get_bool('nice_urls', false)) {
$parts['path'] = "$install_dir/$page";
} else {
$parts['path'] = "$install_dir/index.php";
$query = empty($query) ? "q=$page" : "q=$page&$query";
}
$parts['query'] = $query; // http_build_query($query);
$parts['fragment'] = $fragment; // http_build_query($hash);
return unparse_url($parts);
}
/**
* Take the current URL and modify some parameters
*/
function modify_current_url(array $changes): string
{
return modify_url($_SERVER['REQUEST_URI'], $changes);
}
function modify_url(string $url, array $changes): string
{
$parts = parse_url($url);
$params = [];
if (isset($parts['query'])) {
parse_str($parts['query'], $params);
}
foreach ($changes as $k => $v) {
if (is_null($v) and isset($params[$k])) {
unset($params[$k]);
}
$params[$k] = $v;
}
$parts['query'] = http_build_query($params);
return unparse_url($parts);
}
/**
* 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;
}
/**
* If HTTP_REFERER is set, and not blacklisted, then return it
* Else return a default $dest
*/
function referer_or(string $dest, ?array $blacklist=null): string
{
if (empty($_SERVER['HTTP_REFERER'])) {
return $dest;
}
if ($blacklist) {
foreach ($blacklist as $b) {
if (strstr($_SERVER['HTTP_REFERER'], $b)) {
return $dest;
}
}
}
return $_SERVER['HTTP_REFERER'];
}

313
core/user.class.php Normal file
View File

@ -0,0 +1,313 @@
<?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;
}
}
}
/**
* @param int $offset
* @param int $limit
* @return array
*/
public static function by_list(/*int*/ $offset, /*int*/ $limit=50) {
assert('is_numeric($offset)', var_export($offset, true));
assert('is_numeric($limit)', var_export($limit, true));
global $database;
$rows = $database->get_all("SELECT * FROM users WHERE id >= :start AND id < :end", array("start"=>$offset, "end"=>$offset+$limit));
return array_map("_new_user", $rows);
}
/* 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);
}
}

View File

@ -1,262 +0,0 @@
<?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
{
if (defined("UNITTEST")) {
return true;
}
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");
}
}
}

200
core/userclass.class.php Normal file
View File

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

View File

@ -1,211 +0,0 @@
<?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'?");
}
}
}
$_all_false = [];
foreach ((new ReflectionClass('Permissions'))->getConstants() as $k => $v) {
$_all_false[$v] = false;
}
new UserClass("base", null, $_all_false);
unset($_all_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,
Permissions::SET_PRIVATE_IMAGE => true,
Permissions::BULK_DOWNLOAD => true,
]);
new UserClass("hellbanned", "user", [
Permissions::HELLBANNED => true,
]);
new UserClass("admin", "base", [
Permissions::CHANGE_SETTING => true,
Permissions::OVERRIDE_CONFIG => true,
Permissions::BIG_SEARCH => true,
Permissions::MANAGE_EXTENSION_LIST => true,
Permissions::MANAGE_ALIAS_LIST => true,
Permissions::MANAGE_AUTO_TAG => true,
Permissions::MASS_TAG_EDIT => true,
Permissions::VIEW_IP => true,
Permissions::BAN_IP => true,
Permissions::CREATE_USER => true,
Permissions::CREATE_OTHER_USER => 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_COMMENT => true,
Permissions::DELETE_COMMENT => true,
Permissions::BYPASS_COMMENT_CHECKS => true,
Permissions::REPLACE_IMAGE => true,
Permissions::CREATE_IMAGE => true,
Permissions::EDIT_IMAGE_TAG => true,
Permissions::EDIT_IMAGE_SOURCE => true,
Permissions::EDIT_IMAGE_OWNER => true,
Permissions::EDIT_IMAGE_LOCK => true,
Permissions::EDIT_IMAGE_TITLE => true,
Permissions::EDIT_IMAGE_RELATIONSHIPS => true,
Permissions::EDIT_IMAGE_ARTIST => true,
Permissions::BULK_EDIT_IMAGE_TAG => true,
Permissions::BULK_EDIT_IMAGE_SOURCE => true,
Permissions::DELETE_IMAGE => true,
Permissions::BAN_IMAGE => true,
Permissions::VIEW_EVENTLOG => true,
Permissions::IGNORE_DOWNTIME => true,
Permissions::VIEW_REGISTRATIONS => 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::MANAGE_BLOCKS => true,
Permissions::MANAGE_ADMINTOOLS => true,
Permissions::SEND_PM => true,
Permissions::READ_PM => true,
Permissions::VIEW_OTHER_PMS => true, # hm
Permissions::EDIT_FEATURE => true,
Permissions::BULK_EDIT_VOTE => true,
Permissions::EDIT_OTHER_VOTE => true,
Permissions::VIEW_SYSINTO => true,
Permissions::HELLBANNED => false,
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::EDIT_FAVOURITES => 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,
Permissions::BULK_IMPORT =>true,
Permissions::BULK_EXPORT =>true,
Permissions::BULK_DOWNLOAD => true,
Permissions::SET_PRIVATE_IMAGE => true,
Permissions::SET_OTHERS_PRIVATE_IMAGES => true,
]);
@include_once "data/config/user-classes.conf.php";

1807
core/util.inc.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,799 +0,0 @@
<?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.
https://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
),
['..', '.']
);
}
function remove_empty_dirs(string $dir): bool
{
assert(!empty($dir));
$result = true;
if (!is_dir($dir)) {
return false;
}
$items = array_diff(
scandir(
$dir
),
['..', '.']
);
foreach ($items as $item) {
$path = join_path($dir, $item);
if (is_dir($path)) {
$result = $result && remove_empty_dirs($path);
} else {
$result = false;
}
}
if ($result===true) {
$result = $result && rmdir($dir);
}
return $result;
}
function get_files_recursively(string $dir): array
{
assert(!empty($dir));
if (!is_dir($dir)) {
return [];
}
$things = array_diff(
scandir(
$dir
),
['..', '.']
);
$output = [];
foreach ($things as $thing) {
$path = join_path($dir, $thing);
if (is_file($path)) {
$output[] = $path;
} else {
$output = array_merge($output, get_files_recursively($path));
}
}
return $output;
}
/**
* 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 _set_up_shimmie_environment(): void
{
global $tracer_enabled;
if (file_exists("images") && !file_exists("data/images")) {
die_nicely("Upgrade error", "As of Shimmie 2.7 images and thumbs should be moved to data/images and data/thumbs");
}
if (TIMEZONE) {
date_default_timezone_set(TIMEZONE);
}
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;
}
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.')
<p><b>Stack Trace:</b></p><pre>'.$e->getTraceAsString().'</pre>
</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"=>make_link($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];
}
/*
* Generates a unique key for the website to prevent unauthorized access.
*/
function generate_key(int $length = 20)
{
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters [rand(0, strlen($characters) - 1)];
}
return $randomString;
}

View File

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

View File

@ -1,62 +1,78 @@
<?php /** @noinspection PhpUnusedPrivateMethodInspection */
declare(strict_types=1);
<?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)
*/
/**
* 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;
public function __construct(Page $page)
{
parent::__construct();
/**
* @param Page $page
*/
public function __construct(Page $page) {
$this->page = $page;
}
}
class AdminActionEvent extends Event
{
class AdminActionEvent extends Event {
/** @var string */
public $action;
/** @var bool */
public $redirect = true;
public function __construct(string $action)
{
parent::__construct();
/**
* @param string $action
*/
public function __construct(/*string*/ $action) {
$this->action = $action;
}
}
class AdminPage extends Extension
{
/** @var AdminPageTheme */
protected $theme;
public function onPageRequest(PageRequestEvent $event)
{
class AdminPage extends Extension {
public function onPageRequest(PageRequestEvent $event) {
global $page, $user;
if ($event->page_matches("admin")) {
if (!$user->can(Permissions::MANAGE_ADMINTOOLS)) {
if($event->page_matches("admin")) {
if(!$user->can("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(PageMode::REDIRECT);
if($aae->redirect) {
$page->set_mode("redirect");
$page->set_redirect(make_link("admin"));
}
}
@ -64,121 +80,83 @@ class AdminPage extends Extension
}
}
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";
public function onCommand(CommandEvent $event) {
if($event->cmd == "help") {
print " get-page [query string]\n";
print " eg 'get-page post/list'\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 onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
public function onUserBlockBuilding(UserBlockBuildingEvent $event) {
global $user;
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)) {
if($user->can("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();
}
}
private function set_tag_case()
{
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() {
global $database;
$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");
$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);
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", "Set all tags to lowercase");
log_warning("admin", "Set all tags to lowercase", true);
return true;
}
private function recount_tag_use()
{
private function recount_tag_use() {
global $database;
$database->Execute("
UPDATE tags
@ -188,7 +166,106 @@ class AdminPage extends Extension
)
");
$database->Execute("DELETE FROM tags WHERE count=0");
log_warning("admin", "Re-counted tags", "Re-counted tags");
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
}
return true;
}
}

View File

@ -1,89 +1,84 @@
<?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);
<?php
class AdminPageTest extends ShimmiePHPUnitTestCase {
public function testAuth() {
$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_user();
$this->get_page('admin');
$this->assert_response(403);
$this->assert_title("Permission Denied");
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);
$this->log_in_as_admin();
$this->get_page('admin');
$this->assert_response(200);
$this->assert_title("Admin Tools");
}
public function testLowercaseAndSetCase()
{
// Create a problem
public function testLowercase() {
$ts = time(); // we need a tag that hasn't been used before
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
$this->log_in_as_admin();
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "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("post/view/$image_id_1");
$this->assert_title("Image $image_id_1: TeStCase$ts");
// Fix
$this->get_page('admin');
$this->assert_title("Admin Tools");
//$this->click("All tags to lowercase");
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");
// 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");
$this->delete_image($image_id_1);
}
# FIXME: make sure the admin tools actually work
public function testRecount()
{
global $database;
public function testRecount() {
$this->log_in_as_admin();
$this->get_page('admin');
$this->assert_title("Admin Tools");
// 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
//$this->click("Recount tag use");
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 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();
public function testDump() {
$this->log_in_as_admin();
$this->get_page('admin');
$this->assert_title("Admin Tools");
// don't crash
$this->assertTrue(true);
// 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);
}
}

View File

@ -1,13 +1,10 @@
<?php declare(strict_types=1);
use function MicroHTML\INPUT;
<?php
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");
@ -15,14 +12,20 @@ class AdminPageTheme extends Themelet
$page->add_block(new NavBlock());
}
protected function button(string $name, string $action, bool $protected=false): string
{
/**
* @param string $name
* @param string $action
* @param bool $protected
* @return string
*/
protected function button(/*string*/ $name, /*string*/ $action, /*boolean*/ $protected=false) {
$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";
@ -35,20 +38,40 @@ class AdminPageTheme extends Themelet
* 'recount tag use'
* etc
*/
public function display_form()
{
global $page;
public function display_form() {
global $page, $database;
$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 = (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'),
);
$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";
$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;
}
}

View File

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

View File

@ -1,198 +1,167 @@
<?php declare(strict_types=1);
<?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
*/
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
{
class AddAliasEvent extends Event {
/** @var string */
public $oldtag;
/** @var string */
public $newtag;
public function __construct(string $oldtag, string $newtag)
{
parent::__construct();
/**
* @param string $oldtag
* @param string $newtag
*/
public function __construct($oldtag, $newtag) {
$this->oldtag = trim($oldtag);
$this->newtag = trim($newtag);
}
}
class DeleteAliasEvent extends Event
{
public $oldtag;
class AddAliasException extends SCoreException {}
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)
{
class AliasEditor extends Extension {
public function onPageRequest(PageRequestEvent $event) {
global $config, $database, $page, $user;
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"]);
if($event->page_matches("alias")) {
if($event->get_arg(0) == "add") {
if($user->can("manage_alias_list")) {
if(isset($_POST['oldtag']) && isset($_POST['newtag'])) {
try {
send_event(new AddAliasEvent($input['c_oldtag'], $input['c_newtag']));
$page->set_mode(PageMode::REDIRECT);
$aae = new AddAliasEvent($_POST['oldtag'], $_POST['newtag']);
send_event($aae);
$page->set_mode("redirect");
$page->set_redirect(make_link("alias/list"));
} catch (AddAliasException $ex) {
}
catch(AddAliasException $ex) {
$this->theme->display_error(500, "Error adding alias", $ex->getMessage());
}
}
} 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);
}
}
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");
$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");
}
$this->theme->display_aliases($t->table($t->query()), $t->paginator());
} elseif ($event->get_arg(0) == "export") {
$page->set_mode(PageMode::DATA);
$page->set_type(MIME_TYPE_CSV);
}
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");
$page->set_type("text/csv");
$page->set_filename("aliases.csv");
$page->set_data($this->get_alias_csv($database));
} elseif ($event->get_arg(0) == "import") {
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
if (count($_FILES) > 0) {
}
else if($event->get_arg(0) == "import") {
if($user->can("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", "Imported aliases"); # FIXME: how many?
$page->set_mode(PageMode::REDIRECT);
log_info("alias_editor", "Imported aliases from file", true); # FIXME: how many?
$page->set_mode("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;
$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']}");
$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(:newtag)",
["newtag" => $event->newtag]
);
if ($row) {
throw new AddAliasException("{$row['oldtag']} is itself 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");
}
$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"]));
else {
$database->execute("INSERT INTO aliases(oldtag, newtag) VALUES(:oldtag, :newtag)", $pair);
log_info("alias_editor", "Added alias for {$event->oldtag} -> {$event->newtag}", true);
}
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
{
public function onUserBlockBuilding(UserBlockBuildingEvent $event) {
global $user;
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
if($user->can("manage_alias_list")) {
$event->add_link("Alias Editor", make_link("alias/list"));
}
}
private function get_alias_csv(Database $database): string
{
/**
* @param Database $database
* @return string
*/
private function get_alias_csv(Database $database) {
$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;
}
private function add_alias_csv(Database $database, string $csv): int
{
/**
* @param Database $database
* @param string $csv
*/
private function add_alias_csv(Database $database, /*string*/ $csv) {
$csv = str_replace("\r", "\n", $csv);
$i = 0;
foreach (explode("\n", $csv) as $line) {
foreach(explode("\n", $csv) as $line) {
$parts = str_getcsv($line);
if (count($parts) == 2) {
if(count($parts) == 2) {
try {
send_event(new AddAliasEvent($parts[0], $parts[1]));
$i++;
} catch (AddAliasException $ex) {
$aae = new AddAliasEvent($parts[0], $parts[1]);
send_event($aae);
}
catch(AddAliasException $ex) {
$this->theme->display_error(500, "Error adding alias", $ex->getMessage());
}
}
}
return $i;
}
/**
@ -201,9 +170,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(): int
{
return 60;
}
public function get_priority() {return 60;}
}

View File

@ -1,85 +1,104 @@
<?php declare(strict_types=1);
class AliasEditorTest extends ShimmiePHPUnitTestCase
{
public function testAliasList()
{
<?php
class AliasEditorTest extends ShimmiePHPUnitTestCase {
public function testAliasList() {
$this->get_page('alias/list');
$this->assert_response(200);
$this->assert_title("Alias List");
}
public function testAliasListReadOnly()
{
public function testAliasListReadOnly() {
// Check that normal users can't add aliases.
$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 testAliasOneToOne()
{
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();
$this->log_in_as_admin();
$this->get_page("alias/export/aliases.csv");
$this->assert_no_text("test1");
# 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");
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_response(302);
$this->assert_title("Image $image_id: test2");
$this->get_page("post/list/test2/1"); # check that searching for the main tag still works
$this->assert_response(302);
$this->assert_title("Image $image_id: test2");
$this->delete_image($image_id);
send_event(new DeleteAliasEvent("test1"));
$this->get_page('alias/list');
$this->click("Remove");
$this->get_page('alias/list');
$this->assert_title("Alias List");
$this->assert_no_text("test1");
}
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"));
# 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");
$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");
$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");
// 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/multi/1");
$this->assert_title("multi");
$this->assert_no_text("No Images Found");
$this->get_page("post/list/multi tag/1");
$this->get_page("post/list/multi%20tag/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);
send_event(new DeleteAliasEvent("onetag"));
$this->get_page('alias/list');
$this->click("Remove");
$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");
}
}

View File

@ -1,20 +1,60 @@
<?php declare(strict_types=1);
<?php
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($table, $paginator): void
{
public function display_aliases($aliases, $pageNumber, $totalPages) {
global $page, $user;
$can_manage = $user->can(Permissions::MANAGE_ALIAS_LIST);
$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>";
}
$html = "
$table
$paginator
<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>
<p><a href='".make_link("alias/export/aliases.csv")."' download='aliases.csv'>Download as CSV</a></p>
";
@ -29,8 +69,11 @@ 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);
}
}

2389
ext/amazon_s3/lib/S3.php Normal file

File diff suppressed because it is too large Load Diff

75
ext/amazon_s3/main.php Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,9 @@
<?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]);
<?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);
}
}

View File

@ -1,8 +1,11 @@
<?php declare(strict_types=1);
class ArtistsTheme extends Themelet
{
public function get_author_editor_html(string $author): string
{
<?php
class ArtistsTheme extends Themelet {
/**
* @param string $author
* @return string
*/
public function get_author_editor_html(/*string*/ $author) {
$h_author = html_escape($author);
return "
<tr>
@ -15,65 +18,66 @@ class ArtistsTheme extends Themelet
";
}
public function sidebar_options(string $mode, ?int $artistID=null, $is_admin=false): void
{
/**
* @param string $mode
* @param null|int $artistID
* @param bool $is_admin
*/
public function sidebar_options(/*string*/ $mode, $artistID=NULL, $is_admin=FALSE) {
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' value='New Artist'/>
<input type='submit' name='edit' id='edit' value='New Artist'/>
</form>
<form method='post' action='".make_link("artist/edit_artist")."'>
".$user->get_auth_html()."
<input type='submit' name='edit' value='Edit Artist'/>
<input type='submit' name='edit' id='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' value='Delete Artist'/>
<input type='submit' name='edit' id='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' value='Add Alias'/>
<input type='submit' name='edit' id='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' value='Add Member'/>
<input type='submit' name='edit' id='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' value='Add Url'/>
<input type='submit' name='edit' id='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'];
@ -132,8 +136,7 @@ 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'>
@ -153,8 +156,7 @@ 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'>".
@ -164,34 +166,31 @@ 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 = [
$deletionLinkActionArray = array(
'artist' => 'artist/nuke/',
'alias' => 'artist/alias/delete/',
'member' => 'artist/member/delete/',
];
);
$editionLinkActionArray = [
$editionLinkActionArray = array(
'artist' => 'artist/edit/',
'alias' => 'artist/alias/edit/',
'member' => 'artist/member/edit/',
];
);
$typeTextArray = [
$typeTextArray = array(
'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>";
@ -213,12 +212,8 @@ class ArtistsTheme extends Themelet
"<td>".$user_link."</td>".
"<td>".$artist['posts']."</td>";
if (!$user->is_anonymous()) {
$html .= "<td>".$edit_link."</td>";
}
if ($user->can(Permissions::ARTISTS_ADMIN)) {
$html .= "<td>".$del_link."</td>";
}
if(!$user->is_anonymous()) $html .= "<td>".$edit_link."</td>";
if($user->is_admin()) $html .= "<td>".$del_link."</td>";
$html .= "</tr>";
}
@ -232,8 +227,7 @@ 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 = '
@ -251,8 +245,7 @@ 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 = '
@ -270,8 +263,7 @@ 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 = '
@ -289,15 +281,14 @@ 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" id="alias" value="'.$alias['alias'].'" />
<input type="text" name="alias" value="'.$alias['alias'].'" />
<input type="hidden" name="aliasID" value="'.$alias['id'].'" />
<input type="submit" value="Submit" />
</form>
@ -307,15 +298,14 @@ 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" id="url" value="'.$url['url'].'" />
<input type="text" name="url" value="'.$url['url'].'" />
<input type="hidden" name="urlID" value="'.$url['id'].'" />
<input type="submit" value="Submit" />
</form>
@ -325,15 +315,14 @@ 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="name">Member name:</label>
<input type="text" name="name" id="name" value="'.$member['name'].'" />
<label for="member">Member name:</label>
<input type="text" name="name" value="'.$member['name'].'" />
<input type="hidden" name="memberID" value="'.$member['id'].'" />
<input type="submit" value="Submit" />
</form>
@ -343,8 +332,7 @@ 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>";
@ -355,12 +343,8 @@ 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>
@ -368,12 +352,8 @@ 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);
@ -383,12 +363,8 @@ 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>
@ -400,7 +376,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">'.
@ -411,10 +387,15 @@ class ArtistsTheme extends Themelet
$page->add_block(new Block("Artist Images", $artist_images, "main", 20));
}
private function render_aliases(array $aliases, bool $userIsLogged, bool $userIsAdmin): string
{
/**
* @param $aliases
* @param $userIsLogged
* @param $userIsAdmin
* @return string
*/
private function render_aliases($aliases, $userIsLogged, $userIsAdmin) {
$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>";
@ -423,13 +404,11 @@ 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>";
@ -442,12 +421,10 @@ class ArtistsTheme extends Themelet
$html .= "<tr>
<td class='left'>&nbsp;</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>";
}
@ -456,10 +433,15 @@ class ArtistsTheme extends Themelet
return $html;
}
private function render_members(array $members, bool $userIsLogged, bool $userIsAdmin): string
{
/**
* @param $members
* @param $userIsLogged
* @param $userIsAdmin
* @return string
*/
private function render_members($members, $userIsLogged, $userIsAdmin) {
$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>";
@ -467,12 +449,10 @@ 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>";
@ -485,12 +465,10 @@ class ArtistsTheme extends Themelet
$html .= "<tr>
<td class='left'>&nbsp;</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>";
}
@ -499,10 +477,15 @@ class ArtistsTheme extends Themelet
return $html;
}
private function render_urls(array $urls, bool $userIsLogged, bool $userIsAdmin): string
{
/**
* @param $urls
* @param $userIsLogged
* @param $userIsAdmin
* @return string
*/
private function render_urls($urls, $userIsLogged, $userIsAdmin) {
$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>";
@ -511,13 +494,11 @@ 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>";
@ -530,13 +511,11 @@ class ArtistsTheme extends Themelet
$html .= "<tr>
<td class='left'>&nbsp;</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>";
}
@ -546,13 +525,5 @@ 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>
';
}
}

View File

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

View File

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

View File

@ -1,335 +0,0 @@
<?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 AutoTaggerException extends SCoreException
{
}
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(MIME_TYPE_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;
}
}

View File

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

View File

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

View File

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

View File

@ -14,7 +14,8 @@ ul.tagit li.tagit-choice {
-webkit-border-radius: 6px;
border: 1px solid #CAD8F3;
background: #DEE7F8 none;
background: none;
background-color: #DEE7F8;
font-weight: normal;
}

View File

@ -1,66 +1,44 @@
<?php declare(strict_types=1);
<?php
/*
* Name: Autocomplete
* Author: Daku <admin@codeanimu.net>
* Description: Adds autocomplete to search & tagging.
*/
class AutoComplete extends Extension
{
/** @var AutoCompleteTheme */
protected $theme;
class AutoComplete extends Extension {
public function get_priority() {return 30;} // before Home
public function get_priority(): int
{
return 30;
} // before Home
public function onPageRequest(PageRequestEvent $event) {
global $page, $database;
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(MIME_TYPE_JSON);
$s = strtolower($_GET["s"]);
if (
$s == '' ||
$s[0] == '_' ||
$s[0] == '%' ||
strlen($s) > 32
) {
$page->set_data("{}");
return;
}
if($event->page_matches("api/internal/autocomplete")) {
if(!isset($_GET["s"])) return;
//$limit = 0;
$cache_key = "autocomplete-$s";
$cache_key = "autocomplete-" . strtolower($_GET["s"]);
$limitSQL = "";
$s = str_replace('_', '\_', $s);
$s = str_replace('%', '\%', $s);
$SQLarr = ["search"=>"$s%"]; #, "cat_search"=>"%:$s%"];
if (isset($_GET["limit"]) && $_GET["limit"] !== 0) {
$SQLarr = array("search"=>$_GET["s"]."%");
if(isset($_GET["limit"]) && $_GET["limit"] !== 0){
$limitSQL = "LIMIT :limit";
$SQLarr['limit'] = $_GET["limit"];
$cache_key .= "-" . $_GET["limit"];
}
$res = $cache->get($cache_key);
if (!$res) {
$res = $database->get_pairs(
"
$res = $database->cache->get($cache_key);
if(!$res) {
$res = $database->get_pairs($database->scoreql_to_sql("
SELECT tag, count
FROM tags
WHERE LOWER(tag) LIKE LOWER(:search)
-- OR LOWER(tag) LIKE LOWER(:cat_search)
WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:search)
AND count > 0
ORDER BY count DESC
$limitSQL",
$SQLarr
$limitSQL"), $SQLarr
);
$cache->set($cache_key, $res, 600);
$database->cache->set($cache_key, $res, 600);
}
$page->set_mode("data");
$page->set_type("application/json");
$page->set_data(json_encode($res));
}

View File

@ -1,7 +1,7 @@
document.addEventListener('DOMContentLoaded', () => {
$(function(){
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 @@ document.addEventListener('DOMContentLoaded', () => {
);
},
error : function (request, status, error) {
console.log(error);
alert(error);
}
});
},
@ -66,7 +66,7 @@ document.addEventListener('DOMContentLoaded', () => {
if(keyCode == 32) {
e.preventDefault();
$('.autocomplete_tags').tagit('createTag', $(this).val());
$('[name=search]').tagit('createTag', $(this).val());
$(this).autocomplete('close');
} else if (keyCode == 9) {
e.preventDefault();

View File

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

View File

@ -1,14 +1,12 @@
<?php declare(strict_types=1);
<?php
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 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("<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('<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' />");
}

View File

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

View File

@ -1,9 +1,27 @@
<?php declare(strict_types=1);
<?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"
*/
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=
@ -35,38 +53,34 @@ xanax
");
}
public function onCommentPosting(CommentPostingEvent $event)
{
public function onCommentPosting(CommentPostingEvent $event) {
global $user;
if (!$user->can(Permissions::BYPASS_COMMENT_CHECKS)) {
if(!$user->can("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 = [];
foreach ($this->get_words() as $word) {
if ($word[0] == '/') {
if (preg_match($word, "") === false) {
$failed = array();
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);
@ -74,35 +88,40 @@ 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(string $comment, SCoreException $ex): void
{
private function test_text($comment, $ex) {
$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;
}
}
}
}
private function get_words(): array
{
/**
* @return string[]
*/
private function get_words() {
global $config;
$words = [];
$words = array();
$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;
}
@ -112,8 +131,6 @@ xanax
return $words;
}
public function get_priority(): int
{
return 30;
}
public function get_priority() {return 30;}
}

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