Revert "Merge tag 'v2.10.6'"
This reverts commit122ea4ab9e, reversing changes made toc54a11e250.
This commit is contained in:
@@ -1,56 +0,0 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
|
||||
{
|
||||
"name": "Shimmie",
|
||||
"build": {
|
||||
"context": "..",
|
||||
"dockerfile": "../Dockerfile",
|
||||
"target": "devcontainer"
|
||||
},
|
||||
|
||||
"workspaceMount": "source=${localWorkspaceFolder},target=/app,type=bind",
|
||||
"workspaceFolder": "/app",
|
||||
|
||||
"forwardPorts": [8000],
|
||||
"portsAttributes": {
|
||||
"8000": {
|
||||
"label": "Application",
|
||||
"onAutoForward": "openPreview"
|
||||
}
|
||||
},
|
||||
"otherPortsAttributes": {
|
||||
"onAutoForward": "silent"
|
||||
},
|
||||
|
||||
"updateContentCommand": "composer install",
|
||||
"postCreateCommand": "./.docker/entrypoint.sh unitd --no-daemon --control unix:/var/run/control.unit.sock",
|
||||
"containerEnv": {
|
||||
"UID": "2000",
|
||||
"GID": "2000",
|
||||
"UPLOAD_MAX_FILESIZE": "50M",
|
||||
"INSTALL_DSN": "sqlite:data/shimmie.dev.sqlite"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"recca0120.vscode-phpunit",
|
||||
"ryanluker.vscode-coverage-gutters",
|
||||
"xdebug.php-debug",
|
||||
"DEVSENSE.phptools-vscode",
|
||||
"ms-azuretools.vscode-docker"
|
||||
],
|
||||
"settings": {
|
||||
"phpunit.args": [
|
||||
"--configuration", "${workspaceFolder}/tests/phpunit.xml",
|
||||
"--coverage-clover", "data/coverage.clover"
|
||||
],
|
||||
"coverage-gutters.coverageFileNames": [
|
||||
"data/coverage.clover"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "devcontainer"
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
{
|
||||
"listeners": {
|
||||
"*:8000": {
|
||||
"pass": "routes",
|
||||
"forwarded": {
|
||||
"client_ip": "X-Forwarded-For",
|
||||
"recursive": false,
|
||||
"source": [
|
||||
"172.17.0.0/16"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"match": {
|
||||
"uri": "~/_(thumbs|images)/.*"
|
||||
},
|
||||
"action": {
|
||||
"share": [
|
||||
"`/app/data/${uri.replace(/_(thumbs|images)\\/(..)(..)(.*?)\\/.*/, '$1/$2/$3/$2$3$4')}`",
|
||||
"`/app/data/${uri.replace(/_(thumbs|images)\\/(..)(.*?)\\/.*/, '$1/$2/$2$3')}`"
|
||||
],
|
||||
"response_headers": {
|
||||
"Cache-Control": "public, max-age=31556926"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": {
|
||||
"share": [
|
||||
"/app/$uri"
|
||||
],
|
||||
"types": [
|
||||
"image/*",
|
||||
"application/javascript",
|
||||
"text/css",
|
||||
"application/sourcemap",
|
||||
"!"
|
||||
],
|
||||
"response_headers": {
|
||||
"Cache-Control": "public, max-age=31556926"
|
||||
},
|
||||
"fallback": {
|
||||
"pass": "applications/shimmie"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"applications": {
|
||||
"shimmie": {
|
||||
"type": "php",
|
||||
"user": "shimmie",
|
||||
"root": "/app/",
|
||||
"script": "index.php",
|
||||
"working_directory": "/app/",
|
||||
"options": {
|
||||
"admin": {
|
||||
"memory_limit": "256M",
|
||||
"upload_max_filesize": "$UPLOAD_MAX_FILESIZE",
|
||||
"post_max_size": "$UPLOAD_MAX_FILESIZE"
|
||||
}
|
||||
},
|
||||
"processes": {
|
||||
"max": 8,
|
||||
"spare": 2,
|
||||
"idle_timeout": 60
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"http": {
|
||||
"max_body_size": 1048576000,
|
||||
"static": {
|
||||
"mime_types": {
|
||||
"application/sourcemap": [".map"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# if user shimmie doesn't already exist, create it
|
||||
if ! id -u shimmie >/dev/null 2>&1; then
|
||||
groupadd -g $GID shimmie || true
|
||||
useradd -ms /bin/bash -u $UID -g $GID shimmie || true
|
||||
fi
|
||||
mkdir -p /app/data
|
||||
chown shimmie:shimmie /app/data
|
||||
|
||||
rm -rf /var/lib/unit/*
|
||||
|
||||
envsubst '$UPLOAD_MAX_FILESIZE' < /app/.docker/entrypoint.d/config.json.tmpl > /app/.docker/entrypoint.d/config.json
|
||||
|
||||
WAITLOOPS=5
|
||||
SLEEPSEC=1
|
||||
|
||||
curl_put()
|
||||
{
|
||||
RET=$(/usr/bin/curl -s -w '%{http_code}' -X PUT --data-binary @$1 --unix-socket /var/run/control.unit.sock http://localhost/$2)
|
||||
RET_BODY=$(echo $RET | /bin/sed '$ s/...$//')
|
||||
RET_STATUS=$(echo $RET | /usr/bin/tail -c 4)
|
||||
if [ "$RET_STATUS" -ne "200" ]; then
|
||||
echo "$0: Error: HTTP response status code is '$RET_STATUS'"
|
||||
echo "$RET_BODY"
|
||||
return 1
|
||||
else
|
||||
echo "$0: OK: HTTP response status code is '$RET_STATUS'"
|
||||
echo "$RET_BODY"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ "$1" = "unitd" ] || [ "$1" = "unitd-debug" ]; then
|
||||
if /usr/bin/find "/var/lib/unit/" -mindepth 1 -print -quit 2>/dev/null | /bin/grep -q .; then
|
||||
echo "$0: /var/lib/unit/ is not empty, skipping initial configuration..."
|
||||
else
|
||||
echo "$0: Launching Unit daemon to perform initial configuration..."
|
||||
/usr/sbin/$1 --control unix:/var/run/control.unit.sock
|
||||
|
||||
for i in $(/usr/bin/seq $WAITLOOPS); do
|
||||
if [ ! -S /var/run/control.unit.sock ]; then
|
||||
echo "$0: Waiting for control socket to be created..."
|
||||
/bin/sleep $SLEEPSEC
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
# even when the control socket exists, it does not mean unit has finished initialisation
|
||||
# this curl call will get a reply once unit is fully launched
|
||||
/usr/bin/curl -s -X GET --unix-socket /var/run/control.unit.sock http://localhost/
|
||||
if /usr/bin/find "/app/.docker/entrypoint.d/" -mindepth 1 -print -quit 2>/dev/null | /bin/grep -q .; then
|
||||
echo "$0: /app/.docker/entrypoint.d/ is not empty, applying initial configuration..."
|
||||
|
||||
echo "$0: Looking for certificate bundles in /app/.docker/entrypoint.d/..."
|
||||
for f in $(/usr/bin/find /app/.docker/entrypoint.d/ -type f -name "*.pem"); do
|
||||
echo "$0: Uploading certificates bundle: $f"
|
||||
curl_put $f "certificates/$(basename $f .pem)"
|
||||
done
|
||||
echo "$0: Looking for JavaScript modules in /app/.docker/entrypoint.d/..."
|
||||
for f in $(/usr/bin/find /app/.docker/entrypoint.d/ -type f -name "*.js"); do
|
||||
echo "$0: Uploading JavaScript module: $f"
|
||||
curl_put $f "js_modules/$(basename $f .js)"
|
||||
done
|
||||
|
||||
echo "$0: Looking for configuration snippets in /app/.docker/entrypoint.d/..."
|
||||
for f in $(/usr/bin/find /app/.docker/entrypoint.d/ -type f -name "*.json"); do
|
||||
echo "$0: Applying configuration $f";
|
||||
curl_put $f "config"
|
||||
done
|
||||
echo "$0: Looking for shell scripts in /app/.docker/entrypoint.d/..."
|
||||
for f in $(/usr/bin/find /app/.docker/entrypoint.d/ -type f -name "*.sh"); do
|
||||
echo "$0: Launching $f";
|
||||
"$f"
|
||||
done
|
||||
|
||||
# warn on filetypes we don't know what to do with
|
||||
for f in $(/usr/bin/find /app/.docker/entrypoint.d/ -type f -not -name "*.sh" -not -name "*.json" -not -name "*.pem" -not -name "*.js"); do
|
||||
echo "$0: Ignoring $f";
|
||||
done
|
||||
else
|
||||
echo "$0: /app/.docker/entrypoint.d/ is empty, creating 'welcome' configuration..."
|
||||
curl_put /usr/share/unit/welcome/welcome.json "config"
|
||||
fi
|
||||
echo "$0: Stopping Unit daemon after initial configuration..."
|
||||
kill -TERM $(/bin/cat /var/run/unit.pid)
|
||||
|
||||
for i in $(/usr/bin/seq $WAITLOOPS); do
|
||||
if [ -S /var/run/control.unit.sock ]; then
|
||||
echo "$0: Waiting for control socket to be removed..."
|
||||
/bin/sleep $SLEEPSEC
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -S /var/run/control.unit.sock ]; then
|
||||
kill -KILL $(/bin/cat /var/run/unit.pid)
|
||||
rm -f /var/run/control.unit.sock
|
||||
fi
|
||||
echo
|
||||
echo "$0: Unit initial configuration complete; ready for start up..."
|
||||
echo
|
||||
fi
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set Git config
|
||||
run: |
|
||||
git config --local user.email "actions@github.com"
|
||||
|
||||
18
.github/workflows/publish.yml
vendored
18
.github/workflows/publish.yml
vendored
@@ -3,9 +3,7 @@ name: Publish
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: Tests
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
branches: main
|
||||
types: completed
|
||||
workflow_dispatch:
|
||||
push:
|
||||
@@ -18,18 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' || github.event_name == 'push' }}
|
||||
steps:
|
||||
- name: Checkout triggering commit
|
||||
if: ${{ github.event_name == 'workflow_run' }}
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
- name: Checkout main commit
|
||||
if: ${{ github.event_name != 'workflow_run' }}
|
||||
uses: actions/checkout@v4
|
||||
- name: Set build vars
|
||||
run: |
|
||||
echo "BUILD_TIME=$(date +'%Y-%m-%dT%H:%M:%S')" >> $GITHUB_ENV
|
||||
echo "BUILD_HASH=$GITHUB_SHA" >> $GITHUB_ENV
|
||||
- uses: actions/checkout@master
|
||||
- name: Publish to Registry
|
||||
uses: elgohr/Publish-Docker-Github-Action@main
|
||||
with:
|
||||
@@ -38,5 +25,4 @@ jobs:
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
cache: ${{ github.event_name != 'schedule' }}
|
||||
buildoptions: "--build-arg RUN_TESTS=false"
|
||||
buildargs: BUILD_TIME,BUILD_HASH
|
||||
tag_semver: true
|
||||
|
||||
49
.github/workflows/release.yml
vendored
49
.github/workflows/release.yml
vendored
@@ -11,31 +11,52 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Get version from tag
|
||||
id: get_version
|
||||
run: echo VERSION=${GITHUB_REF/refs\/tags\/v/} >> $GITHUB_OUTPUT
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/}
|
||||
|
||||
- name: Check and set sys_config
|
||||
run: |
|
||||
grep ${{ steps.get_version.outputs.VERSION }} core/sys_config.php
|
||||
echo "_d('BUILD_TIME', '$(date +'%Y-%m-%dT%H:%M:%S')');" >> core/sys_config.php
|
||||
echo "_d('BUILD_HASH', '$GITHUB_SHA');" >> core/sys_config.php
|
||||
- name: Test version in sys_config
|
||||
run: grep ${{ steps.get_version.outputs.VERSION }} core/sys_config.php
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
composer install --no-dev --no-progress
|
||||
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
|
||||
uses: softprops/action-gh-release@v1
|
||||
id: create_release
|
||||
uses: actions/create-release@latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: Shimmie ${{ steps.get_version.outputs.VERSION }}
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Shimmie ${{ steps.get_version.outputs.VERSION }}
|
||||
body: Automated release from tags
|
||||
files: |
|
||||
../shimmie2-${{ steps.get_version.outputs.VERSION }}.zip
|
||||
../shimmie2-${{ steps.get_version.outputs.VERSION }}.tgz
|
||||
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
|
||||
|
||||
115
.github/workflows/tests.yml
vendored
115
.github/workflows/tests.yml
vendored
@@ -2,9 +2,6 @@ name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 2 * * 0' # Weekly on Sundays at 02:00
|
||||
@@ -15,9 +12,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
- name: Set Up Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
vendor
|
||||
@@ -29,7 +26,7 @@ jobs:
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@master
|
||||
with:
|
||||
php-version: 8.3
|
||||
php-version: 8.1
|
||||
- name: Format
|
||||
run: ./vendor/bin/php-cs-fixer fix && git diff --exit-code
|
||||
|
||||
@@ -38,11 +35,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Set Up Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
vendor
|
||||
@@ -55,97 +52,79 @@ jobs:
|
||||
configuration: tests/phpstan.neon
|
||||
memory_limit: 1G
|
||||
|
||||
upgrade:
|
||||
name: Upgrade from 2.9 ${{ matrix.database }}
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['8.3']
|
||||
database: ['pgsql', 'mysql', 'sqlite']
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout current
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Travel to past
|
||||
# is there a way to programatically get "the most recent
|
||||
# tagged minor version of the previous major version"?
|
||||
run: git checkout branch-2.9
|
||||
- name: Set Up Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
vendor
|
||||
key: vendor-${{ matrix.php }}-${{ hashFiles('composer.lock') }}
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@master
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
- name: Set up database
|
||||
run: ./tests/setup-db.sh "${{ matrix.database }}"
|
||||
- name: Install PHP dependencies
|
||||
run: composer install --no-progress
|
||||
- name: Install old version
|
||||
run: |
|
||||
php index.php
|
||||
cat data/config/shimmie.conf.php
|
||||
- name: Check old version works
|
||||
run: |
|
||||
php index.php get-page / > old.out
|
||||
grep -q 'Welcome to Shimmie 2.9' old.out || cat old.out
|
||||
rm -f old.out
|
||||
- name: Upgrade
|
||||
run: |
|
||||
git checkout ${{ github.sha }}
|
||||
composer install --no-progress
|
||||
php index.php db-upgrade
|
||||
- name: Check new version works
|
||||
run: |
|
||||
php index.php page:get / > new.out
|
||||
grep -q 'Welcome to Shimmie 2.10' new.out || cat new.out
|
||||
rm -f new.out
|
||||
|
||||
test:
|
||||
name: PHP ${{ matrix.php }} / DB ${{ matrix.database }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: ['8.1', '8.2', '8.3']
|
||||
php: ['8.1', '8.2']
|
||||
database: ['pgsql', 'mysql', 'sqlite']
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set Up Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
vendor
|
||||
key: vendor-${{ matrix.php }}-${{ hashFiles('composer.lock') }}
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@master
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
coverage: pcov
|
||||
extensions: mbstring
|
||||
|
||||
- name: Set up database
|
||||
run: ./tests/setup-db.sh "${{ matrix.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 --no-progress
|
||||
run: composer update && composer install --prefer-dist --no-progress
|
||||
|
||||
- name: Run test suite
|
||||
run: |
|
||||
if [[ "${{ matrix.php }}" == "8.3" ]]; then
|
||||
vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover=data/coverage.clover
|
||||
else
|
||||
vendor/bin/phpunit --configuration tests/phpunit.xml
|
||||
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
|
||||
if: matrix.php == '8.3'
|
||||
if: matrix.php == '8.1'
|
||||
run: |
|
||||
vendor/bin/ocular code-coverage:upload --format=php-clover data/coverage.clover
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,9 +1,16 @@
|
||||
backup
|
||||
data
|
||||
.docker/entrypoint.d/config.json
|
||||
images
|
||||
thumbs
|
||||
*.phar
|
||||
*.sqlite
|
||||
*.cache
|
||||
.devcontainer
|
||||
trace.json
|
||||
|
||||
#Composer
|
||||
composer.phar
|
||||
composer.lock
|
||||
/vendor/
|
||||
|
||||
# Created by http://www.gitignore.io
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
# any requests for files which don't physically exist should be handled by index.php
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^(.*)$ index.php?q=$1&%{QUERY_STRING} "[L,B= ?,BNP]"
|
||||
RewriteRule ^(.*)$ index.php?q=$1&%{QUERY_STRING} [L]
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_expires.c>
|
||||
|
||||
@@ -14,5 +14,4 @@ return $_phpcs_config->setRules([
|
||||
'array_syntax' => ['syntax' => 'short'],
|
||||
])
|
||||
->setFinder($_phpcs_finder)
|
||||
->setCacheFile("data/php-cs-fixer.cache")
|
||||
;
|
||||
;
|
||||
82
Dockerfile
82
Dockerfile
@@ -1,77 +1,49 @@
|
||||
ARG PHP_VERSION=8.2
|
||||
|
||||
# Tree of layers:
|
||||
# base
|
||||
# ├── dev-tools
|
||||
# │ ├── build
|
||||
# │ │ └── tests
|
||||
# │ └── devcontainer
|
||||
# └── run (copies built artifacts out of build)
|
||||
|
||||
# Install base packages
|
||||
# Things which all stages (build, test, run) need
|
||||
# Install base packages which all stages (build, test, run) need
|
||||
FROM debian:bookworm AS base
|
||||
COPY --from=mwader/static-ffmpeg:6.1 /ffmpeg /ffprobe /usr/local/bin/
|
||||
RUN apt update && \
|
||||
apt upgrade -y && \
|
||||
apt install -y curl && \
|
||||
curl --output /usr/share/keyrings/nginx-keyring.gpg https://unit.nginx.org/keys/nginx-keyring.gpg && \
|
||||
echo 'deb [signed-by=/usr/share/keyrings/nginx-keyring.gpg] https://packages.nginx.org/unit/debian/ bookworm unit' > /etc/apt/sources.list.d/unit.list && \
|
||||
apt update && apt install -y --no-install-recommends \
|
||||
php${PHP_VERSION}-cli \
|
||||
php${PHP_VERSION}-gd php${PHP_VERSION}-zip php${PHP_VERSION}-xml php${PHP_VERSION}-mbstring php${PHP_VERSION}-curl \
|
||||
RUN apt update && apt upgrade -y && apt install -y \
|
||||
php${PHP_VERSION}-cli php${PHP_VERSION}-gd php${PHP_VERSION}-zip php${PHP_VERSION}-xml php${PHP_VERSION}-mbstring \
|
||||
php${PHP_VERSION}-pgsql php${PHP_VERSION}-mysql php${PHP_VERSION}-sqlite3 \
|
||||
php${PHP_VERSION}-memcached \
|
||||
curl imagemagick zip unzip unit unit-php gettext && \
|
||||
gosu curl imagemagick ffmpeg zip unzip && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
RUN ln -sf /dev/stderr /var/log/unit.log
|
||||
|
||||
# Install dev packages
|
||||
# Things which are only needed during development - Composer has 100MB of
|
||||
# dependencies, so let's avoid including that in the final image
|
||||
FROM base AS dev-tools
|
||||
RUN apt update && apt upgrade -y && \
|
||||
apt install -y composer php${PHP_VERSION}-xdebug git procps net-tools vim && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
ENV XDEBUG_MODE=coverage
|
||||
# Composer has 100MB of dependencies, and we only need that during build and test
|
||||
FROM base AS composer
|
||||
RUN apt update && apt upgrade -y && apt install -y composer php${PHP_VERSION}-xdebug && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# "Build" shimmie (composer install)
|
||||
# Done in its own stage so that we don't meed to include all the
|
||||
# composer fluff in the final image
|
||||
FROM dev-tools AS build
|
||||
# "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 composer AS app
|
||||
COPY composer.json composer.lock /app/
|
||||
WORKDIR /app
|
||||
RUN composer install --no-dev --no-progress
|
||||
RUN composer install --no-dev
|
||||
COPY . /app/
|
||||
|
||||
# Tests in their own image.
|
||||
# Re-run composer install to get dev dependencies
|
||||
FROM build AS tests
|
||||
RUN composer install --no-progress
|
||||
# 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 composer AS tests
|
||||
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 '=== Coverage ===' && XDEBUG_MODE=coverage ./vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-text && \
|
||||
echo '=== Cleaning ===' && rm -rf data)
|
||||
|
||||
# Devcontainer target
|
||||
# Contains all of the build and debug tools, but no code, since
|
||||
# that's mounted from the host
|
||||
FROM dev-tools AS devcontainer
|
||||
EXPOSE 8000
|
||||
|
||||
# Actually run shimmie
|
||||
FROM base AS run
|
||||
FROM base
|
||||
EXPOSE 8000
|
||||
# HEALTHCHECK --interval=1m --timeout=3s CMD curl --fail http://127.0.0.1:8000/ || exit 1
|
||||
ARG BUILD_TIME=unknown BUILD_HASH=unknown
|
||||
ENV UID=1000 GID=1000 UPLOAD_MAX_FILESIZE=50M
|
||||
COPY --from=build /app /app
|
||||
HEALTHCHECK --interval=1m --timeout=3s CMD curl --fail http://127.0.0.1:8000/ || exit 1
|
||||
ENV UID=1000 \
|
||||
GID=1000 \
|
||||
UPLOAD_MAX_FILESIZE=50M
|
||||
COPY --from=app /app /app
|
||||
|
||||
WORKDIR /app
|
||||
RUN echo "_d('BUILD_TIME', '$BUILD_TIME');" >> core/sys_config.php && \
|
||||
echo "_d('BUILD_HASH', '$BUILD_HASH');" >> core/sys_config.php
|
||||
ENTRYPOINT ["/app/.docker/entrypoint.sh"]
|
||||
CMD ["unitd", "--no-daemon", "--control", "unix:/var/run/control.unit.sock"]
|
||||
CMD ["/bin/sh", "/app/tests/docker-init.sh"]
|
||||
|
||||
15
README.md
15
README.md
@@ -10,12 +10,10 @@
|
||||
|
||||
# Shimmie
|
||||
|
||||
[](https://github.com/shish/shimmie2/actions)
|
||||
[](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=main)
|
||||
[](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=main)
|
||||
[](https://matrix.to/#/#shimmie:matrix.org)
|
||||
[](https://github.com/shish/shimmie2/actions)
|
||||
[](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=master)
|
||||
[](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=master)
|
||||
|
||||
[](https://codespaces.new/shish/shimmie2?quickstart=1)
|
||||
|
||||
# Documentation
|
||||
|
||||
@@ -28,6 +26,13 @@
|
||||
* [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.
|
||||
|
||||
@@ -35,31 +35,30 @@
|
||||
"ext-pdo": "*",
|
||||
"ext-json": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"flexihash/flexihash": "^2.0",
|
||||
"ifixit/php-akismet": "^1.0",
|
||||
"google/recaptcha": "^1.1",
|
||||
"shish/eventtracer-php": "^2.0",
|
||||
"shish/ffsphp": "^1.3",
|
||||
"shish/microbundler": "^1.0",
|
||||
"shish/microcrud": "^2.0",
|
||||
"shish/microhtml": "^2.2",
|
||||
"shish/gqla": "dev-main",
|
||||
"enshrined/svg-sanitize": "^0.16",
|
||||
"bower-asset/jquery": "^1.12",
|
||||
"bower-asset/jquery-timeago": "^1.5",
|
||||
"bower-asset/js-cookie": "^2.1",
|
||||
"psr/simple-cache": "^1.0",
|
||||
"sabre/cache": "^2.0.1",
|
||||
"naroga/redis-cache": "dev-master",
|
||||
"aws/aws-sdk-php": "^3.294",
|
||||
"symfony/console": "6.4.x-dev"
|
||||
|
||||
"flexihash/flexihash" : "^2.0",
|
||||
"ifixit/php-akismet" : "^1.0",
|
||||
"google/recaptcha" : "^1.1",
|
||||
"shish/eventtracer-php" : "^2.0",
|
||||
"shish/ffsphp" : "^1.0",
|
||||
"shish/microcrud" : "^2.0",
|
||||
"shish/microhtml" : "^2.0",
|
||||
"shish/gqla" : "dev-main",
|
||||
"enshrined/svg-sanitize" : "^0.16",
|
||||
|
||||
"bower-asset/jquery" : "^1.12",
|
||||
"bower-asset/jquery-timeago" : "^1.5",
|
||||
"bower-asset/js-cookie" : "^2.1",
|
||||
"psr/simple-cache" : "^1.0",
|
||||
"sabre/cache" : "^2.0.1",
|
||||
"naroga/redis-cache": "dev-master"
|
||||
},
|
||||
|
||||
"require-dev" : {
|
||||
"phpunit/phpunit" : "10.5.3",
|
||||
"friendsofphp/php-cs-fixer" : "3.41.1",
|
||||
"scrutinizer/ocular": "1.9",
|
||||
"phpstan/phpstan": "1.10.50"
|
||||
"phpunit/phpunit" : "^9.0",
|
||||
"friendsofphp/php-cs-fixer" : "^3.12",
|
||||
"scrutinizer/ocular": "dev-master",
|
||||
"phpstan/phpstan": "1.10.x-dev"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-memcache": "memcache caching",
|
||||
|
||||
3827
composer.lock
generated
3827
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,6 @@ namespace Shimmie2;
|
||||
|
||||
use MicroHTML\HTMLElement;
|
||||
|
||||
use function MicroHTML\{emptyHTML,rawHTML,HTML,HEAD,BODY};
|
||||
|
||||
require_once "core/event.php";
|
||||
|
||||
enum PageMode: string
|
||||
@@ -19,22 +17,6 @@ enum PageMode: string
|
||||
case MANUAL = 'manual';
|
||||
}
|
||||
|
||||
class Cookie
|
||||
{
|
||||
public string $name;
|
||||
public string $value;
|
||||
public int $time;
|
||||
public string $path;
|
||||
|
||||
public function __construct(string $name, string $value, int $time, string $path)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->value = $value;
|
||||
$this->time = $time;
|
||||
$this->path = $path;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Page
|
||||
*
|
||||
@@ -129,7 +111,6 @@ class BasePage
|
||||
public string $title = "";
|
||||
public string $heading = "";
|
||||
public string $subheading = "";
|
||||
public bool $left_enabled = true;
|
||||
|
||||
/** @var string[] */
|
||||
public array $html_headers = [];
|
||||
@@ -137,7 +118,7 @@ class BasePage
|
||||
/** @var string[] */
|
||||
public array $http_headers = [];
|
||||
|
||||
/** @var Cookie[] */
|
||||
/** @var string[][] */
|
||||
public array $cookies = [];
|
||||
|
||||
/** @var Block[] */
|
||||
@@ -174,11 +155,6 @@ class BasePage
|
||||
$this->flash[] = $message;
|
||||
}
|
||||
|
||||
public function disable_left(): void
|
||||
{
|
||||
$this->left_enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a line to the HTML head section.
|
||||
*/
|
||||
@@ -209,7 +185,7 @@ class BasePage
|
||||
public function add_cookie(string $name, string $value, int $time, string $path): void
|
||||
{
|
||||
$full_name = COOKIE_PREFIX . "_" . $name;
|
||||
$this->cookies[] = new Cookie($full_name, $value, $time, $path);
|
||||
$this->cookies[] = [$full_name, $value, $time, $path];
|
||||
}
|
||||
|
||||
public function get_cookie(string $name): ?string
|
||||
@@ -270,7 +246,7 @@ class BasePage
|
||||
header($head);
|
||||
}
|
||||
foreach ($this->cookies as $c) {
|
||||
setcookie($c->name, $c->value, $c->time, $c->path);
|
||||
setcookie($c[0], $c[1], $c[2], $c[3]);
|
||||
}
|
||||
} else {
|
||||
print "Error: Headers have already been sent to the client.";
|
||||
@@ -282,7 +258,7 @@ class BasePage
|
||||
*/
|
||||
public function display(): void
|
||||
{
|
||||
if ($this->mode != PageMode::MANUAL) {
|
||||
if ($this->mode!=PageMode::MANUAL) {
|
||||
$this->send_headers();
|
||||
}
|
||||
|
||||
@@ -308,7 +284,7 @@ class BasePage
|
||||
assert($this->file, "file should not be null with PageMode::FILE");
|
||||
|
||||
// https://gist.github.com/codler/3906826
|
||||
$size = filesize_ex($this->file); // File size
|
||||
$size = filesize($this->file); // File size
|
||||
$length = $size; // Content length
|
||||
$start = 0; // Start byte
|
||||
$end = $size - 1; // End byte
|
||||
@@ -383,6 +359,8 @@ class BasePage
|
||||
$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);
|
||||
@@ -393,18 +371,7 @@ class BasePage
|
||||
$config_latest = max($config_latest, filemtime($conf));
|
||||
}
|
||||
|
||||
$css_cache_file = $this->get_css_cache_file($theme_name, $config_latest);
|
||||
$this->add_html_header("<link rel='stylesheet' href='$data_href/$css_cache_file' type='text/css'>", 43);
|
||||
|
||||
$initjs_cache_file = $this->get_initjs_cache_file($theme_name, $config_latest);
|
||||
$this->add_html_header("<script src='$data_href/$initjs_cache_file' type='text/javascript'></script>", 44);
|
||||
|
||||
$js_cache_file = $this->get_js_cache_file($theme_name, $config_latest);
|
||||
$this->add_html_header("<script defer src='$data_href/$js_cache_file' type='text/javascript'></script>", 44);
|
||||
}
|
||||
|
||||
private function get_css_cache_file(string $theme_name, int $config_latest): string
|
||||
{
|
||||
/*** Generate CSS cache files ***/
|
||||
$css_latest = $config_latest;
|
||||
$css_files = array_merge(
|
||||
zglob("ext/{" . Extension::get_enabled_extensions_as_string() . "}/style.css"),
|
||||
@@ -416,47 +383,26 @@ class BasePage
|
||||
$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)) {
|
||||
$mcss = new \MicroBundler\MicroBundler();
|
||||
foreach($css_files as $css) {
|
||||
$mcss->addSource($css);
|
||||
$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";
|
||||
}
|
||||
$mcss->save($css_cache_file);
|
||||
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);
|
||||
|
||||
return $css_cache_file;
|
||||
}
|
||||
|
||||
private function get_initjs_cache_file(string $theme_name, int $config_latest): string
|
||||
{
|
||||
$js_latest = $config_latest;
|
||||
$js_files = array_merge(
|
||||
zglob("ext/{" . Extension::get_enabled_extensions_as_string() . "}/init.js"),
|
||||
zglob("themes/$theme_name/init.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/initscript/{$theme_name}.{$js_latest}.{$js_md5}.js");
|
||||
if (!file_exists($js_cache_file)) {
|
||||
$mcss = new \MicroBundler\MicroBundler();
|
||||
foreach($js_files as $js) {
|
||||
$mcss->addSource($js);
|
||||
}
|
||||
$mcss->save($js_cache_file);
|
||||
}
|
||||
|
||||
return $js_cache_file;
|
||||
}
|
||||
|
||||
private function get_js_cache_file(string $theme_name, int $config_latest): string
|
||||
{
|
||||
/*** Generate JS cache files ***/
|
||||
$js_latest = $config_latest;
|
||||
$js_files = array_merge(
|
||||
[
|
||||
"vendor/bower-asset/jquery/dist/jquery.min.js",
|
||||
"vendor/bower-asset/jquery-timeago/jquery.timeago.js",
|
||||
"vendor/bower-asset/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/{" . implode(",", $this->get_theme_scripts()) . "}")
|
||||
@@ -467,18 +413,18 @@ class BasePage
|
||||
$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)) {
|
||||
$mcss = new \MicroBundler\MicroBundler();
|
||||
foreach($js_files as $js) {
|
||||
$mcss->addSource($js);
|
||||
$js_data = "";
|
||||
foreach ($js_files as $file) {
|
||||
$js_data .= file_get_contents($file) . "\n";
|
||||
}
|
||||
$mcss->save($js_cache_file);
|
||||
file_put_contents($js_cache_file, $js_data);
|
||||
}
|
||||
|
||||
return $js_cache_file;
|
||||
$this->add_html_header("<script defer src='$data_href/$js_cache_file' type='text/javascript'></script>", 44);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string[] A list of stylesheets relative to the theme root.
|
||||
* @return array A list of stylesheets relative to the theme root.
|
||||
*/
|
||||
protected function get_theme_stylesheets(): array
|
||||
{
|
||||
@@ -487,16 +433,13 @@ class BasePage
|
||||
|
||||
|
||||
/**
|
||||
* @return string[] A list of script files relative to the theme root.
|
||||
* @return array A list of script files relative to the theme root.
|
||||
*/
|
||||
protected function get_theme_scripts(): array
|
||||
{
|
||||
return ["script.js"];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: NavLink[], 1: NavLink[]}
|
||||
*/
|
||||
protected function get_nav_links(): array
|
||||
{
|
||||
$pnbe = send_event(new PageNavBuildingEvent());
|
||||
@@ -506,14 +449,14 @@ class BasePage
|
||||
$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) {
|
||||
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) {
|
||||
if ($active_link!==null) {
|
||||
$psnbe = send_event(new PageSubNavBuildingEvent($active_link->name));
|
||||
$sub_links = $psnbe->links;
|
||||
} else {
|
||||
@@ -523,23 +466,22 @@ class BasePage
|
||||
|
||||
// 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) {
|
||||
if ($sub_link->active===true) {
|
||||
$sub_links = $psnbe->links;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If the active link has been detected, we break out
|
||||
if ($sub_links !== null) {
|
||||
if ($sub_links!==null) {
|
||||
$link->active = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sub_links = $sub_links ?? [];
|
||||
|
||||
usort($nav_links, fn (NavLink $a, NavLink $b) => $a->order - $b->order);
|
||||
usort($sub_links, fn (NavLink $a, NavLink $b) => $a->order - $b->order);
|
||||
$sub_links = $sub_links??[];
|
||||
usort($nav_links, "Shimmie2\sort_nav_links");
|
||||
usort($sub_links, "Shimmie2\sort_nav_links");
|
||||
|
||||
return [$nav_links, $sub_links];
|
||||
}
|
||||
@@ -547,26 +489,18 @@ class BasePage
|
||||
/**
|
||||
* turns the Page into HTML
|
||||
*/
|
||||
public function render(): void
|
||||
public function render()
|
||||
{
|
||||
global $config, $user;
|
||||
$head_html = $this->head_html();
|
||||
$body_html = $this->body_html();
|
||||
|
||||
$head = $this->head_html();
|
||||
$body = $this->body_html();
|
||||
|
||||
$body_attrs = [
|
||||
"data-userclass" => $user->class->name,
|
||||
"data-base-href" => get_base_href(),
|
||||
];
|
||||
|
||||
print emptyHTML(
|
||||
rawHTML("<!doctype html>"),
|
||||
HTML(
|
||||
["lang" => "en"],
|
||||
HEAD(rawHTML($head)),
|
||||
BODY($body_attrs, rawHTML($body))
|
||||
)
|
||||
);
|
||||
print <<<EOD
|
||||
<!doctype html>
|
||||
<html class="no-js" lang="en">
|
||||
$head_html
|
||||
$body_html
|
||||
</html>
|
||||
EOD;
|
||||
}
|
||||
|
||||
protected function head_html(): string
|
||||
@@ -574,8 +508,10 @@ class BasePage
|
||||
$html_header_html = $this->get_all_html_headers();
|
||||
|
||||
return "
|
||||
<title>{$this->title}</title>
|
||||
$html_header_html
|
||||
<head>
|
||||
<title>{$this->title}</title>
|
||||
$html_header_html
|
||||
</head>
|
||||
";
|
||||
}
|
||||
|
||||
@@ -602,23 +538,30 @@ class BasePage
|
||||
}
|
||||
}
|
||||
|
||||
$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 "
|
||||
<header>
|
||||
<h1>{$this->heading}</h1>
|
||||
$sub_block_html
|
||||
</header>
|
||||
<nav>
|
||||
$left_block_html
|
||||
</nav>
|
||||
<article>
|
||||
$flash_html
|
||||
$main_block_html
|
||||
</article>
|
||||
<footer>
|
||||
$footer_html
|
||||
</footer>
|
||||
<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>
|
||||
";
|
||||
}
|
||||
|
||||
@@ -633,7 +576,7 @@ class BasePage
|
||||
<a href=\"https://code.shishnet.org/shimmie2/\">Shimmie</a> ©
|
||||
<a href=\"https://www.shishnet.org/\">Shish</a> &
|
||||
<a href=\"https://github.com/shish/shimmie2/graphs/contributors\">The Team</a>
|
||||
2007-2024,
|
||||
2007-2023,
|
||||
based on the Danbooru concept.
|
||||
$debug
|
||||
$contact
|
||||
@@ -643,10 +586,9 @@ class BasePage
|
||||
|
||||
class PageNavBuildingEvent extends Event
|
||||
{
|
||||
/** @var NavLink[] */
|
||||
public array $links = [];
|
||||
|
||||
public function add_nav_link(string $name, Link $link, string $desc, ?bool $active = null, int $order = 50): void
|
||||
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);
|
||||
}
|
||||
@@ -656,16 +598,15 @@ class PageSubNavBuildingEvent extends Event
|
||||
{
|
||||
public string $parent;
|
||||
|
||||
/** @var NavLink[] */
|
||||
public array $links = [];
|
||||
|
||||
public function __construct(string $parent)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->parent = $parent;
|
||||
$this->parent= $parent;
|
||||
}
|
||||
|
||||
public function add_nav_link(string $name, Link $link, string|HTMLElement $desc, ?bool $active = null, int $order = 50): void
|
||||
public function add_nav_link(string $name, Link $link, string|HTMLElement $desc, ?bool $active = null, int $order = 50)
|
||||
{
|
||||
$this->links[] = new NavLink($name, $link, $desc, $active, $order);
|
||||
}
|
||||
@@ -687,8 +628,8 @@ class NavLink
|
||||
$this->link = $link;
|
||||
$this->description = $description;
|
||||
$this->order = $order;
|
||||
if ($active == null) {
|
||||
$query = _get_query();
|
||||
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), "/");
|
||||
@@ -698,7 +639,7 @@ class NavLink
|
||||
} else {
|
||||
$this->active = self::is_active([$link->page], $front_page);
|
||||
}
|
||||
} elseif ($query === $link->page) {
|
||||
} elseif ($query===$link->page) {
|
||||
$this->active = true;
|
||||
} else {
|
||||
$this->active = self::is_active([$link->page]);
|
||||
@@ -708,26 +649,23 @@ class NavLink
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $pages_matched
|
||||
*/
|
||||
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 ?? _get_query();
|
||||
$url = $url??ltrim(_get_query(), "/");
|
||||
|
||||
$re1 = '.*?';
|
||||
$re2 = '((?:[a-z][a-z_]+))';
|
||||
$re1='.*?';
|
||||
$re2='((?:[a-z][a-z_]+))';
|
||||
|
||||
if (preg_match_all("/".$re1.$re2."/is", $url, $matches)) {
|
||||
$url = $matches[1][0];
|
||||
$url=$matches[1][0];
|
||||
}
|
||||
|
||||
$count_pages_matched = count($pages_matched);
|
||||
|
||||
for ($i = 0; $i < $count_pages_matched; $i++) {
|
||||
for ($i=0; $i < $count_pages_matched; $i++) {
|
||||
if ($url == $pages_matched[$i]) {
|
||||
return true;
|
||||
}
|
||||
@@ -736,3 +674,8 @@ class NavLink
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function sort_nav_links(NavLink $a, NavLink $b): int
|
||||
{
|
||||
return $a->order - $b->order;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Shimmie2;
|
||||
|
||||
use MicroHTML\HTMLElement;
|
||||
|
||||
use function MicroHTML\{A,B,BR,IMG,emptyHTML,joinHTML};
|
||||
use function MicroHTML\{A,B,BR,IMG,OPTION,SELECT,emptyHTML};
|
||||
|
||||
/**
|
||||
* Class BaseThemelet
|
||||
@@ -70,44 +70,39 @@ class BaseThemelet
|
||||
}
|
||||
|
||||
$custom_classes = "";
|
||||
if (Extension::is_enabled(RelationshipsInfo::KEY)) {
|
||||
if ($image['parent_id'] !== null) {
|
||||
if (class_exists("Shimmie2\Relationships")) {
|
||||
if (property_exists($image, 'parent_id') && $image->parent_id !== null) {
|
||||
$custom_classes .= "shm-thumb-has_parent ";
|
||||
}
|
||||
if ($image['has_children']) {
|
||||
if (property_exists($image, 'has_children') && bool_escape($image->has_children)) {
|
||||
$custom_classes .= "shm-thumb-has_child ";
|
||||
}
|
||||
}
|
||||
|
||||
$attrs = [
|
||||
"href" => $view_link,
|
||||
"class" => "thumb shm-thumb shm-thumb-link $custom_classes",
|
||||
"data-tags" => $tags,
|
||||
"data-height" => $image->height,
|
||||
"data-width" => $image->width,
|
||||
"data-mime" => $image->get_mime(),
|
||||
"data-post-id" => $id,
|
||||
];
|
||||
if(Extension::is_enabled(RatingsInfo::KEY)) {
|
||||
$attrs["data-rating"] = $image['rating'];
|
||||
}
|
||||
|
||||
return A(
|
||||
$attrs,
|
||||
[
|
||||
"href"=>$view_link,
|
||||
"class"=>"thumb shm-thumb shm-thumb-link $custom_classes",
|
||||
"data-tags"=>$tags,
|
||||
"data-height"=>$image->height,
|
||||
"data-width"=>$image->width,
|
||||
"data-mime"=>$image->get_mime(),
|
||||
"data-post-id"=>$id,
|
||||
],
|
||||
IMG(
|
||||
[
|
||||
"id" => "thumb_$id",
|
||||
"title" => $tip,
|
||||
"alt" => $tip,
|
||||
"height" => $tsize[1],
|
||||
"width" => $tsize[0],
|
||||
"src" => $thumb_link,
|
||||
"id"=>"thumb_$id",
|
||||
"title"=>$tip,
|
||||
"alt"=>$tip,
|
||||
"height"=>$tsize[1],
|
||||
"width"=>$tsize[0],
|
||||
"src"=>$thumb_link,
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function display_paginator(Page $page, string $base, ?string $query, int $page_number, int $total_pages, bool $show_random = false): void
|
||||
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;
|
||||
@@ -117,18 +112,18 @@ class BaseThemelet
|
||||
|
||||
$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))."'>");
|
||||
$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='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): HTMLElement
|
||||
{
|
||||
return A(["href" => make_link($base_url.'/'.$page, $query)], $name);
|
||||
return A(["href"=>make_link($base_url.'/'.$page, $query)], $name);
|
||||
}
|
||||
|
||||
private function gen_page_link_block(string $base_url, ?string $query, int $page, int $current_page, string $name): HTMLElement
|
||||
@@ -140,6 +135,19 @@ class BaseThemelet
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
protected function implode(string|HTMLElement $glue, array $pieces): HTMLElement
|
||||
{
|
||||
$out = emptyHTML();
|
||||
$n = 0;
|
||||
foreach ($pieces as $piece) {
|
||||
if ($n++ > 0) {
|
||||
$out->appendChild($glue);
|
||||
}
|
||||
$out->appendChild($piece);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function build_paginator(int $current_page, int $total_pages, string $base_url, ?string $query, bool $show_random): HTMLElement
|
||||
{
|
||||
$next = $current_page + 1;
|
||||
@@ -167,10 +175,10 @@ class BaseThemelet
|
||||
foreach (range($start, $end) as $i) {
|
||||
$pages[] = $this->gen_page_link_block($base_url, $query, $i, $current_page, (string)$i);
|
||||
}
|
||||
$pages_html = joinHTML(" | ", $pages);
|
||||
$pages_html = $this->implode(" | ", $pages);
|
||||
|
||||
return emptyHTML(
|
||||
joinHTML(" | ", [
|
||||
$this->implode(" | ", [
|
||||
$first_html,
|
||||
$prev_html,
|
||||
$random_html,
|
||||
|
||||
@@ -45,7 +45,7 @@ class Block
|
||||
*/
|
||||
public bool $is_content = true;
|
||||
|
||||
public function __construct(string $header = null, string|\MicroHTML\HTMLElement $body = null, string $section = "main", int $position = 50, string $id = null)
|
||||
public function __construct(string $header=null, string|\MicroHTML\HTMLElement $body=null, string $section="main", int $position=50, string $id=null)
|
||||
{
|
||||
$this->header = $header;
|
||||
$this->body = (string)$body;
|
||||
@@ -63,7 +63,7 @@ class Block
|
||||
/**
|
||||
* Get the HTML for this block.
|
||||
*/
|
||||
public function get_html(bool $hidable = false): string
|
||||
public function get_html(bool $hidable=false): string
|
||||
{
|
||||
$h = $this->header;
|
||||
$b = $this->body;
|
||||
|
||||
@@ -10,8 +10,8 @@ class EventTracingCache implements CacheInterface
|
||||
{
|
||||
private CacheInterface $engine;
|
||||
private \EventTracer $tracer;
|
||||
private int $hits = 0;
|
||||
private int $misses = 0;
|
||||
private int $hits=0;
|
||||
private int $misses=0;
|
||||
|
||||
public function __construct(CacheInterface $engine, \EventTracer $tracer)
|
||||
{
|
||||
@@ -19,7 +19,7 @@ class EventTracingCache implements CacheInterface
|
||||
$this->tracer = $tracer;
|
||||
}
|
||||
|
||||
public function get($key, $default = null)
|
||||
public function get($key, $default=null)
|
||||
{
|
||||
if ($key === "__etc_cache_hits") {
|
||||
return $this->hits;
|
||||
@@ -29,7 +29,7 @@ class EventTracingCache implements CacheInterface
|
||||
}
|
||||
|
||||
$sentinel = "__etc_sentinel";
|
||||
$this->tracer->begin("Cache Get", ["key" => $key]);
|
||||
$this->tracer->begin("Cache Get", ["key"=>$key]);
|
||||
$val = $this->engine->get($key, $sentinel);
|
||||
if ($val != $sentinel) {
|
||||
$res = "hit";
|
||||
@@ -39,13 +39,13 @@ class EventTracingCache implements CacheInterface
|
||||
$val = $default;
|
||||
$this->misses++;
|
||||
}
|
||||
$this->tracer->end(null, ["result" => $res]);
|
||||
$this->tracer->end(null, ["result"=>$res]);
|
||||
return $val;
|
||||
}
|
||||
|
||||
public function set($key, $value, $ttl = null)
|
||||
{
|
||||
$this->tracer->begin("Cache Set", ["key" => $key, "ttl" => $ttl]);
|
||||
$this->tracer->begin("Cache Set", ["key"=>$key, "ttl"=>$ttl]);
|
||||
$val = $this->engine->set($key, $value, $ttl);
|
||||
$this->tracer->end();
|
||||
return $val;
|
||||
@@ -53,7 +53,7 @@ class EventTracingCache implements CacheInterface
|
||||
|
||||
public function delete($key)
|
||||
{
|
||||
$this->tracer->begin("Cache Delete", ["key" => $key]);
|
||||
$this->tracer->begin("Cache Delete", ["key"=>$key]);
|
||||
$val = $this->engine->delete($key);
|
||||
$this->tracer->end();
|
||||
return $val;
|
||||
@@ -67,11 +67,6 @@ class EventTracingCache implements CacheInterface
|
||||
return $val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $keys
|
||||
* @param mixed $default
|
||||
* @return iterable<mixed>
|
||||
*/
|
||||
public function getMultiple($keys, $default = null)
|
||||
{
|
||||
$this->tracer->begin("Cache Get Multiple", ["keys" => $keys]);
|
||||
@@ -80,9 +75,6 @@ class EventTracingCache implements CacheInterface
|
||||
return $val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $values
|
||||
*/
|
||||
public function setMultiple($values, $ttl = null)
|
||||
{
|
||||
$this->tracer->begin("Cache Set Multiple", ["keys" => array_keys($values)]);
|
||||
@@ -91,9 +83,6 @@ class EventTracingCache implements CacheInterface
|
||||
return $val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $keys
|
||||
*/
|
||||
public function deleteMultiple($keys)
|
||||
{
|
||||
$this->tracer->begin("Cache Delete Multiple", ["keys" => $keys]);
|
||||
@@ -104,35 +93,33 @@ class EventTracingCache implements CacheInterface
|
||||
|
||||
public function has($key)
|
||||
{
|
||||
$this->tracer->begin("Cache Has", ["key" => $key]);
|
||||
$this->tracer->begin("Cache Has", ["key"=>$key]);
|
||||
$val = $this->engine->has($key);
|
||||
$this->tracer->end(null, ["exists" => $val]);
|
||||
$this->tracer->end(null, ["exists"=>$val]);
|
||||
return $val;
|
||||
}
|
||||
}
|
||||
|
||||
function loadCache(?string $dsn): CacheInterface
|
||||
{
|
||||
$matches = [];
|
||||
$c = null;
|
||||
if ($dsn && !isset($_GET['DISABLE_CACHE'])) {
|
||||
$url = parse_url($dsn);
|
||||
if($url) {
|
||||
if ($url['scheme'] == "memcached" || $url['scheme'] == "memcache") {
|
||||
$memcache = new \Memcached();
|
||||
$memcache->addServer($url['host'], $url['port']);
|
||||
$c = new \Sabre\Cache\Memcached($memcache);
|
||||
} elseif ($url['scheme'] == "apc") {
|
||||
$c = new \Sabre\Cache\Apcu();
|
||||
} elseif ($url['scheme'] == "redis") {
|
||||
$redis = new \Predis\Client([
|
||||
'scheme' => 'tcp',
|
||||
'host' => $url['host'] ?? "127.0.0.1",
|
||||
'port' => $url['port'] ?? 6379,
|
||||
'username' => $url['user'] ?? null,
|
||||
'password' => $url['pass'] ?? null,
|
||||
], ['prefix' => 'shm:']);
|
||||
$c = new \Naroga\RedisCache\Redis($redis);
|
||||
}
|
||||
if ($dsn && preg_match("#(.*)://(.*)#", $dsn, $matches) && !isset($_GET['DISABLE_CACHE'])) {
|
||||
if ($matches[1] == "memcached" || $matches[1] == "memcache") {
|
||||
$hp = explode(":", $matches[2]);
|
||||
$memcache = new \Memcached();
|
||||
$memcache->addServer($hp[0], (int)$hp[1]);
|
||||
$c = new \Sabre\Cache\Memcached($memcache);
|
||||
} elseif ($matches[1] == "apc") {
|
||||
$c = new \Sabre\Cache\Apcu();
|
||||
} elseif ($matches[1] == "redis") {
|
||||
$hp = explode(":", $matches[2]);
|
||||
$redis = new \Predis\Client([
|
||||
'scheme' => 'tcp',
|
||||
'host' => $hp[0],
|
||||
'port' => (int)$hp[1]
|
||||
], ['prefix' => 'shm:']);
|
||||
$c = new \Naroga\RedisCache\Redis($redis);
|
||||
}
|
||||
}
|
||||
if(is_null($c)) {
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use Symfony\Component\Console\Input\{ArgvInput,InputOption,InputDefinition,InputInterface};
|
||||
use Symfony\Component\Console\Output\{OutputInterface,ConsoleOutput};
|
||||
|
||||
class CliApp extends \Symfony\Component\Console\Application
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('Shimmie', VERSION);
|
||||
$this->setAutoExit(false);
|
||||
}
|
||||
|
||||
protected function getDefaultInputDefinition(): InputDefinition
|
||||
{
|
||||
$definition = parent::getDefaultInputDefinition();
|
||||
$definition->addOption(new InputOption(
|
||||
'--user',
|
||||
'-u',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Log in as the given user'
|
||||
));
|
||||
|
||||
return $definition;
|
||||
}
|
||||
|
||||
public function run(InputInterface $input = null, OutputInterface $output = null): int
|
||||
{
|
||||
global $user;
|
||||
|
||||
$input ??= new ArgvInput();
|
||||
$output ??= new ConsoleOutput();
|
||||
|
||||
if ($input->hasParameterOption(['--user', '-u'])) {
|
||||
$name = $input->getParameterOption(['--user', '-u']);
|
||||
$user = User::by_name($name);
|
||||
if (is_null($user)) {
|
||||
die("Unknown user '$name'\n");
|
||||
} else {
|
||||
send_event(new UserLoginEvent($user));
|
||||
}
|
||||
}
|
||||
|
||||
$log_level = SCORE_LOG_WARNING;
|
||||
if (true === $input->hasParameterOption(['--quiet', '-q'], true)) {
|
||||
$log_level = SCORE_LOG_ERROR;
|
||||
} else {
|
||||
if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || 3 === $input->getParameterOption('--verbose', false, true)) {
|
||||
$log_level = SCORE_LOG_DEBUG;
|
||||
} elseif ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || 2 === $input->getParameterOption('--verbose', false, true)) {
|
||||
$log_level = SCORE_LOG_DEBUG;
|
||||
} elseif ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true)) {
|
||||
$log_level = SCORE_LOG_INFO;
|
||||
}
|
||||
}
|
||||
if (!defined("CLI_LOG_LEVEL")) {
|
||||
define("CLI_LOG_LEVEL", $log_level);
|
||||
}
|
||||
|
||||
return parent::run($input, $output);
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,10 @@ namespace Shimmie2;
|
||||
class CommandBuilder
|
||||
{
|
||||
private string $executable;
|
||||
/** @var string[] */
|
||||
private array $args = [];
|
||||
/** @var string[] */
|
||||
public array $output;
|
||||
|
||||
public function __construct(string $executable)
|
||||
public function __construct(String $executable)
|
||||
{
|
||||
if (empty($executable)) {
|
||||
throw new \InvalidArgumentException("executable cannot be empty");
|
||||
@@ -63,7 +61,7 @@ class CommandBuilder
|
||||
|
||||
log_debug('command_builder', "Command `$cmd` returned $ret and outputted $output");
|
||||
|
||||
if ($fail_on_non_zero_return && (int)$ret !== (int)0) {
|
||||
if ($fail_on_non_zero_return&&(int)$ret!==(int)0) {
|
||||
throw new SCoreException("Command `$cmd` failed, returning $ret and outputting $output");
|
||||
}
|
||||
return $ret;
|
||||
|
||||
118
core/config.php
118
core/config.php
@@ -16,7 +16,7 @@ interface Config
|
||||
* so that the next time a page is loaded it will use the new
|
||||
* configuration.
|
||||
*/
|
||||
public function save(string $name = null): void;
|
||||
public function save(string $name=null): void;
|
||||
|
||||
//@{ /*--------------------------------- SET ------------------------------------------------------*/
|
||||
/**
|
||||
@@ -41,8 +41,6 @@ interface Config
|
||||
|
||||
/**
|
||||
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
||||
*
|
||||
* @param mixed[] $value
|
||||
*/
|
||||
public function set_array(string $name, array $value): void;
|
||||
//@} /*--------------------------------------------------------------------------------------------*/
|
||||
@@ -95,8 +93,6 @@ interface Config
|
||||
* 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 mixed[] $value
|
||||
*/
|
||||
public function set_default_array(string $name, array $value): void;
|
||||
//@} /*--------------------------------------------------------------------------------------------*/
|
||||
@@ -105,30 +101,27 @@ interface Config
|
||||
/**
|
||||
* 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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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.
|
||||
*
|
||||
* @param mixed[] $default
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function get_array(string $name, ?array $default = []): ?array;
|
||||
public function get_array(string $name, ?array $default=[]): ?array;
|
||||
//@} /*--------------------------------------------------------------------------------------------*/
|
||||
}
|
||||
|
||||
@@ -141,7 +134,6 @@ interface Config
|
||||
*/
|
||||
abstract class BaseConfig implements Config
|
||||
{
|
||||
/** @var array<string, mixed> */
|
||||
public array $values = [];
|
||||
|
||||
public function set_int(string $name, ?int $value): void
|
||||
@@ -170,7 +162,7 @@ abstract class BaseConfig implements Config
|
||||
|
||||
public function set_array(string $name, ?array $value): void
|
||||
{
|
||||
if ($value != null) {
|
||||
if ($value!=null) {
|
||||
$this->values[$name] = implode(",", $value);
|
||||
} else {
|
||||
$this->values[$name] = null;
|
||||
@@ -213,32 +205,17 @@ abstract class BaseConfig implements Config
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of int|null
|
||||
* @param T $default
|
||||
* @return T|int
|
||||
*/
|
||||
public function get_int(string $name, ?int $default = null): ?int
|
||||
public function get_int(string $name, ?int $default=null): ?int
|
||||
{
|
||||
return (int)($this->get($name, $default));
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of float|null
|
||||
* @param T $default
|
||||
* @return T|float
|
||||
*/
|
||||
public function get_float(string $name, ?float $default = null): ?float
|
||||
public function get_float(string $name, ?float $default=null): ?float
|
||||
{
|
||||
return (float)($this->get($name, $default));
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of string|null
|
||||
* @param T $default
|
||||
* @return T|string
|
||||
*/
|
||||
public function get_string(string $name, ?string $default = null): ?string
|
||||
public function get_string(string $name, ?string $default=null): ?string
|
||||
{
|
||||
$val = $this->get($name, $default);
|
||||
if (!is_string($val) && !is_null($val)) {
|
||||
@@ -247,34 +224,17 @@ abstract class BaseConfig implements Config
|
||||
return $val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of bool|null
|
||||
* @param T $default
|
||||
* @return T|bool
|
||||
*/
|
||||
public function get_bool(string $name, ?bool $default = null): ?bool
|
||||
public function get_bool(string $name, ?bool $default=null): ?bool
|
||||
{
|
||||
return bool_escape($this->get($name, $default));
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of array<string>|null
|
||||
* @param T $default
|
||||
* @return T|array<string>
|
||||
*/
|
||||
public function get_array(string $name, ?array $default = null): ?array
|
||||
public function get_array(string $name, ?array $default=[]): ?array
|
||||
{
|
||||
$val = $this->get($name);
|
||||
if(is_null($val)) {
|
||||
return $default;
|
||||
}
|
||||
if(empty($val)) {
|
||||
return [];
|
||||
}
|
||||
return explode(",", $val);
|
||||
return explode(",", $this->get($name, ""));
|
||||
}
|
||||
|
||||
private function get(string $name, mixed $default = null): mixed
|
||||
private function get(string $name, $default=null)
|
||||
{
|
||||
if (isset($this->values[$name])) {
|
||||
return $this->values[$name];
|
||||
@@ -318,30 +278,30 @@ class DatabaseConfig extends BaseConfig
|
||||
$this->table_name = $table_name;
|
||||
$this->sub_value = $sub_value;
|
||||
$this->sub_column = $sub_column;
|
||||
$this->cache_name = empty($sub_value) ? "config" : "config_{$sub_column}_{$sub_value}";
|
||||
$this->values = cache_get_or_set($this->cache_name, fn () => $this->get_values());
|
||||
$this->cache_name = empty($sub_value) ? "config" : "config_{$sub_value}";
|
||||
|
||||
$cached = $cache->get($this->cache_name);
|
||||
if (!is_null($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($this->cache_name, $this->values);
|
||||
}
|
||||
}
|
||||
|
||||
private function get_values(): mixed
|
||||
{
|
||||
$values = [];
|
||||
|
||||
$query = "SELECT name, value FROM {$this->table_name}";
|
||||
$args = [];
|
||||
|
||||
if (!empty($this->sub_column) && !empty($this->sub_value)) {
|
||||
$query .= " WHERE {$this->sub_column} = :sub_value";
|
||||
$args["sub_value"] = $this->sub_value;
|
||||
}
|
||||
|
||||
foreach ($this->database->get_all($query, $args) as $row) {
|
||||
$values[$row["name"]] = $row["value"];
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
public function save(string $name = null): void
|
||||
public function save(string $name=null): void
|
||||
{
|
||||
global $cache;
|
||||
|
||||
@@ -352,10 +312,10 @@ class DatabaseConfig extends BaseConfig
|
||||
}
|
||||
} else {
|
||||
$query = "DELETE FROM {$this->table_name} WHERE name = :name";
|
||||
$args = ["name" => $name];
|
||||
$args = ["name"=>$name];
|
||||
$cols = ["name","value"];
|
||||
$params = [":name",":value"];
|
||||
if (!empty($this->sub_column) && !empty($this->sub_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;
|
||||
@@ -364,7 +324,7 @@ class DatabaseConfig extends BaseConfig
|
||||
|
||||
$this->database->execute($query, $args);
|
||||
|
||||
$args["value"] = $this->values[$name];
|
||||
$args["value"] =$this->values[$name];
|
||||
$this->database->execute(
|
||||
"INSERT INTO {$this->table_name} (".join(",", $cols).") VALUES (".join(",", $params).")",
|
||||
$args
|
||||
|
||||
@@ -7,8 +7,6 @@ namespace Shimmie2;
|
||||
use FFSPHP\PDO;
|
||||
use FFSPHP\PDOStatement;
|
||||
|
||||
require_once __DIR__ . '/exceptions.php';
|
||||
|
||||
enum DatabaseDriverID: string
|
||||
{
|
||||
case MYSQL = "mysql";
|
||||
@@ -16,28 +14,8 @@ enum DatabaseDriverID: string
|
||||
case SQLITE = "sqlite";
|
||||
}
|
||||
|
||||
class DatabaseException extends SCoreException
|
||||
{
|
||||
public string $query;
|
||||
/** @var array<string, mixed> */
|
||||
public array $args;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $args
|
||||
*/
|
||||
public function __construct(string $msg, string $query, array $args)
|
||||
{
|
||||
parent::__construct($msg);
|
||||
$this->error = $msg;
|
||||
$this->query = $query;
|
||||
$this->args = $args;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class for controlled database access
|
||||
*
|
||||
* @phpstan-type QueryArgs array<string, string|int|bool|null>
|
||||
*/
|
||||
class Database
|
||||
{
|
||||
@@ -58,7 +36,6 @@ class Database
|
||||
* How many queries this DB object has run
|
||||
*/
|
||||
public int $query_count = 0;
|
||||
/** @var string[] */
|
||||
public array $queries = [];
|
||||
|
||||
public function __construct(string $dsn)
|
||||
@@ -80,7 +57,7 @@ class Database
|
||||
private function connect_engine(): void
|
||||
{
|
||||
if (preg_match("/^([^:]*)/", $this->dsn, $matches)) {
|
||||
$db_proto = $matches[1];
|
||||
$db_proto=$matches[1];
|
||||
} else {
|
||||
throw new SCoreException("Can't figure out database engine");
|
||||
}
|
||||
@@ -129,28 +106,6 @@ class Database
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param callable():T $callback
|
||||
* @return T
|
||||
*/
|
||||
public function with_savepoint(callable $callback, string $name = "sp"): mixed
|
||||
{
|
||||
global $_tracer;
|
||||
try {
|
||||
$_tracer->begin("Savepoint $name");
|
||||
$this->execute("SAVEPOINT $name");
|
||||
$ret = $callback();
|
||||
$this->execute("RELEASE SAVEPOINT $name");
|
||||
$_tracer->end();
|
||||
return $ret;
|
||||
} catch (\Exception $e) {
|
||||
$this->execute("ROLLBACK TO SAVEPOINT $name");
|
||||
$_tracer->end();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function get_engine(): DBEngine
|
||||
{
|
||||
if (is_null($this->engine)) {
|
||||
@@ -174,18 +129,16 @@ class Database
|
||||
return $this->get_engine()->get_version($this->get_db());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryArgs $args
|
||||
*/
|
||||
private function count_time(string $method, float $start, string $query, ?array $args): void
|
||||
{
|
||||
global $_tracer, $tracer_enabled;
|
||||
$dur = ftime() - $start;
|
||||
// trim whitespace
|
||||
$query = preg_replace('/[\n\t ]+/m', ' ', $query);
|
||||
$query = preg_replace('/[\n\t ]/m', ' ', $query);
|
||||
$query = preg_replace('/ +/m', ' ', $query);
|
||||
$query = trim($query);
|
||||
if ($tracer_enabled) {
|
||||
$_tracer->complete($start * 1000000, $dur * 1000000, "DB Query", ["query" => $query, "args" => $args, "method" => $method]);
|
||||
$_tracer->complete($start * 1000000, $dur * 1000000, "DB Query", ["query"=>$query, "args"=>$args, "method"=>$method]);
|
||||
}
|
||||
$this->queries[] = $query;
|
||||
$this->query_count++;
|
||||
@@ -197,32 +150,31 @@ class Database
|
||||
$this->get_engine()->set_timeout($this->get_db(), $time);
|
||||
}
|
||||
|
||||
public function notify(string $channel, ?string $data = null): void
|
||||
public function notify(string $channel, ?string $data=null): void
|
||||
{
|
||||
$this->get_engine()->notify($this->get_db(), $channel, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryArgs $args
|
||||
*/
|
||||
public function _execute(string $query, array $args = []): PDOStatement
|
||||
{
|
||||
try {
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? "unknown uri";
|
||||
return $this->get_db()->execute(
|
||||
"-- $uri\n" .
|
||||
$ret = $this->get_db()->execute(
|
||||
"-- " . str_replace("%2F", "/", urlencode($_GET['q'] ?? '')). "\n" .
|
||||
$query,
|
||||
$args
|
||||
);
|
||||
if ($ret === false) {
|
||||
throw new SCoreException("Query failed", $query);
|
||||
}
|
||||
/** @noinspection PhpIncompatibleReturnTypeInspection */
|
||||
return $ret;
|
||||
} catch (\PDOException $pdoe) {
|
||||
throw new DatabaseException($pdoe->getMessage(), $query, $args);
|
||||
throw new SCoreException($pdoe->getMessage(), $query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query with no return
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
*/
|
||||
public function execute(string $query, array $args = []): PDOStatement
|
||||
{
|
||||
@@ -234,9 +186,6 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a 2D array.
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
* @return array<array<string, mixed>>
|
||||
*/
|
||||
public function get_all(string $query, array $args = []): array
|
||||
{
|
||||
@@ -248,8 +197,6 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a iterable object for use with generators.
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
*/
|
||||
public function get_all_iterable(string $query, array $args = []): PDOStatement
|
||||
{
|
||||
@@ -261,9 +208,6 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a single row.
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function get_row(string $query, array $args = []): ?array
|
||||
{
|
||||
@@ -275,9 +219,6 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the first column of each row.
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function get_col(string $query, array $args = []): array
|
||||
{
|
||||
@@ -289,8 +230,6 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the first column of each row as a single iterable object.
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
*/
|
||||
public function get_col_iterable(string $query, array $args = []): \Generator
|
||||
{
|
||||
@@ -304,9 +243,6 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the the first column => the second column.
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function get_pairs(string $query, array $args = []): array
|
||||
{
|
||||
@@ -319,8 +255,6 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the the first column => the second column as an iterable object.
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
*/
|
||||
public function get_pairs_iterable(string $query, array $args = []): \Generator
|
||||
{
|
||||
@@ -334,10 +268,8 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a single value, or null.
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
*/
|
||||
public function get_one(string $query, array $args = []): mixed
|
||||
public function get_one(string $query, array $args = [])
|
||||
{
|
||||
$_start = ftime();
|
||||
$row = $this->_execute($query, $args)->fetch();
|
||||
@@ -347,15 +279,13 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and returns a bool indicating if any data was returned
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
*/
|
||||
public function exists(string $query, array $args = []): bool
|
||||
{
|
||||
$_start = ftime();
|
||||
$row = $this->_execute($query, $args)->fetch();
|
||||
$this->count_time("exists", $_start, $query, $args);
|
||||
if ($row == null) {
|
||||
if ($row==null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -407,8 +337,7 @@ class Database
|
||||
$this->get_all("SELECT name FROM sqlite_master WHERE type = 'table'")
|
||||
);
|
||||
} else {
|
||||
$did = (string)$this->get_engine()->id;
|
||||
throw new SCoreException("Can't count tables for database type {$did}");
|
||||
throw new SCoreException("Can't count tables for database type {$this->get_engine()->id}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,7 +346,7 @@ class Database
|
||||
return $this->get_db();
|
||||
}
|
||||
|
||||
public function standardise_boolean(string $table, string $column, bool $include_postgres = false): void
|
||||
public function standardise_boolean(string $table, string $column, bool $include_postgres=false): void
|
||||
{
|
||||
$d = $this->get_driver_id();
|
||||
if ($d == DatabaseDriverID::MYSQL) {
|
||||
|
||||
@@ -16,7 +16,7 @@ abstract class DBEngine
|
||||
{
|
||||
public DatabaseDriverID $id;
|
||||
|
||||
public function init(PDO $db): void
|
||||
public function init(PDO $db)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -30,18 +30,18 @@ abstract class DBEngine
|
||||
return 'CREATE TABLE '.$name.' ('.$data.')';
|
||||
}
|
||||
|
||||
abstract public function set_timeout(PDO $db, ?int $time): void;
|
||||
abstract public function set_timeout(PDO $db, ?int $time);
|
||||
|
||||
abstract public function get_version(PDO $db): string;
|
||||
|
||||
abstract public function notify(PDO $db, string $channel, ?string $data = null): void;
|
||||
abstract public function notify(PDO $db, string $channel, ?string $data=null): void;
|
||||
}
|
||||
|
||||
class MySQL extends DBEngine
|
||||
{
|
||||
public DatabaseDriverID $id = DatabaseDriverID::MYSQL;
|
||||
|
||||
public function init(PDO $db): void
|
||||
public function init(PDO $db)
|
||||
{
|
||||
$db->exec("SET NAMES utf8;");
|
||||
}
|
||||
@@ -66,13 +66,13 @@ class MySQL extends DBEngine
|
||||
// $db->exec("SET SESSION MAX_EXECUTION_TIME=".$time.";");
|
||||
}
|
||||
|
||||
public function notify(PDO $db, string $channel, ?string $data = null): void
|
||||
public function notify(PDO $db, string $channel, ?string $data=null): void
|
||||
{
|
||||
}
|
||||
|
||||
public function get_version(PDO $db): string
|
||||
{
|
||||
return false_throws($db->query('select version()'))->fetch()[0];
|
||||
return $db->query('select version()')->fetch()[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ class PostgreSQL extends DBEngine
|
||||
{
|
||||
public DatabaseDriverID $id = DatabaseDriverID::PGSQL;
|
||||
|
||||
public function init(PDO $db): void
|
||||
public function init(PDO $db)
|
||||
{
|
||||
if (array_key_exists('REMOTE_ADDR', $_SERVER)) {
|
||||
$db->exec("SET application_name TO 'shimmie [{$_SERVER['REMOTE_ADDR']}]';");
|
||||
@@ -113,7 +113,7 @@ class PostgreSQL extends DBEngine
|
||||
$db->exec("SET statement_timeout TO ".$time.";");
|
||||
}
|
||||
|
||||
public function notify(PDO $db, string $channel, ?string $data = null): void
|
||||
public function notify(PDO $db, string $channel, ?string $data=null): void
|
||||
{
|
||||
if ($data) {
|
||||
$db->exec("NOTIFY $channel, '$data';");
|
||||
@@ -124,44 +124,44 @@ class PostgreSQL extends DBEngine
|
||||
|
||||
public function get_version(PDO $db): string
|
||||
{
|
||||
return false_throws($db->query('select version()'))->fetch()[0];
|
||||
return $db->query('select version()')->fetch()[0];
|
||||
}
|
||||
}
|
||||
|
||||
// shimmie functions for export to sqlite
|
||||
function _unix_timestamp(string $date): int
|
||||
function _unix_timestamp($date): int
|
||||
{
|
||||
return strtotime_ex($date);
|
||||
return strtotime($date);
|
||||
}
|
||||
function _now(): string
|
||||
{
|
||||
return date("Y-m-d H:i:s");
|
||||
}
|
||||
function _floor(float|int $a): float
|
||||
function _floor($a): float
|
||||
{
|
||||
return floor($a);
|
||||
}
|
||||
function _log(float $a, ?float $b = null): float
|
||||
function _log($a, $b=null): float
|
||||
{
|
||||
if (is_null($b)) {
|
||||
return log($a);
|
||||
} else {
|
||||
return log($b, $a);
|
||||
return log($a, $b);
|
||||
}
|
||||
}
|
||||
function _isnull(mixed $a): bool
|
||||
function _isnull($a): bool
|
||||
{
|
||||
return is_null($a);
|
||||
}
|
||||
function _md5(string $a): string
|
||||
function _md5($a): string
|
||||
{
|
||||
return md5($a);
|
||||
}
|
||||
function _concat(string $a, string $b): string
|
||||
function _concat($a, $b): string
|
||||
{
|
||||
return $a . $b;
|
||||
}
|
||||
function _lower(string $a): string
|
||||
function _lower($a): string
|
||||
{
|
||||
return strtolower($a);
|
||||
}
|
||||
@@ -169,7 +169,7 @@ function _rand(): int
|
||||
{
|
||||
return rand();
|
||||
}
|
||||
function _ln(float $n): float
|
||||
function _ln($n): float
|
||||
{
|
||||
return log($n);
|
||||
}
|
||||
@@ -178,7 +178,7 @@ class SQLite extends DBEngine
|
||||
{
|
||||
public DatabaseDriverID $id = DatabaseDriverID::SQLITE;
|
||||
|
||||
public function init(PDO $db): void
|
||||
public function init(PDO $db)
|
||||
{
|
||||
ini_set('sqlite.assoc_case', '0');
|
||||
$db->exec("PRAGMA foreign_keys = ON;");
|
||||
@@ -225,12 +225,12 @@ class SQLite extends DBEngine
|
||||
// There doesn't seem to be such a thing for SQLite, so it does nothing
|
||||
}
|
||||
|
||||
public function notify(PDO $db, string $channel, ?string $data = null): void
|
||||
public function notify(PDO $db, string $channel, ?string $data=null): void
|
||||
{
|
||||
}
|
||||
|
||||
public function get_version(PDO $db): string
|
||||
{
|
||||
return false_throws($db->query('select sqlite_version()'))->fetch()[0];
|
||||
return $db->query('select sqlite_version()')->fetch()[0];
|
||||
}
|
||||
}
|
||||
|
||||
122
core/event.php
122
core/event.php
@@ -46,28 +46,25 @@ class InitExtEvent extends Event
|
||||
*/
|
||||
class PageRequestEvent extends Event
|
||||
{
|
||||
public string $method;
|
||||
public string $path;
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
public array $args;
|
||||
public $args;
|
||||
public int $arg_count;
|
||||
public int $part_count;
|
||||
|
||||
public function __construct(string $method, string $path)
|
||||
public function __construct(string $path)
|
||||
{
|
||||
parent::__construct();
|
||||
global $config;
|
||||
|
||||
$this->method = $method;
|
||||
// trim starting slashes
|
||||
$path = ltrim($path, "/");
|
||||
|
||||
// if we're looking at the root of the install,
|
||||
// use the default front page
|
||||
if ($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);
|
||||
}
|
||||
$this->path = $path;
|
||||
|
||||
// break the path into parts
|
||||
$args = explode('/', $path);
|
||||
@@ -90,7 +87,7 @@ class PageRequestEvent extends Event
|
||||
return false;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $this->part_count; $i++) {
|
||||
for ($i=0; $i<$this->part_count; $i++) {
|
||||
if ($parts[$i] != $this->args[$i]) {
|
||||
return false;
|
||||
}
|
||||
@@ -106,7 +103,7 @@ class PageRequestEvent extends Event
|
||||
{
|
||||
$offset = $this->part_count + $n;
|
||||
if ($offset >= 0 && $offset < $this->arg_count) {
|
||||
return rawurldecode($this->args[$offset]);
|
||||
return $this->args[$offset];
|
||||
} else {
|
||||
$nm1 = $this->arg_count - 1;
|
||||
throw new UserErrorException("Requested an invalid page argument {$offset} / {$nm1}");
|
||||
@@ -117,11 +114,11 @@ class PageRequestEvent extends Event
|
||||
* 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
|
||||
public function try_page_num(int $n, ?int $max=null): int
|
||||
{
|
||||
if ($this->count_args() > $n) {
|
||||
$i = $this->get_arg($n);
|
||||
if (is_numberish($i) && int_escape($i) > 0) {
|
||||
if (is_numeric($i) && int_escape($i) > 0) {
|
||||
return page_number($i, $max);
|
||||
} else {
|
||||
return 0;
|
||||
@@ -143,39 +140,11 @@ class PageRequestEvent extends Event
|
||||
* Many things use these functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function get_search_terms(): array
|
||||
{
|
||||
$search_terms = [];
|
||||
if ($this->count_args() === 2) {
|
||||
$str = $this->get_arg(0);
|
||||
|
||||
// decode legacy caret-encoding just in case
|
||||
// somebody has bookmarked such a URL
|
||||
$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];
|
||||
}
|
||||
}
|
||||
$str = $out;
|
||||
// end legacy
|
||||
|
||||
$search_terms = Tag::explode($str);
|
||||
$search_terms = Tag::explode(Tag::decaret($this->get_arg(0)));
|
||||
}
|
||||
return $search_terms;
|
||||
}
|
||||
@@ -202,12 +171,73 @@ class PageRequestEvent extends Event
|
||||
}
|
||||
|
||||
|
||||
class CliGenEvent extends Event
|
||||
/**
|
||||
* Sent when index.php is called from the command line
|
||||
*/
|
||||
class CommandEvent extends Event
|
||||
{
|
||||
public function __construct(
|
||||
public \Symfony\Component\Console\Application $app
|
||||
) {
|
||||
public string $cmd = "help";
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
public array $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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,15 @@ namespace Shimmie2;
|
||||
*/
|
||||
class SCoreException extends \RuntimeException
|
||||
{
|
||||
public ?string $query;
|
||||
public string $error;
|
||||
public int $http_code = 500;
|
||||
|
||||
public function __construct(string $msg)
|
||||
public function __construct(string $msg, ?string $query=null)
|
||||
{
|
||||
parent::__construct($msg);
|
||||
$this->error = $msg;
|
||||
$this->query = $query;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,9 @@ abstract class Extension
|
||||
protected Themelet $theme;
|
||||
public ExtensionInfo $info;
|
||||
|
||||
/** @var string[] */
|
||||
private static array $enabled_extensions = [];
|
||||
|
||||
public function __construct(?string $class = null)
|
||||
public function __construct($class = null)
|
||||
{
|
||||
$class = $class ?? get_called_class();
|
||||
$this->theme = $this->get_theme_object($class);
|
||||
@@ -43,13 +42,9 @@ abstract class Extension
|
||||
$normal = "Shimmie2\\{$base}Theme";
|
||||
|
||||
if (class_exists($custom)) {
|
||||
$c = new $custom();
|
||||
assert(is_a($c, Themelet::class));
|
||||
return $c;
|
||||
return new $custom();
|
||||
} elseif (class_exists($normal)) {
|
||||
$n = new $normal();
|
||||
assert(is_a($n, Themelet::class));
|
||||
return $n;
|
||||
return new $normal();
|
||||
} else {
|
||||
return new Themelet();
|
||||
}
|
||||
@@ -74,7 +69,7 @@ abstract class Extension
|
||||
$extras
|
||||
) as $key) {
|
||||
$ext = ExtensionInfo::get_by_key($key);
|
||||
if ($ext === null || !$ext->is_supported()) {
|
||||
if ($ext===null || !$ext->is_supported()) {
|
||||
continue;
|
||||
}
|
||||
// FIXME: error if one of our dependencies isn't supported
|
||||
@@ -87,14 +82,11 @@ abstract class Extension
|
||||
}
|
||||
}
|
||||
|
||||
public static function is_enabled(string $key): bool
|
||||
public static function is_enabled(string $key): ?bool
|
||||
{
|
||||
return in_array($key, self::$enabled_extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function get_enabled_extensions(): array
|
||||
{
|
||||
return self::$enabled_extensions;
|
||||
@@ -110,7 +102,7 @@ abstract class Extension
|
||||
return $config->get_int($name, 0);
|
||||
}
|
||||
|
||||
protected function set_version(string $name, int $ver): void
|
||||
protected function set_version(string $name, int $ver)
|
||||
{
|
||||
global $config;
|
||||
$config->set_int($name, $ver);
|
||||
@@ -118,10 +110,6 @@ abstract class Extension
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionNotFound extends SCoreException
|
||||
{
|
||||
}
|
||||
|
||||
enum ExtensionVisibility
|
||||
{
|
||||
case DEFAULT;
|
||||
@@ -135,7 +123,7 @@ abstract class ExtensionInfo
|
||||
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 SHISH_AUTHOR = [self::SHISH_NAME=>self::SHISH_EMAIL];
|
||||
|
||||
public const LICENSE_GPLV2 = "GPLv2";
|
||||
public const LICENSE_MIT = "MIT";
|
||||
@@ -149,11 +137,8 @@ abstract class ExtensionInfo
|
||||
public string $name;
|
||||
public string $license;
|
||||
public string $description;
|
||||
/** @var array<string, string|null> */
|
||||
public array $authors = [];
|
||||
/** @var string[] */
|
||||
public array $dependencies = [];
|
||||
/** @var string[] */
|
||||
public array $conflicts = [];
|
||||
public ExtensionVisibility $visibility = ExtensionVisibility::DEFAULT;
|
||||
public ?string $link = null;
|
||||
@@ -167,7 +152,7 @@ abstract class ExtensionInfo
|
||||
|
||||
public function is_supported(): bool
|
||||
{
|
||||
if ($this->supported === null) {
|
||||
if ($this->supported===null) {
|
||||
$this->check_support();
|
||||
}
|
||||
return $this->supported;
|
||||
@@ -175,17 +160,14 @@ abstract class ExtensionInfo
|
||||
|
||||
public function get_support_info(): string
|
||||
{
|
||||
if ($this->supported === null) {
|
||||
if ($this->supported===null) {
|
||||
$this->check_support();
|
||||
}
|
||||
return $this->support_info;
|
||||
}
|
||||
|
||||
/** @var array<string, ExtensionInfo> */
|
||||
private static array $all_info_by_key = [];
|
||||
/** @var array<string, ExtensionInfo> */
|
||||
private static array $all_info_by_class = [];
|
||||
/** @var string[] */
|
||||
private static array $core_extensions = [];
|
||||
|
||||
protected function __construct()
|
||||
@@ -202,7 +184,7 @@ abstract class ExtensionInfo
|
||||
return Extension::is_enabled($this->key);
|
||||
}
|
||||
|
||||
private function check_support(): void
|
||||
private function check_support()
|
||||
{
|
||||
global $database;
|
||||
$this->support_info = "";
|
||||
@@ -221,25 +203,16 @@ abstract class ExtensionInfo
|
||||
$this->supported = empty($this->support_info);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ExtensionInfo[]
|
||||
*/
|
||||
public static function get_all(): array
|
||||
{
|
||||
return array_values(self::$all_info_by_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function get_all_keys(): array
|
||||
{
|
||||
return array_keys(self::$all_info_by_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function get_core_extensions(): array
|
||||
{
|
||||
return self::$core_extensions;
|
||||
@@ -262,22 +235,21 @@ abstract class ExtensionInfo
|
||||
return self::$all_info_by_class[$normal];
|
||||
} else {
|
||||
$infos = print_r(array_keys(self::$all_info_by_class), true);
|
||||
throw new ExtensionNotFound("$normal not found in {$infos}");
|
||||
throw new SCoreException("$normal not found in {$infos}");
|
||||
}
|
||||
}
|
||||
|
||||
public static function load_all_extension_info(): void
|
||||
public static function load_all_extension_info()
|
||||
{
|
||||
foreach (get_subclasses_of(ExtensionInfo::class) as $class) {
|
||||
foreach (get_subclasses_of("Shimmie2\ExtensionInfo") as $class) {
|
||||
$extension_info = new $class();
|
||||
assert(is_a($extension_info, ExtensionInfo::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) {
|
||||
if ($extension_info->core===true) {
|
||||
self::$core_extensions[] = $extension_info->key;
|
||||
}
|
||||
}
|
||||
@@ -291,7 +263,7 @@ abstract class ExtensionInfo
|
||||
*/
|
||||
abstract class FormatterExtension extends Extension
|
||||
{
|
||||
public function onTextFormatting(TextFormattingEvent $event): void
|
||||
public function onTextFormatting(TextFormattingEvent $event)
|
||||
{
|
||||
$event->formatted = $this->format($event->formatted);
|
||||
$event->stripped = $this->strip($event->stripped);
|
||||
@@ -309,83 +281,86 @@ abstract class FormatterExtension extends Extension
|
||||
*/
|
||||
abstract class DataHandlerExtension extends Extension
|
||||
{
|
||||
/** @var string[] */
|
||||
protected array $SUPPORTED_MIME = [];
|
||||
|
||||
public function onDataUpload(DataUploadEvent $event): void
|
||||
protected function move_upload_to_archive(DataUploadEvent $event)
|
||||
{
|
||||
global $config;
|
||||
|
||||
if ($this->supported_mime($event->mime)) {
|
||||
if (!$this->check_contents($event->tmpname)) {
|
||||
// We DO support this extension - but the file looks corrupt
|
||||
throw new UploadException("Invalid or corrupted file");
|
||||
}
|
||||
|
||||
$existing = Image::by_hash(md5_file_ex($event->tmpname));
|
||||
if (!is_null($existing)) {
|
||||
if ($config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER) == ImageConfig::COLLISION_MERGE) {
|
||||
// Right now tags are the only thing that get merged, so
|
||||
// we can just send a TagSetEvent - in the future we might
|
||||
// want a dedicated MergeEvent?
|
||||
if(!empty($event->metadata['tags'])) {
|
||||
send_event(new TagSetEvent($existing, array_merge($existing->get_tag_array(), $event->metadata['tags'])));
|
||||
}
|
||||
$event->images[] = $existing;
|
||||
return;
|
||||
} else {
|
||||
throw new UploadException(">>{$existing->id} already has hash {$existing->hash}");
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new Image object
|
||||
$filename = $event->tmpname;
|
||||
assert(is_readable($filename));
|
||||
$image = new Image();
|
||||
$image->tmp_file = $filename;
|
||||
$image->filesize = filesize_ex($filename);
|
||||
$image->hash = md5_file_ex($filename);
|
||||
$image->filename = (($pos = strpos($event->metadata['filename'], '?')) !== false) ? substr($event->metadata['filename'], 0, $pos) : $event->metadata['filename'];
|
||||
$image->set_mime(MimeType::get_for_file($filename, get_file_ext($event->metadata["filename"]) ?? null));
|
||||
if (empty($image->get_mime())) {
|
||||
throw new UploadException("Unable to determine MIME for $filename");
|
||||
}
|
||||
try {
|
||||
send_event(new MediaCheckPropertiesEvent($image));
|
||||
} catch (MediaException $e) {
|
||||
throw new UploadException("Unable to scan media properties $filename / $image->filename / $image->hash: ".$e->getMessage());
|
||||
}
|
||||
$image->save_to_db(); // Ensure the image has a DB-assigned ID
|
||||
|
||||
// Let everybody else know, so that TagEdit can set tags, Ratings can set ratings, etc
|
||||
$iae = send_event(new ImageAdditionEvent($image, $event->metadata));
|
||||
|
||||
// If everything is OK, then move the file to the archive
|
||||
$filename = warehouse_path(Image::IMAGE_DIR, $event->hash);
|
||||
if (!@copy($event->tmpname, $filename)) {
|
||||
$errors = error_get_last();
|
||||
throw new UploadException(
|
||||
"Failed to copy file from uploads ({$event->tmpname}) to archive ($filename): ".
|
||||
"{$errors['type']} / {$errors['message']}"
|
||||
);
|
||||
}
|
||||
|
||||
$event->images[] = $iae->image;
|
||||
$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 onThumbnailGeneration(ThumbnailGenerationEvent $event): void
|
||||
public function onDataUpload(DataUploadEvent $event)
|
||||
{
|
||||
$supported_mime = $this->supported_mime($event->mime);
|
||||
$check_contents = $this->check_contents($event->tmpname);
|
||||
if ($supported_mime && $check_contents) {
|
||||
$this->move_upload_to_archive($event);
|
||||
send_event(new ThumbnailGenerationEvent($event->hash, $event->mime));
|
||||
|
||||
/* 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("Post to replace does not exist!");
|
||||
}
|
||||
if ($existing->hash === $event->hash) {
|
||||
throw new UploadException("The uploaded post 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->hash), $event->metadata);
|
||||
send_event(new ImageReplaceEvent($event->replace_id, $image));
|
||||
$_id = $event->replace_id;
|
||||
assert(!is_null($_id));
|
||||
$event->image_id = $_id;
|
||||
} else {
|
||||
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata);
|
||||
$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, $locked));
|
||||
}
|
||||
}
|
||||
} elseif ($supported_mime && !$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_mime($event->image->get_mime())) {
|
||||
if ($this->supported_mime($event->mime)) {
|
||||
if ($event->force) {
|
||||
$result = $this->create_thumb($event->image);
|
||||
$result = $this->create_thumb($event->hash, $event->mime);
|
||||
} else {
|
||||
$outname = $event->image->get_thumb_filename();
|
||||
$outname = warehouse_path(Image::THUMBNAIL_DIR, $event->hash);
|
||||
if (file_exists($outname)) {
|
||||
return;
|
||||
}
|
||||
$result = $this->create_thumb($event->image);
|
||||
$result = $this->create_thumb($event->hash, $event->mime);
|
||||
}
|
||||
}
|
||||
if ($result) {
|
||||
@@ -393,48 +368,65 @@ abstract class DataHandlerExtension extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onDisplayingImage(DisplayingImageEvent $event): void
|
||||
public function onDisplayingImage(DisplayingImageEvent $event)
|
||||
{
|
||||
global $config, $page;
|
||||
global $page;
|
||||
if ($this->supported_mime($event->image->get_mime())) {
|
||||
// @phpstan-ignore-next-line
|
||||
$this->theme->display_image($event->image);
|
||||
if ($config->get_bool(ImageConfig::SHOW_META) && method_exists($this->theme, "display_metadata")) {
|
||||
$this->theme->display_metadata($event->image);
|
||||
}
|
||||
$this->theme->display_image($page, $event->image);
|
||||
}
|
||||
}
|
||||
|
||||
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event): void
|
||||
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
|
||||
{
|
||||
if ($this->supported_mime($event->image->get_mime())) {
|
||||
$this->media_check_properties($event);
|
||||
}
|
||||
}
|
||||
|
||||
protected function create_image_from_data(string $filename, array $metadata): Image
|
||||
{
|
||||
$image = new Image();
|
||||
|
||||
assert(is_readable($filename));
|
||||
$image->filesize = filesize($filename);
|
||||
$image->hash = md5_file($filename);
|
||||
$image->filename = (($pos = strpos($metadata['filename'], '?')) !== false) ? substr($metadata['filename'], 0, $pos) : $metadata['filename'];
|
||||
$image->set_mime(MimeType::get_for_file($filename, get_file_ext($metadata["filename"]) ?? null));
|
||||
$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
|
||||
$image->source = $metadata['source'];
|
||||
|
||||
if (empty($image->get_mime())) {
|
||||
throw new UploadException("Unable to determine MIME for $filename");
|
||||
}
|
||||
try {
|
||||
send_event(new MediaCheckPropertiesEvent($image));
|
||||
} catch (MediaException $e) {
|
||||
throw new UploadException("Unable to scan media properties $filename / $image->filename / $image->hash: ".$e->getMessage());
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
abstract protected function media_check_properties(MediaCheckPropertiesEvent $event): void;
|
||||
abstract protected function check_contents(string $tmpname): bool;
|
||||
abstract protected function create_thumb(Image $image): bool;
|
||||
abstract protected function create_thumb(string $hash, string $mime): bool;
|
||||
|
||||
protected function supported_mime(string $mime): bool
|
||||
{
|
||||
return MimeType::matches_array($mime, $this->SUPPORTED_MIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function get_all_supported_mimes(): array
|
||||
{
|
||||
$arr = [];
|
||||
foreach (get_subclasses_of(DataHandlerExtension::class) as $handler) {
|
||||
foreach (get_subclasses_of("Shimmie2\DataHandlerExtension") as $handler) {
|
||||
$handler = (new $handler());
|
||||
assert(is_a($handler, DataHandlerExtension::class));
|
||||
$arr = array_merge($arr, $handler->SUPPORTED_MIME);
|
||||
}
|
||||
|
||||
// Not sure how to handle this otherwise, don't want to set up a whole other event for this one class
|
||||
if (Extension::is_enabled(TranscodeImageInfo::KEY)) {
|
||||
if (class_exists("Shimmie2\TranscodeImage")) {
|
||||
$arr = array_merge($arr, TranscodeImage::get_enabled_mimes());
|
||||
}
|
||||
|
||||
@@ -442,9 +434,6 @@ abstract class DataHandlerExtension extends Extension
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function get_all_supported_exts(): array
|
||||
{
|
||||
$arr = [];
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use MicroCRUD\TextColumn;
|
||||
|
||||
use function MicroHTML\INPUT;
|
||||
|
||||
class AutoCompleteColumn extends TextColumn
|
||||
{
|
||||
public function read_input(array $inputs): \MicroHTML\HTMLElement
|
||||
{
|
||||
return INPUT([
|
||||
"type" => "text",
|
||||
"name" => "r_{$this->name}",
|
||||
"class" => "autocomplete_tags",
|
||||
"placeholder" => $this->title,
|
||||
"value" => @$inputs["r_{$this->name}"]
|
||||
]);
|
||||
}
|
||||
|
||||
public function create_input(array $inputs): \MicroHTML\HTMLElement
|
||||
{
|
||||
return INPUT([
|
||||
"type" => "text",
|
||||
"name" => "c_{$this->name}",
|
||||
"class" => "autocomplete_tags",
|
||||
"placeholder" => $this->title,
|
||||
"value" => @$inputs["c_{$this->name}"]
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -9,20 +9,25 @@ namespace Shimmie2;
|
||||
*/
|
||||
class ImageAdditionEvent extends Event
|
||||
{
|
||||
public User $user;
|
||||
public bool $merged = false;
|
||||
|
||||
/**
|
||||
* Inserts a new image into the database with its associated
|
||||
* information.
|
||||
*
|
||||
* @param mixed[] $metadata
|
||||
* information. Also calls TagSetEvent to set the tags for
|
||||
* this new image.
|
||||
*/
|
||||
public function __construct(
|
||||
public Image $image,
|
||||
public array $metadata,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
|
||||
class ImageAdditionException extends SCoreException
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* An image is being deleted.
|
||||
*/
|
||||
@@ -47,25 +52,18 @@ class ImageDeletionEvent extends Event
|
||||
*/
|
||||
class ImageReplaceEvent extends Event
|
||||
{
|
||||
public string $old_hash;
|
||||
public string $new_hash;
|
||||
|
||||
/**
|
||||
* Replaces an image file.
|
||||
* 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(
|
||||
public Image $image,
|
||||
public string $tmp_filename,
|
||||
public int $id,
|
||||
public Image $image
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->old_hash = $image->hash;
|
||||
$hash = md5_file($tmp_filename);
|
||||
assert($hash !== false, "Failed to hash file $tmp_filename");
|
||||
$this->new_hash = $hash;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +82,9 @@ class ThumbnailGenerationEvent extends Event
|
||||
* Request a thumbnail be made for an image object
|
||||
*/
|
||||
public function __construct(
|
||||
public Image $image,
|
||||
public bool $force = false
|
||||
public string $hash,
|
||||
public string $mime,
|
||||
public bool $force=false
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->generated = false;
|
||||
|
||||
@@ -8,13 +8,6 @@ use GQLA\Type;
|
||||
use GQLA\Field;
|
||||
use GQLA\Query;
|
||||
|
||||
enum ImagePropType
|
||||
{
|
||||
case BOOL;
|
||||
case INT;
|
||||
case STRING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Image
|
||||
*
|
||||
@@ -23,18 +16,15 @@ enum ImagePropType
|
||||
* As of 2.2, this no longer necessarily represents an
|
||||
* image per se, but could be a video, sound file, or any
|
||||
* other supported upload type.
|
||||
*
|
||||
* @implements \ArrayAccess<string, mixed>
|
||||
*/
|
||||
#[\AllowDynamicProperties]
|
||||
#[Type(name: "Post")]
|
||||
class Image implements \ArrayAccess
|
||||
class Image
|
||||
{
|
||||
public const IMAGE_DIR = "images";
|
||||
public const THUMBNAIL_DIR = "thumbs";
|
||||
|
||||
private bool $in_db = false;
|
||||
|
||||
public int $id;
|
||||
public ?int $id = null;
|
||||
#[Field]
|
||||
public int $height = 0;
|
||||
#[Field]
|
||||
@@ -54,9 +44,9 @@ class Image implements \ArrayAccess
|
||||
public int $owner_id;
|
||||
public string $owner_ip;
|
||||
#[Field]
|
||||
public string $posted;
|
||||
public ?string $posted = null;
|
||||
#[Field]
|
||||
public ?string $source = null;
|
||||
public ?string $source;
|
||||
#[Field]
|
||||
public bool $locked = false;
|
||||
public ?bool $lossless = null;
|
||||
@@ -65,92 +55,39 @@ class Image implements \ArrayAccess
|
||||
public ?bool $image = null;
|
||||
public ?bool $audio = null;
|
||||
public ?int $length = null;
|
||||
public ?string $tmp_file = null;
|
||||
|
||||
/** @var array<string, ImagePropType> */
|
||||
public static array $prop_types = [];
|
||||
/** @var array<string, mixed> */
|
||||
private array $dynamic_props = [];
|
||||
public static array $bool_props = ["locked", "lossless", "video", "audio", "image"];
|
||||
public static array $int_props = ["id", "owner_id", "height", "width", "filesize", "length"];
|
||||
|
||||
/**
|
||||
* One will very rarely construct an image directly, more common
|
||||
* would be to use Image::by_id, Image::by_hash, etc.
|
||||
*
|
||||
* @param array<string|int, mixed>|null $row
|
||||
*/
|
||||
public function __construct(?array $row = null)
|
||||
public function __construct(?array $row=null)
|
||||
{
|
||||
if (!is_null($row)) {
|
||||
foreach ($row as $name => $value) {
|
||||
// some databases return both key=>value and numeric indices,
|
||||
// we only want the key=>value ones
|
||||
if (is_numeric($name)) {
|
||||
continue;
|
||||
} elseif(property_exists($this, $name)) {
|
||||
$t = (new \ReflectionProperty($this, $name))->getType();
|
||||
assert(!is_null($t));
|
||||
if(is_a($t, \ReflectionNamedType::class)) {
|
||||
if(is_null($value)) {
|
||||
$this->$name = null;
|
||||
} else {
|
||||
$this->$name = match($t->getName()) {
|
||||
"int" => int_escape((string)$value),
|
||||
"bool" => bool_escape((string)$value),
|
||||
"string" => (string)$value,
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} elseif(array_key_exists($name, static::$prop_types)) {
|
||||
if (is_null($value)) {
|
||||
$value = null;
|
||||
} else {
|
||||
$value = match(static::$prop_types[$name]) {
|
||||
ImagePropType::BOOL => bool_escape((string)$value),
|
||||
ImagePropType::INT => int_escape((string)$value),
|
||||
ImagePropType::STRING => (string)$value,
|
||||
};
|
||||
}
|
||||
$this->dynamic_props[$name] = $value;
|
||||
// some databases use table.name rather than name
|
||||
$name = str_replace("images.", "", $name);
|
||||
|
||||
// hax, this is likely the cause of much scrutinizer-ci complaints.
|
||||
if (is_null($value)) {
|
||||
$this->$name = null;
|
||||
} elseif (in_array($name, self::$bool_props)) {
|
||||
$this->$name = bool_escape((string)$value);
|
||||
} elseif (in_array($name, self::$int_props)) {
|
||||
$this->$name = int_escape((string)$value);
|
||||
} else {
|
||||
// Database table has a column we don't know about,
|
||||
// it isn't static and it isn't a known prop_type -
|
||||
// maybe from an old extension that has since been
|
||||
// disabled? Just ignore it.
|
||||
if(defined('UNITTEST')) {
|
||||
throw new \Exception("Unknown column $name in images table");
|
||||
}
|
||||
$this->$name = $value;
|
||||
}
|
||||
}
|
||||
$this->in_db = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
assert(is_string($offset));
|
||||
return array_key_exists($offset, static::$prop_types);
|
||||
}
|
||||
public function offsetGet(mixed $offset): mixed
|
||||
{
|
||||
assert(is_string($offset));
|
||||
if(!$this->offsetExists($offset)) {
|
||||
throw new \OutOfBoundsException("Undefined dynamic property: $offset");
|
||||
}
|
||||
return $this->dynamic_props[$offset] ?? null;
|
||||
}
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
assert(is_string($offset));
|
||||
$this->dynamic_props[$offset] = $value;
|
||||
}
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
assert(is_string($offset));
|
||||
unset($this->dynamic_props[$offset]);
|
||||
}
|
||||
|
||||
#[Field(name: "post_id")]
|
||||
public function graphql_oid(): int
|
||||
{
|
||||
@@ -166,11 +103,11 @@ class Image implements \ArrayAccess
|
||||
public static function by_id(int $post_id): ?Image
|
||||
{
|
||||
global $database;
|
||||
if ($post_id > 2 ** 32) {
|
||||
if ($post_id > 2**32) {
|
||||
// for some reason bots query huge numbers and pollute the DB error logs...
|
||||
return null;
|
||||
}
|
||||
$row = $database->get_row("SELECT * FROM images WHERE images.id=:id", ["id" => $post_id]);
|
||||
$row = $database->get_row("SELECT * FROM images WHERE images.id=:id", ["id"=>$post_id]);
|
||||
return ($row ? new Image($row) : null);
|
||||
}
|
||||
|
||||
@@ -178,29 +115,26 @@ class Image implements \ArrayAccess
|
||||
{
|
||||
global $database;
|
||||
$hash = strtolower($hash);
|
||||
$row = $database->get_row("SELECT images.* FROM images WHERE hash=:hash", ["hash" => $hash]);
|
||||
$row = $database->get_row("SELECT images.* FROM images WHERE hash=:hash", ["hash"=>$hash]);
|
||||
return ($row ? new Image($row) : null);
|
||||
}
|
||||
|
||||
public static function by_id_or_hash(string $id): ?Image
|
||||
{
|
||||
return (is_numberish($id) && strlen($id) != 32) ? Image::by_id((int)$id) : Image::by_hash($id);
|
||||
return (is_numeric($id) && strlen($id) != 32) ? Image::by_id((int)$id) : Image::by_hash($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $tags
|
||||
*/
|
||||
public static function by_random(array $tags = [], int $limit_range = 0): ?Image
|
||||
public static function by_random(array $tags=[], int $limit_range=0): ?Image
|
||||
{
|
||||
$max = Search::count_images($tags);
|
||||
$max = Image::count_images($tags);
|
||||
if ($max < 1) {
|
||||
return null;
|
||||
} // From Issue #22 - opened by HungryFeline on May 30, 2011.
|
||||
} // From Issue #22 - opened by HungryFeline on May 30, 2011.
|
||||
if ($limit_range > 0 && $max > $limit_range) {
|
||||
$max = $limit_range;
|
||||
}
|
||||
$rand = mt_rand(0, $max - 1);
|
||||
$set = Search::find_images($rand, 1, $tags);
|
||||
$rand = mt_rand(0, $max-1);
|
||||
$set = Image::find_images($rand, 1, $tags);
|
||||
if (count($set) > 0) {
|
||||
return $set[0];
|
||||
} else {
|
||||
@@ -208,6 +142,136 @@ class Image implements \ArrayAccess
|
||||
}
|
||||
}
|
||||
|
||||
private static function find_images_internal(int $start = 0, ?int $limit = null, array $tags=[]): iterable
|
||||
{
|
||||
global $database, $user;
|
||||
|
||||
if ($start < 0) {
|
||||
$start = 0;
|
||||
}
|
||||
if ($limit !== null && $limit < 1) {
|
||||
$limit = 1;
|
||||
}
|
||||
|
||||
if (SPEED_HAX) {
|
||||
if (!$user->can(Permissions::BIG_SEARCH) and count($tags) > 3) {
|
||||
throw new PermissionDeniedException("Anonymous users may only search for up to 3 tags at a time");
|
||||
}
|
||||
}
|
||||
|
||||
$querylet = Image::build_search_querylet($tags, $limit, $start);
|
||||
return $database->get_all_iterable($querylet->sql, $querylet->variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for an array of images
|
||||
*
|
||||
* @param string[] $tags
|
||||
* @return Image[]
|
||||
*/
|
||||
#[Query(name: "posts", type: "[Post!]!", args: ["tags" => "[string!]"])]
|
||||
public static function find_images(int $offset = 0, ?int $limit = null, array $tags=[]): array
|
||||
{
|
||||
$result = self::find_images_internal($offset, $limit, $tags);
|
||||
|
||||
$images = [];
|
||||
foreach ($result as $row) {
|
||||
$images[] = new Image($row);
|
||||
}
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for an array of images, returning a iterable object of Image
|
||||
*/
|
||||
public static function find_images_iterable(int $start = 0, ?int $limit = null, array $tags=[]): \Generator
|
||||
{
|
||||
$result = self::find_images_internal($start, $limit, $tags);
|
||||
foreach ($result as $row) {
|
||||
yield new Image($row);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Image-related utility functions
|
||||
*/
|
||||
|
||||
public static function count_total_images(): int
|
||||
{
|
||||
global $cache, $database;
|
||||
$total = $cache->get("image-count");
|
||||
if (is_null($total)) {
|
||||
$total = (int)$database->get_one("SELECT COUNT(*) FROM images");
|
||||
$cache->set("image-count", $total, 600);
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
public static function count_tag(string $tag): int
|
||||
{
|
||||
global $database;
|
||||
return (int)$database->get_one(
|
||||
"SELECT count FROM tags WHERE LOWER(tag) = LOWER(:tag)",
|
||||
["tag"=>$tag]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of image results for a given search
|
||||
*
|
||||
* @param String[] $tags
|
||||
*/
|
||||
public static function count_images(array $tags=[]): int
|
||||
{
|
||||
global $cache, $database;
|
||||
$tag_count = count($tags);
|
||||
|
||||
if (SPEED_HAX && $tag_count === 0) {
|
||||
// total number of images in the DB
|
||||
$total = self::count_total_images();
|
||||
} elseif (SPEED_HAX && $tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) {
|
||||
if (!str_starts_with($tags[0], "-")) {
|
||||
// one tag - we can look that up directly
|
||||
$total = self::count_tag($tags[0]);
|
||||
} else {
|
||||
// one negative tag - subtract from the total
|
||||
$total = self::count_total_images() - self::count_tag(substr($tags[0], 1));
|
||||
}
|
||||
} else {
|
||||
// complex query
|
||||
// implode(tags) can be too long for memcache...
|
||||
$cache_key = "image-count:" . md5(Tag::implode($tags));
|
||||
$total = $cache->get($cache_key);
|
||||
if (is_null($total)) {
|
||||
if (Extension::is_enabled(RatingsInfo::KEY)) {
|
||||
$tags[] = "rating:*";
|
||||
}
|
||||
$querylet = Image::build_search_querylet($tags);
|
||||
$total = (int)$database->get_one("SELECT COUNT(*) AS cnt FROM ($querylet->sql) AS tbl", $querylet->variables);
|
||||
if (SPEED_HAX && $total > 5000) {
|
||||
// when we have a ton of images, the count
|
||||
// won't change dramatically very often
|
||||
$cache->set($cache_key, $total, 3600);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_null($total)) {
|
||||
return 0;
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of pages for a given search
|
||||
*
|
||||
* @param String[] $tags
|
||||
*/
|
||||
public static function count_pages(array $tags=[]): int
|
||||
{
|
||||
global $config;
|
||||
return (int)ceil(Image::count_images($tags) / $config->get_int(IndexConfig::IMAGES));
|
||||
}
|
||||
|
||||
/*
|
||||
* Accessors & mutators
|
||||
*/
|
||||
@@ -218,9 +282,9 @@ class Image implements \ArrayAccess
|
||||
* Rather than simply $this_id + 1, one must take into account
|
||||
* deleted images and search queries
|
||||
*
|
||||
* @param string[] $tags
|
||||
* @param String[] $tags
|
||||
*/
|
||||
public function get_next(array $tags = [], bool $next = true): ?Image
|
||||
public function get_next(array $tags=[], bool $next=true): ?Image
|
||||
{
|
||||
global $database;
|
||||
|
||||
@@ -232,18 +296,31 @@ class Image implements \ArrayAccess
|
||||
$dir = "ASC";
|
||||
}
|
||||
|
||||
$tags[] = 'id'. $gtlt . $this->id;
|
||||
$tags[] = 'order:id_'. strtolower($dir);
|
||||
$images = Search::find_images(0, 1, $tags);
|
||||
return (count($images) > 0) ? $images[0] : null;
|
||||
if (count($tags) === 0) {
|
||||
$row = $database->get_row('
|
||||
SELECT images.*
|
||||
FROM images
|
||||
WHERE images.id '.$gtlt.' '.$this->id.'
|
||||
ORDER BY images.id '.$dir.'
|
||||
LIMIT 1
|
||||
');
|
||||
} else {
|
||||
$tags[] = 'id'. $gtlt . $this->id;
|
||||
$tags[] = 'order:id_'. strtolower($dir);
|
||||
$querylet = Image::build_search_querylet($tags);
|
||||
$querylet->append_sql(' LIMIT 1');
|
||||
$row = $database->get_row($querylet->sql, $querylet->variables);
|
||||
}
|
||||
|
||||
return ($row ? new Image($row) : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* The reverse of get_next
|
||||
*
|
||||
* @param string[] $tags
|
||||
* @param String[] $tags
|
||||
*/
|
||||
public function get_prev(array $tags = []): ?Image
|
||||
public function get_prev(array $tags=[]): ?Image
|
||||
{
|
||||
return $this->get_next($tags, false);
|
||||
}
|
||||
@@ -254,9 +331,7 @@ class Image implements \ArrayAccess
|
||||
#[Field(name: "owner")]
|
||||
public function get_owner(): User
|
||||
{
|
||||
$user = User::by_id($this->owner_id);
|
||||
assert(!is_null($user));
|
||||
return $user;
|
||||
return User::by_id($this->owner_id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -267,73 +342,92 @@ class Image implements \ArrayAccess
|
||||
global $database;
|
||||
if ($owner->id != $this->owner_id) {
|
||||
$database->execute("
|
||||
UPDATE images
|
||||
SET owner_id=:owner_id
|
||||
WHERE id=:id
|
||||
", ["owner_id" => $owner->id, "id" => $this->id]);
|
||||
UPDATE images
|
||||
SET owner_id=:owner_id
|
||||
WHERE id=:id
|
||||
", ["owner_id"=>$owner->id, "id"=>$this->id]);
|
||||
log_info("core_image", "Owner for Post #{$this->id} set to {$owner->name}");
|
||||
}
|
||||
}
|
||||
|
||||
public function save_to_db(): void
|
||||
public function save_to_db()
|
||||
{
|
||||
global $database, $user;
|
||||
$cut_name = substr($this->filename, 0, 255);
|
||||
|
||||
$props_to_save = [
|
||||
"filename" => substr($this->filename, 0, 255),
|
||||
"filesize" => $this->filesize,
|
||||
"hash" => $this->hash,
|
||||
"mime" => strtolower($this->mime),
|
||||
"ext" => strtolower($this->ext),
|
||||
"source" => $this->source,
|
||||
"width" => $this->width,
|
||||
"height" => $this->height,
|
||||
"lossless" => $this->lossless,
|
||||
"video" => $this->video,
|
||||
"video_codec" => $this->video_codec,
|
||||
"image" => $this->image,
|
||||
"audio" => $this->audio,
|
||||
"length" => $this->length
|
||||
];
|
||||
if (!$this->in_db) {
|
||||
$props_to_save["owner_id"] = $user->id;
|
||||
$props_to_save["owner_ip"] = get_real_ip();
|
||||
$props_to_save["posted"] = date('Y-m-d H:i:s', time());
|
||||
|
||||
$props_sql = implode(", ", array_keys($props_to_save));
|
||||
$vals_sql = implode(", ", array_map(fn ($prop) => ":$prop", array_keys($props_to_save)));
|
||||
if (is_null($this->posted) || $this->posted == "") {
|
||||
$this->posted = date('Y-m-d H:i:s', time());
|
||||
}
|
||||
|
||||
if (is_null($this->id)) {
|
||||
$database->execute(
|
||||
"INSERT INTO images($props_sql) VALUES ($vals_sql)",
|
||||
$props_to_save,
|
||||
"INSERT INTO images(
|
||||
owner_id, owner_ip,
|
||||
filename, filesize,
|
||||
hash, mime, ext,
|
||||
width, height,
|
||||
posted, source
|
||||
)
|
||||
VALUES (
|
||||
:owner_id, :owner_ip,
|
||||
:filename, :filesize,
|
||||
:hash, :mime, :ext,
|
||||
0, 0,
|
||||
:posted, :source
|
||||
)",
|
||||
[
|
||||
"owner_id" => $user->id, "owner_ip" => get_real_ip(),
|
||||
"filename" => $cut_name, "filesize" => $this->filesize,
|
||||
"hash" => $this->hash, "mime" => strtolower($this->mime),
|
||||
"ext" => strtolower($this->ext),
|
||||
"posted" => $this->posted, "source" => $this->source
|
||||
]
|
||||
);
|
||||
$this->id = $database->get_last_insert_id('images_id_seq');
|
||||
$this->in_db = true;
|
||||
} else {
|
||||
$props_sql = implode(", ", array_map(fn ($prop) => "$prop = :$prop", array_keys($props_to_save)));
|
||||
$database->execute(
|
||||
"UPDATE images SET $props_sql WHERE id = :id",
|
||||
array_merge(
|
||||
$props_to_save,
|
||||
["id" => $this->id]
|
||||
)
|
||||
"UPDATE images SET ".
|
||||
"filename = :filename, filesize = :filesize, hash = :hash, ".
|
||||
"mime = :mime, ext = :ext, width = 0, height = 0, ".
|
||||
"posted = :posted, source = :source ".
|
||||
"WHERE id = :id",
|
||||
[
|
||||
"filename" => $cut_name,
|
||||
"filesize" => $this->filesize,
|
||||
"hash" => $this->hash,
|
||||
"mime" => strtolower($this->mime),
|
||||
"ext" => strtolower($this->ext),
|
||||
"posted" => $this->posted,
|
||||
"source" => $this->source,
|
||||
"id" => $this->id,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// For the future: automatically save dynamic props instead of
|
||||
// requiring each extension to do it manually.
|
||||
/*
|
||||
$props_sql = "UPDATE images SET ";
|
||||
$props_sql .= implode(", ", array_map(fn ($prop) => "$prop = :$prop", array_keys($this->dynamic_props)));
|
||||
$props_sql .= " WHERE id = :id";
|
||||
$database->execute($props_sql, array_merge($this->dynamic_props, ["id" => $this->id]));
|
||||
*/
|
||||
$database->execute(
|
||||
"UPDATE images SET ".
|
||||
"lossless = :lossless, ".
|
||||
"video = :video, video_codec = :video_codec, audio = :audio,image = :image, ".
|
||||
"height = :height, width = :width, ".
|
||||
"length = :length WHERE id = :id",
|
||||
[
|
||||
"id" => $this->id,
|
||||
"width" => $this->width ?? 0,
|
||||
"height" => $this->height ?? 0,
|
||||
"lossless" => $this->lossless,
|
||||
"video" => $this->video,
|
||||
"video_codec" => $this->video_codec,
|
||||
"image" => $this->image,
|
||||
"audio" => $this->audio,
|
||||
"length" => $this->length
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this image's tags as an array.
|
||||
*
|
||||
* @return string[]
|
||||
* @return String[]
|
||||
*/
|
||||
#[Field(name: "tags", type: "[string!]!")]
|
||||
public function get_tag_array(): array
|
||||
@@ -341,12 +435,12 @@ class Image implements \ArrayAccess
|
||||
global $database;
|
||||
if (!isset($this->tag_array)) {
|
||||
$this->tag_array = $database->get_col("
|
||||
SELECT tag
|
||||
FROM image_tags
|
||||
JOIN tags ON image_tags.tag_id = tags.id
|
||||
WHERE image_id=:id
|
||||
ORDER BY tag
|
||||
", ["id" => $this->id]);
|
||||
SELECT tag
|
||||
FROM image_tags
|
||||
JOIN tags ON image_tags.tag_id = tags.id
|
||||
WHERE image_id=:id
|
||||
ORDER BY tag
|
||||
", ["id"=>$this->id]);
|
||||
sort($this->tag_array);
|
||||
}
|
||||
return $this->tag_array;
|
||||
@@ -440,9 +534,6 @@ class Image implements \ArrayAccess
|
||||
*/
|
||||
public function get_image_filename(): string
|
||||
{
|
||||
if(!is_null($this->tmp_file)) {
|
||||
return $this->tmp_file;
|
||||
}
|
||||
return warehouse_path(self::IMAGE_DIR, $this->hash);
|
||||
}
|
||||
|
||||
@@ -476,18 +567,22 @@ class Image implements \ArrayAccess
|
||||
* Get the image's mime type.
|
||||
*/
|
||||
#[Field(name: "mime")]
|
||||
public function get_mime(): string
|
||||
public function get_mime(): ?string
|
||||
{
|
||||
if ($this->mime === MimeType::WEBP && $this->lossless) {
|
||||
if ($this->mime===MimeType::WEBP&&$this->lossless) {
|
||||
return MimeType::WEBP_LOSSLESS;
|
||||
}
|
||||
return strtolower($this->mime);
|
||||
$m = $this->mime;
|
||||
if (is_null($m)) {
|
||||
$m = MimeMap::get_for_extension($this->ext)[0];
|
||||
}
|
||||
return $m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the image's mime type.
|
||||
*/
|
||||
public function set_mime(string $mime): void
|
||||
public function set_mime($mime): void
|
||||
{
|
||||
$this->mime = $mime;
|
||||
$ext = FileExtension::get_for_mime($this->get_mime());
|
||||
@@ -515,7 +610,7 @@ class Image implements \ArrayAccess
|
||||
$new_source = null;
|
||||
}
|
||||
if ($new_source != $old_source) {
|
||||
$database->execute("UPDATE images SET source=:source WHERE id=:id", ["source" => $new_source, "id" => $this->id]);
|
||||
$database->execute("UPDATE images SET source=:source WHERE id=:id", ["source"=>$new_source, "id"=>$this->id]);
|
||||
log_info("core_image", "Source for Post #{$this->id} set to: $new_source (was $old_source)");
|
||||
}
|
||||
}
|
||||
@@ -532,9 +627,8 @@ class Image implements \ArrayAccess
|
||||
{
|
||||
global $database;
|
||||
if ($locked !== $this->locked) {
|
||||
$database->execute("UPDATE images SET locked=:yn WHERE id=:id", ["yn" => $locked, "id" => $this->id]);
|
||||
$s = $locked ? "locked" : "unlocked";
|
||||
log_info("core_image", "Setting Post #{$this->id} to $s");
|
||||
$database->execute("UPDATE images SET locked=:yn WHERE id=:id", ["yn"=>$locked, "id"=>$this->id]);
|
||||
log_info("core_image", "Setting Post #{$this->id} lock to: $locked");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,38 +648,39 @@ class Image implements \ArrayAccess
|
||||
FROM image_tags
|
||||
WHERE image_id = :id
|
||||
)
|
||||
", ["id" => $this->id]);
|
||||
", ["id"=>$this->id]);
|
||||
$database->execute("
|
||||
DELETE
|
||||
FROM image_tags
|
||||
WHERE image_id=:id
|
||||
", ["id" => $this->id]);
|
||||
DELETE
|
||||
FROM image_tags
|
||||
WHERE image_id=:id
|
||||
", ["id"=>$this->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tags for this image.
|
||||
*
|
||||
* @param string[] $unfiltered_tags
|
||||
*/
|
||||
public function set_tags(array $unfiltered_tags): void
|
||||
{
|
||||
global $cache, $database, $page;
|
||||
|
||||
$tags = array_unique($unfiltered_tags);
|
||||
$unfiltered_tags = array_unique($unfiltered_tags);
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
$tags = [];
|
||||
foreach ($unfiltered_tags as $tag) {
|
||||
if (mb_strlen($tag, 'UTF-8') > 255) {
|
||||
throw new TagSetException("Can't set a tag longer than 255 characters");
|
||||
$page->flash("Can't set a tag longer than 255 characters");
|
||||
continue;
|
||||
}
|
||||
if (str_starts_with($tag, "-")) {
|
||||
throw new TagSetException("Can't set a tag which starts with a minus");
|
||||
}
|
||||
if (str_contains($tag, "*")) {
|
||||
throw new TagSetException("Can't set a tag which contains a wildcard (*)");
|
||||
$page->flash("Can't set a tag which starts with a minus");
|
||||
continue;
|
||||
}
|
||||
|
||||
$tags[] = $tag;
|
||||
}
|
||||
|
||||
if (count($tags) <= 0) {
|
||||
throw new TagSetException('Tried to set zero tags');
|
||||
throw new SCoreException('Tried to set zero tags');
|
||||
}
|
||||
|
||||
if (strtolower(Tag::implode($tags)) != strtolower($this->get_tag_list())) {
|
||||
@@ -604,7 +699,7 @@ class Image implements \ArrayAccess
|
||||
FROM image_tags
|
||||
WHERE image_id = :id
|
||||
)
|
||||
", ["id" => $this->id]);
|
||||
", ["id"=>$this->id]);
|
||||
|
||||
log_info("core_image", "Tags for Post #{$this->id} set to: ".Tag::implode($tags));
|
||||
$cache->delete("image-{$this->id}-tags");
|
||||
@@ -618,34 +713,247 @@ class Image implements \ArrayAccess
|
||||
{
|
||||
global $database;
|
||||
$this->delete_tags_from_image();
|
||||
$database->execute("DELETE FROM images WHERE id=:id", ["id" => $this->id]);
|
||||
$database->execute("DELETE FROM images WHERE id=:id", ["id"=>$this->id]);
|
||||
log_info("core_image", 'Deleted Post #'.$this->id.' ('.$this->hash.')');
|
||||
$this->remove_image_only(quiet: true);
|
||||
|
||||
unlink($this->get_image_filename());
|
||||
unlink($this->get_thumb_filename());
|
||||
}
|
||||
|
||||
/**
|
||||
* This function removes an image (and thumbnail) from the DISK ONLY.
|
||||
* It DOES NOT remove anything from the database.
|
||||
*/
|
||||
public function remove_image_only(bool $quiet = false): void
|
||||
public function remove_image_only(): void
|
||||
{
|
||||
$img_del = @unlink($this->get_image_filename());
|
||||
$thumb_del = @unlink($this->get_thumb_filename());
|
||||
if($img_del && $thumb_del) {
|
||||
if(!$quiet) {
|
||||
log_info("core_image", "Deleted files for Post #{$this->id} ({$this->hash})");
|
||||
}
|
||||
} else {
|
||||
$img = $img_del ? '' : ' image';
|
||||
$thumb = $thumb_del ? '' : ' thumbnail';
|
||||
log_error('core_image', "Failed to delete files for Post #{$this->id}{$img}{$thumb}");
|
||||
}
|
||||
log_info("core_image", 'Removed Post File ('.$this->hash.')');
|
||||
@unlink($this->get_image_filename());
|
||||
@unlink($this->get_thumb_filename());
|
||||
}
|
||||
|
||||
public function parse_link_template(string $tmpl, int $n = 0): string
|
||||
public function parse_link_template(string $tmpl, int $n=0): string
|
||||
{
|
||||
$plte = send_event(new ParseLinkTemplateEvent($tmpl, $this));
|
||||
$tmpl = $plte->link;
|
||||
return load_balance_url($tmpl, $this->hash, $n);
|
||||
}
|
||||
|
||||
private static function tag_or_wildcard_to_ids(string $tag): array
|
||||
{
|
||||
global $database;
|
||||
$sq = "SELECT id FROM tags WHERE LOWER(tag) LIKE LOWER(:tag)";
|
||||
if ($database->get_driver_id() === DatabaseDriverID::SQLITE) {
|
||||
$sq .= "ESCAPE '\\'";
|
||||
}
|
||||
return $database->get_col($sq, ["tag" => Tag::sqlify($tag)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param String[] $terms
|
||||
*/
|
||||
private static function build_search_querylet(
|
||||
array $terms,
|
||||
?int $limit=null,
|
||||
?int $offset=null
|
||||
): Querylet {
|
||||
global $config;
|
||||
|
||||
$tag_conditions = [];
|
||||
$img_conditions = [];
|
||||
$order = null;
|
||||
|
||||
/*
|
||||
* Turn a bunch of strings into a bunch of TagCondition
|
||||
* and ImgCondition objects
|
||||
*/
|
||||
$stpen = 0; // search term parse event number
|
||||
foreach (array_merge([null], $terms) as $term) {
|
||||
$stpe = send_event(new SearchTermParseEvent($stpen++, $term, $terms));
|
||||
$order ??= $stpe->order;
|
||||
$img_conditions = array_merge($img_conditions, $stpe->img_conditions);
|
||||
$tag_conditions = array_merge($tag_conditions, $stpe->tag_conditions);
|
||||
}
|
||||
|
||||
$order = ($order ?: "images.".$config->get_string(IndexConfig::ORDER));
|
||||
|
||||
/*
|
||||
* Turn a bunch of Querylet objects into a base query
|
||||
*
|
||||
* Must follow the format
|
||||
*
|
||||
* SELECT images.*
|
||||
* FROM (...) AS images
|
||||
* WHERE (...)
|
||||
*
|
||||
* ie, return a set of images.* columns, and end with a WHERE
|
||||
*/
|
||||
|
||||
// no tags, do a simple search
|
||||
if (count($tag_conditions) === 0) {
|
||||
$query = new Querylet("SELECT images.* FROM images WHERE 1=1");
|
||||
}
|
||||
|
||||
// one tag sorted by ID - we can fetch this from the image_tags table,
|
||||
// and do the offset / limit there, which is 10x faster than fetching
|
||||
// all the image_tags and doing the offset / limit on the result.
|
||||
elseif (
|
||||
count($tag_conditions) === 1
|
||||
&& empty($img_conditions)
|
||||
&& ($order == "id DESC" || $order == "images.id DESC")
|
||||
&& !is_null($offset)
|
||||
&& !is_null($limit)
|
||||
) {
|
||||
$tc = $tag_conditions[0];
|
||||
$in = $tc->positive ? "IN" : "NOT IN";
|
||||
// IN (SELECT id FROM tags) is 100x slower than doing a separate
|
||||
// query and then a second query for IN(first_query_results)??
|
||||
$tag_array = self::tag_or_wildcard_to_ids($tc->tag);
|
||||
if (count($tag_array) == 0) {
|
||||
// if wildcard expanded to nothing, take a shortcut
|
||||
if ($tc->positive) {
|
||||
$query = new Querylet("SELECT images.* FROM images WHERE 1=0");
|
||||
} else {
|
||||
$query = new Querylet("SELECT images.* FROM images WHERE 1=1");
|
||||
}
|
||||
} else {
|
||||
$set = implode(', ', $tag_array);
|
||||
$query = new Querylet("
|
||||
SELECT images.*
|
||||
FROM images INNER JOIN (
|
||||
SELECT it.image_id
|
||||
FROM image_tags it
|
||||
WHERE it.tag_id $in ($set)
|
||||
ORDER BY it.image_id DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
) a on a.image_id = images.id
|
||||
ORDER BY images.id DESC
|
||||
", ["limit"=>$limit, "offset"=>$offset]);
|
||||
// don't offset at the image level because
|
||||
// we already offset at the image_tags level
|
||||
$order = null;
|
||||
$limit = null;
|
||||
$offset = null;
|
||||
}
|
||||
}
|
||||
|
||||
// more than one tag, or more than zero other conditions, or a non-default sort order
|
||||
else {
|
||||
$positive_tag_id_array = [];
|
||||
$positive_wildcard_id_array = [];
|
||||
$negative_tag_id_array = [];
|
||||
$all_nonexistent_negatives = true;
|
||||
|
||||
foreach ($tag_conditions as $tq) {
|
||||
$tag_ids = self::tag_or_wildcard_to_ids($tq->tag);
|
||||
$tag_count = count($tag_ids);
|
||||
|
||||
if ($tq->positive) {
|
||||
$all_nonexistent_negatives = false;
|
||||
if ($tag_count== 0) {
|
||||
# one of the positive tags had zero results, therefor there
|
||||
# can be no results; "where 1=0" should shortcut things
|
||||
return new Querylet("SELECT images.* FROM images WHERE 1=0");
|
||||
} elseif ($tag_count==1) {
|
||||
// All wildcard terms that qualify for a single tag can be treated the same as non-wildcards
|
||||
$positive_tag_id_array[] = $tag_ids[0];
|
||||
} else {
|
||||
// Terms that resolve to multiple tags act as an OR within themselves
|
||||
// and as an AND in relation to all other terms,
|
||||
$positive_wildcard_id_array[] = $tag_ids;
|
||||
}
|
||||
} else {
|
||||
if ($tag_count > 0) {
|
||||
$all_nonexistent_negatives = false;
|
||||
// Unlike positive criteria, negative criteria are all handled in an OR fashion,
|
||||
// so we can just compile them all into a single sub-query.
|
||||
$negative_tag_id_array = array_merge($negative_tag_id_array, $tag_ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert($positive_tag_id_array || $positive_wildcard_id_array || $negative_tag_id_array || $all_nonexistent_negatives, @$_GET['q']);
|
||||
|
||||
if ($all_nonexistent_negatives) {
|
||||
$query = new Querylet("SELECT images.* FROM images WHERE 1=1");
|
||||
} elseif (!empty($positive_tag_id_array) || !empty($positive_wildcard_id_array)) {
|
||||
$inner_joins = [];
|
||||
if (!empty($positive_tag_id_array)) {
|
||||
foreach ($positive_tag_id_array as $tag) {
|
||||
$inner_joins[] = "= $tag";
|
||||
}
|
||||
}
|
||||
if (!empty($positive_wildcard_id_array)) {
|
||||
foreach ($positive_wildcard_id_array as $tags) {
|
||||
$positive_tag_id_list = join(', ', $tags);
|
||||
$inner_joins[] = "IN ($positive_tag_id_list)";
|
||||
}
|
||||
}
|
||||
|
||||
$first = array_shift($inner_joins);
|
||||
$sub_query = "SELECT it.image_id FROM image_tags it ";
|
||||
$i = 0;
|
||||
foreach ($inner_joins as $inner_join) {
|
||||
$i++;
|
||||
$sub_query .= " INNER JOIN image_tags it$i ON it$i.image_id = it.image_id AND it$i.tag_id $inner_join ";
|
||||
}
|
||||
if (!empty($negative_tag_id_array)) {
|
||||
$negative_tag_id_list = join(', ', $negative_tag_id_array);
|
||||
$sub_query .= " LEFT JOIN image_tags negative ON negative.image_id = it.image_id AND negative.tag_id IN ($negative_tag_id_list) ";
|
||||
}
|
||||
$sub_query .= "WHERE it.tag_id $first ";
|
||||
if (!empty($negative_tag_id_array)) {
|
||||
$sub_query .= " AND negative.image_id IS NULL";
|
||||
}
|
||||
$sub_query .= " GROUP BY it.image_id ";
|
||||
|
||||
$query = new Querylet("
|
||||
SELECT images.*
|
||||
FROM images
|
||||
INNER JOIN ($sub_query) a on a.image_id = images.id
|
||||
");
|
||||
} elseif (!empty($negative_tag_id_array)) {
|
||||
$negative_tag_id_list = join(', ', $negative_tag_id_array);
|
||||
$query = new Querylet("
|
||||
SELECT images.*
|
||||
FROM images
|
||||
LEFT JOIN image_tags negative ON negative.image_id = images.id AND negative.tag_id in ($negative_tag_id_list)
|
||||
WHERE negative.image_id IS NULL
|
||||
");
|
||||
} else {
|
||||
throw new SCoreException("No criteria specified");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Merge all the image metadata searches into one generic querylet
|
||||
* and append to the base querylet with "AND blah"
|
||||
*/
|
||||
if (!empty($img_conditions)) {
|
||||
$n = 0;
|
||||
$img_sql = "";
|
||||
$img_vars = [];
|
||||
foreach ($img_conditions as $iq) {
|
||||
if ($n++ > 0) {
|
||||
$img_sql .= " AND";
|
||||
}
|
||||
if (!$iq->positive) {
|
||||
$img_sql .= " NOT";
|
||||
}
|
||||
$img_sql .= " (" . $iq->qlet->sql . ")";
|
||||
$img_vars = array_merge($img_vars, $iq->qlet->variables);
|
||||
}
|
||||
$query->append_sql(" AND ");
|
||||
$query->append(new Querylet($img_sql, $img_vars));
|
||||
}
|
||||
|
||||
if (!is_null($order)) {
|
||||
$query->append(new Querylet(" ORDER BY ".$order));
|
||||
}
|
||||
if (!is_null($limit)) {
|
||||
$query->append(new Querylet(" LIMIT :limit ", ["limit" => $limit]));
|
||||
$query->append(new Querylet(" OFFSET :offset ", ["offset" => $offset]));
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,41 +12,42 @@ namespace Shimmie2;
|
||||
* Add a directory full of images
|
||||
*
|
||||
* @param string $base
|
||||
* @param string[] $extra_tags
|
||||
* @return UploadResult[]
|
||||
* @return array
|
||||
*/
|
||||
function add_dir(string $base, array $extra_tags = []): array
|
||||
function add_dir(string $base): array
|
||||
{
|
||||
global $database;
|
||||
$results = [];
|
||||
|
||||
foreach (list_files($base) as $full_path) {
|
||||
$short_path = str_replace($base, "", $full_path);
|
||||
$filename = basename($full_path);
|
||||
|
||||
$tags = array_merge(path_to_tags($short_path), $extra_tags);
|
||||
$tags = path_to_tags($short_path);
|
||||
$result = "$short_path (".str_replace(" ", ", ", $tags).")... ";
|
||||
try {
|
||||
$more_results = $database->with_savepoint(function () use ($full_path, $filename, $tags) {
|
||||
$dae = send_event(new DataUploadEvent($full_path, [
|
||||
'filename' => pathinfo($filename, PATHINFO_BASENAME),
|
||||
'tags' => $tags,
|
||||
'source' => null,
|
||||
]));
|
||||
$results = [];
|
||||
foreach($dae->images as $image) {
|
||||
$results[] = new UploadSuccess($filename, $image->id);
|
||||
}
|
||||
return $results;
|
||||
});
|
||||
$results = array_merge($results, $more_results);
|
||||
add_image($full_path, $filename, $tags);
|
||||
$result .= "ok";
|
||||
} catch (UploadException $ex) {
|
||||
$results[] = new UploadError($filename, $ex->getMessage());
|
||||
$result .= "failed: ".$ex->getMessage();
|
||||
}
|
||||
$results[] = $result;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DataUploadEvent for a file.
|
||||
*/
|
||||
function add_image(string $tmpname, string $filename, string $tags, ?string $source=null): DataUploadEvent
|
||||
{
|
||||
return send_event(new DataUploadEvent($tmpname, [
|
||||
'filename' => pathinfo($filename, PATHINFO_BASENAME),
|
||||
'tags' => Tag::explode($tags),
|
||||
'source' => $source,
|
||||
]));
|
||||
}
|
||||
|
||||
function get_file_ext(string $filename): ?string
|
||||
{
|
||||
return pathinfo($filename)['extension'] ?? null;
|
||||
@@ -60,7 +61,7 @@ function get_file_ext(string $filename): ?string
|
||||
* @param int $orig_width
|
||||
* @param int $orig_height
|
||||
* @param bool $use_dpi_scaling Enables the High-DPI scaling.
|
||||
* @return array{0: int, 1: int}
|
||||
* @return array
|
||||
*/
|
||||
function get_thumbnail_size(int $orig_width, int $orig_height, bool $use_dpi_scaling = false): array
|
||||
{
|
||||
@@ -108,60 +109,53 @@ function get_thumbnail_size(int $orig_width, int $orig_height, bool $use_dpi_sca
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: int, 1: int, 2: float}
|
||||
*/
|
||||
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);
|
||||
$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];
|
||||
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{0: int, 1: int}
|
||||
* @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);
|
||||
$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(Image $image, string $engine = null): void
|
||||
function create_image_thumb(string $hash, string $mime, 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(
|
||||
$image->get_image_filename(),
|
||||
$image->get_thumb_filename(),
|
||||
get_thumbnail_max_size_scaled(),
|
||||
$image->get_mime(),
|
||||
$inname,
|
||||
$outname,
|
||||
$tsize,
|
||||
$mime,
|
||||
$engine,
|
||||
$config->get_string(ImageConfig::THUMB_FIT)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array{0: int, 1: int} $tsize
|
||||
*/
|
||||
function create_scaled_image(
|
||||
string $inname,
|
||||
string $outname,
|
||||
array $tsize,
|
||||
string $mime,
|
||||
?string $engine = null,
|
||||
?string $resize_type = null
|
||||
): void {
|
||||
|
||||
function create_scaled_image(string $inname, string $outname, array $tsize, string $mime, ?string $engine = null, ?string $resize_type = null)
|
||||
{
|
||||
global $config;
|
||||
if (empty($engine)) {
|
||||
$engine = $config->get_string(ImageConfig::THUMB_ENGINE);
|
||||
@@ -193,7 +187,7 @@ function redirect_to_next_image(Image $image): void
|
||||
global $page;
|
||||
|
||||
if (isset($_GET['search'])) {
|
||||
$search_terms = Tag::explode($_GET['search']);
|
||||
$search_terms = Tag::explode(Tag::decaret($_GET['search']));
|
||||
$query = "search=" . url_escape($_GET['search']);
|
||||
} else {
|
||||
$search_terms = [];
|
||||
@@ -203,7 +197,7 @@ function redirect_to_next_image(Image $image): void
|
||||
$target_image = $image->get_next($search_terms);
|
||||
|
||||
if ($target_image === null) {
|
||||
$redirect_target = referer_or(search_link(), ['post/view']);
|
||||
$redirect_target = referer_or(make_link("post/list"), ['post/view']);
|
||||
} else {
|
||||
$redirect_target = make_link("post/view/{$target_image->id}", null, $query);
|
||||
}
|
||||
|
||||
@@ -4,17 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use GQLA\Query;
|
||||
|
||||
class Querylet
|
||||
{
|
||||
/**
|
||||
* @param string $sql
|
||||
* @param array<string, mixed> $variables
|
||||
*/
|
||||
public function __construct(
|
||||
public string $sql,
|
||||
public array $variables = [],
|
||||
public array $variables=[],
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -23,13 +17,23 @@ class Querylet
|
||||
$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
|
||||
{
|
||||
public function __construct(
|
||||
public string $tag,
|
||||
public bool $positive = true,
|
||||
public bool $positive,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -38,391 +42,7 @@ class ImgCondition
|
||||
{
|
||||
public function __construct(
|
||||
public Querylet $qlet,
|
||||
public bool $positive = true,
|
||||
public bool $positive,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
class Search
|
||||
{
|
||||
/** @var list<string> */
|
||||
public static array $_search_path = [];
|
||||
|
||||
/**
|
||||
* @param list<string> $tags
|
||||
*/
|
||||
private static function find_images_internal(int $start = 0, ?int $limit = null, array $tags = []): \FFSPHP\PDOStatement
|
||||
{
|
||||
global $database, $user;
|
||||
|
||||
if ($start < 0) {
|
||||
$start = 0;
|
||||
}
|
||||
if ($limit !== null && $limit < 1) {
|
||||
$limit = 1;
|
||||
}
|
||||
|
||||
if (SPEED_HAX) {
|
||||
if (!$user->can(Permissions::BIG_SEARCH) and count($tags) > 3) {
|
||||
throw new PermissionDeniedException("Anonymous users may only search for up to 3 tags at a time");
|
||||
}
|
||||
}
|
||||
|
||||
[$tag_conditions, $img_conditions, $order] = self::terms_to_conditions($tags);
|
||||
$querylet = self::build_search_querylet($tag_conditions, $img_conditions, $order, $limit, $start);
|
||||
return $database->get_all_iterable($querylet->sql, $querylet->variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for an array of images
|
||||
*
|
||||
* @param list<string> $tags
|
||||
* @return Image[]
|
||||
*/
|
||||
#[Query(name: "posts", type: "[Post!]!", args: ["tags" => "[string!]"])]
|
||||
public static function find_images(int $offset = 0, ?int $limit = null, array $tags = []): array
|
||||
{
|
||||
$result = self::find_images_internal($offset, $limit, $tags);
|
||||
|
||||
$images = [];
|
||||
foreach ($result as $row) {
|
||||
$images[] = new Image($row);
|
||||
}
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for an array of images, returning a iterable object of Image
|
||||
*
|
||||
* @param list<string> $tags
|
||||
* @return \Generator<Image>
|
||||
*/
|
||||
public static function find_images_iterable(int $start = 0, ?int $limit = null, array $tags = []): \Generator
|
||||
{
|
||||
$result = self::find_images_internal($start, $limit, $tags);
|
||||
foreach ($result as $row) {
|
||||
yield new Image($row);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific set of images, in the order that the set specifies,
|
||||
* with all the search stuff (rating filters etc) taken into account
|
||||
*
|
||||
* @param int[] $ids
|
||||
* @return Image[]
|
||||
*/
|
||||
public static function get_images(array $ids): array
|
||||
{
|
||||
$visible_images = [];
|
||||
foreach(Search::find_images(tags: ["id=" . implode(",", $ids)]) as $image) {
|
||||
$visible_images[$image->id] = $image;
|
||||
}
|
||||
$visible_ids = array_keys($visible_images);
|
||||
|
||||
$visible_popular_ids = array_filter($ids, fn ($id) => in_array($id, $visible_ids));
|
||||
$images = array_map(fn ($id) => $visible_images[$id], $visible_popular_ids);
|
||||
return $images;
|
||||
}
|
||||
|
||||
/*
|
||||
* Image-related utility functions
|
||||
*/
|
||||
|
||||
public static function count_tag(string $tag): int
|
||||
{
|
||||
global $database;
|
||||
return (int)$database->get_one(
|
||||
"SELECT count FROM tags WHERE LOWER(tag) = LOWER(:tag)",
|
||||
["tag" => $tag]
|
||||
);
|
||||
}
|
||||
|
||||
private static function count_total_images(): int
|
||||
{
|
||||
global $database;
|
||||
return cache_get_or_set("image-count", fn () => (int)$database->get_one("SELECT COUNT(*) FROM images"), 600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of image results for a given search
|
||||
*
|
||||
* @param list<string> $tags
|
||||
*/
|
||||
public static function count_images(array $tags = []): int
|
||||
{
|
||||
global $cache, $database;
|
||||
$tag_count = count($tags);
|
||||
|
||||
// SPEED_HAX ignores the fact that extensions can add img_conditions
|
||||
// even when there are no tags being searched for
|
||||
if (SPEED_HAX && $tag_count === 0) {
|
||||
// total number of images in the DB
|
||||
$total = self::count_total_images();
|
||||
} elseif (SPEED_HAX && $tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) {
|
||||
if (!str_starts_with($tags[0], "-")) {
|
||||
// one positive tag - we can look that up directly
|
||||
$total = self::count_tag($tags[0]);
|
||||
} else {
|
||||
// one negative tag - subtract from the total
|
||||
$total = self::count_total_images() - self::count_tag(substr($tags[0], 1));
|
||||
}
|
||||
} else {
|
||||
// complex query
|
||||
// implode(tags) can be too long for memcache, so use the hash of tags as the key
|
||||
$cache_key = "image-count:" . md5(Tag::implode($tags));
|
||||
$total = $cache->get($cache_key);
|
||||
if (is_null($total)) {
|
||||
[$tag_conditions, $img_conditions, $order] = self::terms_to_conditions($tags);
|
||||
$querylet = self::build_search_querylet($tag_conditions, $img_conditions, null);
|
||||
$total = (int)$database->get_one("SELECT COUNT(*) AS cnt FROM ($querylet->sql) AS tbl", $querylet->variables);
|
||||
if (SPEED_HAX && $total > 5000) {
|
||||
// when we have a ton of images, the count
|
||||
// won't change dramatically very often
|
||||
$cache->set($cache_key, $total, 3600);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return list<int>
|
||||
*/
|
||||
private static function tag_or_wildcard_to_ids(string $tag): array
|
||||
{
|
||||
global $database;
|
||||
$sq = "SELECT id FROM tags WHERE LOWER(tag) LIKE LOWER(:tag)";
|
||||
if ($database->get_driver_id() === DatabaseDriverID::SQLITE) {
|
||||
$sq .= "ESCAPE '\\'";
|
||||
}
|
||||
return $database->get_col($sq, ["tag" => Tag::sqlify($tag)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a human input string into a an abstract search query
|
||||
*
|
||||
* @param string[] $terms
|
||||
* @return array{0: TagCondition[], 1: ImgCondition[], 2: string}
|
||||
*/
|
||||
private static function terms_to_conditions(array $terms): array
|
||||
{
|
||||
global $config;
|
||||
|
||||
$tag_conditions = [];
|
||||
$img_conditions = [];
|
||||
$order = null;
|
||||
|
||||
/*
|
||||
* Turn a bunch of strings into a bunch of TagCondition
|
||||
* and ImgCondition objects
|
||||
*/
|
||||
$stpen = 0; // search term parse event number
|
||||
foreach (array_merge([null], $terms) as $term) {
|
||||
$stpe = send_event(new SearchTermParseEvent($stpen++, $term, $terms));
|
||||
$order ??= $stpe->order;
|
||||
$img_conditions = array_merge($img_conditions, $stpe->img_conditions);
|
||||
$tag_conditions = array_merge($tag_conditions, $stpe->tag_conditions);
|
||||
}
|
||||
|
||||
$order = ($order ?: "images.".$config->get_string(IndexConfig::ORDER));
|
||||
|
||||
return [$tag_conditions, $img_conditions, $order];
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn an abstract search query into an SQL Querylet
|
||||
*
|
||||
* @param TagCondition[] $tag_conditions
|
||||
* @param ImgCondition[] $img_conditions
|
||||
*/
|
||||
private static function build_search_querylet(
|
||||
array $tag_conditions,
|
||||
array $img_conditions,
|
||||
?string $order = null,
|
||||
?int $limit = null,
|
||||
?int $offset = null
|
||||
): Querylet {
|
||||
// no tags, do a simple search
|
||||
if (count($tag_conditions) === 0) {
|
||||
static::$_search_path[] = "no_tags";
|
||||
$query = new Querylet("SELECT images.* FROM images WHERE 1=1");
|
||||
}
|
||||
|
||||
// one tag sorted by ID - we can fetch this from the image_tags table,
|
||||
// and do the offset / limit there, which is 10x faster than fetching
|
||||
// all the image_tags and doing the offset / limit on the result.
|
||||
elseif (
|
||||
count($tag_conditions) === 1
|
||||
&& $tag_conditions[0]->positive
|
||||
// We can only do this if img_conditions is empty, because
|
||||
// we're going to apply the offset / limit to the image_tags
|
||||
// subquery, and applying extra conditions to the top-level
|
||||
// query might reduce the total results below the target limit
|
||||
&& empty($img_conditions)
|
||||
// We can only do this if we're sorting by ID, because
|
||||
// we're going to be using the image_tags table, which
|
||||
// only has image_id and tag_id, not any other columns
|
||||
&& ($order == "id DESC" || $order == "images.id DESC")
|
||||
// This is only an optimisation if we are applying limit
|
||||
// and offset
|
||||
&& !is_null($limit)
|
||||
&& !is_null($offset)
|
||||
) {
|
||||
static::$_search_path[] = "fast";
|
||||
$tc = $tag_conditions[0];
|
||||
// IN (SELECT id FROM tags) is 100x slower than doing a separate
|
||||
// query and then a second query for IN(first_query_results)??
|
||||
$tag_array = self::tag_or_wildcard_to_ids($tc->tag);
|
||||
if (count($tag_array) == 0) {
|
||||
// if wildcard expanded to nothing, take a shortcut
|
||||
static::$_search_path[] = "invalid_tag";
|
||||
$query = new Querylet("SELECT images.* FROM images WHERE 1=0");
|
||||
} else {
|
||||
$set = implode(', ', $tag_array);
|
||||
$query = new Querylet("
|
||||
SELECT images.*
|
||||
FROM images INNER JOIN (
|
||||
SELECT DISTINCT it.image_id
|
||||
FROM image_tags it
|
||||
WHERE it.tag_id IN ($set)
|
||||
ORDER BY it.image_id DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
) a on a.image_id = images.id
|
||||
WHERE 1=1
|
||||
", ["limit" => $limit, "offset" => $offset]);
|
||||
// don't offset at the image level because
|
||||
// we already offset at the image_tags level
|
||||
$limit = null;
|
||||
$offset = null;
|
||||
}
|
||||
}
|
||||
|
||||
// more than one tag, or more than zero other conditions, or a non-default sort order
|
||||
else {
|
||||
static::$_search_path[] = "general";
|
||||
$positive_tag_id_array = [];
|
||||
$positive_wildcard_id_array = [];
|
||||
$negative_tag_id_array = [];
|
||||
$all_nonexistent_negatives = true;
|
||||
|
||||
foreach ($tag_conditions as $tq) {
|
||||
$tag_ids = self::tag_or_wildcard_to_ids($tq->tag);
|
||||
$tag_count = count($tag_ids);
|
||||
|
||||
if ($tq->positive) {
|
||||
$all_nonexistent_negatives = false;
|
||||
if ($tag_count == 0) {
|
||||
# one of the positive tags had zero results, therefor there
|
||||
# can be no results; "where 1=0" should shortcut things
|
||||
static::$_search_path[] = "invalid_tag";
|
||||
return new Querylet("SELECT images.* FROM images WHERE 1=0");
|
||||
} elseif ($tag_count == 1) {
|
||||
// All wildcard terms that qualify for a single tag can be treated the same as non-wildcards
|
||||
$positive_tag_id_array[] = $tag_ids[0];
|
||||
} else {
|
||||
// Terms that resolve to multiple tags act as an OR within themselves
|
||||
// and as an AND in relation to all other terms,
|
||||
$positive_wildcard_id_array[] = $tag_ids;
|
||||
}
|
||||
} else {
|
||||
if ($tag_count > 0) {
|
||||
$all_nonexistent_negatives = false;
|
||||
// Unlike positive criteria, negative criteria are all handled in an OR fashion,
|
||||
// so we can just compile them all into a single sub-query.
|
||||
$negative_tag_id_array = array_merge($negative_tag_id_array, $tag_ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert($positive_tag_id_array || $positive_wildcard_id_array || $negative_tag_id_array || $all_nonexistent_negatives, @$_GET['q']);
|
||||
|
||||
if ($all_nonexistent_negatives) {
|
||||
static::$_search_path[] = "all_nonexistent_negatives";
|
||||
$query = new Querylet("SELECT images.* FROM images WHERE 1=1");
|
||||
} elseif (!empty($positive_tag_id_array) || !empty($positive_wildcard_id_array)) {
|
||||
static::$_search_path[] = "some_positives";
|
||||
$inner_joins = [];
|
||||
if (!empty($positive_tag_id_array)) {
|
||||
foreach ($positive_tag_id_array as $tag) {
|
||||
$inner_joins[] = "= $tag";
|
||||
}
|
||||
}
|
||||
if (!empty($positive_wildcard_id_array)) {
|
||||
foreach ($positive_wildcard_id_array as $tags) {
|
||||
$positive_tag_id_list = join(', ', $tags);
|
||||
$inner_joins[] = "IN ($positive_tag_id_list)";
|
||||
}
|
||||
}
|
||||
|
||||
$first = array_shift($inner_joins);
|
||||
$sub_query = "SELECT DISTINCT it.image_id FROM image_tags it ";
|
||||
$i = 0;
|
||||
foreach ($inner_joins as $inner_join) {
|
||||
$i++;
|
||||
$sub_query .= " INNER JOIN image_tags it$i ON it$i.image_id = it.image_id AND it$i.tag_id $inner_join ";
|
||||
}
|
||||
if (!empty($negative_tag_id_array)) {
|
||||
$negative_tag_id_list = join(', ', $negative_tag_id_array);
|
||||
$sub_query .= " LEFT JOIN image_tags negative ON negative.image_id = it.image_id AND negative.tag_id IN ($negative_tag_id_list) ";
|
||||
}
|
||||
$sub_query .= "WHERE it.tag_id $first ";
|
||||
if (!empty($negative_tag_id_array)) {
|
||||
$sub_query .= " AND negative.image_id IS NULL";
|
||||
}
|
||||
$sub_query .= " GROUP BY it.image_id ";
|
||||
|
||||
$query = new Querylet("
|
||||
SELECT images.*
|
||||
FROM images
|
||||
INNER JOIN ($sub_query) a on a.image_id = images.id
|
||||
");
|
||||
} elseif (!empty($negative_tag_id_array)) {
|
||||
static::$_search_path[] = "only_negative_tags";
|
||||
$negative_tag_id_list = join(', ', $negative_tag_id_array);
|
||||
$query = new Querylet("
|
||||
SELECT images.*
|
||||
FROM images
|
||||
LEFT JOIN image_tags negative ON negative.image_id = images.id AND negative.tag_id in ($negative_tag_id_list)
|
||||
WHERE negative.image_id IS NULL
|
||||
");
|
||||
} else {
|
||||
throw new SCoreException("No criteria specified");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Merge all the image metadata searches into one generic querylet
|
||||
* and append to the base querylet with "AND blah"
|
||||
*/
|
||||
if (!empty($img_conditions)) {
|
||||
$n = 0;
|
||||
$img_sql = "";
|
||||
$img_vars = [];
|
||||
foreach ($img_conditions as $iq) {
|
||||
if ($n++ > 0) {
|
||||
$img_sql .= " AND";
|
||||
}
|
||||
if (!$iq->positive) {
|
||||
$img_sql .= " NOT";
|
||||
}
|
||||
$img_sql .= " (" . $iq->qlet->sql . ")";
|
||||
$img_vars = array_merge($img_vars, $iq->qlet->variables);
|
||||
}
|
||||
$query->append(new Querylet(" AND "));
|
||||
$query->append(new Querylet($img_sql, $img_vars));
|
||||
}
|
||||
|
||||
if(!is_null($order)) {
|
||||
$query->append(new Querylet(" ORDER BY ".$order));
|
||||
}
|
||||
|
||||
if (!is_null($limit)) {
|
||||
$query->append(new Querylet(" LIMIT :limit ", ["limit" => $limit]));
|
||||
$query->append(new Querylet(" OFFSET :offset ", ["offset" => $offset]));
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,10 +26,14 @@ class TagUsage
|
||||
* @return TagUsage[]
|
||||
*/
|
||||
#[Query(name: "tags", type: '[TagUsage!]!')]
|
||||
public static function tags(string $search, int $limit = 10): array
|
||||
public static function tags(string $search, int $limit=10): array
|
||||
{
|
||||
global $cache, $database;
|
||||
|
||||
if (!$search) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$search = strtolower($search);
|
||||
if (
|
||||
$search == '' ||
|
||||
@@ -44,16 +48,16 @@ class TagUsage
|
||||
$limitSQL = "";
|
||||
$search = str_replace('_', '\_', $search);
|
||||
$search = str_replace('%', '\%', $search);
|
||||
$SQLarr = ["search" => "$search%"]; #, "cat_search"=>"%:$search%"];
|
||||
$SQLarr = ["search"=>"$search%"]; #, "cat_search"=>"%:$search%"];
|
||||
if ($limit !== 0) {
|
||||
$limitSQL = "LIMIT :limit";
|
||||
$SQLarr['limit'] = $limit;
|
||||
$cache_key .= "-" . $limit;
|
||||
}
|
||||
|
||||
$res = cache_get_or_set(
|
||||
$cache_key,
|
||||
fn () => $database->get_pairs(
|
||||
$res = $cache->get($cache_key);
|
||||
if (is_null($res)) {
|
||||
$res = $database->get_pairs(
|
||||
"
|
||||
SELECT tag, count
|
||||
FROM tags
|
||||
@@ -64,9 +68,9 @@ class TagUsage
|
||||
$limitSQL
|
||||
",
|
||||
$SQLarr
|
||||
),
|
||||
600
|
||||
);
|
||||
);
|
||||
$cache->set($cache_key, $res, 600);
|
||||
}
|
||||
|
||||
$counts = [];
|
||||
foreach ($res as $k => $v) {
|
||||
@@ -86,8 +90,7 @@ class TagUsage
|
||||
*/
|
||||
class Tag
|
||||
{
|
||||
/** @var array<string, int> */
|
||||
private static array $tag_id_cache = [];
|
||||
private static $tag_id_cache = [];
|
||||
|
||||
public static function get_or_create_id(string $tag): int
|
||||
{
|
||||
@@ -101,17 +104,17 @@ class Tag
|
||||
|
||||
$id = $database->get_one(
|
||||
"SELECT id FROM tags WHERE LOWER(tag) = LOWER(:tag)",
|
||||
["tag" => $tag]
|
||||
["tag"=>$tag]
|
||||
);
|
||||
if (empty($id)) {
|
||||
// a new tag
|
||||
$database->execute(
|
||||
"INSERT INTO tags(tag) VALUES (:tag)",
|
||||
["tag" => $tag]
|
||||
["tag"=>$tag]
|
||||
);
|
||||
$id = $database->get_one(
|
||||
"SELECT id FROM tags WHERE LOWER(tag) = LOWER(:tag)",
|
||||
["tag" => $tag]
|
||||
["tag"=>$tag]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,19 +122,18 @@ class Tag
|
||||
return $id;
|
||||
}
|
||||
|
||||
/** @param string[] $tags */
|
||||
public static function implode(array $tags): string
|
||||
{
|
||||
sort($tags, SORT_FLAG_CASE | SORT_STRING);
|
||||
sort($tags, SORT_FLAG_CASE|SORT_STRING);
|
||||
return implode(' ', $tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a human-supplied string into a valid tag array.
|
||||
*
|
||||
* @return string[]
|
||||
* #return string[]
|
||||
*/
|
||||
public static function explode(string $tags, bool $tagme = true): array
|
||||
public static function explode(string $tags, bool $tagme=true): array
|
||||
{
|
||||
global $database;
|
||||
|
||||
@@ -149,7 +151,7 @@ class Tag
|
||||
$new = [];
|
||||
$i = 0;
|
||||
$tag_count = count($tag_array);
|
||||
while ($i < $tag_count) {
|
||||
while ($i<$tag_count) {
|
||||
$tag = $tag_array[$i];
|
||||
$negative = '';
|
||||
if (!empty($tag) && ($tag[0] == '-')) {
|
||||
@@ -163,7 +165,7 @@ class Tag
|
||||
FROM aliases
|
||||
WHERE LOWER(oldtag)=LOWER(:tag)
|
||||
",
|
||||
["tag" => $tag]
|
||||
["tag"=>$tag]
|
||||
);
|
||||
if (empty($newtags)) {
|
||||
//tag has no alias, use old tag
|
||||
@@ -197,13 +199,9 @@ class Tag
|
||||
public static function sanitize(string $tag): string
|
||||
{
|
||||
$tag = preg_replace("/\s/", "", $tag); # whitespace
|
||||
assert($tag !== null);
|
||||
$tag = preg_replace('/\x20[\x0e\x0f]/', '', $tag); # unicode RTL
|
||||
assert($tag !== null);
|
||||
$tag = preg_replace("/\.+/", ".", $tag); # strings of dots?
|
||||
assert($tag !== null);
|
||||
$tag = preg_replace("/^(\.+[\/\\\\])+/", "", $tag); # trailing slashes?
|
||||
assert($tag !== null);
|
||||
$tag = trim($tag, ", \t\n\r\0\x0B");
|
||||
|
||||
if ($tag == ".") {
|
||||
@@ -216,13 +214,9 @@ class Tag
|
||||
return $tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $tags1
|
||||
* @param string[] $tags2
|
||||
*/
|
||||
public static function compare(array $tags1, array $tags2): bool
|
||||
{
|
||||
if (count($tags1) !== count($tags2)) {
|
||||
if (count($tags1)!==count($tags2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -234,11 +228,6 @@ class Tag
|
||||
return $tags1 == $tags2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $source
|
||||
* @param string[] $remove
|
||||
* @return string[]
|
||||
*/
|
||||
public static function get_diff_tags(array $source, array $remove): array
|
||||
{
|
||||
$before = array_map('strtolower', $source);
|
||||
@@ -252,10 +241,6 @@ class Tag
|
||||
return $after;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $tags
|
||||
* @return string[]
|
||||
*/
|
||||
public static function sanitize_array(array $tags): array
|
||||
{
|
||||
global $page;
|
||||
@@ -287,4 +272,53 @@ class Tag
|
||||
// $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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Shimmie2;
|
||||
* and other such things that aren't ready yet
|
||||
*/
|
||||
|
||||
function install(): void
|
||||
function install()
|
||||
{
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
@@ -46,16 +46,11 @@ function install(): void
|
||||
if ($dsn) {
|
||||
do_install($dsn);
|
||||
} else {
|
||||
if (PHP_SAPI == 'cli') {
|
||||
print("INSTALL_DSN needs to be set for CLI installation\n");
|
||||
exit(1);
|
||||
} else {
|
||||
ask_questions();
|
||||
}
|
||||
ask_questions();
|
||||
}
|
||||
}
|
||||
|
||||
function get_dsn(): ?string
|
||||
function get_dsn()
|
||||
{
|
||||
if (getenv("INSTALL_DSN")) {
|
||||
$dsn = getenv("INSTALL_DSN");
|
||||
@@ -71,7 +66,7 @@ function get_dsn(): ?string
|
||||
return $dsn;
|
||||
}
|
||||
|
||||
function do_install(string $dsn): void
|
||||
function do_install($dsn)
|
||||
{
|
||||
try {
|
||||
create_dirs();
|
||||
@@ -82,7 +77,7 @@ function do_install(string $dsn): void
|
||||
}
|
||||
}
|
||||
|
||||
function ask_questions(): void
|
||||
function ask_questions()
|
||||
{
|
||||
$warnings = [];
|
||||
$errors = [];
|
||||
@@ -119,9 +114,9 @@ function ask_questions(): void
|
||||
";
|
||||
}
|
||||
|
||||
$db_s = in_array(DatabaseDriverID::SQLITE->value, $drivers) ? '<option value="'. DatabaseDriverID::SQLITE->value .'">SQLite</option>' : "";
|
||||
$db_m = in_array(DatabaseDriverID::MYSQL->value, $drivers) ? '<option value="'. DatabaseDriverID::MYSQL->value .'">MySQL</option>' : "";
|
||||
$db_p = in_array(DatabaseDriverID::PGSQL->value, $drivers) ? '<option value="'. DatabaseDriverID::PGSQL->value .'">PostgreSQL</option>' : "";
|
||||
$db_s = in_array(DatabaseDriverID::SQLITE->value, $drivers) ? '<option value="'. DatabaseDriverID::SQLITE->value .'">SQLite</option>' : "";
|
||||
|
||||
$warn_msg = $warnings ? "<h3>Warnings</h3>".implode("\n<p>", $warnings) : "";
|
||||
$err_msg = $errors ? "<h3>Errors</h3>".implode("\n<p>", $errors) : "";
|
||||
@@ -137,9 +132,9 @@ function ask_questions(): void
|
||||
<tr>
|
||||
<th>Type:</th>
|
||||
<td><select name="database_type" id="database_type" onchange="update_qs();">
|
||||
$db_s
|
||||
$db_m
|
||||
$db_m
|
||||
$db_p
|
||||
$db_s
|
||||
</select></td>
|
||||
</tr>
|
||||
<tr class="dbconf mysql pgsql">
|
||||
@@ -166,9 +161,13 @@ function ask_questions(): void
|
||||
return document.querySelectorAll(n);
|
||||
}
|
||||
function update_qs() {
|
||||
q('.dbconf').forEach(el => el.style.display = 'none');
|
||||
Array.prototype.forEach.call(q('.dbconf'), function(el, i){
|
||||
el.style.display = 'none';
|
||||
});
|
||||
let seldb = q("#database_type")[0].value || "none";
|
||||
q('.'+seldb).forEach(el => el.style.display = null);
|
||||
Array.prototype.forEach.call(q('.'+seldb), function(el, i){
|
||||
el.style.display = null;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</form>
|
||||
@@ -179,9 +178,8 @@ function ask_questions(): void
|
||||
The username provided must have access to create tables within the database.
|
||||
</p>
|
||||
<p class="dbconf sqlite">
|
||||
SQLite with default settings is fine for tens of users with thousands
|
||||
of images. For thousands of users or millions of images, postgres is
|
||||
recommended.
|
||||
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;
|
||||
@@ -192,7 +190,7 @@ EOD
|
||||
}
|
||||
|
||||
|
||||
function create_dirs(): void
|
||||
function create_dirs()
|
||||
{
|
||||
$data_exists = file_exists("data") || mkdir("data");
|
||||
$data_writable = $data_exists && (is_writable("data") || chmod("data", 0755));
|
||||
@@ -209,7 +207,7 @@ function create_dirs(): void
|
||||
}
|
||||
}
|
||||
|
||||
function create_tables(Database $db): void
|
||||
function create_tables(Database $db)
|
||||
{
|
||||
try {
|
||||
if ($db->count_tables() > 0) {
|
||||
@@ -294,8 +292,6 @@ function create_tables(Database $db): void
|
||||
if ($db->is_transaction_open()) {
|
||||
$db->commit();
|
||||
}
|
||||
// Ensure that we end this code in a transaction (for testing)
|
||||
$db->begin_transaction();
|
||||
} catch (\PDOException $e) {
|
||||
throw new InstallerException(
|
||||
"PDO Error:",
|
||||
@@ -308,7 +304,7 @@ function create_tables(Database $db): void
|
||||
}
|
||||
}
|
||||
|
||||
function write_config(string $dsn): void
|
||||
function write_config($dsn)
|
||||
{
|
||||
$file_content = "<" . "?php\ndefine('DATABASE_DSN', '$dsn');\n";
|
||||
|
||||
@@ -317,16 +313,11 @@ function write_config(string $dsn): void
|
||||
}
|
||||
|
||||
if (file_put_contents("data/config/shimmie.conf.php", $file_content, LOCK_EX)) {
|
||||
if (PHP_SAPI == 'cli') {
|
||||
print("Installation Successful\n");
|
||||
exit(0);
|
||||
} else {
|
||||
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>."
|
||||
);
|
||||
}
|
||||
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(
|
||||
|
||||
@@ -16,12 +16,12 @@ 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",
|
||||
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",
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -31,7 +31,7 @@ const LOGGING_LEVEL_NAMES = [
|
||||
* 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): void
|
||||
function log_msg(string $section, int $priority, string $message, ?string $flash=null)
|
||||
{
|
||||
global $page;
|
||||
send_event(new LogEvent($section, $priority, $message));
|
||||
@@ -47,23 +47,23 @@ function log_msg(string $section, int $priority, string $message, ?string $flash
|
||||
}
|
||||
|
||||
// More shorthand ways of logging
|
||||
function log_debug(string $section, string $message, ?string $flash = null): void
|
||||
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): void
|
||||
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): void
|
||||
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): void
|
||||
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): void
|
||||
function log_critical(string $section, string $message, ?string $flash=null)
|
||||
{
|
||||
log_msg($section, SCORE_LOG_CRITICAL, $message, $flash);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Shimmie2;
|
||||
|
||||
use MicroHTML\HTMLElement;
|
||||
|
||||
use function MicroHTML\{emptyHTML};
|
||||
use function MicroHTML\emptyHTML;
|
||||
use function MicroHTML\A;
|
||||
use function MicroHTML\FORM;
|
||||
use function MicroHTML\INPUT;
|
||||
@@ -15,16 +15,20 @@ use function MicroHTML\OPTION;
|
||||
use function MicroHTML\PRE;
|
||||
use function MicroHTML\P;
|
||||
use function MicroHTML\SELECT;
|
||||
use function MicroHTML\SPAN;
|
||||
use function MicroHTML\{TABLE,THEAD,TFOOT,TR,TH,TD};
|
||||
use function MicroHTML\TABLE;
|
||||
use function MicroHTML\THEAD;
|
||||
use function MicroHTML\TFOOT;
|
||||
use function MicroHTML\TR;
|
||||
use function MicroHTML\TH;
|
||||
use function MicroHTML\TD;
|
||||
|
||||
function SHM_FORM(string $target, string $method = "POST", bool $multipart = false, string $form_id = "", string $onsubmit = "", string $name = ""): HTMLElement
|
||||
function SHM_FORM(string $target, string $method="POST", bool $multipart=false, string $form_id="", string $onsubmit="", string $name=""): HTMLElement
|
||||
{
|
||||
global $user;
|
||||
|
||||
$attrs = [
|
||||
"action" => make_link($target),
|
||||
"method" => $method
|
||||
"action"=>make_link($target),
|
||||
"method"=>$method
|
||||
];
|
||||
|
||||
if ($form_id) {
|
||||
@@ -41,35 +45,26 @@ function SHM_FORM(string $target, string $method = "POST", bool $multipart = fal
|
||||
}
|
||||
return FORM(
|
||||
$attrs,
|
||||
INPUT(["type" => "hidden", "name" => "q", "value" => $target]),
|
||||
INPUT(["type"=>"hidden", "name"=>"q", "value"=>$target]),
|
||||
$method == "GET" ? "" : $user->get_auth_microhtml()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string|HTMLElement|null> $children
|
||||
*/
|
||||
function SHM_SIMPLE_FORM(string $target, ...$children): HTMLElement
|
||||
function SHM_SIMPLE_FORM($target, ...$children): HTMLElement
|
||||
{
|
||||
$form = SHM_FORM($target);
|
||||
$form->appendChild(emptyHTML(...$children));
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $args
|
||||
*/
|
||||
function SHM_SUBMIT(string $text, array $args = []): HTMLElement
|
||||
function SHM_SUBMIT(string $text, array $args=[]): HTMLElement
|
||||
{
|
||||
$args["type"] = "submit";
|
||||
$args["value"] = $text;
|
||||
return INPUT($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $args
|
||||
*/
|
||||
function SHM_A(string $href, string|HTMLElement $text, string $id = "", string $class = "", array $args = []): HTMLElement
|
||||
function SHM_A(string $href, string|HTMLElement $text, string $id="", string $class="", array $args=[]): HTMLElement
|
||||
{
|
||||
$args["href"] = make_link($href);
|
||||
|
||||
@@ -86,24 +81,24 @@ function SHM_A(string $href, string|HTMLElement $text, string $id = "", string $
|
||||
function SHM_COMMAND_EXAMPLE(string $ex, string $desc): HTMLElement
|
||||
{
|
||||
return DIV(
|
||||
["class" => "command_example"],
|
||||
["class"=>"command_example"],
|
||||
PRE($ex),
|
||||
P($desc)
|
||||
);
|
||||
}
|
||||
|
||||
function SHM_USER_FORM(User $duser, string $target, string $title, HTMLElement $body, HTMLElement|string $foot): HTMLElement
|
||||
function SHM_USER_FORM(User $duser, string $target, string $title, $body, $foot): HTMLElement
|
||||
{
|
||||
if (is_string($foot)) {
|
||||
$foot = TFOOT(TR(TD(["colspan" => "2"], INPUT(["type" => "submit", "value" => $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]),
|
||||
INPUT(["type"=>'hidden', "name"=>'id', "value"=>$duser->id]),
|
||||
TABLE(
|
||||
["class" => "form"],
|
||||
THEAD(TR(TH(["colspan" => "2"], $title))),
|
||||
["class"=>"form"],
|
||||
THEAD(TR(TH(["colspan"=>"2"], $title))),
|
||||
$body,
|
||||
$foot
|
||||
)
|
||||
@@ -115,14 +110,14 @@ function SHM_USER_FORM(User $duser, string $target, string $title, HTMLElement $
|
||||
* Generates a <select> element and sets up the given options.
|
||||
*
|
||||
* @param string $name The name attribute of <select>.
|
||||
* @param array<string|int, string> $options An array of pairs of parameters for <option> tags. First one is value, second one is text. Example: ('optionA', 'Choose Option A').
|
||||
* @param array<string> $selected_options The values of options that should be pre-selected.
|
||||
* @param array $options An array of pairs of parameters for <option> tags. First one is value, second one is text. Example: ('optionA', 'Choose Option A').
|
||||
* @param array $selected_options The values of options that should be pre-selected.
|
||||
* @param bool $required Wether the <select> element is required.
|
||||
* @param bool $multiple Wether the <select> element is multiple-choice.
|
||||
* @param bool $empty_option Whether the first option should be an empty one.
|
||||
* @param array<string, mixed> $attrs Additional attributes dict for <select>. Example: ["id"=>"some_id", "class"=>"some_class"].
|
||||
* @param array $attrs Additional attributes dict for <select>. Example: ["id"=>"some_id", "class"=>"some_class"].
|
||||
*/
|
||||
function SHM_SELECT(string $name, array $options, array $selected_options = [], bool $required = false, bool $multiple = false, bool $empty_option = false, array $attrs = []): HTMLElement
|
||||
function SHM_SELECT(string $name, array $options, array $selected_options=[], bool $required=false, bool $multiple=false, bool $empty_option=false, array $attrs=[]): HTMLElement
|
||||
{
|
||||
if ($required) {
|
||||
$attrs["required"] = "";
|
||||
@@ -148,36 +143,11 @@ function SHM_SELECT(string $name, array $options, array $selected_options = [],
|
||||
return SELECT($attrs, ...$_options);
|
||||
}
|
||||
|
||||
function SHM_OPTION(string $value, string $text, bool $selected = false): HTMLElement
|
||||
function SHM_OPTION(string $value, string $text, bool $selected=false): HTMLElement
|
||||
{
|
||||
if ($selected) {
|
||||
return OPTION(["value" => $value, "selected" => ""], $text);
|
||||
return OPTION(["value"=>$value, "selected"=>""], $text);
|
||||
}
|
||||
|
||||
return OPTION(["value" => $value], $text);
|
||||
}
|
||||
|
||||
function SHM_POST_INFO(
|
||||
string $title,
|
||||
HTMLElement|string|null $view = null,
|
||||
HTMLElement|string|null $edit = null,
|
||||
string|null $link = null,
|
||||
): HTMLElement {
|
||||
if(!is_null($view) && !is_null($edit)) {
|
||||
$show = emptyHTML(
|
||||
SPAN(["class" => "view"], $view),
|
||||
SPAN(["class" => "edit"], $edit),
|
||||
);
|
||||
} elseif(!is_null($edit)) {
|
||||
$show = $edit;
|
||||
} elseif(!is_null($view)) {
|
||||
$show = $view;
|
||||
} else {
|
||||
$show = "???";
|
||||
}
|
||||
return TR(
|
||||
["data-row" => $title],
|
||||
TH(["width" => "50px"], $link ? A(["href" => $link], $title) : $title),
|
||||
TD($show)
|
||||
);
|
||||
return OPTION(["value"=>$value], $text);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,6 @@ namespace Shimmie2;
|
||||
|
||||
/**
|
||||
* Return the unique elements of an array, case insensitively
|
||||
*
|
||||
* @param array<string> $array
|
||||
* @return list<string>
|
||||
*/
|
||||
function array_iunique(array $array): array
|
||||
{
|
||||
@@ -39,11 +36,7 @@ function array_iunique(array $array): array
|
||||
*/
|
||||
function ip_in_range(string $IP, string $CIDR): bool
|
||||
{
|
||||
$parts = explode("/", $CIDR);
|
||||
if(count($parts) == 1) {
|
||||
$parts[1] = "32";
|
||||
}
|
||||
list($net, $mask) = $parts;
|
||||
list($net, $mask) = explode("/", $CIDR);
|
||||
|
||||
$ip_net = ip2long($net);
|
||||
$ip_mask = ~((1 << (32 - (int)$mask)) - 1);
|
||||
@@ -57,16 +50,42 @@ function ip_in_range(string $IP, string $CIDR): bool
|
||||
|
||||
/**
|
||||
* 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 $dir): void
|
||||
function deltree(string $f): void
|
||||
{
|
||||
$di = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::KEY_AS_PATHNAME);
|
||||
$ri = new \RecursiveIteratorIterator($di, \RecursiveIteratorIterator::CHILD_FIRST);
|
||||
/** @var \SplFileInfo $file */
|
||||
foreach ($ri as $filename => $file) {
|
||||
$file->isDir() ? rmdir($filename) : unlink($filename);
|
||||
//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);
|
||||
}
|
||||
}
|
||||
rmdir($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,13 +98,9 @@ function full_copy(string $source, string $target): void
|
||||
if (is_dir($source)) {
|
||||
@mkdir($target);
|
||||
|
||||
$d = dir_ex($source);
|
||||
$d = dir($source);
|
||||
|
||||
while (true) {
|
||||
$entry = $d->read();
|
||||
if ($entry === false) {
|
||||
break;
|
||||
}
|
||||
while (false !== ($entry = $d->read())) {
|
||||
if ($entry == '.' || $entry == '..') {
|
||||
continue;
|
||||
}
|
||||
@@ -105,10 +120,8 @@ function full_copy(string $source, string $target): void
|
||||
|
||||
/**
|
||||
* Return a list of all the regular files in a directory and subdirectories
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
function list_files(string $base, string $_sub_dir = ""): array
|
||||
function list_files(string $base, string $_sub_dir=""): array
|
||||
{
|
||||
assert(is_dir($base));
|
||||
|
||||
@@ -116,7 +129,7 @@ function list_files(string $base, string $_sub_dir = ""): array
|
||||
|
||||
$files = [];
|
||||
$dir = opendir("$base/$_sub_dir");
|
||||
if ($dir === false) {
|
||||
if ($dir===false) {
|
||||
throw new SCoreException("Unable to open directory $base/$_sub_dir");
|
||||
}
|
||||
try {
|
||||
@@ -159,16 +172,12 @@ function flush_output(): void
|
||||
function stream_file(string $file, int $start, int $end): void
|
||||
{
|
||||
$fp = fopen($file, 'r');
|
||||
if(!$fp) {
|
||||
throw new \Exception("Failed to open $file");
|
||||
}
|
||||
try {
|
||||
fseek($fp, $start);
|
||||
$buffer = 1024 * 1024;
|
||||
while (!feof($fp) && ($p = ftell($fp)) <= $end) {
|
||||
if ($p + $buffer > $end) {
|
||||
$buffer = $end - $p + 1;
|
||||
assert($buffer >= 0);
|
||||
}
|
||||
echo fread($fp, $buffer);
|
||||
flush_output();
|
||||
@@ -188,13 +197,13 @@ function stream_file(string $file, int $start, int $end): void
|
||||
# http://www.php.net/manual/en/function.http-parse-headers.php#112917
|
||||
if (!function_exists('http_parse_headers')) {
|
||||
/**
|
||||
* @return array<string, string|string[]>
|
||||
* #return string[]
|
||||
*/
|
||||
function http_parse_headers(string $raw_headers): array
|
||||
{
|
||||
$headers = [];
|
||||
$headers = []; // $headers = [];
|
||||
|
||||
foreach (explode("\n", $raw_headers) as $h) {
|
||||
foreach (explode("\n", $raw_headers) as $i => $h) {
|
||||
$h = explode(':', $h, 2);
|
||||
|
||||
if (isset($h[1])) {
|
||||
@@ -216,8 +225,6 @@ if (!function_exists('http_parse_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.
|
||||
*
|
||||
* @param array<string, mixed> $headers
|
||||
*/
|
||||
function find_header(array $headers, string $name): ?string
|
||||
{
|
||||
@@ -240,22 +247,20 @@ function find_header(array $headers, string $name): ?string
|
||||
if (!function_exists('mb_strlen')) {
|
||||
// TODO: we should warn the admin that they are missing multibyte support
|
||||
/** @noinspection PhpUnusedParameterInspection */
|
||||
function mb_strlen(string $str, string $encoding): int
|
||||
function mb_strlen($str, $encoding): int
|
||||
{
|
||||
return strlen($str);
|
||||
}
|
||||
function mb_internal_encoding(string $encoding): void
|
||||
function mb_internal_encoding($encoding): void
|
||||
{
|
||||
}
|
||||
function mb_strtolower(string $str): string
|
||||
function mb_strtolower($str): string
|
||||
{
|
||||
return strtolower($str);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string[]
|
||||
*/
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
function get_subclasses_of(string $parent): array
|
||||
{
|
||||
$result = [];
|
||||
@@ -270,8 +275,6 @@ function get_subclasses_of(string $parent): array
|
||||
|
||||
/**
|
||||
* Like glob, with support for matching very long patterns with braces.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
function zglob(string $pattern): array
|
||||
{
|
||||
@@ -293,6 +296,52 @@ function zglob(string $pattern): array
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(array $parsed_url): string
|
||||
{
|
||||
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
|
||||
$host = $parsed_url['host'] ?? '';
|
||||
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
|
||||
$user = $parsed_url['user'] ?? '';
|
||||
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
|
||||
$pass = ($user || $pass) ? "$pass@" : '';
|
||||
$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";
|
||||
}
|
||||
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Input / Output Sanitising *
|
||||
@@ -347,7 +396,7 @@ function url_escape(?string $input): string
|
||||
/**
|
||||
* Turn all manner of HTML / INI / JS / DB booleans into a PHP one
|
||||
*/
|
||||
function bool_escape(mixed $input): bool
|
||||
function bool_escape($input): bool
|
||||
{
|
||||
/*
|
||||
Sometimes, I don't like PHP -- this, is one of those times...
|
||||
@@ -390,7 +439,7 @@ function no_escape(string $input): string
|
||||
* 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
|
||||
function page_number(string $input, ?int $max=null): int
|
||||
{
|
||||
if (!is_numeric($input)) {
|
||||
$pageNumber = 0;
|
||||
@@ -401,23 +450,10 @@ function page_number(string $input, ?int $max = null): int
|
||||
} else {
|
||||
$pageNumber = $input - 1;
|
||||
}
|
||||
return (int)$pageNumber;
|
||||
return $pageNumber;
|
||||
}
|
||||
|
||||
function is_numberish(string $s): bool
|
||||
{
|
||||
return is_numeric($s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Because apparently phpstan thinks that if $i is an int, type(-$i) == int|float
|
||||
*/
|
||||
function negative_int(int $i): int
|
||||
{
|
||||
return -$i;
|
||||
}
|
||||
|
||||
function clamp(int $val, ?int $min = null, ?int $max = null): int
|
||||
function clamp(int $val, ?int $min=null, ?int $max=null): int
|
||||
{
|
||||
if (!is_null($min) && $val < $min) {
|
||||
$val = $min;
|
||||
@@ -435,7 +471,7 @@ function clamp(int $val, ?int $min = null, ?int $max = null): int
|
||||
* 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
|
||||
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) {
|
||||
@@ -492,17 +528,17 @@ function to_shorthand_int(int $int): string
|
||||
{
|
||||
assert($int >= 0);
|
||||
|
||||
return match (true) {
|
||||
$int >= pow(1024, 4) * 10 => sprintf("%.0fTB", $int / pow(1024, 4)),
|
||||
$int >= pow(1024, 4) => sprintf("%.1fTB", $int / pow(1024, 4)),
|
||||
$int >= pow(1024, 3) * 10 => sprintf("%.0fGB", $int / pow(1024, 3)),
|
||||
$int >= pow(1024, 3) => sprintf("%.1fGB", $int / pow(1024, 3)),
|
||||
$int >= pow(1024, 2) * 10 => sprintf("%.0fMB", $int / pow(1024, 2)),
|
||||
$int >= pow(1024, 2) => sprintf("%.1fMB", $int / pow(1024, 2)),
|
||||
$int >= pow(1024, 1) * 10 => sprintf("%.0fKB", $int / pow(1024, 1)),
|
||||
$int >= pow(1024, 1) => sprintf("%.1fKB", $int / pow(1024, 1)),
|
||||
default => (string)$int,
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
abstract class TIME_UNITS
|
||||
{
|
||||
@@ -513,12 +549,12 @@ abstract class TIME_UNITS
|
||||
public const DAYS = "d";
|
||||
public const YEARS = "y";
|
||||
public const CONVERSION = [
|
||||
self::MILLISECONDS => 1000,
|
||||
self::SECONDS => 60,
|
||||
self::MINUTES => 60,
|
||||
self::HOURS => 24,
|
||||
self::DAYS => 365,
|
||||
self::YEARS => PHP_INT_MAX
|
||||
self::MILLISECONDS=>1000,
|
||||
self::SECONDS=>60,
|
||||
self::MINUTES=>60,
|
||||
self::HOURS=>24,
|
||||
self::DAYS=>365,
|
||||
self::YEARS=>PHP_INT_MAX
|
||||
];
|
||||
}
|
||||
function format_milliseconds(int $input, string $min_unit = TIME_UNITS::SECONDS): string
|
||||
@@ -529,17 +565,17 @@ function format_milliseconds(int $input, string $min_unit = TIME_UNITS::SECONDS)
|
||||
|
||||
$found = false;
|
||||
|
||||
foreach (TIME_UNITS::CONVERSION as $unit => $conversion) {
|
||||
foreach (TIME_UNITS::CONVERSION as $unit=>$conversion) {
|
||||
$count = $remainder % $conversion;
|
||||
$remainder = floor($remainder / $conversion);
|
||||
|
||||
if ($found || $unit == $min_unit) {
|
||||
if ($found||$unit==$min_unit) {
|
||||
$found = true;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($count == 0 && $remainder < 1) {
|
||||
if ($count==0&&$remainder<1) {
|
||||
break;
|
||||
}
|
||||
$output = "$count".$unit." ".$output;
|
||||
@@ -560,7 +596,7 @@ function parse_to_milliseconds(string $input): int
|
||||
$output += $length;
|
||||
}
|
||||
} else {
|
||||
foreach (TIME_UNITS::CONVERSION as $unit => $conversion) {
|
||||
foreach (TIME_UNITS::CONVERSION as $unit=>$conversion) {
|
||||
if (preg_match('/([0-9]+)'.$unit.'/i', $input, $match)) {
|
||||
$length = $match[1];
|
||||
if (is_numeric($length)) {
|
||||
@@ -577,10 +613,10 @@ function parse_to_milliseconds(string $input): int
|
||||
/**
|
||||
* Turn a date into a time, a date, an "X minutes ago...", etc
|
||||
*/
|
||||
function autodate(string $date, bool $html = true): string
|
||||
function autodate(string $date, bool $html=true): string
|
||||
{
|
||||
$cpu = date('c', strtotime_ex($date));
|
||||
$hum = date('F j, Y; H:i', strtotime_ex($date));
|
||||
$cpu = date('c', strtotime($date));
|
||||
$hum = date('F j, Y; H:i', strtotime($date));
|
||||
return ($html ? "<time datetime='$cpu'>$hum</time>" : $hum);
|
||||
}
|
||||
|
||||
@@ -613,10 +649,6 @@ function isValidDate(string $date): bool
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $inputs
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function validate_input(array $inputs): array
|
||||
{
|
||||
$outputs = [];
|
||||
@@ -649,7 +681,6 @@ function validate_input(array $inputs): array
|
||||
}
|
||||
$outputs[$key] = $id;
|
||||
} elseif (in_array('user_name', $flags)) {
|
||||
// @phpstan-ignore-next-line - phpstan thinks $value can never be empty?
|
||||
if (strlen($value) < 1) {
|
||||
throw new InvalidInput("Username must be at least 1 character");
|
||||
} elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $value)) {
|
||||
@@ -660,7 +691,8 @@ function validate_input(array $inputs): array
|
||||
}
|
||||
$outputs[$key] = $value;
|
||||
} elseif (in_array('user_class', $flags)) {
|
||||
if (!array_key_exists($value, UserClass::$known_classes)) {
|
||||
global $_shm_user_classes;
|
||||
if (!array_key_exists($value, $_shm_user_classes)) {
|
||||
throw new InvalidInput("Invalid user class: ".html_escape($value));
|
||||
}
|
||||
$outputs[$key] = $value;
|
||||
@@ -677,7 +709,7 @@ function validate_input(array $inputs): array
|
||||
} 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_ex(trim($value)));
|
||||
$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);
|
||||
@@ -736,12 +768,6 @@ function join_path(string ...$paths): string
|
||||
|
||||
/**
|
||||
* Perform callback on each item returned by an iterator.
|
||||
*
|
||||
* @template T
|
||||
* @template U
|
||||
* @param callable(U):T $callback
|
||||
* @param \iterator<U> $iter
|
||||
* @return \Generator<T>
|
||||
*/
|
||||
function iterator_map(callable $callback, \iterator $iter): \Generator
|
||||
{
|
||||
@@ -752,26 +778,20 @@ function iterator_map(callable $callback, \iterator $iter): \Generator
|
||||
|
||||
/**
|
||||
* Perform callback on each item returned by an iterator and combine the result into an array.
|
||||
*
|
||||
* @template T
|
||||
* @template U
|
||||
* @param callable(U):T $callback
|
||||
* @param \iterator<U> $iter
|
||||
* @return array<T>
|
||||
*/
|
||||
function iterator_map_to_array(callable $callback, \iterator $iter): array
|
||||
{
|
||||
return iterator_to_array(iterator_map($callback, $iter));
|
||||
}
|
||||
|
||||
function stringer(mixed $s): string
|
||||
function stringer($s): string
|
||||
{
|
||||
if (is_array($s)) {
|
||||
if (isset($s[0])) {
|
||||
return "[" . implode(", ", array_map("Shimmie2\stringer", $s)) . "]";
|
||||
} else {
|
||||
$pairs = [];
|
||||
foreach ($s as $k => $v) {
|
||||
foreach ($s as $k=>$v) {
|
||||
$pairs[] = "\"$k\"=>" . stringer($v);
|
||||
}
|
||||
return "[" . implode(", ", $pairs) . "]";
|
||||
@@ -794,24 +814,3 @@ function stringer(mixed $s): string
|
||||
}
|
||||
return "<Unstringable>";
|
||||
}
|
||||
|
||||
/**
|
||||
* If a value is in the cache, return it; otherwise, call the callback
|
||||
* to generate it and store it in the cache.
|
||||
*
|
||||
* @template T
|
||||
* @param string $key
|
||||
* @param callable():T $callback
|
||||
* @param int|null $ttl
|
||||
* @return T
|
||||
*/
|
||||
function cache_get_or_set(string $key, callable $callback, ?int $ttl = null): mixed
|
||||
{
|
||||
global $cache;
|
||||
$value = $cache->get($key);
|
||||
if ($value === null) {
|
||||
$value = $callback();
|
||||
$cache->set($key, $value, $ttl);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Shimmie2;
|
||||
* be included right at the very start of index.php and tests/bootstrap.php
|
||||
*/
|
||||
|
||||
function die_nicely(string $title, string $body, int $code = 0): void
|
||||
function die_nicely($title, $body, $code=0)
|
||||
{
|
||||
print("<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
@@ -17,7 +17,6 @@ function die_nicely(string $title, string $body, int $code = 0): void
|
||||
<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\">
|
||||
<link rel=\"stylesheet\" href=\"ext/static_files/installer.css\" type=\"text/css\">
|
||||
</head>
|
||||
<body>
|
||||
<div id=\"installer\">
|
||||
|
||||
@@ -21,10 +21,7 @@ function _load_event_listeners(): void
|
||||
{
|
||||
global $_shm_event_listeners;
|
||||
|
||||
$ver = preg_replace("/[^a-zA-Z0-9\.]/", "_", VERSION);
|
||||
$key = md5(Extension::get_enabled_extensions_as_string());
|
||||
|
||||
$cache_path = data_path("cache/event_listeners/el.$ver.$key.php");
|
||||
$cache_path = data_path("cache/shm_event_listeners.php");
|
||||
if (SPEED_HAX && file_exists($cache_path)) {
|
||||
require_once($cache_path);
|
||||
} else {
|
||||
@@ -48,7 +45,7 @@ function _set_event_listeners(): void
|
||||
global $_shm_event_listeners;
|
||||
$_shm_event_listeners = [];
|
||||
|
||||
foreach (get_subclasses_of(Extension::class) as $class) {
|
||||
foreach (get_subclasses_of("Shimmie2\Extension") as $class) {
|
||||
/** @var Extension $extension */
|
||||
$extension = new $class();
|
||||
|
||||
@@ -75,16 +72,11 @@ function _namespaced_class_name(string $class): string
|
||||
return str_replace("Shimmie2\\", "", $class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump the event listeners to a file for faster loading.
|
||||
*
|
||||
* @param array<string, array<int, Extension>> $event_listeners
|
||||
*/
|
||||
function _dump_event_listeners(array $event_listeners, string $path): void
|
||||
{
|
||||
$p = "<"."?php\nnamespace Shimmie2;\n";
|
||||
|
||||
foreach (get_subclasses_of(Extension::class) as $class) {
|
||||
foreach (get_subclasses_of("Shimmie2\Extension") as $class) {
|
||||
$scn = _namespaced_class_name($class);
|
||||
$p .= "\$$scn = new $scn(); ";
|
||||
}
|
||||
@@ -108,7 +100,7 @@ global $_shm_event_count;
|
||||
$_shm_event_count = 0;
|
||||
$_shm_timeout = null;
|
||||
|
||||
function shm_set_timeout(?int $timeout = null): void
|
||||
function shm_set_timeout(?int $timeout=null): void
|
||||
{
|
||||
global $_shm_timeout;
|
||||
if ($timeout) {
|
||||
@@ -163,7 +155,7 @@ function send_event(Event $event): Event
|
||||
if ($tracer_enabled) {
|
||||
$_tracer->end();
|
||||
}
|
||||
if ($event->stop_processing === true) {
|
||||
if ($event->stop_processing===true) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param T|false $x
|
||||
* @return T
|
||||
*/
|
||||
function false_throws(mixed $x, ?callable $errorgen = null): mixed
|
||||
{
|
||||
if($x === false) {
|
||||
$msg = "Unexpected false";
|
||||
if($errorgen) {
|
||||
$msg = $errorgen();
|
||||
}
|
||||
throw new \Exception($msg);
|
||||
}
|
||||
return $x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param T|null $x
|
||||
* @return T
|
||||
*/
|
||||
function null_throws(mixed $x, ?callable $errorgen = null): mixed
|
||||
{
|
||||
if($x === null) {
|
||||
$msg = "Unexpected null";
|
||||
if($errorgen) {
|
||||
$msg = $errorgen();
|
||||
}
|
||||
throw new \Exception($msg);
|
||||
}
|
||||
return $x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int<1,max> $depth
|
||||
*/
|
||||
function json_encode_ex(mixed $value, int|null $flags = 0, int $depth = 512): string
|
||||
{
|
||||
return false_throws(json_encode($value, $flags, $depth), "json_last_error_msg");
|
||||
}
|
||||
|
||||
function strtotime_ex(string $time, int|null $now = null): int
|
||||
{
|
||||
return false_throws(strtotime($time, $now));
|
||||
}
|
||||
|
||||
function md5_file_ex(string $filename, bool|null $raw_output = false): string
|
||||
{
|
||||
return false_throws(md5_file($filename, $raw_output));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
function glob_ex(string $pattern, int|null $flags = 0): array
|
||||
{
|
||||
return false_throws(glob($pattern, $flags));
|
||||
}
|
||||
|
||||
function file_get_contents_ex(string $filename): string
|
||||
{
|
||||
return false_throws(file_get_contents($filename));
|
||||
}
|
||||
|
||||
function filesize_ex(string $filename): int
|
||||
{
|
||||
return false_throws(filesize($filename));
|
||||
}
|
||||
|
||||
function inet_ntop_ex(string $in_addr): string
|
||||
{
|
||||
return false_throws(inet_ntop($in_addr));
|
||||
}
|
||||
|
||||
function inet_pton_ex(string $ip_address): string
|
||||
{
|
||||
return false_throws(inet_pton($ip_address));
|
||||
}
|
||||
|
||||
function dir_ex(string $directory): \Directory
|
||||
{
|
||||
return false_throws(dir($directory));
|
||||
}
|
||||
|
||||
function exec_ex(string $command): string
|
||||
{
|
||||
return false_throws(exec($command));
|
||||
}
|
||||
|
||||
function filter_var_ex(mixed $variable, int $filter = FILTER_DEFAULT, mixed $options = null): mixed
|
||||
{
|
||||
return false_throws(filter_var($variable, $filter, $options));
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace Shimmie2;
|
||||
* 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.
|
||||
* data/config/shimmie.conf.php
|
||||
*
|
||||
* Do NOT change them in this file. These are the defaults only!
|
||||
*
|
||||
@@ -17,13 +17,13 @@ namespace Shimmie2;
|
||||
* define("SPEED_HAX", true);
|
||||
*/
|
||||
|
||||
function _d(string $name, mixed $value): void
|
||||
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
|
||||
@@ -31,10 +31,10 @@ _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", "v2.10.6"); // string shimmie version
|
||||
_d("VERSION", "2.10.0-alpha$_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
|
||||
_d("TRUSTED_PROXIES", []); // array trust "X-Real-IP" / "X-Forwarded-For" / "X-Forwarded-Proto" headers from these IP ranges
|
||||
_d("REVERSE_PROXY_X_HEADERS", false); // boolean get request IPs from "X-Real-IP" and protocol from "X-Forwarded-Proto" HTTP headers
|
||||
|
||||
@@ -1,282 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
if(class_exists("\\PHPUnit\\Framework\\TestCase")) {
|
||||
abstract class ShimmiePHPUnitTestCase extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
protected static string $anon_name = "anonymous";
|
||||
protected static string $admin_name = "demo";
|
||||
protected static string $user_name = "test";
|
||||
protected string $wipe_time = "test";
|
||||
|
||||
/**
|
||||
* Start a DB transaction for each test class
|
||||
*/
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
global $_tracer, $database;
|
||||
$_tracer->begin(get_called_class());
|
||||
$database->begin_transaction();
|
||||
parent::setUpBeforeClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a savepoint for each test
|
||||
*/
|
||||
public function setUp(): void
|
||||
{
|
||||
global $database, $_tracer;
|
||||
$_tracer->begin($this->name());
|
||||
$_tracer->begin("setUp");
|
||||
$class = str_replace("Test", "", get_class($this));
|
||||
try {
|
||||
if (!ExtensionInfo::get_for_extension_class($class)->is_supported()) {
|
||||
$this->markTestSkipped("$class not supported with this database");
|
||||
}
|
||||
} catch (ExtensionNotFound $e) {
|
||||
// ignore - this is a core test rather than an extension test
|
||||
}
|
||||
|
||||
// Set up a clean environment for each test
|
||||
$database->execute("SAVEPOINT test_start");
|
||||
self::log_out();
|
||||
foreach ($database->get_col("SELECT id FROM images") as $image_id) {
|
||||
send_event(new ImageDeletionEvent(Image::by_id((int)$image_id), true));
|
||||
}
|
||||
|
||||
$_tracer->end(); # setUp
|
||||
$_tracer->begin("test");
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
global $_tracer, $database;
|
||||
$database->execute("ROLLBACK TO test_start");
|
||||
$_tracer->end(); # test
|
||||
$_tracer->end(); # $this->getName()
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass(): void
|
||||
{
|
||||
parent::tearDownAfterClass();
|
||||
global $_tracer, $database;
|
||||
$database->rollback();
|
||||
$_tracer->end(); # get_called_class()
|
||||
$_tracer->clear();
|
||||
$_tracer->flush("data/test-trace.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $args
|
||||
* @return array<string, string|mixed[]>
|
||||
*/
|
||||
private static function check_args(array $args): array
|
||||
{
|
||||
if (!$args) {
|
||||
return [];
|
||||
}
|
||||
foreach ($args as $k => $v) {
|
||||
if (is_array($v)) {
|
||||
$args[$k] = $v;
|
||||
} else {
|
||||
$args[$k] = (string)$v;
|
||||
}
|
||||
}
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $get_args
|
||||
* @param array<string, mixed> $post_args
|
||||
*/
|
||||
protected static function request(
|
||||
string $method,
|
||||
string $page_name,
|
||||
array $get_args = [],
|
||||
array $post_args = []
|
||||
): Page {
|
||||
// use a fresh page
|
||||
global $page;
|
||||
$get_args = self::check_args($get_args);
|
||||
$post_args = self::check_args($post_args);
|
||||
|
||||
if (str_contains($page_name, "?")) {
|
||||
throw new \RuntimeException("Query string included in page name");
|
||||
}
|
||||
$_SERVER['REQUEST_URI'] = make_link($page_name, http_build_query($get_args));
|
||||
$_GET = $get_args;
|
||||
$_POST = $post_args;
|
||||
$page = new Page();
|
||||
send_event(new PageRequestEvent($method, $page_name));
|
||||
if ($page->mode == PageMode::REDIRECT) {
|
||||
$page->code = 302;
|
||||
}
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $args
|
||||
*/
|
||||
protected static function get_page(string $page_name, array $args = []): Page
|
||||
{
|
||||
return self::request("GET", $page_name, $args, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $args
|
||||
*/
|
||||
protected static function post_page(string $page_name, array $args = []): Page
|
||||
{
|
||||
return self::request("POST", $page_name, [], $args);
|
||||
}
|
||||
|
||||
// page things
|
||||
protected function assert_title(string $title): void
|
||||
{
|
||||
global $page;
|
||||
$this->assertStringContainsString($title, $page->title);
|
||||
}
|
||||
|
||||
protected function assert_title_matches(string $title): void
|
||||
{
|
||||
global $page;
|
||||
$this->assertStringMatchesFormat($title, $page->title);
|
||||
}
|
||||
|
||||
protected function assert_no_title(string $title): void
|
||||
{
|
||||
global $page;
|
||||
$this->assertStringNotContainsString($title, $page->title);
|
||||
}
|
||||
|
||||
protected function assert_response(int $code): void
|
||||
{
|
||||
global $page;
|
||||
$this->assertEquals($code, $page->code);
|
||||
}
|
||||
|
||||
protected function page_to_text(string $section = null): string
|
||||
{
|
||||
global $page;
|
||||
if ($page->mode == PageMode::PAGE) {
|
||||
$text = $page->title . "\n";
|
||||
foreach ($page->blocks as $block) {
|
||||
if (is_null($section) || $section == $block->section) {
|
||||
$text .= $block->header . "\n";
|
||||
$text .= $block->body . "\n\n";
|
||||
}
|
||||
}
|
||||
return $text;
|
||||
} elseif ($page->mode == PageMode::DATA) {
|
||||
return $page->data;
|
||||
} else {
|
||||
$this->fail("Page mode is {$page->mode->name} (only PAGE and DATA are supported)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the page contains the given text somewhere in the blocks
|
||||
*/
|
||||
protected function assert_text(string $text, string $section = null): void
|
||||
{
|
||||
$this->assertStringContainsString($text, $this->page_to_text($section));
|
||||
}
|
||||
|
||||
protected function assert_no_text(string $text, string $section = null): void
|
||||
{
|
||||
$this->assertStringNotContainsString($text, $this->page_to_text($section));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the page contains the given text somewhere in the binary data
|
||||
*/
|
||||
protected function assert_content(string $content): void
|
||||
{
|
||||
global $page;
|
||||
$this->assertStringContainsString($content, $page->data);
|
||||
}
|
||||
|
||||
protected function assert_no_content(string $content): void
|
||||
{
|
||||
global $page;
|
||||
$this->assertStringNotContainsString($content, $page->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $tags
|
||||
* @param int[] $results
|
||||
*/
|
||||
protected function assert_search_results(array $tags, array $results): void
|
||||
{
|
||||
$images = Search::find_images(0, null, $tags);
|
||||
$ids = [];
|
||||
foreach ($images as $image) {
|
||||
$ids[] = $image->id;
|
||||
}
|
||||
$this->assertEquals($results, $ids);
|
||||
}
|
||||
|
||||
protected function assertException(string $type, callable $function): \Exception|null
|
||||
{
|
||||
$exception = null;
|
||||
try {
|
||||
call_user_func($function);
|
||||
} catch (\Exception $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
|
||||
self::assertThat(
|
||||
$exception,
|
||||
new \PHPUnit\Framework\Constraint\Exception($type),
|
||||
"Expected exception of type $type, but got " . ($exception ? get_class($exception) : "none")
|
||||
);
|
||||
return $exception;
|
||||
}
|
||||
|
||||
// user things
|
||||
protected static function log_in_as_admin(): void
|
||||
{
|
||||
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
|
||||
}
|
||||
|
||||
protected static function log_in_as_user(): void
|
||||
{
|
||||
send_event(new UserLoginEvent(User::by_name(self::$user_name)));
|
||||
}
|
||||
|
||||
protected static function log_out(): void
|
||||
{
|
||||
global $config;
|
||||
send_event(new UserLoginEvent(User::by_id($config->get_int("anon_id", 0))));
|
||||
}
|
||||
|
||||
// post things
|
||||
protected function post_image(string $filename, string $tags): int
|
||||
{
|
||||
$dae = send_event(new DataUploadEvent($filename, [
|
||||
"filename" => $filename,
|
||||
"tags" => Tag::explode($tags),
|
||||
"source" => null,
|
||||
]));
|
||||
if(count($dae->images) == 0) {
|
||||
throw new \Exception("Upload failed :(");
|
||||
}
|
||||
return $dae->images[0]->id;
|
||||
}
|
||||
|
||||
protected function delete_image(int $image_id): void
|
||||
{
|
||||
$img = Image::by_id($image_id);
|
||||
if ($img) {
|
||||
send_event(new ImageDeletionEvent($img, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
abstract class ShimmiePHPUnitTestCase
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
require_once "core/imageboard/image.php";
|
||||
|
||||
class ImageTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testLoadData(): void
|
||||
{
|
||||
$this->log_in_as_user();
|
||||
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "question? colon:thing exclamation!");
|
||||
$image = Image::by_id($image_id_1);
|
||||
$this->assertNull($image->source);
|
||||
}
|
||||
}
|
||||
@@ -1,524 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Depends;
|
||||
use PHPUnit\Framework\Constraint\IsEqual;
|
||||
|
||||
require_once "core/imageboard/search.php";
|
||||
|
||||
class SearchTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testWeirdTags(): void
|
||||
{
|
||||
$this->log_in_as_user();
|
||||
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "question? colon:thing exclamation!");
|
||||
$image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "question. colon_thing exclamation%");
|
||||
|
||||
$this->assert_search_results(["question?"], [$image_id_1]);
|
||||
$this->assert_search_results(["question."], [$image_id_2]);
|
||||
$this->assert_search_results(["colon:thing"], [$image_id_1]);
|
||||
$this->assert_search_results(["colon_thing"], [$image_id_2]);
|
||||
$this->assert_search_results(["exclamation!"], [$image_id_1]);
|
||||
$this->assert_search_results(["exclamation%"], [$image_id_2]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function testUpload(): array
|
||||
{
|
||||
$this->log_in_as_user();
|
||||
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "thing computer screenshot pbx phone");
|
||||
$image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "thing computer computing bedroom workshop");
|
||||
$this->log_out();
|
||||
|
||||
# make sure both uploads were ok
|
||||
$this->assertTrue($image_id_1 > 0);
|
||||
$this->assertTrue($image_id_2 > 0);
|
||||
|
||||
return [$image_id_1, $image_id_2];
|
||||
}
|
||||
|
||||
|
||||
/** ******************************************************
|
||||
* Test turning a string into an abstract query
|
||||
*
|
||||
* @param string $tags
|
||||
* @param TagCondition[] $expected_tag_conditions
|
||||
* @param ImgCondition[] $expected_img_conditions
|
||||
* @param string $expected_order
|
||||
*/
|
||||
private function assert_TTC(
|
||||
string $tags,
|
||||
array $expected_tag_conditions,
|
||||
array $expected_img_conditions,
|
||||
string $expected_order,
|
||||
): void {
|
||||
$class = new \ReflectionClass(Search::class);
|
||||
$terms_to_conditions = $class->getMethod("terms_to_conditions");
|
||||
$terms_to_conditions->setAccessible(true); // Use this if you are running PHP older than 8.1.0
|
||||
|
||||
$obj = new Search();
|
||||
[$tag_conditions, $img_conditions, $order] = $terms_to_conditions->invokeArgs($obj, [Tag::explode($tags, false)]);
|
||||
|
||||
static::assertThat(
|
||||
[
|
||||
"tags" => $expected_tag_conditions,
|
||||
"imgs" => $expected_img_conditions,
|
||||
"order" => $expected_order,
|
||||
],
|
||||
new IsEqual([
|
||||
"tags" => $tag_conditions,
|
||||
"imgs" => $img_conditions,
|
||||
"order" => $order,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
public function testTTC_Empty(): void
|
||||
{
|
||||
$this->assert_TTC(
|
||||
"",
|
||||
[
|
||||
],
|
||||
[
|
||||
new ImgCondition(new Querylet("trash != :true", ["true" => true])),
|
||||
new ImgCondition(new Querylet("private != :true OR owner_id = :private_owner_id", [
|
||||
"private_owner_id" => 1,
|
||||
"true" => true])),
|
||||
new ImgCondition(new Querylet("rating IN ('?', 's', 'q', 'e')", [])),
|
||||
],
|
||||
"images.id DESC"
|
||||
);
|
||||
}
|
||||
|
||||
public function testTTC_Hash(): void
|
||||
{
|
||||
$this->assert_TTC(
|
||||
"hash=1234567890",
|
||||
[
|
||||
],
|
||||
[
|
||||
new ImgCondition(new Querylet("trash != :true", ["true" => true])),
|
||||
new ImgCondition(new Querylet("private != :true OR owner_id = :private_owner_id", [
|
||||
"private_owner_id" => 1,
|
||||
"true" => true])),
|
||||
new ImgCondition(new Querylet("rating IN ('?', 's', 'q', 'e')", [])),
|
||||
new ImgCondition(new Querylet("images.hash = :hash", ["hash" => "1234567890"])),
|
||||
],
|
||||
"images.id DESC"
|
||||
);
|
||||
}
|
||||
|
||||
public function testTTC_Ratio(): void
|
||||
{
|
||||
$this->assert_TTC(
|
||||
"ratio=42:12345",
|
||||
[
|
||||
],
|
||||
[
|
||||
new ImgCondition(new Querylet("trash != :true", ["true" => true])),
|
||||
new ImgCondition(new Querylet("private != :true OR owner_id = :private_owner_id", [
|
||||
"private_owner_id" => 1,
|
||||
"true" => true])),
|
||||
new ImgCondition(new Querylet("rating IN ('?', 's', 'q', 'e')", [])),
|
||||
new ImgCondition(new Querylet("width / :width1 = height / :height1", ['width1' => 42,
|
||||
'height1' => 12345])),
|
||||
],
|
||||
"images.id DESC"
|
||||
);
|
||||
}
|
||||
|
||||
public function testTTC_Order(): void
|
||||
{
|
||||
$this->assert_TTC(
|
||||
"order=score",
|
||||
[
|
||||
],
|
||||
[
|
||||
new ImgCondition(new Querylet("trash != :true", ["true" => true])),
|
||||
new ImgCondition(new Querylet("private != :true OR owner_id = :private_owner_id", [
|
||||
"private_owner_id" => 1,
|
||||
"true" => true])),
|
||||
new ImgCondition(new Querylet("rating IN ('?', 's', 'q', 'e')", [])),
|
||||
],
|
||||
"images.numeric_score DESC"
|
||||
);
|
||||
}
|
||||
|
||||
/** ******************************************************
|
||||
* Test turning an abstract query into SQL + fetching the results
|
||||
*
|
||||
* @param string[] $tcs
|
||||
* @param string[] $ics
|
||||
* @param string $order
|
||||
* @param int $limit
|
||||
* @param int $start
|
||||
* @param int[] $res
|
||||
* @param string[] $path
|
||||
*/
|
||||
private function assert_BSQ(
|
||||
array $tcs = [],
|
||||
array $ics = [],
|
||||
string $order = "id DESC",
|
||||
int $limit = 9999,
|
||||
int $start = 0,
|
||||
array $res = [],
|
||||
array $path = null,
|
||||
): void {
|
||||
global $database;
|
||||
|
||||
$tcs = array_map(
|
||||
fn ($tag) => ($tag[0] == "-") ?
|
||||
new TagCondition(substr($tag, 1), false) :
|
||||
new TagCondition($tag),
|
||||
$tcs
|
||||
);
|
||||
|
||||
$ics = array_map(
|
||||
fn ($ic) => send_event(new SearchTermParseEvent(0, $ic, []))->img_conditions,
|
||||
$ics
|
||||
);
|
||||
$ics = array_merge(...$ics);
|
||||
|
||||
Search::$_search_path = [];
|
||||
|
||||
$class = new \ReflectionClass(Search::class);
|
||||
$build_search_querylet = $class->getMethod("build_search_querylet");
|
||||
$build_search_querylet->setAccessible(true); // Use this if you are running PHP older than 8.1.0
|
||||
|
||||
$obj = new Search();
|
||||
$querylet = $build_search_querylet->invokeArgs($obj, [$tcs, $ics, $order, $limit, $start]);
|
||||
|
||||
$results = $database->get_all($querylet->sql, $querylet->variables);
|
||||
|
||||
static::assertThat(
|
||||
[
|
||||
"res" => array_map(fn ($row) => $row['id'], $results),
|
||||
"path" => Search::$_search_path,
|
||||
],
|
||||
new IsEqual([
|
||||
"res" => $res,
|
||||
"path" => $path ?? Search::$_search_path,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/* * * * * * * * * * *
|
||||
* No-tag search *
|
||||
* * * * * * * * * * */
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_NoTags(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: [],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["no_tags"],
|
||||
);
|
||||
}
|
||||
|
||||
/* * * * * * * * * * *
|
||||
* Fast-path search *
|
||||
* * * * * * * * * * */
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_FastPath_NoResults(): void
|
||||
{
|
||||
$this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["maumaumau"],
|
||||
res: [],
|
||||
path: ["fast", "invalid_tag"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_FastPath_OneResult(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["pbx"],
|
||||
res: [$image_ids[0]],
|
||||
path: ["fast"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_FastPath_ManyResults(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["computer"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["fast"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_FastPath_WildNoResults(): void
|
||||
{
|
||||
$this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["asdfasdf*"],
|
||||
res: [],
|
||||
path: ["fast", "invalid_tag"],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only the first image matches both the wildcard and the tag.
|
||||
* This checks for a bug where searching for "a* b" would return
|
||||
* an image tagged "a1 a2" because the number of matched tags
|
||||
* was equal to the number of searched tags.
|
||||
*
|
||||
* https://github.com/shish/shimmie2/issues/547
|
||||
*/
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_FastPath_WildOneResult(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["screen*"],
|
||||
res: [$image_ids[0]],
|
||||
path: ["fast"],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the fast path doesn't return duplicate results
|
||||
* when a wildcard matches one image multiple times.
|
||||
*/
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_FastPath_WildManyResults(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
// two images match comp* - one matches it once, one matches it twice
|
||||
$this->assert_BSQ(
|
||||
tcs: ["comp*"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["fast"],
|
||||
);
|
||||
}
|
||||
|
||||
/* * * * * * * * * * *
|
||||
* General search *
|
||||
* * * * * * * * * * */
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_NoResults(): void
|
||||
{
|
||||
$this->testUpload();
|
||||
# multiple tags, one of which doesn't exist
|
||||
# (test the "one tag doesn't exist = no hits" path)
|
||||
$this->assert_BSQ(
|
||||
tcs: ["computer", "not_a_tag"],
|
||||
res: [],
|
||||
path: ["general", "invalid_tag"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_OneResult(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["computer", "screenshot"],
|
||||
res: [$image_ids[0]],
|
||||
path: ["general", "some_positives"],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only the first image matches both the wildcard and the tag.
|
||||
* This checks for a bug where searching for "a* b" would return
|
||||
* an image tagged "a1 a2" because the number of matched tags
|
||||
* was equal to the number of searched tags.
|
||||
*
|
||||
* https://github.com/shish/shimmie2/issues/547
|
||||
*/
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_WildOneResult(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["comp*", "screenshot"],
|
||||
res: [$image_ids[0]],
|
||||
path: ["general", "some_positives"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_ManyResults(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["computer", "thing"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["general", "some_positives"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_WildManyResults(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["comp*", "-asdf"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["general", "some_positives"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_SubtractValidFromResults(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["computer", "-pbx"],
|
||||
res: [$image_ids[1]],
|
||||
path: ["general", "some_positives"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_SubtractNotValidFromResults(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["computer", "-not_a_tag"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["general", "some_positives"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_SubtractValidFromDefault(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
// negative tag alone, should remove the image with that tag
|
||||
$this->assert_BSQ(
|
||||
tcs: ["-pbx"],
|
||||
res: [$image_ids[1]],
|
||||
path: ["general", "only_negative_tags"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_SubtractNotValidFromDefault(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
// negative that doesn't exist, should return all results
|
||||
$this->assert_BSQ(
|
||||
tcs: ["-not_a_tag"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["general", "all_nonexistent_negatives"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_SubtractMultipleNotValidFromDefault(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
// multiple negative tags that don't exist, should return all results
|
||||
$this->assert_BSQ(
|
||||
tcs: ["-not_a_tag", "-also_not_a_tag"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["general", "all_nonexistent_negatives"],
|
||||
);
|
||||
}
|
||||
|
||||
/* * * * * * * * * * *
|
||||
* Meta Search *
|
||||
* * * * * * * * * * */
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_ImgCond_NoResults(): void
|
||||
{
|
||||
$this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
ics: ["hash=1234567890"],
|
||||
res: [],
|
||||
path: ["no_tags"],
|
||||
);
|
||||
$this->assert_BSQ(
|
||||
ics: ["ratio=42:12345"],
|
||||
res: [],
|
||||
path: ["no_tags"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_ImgCond_OneResult(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
ics: ["hash=feb01bab5698a11dd87416724c7a89e3"],
|
||||
res: [$image_ids[0]],
|
||||
path: ["no_tags"],
|
||||
);
|
||||
$this->assert_BSQ(
|
||||
ics: ["id={$image_ids[1]}"],
|
||||
res: [$image_ids[1]],
|
||||
path: ["no_tags"],
|
||||
);
|
||||
$this->assert_BSQ(
|
||||
ics: ["filename=screenshot"],
|
||||
res: [$image_ids[0]],
|
||||
path: ["no_tags"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_ImgCond_ManyResults(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
|
||||
$this->assert_BSQ(
|
||||
ics: ["size=640x480"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["no_tags"],
|
||||
);
|
||||
$this->assert_BSQ(
|
||||
ics: ["tags=5"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["no_tags"],
|
||||