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"],
|
||||
);
|
||||
$this->assert_BSQ(
|
||||
ics: ["ext=jpg"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["no_tags"],
|
||||
);
|
||||
}
|
||||
|
||||
/* * * * * * * * * * *
|
||||
* Mixed *
|
||||
* * * * * * * * * * */
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_TagCondWithImgCond(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
// multiple tags, many results
|
||||
$this->assert_BSQ(
|
||||
tcs: ["computer"],
|
||||
ics: ["size=640x480"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["general", "some_positives"],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* get_images
|
||||
*/
|
||||
#[Depends('testUpload')]
|
||||
public function test_get_images(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
|
||||
$res = Search::get_images($image_ids);
|
||||
$this->assertGreaterThan($res[0]->id, $res[1]->id);
|
||||
|
||||
$res = Search::get_images(array_reverse($image_ids));
|
||||
$this->assertLessThan($res[0]->id, $res[1]->id);
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
class StdLibExTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testJsonEncodeOk(): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
'{"a":1,"b":2,"c":3,"d":4,"e":5}',
|
||||
json_encode_ex(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5])
|
||||
);
|
||||
}
|
||||
|
||||
public function testJsonEncodeError(): void
|
||||
{
|
||||
$e = $this->assertException(\Exception::class, function () {
|
||||
json_encode_ex("\xB1\x31");
|
||||
});
|
||||
$this->assertEquals(
|
||||
"Malformed UTF-8 characters, possibly incorrectly encoded",
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once "core/imageboard/tag.php";
|
||||
|
||||
class TagTest extends TestCase
|
||||
{
|
||||
public function test_compare(): void
|
||||
{
|
||||
$this->assertFalse(Tag::compare(["foo"], ["bar"]));
|
||||
$this->assertFalse(Tag::compare(["foo"], ["foo", "bar"]));
|
||||
$this->assertTrue(Tag::compare([], []));
|
||||
$this->assertTrue(Tag::compare(["foo"], ["FoO"]));
|
||||
$this->assertTrue(Tag::compare(["foo", "bar"], ["bar", "FoO"]));
|
||||
}
|
||||
}
|
@ -1,273 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\Depends;
|
||||
|
||||
require_once "core/urls.php";
|
||||
|
||||
class UrlsTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* An integration test for
|
||||
* - search_link()
|
||||
* - make_link()
|
||||
* - _get_query()
|
||||
* - get_search_terms()
|
||||
*/
|
||||
#[Depends("test_search_link")]
|
||||
public function test_get_search_terms_from_search_link(): void
|
||||
{
|
||||
/**
|
||||
* @param array<string> $vars
|
||||
* @return array<string>
|
||||
*/
|
||||
$gst = function (array $terms): array {
|
||||
$pre = new PageRequestEvent("GET", _get_query(search_link($terms)));
|
||||
$pre->page_matches("post/list");
|
||||
return $pre->get_search_terms();
|
||||
};
|
||||
|
||||
global $config;
|
||||
foreach([true, false] as $nice_urls) {
|
||||
$config->set_bool('nice_urls', $nice_urls);
|
||||
|
||||
$this->assertEquals(
|
||||
["bar", "foo"],
|
||||
$gst(["foo", "bar"])
|
||||
);
|
||||
$this->assertEquals(
|
||||
["AC/DC"],
|
||||
$gst(["AC/DC"])
|
||||
);
|
||||
$this->assertEquals(
|
||||
["cat*", "rating=?"],
|
||||
$gst(["rating=?", "cat*"]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[Depends("test_get_base_href")]
|
||||
public function test_make_link(): void
|
||||
{
|
||||
global $config;
|
||||
foreach([true, false] as $nice_urls) {
|
||||
$config->set_bool('nice_urls', $nice_urls);
|
||||
|
||||
// basic
|
||||
$this->assertEquals(
|
||||
$nice_urls ? "/test/foo" : "/test/index.php?q=foo",
|
||||
make_link("foo")
|
||||
);
|
||||
|
||||
// remove leading slash from path
|
||||
$this->assertEquals(
|
||||
$nice_urls ? "/test/foo" : "/test/index.php?q=foo",
|
||||
make_link("/foo")
|
||||
);
|
||||
|
||||
// query
|
||||
$this->assertEquals(
|
||||
$nice_urls ? "/test/foo?a=1&b=2" : "/test/index.php?q=foo&a=1&b=2",
|
||||
make_link("foo", "a=1&b=2")
|
||||
);
|
||||
|
||||
// hash
|
||||
$this->assertEquals(
|
||||
$nice_urls ? "/test/foo#cake" : "/test/index.php?q=foo#cake",
|
||||
make_link("foo", null, "cake")
|
||||
);
|
||||
|
||||
// query + hash
|
||||
$this->assertEquals(
|
||||
$nice_urls ? "/test/foo?a=1&b=2#cake" : "/test/index.php?q=foo&a=1&b=2#cake",
|
||||
make_link("foo", "a=1&b=2", "cake")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[Depends("test_make_link")]
|
||||
public function test_search_link(): void
|
||||
{
|
||||
global $config;
|
||||
foreach([true, false] as $nice_urls) {
|
||||
$config->set_bool('nice_urls', $nice_urls);
|
||||
|
||||
$this->assertEquals(
|
||||
$nice_urls ? "/test/post/list/bar%20foo/1" : "/test/index.php?q=post/list/bar%20foo/1",
|
||||
search_link(["foo", "bar"])
|
||||
);
|
||||
$this->assertEquals(
|
||||
$nice_urls ? "/test/post/list/AC%2FDC/1" : "/test/index.php?q=post/list/AC%2FDC/1",
|
||||
search_link(["AC/DC"])
|
||||
);
|
||||
$this->assertEquals(
|
||||
$nice_urls ? "/test/post/list/cat%2A%20rating%3D%3F/1" : "/test/index.php?q=post/list/cat%2A%20rating%3D%3F/1",
|
||||
search_link(["rating=?", "cat*"])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[Depends("test_get_base_href")]
|
||||
public function test_get_query(): void
|
||||
{
|
||||
// just validating an assumption that this test relies upon
|
||||
$this->assertEquals(get_base_href(), "/test");
|
||||
|
||||
$this->assertEquals(
|
||||
"tasty/cake",
|
||||
_get_query("/test/tasty/cake"),
|
||||
'http://$SERVER/$INSTALL_DIR/$PATH should return $PATH'
|
||||
);
|
||||
$this->assertEquals(
|
||||
"tasty/cake",
|
||||
_get_query("/test/index.php?q=tasty/cake"),
|
||||
'http://$SERVER/$INSTALL_DIR/index.php?q=$PATH should return $PATH'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"tasty/cake%20pie",
|
||||
_get_query("/test/index.php?q=tasty/cake%20pie"),
|
||||
'URL encoded paths should be left alone'
|
||||
);
|
||||
$this->assertEquals(
|
||||
"tasty/cake%20pie",
|
||||
_get_query("/test/tasty/cake%20pie"),
|
||||
'URL encoded queries should be left alone'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"",
|
||||
_get_query("/test/"),
|
||||
'If just viewing install directory, should return /'
|
||||
);
|
||||
$this->assertEquals(
|
||||
"",
|
||||
_get_query("/test/index.php"),
|
||||
'If just viewing index.php, should return /'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"post/list/tasty%2Fcake/1",
|
||||
_get_query("/test/post/list/tasty%2Fcake/1"),
|
||||
'URL encoded niceurls should be left alone, even encoded slashes'
|
||||
);
|
||||
$this->assertEquals(
|
||||
"post/list/tasty%2Fcake/1",
|
||||
_get_query("/test/index.php?q=post/list/tasty%2Fcake/1"),
|
||||
'URL encoded uglyurls should be left alone, even encoded slashes'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_is_https_enabled(): void
|
||||
{
|
||||
$this->assertFalse(is_https_enabled(), "HTTPS should be disabled by default");
|
||||
|
||||
$_SERVER['HTTPS'] = "on";
|
||||
$this->assertTrue(is_https_enabled(), "HTTPS should be enabled when set to 'on'");
|
||||
unset($_SERVER['HTTPS']);
|
||||
}
|
||||
|
||||
public function test_get_base_href(): void
|
||||
{
|
||||
// PHP_SELF should point to "the currently executing script
|
||||
// relative to the document root"
|
||||
$this->assertEquals("", get_base_href(["PHP_SELF" => "/index.php"]));
|
||||
$this->assertEquals("/mydir", get_base_href(["PHP_SELF" => "/mydir/index.php"]));
|
||||
|
||||
// SCRIPT_FILENAME should point to "the absolute pathname of
|
||||
// the currently executing script" and DOCUMENT_ROOT should
|
||||
// point to "the document root directory under which the
|
||||
// current script is executing"
|
||||
$this->assertEquals("", get_base_href([
|
||||
"PHP_SELF" => "<invalid>",
|
||||
"SCRIPT_FILENAME" => "/var/www/html/index.php",
|
||||
"DOCUMENT_ROOT" => "/var/www/html",
|
||||
]), "root directory");
|
||||
$this->assertEquals("/mydir", get_base_href([
|
||||
"PHP_SELF" => "<invalid>",
|
||||
"SCRIPT_FILENAME" => "/var/www/html/mydir/index.php",
|
||||
"DOCUMENT_ROOT" => "/var/www/html",
|
||||
]), "subdirectory");
|
||||
$this->assertEquals("", get_base_href([
|
||||
"PHP_SELF" => "<invalid>",
|
||||
"SCRIPT_FILENAME" => "/var/www/html/index.php",
|
||||
"DOCUMENT_ROOT" => "/var/www/html/",
|
||||
]), "trailing slash in DOCUMENT_ROOT root should be ignored");
|
||||
$this->assertEquals("/mydir", get_base_href([
|
||||
"PHP_SELF" => "<invalid>",
|
||||
"SCRIPT_FILENAME" => "/var/www/html/mydir/index.php",
|
||||
"DOCUMENT_ROOT" => "/var/www/html/",
|
||||
]), "trailing slash in DOCUMENT_ROOT subdir should be ignored");
|
||||
}
|
||||
|
||||
#[Depends("test_is_https_enabled")]
|
||||
#[Depends("test_get_base_href")]
|
||||
public function test_make_http(): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
"http://cli-command/test/foo",
|
||||
make_http("foo"),
|
||||
"relative to shimmie root"
|
||||
);
|
||||
$this->assertEquals(
|
||||
"http://cli-command/foo",
|
||||
make_http("/foo"),
|
||||
"relative to web server"
|
||||
);
|
||||
$this->assertEquals(
|
||||
"https://foo.com",
|
||||
make_http("https://foo.com"),
|
||||
"absolute URL should be left alone"
|
||||
);
|
||||
}
|
||||
|
||||
public function test_modify_url(): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
"/foo/bar?a=3&b=2",
|
||||
modify_url("/foo/bar?a=1&b=2", ["a" => "3"])
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"https://blah.com/foo/bar?b=2",
|
||||
modify_url("https://blah.com/foo/bar?a=1&b=2", ["a" => null])
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"/foo/bar",
|
||||
modify_url("/foo/bar?a=1&b=2", ["a" => null, "b" => null])
|
||||
);
|
||||
}
|
||||
|
||||
public function test_referer_or(): void
|
||||
{
|
||||
unset($_SERVER['HTTP_REFERER']);
|
||||
$this->assertEquals(
|
||||
"foo",
|
||||
referer_or("foo")
|
||||
);
|
||||
|
||||
$_SERVER['HTTP_REFERER'] = "cake";
|
||||
$this->assertEquals(
|
||||
"cake",
|
||||
referer_or("foo")
|
||||
);
|
||||
|
||||
$_SERVER['HTTP_REFERER'] = "cake";
|
||||
$this->assertEquals(
|
||||
"foo",
|
||||
referer_or("foo", ["cake"])
|
||||
);
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
global $config;
|
||||
$config->set_bool('nice_urls', true);
|
||||
parent::tearDown();
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ require_once "core/basepage.php";
|
||||
|
||||
class BasePageTest extends TestCase
|
||||
{
|
||||
public function test_page(): void
|
||||
public function test_page()
|
||||
{
|
||||
$page = new BasePage();
|
||||
$page->set_mode(PageMode::PAGE);
|
||||
@ -20,7 +20,7 @@ class BasePageTest extends TestCase
|
||||
$this->assertTrue(true); // doesn't crash
|
||||
}
|
||||
|
||||
public function test_file(): void
|
||||
public function test_file()
|
||||
{
|
||||
$page = new BasePage();
|
||||
$page->set_mode(PageMode::FILE);
|
||||
@ -31,7 +31,7 @@ class BasePageTest extends TestCase
|
||||
$this->assertTrue(true); // doesn't crash
|
||||
}
|
||||
|
||||
public function test_data(): void
|
||||
public function test_data()
|
||||
{
|
||||
$page = new BasePage();
|
||||
$page->set_mode(PageMode::DATA);
|
||||
@ -42,7 +42,7 @@ class BasePageTest extends TestCase
|
||||
$this->assertTrue(true); // doesn't crash
|
||||
}
|
||||
|
||||
public function test_redirect(): void
|
||||
public function test_redirect()
|
||||
{
|
||||
$page = new BasePage();
|
||||
$page->set_mode(PageMode::REDIRECT);
|
@ -10,7 +10,7 @@ require_once "core/block.php";
|
||||
|
||||
class BlockTest extends TestCase
|
||||
{
|
||||
public function test_basic(): void
|
||||
public function test_basic()
|
||||
{
|
||||
$b = new Block("head", "body");
|
||||
$this->assertEquals(
|
@ -6,15 +6,15 @@ namespace Shimmie2;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class InitTest extends TestCase
|
||||
class TestInit extends TestCase
|
||||
{
|
||||
public function testInitExt(): void
|
||||
public function testInitExt()
|
||||
{
|
||||
send_event(new InitExtEvent());
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function testDatabaseUpgrade(): void
|
||||
public function testDatabaseUpgrade()
|
||||
{
|
||||
send_event(new DatabaseUpgradeEvent());
|
||||
$this->assertTrue(true);
|
@ -10,7 +10,7 @@ require_once "core/polyfills.php";
|
||||
|
||||
class PolyfillsTest extends TestCase
|
||||
{
|
||||
public function test_html_escape(): void
|
||||
public function test_html_escape()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"Foo & <main>",
|
||||
@ -26,7 +26,7 @@ class PolyfillsTest extends TestCase
|
||||
$this->assertEquals(html_escape(html_unescape($x)), $x);
|
||||
}
|
||||
|
||||
public function test_int_escape(): void
|
||||
public function test_int_escape()
|
||||
{
|
||||
$this->assertEquals(0, int_escape(""));
|
||||
$this->assertEquals(1, int_escape("1"));
|
||||
@ -35,13 +35,13 @@ class PolyfillsTest extends TestCase
|
||||
$this->assertEquals(0, int_escape(null));
|
||||
}
|
||||
|
||||
public function test_url_escape(): void
|
||||
public function test_url_escape()
|
||||
{
|
||||
$this->assertEquals("%5E%5Co%2F%5E", url_escape("^\o/^"));
|
||||
$this->assertEquals("", url_escape(null));
|
||||
}
|
||||
|
||||
public function test_bool_escape(): void
|
||||
public function test_bool_escape()
|
||||
{
|
||||
$this->assertTrue(bool_escape(true));
|
||||
$this->assertFalse(bool_escape(false));
|
||||
@ -71,7 +71,7 @@ class PolyfillsTest extends TestCase
|
||||
$this->assertFalse(bool_escape("0"));
|
||||
}
|
||||
|
||||
public function test_clamp(): void
|
||||
public function test_clamp()
|
||||
{
|
||||
$this->assertEquals(5, clamp(0, 5, 10)); // too small
|
||||
$this->assertEquals(5, clamp(5, 5, 10)); // lower limit
|
||||
@ -83,7 +83,7 @@ class PolyfillsTest extends TestCase
|
||||
$this->assertEquals(42, clamp(42, null, null)); // no limit
|
||||
}
|
||||
|
||||
public function test_truncate(): void
|
||||
public function test_truncate()
|
||||
{
|
||||
$this->assertEquals("test words", truncate("test words", 10));
|
||||
$this->assertEquals("test...", truncate("test...", 9));
|
||||
@ -91,16 +91,13 @@ class PolyfillsTest extends TestCase
|
||||
$this->assertEquals("te...", truncate("te...", 2));
|
||||
}
|
||||
|
||||
public function test_to_shorthand_int(): void
|
||||
public function test_to_shorthand_int()
|
||||
{
|
||||
// 0-9 should have 1 decimal place, 10+ should have none
|
||||
$this->assertEquals("1.1GB", to_shorthand_int(1231231231));
|
||||
$this->assertEquals("10KB", to_shorthand_int(10240));
|
||||
$this->assertEquals("9.2KB", to_shorthand_int(9440));
|
||||
$this->assertEquals("2", to_shorthand_int(2));
|
||||
}
|
||||
|
||||
public function test_parse_shorthand_int(): void
|
||||
public function test_parse_shorthand_int()
|
||||
{
|
||||
$this->assertEquals(-1, parse_shorthand_int("foo"));
|
||||
$this->assertEquals(33554432, parse_shorthand_int("32M"));
|
||||
@ -108,21 +105,21 @@ class PolyfillsTest extends TestCase
|
||||
$this->assertEquals(1231231231, parse_shorthand_int("1231231231"));
|
||||
}
|
||||
|
||||
public function test_format_milliseconds(): void
|
||||
public function test_format_milliseconds()
|
||||
{
|
||||
$this->assertEquals("", format_milliseconds(5));
|
||||
$this->assertEquals("5s", format_milliseconds(5000));
|
||||
$this->assertEquals("1y 213d 16h 53m 20s", format_milliseconds(50000000000));
|
||||
}
|
||||
|
||||
public function test_parse_to_milliseconds(): void
|
||||
public function test_parse_to_milliseconds()
|
||||
{
|
||||
$this->assertEquals(10, parse_to_milliseconds("10"));
|
||||
$this->assertEquals(5000, parse_to_milliseconds("5s"));
|
||||
$this->assertEquals(50000000000, parse_to_milliseconds("1y 213d 16h 53m 20s"));
|
||||
}
|
||||
|
||||
public function test_autodate(): void
|
||||
public function test_autodate()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<time datetime='2012-06-23T16:14:22+00:00'>June 23, 2012; 16:14</time>",
|
||||
@ -130,7 +127,7 @@ class PolyfillsTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function test_validate_input(): void
|
||||
public function test_validate_input()
|
||||
{
|
||||
$_POST = [
|
||||
"foo" => " bar ",
|
||||
@ -138,20 +135,20 @@ class PolyfillsTest extends TestCase
|
||||
"num" => "42",
|
||||
];
|
||||
$this->assertEquals(
|
||||
["foo" => "bar"],
|
||||
validate_input(["foo" => "string,trim,lower"])
|
||||
["foo"=>"bar"],
|
||||
validate_input(["foo"=>"string,trim,lower"])
|
||||
);
|
||||
//$this->assertEquals(
|
||||
// ["to_null"=>null],
|
||||
// validate_input(["to_null"=>"string,trim,nullify"])
|
||||
//);
|
||||
$this->assertEquals(
|
||||
["num" => 42],
|
||||
validate_input(["num" => "int"])
|
||||
["num"=>42],
|
||||
validate_input(["num"=>"int"])
|
||||
);
|
||||
}
|
||||
|
||||
public function test_sanitize_path(): void
|
||||
public function test_sanitize_path()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"one",
|
||||
@ -194,7 +191,7 @@ class PolyfillsTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function test_join_path(): void
|
||||
public function test_join_path()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"one",
|
||||
@ -222,36 +219,11 @@ class PolyfillsTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function test_stringer(): void
|
||||
public function test_stringer()
|
||||
{
|
||||
$this->assertEquals(
|
||||
'["foo"=>"bar", "baz"=>[1, 2, 3], "qux"=>["a"=>"b"]]',
|
||||
stringer(["foo" => "bar", "baz" => [1,2,3], "qux" => ["a" => "b"]])
|
||||
stringer(["foo"=>"bar", "baz"=>[1,2,3], "qux"=>["a"=>"b"]])
|
||||
);
|
||||
}
|
||||
|
||||
public function test_ip_in_range(): void
|
||||
{
|
||||
$this->assertTrue(ip_in_range("1.2.3.4", "1.2.0.0/16"));
|
||||
$this->assertFalse(ip_in_range("4.3.2.1", "1.2.0.0/16"));
|
||||
|
||||
// A single IP should be interpreted as a /32
|
||||
$this->assertTrue(ip_in_range("1.2.3.4", "1.2.3.4"));
|
||||
}
|
||||
|
||||
public function test_deltree(): void
|
||||
{
|
||||
$tmp = sys_get_temp_dir();
|
||||
$dir = "$tmp/test_deltree";
|
||||
mkdir($dir);
|
||||
file_put_contents("$dir/foo", "bar");
|
||||
mkdir("$dir/baz");
|
||||
file_put_contents("$dir/baz/.qux", "quux");
|
||||
$this->assertTrue(file_exists($dir));
|
||||
$this->assertTrue(file_exists("$dir/foo"));
|
||||
$this->assertTrue(file_exists("$dir/baz"));
|
||||
$this->assertTrue(file_exists("$dir/baz/.qux"));
|
||||
deltree($dir);
|
||||
$this->assertFalse(file_exists($dir));
|
||||
}
|
||||
}
|
35
core/tests/tag.test.php
Normal file
35
core/tests/tag.test.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once "core/imageboard/tag.php";
|
||||
|
||||
class TagTest extends TestCase
|
||||
{
|
||||
public function test_caret()
|
||||
{
|
||||
$this->assertEquals("foo", Tag::decaret("foo"));
|
||||
$this->assertEquals("foo?", Tag::decaret("foo^q"));
|
||||
$this->assertEquals("a^b/c\\d?e&f", Tag::decaret("a^^b^sc^bd^qe^af"));
|
||||
}
|
||||
|
||||
public function test_decaret()
|
||||
{
|
||||
$this->assertEquals("foo", Tag::caret("foo"));
|
||||
$this->assertEquals("foo^q", Tag::caret("foo?"));
|
||||
$this->assertEquals("a^^b^sc^bd^qe^af", Tag::caret("a^b/c\\d?e&f"));
|
||||
}
|
||||
|
||||
public function test_compare()
|
||||
{
|
||||
$this->assertFalse(Tag::compare(["foo"], ["bar"]));
|
||||
$this->assertFalse(Tag::compare(["foo"], ["foo", "bar"]));
|
||||
$this->assertTrue(Tag::compare([], []));
|
||||
$this->assertTrue(Tag::compare(["foo"], ["FoO"]));
|
||||
$this->assertTrue(Tag::compare(["foo", "bar"], ["bar", "FoO"]));
|
||||
}
|
||||
}
|
105
core/tests/urls.test.php
Normal file
105
core/tests/urls.test.php
Normal file
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once "core/urls.php";
|
||||
|
||||
class UrlsTest extends TestCase
|
||||
{
|
||||
public function test_make_link()
|
||||
{
|
||||
// basic
|
||||
$this->assertEquals(
|
||||
"/test/foo",
|
||||
make_link("foo")
|
||||
);
|
||||
|
||||
// remove leading slash from path
|
||||
$this->assertEquals(
|
||||
"/test/foo",
|
||||
make_link("/foo")
|
||||
);
|
||||
|
||||
// query
|
||||
$this->assertEquals(
|
||||
"/test/foo?a=1&b=2",
|
||||
make_link("foo", "a=1&b=2")
|
||||
);
|
||||
|
||||
// hash
|
||||
$this->assertEquals(
|
||||
"/test/foo#cake",
|
||||
make_link("foo", null, "cake")
|
||||
);
|
||||
|
||||
// query + hash
|
||||
$this->assertEquals(
|
||||
"/test/foo?a=1&b=2#cake",
|
||||
make_link("foo", "a=1&b=2", "cake")
|
||||
);
|
||||
}
|
||||
|
||||
public function test_make_http()
|
||||
{
|
||||
// relative to shimmie install
|
||||
$this->assertEquals(
|
||||
"http://cli-command/test/foo",
|
||||
make_http("foo")
|
||||
);
|
||||
|
||||
// relative to web server
|
||||
$this->assertEquals(
|
||||
"http://cli-command/foo",
|
||||
make_http("/foo")
|
||||
);
|
||||
|
||||
// absolute
|
||||
$this->assertEquals(
|
||||
"https://foo.com",
|
||||
make_http("https://foo.com")
|
||||
);
|
||||
}
|
||||
|
||||
public function test_modify_url()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"/foo/bar?a=3&b=2",
|
||||
modify_url("/foo/bar?a=1&b=2", ["a"=>"3"])
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"https://blah.com/foo/bar?b=2",
|
||||
modify_url("https://blah.com/foo/bar?a=1&b=2", ["a"=>null])
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"/foo/bar",
|
||||
modify_url("/foo/bar?a=1&b=2", ["a"=>null, "b"=>null])
|
||||
);
|
||||
}
|
||||
|
||||
public function test_referer_or()
|
||||
{
|
||||
unset($_SERVER['HTTP_REFERER']);
|
||||
$this->assertEquals(
|
||||
"foo",
|
||||
referer_or("foo")
|
||||
);
|
||||
|
||||
$_SERVER['HTTP_REFERER'] = "cake";
|
||||
$this->assertEquals(
|
||||
"cake",
|
||||
referer_or("foo")
|
||||
);
|
||||
|
||||
$_SERVER['HTTP_REFERER'] = "cake";
|
||||
$this->assertEquals(
|
||||
"foo",
|
||||
referer_or("foo", ["cake"])
|
||||
);
|
||||
}
|
||||
}
|
@ -10,42 +10,42 @@ require_once "core/util.php";
|
||||
|
||||
class UtilTest extends TestCase
|
||||
{
|
||||
public function test_get_theme(): void
|
||||
public function test_get_theme()
|
||||
{
|
||||
$this->assertEquals("default", get_theme());
|
||||
}
|
||||
|
||||
public function test_get_memory_limit(): void
|
||||
public function test_get_memory_limit()
|
||||
{
|
||||
get_memory_limit();
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function test_check_gd_version(): void
|
||||
public function test_check_gd_version()
|
||||
{
|
||||
check_gd_version();
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function test_check_im_version(): void
|
||||
public function test_check_im_version()
|
||||
{
|
||||
check_im_version();
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function test_human_filesize(): void
|
||||
public function test_human_filesize()
|
||||
{
|
||||
$this->assertEquals("123.00B", human_filesize(123));
|
||||
$this->assertEquals("123B", human_filesize(123, 0));
|
||||
$this->assertEquals("120.56KB", human_filesize(123456));
|
||||
}
|
||||
|
||||
public function test_generate_key(): void
|
||||
public function test_generate_key()
|
||||
{
|
||||
$this->assertEquals(20, strlen(generate_key()));
|
||||
}
|
||||
|
||||
public function test_warehouse_path(): void
|
||||
public function test_warehouse_path()
|
||||
{
|
||||
$hash = "7ac19c10d6859415";
|
||||
|
||||
@ -105,7 +105,7 @@ class UtilTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function test_load_balance_url(): void
|
||||
public function test_load_balance_url()
|
||||
{
|
||||
$hash = "7ac19c10d6859415";
|
||||
$ext = "jpg";
|
||||
@ -123,42 +123,42 @@ class UtilTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function test_path_to_tags(): void
|
||||
public function test_path_to_tags()
|
||||
{
|
||||
$this->assertEquals(
|
||||
[],
|
||||
"",
|
||||
path_to_tags("nope.jpg")
|
||||
);
|
||||
$this->assertEquals(
|
||||
[],
|
||||
"",
|
||||
path_to_tags("\\")
|
||||
);
|
||||
$this->assertEquals(
|
||||
[],
|
||||
"",
|
||||
path_to_tags("/")
|
||||
);
|
||||
$this->assertEquals(
|
||||
[],
|
||||
"",
|
||||
path_to_tags("C:\\")
|
||||
);
|
||||
$this->assertEquals(
|
||||
["test", "tag"],
|
||||
"test tag",
|
||||
path_to_tags("123 - test tag.jpg")
|
||||
);
|
||||
$this->assertEquals(
|
||||
["foo", "bar"],
|
||||
"foo bar",
|
||||
path_to_tags("/foo/bar/baz.jpg")
|
||||
);
|
||||
$this->assertEquals(
|
||||
["cake", "pie", "foo", "bar"],
|
||||
"cake pie foo bar",
|
||||
path_to_tags("/foo/bar/123 - cake pie.jpg")
|
||||
);
|
||||
$this->assertEquals(
|
||||
["bacon", "lemon"],
|
||||
"bacon lemon",
|
||||
path_to_tags("\\bacon\\lemon\\baz.jpg")
|
||||
);
|
||||
$this->assertEquals(
|
||||
["category:tag"],
|
||||
"category:tag",
|
||||
path_to_tags("/category:/tag/baz.jpg")
|
||||
);
|
||||
}
|
142
core/urls.php
142
core/urls.php
@ -4,12 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use PhpParser\Node\Expr\Cast\Double;
|
||||
|
||||
class Link
|
||||
{
|
||||
public ?string $page;
|
||||
public ?string $query;
|
||||
|
||||
public function __construct(?string $page = null, ?string $query = null)
|
||||
public function __construct(?string $page=null, ?string $query=null)
|
||||
{
|
||||
$this->page = $page;
|
||||
$this->query = $query;
|
||||
@ -21,30 +23,13 @@ class Link
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a link to a search page for given terms,
|
||||
* with all the appropriate escaping
|
||||
*
|
||||
* @param string[] $terms
|
||||
*/
|
||||
function search_link(array $terms = [], int $page = 1): string
|
||||
{
|
||||
if($terms) {
|
||||
$q = url_escape(Tag::implode($terms));
|
||||
return make_link("post/list/$q/$page");
|
||||
} else {
|
||||
return make_link("post/list/$page");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out the correct way to link to a page, taking into account
|
||||
* things like the nice URLs setting.
|
||||
*
|
||||
* eg make_link("foo/bar") becomes either "/v2/foo/bar" (niceurls) or
|
||||
* "/v2/index.php?q=foo/bar" (uglyurls)
|
||||
* eg make_link("post/list") becomes "/v2/index.php?q=post/list"
|
||||
*/
|
||||
function make_link(?string $page = null, ?string $query = null, ?string $fragment = null): string
|
||||
function make_link(?string $page=null, ?string $query=null, ?string $fragment=null): string
|
||||
{
|
||||
global $config;
|
||||
|
||||
@ -67,130 +52,17 @@ function make_link(?string $page = null, ?string $query = null, ?string $fragmen
|
||||
return unparse_url($parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out the current page from a link that make_link() generated
|
||||
*
|
||||
* SHIT: notes for the future, because the web stack is a pile of hacks
|
||||
*
|
||||
* - According to some specs, "/" is for URL dividers with heiracial
|
||||
* significance and %2F is for slashes that are just slashes. This
|
||||
* is what shimmie currently does - eg if you search for "AC/DC",
|
||||
* the shimmie URL will be /post/list/AC%2FDC/1
|
||||
* - According to some other specs "/" and "%2F" are identical...
|
||||
* - PHP's $_GET[] automatically urldecodes the inputs so we can't
|
||||
* tell the difference between q=foo/bar and q=foo%2Fbar
|
||||
* - REQUEST_URI contains the exact URI that was given to us, so we
|
||||
* can parse it for ourselves
|
||||
* - <input type='hidden' name='q' value='post/list'> generates
|
||||
* q=post%2Flist
|
||||
*
|
||||
* This function should always return strings with no leading slashes
|
||||
*/
|
||||
function _get_query(?string $uri = null): string
|
||||
{
|
||||
$parsed_url = parse_url($uri ?? $_SERVER['REQUEST_URI']);
|
||||
|
||||
// if we're looking at http://site.com/$INSTALL_DIR/index.php,
|
||||
// then get the query from the "q" parameter
|
||||
if(($parsed_url["path"] ?? "") == (get_base_href() . "/index.php")) {
|
||||
// $q = $_GET["q"] ?? "";
|
||||
// default to looking at the root
|
||||
$q = "";
|
||||
// (we need to manually parse the query string because PHP's $_GET
|
||||
// does an extra round of URL decoding, which we don't want)
|
||||
foreach(explode('&', $parsed_url['query'] ?? "") as $z) {
|
||||
$qps = explode('=', $z, 2);
|
||||
if(count($qps) == 2 && $qps[0] == "q") {
|
||||
$q = $qps[1];
|
||||
}
|
||||
}
|
||||
// if we have no slashes, but do have an encoded
|
||||
// slash, then we _probably_ encoded too much
|
||||
if(!str_contains($q, "/") && str_contains($q, "%2F")) {
|
||||
$q = rawurldecode($q);
|
||||
}
|
||||
}
|
||||
|
||||
// if we're looking at http://site.com/$INSTALL_DIR/$PAGE,
|
||||
// then get the query from the path
|
||||
else {
|
||||
$q = substr($parsed_url["path"] ?? "", strlen(get_base_href() . "/"));
|
||||
}
|
||||
|
||||
assert(!str_starts_with($q, "/"));
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* This function should always return strings with no trailing
|
||||
* slashes, so that it can be used like `get_base_href() . "/data/asset.abc"`
|
||||
*
|
||||
* @param array<string, string>|null $server_settings
|
||||
*/
|
||||
function get_base_href(?array $server_settings = null): string
|
||||
{
|
||||
if (defined("BASE_HREF") && !empty(BASE_HREF)) {
|
||||
return BASE_HREF;
|
||||
}
|
||||
$server_settings = $server_settings ?? $_SERVER;
|
||||
if(str_ends_with($server_settings['PHP_SELF'], 'index.php')) {
|
||||
$self = $server_settings['PHP_SELF'];
|
||||
} elseif(isset($server_settings['SCRIPT_FILENAME']) && isset($server_settings['DOCUMENT_ROOT'])) {
|
||||
$self = substr($server_settings['SCRIPT_FILENAME'], strlen(rtrim($server_settings['DOCUMENT_ROOT'], "/")));
|
||||
} else {
|
||||
die("PHP_SELF or SCRIPT_FILENAME need to be set");
|
||||
}
|
||||
$dir = dirname($self);
|
||||
$dir = str_replace("\\", "/", $dir);
|
||||
$dir = rtrim($dir, "/");
|
||||
return $dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* The opposite of the standard library's parse_url
|
||||
*
|
||||
* @param array<string, string|int> $parsed_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";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Take the current URL and modify some parameters
|
||||
*
|
||||
* @param array<string, mixed> $changes
|
||||
*/
|
||||
function modify_current_url(array $changes): string
|
||||
{
|
||||
return modify_url($_SERVER['REQUEST_URI'], $changes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a URL and modify some parameters
|
||||
*
|
||||
* @param array<string, mixed> $changes
|
||||
*/
|
||||
function modify_url(string $url, array $changes): string
|
||||
{
|
||||
/** @var array<string, mixed> */
|
||||
$parts = parse_url($url);
|
||||
|
||||
$params = [];
|
||||
@ -231,10 +103,8 @@ function make_http(string $link): string
|
||||
/**
|
||||
* If HTTP_REFERER is set, and not blacklisted, then return it
|
||||
* Else return a default $dest
|
||||
*
|
||||
* @param string[]|null $blacklist
|
||||
*/
|
||||
function referer_or(string $dest, ?array $blacklist = null): string
|
||||
function referer_or(string $dest, ?array $blacklist=null): string
|
||||
{
|
||||
if (empty($_SERVER['HTTP_REFERER'])) {
|
||||
return $dest;
|
||||
|
@ -11,6 +11,12 @@ use MicroHTML\HTMLElement;
|
||||
|
||||
use function MicroHTML\INPUT;
|
||||
|
||||
function _new_user(array $row): User
|
||||
{
|
||||
return new User($row);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class User
|
||||
*
|
||||
@ -44,19 +50,20 @@ class User
|
||||
* One will very rarely construct a user directly, more common
|
||||
* would be to use User::by_id, User::by_session, etc.
|
||||
*
|
||||
* @param array<string|int, mixed> $row
|
||||
* @throws SCoreException
|
||||
*/
|
||||
public function __construct(array $row)
|
||||
{
|
||||
global $_shm_user_classes;
|
||||
|
||||
$this->id = int_escape((string)$row['id']);
|
||||
$this->name = $row['name'];
|
||||
$this->email = $row['email'];
|
||||
$this->join_date = $row['joindate'];
|
||||
$this->passhash = $row['pass'];
|
||||
|
||||
if (array_key_exists($row["class"], UserClass::$known_classes)) {
|
||||
$this->class = UserClass::$known_classes[$row["class"]];
|
||||
if (array_key_exists($row["class"], $_shm_user_classes)) {
|
||||
$this->class = $_shm_user_classes[$row["class"]];
|
||||
} else {
|
||||
throw new SCoreException("User '{$this->name}' has invalid class '{$row["class"]}'");
|
||||
}
|
||||
@ -91,7 +98,7 @@ class User
|
||||
} else {
|
||||
$query = "SELECT * FROM users WHERE name = :name AND md5(pass || :ip) = :sess";
|
||||
}
|
||||
$row = $database->get_row($query, ["name" => $name, "ip" => get_session_ip($config), "sess" => $session]);
|
||||
$row = $database->get_row($query, ["name"=>$name, "ip"=>get_session_ip($config), "sess"=>$session]);
|
||||
$cache->set("user-session:$name-$session", $row, 600);
|
||||
}
|
||||
return is_null($row) ? null : new User($row);
|
||||
@ -106,7 +113,7 @@ class User
|
||||
return new User($cached);
|
||||
}
|
||||
}
|
||||
$row = $database->get_row("SELECT * FROM users WHERE id = :id", ["id" => $id]);
|
||||
$row = $database->get_row("SELECT * FROM users WHERE id = :id", ["id"=>$id]);
|
||||
if ($id === 1) {
|
||||
$cache->set('user-id:'.$id, $row, 600);
|
||||
}
|
||||
@ -117,7 +124,7 @@ class User
|
||||
public static function by_name(string $name): ?User
|
||||
{
|
||||
global $database;
|
||||
$row = $database->get_row("SELECT * FROM users WHERE LOWER(name) = LOWER(:name)", ["name" => $name]);
|
||||
$row = $database->get_row("SELECT * FROM users WHERE LOWER(name) = LOWER(:name)", ["name"=>$name]);
|
||||
return is_null($row) ? null : new User($row);
|
||||
}
|
||||
|
||||
@ -181,7 +188,7 @@ class User
|
||||
public function set_class(string $class): void
|
||||
{
|
||||
global $database;
|
||||
$database->execute("UPDATE users SET class=:class WHERE id=:id", ["class" => $class, "id" => $this->id]);
|
||||
$database->execute("UPDATE users SET class=:class WHERE id=:id", ["class"=>$class, "id"=>$this->id]);
|
||||
log_info("core-user", 'Set class for '.$this->name.' to '.$class);
|
||||
}
|
||||
|
||||
@ -193,7 +200,7 @@ class User
|
||||
}
|
||||
$old_name = $this->name;
|
||||
$this->name = $name;
|
||||
$database->execute("UPDATE users SET name=:name WHERE id=:id", ["name" => $this->name, "id" => $this->id]);
|
||||
$database->execute("UPDATE users SET name=:name WHERE id=:id", ["name"=>$this->name, "id"=>$this->id]);
|
||||
log_info("core-user", "Changed username for {$old_name} to {$this->name}");
|
||||
}
|
||||
|
||||
@ -201,15 +208,19 @@ class User
|
||||
{
|
||||
global $database;
|
||||
$hash = password_hash($password, PASSWORD_BCRYPT);
|
||||
$this->passhash = $hash;
|
||||
$database->execute("UPDATE users SET pass=:hash WHERE id=:id", ["hash" => $this->passhash, "id" => $this->id]);
|
||||
log_info("core-user", 'Set password for '.$this->name);
|
||||
if (is_string($hash)) {
|
||||
$this->passhash = $hash;
|
||||
$database->execute("UPDATE users SET pass=:hash WHERE id=:id", ["hash"=>$this->passhash, "id"=>$this->id]);
|
||||
log_info("core-user", 'Set password for '.$this->name);
|
||||
} else {
|
||||
throw new SCoreException("Failed to hash password");
|
||||
}
|
||||
}
|
||||
|
||||
public function set_email(string $address): void
|
||||
{
|
||||
global $database;
|
||||
$database->execute("UPDATE users SET email=:email WHERE id=:id", ["email" => $address, "id" => $this->id]);
|
||||
$database->execute("UPDATE users SET email=:email WHERE id=:id", ["email"=>$address, "id"=>$this->id]);
|
||||
log_info("core-user", 'Set email for '.$this->name);
|
||||
}
|
||||
|
||||
@ -273,7 +284,7 @@ class User
|
||||
public function get_auth_microhtml(): HTMLElement
|
||||
{
|
||||
$at = $this->get_auth_token();
|
||||
return INPUT(["type" => "hidden", "name" => "auth_token", "value" => $at]);
|
||||
return INPUT(["type"=>"hidden", "name"=>"auth_token", "value"=>$at]);
|
||||
}
|
||||
|
||||
public function check_auth_token(): bool
|
||||
|
@ -6,6 +6,13 @@ namespace Shimmie2;
|
||||
|
||||
use GQLA\Type;
|
||||
use GQLA\Field;
|
||||
use GQLA\Query;
|
||||
|
||||
/**
|
||||
* @global UserClass[] $_shm_user_classes
|
||||
*/
|
||||
global $_shm_user_classes;
|
||||
$_shm_user_classes = [];
|
||||
|
||||
/**
|
||||
* Class UserClass
|
||||
@ -13,40 +20,31 @@ use GQLA\Field;
|
||||
#[Type(name: "UserClass")]
|
||||
class UserClass
|
||||
{
|
||||
/** @var array<string, UserClass> */
|
||||
public static array $known_classes = [];
|
||||
|
||||
#[Field]
|
||||
public ?string $name = null;
|
||||
public ?UserClass $parent = null;
|
||||
|
||||
/** @var array<string, bool> */
|
||||
public array $abilities = [];
|
||||
|
||||
/**
|
||||
* @param array<string, bool> $abilities
|
||||
*/
|
||||
public function __construct(string $name, string $parent = null, array $abilities = [])
|
||||
{
|
||||
global $_shm_user_classes;
|
||||
|
||||
$this->name = $name;
|
||||
$this->abilities = $abilities;
|
||||
|
||||
if (!is_null($parent)) {
|
||||
$this->parent = static::$known_classes[$parent];
|
||||
$this->parent = $_shm_user_classes[$parent];
|
||||
}
|
||||
|
||||
static::$known_classes[$name] = $this;
|
||||
$_shm_user_classes[$name] = $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
#[Field(type: "[Permission!]!")]
|
||||
public function permissions(): array
|
||||
{
|
||||
global $_all_false;
|
||||
$perms = [];
|
||||
foreach ((new \ReflectionClass(Permissions::class))->getConstants() as $k => $v) {
|
||||
foreach ((new \ReflectionClass('\Shimmie2\Permissions'))->getConstants() as $k => $v) {
|
||||
if ($this->can($v)) {
|
||||
$perms[] = $v;
|
||||
}
|
||||
@ -66,9 +64,10 @@ class UserClass
|
||||
} elseif (!is_null($this->parent)) {
|
||||
return $this->parent->can($ability);
|
||||
} else {
|
||||
global $_shm_user_classes;
|
||||
$min_dist = 9999;
|
||||
$min_ability = null;
|
||||
foreach (UserClass::$known_classes['base']->abilities as $a => $cando) {
|
||||
foreach ($_shm_user_classes['base']->abilities as $a => $cando) {
|
||||
$v = levenshtein($ability, $a);
|
||||
if ($v < $min_dist) {
|
||||
$min_dist = $v;
|
||||
@ -81,8 +80,7 @@ class UserClass
|
||||
}
|
||||
|
||||
$_all_false = [];
|
||||
foreach ((new \ReflectionClass(Permissions::class))->getConstants() as $k => $v) {
|
||||
assert(is_string($v));
|
||||
foreach ((new \ReflectionClass('\Shimmie2\Permissions'))->getConstants() as $k => $v) {
|
||||
$_all_false[$v] = false;
|
||||
}
|
||||
new UserClass("base", null, $_all_false);
|
||||
@ -90,7 +88,6 @@ unset($_all_false);
|
||||
|
||||
// Ghost users can't do anything
|
||||
new UserClass("ghost", "base", [
|
||||
Permissions::READ_PM => true,
|
||||
]);
|
||||
|
||||
// Anonymous users can't do anything by default, but
|
||||
@ -222,10 +219,10 @@ new UserClass("admin", "base", [
|
||||
Permissions::APPROVE_COMMENT => true,
|
||||
Permissions::BYPASS_IMAGE_APPROVAL => true,
|
||||
|
||||
Permissions::CRON_RUN => true,
|
||||
Permissions::CRON_RUN =>true,
|
||||
|
||||
Permissions::BULK_IMPORT => true,
|
||||
Permissions::BULK_EXPORT => true,
|
||||
Permissions::BULK_IMPORT =>true,
|
||||
Permissions::BULK_EXPORT =>true,
|
||||
Permissions::BULK_DOWNLOAD => true,
|
||||
Permissions::BULK_PARENT_CHILD => true,
|
||||
|
||||
|
251
core/util.php
251
core/util.php
@ -54,8 +54,8 @@ function contact_link(): ?string
|
||||
function is_https_enabled(): bool
|
||||
{
|
||||
// check forwarded protocol
|
||||
if (is_trusted_proxy() && !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') {
|
||||
$_SERVER['HTTPS'] = 'on';
|
||||
if (REVERSE_PROXY_X_HEADERS && !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') {
|
||||
$_SERVER['HTTPS']='on';
|
||||
}
|
||||
return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
}
|
||||
@ -80,10 +80,10 @@ function get_memory_limit(): int
|
||||
global $config;
|
||||
|
||||
// thumbnail generation requires lots of memory
|
||||
$default_limit = 8 * 1024 * 1024; // 8 MB of memory is PHP's default.
|
||||
$default_limit = 8*1024*1024; // 8 MB of memory is PHP's default.
|
||||
$shimmie_limit = $config->get_int(MediaConfig::MEM_LIMIT);
|
||||
|
||||
if ($shimmie_limit < 3 * 1024 * 1024) {
|
||||
if ($shimmie_limit < 3*1024*1024) {
|
||||
// we aren't going to fit, override
|
||||
$shimmie_limit = $default_limit;
|
||||
}
|
||||
@ -143,43 +143,30 @@ function check_gd_version(): int
|
||||
*/
|
||||
function check_im_version(): int
|
||||
{
|
||||
$convert_check = exec("convert --version");
|
||||
$convert_check = exec("convert");
|
||||
|
||||
return (empty($convert_check) ? 0 : 1);
|
||||
}
|
||||
|
||||
function is_trusted_proxy(): bool
|
||||
{
|
||||
$ra = $_SERVER['REMOTE_ADDR'] ?? "0.0.0.0";
|
||||
// @phpstan-ignore-next-line - TRUSTED_PROXIES is defined in config
|
||||
foreach(TRUSTED_PROXIES as $proxy) {
|
||||
if(ip_in_range($ra, $proxy)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Get request IP
|
||||
*/
|
||||
|
||||
function get_remote_addr()
|
||||
{
|
||||
return $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
/**
|
||||
* Get real IP if behind a reverse proxy
|
||||
*/
|
||||
function get_real_ip(): string
|
||||
|
||||
function get_real_ip()
|
||||
{
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
|
||||
if(is_trusted_proxy()) {
|
||||
if (isset($_SERVER['HTTP_X_REAL_IP'])) {
|
||||
if(filter_var_ex($ip, FILTER_VALIDATE_IP)) {
|
||||
$ip = $_SERVER['HTTP_X_REAL_IP'];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
$last_ip = $ips[count($ips) - 1];
|
||||
if(filter_var_ex($last_ip, FILTER_VALIDATE_IP)) {
|
||||
$ip = $last_ip;
|
||||
}
|
||||
$ip = get_remote_addr();
|
||||
if (REVERSE_PROXY_X_HEADERS && isset($_SERVER['HTTP_X_REAL_IP'])) {
|
||||
$ip = $_SERVER['HTTP_X_REAL_IP'];
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
||||
$ip = "0.0.0.0";
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,7 +181,7 @@ function get_session_ip(Config $config): string
|
||||
{
|
||||
$mask = $config->get_string("session_hash_mask", "255.255.0.0");
|
||||
$addr = get_real_ip();
|
||||
$addr = inet_ntop_ex(inet_pton_ex($addr) & inet_pton_ex($mask));
|
||||
$addr = inet_ntop(inet_pton($addr) & inet_pton($mask));
|
||||
return $addr;
|
||||
}
|
||||
|
||||
@ -218,9 +205,9 @@ function format_text(string $string): string
|
||||
* @param int $splits The number of octet pairs to split the hash into. Caps out at strlen($hash)/2.
|
||||
* @return string
|
||||
*/
|
||||
function warehouse_path(string $base, string $hash, bool $create = true, int $splits = WH_SPLITS): string
|
||||
function warehouse_path(string $base, string $hash, bool $create=true, int $splits = WH_SPLITS): string
|
||||
{
|
||||
$dirs = [DATA_DIR, $base];
|
||||
$dirs =[DATA_DIR, $base];
|
||||
$splits = min($splits, strlen($hash) / 2);
|
||||
for ($i = 0; $i < $splits; $i++) {
|
||||
$dirs[] = substr($hash, $i * 2, 2);
|
||||
@ -241,13 +228,13 @@ function warehouse_path(string $base, string $hash, bool $create = true, int $sp
|
||||
function data_path(string $filename, bool $create = true): string
|
||||
{
|
||||
$filename = join_path("data", $filename);
|
||||
if ($create && !file_exists(dirname($filename))) {
|
||||
if ($create&&!file_exists(dirname($filename))) {
|
||||
mkdir(dirname($filename), 0755, true);
|
||||
}
|
||||
return $filename;
|
||||
}
|
||||
|
||||
function load_balance_url(string $tmpl, string $hash, int $n = 0): string
|
||||
function load_balance_url(string $tmpl, string $hash, int $n=0): string
|
||||
{
|
||||
static $flexihashes = [];
|
||||
$matches = [];
|
||||
@ -285,24 +272,16 @@ function load_balance_url(string $tmpl, string $hash, int $n = 0): string
|
||||
return $tmpl;
|
||||
}
|
||||
|
||||
class FetchException extends \Exception
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|string[]>
|
||||
*/
|
||||
function fetch_url(string $url, string $mfile): array
|
||||
function fetch_url(string $url, string $mfile): ?array
|
||||
{
|
||||
global $config;
|
||||
|
||||
if ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "curl" && function_exists("curl_init")) {
|
||||
$ch = curl_init($url);
|
||||
assert($ch !== false);
|
||||
$fp = false_throws(fopen($mfile, "w"));
|
||||
$fp = fopen($mfile, "w");
|
||||
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
# curl_setopt($ch, CURLOPT_VERBOSE, 1);
|
||||
curl_setopt($ch, CURLOPT_VERBOSE, 1);
|
||||
curl_setopt($ch, CURLOPT_HEADER, 1);
|
||||
curl_setopt($ch, CURLOPT_REFERER, $url);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, "Shimmie-".VERSION);
|
||||
@ -310,37 +289,37 @@ function fetch_url(string $url, string $mfile): array
|
||||
|
||||
$response = curl_exec($ch);
|
||||
if ($response === false) {
|
||||
throw new FetchException("cURL failed: ".curl_error($ch));
|
||||
}
|
||||
if ($response === true) { // we use CURLOPT_RETURNTRANSFER, so this should never happen
|
||||
throw new FetchException("cURL failed successfully??");
|
||||
return null;
|
||||
}
|
||||
|
||||
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||
$header_text = trim(substr($response, 0, $header_size));
|
||||
$headers = http_parse_headers(implode("\n", false_throws(preg_split('/\R/', $header_text))));
|
||||
$headers = http_parse_headers(implode("\n", preg_split('/\R/', rtrim(substr($response, 0, $header_size)))));
|
||||
$body = substr($response, $header_size);
|
||||
|
||||
curl_close($ch);
|
||||
fwrite($fp, $body);
|
||||
fclose($fp);
|
||||
} elseif ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "wget") {
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
if ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "wget") {
|
||||
$s_url = escapeshellarg($url);
|
||||
$s_mfile = escapeshellarg($mfile);
|
||||
system("wget --no-check-certificate $s_url --output-document=$s_mfile");
|
||||
if(!file_exists($mfile)) {
|
||||
throw new FetchException("wget failed");
|
||||
}
|
||||
$headers = [];
|
||||
} elseif ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "fopen") {
|
||||
|
||||
return file_exists($mfile) ? ["ok"=>"true"] : null;
|
||||
}
|
||||
|
||||
if ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "fopen") {
|
||||
$fp_in = @fopen($url, "r");
|
||||
$fp_out = fopen($mfile, "w");
|
||||
if (!$fp_in || !$fp_out) {
|
||||
throw new FetchException("fopen failed");
|
||||
return null;
|
||||
}
|
||||
$length = 0;
|
||||
while (!feof($fp_in) && $length <= $config->get_int(UploadConfig::SIZE)) {
|
||||
$data = false_throws(fread($fp_in, 8192));
|
||||
$data = fread($fp_in, 8192);
|
||||
$length += strlen($data);
|
||||
fwrite($fp_out, $data);
|
||||
}
|
||||
@ -348,22 +327,14 @@ function fetch_url(string $url, string $mfile): array
|
||||
fclose($fp_out);
|
||||
|
||||
$headers = http_parse_headers(implode("\n", $http_response_header));
|
||||
} else {
|
||||
throw new FetchException("No transload engine configured");
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
if (filesize($mfile) == 0) {
|
||||
@unlink($mfile);
|
||||
throw new FetchException("No data found in $url -- perhaps the site has hotlink protection?");
|
||||
}
|
||||
|
||||
return $headers;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
function path_to_tags(string $path): array
|
||||
function path_to_tags(string $path): string
|
||||
{
|
||||
$matches = [];
|
||||
$tags = [];
|
||||
@ -384,7 +355,7 @@ function path_to_tags(string $path): array
|
||||
$category_to_inherit = "";
|
||||
foreach (explode(" ", $dir) as $tag) {
|
||||
$tag = trim($tag);
|
||||
if ($tag == "") {
|
||||
if ($tag=="") {
|
||||
continue;
|
||||
}
|
||||
if (substr_compare($tag, ":", -1) === 0) {
|
||||
@ -392,7 +363,7 @@ function path_to_tags(string $path): array
|
||||
// which is for inheriting to tags on the subfolder
|
||||
$category_to_inherit = $tag;
|
||||
} else {
|
||||
if ($category != "" && !str_contains($tag, ":")) {
|
||||
if ($category!="" && !str_contains($tag, ":")) {
|
||||
// This indicates that category inheritance is active,
|
||||
// and we've encountered a tag that does not specify a category.
|
||||
// So we attach the inherited category to the tag.
|
||||
@ -407,12 +378,9 @@ function path_to_tags(string $path): array
|
||||
$category = $category_to_inherit;
|
||||
}
|
||||
|
||||
return $tags;
|
||||
return implode(" ", $tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
function get_dir_contents(string $dir): array
|
||||
{
|
||||
assert(!empty($dir));
|
||||
@ -421,17 +389,29 @@ function get_dir_contents(string $dir): array
|
||||
return [];
|
||||
}
|
||||
return array_diff(
|
||||
false_throws(scandir($dir)),
|
||||
scandir(
|
||||
$dir
|
||||
),
|
||||
['..', '.']
|
||||
);
|
||||
}
|
||||
|
||||
function remove_empty_dirs(string $dir): bool
|
||||
{
|
||||
assert(!empty($dir));
|
||||
|
||||
$result = true;
|
||||
|
||||
$items = get_dir_contents($dir);
|
||||
;
|
||||
if (!is_dir($dir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$items = array_diff(
|
||||
scandir(
|
||||
$dir
|
||||
),
|
||||
['..', '.']
|
||||
);
|
||||
foreach ($items as $item) {
|
||||
$path = join_path($dir, $item);
|
||||
if (is_dir($path)) {
|
||||
@ -440,21 +420,31 @@ function remove_empty_dirs(string $dir): bool
|
||||
$result = false;
|
||||
}
|
||||
}
|
||||
if ($result === true) {
|
||||
if ($result===true) {
|
||||
$result = rmdir($dir);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
|
||||
function get_files_recursively(string $dir): array
|
||||
{
|
||||
$things = get_dir_contents($dir);
|
||||
assert(!empty($dir));
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$things = array_diff(
|
||||
scandir(
|
||||
$dir
|
||||
),
|
||||
['..', '.']
|
||||
);
|
||||
|
||||
$output = [];
|
||||
|
||||
|
||||
foreach ($things as $thing) {
|
||||
$path = join_path($dir, $thing);
|
||||
if (is_file($path)) {
|
||||
@ -469,8 +459,6 @@ function get_files_recursively(string $dir): array
|
||||
|
||||
/**
|
||||
* Returns amount of files & total size of dir.
|
||||
*
|
||||
* @return array{"path": string, "total_files": int, "total_mb": string}
|
||||
*/
|
||||
function scan_dir(string $path): array
|
||||
{
|
||||
@ -532,11 +520,6 @@ function get_debug_info(): string
|
||||
return $debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects some debug information (execution time, memory usage, queries, etc)
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function get_debug_info_arr(): array
|
||||
{
|
||||
global $cache, $config, $_shm_event_count, $database, $_shm_load_start;
|
||||
@ -550,7 +533,7 @@ function get_debug_info_arr(): array
|
||||
return [
|
||||
"time" => round(ftime() - $_shm_load_start, 2),
|
||||
"dbtime" => round($database->dbtime, 2),
|
||||
"mem_mb" => round(((memory_get_peak_usage(true) + 512) / 1024) / 1024, 2),
|
||||
"mem_mb" => round(((memory_get_peak_usage(true)+512)/1024)/1024, 2),
|
||||
"files" => count(get_included_files()),
|
||||
"query_count" => $database->query_count,
|
||||
// "query_log" => $database->queries,
|
||||
@ -566,9 +549,6 @@ function get_debug_info_arr(): array
|
||||
* Request initialisation stuff *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* @param string[] $files
|
||||
*/
|
||||
function require_all(array $files): void
|
||||
{
|
||||
foreach ($files as $filename) {
|
||||
@ -576,7 +556,7 @@ function require_all(array $files): void
|
||||
}
|
||||
}
|
||||
|
||||
function _load_core_files(): void
|
||||
function _load_core_files()
|
||||
{
|
||||
require_all(array_merge(
|
||||
zglob("core/*.php"),
|
||||
@ -585,20 +565,11 @@ function _load_core_files(): void
|
||||
));
|
||||
}
|
||||
|
||||
function _load_extension_files(): void
|
||||
{
|
||||
ExtensionInfo::load_all_extension_info();
|
||||
Extension::determine_enabled_extensions();
|
||||
require_all(zglob("ext/{".Extension::get_enabled_extensions_as_string()."}/main.php"));
|
||||
}
|
||||
|
||||
function _load_theme_files(): void
|
||||
function _load_theme_files()
|
||||
{
|
||||
$theme = get_theme();
|
||||
require_once('themes/'.$theme.'/page.class.php');
|
||||
require_once('themes/'.$theme.'/themelet.class.php');
|
||||
require_all(zglob("ext/{".Extension::get_enabled_extensions_as_string()."}/theme.php"));
|
||||
require_all(zglob('themes/'.$theme.'/{'.Extension::get_enabled_extensions_as_string().'}.theme.php'));
|
||||
$files = _get_themelet_files($theme);
|
||||
require_all($files);
|
||||
}
|
||||
|
||||
function _set_up_shimmie_environment(): void
|
||||
@ -620,8 +591,20 @@ function _set_up_shimmie_environment(): void
|
||||
// The trace system has a certain amount of memory consumption every time it is used,
|
||||
// so to prevent running out of memory during complex operations code that uses it should
|
||||
// check if tracer output is enabled before making use of it.
|
||||
// @phpstan-ignore-next-line - TRACE_FILE is defined in config
|
||||
$tracer_enabled = !is_null('TRACE_FILE');
|
||||
$tracer_enabled = constant('TRACE_FILE')!==null;
|
||||
}
|
||||
|
||||
|
||||
function _get_themelet_files(string $_theme): array
|
||||
{
|
||||
$base_themelets = [];
|
||||
$base_themelets[] = 'themes/'.$_theme.'/page.class.php';
|
||||
$base_themelets[] = 'themes/'.$_theme.'/themelet.class.php';
|
||||
|
||||
$ext_themelets = zglob("ext/{".Extension::get_enabled_extensions_as_string()."}/theme.php");
|
||||
$custom_themelets = zglob('themes/'.$_theme.'/{'.Extension::get_enabled_extensions_as_string().'}.theme.php');
|
||||
|
||||
return array_merge($base_themelets, $ext_themelets, $custom_themelets);
|
||||
}
|
||||
|
||||
|
||||
@ -633,6 +616,8 @@ function _fatal_error(\Exception $e): void
|
||||
$version = VERSION;
|
||||
$message = $e->getMessage();
|
||||
$phpver = phpversion();
|
||||
$query = is_subclass_of($e, "Shimmie2\SCoreException") ? $e->query : null;
|
||||
$code = is_subclass_of($e, "Shimmie2\SCoreException") ? $e->http_code : 500;
|
||||
|
||||
//$hash = exec("git rev-parse HEAD");
|
||||
//$h_hash = $hash ? "<p><b>Hash:</b> $hash" : "";
|
||||
@ -644,29 +629,19 @@ function _fatal_error(\Exception $e): void
|
||||
foreach ($t as $n => $f) {
|
||||
$c = $f['class'] ?? '';
|
||||
$t = $f['type'] ?? '';
|
||||
$i = $f['file'] ?? 'unknown file';
|
||||
$l = $f['line'] ?? -1;
|
||||
$a = implode(", ", array_map("Shimmie2\stringer", $f['args'] ?? []));
|
||||
print("$n: {$i}({$l}): {$c}{$t}{$f['function']}({$a})\n");
|
||||
print("$n: {$f['file']}({$f['line']}): {$c}{$t}{$f['function']}({$a})\n");
|
||||
}
|
||||
|
||||
print("Message: $message\n");
|
||||
|
||||
if (is_a($e, DatabaseException::class)) {
|
||||
print("Query: {$e->query}\n");
|
||||
print("Args: ".var_export($e->args, true)."\n");
|
||||
if ($query) {
|
||||
print("Query: {$query}\n");
|
||||
}
|
||||
|
||||
print("Version: $version (on $phpver)\n");
|
||||
} else {
|
||||
$query = is_a($e, DatabaseException::class) ? $e->query : null;
|
||||
$code = is_a($e, SCoreException::class) ? $e->http_code : 500;
|
||||
|
||||
$q = "";
|
||||
if(is_a($e, DatabaseException::class)) {
|
||||
$q .= "<p><b>Query:</b> " . html_escape($query);
|
||||
$q .= "<p><b>Args:</b> " . html_escape(var_export($e->args, true));
|
||||
}
|
||||
$q = $query ? "" : "<p><b>Query:</b> " . html_escape($query);
|
||||
if ($code >= 500) {
|
||||
error_log("Shimmie Error: $message (Query: $query)\n{$e->getTraceAsString()}");
|
||||
}
|
||||
@ -702,7 +677,7 @@ function _get_user(): User
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_null($my_user) && $page->get_cookie("user") && $page->get_cookie("session")) {
|
||||
if ($page->get_cookie("user") && $page->get_cookie("session")) {
|
||||
$my_user = User::by_session($page->get_cookie("user"), $page->get_cookie("session"));
|
||||
}
|
||||
if (is_null($my_user)) {
|
||||
@ -713,6 +688,11 @@ function _get_user(): User
|
||||
return $my_user;
|
||||
}
|
||||
|
||||
function _get_query(): string
|
||||
{
|
||||
return (@$_POST["q"] ?: @$_GET["q"]) ?: "/";
|
||||
}
|
||||
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* HTML Generation *
|
||||
@ -737,7 +717,7 @@ function show_ip(string $ip, string $ban_reason): string
|
||||
/**
|
||||
* Make a form tag with relevant auth token and stuff
|
||||
*/
|
||||
function make_form(string $target, string $method = "POST", bool $multipart = false, string $form_id = "", string $onsubmit = ""): string
|
||||
function make_form(string $target, string $method="POST", bool $multipart=false, string $form_id="", string $onsubmit=""): string
|
||||
{
|
||||
global $user;
|
||||
if ($method == "GET") {
|
||||
@ -759,7 +739,7 @@ function make_form(string $target, string $method = "POST", bool $multipart = fa
|
||||
}
|
||||
|
||||
const BYTE_DENOMINATIONS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
function human_filesize(int $bytes, int $decimals = 2): string
|
||||
function human_filesize(int $bytes, $decimals = 2): string
|
||||
{
|
||||
$factor = floor((strlen(strval($bytes)) - 1) / 3);
|
||||
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @BYTE_DENOMINATIONS[$factor];
|
||||
@ -779,12 +759,3 @@ function generate_key(int $length = 20): string
|
||||
|
||||
return $randomString;
|
||||
}
|
||||
|
||||
function shm_tempnam(string $prefix = ""): string
|
||||
{
|
||||
if(!is_dir("data/temp")) {
|
||||
mkdir("data/temp");
|
||||
}
|
||||
$temp = false_throws(realpath("data/temp"));
|
||||
return false_throws(tempnam($temp, $prefix));
|
||||
}
|
||||
|
@ -4,10 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\{InputInterface,InputArgument};
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Sent when the admin page is ready to be added to
|
||||
*/
|
||||
@ -39,7 +35,7 @@ class AdminPage extends Extension
|
||||
/** @var AdminPageTheme */
|
||||
protected Themelet $theme;
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event): void
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $database, $page, $user;
|
||||
|
||||
@ -58,8 +54,6 @@ class AdminPage extends Extension
|
||||
shm_set_timeout(null);
|
||||
$database->set_timeout(null);
|
||||
send_event($aae);
|
||||
} else {
|
||||
throw new SCoreException("Invalid CSRF token");
|
||||
}
|
||||
|
||||
if ($aae->redirect) {
|
||||
@ -71,108 +65,86 @@ class AdminPage extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onCliGen(CliGenEvent $event): void
|
||||
public function onCommand(CommandEvent $event)
|
||||
{
|
||||
$event->app->register('page:get')
|
||||
->addArgument('query', InputArgument::REQUIRED)
|
||||
->addArgument('args', InputArgument::OPTIONAL)
|
||||
->setDescription('Get a page, eg /post/list')
|
||||
->setCode(function (InputInterface $input, OutputInterface $output): int {
|
||||
global $page;
|
||||
$query = $input->getArgument('query');
|
||||
$args = $input->getArgument('args');
|
||||
$_SERVER['REQUEST_URI'] = $query;
|
||||
if (!is_null($args)) {
|
||||
parse_str($args, $_GET);
|
||||
$_SERVER['REQUEST_URI'] .= "?" . $args;
|
||||
}
|
||||
send_event(new PageRequestEvent("GET", $query));
|
||||
$page->display();
|
||||
return Command::SUCCESS;
|
||||
});
|
||||
$event->app->register('page:post')
|
||||
->addArgument('query', InputArgument::REQUIRED)
|
||||
->addArgument('args', InputArgument::OPTIONAL)
|
||||
->setDescription('Post a page, eg ip_ban/delete id=1')
|
||||
->setCode(function (InputInterface $input, OutputInterface $output): int {
|
||||
global $page;
|
||||
$query = $input->getArgument('query');
|
||||
$args = $input->getArgument('args');
|
||||
global $page;
|
||||
if (!is_null($args)) {
|
||||
parse_str($args, $_POST);
|
||||
}
|
||||
send_event(new PageRequestEvent("POST", $query));
|
||||
$page->display();
|
||||
return Command::SUCCESS;
|
||||
});
|
||||
$event->app->register('get-token')
|
||||
->setDescription('Get a CSRF token')
|
||||
->setCode(function (InputInterface $input, OutputInterface $output): int {
|
||||
global $user;
|
||||
$output->writeln($user->get_auth_token());
|
||||
return Command::SUCCESS;
|
||||
});
|
||||
$event->app->register('regen-thumb')
|
||||
->addArgument('id_or_hash', InputArgument::REQUIRED)
|
||||
->setDescription("Regenerate a post's thumbnail")
|
||||
->setCode(function (InputInterface $input, OutputInterface $output): int {
|
||||
$uid = $input->getArgument('id_or_hash');
|
||||
$image = Image::by_id_or_hash($uid);
|
||||
if ($image) {
|
||||
send_event(new ThumbnailGenerationEvent($image, true));
|
||||
} else {
|
||||
$output->writeln("No post with ID '$uid'\n");
|
||||
}
|
||||
return Command::SUCCESS;
|
||||
});
|
||||
$event->app->register('cache:get')
|
||||
->addArgument('key', InputArgument::REQUIRED)
|
||||
->setDescription("Get a cache value")
|
||||
->setCode(function (InputInterface $input, OutputInterface $output): int {
|
||||
global $cache;
|
||||
$key = $input->getArgument('key');
|
||||
$output->writeln(var_export($cache->get($key), true));
|
||||
return Command::SUCCESS;
|
||||
});
|
||||
$event->app->register('cache:set')
|
||||
->addArgument('key', InputArgument::REQUIRED)
|
||||
->addArgument('value', InputArgument::REQUIRED)
|
||||
->setDescription("Set a cache value")
|
||||
->setCode(function (InputInterface $input, OutputInterface $output): int {
|
||||
global $cache;
|
||||
$key = $input->getArgument('key');
|
||||
$value = $input->getArgument('value');
|
||||
$cache->set($key, $value, 60);
|
||||
return Command::SUCCESS;
|
||||
});
|
||||
$event->app->register('cache:del')
|
||||
->addArgument('key', InputArgument::REQUIRED)
|
||||
->setDescription("Delete a cache value")
|
||||
->setCode(function (InputInterface $input, OutputInterface $output): int {
|
||||
global $cache;
|
||||
$key = $input->getArgument('key');
|
||||
$cache->delete($key);
|
||||
return Command::SUCCESS;
|
||||
});
|
||||
if ($event->cmd == "help") {
|
||||
print "\tget-page <query string>\n";
|
||||
print "\t\teg 'get-page post/list'\n\n";
|
||||
print "\tpost-page <query string> <urlencoded params>\n";
|
||||
print "\t\teg 'post-page ip_ban/delete id=1'\n\n";
|
||||
print "\tget-token\n";
|
||||
print "\t\tget a CSRF auth token\n\n";
|
||||
print "\tregen-thumb <id / hash>\n";
|
||||
print "\t\tregenerate a thumbnail\n\n";
|
||||
print "\tcache [get|set|del] [key] <value>\n";
|
||||
print "\t\teg 'cache get config'\n\n";
|
||||
}
|
||||
if ($event->cmd == "get-page") {
|
||||
global $page;
|
||||
$_SERVER['REQUEST_URI'] = $event->args[0];
|
||||
if (isset($event->args[1])) {
|
||||
parse_str($event->args[1], $_GET);
|
||||
$_SERVER['REQUEST_URI'] .= "?" . $event->args[1];
|
||||
}
|
||||
send_event(new PageRequestEvent($event->args[0]));
|
||||
$page->display();
|
||||
}
|
||||
if ($event->cmd == "post-page") {
|
||||
global $page;
|
||||
$_SERVER['REQUEST_METHOD'] = "POST";
|
||||
if (isset($event->args[1])) {
|
||||
parse_str($event->args[1], $_POST);
|
||||
}
|
||||
send_event(new PageRequestEvent($event->args[0]));
|
||||
$page->display();
|
||||
}
|
||||
if ($event->cmd == "get-token") {
|
||||
global $user;
|
||||
print($user->get_auth_token());
|
||||
}
|
||||
if ($event->cmd == "regen-thumb") {
|
||||
$uid = $event->args[0];
|
||||
$image = Image::by_id_or_hash($uid);
|
||||
if ($image) {
|
||||
send_event(new ThumbnailGenerationEvent($image->hash, $image->get_mime(), true));
|
||||
} else {
|
||||
print("No post with ID '$uid'\n");
|
||||
}
|
||||
}
|
||||
if ($event->cmd == "cache") {
|
||||
global $cache;
|
||||
$cmd = $event->args[0];
|
||||
$key = $event->args[1];
|
||||
switch ($cmd) {
|
||||
case "get":
|
||||
var_export($cache->get($key));
|
||||
break;
|
||||
case "set":
|
||||
$cache->set($key, $event->args[2], 60);
|
||||
break;
|
||||
case "del":
|
||||
$cache->delete($key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onAdminBuilding(AdminBuildingEvent $event): void
|
||||
public function onAdminBuilding(AdminBuildingEvent $event)
|
||||
{
|
||||
$this->theme->display_page();
|
||||
}
|
||||
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if ($event->parent === "system") {
|
||||
if ($event->parent==="system") {
|
||||
if ($user->can(Permissions::MANAGE_ADMINTOOLS)) {
|
||||
$event->add_nav_link("admin", new Link('admin'), "Board Admin");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event): void
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if ($user->can(Permissions::MANAGE_ADMINTOOLS)) {
|
||||
|
@ -6,7 +6,7 @@ namespace Shimmie2;
|
||||
|
||||
class AdminPageTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testAuth(): void
|
||||
public function testAuth()
|
||||
{
|
||||
send_event(new UserLoginEvent(User::by_name(self::$anon_name)));
|
||||
$page = $this->get_page('admin');
|
||||
@ -23,4 +23,19 @@ class AdminPageTest extends ShimmiePHPUnitTestCase
|
||||
$this->assertEquals(200, $page->code);
|
||||
$this->assertEquals("Admin Tools", $page->title);
|
||||
}
|
||||
|
||||
public function testCommands()
|
||||
{
|
||||
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
|
||||
ob_start();
|
||||
send_event(new CommandEvent(["index.php", "help"]));
|
||||
send_event(new CommandEvent(["index.php", "get-page", "post/list"]));
|
||||
send_event(new CommandEvent(["index.php", "post-page", "post/list", "foo=bar"]));
|
||||
send_event(new CommandEvent(["index.php", "get-token"]));
|
||||
send_event(new CommandEvent(["index.php", "regen-thumb", "42"]));
|
||||
ob_end_clean();
|
||||
|
||||
// don't crash
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ class AdminPageTheme extends Themelet
|
||||
/*
|
||||
* Show the basics of a page, for other extensions to add to
|
||||
*/
|
||||
public function display_page(): void
|
||||
public function display_page()
|
||||
{
|
||||
global $page;
|
||||
|
||||
|
@ -19,12 +19,12 @@ class AliasTable extends Table
|
||||
$this->size = 100;
|
||||
$this->limit = 1000000;
|
||||
$this->set_columns([
|
||||
new AutoCompleteColumn("oldtag", "Old Tag"),
|
||||
new AutoCompleteColumn("newtag", "New Tag"),
|
||||
new TextColumn("oldtag", "Old Tag"),
|
||||
new TextColumn("newtag", "New Tag"),
|
||||
new ActionColumn("oldtag"),
|
||||
]);
|
||||
$this->order_by = ["oldtag"];
|
||||
$this->table_attrs = ["class" => "zebra form"];
|
||||
$this->table_attrs = ["class" => "zebra"];
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,7 +61,7 @@ class AliasEditor extends Extension
|
||||
/** @var AliasEditorTheme */
|
||||
protected Themelet $theme;
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event): void
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $config, $database, $page, $user;
|
||||
|
||||
@ -69,7 +69,7 @@ class AliasEditor extends Extension
|
||||
if ($event->get_arg(0) == "add") {
|
||||
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
|
||||
$user->ensure_authed();
|
||||
$input = validate_input(["c_oldtag" => "string", "c_newtag" => "string"]);
|
||||
$input = validate_input(["c_oldtag"=>"string", "c_newtag"=>"string"]);
|
||||
try {
|
||||
send_event(new AddAliasEvent($input['c_oldtag'], $input['c_newtag']));
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
@ -81,7 +81,7 @@ class AliasEditor extends Extension
|
||||
} elseif ($event->get_arg(0) == "remove") {
|
||||
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
|
||||
$user->ensure_authed();
|
||||
$input = validate_input(["d_oldtag" => "string"]);
|
||||
$input = validate_input(["d_oldtag"=>"string"]);
|
||||
send_event(new DeleteAliasEvent($input['d_oldtag']));
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("alias/list"));
|
||||
@ -105,7 +105,7 @@ class AliasEditor extends Extension
|
||||
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
|
||||
if (count($_FILES) > 0) {
|
||||
$tmp = $_FILES['alias_file']['tmp_name'];
|
||||
$contents = file_get_contents_ex($tmp);
|
||||
$contents = file_get_contents($tmp);
|
||||
$this->add_alias_csv($contents);
|
||||
log_info("alias_editor", "Imported aliases from file", "Imported aliases"); # FIXME: how many?
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
@ -120,13 +120,13 @@ class AliasEditor extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onAddAlias(AddAliasEvent $event): void
|
||||
public function onAddAlias(AddAliasEvent $event)
|
||||
{
|
||||
global $database;
|
||||
|
||||
$row = $database->get_row(
|
||||
"SELECT * FROM aliases WHERE lower(oldtag)=lower(:oldtag)",
|
||||
["oldtag" => $event->oldtag]
|
||||
["oldtag"=>$event->oldtag]
|
||||
);
|
||||
if ($row) {
|
||||
throw new AddAliasException("{$row['oldtag']} is already an alias for {$row['newtag']}");
|
||||
@ -147,21 +147,21 @@ class AliasEditor extends Extension
|
||||
log_info("alias_editor", "Added alias for {$event->oldtag} -> {$event->newtag}", "Added alias");
|
||||
}
|
||||
|
||||
public function onDeleteAlias(DeleteAliasEvent $event): void
|
||||
public function onDeleteAlias(DeleteAliasEvent $event)
|
||||
{
|
||||
global $database;
|
||||
$database->execute("DELETE FROM aliases WHERE oldtag=:oldtag", ["oldtag" => $event->oldtag]);
|
||||
log_info("alias_editor", "Deleted alias for {$event->oldtag}", "Deleted alias");
|
||||
}
|
||||
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
|
||||
{
|
||||
if ($event->parent == "tags") {
|
||||
if ($event->parent=="tags") {
|
||||
$event->add_nav_link("aliases", new Link('alias/list'), "Aliases", NavLink::is_active(["alias"]));
|
||||
}
|
||||
}
|
||||
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event): void
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
|
||||
@ -174,7 +174,6 @@ class AliasEditor extends Extension
|
||||
$csv = "";
|
||||
$aliases = $database->get_pairs("SELECT oldtag, newtag FROM aliases ORDER BY newtag");
|
||||
foreach ($aliases as $old => $new) {
|
||||
assert(is_string($new));
|
||||
$csv .= "\"$old\",\"$new\"\n";
|
||||
}
|
||||
return $csv;
|
||||
|
@ -6,14 +6,14 @@ namespace Shimmie2;
|
||||
|
||||
class AliasEditorTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testAliasList(): void
|
||||
public function testAliasList()
|
||||
{
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_response(200);
|
||||
$this->assert_title("Alias List");
|
||||
}
|
||||
|
||||
public function testAliasListReadOnly(): void
|
||||
public function testAliasListReadOnly()
|
||||
{
|
||||
$this->log_in_as_user();
|
||||
$this->get_page('alias/list');
|
||||
@ -26,7 +26,7 @@ class AliasEditorTest extends ShimmiePHPUnitTestCase
|
||||
$this->assert_no_text("Add");
|
||||
}
|
||||
|
||||
public function testAliasOneToOne(): void
|
||||
public function testAliasOneToOne()
|
||||
{
|
||||
$this->log_in_as_admin();
|
||||
|
||||
@ -54,7 +54,7 @@ class AliasEditorTest extends ShimmiePHPUnitTestCase
|
||||
$this->assert_no_text("test1");
|
||||
}
|
||||
|
||||
public function testAliasOneToMany(): void
|
||||
public function testAliasOneToMany()
|
||||
{
|
||||
$this->log_in_as_admin();
|
||||
|
||||
|
@ -18,11 +18,11 @@ class AliasEditorTheme extends Themelet
|
||||
{
|
||||
global $page, $user;
|
||||
|
||||
$html = emptyHTML($table, BR(), $paginator, BR(), SHM_A("alias/export/aliases.csv", "Download as CSV", args: ["download" => "aliases.csv"]));
|
||||
$html = emptyHTML($table, BR(), $paginator, BR(), SHM_A("alias/export/aliases.csv", "Download as CSV", args: ["download"=>"aliases.csv"]));
|
||||
|
||||
$bulk_form = SHM_FORM("alias/import", multipart: true);
|
||||
$bulk_form->appendChild(
|
||||
INPUT(["type" => "file", "name" => "alias_file"]),
|
||||
INPUT(["type"=>"file", "name"=>"alias_file"]),
|
||||
SHM_SUBMIT("Upload List")
|
||||
);
|
||||
$bulk_html = emptyHTML($bulk_form);
|
||||
|
@ -10,7 +10,7 @@ class ApprovalInfo extends ExtensionInfo
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Approval";
|
||||
public array $authors = ["Matthew Barbour" => "matthew@darkholme.net"];
|
||||
public array $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public string $license = self::LICENSE_WTFPL;
|
||||
public string $description = "Adds an approval step to the upload/import process.";
|
||||
}
|
||||
|
@ -16,18 +16,17 @@ class Approval extends Extension
|
||||
/** @var ApprovalTheme */
|
||||
protected Themelet $theme;
|
||||
|
||||
public function onInitExt(InitExtEvent $event): void
|
||||
public function onInitExt(InitExtEvent $event)
|
||||
{
|
||||
global $config;
|
||||
|
||||
$config->set_default_bool(ApprovalConfig::IMAGES, false);
|
||||
$config->set_default_bool(ApprovalConfig::COMMENTS, false);
|
||||
|
||||
Image::$prop_types["approved"] = ImagePropType::BOOL;
|
||||
Image::$prop_types["approved_by_id"] = ImagePropType::INT;
|
||||
Image::$bool_props[] = "approved";
|
||||
}
|
||||
|
||||
public function onImageAddition(ImageAdditionEvent $event): void
|
||||
public function onImageAddition(ImageAdditionEvent $event)
|
||||
{
|
||||
global $user, $config;
|
||||
|
||||
@ -36,7 +35,7 @@ class Approval extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event): void
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $page, $user;
|
||||
|
||||
@ -71,37 +70,37 @@ class Approval extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onSetupBuilding(SetupBuildingEvent $event): void
|
||||
public function onSetupBuilding(SetupBuildingEvent $event)
|
||||
{
|
||||
$this->theme->display_admin_block($event);
|
||||
}
|
||||
|
||||
public function onAdminBuilding(AdminBuildingEvent $event): void
|
||||
public function onAdminBuilding(AdminBuildingEvent $event)
|
||||
{
|
||||
$this->theme->display_admin_form();
|
||||
}
|
||||
|
||||
public function onAdminAction(AdminActionEvent $event): void
|
||||
public function onAdminAction(AdminActionEvent $event)
|
||||
{
|
||||
global $database, $user;
|
||||
|
||||
$action = $event->action;
|
||||
$event->redirect = true;
|
||||
if ($action === "approval") {
|
||||
if ($action==="approval") {
|
||||
$approval_action = $_POST["approval_action"];
|
||||
switch ($approval_action) {
|
||||
case "approve_all":
|
||||
$database->set_timeout(null); // These updates can take a little bit
|
||||
$database->execute(
|
||||
"UPDATE images SET approved = :true, approved_by_id = :approved_by_id WHERE approved = :false",
|
||||
["approved_by_id" => $user->id, "true" => true, "false" => false]
|
||||
["approved_by_id"=>$user->id, "true"=>true, "false"=>false]
|
||||
);
|
||||
break;
|
||||
case "disapprove_all":
|
||||
$database->set_timeout(null); // These updates can take a little bit
|
||||
$database->execute(
|
||||
"UPDATE images SET approved = :false, approved_by_id = NULL WHERE approved = :true",
|
||||
["true" => true, "false" => false]
|
||||
["true"=>true, "false"=>false]
|
||||
);
|
||||
break;
|
||||
default:
|
||||
@ -111,36 +110,36 @@ class Approval extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onDisplayingImage(DisplayingImageEvent $event): void
|
||||
public function onDisplayingImage(DisplayingImageEvent $event)
|
||||
{
|
||||
global $page;
|
||||
|
||||
if (!$this->check_permissions(($event->image))) {
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link());
|
||||
$page->set_redirect(make_link("post/list"));
|
||||
}
|
||||
}
|
||||
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if ($event->parent == "posts") {
|
||||
if ($event->parent=="posts") {
|
||||
if ($user->can(Permissions::APPROVE_IMAGE)) {
|
||||
$event->add_nav_link("posts_unapproved", new Link('/post/list/approved%3Ano/1'), "Pending Approval", null, 60);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event): void
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if ($user->can(Permissions::APPROVE_IMAGE)) {
|
||||
$event->add_link("Pending Approval", search_link(["approved:no"]), 60);
|
||||
$event->add_link("Pending Approval", make_link("/post/list/approved%3Ano/1"), 60);
|
||||
}
|
||||
}
|
||||
|
||||
public const SEARCH_REGEXP = "/^approved:(yes|no)/";
|
||||
public function onSearchTermParse(SearchTermParseEvent $event): void
|
||||
public function onSearchTermParse(SearchTermParseEvent $event)
|
||||
{
|
||||
global $user, $config;
|
||||
|
||||
@ -148,7 +147,7 @@ class Approval extends Extension
|
||||
$matches = [];
|
||||
|
||||
if (is_null($event->term) && $this->no_approval_query($event->context)) {
|
||||
$event->add_querylet(new Querylet("approved = :true", ["true" => true]));
|
||||
$event->add_querylet(new Querylet("approved = :true", ["true"=>true]));
|
||||
}
|
||||
|
||||
if (is_null($event->term)) {
|
||||
@ -156,27 +155,25 @@ class Approval extends Extension
|
||||
}
|
||||
if (preg_match(self::SEARCH_REGEXP, strtolower($event->term), $matches)) {
|
||||
if ($user->can(Permissions::APPROVE_IMAGE) && $matches[1] == "no") {
|
||||
$event->add_querylet(new Querylet("approved != :true", ["true" => true]));
|
||||
$event->add_querylet(new Querylet("approved != :true", ["true"=>true]));
|
||||
} else {
|
||||
$event->add_querylet(new Querylet("approved = :true", ["true" => true]));
|
||||
$event->add_querylet(new Querylet("approved = :true", ["true"=>true]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onHelpPageBuilding(HelpPageBuildingEvent $event): void
|
||||
public function onHelpPageBuilding(HelpPageBuildingEvent $event)
|
||||
{
|
||||
global $user, $config;
|
||||
if ($event->key === HelpPages::SEARCH) {
|
||||
if ($event->key===HelpPages::SEARCH) {
|
||||
if ($user->can(Permissions::APPROVE_IMAGE) && $config->get_bool(ApprovalConfig::IMAGES)) {
|
||||
$event->add_block(new Block("Approval", $this->theme->get_help_html()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $context
|
||||
*/
|
||||
|
||||
private function no_approval_query(array $context): bool
|
||||
{
|
||||
foreach ($context as $term) {
|
||||
@ -187,23 +184,23 @@ class Approval extends Extension
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function approve_image(int $image_id): void
|
||||
public static function approve_image($image_id)
|
||||
{
|
||||
global $database, $user;
|
||||
|
||||
$database->execute(
|
||||
"UPDATE images SET approved = :true, approved_by_id = :approved_by_id WHERE id = :id AND approved = :false",
|
||||
["approved_by_id" => $user->id, "id" => $image_id, "true" => true, "false" => false]
|
||||
["approved_by_id"=>$user->id, "id"=>$image_id, "true"=>true, "false"=>false]
|
||||
);
|
||||
}
|
||||
|
||||
public static function disapprove_image(int $image_id): void
|
||||
public static function disapprove_image($image_id)
|
||||
{
|
||||
global $database;
|
||||
|
||||
$database->execute(
|
||||
"UPDATE images SET approved = :false, approved_by_id = NULL WHERE id = :id AND approved = :true",
|
||||
["id" => $image_id, "true" => true, "false" => false]
|
||||
["id"=>$image_id, "true"=>true, "false"=>false]
|
||||
);
|
||||
}
|
||||
|
||||
@ -211,13 +208,13 @@ class Approval extends Extension
|
||||
{
|
||||
global $user, $config;
|
||||
|
||||
if ($config->get_bool(ApprovalConfig::IMAGES) && $image['approved'] === false && !$user->can(Permissions::APPROVE_IMAGE) && $user->id !== $image->owner_id) {
|
||||
if ($config->get_bool(ApprovalConfig::IMAGES) && $image->approved===false && !$user->can(Permissions::APPROVE_IMAGE) && $user->id!==$image->owner_id) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function onImageDownloading(ImageDownloadingEvent $event): void
|
||||
public function onImageDownloading(ImageDownloadingEvent $event)
|
||||
{
|
||||
/**
|
||||
* Deny images upon insufficient permissions.
|
||||
@ -227,19 +224,19 @@ class Approval extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void
|
||||
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
|
||||
{
|
||||
global $user, $config;
|
||||
if ($user->can(Permissions::APPROVE_IMAGE) && $config->get_bool(ApprovalConfig::IMAGES)) {
|
||||
$event->add_part($this->theme->get_image_admin_html($event->image));
|
||||
$event->add_part((string)$this->theme->get_image_admin_html($event->image));
|
||||
}
|
||||
}
|
||||
|
||||
public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event): void
|
||||
public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event)
|
||||
{
|
||||
global $user, $config;
|
||||
|
||||
if ($user->can(Permissions::APPROVE_IMAGE) && $config->get_bool(ApprovalConfig::IMAGES)) {
|
||||
if ($user->can(Permissions::APPROVE_IMAGE)&& $config->get_bool(ApprovalConfig::IMAGES)) {
|
||||
if (in_array("approved:no", $event->search_terms)) {
|
||||
$event->add_action("bulk_approve_image", "Approve", "a");
|
||||
} else {
|
||||
@ -248,7 +245,7 @@ class Approval extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onBulkAction(BulkActionEvent $event): void
|
||||
public function onBulkAction(BulkActionEvent $event)
|
||||
{
|
||||
global $page, $user;
|
||||
|
||||
@ -276,7 +273,7 @@ class Approval extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void
|
||||
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
|
||||
{
|
||||
global $database;
|
||||
|
||||
|
@ -14,16 +14,16 @@ class ApprovalTheme extends Themelet
|
||||
{
|
||||
public function get_image_admin_html(Image $image): HTMLElement
|
||||
{
|
||||
if ($image['approved'] === true) {
|
||||
if ($image->approved===true) {
|
||||
$form = SHM_SIMPLE_FORM(
|
||||
'disapprove_image/'.$image->id,
|
||||
INPUT(["type" => 'hidden', "name" => 'image_id', "value" => $image->id]),
|
||||
INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image->id]),
|
||||
SHM_SUBMIT("Disapprove")
|
||||
);
|
||||
} else {
|
||||
$form = SHM_SIMPLE_FORM(
|
||||
'approve_image/'.$image->id,
|
||||
INPUT(["type" => 'hidden', "name" => 'image_id', "value" => $image->id]),
|
||||
INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image->id]),
|
||||
SHM_SUBMIT("Approve")
|
||||
);
|
||||
}
|
||||
@ -40,21 +40,21 @@ class ApprovalTheme extends Themelet
|
||||
);
|
||||
}
|
||||
|
||||
public function display_admin_block(SetupBuildingEvent $event): void
|
||||
public function display_admin_block(SetupBuildingEvent $event)
|
||||
{
|
||||
$sb = $event->panel->create_new_block("Approval");
|
||||
$sb->add_bool_option(ApprovalConfig::IMAGES, "Posts: ");
|
||||
}
|
||||
|
||||
public function display_admin_form(): void
|
||||
public function display_admin_form()
|
||||
{
|
||||
global $page;
|
||||
|
||||
$form = SHM_SIMPLE_FORM(
|
||||
"admin/approval",
|
||||
BUTTON(["name" => 'approval_action', "value" => 'approve_all'], "Approve All Posts"),
|
||||
BUTTON(["name"=>'approval_action', "value"=>'approve_all'], "Approve All Posts"),
|
||||
" ",
|
||||
BUTTON(["name" => 'approval_action', "value" => 'disapprove_all'], "Disapprove All Posts"),
|
||||
BUTTON(["name"=>'approval_action', "value"=>'disapprove_all'], "Disapprove All Posts"),
|
||||
);
|
||||
|
||||
$page->add_block(new Block("Approval", $form));
|
||||
|
@ -11,7 +11,7 @@ class ArtistsInfo extends ExtensionInfo
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Artists System";
|
||||
public string $url = self::SHIMMIE_URL;
|
||||
public array $authors = ["Sein Kraft" => "mail@seinkraft.info","Alpha" => "alpha@furries.com.ar"];
|
||||
public array $authors = ["Sein Kraft"=>"mail@seinkraft.info","Alpha"=>"alpha@furries.com.ar"];
|
||||
public string $license = self::LICENSE_GPLV2;
|
||||
public string $description = "Simple artists extension";
|
||||
public bool $beta = true;
|
||||
|
@ -19,23 +19,12 @@ class AuthorSetEvent extends Event
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-type ArtistArtist array{id:int,artist_id:int,user_name:string,name:string,notes:string,type:string,posts:int}
|
||||
* @phpstan-type ArtistAlias array{id:int,alias_id:int,alias_name:string,alias:string}
|
||||
* @phpstan-type ArtistMember array{id:int,name:string}
|
||||
* @phpstan-type ArtistUrl array{id:int,url:string}
|
||||
*/
|
||||
class Artists extends Extension
|
||||
{
|
||||
/** @var ArtistsTheme */
|
||||
protected Themelet $theme;
|
||||
|
||||
public function onInitExt(InitExtEvent $event): void
|
||||
{
|
||||
Image::$prop_types["author"] = ImagePropType::STRING;
|
||||
}
|
||||
|
||||
public function onImageInfoSet(ImageInfoSetEvent $event): void
|
||||
public function onImageInfoSet(ImageInfoSetEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if ($user->can(Permissions::EDIT_IMAGE_ARTIST) && isset($_POST["tag_edit__author"])) {
|
||||
@ -43,7 +32,7 @@ class Artists extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event): void
|
||||
public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
$artistName = $this->get_artistName_by_imageID($event->image->id);
|
||||
@ -52,7 +41,7 @@ class Artists extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onSearchTermParse(SearchTermParseEvent $event): void
|
||||
public function onSearchTermParse(SearchTermParseEvent $event)
|
||||
{
|
||||
if (is_null($event->term)) {
|
||||
return;
|
||||
@ -61,18 +50,18 @@ class Artists extends Extension
|
||||
$matches = [];
|
||||
if (preg_match("/^(author|artist)[=|:](.*)$/i", $event->term, $matches)) {
|
||||
$char = $matches[2];
|
||||
$event->add_querylet(new Querylet("author = :author_char", ["author_char" => $char]));
|
||||
$event->add_querylet(new Querylet("author = :author_char", ["author_char"=>$char]));
|
||||
}
|
||||
}
|
||||
|
||||
public function onHelpPageBuilding(HelpPageBuildingEvent $event): void
|
||||
public function onHelpPageBuilding(HelpPageBuildingEvent $event)
|
||||
{
|
||||
if ($event->key === HelpPages::SEARCH) {
|
||||
if ($event->key===HelpPages::SEARCH) {
|
||||
$event->add_block(new Block("Artist", $this->theme->get_help_html()));
|
||||
}
|
||||
}
|
||||
|
||||
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void
|
||||
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
|
||||
{
|
||||
global $config, $database;
|
||||
|
||||
@ -124,7 +113,7 @@ class Artists extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onAuthorSet(AuthorSetEvent $event): void
|
||||
public function onAuthorSet(AuthorSetEvent $event)
|
||||
{
|
||||
global $database;
|
||||
|
||||
@ -161,11 +150,11 @@ class Artists extends Extension
|
||||
|
||||
$database->execute(
|
||||
"UPDATE images SET author = :author WHERE id = :id",
|
||||
['author' => $artistName, 'id' => $event->image->id]
|
||||
['author'=>$artistName, 'id'=>$event->image->id]
|
||||
);
|
||||
}
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event): void
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $page, $user;
|
||||
|
||||
@ -220,7 +209,7 @@ class Artists extends Extension
|
||||
$userIsLogged = !$user->is_anonymous();
|
||||
$userIsAdmin = $user->can(Permissions::ARTISTS_ADMIN);
|
||||
|
||||
$images = Search::find_images(limit: 4, tags: Tag::explode($artist['name']));
|
||||
$images = Image::find_images(limit: 4, tags: Tag::explode($artist['name']));
|
||||
|
||||
$this->theme->show_artist($artist, $aliases, $members, $urls, $images, $userIsLogged, $userIsAdmin);
|
||||
/*
|
||||
@ -430,28 +419,28 @@ class Artists extends Extension
|
||||
private function get_artistName_by_imageID(int $imageID): string
|
||||
{
|
||||
global $database;
|
||||
$result = $database->get_row("SELECT author FROM images WHERE id = :id", ['id' => $imageID]);
|
||||
$result = $database->get_row("SELECT author FROM images WHERE id = :id", ['id'=>$imageID]);
|
||||
return $result['author'] ?? "";
|
||||
}
|
||||
|
||||
private function url_exists_by_url(string $url): bool
|
||||
{
|
||||
global $database;
|
||||
$result = $database->get_one("SELECT COUNT(1) FROM artist_urls WHERE url = :url", ['url' => $url]);
|
||||
$result = $database->get_one("SELECT COUNT(1) FROM artist_urls WHERE url = :url", ['url'=>$url]);
|
||||
return ($result != 0);
|
||||
}
|
||||
|
||||
private function member_exists_by_name(string $member): bool
|
||||
{
|
||||
global $database;
|
||||
$result = $database->get_one("SELECT COUNT(1) FROM artist_members WHERE name = :name", ['name' => $member]);
|
||||
$result = $database->get_one("SELECT COUNT(1) FROM artist_members WHERE name = :name", ['name'=>$member]);
|
||||
return ($result != 0);
|
||||
}
|
||||
|
||||
private function alias_exists_by_name(string $alias): bool
|
||||
{
|
||||
global $database;
|
||||
$result = $database->get_one("SELECT COUNT(1) FROM artist_alias WHERE alias = :alias", ['alias' => $alias]);
|
||||
$result = $database->get_one("SELECT COUNT(1) FROM artist_alias WHERE alias = :alias", ['alias'=>$alias]);
|
||||
return ($result != 0);
|
||||
}
|
||||
|
||||
@ -460,7 +449,7 @@ class Artists extends Extension
|
||||
global $database;
|
||||
$result = $database->get_one(
|
||||
"SELECT COUNT(1) FROM artist_alias WHERE artist_id = :artist_id AND alias = :alias",
|
||||
['artist_id' => $artistID, 'alias' => $alias]
|
||||
['artist_id'=>$artistID, 'alias'=>$alias]
|
||||
);
|
||||
return ($result != 0);
|
||||
}
|
||||
@ -468,85 +457,76 @@ class Artists extends Extension
|
||||
private function get_artistID_by_url(string $url): int
|
||||
{
|
||||
global $database;
|
||||
return (int)$database->get_one("SELECT artist_id FROM artist_urls WHERE url = :url", ['url' => $url]);
|
||||
return (int)$database->get_one("SELECT artist_id FROM artist_urls WHERE url = :url", ['url'=>$url]);
|
||||
}
|
||||
|
||||
private function get_artistID_by_memberName(string $member): int
|
||||
{
|
||||
global $database;
|
||||
return (int)$database->get_one("SELECT artist_id FROM artist_members WHERE name = :name", ['name' => $member]);
|
||||
return (int)$database->get_one("SELECT artist_id FROM artist_members WHERE name = :name", ['name'=>$member]);
|
||||
}
|
||||
|
||||
private function get_artistName_by_artistID(int $artistID): string
|
||||
{
|
||||
global $database;
|
||||
return (string)$database->get_one("SELECT name FROM artists WHERE id = :id", ['id' => $artistID]);
|
||||
return (string)$database->get_one("SELECT name FROM artists WHERE id = :id", ['id'=>$artistID]);
|
||||
}
|
||||
|
||||
private function get_artistID_by_aliasID(int $aliasID): int
|
||||
{
|
||||
global $database;
|
||||
return (int)$database->get_one("SELECT artist_id FROM artist_alias WHERE id = :id", ['id' => $aliasID]);
|
||||
return (int)$database->get_one("SELECT artist_id FROM artist_alias WHERE id = :id", ['id'=>$aliasID]);
|
||||
}
|
||||
|
||||
private function get_artistID_by_memberID(int $memberID): int
|
||||
{
|
||||
global $database;
|
||||
return (int)$database->get_one("SELECT artist_id FROM artist_members WHERE id = :id", ['id' => $memberID]);
|
||||
return (int)$database->get_one("SELECT artist_id FROM artist_members WHERE id = :id", ['id'=>$memberID]);
|
||||
}
|
||||
|
||||
private function get_artistID_by_urlID(int $urlID): int
|
||||
{
|
||||
global $database;
|
||||
return (int)$database->get_one("SELECT artist_id FROM artist_urls WHERE id = :id", ['id' => $urlID]);
|
||||
return (int)$database->get_one("SELECT artist_id FROM artist_urls WHERE id = :id", ['id'=>$urlID]);
|
||||
}
|
||||
|
||||
private function delete_alias(int $aliasID): void
|
||||
private function delete_alias(int $aliasID)
|
||||
{
|
||||
global $database;
|
||||
$database->execute("DELETE FROM artist_alias WHERE id = :id", ['id' => $aliasID]);
|
||||
$database->execute("DELETE FROM artist_alias WHERE id = :id", ['id'=>$aliasID]);
|
||||
}
|
||||
|
||||
private function delete_url(int $urlID): void
|
||||
private function delete_url(int $urlID)
|
||||
{
|
||||
global $database;
|
||||
$database->execute("DELETE FROM artist_urls WHERE id = :id", ['id' => $urlID]);
|
||||
$database->execute("DELETE FROM artist_urls WHERE id = :id", ['id'=>$urlID]);
|
||||
}
|
||||
|
||||
private function delete_member(int $memberID): void
|
||||
private function delete_member(int $memberID)
|
||||
{
|
||||
global $database;
|
||||
$database->execute("DELETE FROM artist_members WHERE id = :id", ['id' => $memberID]);
|
||||
$database->execute("DELETE FROM artist_members WHERE id = :id", ['id'=>$memberID]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArtistAlias
|
||||
*/
|
||||
private function get_alias_by_id(int $aliasID): array
|
||||
{
|
||||
global $database;
|
||||
return $database->get_row("SELECT * FROM artist_alias WHERE id = :id", ['id' => $aliasID]);
|
||||
return $database->get_row("SELECT * FROM artist_alias WHERE id = :id", ['id'=>$aliasID]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArtistUrl
|
||||
*/
|
||||
private function get_url_by_id(int $urlID): array
|
||||
{
|
||||
global $database;
|
||||
return $database->get_row("SELECT * FROM artist_urls WHERE id = :id", ['id' => $urlID]);
|
||||
return $database->get_row("SELECT * FROM artist_urls WHERE id = :id", ['id'=>$urlID]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArtistMember
|
||||
*/
|
||||
private function get_member_by_id(int $memberID): array
|
||||
{
|
||||
global $database;
|
||||
return $database->get_row("SELECT * FROM artist_members WHERE id = :id", ['id' => $memberID]);
|
||||
return $database->get_row("SELECT * FROM artist_members WHERE id = :id", ['id'=>$memberID]);
|
||||
}
|
||||
|
||||
private function update_artist(): void
|
||||
private function update_artist()
|
||||
{
|
||||
global $user;
|
||||
$inputs = validate_input([
|
||||
@ -578,13 +558,13 @@ class Artists extends Extension
|
||||
global $database;
|
||||
$database->execute(
|
||||
"UPDATE artists SET name = :name, notes = :notes, updated = now(), user_id = :user_id WHERE id = :id",
|
||||
['name' => $name, 'notes' => $notes, 'user_id' => $userID, 'id' => $artistID]
|
||||
['name'=>$name, 'notes'=>$notes, 'user_id'=>$userID, 'id'=>$artistID]
|
||||
);
|
||||
|
||||
// ALIAS MATCHING SECTION
|
||||
$i = 0;
|
||||
$aliasesAsArray = is_null($aliasesAsString) ? [] : explode(" ", $aliasesAsString);
|
||||
$aliasesIDsAsArray = is_null($aliasesIDsAsString) ? [] : array_map(fn ($n) => int_escape($n), explode(" ", $aliasesIDsAsString));
|
||||
$aliasesIDsAsArray = is_null($aliasesIDsAsString) ? [] : explode(" ", $aliasesIDsAsString);
|
||||
while ($i < count($aliasesAsArray)) {
|
||||
// if an alias was updated
|
||||
if ($i < count($aliasesIDsAsArray)) {
|
||||
@ -604,7 +584,7 @@ class Artists extends Extension
|
||||
// MEMBERS MATCHING SECTION
|
||||
$i = 0;
|
||||
$membersAsArray = is_null($membersAsString) ? [] : explode(" ", $membersAsString);
|
||||
$membersIDsAsArray = is_null($membersIDsAsString) ? [] : array_map(fn ($n) => int_escape($n), explode(" ", $membersIDsAsString));
|
||||
$membersIDsAsArray = is_null($membersIDsAsString) ? [] : explode(" ", $membersIDsAsString);
|
||||
while ($i < count($membersAsArray)) {
|
||||
// if a member was updated
|
||||
if ($i < count($membersIDsAsArray)) {
|
||||
@ -623,11 +603,10 @@ class Artists extends Extension
|
||||
|
||||
// URLS MATCHING SECTION
|
||||
$i = 0;
|
||||
assert(is_string($urlsAsString));
|
||||
$urlsAsString = str_replace("\r\n", "\n", $urlsAsString);
|
||||
$urlsAsString = str_replace("\n\r", "\n", $urlsAsString);
|
||||
$urlsAsArray = empty($urlsAsString) ? [] : explode("\n", $urlsAsString);
|
||||
$urlsIDsAsArray = is_null($urlsIDsAsString) ? [] : array_map(fn ($n) => int_escape($n), explode(" ", $urlsIDsAsString));
|
||||
$urlsAsArray = is_null($urlsAsString) ? [] : explode("\n", $urlsAsString);
|
||||
$urlsIDsAsArray = is_null($urlsIDsAsString) ? [] : explode(" ", $urlsIDsAsString);
|
||||
while ($i < count($urlsAsArray)) {
|
||||
// if an URL was updated
|
||||
if ($i < count($urlsIDsAsArray)) {
|
||||
@ -645,7 +624,7 @@ class Artists extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
private function update_alias(): void
|
||||
private function update_alias()
|
||||
{
|
||||
global $user;
|
||||
$inputs = validate_input([
|
||||
@ -655,16 +634,16 @@ class Artists extends Extension
|
||||
$this->save_existing_alias($inputs['aliasID'], $inputs['alias'], $user->id);
|
||||
}
|
||||
|
||||
private function save_existing_alias(int $aliasID, string $alias, int $userID): void
|
||||
private function save_existing_alias(int $aliasID, string $alias, int $userID)
|
||||
{
|
||||
global $database;
|
||||
$database->execute(
|
||||
"UPDATE artist_alias SET alias = :alias, updated = now(), user_id = :user_id WHERE id = :id",
|
||||
['alias' => $alias, 'user_id' => $userID, 'id' => $aliasID]
|
||||
['alias'=>$alias, 'user_id'=>$userID, 'id'=>$aliasID]
|
||||
);
|
||||
}
|
||||
|
||||
private function update_url(): void
|
||||
private function update_url()
|
||||
{
|
||||
global $user;
|
||||
$inputs = validate_input([
|
||||
@ -674,16 +653,16 @@ class Artists extends Extension
|
||||
$this->save_existing_url($inputs['urlID'], $inputs['url'], $user->id);
|
||||
}
|
||||
|
||||
private function save_existing_url(int $urlID, string $url, int $userID): void
|
||||
private function save_existing_url(int $urlID, string $url, int $userID)
|
||||
{
|
||||
global $database;
|
||||
$database->execute(
|
||||
"UPDATE artist_urls SET url = :url, updated = now(), user_id = :user_id WHERE id = :id",
|
||||
['url' => $url, 'user_id' => $userID, 'id' => $urlID]
|
||||
['url'=>$url, 'user_id'=>$userID, 'id'=>$urlID]
|
||||
);
|
||||
}
|
||||
|
||||
private function update_member(): void
|
||||
private function update_member()
|
||||
{
|
||||
global $user;
|
||||
$inputs = validate_input([
|
||||
@ -693,12 +672,12 @@ class Artists extends Extension
|
||||
$this->save_existing_member($inputs['memberID'], $inputs['name'], $user->id);
|
||||
}
|
||||
|
||||
private function save_existing_member(int $memberID, string $memberName, int $userID): void
|
||||
private function save_existing_member(int $memberID, string $memberName, int $userID)
|
||||
{
|
||||
global $database;
|
||||
$database->execute(
|
||||
"UPDATE artist_members SET name = :name, updated = now(), user_id = :user_id WHERE id = :id",
|
||||
['name' => $memberName, 'user_id' => $userID, 'id' => $memberID]
|
||||
['name'=>$memberName, 'user_id'=>$userID, 'id'=>$memberID]
|
||||
);
|
||||
}
|
||||
|
||||
@ -754,7 +733,6 @@ class Artists extends Extension
|
||||
}
|
||||
|
||||
if (!is_null($urls)) {
|
||||
assert(is_string($urls));
|
||||
//delete double "separators"
|
||||
$urls = str_replace("\r\n", "\n", $urls);
|
||||
$urls = str_replace("\n\r", "\n", $urls);
|
||||
@ -775,7 +753,7 @@ class Artists extends Extension
|
||||
$database->execute("
|
||||
INSERT INTO artists (user_id, name, notes, created, updated)
|
||||
VALUES (:user_id, :name, :notes, now(), now())
|
||||
", ['user_id' => $user->id, 'name' => $name, 'notes' => $notes]);
|
||||
", ['user_id'=>$user->id, 'name'=>$name, 'notes'=>$notes]);
|
||||
return $database->get_last_insert_id('artists_id_seq');
|
||||
}
|
||||
|
||||
@ -784,20 +762,17 @@ class Artists extends Extension
|
||||
global $database;
|
||||
$result = $database->get_one(
|
||||
"SELECT COUNT(1) FROM artists WHERE name = :name",
|
||||
['name' => $name]
|
||||
['name'=>$name]
|
||||
);
|
||||
return ($result != 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArtistArtist
|
||||
*/
|
||||
private function get_artist(int $artistID): array
|
||||
{
|
||||
global $database;
|
||||
$result = $database->get_row(
|
||||
"SELECT * FROM artists WHERE id = :id",
|
||||
['id' => $artistID]
|
||||
['id'=>$artistID]
|
||||
);
|
||||
|
||||
$result["name"] = stripslashes($result["name"]);
|
||||
@ -806,15 +781,12 @@ class Artists extends Extension
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArtistMember[]
|
||||
*/
|
||||
private function get_members(int $artistID): array
|
||||
{
|
||||
global $database;
|
||||
$result = $database->get_all(
|
||||
"SELECT * FROM artist_members WHERE artist_id = :artist_id",
|
||||
['artist_id' => $artistID]
|
||||
['artist_id'=>$artistID]
|
||||
);
|
||||
|
||||
$num = count($result);
|
||||
@ -825,15 +797,12 @@ class Artists extends Extension
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArtistUrl[]
|
||||
*/
|
||||
private function get_urls(int $artistID): array
|
||||
{
|
||||
global $database;
|
||||
$result = $database->get_all(
|
||||
"SELECT id, url FROM artist_urls WHERE artist_id = :artist_id",
|
||||
['artist_id' => $artistID]
|
||||
['artist_id'=>$artistID]
|
||||
);
|
||||
|
||||
$num = count($result);
|
||||
@ -849,7 +818,7 @@ class Artists extends Extension
|
||||
global $database;
|
||||
return (int)$database->get_one(
|
||||
"SELECT id FROM artists WHERE name = :name",
|
||||
['name' => $name]
|
||||
['name'=>$name]
|
||||
);
|
||||
}
|
||||
|
||||
@ -859,23 +828,23 @@ class Artists extends Extension
|
||||
|
||||
return (int)$database->get_one(
|
||||
"SELECT artist_id FROM artist_alias WHERE alias = :alias",
|
||||
['alias' => $alias]
|
||||
['alias'=>$alias]
|
||||
);
|
||||
}
|
||||
|
||||
private function delete_artist(int $artistID): void
|
||||
private function delete_artist(int $artistID)
|
||||
{
|
||||
global $database;
|
||||
$database->execute(
|
||||
"DELETE FROM artists WHERE id = :id",
|
||||
['id' => $artistID]
|
||||
['id'=>$artistID]
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* HERE WE GET THE LIST OF ALL ARTIST WITH PAGINATION
|
||||
*/
|
||||
private function get_listing(PageRequestEvent $event): void
|
||||
private function get_listing(PageRequestEvent $event)
|
||||
{
|
||||
global $config, $database;
|
||||
|
||||
@ -931,8 +900,8 @@ class Artists extends Extension
|
||||
LIMIT :offset, :limit
|
||||
",
|
||||
[
|
||||
"offset" => $pageNumber * $artistsPerPage,
|
||||
"limit" => $artistsPerPage
|
||||
"offset"=>$pageNumber * $artistsPerPage,
|
||||
"limit"=>$artistsPerPage
|
||||
]
|
||||
);
|
||||
|
||||
@ -953,7 +922,7 @@ class Artists extends Extension
|
||||
ON a.id = aa.artist_id
|
||||
");
|
||||
|
||||
$totalPages = (int)ceil($count / $artistsPerPage);
|
||||
$totalPages = ceil($count / $artistsPerPage);
|
||||
|
||||
$this->theme->list_artists($listing, $pageNumber + 1, $totalPages);
|
||||
}
|
||||
@ -961,7 +930,7 @@ class Artists extends Extension
|
||||
/*
|
||||
* HERE WE ADD AN ALIAS
|
||||
*/
|
||||
private function add_urls(): void
|
||||
private function add_urls()
|
||||
{
|
||||
global $user;
|
||||
$inputs = validate_input([
|
||||
@ -978,17 +947,17 @@ class Artists extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
private function save_new_url(int $artistID, string $url, int $userID): void
|
||||
private function save_new_url(int $artistID, string $url, int $userID)
|
||||
{
|
||||
global $database;
|
||||
|
||||
$database->execute(
|
||||
"INSERT INTO artist_urls (artist_id, created, updated, url, user_id) VALUES (:artist_id, now(), now(), :url, :user_id)",
|
||||
['artist' => $artistID, 'url' => $url, 'user_id' => $userID]
|
||||
['artist'=>$artistID, 'url'=>$url, 'user_id'=>$userID]
|
||||
);
|
||||
}
|
||||
|
||||
private function add_alias(): void
|
||||
private function add_alias()
|
||||
{
|
||||
global $user;
|
||||
$inputs = validate_input([
|
||||
@ -1005,17 +974,17 @@ class Artists extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
private function save_new_alias(int $artistID, string $alias, int $userID): void
|
||||
private function save_new_alias(int $artistID, string $alias, int $userID)
|
||||
{
|
||||
global $database;
|
||||
|
||||
$database->execute(
|
||||
"INSERT INTO artist_alias (artist_id, created, updated, alias, user_id) VALUES (:artist_id, now(), now(), :alias, :user_id)",
|
||||
['artist_id' => $artistID, 'alias' => $alias, 'user_id' => $userID]
|
||||
['artist_id'=>$artistID, 'alias'=>$alias, 'user_id'=>$userID]
|
||||
);
|
||||
}
|
||||
|
||||
private function add_members(): void
|
||||
private function add_members()
|
||||
{
|
||||
global $user;
|
||||
$inputs = validate_input([
|
||||
@ -1032,13 +1001,13 @@ class Artists extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
private function save_new_member(int $artistID, string $member, int $userID): void
|
||||
private function save_new_member(int $artistID, string $member, int $userID)
|
||||
{
|
||||
global $database;
|
||||
|
||||
$database->execute(
|
||||
"INSERT INTO artist_members (artist_id, name, created, updated, user_id) VALUES (:artist_id, :name, now(), now(), :user_id)",
|
||||
['artist' => $artistID, 'name' => $member, 'user_id' => $userID]
|
||||
['artist'=>$artistID, 'name'=>$member, 'user_id'=>$userID]
|
||||
);
|
||||
}
|
||||
|
||||
@ -1048,7 +1017,7 @@ class Artists extends Extension
|
||||
|
||||
$result = $database->get_one(
|
||||
"SELECT COUNT(1) FROM artist_members WHERE artist_id = :artist_id AND name = :name",
|
||||
['artist_id' => $artistID, 'name' => $member]
|
||||
['artist_id'=>$artistID, 'name'=>$member]
|
||||
);
|
||||
return ($result != 0);
|
||||
}
|
||||
@ -1059,15 +1028,13 @@ class Artists extends Extension
|
||||
|
||||
$result = $database->get_one(
|
||||
"SELECT COUNT(1) FROM artist_urls WHERE artist_id = :artist_id AND url = :url",
|
||||
['artist_id' => $artistID, 'url' => $url]
|
||||
['artist_id'=>$artistID, 'url'=>$url]
|
||||
);
|
||||
return ($result != 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* HERE WE GET THE INFO OF THE ALIAS
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function get_alias(int $artistID): array
|
||||
{
|
||||
@ -1078,7 +1045,7 @@ class Artists extends Extension
|
||||
FROM artist_alias
|
||||
WHERE artist_id = :artist_id
|
||||
ORDER BY alias ASC
|
||||
", ['artist_id' => $artistID]);
|
||||
", ['artist_id'=>$artistID]);
|
||||
|
||||
$rc = count($result);
|
||||
for ($i = 0 ; $i < $rc ; $i++) {
|
||||
|
@ -6,7 +6,7 @@ namespace Shimmie2;
|
||||
|
||||
class ArtistsTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testSearch(): void
|
||||
public function testSearch()
|
||||
{
|
||||
global $user;
|
||||
$this->log_in_as_user();
|
||||
|
@ -7,26 +7,20 @@ namespace Shimmie2;
|
||||
use MicroHTML\HTMLElement;
|
||||
|
||||
use function MicroHTML\emptyHTML;
|
||||
use function MicroHTML\{INPUT,P};
|
||||
use function MicroHTML\{INPUT,P,SPAN,TD,TH,TR};
|
||||
|
||||
/**
|
||||
* @phpstan-type ArtistArtist array{id:int,artist_id:int,user_name:string,name:string,notes:string,type:string,posts:int}
|
||||
* @phpstan-type ArtistAlias array{id:int,alias_id:int,alias_name:string,alias:string}
|
||||
* @phpstan-type ArtistMember array{id:int,name:string}
|
||||
* @phpstan-type ArtistUrl array{id:int,url:string}
|
||||
*/
|
||||
class ArtistsTheme extends Themelet
|
||||
{
|
||||
public function get_author_editor_html(string $author): HTMLElement
|
||||
public function get_author_editor_html(string $author): string
|
||||
{
|
||||
return SHM_POST_INFO(
|
||||
"Author",
|
||||
$author,
|
||||
INPUT(["type" => "text", "name" => "tag_edit__author", "value" => $author])
|
||||
);
|
||||
$h_author = html_escape($author);
|
||||
return (string)TR(TH("Author", TD(
|
||||
SPAN(["class"=>"view"], $h_author),
|
||||
INPUT(["class"=>"edit", "type"=>"text", "name"=>"tag_edit__author", "value"=>$h_author])
|
||||
)));
|
||||
}
|
||||
|
||||
public function sidebar_options(string $mode, ?int $artistID = null, bool $is_admin = false): void
|
||||
public function sidebar_options(string $mode, ?int $artistID=null, $is_admin=false): void
|
||||
{
|
||||
global $page, $user;
|
||||
|
||||
@ -83,13 +77,7 @@ class ArtistsTheme extends Themelet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ArtistArtist $artist
|
||||
* @param ArtistAlias[] $aliases
|
||||
* @param ArtistMember[] $members
|
||||
* @param ArtistUrl[] $urls
|
||||
*/
|
||||
public function show_artist_editor(array $artist, array $aliases, array $members, array $urls): void
|
||||
public function show_artist_editor($artist, $aliases, $members, $urls)
|
||||
{
|
||||
global $user;
|
||||
|
||||
@ -124,7 +112,7 @@ class ArtistsTheme extends Themelet
|
||||
$urlsString .= $url["url"]."\n";
|
||||
$urlsIDsString .= $url["id"]." ";
|
||||
}
|
||||
$urlsString = substr($urlsString, 0, strlen($urlsString) - 1);
|
||||
$urlsString = substr($urlsString, 0, strlen($urlsString) -1);
|
||||
$urlsIDsString = rtrim($urlsIDsString);
|
||||
|
||||
$html = '
|
||||
@ -149,7 +137,7 @@ class ArtistsTheme extends Themelet
|
||||
$page->add_block(new Block("Edit artist", $html, "main", 10));
|
||||
}
|
||||
|
||||
public function new_artist_composer(): void
|
||||
public function new_artist_composer()
|
||||
{
|
||||
global $page, $user;
|
||||
|
||||
@ -170,10 +158,7 @@ class ArtistsTheme extends Themelet
|
||||
$page->add_block(new Block("Artists", $html, "main", 10));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ArtistArtist[] $artists
|
||||
*/
|
||||
public function list_artists(array $artists, int $pageNumber, int $totalPages): void
|
||||
public function list_artists($artists, $pageNumber, $totalPages)
|
||||
{
|
||||
global $user, $page;
|
||||
|
||||
@ -252,7 +237,7 @@ class ArtistsTheme extends Themelet
|
||||
$this->display_paginator($page, "artist/list", null, $pageNumber, $totalPages);
|
||||
}
|
||||
|
||||
public function show_new_alias_composer(int $artistID): void
|
||||
public function show_new_alias_composer($artistID)
|
||||
{
|
||||
global $user;
|
||||
|
||||
@ -271,7 +256,7 @@ class ArtistsTheme extends Themelet
|
||||
$page->add_block(new Block("Artist Aliases", $html, "main", 20));
|
||||
}
|
||||
|
||||
public function show_new_member_composer(int $artistID): void
|
||||
public function show_new_member_composer($artistID)
|
||||
{
|
||||
global $user;
|
||||
|
||||
@ -290,7 +275,7 @@ class ArtistsTheme extends Themelet
|
||||
$page->add_block(new Block("Artist members", $html, "main", 30));
|
||||
}
|
||||
|
||||
public function show_new_url_composer(int $artistID): void
|
||||
public function show_new_url_composer($artistID)
|
||||
{
|
||||
global $user;
|
||||
|
||||
@ -309,10 +294,7 @@ class ArtistsTheme extends Themelet
|
||||
$page->add_block(new Block("Artist URLs", $html, "main", 40));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ArtistAlias $alias
|
||||
*/
|
||||
public function show_alias_editor(array $alias): void
|
||||
public function show_alias_editor($alias)
|
||||
{
|
||||
global $user;
|
||||
|
||||
@ -330,10 +312,7 @@ class ArtistsTheme extends Themelet
|
||||
$page->add_block(new Block("Edit Alias", $html, "main", 10));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ArtistUrl $url
|
||||
*/
|
||||
public function show_url_editor(array $url): void
|
||||
public function show_url_editor($url)
|
||||
{
|
||||
global $user;
|
||||
|
||||
@ -351,10 +330,7 @@ class ArtistsTheme extends Themelet
|
||||
$page->add_block(new Block("Edit URL", $html, "main", 10));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ArtistMember $member
|
||||
*/
|
||||
public function show_member_editor(array $member): void
|
||||
public function show_member_editor($member)
|
||||
{
|
||||
global $user;
|
||||
|
||||
@ -372,18 +348,11 @@ class ArtistsTheme extends Themelet
|
||||
$page->add_block(new Block("Edit Member", $html, "main", 10));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ArtistArtist $artist
|
||||
* @param ArtistAlias[] $aliases
|
||||
* @param ArtistMember[] $members
|
||||
* @param ArtistUrl[] $urls
|
||||
* @param Image[] $images
|
||||
*/
|
||||
public function show_artist(array $artist, array $aliases, array $members, array $urls, array $images, bool $userIsLogged, bool $userIsAdmin): void
|
||||
public function show_artist($artist, $aliases, $members, $urls, $images, $userIsLogged, $userIsAdmin)
|
||||
{
|
||||
global $page;
|
||||
|
||||
$artist_link = "<a href='".search_link([$artist['name']])."'>".str_replace("_", " ", $artist['name'])."</a>";
|
||||
$artist_link = "<a href='".make_link("post/list/".$artist['name']."/1")."'>".str_replace("_", " ", $artist['name'])."</a>";
|
||||
|
||||
$html = "<table id='poolsList' class='zebra'>
|
||||
<thead>
|
||||
@ -447,9 +416,6 @@ class ArtistsTheme extends Themelet
|
||||
$page->add_block(new Block("Artist Posts", $artist_images, "main", 20));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ArtistAlias[] $aliases
|
||||
*/
|
||||
private function render_aliases(array $aliases, bool $userIsLogged, bool $userIsAdmin): string
|
||||
{
|
||||
$html = "";
|
||||
@ -496,9 +462,6 @@ class ArtistsTheme extends Themelet
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ArtistMember[] $members
|
||||
*/
|
||||
private function render_members(array $members, bool $userIsLogged, bool $userIsAdmin): string
|
||||
{
|
||||
$html = "";
|
||||
@ -543,9 +506,6 @@ class ArtistsTheme extends Themelet
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ArtistUrl[] $urls
|
||||
*/
|
||||
private function render_urls(array $urls, bool $userIsLogged, bool $userIsAdmin): string
|
||||
{
|
||||
$html = "";
|
||||
|
@ -10,7 +10,7 @@ class AutoTaggerInfo extends ExtensionInfo
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Auto-Tagger";
|
||||
public array $authors = ["Matthew Barbour" => "matthew@darkholme.net"];
|
||||
public array $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public string $license = self::LICENSE_WTFPL;
|
||||
public string $description = "Provides several automatic tagging functions";
|
||||
}
|
||||
|
@ -21,12 +21,12 @@ class AutoTaggerTable extends Table
|
||||
$this->size = 100;
|
||||
$this->limit = 1000000;
|
||||
$this->set_columns([
|
||||
new AutoCompleteColumn("tag", "Tag"),
|
||||
new AutoCompleteColumn("additional_tags", "Additional Tags"),
|
||||
new TextColumn("tag", "Tag"),
|
||||
new TextColumn("additional_tags", "Additional Tags"),
|
||||
new ActionColumn("tag"),
|
||||
]);
|
||||
$this->order_by = ["tag"];
|
||||
$this->table_attrs = ["class" => "zebra form"];
|
||||
$this->table_attrs = ["class" => "zebra"];
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@ class AutoTagger extends Extension
|
||||
/** @var AutoTaggerTheme */
|
||||
protected Themelet $theme;
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event): void
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $config, $database, $page, $user;
|
||||
|
||||
@ -75,7 +75,7 @@ class AutoTagger extends Extension
|
||||
if ($event->get_arg(0) == "add") {
|
||||
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
|
||||
$user->ensure_authed();
|
||||
$input = validate_input(["c_tag" => "string", "c_additional_tags" => "string"]);
|
||||
$input = validate_input(["c_tag"=>"string", "c_additional_tags"=>"string"]);
|
||||
try {
|
||||
send_event(new AddAutoTagEvent($input['c_tag'], $input['c_additional_tags']));
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
@ -87,7 +87,7 @@ class AutoTagger extends Extension
|
||||
} elseif ($event->get_arg(0) == "remove") {
|
||||
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
|
||||
$user->ensure_authed();
|
||||
$input = validate_input(["d_tag" => "string"]);
|
||||
$input = validate_input(["d_tag"=>"string"]);
|
||||
send_event(new DeleteAutoTagEvent($input['d_tag']));
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("auto_tag/list"));
|
||||
@ -111,7 +111,7 @@ class AutoTagger extends Extension
|
||||
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
|
||||
if (count($_FILES) > 0) {
|
||||
$tmp = $_FILES['auto_tag_file']['tmp_name'];
|
||||
$contents = file_get_contents_ex($tmp);
|
||||
$contents = file_get_contents($tmp);
|
||||
$count = $this->add_auto_tag_csv($contents);
|
||||
log_info(AutoTaggerInfo::KEY, "Imported $count auto-tag definitions from file from file", "Imported $count auto-tag definitions");
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
@ -126,14 +126,14 @@ class AutoTagger extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
|
||||
{
|
||||
if ($event->parent == "tags") {
|
||||
if ($event->parent=="tags") {
|
||||
$event->add_nav_link("auto_tag", new Link('auto_tag/list'), "Auto-Tag", NavLink::is_active(["auto_tag"]));
|
||||
}
|
||||
}
|
||||
|
||||
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void
|
||||
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
|
||||
{
|
||||
global $database;
|
||||
|
||||
@ -153,27 +153,27 @@ class AutoTagger extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onTagSet(TagSetEvent $event): void
|
||||
public function onTagSet(TagSetEvent $event)
|
||||
{
|
||||
$results = $this->apply_auto_tags($event->new_tags);
|
||||
$results = $this->apply_auto_tags($event->tags);
|
||||
if (!empty($results)) {
|
||||
$event->new_tags = $results;
|
||||
$event->tags = $results;
|
||||
}
|
||||
}
|
||||
|
||||
public function onAddAutoTag(AddAutoTagEvent $event): void
|
||||
public function onAddAutoTag(AddAutoTagEvent $event)
|
||||
{
|
||||
global $page;
|
||||
$this->add_auto_tag($event->tag, $event->additional_tags);
|
||||
$page->flash("Added Auto-Tag");
|
||||
}
|
||||
|
||||
public function onDeleteAutoTag(DeleteAutoTagEvent $event): void
|
||||
public function onDeleteAutoTag(DeleteAutoTagEvent $event)
|
||||
{
|
||||
$this->remove_auto_tag($event->tag);
|
||||
}
|
||||
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event): void
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
|
||||
@ -186,7 +186,6 @@ class AutoTagger extends Extension
|
||||
$csv = "";
|
||||
$pairs = $database->get_pairs("SELECT tag, additional_tags FROM auto_tag ORDER BY tag");
|
||||
foreach ($pairs as $old => $new) {
|
||||
assert(is_string($new));
|
||||
$csv .= "\"$old\",\"$new\"\n";
|
||||
}
|
||||
return $csv;
|
||||
@ -210,10 +209,10 @@ class AutoTagger extends Extension
|
||||
return $i;
|
||||
}
|
||||
|
||||
private function add_auto_tag(string $tag, string $additional_tags): void
|
||||
private function add_auto_tag(string $tag, string $additional_tags)
|
||||
{
|
||||
global $database;
|
||||
$existing_tags = $database->get_one("SELECT additional_tags FROM auto_tag WHERE LOWER(tag)=LOWER(:tag)", ["tag" => $tag]);
|
||||
$existing_tags = $database->get_one("SELECT additional_tags FROM auto_tag WHERE LOWER(tag)=LOWER(:tag)", ["tag"=>$tag]);
|
||||
if (!is_null($existing_tags)) {
|
||||
// Auto Tags already exist, so we will append new tags to the existing one
|
||||
$tag = Tag::sanitize($tag);
|
||||
@ -227,7 +226,7 @@ class AutoTagger extends Extension
|
||||
|
||||
$database->execute(
|
||||
"UPDATE auto_tag set additional_tags=:existing_tags where tag=:tag",
|
||||
["tag" => $tag, "existing_tags" => Tag::implode($existing_tags)]
|
||||
["tag"=>$tag, "existing_tags"=>Tag::implode($existing_tags)]
|
||||
);
|
||||
log_info(
|
||||
AutoTaggerInfo::KEY,
|
||||
@ -239,7 +238,7 @@ class AutoTagger extends Extension
|
||||
|
||||
$database->execute(
|
||||
"INSERT INTO auto_tag(tag, additional_tags) VALUES(:tag, :additional_tags)",
|
||||
["tag" => $tag, "additional_tags" => Tag::implode($additional_tags)]
|
||||
["tag"=>$tag, "additional_tags"=>Tag::implode($additional_tags)]
|
||||
);
|
||||
|
||||
log_info(
|
||||
@ -251,12 +250,12 @@ class AutoTagger extends Extension
|
||||
$this->apply_new_auto_tag($tag);
|
||||
}
|
||||
|
||||
private function apply_new_auto_tag(string $tag): void
|
||||
private function apply_new_auto_tag(string $tag)
|
||||
{
|
||||
global $database;
|
||||
$tag_id = $database->get_one("SELECT id FROM tags WHERE LOWER(tag) = LOWER(:tag)", ["tag" => $tag]);
|
||||
$tag_id = $database->get_one("SELECT id FROM tags WHERE LOWER(tag) = LOWER(:tag)", ["tag"=>$tag]);
|
||||
if (!empty($tag_id)) {
|
||||
$image_ids = $database->get_col_iterable("SELECT image_id FROM image_tags WHERE tag_id = :tag_id", ["tag_id" => $tag_id]);
|
||||
$image_ids = $database->get_col_iterable("SELECT image_id FROM image_tags WHERE tag_id = :tag_id", ["tag_id"=>$tag_id]);
|
||||
foreach ($image_ids as $image_id) {
|
||||
$image_id = (int) $image_id;
|
||||
$image = Image::by_id($image_id);
|
||||
@ -265,7 +264,7 @@ class AutoTagger extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
private function remove_auto_tag(string $tag): void
|
||||
private function remove_auto_tag(String $tag)
|
||||
{
|
||||
global $database;
|
||||
|
||||
@ -273,10 +272,9 @@ class AutoTagger extends Extension
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $tags_mixed
|
||||
* @return string[]
|
||||
* #param string[] $tags_mixed
|
||||
*/
|
||||
private function apply_auto_tags(array $tags_mixed): array
|
||||
private function apply_auto_tags(array $tags_mixed): ?array
|
||||
{
|
||||
global $database;
|
||||
|
||||
|
@ -6,14 +6,14 @@ namespace Shimmie2;
|
||||
|
||||
class AutoTaggerTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testAutoTaggerList(): void
|
||||
public function testAutoTaggerList()
|
||||
{
|
||||
$this->get_page('auto_tag/list');
|
||||
$this->assert_response(200);
|
||||
$this->assert_title("Auto-Tag");
|
||||
}
|
||||
|
||||
public function testAutoTaggerListReadOnly(): void
|
||||
public function testAutoTaggerListReadOnly()
|
||||
{
|
||||
$this->log_in_as_user();
|
||||
$this->get_page('auto_tag/list');
|
||||
@ -26,7 +26,7 @@ class AutoTaggerTest extends ShimmiePHPUnitTestCase
|
||||
$this->assert_no_text("value=\"Add\"");
|
||||
}
|
||||
|
||||
public function testAutoTagger(): void
|
||||
public function testAutoTagger()
|
||||
{
|
||||
$this->log_in_as_admin();
|
||||
|
||||
|
@ -4,8 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use MicroHTML\HTMLElement;
|
||||
|
||||
class AutoTaggerTheme extends Themelet
|
||||
{
|
||||
/**
|
||||
@ -13,7 +11,7 @@ class AutoTaggerTheme extends Themelet
|
||||
*
|
||||
* Note: $can_manage = whether things like "add new alias" should be shown
|
||||
*/
|
||||
public function display_auto_tagtable(HTMLElement $table, HTMLElement $paginator): void
|
||||
public function display_auto_tagtable($table, $paginator): void
|
||||
{
|
||||
global $page, $user;
|
||||
|
||||
|
@ -10,6 +10,6 @@ class AutoCompleteInfo extends ExtensionInfo
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Autocomplete";
|
||||
public array $authors = ["Daku" => "admin@codeanimu.net"];
|
||||
public array $authors = ["Daku"=>"admin@codeanimu.net"];
|
||||
public string $description = "Adds autocomplete to search & tagging.";
|
||||
}
|
||||
|
7
ext/autocomplete/lib/jquery-ui.min.css
vendored
Normal file
7
ext/autocomplete/lib/jquery-ui.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
13
ext/autocomplete/lib/jquery-ui.min.js
vendored
Normal file
13
ext/autocomplete/lib/jquery-ui.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
ext/autocomplete/lib/jquery-ui.theme.min.css
vendored
Normal file
5
ext/autocomplete/lib/jquery-ui.theme.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
69
ext/autocomplete/lib/jquery.tagit.css
Normal file
69
ext/autocomplete/lib/jquery.tagit.css
Normal file
@ -0,0 +1,69 @@
|
||||
ul.tagit {
|
||||
padding: 1px 5px;
|
||||
overflow: auto;
|
||||
margin-left: inherit; /* usually we don't want the regular ul margins. */
|
||||
margin-right: inherit;
|
||||
}
|
||||
ul.tagit li {
|
||||
display: block;
|
||||
float: left;
|
||||
margin: 2px 5px 2px 0;
|
||||
}
|
||||
ul.tagit li.tagit-choice {
|
||||
position: relative;
|
||||
line-height: inherit;
|
||||
}
|
||||
input.tagit-hidden-field {
|
||||
display: none;
|
||||
}
|
||||
ul.tagit li.tagit-choice-read-only {
|
||||
padding: .2em .5em .2em .5em;
|
||||
}
|
||||
|
||||
ul.tagit li.tagit-choice-editable {
|
||||
padding: .2em 18px .2em .5em;
|
||||
}
|
||||
|
||||
ul.tagit li.tagit-new {
|
||||
padding: .25em 4px .25em 0;
|
||||
}
|
||||
|
||||
ul.tagit li.tagit-choice a.tagit-label {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
ul.tagit li.tagit-choice .tagit-close {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: .1em;
|
||||
top: 50%;
|
||||
margin-top: -8px;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
/* used for some custom themes that don't need image icons */
|
||||
ul.tagit li.tagit-choice .tagit-close .text-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
ul.tagit li.tagit-choice input {
|
||||
display: block;
|
||||
float: left;
|
||||
margin: 2px 5px 2px 0;
|
||||
}
|
||||
ul.tagit input[type="text"] {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
|
||||
-moz-box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: inherit;
|
||||
background-color: inherit;
|
||||
outline: none;
|
||||
}
|
18
ext/autocomplete/lib/tag-it.min.js
vendored
Normal file
18
ext/autocomplete/lib/tag-it.min.js
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
//Removed TAB keybind
|
||||
;(function(b){b.widget("ui.tagit",{options:{allowDuplicates:!1,caseSensitive:!0,fieldName:"tags",placeholderText:null,readOnly:!1,removeConfirmation:!1,tagLimit:null,availableTags:[],autocomplete:{},showAutocompleteOnFocus:!1,allowSpaces:!1,singleField:!1,singleFieldDelimiter:",",singleFieldNode:null,animate:!0,tabIndex:null,beforeTagAdded:null,afterTagAdded:null,beforeTagRemoved:null,afterTagRemoved:null,onTagClicked:null,onTagLimitExceeded:null,onTagAdded:null,onTagRemoved:null,tagSource:null},_create:function(){var a=
|
||||
this;this.element.is("input")?(this.tagList=b("<ul></ul>").insertAfter(this.element),this.options.singleField=!0,this.options.singleFieldNode=this.element,this.element.addClass("tagit-hidden-field")):this.tagList=this.element.find("ul, ol").andSelf().last();this.tagInput=b('<input type="text" />').addClass("ui-widget-content");this.options.readOnly&&this.tagInput.attr("disabled","disabled");this.options.tabIndex&&this.tagInput.attr("tabindex",this.options.tabIndex);this.options.placeholderText&&this.tagInput.attr("placeholder",
|
||||
this.options.placeholderText);this.options.autocomplete.source||(this.options.autocomplete.source=function(a,e){var d=a.term.toLowerCase(),c=b.grep(this.options.availableTags,function(a){return 0===a.toLowerCase().indexOf(d)});this.options.allowDuplicates||(c=this._subtractArray(c,this.assignedTags()));e(c)});this.options.showAutocompleteOnFocus&&(this.tagInput.focus(function(b,d){a._showAutocomplete()}),"undefined"===typeof this.options.autocomplete.minLength&&(this.options.autocomplete.minLength=
|
||||
0));b.isFunction(this.options.autocomplete.source)&&(this.options.autocomplete.source=b.proxy(this.options.autocomplete.source,this));b.isFunction(this.options.tagSource)&&(this.options.tagSource=b.proxy(this.options.tagSource,this));this.tagList.addClass("tagit").addClass("ui-widget ui-widget-content ui-corner-all").append(b('<li class="tagit-new"></li>').append(this.tagInput)).click(function(d){var c=b(d.target);c.hasClass("tagit-label")?(c=c.closest(".tagit-choice"),c.hasClass("removed")||a._trigger("onTagClicked",
|
||||
d,{tag:c,tagLabel:a.tagLabel(c)})):a.tagInput.focus()});var c=!1;if(this.options.singleField)if(this.options.singleFieldNode){var d=b(this.options.singleFieldNode),f=d.val().split(this.options.singleFieldDelimiter);d.val("");b.each(f,function(b,d){a.createTag(d,null,!0);c=!0})}else this.options.singleFieldNode=b('<input type="hidden" style="display:none;" value="" name="'+this.options.fieldName+'" />'),this.tagList.after(this.options.singleFieldNode);c||this.tagList.children("li").each(function(){b(this).hasClass("tagit-new")||
|
||||
(a.createTag(b(this).text(),b(this).attr("class"),!0),b(this).remove())});this.tagInput.keydown(function(c){if(c.which==b.ui.keyCode.BACKSPACE&&""===a.tagInput.val()){var d=a._lastTag();!a.options.removeConfirmation||d.hasClass("remove")?a.removeTag(d):a.options.removeConfirmation&&d.addClass("remove ui-state-highlight")}else a.options.removeConfirmation&&a._lastTag().removeClass("remove ui-state-highlight");if(c.which===b.ui.keyCode.COMMA&&!1===c.shiftKey||c.which===b.ui.keyCode.ENTER||c.which==
|
||||
c.which==b.ui.keyCode.SPACE&&!0!==a.options.allowSpaces&&('"'!=b.trim(a.tagInput.val()).replace(/^s*/,"").charAt(0)||'"'==b.trim(a.tagInput.val()).charAt(0)&&'"'==b.trim(a.tagInput.val()).charAt(b.trim(a.tagInput.val()).length-1)&&0!==b.trim(a.tagInput.val()).length-1))c.which===b.ui.keyCode.ENTER&&""===a.tagInput.val()||c.preventDefault(),a.options.autocomplete.autoFocus&&a.tagInput.data("autocomplete-open")||(a.tagInput.autocomplete("close"),a.createTag(a._cleanedInput()))}).blur(function(b){a.tagInput.data("autocomplete-open")||
|
||||
a.createTag(a._cleanedInput())});if(this.options.availableTags||this.options.tagSource||this.options.autocomplete.source)d={select:function(b,c){a.createTag(c.item.value);return!1}},b.extend(d,this.options.autocomplete),d.source=this.options.tagSource||d.source,this.tagInput.autocomplete(d).bind("autocompleteopen.tagit",function(b,c){a.tagInput.data("autocomplete-open",!0)}).bind("autocompleteclose.tagit",function(b,c){a.tagInput.data("autocomplete-open",!1)}),this.tagInput.autocomplete("widget").addClass("tagit-autocomplete")},
|
||||
destroy:function(){b.Widget.prototype.destroy.call(this);this.element.unbind(".tagit");this.tagList.unbind(".tagit");this.tagInput.removeData("autocomplete-open");this.tagList.removeClass("tagit ui-widget ui-widget-content ui-corner-all tagit-hidden-field");this.element.is("input")?(this.element.removeClass("tagit-hidden-field"),this.tagList.remove()):(this.element.children("li").each(function(){b(this).hasClass("tagit-new")?b(this).remove():(b(this).removeClass("tagit-choice ui-widget-content ui-state-default ui-state-highlight ui-corner-all remove tagit-choice-editable tagit-choice-read-only"),
|
||||
b(this).text(b(this).children(".tagit-label").text()))}),this.singleFieldNode&&this.singleFieldNode.remove());return this},_cleanedInput:function(){return b.trim(this.tagInput.val().replace(/^"(.*)"$/,"$1"))},_lastTag:function(){return this.tagList.find(".tagit-choice:last:not(.removed)")},_tags:function(){return this.tagList.find(".tagit-choice:not(.removed)")},assignedTags:function(){var a=this,c=[];this.options.singleField?(c=b(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter),
|
||||
""===c[0]&&(c=[])):this._tags().each(function(){c.push(a.tagLabel(this))});return c},_updateSingleTagsField:function(a){b(this.options.singleFieldNode).val(a.join(this.options.singleFieldDelimiter)).trigger("change")},_subtractArray:function(a,c){for(var d=[],f=0;f<a.length;f++)-1==b.inArray(a[f],c)&&d.push(a[f]);return d},tagLabel:function(a){return this.options.singleField?b(a).find(".tagit-label:first").text():b(a).find("input:first").val()},_showAutocomplete:function(){this.tagInput.autocomplete("search",
|
||||
"")},_findTagByLabel:function(a){var c=this,d=null;this._tags().each(function(f){if(c._formatStr(a)==c._formatStr(c.tagLabel(this)))return d=b(this),!1});return d},_isNew:function(a){return!this._findTagByLabel(a)},_formatStr:function(a){return this.options.caseSensitive?a:b.trim(a.toLowerCase())},_effectExists:function(a){return Boolean(b.effects&&(b.effects[a]||b.effects.effect&&b.effects.effect[a]))},createTag:function(a,c,d){var f=this;a=b.trim(a);this.options.preprocessTag&&(a=this.options.preprocessTag(a));
|
||||
if(""===a)return!1;if(!this.options.allowDuplicates&&!this._isNew(a))return a=this._findTagByLabel(a),!1!==this._trigger("onTagExists",null,{existingTag:a,duringInitialization:d})&&this._effectExists("highlight")&&a.effect("highlight"),!1;if(this.options.tagLimit&&this._tags().length>=this.options.tagLimit)return this._trigger("onTagLimitExceeded",null,{duringInitialization:d}),!1;var g=b(this.options.onTagClicked?'<a class="tagit-label"></a>':'<span class="tagit-label"></span>').text(a),e=b("<li></li>").addClass("tagit-choice ui-widget-content ui-state-default ui-corner-all").addClass(c).append(g);
|
||||
this.options.readOnly?e.addClass("tagit-choice-read-only"):(e.addClass("tagit-choice-editable"),c=b("<span></span>").addClass("ui-icon ui-icon-close"),c=b('<a><span class="text-icon">\u00d7</span></a>').addClass("tagit-close").append(c).click(function(a){f.removeTag(e)}),e.append(c));this.options.singleField||(g=g.html(),e.append('<input type="hidden" value="'+g+'" name="'+this.options.fieldName+'" class="tagit-hidden-field" />'));!1!==this._trigger("beforeTagAdded",null,{tag:e,tagLabel:this.tagLabel(e),
|
||||
duringInitialization:d})&&(this.options.singleField&&(g=this.assignedTags(),g.push(a),this._updateSingleTagsField(g)),this._trigger("onTagAdded",null,e),this.tagInput.val(""),this.tagInput.parent().before(e),this._trigger("afterTagAdded",null,{tag:e,tagLabel:this.tagLabel(e),duringInitialization:d}),this.options.showAutocompleteOnFocus&&!d&&setTimeout(function(){f._showAutocomplete()},0))},removeTag:function(a,c){c="undefined"===typeof c?this.options.animate:c;a=b(a);this._trigger("onTagRemoved",
|
||||
null,a);if(!1!==this._trigger("beforeTagRemoved",null,{tag:a,tagLabel:this.tagLabel(a)})){if(this.options.singleField){var d=this.assignedTags(),f=this.tagLabel(a),d=b.grep(d,function(a){return a!=f});this._updateSingleTagsField(d)}if(c){a.addClass("removed");var d=this._effectExists("blind")?["blind",{direction:"horizontal"},"fast"]:["fast"],g=this;d.push(function(){a.remove();g._trigger("afterTagRemoved",null,{tag:a,tagLabel:g.tagLabel(a)})});a.fadeOut("fast").hide.apply(a,d).dequeue()}else a.remove(),
|
||||
this._trigger("afterTagRemoved",null,{tag:a,tagLabel:this.tagLabel(a)})}},removeTagByLabel:function(a,b){var d=this._findTagByLabel(a);if(!d)throw"No such tag exists with the name '"+a+"'";this.removeTag(d,b)},removeAll:function(){var a=this;this._tags().each(function(b,d){a.removeTag(d,!1)})}})})(jQuery);
|
97
ext/autocomplete/lib/tagit.ui-zendesk.css
Normal file
97
ext/autocomplete/lib/tagit.ui-zendesk.css
Normal file
@ -0,0 +1,97 @@
|
||||
|
||||
/* Optional scoped theme for tag-it which mimics the zendesk widget. */
|
||||
|
||||
|
||||
ul.tagit {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: #C6C6C6;
|
||||
background: inherit;
|
||||
}
|
||||
ul.tagit li.tagit-choice {
|
||||
-moz-border-radius: 6px;
|
||||
border-radius: 6px;
|
||||
-webkit-border-radius: 6px;
|
||||
border: 1px solid #CAD8F3;
|
||||
|
||||
background: #DEE7F8 none;
|
||||
|
||||
font-weight: normal;
|
||||
}
|
||||
ul.tagit li.tagit-choice .tagit-label:not(a) {
|
||||
color: #555;
|
||||
}
|
||||
ul.tagit li.tagit-choice a.tagit-close {
|
||||
text-decoration: none;
|
||||
}
|
||||
ul.tagit li.tagit-choice .tagit-close {
|
||||
right: .4em;
|
||||
}
|
||||
ul.tagit li.tagit-choice .ui-icon {
|
||||
display: none;
|
||||
}
|
||||
ul.tagit li.tagit-choice .tagit-close .text-icon {
|
||||
display: inline;
|
||||
font-family: arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
color: #777;
|
||||
}
|
||||
ul.tagit li.tagit-choice:hover, ul.tagit li.tagit-choice.remove {
|
||||
background-color: #bbcef1;
|
||||
border-color: #6d95e0;
|
||||
}
|
||||
ul.tagit li.tagit-choice a.tagLabel:hover,
|
||||
ul.tagit li.tagit-choice a.tagit-close .text-icon:hover {
|
||||
color: #222;
|
||||
}
|
||||
ul.tagit input[type="text"] {
|
||||
color: #333333;
|
||||
background: none;
|
||||
}
|
||||
.ui-widget {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/* Forked from a jQuery UI theme, so that we don't require the jQuery UI CSS as a dependency. */
|
||||
.tagit-autocomplete.ui-autocomplete { position: absolute; cursor: default; }
|
||||
* html .tagit-autocomplete.ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
|
||||
.tagit-autocomplete.ui-menu {
|
||||
list-style:none;
|
||||
padding: 2px;
|
||||
margin: 0;
|
||||
display:block;
|
||||
float: left;
|
||||
}
|
||||
.tagit-autocomplete.ui-menu .ui-menu {
|
||||
margin-top: -3px;
|
||||
}
|
||||
.tagit-autocomplete.ui-menu .ui-menu-item {
|
||||
margin:0;
|
||||
padding: 0;
|
||||
zoom: 1;
|
||||
float: left;
|
||||
clear: left;
|
||||
width: 100%;
|
||||
}
|
||||
.tagit-autocomplete.ui-menu .ui-menu-item a {
|
||||
text-decoration:none;
|
||||
display:block;
|
||||
padding:.2em .4em;
|
||||
line-height:1.5;
|
||||
zoom:1;
|
||||
}
|
||||
.tagit-autocomplete .ui-menu .ui-menu-item a.ui-state-hover,
|
||||
.tagit-autocomplete .ui-menu .ui-menu-item a.ui-state-active {
|
||||
font-weight: normal;
|
||||
margin: -1px;
|
||||
}
|
||||
.tagit-autocomplete.ui-widget-content { border: 1px solid #aaaaaa; background: #ffffff 50% 50% repeat-x; color: #222222; }
|
||||
.tagit-autocomplete.ui-corner-all, .tagit-autocomplete .ui-corner-all { -moz-border-radius: 4px; -webkit-border-radius: 4px; -khtml-border-radius: 4px; border-radius: 4px; }
|
||||
.tagit-autocomplete .ui-state-hover, .tagit-autocomplete .ui-state-focus { border: 1px solid #999999; background: #dadada; font-weight: normal; color: #212121; }
|
||||
.tagit-autocomplete .ui-state-active { border: 1px solid #aaaaaa; }
|
||||
|
||||
.tagit-autocomplete .ui-widget-content { border: 1px solid #aaaaaa; }
|
||||
.tagit .ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px,1px,1px,1px); }
|
||||
|
||||
|
@ -6,34 +6,40 @@ namespace Shimmie2;
|
||||
|
||||
class AutoComplete extends Extension
|
||||
{
|
||||
/** @var AutoCompleteTheme */
|
||||
protected Themelet $theme;
|
||||
|
||||
public function get_priority(): int
|
||||
{
|
||||
return 30;
|
||||
} // before Home
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event): void
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $page;
|
||||
|
||||
if ($event->page_matches("api/internal/autocomplete")) {
|
||||
$limit = (int)($_GET["limit"] ?? 1000);
|
||||
$limit = (int)($_GET["limit"] ?? 0);
|
||||
$s = $_GET["s"] ?? "";
|
||||
|
||||
$res = $this->complete($s, $limit);
|
||||
|
||||
$page->set_mode(PageMode::DATA);
|
||||
$page->set_mime(MimeType::JSON);
|
||||
$page->set_data(json_encode_ex($res));
|
||||
$page->set_data(json_encode($res));
|
||||
}
|
||||
|
||||
$this->theme->build_autocomplete($page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, int>
|
||||
*/
|
||||
private function complete(string $search, int $limit): array
|
||||
{
|
||||
global $cache, $database;
|
||||
|
||||
if (!$search) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$search = strtolower($search);
|
||||
if (
|
||||
$search == '' ||
|
||||
@ -45,32 +51,34 @@ class AutoComplete extends Extension
|
||||
}
|
||||
|
||||
# memcache keys can't contain spaces
|
||||
$cache_key = "autocomplete:$limit:" . md5($search);
|
||||
$cache_key = "autocomplete:" . md5($search);
|
||||
$limitSQL = "";
|
||||
$search = str_replace('_', '\_', $search);
|
||||
$search = str_replace('%', '\%', $search);
|
||||
$SQLarr = [
|
||||
"search" => "$search%",
|
||||
"cat_search" => Extension::is_enabled(TagCategoriesInfo::KEY) ? "%:$search%" : "",
|
||||
];
|
||||
$SQLarr = ["search"=>"$search%"]; #, "cat_search"=>"%:$search%"];
|
||||
if ($limit !== 0) {
|
||||
$limitSQL = "LIMIT :limit";
|
||||
$SQLarr['limit'] = $limit;
|
||||
$cache_key .= "-" . $limit;
|
||||
}
|
||||
|
||||
return 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
|
||||
WHERE (
|
||||
LOWER(tag) LIKE LOWER(:search)
|
||||
OR LOWER(tag) LIKE LOWER(:cat_search)
|
||||
)
|
||||
WHERE LOWER(tag) LIKE LOWER(:search)
|
||||
-- OR LOWER(tag) LIKE LOWER(:cat_search)
|
||||
AND count > 0
|
||||
ORDER BY count DESC, tag ASC
|
||||
ORDER BY count DESC
|
||||
$limitSQL
|
||||
",
|
||||
$SQLarr
|
||||
), 600);
|
||||
",
|
||||
$SQLarr
|
||||
);
|
||||
$cache->set($cache_key, $res, 600);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
|
@ -1,256 +1,115 @@
|
||||
/**
|
||||
* Find the word currently being typed in the given element
|
||||
*
|
||||
* @param {HTMLInputElement} element
|
||||
* @returns {string}
|
||||
*/
|
||||
function getCurrentWord(element) {
|
||||
let text = element.value;
|
||||
let pos = element.selectionStart;
|
||||
var start = text.lastIndexOf(' ', pos-1);
|
||||
if(start === -1) {
|
||||
start = 0;
|
||||
}
|
||||
else {
|
||||
start++; // skip the space
|
||||
}
|
||||
return text.substring(start, pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whenever input changes, look at what word is currently
|
||||
* being typed, and fetch completions for it.
|
||||
*
|
||||
* @param {HTMLInputElement} element
|
||||
*/
|
||||
function updateCompletions(element) {
|
||||
// Reset selction, but no need to validate and re-render
|
||||
// highlightCompletion(element, -1);
|
||||
element.selected_completion = -1;
|
||||
|
||||
// get the word before the cursor
|
||||
var word = getCurrentWord(element);
|
||||
|
||||
// search for completions
|
||||
if(element.completer_timeout !== null) {
|
||||
clearTimeout(element.completer_timeout);
|
||||
element.completer_timeout = null;
|
||||
}
|
||||
if(word === '' || word === '-') {
|
||||
element.completions = {};
|
||||
renderCompletions(element);
|
||||
}
|
||||
else {
|
||||
element.completer_timeout = setTimeout(() => {
|
||||
const wordWithoutMinus = word.replace(/^-/, '');
|
||||
fetch((document.body.getAttribute("data-base-href") ?? "") + '/api/internal/autocomplete?s=' + wordWithoutMinus).then(
|
||||
(response) => response.json()
|
||||
).then((json) => {
|
||||
if(element.selected_completion !== -1) {
|
||||
return; // user has started to navigate the completions, so don't update them
|
||||
}
|
||||
element.completions = json;
|
||||
renderCompletions(element);
|
||||
});
|
||||
}, 250);
|
||||
renderCompletions(element);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight the nth completion
|
||||
*
|
||||
* @param {HTMLInputElement} element
|
||||
* @param {number} n
|
||||
*/
|
||||
function highlightCompletion(element, n) {
|
||||
if(!element.completions) return;
|
||||
element.selected_completion = Math.min(
|
||||
Math.max(n, -1),
|
||||
Object.keys(element.completions).length-1
|
||||
);
|
||||
renderCompletions(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the completion block
|
||||
*
|
||||
* @param {HTMLInputElement} element
|
||||
*/
|
||||
function renderCompletions(element) {
|
||||
let completions = element.completions;
|
||||
let selected_completion = element.selected_completion;
|
||||
|
||||
// remove any existing completion block
|
||||
hideCompletions();
|
||||
|
||||
// if there are no completions, don't render anything
|
||||
if(Object.keys(completions).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let completions_el = document.createElement('ul');
|
||||
completions_el.className = 'autocomplete_completions';
|
||||
completions_el.id = 'completions';
|
||||
|
||||
// add children for top completions, with the selected one highlighted
|
||||
let word = getCurrentWord(element);
|
||||
Object.keys(completions).filter(
|
||||
(key) => {
|
||||
let k = key.toLowerCase();
|
||||
let w = word.replace(/^-/, '').toLowerCase();
|
||||
return (k.startsWith(w) || k.split(':').some((k) => k.startsWith(w)))
|
||||
}
|
||||
).slice(0, 100).forEach((key, i) => {
|
||||
let value = completions[key];
|
||||
let li = document.createElement('li');
|
||||
li.innerText = key + ' (' + value + ')';
|
||||
if(i === selected_completion) {
|
||||
li.className = 'selected';
|
||||
}
|
||||
// on hover, select the completion
|
||||
// (use mousemove rather than mouseover, because
|
||||
// if the mouse is stationary, then we want the
|
||||
// keyboard to be able to override it)
|
||||
li.addEventListener('mousemove', () => {
|
||||
// avoid re-rendering if the completion is already selected
|
||||
if(element.selected_completion !== i) {
|
||||
highlightCompletion(element, i);
|
||||
}
|
||||
});
|
||||
// on click, set the completion
|
||||
// (mousedown is used instead of click because click is
|
||||
// fired after blur, which causes the completion block to
|
||||
// be removed before the click event is handled)
|
||||
li.addEventListener('mousedown', (event) => {
|
||||
setCompletion(element, key);
|
||||
event.preventDefault();
|
||||
});
|
||||
li.addEventListener('touchstart', (event) => {
|
||||
setCompletion(element, key);
|
||||
event.preventDefault();
|
||||
});
|
||||
completions_el.appendChild(li);
|
||||
});
|
||||
|
||||
// insert the completion block after the element
|
||||
if(element.parentNode) {
|
||||
element.parentNode.insertBefore(completions_el, element.nextSibling);
|
||||
let br = element.getBoundingClientRect();
|
||||
completions_el.style.minWidth = br.width + 'px';
|
||||
completions_el.style.maxWidth = 'calc(100vw - 2rem - ' + br.left + 'px)';
|
||||
completions_el.style.left = window.scrollX + br.left + 'px';
|
||||
completions_el.style.top = window.scrollY + (br.top + br.height) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* hide the completions block
|
||||
*/
|
||||
function hideCompletions() {
|
||||
document.querySelectorAll('.autocomplete_completions').forEach((element) => {
|
||||
element.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current word to the given completion
|
||||
*
|
||||
* @param {HTMLInputElement} element
|
||||
* @param {string} new_word
|
||||
*/
|
||||
function setCompletion(element, new_word) {
|
||||
let text = element.value;
|
||||
let pos = element.selectionStart;
|
||||
|
||||
// get the word before the cursor
|
||||
var start = text.lastIndexOf(' ', pos-1);
|
||||
if(start === -1) {
|
||||
start = 0;
|
||||
}
|
||||
else {
|
||||
start++; // skip the space
|
||||
}
|
||||
var end = text.indexOf(' ', pos);
|
||||
if(end === -1) {
|
||||
end = text.length;
|
||||
}
|
||||
|
||||
// replace the word with the completion
|
||||
if(text[start] === '-') {
|
||||
new_word = '-' + new_word;
|
||||
}
|
||||
new_word += ' ';
|
||||
element.value = text.substring(0, start) + new_word + text.substring(end);
|
||||
element.selectionStart = start + new_word.length;
|
||||
element.selectionEnd = start + new_word.length;
|
||||
|
||||
// reset metadata
|
||||
element.completions = {};
|
||||
element.selected_completion = -1;
|
||||
if(element.completer_timeout) {
|
||||
clearTimeout(element.completer_timeout);
|
||||
element.completer_timeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Find all elements with class 'autocomplete_tags'
|
||||
document.querySelectorAll('.autocomplete_tags').forEach((element) => {
|
||||
// set metadata
|
||||
element.completions = {};
|
||||
element.selected_completion = -1;
|
||||
element.completer_timeout = null;
|
||||
var metatags = ['order:id', 'order:width', 'order:height', 'order:filesize', 'order:filename', 'order:favorites'];
|
||||
|
||||
// disable built-in autocomplete
|
||||
element.setAttribute('autocomplete', 'off');
|
||||
|
||||
// safari treats spellcheck as a form of autocomplete
|
||||
element.setAttribute('spellcheck', 'off');
|
||||
|
||||
// when element is focused, add completion block
|
||||
element.addEventListener('focus', () => {
|
||||
updateCompletions(element);
|
||||
});
|
||||
|
||||
// when element is blurred, remove completion block
|
||||
element.addEventListener('blur', () => {
|
||||
hideCompletions();
|
||||
});
|
||||
|
||||
// when cursor is moved, change current completion
|
||||
document.addEventListener('selectionchange', () => {
|
||||
// if element is focused
|
||||
if(document.activeElement === element) {
|
||||
updateCompletions(element);
|
||||
$('.autocomplete_tags').tagit({
|
||||
singleFieldDelimiter: ' ',
|
||||
beforeTagAdded: function(event, ui) {
|
||||
if(metatags.indexOf(ui.tagLabel) !== -1) {
|
||||
ui.tag.addClass('tag-metatag');
|
||||
} else {
|
||||
console.log(ui.tagLabel);
|
||||
// give special class to negative tags
|
||||
if(ui.tagLabel[0] === '-') {
|
||||
ui.tag.addClass('tag-negative');
|
||||
}else{
|
||||
ui.tag.addClass('tag-positive');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
autocomplete : ({
|
||||
source: function (request, response) {
|
||||
var ac_metatags = $.map(
|
||||
$.grep(metatags, function(s) {
|
||||
// Only show metatags for strings longer than one character
|
||||
return (request.term.length > 1 && s.indexOf(request.term) === 0);
|
||||
}),
|
||||
function(item) {
|
||||
return {
|
||||
label : item + ' [metatag]',
|
||||
value : item
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
element.addEventListener('keydown', (event) => {
|
||||
// up / down should select previous / next completion
|
||||
if(event.code === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
highlightCompletion(element, element.selected_completion-1);
|
||||
}
|
||||
else if(event.code === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
highlightCompletion(element, element.selected_completion+1);
|
||||
}
|
||||
// if enter or right are pressed while a completion is selected, add the selected completion
|
||||
else if((event.code === "Enter" || event.code == "ArrowRight") && element.selected_completion !== -1) {
|
||||
event.preventDefault();
|
||||
setCompletion(element, Object.keys(element.completions)[element.selected_completion]);
|
||||
}
|
||||
// if escape is pressed, hide the completion block
|
||||
else if(event.code === "Escape") {
|
||||
event.preventDefault();
|
||||
hideCompletions();
|
||||
}
|
||||
});
|
||||
var isNegative = (request.term[0] === '-');
|
||||
$.ajax({
|
||||
url: base_href + '/api/internal/autocomplete',
|
||||
data: {'s': (isNegative ? request.term.substring(1) : request.term)},
|
||||
dataType : 'json',
|
||||
type : 'GET',
|
||||
success : function (data) {
|
||||
response(
|
||||
$.merge(ac_metatags,
|
||||
$.map(data, function (count, item) {
|
||||
item = (isNegative ? '-'+item : item);
|
||||
return {
|
||||
label : item + ' ('+count+')',
|
||||
value : item
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
error : function (request, status, error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
minLength: 1
|
||||
})
|
||||
});
|
||||
|
||||
// on change, update completions
|
||||
element.addEventListener('input', () => {
|
||||
updateCompletions(element);
|
||||
});
|
||||
$('#tag_editor,[name="bulk_tags"]').tagit({
|
||||
singleFieldDelimiter: ' ',
|
||||
autocomplete : ({
|
||||
source: function (request, response) {
|
||||
$.ajax({
|
||||
url: base_href + '/api/internal/autocomplete',
|
||||
data: {'s': request.term},
|
||||
dataType : 'json',
|
||||
type : 'GET',
|
||||
success : function (data) {
|
||||
response(
|
||||
$.map(data, function (count, item) {
|
||||
return {
|
||||
label : item + ' ('+count+')',
|
||||
value : item
|
||||
};
|
||||
})
|
||||
);
|
||||
},
|
||||
error : function (request, status, error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
minLength: 1
|
||||
})
|
||||
});
|
||||
|
||||
$('.ui-autocomplete-input').keydown(function(e) {
|
||||
var keyCode = e.keyCode || e.which;
|
||||
|
||||
//Stop tags containing space.
|
||||
if(keyCode === 32) {
|
||||
e.preventDefault();
|
||||
var el = $('.ui-widget-content:focus');
|
||||
|
||||
//Find the correct element in a page with multiple tagit input boxes.
|
||||
$('.autocomplete_tags').each(function(_,n){
|
||||
if (n.parentNode.contains(el[0])){
|
||||
$(n.parentNode).find('.autocomplete_tags').tagit('createTag', el.val());
|
||||
}
|
||||
});
|
||||
$(this).autocomplete('close');
|
||||
} else if (keyCode === 9) {
|
||||
e.preventDefault();
|
||||
|
||||
var tag = $('.tagit-autocomplete[style*=\"display: block\"] > li:focus, .tagit-autocomplete[style*=\"display: block\"] > li:first').first();
|
||||
if(tag.length){
|
||||
$(tag).click();
|
||||
$('.ui-autocomplete-input').val(''); //If tag already exists, make sure to remove duplicate.
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,22 +1,9 @@
|
||||
.autocomplete_completions {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
border: 1px solid #ccc;
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
padding: 5px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
}
|
||||
.autocomplete_completions LI {
|
||||
padding: 0.15em;
|
||||
}
|
||||
.autocomplete_completions .selected {
|
||||
background-color: #ccc;
|
||||
outline: none;
|
||||
}
|
||||
#Navigationleft .blockbody { overflow: visible; }
|
||||
|
||||
.tagit { background: white !important; border: 1px solid grey !important; cursor: text; }
|
||||
.tagit-choice { cursor: initial; }
|
||||
input[name=search] ~ input[type=submit] { display: inline-block !important; }
|
||||
|
||||
.tag-negative { background: #ff8080 !important; }
|
||||
.tag-positive { background: #40bf40 !important; }
|
||||
.tag-metatag { background: #eaa338 !important; }
|
||||
|
@ -6,41 +6,12 @@ namespace Shimmie2;
|
||||
|
||||
class AutoCompleteTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testAuth(): void
|
||||
public function testAuth()
|
||||
{
|
||||
$this->log_in_as_user();
|
||||
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "some_tag");
|
||||
|
||||
send_event(new UserLoginEvent(User::by_name(self::$anon_name)));
|
||||
$page = $this->get_page('api/internal/autocomplete', ["s" => "not-a-tag"]);
|
||||
$page = $this->get_page('api/internal/autocomplete', ["s"=>"not-a-tag"]);
|
||||
$this->assertEquals(200, $page->code);
|
||||
$this->assertEquals(PageMode::DATA, $page->mode);
|
||||
$this->assertEquals("[]", $page->data);
|
||||
|
||||
$page = $this->get_page('api/internal/autocomplete', ["s" => "so"]);
|
||||
$this->assertEquals(200, $page->code);
|
||||
$this->assertEquals(PageMode::DATA, $page->mode);
|
||||
$this->assertEquals('{"some_tag":1}', $page->data);
|
||||
}
|
||||
|
||||
public function testCategories(): void
|
||||
{
|
||||
$this->log_in_as_user();
|
||||
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "artist:bob");
|
||||
|
||||
$page = $this->get_page('api/internal/autocomplete', ["s" => "bob"]);
|
||||
$this->assertEquals(200, $page->code);
|
||||
$this->assertEquals(PageMode::DATA, $page->mode);
|
||||
$this->assertEquals('{"artist:bob":1}', $page->data);
|
||||
|
||||
$page = $this->get_page('api/internal/autocomplete', ["s" => "art"]);
|
||||
$this->assertEquals(200, $page->code);
|
||||
$this->assertEquals(PageMode::DATA, $page->mode);
|
||||
$this->assertEquals('{"artist:bob":1}', $page->data);
|
||||
|
||||
$page = $this->get_page('api/internal/autocomplete', ["s" => "artist:"]);
|
||||
$this->assertEquals(200, $page->code);
|
||||
$this->assertEquals(PageMode::DATA, $page->mode);
|
||||
$this->assertEquals('{"artist:bob":1}', $page->data);
|
||||
}
|
||||
}
|
||||
|
19
ext/autocomplete/theme.php
Normal file
19
ext/autocomplete/theme.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
class AutoCompleteTheme extends Themelet
|
||||
{
|
||||
public function build_autocomplete(Page $page)
|
||||
{
|
||||
$base_href = get_base_href();
|
||||
// TODO: AJAX test and fallback.
|
||||
|
||||
$page->add_html_header("<script defer src='$base_href/ext/autocomplete/lib/jquery-ui.min.js' type='text/javascript'></script>");
|
||||
$page->add_html_header("<script defer src='$base_href/ext/autocomplete/lib/tag-it.min.js' type='text/javascript'></script>");
|
||||
$page->add_html_header('<link rel="stylesheet" type="text/css" href="//ajax.googleapis.com/ajax/libs/jqueryui/1/themes/flick/jquery-ui.css">');
|
||||
$page->add_html_header("<link rel='stylesheet' type='text/css' href='$base_href/ext/autocomplete/lib/jquery.tagit.css' />");
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ namespace Shimmie2;
|
||||
|
||||
class BanWords extends Extension
|
||||
{
|
||||
public function onInitExt(InitExtEvent $event): void
|
||||
public function onInitExt(InitExtEvent $event)
|
||||
{
|
||||
global $config;
|
||||
$config->set_default_string('banned_words', "
|
||||
@ -39,7 +39,7 @@ xanax
|
||||
");
|
||||
}
|
||||
|
||||
public function onCommentPosting(CommentPostingEvent $event): void
|
||||
public function onCommentPosting(CommentPostingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if (!$user->can(Permissions::BYPASS_COMMENT_CHECKS)) {
|
||||
@ -47,17 +47,17 @@ xanax
|
||||
}
|
||||
}
|
||||
|
||||
public function onSourceSet(SourceSetEvent $event): void
|
||||
public function onSourceSet(SourceSetEvent $event)
|
||||
{
|
||||
$this->test_text($event->source, new SCoreException("Source contains banned terms"));
|
||||
}
|
||||
|
||||
public function onTagSet(TagSetEvent $event): void
|
||||
public function onTagSet(TagSetEvent $event)
|
||||
{
|
||||
$this->test_text(Tag::implode($event->new_tags), new SCoreException("Tags contain banned terms"));
|
||||
$this->test_text(Tag::implode($event->tags), new SCoreException("Tags contain banned terms"));
|
||||
}
|
||||
|
||||
public function onSetupBuilding(SetupBuildingEvent $event): void
|
||||
public function onSetupBuilding(SetupBuildingEvent $event)
|
||||
{
|
||||
$sb = $event->panel->create_new_block("Banned Phrases");
|
||||
$sb->add_label("One per line, lines that start with slashes are treated as regex<br/>");
|
||||
@ -97,9 +97,6 @@ xanax
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function get_words(): array
|
||||
{
|
||||
global $config;
|
||||
|
@ -6,7 +6,7 @@ namespace Shimmie2;
|
||||
|
||||
class BanWordsTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function check_blocked(int $image_id, string $words): void
|
||||
public function check_blocked($image_id, $words)
|
||||
{
|
||||
global $user;
|
||||
try {
|
||||
@ -17,7 +17,7 @@ class BanWordsTest extends ShimmiePHPUnitTestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function testWordBan(): void
|
||||
public function testWordBan()
|
||||
{
|
||||
global $config;
|
||||
$config->set_string("banned_words", "viagra\nporn\n\n/http:.*\.cn\//");
|
||||
|
@ -7,12 +7,6 @@ namespace Shimmie2;
|
||||
class BBCode extends FormatterExtension
|
||||
{
|
||||
public function format(string $text): string
|
||||
{
|
||||
$text = $this->_format($text);
|
||||
return "<span class='bbcode'>$text</span>";
|
||||
}
|
||||
|
||||
public function _format(string $text): string
|
||||
{
|
||||
$text = $this->extract_code($text);
|
||||
foreach ([
|
||||
@ -103,8 +97,8 @@ class BBCode extends FormatterExtension
|
||||
}
|
||||
|
||||
$beginning = substr($text, 0, $start);
|
||||
$middle = str_rot13(substr($text, $start + $l1, ($end - $start - $l1)));
|
||||
$ending = substr($text, $end + $l2, (strlen($text) - $end + $l2));
|
||||
$middle = str_rot13(substr($text, $start+$l1, ($end-$start-$l1)));
|
||||
$ending = substr($text, $end + $l2, (strlen($text)-$end+$l2));
|
||||
|
||||
$text = $beginning . $middle . $ending;
|
||||
}
|
||||
@ -137,8 +131,8 @@ class BBCode extends FormatterExtension
|
||||
}
|
||||
|
||||
$beginning = substr($text, 0, $start);
|
||||
$middle = base64_encode(substr($text, $start + $l1, ($end - $start - $l1)));
|
||||
$ending = substr($text, $end + $l2, (strlen($text) - $end + $l2));
|
||||
$middle = base64_encode(substr($text, $start+$l1, ($end-$start-$l1)));
|
||||
$ending = substr($text, $end + $l2, (strlen($text)-$end+$l2));
|
||||
|
||||
$text = $beginning . "[code!]" . $middle . "[/code!]" . $ending;
|
||||
}
|
||||
@ -161,10 +155,10 @@ class BBCode extends FormatterExtension
|
||||
}
|
||||
|
||||
$beginning = substr($text, 0, $start);
|
||||
$middle = base64_decode(substr($text, $start + $l1, ($end - $start - $l1)));
|
||||
$ending = substr($text, $end + $l2, (strlen($text) - $end + $l2));
|
||||
$middle = base64_decode(substr($text, $start+$l1, ($end-$start-$l1)));
|
||||
$ending = substr($text, $end + $l2, (strlen($text)-$end+$l2));
|
||||
|
||||
$text = $beginning . "<pre class='code'>" . $middle . "</pre>" . $ending;
|
||||
$text = $beginning . "<pre>" . $middle . "</pre>" . $ending;
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
@ -1,20 +1,18 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll(".shm-clink").forEach(function(el) {
|
||||
var target_id = el.getAttribute("data-clink-sel");
|
||||
if(target_id && document.querySelectorAll(target_id).length > 0) {
|
||||
$(".shm-clink").each(function(idx, elm) {
|
||||
var target_id = $(elm).data("clink-sel");
|
||||
if(target_id && $(target_id).length > 0) {
|
||||
// if the target comment is already on this page, don't bother
|
||||
// switching pages
|
||||
el.setAttribute("href", target_id);
|
||||
|
||||
$(elm).attr("href", target_id);
|
||||
// highlight it when clicked
|
||||
el.addEventListener("click", function(e) {
|
||||
$(elm).click(function(e) {
|
||||
// This needs jQuery UI
|
||||
$(target_id).highlight();
|
||||
});
|
||||
|
||||
// vanilla target name should already be in the URL tag, but this
|
||||
// will include the anon ID as displayed on screen
|
||||
el.innerHTML = "@"+document.querySelector(target_id+" .username").innerHTML;
|
||||
$(elm).html("@"+$(target_id+" .username").html());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,15 +1,16 @@
|
||||
.bbcode PRE.code {
|
||||
background: #DEDEDE;
|
||||
font-size: 0.9rem;
|
||||
|
||||
CODE {
|
||||
background: #DEDEDE;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.bbcode BLOCKQUOTE {
|
||||
BLOCKQUOTE {
|
||||
border: 1px solid black;
|
||||
padding: 8px;
|
||||
background: #DDD;
|
||||
}
|
||||
.bbcode .anchor A.alink {
|
||||
.anchor A.alink {
|
||||
visibility: hidden;
|
||||
}
|
||||
.bbcode .anchor:hover A.alink {
|
||||
.anchor:hover A.alink {
|
||||
visibility: visible;
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ namespace Shimmie2;
|
||||
|
||||
class BBCodeTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testBasics(): void
|
||||
public function testBasics()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<b>bold</b><i>italic</i>",
|
||||
@ -14,7 +14,7 @@ class BBCodeTest extends ShimmiePHPUnitTestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testStacking(): void
|
||||
public function testStacking()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<b>B</b><i>I</i><b>B</b>",
|
||||
@ -26,7 +26,7 @@ class BBCodeTest extends ShimmiePHPUnitTestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testFailure(): void
|
||||
public function testFailure()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"[b]bold[i]italic",
|
||||
@ -34,15 +34,15 @@ class BBCodeTest extends ShimmiePHPUnitTestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testCode(): void
|
||||
public function testCode()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<pre class='code'>[b]bold[/b]</pre>",
|
||||
"<pre>[b]bold[/b]</pre>",
|
||||
$this->filter("[code][b]bold[/b][/code]")
|
||||
);
|
||||
}
|
||||
|
||||
public function testNestedList(): void
|
||||
public function testNestedList()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<ul><li>a<ul><li>a<li>b</ul><li>b</ul>",
|
||||
@ -54,7 +54,7 @@ class BBCodeTest extends ShimmiePHPUnitTestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testSpoiler(): void
|
||||
public function testSpoiler()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<span style=\"background-color:#000; color:#000;\">ShishNet</span>",
|
||||
@ -69,7 +69,7 @@ class BBCodeTest extends ShimmiePHPUnitTestCase
|
||||
# "[spoiler]ShishNet");
|
||||
}
|
||||
|
||||
public function testURL(): void
|
||||
public function testURL()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<a href=\"https://shishnet.org\">https://shishnet.org</a>",
|
||||
@ -85,7 +85,7 @@ class BBCodeTest extends ShimmiePHPUnitTestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testEmailURL(): void
|
||||
public function testEmailURL()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<a href=\"mailto:spam@shishnet.org\">spam@shishnet.org</a>",
|
||||
@ -93,7 +93,7 @@ class BBCodeTest extends ShimmiePHPUnitTestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testAnchor(): void
|
||||
public function testAnchor()
|
||||
{
|
||||
$this->assertEquals(
|
||||
'<span class="anchor">Rules <a class="alink" href="#bb-rules" name="bb-rules" title="link to this anchor"> ¶ </a></span>',
|
||||
@ -101,19 +101,19 @@ class BBCodeTest extends ShimmiePHPUnitTestCase
|
||||
);
|
||||
}
|
||||
|
||||
private function filter(string $in): string
|
||||
private function filter($in): string
|
||||
{
|
||||
$bb = new BBCode();
|
||||
return $bb->_format($in);
|
||||
return $bb->format($in);
|
||||
}
|
||||
|
||||
private function strip(string $in): string
|
||||
private function strip($in): string
|
||||
{
|
||||
$bb = new BBCode();
|
||||
return $bb->strip($in);
|
||||
}
|
||||
|
||||
public function testSiteLinks(): void
|
||||
public function testSiteLinks()
|
||||
{
|
||||
$this->assertEquals(
|
||||
'<a class="shm-clink" data-clink-sel="" href="/test/post/view/123">>>123</a>',
|
||||
|
@ -9,7 +9,7 @@ class Biography extends Extension
|
||||
/** @var BiographyTheme */
|
||||
protected Themelet $theme;
|
||||
|
||||
public function onUserPageBuilding(UserPageBuildingEvent $event): void
|
||||
public function onUserPageBuilding(UserPageBuildingEvent $event)
|
||||
{
|
||||
global $page, $user;
|
||||
$duser = $event->display_user;
|
||||
@ -23,7 +23,7 @@ class Biography extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event): void
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $page, $user, $user_config;
|
||||
if ($event->page_matches("biography")) {
|
||||
|
@ -6,10 +6,10 @@ namespace Shimmie2;
|
||||
|
||||
class BiographyTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testBio(): void
|
||||
public function testBio()
|
||||
{
|
||||
$this->log_in_as_user();
|
||||
$this->post_page("biography", ["biography" => "My bio goes here"]);
|
||||
$this->post_page("biography", ["biography"=>"My bio goes here"]);
|
||||
$this->get_page("user/" . self::$user_name);
|
||||
$this->assert_text("My bio goes here");
|
||||
|
||||
|
@ -8,16 +8,16 @@ use function MicroHTML\TEXTAREA;
|
||||
|
||||
class BiographyTheme extends Themelet
|
||||
{
|
||||
public function display_biography(Page $page, string $bio): void
|
||||
public function display_biography(Page $page, string $bio)
|
||||
{
|
||||
$page->add_block(new Block("About Me", format_text($bio), "main", 30, "about-me"));
|
||||
}
|
||||
|
||||
public function display_composer(Page $page, string $bio): void
|
||||
public function display_composer(Page $page, string $bio)
|
||||
{
|
||||
$html = SHM_SIMPLE_FORM(
|
||||
make_link("biography"),
|
||||
TEXTAREA(["style" => "width: 100%", "rows" => "6", "name" => "biography"], $bio),
|
||||
TEXTAREA(["style"=>"width: 100%", "rows"=>"6", "name"=>"biography"], $bio),
|
||||
SHM_SUBMIT("Save")
|
||||
);
|
||||
|
||||
|
@ -9,7 +9,7 @@ class Blocks extends Extension
|
||||
/** @var BlocksTheme */
|
||||
protected Themelet $theme;
|
||||
|
||||
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void
|
||||
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
|
||||
{
|
||||
global $database;
|
||||
if ($this->get_version("ext_blocks_version") < 1) {
|
||||
@ -31,17 +31,17 @@ class Blocks extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if ($event->parent === "system") {
|
||||
if ($event->parent==="system") {
|
||||
if ($user->can(Permissions::MANAGE_BLOCKS)) {
|
||||
$event->add_nav_link("blocks", new Link('blocks/list'), "Blocks Editor");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event): void
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if ($user->can(Permissions::MANAGE_BLOCKS)) {
|
||||
@ -49,11 +49,15 @@ class Blocks extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event): void
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $cache, $database, $page, $user;
|
||||
|
||||
$blocks = cache_get_or_set("blocks", fn () => $database->get_all("SELECT * FROM blocks"), 600);
|
||||
$blocks = $cache->get("blocks");
|
||||
if (is_null($blocks)) {
|
||||
$blocks = $database->get_all("SELECT * FROM blocks");
|
||||
$cache->set("blocks", $blocks, 600);
|
||||
}
|
||||
foreach ($blocks as $block) {
|
||||
$path = implode("/", $event->args);
|
||||
if (strlen($path) < 4000 && fnmatch($block['pages'], $path)) {
|
||||
@ -74,7 +78,7 @@ class Blocks extends Extension
|
||||
$database->execute("
|
||||
INSERT INTO blocks (pages, title, area, priority, content, userclass)
|
||||
VALUES (:pages, :title, :area, :priority, :content, :userclass)
|
||||
", ['pages' => $_POST['pages'], 'title' => $_POST['title'], 'area' => $_POST['area'], 'priority' => (int)$_POST['priority'], 'content' => $_POST['content'], 'userclass' => $_POST['userclass']]);
|
||||
", ['pages'=>$_POST['pages'], 'title'=>$_POST['title'], 'area'=>$_POST['area'], 'priority'=>(int)$_POST['priority'], 'content'=>$_POST['content'], 'userclass'=>$_POST['userclass']]);
|
||||
log_info("blocks", "Added Block #".($database->get_last_insert_id('blocks_id_seq'))." (".$_POST['title'].")");
|
||||
$cache->delete("blocks");
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
@ -87,13 +91,13 @@ class Blocks extends Extension
|
||||
$database->execute("
|
||||
DELETE FROM blocks
|
||||
WHERE id=:id
|
||||
", ['id' => $_POST['id']]);
|
||||
", ['id'=>$_POST['id']]);
|
||||
log_info("blocks", "Deleted Block #".$_POST['id']);
|
||||
} else {
|
||||
$database->execute("
|
||||
UPDATE blocks SET pages=:pages, title=:title, area=:area, priority=:priority, content=:content, userclass=:userclass
|
||||
WHERE id=:id
|
||||
", ['pages' => $_POST['pages'], 'title' => $_POST['title'], 'area' => $_POST['area'], 'priority' => (int)$_POST['priority'], 'content' => $_POST['content'], 'userclass' => $_POST['userclass'], 'id' => $_POST['id']]);
|
||||
", ['pages'=>$_POST['pages'], 'title'=>$_POST['title'], 'area'=>$_POST['area'], 'priority'=>(int)$_POST['priority'], 'content'=>$_POST['content'], 'userclass'=>$_POST['userclass'], 'id'=>$_POST['id']]);
|
||||
log_info("blocks", "Updated Block #".$_POST['id']." (".$_POST['title'].")");
|
||||
}
|
||||
$cache->delete("blocks");
|
||||
|
@ -6,7 +6,7 @@ namespace Shimmie2;
|
||||
|
||||
class BlocksTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testBlocks(): void
|
||||
public function testBlocks()
|
||||
{
|
||||
$this->log_in_as_admin();
|
||||
$this->get_page("blocks/list");
|
||||
|
@ -16,38 +16,35 @@ use function MicroHTML\OPTION;
|
||||
|
||||
class BlocksTheme extends Themelet
|
||||
{
|
||||
/**
|
||||
* @param array<array{id:int,title:string,area:string,priority:int,userclass:string,pages:string,content:string}> $blocks
|
||||
*/
|
||||
public function display_blocks(array $blocks): void
|
||||
public function display_blocks($blocks)
|
||||
{
|
||||
global $page;
|
||||
|
||||
$html = TABLE(["class" => "form", "style" => "width: 100%;"]);
|
||||
$html = TABLE(["class"=>"form", "style"=>"width: 100%;"]);
|
||||
foreach ($blocks as $block) {
|
||||
$html->appendChild(SHM_SIMPLE_FORM(
|
||||
"blocks/update",
|
||||
TR(
|
||||
INPUT(["type" => "hidden", "name" => "id", "value" => $block['id']]),
|
||||
INPUT(["type"=>"hidden", "name"=>"id", "value"=>$block['id']]),
|
||||
TH("Title"),
|
||||
TD(INPUT(["type" => "text", "name" => "title", "value" => $block['title']])),
|
||||
TD(INPUT(["type"=>"text", "name"=>"title", "value"=>$block['title']])),
|
||||
TH("Area"),
|
||||
TD(INPUT(["type" => "text", "name" => "area", "value" => $block['area']])),
|
||||
TD(INPUT(["type"=>"text", "name"=>"area", "value"=>$block['area']])),
|
||||
TH("Priority"),
|
||||
TD(INPUT(["type" => "text", "name" => "priority", "value" => $block['priority']])),
|
||||
TD(INPUT(["type"=>"text", "name"=>"priority", "value"=>$block['priority']])),
|
||||
TH("User Class"),
|
||||
TD(INPUT(["type" => "text", "name" => "userclass", "value" => $block['userclass']])),
|
||||
TD(INPUT(["type"=>"text", "name"=>"userclass", "value"=>$block['userclass']])),
|
||||
TH("Pages"),
|
||||
TD(INPUT(["type" => "text", "name" => "pages", "value" => $block['pages']])),
|
||||
TD(INPUT(["type"=>"text", "name"=>"pages", "value"=>$block['pages']])),
|
||||
TH("Delete"),
|
||||
TD(INPUT(["type" => "checkbox", "name" => "delete"])),
|
||||
TD(INPUT(["type" => "submit", "value" => "Save"]))
|
||||
TD(INPUT(["type"=>"checkbox", "name"=>"delete"])),
|
||||
TD(INPUT(["type"=>"submit", "value"=>"Save"]))
|
||||
),
|
||||
TR(
|
||||
TD(["colspan" => "13"], TEXTAREA(["rows" => "5", "name" => "content"], $block['content']))
|
||||
TD(["colspan"=>"13"], TEXTAREA(["rows"=>"5", "name"=>"content"], $block['content']))
|
||||
),
|
||||
TR(
|
||||
TD(["colspan" => "13"], rawHTML(" "))
|
||||
TD(["colspan"=>"13"], rawHTML(" "))
|
||||
),
|
||||
));
|
||||
}
|
||||
@ -56,19 +53,19 @@ class BlocksTheme extends Themelet
|
||||
"blocks/add",
|
||||
TR(
|
||||
TH("Title"),
|
||||
TD(INPUT(["type" => "text", "name" => "title", "value" => ""])),
|
||||
TD(INPUT(["type"=>"text", "name"=>"title", "value"=>""])),
|
||||
TH("Area"),
|
||||
TD(SELECT(["name" => "area"], OPTION("left"), OPTION("main"))),
|
||||
TD(SELECT(["name"=>"area"], OPTION("left"), OPTION("main"))),
|
||||
TH("Priority"),
|
||||
TD(INPUT(["type" => "text", "name" => "priority", "value" => '50'])),
|
||||
TD(INPUT(["type"=>"text", "name"=>"priority", "value"=>'50'])),
|
||||
TH("User Class"),
|
||||
TD(INPUT(["type" => "text", "name" => "userclass", "value" => ""])),
|
||||
TD(INPUT(["type"=>"text", "name"=>"userclass", "value"=>""])),
|
||||
TH("Pages"),
|
||||
TD(INPUT(["type" => "text", "name" => "pages", "value" => 'post/list*'])),
|
||||
TD(["colspan" => '3'], INPUT(["type" => "submit", "value" => "Add"]))
|
||||
TD(INPUT(["type"=>"text", "name"=>"pages", "value"=>'post/list*'])),
|
||||
TD(["colspan"=>'3'], INPUT(["type"=>"submit", "value"=>"Add"]))
|
||||
),
|
||||
TR(
|
||||
TD(["colspan" => "13"], TEXTAREA(["rows" => "5", "name" => "content"]))
|
||||
TD(["colspan"=>"13"], TEXTAREA(["rows"=>"5", "name"=>"content"]))
|
||||
),
|
||||
));
|
||||
|
||||
|
@ -11,8 +11,10 @@ class BlotterInfo extends ExtensionInfo
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Blotter";
|
||||
public string $url = "http://seemslegit.com/";
|
||||
public array $authors = ["Zach Hall" => "zach@sosguy.net"];
|
||||
public array $authors = ["Zach Hall"=>"zach@sosguy.net"];
|
||||
public string $license = self::LICENSE_GPLV2;
|
||||
public string $description = "Displays brief updates about whatever you want on every page.";
|
||||
public ?string $documentation = "Colors and positioning can be configured to match your site's design.<p>Development TODO at https://github.com/zshall/shimmie2/issues";
|
||||
public string $description = "Displays brief updates about whatever you want on every page.
|
||||
Colors and positioning can be configured to match your site's design.
|
||||
|
||||
Development TODO at https://github.com/zshall/shimmie2/issues";
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ class Blotter extends Extension
|
||||
/** @var BlotterTheme */
|
||||
protected Themelet $theme;
|
||||
|
||||
public function onInitExt(InitExtEvent $event): void
|
||||
public function onInitExt(InitExtEvent $event)
|
||||
{
|
||||
global $config;
|
||||
$config->set_default_int("blotter_recent", 5);
|
||||
@ -17,7 +17,7 @@ class Blotter extends Extension
|
||||
$config->set_default_string("blotter_position", "subheading");
|
||||
}
|
||||
|
||||
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void
|
||||
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
|
||||
{
|
||||
global $database;
|
||||
|
||||
@ -31,7 +31,7 @@ class Blotter extends Extension
|
||||
// Insert sample data:
|
||||
$database->execute(
|
||||
"INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), :text, :important)",
|
||||
["text" => "Installed the blotter extension!", "important" => true]
|
||||
["text"=>"Installed the blotter extension!", "important"=>true]
|
||||
);
|
||||
log_info("blotter", "Installed tables for blotter extension.");
|
||||
$this->set_version("blotter_version", 2);
|
||||
@ -42,7 +42,7 @@ class Blotter extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onSetupBuilding(SetupBuildingEvent $event): void
|
||||
public function onSetupBuilding(SetupBuildingEvent $event)
|
||||
{
|
||||
$sb = $event->panel->create_new_block("Blotter");
|
||||
$sb->add_int_option("blotter_recent", "<br />Number of recent entries to display: ");
|
||||
@ -50,10 +50,10 @@ class Blotter extends Extension
|
||||
$sb->add_choice_option("blotter_position", ["Top of page" => "subheading", "In navigation bar" => "left"], "<br>Position: ");
|
||||
}
|
||||
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if ($event->parent === "system") {
|
||||
if ($event->parent==="system") {
|
||||
if ($user->can(Permissions::BLOTTER_ADMIN)) {
|
||||
$event->add_nav_link("blotter", new Link('blotter/editor'), "Blotter Editor");
|
||||
}
|
||||
@ -61,7 +61,7 @@ class Blotter extends Extension
|
||||
}
|
||||
|
||||
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event): void
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if ($user->can(Permissions::BLOTTER_ADMIN)) {
|
||||
@ -69,7 +69,7 @@ class Blotter extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event): void
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $page, $database, $user;
|
||||
if ($event->page_matches("blotter") && $event->count_args() > 0) {
|
||||
@ -100,7 +100,7 @@ class Blotter extends Extension
|
||||
// Now insert into db:
|
||||
$database->execute(
|
||||
"INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), :text, :important)",
|
||||
["text" => $entry_text, "important" => $important]
|
||||
["text"=>$entry_text, "important"=>$important]
|
||||
);
|
||||
log_info("blotter", "Added Message: $entry_text");
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
@ -115,7 +115,7 @@ class Blotter extends Extension
|
||||
$this->theme->display_permission_denied();
|
||||
} else {
|
||||
$id = int_escape($_POST['id']);
|
||||
$database->execute("DELETE FROM blotter WHERE id=:id", ["id" => $id]);
|
||||
$database->execute("DELETE FROM blotter WHERE id=:id", ["id"=>$id]);
|
||||
log_info("blotter", "Removed Entry #$id");
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("blotter/editor"));
|
||||
@ -136,7 +136,7 @@ class Blotter extends Extension
|
||||
$this->display_blotter();
|
||||
}
|
||||
|
||||
private function display_blotter(): void
|
||||
private function display_blotter()
|
||||
{
|
||||
global $database, $config;
|
||||
$limit = $config->get_int("blotter_recent", 5);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user